Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 34 additions & 3 deletions packages/library/src/common/runtime/mapper.library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CONTEXT, getContext } from '#library/utility.library.js';
import { isNullish, isNumber, isString, isSafeKey } from '#library/assertion.library.js';
import { getStorage } from '#library/storage.library.js';

export interface GeoLookupResult {
lat?: number;
Expand Down Expand Up @@ -120,9 +121,39 @@ export const getStashedGeo = (): GeoConfig | undefined => {
const raw = localStorage.getItem('_map_');
if (raw) {
const parsed = JSON.parse(raw);
const coords = parsed?.geolocation?.coords;
if (isNumber(coords?.latitude) && isNumber(coords?.longitude)) {
return { latitude: coords.latitude, longitude: coords.longitude };
const coords = parsed?.geolocation?.coords ?? parsed?.coords ?? parsed;
const lat = coords?.latitude ?? coords?.lat;
const lng = coords?.longitude ?? coords?.lng ?? coords?.lon ?? coords?.long;
if (isNumber(lat) && isNumber(lng)) {
return { latitude: lat, longitude: lng };
}
}
}
} catch {
// ignore storage access errors
}
} else if (type === CONTEXT.NodeJS || type === CONTEXT.Deno) {
try {
const raw = getStorage<any>('_map_') ?? getStorage<any>('TEMPO_GEO');
if (raw) {
if (typeof raw === 'string' && raw.includes(',')) {
const parts = raw.split(',').map(s => parseFloat(s.trim()));
if (parts.length >= 2 && isNumber(parts[0]) && isNumber(parts[1])) {
return { latitude: parts[0], longitude: parts[1] };
}
}
if (typeof raw === 'object') {
const coords = raw.geolocation?.coords ?? raw.coords ?? raw;
const lat = coords?.latitude ?? coords?.lat;
const lng = coords?.longitude ?? coords?.lng ?? coords?.lon ?? coords?.long;
if (isNumber(lat) && isNumber(lng)) {
const result: GeoConfig = { latitude: lat, longitude: lng };
const elevation = raw.elevation ?? coords.elevation;
if (isNumber(elevation)) result.elevation = elevation;
if (raw.sphere === 'north' || raw.sphere === 'south') result.sphere = raw.sphere;
if (isString(raw.country)) result.country = raw.country;
if (isString(raw.city)) result.city = raw.city;
return result;
}
}
}
Expand Down
54 changes: 53 additions & 1 deletion packages/library/test/common/runtime/mapper.common.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { coerceGeo, geoLookup, resolveGeoCoordinates } from '../../../src/common/runtime/mapper.library.js';
import { coerceGeo, geoLookup, resolveGeoCoordinates, getStashedGeo } from '../../../src/common/runtime/mapper.library.js';
import { setStorage } from '#library/storage.library.js';

describe('common/runtime/mapper.library', () => {
afterEach(() => {
Expand Down Expand Up @@ -61,8 +62,59 @@ describe('common/runtime/mapper.library', () => {
expect(result.lat).toBe(-33.8688);
expect(result.lng).toBe(151.2093);
expect(result.city).toBe('Sydney');
expect(mockFetch).toHaveBeenCalledTimes(1);
} finally {
vi.unstubAllGlobals();
}
});

it('getStashedGeo in Node.js returns undefined by default without developer configuration', () => {
expect(getStashedGeo()).toBeUndefined();
});
Comment thread
magmacomputing marked this conversation as resolved.
Outdated

it('getStashedGeo in Node.js discovers coordinates set explicitly via setStorage', () => {
try {
setStorage('_map_', {
geolocation: { coords: { latitude: -33.8688, longitude: 151.2093 } },
city: 'Sydney',
});

const stashed = getStashedGeo();
expect(stashed).toBeDefined();
expect(stashed?.latitude).toBe(-33.8688);
expect(stashed?.longitude).toBe(151.2093);
expect(stashed?.city).toBe('Sydney');
} finally {
setStorage('_map_', undefined);
}
});

it('getStashedGeo in Node.js discovers coordinates pre-seeded in process.env.TEMPO_GEO', () => {
try {
setStorage('_map_', undefined);
process.env.TEMPO_GEO = '{"latitude": 37.7749, "longitude": -122.4194, "city": "San Francisco"}';

const stashed = getStashedGeo();
expect(stashed).toBeDefined();
expect(stashed?.latitude).toBe(37.7749);
expect(stashed?.longitude).toBe(-122.4194);
expect(stashed?.city).toBe('San Francisco');
} finally {
delete process.env.TEMPO_GEO;
}
});

it('getStashedGeo in Node.js supports comma-separated string coordinates in TEMPO_GEO', () => {
try {
setStorage('_map_', undefined);
process.env.TEMPO_GEO = '51.5074, -0.1278';

const stashed = getStashedGeo();
expect(stashed).toBeDefined();
expect(stashed?.latitude).toBe(51.5074);
expect(stashed?.longitude).toBe(-0.1278);
} finally {
delete process.env.TEMPO_GEO;
}
});
});
6 changes: 3 additions & 3 deletions packages/plugins/.setup/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"packageName": "@magmacomputing/tempo-plugin-batch",
"plan": "community",
"status": "active",
"version": "1.0.2"
"version": "1.1.0"
},
{
"id": "finance",
Expand Down Expand Up @@ -60,7 +60,7 @@
"packageName": "@magmacomputing/tempo-plugin-ai",
"plan": "community",
"status": "active",
"version": "1.2.1"
"version": "2.0.0"
},
{
"id": "ticker",
Expand All @@ -78,7 +78,7 @@
"packageName": "@magmacomputing/tempo-plugin-geo",
"plan": "community",
"status": "active",
"version": "0.1.0"
"version": "1.0.0"
},
{
"id": "_std",
Expand Down
12 changes: 7 additions & 5 deletions packages/plugins/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ All notable changes to the `@magmacomputing/tempo-plugin-ai` project will be doc
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.1] - 2026-09-07
## [2.0.0] - 2026-09-08

### Security & Reliability
### Breaking Changes & Major Release
- **Standardized on Tempo v4.1.0+ Community Core**:
- Requires `@magmacomputing/tempo` `^4.1.0`.
- Migrated configuration discovery to the dedicated `pluginOptions.ai` configuration slot introduced in Tempo v4.1.0, deprecating legacy `plugins.ai` dictionary passing.
- **Self-Contained Network Transport (`fetch.ts`)**:
- Decoupled network request utilities from `@magmacomputing/tempo/library` into a self-contained local transport helper.
- Added chunk-by-chunk stream consumption via `res.body.getReader()` with proactive byte accounting and immediate reader cancellation (`await reader.cancel()`).
- Enforced upfront `Content-Length` checks against `maxBytes` and strictly bounded stream reads to prevent unbounded memory allocation and OS thread starvation.
- Decoupled network transport entirely from `@magmacomputing/tempo/library` (removing reliance on core internal `HttpError` and `fetchRequest`).
- Implemented proactive stream consumption via `res.body.getReader()` with strict byte accounting and upfront `Content-Length` enforcement against `maxBytes` to prevent memory exhaustion.

## [1.2.0] - 2026-09-06

Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo-plugin-ai",
"version": "1.2.1",
"version": "2.0.0",
"description": "Tempo community plugin for LLM-powered natural language parsing.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
9 changes: 9 additions & 0 deletions packages/plugins/batch/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to the `@magmacomputing/tempo-plugin-batch` project will be documented in this file.

## [1.1.0] - 2026-09-08

### Added & Enhanced
- **Worker Thread Runtime Flag Inheritance (`execArgv`)**:
- Configured `BatchOrchestrator` to forward `process.execArgv` to spawned worker threads.
- Workers automatically inherit native runtime flags (such as `--harmony-temporal`) or custom loader configurations (`--import`) without bundling or depending on a polyfill, strictly preserving Tempo's zero-dependency, native-first Temporal philosophy.
- **Shorthand Mutation Parsing**:
- Added support for relative duration shorthand strings (e.g. `+1d`, `+1w`, `+1h`, `+1m`) alongside `{ Term: Value }` mutation objects.

## [1.0.2] - 2026-08-20

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/batch/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo-plugin-batch",
"version": "1.0.2",
"version": "1.1.0",
"description": "Tempo community plugin bringing C-level parallelization to massive date arrays via SharedArrayBuffer and Worker Threads.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
73 changes: 67 additions & 6 deletions packages/plugins/batch/src/BatchOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,65 @@ export interface BatchOptions {
rehydrate?: boolean;
}

const ALLOWED_FLAGS_WITH_VALUE = new Set([
'--import',
'--loader',
'--experimental-loader',
'-r',
'--require',
]);

const ALLOWED_STANDALONE_FLAGS = new Set([
'--experimental-vm-modules',
'--experimental-temporal',
'--experimental-specifier-resolution',
'--inspect',
'--inspect-brk',
'--trace-warnings',
'--no-warnings',
'--trace-deprecation',
'--no-deprecation',
]);

/**
* Orchestrates the parallel execution of a mutation or formatting operation across an array of epochs.
*/
export class BatchOrchestrator {
/**
* Sanitizes process.execArgv using a positive AllowList of worker-safe options (loaders, polyfills, inspection).
* Any V8 memory/optimization flags or process-level flags are safely dropped.
*
* @param argv - Array of Node CLI options (defaults to process.execArgv)
* @returns Sanitized array of CLI options safe for worker_threads
* @internal
*/
static sanitizeExecArgv(argv: string[] = process.execArgv || []): string[] {
const result: string[] = [];

for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
const eqIdx = arg.indexOf('=');
const hasEq = eqIdx !== -1;
const rawFlag = hasEq ? arg.slice(0, eqIdx) : arg;
const flag = rawFlag.replace(/_/g, '-');

if (ALLOWED_FLAGS_WITH_VALUE.has(flag)) {
if (hasEq) {
result.push(arg);
} else if (i + 1 < argv.length) {
result.push(arg, argv[++i]);
}
continue;
}

if (ALLOWED_STANDALONE_FLAGS.has(flag)) {
result.push(arg);
continue;
}
}

return result;
}
/**
* Transforms an array of epochs using a worker pool.
* @param epochs Array of raw millisecond epoch numbers.
Expand All @@ -36,9 +91,8 @@ export class BatchOrchestrator {
if (epochs.length === 0) return [];

if (options.threads !== undefined) {
if (!Number.isInteger(options.threads) || options.threads <= 0) {
if (!Number.isInteger(options.threads) || options.threads <= 0)
throw new Error("options.threads must be a positive integer");
}
}

const threadCount = options.threads ?? os.cpus().length;
Expand Down Expand Up @@ -82,6 +136,9 @@ export class BatchOrchestrator {
const workers: Promise<void>[] = [];
const actualThreads = Math.min(threadCount, Math.ceil(epochs.length / chunkSize));

const sanitized = this.sanitizeExecArgv();
const execArgv = sanitized.length > 0 ? sanitized : undefined;

for (let i = 0; i < actualThreads; i++) {
const startIdx = i * chunkSize;
const endIdx = Math.min((i + 1) * chunkSize, epochs.length);
Expand All @@ -94,8 +151,9 @@ export class BatchOrchestrator {
outputBuffer,
startIdx,
endIdx,
operation
}
operation,
},
...(execArgv ? { execArgv } : {}),
});
worker.on('message', (msg: any) => {
if (msg.status === 'done') resolve();
Expand Down Expand Up @@ -135,6 +193,8 @@ export class BatchOrchestrator {
private static async _transformWithPostMessage(epochs: number[], operation: string, threadCount: number, chunkSize: number, options: BatchOptions): Promise<any[]> {
const workers: Promise<any[]>[] = [];
const actualThreads = Math.min(threadCount, Math.ceil(epochs.length / chunkSize));
const sanitized = this.sanitizeExecArgv();
const execArgv = sanitized.length > 0 ? sanitized : undefined;

for (let i = 0; i < actualThreads; i++) {
const startIdx = i * chunkSize;
Expand All @@ -146,8 +206,9 @@ export class BatchOrchestrator {
workerData: {
mode: 'postMessage',
chunk,
operation
}
operation,
},
...(execArgv ? { execArgv } : {}),
});
worker.on('message', (msg: any) => {
if (msg.status === 'done') resolve(msg.result);
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/batch/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BatchOrchestrator, type BatchOptions } from './BatchOrchestrator.js';
import { definePlugin, type TempoPlugin } from '@magmacomputing/tempo/plugin/sdk';

export { BatchOrchestrator };
export type { BatchOptions };

declare module '@magmacomputing/tempo' {
Expand Down
31 changes: 26 additions & 5 deletions packages/plugins/batch/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import { workerData, parentPort } from 'node:worker_threads';
import { Tempo } from '@magmacomputing/tempo';

function applyMutation(t: any, op: any) {
if (typeof op === 'string') {
const match = op.trim().match(/^([+-]?\d+)\s*([a-zA-Z]+)$/);
if (match) {
const count = parseInt(match[1], 10);
const unit = match[2].toLowerCase();
const unitMap: Record<string, string> = {
d: 'days', day: 'days', days: 'days',
w: 'weeks', week: 'weeks', weeks: 'weeks',
m: 'minutes', min: 'minutes', mins: 'minutes', minute: 'minutes', minutes: 'minutes',
h: 'hours', hr: 'hours', hrs: 'hours', hour: 'hours', hours: 'hours',
s: 'seconds', sec: 'seconds', secs: 'seconds', second: 'seconds', seconds: 'seconds',
mo: 'months', month: 'months', months: 'months',
y: 'years', yr: 'years', yrs: 'years', year: 'years', years: 'years'
};
const mappedUnit = unitMap[unit];
if (mappedUnit) {
return t.add({ [mappedUnit]: count });
}
}
}
return t.add(op);
}

async function run() {
if (!parentPort) return;

Expand All @@ -14,11 +38,8 @@ async function run() {

for (let i = startIdx; i < endIdx; i++) {
const epoch = inputView[i];
// Using Tempo to mutate. In a full implementation, we'd have robust parsing of the 'operation' string.
// For this prototype, we assume the operation is an add operation (e.g. "+1w").
// We get the mutated epoch number and put it back into the buffer.
const t = new Tempo(epoch);
const resultT = t.add(operation);
const resultT = applyMutation(t, operation);
outputView[i] = resultT.epoch.ms;
}
parentPort.postMessage({ status: 'done' });
Expand All @@ -30,7 +51,7 @@ async function run() {
for (let i = 0; i < chunk.length; i++) {
const epoch = chunk[i];
const t = new Tempo(epoch);
const resultT = t.add(operation);
const resultT = applyMutation(t, operation);
result[i] = resultT.epoch.ms;
}

Expand Down
Loading
Loading