From 336302b6c589d77ef13921f4be4aaf6d80fe789b Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 25 May 2026 13:51:19 +1000 Subject: [PATCH 1/8] bump 2.11.1 --- packages/library/package.json | 4 ++-- packages/tempo/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/library/package.json b/packages/library/package.json index 954a183..58d1735 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.11.0", + "version": "2.12.0", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", @@ -99,4 +99,4 @@ "optionalDependencies": { "@js-temporal/polyfill": "^0.5.1" } -} \ No newline at end of file +} diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 9c87ce2..c9057e7 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.11.0", + "version": "2.12.0", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -242,4 +242,4 @@ "doc": "doc", "test": "test" } -} \ No newline at end of file +} From 9c8655b261d71d6b658582f7f5fcba910e5fc98d Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 25 May 2026 15:28:49 +1000 Subject: [PATCH 2/8] .until() / .since() doc tidy --- packages/tempo/doc/tempo.duration.md | 26 ++++++++++++-------- packages/tempo/src/module/module.duration.ts | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/tempo/doc/tempo.duration.md b/packages/tempo/doc/tempo.duration.md index 3bca4b5..324c00f 100644 --- a/packages/tempo/doc/tempo.duration.md +++ b/packages/tempo/doc/tempo.duration.md @@ -28,7 +28,7 @@ const duration = now.until(xmas); now.until('afternoon', 'minutes'); // → 84.45 (fractional: 'afternoon' has a fixed time) now.until('xmas', 'days'); // → 219 (whole number — see note below) now.until('xmas', 'weeks'); // → 31.28 (fractional — weeks don't divide evenly into days) -now.until(Tempo.Now(), 'hours'); // → 48 (targets can also be Temporal/Tempo instances) +now.until(Tempo.now(), 'hours'); // → 48 (targets can also be Temporal/Tempo instances) ``` ::: tip Date-only targets inherit the current time @@ -41,23 +41,24 @@ When a target resolves to a **date without a time component** (e.g. `'xmas'`, `' This matches natural-language intuition: *"How many days until Christmas?"* expects `219`, not `219.43`. Targets with an **explicit time** (e.g. `'afternoon'`, `'9am'`) always produce fractional values because the target time differs from the anchor's current time-of-day. ::: -::: warning Return Types -If you call `.until()` **without** a unit, it returns a `Tempo.Duration` object, onto which you can chain `.balance()` and `.format()` (see below). -If you provide a unit (like `'days'`), it returns a primitive JavaScript `Number`. Calling `.balance()` on a Number will throw an error. -::: - ### `.since()` Calculates the time elapsed *since* a past date. By default, it returns a human-readable localized string (powered by `Intl.RelativeTimeFormat`). ```javascript +const now = new Tempo({ locale: 'en-US' }); const birthday = new Tempo('1990-05-10'); // 1. Returns localized relative string based on the given unit -birthday.since('now', 'years'); // → "36 years ago" (depending on locale) -birthday.since('now', 'days'); // → "13,150 days ago" +now.since(birthday, 'years'); // → "36 years ago" (depending on locale) +now.since(birthday, 'days'); // → "13,150 days ago" + +// 2. Pass a custom formatter for natural language output (e.g. "yesterday") +const yesterday = now.add({ days: -1 }); +const autoFormat = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' }); +now.since(yesterday, { unit: 'days', intl: { relativeTime: { format: autoFormat } } }); // → "yesterday" -// 2. Returns an ISO 8601 Duration String if no unit is provided -birthday.since('yesterday'); // → "-P35Y11M29DT9H32M19.402S" +// 3. Returns an ISO 8601 Duration String if no unit is provided +now.since(birthday); // → "-P36Y..." ``` ::: info Return Type @@ -77,6 +78,11 @@ console.log(dur.days); // 15 ## Intelligent Balancing +::: warning Return Types +If you call `.until()` **without** a unit, it returns a `Tempo.Duration` object, onto which you can chain `.balance()` and `.format()` (see below). +If you provide a unit (like `'days'`), it returns a primitive JavaScript `Number`. Calling `.balance()` on a Number will throw an error. +::: + Sometimes you have a raw number of days (e.g. `365 days`) and you want to mathematically "balance" it into larger units (like `1 year`). Tempo provides the `.balance()` method directly on the Duration object. ### Strict Calendar Math diff --git a/packages/tempo/src/module/module.duration.ts b/packages/tempo/src/module/module.duration.ts index 9d3d4ad..2038312 100644 --- a/packages/tempo/src/module/module.duration.ts +++ b/packages/tempo/src/module/module.duration.ts @@ -196,7 +196,7 @@ function duration(this: Tempo, type: 'until' | 'since', arg?: any, until?: any) .map(nbr => nbr.toString().padStart(3, '0')) .join('') const rtConfig = (this as any).config.intl?.relativeTime; - const rtOptions = opts['relativeTime']; + const rtOptions = opts['intl']?.relativeTime || opts['relativeTime']; const rtf = (isFunction(rtOptions) ? rtOptions : rtOptions?.format) || (isFunction(rtConfig) ? rtConfig : rtConfig?.format) From f20f4191dd20dc75ee7acb966676f723d431adaa Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 25 May 2026 15:49:31 +1000 Subject: [PATCH 3/8] more doc fixes --- package-lock.json | 10 +++++----- packages/tempo/doc/tempo.duration.md | 21 +++++++++++---------- packages/tempo/package.json | 4 ++-- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6572a5..5664835 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tempo-monorepo", - "version": "2.10.2", + "version": "2.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "2.10.2", + "version": "2.11.0", "workspaces": [ "packages/*" ], @@ -5672,7 +5672,7 @@ }, "packages/library": { "name": "@magmacomputing/library", - "version": "2.10.2", + "version": "2.12.0", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -5683,14 +5683,14 @@ }, "packages/tempo": { "name": "@magmacomputing/tempo", - "version": "2.10.2", + "version": "2.12.0", "license": "MIT", "dependencies": { "tslib": "^2.8.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.10.2", + "@magmacomputing/library": "*", "@rollup/plugin-alias": "^6.0.0", "esbuild": "^0.25.12", "javascript-obfuscator": "^5.4.2", diff --git a/packages/tempo/doc/tempo.duration.md b/packages/tempo/doc/tempo.duration.md index 324c00f..d277611 100644 --- a/packages/tempo/doc/tempo.duration.md +++ b/packages/tempo/doc/tempo.duration.md @@ -25,10 +25,10 @@ const xmas = new Tempo('2026-12-25'); const duration = now.until(xmas); // 2. Or, calculate relative to a specific unit (returns a primitive Number) -now.until('afternoon', 'minutes'); // → 84.45 (fractional: 'afternoon' has a fixed time) -now.until('xmas', 'days'); // → 219 (whole number — see note below) -now.until('xmas', 'weeks'); // → 31.28 (fractional — weeks don't divide evenly into days) -now.until(Tempo.now(), 'hours'); // → 48 (targets can also be Temporal/Tempo instances) +now.until('afternoon', 'minutes'); // → ~84.45 (example output: fractional) +now.until('xmas', 'days'); // → ~219 (example output: whole number — see note below) +now.until('xmas', 'weeks'); // → ~31.28 (example output: fractional) +now.until(now.add({ days: 2 }), 'hours'); // → 48 (targets can also be Temporal/Tempo instances) ``` ::: tip Date-only targets inherit the current time @@ -45,20 +45,21 @@ This matches natural-language intuition: *"How many days until Christmas?"* expe Calculates the time elapsed *since* a past date. By default, it returns a human-readable localized string (powered by `Intl.RelativeTimeFormat`). ```javascript -const now = new Tempo({ locale: 'en-US' }); +const anchor = new Tempo('2026-05-10', { locale: 'en-US' }); const birthday = new Tempo('1990-05-10'); // 1. Returns localized relative string based on the given unit -now.since(birthday, 'years'); // → "36 years ago" (depending on locale) -now.since(birthday, 'days'); // → "13,150 days ago" +// Note: Tempo uses a compact ('narrow') Intl style by default +anchor.since(birthday, 'years'); // → "36y ago" (deterministic) +anchor.since(birthday, 'days'); // → "13,149d ago" (deterministic) // 2. Pass a custom formatter for natural language output (e.g. "yesterday") -const yesterday = now.add({ days: -1 }); +const yesterday = anchor.add({ days: -1 }); const autoFormat = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' }); -now.since(yesterday, { unit: 'days', intl: { relativeTime: { format: autoFormat } } }); // → "yesterday" +anchor.since(yesterday, { unit: 'days', intl: { relativeTime: { format: autoFormat } } }); // → "yesterday" // 3. Returns an ISO 8601 Duration String if no unit is provided -now.since(birthday); // → "-P36Y..." +anchor.since(birthday); // → "-P36Y..." ``` ::: info Return Type diff --git a/packages/tempo/package.json b/packages/tempo/package.json index c9057e7..b7e8dfb 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -227,7 +227,7 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.11.0", + "@magmacomputing/library": "2.12.0", "@rollup/plugin-alias": "^6.0.0", "esbuild": "^0.25.12", "javascript-obfuscator": "^5.4.2", @@ -242,4 +242,4 @@ "doc": "doc", "test": "test" } -} +} \ No newline at end of file From 1eaaed9a997d5e7ed6f41c17fcf88e9eb42aa189 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Tue, 26 May 2026 13:01:08 +1000 Subject: [PATCH 4/8] PR v2.11.1 --- package.json | 2 +- packages/library/package.json | 4 +-- packages/tempo/CHANGELOG.md | 10 ++++++++ packages/tempo/bin/resolve-types.ts | 2 +- packages/tempo/doc/tempo.duration.md | 37 +++++++++++++++++++++++----- packages/tempo/doc/tempo.license.md | 4 +-- packages/tempo/package.json | 4 +-- packages/tempo/src/library.index.ts | 2 ++ packages/tempo/src/tempo.class.ts | 9 ++++--- 9 files changed, 57 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index e3ece94..fbabe36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.11.0", + "version": "2.11.1", "private": true, "description": "Magma Computing Monorepo", "repository": { diff --git a/packages/library/package.json b/packages/library/package.json index 58d1735..bc68e60 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.12.0", + "version": "2.11.1", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", @@ -99,4 +99,4 @@ "optionalDependencies": { "@js-temporal/polyfill": "^0.5.1" } -} +} \ No newline at end of file diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md index 6923dd7..a5d5d81 100644 --- a/packages/tempo/CHANGELOG.md +++ b/packages/tempo/CHANGELOG.md @@ -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). +## [2.11.1] - 2026-05-26 + +### Added +- **ISO 8601 Convenience Getter**: Added the `.iso` getter to `Tempo` instances to provide a fast, familiar, and UTC-safe mechanism for retrieving the standard ISO 8601 string representation (analogous to `Date.toISOString()`). +- **Type Utilities Export**: Exported `KeyOf`, `ValueOf`, `OwnOf`, and `EntryOf` type utilities in `library.index.ts` to provide cleaner TypeScript typing for downstream consumers utilizing `enumify`. + +### Fixed +- **Build Pipeline Path Resolution**: Fixed a critical path-resolution bug in the `resolve-types.ts` script where `library.index.d.ts` was incorrectly identified as being inside the `lib/` directory. This ensures proper `.d.ts` generation and restores JSDoc/typings for downstream consumers. +- **Documentation Domains**: Updated all premium extension documentation (`tempo.license.md`) and internal plugins to use the centralized `registry.magmacomputing.com.au` domain for license verification and the UI portal. + ## [2.11.0] - 2026-05-25 ### Added diff --git a/packages/tempo/bin/resolve-types.ts b/packages/tempo/bin/resolve-types.ts index f1c434e..8163929 100644 --- a/packages/tempo/bin/resolve-types.ts +++ b/packages/tempo/bin/resolve-types.ts @@ -49,7 +49,7 @@ function rewrite(filePath: string) { const content = fs.readFileSync(filePath, 'utf8'); const relToDist = path.relative(DIST_DIR, filePath); const depth = relToDist.split(path.sep).length - 1; - const isInsideLib = relToDist.startsWith('lib'); + const isInsideLib = relToDist.startsWith(`lib${path.sep}`); let replacement: string; if (isInsideLib) { diff --git a/packages/tempo/doc/tempo.duration.md b/packages/tempo/doc/tempo.duration.md index d277611..4b16f9d 100644 --- a/packages/tempo/doc/tempo.duration.md +++ b/packages/tempo/doc/tempo.duration.md @@ -72,9 +72,9 @@ When you call `until()` (or `Tempo.duration()`), Tempo returns an Extended Data ```javascript const dur = Tempo.duration('P1Y2M15D'); -console.log(dur.years); // 1 -console.log(dur.months); // 2 -console.log(dur.days); // 15 +console.log(dur.years); // 1 +console.log(dur.months); // 2 +console.log(dur.days); // 15 ``` ## Intelligent Balancing @@ -86,12 +86,22 @@ If you provide a unit (like `'days'`), it returns a primitive JavaScript `Number Sometimes you have a raw number of days (e.g. `365 days`) and you want to mathematically "balance" it into larger units (like `1 year`). Tempo provides the `.balance()` method directly on the Duration object. +::: tip When to use .balance() +Because Tempo injects `{ largestUnit: 'years' }` by default, `.until()` is usually perfectly balanced out of the box. However, you will still need to chain `.balance()` in three specific scenarios: +1. **Raw Durations:** When you manually construct an unbalanced duration (e.g., `Tempo.duration({ days: 365 })`). +2. **Cross-Timezone Math:** If you run `.until()` between two *different* timezones, Tempo restricts the result to `hours` to prevent calendar ambiguity. Chaining `.balance()` forces the rollup into Years/Months. +3. **Nominal Math:** When you want to force commercial math (`{ nominal: true }`) instead of strict calendar math. +::: + ### Strict Calendar Math -By default, `.balance()` uses the `relativeTo` anchor captured during `.until()` to perform perfect calendar math. +By default, `.balance()` uses a `relativeTo` anchor to perform perfect calendar math. ```javascript -// Automatically balances 365 days into exactly "1 year" (or 11mo 30d if a leap year!) -const balanced = new Tempo().until('xmas').balance(); +// A manually created duration is unbalanced by default: { days: 365 } +const rawDuration = Tempo.duration({ days: 365 }); + +// Balance it using an anchor: safely converts it to { years: 1 } (or 11mo 30d if a leap year!) +const balanced = rawDuration.balance({ relativeTo: '2026-01-01' }); ``` ### Nominal (Commercial) Math @@ -105,6 +115,21 @@ console.log(commercialDur.years); // 1 console.log(commercialDur.days); // 0 ``` +### Balancing Downwards (Un-balancing) +You can also force a duration to roll *downwards* into smaller units by specifying a `largestUnit`. This is incredibly useful for UI countdown timers (e.g., displaying "48 Hours" instead of "2 Days") or per-diem billing calculations where you need to know exactly how many days are in a specific year. + +```javascript +const yearlyLicense = Tempo.duration({ years: 1 }); + +// Un-balance the year down into exact days (365 or 366 depending on the anchor) +const exactDays = yearlyLicense.balance({ + largestUnit: 'days', + relativeTo: '2024-01-01' +}); + +console.log(exactDays.days); // 366 (2024 is a leap year!) +``` + ## Formatting Absolute Durations Once you have a balanced duration, you can instantly render it as a highly localized, plural-aware string using the `.format()` method. diff --git a/packages/tempo/doc/tempo.license.md b/packages/tempo/doc/tempo.license.md index 6ba0a1b..df09bac 100644 --- a/packages/tempo/doc/tempo.license.md +++ b/packages/tempo/doc/tempo.license.md @@ -41,7 +41,7 @@ console.log(t.term.astronomy); **How to get it:** 1. Run `npm install @magmacomputing/tempo-plugin-astro` in your project. -2. Visit [magmacomputing.com.au/tempo/license.html](https://magmacomputing.com.au/tempo/license.html) to request your **one-year expiry key**, which will be instantly issued to your inbox. +2. Visit [registry.magmacomputing.com.au/tempo/](https://registry.magmacomputing.com.au/tempo/) to request your **one-year expiry key**, which will be instantly issued to your inbox. ## ⚙️ Applying Your License Key @@ -143,7 +143,7 @@ globalThis.TEMPO_LICENSE_KEY = 'ey...'; To verify license validity and prevent abuse, Tempo's licensing engine performs background synchronization with our revocation registry: * **Outbound Request:** When a license key is active, Tempo asynchronously fetches a cryptographically signed revocation list (JWS). -* **Endpoint:** `https://api.magmacomputing.com.au/tempo/v1/revoked.jws` (useful for configuring Content Security Policies (CSP) or egress firewall rules). +* **Endpoint:** `https://registry.magmacomputing.com.au/tempo/v1/revoked.jws` (useful for configuring Content Security Policies (CSP) or egress firewall rules). * **Frequency:** The revocation check occurs once every **7 days**. The last-checked state is cached to avoid redundant network traffic on subsequent startups. * **Offline Resilience (Fail-Open):** If your application is offline, behind a strict firewall, or the registry server is temporarily unreachable, the validation **fails open**. Tempo logs a warning in the console but continues to grant access to premium features (relying on the local cryptographic expiration of the JWT). diff --git a/packages/tempo/package.json b/packages/tempo/package.json index b7e8dfb..1da3249 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.12.0", + "version": "2.11.1", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -227,7 +227,7 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.12.0", + "@magmacomputing/library": "2.11.1", "@rollup/plugin-alias": "^6.0.0", "esbuild": "^0.25.12", "javascript-obfuscator": "^5.4.2", diff --git a/packages/tempo/src/library.index.ts b/packages/tempo/src/library.index.ts index 7af43cf..9a7293a 100644 --- a/packages/tempo/src/library.index.ts +++ b/packages/tempo/src/library.index.ts @@ -9,3 +9,5 @@ export { Cipher } from '#library/cipher.class.js'; export { enumify, type Enum } from '#library/enumerate.library.js'; export { proxify } from '#library/proxy.library.js'; export { stringify, objectify, cloneify } from '#library/serialize.library.js'; + +export type { OwnOf, KeyOf, ValueOf, EntryOf } from '#library/type.library.js'; diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts index 80b18d5..f4bc4fa 100644 --- a/packages/tempo/src/tempo.class.ts +++ b/packages/tempo/src/tempo.class.ts @@ -1222,6 +1222,8 @@ export class Tempo { /** Resolve the instance to a Temporal.ZonedDateTime (with optional callback) */ #resolve(cb?: (zdt: Temporal.ZonedDateTime) => T): T | Temporal.ZonedDateTime { + const now = this.#now.toZonedDateTimeISO('UTC'); + if (!this.#zdt) { try { const skip = [this.#local.parse.format, this.#local.parse.term, this.#local.parse.result] @@ -1231,7 +1233,7 @@ export class Tempo { this.#errored = true; const msg = `Tempo parse returned undefined for: ${String(this.#tempo)}`; Tempo.#dbg.error(this.#local.config, msg); - this.#zdt = this.#now.toZonedDateTimeISO('UTC'); + this.#zdt = now; } secure(this.#local.config); secure(this.#local.parse, new WeakSet(skip)); @@ -1240,7 +1242,7 @@ export class Tempo { const msg = `Cannot create Tempo: ${(err as Error).message}\n${(err as Error).stack}`; if (this.#local.config.catch === true) { Tempo.#dbg.error(this.#local.config, msg); // log as error if in catch-mode - this.#zdt = this.#now.toZonedDateTimeISO('UTC'); + this.#zdt = now; } else { Tempo.#dbg.error(this.#local.config, err, msg); // log as error then re-throw throw err; @@ -1248,7 +1250,7 @@ export class Tempo { } } - const zdt = isZonedDateTime(this.#zdt) ? this.#zdt : this.#now.toZonedDateTimeISO('UTC'); + const zdt = isZonedDateTime(this.#zdt) ? this.#zdt : now; return cb?.(zdt) ?? zdt; } @@ -1399,6 +1401,7 @@ export class Tempo { /** Full weekday name (e.g., 'Monday') */ get wkd() { return Tempo.WEEKDAYS.keyOf(this.toDateTime().dayOfWeek as t.Weekday) } /** iso weekday number: Mon=1, Sun=7 */ get dow() { return this.toDateTime().dayOfWeek as t.Weekday } /** Nanoseconds since Unix epoch (BigInt) */ get nano() { return this.toDateTime().epochNanoseconds } + /** Standard ISO 8601 string in UTC */ get iso() { return this.toDate().toISOString() } /** `true` if the underlying date-time is valid. */ get isValid() { return this.#resolve(zdt => !this.#errored && isZonedDateTime(zdt)); } /** list of registered terms and their available range keys */ From 7947dbef8557418c6c77f0463b2bf7b1f5fe7422 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Tue, 26 May 2026 13:29:55 +1000 Subject: [PATCH 5/8] failing tests --- packages/tempo/src/support/support.runtime.ts | 14 ++++++++++++-- packages/tempo/test/core/static.test.ts | 2 +- packages/tempo/vitest.config.ts | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/tempo/src/support/support.runtime.ts b/packages/tempo/src/support/support.runtime.ts index 054b493..dcc4e3f 100644 --- a/packages/tempo/src/support/support.runtime.ts +++ b/packages/tempo/src/support/support.runtime.ts @@ -79,19 +79,29 @@ export class TempoRuntime { /** * Record a Term in the discovery database. * Validates the shape before storing so malformed entries cannot corrupt state. + * Replaces existing terms with the same key to support HMR and test module cache resets. */ addTerm(term: TermPlugin): void { if (!term || typeof term.key !== 'string') return; - if (!this.pluginsDb.terms.some(t => t.key === term.key)) - this.pluginsDb.terms.push(term); + const idx = this.pluginsDb.terms.findIndex(t => t.key === term.key); + if (idx >= 0) this.pluginsDb.terms[idx] = term; + else this.pluginsDb.terms.push(term); } /** * Record a Plugin in the discovery database. * Guards against duplicate entries. + * Replaces existing plugins with the same name to support HMR and test module cache resets. */ addPlugin(plugin: any): void { if (!plugin) return; + if (plugin.name) { + const idx = this.pluginsDb.plugins.findIndex(p => p.name === plugin.name); + if (idx >= 0) { + this.pluginsDb.plugins[idx] = plugin; + return; + } + } if (!this.pluginsDb.plugins.includes(plugin)) this.pluginsDb.plugins.push(plugin); } diff --git a/packages/tempo/test/core/static.test.ts b/packages/tempo/test/core/static.test.ts index 190f0ef..9a76bcd 100644 --- a/packages/tempo/test/core/static.test.ts +++ b/packages/tempo/test/core/static.test.ts @@ -9,7 +9,7 @@ describe(`${label}`, () => { test(`${label} get the properties`, () => { expect(Tempo.properties.toSorted()) - .toEqual(['yy', 'yw', 'mm', 'dd', 'hh', 'mi', 'ss', 'ms', 'us', 'ns', 'ff', 'fmt', 'ww', 'tz', 'cal', 'ts', 'dow', 'mmm', 'mon', 'www', 'wkd', 'day', 'nano', 'term', 'terms', 'config', 'epoch', 'parse', 'ranges', 'isValid'].toSorted()) + .toEqual(['yy', 'yw', 'mm', 'dd', 'hh', 'mi', 'ss', 'ms', 'us', 'ns', 'ff', 'fmt', 'ww', 'tz', 'cal', 'ts', 'dow', 'mmm', 'mon', 'www', 'wkd', 'day', 'nano', 'term', 'terms', 'config', 'epoch', 'parse', 'ranges', 'isValid', 'iso'].toSorted()) }) test(`${label} get the elements`, () => { diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index cf19244..145c657 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -42,6 +42,7 @@ export default defineConfig({ }, resolve: { alias: isDist ? [ + { find: /^#tempo\/license$/, replacement: resolve(__dirname, './dist/support/support.license.js') }, { find: /^#tempo\/core$/, replacement: resolve(__dirname, './dist/core.index.js') }, { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/module/module.duration.js') }, From b71e764b371653392f6252b197721c77f4ee5cd7 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Tue, 26 May 2026 18:00:22 +1000 Subject: [PATCH 6/8] working on tests --- packages/library/src/common/serialize.library.ts | 9 +++++++-- packages/tempo/rollup.config.js | 8 +++++++- packages/tempo/test/plugins/term-dispatch.core.test.ts | 4 ++++ packages/tempo/test/plugins/term-shorthand.test.ts | 4 ++++ packages/tempo/test/plugins/term.test.ts | 3 +++ packages/tempo/test/plugins/ticker.active.test.ts | 3 +++ packages/tempo/test/plugins/ticker.hang.test.ts | 3 +++ packages/tempo/test/plugins/ticker.options.test.ts | 3 +++ packages/tempo/test/plugins/ticker.pulse.test.ts | 4 ++++ packages/tempo/test/plugins/ticker.stop.test.ts | 5 +++++ 10 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/library/src/common/serialize.library.ts b/packages/library/src/common/serialize.library.ts index f646421..287fa3d 100644 --- a/packages/library/src/common/serialize.library.ts +++ b/packages/library/src/common/serialize.library.ts @@ -12,8 +12,13 @@ export const Registry = (globalThis as any)[sym.$SerializerRegistry] ??= new Map export const registerSerializable = (name: string, cls: Function) => { const key = name.startsWith('$') ? name : `$${name}`; - if (Registry.has(key)) - throw new Error(`[registerSerializable] Collision: '${key}' is already registered with ${Registry.get(key)?.name || 'anonymous constructor'}`); + if (Registry.has(key)) { + const existingCls = Registry.get(key); + if (existingCls === cls || existingCls?.toString() === cls.toString()) { + return; // Silently allow idempotent dual-registration + } + throw new Error(`[registerSerializable] Collision: '${key}' is already registered with ${existingCls?.name || 'anonymous constructor'}`); + } Registry.set(key, cls); } diff --git a/packages/tempo/rollup.config.js b/packages/tempo/rollup.config.js index 1b51f9d..798752b 100644 --- a/packages/tempo/rollup.config.js +++ b/packages/tempo/rollup.config.js @@ -75,7 +75,13 @@ export default [ format: 'es', sourcemap: false }, - external: ['@js-temporal/polyfill'], + external: [ + '@js-temporal/polyfill', + /^@magmacomputing\/tempo/, + /^@magmacomputing\/library/, + /^#library/, + /^#tempo/ + ], plugins: [ resolve({ extensions: ['.js', '.ts'], moduleDirectories: ['node_modules'] }), esbuild({ target: 'esnext', minify: false }), diff --git a/packages/tempo/test/plugins/term-dispatch.core.test.ts b/packages/tempo/test/plugins/term-dispatch.core.test.ts index cfb17bb..fcce81d 100644 --- a/packages/tempo/test/plugins/term-dispatch.core.test.ts +++ b/packages/tempo/test/plugins/term-dispatch.core.test.ts @@ -5,6 +5,10 @@ import '#tempo/format'; import '#tempo/term'; describe('Term Dispatch Refactor', () => { + beforeEach(() => { + Tempo.init(); + }); + it('should set term by index (#quarter: 2)', () => { const t = new Tempo('2024-01-01T00:00:00', { timeZone: 'America/New_York' }); const t2 = t.set({ '#quarter': 2 }); diff --git a/packages/tempo/test/plugins/term-shorthand.test.ts b/packages/tempo/test/plugins/term-shorthand.test.ts index c0d1c85..b13b7ff 100644 --- a/packages/tempo/test/plugins/term-shorthand.test.ts +++ b/packages/tempo/test/plugins/term-shorthand.test.ts @@ -1,6 +1,10 @@ import { Tempo } from '#tempo' describe('Tempo Term Literacy (Namespace Shorthand)', () => { + beforeEach(() => { + Tempo.init(); + }); + describe('.set() shorthand', () => { test('set("#period.morning") sets to the start of morning', () => { const t = new Tempo('2026-01-01T12:00:00', { sphere: 'north' }) diff --git a/packages/tempo/test/plugins/term.test.ts b/packages/tempo/test/plugins/term.test.ts index 9c13b7b..8a49d4f 100644 --- a/packages/tempo/test/plugins/term.test.ts +++ b/packages/tempo/test/plugins/term.test.ts @@ -6,6 +6,9 @@ const label = 'term:'; * Test the Tempo term plugins */ describe(`${label}`, () => { + beforeEach(() => { + Tempo.init(); + }); test(`${label} check for the {quarter} plugin`, () => { const qtr = Tempo.terms.find(({ key }: any) => key === 'qtr'); diff --git a/packages/tempo/test/plugins/ticker.active.test.ts b/packages/tempo/test/plugins/ticker.active.test.ts index 192c3a6..7cf8d99 100644 --- a/packages/tempo/test/plugins/ticker.active.test.ts +++ b/packages/tempo/test/plugins/ticker.active.test.ts @@ -2,6 +2,9 @@ import { Tempo } from '#tempo'; import { Ticker } from '#tempo/plugin/extend/extend.ticker.js'; describe('Ticker Management (Static Registry)', () => { + beforeEach(() => { + Tempo.init(); + }); it('should track active tickers in the registry', async () => { const initialCount = Ticker.active.length; diff --git a/packages/tempo/test/plugins/ticker.hang.test.ts b/packages/tempo/test/plugins/ticker.hang.test.ts index d4fcb16..c0e1378 100644 --- a/packages/tempo/test/plugins/ticker.hang.test.ts +++ b/packages/tempo/test/plugins/ticker.hang.test.ts @@ -2,6 +2,9 @@ import { Tempo } from '#tempo'; import '#tempo/plugin/extend/extend.ticker.js'; describe('Ticker Pledge Refactor Verification', () => { + beforeEach(() => { + Tempo.init(); + }); test('should terminate async iteration immediately when stop() is called (Pledge)', async () => { const t = Tempo.ticker({ seconds: 1 }); diff --git a/packages/tempo/test/plugins/ticker.options.test.ts b/packages/tempo/test/plugins/ticker.options.test.ts index b973d74..ec6da5a 100644 --- a/packages/tempo/test/plugins/ticker.options.test.ts +++ b/packages/tempo/test/plugins/ticker.options.test.ts @@ -2,6 +2,9 @@ import { Tempo } from '#tempo'; import '#tempo/plugin/extend/extend.ticker.js'; describe('Tempo.ticker Options & Enhancements', () => { + beforeEach(() => { + Tempo.init(); + }); test('ticker with limit (callback)', async () => { let count = 0; diff --git a/packages/tempo/test/plugins/ticker.pulse.test.ts b/packages/tempo/test/plugins/ticker.pulse.test.ts index ae91533..b579450 100644 --- a/packages/tempo/test/plugins/ticker.pulse.test.ts +++ b/packages/tempo/test/plugins/ticker.pulse.test.ts @@ -2,6 +2,10 @@ import { Tempo } from '#tempo'; import '#tempo/plugin/extend/extend.ticker.js'; describe('Ticker Pulse Behavior', () => { + beforeEach(() => { + Tempo.init(); + }); + test('limit: 1 should result in 1 pulse currently', async () => { let count = 0; const t = Tempo.ticker({ seconds: 0.1, limit: 1 }, () => count++); diff --git a/packages/tempo/test/plugins/ticker.stop.test.ts b/packages/tempo/test/plugins/ticker.stop.test.ts index f8fbf77..e3eee29 100644 --- a/packages/tempo/test/plugins/ticker.stop.test.ts +++ b/packages/tempo/test/plugins/ticker.stop.test.ts @@ -2,6 +2,11 @@ import { Tempo } from '#tempo'; import '#tempo/plugin/extend/extend.ticker.js'; describe('Ticker Stop Listener', () => { + beforeEach(() => { + Tempo.init(); + }); + + it('should register and invoke stop listeners with pulse callback signature', () => { let calls = 0; let receivedTempo: any; From 575669f542e0b8d6baeb0e0598e3f21fec136733 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Tue, 26 May 2026 18:24:44 +1000 Subject: [PATCH 7/8] vitest.config --- packages/tempo/vitest.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 145c657..26748c1 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -63,6 +63,9 @@ export default defineConfig({ { find: /^#library$/, replacement: resolve(__dirname, '../library/dist/common.index.js') }, ] : [ { find: /^#tempo\/license$/, replacement: isPremiumAvailable ? (licensePremium as string) : licenseDefault }, + { find: /^@magmacomputing\/tempo\/plugin$/, replacement: resolve(__dirname, './src/plugin/plugin.index.ts') }, + { find: /^@magmacomputing\/tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, + { find: /^@magmacomputing\/tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, { find: /^#tempo\/core$/, replacement: resolve(__dirname, './src/core.index.ts') }, { find: /^#tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, { find: /^#tempo\/term\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/$1') }, From 8d53bb3d35c94f634892d1ba056c5cb185270630 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Tue, 26 May 2026 18:33:30 +1000 Subject: [PATCH 8/8] indentation --- packages/tempo/test/plugins/ticker.options.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tempo/test/plugins/ticker.options.test.ts b/packages/tempo/test/plugins/ticker.options.test.ts index ec6da5a..fbaa599 100644 --- a/packages/tempo/test/plugins/ticker.options.test.ts +++ b/packages/tempo/test/plugins/ticker.options.test.ts @@ -2,9 +2,9 @@ import { Tempo } from '#tempo'; import '#tempo/plugin/extend/extend.ticker.js'; describe('Tempo.ticker Options & Enhancements', () => { - beforeEach(() => { - Tempo.init(); - }); + beforeEach(() => { + Tempo.init(); + }); test('ticker with limit (callback)', async () => { let count = 0;