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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.2.5] - 2026-04-20

### Added
- **Cross-Bundle Singleton Stability**: Implemented a symbol-based brand check for `TempoRuntime` to ensure reliable singleton resolution even when multiple versions of the library are loaded.

### Changed
- **Consolidated Internal Storage**: Merged redundant internal term/plugin arrays into a unified, validated `pluginsDb` within `TempoRuntime`, reducing memory overhead and improving consistency.
- **Refined Year Semantics**: Normalized the `year` component in term templates to intelligently distinguish between relative offsets (e.g., year `0`) and absolute historical years (e.g., year `2000`).
- **Improved Type Safety**: Introduced `MatchResult` as a type alias for `Internal.Match` to resolve naming conflicts with the `Match` runtime class, while maintaining the public `Match` export for backward compatibility.

### Fixed
- **Term Resolution Accuracy**: Fixed a sorting bug in the yearly-cycle resolution engine that caused incorrect anchor identification for non-calendar-ordered term groups (e.g., seasons).
- **Documentation Integrity**: Updated architecture and README guides to point to the correct `#tempo/support` module and provided functional, complete importmap examples for browser environments.
- **HTML Standards Compliance**: Wrapped library demonstration and test pages in proper HTML5 skeletons to ensure consistent rendering and prevent quirks-mode issues.
- **Package Optimization**: Refined `sideEffects` in `package.json` to exclude non-published source files, improving tree-shaking for consumer builds.


## [2.2.4] - 2026-04-19

### Fixed
Expand Down
11 changes: 6 additions & 5 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "2.2.4",
"version": "2.2.5",
"private": true,
"description": "Magma Computing Monorepo",
"repository": {
Expand Down Expand Up @@ -41,4 +41,4 @@
"typescript": "^6.0.2",
"vitest": "^2.1.8"
}
}
}
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": "2.2.4",
"version": "2.2.5",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
6 changes: 3 additions & 3 deletions packages/library/src/common/array.library.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { asString } from '#library/coercion.library.js';
import { extract, ownEntries } from '#library/primitive.library.js';
import { stringify } from '#library/serialize.library.js';
import { isNumber, isDate, isTempo, isObject, isDefined, isUndefined, isFunction, nullToValue } from '#library/type.library.js';
import { isNumber, isDate, isObject, isDefined, isUndefined, isFunction, nullToValue } from '#library/type.library.js';
import type { Property } from '#library/type.library.js';

// adapted from https://jsbin.com/insert/4/edit?js,output
Expand Down Expand Up @@ -54,8 +54,8 @@ export function sortBy<T extends Property<T>>(...keys: (PropertyKey | SortBy)[])
switch (true) {
case isNumber(valueA) && isNumber(valueB):
case isDate(valueA) && isDate(valueB):
case isTempo(valueA) && isTempo(valueB):
result = dir * (valueA - valueB);
case isObject(valueA) && isObject(valueB) && typeof valueA.valueOf() === 'number' && typeof valueB.valueOf() === 'number':
result = (dir as any) * ((valueA as any) - (valueB as any));
break;

default:
Expand Down
7 changes: 1 addition & 6 deletions packages/library/src/common/type.library.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import lib from '#library/symbol.library.js';

const $isTempo = Symbol.for('$isTempo');
const registry: Instance[] = []; // global types for getType

/** the primitive type reported by toStringTag() */
Expand Down Expand Up @@ -115,10 +114,6 @@ export const isPlainYearMonth = <T>(obj: T): obj is Extract<T, Temporal.PlainYea
export const isPlainMonthDay = <T>(obj: T): obj is Extract<T, Temporal.PlainMonthDay> => isType(obj, 'Temporal.PlainMonthDay') || (!!(globalThis as any).Temporal?.PlainMonthDay && (obj as any) instanceof (globalThis as any).Temporal.PlainMonthDay);

// non-standard Objects
export const isTempo = <T>(obj?: T): obj is Extract<T, GetType<'Tempo'>> => {
const raw = (obj as any)?.[lib.$Target] ?? obj; // bypass Proxy traps
return !!(raw?.[$isTempo]);
}
export const isEnum = <T, E extends Property<any>>(obj?: T): obj is Extract<T, GetType<'Enumify', E>> => isType(obj, 'Enumify');
export const isPledge = <T, P = any>(obj?: T): obj is Extract<T, GetType<'Pledge', P>> => isType(obj, 'Pledge');

Expand Down Expand Up @@ -172,7 +167,7 @@ const isClassConstructor = (obj: any): boolean => {
const tag = raw?.[Symbol.toStringTag] ?? raw?.prototype?.[Symbol.toStringTag];

// Absolute bypass for Tempo and Temporal identities (using global brands)
if (raw?.[$isTempo] || name === 'Tempo' || tag === 'Tempo' || (typeof tag === 'string' && (tag.startsWith('Temporal.') || tag.startsWith('Tempo.')))) return true;
if (name === 'Tempo' || tag === 'Tempo' || (typeof tag === 'string' && (tag.startsWith('Temporal.') || tag.startsWith('Tempo.')))) return true;
if (typeof tag === 'string' && tag.endsWith('Function')) return false; // check the tag directly to avoid misidentifying function as class

const globalRegistry = (globalThis as any)[lib.$Registry] ?? [];
Expand Down
21 changes: 15 additions & 6 deletions packages/tempo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Working with `Date` in JavaScript has historically been painful. The new `Tempor
- **Natural Language**: Supports word-based numbers (0-10) in relative parsing (e.g., "two days ago").
- **Fluent API**: Chainable methods for adding, subtracting, and setting date-times (similar to Moment.js).
- **Formatting**: Use custom tokens to format date-times in a way that is both intuitive and flexible.
- **Plugin**: Extend core functionality safely; built-ins (like the Ticker) are ready-to-use in the full package, or can be opted-into via side-effect imports when using the lean Core engine.
- **Plugin**: Extend core functionality safely; all extensions (including the Ticker) are opted-into via side-effect imports or explicit registration, ensuring a lean footprint even in the full package.
- **Terms**: Access complex date ranges (Quarters, Seasons, Zodiacs) easily.
- **Immutable**: Operations (like `set` and `add`) return a new `Tempo` instance, ensuring thread safety and predictability.
## 🤔 Why Tempo?
Expand Down Expand Up @@ -87,12 +87,14 @@ Since Tempo is a native ESM package, you can use it directly in modern browsers
<script type="importmap">
{
"imports": {
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/bundle"
"@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"
}
}
</script>
<script type="module">
import Tempo from '@magmacomputing/tempo';
import '@js-temporal/polyfill';
import { Tempo } from '@magmacomputing/tempo';
const t = new Tempo('next friday');
console.log(t.format('{mon} {day}'));
</script>
Expand All @@ -102,7 +104,8 @@ Since Tempo is a native ESM package, you can use it directly in modern browsers
For environments without `importmap` support or simple prototypes, use the global bundle. This automatically attaches the `Tempo` class to the `window` object.

```html
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/global"></script>
<script src="https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5/dist/index.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.js"></script>
<script>
const t = new Tempo('tomorrow');
console.log(t.toString());
Expand All @@ -116,12 +119,15 @@ For maximum performance, you can use the lean **Core** engine and opt-in to spec
<script type="importmap">
{
"imports": {
"@magmacomputing/tempo/core": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/core",
"@magmacomputing/tempo/mutate": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/mutate"
"@js-temporal/polyfill": "https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5/dist/index.esm.js",
"@magmacomputing/tempo/core": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/core.index.js",
"@magmacomputing/tempo/mutate": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/plugin/module/module.mutate.js",
"@magmacomputing/library": "https://cdn.jsdelivr.net/npm/@magmacomputing/library@2/dist/common.index.js"
}
}
</script>
<script type="module">
import '@js-temporal/polyfill';
import { Tempo } from '@magmacomputing/tempo/core';
import { MutateModule } from '@magmacomputing/tempo/mutate';

Expand All @@ -133,6 +139,9 @@ For maximum performance, you can use the lean **Core** engine and opt-in to spec
</script>
```

> [!TIP]
> **CDN Versioning**: The examples above use pinned versions (`@magmacomputing/tempo@2`, `@magmacomputing/library@2`, `@js-temporal/polyfill@0.5`) for production stability. To use the latest releases, you can omit the version string from every URL (e.g., remove `@2` from all Magma entries and `@0.5` from the polyfill). Ensure all `@magmacomputing/...` entries resolve to the same release to avoid mixed-version loading.

---

## 📚 Documentation
Expand Down
7 changes: 3 additions & 4 deletions packages/tempo/bin/core.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Tempo, enums } from '#tempo/core';
import { stringify, objectify, enumify, getType } from '#library';
import { Token, Snippet } from '#tempo/tempo.default.js';
import { stringify, objectify, enumify, getType, Pledge } from '#library';

// Pre-load Tempo and Token to the global scope for ease of use in the core REPL
Object.assign(globalThis, { Tempo, Token, Snippet, getType, stringify, objectify, enumify, enums });
// Pre-load core symbols (Tempo, getType, stringify, objectify, enumify, Pledge, enums) to the global scope
Object.assign(globalThis, { Tempo, getType, stringify, objectify, enumify, Pledge, enums });

console.log(`\n\x1b[38;2;252;194;1m\x1b[1m ⏳ Tempo (core) \x1b[0m\x1b[38;2;45;212;191mREPL initialized (core only).\x1b[0m\n`);

Expand Down
19 changes: 8 additions & 11 deletions packages/tempo/doc/Tempo.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,10 @@ Tempo is an ESM-first library. You can use it in the browser without a build ste
<script type="importmap">
{
"imports": {
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/tempo.index.js",
"@magmacomputing/tempo/core": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/core.index.js",
"@magmacomputing/tempo/ticker": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/plugin/extend/extend.ticker.js",
"@magmacomputing/tempo/duration": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/plugin/module/module.duration.js",
"@magmacomputing/tempo/format": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/plugin/module/module.format.js",
"@magmacomputing/tempo/plugin": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/plugin/plugin.index.js",
"@magmacomputing/tempo/enums": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/tempo.enum.js",
"@magmacomputing/tempo/library": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/dist/library.index.js",
"@magmacomputing/library": "https://cdn.jsdelivr.net/npm/@magmacomputing/library/dist/common.index.js",
"@js-temporal/polyfill": "https://cdn.jsdelivr.net/npm/@js-temporal/polyfill/dist/index.esm.min.js"
"@magmacomputing/tempo/core": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/core.index.js",
"@magmacomputing/tempo/mutate": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/plugin/module/module.mutate.js",
"@magmacomputing/library": "https://cdn.jsdelivr.net/npm/@magmacomputing/library@2/dist/common.index.js",
"@js-temporal/polyfill": "https://cdn.jsdelivr.net/npm/@js-temporal/polyfill@0.5/dist/index.esm.js"
}
}
</script>
Expand All @@ -49,12 +43,15 @@ Tempo is an ESM-first library. You can use it in the browser without a build ste
For legacy environments or simple prototypes, use the single-file bundle:

```html
<script src="/path/to/tempo/dist/tempo.bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.js"></script>
<script>
const t = new Tempo();
</script>
```

> [!TIP]
> **CDN Versioning**: The examples above use pinned versions (`@magmacomputing/tempo@2`, `@magmacomputing/library@2`, `@js-temporal/polyfill@0.5`) for production stability. To use the latest releases, you can omit the version string from every URL (e.g., remove `@2` from all Magma entries and `@0.5` from the polyfill). Ensure all `@magmacomputing/...` entries resolve to the same release to avoid mixed-version loading.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
---

## Installation
Expand Down
21 changes: 21 additions & 0 deletions packages/tempo/doc/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
Tempo v2.0.1 introduces several industry-leading architectural patterns designed for maximum resilience in complex Monorepo and Proxy-wrapped environments.

## 🌐 Shared Global Registry

### TempoRuntime — single hardened bridge (v2.2+)

Prior to v2.2, Tempo spread its inter-module state across many `globalThis[Symbol.for(…)]` slots (`$terms`, `$extends`, `$modules`, `$installed`, `$reset`, `$Plugins`, `$Register`). Each slot was a potential tamper target and the scattered writes made the global namespace harder to audit.

As of v2.2, all of that bookkeeping is consolidated inside a single **`TempoRuntime`** object (`#tempo/support`). The runtime is stored on `globalThis` under one hardened property:

```typescript
Symbol.for('magmacomputing/tempo/runtime')
```

The property descriptor is `enumerable: false, configurable: false, writable: false`. External code can neither replace nor delete the runtime.

**Benefits:**
- **Reduced global footprint** — one slot instead of seven.
- **Centralised hardening** — input validation (`addTerm`, `addPlugin`) and hook management (`setRegisterHook`, `fireRegisterHook`) live in one place.
- **Scoped runtimes (Experimental)** — `TempoRuntime.createScoped()` returns a fresh, isolated runtime that is *not* stored on `globalThis`, enabling clean test isolation without globalThis manipulation. **Note**: Scoped runtimes are currently an experimental internal feature and are not yet fully threaded through all core utilities. Scoped runtimes are not pinned to `globalThis`, lack the `defineProperty` descriptor protections of the primary instance, and instead rely solely on the lexical reference returned (contrasting with the hardened `getRuntime()` and `globalThis[BRIDGE]` behavior). Implementation examples of this test-scoping pattern can be found in [plugin_registration.test.ts](../test/plugin_registration.test.ts) and [duration.core.test.ts](../test/duration.core.test.ts).
- **Multi-bundle / HMR safety** — `getRuntime()` checks `globalThis[BRIDGE]` before constructing, so two bundle copies of Tempo always share the same runtime object, preserving the original split-brain guarantee.

**User-facing "Global Discovery" slots remain on `globalThis`.** The `sym.$Tempo` slot (and custom discovery symbols passed to `Tempo.init`) are intentionally user-readable, so they stay as ordinary writable properties. Only internal bookkeeping moved into the runtime.

To solve the "Split-Brain" issue inherent in monorepo development (where multiple instances of the same library might be loaded), Tempo utilizes a **Shared Global Registry**. By leveraging `Symbol.for('magmacomputing/library/registry')` on `globalThis`, all versions of the Tempo and Library packages share a unified type-identification engine. This ensures that classes are correctly identified as constructors even when loaded across different module boundaries.

## 🕵️ Decoupled Logging (Logify)
Expand Down
21 changes: 20 additions & 1 deletion packages/tempo/doc/releases/v2.x.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
# 📜 Version 2.x History

## [v2.2.3] - 2026-04-20
## [v2.2.5] - 2026-04-20
### 🏗️ Modular Hardening
- **Singleton Resilience**: Replaced `instanceof` checks in `TempoRuntime` with a cross-bundle brand check, ensuring singletons are correctly adopted across bundle boundaries and HMR reloads.
- **Unified Discovery**: Consolidated internal storage for term and extension plugins into a single, validated `pluginsDb` structure.
- **Resolved Shadowing**: Eliminated parameter shadowing in runtime hook methods to ensure reliable event emission.

### 🔍 Term Resolution Refinements
- **Chronological Stability**: Enforced chronological sorting in `resolveCycleWindow` for accurate cycle anchor identification.
- **Absolute vs Relative Years**: Normalized year handling to allow templates to mix relative offsets and absolute historical years seamlessly.

### 📚 Documentation & UX
- **Standards Mode**: Modernized all public demonstration files with proper HTML5 skeletons and metadata.
- **Contextual Guidance**: Updated architecture docs and READMEs with accurate module paths and functional importmap examples.

## [v2.2.4] - 2026-04-19
### 🛡️ Production Safety
- **Immutable Compatibility**: Added redefinition guards to `TickerModule` to prevent errors on already-frozen classes.
- **ESM Integrity**: Bundled `tslib` into granular ESM builds to resolve resolution failures in standard browser environments.

## [v2.2.3] - 2026-04-19
### New Features

- Modular parse engine and standalone parsing support
Expand Down
22 changes: 16 additions & 6 deletions packages/tempo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
"version": "2.2.4",
"version": "2.2.5",
"description": "The Tempo core library",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand All @@ -27,11 +27,13 @@
"**/*-polyfill.ts",
"**/module.*.js",
"**/module.*.ts",
"**/tempo.index.js",
"src/tempo.index.ts"
"**/tempo.index.js"
],
"main": "dist/tempo.index.js",
"types": "dist/tempo.index.d.ts",
"browser": "dist/tempo.bundle.js",
"unpkg": "dist/tempo.bundle.js",
"jsdelivr": "dist/tempo.bundle.js",
"imports": {
"#library": "@magmacomputing/library",
"#library/*.js": "@magmacomputing/library/common/*.js",
Expand Down Expand Up @@ -91,6 +93,14 @@
"development": "./src/plugin/term/term.*.ts",
"default": "./dist/plugin/term/term.*.js"
},
"#tempo/support": {
"development": "./src/support/support.index.ts",
"default": "./dist/support/support.index.js"
},
"#tempo/support/*.js": {
"development": "./src/support/*.ts",
"default": "./dist/support/*.js"
},
"#tempo/*.js": {
"development": "./src/*.ts",
"default": "./dist/*.js"
Expand All @@ -102,8 +112,8 @@
"import": "./dist/tempo.index.js"
},
"./enums": {
"types": "./dist/tempo.enum.d.ts",
"import": "./dist/tempo.enum.js"
"types": "./dist/support/tempo.enum.d.ts",
"import": "./dist/support/tempo.enum.js"
},
"./extend/*": {
"types": "./dist/plugin/extend/extend.*.d.ts",
Expand Down Expand Up @@ -199,7 +209,7 @@
},
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
"@magmacomputing/library": "2.2.4",
"@magmacomputing/library": "2.2.5",
"@rollup/plugin-alias": "^6.0.0",
"cross-env": "^7.0.3",
"magic-string": "^0.30.21",
Expand Down
Loading