Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a81b523
Add CI workflow for Tempo project
magmacomputing Apr 25, 2026
b4ba0e7
Refactor CI workflow for Node.js setup and tests
magmacomputing Apr 25, 2026
c6d7a15
bump vitest
magmacomputing Apr 26, 2026
12eceea
working on ci.yml
magmacomputing Apr 26, 2026
fdc351e
review 1st pass
magmacomputing Apr 26, 2026
64299a9
patch env in CI.yml
magmacomputing Apr 26, 2026
06aabcd
debug in compact.time
magmacomputing Apr 26, 2026
4fadfc7
debug in tempo.class
magmacomputing Apr 26, 2026
23d7c89
debug Intl.Locale
magmacomputing Apr 26, 2026
c4c6e39
test Intl.Local('en-us')
magmacomputing Apr 26, 2026
79d38a6
mdy Fallback list
magmacomputing Apr 26, 2026
e8ad9e9
clean-up timeZones fallback
magmacomputing Apr 26, 2026
ce08a9b
more debug fallback timeZones
magmacomputing Apr 26, 2026
0100a75
cut down on debug
magmacomputing Apr 26, 2026
725f66f
isMonthDay
magmacomputing Apr 26, 2026
987ee49
fix to $setEvents
magmacomputing Apr 26, 2026
fad7235
add bench/ for CI testing
magmacomputing Apr 26, 2026
f9d2997
Options grouping, isMonthDay
magmacomputing Apr 27, 2026
430563b
merge with main
magmacomputing Apr 27, 2026
7d2ae4a
complete merge
magmacomputing Apr 27, 2026
25e667f
post-merge baseline
magmacomputing Apr 27, 2026
a2ca7eb
rm spyOn from test-scripts
magmacomputing Apr 27, 2026
2f77c22
Tempo.config
magmacomputing Apr 27, 2026
a4f06f0
test-cases passing
magmacomputing Apr 28, 2026
69349bf
doc/tempo.month-day
magmacomputing Apr 28, 2026
bca222b
test sbox
magmacomputing Apr 28, 2026
40714f9
draw a line... layout ordering
magmacomputing Apr 28, 2026
980b249
new scripts in package.json
magmacomputing Apr 28, 2026
68dd00a
PR review #1 updates
magmacomputing Apr 28, 2026
dcaf82c
PR review #2 updates
magmacomputing Apr 28, 2026
e40dda0
PR review #3 updates
magmacomputing Apr 29, 2026
da96de0
#library
magmacomputing Apr 29, 2026
4537efa
resolve some final typings
magmacomputing Apr 29, 2026
c93f67f
realign test-scripts
magmacomputing Apr 29, 2026
a9db3f1
plan
magmacomputing Apr 30, 2026
6dbcfcf
PR review #4 updates
magmacomputing Apr 30, 2026
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
26 changes: 7 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('packages/tempo/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
cache: 'npm'
- name: Install monorepo dependencies
run: npm ci
working-directory: ${{ github.workspace }}
Expand All @@ -51,32 +45,26 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('packages/tempo/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
cache: 'npm'
- name: Install monorepo dependencies
run: npm ci
working-directory: ${{ github.workspace }}
- name: Write parsePrefilter setup file
run: |
echo "import { Tempo } from '../src/tempo.index.ts';\nTempo.init({ parsePrefilter: true });" > packages/tempo/test/ci.prefilter.setup.js
- name: Run all tests with parsePrefilter
run: npm test
working-directory: packages/tempo
env:
TEMPO_PREFILTER_CI: 'true'
- name: Run end-to-end benchmark
run: npx tsx --conditions=development bench/bench.parse.prefilter.e2e.ts > bench-output.json
run: npx tsx --conditions=development bench/bench.parse.prefilter.e2e.ts > bench-output.json 2> bench-error.log
working-directory: packages/tempo
- name: Upload benchmark output
if: always()
uses: actions/upload-artifact@v4
with:
name: bench-parse-prefilter-e2e
path: packages/tempo/bench-output.json
path: |
packages/tempo/bench-output.json
packages/tempo/bench-error.log
- name: Validate benchmark output
run: |
node -e "const r=require('./packages/tempo/bench-output.json');if(!r.success){console.error('Benchmark failed:',r.errors);process.exit(1)}else{console.log('Benchmark passed.')}"
Expand Down
39 changes: 35 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.4.0] - 2026-04-24
## [2.7.0] - 2026-04-27

### Added
- **Sandbox Factory Mode**: Introduced `Tempo.create()`, a static factory method for creating isolated `Tempo` subclasses with independent configurations and registries, preventing global state leakage.
- **Layout Controller Framework**: Added a classification-based layout controller to `engine.layout`, enabling future input-aware parsing optimizations.
- **Grouped Configuration Options**: Consolidated `monthDay` and `relativeTime` options into nested objects.
- **Internal layout detection**: Added `isMonthDay` detection for improved regional layout resolution.
- **CI Benchmarks**: Added performance benchmarking suite to CI.

### Fixed
- **Event Overrides**: Fixed `$setEvents` logic to correctly handle custom event overrides.
- **TimeZone Fallbacks**: Improved and cleaned up the IANA TimeZone fallback list.
- **Intl.Locale Debugging**: Enhanced diagnostic logging for locale resolution.

## [2.6.0] - 2026-04-25

### Added
- **Standardized UTC Offsets**: Added `normalizeUtcOffset` utility for transforming informal UTC-offset strings.
- **Custom Layout Order**: Added `layoutOrder` option to customize parsing element precedence.

### Changed
- **Layout Order Resolver**: Extracted layout-ordering logic into a dedicated module to improve maintainability and testability.
- **Season Scope Simplification (Breaking)**: Removed Chinese-specific object from `term.season` scope.
- **Refined TimeZone Normalization**: Improved UTC offset handling during initialization.

### Fixed
- **Layout Pattern Resolution**: Fixed ordering to respect intended sequence.

## [2.5.0] - 2026-04-24

### Added
- **Sandbox Factory Mode**: Introduced `Tempo.create()`, a static factory method for creating isolated `Tempo` subclasses with independent configurations and registries.
- **Layout Order Resolver Module**: Extracted layout-ordering decision logic into a dedicated `engine.layout` module.
- **Layout Controller Framework**: Implemented minimal controller-map infrastructure for future input-class pre-filtering.
- **Debug Layout Order Visibility**: Added optional debug output in `Tempo.#swapLayout` to emit the resolved layout order for diagnostics.

### Changed
- **Internal Layout Resolution**: Refactored `Tempo.#swapLayout` to delegate ordering to the external resolver.
- **Alias Precedence**: User-defined `event` and `period` aliases now take precedence over built-in aliases.
- **Module Path Flattening**: Relocated core modules to `src/module/` for a flatter, more intuitive internal architecture.

### Fixed
- **Determinism Coverage**: Added comprehensive unit tests for layout resolution and multi-pair swap handling.

## [2.4.0] - (Skipped)

_Version 2.4.0 was not released; the project merged new functionality from 2.4.0 into 2.5.0._

## [2.3.0] - 2026-04-22

Expand Down
21 changes: 16 additions & 5 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "2.6.0",
"version": "2.7.0",
"private": true,
"description": "Magma Computing Monorepo",
"repository": {
Expand Down
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.6.0",
"version": "2.7.0",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/library/src/common/array.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export function sortBy<T extends Property<T>>(...keys: (PropertyKey | SortBy)[])
if (result === 0) { // no need to look further if result !== 0
const dir = key.dir === 'desc' ? -1 : 1;
const field = key.field + (key.index ? `[${key.index}]` : '');
const valueA = extract(left, field, nullishToValue(key.default, 0));
const valueB = extract(right, field, nullishToValue(key.default, 0));
const valueA = extract(left, field, nullishToValue(key.default, 0));
const valueB = extract(right, field, nullishToValue(key.default, 0));

switch (true) {
case isNumber(valueA) && isNumber(valueB):
Expand Down
20 changes: 10 additions & 10 deletions packages/library/src/common/assertion.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const isPrimitive = (obj?: unknown): obj is Primitive => isType(obj, 'Str
export const isReference = (obj?: unknown): obj is Object => !isPrimitive(obj);
export const isIterable = <T>(obj: unknown): obj is Iterable<T> => Symbol.iterator in Object(obj) && !isString(obj);

export const isString = <T>(obj?: T): obj is Extract<T, string> => isType(obj, 'String');
export const isNumber = <T>(obj?: T): obj is Extract<T, number> => isType(obj, 'Number');
export const isString = <T>(obj: T): obj is T & string => isType(obj, 'String');
export const isNumber = <T>(obj: T): obj is T & number => isType(obj, 'Number');
export const isFiniteNumber = <T>(obj?: T): obj is Extract<T, number> => isType(obj, 'Number') && isFinite(obj as number);

/** test if can convert String to Numeric */
Expand All @@ -32,26 +32,26 @@ export const isInteger = <T>(obj?: T): obj is Extract<T, bigint> => isType(obj,
export const isIntegerLike = <T>(obj?: T): obj is Extract<T, string> => isType(obj, 'String') && /^-?[0-9]+n$/.test(obj as string);
export const isDigit = <T>(obj?: T): obj is Extract<T, number | bigint> => isType(obj, 'Number', 'BigInt');
export const isBoolean = <T>(obj?: T): obj is Extract<T, boolean> => isType(obj, 'Boolean');
export const isArray = <T>(obj: unknown): obj is T[] => isType(obj, 'Array');
export const isArray = <T>(obj: T): obj is T & any[] => isType(obj, 'Array');
export const isArrayLike = <T>(obj: any): obj is ArrayLike<T> => protoType(obj) === 'Object' && 'length' in obj && Object.keys(obj).every(key => key === 'length' || !isNaN(Number(key)));
export const isObject = <T>(obj?: T): obj is Extract<T, object> => isType(obj, 'Object');
export const isDate = <T>(obj?: T): obj is Extract<T, Date> => isType(obj, 'Date');
export const isObject = <T>(obj: T): obj is T & Property<any> => isType(obj, 'Object');
export const isDate = <T>(obj: T): obj is T & Date => isType(obj, 'Date');
export const isRegExp = <T>(obj?: T): obj is Extract<T, RegExp> => isType(obj, 'RegExp');
export const isRegExpLike = <T>(obj?: T): obj is Extract<T, string> => isType(obj, 'String') && /^\/.*\/$/.test(obj as string);
export const isSymbol = <T>(obj?: T): obj is Extract<T, symbol> => isType(obj, 'Symbol');
export const isSymbolFor = <T>(obj?: T): obj is Extract<T, symbol> => isType<symbol>(obj, 'Symbol') && Symbol.keyFor(obj) !== undefined;
export const isPropertyKey = (obj?: unknown): obj is PropertyKey => isType<PropertyKey>(obj, 'String', 'Number', 'Symbol');

export const isNull = <T>(obj?: T): obj is Extract<T, null> => isType(obj, 'Null');
export const isNullish = <T>(obj: T): obj is Extract<T, Nullish> => isType<undefined | null | void>(obj, 'Null', 'Undefined', 'Void', 'Empty');
export const isUndefined = <T>(obj?: T): obj is undefined => isType<undefined>(obj, 'Undefined', 'Void', 'Empty');
export const isNull = <T>(obj: T): obj is T & null => isType(obj, 'Null');
export const isNullish = <T>(obj: T): obj is T & Nullish => isType<undefined | null | void>(obj, 'Null', 'Undefined', 'Void', 'Empty');
export const isUndefined = <T>(obj: T): obj is T & undefined => isType<undefined>(obj, 'Undefined', 'Void', 'Empty');
export const isDefined = <T>(obj: T): obj is NonNullable<T> => !isNullish(obj);

export const isClass = <T>(obj?: T): obj is Extract<T, Function> => isType(obj, 'Class');
export const isFunction = <T>(obj?: T): obj is Extract<T, Function> => isType(obj, 'Function', 'AsyncFunction');
export const isPromise = <T>(obj?: T): obj is Extract<T, Promise<any>> => isType(obj, 'Promise');
export const isMap = <T, K = any, V = any>(obj?: T): obj is Extract<T, Map<K, V>> => isType(obj, 'Map');
export const isSet = <T, K = any>(obj?: T): obj is Extract<T, Set<K>> => isType(obj, 'Set');
export const isMap = <T>(obj: T): obj is T & Map<any, any> => isType(obj, 'Map');
export const isSet = <T>(obj: T): obj is T & Set<any> => isType(obj, 'Set');
export const isError = <T>(err?: T): err is Extract<T, Error> => isType(err, 'Error');

export const isTemporal = <T>(obj: T): obj is Extract<T, Temporals> => protoType(obj).startsWith('Temporal.') || (!!(globalThis as any).Temporal && (
Expand Down
22 changes: 12 additions & 10 deletions packages/library/src/common/class.library.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ownEntries } from '#library/primitive.library.js';
import { $ImmutableSkip } from '#library/symbol.library.js';
import { registerSerializable } from '#library/serialize.library.js';
import { type Constructor, type Type, registerType } from '#library/type.library.js';

Expand Down Expand Up @@ -26,15 +26,17 @@ export function Immutable<T extends Constructor>(value: T, { kind, name, addInit

addInitializer(() => { // wait for construction to complete
const protect = (obj: object) => { // protect existing members
ownEntries(Object.getOwnPropertyDescriptors(obj))
.filter(([name]) => name !== 'constructor') // dont touch the constructor
.forEach(([name, { configurable, writable }]) => {
if (configurable) {
const update: PropertyDescriptor = { configurable: false };
if (writable) update.writable = false; // only data descriptors have 'writable'
Object.defineProperty(obj, name, update);
}
});
const skip = (obj as any)[$ImmutableSkip] ?? (obj as any).$ImmutableSkip ?? (obj.constructor as any)?.[$ImmutableSkip] ?? (obj.constructor as any)?.$ImmutableSkip ?? [];
Reflect.ownKeys(obj).forEach(name => {
if (name === 'constructor' || (Array.isArray(skip) && skip.some(s => String(s) === String(name)))) return;

const desc = Object.getOwnPropertyDescriptor(obj, name);
if (desc?.configurable) {
const update: PropertyDescriptor = { configurable: false };
if (desc.writable) update.writable = false; // only data descriptors have 'writable'
Object.defineProperty(obj, name, update);
}
});
};

protect(value); // protect original static members
Expand Down
15 changes: 4 additions & 11 deletions packages/library/src/common/coercion.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@ export function asArray<T>(arr: Exclude<ArrayLike<T>, string> | undefined): T[];
export function asArray<T>(arr: T | Exclude<Iterable<T> | undefined, string>): NonNullable<T>[];
export function asArray<T, K>(arr: Iterable<T> | ArrayLike<T>, fill: K): K[];
export function asArray<T, K>(arr: T | Iterable<T> | ArrayLike<T> = [], fill?: K): (T | K)[] {
switch (true) {
case isArrayLike<T>(arr): // allow for {length:nn} objects
case isIterable<T>(arr) && !isString(arr): // dont iterate Strings
return Array.from<T, K>(arr, val => {
return isUndefined(fill) || isDefined(val)
? val as unknown as K // if no {fill}, then use {val}
: clone(fill) // clone {fill} to create new Objects
});
const mapFn = (val: unknown) => (isUndefined(fill) || isDefined(val)) ? val as unknown as K : clone(fill);

default:
return Array.of(arr);
}
return (isArrayLike<T>(arr) || (isIterable<T>(arr) && !isString(arr)))
? Array.from<T, K>(arr as Iterable<T>, mapFn)
: [arr as T] as (T | K)[];
}

/** stringify if not nullish */
Expand Down
52 changes: 36 additions & 16 deletions packages/library/src/common/object.library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ownKeys, ownEntries } from '#library/primitive.library.js';
import { isObject, isArray, isReference, isFunction, isDefined, isNullish } from '#library/assertion.library.js';
import { isObject, isArray, isReference, isFunction, isDefined, isNullish, isMap, isSet } from '#library/assertion.library.js';
import type { Extend, Property } from '#library/type.library.js';

/** remove quotes around property names */
Expand All @@ -22,24 +22,44 @@ export const asObject = <T>(obj?: Record<PropertyKey, any>) => {
return temp as T;
}

/** deep-compare object values for equality */
export const isEqual = (obj1: any = {}, obj2: any = {}): boolean => {
const keys = new Set<PropertyKey>(); // union of unique keys from both Objects
const keys1 = isFunction(obj1.keys) ? Array.from<PropertyKey>(obj1.keys()) : ownKeys(obj1);
const keys2 = isFunction(obj2.keys) ? Array.from<PropertyKey>(obj2.keys()) : ownKeys(obj2);
/** deep-compare object and array values for equality */
export const isEqual = (a: any, b: any): boolean => {
if (a === b) return true;
if (isNullish(a) || isNullish(b)) return a === b;
if (typeof a !== typeof b) return false;

keys1.forEach(key => keys.add(key));
keys2.forEach(key => keys.add(key));
if (isArray(a) && isArray(b)) {
const left = a as any[], right = b as any[];
return left.length === right.length && left.every((v, i) => isEqual(v, right[i]));
}

return [...keys] // cast as Array
.every(key => {
const val1 = obj1[key];
const val2 = obj2[key];
if (isMap(a) && isMap(b)) {
const left = a as Map<any, any>, right = b as Map<any, any>;
return left.size === right.size &&
Array.from(left.keys()).every(k => right.has(k) && isEqual(left.get(k), right.get(k)));
}

return isReference(val1) && isReference(val2)
? isEqual(val1, val2) // recurse into object
: val1 === val2
})
if (isSet(a) && isSet(b)) {
const left = a as Set<any>, right = b as Set<any>;
return left.size === right.size &&
Array.from(left).every(v => right.has(v));
}

if (isObject(a) && isObject(b)) {
const left = a as any, right = b as any;
const keys = new Set<PropertyKey>();
const keys1 = (isFunction(left.keys) && Object.getPrototypeOf(left) !== Object.prototype) ? Array.from<PropertyKey>(left.keys()) : ownKeys(left);
const keys2 = (isFunction(right.keys) && Object.getPrototypeOf(right) !== Object.prototype) ? Array.from<PropertyKey>(right.keys()) : ownKeys(right);

keys1.forEach(k => keys.add(k));
keys2.forEach(k => keys.add(k));

if (keys.size !== keys1.length || keys.size !== keys2.length) return false;

return [...keys].every(k => isEqual(left[k], right[k]));
Comment thread
magmacomputing marked this conversation as resolved.
}

return false;
}

/** find all methods on an Object */
Expand Down
4 changes: 3 additions & 1 deletion packages/library/src/common/proxy.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ function factory<T extends object>(target: T, options: ProxyOptions = {}): T {
pending.delete(k);
}
// silent mark to avoid redundant discovery
if (Reflect.isExtensible(t) && !Reflect.has(t, k))
// Only define if object is extensible and not frozen
if (Reflect.isExtensible(t) && !Object.isFrozen(t) && !Reflect.has(t, k)) {
Object.defineProperty(t, k, { value: undefined, writable: true, enumerable: false, configurable: true });
}
}

const val = Reflect.get(t, k, r);
Expand Down
3 changes: 2 additions & 1 deletion packages/library/src/common/symbol.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ export const $Logify: unique symbol = Symbol.for('$LibraryLogify') as any;
export const $Registry: unique symbol = Symbol.for('$LibraryRegistry') as any;
export const $Register: unique symbol = Symbol.for('$LibraryRegister') as any;
export const $SerializerRegistry: unique symbol = Symbol.for('$LibrarySerializerRegistry') as any;
export const $ImmutableSkip: unique symbol = Symbol.for('$LibraryImmutableSkip') as any;
export const $Identity: unique symbol = Symbol.for('$LibraryIdentity') as any;

export const sym = {
$Target, $Discover, $Extensible, $Inspect, $Logify, $Registry, $Register, $SerializerRegistry, $Identity
$Target, $Discover, $Extensible, $Inspect, $Logify, $Registry, $Register, $SerializerRegistry, $Identity, $ImmutableSkip
} as const;

/** identify and mark a Logify configuration object */
Expand Down
Loading
Loading