From f00ef1ddad4990e1454af8a51291f81636d0df1b Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Sun, 19 Jul 2026 11:29:10 +1000 Subject: [PATCH 01/13] 1st commit --- packages/plugins/finance/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugins/finance/package.json b/packages/plugins/finance/package.json index a769e42b..ef4396e1 100644 --- a/packages/plugins/finance/package.json +++ b/packages/plugins/finance/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo-plugin-finance", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "description": "Tempo Community Plugin: Finance namespace and fiscal year utilities", "main": "dist/index.js", @@ -50,4 +50,4 @@ "import": "./dist/index.js" } } -} \ No newline at end of file +} From 1c760035bb578484152b9e8c6a188d733768586f Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 20 Jul 2026 09:10:13 +1000 Subject: [PATCH 02/13] pre ESLint --- .../library/src/browser/mapper.library.ts | 58 ++++++++++++--- packages/library/src/browser/tapper.class.ts | 46 ++++++++++-- .../library/src/browser/webstore.class.ts | 71 +++++++++++++++++-- .../library/src/browser/window.library.ts | 32 +++++++++ packages/library/src/server/auth.library.ts | 7 +- packages/library/src/server/file.library.ts | 45 ++++++++++++ packages/plugins/finance/package.json | 4 +- packages/tempo/.vitepress/config.ts | 8 ++- .../tempo/.vitepress/theme/data/catalog.json | 2 +- .../doc/6-utility-library/tempo.library.md | 14 ++++ packages/tempo/package.json | 2 +- packages/tempo/typedoc.library.json | 20 ++++++ 12 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 packages/tempo/typedoc.library.json diff --git a/packages/library/src/browser/mapper.library.ts b/packages/library/src/browser/mapper.library.ts index b44530f3..623e919c 100644 --- a/packages/library/src/browser/mapper.library.ts +++ b/packages/library/src/browser/mapper.library.ts @@ -44,10 +44,14 @@ const store = await new Promise((resolve, reject) => { }) /** - * attempt geolocation.getCurrentPosition() - * -> if user allows, then return geo-coordinates - * -> if not allowed, then set error = GeolocationPositionError - * -> if not support, then set error = NOT_SUPPORTED + * Attempt geolocation via navigator.getCurrentPosition(). + * + * @param opts - Options for the geolocation attempt + * @returns A promise resolving to the device's coordinates + * @example + * ```ts + * const pos = await geoLocation(); + * ``` */ export const geoLocation = (opts = {} as MapOpts) => new Promise((resolve, reject) => { @@ -90,7 +94,16 @@ export const geoLocation = (opts = {} as MapOpts) => store?.set(MAP_KEY, mapStore); // stash currentPosition to localStorage }) -/** format coordinates as a GeocoderRequest["location"] object */ +/** + * Format coordinates as a GeocoderRequest["location"] object. + * + * @param coords - Optional coordinates to format; defaults to current location + * @returns A promise resolving to a GeocoderRequest + * @example + * ```ts + * const req = await geoCoords(); + * ``` + */ export const geoCoords = (coords?: google.maps.GeocoderRequest) => new Promise((resolve, reject) => { if (!isNullish(coords)) @@ -107,7 +120,17 @@ export const geoCoords = (coords?: google.maps.GeocoderRequest) => // the following functions need the Map API enabled on the web-site's index page // https://developers.google.com/maps/documentation/javascript/load-maps-js-api -/** Make a 'maps' request on google API */ +/** + * Make a 'maps' request to the Google Geocoding API. + * + * @param coords - Coordinates to query + * @param opts - Request options + * @returns A promise resolving to the geocoder response + * @example + * ```ts + * const res = await mapQuery({ location: { lat: 0, lng: 0 } }); + * ``` + */ export const mapQuery = (coords?: google.maps.GeocoderRequest, opts = {} as MapOpts) => new Promise((resolve, reject) => { opts = Object.assign({}, defaults, opts); @@ -153,8 +176,16 @@ export const mapQuery = (coords?: google.maps.GeocoderRequest, opts = {} as MapO }) /** - * get Hemisphere ('north' | 'south' | null) - * for supplied coordinates (else query current geolocation) + * Get Hemisphere ('north' | 'south' | null) for supplied coordinates + * or current geolocation if none supplied. + * + * @param coords - Optional coordinates + * @param opts - Request options + * @returns A promise resolving to the hemisphere string + * @example + * ```ts + * const sphere = await mapHemisphere(); + * ``` */ export const mapHemisphere = (coords?: google.maps.GeocoderRequest, opts = {} as MapOpts) => mapQuery(coords, opts) // ask Google @@ -183,8 +214,15 @@ export const mapHemisphere = (coords?: google.maps.GeocoderRequest, opts = {} as }) /** - * query google-maps for a best-guess address at supplied {lat,lng} co-ordinates - * (default current location) + * Query google-maps for a best-guess address at supplied {lat,lng} coordinates. + * + * @param coords - Optional coordinates; defaults to current location + * @param opts - Request options + * @returns A promise resolving to the best-guess address object + * @example + * ```ts + * const address = await mapAddress(); + * ``` */ export const mapAddress = (coords?: google.maps.GeocoderRequest, opts = {} as MapOpts) => mapQuery(coords, opts) diff --git a/packages/library/src/browser/tapper.class.ts b/packages/library/src/browser/tapper.class.ts index d94ffca8..9247377a 100644 --- a/packages/library/src/browser/tapper.class.ts +++ b/packages/library/src/browser/tapper.class.ts @@ -3,8 +3,13 @@ import { isEmpty, isFunction } from '#library/assertion.library.js'; import type { ValueOf } from '#library/type.library.js'; /** - * A Wrapper Class around HammerJS. - * manages single/double/triple Tap events + * A Wrapper Class around HammerJS. + * Manages single, double, and triple tap events on a given element. + * + * @example + * ```ts + * const tapper = new Tapper('#my-button', [Tapper.EVENT.SingleTap, () => console.log('Tapped!')]); + * ``` */ export class Tapper { static EVENT = enumify({ @@ -49,7 +54,12 @@ export class Tapper { self.on(...setup); } - /** list of callbacks to fire on 'singleTap', or tuple of events/callbacks to fire */ + /** + * Register a list of callbacks to fire on 'singleTap', or a tuple of events/callbacks to fire. + * + * @param events - Callbacks for singleTap or Tuples of [Event, Callback] + * @returns The Tapper instance for chaining + */ on(...events: (Tapper.Callback | Tapper.Tuple)[]) { events .forEach(arg => { @@ -66,7 +76,12 @@ export class Tapper { return this; } - /** stop event listeners (default is 'all' listeners on this instance) */ + /** + * Stop event listeners. Defaults to 'all' listeners on this instance if none specified. + * + * @param events - Specific events to stop listening for + * @returns The Tapper instance for chaining + */ off(...events: Tapper.EVENT[]) { if (isEmpty(events)) events.push(...Tapper.EVENT.values()); @@ -77,6 +92,13 @@ export class Tapper { return this; } + /** + * Enable or disable specific event listeners (or all if none specified). + * + * @param enable - Whether to enable or disable the events + * @param events - Specific events to target + * @returns The Tapper instance for chaining + */ enable(enable = true, ...events: Tapper.EVENT[]) { if (isEmpty(events)) events.push(...Tapper.EVENT.values()); @@ -89,7 +111,11 @@ export class Tapper { return this; } - /** list details about this instance */ + /** + * List details about the active Hammer instances managed by this Tapper. + * + * @returns Array of Hammer instance details + */ list() { return this.#hammer.map((hammer: any) => ({ element: hammer.element, @@ -97,12 +123,18 @@ export class Tapper { })) } - /** stop all event listeners on this instance */ + /** + * Stop all event listeners on this instance. + * + * @returns The Tapper instance for chaining + */ clear() { return this.off(); } - /** detach Tapper Manager */ + /** + * Detach and destroy all underlying Hammer instances. + */ destroy() { this.#hammer.forEach(hammer => hammer.destroy()); } diff --git a/packages/library/src/browser/webstore.class.ts b/packages/library/src/browser/webstore.class.ts index 8ef3ce4b..c927745c 100644 --- a/packages/library/src/browser/webstore.class.ts +++ b/packages/library/src/browser/webstore.class.ts @@ -13,17 +13,32 @@ type STORAGE = ValueOf /** * Wrapper around local / session Browser Storage. * Refactored for lazy-initialization to ensure side-effect free imports. + * + * @example + * ```ts + * const store = WebStore.local; + * store.set('user', { id: 1, name: 'Alice' }); + * const user = store.get('user'); + * ``` */ export class WebStore { private static _localInstance?: WebStore; private static _sessionInstance?: WebStore; - /** Lazy getter for localStorage wrapper */ + /** + * Lazy getter for the localStorage wrapper instance. + * + * @returns The WebStore instance for localStorage + */ static get local() { return WebStore._localInstance ??= new WebStore(STORAGE.Local); } - /** Lazy getter for sessionStorage wrapper */ + /** + * Lazy getter for the sessionStorage wrapper instance. + * + * @returns The WebStore instance for sessionStorage + */ static get session() { return WebStore._sessionInstance ??= new WebStore(STORAGE.Session); } @@ -43,8 +58,13 @@ export class WebStore { this.#type = storage; } - public get(key: PropertyKey): T | null; - public get(key: PropertyKey, dflt: T): T; + /** + * Retrieve a value from storage, optionally providing a default. + * + * @param key - The property key to retrieve + * @param dflt - An optional default value if the key does not exist + * @returns The parsed object from storage, or the default/null if not found + */ public get(key: PropertyKey, dflt?: T) { const str = this.#storage.getItem(stringify(key)); return isString(str) @@ -52,6 +72,14 @@ export class WebStore { : (dflt ?? null) } + /** + * Store or merge a value into storage for a given key. + * + * @param key - The property key to set. If nullish, clears the store. + * @param obj - The value to store + * @param opt - Options (merge behavior) + * @returns The WebStore instance for chaining + */ public set(key?: PropertyKey, obj?: unknown, opt = { merge: true }) { if (isNullish(key)) // synonym for 'clear' return this.clear(); @@ -99,27 +127,56 @@ export class WebStore { } } + /** + * Clear all items from this storage instance. + * + * @returns The WebStore instance for chaining + */ public clear() { this.#storage.clear(); return this; } + /** + * Delete specific keys from storage. + * + * @param keys - The keys to remove + * @returns The WebStore instance for chaining + */ public del(...keys: PropertyKey[]) { // list of keys to remove keys .forEach(key => this.#storage.removeItem(stringify(key))) return this; } + /** + * Get an array of keys currently in storage. If specific keys are provided, filters by those. + * + * @param keys - Optional keys to filter by + * @returns An array of string/symbol keys + */ public keys(...keys: PropertyKey[]) { // list of keys (or all) return this.entries(...keys) .map(([key,]) => key) } + /** + * Get an array of values currently in storage. + * + * @param keys - Optional keys to retrieve values for + * @returns An array of parsed values + */ public values(...keys: PropertyKey[]) { // list of keys (or all) to lookup return this.entries(...keys) .map(([, val]) => val) } + /** + * Get an array of [key, value] entries currently in storage. + * + * @param keys - Optional keys to filter the entries by + * @returns An array of key-value tuples + */ public entries(...keys: PropertyKey[]) { // list of keys (or all) to lookup const wanted = new Set(keys.map(key => stringify(key))); @@ -128,6 +185,12 @@ export class WebStore { .filter(([key]) => isEmpty(keys) || wanted.has(stringify(key))) } + /** + * Populate the storage from an existing object map. + * + * @param store - The object/dictionary to populate from + * @returns The WebStore instance for chaining + */ public from(store: Property) { ownEntries(store) .forEach(([key, val]) => this.set(key, val)) diff --git a/packages/library/src/browser/window.library.ts b/packages/library/src/browser/window.library.ts index 3feacd0f..33477b4a 100644 --- a/packages/library/src/browser/window.library.ts +++ b/packages/library/src/browser/window.library.ts @@ -1,4 +1,36 @@ +/** + * Displays a native browser alert dialog with the provided message. + * + * @param msg - The message to display in the alert dialog + * @example + * ```ts + * alert('Operation completed successfully!'); + * ``` + */ export const alert = (msg: any) => window.alert(msg); + +/** + * Displays a native browser prompt dialog, asking the user for input. + * + * @param msg - The message/question to display + * @param dflt - An optional default value for the input field + * @returns The text entered by the user, or null if cancelled + * @example + * ```ts + * const name = prompt('What is your name?', 'Guest'); + * ``` + */ export const prompt = (msg: any, dflt?: any) => window.prompt(msg, dflt); + +/** + * Displays a native browser confirmation dialog with OK/Cancel buttons. + * + * @param msg - The message to display (e.g., 'Are you sure?') + * @returns True if the user clicked OK, false otherwise + * @example + * ```ts + * const isSure = confirm('Are you sure you want to delete this?'); + * ``` + */ export const confirm = (msg?: string) => window.confirm(msg); diff --git a/packages/library/src/server/auth.library.ts b/packages/library/src/server/auth.library.ts index d77095cd..0a5d9903 100644 --- a/packages/library/src/server/auth.library.ts +++ b/packages/library/src/server/auth.library.ts @@ -6,7 +6,8 @@ const MAX_PAYLOAD_LENGTH = 4096; // 4 KB /** * Decodes a JWT payload without verifying its signature. * - * @WARNING This function does NOT perform signature verification. + * @remarks + * **WARNING:** This function does NOT perform signature verification. * It strictly decodes the payload for inspection. To ensure the integrity * and authenticity of the token, you MUST verify the signature using * a trusted library (e.g., jsonwebtoken) and your secret/public key. @@ -14,6 +15,10 @@ const MAX_PAYLOAD_LENGTH = 4096; // 4 KB * @param token - The JWT string to decode * @throws {Error} If the token is malformed or the payload cannot be parsed * @returns The parsed JSON payload of the JWT + * @example + * ```ts + * const payload = decodeJWTPayload(token); + * ``` */ export const decodeJWTPayload = (token: string): T => { if (token.length > MAX_TOKEN_LENGTH) diff --git a/packages/library/src/server/file.library.ts b/packages/library/src/server/file.library.ts index 5fb513d6..d8064121 100644 --- a/packages/library/src/server/file.library.ts +++ b/packages/library/src/server/file.library.ts @@ -4,6 +4,10 @@ import * as path from 'node:path'; import { ifNumeric } from '#library/coercion.library.js'; +/** + * A utility class for sandboxed file operations within a temporary directory. + * Prevents path traversal and forces all operations to occur in os.tmpdir(). + */ export class File { static tmpDir = os.tmpdir(); static encoding: BufferEncoding = 'utf8'; @@ -31,6 +35,16 @@ export class File { return targetPath; } + /** + * Read a file's contents from the temporary directory. + * + * @param file - The filename to read + * @returns A promise resolving to the file contents (coerced to number/bigint if applicable) + * @example + * ```ts + * const content = await File.read('data.txt'); + * ``` + */ static read = (file: string): Promise => new Promise((resolve, reject) => { try { const target = File._resolvePath(file); @@ -47,6 +61,17 @@ export class File { } }) + /** + * Write content to a file in the temporary directory. + * + * @param file - The filename to write to + * @param doc - The content to write + * @returns A promise resolving to the written content + * @example + * ```ts + * await File.write('output.json', '{"status":"ok"}'); + * ``` + */ static write = (file: string, doc: string | NodeJS.ArrayBufferView) => new Promise((resolve, reject) => { try { const target = File._resolvePath(file); @@ -56,6 +81,16 @@ export class File { } }) + /** + * Check if a file exists in the temporary directory. + * + * @param file - The filename to check + * @returns A promise resolving to true if the file exists, false otherwise + * @example + * ```ts + * const hasConfig = await File.exist('config.json'); + * ``` + */ static exist = (file: string) => new Promise((resolve, reject) => { try { const target = File._resolvePath(file); @@ -69,6 +104,16 @@ export class File { } }) + /** + * Remove a file from the temporary directory. + * + * @param file - The filename to remove + * @returns A promise resolving when the file is deleted + * @example + * ```ts + * await File.remove('temp-data.txt'); + * ``` + */ static remove = (file: string) => new Promise((resolve, reject) => { try { const target = File._resolvePath(file); diff --git a/packages/plugins/finance/package.json b/packages/plugins/finance/package.json index ef4396e1..5e8eab55 100644 --- a/packages/plugins/finance/package.json +++ b/packages/plugins/finance/package.json @@ -38,10 +38,10 @@ "plan": "community" }, "peerDependencies": { - "@magmacomputing/tempo": "^3.8.0" + "@magmacomputing/tempo": "^3.9.0" }, "devDependencies": { - "@magmacomputing/tempo": "^3.8.0", + "@magmacomputing/tempo": "^3.9.0", "vitest": "^1.0.0" }, "exports": { diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 6a02c545..1b1b429e 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -12,6 +12,7 @@ if (typeof (globalThis as any).Temporal === 'undefined') { } import typedocSidebar from '../doc/api/typedoc-sidebar.json' +import librarySidebar from '../doc/api/library/typedoc-sidebar.json' export default defineConfig({ base: '/magma/', @@ -96,7 +97,12 @@ export default defineConfig({ { text: 'Enumerators', link: '/doc/6-utility-library/tempo.enumerators' }, { text: 'Serializers', link: '/doc/6-utility-library/tempo.serializers' }, { text: 'Decorators', link: '/doc/6-utility-library/tempo.decorators' }, - { text: 'Advanced Promises (Pledge)', link: '/doc/6-utility-library/tempo.pledge' } + { text: 'Advanced Promises (Pledge)', link: '/doc/6-utility-library/tempo.pledge' }, + { + text: 'Library API', + collapsed: true, + items: librarySidebar + } ] }, { diff --git a/packages/tempo/.vitepress/theme/data/catalog.json b/packages/tempo/.vitepress/theme/data/catalog.json index b52b08a1..d5e3b9d5 100644 --- a/packages/tempo/.vitepress/theme/data/catalog.json +++ b/packages/tempo/.vitepress/theme/data/catalog.json @@ -24,7 +24,7 @@ "packageName": "@magmacomputing/tempo-plugin-finance", "plan": "community", "status": "active", - "version": "1.0.1" + "version": "1.0.2" }, { "id": "snap", diff --git a/packages/tempo/doc/6-utility-library/tempo.library.md b/packages/tempo/doc/6-utility-library/tempo.library.md index bf1d6eda..86bf57e3 100644 --- a/packages/tempo/doc/6-utility-library/tempo.library.md +++ b/packages/tempo/doc/6-utility-library/tempo.library.md @@ -56,4 +56,18 @@ Tempo provides a specialized wrapper around `Promise.withResolvers()` called `Pl πŸ‘‰ **[Read the full Pledge Guide](./tempo.pledge.md)** for advanced usage with callbacks, debugging tags, and lifecycle management. +
+ +## 5. Exhaustive API Reference + +> [!NOTE] +> These are isolated, standalone utility functions and classes developed internally to support our various applications. They are entirely free to use and are documented here as a convenience reference for our users. + +While some of these utilities may be used internally by the Tempo library, many are completely independent (such as the browser and server-specific functions). They do not declare external dependencies, keeping them lightweight and portable. + +The library is split into domain-specific modules: +- **Browser**: Functions and classes that rely on browser APIs (e.g., `window`, `localStorage`, `Geolocation`). +- **Server**: Node.js specific utilities (e.g., file system access, server-side JWT decoding). +- **Common** *(coming soon)*: Runtime-agnostic utilities shared across all environments. +You can browse the full API reference in the sidebar below this section. diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 1250171e..d486156f 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -209,7 +209,7 @@ "prebuild": "npm run build:version", "clean": "magma-cli rm dist && (node ../../node_modules/typescript-7/bin/tsc -b --clean || true)", "prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && if [ -z \"$TEMPO_LICENSE_PATH\" ] || [ ! -f \"$TEMPO_LICENSE_PATH\" ]; then echo '🚨 ERROR: TEMPO_LICENSE_PATH is missing or invalid. Cannot publish Premium build.'; exit 1; fi && npm run build", - "docs:api": "typedoc", + "docs:api": "typedoc && typedoc --options typedoc.library.json", "docs:dev": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress dev", "docs:build": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress build", "docs:preview": "vitepress preview", diff --git a/packages/tempo/typedoc.library.json b/packages/tempo/typedoc.library.json new file mode 100644 index 00000000..fcb37268 --- /dev/null +++ b/packages/tempo/typedoc.library.json @@ -0,0 +1,20 @@ +{ + "entryPoints": [ + "../library/src/browser.index.ts", + "../library/src/server.index.ts" + ], + "out": "doc/api/library", + "tsconfig": "../library/tsconfig.json", + "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"], + "hideBreadcrumbs": true, + "hidePageTitle": true, + "disableSources": true, + "parametersFormat": "table", + "outputFileStrategy": "members", + "flattenOutputFiles": true, + "expandObjects": true, + "useCodeBlocks": true, + "readme": "none", + "excludeInternal": true, + "githubPages": false +} From c8eed932df1232fcf6799d5c6156208f0e65e89a Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 20 Jul 2026 10:24:26 +1000 Subject: [PATCH 03/13] post TSDoc --- packages/library/src/common/array.library.ts | 123 +++++++++++- .../library/src/common/assertion.library.ts | 63 +++++- .../library/src/common/boundary.library.ts | 7 + packages/library/src/common/buffer.library.ts | 69 ++++++- packages/library/src/common/cipher.library.ts | 68 ++++++- packages/library/src/common/class.library.ts | 59 +++++- .../library/src/common/coercion.library.ts | 103 +++++++++- .../library/src/common/enumerate.library.ts | 22 ++- .../library/src/common/function.library.ts | 46 ++++- .../src/common/international.library.ts | 129 ++++++++++-- packages/library/src/common/logger.class.ts | 20 +- packages/library/src/common/number.library.ts | 67 ++++++- packages/library/src/common/object.library.ts | 119 ++++++++++- packages/library/src/common/pledge.class.ts | 49 ++++- .../library/src/common/primitive.library.ts | 82 +++++++- packages/library/src/common/proxy.library.ts | 79 +++++++- .../library/src/common/reflection.library.ts | 108 ++++++++-- .../library/src/common/request.library.ts | 25 ++- .../library/src/common/scopedset.class.ts | 7 +- .../library/src/common/serialize.library.ts | 74 ++++++- .../library/src/common/storage.library.ts | 33 +++- packages/library/src/common/string.library.ts | 186 ++++++++++++++++-- packages/library/src/common/symbol.library.ts | 12 +- .../library/src/common/temporal.library.ts | 90 ++++++--- packages/library/src/common/type.library.ts | 50 +++-- .../library/src/common/utility.library.ts | 49 ++++- .../library/src/common/webtoken.library.ts | 29 ++- .../test/common/number_library.test.ts | 14 +- packages/tempo/typedoc.library.json | 8 +- 29 files changed, 1581 insertions(+), 209 deletions(-) diff --git a/packages/library/src/common/array.library.ts b/packages/library/src/common/array.library.ts index dbdf3a5a..10b3adae 100644 --- a/packages/library/src/common/array.library.ts +++ b/packages/library/src/common/array.library.ts @@ -5,7 +5,19 @@ import { isNumber, isDate, isObject, isDefined, isUndefined, isFunction } from ' import type { Property } from '#library/type.library.js'; // adapted from https://jsbin.com/insert/4/edit?js,output -/** insert a value into an Array by its sorted position */ +/** + * Inserts a value into an array at its sorted position using binary search. + * Supports both primitive values and objects (via a sorting key). + * + * @param arr - The array to insert into (defaults to an empty array) + * @param val - The value to insert + * @param key - The property key to sort by if inserting objects + * @returns The mutated array containing the inserted value + * @example + * ```ts + * sortInsert([1, 3], 2); // [1, 2, 3] + * ``` + */ export const sortInsert = (arr: T[] = [], val: T, key?: K) => { const obj = isObject(val) && isDefined(key); // array of Objects let low = 0, high = arr.length; @@ -35,7 +47,18 @@ export interface SortBy { index?: number | '*'; default?: any; } -/** provide a sort-function to order a set of keys */ + +/** + * Creates a comparison function for sorting an array of objects by multiple keys. + * + * @param keys - The keys or sorting options to apply in priority order + * @returns A comparison function suitable for `Array.prototype.sort()` + * @example + * ```ts + * const arr = [{ a: 1, b: 'z' }, { a: 1, b: 'a' }]; + * arr.sort(sortBy('a', { field: 'b', dir: 'desc' })); + * ``` + */ export function sortBy>(...keys: (PropertyKey | SortBy)[]) { const sortOptions = keys // coerce string => SortBy .flat() // flatten Array-of-Array @@ -69,16 +92,46 @@ export function sortBy>(...keys: (PropertyKey | SortBy)[]) } } -/** return an array sorted-by a series of keys */ +/** + * Sorts an array of objects in place by a series of keys. + * + * @param array - The array to sort + * @param keys - The keys or sorting options to apply + * @returns The sorted array + * @example + * ```ts + * const sorted = sortKey(users, 'lastName', 'firstName'); + * ``` + */ export function sortKey>(array: T[], ...keys: (PropertyKey | SortBy)[]) { return array.sort(sortBy(...keys)); } type GroupFn> = (value: T, index?: number) => PropertyKey -/** group array of objects by the return value of the passed callback. */ +/** + * Groups an array of objects by the return value of a callback function. + * + * @param arr - The array of objects to group + * @param grpFn - The callback function returning the group key + * @returns A record containing arrays of grouped objects + * @example + * ```ts + * const groups = byKey([{ id: 1, type: 'A' }, { id: 2, type: 'A' }], itm => itm.type); + * ``` + */ export function byKey>(arr: T[], grpFn: GroupFn): Record; -/** group array of objects according to a list of key fields. */ +/** + * Groups an array of objects by a sequence of key fields. + * + * @param arr - The array of objects to group + * @param keys - The sequence of object keys to group by + * @returns A record containing arrays of grouped objects + * @example + * ```ts + * const groups = byKey(users, 'department', 'role'); + * ``` + */ export function byKey>(arr: T[], ...keys: (keyof T)[]): Record; export function byKey>(arr: T[], fnKey: GroupFn | keyof T, ...keys: (keyof T)[]) { if (isFunction(fnKey)) @@ -95,9 +148,29 @@ export function byKey>(arr: T[], fnKey: GroupFn | key ) } -/** group array of objects by the return value of the passed callback, but only the 'last' entry */ +/** + * Groups an array of objects by a callback function, retaining only the last entry per group. + * + * @param arr - The array of objects to group + * @param grpFn - The callback function returning the group key + * @returns A record containing the last object from each group + * @example + * ```ts + * const latest = byLkp(events, ev => ev.id); + * ``` + */ export function byLkp>(arr: T[], grpFn: GroupFn): Record; -/** group array of objects according to a list of key fields, but only the 'last' entry */ +/** + * Groups an array of objects by key fields, retaining only the last entry per group. + * + * @param arr - The array of objects to group + * @param keys - The sequence of object keys to group by + * @returns A record containing the last object from each group + * @example + * ```ts + * const latest = byLkp(events, 'type'); + * ``` + */ export function byLkp>(arr: T[], ...keys: (keyof T)[]): Record; export function byLkp>(arr: T[], fnKey: GroupFn | keyof T, ...keys: (keyof T)[]) { const group = isFunction(fnKey) @@ -108,13 +181,32 @@ export function byLkp>(arr: T[], fnKey: GroupFn | key .reduce((acc, [key, grp]) => Object.assign(acc, { [key]: grp?.pop() }), {} as Record) } -/** clear down an Array */ +/** + * Clears an array in-place, removing all elements and filling with null before truncation. + * + * @param arr - The array to clear + * @returns The cleared array + * @example + * ```ts + * const arr = [1, 2, 3]; + * clear(arr); // arr is now [] + * ``` + */ export function clear(arr: T[]) { arr.fill(null as any).length = 0; return arr; } -/** return cartesian-product of Array of Arrays */ +/** + * Generates the cartesian product of multiple arrays. + * + * @param args - The arrays to combine + * @returns An array of combinations (each combination is an array) + * @example + * ```ts + * cartesian([1, 2], ['a', 'b']); // [[1, 'a'], [1, 'b'], [2, 'a'], [2, 'b']] + * ``` + */ export function cartesian(...args: T[][]): T[][] { const [a, b = [], ...c] = args; const cartFn = (a: any[], b: any[]) => ([] as any[]).concat(...a.map(d => b.map(e => ([] as any[]).concat(d, e)))); @@ -124,7 +216,18 @@ export function cartesian(...args: T[][]): T[][] { : (a || []) as T[][]; } -/** tap into an Array */ +/** + * Executes a callback with the provided array and returns the array. + * Useful for side-effects (e.g., logging) in the middle of a function chain. + * + * @param arr - The array to tap into + * @param fn - The callback to execute + * @returns The original array + * @example + * ```ts + * tap([1, 2, 3], console.log).map(x => x * 2); + * ``` + */ export function tap(arr: T[], fn: (value: T[]) => void) { fn(arr); return arr; diff --git a/packages/library/src/common/assertion.library.ts b/packages/library/src/common/assertion.library.ts index c7132e59..3932cbd9 100644 --- a/packages/library/src/common/assertion.library.ts +++ b/packages/library/src/common/assertion.library.ts @@ -2,7 +2,17 @@ import { sym } from '#library/symbol.library.js'; import { getType, protoType, asType } from '#library/type.library.js'; import type { Type, Primitive, Nullish, Temporals, Property, GetType } from '#library/type.library.js'; -/** assert value is one of a list of Types */ +/** + * Asserts if a value matches one of the provided types from the Type system. + * + * @param obj - The value to check + * @param types - The list of valid Types + * @returns True if the value matches one of the specified types + * @example + * ```ts + * if (isType(value, 'String', 'Number')) { ... } + * ``` + */ export const isType = (obj: unknown, ...types: Type[]): obj is T => types.includes(getType(obj)); /** Type-Guards: assert \ is of \ */ @@ -14,7 +24,18 @@ export const isString = (obj: unknown): obj is string => isType(obj, 'St export const isNumber = (obj: unknown): obj is number => isType(obj, 'Number'); export const isFiniteNumber = (obj: unknown): obj is number => isType(obj, 'Number') && isFinite(obj as number); -/** test if can convert String to Numeric */ +/** + * Tests if a value can be safely converted to a numeric value. + * Handles strings, numbers, and BigInts, verifying finite properties and valid formats. + * + * @param str - The value to test + * @returns True if the value can be evaluated numerically + * @example + * ```ts + * isNumeric('123'); // true + * isNumeric('abc'); // false + * ``` + */ export function isNumeric(str?: any): boolean { const type = typeof str; switch (type) { @@ -94,7 +115,18 @@ export const isPledge =

(obj: unknown): obj is GetType<'Pledge', P> => export const isExtensible = (obj: any): obj is any => isDefined(obj?.[sym.$Extensible]); export const isTarget = (obj: any): obj is any => isDefined(obj?.[sym.$Target]); -/** object has no values */ +/** + * Checks if a value is effectively empty. + * Returns true for nullish values, empty objects, empty strings, NaN, empty arrays, and empty sets/maps. + * + * @param obj - The value to check + * @returns True if the value is empty + * @example + * ```ts + * isEmpty([]); // true + * isEmpty({ a: 1 }); // false + * ``` + */ export const isEmpty = (obj?: T) => false || isNullish(obj) || (isObject(obj) && (Reflect.ownKeys(obj).length === 0)) @@ -104,9 +136,34 @@ export const isEmpty = (obj?: T) => false || (isSet(obj) && (obj.size === 0)) || (isMap(obj) && (obj.size === 0)) +/** + * Asserts a condition is true, otherwise throws an Error. + * + * @param condition - The boolean condition that must be true + * @param message - The error message to throw if the condition is false + * @throws {Error} If the condition evaluates to false + * @example + * ```ts + * assertCondition(user.isLoggedIn, 'User must be logged in'); + * ``` + */ export function assertCondition(condition: boolean, message?: string): asserts condition { if (!condition) throw new Error(message); } + +/** + * Asserts a value is a string, otherwise throws an Error. + * + * @param str - The value to assert as a string + * @throws {Error} If the value is not a string + */ export function assertString(str: unknown): asserts str is string { assertCondition(isString(str), `Invalid string: ${str}`) }; + +/** + * A TypeScript exhaustiveness check that throws an error if reached at runtime. + * + * @param val - The value that should never exist + * @throws {Error} Always throws an error + */ export function assertNever(val: never): asserts val is never { throw new Error(`Unexpected object: ${val}`) }; diff --git a/packages/library/src/common/boundary.library.ts b/packages/library/src/common/boundary.library.ts index 581af3ad..3361a6b5 100644 --- a/packages/library/src/common/boundary.library.ts +++ b/packages/library/src/common/boundary.library.ts @@ -22,6 +22,13 @@ export interface BoundaryContext { /** * Global Error Boundary Utility. * Decouples the decision to throw an error from the act of logging it. + * + * @param err - The Error object or error string to raise + * @param context - Optional boundary context configuration + * @example + * ```ts + * raise('Invalid input', { catch: true, logger: myLogger }); + * ``` */ export function raise(err: Error | string, context: BoundaryContext = {}): void { const error = isString(err) ? new Error(err) : err; diff --git a/packages/library/src/common/buffer.library.ts b/packages/library/src/common/buffer.library.ts index 7447570b..bbbc6c6f 100644 --- a/packages/library/src/common/buffer.library.ts +++ b/packages/library/src/common/buffer.library.ts @@ -2,13 +2,42 @@ import { stringify, objectify } from '#library/serialize.library.js'; const CHUNK_SIZE = 8192; -/** serialize any object and encode string into a Uint8Array */ +/** + * Serializes any object to a string and encodes it into a Uint8Array. + * + * @param str - The object or string to serialize and encode + * @returns A Uint8Array containing the encoded data + * @example + * ```ts + * const buf = encodeBuffer({ a: 1 }); + * ``` + */ export const encodeBuffer = (str: any) => new TextEncoder().encode(stringify(str)); -/** decode a Uint8Array back to a string */ +/** + * Decodes a Uint8Array or ArrayBuffer back into a string. + * + * @param buf - The buffer to decode + * @param encoding - The text encoding to use (default: 'utf-8') + * @returns The decoded string + * @example + * ```ts + * const str = decodeBuffer(buf); + * ``` + */ export const decodeBuffer = (buf: Uint8Array | ArrayBuffer, encoding = 'utf-8') => new TextDecoder(encoding).decode(buf); -/** encode a raw Uint8Array into a Base64 string natively */ +/** + * Encodes a raw Uint8Array into a Base64 string. + * Uses native `Buffer` in Node.js and fallbacks to `btoa` in browsers. + * + * @param buffer - The raw Uint8Array to encode + * @returns The Base64 string representation + * @example + * ```ts + * const b64 = bufferToBase64(new Uint8Array([104, 105])); + * ``` + */ export const bufferToBase64 = (buffer: Uint8Array) => { if (typeof Buffer !== 'undefined') return Buffer.from(buffer).toString('base64'); @@ -20,7 +49,17 @@ export const bufferToBase64 = (buffer: Uint8Array) => { return btoa(binary); } -/** decode a Base64 string into a raw Uint8Array natively */ +/** + * Decodes a Base64 string into a raw Uint8Array. + * Uses native `Buffer` in Node.js and fallbacks to `atob` in browsers. + * + * @param base64 - The Base64 string to decode + * @returns A Uint8Array of the decoded data + * @example + * ```ts + * const buf = base64ToBuffer('aGk='); + * ``` + */ export const base64ToBuffer = (base64: string) => { if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(base64, 'base64')); @@ -34,14 +73,32 @@ export const base64ToBuffer = (base64: string) => { return bytes; } -/** serialize any object and encode it to Base64 */ +/** + * Serializes any object, encodes it to a buffer, and outputs a Base64 string. + * + * @param input - The object to serialize and encode + * @returns A Base64 string representation of the serialized object + * @example + * ```ts + * const token = encodeBase64({ user: 'michael' }); + * ``` + */ export const encodeBase64 = (input: unknown): string => { const str = stringify(input); return bufferToBase64(encodeBuffer(str)); } -/** decode a Base64 string and deserialize it back into an object */ +/** + * Decodes a Base64 string and deserializes it back into a typed object. + * + * @param base64 - The Base64 string to decode + * @returns The deserialized object + * @example + * ```ts + * const obj = decodeBase64(token); + * ``` + */ export const decodeBase64 = (base64 = ''): T => { const uint8 = base64ToBuffer(base64); const str = decodeBuffer(uint8); diff --git a/packages/library/src/common/cipher.library.ts b/packages/library/src/common/cipher.library.ts index bee441ca..56381814 100644 --- a/packages/library/src/common/cipher.library.ts +++ b/packages/library/src/common/cipher.library.ts @@ -1,6 +1,6 @@ import { toHex } from '#library/number.library.js'; import { asString, asError } from '#library/coercion.library.js'; -import { isError, isString } from '#library/assertion.library.js'; +import { isError } from '#library/assertion.library.js'; import { bufferToBase64, base64ToBuffer, encodeBuffer, decodeBuffer } from '#library/buffer.library.js'; const crypto = globalThis.crypto; @@ -14,7 +14,8 @@ export const keys = { } as const; // Module-scoped state for ephemeral keys -const _cryptoKey = subtle.generateKey({ name: keys.TypeKey, length: 128 }, false, ['encrypt', 'decrypt']) +const _cryptoKey = subtle + .generateKey({ name: keys.TypeKey, length: 128 }, false, ['encrypt', 'decrypt']) .catch(asError); const _asymmetricKey = subtle.generateKey({ @@ -25,9 +26,26 @@ const _asymmetricKey = subtle.generateKey({ }, false, ['sign', 'verify']) .catch(asError); -/** random UUID */ +/** + * Generates a random, short UUID key based on standard Web Crypto API UUIDs. + * + * @returns A randomly generated short string + * @example + * ```ts + * const key = randomKey(); // 'e8b7a421' + * ``` + */ export const randomKey = () => crypto.randomUUID().split('-')[0]; +/** + * Generates a Hash-based Message Authentication Code (HMAC) for a given source payload. + * + * @param source - The data to hash + * @param secret - The secret key used for hashing + * @param alg - The hash algorithm to use (default: 'SHA-512') + * @param len - Optional length to truncate the resulting hex string + * @returns A promise resolving to the HMAC hex string + */ export const hmac = async (source: string | Object, secret: string, alg = 'SHA-512', len?: number) => { const encoder = new TextEncoder(); const keyData = encoder.encode(secret); @@ -46,6 +64,14 @@ export const hmac = async (source: string | Object, secret: string, alg = 'SHA-5 return toHex(Array.from(new Uint8Array(signature)), len); }; +/** + * Computes a cryptographic hash digest for the provided source payload. + * + * @param source - The data to hash + * @param len - Optional length to truncate the resulting hex string + * @param alg - The hash algorithm to use (default: 'SHA-256') + * @returns A promise resolving to the hash hex string + */ export const hash = async (source: string | Object, len?: number, alg = 'SHA-256') => { const buffer = encodeBuffer(asString(source)); const hashBuf = await subtle.digest(alg, buffer); @@ -53,6 +79,12 @@ export const hash = async (source: string | Object, len?: number, alg = 'SHA-256 return toHex(Array.from(new Uint8Array(hashBuf)), len); } +/** + * Encrypts arbitrary data using AES-GCM and a module-scoped ephemeral symmetric key. + * + * @param data - The data to encrypt + * @returns A promise resolving to the Base64-encoded encrypted string (including IV) + */ export const encrypt = async (data: any) => { const iv = crypto.getRandomValues(new Uint8Array(16)); const key = await _cryptoKey; @@ -67,6 +99,12 @@ export const encrypt = async (data: any) => { return bufferToBase64(combined); } +/** + * Decrypts a Base64-encoded encrypted string using the module-scoped ephemeral symmetric key. + * + * @param secret - The encrypted string (or a promise resolving to one) + * @returns A promise resolving to the decrypted, deserialized data + */ export const decrypt = async (secret: Promise | string) => { const [str, key] = await Promise.all([secret, _cryptoKey]); if (isError(key)) throw new Error(`Cipher: Key generation failed: ${key.message}`, { cause: key }); @@ -80,6 +118,12 @@ export const decrypt = async (secret: Promise | string) => { .then(decodeBuffer); } +/** + * Signs arbitrary data using RSASSA-PKCS1-v1_5 and a module-scoped ephemeral asymmetric keypair. + * + * @param doc - The data to sign + * @returns A promise resolving to the Uint8Array signature + */ export const sign = async (doc: any) => { const keypair = await _asymmetricKey; if (isError(keypair)) throw new Error(`Cipher: Key generation failed: ${keypair.message}`, { cause: keypair }); @@ -89,6 +133,13 @@ export const sign = async (doc: any) => { .then(result => new Uint8Array(result)); } +/** + * Verifies a signature against the provided data using the module-scoped ephemeral asymmetric public key. + * + * @param signature - The signature buffer to verify + * @param doc - The original data payload + * @returns A promise resolving to a boolean indicating verification success + */ export const verify = async (signature: Promise | ArrayBuffer | Uint8Array, doc: any) => { const [buffer, keypair] = await Promise.all([signature, _asymmetricKey]); if (isError(keypair)) throw new Error(`Cipher: Key generation failed: ${keypair.message}`, { cause: keypair }); @@ -97,6 +148,12 @@ export const verify = async (signature: Promise | ArrayBuffer | Uin return subtle.verify(keys.SignKey, keypair.publicKey, buffer as BufferSource, encodeBuffer(doc)); } +/** + * Imports a PEM-formatted public key string into a native Web Crypto API CryptoKey object. + * + * @param pem - The PEM-formatted public key string + * @returns A promise resolving to the imported CryptoKey + */ export const importPublicKey = async (pem: string): Promise => { const pemHeader = '-----BEGIN PUBLIC KEY-----'; const pemFooter = '-----END PUBLIC KEY-----'; @@ -119,6 +176,11 @@ export const importPublicKey = async (pem: string): Promise => { ); } +/** + * Generates a new RSASSA-PKCS1-v1_5 asymmetric keypair using the Web Crypto API. + * + * @returns A promise resolving to the generated CryptoKeyPair + */ export const generateKeyPair = async (): Promise => { return subtle.generateKey({ name: keys.SignKey, diff --git a/packages/library/src/common/class.library.ts b/packages/library/src/common/class.library.ts index e3d85a23..82745cc2 100644 --- a/packages/library/src/common/class.library.ts +++ b/packages/library/src/common/class.library.ts @@ -20,7 +20,7 @@ function getClassName(value: T, contextName: string | sym /** * Shared helper to create an immutable or secure class wrapper * - * @note **Workaround:** When TS 7.0 (targeting ES2022) emits its `__esDecorate` IIFE, aggressive + * @remarks **Workaround:** When TS 7.0 (targeting ES2022) emits its `__esDecorate` IIFE, aggressive * bundlers and minifiers (like `Rollup`, `Terser`, or `esbuild`) frequently compress the variable * declarations into chained assignments (e.g. `var Class = _classThis = class`). This breaks JS * evaluation order and overwrites the decorated wrapper with the original class. @@ -104,11 +104,22 @@ function hardenClassStaticsAndPrototypes(value: any, wrapper: any, skip: any) { } /** - * Decorator to secure a class with a mutation-throwing Proxy (noisy immutability). + * A class decorator that secures a class instance with a mutation-throwing Proxy. + * Provides "noisy immutability" by throwing an error if modifications are attempted. * - * @note **Workaround:** To protect against aggressive bundlers (Rollup/Terser) mutating the TS 7.0 + * @remarks + * **Workaround:** To protect against aggressive bundlers (Rollup/Terser) mutating the TS 7.0 * ES2022 decorator IIFE structure, users must append `return Object.freeze(this) as this;` * (or the `secure` equivalent) to their constructors to ensure immutability survives production bundling. + * + * @param value - The class constructor to secure + * @param context - The decorator context + * @returns The secured class wrapper + * @example + * ```ts + * @Securable + * class Config { ... } + * ``` */ export function Securable(value: T, { kind, name, addInitializer }: ClassDecoratorContext): T | void { const finalName = getClassName(value, name); @@ -122,11 +133,22 @@ export function Securable(value: T, { kind, name, addInit } /** - * Decorator to freeze a Class to prevent modification (silent immutability). + * A class decorator that freezes a class instance to prevent modification. + * Provides "silent immutability" by silently ignoring modifications in non-strict mode. * - * @note **Workaround:** To protect against aggressive bundlers (Rollup/Terser) mutating the TS 7.0 + * @remarks + * **Workaround:** To protect against aggressive bundlers (Rollup/Terser) mutating the TS 7.0 * ES2022 decorator IIFE structure, users must append `return Object.freeze(this) as this;` to their * constructors to ensure immutability survives production bundling. + * + * @param value - The class constructor to freeze + * @param context - The decorator context + * @returns The immutable class wrapper + * @example + * ```ts + * @Immutable + * class Config { ... } + * ``` */ export function Immutable(value: T, { kind, name, addInitializer }: ClassDecoratorContext): T | void { const finalName = getClassName(value, name); @@ -140,7 +162,18 @@ export function Immutable(value: T, { kind, name, addInit } } -/** register a Class for serialization */ +/** + * A class decorator that registers a class for serialization with the runtime type system. + * + * @param value - The class constructor to register + * @param context - The decorator context + * @returns The original class constructor + * @example + * ```ts + * @Serializable + * class DataModel { ... } + * ``` + */ export function Serializable(value: T, { kind, name, addInitializer }: ClassDecoratorContext): T | void { const finalName = getClassName(value, name); @@ -159,7 +192,19 @@ export function Serializable(value: T, { kind, name, addI } } -/** make a Class not instantiable */ +/** + * A class decorator that prevents instantiation of the class. + * Useful for grouping static methods together without allowing instances to be created. + * + * @param value - The class constructor to make static + * @param context - The decorator context + * @returns A wrapper that throws a TypeError when instantiated + * @example + * ```ts + * @Static + * class MathUtils { ... } + * ``` + */ export function Static(value: T, { kind, name }: ClassDecoratorContext): T | void { const finalName = getClassName(value, name) as Type; diff --git a/packages/library/src/common/coercion.library.ts b/packages/library/src/common/coercion.library.ts index 4f75a8f5..b71586b8 100644 --- a/packages/library/src/common/coercion.library.ts +++ b/packages/library/src/common/coercion.library.ts @@ -2,7 +2,18 @@ import { clone, stringify } from '#library/serialize.library.js'; import { asType } from '#library/type.library.js'; import { isIntegerLike, isArrayLike, isDefined, isInteger, isIterable, isNullish, isString, isUndefined, isNumber, isNumeric, isError, isObject } from '#library/assertion.library.js'; -/** Coerce {value} into {value[]} ( if not already ), with optional {fill} Object */ +/** + * Coerces a value into an array. If the value is already an array-like or iterable, + * it is converted to an array. An optional `fill` value can be provided to map over the elements. + * + * @param arr - The value to coerce into an array + * @param fill - Optional value to map over the elements + * @returns An array containing the coerced values + * @example + * ```ts + * const arr = asArray('hello'); // ['hello'] + * ``` + */ export function asArray(arr: Exclude, string> | undefined): T[]; export function asArray(arr: T | Exclude | undefined, string>): NonNullable[]; export function asArray(arr: Iterable | ArrayLike, fill: K): K[]; @@ -14,7 +25,16 @@ export function asArray(arr: T | Iterable | ArrayLike = [], fill?: K : [arr as T] as (T | K)[]; } -/** stringify if not nullish */ +/** + * Coerces a value to a string using serialization if it is not nullish. + * + * @param str - The value to coerce to a string + * @returns The stringified value, or an empty string if nullish + * @example + * ```ts + * const str = asString({ a: 1 }); // '{"a":1}' + * ``` + */ export function asString(str?: T) { return isNullish(str) ? '' @@ -23,12 +43,30 @@ export function asString(str?: T) { : stringify(str); } -/** convert String | Number | BigInt to Number */ +/** + * Coerces a String, Number, or BigInt to a Number. + * + * @param str - The value to coerce + * @returns The coerced Number + * @example + * ```ts + * const num = asNumber(123n); // 123 + * ``` + */ export function asNumber(str?: string | number | bigint) { return parseFloat(str?.toString() ?? 'NaN'); } -/** convert String | Number to BigInt */ +/** + * Coerces a String or Number to a BigInt. + * + * @param str - The value to coerce + * @returns The coerced BigInt + * @example + * ```ts + * const big = asInteger(123.45); // 123n + * ``` + */ export function asInteger(str?: T) { const arg = asType(str); @@ -46,7 +84,17 @@ export function asInteger(str?: T) { } } -/** return as Number if possible, else original String */ +/** + * Returns the value as a Number or BigInt if possible, otherwise returns the original string. + * + * @param str - The string, number, or bigint to process + * @param stripZero - Whether to strip leading zeros when evaluating numeric strings + * @returns The numeric coercion or the original string + * @example + * ```ts + * const num = ifNumeric('123'); // 123 + * ``` + */ export const ifNumeric = (str: string | number | bigint, stripZero = false) => { switch (true) { case isInteger(str): { @@ -72,11 +120,54 @@ export const ifNumeric = (str: string | number | bigint, stripZero = false) => { } } +/** + * Returns the value if defined, otherwise 0. + * + * @param obj - The value to check + * @returns The value or 0 + * @example + * ```ts + * const val = nullishToZero(null); // 0 + * ``` + */ export const nullishToZero = (obj: T) => obj ?? 0; + +/** + * Returns the value if defined, otherwise an empty string. + * + * @param obj - The value to check + * @returns The value or an empty string + * @example + * ```ts + * const val = nullishToEmpty(undefined); // '' + * ``` + */ export const nullishToEmpty = (obj: T) => obj ?? ''; + +/** + * Returns the value if defined, otherwise the fallback value. + * + * @param obj - The value to check + * @param value - The fallback value + * @returns The original value or the fallback value + * @example + * ```ts + * const val = nullishToValue(null, 'default'); // 'default' + * ``` + */ export const nullishToValue = (obj: T, value: R) => obj ?? value; -/** coerce an unknown value into an Error instance */ +/** + * Coerces an unknown value into a proper Error instance. + * Preserves the name, message, stack, and code if available. + * + * @param err - The unknown error value + * @returns An Error instance + * @example + * ```ts + * const err = asError('Something went wrong'); + * ``` + */ export function asError(err: unknown): Error & { code?: string | number } { if (isError(err)) return err as Error & { code?: string | number }; diff --git a/packages/library/src/common/enumerate.library.ts b/packages/library/src/common/enumerate.library.ts index f8a74a13..a8395049 100644 --- a/packages/library/src/common/enumerate.library.ts +++ b/packages/library/src/common/enumerate.library.ts @@ -70,16 +70,19 @@ function value(val: any) { } /** - * # Enumify - * create a Proxy-based Registry (Enum) from an Object or Array. - * Enums are immutable (frozen) and provide methods for iteration, search, and extension. + * Creates a Proxy-based Registry (Enum) from an Object or Array. + * Enums are immutable (frozen) and provide methods for iteration, search, and extension. + * Arrays are converted to zero-indexed objects (e.g., `['A']` becomes `{ A: 0 }`). * + * @param list - The array or object to convert into an Enum + * @param frozen - Whether to freeze the resulting Enum (default: true) + * @returns An immutable Enumify registry object * @example - * ```typescript + * ```ts * const Status = enumify(['Active', 'Inactive', 'Pending']); - * console.log(Status.Active); // 0 - * console.log(Status.has('Active')); // true - * console.log(Status.keys()); // ['Active', 'Inactive', 'Pending'] + * console.log(Status.Active); // 0 + * console.log(Status.has('Active'));// true + * console.log(Status.keys()); // ['Active', 'Inactive', 'Pending'] * ``` */ export function enumify(list: T, frozen?: boolean): Enum.wrap>; @@ -111,7 +114,10 @@ export function enumify(this: any, list: T, frozen = true): any { return proxify(target, true, frozen); // proxy is ALWAYS frozen (read-only), but target is only 'locked' if requested } -/** create an entry in the Serialization Registry to describe how to rebuild an Enum */ +/** + * A class wrapper for Enumify to register it with the serialization system. + * Allows Enums to be properly serialized and deserialized. + */ @Serializable export class Enumify { constructor(list: Property) { diff --git a/packages/library/src/common/function.library.ts b/packages/library/src/common/function.library.ts index 71b73d4d..91c9b492 100644 --- a/packages/library/src/common/function.library.ts +++ b/packages/library/src/common/function.library.ts @@ -53,7 +53,17 @@ function serialize(val: any, seen = new WeakSet()): string { }); } -/** curry a Function to allow partial calls */ +/** + * Curries a function to allow partial application of its arguments. + * + * @param fn - The original function to curry + * @returns A curried version of the function + * @example + * ```ts + * const add = curry((a: number, b: number) => a + b); + * add(1)(2); // 3 + * ``` + */ export function curry(fn: (...args: Args) => Res): Curry { return function curried(...args: any[]): any { return (args.length >= fn.length) @@ -62,7 +72,16 @@ export function curry(fn: (...args: Args) => Res): Curr } as Curry; } -/** generic function to memoize repeated function calls */ +/** + * Memoizes a function, caching its return values based on serialized arguments. + * + * @param fn - The function to memoize + * @returns A memoized version of the function + * @example + * ```ts + * const expensive = memoizeFunction((a, b) => a * b); + * ``` + */ export function memoizeFunction any>(fn: F): F { const cache = new Map>(); // using a Map for better key handling than plain objects @@ -79,12 +98,31 @@ export function memoizeFunction any>(fn: F): F { const wm = new WeakMap>(); -/** manually clear the memoization cache for an object */ +/** + * Manually clears the memoization cache for an object instance. + * + * @param obj - The object whose cache should be cleared + * @example + * ```ts + * clearCache(myInstance); + * ``` + */ export function clearCache(obj: object) { wm.delete(obj); } -/** define a Descriptor for an Object's memoized-method */ +/** + * Defines a PropertyDescriptor for an object's memoized method. + * Caches the results of method calls on a per-instance basis. + * + * @param name - The name of the method + * @param fn - The method implementation + * @returns A PropertyDescriptor containing the memoization logic + * @example + * ```ts + * Object.defineProperty(target, 'calc', memoizeMethod('calc', () => 42)); + * ``` + */ export function memoizeMethod, T = any>(name: PropertyKey, fn: (this: Context, ...args: any[]) => T) { return { enumerable: false, diff --git a/packages/library/src/common/international.library.ts b/packages/library/src/common/international.library.ts index 14515e19..35650f81 100644 --- a/packages/library/src/common/international.library.ts +++ b/packages/library/src/common/international.library.ts @@ -47,12 +47,31 @@ const getDF = memoizeFunction((locale?: string, options?: any) => { * (using 'Intl' namespace objects) */ -/** return the system's current TimeZone, Calendar, and Locale */ +/** + * Retrieves the system's current TimeZone, Calendar, and Locale information + * by resolving the default `Intl.DateTimeFormat` options. + * + * @returns The resolved DateTimeFormat options + * @example + * ```ts + * const { timeZone, locale } = getDateTimeFormat(); + * ``` + */ export function getDateTimeFormat() { return getDTF().resolvedOptions(); } -/** return the canonicalized locale string, or undefined if invalid */ +/** + * Returns the canonicalized locale string, or undefined if the locale is invalid. + * Uses `Intl.getCanonicalLocales` for strict validation. + * + * @param locale - The locale string to validate (e.g., 'en_US' or 'en-US') + * @returns The canonical locale string, or undefined on failure + * @example + * ```ts + * canonicalLocale('en_US'); // 'en-US' + * ``` + */ export function canonicalLocale(locale: string): string | undefined { try { return Intl.getCanonicalLocales(locale.replace(/_/g, '-'))[0]; @@ -62,7 +81,21 @@ export function canonicalLocale(locale: string): string | undefined { } } -/** return a localized relative time string (e.g., 'in 2 days') */ +/** + * Returns a localized relative time string using `Intl.RelativeTimeFormat`. + * Falls back to a basic string representation if formatting fails. + * + * @param value - The numeric value to format (e.g., 2) + * @param unit - The time unit (e.g., 'days', 'hours') + * @param locale - Optional locale string + * @param style - The formatting style (default: 'narrow') + * @param numeric - The numeric formatting preference (default: 'always') + * @returns The localized relative time string + * @example + * ```ts + * getRelativeTime(2, 'days', 'en'); // 'in 2 days' + * ``` + */ export function getRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, locale?: string, style: Intl.RelativeTimeFormatStyle = 'narrow', numeric: Intl.RelativeTimeFormatNumeric = 'always') { try { return getRTF(locale, style, numeric).format(value, unit); @@ -71,7 +104,20 @@ export function getRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit } } -/** return a localized list string (e.g., 'A, B, and C') */ +/** + * Returns a localized list string using `Intl.ListFormat`. + * Falls back to a simple comma-joined string if formatting fails. + * + * @param list - The array of strings to format + * @param locale - Optional locale string + * @param type - The list format type (default: 'conjunction') + * @param style - The list format style (default: 'long') + * @returns The localized list string + * @example + * ```ts + * formatList(['A', 'B', 'C'], 'en'); // 'A, B, and C' + * ``` + */ export function formatList(list: string[], locale?: string, type: Intl.ListFormatType = 'conjunction', style: Intl.ListFormatStyle = 'long') { try { return getLF(locale, type, style).format(list); @@ -80,12 +126,31 @@ export function formatList(list: string[], locale?: string, type: Intl.ListForma } } -/** return a localized duration string natively (using Intl.DurationFormat) */ +/** + * Returns a localized duration string using `Intl.DurationFormat`. + * Note: Requires an environment that supports `Intl.DurationFormat`. + * + * @param duration - The duration object or value to format + * @param locale - Optional locale string + * @param options - Optional format configuration + * @returns The localized duration string + */ export function formatDuration(duration: any, locale?: string, options?: any) { return getDF(locale, options).format(duration); } -/** return a localized number string */ +/** + * Returns a localized number string using `Intl.NumberFormat`. + * + * @param value - The numeric value to format + * @param locale - Optional locale string + * @param options - Optional format configuration + * @returns The localized number string + * @example + * ```ts + * formatNumber(1234.5, 'de-DE'); // '1.234,5' + * ``` + */ export function formatNumber(value: number, locale?: string, options?: Intl.NumberFormatOptions) { try { return getNF(locale, options).format(value); @@ -94,7 +159,15 @@ export function formatNumber(value: number, locale?: string, options?: Intl.Numb } } -/** return a localized day period string (e.g., 'AM', 'PM', 'de la maΓ±ana') */ +/** + * Returns a localized day period string using `Intl.DateTimeFormat`. + * Extracts the 'dayPeriod' token from the formatted parts. + * + * @param value - The numeric epoch time value + * @param locale - Optional locale string + * @param options - Optional format configuration + * @returns The localized day period string (e.g., 'AM', 'PM', 'de la maΓ±ana') + */ export function formatDayPeriod(value: number, locale?: string, options?: Intl.DateTimeFormatOptions) { try { const parts = getDTF(locale, options).formatToParts(value); @@ -113,9 +186,30 @@ export function formatUnit(value: number, unit: string, locale?: string, unitDis } } +/** + * Formats a numeric value as a localized currency string. + * + * @param str - The numeric value or string to format + * @param scale - The maximum number of fractional digits (default: 2) + * @param currency - The ISO 4217 currency code (default: 'AUD') + * @param locale - Optional locale string (defaults to system locale) + * @returns The localized currency string + * @example + * ```ts + * formatCurrency(1234.5, 2, 'USD'); // '$1,234.50' + * ``` + */ +export function formatCurrency(str: string | number, scale = 2, currency = 'AUD', locale?: string) { + try { + return getNF(locale, { style: 'currency', currency, maximumFractionDigits: scale }).format(Number(str) || 0); + } catch (e) { + return `${currency} ${str}`; + } +} + /** * try to infer hemisphere using the timezone's daylight-savings setting - * @note This implementation intentionally differs from the version in `tempo-fns` + * @remarks This implementation intentionally differs from the version in `tempo-fns` * (including specific fallback and return behaviors). Do not directly synchronize them. */ export function getHemisphere(timeZone: string = getDateTimeFormat().timeZone) { @@ -143,14 +237,19 @@ type input = { } type result = { weekOfYear: number, yearOfWeek: number }; /** - * Polyfill fallback for ISO 8601 Week of Year and Year of Week. + * Polyfill fallback for ISO 8601 Week of Year and Year of Week calculations. * * Introduced because highly experimental native browser implementations of the Temporal API * (e.g., Chrome/Firefox behind flags) currently return `undefined` for `weekOfYear` and `yearOfWeek` * on ZonedDateTime objects. The TC39 spec moved toward calendar-dependent definitions, * causing divergence between the @js-temporal/polyfill (which returns numbers) and native browsers (which return undefined). - * @note This implementation intentionally differs from the version in `tempo-fns` - * (including specific fallback and return behaviors). Do not directly synchronize them. + * + * @param zdt - The ZonedDateTime or matching input object + * @returns An object containing the weekOfYear and yearOfWeek + * @example + * ```ts + * const { weekOfYear } = getISOWeekOfYear(Temporal.Now.zonedDateTimeISO()); + * ``` */ export function getISOWeekOfYear(zdt: input): result { if (isDefined(zdt.weekOfYear) && isDefined(zdt.yearOfWeek)) @@ -176,10 +275,16 @@ export function getISOWeekOfYear(zdt: input): result { } /** - * Probe the runtime to see if the locale defaults to Month-Day-Year order. + * Probes the runtime to see if the locale defaults to Month-Day-Year (MDY) order. + * Useful for resolving ambiguous dates like '12/11/2024'. + * + * @param locale - The locale string to probe + * @returns True if the locale uses MDY format, false otherwise * @example + * ```ts * probeMDY('en-US') // true * probeMDY('en-GB') // false + * ``` */ export function probeMDY(locale: string): boolean { try { diff --git a/packages/library/src/common/logger.class.ts b/packages/library/src/common/logger.class.ts index 60cf9972..5869fb02 100644 --- a/packages/library/src/common/logger.class.ts +++ b/packages/library/src/common/logger.class.ts @@ -29,6 +29,17 @@ const Level = { [Method.Trace]: LOG.Trace, } as const; +/** + * Parses a debug level (numeric or string) into a strongly-typed LOG enumeration value. + * + * @param level - The log level to parse + * @param fallback - The default log level to return if parsing fails (default: Info) + * @returns The resolved LOG enumeration value + * @example + * ```ts + * parseLogLevel('debug'); // LOG.Debug + * ``` + */ export function parseLogLevel(level?: DebugLevel, fallback: LOG = LOG.Info): LOG { if (isNumber(level)) return (level >= LOG.Off && level <= LOG.Trace) ? level as LOG : fallback; if (isString(level)) return Level[level.toLowerCase() as Method] ?? fallback; @@ -37,7 +48,14 @@ export function parseLogLevel(level?: DebugLevel, fallback: LOG = LOG.Info): LOG /** * A lightweight, dependency-free namespaced logger. - * Decoupled from error handling and boundaries. + * Decoupled from error handling and boundaries, utilizing standard `console` methods. + * Supports configurable log levels and structured object output. + * + * @example + * ```ts + * const log = new Logger('App', LOG.Debug); + * log.info('Started'); // [App] Started + * ``` */ export class Logger { #namespace: string; diff --git a/packages/library/src/common/number.library.ts b/packages/library/src/common/number.library.ts index a1a40892..4dcc0036 100644 --- a/packages/library/src/common/number.library.ts +++ b/packages/library/src/common/number.library.ts @@ -1,7 +1,18 @@ import { asArray, asNumber, ifNumeric } from '#library/coercion.library.js'; import type { TValues } from '#library/type.library.js'; -/** show Hex value of a number */ +/** + * Converts a number or array of numbers into a contiguous hexadecimal string. + * Flattens nested arrays and filters out non-integers. + * + * @param num - The number(s) to convert + * @param len - Optional maximum length of the resulting string + * @returns The hexadecimal string representation + * @example + * ```ts + * toHex([255, 16]); // 'ff10' + * ``` + */ export const toHex = (num: TValues = [], len?: number) => asArray(num) // ensure array .flat(1_000_000) // flatten any arrays to arbitrary depth @@ -11,7 +22,17 @@ export const toHex = (num: TValues = [], len?: number) => .toLowerCase() .substring(0, len ?? Number.MAX_SAFE_INTEGER) -/** apply an Ordinal suffix */ +/** + * Appends an ordinal suffix (st, nd, rd, th) to a number. + * + * @param idx - The number to format (defaults to 0) + * @returns The number formatted as a string with its ordinal suffix + * @example + * ```ts + * suffix(1); // '1st' + * suffix(23); // '23rd' + * ``` + */ export const suffix = (idx: number = 0) => { const str = String(idx); @@ -27,22 +48,48 @@ export const suffix = (idx: number = 0) => { } } -/** split a value into an array */ +/** + * Splits a number or string by a delimiter and parses the chunks. + * + * @param nbr - The value to split + * @param chr - The delimiter character (default: '.') + * @param zero - Whether to strip leading zeros during numeric conversion (default: true) + * @returns An array of parsed numeric or string chunks + * @example + * ```ts + * split('12.34'); // [12, 34] + * ``` + */ export function split(nbr: T, chr?: string, zero?: boolean): number[]; export function split(nbr: T, chr?: string, zero?: boolean): (string | number)[]; export function split(nbr?: T, chr: string = '.', zero: boolean = true): any[] { return nbr?.toString().split(chr).map(val => ifNumeric(val, zero)) || [] -}; +} -/** fix a string to set decimal precision */ +/** + * Formats a number to a fixed number of decimal places. + * + * @param nbr - The number or string to format + * @param max - The maximum number of decimal places (default: 2) + * @returns The fixed-precision string representation + * @example + * ```ts + * fix(12.3456, 2); // '12.35' + * ``` + */ export const fix = (nbr: string | number = 0, max = 2) => asNumber(nbr).toFixed(max); -/** remove ':' from an HH:MI string, return as number */ +/** + * Removes the colon from an HH:MI time string and returns it as a number. + * + * @param hhmi - The time string (e.g., '14:30') + * @returns The numeric representation (e.g., 1430) + * @example + * ```ts + * asTime('14:30'); // 1430 + * ``` + */ export const asTime = (hhmi: string | number) => Number(String(hhmi).replace(':', '')); - -/** format a value as currency */ -export const asCurrency = (str: string | number, scale = 2, currency = 'AUD') => - asNumber(str).toLocaleString(undefined, { style: 'currency', currency, maximumFractionDigits: scale }); diff --git a/packages/library/src/common/object.library.ts b/packages/library/src/common/object.library.ts index 2806c634..4e082186 100644 --- a/packages/library/src/common/object.library.ts +++ b/packages/library/src/common/object.library.ts @@ -3,14 +3,34 @@ import { isObject, isArray, isFunction, isDefined, isNullish, isMap, isSet } fro import { getType } from '#library/type.library.js'; import type { Extend, Property } from '#library/type.library.js'; -/** remove quotes around property names */ +/** + * Serializes an object to JSON and removes quotes around property names. + * Useful for generating loosely formatted string representations of objects. + * + * @param obj - The object to un-quote + * @returns A stringified JSON representation without quotes around keys + * @example + * ```ts + * unQuoteObj({ a: 1 }); // '{a: 1}' + * ``` + */ export const unQuoteObj = (obj: any) => { return JSON.stringify(obj) ?.replace(/"([^"]+)":/g, '$1: ') ?.replace(/,/g, ', ') } -/** copy enumerable properties to a new Object */ +/** + * Recursively copies enumerable properties of an object into a new object. + * Returns the original value if it is not an object or is nullish. + * + * @param obj - The object to copy + * @returns A new object with the copied properties + * @example + * ```ts + * const copy = asObject({ a: 1 }); + * ``` + */ export const asObject = (obj?: Record) => { if (isNullish(obj) || !isObject(obj)) return obj as T; @@ -23,7 +43,18 @@ export const asObject = (obj?: Record) => { return temp as T; } -/** deep-compare object and array values for equality */ +/** + * Performs a deep comparison between two values to determine if they are equivalent. + * Supports primitives, arrays, maps, sets, and plain objects. + * + * @param a - The first value to compare + * @param b - The second value to compare + * @returns True if the values are deeply equal + * @example + * ```ts + * isEqual({ a: 1 }, { a: 1 }); // true + * ``` + */ export const isEqual = (a: any, b: any): boolean => { if (a === b) return true; if (isNullish(a) || isNullish(b)) return a === b; @@ -63,7 +94,17 @@ export const isEqual = (a: any, b: any): boolean => { return false; } -/** find all methods on an Object */ +/** + * Finds all method names on an object. + * + * @param obj - The object to inspect + * @param all - Whether to traverse the prototype chain (default: false) + * @returns An array of property keys corresponding to functions + * @example + * ```ts + * const methods = getMethods(myClassInstance); + * ``` + */ export const getMethods = (obj: any, all = false) => { const properties = new Set(); let currentObj = obj; @@ -78,7 +119,16 @@ export const getMethods = (obj: any, all = false) => { .filter(key => isFunction(obj[key])); } -/** extract only defined values from Object */ +/** + * Extracts a new object containing only the properties with defined (non-undefined) values. + * + * @param obj - The object to extract from + * @returns A new object without undefined values + * @example + * ```ts + * const clean = ifDefined({ a: 1, b: undefined }); // { a: 1 } + * ``` + */ export function ifDefined>(obj: T) { return ownEntries(obj) .reduce((acc, [key, val]) => { @@ -88,7 +138,17 @@ export function ifDefined>(obj: T) { }, {} as T) } -/** extract a subset of keys from an object */ +/** + * Creates a new object composed of the picked object properties. + * + * @param obj - The source object + * @param keys - The property names to pick + * @returns A new object containing only the picked properties + * @example + * ```ts + * const subset = pick({ a: 1, b: 2, c: 3 }, 'a', 'c'); // { a: 1, c: 3 } + * ``` + */ export const pick = , K extends string>(obj: T, ...keys: K[]): Partial => { const ownKeys = Object.getOwnPropertyNames(obj); @@ -99,15 +159,46 @@ export const pick = , K extends string>(obj: T, ...keys: K }, {} as T); } -/** extract a named key from an array of objects */ +/** + * Extracts a specified named key from an array of objects. + * + * @param objs - The array of objects + * @param key - The property key to extract + * @returns An array of the extracted property values + * @example + * ```ts + * const ids = pluck([{ id: 1 }, { id: 2 }], 'id'); // [1, 2] + * ``` + */ export const pluck = (objs: T[], key: K): T[K][] => objs.map(obj => obj[key]); -/** extend an object with the properties of another */ +/** + * Extends a target object with the properties of one or more source objects. + * Uses `Object.assign` internally. + * + * @param obj - The target object + * @param objs - The source objects + * @returns The extended target object + * @example + * ```ts + * const ext = extend({ a: 1 }, { b: 2 }); // { a: 1, b: 2 } + * ``` + */ export const extend = (obj: T, ...objs: U[]) => Object.assign(obj, ...objs) as T; -/** recursively deep-merge objects */ +/** + * Recursively deeply merges multiple objects into a single new object. + * Does not mutate the source objects. + * + * @param objects - The objects to merge + * @returns A new deeply merged object + * @example + * ```ts + * const merged = deepMerge({ a: { x: 1 } }, { a: { y: 2 } }); // { a: { x: 1, y: 2 } } + * ``` + */ export const deepMerge = >(...objects: Partial[]): T => { return objects.reduce((prev, obj) => { if (!isObject(obj)) return prev; @@ -126,6 +217,16 @@ export const deepMerge = >(...objects: Partia }, {} as any) as T; } +/** + * Returns the count of enumerable own properties on an object. + * + * @param obj - The object to count properties of + * @returns The number of properties + * @example + * ```ts + * const count = countProperties({ a: 1, b: 2 }); // 2 + * ``` + */ export const countProperties = (obj = {}) => ownKeys(obj).length diff --git a/packages/library/src/common/pledge.class.ts b/packages/library/src/common/pledge.class.ts index 1658138b..86431239 100644 --- a/packages/library/src/common/pledge.class.ts +++ b/packages/library/src/common/pledge.class.ts @@ -22,12 +22,16 @@ const _STATE = secure({ }); /** - * Wrap a Promise's resolve/reject/finally methods for later fulfilment. - * with useful methods for tracking the state of the Promise, chaining fulfilment, etc. - ``` - new Pledge({tag: string, onResolve?: () => void, onReject?: () => void, onSettle?: () => void}) - new Pledge(tag?: string) - ``` + * A lightweight wrapper around Promises (utilizing `Promise.withResolvers`) + * that exposes `resolve`, `reject`, and state-tracking methods. + * Useful for deferred execution and managing asynchronous lifecycles. + * + * @example + * ```ts + * const p = new Pledge('MyPledge'); + * p.resolve('Success!'); + * await p.promise; // 'Success!' + * ``` */ @Immutable export class Pledge { @@ -36,7 +40,12 @@ export class Pledge { static get STATE() { return _STATE; } - /** initialize future Pledge instances */ + /** + * Initializes global defaults for all future Pledge instances. + * + * @param arg - The global configuration or tag + * @returns The updated static status configuration + */ static init(arg?: Pledge.Constructor | string) { if (isObject(arg)) { if (isEmpty(arg)) @@ -64,6 +73,11 @@ export class Pledge { return { ..._static, state: _STATE.Pending } as Pledge.Status; } + /** + * Creates a new Pledge instance. + * + * @param arg - An optional configuration object or string tag + */ constructor(arg?: Pledge.Constructor | string) { const opts = isObject(arg) ? arg : { tag: arg as string }; const config = { ..._static, ...ifDefined({ tag: opts.tag, debug: opts.debug, catch: opts.catch, silent: opts.silent }) }; @@ -151,6 +165,12 @@ export class Pledge { return JSON.stringify(this.status); } + /** + * Resolves the underlying promise. + * + * @param value - The value to resolve the promise with + * @returns The internal promise + */ resolve(value: T) { if (this.isPending) { this.#status.settled = value; @@ -163,6 +183,12 @@ export class Pledge { return this.#pledge.promise; } + /** + * Rejects the underlying promise. + * + * @param error - The reason for rejection + * @returns The internal promise + */ reject(error: any) { if (this.isPending) { this.#status.error = error; @@ -175,7 +201,14 @@ export class Pledge { return this.#pledge.promise; } - /** make Pledge 'then-able' by forwarding to internal promise */ + /** + * Makes the Pledge 'then-able', allowing it to be directly awaited + * or chained like a standard Promise. + * + * @param onfulfilled - Callback for when the pledge resolves + * @param onrejected - Callback for when the pledge rejects + * @returns A new promise representing the chained execution + */ then( onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null diff --git a/packages/library/src/common/primitive.library.ts b/packages/library/src/common/primitive.library.ts index 204674a0..4f7a2d3b 100644 --- a/packages/library/src/common/primitive.library.ts +++ b/packages/library/src/common/primitive.library.ts @@ -9,9 +9,15 @@ import type { Obj, KeyOf, ValueOf, EntryOf } from '#library/type.library.js'; */ /** - * ## unwrap * Traverse a Proxy chain and return the underlying raw target object. * Hardened against prototype-climbing bugs and cyclic $Target chains. + * + * @param obj - The object to unwrap + * @returns The underlying raw target object + * @example + * ```ts + * const raw = unwrap(proxyObj); + * ``` */ export function unwrap(obj: T): T { let curr = obj as any; @@ -28,7 +34,18 @@ export function unwrap(obj: T): T { return curr; } -/** Tuple of enumerable entries with string | symbol keys */ +/** + * Returns an array of all enumerable [key, value] entries for an object, + * including properties inherited from the prototype chain if `all` is true. + * + * @param json - The object to extract entries from + * @param all - Whether to include properties from the prototype chain + * @returns An array of [key, value] tuples + * @example + * ```ts + * const entries = ownEntries({ a: 1, b: 2 }); + * ``` + */ export function ownEntries(json: T, all = false): EntryOf[] { if (!json || typeof json !== 'object') return [] as EntryOf[]; @@ -74,17 +91,48 @@ export function ownEntries(json: T, all = false): EntryOf[] { return entries as EntryOf[]; } -/** Array of all enumerable PropertyKeys */ +/** + * Returns an array of all enumerable property keys for an object. + * + * @param json - The object to extract keys from + * @param all - Whether to include keys from the prototype chain + * @returns An array of property keys + * @example + * ```ts + * const keys = ownKeys({ a: 1, b: 2 }); + * ``` + */ export function ownKeys(json: T, all = false): KeyOf[] { return ownEntries(json, all).map(([key]) => key as KeyOf); } -/** Array of all enumerable object values */ +/** + * Returns an array of all enumerable property values for an object. + * + * @param json - The object to extract values from + * @param all - Whether to include values from the prototype chain + * @returns An array of property values + * @example + * ```ts + * const values = ownValues({ a: 1, b: 2 }); + * ``` + */ export function ownValues(json: T, all = false): ValueOf[] { return ownEntries(json, all).map(([_, value]) => value as ValueOf); } -/** Get nested value using dot or bracket notation */ +/** + * Gets a nested value from an object using dot or bracket notation. + * + * @param obj - The object to extract the value from + * @param path - The path to the value using dot or bracket notation + * @param dflt - The default value to return if the path does not exist + * @returns The extracted value or the default value + * @example + * ```ts + * const val = extract(user, 'profile.address.zip', '00000'); + * ``` + */ export function extract(obj: any, path: string | number, dflt?: T): T { if (path === undefined || path === null || path === '') return obj as T; if (obj === null || typeof obj !== 'object') return dflt as T; @@ -97,9 +145,29 @@ export function extract(obj: any, path: string | number, dflt?: T): T { .reduce((acc, field) => acc?.[field] ?? null, obj) ?? dflt; } -/** Return an array with no repeated elements */ +/** + * Returns an array with no repeated elements. + * + * @param arr - The array to extract distinct elements from + * @returns A new array containing only distinct elements + * @example + * ```ts + * const unique = distinct([1, 1, 2, 3]); + * ``` + */ export function distinct(arr: T[]): T[]; -/** return a mapped array with no repeated elements */ +/** + * Returns a mapped array with no repeated elements. + * + * @param arr - The array to extract distinct elements from + * @param mapfn - Optional mapping function to apply before checking for distinctness + * @param thisArg - Optional this context for the mapping function + * @returns A new array containing only distinct elements + * @example + * ```ts + * const uniqueIds = distinct(users, user => user.id); + * ``` + */ export function distinct(arr: T[], mapfn: (value: T, index: number, array: T[]) => S, thisArg?: any): S[]; export function distinct(arr: T[], mapfn?: (value: any, index: number, array: any[]) => any, thisArg?: any) { return mapfn diff --git a/packages/library/src/common/proxy.library.ts b/packages/library/src/common/proxy.library.ts index 8ed14440..cc4259c5 100644 --- a/packages/library/src/common/proxy.library.ts +++ b/packages/library/src/common/proxy.library.ts @@ -147,37 +147,102 @@ function factory(target: T, options: ProxyOptions = {}): T { return result; } -/** Stealth Proxy pattern to allow for on-demand lazy property discovery and registration */ +/** + * Creates a Stealth Proxy pattern to allow for on-demand lazy property discovery and registration. + * Provides deep-freezing and bounding capabilities depending on options. + * + * @param target - The object to proxify + * @param frozen - Whether the proxy should throw on mutation (default: true) + * @param lock - Whether to deep-freeze the underlying target (default: frozen) + * @param skip - A WeakSet of objects to skip during deep-freeze + * @returns The proxified object + * @example + * ```ts + * const p = proxify({ a: 1 }); + * ``` + */ export function proxify(target: T, frozen = true, lock = frozen, skip = new WeakSet()) { return factory(target, { frozen, lock, skip, bind: frozen }); } -/** Create a dynamic Proxy where property access is forwarded to a discovery callback */ +/** + * Creates a dynamic Proxy where property access is forwarded to a discovery callback. + * Useful for virtual objects and lazy-loading data. + * + * @param target - The base object + * @param onGet - Callback fired when an unknown property is accessed + * @param readonly - Whether the proxy should prevent mutations (default: true) + * @returns The delegated Proxy + * @example + * ```ts + * const d = delegate({}, (key) => console.log('Requested:', key)); + * ``` + */ export function delegate(target: T, onGet: (key: string | symbol, target: T) => any, readonly = true) { return factory(target, { onGet, frozen: readonly }); } -/** Wrap an object in a protective Proxy that allows extension but prevents modification */ +/** + * Wraps an object in a protective Proxy that allows extension (adding new keys) + * but prevents modification or deletion of existing keys. + * + * @param target - The object to secure + * @returns The append-only secured Proxy + * @example + * ```ts + * const ref = secureRef({ initial: 1 }); + * ``` + */ export function secureRef(target: T): T { return factory(target, { appendOnly: true }); } -/** Deep-freeze an object and wrap it in a loudly-throwing read-only Proxy */ +/** + * Deep-freezes an object and wraps it in a loudly-throwing read-only Proxy. + * Provides the highest level of noisy immutability. + * + * @param obj - The object to secure + * @param skip - A WeakSet of objects to skip during deep-freeze + * @returns The securely frozen Proxy + * @example + * ```ts + * const safe = secure({ apiKey: '123' }); + * ``` + */ export function secure(obj: T, skip = new WeakSet()): T { return factory(obj, { frozen: true, lock: true, skip, bind: true }); } -/** Create a virtual Proxy where fixed keys are mapped to a callback function */ +/** + * Creates a virtual Proxy where fixed keys are mapped to a callback function. + * + * @param keys - The array of allowed keys (or an object whose ownKeys will be used) + * @param fn - The callback fired when a key is accessed + * @returns The virtual delegator object + * @example + * ```ts + * const v = delegator(['a', 'b'], (key) => key.toUpperCase()); + * ``` + */ export function delegator(keys: K[] | Record, fn: (prop: K) => any): Record { const keyList = Array.isArray(keys) ? keys : Reflect.ownKeys(keys) as K[]; return factory({} as any, { keys: keyList, onGet: fn as any, frozen: true }); } /** - * ## indexedArray * Augments a standard array with a Proxy-based lookup delegate. - * Allows index/enumerable array methods to function natively (e.g. map, filter, [0], length), + * Allows index/enumerable array methods to function natively (e.g., map, filter, length), * while redirecting non-numeric string keys to a custom finder function to lookup items. + * + * @param list - The array to augment + * @param finder - The lookup function for non-numeric keys + * @param readonly - Whether the array should be read-only (default: true) + * @returns The augmented array with record-like string indexing + * @example + * ```ts + * const arr = indexedArray([{ id: 'a' }], key => list.find(x => x.id === key)); + * arr['a']; // { id: 'a' } + * ``` */ export function indexedArray( list: T[], diff --git a/packages/library/src/common/reflection.library.ts b/packages/library/src/common/reflection.library.ts index b8771941..d342e419 100644 --- a/packages/library/src/common/reflection.library.ts +++ b/packages/library/src/common/reflection.library.ts @@ -3,7 +3,17 @@ import { asType, getType } from '#library/type.library.js'; import { isEmpty, isFunction, isPrimitive, isReference } from '#library/assertion.library.js'; import type { Obj, KeyOf, Primitives } from '#library/type.library.js'; -/** mutate Object | Array by excluding values with specified primitive 'types' */ +/** + * Mutates an object or array by deleting properties that match specified primitive types. + * + * @param obj - The object or array to mutate + * @param types - The primitive types to exclude (e.g., 'Function', 'String') + * @returns The mutated object reference + * @example + * ```ts + * exclude({ a: 1, b: () => {} }, 'Function'); // { a: 1 } + * ``` + */ export function exclude(obj: T, ...types: (Primitives | Lowercase)[]) { const exclusions = distinct(types.map(item => item.toLowerCase())) as typeof types; @@ -28,7 +38,18 @@ export function exclude(obj: T, ...types: (Primitives | Lowercase return obj; // return Object reference, even though Object has been mutated } -/** mutate Object | Array reference with properties removed */ +/** + * Mutates an object or array by removing specified properties or indices. + * If no keys are provided, it removes all properties (like a clear operation). + * + * @param obj - The object or array to mutate + * @param keys - The keys or indices to omit + * @returns The mutated object reference + * @example + * ```ts + * omit({ a: 1, b: 2 }, 'a'); // { b: 2 } + * ``` + */ export function omit(obj: T): T // TODO: consider including Map and Set objects ?? export function omit(obj: T, ...keys: PropertyKey[]): T export function omit(obj: T, ...keys: PropertyKey[]) { @@ -54,32 +75,90 @@ export function omit(obj: T, ...keys: PropertyKey[]) { return value; // return Object reference, even though Object has been mutated } -/** remove all ownKeys from an Object | Array */ +/** + * Removes all own properties from an object or array. + * + * @param obj - The object or array to purge + * @returns The mutated object reference + * @example + * ```ts + * purge({ a: 1 }); // {} + * ``` + */ export function purge(obj: T) { return omit(obj); } -/** reset Object */ +/** + * Resets an object by purging all its existing own properties and replacing them + * with the properties from another object. + * + * @param orig - The original object to reset + * @param obj - The object containing the new properties + * @returns The mutated original object reference + * @example + * ```ts + * reset(target, { newProp: 1 }); + * ``` + */ export function reset(orig: T, obj?: T) { return Object.assign(purge(orig), { ...obj }); } -/** return an Object containing all 'own' and 'inherited' enumerable properties */ +/** + * Returns a new object containing all 'own' and 'inherited' enumerable properties + * from the prototype chain of the provided object. + * + * @param json - The object to extract properties from + * @returns A plain object containing the flattened properties + * @example + * ```ts + * const flat = allObject(myInstance); + * ``` + */ export function allObject(json: T) { return Object.fromEntries(ownEntries(json, true)); } -/** create a new object and shadow-copy all own-descriptors from the source */ +/** + * Creates a new object and shadow-copies all own-descriptors (including getters/setters) + * from the source object. + * + * @param source - The object to copy descriptors from + * @returns A new object with identical descriptors + * @example + * ```ts + * const clone = allDescriptors(source); + * ``` + */ export const allDescriptors = (source: T) => { return Object.defineProperties({}, Object.getOwnPropertyDescriptors(source)) as T; } -/** get a string-array of 'getter' names for an object */ +/** + * Retrieves a distinct array of 'getter' names from an object and its prototype chain. + * + * @param obj - The object to inspect + * @returns An array of property keys that have getter functions + * @example + * ```ts + * const getters = getAccessors(myInstance); + * ``` + */ export const getAccessors = (obj: any = {}) => { return ownAccessors(obj, 'get'); } -/** get a string-array of 'setter' names for an object */ +/** + * Retrieves a distinct array of 'setter' names from an object and its prototype chain. + * + * @param obj - The object to inspect + * @returns An array of property keys that have setter functions + * @example + * ```ts + * const setters = setAccessors(myInstance); + * ``` + */ export const setAccessors = (obj: any = {}) => { return ownAccessors(obj, 'set'); } @@ -116,9 +195,16 @@ const ownAccessors = (obj: any = {}, type: 'get' | 'set') => { } /** - * Define a lazy method on a prototype that reifies itself upon first access. - * This allows heavy logic to be deferred (or even loaded via plugin) - * while maintaining a clean, synchronous public API. + * Defines a lazy method on a prototype that reifies (shadows) itself upon first access. + * This allows heavy logic to be deferred while maintaining a clean, synchronous public API. + * + * @param target - The prototype or object to define the method on + * @param key - The method name + * @param factory - A function returning the actual method implementation + * @example + * ```ts + * lazyMethod(MyClass.prototype, 'heavy', () => function() { return 42; }); + * ``` */ export function lazyMethod(target: T, key: PropertyKey, factory: (this: T) => Function) { Object.defineProperty(target, key, { diff --git a/packages/library/src/common/request.library.ts b/packages/library/src/common/request.library.ts index 85cb119a..a0ec81ff 100644 --- a/packages/library/src/common/request.library.ts +++ b/packages/library/src/common/request.library.ts @@ -36,7 +36,19 @@ export class HttpError extends Error { } } -/** get data from a resource-url */ +/** + * Performs an HTTP fetch request with built-in timeout, JSON parsing, and custom prefix handling. + * Automatically throws an `HttpError` if the response is not `ok`. + * + * @param url - The resource URL to fetch + * @param init - Optional RequestInit configuration + * @param config - Optional configuration including timeout and prefix stripping + * @returns A promise resolving to the parsed response body + * @example + * ```ts + * const data = await fetchRequest('https://api.example.com'); + * ``` + */ export const fetchRequest = (url: string | URL, init = {} as RequestInit, config = {} as Config) => { const signallingInit = { ...init, @@ -76,8 +88,15 @@ export const fetchRequest = (url: string | URL, init = {} as RequestInit, con } /** - * get Response headers only (no data). - * useful for just checking that a URL exists + * Performs an HTTP HEAD request to retrieve response headers without the body. + * Useful for verifying URL existence or checking metadata. + * + * @param url - The resource URL to check + * @returns A promise resolving to the response status and headers + * @example + * ```ts + * const { status } = await fetchHead('https://example.com'); + * ``` */ export const fetchHead = (url: string | URL) => { const signal = AbortSignal.timeout(TWO_SECONDS); diff --git a/packages/library/src/common/scopedset.class.ts b/packages/library/src/common/scopedset.class.ts index 19bbd078..90d0968e 100644 --- a/packages/library/src/common/scopedset.class.ts +++ b/packages/library/src/common/scopedset.class.ts @@ -1,12 +1,11 @@ /** - * ## ScopedSet * A lightweight `Set`-compatible container that delegates `has()` lookups to a * parent Set/ScopedSet, but confines `add()` writes to its own-local storage. * * This mirrors JavaScript prototype-chain semantics: - * - A plugin registered globally is **visible** to all sandboxes via `has()`. - * - A plugin registered in a sandbox is **isolated** from the global scope; - * the global `rt.installed` is never written to by sandbox `extend()` calls. + * - Values added to the parent are **visible** to the child via `has()`. + * - Values added to the child are **isolated** from the parent; + * the parent Set is never modified by child `add()` calls. * * @example * const global = new Set(['a']); diff --git a/packages/library/src/common/serialize.library.ts b/packages/library/src/common/serialize.library.ts index 287fa3dd..0087cda2 100644 --- a/packages/library/src/common/serialize.library.ts +++ b/packages/library/src/common/serialize.library.ts @@ -8,7 +8,16 @@ import type { Obj, Type } from '#library/type.library.js'; export const Registry = (globalThis as any)[sym.$SerializerRegistry] ??= new Map(); -/** register a Class for serialization */ +/** + * Registers a Class for custom serialization and deserialization. + * + * @param name - The string identifier for the class (automatically prefixed with '$' if missing) + * @param cls - The class constructor to register + * @example + * ```ts + * registerSerializable('MyClass', MyClass); + * ``` + */ export const registerSerializable = (name: string, cls: Function) => { const key = name.startsWith('$') ? name : `$${name}`; @@ -26,7 +35,18 @@ export const registerSerializable = (name: string, cls: Function) => { // be aware that 'structuredClone' preserves \ values... // and JSON.stringify() does not -/** make a deep-copy, using standard browser or JSON functions */ +/** + * Performs a deep copy using the native `structuredClone` (if available), + * falling back to a `cleanify` JSON strategy otherwise. + * + * @param obj - The object to clone + * @param opts - Optional structuredClone transfer options + * @returns A deep copy of the object + * @example + * ```ts + * const copy = clone(original); + * ``` + */ export function clone(obj: T, opts?: { transfer: any[] }) { try { return globalThis.structuredClone(obj, opts); @@ -35,7 +55,17 @@ export function clone(obj: T, opts?: { transfer: any[] }) { } } -/** return a copy. remove unsupported values (e.g. \, function) */ +/** + * Returns a JSON-clean copy of an object by stringifying and re-parsing. + * This inherently removes unsupported values like functions and `undefined`. + * + * @param obj - The object to clean + * @returns A clean, JSON-compatible object + * @example + * ```ts + * const clean = cleanify({ a: 1, b: undefined }); // { a: 1 } + * ``` + */ export function cleanify(obj: T) { try { return JSON.parse(JSON.stringify(obj)) as T; // run any toString() methods @@ -45,7 +75,18 @@ export function cleanify(obj: T) { } } -/** deep-copy an Object, and optionally replace \ fields with a Sentinel function call */ +/** + * Deep-copies an Object, optionally replacing `` fields + * with a Sentinel function call. Leverages `stringify` and `objectify`. + * + * @param obj - The object to cloneify + * @param sentinel - An optional function to handle reconstructed undefined values + * @returns The deep-copied object + * @example + * ```ts + * const safeCopy = cloneify(original, () => null); + * ``` + */ export function cloneify(obj: T, sentinel?: Function): T { try { return objectify(stringify(obj), sentinel) as T; @@ -139,8 +180,16 @@ function toSymbol(value: PropertyKey) { */ /** - * serialize Objects for string-safe stashing in WebStorage, Cache, etc - * uses JSON.stringify where available, else returns stringified single key:value Object '{[$type]: value}' + * Serializes objects for string-safe stashing in WebStorage, Cache, etc. + * Uses `JSON.stringify` where available, else returns a stringified + * single key:value object (e.g., `{ "$BigInt": "123" }`) for custom types. + * + * @param obj - The object to stringify + * @returns The safely stringified representation + * @example + * ```ts + * stringify(123n); // '{"$BigInt":"123"}' + * ``` */ export function stringify(obj: T) { return stringize(obj, false); @@ -239,7 +288,18 @@ function stringize(obj: T, recurse = true): string { // hide the second para } } -/** rebuild an Object from its stringified representation */ +/** + * Rebuilds an Object from its `stringify`'d string representation. + * Handles custom single key:value type definitions automatically. + * + * @param str - The string to parse + * @param sentinel - Optional function to handle reconstructing undefined/void values + * @returns The deserialized object or original string if parsing fails + * @example + * ```ts + * const obj = objectify('{"$BigInt":"123"}'); // 123n + * ``` + */ export function objectify(str: any, sentinel?: Function): T { if (!isString(str)) return str; // skip parsing diff --git a/packages/library/src/common/storage.library.ts b/packages/library/src/common/storage.library.ts index b16880d4..b075521d 100644 --- a/packages/library/src/common/storage.library.ts +++ b/packages/library/src/common/storage.library.ts @@ -27,13 +27,29 @@ let storage = context.type === CONTEXT.Browser ? getSafeStorage() : mockStorage; -/** select local | session storage */ +/** + * Selects the active browser storage mechanism (localStorage or sessionStorage). + * + * @param store - The storage type to use (default: 'local') + * @returns The selected Storage object + */ export function selStorage(store: 'local' | 'session' = 'local') { const name = (store + 'Storage') as `${typeof store}Storage`; return storage = getSafeStorage(name); } -/** get storage */ +/** + * Retrieves a value from the active storage mechanism across any runtime environment + * (Browser, NodeJS, Deno, GoogleAppsScript). Rebuilds serialized objects automatically. + * + * @param key - The storage key to lookup + * @param dflt - The fallback value if the key does not exist + * @returns The deserialized value, or the default value + * @example + * ```ts + * const user = getStorage<{ name: string }>('user', { name: 'Guest' }); + * ``` + */ export function getStorage(): T; export function getStorage(key: string): T | undefined; export function getStorage(key: string | undefined, dflt?: T): T; @@ -69,7 +85,18 @@ export function getStorage(key?: string, dflt?: T): T | undefined { : dflt; } -/** set / delete storage */ +/** + * Sets or deletes a value in the active storage mechanism across any runtime environment. + * Automatically serializes objects for safe storage. + * + * @param key - The storage key to set + * @param val - The value to store (if undefined, the key is deleted) + * @example + * ```ts + * setStorage('user', { name: 'Alice' }); + * setStorage('user', undefined); // deletes 'user' + * ``` + */ export function setStorage(key: string, val?: T) { const stash = isDefined(val) ? stringify(val) : undefined; const set = isDefined(stash); diff --git a/packages/library/src/common/string.library.ts b/packages/library/src/common/string.library.ts index ab7d6d7c..0c99965a 100644 --- a/packages/library/src/common/string.library.ts +++ b/packages/library/src/common/string.library.ts @@ -9,8 +9,16 @@ import { isString, isObject, isNumeric, assertCondition, assertString } from '#l // (because they are referenced in prototype.library) /** - * clean a string to remove some standard control-characters (tab, line-feed, carriage-return) and trim redundant spaces. - * allow for optional RegExp to specify additional match + * Cleans a string by removing standard control characters (tab, line-feed, carriage-return) + * and trimming redundant spaces. Allows for an optional RegExp to specify additional matches to remove. + * + * @param str - The string to clean + * @param pat - Optional RegExp pattern to remove from the string + * @returns The cleaned string + * @example + * ```ts + * trimAll(' hello \t world \n'); // 'hello world' + * ``` */ export function trimAll(str: string | number, pat?: RegExp) { return str @@ -22,7 +30,16 @@ export function trimAll(str: string | number, pat?: RegExp) { .trim() // leading/trailing } -/** every word has its first letter capitalized */ +/** + * Converts a string to proper case, where the first letter of every word is capitalized. + * + * @param str - The strings to convert (can be multiple arguments or an array) + * @returns The proper-cased string + * @example + * ```ts + * toProperCase('hello world'); // 'Hello World' + * ``` + */ export function toProperCase(...str: T[]) { return str .flat() // in case {str} was already an array @@ -31,7 +48,18 @@ export function toProperCase(...str: T[]) { .join(' ') as T } -/** only the first letter of the entire string is capitalized (locale-aware) */ +/** + * Converts a string to title case, where only the first letter of the entire string is capitalized. + * This is locale-aware if a locale is provided. + * + * @param str - The string to convert + * @param locale - Optional locale for capitalization rules + * @returns The title-cased string + * @example + * ```ts + * toTitleCase('HELLO WORLD'); // 'Hello world' + * ``` + */ export function toTitleCase(str: string, locale?: string): string { try { return str.charAt(0).toLocaleUpperCase(locale) + str.slice(1).toLocaleLowerCase(locale); @@ -42,6 +70,17 @@ export function toTitleCase(str: string, locale?: string): string { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ const PAT = /[A-Z\xC0-\xD6\xD8-\xDE]?[a-z\xDF-\xF6\xF8-\xFF]+|[A-Z\xC0-\xD6\xD8-\xDE]+(?![a-z\xDF-\xF6\xF8-\xFF])|\d+/g; +/** + * Converts a string to camelCase format. + * Handles spaces, punctuation, and mixed casing. + * + * @param sentence - The string to convert + * @returns The camel-cased string + * @example + * ```ts + * toCamelCase('Hello World'); // 'helloWorld' + * ``` + */ export const toCamelCase = (sentence: T) => { let [word, ...rest] = sentence.match(PAT) ?? ['']; @@ -54,6 +93,16 @@ export const toCamelCase = (sentence: T) => { } const HEX = 16; +/** + * Generates a random alphanumeric string of a specified length. + * + * @param len - The desired length of the string (default: 36) + * @returns A random string + * @example + * ```ts + * const str = randomString(10); + * ``` + */ export const randomString = (len = 36) => { let str = ''; @@ -64,7 +113,18 @@ export const randomString = (len = 36) => { return str.substring(0, len); } -/** use sprintf-style formatting on a string */ +/** + * Formats a string using sprintf-style `%s` and `%j` parameter replacements. + * Also supports positional `${digit}` markers. + * + * @param fmt - The format string (or an object to stringify) + * @param msg - The arguments to inject into the format string + * @returns The formatted string + * @example + * ```ts + * sprintf('Hello %s', 'World'); // 'Hello World' + * ``` + */ export function sprintf(fmt: string, ...msg: any[]): string; // either a format-string, followed by arguments export function sprintf(...msg: any[]): string; // or just an array of arguments export function sprintf(fmt: {}, ...msg: any[]) { @@ -90,7 +150,18 @@ export function sprintf(fmt: {}, ...msg: any[]) { return sfmt.replace(regexp, (_, idx) => msg[idx]?.toString?.() || stringify(msg[idx])); } -/** apply a plural suffix, if greater than '1' */ +/** + * Applies a plural suffix to a word if the quantity is not exactly 1 or -1. + * + * @param val - The numerical quantity or an object containing values + * @param word - The singular word + * @param plural - The explicitly provided plural form (defaults to word + 's') + * @returns The appropriate singular or plural string + * @example + * ```ts + * plural(2, 'apple'); // 'apples' + * ``` + */ export const plural = (val: string | number | Record, word: string, plural = word + 's') => { const _plural = (num: string | number | object, word: string, plural = word + 's') => [1, -1].includes(Number(num)) ? word : plural; @@ -106,15 +177,33 @@ type SingularUnit = T extends `${infer S}s` : T : T; -/** strip a plural suffix, if endsWith 's' */ +/** + * Strips a plural 's' suffix from a string if it ends with 's' and is longer than 3 characters. + * + * @param val - The plural string + * @returns The singular string + * @example + * ```ts + * singular('apples'); // 'apple' + * ``` + */ export const singular = (val: T): SingularUnit => (val.endsWith('s') && val.length > 3 ? val.slice(0, -1) : val) as any; /** - * make an Object's values into a Template Literals, and evaluate - * @param templateString string containing ${key} placeholders - * @description WARNING: should not be used with untrusted templateString inputs if any form of evaluation is expected. - * This implementation only supports simple ${key} substitutions from templateData. + * Interpolates an object's values into a Template Literal string containing `${key}` placeholders. + * + * @remarks + * **WARNING:** Should not be used with untrusted templateString inputs if any form of evaluation is expected. + * This implementation only supports simple `${key}` substitutions from templateData. + * + * @param templateString - The string containing `${key}` placeholders + * @returns A function that takes a data object and returns the interpolated string + * @example + * ```ts + * const greet = makeTemplate('Hello ${name}'); + * greet({ name: 'World' }); // 'Hello World' + * ``` */ export const makeTemplate = (templateString: any) => (templateData: any) => @@ -123,10 +212,43 @@ export const makeTemplate = (templateString: any) => return val !== undefined ? String(val) : ''; }); +/** + * Coerces a value to a string, converts it to lowercase, and trims whitespace. + * + * @param str - The value to convert + * @returns The lowercased and trimmed string + * @example + * ```ts + * toLower(' HELLO '); // 'hello' + * ``` + */ export const toLower = (str: T) => isString(str) ? asString(str).toLowerCase().trim() : str; + +/** + * Coerces a value to a string, converts it to uppercase, and trims whitespace. + * + * @param str - The value to convert + * @returns The uppercased and trimmed string + * @example + * ```ts + * toUpper(' hello '); // 'HELLO' + * ``` + */ export const toUpper = (str: T) => isString(str) ? asString(str).toUpperCase().trim() : str; -/** assert string is within bounds */ +/** + * Asserts that a string's length is within specified minimum and maximum bounds. + * Throws an error if the string is invalid or outside the bounds. + * + * @param str - The string to validate + * @param min - The minimum allowed length + * @param max - The maximum allowed length (defaults to min) + * @returns The validated string typed with its length constraint + * @example + * ```ts + * strlen('abc', 1, 5); // 'abc' + * ``` + */ type StrLen = string & { __value__: never }; export const strlen = (str: unknown, min: Min, max?: Max) => { assertString(str); @@ -136,28 +258,54 @@ export const strlen = (str: unknown, min } /** - * pad a string with leading character - * @param nbr input value to pad - * @param len fill-length (default: 2) - * @param fill character (default \ for string and \ for number) - * @returns fixed-length string padded on the left with fill-character + * Pads a string or number with a leading fill character to reach a specified length. + * + * @param nbr - The input value to pad + * @param len - The target length (default: 2) + * @param fill - The fill character (default: space for strings, zero for numbers) + * @returns The left-padded string + * @example + * ```ts + * pad(5, 2); // '05' + * ``` */ export const pad = (nbr: string | number | bigint = 0, len = 2, fill?: string | number) => nbr.toString().padStart(len, nullishToValue(fill, isNumeric(nbr) ? '0' : ' ').toString()); -/** pad a string with non-blocking spaces, to help right-align a display */ +/** + * Pads a numeric or string value with non-breaking spaces. + * Useful for right-aligning displays in monospace environments. + * + * @param str - The value to pad + * @param pad - The target length (default: 6) + * @returns The right-aligned padded string + */ export const padString = (str: string | number | bigint, pad = 6) => (isNumeric(str) ? asNumber(str).toFixed(2).toString() : str.toString() ?? '').padStart(pad, '\u007F'); /** * Reconstructs a string from an array of char codes. * Useful for hiding strings from minifiers and reverse-engineers. + * + * @param codes - The array of character codes + * @returns The reconstructed string + * @example + * ```ts + * reveal([104, 105]); // 'hi' + * ``` */ export const reveal = (codes: number[]): string => codes.map(c => String.fromCharCode(c)).join(''); /** * Converts a string into an array of char codes. - * Useful as a developer utility to generate the array to paste into `reveal()`. + * Useful as a developer utility to generate arrays to paste into `reveal()`. + * + * @param str - The string to conceal + * @returns An array of character codes + * @example + * ```ts + * conceal('hi'); // [104, 105] + * ``` */ export const conceal = (str: string): number[] => str.split('').map(c => c.charCodeAt(0)); diff --git a/packages/library/src/common/symbol.library.ts b/packages/library/src/common/symbol.library.ts index f73e9f55..aa865258 100644 --- a/packages/library/src/common/symbol.library.ts +++ b/packages/library/src/common/symbol.library.ts @@ -18,7 +18,17 @@ export const sym = { $Target, $Discover, $Extensible, $Inspect, $LogConfig, $Registry, $Register, $SerializerRegistry, $Identity, $ImmutableSkip } as const; -/** identify and mark a logging configuration object */ +/** + * Identifies and marks an object as a logging configuration object using a global symbol. + * This allows the library to securely differentiate configs from regular objects. + * + * @param obj - The configuration object to mark + * @returns The marked object + * @example + * ```ts + * const cfg = markConfig({ level: 'debug' }); + * ``` + */ export function markConfig(obj: T): T { if (!(obj as any)[sym.$LogConfig] && Object.isExtensible(obj)) Object.defineProperty(obj, sym.$LogConfig, { value: true, enumerable: false, writable: true, configurable: true }); diff --git a/packages/library/src/common/temporal.library.ts b/packages/library/src/common/temporal.library.ts index 39c35d61..40741271 100644 --- a/packages/library/src/common/temporal.library.ts +++ b/packages/library/src/common/temporal.library.ts @@ -6,29 +6,50 @@ import '#library/temporal.polyfill.js'; // ensure Temporal is available import { isNumber, isObject, isString, isDefined, isZonedDateTime } from '#library/assertion.library.js'; -/** return the current Temporal.Now.instant */ +/** + * Returns the current instant in time using `Temporal.Now.instant()`. + * + * @returns The current Temporal.Instant + */ export function instant() { return Temporal.Now.instant(); } -/** return the current Temporal.Now.plainDateISO */ +/** + * Returns the current plain date (ISO calendar) for the given timezone. + * + * @param timeZone - The time zone to use (default: system local timezone) + * @returns The current Temporal.PlainDate + */ export function today(timeZone: string = Intl.DateTimeFormat().resolvedOptions().timeZone) { return Temporal.Now.plainDateISO(timeZone); } -/** return the current Unix timestamp (seconds) */ +/** + * Returns the current Unix timestamp in seconds. + * + * @returns The current Unix timestamp (seconds) + */ export function unix() { return Math.trunc(instant().epochMilliseconds / 1_000); } -/** return the current Unix timestamp (nanoseconds) */ +/** + * Returns the current Unix timestamp in nanoseconds. + * + * @returns The current Unix timestamp (nanoseconds) + */ export function epoch() { return instant().epochNanoseconds; } /** - * return the January and July offsets (nanoseconds) for a given timezone and year - * @note also maintained in `tempo-fns` β€” please sync changes + * Returns the January and July offsets (in nanoseconds) for a given timezone and year. + * Used for inferring daylight saving time and hemisphere characteristics. + * + * @param timeZone - The IANA timezone string + * @param year - The reference year to calculate offsets for (default: 2024 for stability) + * @returns An object containing the `jan` and `jul` offsets */ export function getOffsets(timeZone: string, year = 2024) { //** use a fixed reference-year (2024) for stability */ const jan = Temporal.PlainDate.from({ year, month: 1, day: 1 }).toZonedDateTime(timeZone).offsetNanoseconds; @@ -38,8 +59,11 @@ export function getOffsets(timeZone: string, year = 2024) { //** use a fixed ref } /** - * return whether the given (or current) date is in Daylight Savings - * @note also maintained in `tempo-fns` β€” please sync changes + * Determines whether the given (or current) date is observing Daylight Saving Time. + * + * @param date - Optional ZonedDateTime or ISO string to check + * @param timeZone - The timezone to use if creating a new date (default: system local) + * @returns True if the date is in DST, false otherwise */ export function isDST(date?: Temporal.ZonedDateTime | string, timeZone: string = Intl.DateTimeFormat().resolvedOptions().timeZone) { const zdt = isString(date) @@ -51,10 +75,13 @@ export function isDST(date?: Temporal.ZonedDateTime | string, timeZone: string = } /** - * Temporal rejects fractional Duration values, so normalise - * fractional parts downwards, e.g. { seconds: 0.1 } β†’ { milliseconds: 100 }. + * Normalizes fractional duration values downwards to smaller units. + * Temporal rejects fractional Duration values (e.g., `{ seconds: 0.1 }`), + * so this function converts them (e.g., to `{ milliseconds: 100 }`). * Mutates the provided duration object. - * @note also maintained in `tempo-fns` β€” please sync changes + * + * @param payload - The record object containing duration properties + * @returns The mutated duration record */ export function normaliseFractionalDurations(payload: Record) { const SCALE: [string, string, number][] = [ @@ -86,9 +113,12 @@ export function normaliseFractionalDurations(payload: Record) { // ───────────────────────────────────────────────── /** - * ## toZonedDateTime - * Create a `Temporal.ZonedDateTime` from a - * property-bag or ISO string. + * Creates a `Temporal.ZonedDateTime` from a property-bag or ISO string. + * Automatically injects the specified timezone if missing from the string. + * + * @param bag - The property bag or ISO string to convert + * @param tz - The fallback timezone (default: 'UTC') + * @returns The created Temporal.ZonedDateTime */ export function toZonedDateTime(bag: Temporal.ZonedDateTimeLike | string, tz: Temporal.TimeZoneLike = 'UTC'): Temporal.ZonedDateTime { if (isString(bag)) { @@ -100,28 +130,33 @@ export function toZonedDateTime(bag: Temporal.ZonedDateTimeLike | string, tz: Te } /** - * ## toPlainDate - * Create a `Temporal.PlainDate` from a - * property-bag or ISO string. + * Creates a `Temporal.PlainDate` from a property-bag or ISO string. + * + * @param bag - The property bag or ISO string to convert + * @returns The created Temporal.PlainDate */ export function toPlainDate(bag: Temporal.PlainDateLike | string): Temporal.PlainDate { return Temporal.PlainDate.from(bag); } /** - * ## toInstant - * Create a `Temporal.Instant` from epoch - * nanoseconds (bigint). + * Creates a `Temporal.Instant` from epoch nanoseconds. + * + * @param epochNanoseconds - The bigint representing nanoseconds since the UNIX epoch + * @returns The created Temporal.Instant */ export function toInstant(epochNanoseconds: bigint): Temporal.Instant { return Temporal.Instant.fromEpochNanoseconds(epochNanoseconds); } /** - * ## getTemporalIds - * Normalize TimeZone and Calendar inputs into a [timeZoneId, calendarId] tuple. - * Accepts either (tz, cal) strings or a single ZonedDateTime-like object. - * Supports both spec-final (flat) and V8 harmony (nested) structures. + * Normalizes TimeZone and Calendar inputs into a `[timeZoneId, calendarId]` tuple. + * Accepts either `(tz, cal)` strings or a single ZonedDateTime-like object. + * Supports both spec-final (flat) and V8 harmony (nested) Temporal structures. + * + * @param tzOrZdt - The TimeZone string, Calendar string, or ZonedDateTime object + * @param cal - The optional Calendar string if `tzOrZdt` is a timezone + * @returns A tuple of `[timeZoneId, calendarId]` */ export function getTemporalIds(zdt: Temporal.ZonedDateTime, cal?: Temporal.CalendarLike): [string, string]; export function getTemporalIds(tz: Temporal.TimeZoneLike, cal?: Temporal.CalendarLike): [string, string]; @@ -158,11 +193,12 @@ export function getTemporalIds(tzOrZdt: any, cal?: any): [string, string] { } /** - * ## normalizeUtcOffset - * Convert informal UTC offset strings into the `Β±HH:MM` format required by Temporal. + * Converts informal UTC offset strings into the `Β±HH:MM` format required by Temporal. * Accepts forms like `'UTC+8'`, `'UTC-9'`, `'UTC+08:00'`, `'UTC-05:30'`. * Returns the input unchanged if it does not match the UTCΒ± pattern. - * @note also maintained in `tempo-fns` β€” please sync changes + * + * @param zone - The informal offset string + * @returns The normalized offset string */ export function normalizeUtcOffset(zone: string): string { const match = /^UTC([+-])(\d{1,2})(?::(\d{2}))?$/i.exec(zone); diff --git a/packages/library/src/common/type.library.ts b/packages/library/src/common/type.library.ts index 8d40139b..6b89da17 100644 --- a/packages/library/src/common/type.library.ts +++ b/packages/library/src/common/type.library.ts @@ -19,13 +19,20 @@ export const getSafeTag = (obj: any): string | undefined => { } /** - * # getType - * return an object's type as a ProperCase string. - * if instance, return Class name. + * Returns an object's type as a ProperCase string (e.g., 'String', 'Number', 'Array'). + * If the object is a registered class instance, returns its class name. * - * @NOTE Load-Order Dependency: - * Consumers must import modules that call registerType() (such as Tempo) - * before calling getType() to ensure custom types are correctly identified. + * @remarks + * **Load-Order Dependency:** Consumers must import modules that call `registerType()` + * before calling `getType()` to ensure custom types are correctly identified. + * + * @param obj - The value to determine the type of + * @param instances - Optional list of additional custom instances to check against + * @returns The string representation of the object's type + * @example + * ```ts + * getType([]); // 'Array' + * ``` */ export const getType = (obj?: any, ...instances: Instance[]): Type => { const raw = (obj as any)?.[sym.$Target] ?? obj; // bypass Proxy traps @@ -59,12 +66,22 @@ export const getType = (obj?: any, ...instances: Instance[]): Type => { } } -/** return TypeValue object */ +/** + * Wraps a value in a TypeValue object containing both its determined type and the original value. + * + * @param value - The value to wrap + * @param instances - Optional list of additional custom instances to check against for typing + * @returns A TypeValue wrapper object + * @example + * ```ts + * const wrapper = asType('hello'); // { type: 'String', value: 'hello' } + * ``` + */ export const asType = (value?: T, ...instances: Instance[]) => ({ type: getType(value, ...instances), value } as TypeValue); /** - * # resetRegistry - * Clear the global type registry for test isolation and deterministic behavior. + * Clears the global type registry. + * Primarily used for test isolation and ensuring deterministic behavior between test suites. */ export const resetRegistry = () => { registry.length = 0; @@ -200,8 +217,17 @@ export type Constructor = new (...args: any[]) => T; export type Instance = { type: Type, class: Constructor } // allow for Class instance re-naming (to avoid minification mangling issues) /** - * register a class with the runtime type system. - * @NOTE Custom types must augment `TypeValueMap` to be recognized by the type system! + * Registers a class constructor with the runtime type system. + * + * @remarks + * Custom types must augment `TypeValueMap` to be strictly recognized by the TypeScript compiler. + * + * @param cls - The class constructor to register + * @param type - Optional explicit type name to register the class under + * @example + * ```ts + * registerType(MyCustomClass, 'MyCustomType'); + * ``` */ export const registerType = (cls: Constructor, type?: Type) => { if (typeof cls !== 'function') return; @@ -216,7 +242,7 @@ export const registerType = (cls: Constructor, type?: Type) => { } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -export type Temporals = typeof Temporal extends { Now: any } ? Exclude : never; +export type Temporals = 'Instant' | 'ZonedDateTime' | 'PlainDateTime' | 'PlainDate' | 'PlainTime' | 'PlainYearMonth' | 'PlainMonthDay' | 'Duration'; export type TemporalObject = Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime | Temporal.Instant | Temporal.Duration; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/packages/library/src/common/utility.library.ts b/packages/library/src/common/utility.library.ts index 45fca1f1..0049e290 100644 --- a/packages/library/src/common/utility.library.ts +++ b/packages/library/src/common/utility.library.ts @@ -5,7 +5,15 @@ import type { Secure, ValueOf } from '#library/type.library.js'; /** General utility functions */ -/** analyze the Call Stack to determine calling Function's name */ +/** + * Analyzes the Call Stack to determine the calling function's name. + * + * @returns The name of the calling function, or undefined if unresolvable + * @example + * ```ts + * function myFunc() { console.log(getCaller()); } // 'myFunc' + * ``` + */ export const getCaller = () => { const stackTrace = new Error().stack // only tested in latest FF and Chrome ?.split('\n') @@ -18,7 +26,16 @@ export const getCaller = () => { return (callerName[1] === 'new') ? callerName[2] : callerName[1].split('.')[0]; } -/** analyze the Call Stack to determine calling Function's name */ +/** + * Analyzes the Call Stack to determine the calling script's URI. + * + * @param nbr - The stack depth to inspect (default: 1) + * @returns The URI of the calling script + * @example + * ```ts + * const scriptUrl = getScript(); + * ``` + */ export const getScript = (nbr = 1) => { const stackTrace = new Error().stack ?.match(/([^ \n\(@])*([a-z]*:\/\/\/?)*?[a-z0-9\/\\]*\.js/ig) @@ -27,12 +44,16 @@ export const getScript = (nbr = 1) => { } /** - * introduce a wait-timer that will Error() on timeout. - * best used with Promise.race([xxx(), sleep()] - * @param msg string to display on a timeout - * @param timeout how many milliseconds to sleep (default 2-seconds) - * @returns Promise\ - * @see Context.Browser + * Introduces a wait-timer that will reject with an Error on timeout. + * Best used with `Promise.race([myTask(), sleep()])`. + * + * @param msg - The string to display on a timeout (default: 'sleep: timed out') + * @param timeout - The number of milliseconds to sleep (default: 2000) + * @returns A Promise that rejects after the specified timeout + * @example + * ```ts + * await Promise.race([fetchData(), sleep('Fetch timeout', 5000)]); + * ``` */ export const sleep = (msg = 'sleep: timed out', timeout = 2000) => new Promise((_, reject) => setTimeout(() => reject(new Error(msg)), timeout)); @@ -48,7 +69,17 @@ export const CONTEXT = { export type CONTEXT = ValueOf type Context = { global: any, type: CONTEXT } -/** determine JavaScript environment context */ +/** + * Determines the current JavaScript environment context. + * Useful for branching logic based on the runtime environment. + * + * @returns An object containing the global scope reference and the CONTEXT type enum + * @example + * ```ts + * const { type } = getContext(); + * if (type === CONTEXT.Browser) { ... } + * ``` + */ export const getContext = (): Context => { const global = globalThis as any; diff --git a/packages/library/src/common/webtoken.library.ts b/packages/library/src/common/webtoken.library.ts index bde5d880..b165fdad 100644 --- a/packages/library/src/common/webtoken.library.ts +++ b/packages/library/src/common/webtoken.library.ts @@ -8,7 +8,17 @@ const formatBase64Url = (base64: string) => base64.replace(/\+/g, '-').replace(/ const toBase64Url = (str: string) => formatBase64Url(bufferToBase64(encodeBuffer(str))); const bufToBase64Url = (buf: Uint8Array) => formatBase64Url(bufferToBase64(buf)); -/** fast, unverified decode of a JWT payload */ +/** + * Performs a fast, unverified decode of a JSON Web Token (JWT) payload. + * Does not verify the signature. Use only for reading public claims. + * + * @param jwt - The JWT string to decode + * @returns The parsed payload object, or null if decoding fails + * @example + * ```ts + * const payload = decodeJWT(token); + * ``` + */ export const decodeJWT = (jwt: string): T | null => { try { const part = jwt.split('.')[1]; @@ -23,7 +33,13 @@ export const decodeJWT = (jwt: string): T | null => { } catch { return null; } } -/** verify a JSON Web Signature */ +/** + * Verifies a JSON Web Signature (JWS) against a provided public key. + * + * @param token - The JWS string to verify + * @param publicKey - The CryptoKey used for verification + * @returns A promise resolving to true if the signature is valid + */ export const verifyJWS = async (token: string, publicKey: CryptoKey): Promise => { try { const parts = token.split('.'); @@ -55,7 +71,14 @@ export const verifyJWS = async (token: string, publicKey: CryptoKey): Promise => { try { const header64 = toBase64Url(JSON.stringify(headers)); diff --git a/packages/library/test/common/number_library.test.ts b/packages/library/test/common/number_library.test.ts index 7bfe0483..014903bb 100644 --- a/packages/library/test/common/number_library.test.ts +++ b/packages/library/test/common/number_library.test.ts @@ -1,32 +1,32 @@ -import { asCurrency } from '#library/number.library.js'; +import { formatCurrency } from '#library/international.library.js'; describe('Number Library', () => { - describe('asCurrency', () => { + describe('formatCurrency', () => { it('should format a number as AUD currency by default', () => { - const result = asCurrency(123.45); + const result = formatCurrency(123.45); // The exact format can depend on the locale, but it should contain the number and currency symbol (or code) expect(result).toMatch(/123\.45/); }); it('should format a string as currency by coercing to number', () => { - const result = asCurrency("123.45"); + const result = formatCurrency("123.45"); expect(result).toMatch(/123\.45/); }); it('should support different currencies (e.g. USD)', () => { - const result = asCurrency(123.45, 2, 'USD'); + const result = formatCurrency(123.45, 2, 'USD'); expect(result).toMatch(/123\.45/); // In many locales USD is $ or USD expect(result).toMatch(/\$|USD/); }); it('should handle zero correctly', () => { - const result = asCurrency(0); + const result = formatCurrency(0); expect(result).toMatch(/0\.00/); }); it('should handle string zero correctly', () => { - const result = asCurrency("0"); + const result = formatCurrency("0"); expect(result).toMatch(/0\.00/); }); }); diff --git a/packages/tempo/typedoc.library.json b/packages/tempo/typedoc.library.json index fcb37268..2028f638 100644 --- a/packages/tempo/typedoc.library.json +++ b/packages/tempo/typedoc.library.json @@ -1,11 +1,15 @@ { "entryPoints": [ + "../library/src/common.index.ts", "../library/src/browser.index.ts", "../library/src/server.index.ts" ], "out": "doc/api/library", "tsconfig": "../library/tsconfig.json", - "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"], + "plugin": [ + "typedoc-plugin-markdown", + "typedoc-vitepress-theme" + ], "hideBreadcrumbs": true, "hidePageTitle": true, "disableSources": true, @@ -17,4 +21,4 @@ "readme": "none", "excludeInternal": true, "githubPages": false -} +} \ No newline at end of file From 85f919f8131bfe5b330b0c3bb98fe3788012124a Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 20 Jul 2026 10:45:24 +1000 Subject: [PATCH 04/13] tempo-fns README --- packages/functions/package.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/functions/package.json b/packages/functions/package.json index ec895bd5..8411159a 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo-fns", - "version": "0.1.0", + "version": "0.1.1", "description": "Tree-shakeable functional utilities for the Temporal API", "author": "Magma Computing Solutions", "license": "MIT", @@ -13,6 +13,10 @@ "scheduling", "cron" ], + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, "repository": { "type": "git", "url": "git+https://github.com/magmacomputing/magma.git", @@ -20,6 +24,12 @@ }, "type": "module", "sideEffects": false, + "files": [ + "README.md", + "CHANGELOG.md", + "LICENSE", + "dist/" + ], "main": "dist/index.js", "types": "dist/index.d.ts", "browser": "dist/tempo-fns.global.js", @@ -60,4 +70,4 @@ "vitepress": "^1.6.4", "vue": "^3.5.39" } -} \ No newline at end of file +} From 7393ecd7dc71fd7638e5f5b714120ded1b5e52ee Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 20 Jul 2026 10:50:05 +1000 Subject: [PATCH 05/13] tempo-fns logo --- packages/functions/README.md | 2 +- packages/functions/package.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/functions/README.md b/packages/functions/README.md index a3494a26..96e836d7 100644 --- a/packages/functions/README.md +++ b/packages/functions/README.md @@ -2,7 +2,7 @@ - @magmacomputing/tempo-fns + @magmacomputing/tempo-fns

@magmacomputing/tempo-fns

diff --git a/packages/functions/package.json b/packages/functions/package.json index 8411159a..112e8c3c 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo-fns", - "version": "0.1.1", + "version": "0.1.2", "description": "Tree-shakeable functional utilities for the Temporal API", "author": "Magma Computing Solutions", "license": "MIT", @@ -28,7 +28,8 @@ "README.md", "CHANGELOG.md", "LICENSE", - "dist/" + "dist/", + "img/" ], "main": "dist/index.js", "types": "dist/index.d.ts", From f62e44c3bc4ad46a17521c4c7346d6bd4963e3d5 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 20 Jul 2026 14:36:27 +1000 Subject: [PATCH 06/13] term folder refactor --- package-lock.json | 16 +- package.json | 2 +- packages/library/package.json | 2 +- packages/plugins/.std/README.md | 42 +++ packages/plugins/.std/package.json | 14 + packages/plugins/.std/src/index.ts | 7 + .../term => plugins/.std/src}/term.quarter.ts | 14 +- .../term => plugins/.std/src}/term.season.ts | 8 +- .../.std/src}/term.timeline.ts | 6 +- .../term => plugins/.std/src}/term.zodiac.ts | 10 +- .../.std/test}/term.test.ts | 0 .../.std/test}/term_unified.test.ts | 0 packages/plugins/.std/test/tsconfig.json | 6 + packages/plugins/.std/tsconfig.json | 20 ++ packages/plugins/astro/src/index.ts | 4 +- .../plugins/batch/src/BatchOrchestrator.ts | 4 + packages/plugins/batch/src/index.ts | 13 + packages/plugins/finance/src/index.ts | 23 ++ packages/plugins/snap/src/index.ts | 20 +- packages/plugins/sync/src/AtomicClock.ts | 1 - packages/plugins/sync/src/AtomicReader.ts | 1 - packages/plugins/sync/src/index.ts | 9 + packages/plugins/vitest.shared.ts | 8 +- packages/tempo/bin/resolve-types.ts | 106 ++++--- .../doc/3-extending-tempo/tempo.modularity.md | 2 +- .../tempo/doc/3-extending-tempo/tempo.term.md | 5 +- packages/tempo/package.json | 27 +- packages/tempo/plan/refactor-path-terms.md | 296 ------------------ packages/tempo/rollup.config.js | 25 +- packages/tempo/src/engine/engine.alias.ts | 3 +- packages/tempo/src/plugin-api.index.ts | 3 +- packages/tempo/src/plugin/term/term.index.ts | 24 +- packages/tempo/src/std.d.ts | 1 + packages/tempo/src/tempo.index.ts | 12 +- packages/tempo/src/tempo.version.ts | 2 +- packages/tempo/src/tsconfig.json | 1 + packages/tempo/test/core/alias-engine.test.ts | 1 + .../tempo/test/core/discovery.getters.test.ts | 4 +- .../tempo/test/plugins/debug_term.test.ts | 37 --- packages/tempo/vitest.config.ts | 9 +- 40 files changed, 330 insertions(+), 458 deletions(-) create mode 100644 packages/plugins/.std/README.md create mode 100644 packages/plugins/.std/package.json create mode 100644 packages/plugins/.std/src/index.ts rename packages/{tempo/src/plugin/term => plugins/.std/src}/term.quarter.ts (86%) rename packages/{tempo/src/plugin/term => plugins/.std/src}/term.season.ts (90%) rename packages/{tempo/src/plugin/term => plugins/.std/src}/term.timeline.ts (92%) rename packages/{tempo/src/plugin/term => plugins/.std/src}/term.zodiac.ts (96%) rename packages/{tempo/test/plugins => plugins/.std/test}/term.test.ts (100%) rename packages/{tempo/test/plugins => plugins/.std/test}/term_unified.test.ts (100%) create mode 100644 packages/plugins/.std/test/tsconfig.json create mode 100644 packages/plugins/.std/tsconfig.json delete mode 100644 packages/tempo/plan/refactor-path-terms.md create mode 100644 packages/tempo/src/std.d.ts delete mode 100644 packages/tempo/test/plugins/debug_term.test.ts diff --git a/package-lock.json b/package-lock.json index 5240079e..aee53abb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tempo-monorepo", - "version": "3.9.3", + "version": "3.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "3.9.3", + "version": "3.10.1", "workspaces": [ "packages/*", "packages/plugins/*" @@ -11039,7 +11039,7 @@ }, "packages/functions": { "name": "@magmacomputing/tempo-fns", - "version": "0.1.0", + "version": "0.1.2", "license": "MIT", "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.3", @@ -11059,7 +11059,7 @@ }, "packages/library": { "name": "@magmacomputing/library", - "version": "3.9.3", + "version": "3.10.1", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -11097,14 +11097,14 @@ }, "packages/plugins/finance": { "name": "@magmacomputing/tempo-plugin-finance", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "devDependencies": { - "@magmacomputing/tempo": "^3.8.0", + "@magmacomputing/tempo": "^3.9.0", "vitest": "^1.0.0" }, "peerDependencies": { - "@magmacomputing/tempo": "^3.8.0" + "@magmacomputing/tempo": "^3.9.0" } }, "packages/plugins/finance/node_modules/@vitest/expect": { @@ -11395,7 +11395,7 @@ }, "packages/tempo": { "name": "@magmacomputing/tempo", - "version": "3.9.3", + "version": "3.10.1", "license": "MIT", "dependencies": { "tslib": "^2.8.1" diff --git a/package.json b/package.json index 955787b2..43c20a9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "3.10.0", + "version": "3.10.1", "private": true, "engines": { "node": ">=20.0.0" diff --git a/packages/library/package.json b/packages/library/package.json index 5cd0e264..db5fbcbf 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "3.10.0", + "version": "3.10.1", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/plugins/.std/README.md b/packages/plugins/.std/README.md new file mode 100644 index 00000000..39229775 --- /dev/null +++ b/packages/plugins/.std/README.md @@ -0,0 +1,42 @@ +# @magmacomputing/tempo-std + +> [!WARNING] +> **Not published separately.** This directory contains the standard built-in Terms that are natively bundled into the `@magmacomputing/tempo` package. + +## Why does this exist? +This package acts as a canonical "showcase" implementation of the Tempo Terms engine. By co-locating the standard Terms alongside other standalone plugins (like Astro or Finance), contributors and plugin authors have a clear reference for how to build their own Terms. + +At build time, the Rollup pipeline inlines these files directly into `@magmacomputing/tempo/dist/term/`, meaning end-users get them automatically without needing to install a separate NPM package. + +## Standard Built-in Terms + +| Key | Scope | Description | +|---|---|---| +| `Quarter` | `financial` | Fiscal quarters (Q1, Q2, Q3, Q4) | +| `Season` | `meteorological` | Meteorological seasons (Spring, Summer, Autumn, Winter) | +| `Zodiac` | `astrological` | Western Zodiac sun signs | +| `Timeline` | `calendar` | Century and Millennium terms | + +## Usage for End-Users +Because these are bundled natively, end-users can access them automatically when importing Tempo: +```ts +import { Tempo } from '@magmacomputing/tempo'; +// The StandardTerms are auto-loaded and available natively on the Tempo instance. +``` +Or to import them manually via the explicit sub-path: +```ts +// Import all standard terms collectively +import { StandardTerms } from '@magmacomputing/tempo/term/standard'; + +// Or import a specific term individually +import { QuarterTerm } from '@magmacomputing/tempo/term/quarter'; +``` + +## Contributing +> [!IMPORTANT] +> **Do not add custom or domain-specific terms here.** +> If you are building a new term for Tempo, create a new standalone plugin directory at `packages/plugins//` instead. + +**Build Notes**: +- Source lives in `packages/plugins/.std/src/` +- Output is routed to `packages/tempo/dist/term/` during the main Tempo build. diff --git a/packages/plugins/.std/package.json b/packages/plugins/.std/package.json new file mode 100644 index 00000000..41e833aa --- /dev/null +++ b/packages/plugins/.std/package.json @@ -0,0 +1,14 @@ +{ + "name": "@magmacomputing/tempo-std", + "version": "1.0.0", + "private": true, + "description": "Standard built-in Terms for @magmacomputing/tempo (showcase implementation β€” not published separately)", + "type": "module", + "scripts": { + "build": "tsc -b", + "test": "vitest run -c ../vitest.shared.ts" + }, + "peerDependencies": { + "@magmacomputing/tempo": "^3.9.x" + } +} diff --git a/packages/plugins/.std/src/index.ts b/packages/plugins/.std/src/index.ts new file mode 100644 index 00000000..91edbaa7 --- /dev/null +++ b/packages/plugins/.std/src/index.ts @@ -0,0 +1,7 @@ +import { QuarterTerm } from './term.quarter.js'; +import { SeasonTerm } from './term.season.js'; +import { ZodiacTerm } from './term.zodiac.js'; +import { TimelineTerm } from './term.timeline.js'; + +export { QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm }; +export const StandardTerms = [QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm]; diff --git a/packages/tempo/src/plugin/term/term.quarter.ts b/packages/plugins/.std/src/term.quarter.ts similarity index 86% rename from packages/tempo/src/plugin/term/term.quarter.ts rename to packages/plugins/.std/src/term.quarter.ts index cdf4fc7c..8440f6c9 100644 --- a/packages/tempo/src/plugin/term/term.quarter.ts +++ b/packages/plugins/.std/src/term.quarter.ts @@ -1,9 +1,7 @@ -import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; -import { logWarn } from '../../support/support.util.js'; -import { COMPASS } from '../../support/support.enum.js'; -import { isNumber } from '#library/assertion.library.js'; -import { asArray } from '#library'; -import type { Tempo } from '../../tempo.class.js'; +import { defineTerm, getTermRange, defineRange, resolveCycleWindow, COMPASS } from '@magmacomputing/tempo/plugin-api'; +import { logWarn } from '@magmacomputing/tempo/plugin-api'; +import { isNumber, asArray } from '@magmacomputing/library'; +import type { Tempo } from '@magmacomputing/tempo'; /** definition of fiscal quarter ranges */ const groups = defineRange([ @@ -27,7 +25,7 @@ function resolve(t: Tempo, anchor?: any): any[] { const list = resolveCycleWindow(t, groups, { anchor, groupBy: ['sphere'] }); - list.forEach(itm => { + list.forEach((itm: any) => { if (isNumber(itm.fiscal)) itm.fiscal += isNumber(itm.year) ? itm.year : 0; }); @@ -54,7 +52,7 @@ export const QuarterTerm = defineTerm({ } }); -declare module '../../tempo.class.js' { +declare module '@magmacomputing/tempo' { interface TempoTermRegistry { qtr: 'Q1' | 'Q2' | 'Q3' | 'Q4'; quarter: { diff --git a/packages/tempo/src/plugin/term/term.season.ts b/packages/plugins/.std/src/term.season.ts similarity index 90% rename from packages/tempo/src/plugin/term/term.season.ts rename to packages/plugins/.std/src/term.season.ts index a7be64b0..222ec400 100644 --- a/packages/tempo/src/plugin/term/term.season.ts +++ b/packages/plugins/.std/src/term.season.ts @@ -1,7 +1,5 @@ -import { getTermRange, defineTerm, defineRange, resolveCycleWindow } from './term.util.js'; -import { logWarn } from '../../support/support.util.js'; -import { COMPASS } from '../../support/support.enum.js'; -import type { Tempo } from '../../tempo.class.js'; +import { getTermRange, defineTerm, defineRange, resolveCycleWindow, logWarn, COMPASS } from '@magmacomputing/tempo/plugin-api'; +import type { Tempo } from '@magmacomputing/tempo'; /** definition of meteorological season ranges */ const groups = defineRange([ @@ -49,7 +47,7 @@ export const SeasonTerm = defineTerm({ } }); -declare module '../../tempo.class.js' { +declare module '@magmacomputing/tempo' { interface TempoTermRegistry { szn: 'Spring' | 'Summer' | 'Autumn' | 'Winter'; season: { diff --git a/packages/tempo/src/plugin/term/term.timeline.ts b/packages/plugins/.std/src/term.timeline.ts similarity index 92% rename from packages/tempo/src/plugin/term/term.timeline.ts rename to packages/plugins/.std/src/term.timeline.ts index 77e55b07..894c9e7e 100644 --- a/packages/tempo/src/plugin/term/term.timeline.ts +++ b/packages/plugins/.std/src/term.timeline.ts @@ -1,5 +1,5 @@ -import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; -import type { Tempo } from '../../tempo.class.js'; +import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '@magmacomputing/tempo/plugin-api'; +import type { Tempo } from '@magmacomputing/tempo'; /** definition of daily time periods */ const groups = defineRange([ @@ -32,7 +32,7 @@ export const TimelineTerm = defineTerm({ } }); -declare module '../../tempo.class.js' { +declare module '@magmacomputing/tempo' { interface TempoTermRegistry { tod: 'Midnight' | 'Early' | 'Morning' | 'Midmorning' | 'Midday' | 'Afternoon' | 'Evening' | 'Night'; timeOfDay: { diff --git a/packages/tempo/src/plugin/term/term.zodiac.ts b/packages/plugins/.std/src/term.zodiac.ts similarity index 96% rename from packages/tempo/src/plugin/term/term.zodiac.ts rename to packages/plugins/.std/src/term.zodiac.ts index 3c37affe..be37653b 100644 --- a/packages/tempo/src/plugin/term/term.zodiac.ts +++ b/packages/plugins/.std/src/term.zodiac.ts @@ -1,6 +1,6 @@ -import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; -import { isNumber } from '#library/assertion.library.js'; -import type { Tempo } from '../../tempo.class.js'; +import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '@magmacomputing/tempo/plugin-api'; +import { isNumber } from '@magmacomputing/library'; +import type { Tempo } from '@magmacomputing/tempo'; /** definition of astrological zodiac ranges */ const groups = defineRange([ @@ -45,7 +45,7 @@ function resolve(t: Tempo, anchor?: any) { const list = resolveCycleWindow(t, groups, { anchor, groupBy: ['group'], group: 'western' }); // calculate the Chinese Zodiac based on the year of the candidate sign - list.forEach(itm => { + list.forEach((itm: any) => { const year = itm.year ?? (anchor?.year); if (isNumber(year)) itm['CN'] = getChineseZodiac(year); }); @@ -97,7 +97,7 @@ function getChineseZodiac(year: number) { } } -declare module '../../tempo.class.js' { +declare module '@magmacomputing/tempo' { interface TempoTermRegistry { zdc: 'Aries' | 'Taurus' | 'Gemini' | 'Cancer' | 'Leo' | 'Virgo' | 'Libra' | 'Scorpio' | 'Sagittarius' | 'Capricorn' | 'Aquarius' | 'Pisces'; zodiac: { diff --git a/packages/tempo/test/plugins/term.test.ts b/packages/plugins/.std/test/term.test.ts similarity index 100% rename from packages/tempo/test/plugins/term.test.ts rename to packages/plugins/.std/test/term.test.ts diff --git a/packages/tempo/test/plugins/term_unified.test.ts b/packages/plugins/.std/test/term_unified.test.ts similarity index 100% rename from packages/tempo/test/plugins/term_unified.test.ts rename to packages/plugins/.std/test/term_unified.test.ts diff --git a/packages/plugins/.std/test/tsconfig.json b/packages/plugins/.std/test/tsconfig.json new file mode 100644 index 00000000..642ee082 --- /dev/null +++ b/packages/plugins/.std/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.test.json", + "include": [ + "**/*.ts" + ] +} diff --git a/packages/plugins/.std/tsconfig.json b/packages/plugins/.std/tsconfig.json new file mode 100644 index 00000000..dfc8aa6c --- /dev/null +++ b/packages/plugins/.std/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true, + "noEmit": false, + "paths": { + "@magmacomputing/tempo/plugin-api": ["../../tempo/src/plugin-api.index.ts"], + "@magmacomputing/tempo": ["../../tempo/src/tempo.index.ts"], + "@magmacomputing/library": ["../../library/src/common.index.ts"], + "#library/*": ["../../library/src/common/*"] + } + }, + "include": ["src/**/*.ts"], + "references": [ + { "path": "../../library/src" }, + { "path": "../../tempo/src" } + ] +} diff --git a/packages/plugins/astro/src/index.ts b/packages/plugins/astro/src/index.ts index 240fc651..22e579a9 100644 --- a/packages/plugins/astro/src/index.ts +++ b/packages/plugins/astro/src/index.ts @@ -106,8 +106,8 @@ function resolveDateBoundary(t: Tempo, anchor?: any) { } /** - * ## AstroTerm - * Exposes precise astronomical calculations as a standard Tempo scope. + * Exposes precise astronomical calculations (equinoxes and solstices) + * as a standard Tempo scope. */ export const AstroTerm = defineTerm({ key, diff --git a/packages/plugins/batch/src/BatchOrchestrator.ts b/packages/plugins/batch/src/BatchOrchestrator.ts index c06d8743..9c4e67ab 100644 --- a/packages/plugins/batch/src/BatchOrchestrator.ts +++ b/packages/plugins/batch/src/BatchOrchestrator.ts @@ -14,6 +14,10 @@ export interface BatchOptions { * Number of threads to use. Defaults to the number of logical CPUs. */ threads?: number; + /** + * If true, the output array will contain hydrated Tempo instances instead of raw epoch numbers. + * Default is false (returns raw epochs for maximum performance). + */ rehydrate?: boolean; } diff --git a/packages/plugins/batch/src/index.ts b/packages/plugins/batch/src/index.ts index a114f5b8..e005453e 100644 --- a/packages/plugins/batch/src/index.ts +++ b/packages/plugins/batch/src/index.ts @@ -5,10 +5,23 @@ export type { BatchOptions }; declare module '@magmacomputing/tempo' { namespace Tempo { + /** + * Orchestrates the parallel execution of a mutation or formatting operation across an array of epochs. + * Automatically distributes the workload across a pool of Web Workers using SharedArrayBuffers (when available). + * + * @param epochs - Array of raw millisecond epoch numbers to process + * @param operation - The tempo mutation string (e.g., '+1w') + * @param options - Execution options including thread count and return format + * @returns A promise resolving to an array of transformed epochs (or Tempo instances) + */ function batch(epochs: number[], operation: string, options?: BatchOptions): Promise; } } +/** + * The Batch Plugin. + * Exposes `Tempo.batch()` for high-performance, multi-threaded array processing. + */ export const BatchPlugin: TempoPlugin = definePlugin({ name: 'batch', install(TempoRef: any) { diff --git a/packages/plugins/finance/src/index.ts b/packages/plugins/finance/src/index.ts index 93da7411..943783d0 100644 --- a/packages/plugins/finance/src/index.ts +++ b/packages/plugins/finance/src/index.ts @@ -6,8 +6,27 @@ import type { Tempo } from '@magmacomputing/tempo'; // Exporting pure functions allows users to import exactly what they need // without incurring the overhead of the full Object-Oriented engine. // ----------------------------------------------------------------------------- +/** + * Calculates the standard fiscal quarter for a given Tempo instance (1-4). + * + * @param tempo - The Tempo instance to evaluate + * @returns The fiscal quarter number (1, 2, 3, or 4) + */ export const fiscalQuarter = (tempo: Tempo) => Math.floor((tempo.mm - 1) / 3) + 1; +/** + * Extracts the tax year for a given Tempo instance. + * Currently defaults to the calendar year (yy). + * + * @param tempo - The Tempo instance to evaluate + * @returns The tax year + */ export const taxYear = (tempo: Tempo) => tempo.yy; +/** + * Creates a predicate function that determines if the given Tempo instance falls on the start of a fiscal year (Jan 1st). + * + * @param tempo - The Tempo instance to evaluate + * @returns A boolean predicate function + */ export const isFiscalYearStart = (tempo: Tempo) => () => tempo.mm === 1 && tempo.dd === 1; // ----------------------------------------------------------------------------- @@ -15,6 +34,10 @@ export const isFiscalYearStart = (tempo: Tempo) => () => tempo.mm === 1 && tempo // Wrap the functions in a Namespace Plugin so they can be injected directly // onto the Tempo instance (e.g., `t.finance.taxYear`) for a fluent experience. // ----------------------------------------------------------------------------- +/** + * The Finance Plugin Namespace. + * Exposes financial utilities directly on the Tempo instance via the `.finance` property. + */ export const FinanceNamespace: TempoPlugin = defineNamespace({ name: 'finance', resolvers: { diff --git a/packages/plugins/snap/src/index.ts b/packages/plugins/snap/src/index.ts index ccdee7de..8149b753 100644 --- a/packages/plugins/snap/src/index.ts +++ b/packages/plugins/snap/src/index.ts @@ -16,8 +16,17 @@ type OneKey = { [Q in keyof O]: O[Q] } : never }[K]; +/** + * Configuration options for snapping a Tempo instance. + * Accepts exactly one time component key mapped to a numeric step value, + * alongside an optional rounding direction. + */ type SnapOptions = OneKey & { direction?: 'up' | 'down' }; +/** + * The Snap Plugin. + * Installs the `snap()` method onto Tempo instances, allowing time to be rounded to specific intervals. + */ export const SnapPlugin: TempoPlugin = definePlugin({ name: 'snap', install(TempoClass: any) { @@ -153,9 +162,14 @@ declare module '@magmacomputing/tempo/core' { /** * Snaps the time to the nearest given interval for the specified unit. * - * @example t.snap() // Snaps to nearest 15 minutes (default) - * @example t.snap({ hh: 1 }) // Snaps to nearest hour - * @example t.snap({ ss: 30 }) // Snaps to nearest 30 seconds + * @param options - The snapping configuration including the time component, step value, and rounding direction. + * @returns A new snapped Tempo instance. + * @example + * ```ts + * t.snap() // Snaps to nearest 15 minutes (default) + * t.snap({ hh: 1 }) // Snaps to nearest hour + * t.snap({ ss: 30 }) // Snaps to nearest 30 seconds + * ``` */ snap(options?: SnapOptions): Tempo; } diff --git a/packages/plugins/sync/src/AtomicClock.ts b/packages/plugins/sync/src/AtomicClock.ts index 60901723..ec8ec451 100644 --- a/packages/plugins/sync/src/AtomicClock.ts +++ b/packages/plugins/sync/src/AtomicClock.ts @@ -9,7 +9,6 @@ export interface ClockOptions { } /** - * # AtomicClock * The master clock that continuously writes the current system time to a SharedArrayBuffer. * This should only be instantiated once on the main thread (or a master worker). */ diff --git a/packages/plugins/sync/src/AtomicReader.ts b/packages/plugins/sync/src/AtomicReader.ts index 715215b4..1f193dd1 100644 --- a/packages/plugins/sync/src/AtomicReader.ts +++ b/packages/plugins/sync/src/AtomicReader.ts @@ -1,7 +1,6 @@ import { Tempo } from '@magmacomputing/tempo'; /** - * # AtomicReader * The client reader that reads the synchronized time from a SharedArrayBuffer. * This should be instantiated in Web Workers or worker_threads. */ diff --git a/packages/plugins/sync/src/index.ts b/packages/plugins/sync/src/index.ts index 20641c1d..fbafbb1d 100644 --- a/packages/plugins/sync/src/index.ts +++ b/packages/plugins/sync/src/index.ts @@ -7,10 +7,15 @@ export { AtomicClock, AtomicReader, ClockOptions }; declare module '@magmacomputing/tempo' { namespace Tempo { const sync: { + /** Starts the master atomic clock on the current thread. */ startClock(options?: ClockOptions): void; + /** Stops the master atomic clock. */ stopClock(): void; + /** Gets the SharedArrayBuffer from the master clock. */ getBuffer(): SharedArrayBuffer; + /** Synchronously reads the exact time from the provided SharedArrayBuffer. */ now(buffer: SharedArrayBuffer): number; + /** Synchronously reads the time and returns a new Tempo instance. */ getTempo(buffer: SharedArrayBuffer): Tempo; } } @@ -18,6 +23,10 @@ declare module '@magmacomputing/tempo' { let _globalClock: AtomicClock | null = null; +/** + * The Sync Plugin. + * Exposes `Tempo.sync` for synchronizing time across multiple threads using SharedArrayBuffers. + */ export const SyncPlugin: TempoPlugin = definePlugin({ name: 'sync', install(TempoRef: any) { diff --git a/packages/plugins/vitest.shared.ts b/packages/plugins/vitest.shared.ts index 6391b94b..bf1683af 100644 --- a/packages/plugins/vitest.shared.ts +++ b/packages/plugins/vitest.shared.ts @@ -34,8 +34,14 @@ export default defineConfig({ { find: /^#tempo\/module$/, replacement: resolve(__dirname, '../tempo/src/module/module.index.ts') }, { find: /^#tempo\/support$/, replacement: resolve(__dirname, '../tempo/src/support/support.index.ts') }, { find: /^#tempo\/tempo\.class\.js$/, replacement: resolve(__dirname, '../tempo/src/tempo.index.ts') }, + { find: /^#tempo\/std$/, replacement: resolve(__dirname, '../plugins/.std/src/index.ts') }, + { find: /^#tempo\/license$/, replacement: resolve(__dirname, '../tempo/src/plugin/license/license.validator.ts') }, { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') }, - { find: /^#tempo\/(.*)$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') } + { find: /^#tempo\/(.*)$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') }, + { find: /^#tempo$/, replacement: resolve(__dirname, '../tempo/src/tempo.index.ts') }, + { find: /^@magmacomputing\/tempo\/plugin-api$/, replacement: resolve(__dirname, '../tempo/src/plugin-api.index.ts') }, + { find: /^@magmacomputing\/tempo$/, replacement: resolve(__dirname, '../tempo/src/tempo.index.ts') }, + { find: /^@magmacomputing\/tempo\/(.*)$/, replacement: resolve(__dirname, '../tempo/src/$1.ts') } ] } }); diff --git a/packages/tempo/bin/resolve-types.ts b/packages/tempo/bin/resolve-types.ts index 829256f5..051540bc 100644 --- a/packages/tempo/bin/resolve-types.ts +++ b/packages/tempo/bin/resolve-types.ts @@ -17,70 +17,82 @@ console.log('Resolving type definitions...'); // 1. Ensure lib directory exists if (!fs.existsSync(LIB_DEST_DIR)) - fs.mkdirSync(LIB_DEST_DIR, { recursive: true }); + fs.mkdirSync(LIB_DEST_DIR, { recursive: true }); // 2. Identify used library modules from Rollup's JS output const usedModules = fs.readdirSync(LIB_DEST_DIR) - .filter(f => f.endsWith('.js')) - .map(f => f.slice(0, -3)); + .filter(f => f.endsWith('.js')) + .map(f => f.slice(0, -3)); // 3. Copy corresponding .d.ts files from library usedModules.forEach(mod => { - const src = path.join(LIB_SRC_DIR, `${mod}.d.ts`); - const dest = path.join(LIB_DEST_DIR, `${mod}.d.ts`); - if (fs.existsSync(src)) - fs.copyFileSync(src, dest); + const src = path.join(LIB_SRC_DIR, `${mod}.d.ts`); + const dest = path.join(LIB_DEST_DIR, `${mod}.d.ts`); + if (fs.existsSync(src)) + fs.copyFileSync(src, dest); }); // 4. Walk through all .d.ts files in dist/ to rewrite aliases function walk(dir: string) { - const files = fs.readdirSync(dir); - for (const file of files) { - const fullPath = path.join(dir, file); - if (fs.statSync(fullPath).isDirectory()) { - walk(fullPath); - } else if (file.endsWith('.d.ts')) { - rewrite(fullPath); - } - } + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + walk(fullPath); + } else if (file.endsWith('.d.ts')) { + rewrite(fullPath); + } + } } 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${path.sep}`); + 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${path.sep}`); - let replacement: string; - if (isInsideLib) { - // If inside lib/, #library/ becomes ./ - replacement = './'; - } else { - // If at root (or elsewhere), #library/ becomes ./lib/ (with relative prefix) - let prefix = ''; - for (let i = 0; i < depth; i++) prefix += '../'; - replacement = `${prefix || './'}lib/`; - } + let replacement: string; + if (isInsideLib) { + // If inside lib/, #library/ becomes ./ + replacement = './'; + } else { + // If at root (or elsewhere), #library/ becomes ./lib/ (with relative prefix) + let prefix = ''; + for (let i = 0; i < depth; i++) prefix += '../'; + replacement = `${prefix || './'}lib/`; + } - // Handle #tempo/license resolution - let prefix = ''; - for (let i = 0; i < depth; i++) prefix += '../'; - let licReplacement = `${prefix || './'}plugin/license/license.validator.js`; + // Handle #tempo/license resolution + let prefix = ''; + for (let i = 0; i < depth; i++) prefix += '../'; + let licReplacement = `${prefix || './'}plugin/license/license.validator.js`; - const updatedContent = content - .replace(/#library\/([^"')]+\.js)/g, (_, libPath) => { - // NOTE: We use path.basename here because the @magmacomputing/library distribution - // is currently flat (dist/common/*.js), and our resolve process flattens all - // used library modules into the local dist/lib/ directory. - const fileName = path.basename(libPath); - return `${replacement}${fileName}`; - }) - .replace(/#library(['"])/g, (_, quote) => `${replacement}index.js${quote}`) - .replace(/#tempo\/license(['"])/g, (_, quote) => `${licReplacement}${quote}`); + const updatedContent = content + .replace(/#library\/([^"')]+\.js)/g, (_, libPath) => { + // NOTE: We use path.basename here because the @magmacomputing/library distribution + // is currently flat (dist/common/*.js), and our resolve process flattens all + // used library modules into the local dist/lib/ directory. + const fileName = path.basename(libPath); + return `${replacement}${fileName}`; + }) + .replace(/#library(['"])/g, (_, quote) => `${replacement}index.js${quote}`) + .replace(/#tempo\/license(['"])/g, (_, quote) => `${licReplacement}${quote}`); - if (content !== updatedContent) { - fs.writeFileSync(filePath, updatedContent); - } + if (content !== updatedContent) { + fs.writeFileSync(filePath, updatedContent); + } +} + +// 5. Copy .std types to dist/term/ +const STD_SRC_DIR = path.resolve('../plugins/.std/dist'); +const STD_DEST_DIR = path.resolve(DIST_DIR, 'term'); + +if (fs.existsSync(STD_SRC_DIR)) { + if (!fs.existsSync(STD_DEST_DIR)) fs.mkdirSync(STD_DEST_DIR, { recursive: true }); + const stdFiles = fs.readdirSync(STD_SRC_DIR).filter(f => f.endsWith('.d.ts')); + stdFiles.forEach(f => { + fs.copyFileSync(path.join(STD_SRC_DIR, f), path.join(STD_DEST_DIR, f)); + }); } walk(DIST_DIR); diff --git a/packages/tempo/doc/3-extending-tempo/tempo.modularity.md b/packages/tempo/doc/3-extending-tempo/tempo.modularity.md index 655537a8..e064b12b 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.modularity.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.modularity.md @@ -68,7 +68,7 @@ Tempo.extend(TermsModule); ``` #### 3. The Surgical Strike (Data-Only) -Best for maximum bundle-size optimization by picking only what you need. +Best for maximum bundle-size optimization by picking only what you need. Note that specific standard terms have their own dedicated sub-paths natively bundled within the main package. ```typescript import { Tempo } from '@magmacomputing/tempo/core'; import { QuarterTerm } from '@magmacomputing/tempo/term/quarter'; diff --git a/packages/tempo/doc/3-extending-tempo/tempo.term.md b/packages/tempo/doc/3-extending-tempo/tempo.term.md index 14e3e748..e738ff82 100644 --- a/packages/tempo/doc/3-extending-tempo/tempo.term.md +++ b/packages/tempo/doc/3-extending-tempo/tempo.term.md @@ -147,7 +147,7 @@ Tempo.extend(TermsModule); ``` ### 3. Surgical Opt-in (Maximum Lite) -Best for maximum bundle-size optimizationβ€”you only load the specific Terms you use. +Best for maximum bundle-size optimizationβ€”you only load the specific Terms you use. Note that specific standard terms have their own dedicated sub-paths natively bundled within the main package. ```typescript import { Tempo } from '@magmacomputing/tempo/core'; import { QuarterTerm } from '@magmacomputing/tempo/term/quarter'; @@ -335,7 +335,8 @@ To ensure a custom `Term` plugin integrates fully with Tempo, follow these guide * **Free-form text**: Longer descriptive fields (e.g., `label`, `description`, `trait`). 8. **IDE Autocomplete (Interface Augmentation)**: To provide a world-class developer experience, always augment the global `TempoTermRegistry` interface with your custom keys and payload types. This ensures IDEs can provide strict type-checking and autocomplete when users access `t.term.myKey`. ```ts - declare module '@magmacomputing/tempo/core' { + // IDE Autocomplete (Interface Augmentation) + declare module '@magmacomputing/tempo' { interface TempoTermRegistry { rsn: 'Spring' | 'Summer' | 'Autumn' | 'Winter'; retailSeason: { diff --git a/packages/tempo/package.json b/packages/tempo/package.json index d486156f..8c3d2abd 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "3.10.0", + "version": "3.10.1", "engines": { "node": ">=20.0.0" }, @@ -39,8 +39,6 @@ "type": "module", "sideEffects": [ "**/tempo.index.js", - "**/plugin/term/term.index.js", - "**/plugin/term/term.index.ts", "**/plugin/extend/extend.*.js", "**/plugin/extend/extend.*.ts", "dist/engine/engine.*.js", @@ -61,6 +59,9 @@ "#tempo": { "default": "./dist/tempo.index.js" }, + "#tempo/std": { + "default": "./dist/term/index.js" + }, "#tempo/core": { "default": "./dist/core.index.js" }, @@ -137,6 +138,26 @@ "types": "./dist/plugin/term/term.*.d.ts", "import": "./dist/plugin/term/term.*.js" }, + "./term/standard": { + "types": "./dist/term/index.d.ts", + "import": "./dist/term/index.js" + }, + "./term/quarter": { + "types": "./dist/term/term.quarter.d.ts", + "import": "./dist/term/term.quarter.js" + }, + "./term/season": { + "types": "./dist/term/term.season.d.ts", + "import": "./dist/term/term.season.js" + }, + "./term/zodiac": { + "types": "./dist/term/term.zodiac.d.ts", + "import": "./dist/term/term.zodiac.js" + }, + "./term/timeline": { + "types": "./dist/term/term.timeline.d.ts", + "import": "./dist/term/term.timeline.js" + }, "./plugin": { "types": "./dist/plugin/plugin.index.d.ts", "import": "./dist/plugin/plugin.index.js" diff --git a/packages/tempo/plan/refactor-path-terms.md b/packages/tempo/plan/refactor-path-terms.md deleted file mode 100644 index de7831d5..00000000 --- a/packages/tempo/plan/refactor-path-terms.md +++ /dev/null @@ -1,296 +0,0 @@ -# Plan: Refactor Built-in Terms into `packages/plugins/.std/` - -> **Status: DEFERRED** β€” agreed design, awaiting execution after other work is complete. - -## Overview - -Move the four built-in data Terms (Quarter, Season, Zodiac, Timeline) from -`packages/tempo/src/plugin/term/` into a new hidden directory -`packages/plugins/.std/`. - -**Delivery:** Rollup inlines the `.std` source into `dist/term/` inside -`@magmacomputing/tempo` at build time β€” mirroring how `@magmacomputing/library` -is inlined into `dist/lib/`. -**No separate npm publish is required.** - ---- - -## Naming: `.std` (hidden directory) - -The existing plugins workspace uses hidden directories for workspace infrastructure -(`.app/`) and visible directories for published plugins (`snap/`, `batch/` etc.). -`.std` inherits that convention: clearly internal, non-publishable, self-documenting. - -| Name | Problem | Verdict | -|---|---|---| -| `standard` | Contributor thinks it's publishable; user tries `npm i @magmacomputing/tempo-plugin-standard` β†’ 404 | ❌ | -| `.std` | Mirrors `.app`, clearly non-publishable | βœ… | - ---- - -## Key Design Decisions (all resolved) - -### Option B β€” `TermsModule` stays in `tempo.index.ts` - -`TermsModule` remains in `packages/tempo/src/tempo.index.ts`. -`.std` exports **only** `StandardTerms` (the array of four terms) and the individual -term exports. This means `.std` has **no need** for `getRuntime` or `onRegistryReset`, -and `plugin-api` needs no new lifecycle surface exposure. - -If a Premium plugin author needs runtime lifecycle access in future, a dedicated -mechanism will be designed at that time. - -### `plugin-api` additions (minimal) - -Only two pure utilities are added β€” no runtime singleton exposure: -- `resolveCycleWindow` (pure calculation function, already in `term.util.ts`) -- `logWarn` (simple console wrapper) - -### Auto-loading safety - -A user dropping a custom term file into `.std/src/` will **not** have it -auto-loaded. Rollup only bundles files reachable through the import graph. -Un-imported files are tree-shaken away. The `StandardTerms` array in -`.std/src/index.ts` is the sole entry gate. - -### `dist/term/` modification risk - -No greater than editing `dist/tempo.class.js`. Standard npm social contract applies. -A `/* Generated β€” do not edit */` header is sufficient deterrent. - ---- - -## Proposed Changes - -### 1 β€” New package: `packages/plugins/.std/` - -#### `packages/plugins/.std/package.json` - -```json -{ - "name": "@magmacomputing/tempo-std", - "version": "1.0.0", - "private": true, - "description": "Standard built-in Terms for @magmacomputing/tempo (showcase implementation β€” not published separately)", - "type": "module", - "peerDependencies": { - "@magmacomputing/tempo": "^3.9.x" - } -} -``` - -`"private": true` β€” hard guard against accidental `npm publish`; also self-documents intent. - -#### `packages/plugins/.std/src/index.ts` - -```ts -export { QuarterTerm } from './term.quarter.js'; -export { SeasonTerm } from './term.season.js'; -export { ZodiacTerm } from './term.zodiac.js'; -export { TimelineTerm } from './term.timeline.js'; - -export const StandardTerms = [QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm]; -``` - -`TermsModule` is **not** here β€” it stays in `tempo.index.ts`. - -#### `packages/plugins/.std/src/term.quarter.ts` / `term.season.ts` / `term.zodiac.ts` / `term.timeline.ts` - -Move verbatim from `packages/tempo/src/plugin/term/`. Update imports: - -```diff -- import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from './term.util.js'; -- import { logWarn } from '../../support/support.util.js'; -- import { COMPASS } from '../../support/support.enum.js'; -- import { isNumber } from '#library/assertion.library.js'; -- import type { Tempo } from '../../tempo.class.js'; - -+ import { defineTerm, getTermRange, defineRange, resolveCycleWindow, logWarn, COMPASS, isNumber } from '@magmacomputing/tempo/plugin-api'; -+ import type { Tempo } from '@magmacomputing/tempo'; -``` - -Module augmentation target: - -```diff -- declare module '../../tempo.class.js' { -+ declare module '@magmacomputing/tempo' { - interface TempoTermRegistry { ... } - } -``` - -#### `packages/plugins/.std/README.md` - -Content must include: -- ⚠️ Banner: "Not published separately" -- Why it exists (source organization, Rollup-inlined delivery) -- The four terms table (key, scope, description) -- How end-users access them (`import '@magmacomputing/tempo'` or `/term/quarter` sub-path) -- Clear instruction: **do not add custom terms here** β€” create a new `packages/plugins//` instead -- Build notes: source β†’ `packages/plugins/.std/src/`, output β†’ `packages/tempo/dist/term/` - -#### `packages/plugins/.std/test/` - -Move term tests from `packages/tempo/test/` (term-related files) alongside the source. - ---- - -### 2 β€” `packages/tempo/src/tsconfig.json` - -```diff - "paths": { - ... -+ "#tempo/std": [ "../../plugins/.std/src/index.ts" ], - } -``` - -No project reference needed β€” `.std` is consumed as source (same pattern as `@magmacomputing/library`). - ---- - -### 3 β€” `packages/tempo/src/tempo.index.ts` - -`TermsModule` body unchanged. Only the import source changes: - -```diff -- import { QuarterTerm } from './plugin/term/term.quarter.js'; -- import { SeasonTerm } from './plugin/term/term.season.js'; -- import { ZodiacTerm } from './plugin/term/term.zodiac.js'; -- import { TimelineTerm } from './plugin/term/term.timeline.js'; - -+ import { StandardTerms } from '#tempo/std'; -``` - -And simplify the StandardTerms reference inside `TermsModule.install()`: - -```diff -- TempoClass.extend([QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm]); -+ TempoClass.extend(StandardTerms); -``` - ---- - -### 4 β€” `packages/tempo/src/plugin/term/term.index.ts` (slimmed) - -Remove all data term imports, `StandardTerms`, and `TermsModule`. Retain framework only: - -```ts -// Framework utilities for external Term plugin authors -export { defineTerm, defineRange, getTermRange, resolveCycleWindow } from './term.util.js'; -export type { TermPlugin, Range, ResolvedRange } from './term.type.js'; -``` - -**Delete:** -- `packages/tempo/src/plugin/term/term.quarter.ts` -- `packages/tempo/src/plugin/term/term.season.ts` -- `packages/tempo/src/plugin/term/term.zodiac.ts` -- `packages/tempo/src/plugin/term/term.timeline.ts` - ---- - -### 5 β€” `packages/tempo/rollup.config.js` - -Extend the `entryFileNames` routing hook (after the existing `dist/lib/` branch): - -```diff - if (id.includes('magma/packages/library') || rel.startsWith('../library')) { - ... - return `lib/${dir}${name}.js`; - } - -+ if (id.includes('magma/packages/plugins/.std') || rel.startsWith('../plugins/.std')) { -+ const match = normalizedRel.match(/plugins\/\.std\/src\/(.*)$/); -+ const modulePath = match ? path.dirname(match[1]) : '.'; -+ const dir = modulePath === '.' ? '' : modulePath + '/'; -+ return `term/${dir}${name}.js`; -+ } -``` - -Output: `dist/term/index.js`, `dist/term/term.quarter.js` … `dist/term/term.timeline.js` - ---- - -### 6 β€” `packages/tempo/src/plugin-api.index.ts` - -Add two pure utilities (no runtime exposure): - -```diff -- export { defineTerm, defineRange, getTermRange } from './plugin/term/term.index.js'; -+ export { defineTerm, defineRange, getTermRange, resolveCycleWindow } from './plugin/term/term.index.js'; -+ export { logWarn } from './support/support.util.js'; -``` - ---- - -### 7 β€” `packages/tempo/package.json` - -#### `imports` β€” one addition - -```diff -+ "#tempo/std": { "default": "./dist/term/index.js" }, -``` - -#### `exports` β€” additive only - -```diff -+ "./term/standard": { "types": "./dist/term/index.d.ts", "import": "./dist/term/index.js" }, -+ "./term/quarter": { "types": "./dist/term/term.quarter.d.ts", "import": "./dist/term/term.quarter.js" }, -+ "./term/season": { "types": "./dist/term/term.season.d.ts", "import": "./dist/term/term.season.js" }, -+ "./term/zodiac": { "types": "./dist/term/term.zodiac.d.ts", "import": "./dist/term/term.zodiac.js" }, -+ "./term/timeline": { "types": "./dist/term/term.timeline.d.ts", "import": "./dist/term/term.timeline.js" }, -``` - -#### `sideEffects` β€” two removals - -```diff -- "**/plugin/term/term.index.js", -- "**/plugin/term/term.index.ts", -``` - -#### `dependencies` β€” no change - -`.std` source is inlined at build time. No new runtime npm dependency. - ---- - -## Documentation Trawl - -Run after code changes are complete. Files requiring review: - -| File | Change | -|---|---| -| `doc/3-extending-tempo/tempo.term.md` L153 | `@magmacomputing/tempo/term/quarter` is now a **real** sub-path β€” confirm/add note | -| `doc/3-extending-tempo/tempo.modularity.md` L74 | Same β€” confirm `/term/quarter` sub-path validity | -| `doc/3-extending-tempo/tempo.term.md` (augmentation example) | Ensure `declare module '@magmacomputing/tempo'` (not `/core`) | -| `doc/9-plugins/` | Consider adding `.std.md` entry or expanding `tempo.term.md` with "Built-in Standard Terms" section | -| `doc/api/Variable.StandardTerms.md` | Delete stale file; regenerate via `npm run docs:api` | -| `doc/api/Variable.TermsModule.md` | Regenerate | -| `doc/api/Function.define*.md` + `getTermRange.md` | Regenerate | -| `doc/api/Interface.TempoTermRegistry.md` | Regenerate | - ---- - -## Verification Plan - -```bash -# 1. Build -cd packages/tempo && npm run build - -# Confirm dist/term/ has 5 files -ls dist/term/ -# β†’ index.js term.quarter.js term.season.js term.zodiac.js term.timeline.js - -# Confirm dist/plugin/term/ has only framework files -ls dist/plugin/term/ -# β†’ term.index.js term.util.js term.type.js - -# 2. Full test suite -npm test - -# 3. REPL smoke test -npm run repl -# tempo.quarter / tempo.season / tempo.zodiac / tempo.timeOfDay all resolve -# import { StandardTerms } from '@magmacomputing/tempo/term/standard' β†’ [4 terms] - -# 4. Plugin compat -cd ../plugins/snap && npm run build # must still pass against updated plugin-api -``` diff --git a/packages/tempo/rollup.config.js b/packages/tempo/rollup.config.js index 8e302fec..a4065167 100644 --- a/packages/tempo/rollup.config.js +++ b/packages/tempo/rollup.config.js @@ -81,6 +81,15 @@ const entryPoints = Object.fromEntries( .filter(([key]) => !isPremiumAvailable || key !== 'plugin/license/license.validator') ); +const stdDir = path.resolve(__dirname, '../plugins/.std/dist'); +if (fs.existsSync(stdDir)) { + const stdFiles = getFiles(stdDir, '.js'); + for (const file of stdFiles) { + const rel = path.relative(stdDir, file).replace(/\.js$/, ''); + entryPoints[`term/${rel}`] = file; + } +} + export default [ ...(isPremiumAvailable ? [{ input: licensePath, @@ -165,7 +174,8 @@ export default [ alias({ entries: [ // Pull in the already-obfuscated monolith! - { find: '#tempo/license', replacement: path.resolve(__dirname, 'dist/plugin/license/license.validator.js') } + { find: '#tempo/license', replacement: path.resolve(__dirname, 'dist/plugin/license/license.validator.js') }, + { find: '#tempo/std', replacement: path.resolve(__dirname, '../plugins/.std/dist/index.js') } ] }), resolve({ extensions: ['.js', '.ts'] }) @@ -201,7 +211,8 @@ export default [ alias({ entries: [ // Pull in the already-obfuscated monolith! - { find: '#tempo/license', replacement: path.resolve(__dirname, 'dist/plugin/license/license.validator.js') } + { find: '#tempo/license', replacement: path.resolve(__dirname, 'dist/plugin/license/license.validator.js') }, + { find: '#tempo/std', replacement: path.resolve(__dirname, '../plugins/.std/dist/index.js') } ] }), resolve({ extensions: ['.js', '.ts'] }), @@ -243,6 +254,13 @@ export default [ return `lib/${dir}${name}.js`; } + if (id.includes('magma/packages/plugins/.std') || rel.startsWith('../plugins/.std')) { + const match = normalizedRel.match(/plugins\/\.std\/(?:src|dist)\/(.*)$/); + const modulePath = match ? path.dirname(match[1]) : '.'; + const dir = modulePath === '.' ? '' : modulePath + '/'; + return `term/${dir}${name}.js`; + } + if (rel.startsWith('..') || rel.includes('node_modules')) { const sanitized = normalizedRel.replace(/^(\.\.\/)+/, ''); const modulePath = path.dirname(sanitized); @@ -256,7 +274,8 @@ export default [ plugins: [ alias({ entries: [ - { find: '#tempo/license', replacement: path.resolve(__dirname, 'dist/plugin/license/license.validator.js') } + { find: '#tempo/license', replacement: path.resolve(__dirname, 'dist/plugin/license/license.validator.js') }, + { find: '#tempo/std', replacement: path.resolve(__dirname, '../plugins/.std/dist/index.js') } ] }), // We DO want to resolve @magmacomputing/library and bundle it into lib/ diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts index c88b3ede..05c5ed3b 100644 --- a/packages/tempo/src/engine/engine.alias.ts +++ b/packages/tempo/src/engine/engine.alias.ts @@ -128,11 +128,12 @@ export class AliasEngine { const aliasKey = `${type}${this.#depth}_${index}` as AliasKey; const shouldOverwrite = !(existing?.type === 'evt' && type === 'per'); - if (baseWord in this.#words) + if (baseWord in this.#words) { logWarn( `[AliasEngine] Collision detected for ${type} alias "${name}". ${shouldOverwrite ? 'Overwriting' : 'Preserving'} existing alias.`, this.#config ); + } if (shouldOverwrite) this.#words[baseWord] = aliasKey; diff --git a/packages/tempo/src/plugin-api.index.ts b/packages/tempo/src/plugin-api.index.ts index 16470a28..5f43316f 100644 --- a/packages/tempo/src/plugin-api.index.ts +++ b/packages/tempo/src/plugin-api.index.ts @@ -15,6 +15,7 @@ export * from './library.index.js'; export * from './plugin/plugin.index.js'; export * from './support/support.enum.js'; export * from './plugin/term/term.index.js'; -export { defineTerm, defineRange, getTermRange } from './plugin/term/term.index.js'; +export { defineTerm, defineRange, getTermRange, resolveCycleWindow } from './plugin/term/term.index.js'; +export { logError, logWarn, logDebug } from './support/support.util.js'; export * from '#tempo/license'; diff --git a/packages/tempo/src/plugin/term/term.index.ts b/packages/tempo/src/plugin/term/term.index.ts index 4e6a0fba..248e4d93 100644 --- a/packages/tempo/src/plugin/term/term.index.ts +++ b/packages/tempo/src/plugin/term/term.index.ts @@ -1,21 +1,3 @@ -import { defineModule } from '../plugin.util.js' -import { getRuntime, onRegistryReset } from '#tempo/support'; -import { Tempo } from '../../tempo.class.js'; -import { QuarterTerm } from './term.quarter.js' -import { SeasonTerm } from './term.season.js' -import { ZodiacTerm } from './term.zodiac.js' -import { TimelineTerm } from './term.timeline.js' - -/** collection of built-in terms for initial registration */ -export const StandardTerms = [QuarterTerm, SeasonTerm, ZodiacTerm, TimelineTerm]; -export { defineTerm, defineRange, getTermRange } from './term.util.js'; - -/** Aggregator module for all standard Terms */ -export const TermsModule = defineModule({ - name: 'TermsModule', - install(this: typeof Tempo, TempoClass: typeof Tempo) { - getRuntime().modules['TermsModule'] = true; // mark as canonical module - onRegistryReset(() => { TempoClass.extend(StandardTerms); }); - TempoClass.extend(StandardTerms); - }, -}); +// Framework utilities for external Term plugin authors +export { defineTerm, defineRange, getTermRange, resolveCycleWindow } from './term.util.js'; +export type { TermPlugin, Range, ResolvedRange } from './term.type.js'; diff --git a/packages/tempo/src/std.d.ts b/packages/tempo/src/std.d.ts new file mode 100644 index 00000000..c1c6da19 --- /dev/null +++ b/packages/tempo/src/std.d.ts @@ -0,0 +1 @@ +export const StandardTerms: any[]; diff --git a/packages/tempo/src/tempo.index.ts b/packages/tempo/src/tempo.index.ts index 8445d71e..bf1f3eec 100644 --- a/packages/tempo/src/tempo.index.ts +++ b/packages/tempo/src/tempo.index.ts @@ -6,7 +6,17 @@ import { FormatModule } from '#tempo/format'; import { MutateModule } from '#tempo/mutate'; import { DurationModule } from '#tempo/duration'; -import { TermsModule } from '#tempo/term'; +import { StandardTerms } from '#tempo/std'; +import { defineModule } from './plugin/plugin.util.js'; + +export const TermsModule = defineModule({ + name: 'TermsModule', + install(this: typeof Tempo, TempoClass: typeof Tempo) { + getRuntime().modules['TermsModule'] = true; + onRegistryReset(() => { TempoClass.extend(StandardTerms); }); + TempoClass.extend(StandardTerms); + }, +}); import { getRuntime } from '#tempo/support'; // Batteries Included: Register standard modules diff --git a/packages/tempo/src/tempo.version.ts b/packages/tempo/src/tempo.version.ts index ed002595..90956f91 100644 --- a/packages/tempo/src/tempo.version.ts +++ b/packages/tempo/src/tempo.version.ts @@ -5,4 +5,4 @@ * ⚠️ This file is auto-updated by `npm run build:version` (see `bin/update-version.mjs`). * Do NOT edit manually β€” your changes will be overwritten on the next build. */ -export const TEMPO_VERSION = '3.10.0'; +export const TEMPO_VERSION = '3.10.1'; diff --git a/packages/tempo/src/tsconfig.json b/packages/tempo/src/tsconfig.json index 3d170a41..e77d8be3 100644 --- a/packages/tempo/src/tsconfig.json +++ b/packages/tempo/src/tsconfig.json @@ -33,6 +33,7 @@ "#tempo/support": [ "./support/support.index.ts" ], "#tempo/support/*": [ "./support/*" ], "#tempo/license": [ "./plugin/license/license.validator.ts" ], + "#tempo/std": [ "./std.d.ts" ], "#tempo/*": [ "./*" ] } }, diff --git a/packages/tempo/test/core/alias-engine.test.ts b/packages/tempo/test/core/alias-engine.test.ts index 4e71f5d3..0fc6bddd 100644 --- a/packages/tempo/test/core/alias-engine.test.ts +++ b/packages/tempo/test/core/alias-engine.test.ts @@ -41,6 +41,7 @@ describe('AliasEngine', () => { const warnSpy = vi.spyOn(logTempo, 'warn'); const engine = new AliasEngine(); engine.registerAliases('evt', [['xmas', '25-Dec'], ['xmas', '24-Dec']]); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Collision detected')); }); diff --git a/packages/tempo/test/core/discovery.getters.test.ts b/packages/tempo/test/core/discovery.getters.test.ts index 4120a43a..ef01a803 100644 --- a/packages/tempo/test/core/discovery.getters.test.ts +++ b/packages/tempo/test/core/discovery.getters.test.ts @@ -3,7 +3,9 @@ import { Tempo } from '#tempo'; const label = 'discovery.getters:'; describe(`${label} static Tempo.terms`, () => { - beforeEach(() => { Tempo.init() }); + beforeEach(() => { + Tempo.init(); + }); test('supports key-based lookup (e.g. qtr)', () => { expect(Tempo.terms.qtr).toBeDefined(); diff --git a/packages/tempo/test/plugins/debug_term.test.ts b/packages/tempo/test/plugins/debug_term.test.ts deleted file mode 100644 index 6d6d3511..00000000 --- a/packages/tempo/test/plugins/debug_term.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Tempo } from '#tempo'; -import { QuarterTerm } from '#tempo/plugin/term/term.quarter.js'; - -describe('Debug QuarterTerm', () => { - let priorConfig: any; - - beforeAll(() => { - priorConfig = { ...Tempo.config }; - Tempo.init({ sphere: 'north' }); // Explicitly lock for testing - }); - - afterAll(() => { - Tempo.init(priorConfig); // Restore global state - }); - - it('should have a resolve method', () => { - expect(typeof QuarterTerm.resolve).toBe('function'); - }); - - it('should return 12 ranges for the 3-cycle window', () => { - const t = new Tempo(); - // @ts-ignore - const list = QuarterTerm.resolve.call(t); - expect(list.length).toBe(12); - }); - - it('should have 4 ranges in the North template', () => { - // @ts-ignore - expect(QuarterTerm.groups.north.length).toBe(4); - }); - - it('should be found in Tempo.#terms', () => { - // @ts-ignore - const term = Tempo.terms.find(t => t.scope === 'quarter'); - expect(term).toBeDefined(); - }); -}); diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 2c2b0cf9..293eca74 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -9,7 +9,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const isDist = process.env.TEST_DIST === 'true'; const polyfill = resolve(__dirname, './bin/temporal-polyfill.ts'); -const ciPrefilterSetup = resolve(__dirname, './test/support/ci.prefilter.setup.ts'); const consoleSpySetup = resolve(__dirname, './test/support/setup.console-spy.ts'); const licensePremium = process.env.TEMPO_LICENSE_PATH ? resolve(process.env.TEMPO_LICENSE_PATH) : undefined; @@ -56,9 +55,7 @@ export default defineConfig({ '**/test/**/*.lazy.test.ts', '**/test/browser/**' ], - setupFiles: process.env.TEMPO_PREFILTER_CI === 'true' - ? [polyfill, consoleSpySetup, ciPrefilterSetup] - : [polyfill, consoleSpySetup], + setupFiles: [polyfill, consoleSpySetup], }, resolve: { alias: isDist ? [ @@ -75,10 +72,12 @@ export default defineConfig({ { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './dist/module/$1.js') }, { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') }, { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './dist/$1.js') }, + { find: /^#tempo\/std$/, replacement: resolve(__dirname, './dist/term/index.js') }, { find: /^#tempo$/, replacement: resolve(__dirname, './dist/tempo.index.js') }, { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/dist/common/$1.js') }, { find: /^#library$/, replacement: resolve(__dirname, '../library/dist/common.index.js') }, { find: /^@magmacomputing\/tempo\/plugin$/, replacement: resolve(__dirname, './dist/plugin/plugin.index.js') }, + { find: /^@magmacomputing\/tempo\/plugin-api$/, replacement: resolve(__dirname, './dist/plugin-api.index.js') }, { find: /^@magmacomputing\/tempo\/plugin\/(.*)$/, replacement: resolve(__dirname, './dist/plugin/$1.js') }, { find: /^@magmacomputing\/tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, { find: /^@magmacomputing\/tempo\/term\/(.*)$/, replacement: resolve(__dirname, './dist/plugin/term/term.$1.js') }, @@ -90,6 +89,7 @@ export default defineConfig({ { find: resolve(__dirname, './src/plugin/license/license.validator.ts'), replacement: isPremiumAvailable ? (licensePremium as string) : licenseDefault }, { find: resolve(__dirname, './src/plugin/license/license.validator.js'), replacement: isPremiumAvailable ? (licensePremium as string) : licenseDefault }, { find: /^@magmacomputing\/tempo\/plugin$/, replacement: resolve(__dirname, './src/plugin/plugin.index.ts') }, + { find: /^@magmacomputing\/tempo\/plugin-api$/, replacement: resolve(__dirname, './src/plugin-api.index.ts') }, { find: /^@magmacomputing\/tempo\/plugin\/(.*)$/, replacement: resolve(__dirname, './src/plugin/$1.ts') }, { find: /^@magmacomputing\/tempo\/term$/, replacement: resolve(__dirname, './src/plugin/term/term.index.ts') }, { find: /^@magmacomputing\/tempo\/term\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/term.$1.ts') }, @@ -108,6 +108,7 @@ export default defineConfig({ { find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './src/module/$1.ts') }, { find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/$1.ts') }, { find: /^#tempo\/(.*)\.js$/, replacement: resolve(__dirname, './src/$1.ts') }, + { find: /^#tempo\/std$/, replacement: resolve(__dirname, '../plugins/.std/src/index.ts') }, { find: /^#tempo$/, replacement: resolve(__dirname, './src/tempo.index.ts') }, { find: /^#library\/(.*)\.js$/, replacement: resolve(__dirname, '../library/src/common/$1.ts') }, { find: /^#library$/, replacement: resolve(__dirname, '../library/src/common.index.ts') }, From c7f8c356a88745c416f47deb2cc6efaff28475b3 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Mon, 20 Jul 2026 16:04:45 +1000 Subject: [PATCH 07/13] PR next review --- CHANGELOG.md | 647 ------------------ packages/library/src/common/array.library.ts | 2 + .../src/common/international.library.ts | 2 +- packages/library/src/common/string.library.ts | 2 +- .../library/src/common/temporal.library.ts | 4 +- .../library/src/common/webtoken.library.ts | 4 +- packages/plugins/sync/CHANGELOG.md | 5 + packages/plugins/sync/package.json | 2 +- packages/plugins/sync/src/index.ts | 7 +- packages/tempo/CHANGELOG.md | 24 + packages/tempo/tsconfig.json | 3 +- 11 files changed, 45 insertions(+), 657 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3ba36c13..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,647 +0,0 @@ - -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [3.10.0] - 2026-07-19 - -### Added -- **Format Token Modifiers**: Introduced new capabilities for chained formatting modifiers. -- **Custom Format Tokens**: Completed the Custom Format Tokens implementation, allowing developers to build custom zero-overhead logic evaluators (like native Intl bridges). - -### Changed -- **Documentation Architecture**: Architectural deep-dives (Localized Parsing, Slick Mutations, Custom Tokens) have been extracted from the Cookbook into specialized Core Concepts guides (e.g. `tempo.parse.md`, `tempo.mutate.md`, `tempo.format.md`) to provide a punchier onboarding experience. - -## [3.9.0] - 2026-07-14 - -### Added -- **Era Parsing Engine**: Upgraded the `ParseModule` and Lexer to natively support parsing historical and future era dates with explicit markers (e.g. `200 BC`, `BC 200`, `2026 CE`). Supports both leading and trailing formats and flawlessly converts to the astronomical ISO 8601 year. -- **Era Formatting & Getters**: Added native support for the `{era}` and `{eraYear}` formatting tokens. Introduced `.era` and `.eraYear` getters on the core `Tempo` class for zero-cost access to historical date components. -- **Auto-Meridiem Spacing**: Implemented the `:space` modifier for the `{h12}` formatting token (e.g., `{h12:space:dots}`). This enables typographically correct spacing before automatically injected meridiems (e.g., `"10:30 a.m."`). - -### Changed -- **Documentation Alignment**: Cleaned up `tempo.config.md` to remove deprecated module references, perfectly aligning examples with the `tempo-workspace` ecosystem (`FinancePlugin` and `AstroTerm`). -- **Getter Documentation**: Created `tempo.getters.md` as the definitive, educational conceptual guide for utilizing Tempo's zero-cost evaluation getters. - -### Fixed -- **Core Typings**: Resolved `any` leakage in `Tempo` core methods by injecting strict overloads for `until()` and `since()` directly into `tempo.class.ts`, ensuring full IDE type-inference flows through to `.format()`. -- **Documentation Badges**: Standardized the Shields.io badge layout across the monorepo to use `

` tags with `inline-block` styling, fixing horizontal alignment issues caused by VitePress CSS overrides and eliminating malformed HTML `` hydration errors. - -## [3.8.0] - 2026-07-11 - -### Fixed -- **Plugin Argument Parsing**: Hardened `Tempo.extend` parsing logic to ensure single-argument discovery objects are not falsely popped as `options`. -- **Registry Merge Contracts**: Corrected documentation in `tempo.registry.md` to accurately define `registryUpdate()` as additive-only, clarifying that `Tempo.extend()` only shadows explicitly wrapped proxy dictionaries (like `formats`). - -### Added -- **Interval Primitive**: Introduced `Interval` as a core primitive in the `@magmacomputing/tempo` package, accessible statically via `Tempo.Interval` or as a decoupled named export. Provides robust `overlaps`, `abuts`, `contains`, `union`, and `intersection` capabilities for native Temporal and Tempo objects. -- **Namespace Architecture (`defineNamespace`)**: Officially launched the new Namespace Plugin architecture. This provides a clean mechanism to attach grouped API surfaces (like `t.finance.*`) onto the core Tempo instance without polluting the global scope or the natural-language parsing engine. -- **Strict Plugin Discrimination**: Core registration utilities (`definePlugin`, `defineTerm`, `defineModule`, `defineNamespace`) now strictly inject a discriminator `type` key (`'plugin' | 'term' | 'module' | 'namespace'`). This ensures internal registries and debugging tools can accurately categorize plugins without relying on loose structural sniffing. -- **Finance Sandbox (`@magmacomputing/tempo-plugin-finance`)**: Introduced the community `finance` package as the official reference implementation for Namespace plugins, complete with best-practice dual-build (ESM/DTS) architectures using `tsup`. - -### Changed -- **Build Pipeline Optimization**: Completely removed `esbuild` from the core transpilation pipeline in favor of a pure `tsc` + `Rollup` + `terser` architecture. This eliminates double-transpilation penalties, resulting in a cleaner, more efficient `dist/` build. -- **ESBuild Decorator Mitigations**: Uncovered a significant bug in `esbuild`'s handling of TS 7.0's new, spec-compliant `__esDecorate` down-leveling where transpiled class expressions drop decorator replacements. Maintained the `Object.freeze` constructor workarounds across the ecosystem to ensure full immutability compliance while tracking upstream bundler patches. -- **Documentation Architecture**: Completely overhauled the documentation repository to utilize a strictly-numbered directory structure (`1-getting-started`, `2-core-concepts`, etc.) that mirrors the VitePress UI 1:1, drastically reducing maintenance overhead and eliminating orphaned files. - -## [3.7.1] - 2026-07-08 - -### Fixed -- **License Admin Isolation**: Restructured the commercial licensing JWT payload to isolate administrative privileges (like `tempo-adm`) into a dedicated root-level `role` claim. This prevents internal admin flags from polluting the `scopes` dictionary and erroneously surfacing as "uninstalled premium plugins" in the `Tempo.terms` registry UI. - -## [3.7.0] - 2026-07-08 - -### Added -- **Runtime Versioning Registry**: Introduced a secure, static `Tempo.versions` registry. This provides zero-burden runtime observability of all loaded core modules and community plugins. -- **Automated Plugin Versioning**: Community plugins now automatically inject their version via a custom ESBuild virtual module pipeline, eliminating the need for magic strings. Internal bundled terms (like `QuarterTerm`) seamlessly inherit the core `TEMPO_VERSION`. - -### Changed -- **Internal Privacy Modernization**: Refactored internal runtime registries (including `_termMap` and other internal configuration variables) to utilize strict ECMAScript private fields (`#`), ensuring complete architectural security against prototype tampering. - -## [3.6.1] - 2026-07-06 - -### Fixed -- **Constructor Shorthand Resolution**: Fixed a critical `TypeError: invalid duration-like` regression introduced in 3.6.0. The `Tempo` constructor now correctly maps shorthand duration objects (e.g., `{ mi: 5 }`) to their fully-qualified `Temporal` plural equivalents (`{ minutes: 5 }`) before evaluating them against the underlying engine. - -## [3.6.0] - 2026-07-05 - -### Added -- **Shorthand Mutation Keys**: Added native support for Tempo's shorthand format tokens (e.g., `mi`, `ss`, `yy`, `ww`) across both `.add()` and `.set()` mutations, streamlining developer experience and aligning TypeScript definitions with the underlying runtime engine. -- **Shorthand Duration Keys**: Expanded shorthand token support directly into the `DurationModule`. You can now seamlessly use shorthand keys for duration instantiation (`Tempo.duration({ mi: 5 })`), comparisons (`t.until(other, 'mi')`), and strict balancing (`t.until(other).balance({ largestUnit: 'mi' })`), bringing total API consistency across the core library. - -### Changed -- **Enum Performance Optimization**: Upgraded internal dictionary traversals across the parsing engine to utilize native, memoized `.keys()` methods provided by the `enumify` registry, eliminating the overhead of standard `Object.keys()` iterations. - -### Fixed -- **Prototype Bleed Vulnerability**: Replaced `in` operator checks with secure `.has()` methods across the core engine and duration parser, ensuring strict enum member validation and completely eliminating the risk of arbitrary property or method-based prototype leakage. - -## [3.5.3] - 2026-07-05 - -### Added -- **Flexible Epoch Getters**: Added a static `Tempo.epoch` getter that perfectly mirrors the instance `.epoch` property, enabling direct retrieval of current Unix timestamps (e.g. `Tempo.epoch.ss`). -- **Static Now Modifiers**: Extended the static `Tempo.now(unit)` method to accept optional string units (`'ns'`, `'us'`, `'ms'`, `'ss'`), defaulting to nanosecond precision for strict backwards compatibility. - -### Changed -- **DRY Refactoring**: Centralized instance and static `epoch` property calculations to a single internal static helper, ensuring absolute uniformity of mathematical transformations across the core library. - -## [3.5.2] - 2026-07-04 - -### Added -- **Minified Global Bundles**: The build pipeline now natively produces highly optimized, minified IIFE bundles (`*.min.js`) for both Tempo Core and all Community Plugins, significantly reducing payload size for developers using CDN `