Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
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.

11 changes: 7 additions & 4 deletions packages/tempo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ 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"
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.esm.js"
}
}
</script>
Expand All @@ -102,7 +102,7 @@ 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/@magmacomputing/tempo@2/dist/tempo.bundle.js"></script>
<script>
const t = new Tempo('tomorrow');
console.log(t.toString());
Expand All @@ -116,8 +116,8 @@ 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"
"@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"
}
}
</script>
Expand All @@ -133,6 +133,9 @@ For maximum performance, you can use the lean **Core** engine and opt-in to spec
</script>
```

> [!TIP]
> **CDN Versioning**: The examples above use `@2` to pin to the current major version. To always reference the **latest** release, you can omit the version string (e.g., `.../@magmacomputing/tempo/dist/tempo.bundle.js`).

---

## 📚 Documentation
Expand Down
2 changes: 1 addition & 1 deletion packages/tempo/bin/core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Tempo, enums } from '#tempo/core';
import { stringify, objectify, enumify, getType } from '#library';
import { Token, Snippet } from '#tempo/tempo.default.js';
import { Token, Snippet } from '#tempo/support/tempo.default.js';

// 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 });
Expand Down
25 changes: 14 additions & 11 deletions packages/tempo/doc/Tempo.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ 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": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.esm.js",
"@magmacomputing/tempo/core": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/core.index.js",
"@magmacomputing/tempo/ticker": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/plugin/extend/extend.ticker.js",
"@magmacomputing/tempo/duration": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/plugin/module/module.duration.js",
"@magmacomputing/tempo/format": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/plugin/module/module.format.js",
"@magmacomputing/tempo/plugin": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/plugin/plugin.index.js",
"@magmacomputing/tempo/enums": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.enum.js",
"@magmacomputing/tempo/library": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/library.index.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.min.js"
}
}
</script>
Expand All @@ -49,12 +49,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 `@2` to pin to the current major version. To always reference the **latest** release, you can omit the version string (e.g., `.../@magmacomputing/tempo/dist/tempo.bundle.js`).

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 (`src/tempo.runtime.ts`). 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.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
**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** — `TempoRuntime.createScoped()` returns a fresh, isolated runtime that is *not* stored on `globalThis`, enabling clean test isolation without globalThis manipulation.
- **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
18 changes: 15 additions & 3 deletions packages/tempo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@
"**/module.*.js",
"**/module.*.ts",
"**/tempo.index.js",
"src/tempo.index.ts"
"src/tempo.index.ts",
"src/support/*.ts"
],
"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 +95,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 +114,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
12 changes: 12 additions & 0 deletions packages/tempo/public/bundle.index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script type="importmap">
{
"imports": {
"@magmacomputing/tempo": "https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@2/dist/tempo.bundle.esm.js"
}
}
</script>
<script type="module">
import Tempo from '@magmacomputing/tempo';
const t = new Tempo('next friday');
console.log(t.format('{mon} {day}'));
</script>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
18 changes: 18 additions & 0 deletions packages/tempo/public/esm.index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script type="importmap">
{
"imports": {
"@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"
}
}
</script>
<script type="module">
import { Tempo } from '@magmacomputing/tempo/core';
import { MutateModule } from '@magmacomputing/tempo/mutate';

// Opt-in to mutation logic
Tempo.extend(MutateModule);

const t = new Tempo().add({ days: 1 });
console.log(t.toString());
</script>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
5 changes: 5 additions & 0 deletions packages/tempo/public/script.index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<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());
</script>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
5 changes: 1 addition & 4 deletions packages/tempo/src/core.index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export * from './tempo.class.js';
export { default as enums } from './tempo.enum.js';

// export common patterns and symbols for custom Layouts
export { Token, Snippet, Match, Default, Guard } from './tempo.default.js';
export { enums, Token, Snippet, Match, Default, Guard } from '#tempo/support';
26 changes: 8 additions & 18 deletions packages/tempo/src/plugin/extend/extend.ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { asArray, isNumeric } from '#library/coercion.library.js'
import { instant, normaliseFractionalDurations } from '#library/temporal.library.js'
import { markConfig } from '#library/symbol.library.js'

import { DURATIONS } from '../../tempo.enum.js'
import { defineExtension } from '../plugin.util.js'
import sym from '../../tempo.symbol.js';
import { DURATIONS } from '../../support/tempo.enum.js'
import { defineExtension, attachStatics } from '../plugin.util.js'
import sym from '../../support/tempo.symbol.js';
import type { Tempo } from '../../tempo.class.js'
import type { TempoType } from '../plugin.type.js'

Expand Down Expand Up @@ -356,10 +356,8 @@ class TickerInstance implements Ticker.Descriptor {
export const TickerModule: Tempo.Extension = defineExtension({
name: 'TickerModule',
install(this: Tempo, TempoClass: TempoType) {
if (Object.hasOwn(TempoClass, 'ticker')) return;

Object.defineProperty(TempoClass, 'ticker', {
value: function (this: TempoType, arg1: any, arg2?: any): Ticker.Instance {
attachStatics(TempoClass, {
ticker: function (this: TempoType, arg1: any, arg2?: any): Ticker.Instance {
const instance = new TickerInstance(this as unknown as TempoType, arg1, arg2);
const proxy = new Proxy((() => instance.stop()) as any, {
get: (_, prop) => {
Expand All @@ -380,17 +378,9 @@ export const TickerModule: Tempo.Extension = defineExtension({

return instance.bootstrap(proxy);
},
writable: true,
configurable: true,
enumerable: true
});

if (Object.hasOwn(TempoClass, 'tickers')) return;

Object.defineProperty(TempoClass, 'tickers', {
get: () => Ticker.active,
enumerable: true,
configurable: true
tickers: {
get: () => Ticker.active
}
});
},
});
2 changes: 1 addition & 1 deletion packages/tempo/src/plugin/module/module.composer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isNumeric } from '#library/coercion.library.js';
import { Match } from '../../tempo.default.js';
import { Match } from '../../support/tempo.default.js';
import { TemporalObject, TypeValue, isInstant, isZonedDateTime, isPlainDate, isPlainDateTime, isTempo } from '#library/type.library.js';
import type { Tempo } from '#tempo/tempo.class.js';

Expand Down
27 changes: 11 additions & 16 deletions packages/tempo/src/plugin/module/module.duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { getAccessors } from '#library/reflection.library.js';
import { ifDefined } from '#library/object.library.js';
import { getRelativeTime } from '#library/international.library.js';

import { defineModule, interpret } from '../plugin.util.js';
import enums from '../../tempo.enum.js';
import sym from '../../tempo.symbol.js';
import { defineInterpreterModule, interpret } from '../plugin.util.js';
import enums from '../../support/tempo.enum.js';
import type { Tempo } from '../../tempo.class.js';

declare module '../../tempo.class.js' {
Expand All @@ -28,6 +27,12 @@ declare module '../../tempo.class.js' {
}
}

declare module '#library/type.library.js' {
interface TypeValueMap<T> {
'Tempo.Duration': { type: 'Tempo.Duration', value: Tempo.Duration };
}
}

/**
* Convert a Temporal.Duration to a full Tempo.Duration object (EDO).
*/
Expand Down Expand Up @@ -149,18 +154,8 @@ duration.toDuration = (input: string | Temporal.DurationLikeObject) => {
/**
* Functional Module to attach duration methods to Tempo.
*/
export const DurationModule: Tempo.Module = defineModule({
name: 'duration',
install(this: Tempo, TempoClass: typeof Tempo) {
// 1. Register logic in the global interpreter registry
const modules = (globalThis as any)[sym.$modules] ??= {};
if (isUndefined(modules['DurationModule'])) {
modules['DurationModule'] = duration;
}

// 2. Inject the static helper
(TempoClass as any).duration = function (this: typeof Tempo, input: any) {
return interpret(this, 'DurationModule', 'toDuration', false, input);
};
export const DurationModule: Tempo.Module = defineInterpreterModule('DurationModule', duration, {
duration(this: typeof Tempo, input: any) {
return interpret(this, 'DurationModule', 'toDuration', false, input);
}
});
4 changes: 2 additions & 2 deletions packages/tempo/src/plugin/module/module.format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { pad } from '#library/string.library.js';
import { ifNumeric } from '#library/coercion.library.js';

import { defineInterpreterModule } from '../plugin.util.js';
import { Match } from '../../tempo.default.js';
import { NumericPattern } from '../../tempo.enum.js';
import { Match } from '../../support/tempo.default.js';
import { NumericPattern } from '../../support/tempo.enum.js';
import type { Tempo } from '../../tempo.class.js';

declare module '../../tempo.class.js' {
Expand Down
4 changes: 2 additions & 2 deletions packages/tempo/src/plugin/module/module.lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import '#library/temporal.polyfill.js';
import { isString, isEmpty, isUndefined, isDefined, isTemporal } from '#library/type.library.js';
import { ownKeys, ownEntries } from '#library/primitive.library.js';
import { pad, singular } from '#library/string.library.js';
import { Match } from '../../tempo.default.js';
import enums from '../../tempo.enum.js';
import { Match } from '../../support/tempo.default.js';
import enums from '../../support/tempo.enum.js';
import * as t from '../../tempo.type.js';

/**
Expand Down
Loading