# Helpers - Specification ## Overview The Helpers module provides a collection of utility functions used across the SVAR state library. It covers three concerns: unique identifier generation (sequential numeric IDs and temporary string IDs), structural equality comparison (deep recursive comparison of primitives, dates, arrays, and objects), and deep cloning. These are stateless utilities with no configuration and no side effects beyond the shared ID counter. ## Quick Reference | Function | Purpose | Returns | | ------------ | ------------------------------------------------- | --------- | | `uid()` | Generate a unique sequential numeric identifier | `number` | | `tempID()` | Generate a temporary string identifier | `string` | | `isTempID()` | Test whether a value is a temporary identifier | `boolean` | | `isSame()` | Deep structural equality comparison of two values | `boolean` | | `deepCopy()` | Create a deep clone of a value | `T` | ## Public Interface ### Type Definitions ```typescript // Shared with other modules in the library type TID = number | string; ``` ### uid() ```typescript uid(): number ``` **Purpose**: Generate a unique sequential numeric identifier. **Returns**: A monotonically increasing integer, starting from the millisecond timestamp at module load time. **Behavior**: - Uses a module-level counter initialized to `Date.now()` (milliseconds since Unix epoch) - Each call returns the current counter value and increments it - IDs are unique within a single runtime session; not globally unique across processes or page reloads - Shares the same counter as `tempID()` - calling either function advances the shared sequence ### tempID() ```typescript tempID(): string ``` **Purpose**: Generate a temporary string identifier with a recognizable prefix, used to identify entities that have not yet been persisted. **Returns**: A string in the format `"temp://"` followed by the current counter value (e.g., `"temp://1709712000000"`). **Behavior**: - Uses the same module-level counter as `uid()` - The returned string is always 20 characters long (7-character prefix + 13-digit counter value), assuming the counter is within the expected range (timestamps from ~2001 onward) - Each call advances the shared counter ### isTempID() ```typescript isTempID(v: string | number): boolean ``` **Purpose**: Test whether a given identifier was generated by `tempID()`. **Parameters**: - `v` - The identifier to test. Accepts both strings and numbers to match the `TID` type used throughout the library. **Returns**: `true` if `v` matches the temporary ID format; `false` otherwise. **Behavior**: - Returns `false` for any non-string value (numbers always fail) - Checks three conditions: the value must be a string, exactly 20 characters long, and the numeric portion after position 7 must parse to a number greater than 1e12 - Does not validate the `"temp://"` prefix explicitly - relies on length and numeric range as heuristics **Edge Cases**: - `isTempID(12345)` returns `false` (number input) - `isTempID("short")` returns `false` (wrong length) - `isTempID("temp://1709712000000")` returns `true` - A non-temp string that happens to be 20 characters with a valid numeric suffix after position 7 could return a false positive ### isSame() ```typescript isSame(v: any, nv: any): boolean ``` **Purpose**: Deep structural equality comparison of two values. Handles primitives, `null`, `Date`, arrays, and plain objects recursively. **Parameters**: - `v` - First value to compare - `nv` - Second value to compare **Returns**: `true` if `v` and `nv` are structurally equal; `false` otherwise. **Behavior**: - **Primitives** (`number`, `string`, `boolean`, `null`): Uses strict equality (`===`) - **Type mismatch**: Returns `false` if `typeof v !== typeof nv` - **null vs non-null**: Returns `false` if exactly one operand is `null` - **Date instances**: Compares by `getTime()` value. Returns `false` only when both are Date instances and their timestamps differ. Two Dates with the same timestamp return `true` - **Arrays**: Both must be arrays, same length, and each element must be structurally equal (compared recursively, back-to-front) - **Objects**: Both must have the same number of keys. Every key in the second object must exist in the first with a structurally equal value (compared recursively) - **Other types** (`function`, `symbol`, `undefined`): Falls through to strict equality (`===`) **Edge Cases**: - `isSame(null, null)` returns `true` (caught by primitive check since `null` is listed) - `isSame([], [])` returns `true` (same length, no elements to differ) - `isSame({}, {})` returns `true` (same key count, no keys to differ) - `isSame([1, 2], [2, 1])` returns `false` (order matters) - `isSame({a: 1}, {a: 1, b: 2})` returns `false` (different key count) - `isSame(undefined, undefined)` returns `true` (falls through to `===`) - `isSame(NaN, NaN)` returns `false` (`NaN !== NaN`) - Circular references cause a stack overflow (no cycle detection) ### deepCopy() ```typescript deepCopy(obj: T): T ``` **Purpose**: Create a deep clone of a value, preserving type structure for primitives, `Date`, arrays, and plain objects. **Parameters**: - `obj` - The value to clone **Returns**: A new value structurally equal to `obj` with no shared references. **Behavior**: - **Non-objects** (primitives, `undefined`, functions): Returned as-is (no copy needed) - **null**: Returned as-is - **Date instances**: Creates a new `Date` with the same timestamp - **Arrays**: Maps each element through `deepCopy` recursively, producing a new array - **Plain objects**: Creates a new object and recursively deep-copies each enumerable own/inherited property via `for...in` **Edge Cases**: - `deepCopy(42)` returns `42` (primitive passthrough) - `deepCopy(null)` returns `null` - `deepCopy(new Date())` returns a new Date with the same timestamp - Does not handle `Map`, `Set`, `RegExp`, typed arrays, or other built-in object types - these are treated as plain objects and lose their prototype - Circular references cause a stack overflow (no cycle detection) - Copies enumerable inherited properties (uses `for...in` without `hasOwnProperty` check) ## Implementation Details ### ID Counter The module maintains a single shared counter, initialized to `new Date().valueOf()` at module load time. Both `uid()` and `tempID()` draw from and advance this counter. This design ensures: - IDs are unique within a session (monotonically increasing) - IDs are roughly time-ordered (starting from a timestamp) - `tempID` values can be detected by `isTempID` through their numeric range (> 1e12, which corresponds to dates after ~2001) The counter is module-scoped and not resettable. In testing environments, the initial value depends on when the module is loaded. ### Object Comparison Strategy `isSame` uses a two-layer approach: the public `isSame` function handles type dispatch and primitives, while the internal `isSameObject` handles recursive object comparison. The key count check in `isSameObject` provides an early exit for objects with different shapes. Array comparison iterates back-to-front, which has no semantic effect but reflects the implementation choice. ### Deep Copy Constraints - Only handles the types commonly used in the SVAR state library: primitives, `Date`, arrays, and plain objects - Not suitable for cloning class instances, DOM nodes, or values with complex prototypes - `for...in` iteration means inherited enumerable properties are copied, which matches the comparison behavior of `isSame` ## Examples ```javascript import { uid, tempID, isTempID, isSame, deepCopy } from "@svar/state"; // --- ID generation --- const id1 = uid(); // e.g., 1709712000000 const id2 = uid(); // 1709712000001 const id3 = uid(); // 1709712000002 const tmp = tempID(); // "temp://1709712000003" isTempID(tmp); // true isTempID(id1); // false (number, not string) isTempID("regular-id"); // false (wrong length/format) // --- Structural equality --- isSame(1, 1); // true isSame("a", "b"); // false isSame(null, null); // true isSame(null, undefined); // false isSame([1, 2, 3], [1, 2, 3]); // true isSame([1, 2], [1, 2, 3]); // false (different length) isSame({ name: "A", value: 1 }, { name: "A", value: 1 }); // true isSame({ name: "A", items: [1, 2] }, { name: "A", items: [1, 2] }); // true (recursive) const d1 = new Date(2024, 0, 1); const d2 = new Date(2024, 0, 1); isSame(d1, d2); // true (same timestamp) // --- Deep copy --- const original = { id: 1, name: "Task", start: new Date(2024, 0, 1), tags: ["urgent", "review"], meta: { priority: 5 }, }; const copy = deepCopy(original); copy.tags.push("done"); copy.meta.priority = 1; copy.start.setFullYear(2025); original.tags; // ["urgent", "review"] (unchanged) original.meta.priority; // 5 (unchanged) original.start.getFullYear(); // 2024 (unchanged) ```