Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ env:
TZ: America/New_York
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
TEMPO_LICENSE_KEY: ""

on:
push:
Expand Down
4,771 changes: 3,943 additions & 828 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "tempo-monorepo",
"version": "3.0.0",
"version": "3.0.1",
"private": true,
"engines": {
"node": ">=20.0.0"
},
Comment thread
magmacomputing marked this conversation as resolved.
"description": "Magma Computing Monorepo",
"repository": {
"type": "git",
Expand All @@ -11,7 +14,7 @@
"packages/*"
],
"scripts": {
"test": "vitest run",
"test": "cross-env TEMPO_LICENSE_KEY=\"\" vitest run",
"build:tempo": "npm run build --workspace=@magmacomputing/tempo",
"build:library": "npm run build --workspace=@magmacomputing/library",
"clean": "tsc -b --clean",
Expand All @@ -23,7 +26,7 @@
"docs:build": "npm run docs:build --workspace=@magmacomputing/tempo",
"docs:preview": "npm run docs:preview --workspace=@magmacomputing/tempo",
"docs:push": "npm run docs:push --workspace=@magmacomputing/tempo",
"test:dist": "npm run build:library && npm run build:tempo && cross-env TEST_DIST=true vitest run"
"test:dist": "npm run build:library && npm run build:tempo && cross-env TEMPO_LICENSE_KEY=\"\" TEST_DIST=true vitest run"
},
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
Expand All @@ -32,21 +35,28 @@
"@types/hammerjs": "^2.0.46",
"@types/jquery": "^4.0.0",
"@types/node": "^25.9.1",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/browser-webdriverio": "^4.1.8",
"@vitest/ui": "^4.1.7",
"cross-env": "^10.1.0",
"markdown-it-mathjax3": "^4.3.2",
"playwright": "^1.60.0",
"rollup": "^4.60.4",
"tslib": "^2.8.1",
"tsx": "^4.22.3",
"typescript": "^6.0.3",
"unplugin-swc": "^1.5.9",
"vitest": "^4.1.7"
"vitest": "^4.1.7",
"webdriverio": "^9.27.2"
},
"overrides": {
"esbuild": "^0.28.0"
},
"allowScripts": {
"esbuild": true,
"@swc/core": true
"@swc/core": true,
"edgedriver@6.3.0": true,
"geckodriver@6.1.0": true
}
}
}
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "3.0.0",
"version": "3.0.1",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
16 changes: 11 additions & 5 deletions packages/library/src/common/pledge.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,14 @@ export class Pledge<T> {
}

[Symbol.dispose]() {
if (this.isPending)
if (this.isPending) {
try {
this.promise.catch(() => {});
} catch {
// best-effort; preserve disposal semantics even if promise is unavailable
}
this.reject(new Error(`Pledge disposed`)); // dispose
}
}

get status() {
Expand Down Expand Up @@ -138,10 +144,10 @@ export class Pledge<T> {
if (this.isPending) {
this.#status.settled = value;
this.#status.state = _STATE.Resolved;
_dbg.debug(this.#status, 'Resolved'); // debug
_dbg.debug(this.#status, 'Resolved'); // debug
this.#pledge.resolve(value); // resolve
}
else _dbg.warn(this.#status, `Pledge was already ${this.state}`);
// else _dbg.warn(this.#status, `Pledge was already ${this.state}`);

return this.#pledge.promise;
}
Expand All @@ -150,10 +156,10 @@ export class Pledge<T> {
if (this.isPending) {
this.#status.error = error;
this.#status.state = _STATE.Rejected;
_dbg.debug(this.#status, 'Rejected', error); // debug
_dbg.debug(this.#status, 'Rejected', error); // debug
this.#pledge.reject(error); // reject
}
else _dbg.warn(this.#status, `Pledge was already ${this.state}`);
// else _dbg.warn(this.#status, `Pledge was already ${this.state}`);

return this.#pledge.promise;
}
Expand Down
58 changes: 58 additions & 0 deletions packages/library/src/common/scopedset.class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* ## ScopedSet
* A lightweight `Set`-compatible container that delegates `has()` lookups to a
* parent Set/ScopedSet, but confines `add()` writes to its own-local storage.
*
* This mirrors JavaScript prototype-chain semantics:
* - A plugin registered globally is **visible** to all sandboxes via `has()`.
* - A plugin registered in a sandbox is **isolated** from the global scope;
* the global `rt.installed` is never written to by sandbox `extend()` calls.
*
* @example
* const global = new Set(['a']);
* const sandbox = new ScopedSet(global);
* sandbox.has('a'); // true (inherited from parent)
* sandbox.add('b');
* sandbox.has('b'); // true (own-local)
* global.has('b'); // false (never written to parent)
*/
export class ScopedSet<T> {
readonly #own = new Set<T>();
readonly #parent: Set<T> | ScopedSet<T> | undefined;

constructor(parent?: Set<T> | ScopedSet<T>) {
this.#parent = parent;
}

/** `true` if value is in own-local storage OR anywhere in the parent chain. */
has(value: T): boolean {
return this.#own.has(value) || (this.#parent?.has(value) ?? false);
}

/** Adds to own-local storage only — never propagates to the parent. */
add(value: T): this {
this.#own.add(value);
return this;
}

/** Removes from own-local storage only. */
delete(value: T): boolean {
return this.#own.delete(value);
}

/** Clears own-local storage only. */
clear(): void {
this.#own.clear();
}

/** Number of own-local entries (does not include parent entries). */
get size(): number { return this.#own.size; }

forEach(cb: (value: T, value2: T, set: Set<T>) => void): void { this.#own.forEach(cb as any); }
values(): IterableIterator<T> { return this.#own.values(); }
keys(): IterableIterator<T> { return this.#own.keys(); }
entries(): IterableIterator<[T, T]> { return this.#own.entries(); }

[Symbol.iterator](): IterableIterator<T> { return this.#own[Symbol.iterator](); }
get [Symbol.toStringTag](): string { return 'ScopedSet'; }
}
4 changes: 2 additions & 2 deletions packages/library/src/common/temporal.polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
if (typeof globalThis.Temporal === 'undefined') {
throw new Error(
'Temporal API is not available. ' +
'Please use a runtime with native Temporal support (Node 22+, Deno, Bun) ' +
'or load a polyfill (e.g. @js-temporal/polyfill) before importing this library.'
'Node 20+ with a polyfill (e.g. @js-temporal/polyfill) is highly recommended. ' +
'Native Temporal is fully reliable only on Node 26+ (while Node 22 may have instabilities).'
);
}

Expand Down
16 changes: 16 additions & 0 deletions packages/library/test/common/pledge.class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ describe('Pledge', () => {
await expect(p.promise).rejects.toThrow('Pledge disposed');
});

test('dispose does not trigger unhandled rejection', async () => {
const unhandled: any[] = [];
const listener = (error: any) => { unhandled.push(error); };
process.on('unhandledRejection', listener);

try {
const p = new Pledge();
p[Symbol.dispose]();
await Promise.resolve();
await Promise.resolve();
expect(unhandled).toHaveLength(0);
} finally {
process.off('unhandledRejection', listener);
}
});

test('callbacks', async () => {
const onResolve = vi.fn();
const onReject = vi.fn();
Expand Down
10 changes: 10 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
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).

## [3.0.1] - 2026-06-11

### Changed
- **Node Engine Constraint**: Clarified the Node.js requirement in `package.json` to `>=20.0.0`. While Tempo supports native `Temporal` in Node 22+, due to known instabilities in Node 22.0.x native Temporal, using Node 20+ with a polyfill like `@js-temporal/polyfill` is highly recommended.
- **Browser Compatibility Checks**: Established a fully automated headless browser test suite (`@vitest/browser`) using WebdriverIO to guarantee that Granular ESM bundles resolve dynamically imported relative paths natively. Browser compatibility is now enforced as a `prepublishOnly` lifecycle gate.

### Fixed
- **Term Scope Isolation**: Fixed a bug where the `Tempo.terms` getter would inappropriately sweep all licensed scopes (including modules and extensions like `ticker`) into the Terms array. `Tempo.terms` now strictly returns only the registered, queryable Term plugins, while preserving raw scopes in `Tempo.license.scopes`.
- **Background Validation Leaks**: Resolved asynchronous test leakage in the licensing test suites by ensuring all background cryptographic verification pledges are explicitly awaited during test teardown.

## [3.0.0] - 2026-06-07

### Changed (Breaking)
Expand Down
4 changes: 3 additions & 1 deletion packages/tempo/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Tempo uses several advanced JavaScript patterns that contributors should be fami
## 🛠️ Local Development

### Prerequisites
- **Node.js 20+** (Tempo requires native `Temporal` support or a robust environment).
- **Node.js 20+** (Tempo requires native `Temporal` support or a robust environment; Node 22.0.x native Temporal has known instabilities, so a polyfill like `@js-temporal/polyfill` is highly recommended).
- **npm v9+** (For monorepo workspace support).

### Setup
Expand All @@ -33,6 +33,8 @@ npm run build -w @magmacomputing/tempo
We use **Vitest** for our test suite. All new features or bug fixes must include corresponding tests.

- **Run all tests**: `npm run test`
- **Run distribution tests**: `npm run test:dist` (Tests the compiled `dist/` bundles)
- **Run browser compatibility tests**: `npm run test:browser` (Requires Chrome/Chromium for headless WebdriverIO execution)
- **Watch mode**: `npm run dev`
- **Coverage**: `npm run coverage`

Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/bench/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"#tempo/plugin/extend/*.js": [ "../src/plugin/extend/*.ts" ],
"#tempo/plugin/term/*.js": [ "../src/plugin/term/*.ts" ],
"#tempo/term/*": [ "../src/plugin/term/term.*.ts" ],
"#tempo/license": [ "../src/support/support.license.ts" ],
"#tempo/license": [ "../src/plugin/license/license.validator.ts" ],
"#tempo/*": [ "../src/*" ]
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/bin/resolve-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function rewrite(filePath: string) {
// Handle #tempo/license resolution
let prefix = '';
for (let i = 0; i < depth; i++) prefix += '../';
let licReplacement = `${prefix || './'}support/support.license.js`;
let licReplacement = `${prefix || './'}plugin/license/license.validator.js`;

const updatedContent = content
.replace(/#library\/([^"')]+\.js)/g, (_, libPath) => {
Expand Down
32 changes: 32 additions & 0 deletions packages/tempo/bin/update-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* bin/update-version.mjs
*
* Reads the version from package.json and rewrites src/tempo.version.ts
* so that Tempo.version always reflects the current published version.
*
* Usage: node bin/update-version.mjs
* Called automatically by `npm run prebuild`.
*/
import pkg from '../package.json' with { type: 'json' };
import { writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const { version } = pkg;
const __dirname = dirname(fileURLToPath(import.meta.url));

const versionFile = resolve(__dirname, '../src/tempo.version.ts');
const content = `/**
* @internal
* Canonical version of the Tempo library.
*
* ⚠️ This file is auto-updated by \`npm run build:version\` (see \`bin/update-version.mjs\`).
* Do NOT edit manually — your changes will be overwritten on the next build.
*/
export const TEMPO_VERSION = '${version}';
`;

writeFileSync(versionFile, content, 'utf-8');
console.log(`✅ Tempo version stamped: ${version}`);

19 changes: 10 additions & 9 deletions packages/tempo/doc/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

`Temporal` is now at Stage 4 and is expected to land broadly in runtimes soon. To avoid needlessly inflating package size with a dependency that will increasingly become unnecessary, `Tempo` does not bundle a `Temporal` polyfill by default.

As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support.
As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support. You can verify browser support at https://caniuse.com/temporal.

While Node.js does not yet enable `Temporal` by default, recent versions (Node 20+) support it via the `--harmony-temporal` flag (or `--js-temporal` in newer builds). This allows you to use `Tempo` without an external polyfill package.
Node.js 26.0.0+ ships native `Temporal` fully enabled by default. Older Node versions may still require an external polyfill or experimental flags depending on the V8 version.

::: warning
Native implementations in Node.js are currently considered experimental and may be incomplete or contain bugs that cause unexpected crashes (e.g., `V8_Fatal` errors in some builds). For mission-critical stability, we strongly recommend using `@js-temporal/polyfill`.
Older Node.js releases that ship `Temporal` behind a feature flag may still have incomplete or experimental implementations. For mission-critical stability in those older environments, we strongly recommend using `@js-temporal/polyfill`.
:::

Please verify support in your actual target runtime(s) and add a polyfill only when needed.
Expand Down Expand Up @@ -49,14 +49,16 @@ const t = new Tempo('next Friday');

### Node.js (with Native Temporal)

If you are using Node.js 20+, you can enable native `Temporal` support without installing a polyfill:
If you are using Node.js 26.0.0 or later, native `Temporal` is fully supported and enabled by default.

For older Node.js releases that still ship `Temporal` behind a flag, you can enable it with:

```bash
node --harmony-temporal my-app.js
```

> [!WARNING]
> Use native support with caution. Some Node.js builds contain incomplete Temporal implementations that can crash on complex arithmetic. See [Temporal Polyfill Note](#temporal-polyfill-note) for details.
> Older Node.js releases that require `--harmony-temporal` may still have incomplete Temporal support. See [Temporal Polyfill Note](#temporal-polyfill-note) for details.

### Node.js (with Polyfill)

Expand Down Expand Up @@ -104,9 +106,8 @@ Add this to your `<head>` to resolve the dependencies:
<script type="importmap">
{
"imports": {
"jsbi": "https://cdn.jsdelivr.net/npm/jsbi@4.3.0/dist/jsbi.mjs",
"@js-temporal/polyfill": "https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5/dist/index.esm.js",
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.esm.js"
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/dist/tempo.bundle.esm.js"
}
}
</script>
Expand Down Expand Up @@ -134,7 +135,7 @@ If you aren't using ESM or just want a simple `<script>` tag for rapid prototypi
<script src="https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5/dist/index.umd.js"></script>

<!-- Load the Tempo Global Bundle -->
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/dist/tempo.bundle.js"></script>

<script>
const t = new Tempo('now');
Expand Down Expand Up @@ -168,5 +169,5 @@ When using the Lite build, the `Tempo` class will have almost no methods (like `

We recommend pinning your versions in production environments to ensure stability.

* **JSDelivr**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/...` (Locks to major version 2)
* **JSDelivr**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/...` (Locks to major version 3)
* **Latest**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/...` (Omit the version string to always receive the latest release. Note that JSDelivr will resolve a missing version tag to the latest published release).
Loading
Loading