j-templates 8.0.16 → 8.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SYNTAX_PRIMER.md CHANGED
@@ -1,6 +1,6 @@
1
- # j-templates Syntax Primer — v4
1
+ # j-templates Syntax Primer — v5
2
2
 
3
- Complete reference for the **j-templates** framework syntax. This documents **j-templates v7.0.98** (see `package.json`). For pattern-oriented guides, see `docs/patterns/`; for step-by-step tutorials, see `docs/tutorials/`.
3
+ Complete reference for the **j-templates** framework syntax. This documents **j-templates v8.0.16** (see `package.json`). For pattern-oriented guides, see `docs/patterns/`; for step-by-step tutorials, see `docs/tutorials/`.
4
4
 
5
5
  > **Core concepts:** Components define UI via `Template()`. State decorators (`@Value`, `@State`, `@Computed`) enable reactivity. DOM functions (`div()`, `button()`) create virtual nodes. No compile step, minimal dependencies.
6
6
 
@@ -1339,42 +1339,59 @@ class Child extends Component {
1339
1339
 
1340
1340
  Both stores share the same API and flattening model, but differ fundamentally in where and how diffing occurs.
1341
1341
 
1342
- **StoreSync** computes diffs synchronously on the main thread. Writes are immediate and consistent — a value written is readable in the same tick. It has no worker overhead, no serialisation constraints, and no `Destroy()` requirement. Use StoreSync for the vast majority of application state: user data, UI state, app config, form data, and any dataset where diff computation is not a bottleneck. `@Computed` uses `StoreSync` internally.
1342
+ **StoreSync** computes diffs synchronously on the main thread. Writes are immediate and consistent — a value written is readable in the same tick. It has no worker overhead and no `Destroy()` requirement. Use StoreSync for the vast majority of application state: user data, UI state, app config, form data, and any dataset where diff computation is not a bottleneck. `@Computed` uses `StoreSync` internally.
1343
1343
 
1344
- **StoreAsync** offloads all diff computation to a dedicated Web Worker via a serialised message queue, computing minimal diffs off the main thread so large dataset operations don't block rendering or input. Use StoreAsync when diffing genuinely large or deeply nested datasets — message feeds, large tables, real-time data. `@ComputedAsync` uses `StoreAsync` internally.
1344
+ **StoreAsync** runs diff computation on a `Worker` you supply, off the main thread, so large dataset operations don't block rendering or input. Use StoreAsync when diffing genuinely large or deeply nested datasets — message feeds, large tables, real-time data. `@ComputedAsync` uses `StoreAsync` internally, with a shared prebuilt worker.
1345
1345
 
1346
1346
  **If you are unsure which to use, start with StoreSync.** StoreAsync introduces meaningful constraints (see below) that are only worth accepting when the dataset size justifies off-thread diffing.
1347
1347
 
1348
1348
  | Aspect | StoreSync | StoreAsync |
1349
1349
  |--------|-----------|------------|
1350
- | Diff execution | Main thread, synchronous | Web Worker, asynchronous |
1350
+ | Diff execution | Main thread, synchronous | Caller-supplied `Worker`, asynchronous |
1351
1351
  | Write consistency | Immediate — readable same tick | Eventual — must `await` before reading |
1352
- | `keyFunc` constraint | None — can close over outer scope | **Must be self-contained** — serialised and executed in worker |
1353
- | Data constraint | Any JS value | **JSON-serialisable only** — no class instances, methods, `Date`, `Map`, `Set`, circular refs |
1354
- | `Destroy()` required | No | Yes — terminates worker and queue |
1352
+ | Construction | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(diffWorker, keyFunc?)` — the worker is built separately (see below) |
1353
+ | `keyFunc`/projections used for diffing | Passed straight to the constructor, can close over outer scope | Must be baked into the worker's own entry file — the constructor's `keyFunc` only resolves aliases locally, on the main thread |
1354
+ | Data constraint | Any JS value | **JSON-serialisable only** — no class instances, methods, `Date`, `Map`, `Set`, circular refs (carried over `postMessage`'s structured clone) |
1355
+ | `Destroy()` required | No | Yes — terminates the worker and queue |
1355
1356
  | Best for | Most app state | Large / real-time datasets |
1356
1357
  | Decorator backend | `@Computed` | `@ComputedAsync` |
1357
1358
 
1358
- ### StoreAsync Constraints
1359
+ ### StoreAsync: Supplying the Worker
1359
1360
 
1360
- StoreAsync's worker is bootstrapped by serialising `keyFunc` and the diff engine and executing them in a worker context. This has two hard constraints:
1361
+ `StoreAsync` doesn't build or serialize a worker itself anymore — you construct a real `Worker` (a bundler-built module, not a Blob) and hand it in. Internally, a worker becomes usable by connecting a `DiffTree` to it with `ConnectWorkerToDiffTree`; that's what a worker *entry file* does.
1361
1362
 
1362
- **1. `keyFunc` must be self-contained — no closed-over variables.**
1363
+ **Most portable option — write your own tiny entry file.** This always works regardless of bundler, because it's a relative import inside your own source tree, exactly like any other bundler-built worker:
1363
1364
 
1364
- The function is serialised and evaluated in the worker context. Any variable from the outer scope will be undefined inside the worker.
1365
+ ```typescript
1366
+ // diff-worker.ts — a separate entry file; your bundler compiles it as its own chunk
1367
+ import { DiffTree } from "j-templates/Store/Diff/diffTree";
1368
+ import { ConnectWorkerToDiffTree } from "j-templates/Store/Diff/diffTreeWorker";
1369
+
1370
+ const diffTree = new DiffTree(/* keyFunc, projections — see below */);
1371
+ ConnectWorkerToDiffTree(diffTree, self as any as Worker);
1372
+ ```
1365
1373
 
1366
1374
  ```typescript
1367
- // ❌ Breaks at runtime — prefix is not accessible in the worker
1368
- const prefix = "user";
1369
- const store = new StoreAsync((val) => val?.id ? `${prefix}_${val.id}` : undefined);
1375
+ // wherever you construct the store
1376
+ import { StoreAsync } from "j-templates/Store";
1377
+
1378
+ const worker = new Worker(new URL("./diff-worker.ts", import.meta.url), { type: "module" });
1379
+ const store = new StoreAsync(worker, keyFunc); // pass the same keyFunc for local alias resolution
1380
+ ```
1370
1381
 
1371
- // ✅ Self-contained — no outer scope references
1372
- const store = new StoreAsync((val) => val?.id ? `user_${val.id}` : undefined);
1382
+ **No custom `keyFunc`/projections needed?** The package ships a ready no-argument entry file, `defaultDiffTreeWorker.js`, which is what `@ComputedAsync` builds internally (see `src/Utils/decorators.ts`) via a relative import from inside the package itself:
1383
+
1384
+ ```typescript
1385
+ const worker = new Worker(
1386
+ new URL("../Store/Diff/defaultDiffTreeWorker.js", import.meta.url),
1387
+ { type: "module" },
1388
+ );
1389
+ const store = new StoreAsync(worker);
1373
1390
  ```
1374
1391
 
1375
- **2. All data must be JSON-serialisable.**
1392
+ That relative path only resolves correctly from *inside* the installed package (it's relative to the file doing the importing), so from your own app code, writing your own entry file (above) is the reliable choice.
1376
1393
 
1377
- The worker communicates via `postMessage`, which uses the structured clone algorithm. Class instances with methods, `Date` objects, `Map`, `Set`, `undefined` values, and circular references will be lost or throw.
1394
+ Because the worker module runs in its own realm, its `keyFunc`/projections can only close over things defined in that file — there's no shared runtime state with the main thread, and nothing is serialised with `.toString()`/`eval` (unlike older versions of this library). Data crossing the worker boundary, via `postMessage`, still goes through the structured clone algorithm, so it must be JSON-serialisable:
1378
1395
 
1379
1396
  ```typescript
1380
1397
  // ❌ Methods and class instances are stripped by structured clone
@@ -1392,6 +1409,7 @@ class Store {
1392
1409
  constructor(keyFunc?: (value: any) => string | undefined);
1393
1410
  Get<O>(id: string): O | undefined; // Returns undefined if not found
1394
1411
  Get<O>(id: string, defaultValue: O): O; // Creates and returns default if not found
1412
+ Has(id: string): boolean; // Whether a root object exists for id
1395
1413
  }
1396
1414
  ```
1397
1415
 
@@ -1399,6 +1417,7 @@ class Store {
1399
1417
 
1400
1418
  ```typescript
1401
1419
  class StoreSync extends Store {
1420
+ constructor(keyFunc?: (value: any) => string | undefined, projections?: DiffTreeProjectionMap);
1402
1421
  Write(data: unknown, key?: string): void;
1403
1422
  Patch(key: string, patch: unknown): void; // Deep merge; throws if key not found
1404
1423
  Push(key: string, ...data: unknown[]): void;
@@ -1411,14 +1430,39 @@ class StoreSync extends Store {
1411
1430
 
1412
1431
  ```typescript
1413
1432
  class StoreAsync extends Store {
1433
+ constructor(diffWorker: Worker, keyFunc?: (value: any) => string | undefined);
1414
1434
  async Write(data: unknown, key?: string): Promise<void>;
1415
1435
  async Patch(key: string, patch: unknown): Promise<void>; // Deep merge; throws if key not found
1416
1436
  async Push(key: string, ...data: unknown[]): Promise<void>;
1417
1437
  async Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<unknown[]>;
1418
- Destroy(): void; // Always call when service is destroyed — terminates worker
1438
+ Destroy(): void; // Always call when service is destroyed — terminates the worker
1419
1439
  }
1420
1440
  ```
1421
1441
 
1442
+ ### Projections — Derived Values Computed Inside the Store
1443
+
1444
+ A projection is a derived value the store computes for you and stores alongside your written data, readable through `Get` like anything else. Declare a map of them at construction (`StoreSync`'s second argument; for `StoreAsync`, baked into the worker's `DiffTree` as shown above):
1445
+
1446
+ ```typescript
1447
+ import { StoreSync } from "j-templates/Store";
1448
+ import { PROJECTION_PREFIX } from "j-templates/Store/Diff/diffTree";
1449
+
1450
+ const store = new StoreSync((value: any) => value?.id, {
1451
+ activeCount: {
1452
+ reads: ["todos"],
1453
+ projection: (keys: string[], todos: Todo[]) => todos.filter((t) => !t.completed).length,
1454
+ },
1455
+ });
1456
+
1457
+ store.Write(todos, "todos");
1458
+ store.Get<number>(`${PROJECTION_PREFIX}activeCount`); // "$projection_activeCount"
1459
+ ```
1460
+
1461
+ - `reads` is a list of dot-separated paths: a plain tree path (`"todos"`), a keyed entity's id (whatever `keyFunc` returns for it), or another projection's output via `` `${PROJECTION_PREFIX}otherId` `` (i.e. `"$projection_otherId"`) — chaining. The dependency graph is topologically sorted at construction, so a cycle (or a read of an unregistered id) throws immediately rather than on first write.
1462
+ - `projection` is called as `(keys, ...values)` — every resolved read path, then each value positionally in the same order as `reads`.
1463
+ - The final `reads` entry may instead be a wildcard root prefix (e.g. `"task_*"`, no dot nesting) — every root key with that prefix is read and appended as its own trailing key/value pair, so `projection` receives one extra `(key, value)` pair per match. Only the last read may be a wildcard, and it can't target a projection's `$projection_<id>` key.
1464
+ - A projection only runs once every one of its `reads` resolves — a path never written, an entity never seen, or a projection that hasn't produced a value yet blocks it (and blocks anything chaining off it). It re-runs whenever a read's resolved value changes, and its result is diffed and applied in place like any other write, so only the fields that actually changed reach reactive readers.
1465
+
1422
1466
  ### keyFunc and Automatic Flattening
1423
1467
 
1424
1468
  `keyFunc` teaches the store how to extract an ID from any object. On `Write` and `Push`, the store eagerly recurses the entire object tree, calling `keyFunc` on each node. Any node that returns a valid ID is registered as an independently addressable entry in the flat internal map. The original shape is preserved because stored objects are `ObservableNode` proxies — nested objects are virtual getters into the flat map rather than copies, which is what allows `Get` to return the original structure while `Patch` operates on individual entries.
@@ -1462,7 +1506,7 @@ If an object has no `id` property (or `keyFunc` returns `undefined`), it is stor
1462
1506
  | Splice | sync | async (Promise) |
1463
1507
  | Get | sync | sync |
1464
1508
  | Destroy | N/A | `Destroy(): void` — required |
1465
- | keyFunc constraint | None | Self-contained, no closed-over vars |
1509
+ | Construction | `keyFunc`/`projections` args | `diffWorker` (required) + local `keyFunc` |
1466
1510
  | Data constraint | Any JS value | JSON-serialisable only |
1467
1511
 
1468
1512
  ---
@@ -1499,9 +1543,12 @@ scope whose value doesn't derive from reactive state, use `Basic` instead, which
1499
1543
  ### Service Patterns
1500
1544
 
1501
1545
  ```typescript
1502
- // Derived state in services
1546
+ // Derived state in services — worker built once, e.g. in a module-level helper
1547
+ // (see "StoreAsync: Supplying the Worker" above for how diff-worker.ts is written)
1548
+ const worker = new Worker(new URL("./diff-worker.ts", import.meta.url), { type: "module" });
1549
+
1503
1550
  class DataService implements IDestroyable {
1504
- private store = new StoreAsync((value) => value.id);
1551
+ private store = new StoreAsync(worker, (value) => value.id);
1505
1552
  private derived = ObservableScope.Create(() => {
1506
1553
  const items = this.store.Get<Item[]>("items", []);
1507
1554
  return ObservableNode.Unwrap(items).filter(i => i.active);
@@ -1531,6 +1578,7 @@ namespace ObservableNode {
1531
1578
  Create<T>(value: T): T; // Wrap in reactive proxy
1532
1579
  Unwrap<T>(value: T): T; // Get raw value from proxy
1533
1580
  Clone<T>(value: T): T; // Strip proxies into plain data (mutates plain objects in place)
1581
+ Snapshot<T>(proxy: T): T | undefined; // Immutable cached plain-value snapshot of a node
1534
1582
  Update(value: unknown, prop?: string | number): void; // Manually trigger change on a node/property
1535
1583
  Apply(rootNode: any, value: any): void; // Merge a full value in-place, preserving identity (public)
1536
1584
  ApplyDiff(rootNode: any, diffResult: JsonDiffResult): void; // Apply diff in-place (internal — used by Store/@Computed)
@@ -1540,6 +1588,8 @@ namespace ObservableNode {
1540
1588
 
1541
1589
  **`Apply` vs `ApplyDiff`:** `Apply(rootNode, value)` merges a full replacement value into an observable node in-place, preserving the node's reference (so `===` checks and DOM reuse stay stable). Use this when you want to update an observable node in place — assigning a property directly on an observable node does **not** generate a diff. `ApplyDiff` is internal-only (used by `StoreSync`/`StoreAsync`/`@Computed`) — ignore it.
1542
1590
 
1591
+ **`Snapshot` and `toJSON`.** `ObservableNode.Snapshot(proxy)` returns an immutable plain copy of a node's current value (arrays/objects only; keyed children are read through their root). The same snapshot is returned on repeated calls until a write invalidates it, and unchanged nested parts are shared between snapshots, so `===` holds for anything that hasn't changed — useful for cheap change detection or handing data to non-reactive code. Calling `.toJSON()` on an observable node (e.g. via `JSON.stringify`) returns this same cached snapshot.
1592
+
1543
1593
  **Array operations on ObservableNode proxies:** `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` — all trigger reactive updates.
1544
1594
 
1545
1595
  ---
@@ -1568,7 +1618,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1568
1618
  10. **`@Computed` only tracks what the getter touches.** Returning `this.tasks` without reading item properties won't re-trigger on per-item mutations. Touch every property you track.
1569
1619
  11. **`scope()`/`gate()`/`peek()`/`mapped()` throw outside a watch context.** They must be called inside a children function, `data:`/`props:`/`attrs:` function, `@Scope`/`@Computed`/`@ComputedAsync` getter, or `@Watch` callback — not inside `on:` event handlers.
1570
1620
  12. **`Injector` is not publicly exported.** Use `@Inject` and `this.Injector` on components.
1571
- 13. **StoreAsync data must be JSON-serialisable** and `keyFunc` must be self-contained (no closed-over variables). Always `await` StoreAsync writes before reading.
1621
+ 13. **StoreAsync data must be JSON-serialisable** (carried over `postMessage`'s structured clone), and `StoreAsync` now takes an already-running `Worker` as its first constructor argument — any `keyFunc`/projections needed for diffing must be baked into that worker's own entry file, not passed to `StoreAsync` directly. Always `await` StoreAsync writes before reading.
1572
1622
  14. **Two-way binding needs reactive props** (`props: () => ({ value })`). A static `props: { value }` object causes input focus loss.
1573
1623
  15. **The Destructuring Trap.** Reading a scope or `this.Data` at the top of `Template()` subscribes the whole component — the React instinct to hoist state reads is backwards here. Read scopes inside children functions or `data:` bindings for fine-grained updates. See Mental Model.
1574
1624
  16. **`scope()`/`gate()`/`peek()` ID collisions are per-scope.** Multiple calls to the same helper in one watch context without IDs silently resolve to the first scope. Provide distinct IDs when calling the same helper more than once in a single ObservableScope definition.
@@ -1645,12 +1695,13 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1645
1695
  | **ObservableNode** | A reactive proxy wrapper around objects/arrays that tracks property-level mutations |
1646
1696
  | **Injector** | Scoped dependency injection container with parent-chain resolution (not publicly exported) |
1647
1697
  | **keyFunc** | A function passed to Store that extracts an ID from objects for automatic flattening |
1698
+ | **Projection** | A derived value declared at Store construction (`reads` + a computing function), stored and read like any other data at `$projection_<id>` |
1648
1699
 
1649
1700
  ---
1650
1701
 
1651
1702
  ## References
1652
1703
 
1653
- - **Source of truth:** `src/` (this primer documents `j-templates` v7.0.98).
1704
+ - **Source of truth:** `src/` (this primer documents `j-templates` v8.0.16).
1654
1705
  - **Pattern guides:** `docs/patterns/01-components.md`, `docs/patterns/02-reactivity.md`, `docs/patterns/03-templates-and-data.md`, `docs/patterns/04-dependency-injection.md`.
1655
1706
  - **Tutorials:** `docs/tutorials/` (01-getting-started through 08-building-complete-app).
1656
1707
  - **Worked example:** `examples/smart-tasks/src/` (the Smart Tasks app used above).
@@ -16,11 +16,10 @@ type IDiffTreeAsync = {
16
16
  export declare class DiffAsync implements IDiffTreeAsync {
17
17
  private workerQueue;
18
18
  /**
19
- * Creates a DiffAsync instance and initializes the worker.
20
- * @param keyFunc - Optional function to extract a key from objects
21
- * @param projections - Optional map of derived-value projections, keyed by id. Each
22
- * projection's function is serialized with `.toString()` and `eval`'d back into a
23
- * function in the worker, the same mechanic used for `keyFunc`.
19
+ * Creates a DiffAsync instance around an already-running worker.
20
+ * @param diffWorker - A `Worker` running a `DiffTree` connected via
21
+ * `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry, or a custom
22
+ * entry file for a non-default `keyFunc`/`projections` baked into the worker thread).
24
23
  */
25
24
  constructor(diffWorker: Worker);
26
25
  /**
@@ -8,11 +8,10 @@ import { WorkerQueue } from "./workerQueue";
8
8
  */
9
9
  export class DiffAsync {
10
10
  /**
11
- * Creates a DiffAsync instance and initializes the worker.
12
- * @param keyFunc - Optional function to extract a key from objects
13
- * @param projections - Optional map of derived-value projections, keyed by id. Each
14
- * projection's function is serialized with `.toString()` and `eval`'d back into a
15
- * function in the worker, the same mechanic used for `keyFunc`.
11
+ * Creates a DiffAsync instance around an already-running worker.
12
+ * @param diffWorker - A `Worker` running a `DiffTree` connected via
13
+ * `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry, or a custom
14
+ * entry file for a non-default `keyFunc`/`projections` baked into the worker thread).
16
15
  */
17
16
  constructor(diffWorker) {
18
17
  this.workerQueue = new WorkerQueue(diffWorker);
@@ -35,9 +35,11 @@ export interface IDiffTreeConstructor {
35
35
  }, projections?: DiffTreeProjectionMap): IDiffTree;
36
36
  }
37
37
  /**
38
- * A pure function computing a derived value from the current values at `reads`, passed
39
- * positionally in the same order, so each parameter can be typed at the call site instead of
40
- * cast out of a generic read.
38
+ * A pure function computing a derived value from the current values at `reads`. Called with
39
+ * the resolved read paths (`keys`) first, then each value positionally in the same order as
40
+ * `reads`, so each value parameter can be typed at the call site instead of cast out of a
41
+ * generic read. A trailing wildcard read expands to one key/value pair per matched root key,
42
+ * so `keys` lets the function tell which root each trailing value came from.
41
43
  */
42
44
  export type DiffTreeProjectionFunc = (keys: string[], ...values: any[]) => any;
43
45
  /**
@@ -46,6 +48,11 @@ export type DiffTreeProjectionFunc = (keys: string[], ...values: any[]) => any;
46
48
  * function computing its result from their current values, positionally. Declaring `reads`
47
49
  * up front lets the tree topologically sort projections and reject a dependency cycle at
48
50
  * construction instead of on first write.
51
+ *
52
+ * The final entry in `reads` may instead be a wildcard root key (e.g. `"user_*"`, no dot
53
+ * nesting) matching every root key with that prefix; each match is read and passed to the
54
+ * projection as its own trailing key/value pair. A wildcard read can't be chained off a
55
+ * projection's `$projection_<id>` key and can't appear anywhere but last.
49
56
  */
50
57
  export type DiffTreeProjectionDefinition = {
51
58
  reads: string[];
@@ -67,9 +74,11 @@ export type DiffSpliceResult<T = unknown> = {
67
74
  diffResult: JsonDiffResult;
68
75
  };
69
76
  /**
70
- * Internal diff tree implementation.
71
- * Maintains root state and computes diffs for path/value updates.
72
- * @private
77
+ * Diff tree implementation. Maintains root state and computes diffs for path/value updates.
78
+ * Used directly by `DiffSync`, and is the building block for a `DiffAsync` worker entry
79
+ * file: construct one with the desired `keyFunc`/`projections` and connect it to the worker
80
+ * thread with `ConnectWorkerToDiffTree` (see `defaultDiffTreeWorker.ts` for the no-argument
81
+ * case, or a custom entry file when either is needed).
73
82
  */
74
83
  export declare class DiffTree {
75
84
  private keyFunc?;
@@ -146,6 +146,9 @@ function ChildSnapshot(source, keyFunc, parent, child) {
146
146
  * sequentially: a read of another projection's `$projection_<id>` output is a dependency
147
147
  * edge, so that projection is sorted earlier. Runs once, at construction, so a cycle (or a
148
148
  * read of an id that isn't registered) is rejected immediately instead of on first write.
149
+ * Also validates each projection's wildcard reads here, at construction: a wildcard must be
150
+ * the final read, must not have dot nesting, and can't target a projection's
151
+ * `$projection_<id>` key.
149
152
  */
150
153
  function BuildProjectionOrder(projections) {
151
154
  const order = [];
@@ -199,6 +202,11 @@ function BuildProjectionOrder(projections) {
199
202
  * makes a blocked dependency propagate for free: a projection that never runs never writes
200
203
  * `$projection_<id>`, so anything chaining off it stays blocked in turn.
201
204
  *
205
+ * A trailing wildcard read is expanded against the current root keys before the projection
206
+ * runs: every root key matching the prefix is read and appended as its own key/value pair,
207
+ * so the projection is called with `(keys, ...values)` where `keys` is every resolved read
208
+ * path (fixed reads first, then each matched wildcard key) positional with `values`.
209
+ *
202
210
  * @param source - The root tree object
203
211
  * @param keyFunc - Optional function to extract a key from objects, for reading through roots
204
212
  * @param projections - Dependency-sorted projection entries, from `BuildProjectionOrder`
@@ -336,9 +344,11 @@ function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc)
336
344
  return spliceResult;
337
345
  }
338
346
  /**
339
- * Internal diff tree implementation.
340
- * Maintains root state and computes diffs for path/value updates.
341
- * @private
347
+ * Diff tree implementation. Maintains root state and computes diffs for path/value updates.
348
+ * Used directly by `DiffSync`, and is the building block for a `DiffAsync` worker entry
349
+ * file: construct one with the desired `keyFunc`/`projections` and connect it to the worker
350
+ * thread with `ConnectWorkerToDiffTree` (see `defaultDiffTreeWorker.ts` for the no-argument
351
+ * case, or a custom entry file when either is needed).
342
352
  */
343
353
  export class DiffTree {
344
354
  /**
@@ -78,7 +78,7 @@ export class Store {
78
78
  if (results.length > 1 || results[0].path.length > 1)
79
79
  throw `Unable to initialize root path ${rootPath} with ${results.length} results and initial path ${results[0].path}`;
80
80
  const rootData = this.cloneData ? JsonDeepClone(results[0].value) : results[0].value;
81
- const newRootObject = this.createNode({ [rootPath]: rootData });
81
+ const newRootObject = this.createNode({ [rootPath]: rootData }, false);
82
82
  this.rootMap.set(rootPath, newRootObject);
83
83
  return;
84
84
  }
@@ -9,8 +9,9 @@ import { Store } from "./store";
9
9
  * without blocking the main thread.
10
10
  *
11
11
  * @example
12
- * // Creating a StoreAsync instance
13
- * const store = new StoreAsync();
12
+ * // Creating a StoreAsync instance from a bundler-built worker entry file
13
+ * const worker = new Worker(new URL("./Store/Diff/defaultDiffTreeWorker.js", import.meta.url), { type: "module" });
14
+ * const store = new StoreAsync(worker);
14
15
  *
15
16
  * // Writing data to the store asynchronously
16
17
  * await store.Write({ name: "John", age: 30 }, "user");
@@ -44,18 +45,21 @@ export declare class StoreAsync extends Store {
44
45
  private queue;
45
46
  /**
46
47
  * Creates an instance of StoreAsync.
47
- * @param keyFunc Optional function to generate a key for a given data value.
48
- * @param projections Optional map of derived-value projections, keyed by id. A
49
- * projection's result is readable through `Get` at `$projection_<id>`. Each projection's
50
- * function is serialized with `.toString()` and `eval`'d back into a function in the
51
- * worker, the same mechanic used for `keyFunc`, so it must not close over state that
52
- * doesn't survive that round trip.
48
+ * @param diffWorker A `Worker` running a `DiffTree` connected via
49
+ * `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry). Any
50
+ * `keyFunc`/projections the diff needs must be baked into that worker's own entry file,
51
+ * since the worker now runs standard bundled code rather than a serialized function.
52
+ * @param keyFunc Optional function to generate a key for a given data value, used locally
53
+ * by this store to resolve root objects. Pass the same key logic the worker's `DiffTree`
54
+ * was constructed with, if any.
53
55
  */
54
56
  constructor(diffWorker: Worker, keyFunc?: (value: any) => string | undefined);
55
57
  /**
56
58
  * Returns a promise that resolve once all current queued writes have completed
57
59
  */
58
60
  Flush(): Promise<void>;
61
+ private nextWritePromise;
62
+ private nextWriteBatch;
59
63
  /**
60
64
  * Writes data to the store asynchronously.
61
65
  * This method ensures that write operations are queued and executed in a non-blocking manner.
@@ -12,8 +12,9 @@ import { Store } from "./store";
12
12
  * without blocking the main thread.
13
13
  *
14
14
  * @example
15
- * // Creating a StoreAsync instance
16
- * const store = new StoreAsync();
15
+ * // Creating a StoreAsync instance from a bundler-built worker entry file
16
+ * const worker = new Worker(new URL("./Store/Diff/defaultDiffTreeWorker.js", import.meta.url), { type: "module" });
17
+ * const store = new StoreAsync(worker);
17
18
  *
18
19
  * // Writing data to the store asynchronously
19
20
  * await store.Write({ name: "John", age: 30 }, "user");
@@ -37,12 +38,13 @@ import { Store } from "./store";
37
38
  export class StoreAsync extends Store {
38
39
  /**
39
40
  * Creates an instance of StoreAsync.
40
- * @param keyFunc Optional function to generate a key for a given data value.
41
- * @param projections Optional map of derived-value projections, keyed by id. A
42
- * projection's result is readable through `Get` at `$projection_<id>`. Each projection's
43
- * function is serialized with `.toString()` and `eval`'d back into a function in the
44
- * worker, the same mechanic used for `keyFunc`, so it must not close over state that
45
- * doesn't survive that round trip.
41
+ * @param diffWorker A `Worker` running a `DiffTree` connected via
42
+ * `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry). Any
43
+ * `keyFunc`/projections the diff needs must be baked into that worker's own entry file,
44
+ * since the worker now runs standard bundled code rather than a serialized function.
45
+ * @param keyFunc Optional function to generate a key for a given data value, used locally
46
+ * by this store to resolve root objects. Pass the same key logic the worker's `DiffTree`
47
+ * was constructed with, if any.
46
48
  */
47
49
  constructor(diffWorker, keyFunc) {
48
50
  super(keyFunc, false);
@@ -51,6 +53,8 @@ export class StoreAsync extends Store {
51
53
  * @private
52
54
  */
53
55
  this.queue = new AsyncQueue();
56
+ this.nextWritePromise = null;
57
+ this.nextWriteBatch = null;
54
58
  this.diff = new DiffAsync(diffWorker);
55
59
  }
56
60
  /**
@@ -67,13 +71,21 @@ export class StoreAsync extends Store {
67
71
  * @throws Will throw an error if no key is provided for the data.
68
72
  */
69
73
  async Write(data, key) {
70
- await this.queue.Next(async () => {
71
- key = key || this.keyFunc?.(data);
72
- if (!key)
73
- throw "No key provided for data";
74
- const diffResult = await this.diff.DiffPath(key, data);
75
- this.UpdateRootMap(diffResult);
74
+ const nextWriteBatch = this.nextWriteBatch ??= [];
75
+ const init = nextWriteBatch.length === 0;
76
+ nextWriteBatch.push({
77
+ path: key || this.keyFunc?.(data),
78
+ value: data
76
79
  });
80
+ if (init) {
81
+ this.nextWritePromise = this.queue.Next(async () => {
82
+ await Promise.resolve();
83
+ this.nextWriteBatch = null;
84
+ const diffResult = await this.diff.DiffBatch(nextWriteBatch);
85
+ this.UpdateRootMap(diffResult);
86
+ });
87
+ }
88
+ return this.nextWritePromise;
77
89
  }
78
90
  /**
79
91
  * Patches an existing value in the store with new data asynchronously.
@@ -38,7 +38,15 @@ export declare namespace ObservableNode {
38
38
  * @param value The plain value (object, array, or primitive) to make observable.
39
39
  * @returns A proxied version of the value that emits change events.
40
40
  */
41
- function Create<T>(value: T): T;
41
+ function Create<T>(value: T, unwrapValue?: boolean): T;
42
+ /**
43
+ * Returns an immutable, cached snapshot of an observable node's current value (plain
44
+ * objects/arrays, with keyed children read through their root). The same snapshot is
45
+ * returned until a write invalidates it, and unchanged nested parts are shared between
46
+ * snapshots, so `===` holds for any part that hasn't changed.
47
+ * @param proxy The observable node to snapshot.
48
+ * @returns The snapshot, or `undefined` if `proxy` is not an observable node.
49
+ */
42
50
  function Snapshot<T>(proxy: T): T | undefined;
43
51
  /**
44
52
  * Marks an observable node or its property as changed, triggering reactive updates.
@@ -69,12 +77,24 @@ export declare namespace ObservableNode {
69
77
  * @remarks No-op when `value` deep-equals the node's current value (empty diff).
70
78
  */
71
79
  function Apply(rootNode: any, value: any): void;
80
+ /**
81
+ * Splices an observable node's underlying array in-place, invalidating its snapshot and
82
+ * touching its scope so reactive readers update.
83
+ * @param rootNode The observable array node to splice.
84
+ * @param start Index at which to start changing the array.
85
+ * @param deleteCount Number of elements to remove starting at `start`.
86
+ * @param items Elements to insert at `start`.
87
+ * @param cloneData Whether to deep-clone `items` before inserting them (true by default,
88
+ * so the store isn't left holding a reference the caller can still mutate).
89
+ */
72
90
  function ApplySplice(rootNode: any, start: number, deleteCount: number, items: any[], cloneData?: boolean): void;
73
91
  /**
74
92
  * Applies a JSON diff result to an observable node, efficiently updating only changed properties.
75
93
  * Optimizes nested object updates by computing paths incrementally and touching modified properties.
76
94
  * @param rootNode The observable node to apply the diff to.
77
95
  * @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
96
+ * @param cloneData Whether to deep-clone each diff value before writing it in (true by
97
+ * default), so the store isn't left holding a reference the caller can still mutate.
78
98
  */
79
99
  function ApplyDiff(rootNode: any, diffResult: JsonDiffResult, cloneData?: boolean): void;
80
100
  /**
@@ -84,5 +104,5 @@ export declare namespace ObservableNode {
84
104
  * @param alias Optional function to transform values before making them observable.
85
105
  * @returns A function that creates observable nodes from plain values.
86
106
  */
87
- function CreateFactory(alias?: (value: any, reactive?: boolean) => any | undefined): <T>(value: T) => T;
107
+ function CreateFactory(alias?: (value: any, reactive?: boolean) => any | undefined): <T>(value: T, unwrapValue?: boolean) => T;
88
108
  }
@@ -321,8 +321,9 @@ function CreateProxyFactory(alias) {
321
321
  return value;
322
322
  }
323
323
  }
324
- return function CreateProxy(value) {
325
- value = UnwrapProxy(value);
324
+ return function CreateProxy(value, unwrapValue = true) {
325
+ if (unwrapValue)
326
+ value = UnwrapProxy(value);
326
327
  return CreateProxyFromValue(value);
327
328
  };
328
329
  }
@@ -373,10 +374,18 @@ export var ObservableNode;
373
374
  * @param value The plain value (object, array, or primitive) to make observable.
374
375
  * @returns A proxied version of the value that emits change events.
375
376
  */
376
- function Create(value) {
377
- return DefaultCreateProxy(value);
377
+ function Create(value, unwrapValue = true) {
378
+ return DefaultCreateProxy(value, unwrapValue);
378
379
  }
379
380
  ObservableNode.Create = Create;
381
+ /**
382
+ * Returns an immutable, cached snapshot of an observable node's current value (plain
383
+ * objects/arrays, with keyed children read through their root). The same snapshot is
384
+ * returned until a write invalidates it, and unchanged nested parts are shared between
385
+ * snapshots, so `===` holds for any part that hasn't changed.
386
+ * @param proxy The observable node to snapshot.
387
+ * @returns The snapshot, or `undefined` if `proxy` is not an observable node.
388
+ */
380
389
  function Snapshot(proxy) {
381
390
  if (proxy[IS_NODE])
382
391
  return proxy[NODE_SNAPSHOT];
@@ -453,6 +462,16 @@ export var ObservableNode;
453
462
  ApplyDiff(rootNode, diff);
454
463
  }
455
464
  ObservableNode.Apply = Apply;
465
+ /**
466
+ * Splices an observable node's underlying array in-place, invalidating its snapshot and
467
+ * touching its scope so reactive readers update.
468
+ * @param rootNode The observable array node to splice.
469
+ * @param start Index at which to start changing the array.
470
+ * @param deleteCount Number of elements to remove starting at `start`.
471
+ * @param items Elements to insert at `start`.
472
+ * @param cloneData Whether to deep-clone `items` before inserting them (true by default,
473
+ * so the store isn't left holding a reference the caller can still mutate).
474
+ */
456
475
  function ApplySplice(rootNode, start, deleteCount, items, cloneData = true) {
457
476
  const root = rootNode[NODE_VALUE];
458
477
  InvalidateSnapshotForValue(rootNode[NODE_VALUE]);
@@ -466,6 +485,8 @@ export var ObservableNode;
466
485
  * Optimizes nested object updates by computing paths incrementally and touching modified properties.
467
486
  * @param rootNode The observable node to apply the diff to.
468
487
  * @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
488
+ * @param cloneData Whether to deep-clone each diff value before writing it in (true by
489
+ * default), so the store isn't left holding a reference the caller can still mutate.
469
490
  */
470
491
  function ApplyDiff(rootNode, diffResult, cloneData = true) {
471
492
  const root = rootNode[NODE_VALUE];
@@ -140,7 +140,9 @@ export declare namespace ObservableScope {
140
140
  * Creates a lightweight basic scope that stores a value directly without a proxy.
141
141
  * Used by `@Value` for primitives. Basic scopes do not automatically track dependencies —
142
142
  * after creation, `ObservableScope.Update` must be called for the scope to emit. They also do
143
- * not cache a value internally: the valueFunction is invoked on every read.
143
+ * not cache a value internally: the valueFunction is invoked on every read, with dependency
144
+ * tracking suspended, so any reactive value it reads is never registered as a dependency of
145
+ * whichever scope happened to be executing when the read occurred.
144
146
  * @template T The type of value stored in the scope.
145
147
  * @param valueFunction Function that returns the scope's value.
146
148
  * @returns A new basic observable scope.
@@ -565,7 +565,9 @@ export var ObservableScope;
565
565
  * Creates a lightweight basic scope that stores a value directly without a proxy.
566
566
  * Used by `@Value` for primitives. Basic scopes do not automatically track dependencies —
567
567
  * after creation, `ObservableScope.Update` must be called for the scope to emit. They also do
568
- * not cache a value internally: the valueFunction is invoked on every read.
568
+ * not cache a value internally: the valueFunction is invoked on every read, with dependency
569
+ * tracking suspended, so any reactive value it reads is never registered as a dependency of
570
+ * whichever scope happened to be executing when the read occurred.
569
571
  * @template T The type of value stored in the scope.
570
572
  * @param valueFunction Function that returns the scope's value.
571
573
  * @returns A new basic observable scope.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "8.0.16",
3
+ "version": "8.0.18",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",