j-templates 8.0.28 → 8.1.1

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,14 +1,14 @@
1
1
  # j-templates Syntax Primer
2
2
 
3
- Oct 13, 2026 · @Jay Landrum
3
+ Oct 17, 2026 · @Jay Landrum
4
4
 
5
- Reference for building apps with j-templates 8.0.27. Every behavioral claim was checked by running the library in jsdom; claims taken from type declarations or source alone are marked as such.
5
+ Reference for building apps with j-templates 8.1.0. Every behavioral claim was checked by running the library in jsdom; claims taken from type declarations or source alone are marked as such.
6
6
 
7
7
  ## How to use this document
8
8
 
9
9
  Read Mental model, then Templates and State decorators before writing a component. Check the Traps checklist before shipping.
10
10
 
11
- - **Version:** j-templates 8.0.27. The 8.0.19 claims were tested in jsdom with TypeScript (`experimentalDecorators`). 8.0.21 changed `peek`, how `@Computed` notifies readers, and emitter re-entry. 8.0.23 moved `Bound()` after the first render and added `Connected()`. 8.0.24 fixed dotted projection reads through keyed entities. 8.0.27 batches `StoreAsync` changes. Each of those sections was re-verified in jsdom against the version that changed it.
11
+ - **Version:** j-templates 8.1.0. The 8.0.19 claims were tested in jsdom with TypeScript (`experimentalDecorators`). 8.0.21 changed `peek`, how `@Computed` notifies readers, and emitter re-entry. 8.0.23 moved `Bound()` after the first render and added `Connected()`. 8.0.24 fixed dotted projection reads through keyed entities. 8.0.27 batches `StoreAsync` changes. 8.1.0 added `PushStore` and `DiffTreeWriter`, and moved the diff types and `PROJECTION_PREFIX` to `j-templates/Store/Diff`. Each of those sections was re-verified in jsdom against the version that changed it.
12
12
  - **Not verified:** a real browser `Worker` (a structured-clone shim stood in), real focus behavior, and the `docs/`, `examples/` and `src/` folders the package does not ship.
13
13
  - **Convention:** `this.x` in examples is a component field decorated with `@Value` or `@State` unless shown otherwise. Imports are omitted after the first example.
14
14
 
@@ -56,9 +56,10 @@ npm install j-templates
56
56
  | `j-templates` | `Component`, `scope`, `gate`, `peek`, `mapped`; type `vNode` |
57
57
  | `j-templates/DOM` | element functions (below), `text`, `fragment`, `CreateRootPropertyAssignment`, `CreateEventAssignment`, `PropertyAssignment`, `EventAssignment` |
58
58
  | `j-templates/Utils` | `Value`, `State`, `Computed`, `ComputedAsync`, `Scope`, `Watch`, `Inject`, `Destroy`, `Bound`, `Animation`, `AnimationType`, `Injector`; type `IDestroyable` |
59
- | `j-templates/Store` | `Store`, `StoreSync`, `StoreAsync`, `ObservableScope`, `ObservableNode` |
59
+ | `j-templates/Store` | `ReadStore`, `Store`, `StoreSync`, `StoreAsync`, `PushStore`, `ObservableScope`, `ObservableNode` |
60
+ | `j-templates/Store/Diff` | `PROJECTION_PREFIX`, `DIFF_TREE_EVENT`, `DIFF_TREE_ERROR`; types `DiffTreeProjectionMap`, `DiffTreeProjectionDefinition`, `DiffTreeProjectionFunc`, `IDiffPushEvent`, `DiffSpliceResult`, `IDiffTree` and the batch types. Types and constants only, so a worker bundle can import it. |
60
61
 
61
- **Deep imports used below:** `j-templates/Store/Diff/diffTree` (`DiffTree`, `PROJECTION_PREFIX`), `j-templates/Store/Diff/diffTreeWorker` (`ConnectWorkerToDiffTree`), `j-templates/Node/vNode` (`vNode.Destroy`). `j-templates/Store/Diff/defaultDiffTreeWorker` runs worker code on import and throws outside a worker (`self is not defined`).
62
+ **Deep imports used below:** `j-templates/Store/Diff/diffTree` (`DiffTree`), `j-templates/Store/Diff/diffTreeWorker` (`ConnectWorkerToDiffTree`), `j-templates/Store/Diff/diffTreeWriter` (`DiffTreeWriter`), `j-templates/Node/vNode` (`vNode.Destroy`). `j-templates/Store/Diff/defaultDiffTreeWorker` runs worker code on import and throws outside a worker (`self is not defined`).
62
63
 
63
64
  **Element functions** (all verified present):
64
65
 
@@ -532,15 +533,15 @@ class Injector {
532
533
 
533
534
  ## Store
534
535
 
535
- A store holds reactive data under string keys and diffs every write into existing objects in place. Use `StoreSync` unless diffing large data blocks the main thread.
536
+ A store holds reactive data under string keys and diffs every write into existing objects in place. Use `StoreSync` unless diffing large data blocks the main thread. Use `PushStore` when a worker produces the data itself, such as the result of a computation it runs.
536
537
 
537
- | | `StoreSync` | `StoreAsync` |
538
- | --- | --- | --- |
539
- | Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` |
540
- | Diffing | main thread, synchronous | in the `Worker` you supply |
541
- | Writes | `void`; readable immediately | `Promise`; batched per tick, readable only after `await` |
542
- | `Destroy()` | none | required; terminates the worker |
543
- | Backs | nothing (`@Computed` uses `ObservableNode` directly) | `@ComputedAsync` |
538
+ | | `StoreSync` | `StoreAsync` | `PushStore` |
539
+ | --- | --- | --- | --- |
540
+ | Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` | `new PushStore(source, keyFunc?, cloneData?)` |
541
+ | Diffing | main thread, synchronous | in the `Worker` you supply | in the `DiffTreeWriter` that pushes to it |
542
+ | Writes | `void`; readable immediately | `Promise`; batched per tick, readable only after `await` | none; read-only. Changes arrive when the writer calls `Commit()` |
543
+ | `Destroy()` | none | required; terminates the worker | required; stops listening, leaves the worker running |
544
+ | Backs | nothing (`@Computed` uses `ObservableNode` directly) | `@ComputedAsync` | nothing |
544
545
 
545
546
  ### API
546
547
 
@@ -554,7 +555,7 @@ Push(key: string, ...items: unknown[]); // append to the array at key
554
555
  Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]); // returns removed items
555
556
  ```
556
557
 
557
- `StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below).
558
+ `StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below). `PushStore` has only `Get` and `Has`, plus `Error` (see PushStore and DiffTreeWriter).
558
559
 
559
560
  ### keyFunc and flattening
560
561
 
@@ -604,7 +605,7 @@ Data crosses the worker boundary by structured clone. Class instances arrive as
604
605
  A projection is a derived value the store computes and stores under `$projection_<name>`. Pass them as `StoreSync`'s second argument (for `StoreAsync`, to the worker's `DiffTree`).
605
606
 
606
607
  ```typescript
607
- import { PROJECTION_PREFIX } from "j-templates/Store/Diff/diffTree"; // "$projection_"
608
+ import { PROJECTION_PREFIX } from "j-templates/Store/Diff"; // "$projection_"
608
609
 
609
610
  const store = new StoreSync((v: any) => v?.id, {
610
611
  activeCount: { reads: ["todos"], projection: (keys, todos) => todos.filter((t) => !t.done).length },
@@ -620,7 +621,38 @@ store.Get<number>(`${PROJECTION_PREFIX}activeCount`);
620
621
  - A projection has no value until every read resolves. It re-runs when a read's value changes.
621
622
  - A cycle or a read of an undefined projection throws at construction.
622
623
  - A dotted read can pass through a keyed entity (`"doc.owner.name"`, where `owner` has an id). It reads the entity's own root, so a `Patch` to the entity reruns the projection with the new value (verified; before 8.0.24 such a read returned the stale embedded copy).
623
- - `StoreSync` and the `StoreAsync` worker run projections for you. `DiffTree.DiffPath`, `PatchPath`, `PushPath` and `SplicePath` don't (8.0.27): code that drives a `DiffTree` directly, such as a test, must call `tree.RunProjections()` after its writes (verified).
624
+ - `StoreSync` and the `StoreAsync` worker run projections for you, and a `DiffTreeWriter` runs them in `Commit()`. `DiffTree.DiffPath`, `PatchPath`, `PushPath` and `SplicePath` don't (8.0.27): code that drives a `DiffTree` directly, such as a test, must call `tree.RunProjections()` after its writes (verified).
625
+
626
+ ### PushStore and DiffTreeWriter
627
+
628
+ `StoreAsync` only changes when the main thread writes. When a worker produces the data itself, the worker owns the tree instead: it writes into a `DiffTreeWriter` and calls `Commit()`, which runs projections and posts every change since the last commit as one message. A `PushStore` on the main thread applies each message as it arrives (8.1.0).
629
+
630
+ ```typescript
631
+ // engine-worker.ts (bundled as its own worker entry)
632
+ import { DiffTreeWriter } from "j-templates/Store/Diff/diffTreeWriter";
633
+ const tree = new DiffTreeWriter(self, (v: any) => v?.id, projections);
634
+ self.onmessage = async (e) => {
635
+ const results = await compute(e.data); // the worker's own work
636
+ tree.DiffPath("results", results);
637
+ tree.DiffPath("meta", { finishedAt: Date.now() });
638
+ tree.Commit(); // one message: both writes and the projections
639
+ };
640
+
641
+ // main thread
642
+ const worker = new Worker(new URL("./engine-worker.ts", import.meta.url), { type: "module" });
643
+ const store = new PushStore(worker, (v: any) => v?.id); // before the first Commit()
644
+ store.Get("results"); // reactive; undefined until the first commit
645
+ store.Destroy(); // stops listening; terminate the worker yourself
646
+ ```
647
+
648
+ - The writer is a `DiffTree`: write with `DiffPath`, `PatchPath`, `PushPath` and `SplicePath`. Nothing reaches the store until `Commit()`, and a `Commit()` with no changes posts nothing (verified).
649
+ - Projections run once per `Commit()`, after the batch's writes, and their results arrive in the same message, so readers never see a projection out of step with the writes it came from (verified). Calling `RunProjections()` on a writer throws (from source).
650
+ - Use the same `keyFunc` on both sides. Keyed entities resolve through their own roots, as in any store (verified).
651
+ - `source` is any `EventTarget` that delivers the writer's messages as `"message"` events: a `Worker`, a `MessagePort` (started for you), or a plain `EventTarget` on the same thread. The store ignores messages whose `type` isn't `DIFF_TREE_EVENT` or `DIFF_TREE_ERROR` (verified). It listens with `addEventListener` and never sets `onmessage` (from source). The worker can carry its own requests and replies on the same channel.
652
+ - **Create the store before the first `Commit()`.** Messages are diffs against the writer's tree, so a store created later misses the earlier changes and holds partial roots (from source). There is no catch-up message.
653
+ - **A failed change doesn't stop either side.** After a write throws, every later write before `Commit()` throws the same error. `Commit()` still sends the writes made before the failure, skips projections, then posts `DIFF_TREE_ERROR`. The store applies the changes and sets `Error` (verified). The next commit brings projections up to date.
654
+ - `store.Error` is reactive: the last commit's error, or `null`. It's cleared when a later commit's changes arrive (verified).
655
+ - `cloneData` (default `false`): a `Worker` or `MessagePort` already delivers a structured-clone copy, so no copy is made. A same-thread `EventTarget` delivers the writer's own objects; post copies, or pass `true`. The flag only covers new roots and splices; changes to an existing root are always copied (from source).
624
656
 
625
657
  ## ObservableScope and ObservableNode
626
658
 
@@ -738,6 +770,23 @@ function fakeWorker(tree = new DiffTree()) {
738
770
  const store = new StoreAsync(fakeWorker(new DiffTree((v: any) => v?.id)), (v: any) => v?.id);
739
771
  ```
740
772
 
773
+ 4. For `PushStore`, use a plain `EventTarget` as the channel. Deliver copies a tick later, as a worker would, and await a timeout before reading:
774
+
775
+ ```typescript
776
+ import { DiffTreeWriter } from "j-templates/Store/Diff/diffTreeWriter";
777
+
778
+ const target = new EventTarget();
779
+ const port = { postMessage: (d: unknown) => setTimeout(() =>
780
+ target.dispatchEvent(new MessageEvent("message", { data: structuredClone(d) }))) };
781
+ const writer = new DiffTreeWriter(port);
782
+ const store = new PushStore(target);
783
+
784
+ writer.DiffPath("a", { value: 1 });
785
+ writer.Commit();
786
+ await new Promise((r) => setTimeout(r));
787
+ store.Get<any>("a").value; // 1
788
+ ```
789
+
741
790
  jsdom has no `innerText`; use `textContent` in tests.
742
791
 
743
792
  ## Worked example: Smart Tasks
@@ -926,6 +975,7 @@ Component.Attach(document.getElementById("app")!, app({}));
926
975
  23) **Equal-value writes still notify.** Assigning the current value again re-runs every reader. Guard repeated writes or read through `gate()`.
927
976
  24) **`JSON.stringify(node)` and `ObservableNode.Snapshot(node)` don't subscribe.** Read properties directly, or `ObservableNode.Clone(node)` to track everything.
928
977
  25) **Don't update a scope from its own emit.** Calling `ObservableScope.Update(s)` from a `Watch(s, …)` callback, or any write that re-emits the emitter currently notifying, throws `Emitter.Emit: re-entry during emit is not supported` (verified). Defer the write (`queueMicrotask`) or restructure.
978
+ 26) **A `PushStore` sees only what's committed after it exists.** Create it before the writer's first `Commit()`, and call `Commit()` after every batch of writes.
929
979
 
930
980
  ## Debugging
931
981
 
@@ -956,6 +1006,11 @@ Component.Attach(document.getElementById("app")!, app({}));
956
1006
  | `Key not found in store` from `StoreAsync.Patch` right after a `Write` | the key is an entity id flattened out of a write that hasn't been applied yet | await the `Write` first |
957
1007
  | Every `StoreAsync` change rejects with the same old error | an earlier change failed in the worker | fix that change; replace the store |
958
1008
  | A test's projection never updates | the test drives a `DiffTree` directly | call `tree.RunProjections()` after each write |
1009
+ | `PushStore` never gets the writer's data | the worker wrote but didn't call `Commit()` | call `Commit()` after each batch |
1010
+ | `PushStore` holds partial or missing roots that the writer has | the store was created after the writer's first `Commit()` | create the store with the worker, before any commit |
1011
+ | Every `DiffTreeWriter` write throws the same old error | an earlier write in the batch failed | call `Commit()`: it sends the applied writes and the error, and the next batch starts clean |
1012
+ | `RunProjections` throws on a `DiffTreeWriter` | projections run in `Commit()` | call `Commit()` instead |
1013
+ | `PROJECTION_PREFIX` is `undefined`, projection keys read `"undefined<id>"` | imported from `j-templates/Store/Diff/diffTree`, its path before 8.1.0 | import it from `j-templates/Store/Diff` |
959
1014
 
960
1015
  ## Corrections from the v5 primer
961
1016
 
@@ -1,5 +1,5 @@
1
1
  import { JsonDiffResult } from "../../Utils/json";
2
- import { DiffSpliceResult, DiffTreeProjectionMap, IDiffTree } from "./diffTree";
2
+ import { DiffSpliceResult, DiffTreeProjectionMap, IDiffTree } from "./diffTree.types";
3
3
  /**
4
4
  * Synchronous diff implementation.
5
5
  * Computes diffs immediately without queuing or workers.
@@ -1,72 +1,5 @@
1
1
  import { type JsonDiffResult } from "../../Utils/json";
2
- /**
3
- * Interface for diff tree operations.
4
- */
5
- export interface IDiffTree {
6
- /**
7
- * Computes the diff between a new value and the current value at a path.
8
- * @param path - Dot-separated path to the value
9
- * @param value - The new value to compare
10
- * @returns Diff results showing changes
11
- */
12
- DiffPath(path: string, value: any): JsonDiffResult;
13
- PatchPath(path: string, patch: any): JsonDiffResult;
14
- SplicePath(path: string, start: number, deleteCount: number | undefined, items: any[]): DiffSpliceResult;
15
- PushPath(path: string, items: any[]): JsonDiffResult;
16
- }
17
- /**
18
- * Constructor interface for IDiffTree.
19
- */
20
- export interface IDiffTreeConstructor {
21
- /**
22
- * Creates a new IDiffTree instance.
23
- * @param keyFunc - Optional function to extract a key from a value
24
- * @param projections - Optional map of derived-value projections, keyed by id
25
- */
26
- new (keyFunc?: {
27
- (val: any): string;
28
- }, projections?: DiffTreeProjectionMap): IDiffTree;
29
- }
30
- /**
31
- * A pure function computing a derived value from the current values at `reads`. Called with
32
- * the resolved read paths (`keys`) first, then each value positionally in the same order as
33
- * `reads`, so each value parameter can be typed at the call site instead of cast out of a
34
- * generic read. A trailing wildcard read expands to one key/value pair per matched root key,
35
- * so `keys` lets the function tell which root each trailing value came from.
36
- */
37
- export type DiffTreeProjectionFunc = (keys: string[], ...values: any[]) => any;
38
- /**
39
- * A projection's declaration: the dot-separated paths it reads (a plain tree path, an
40
- * entity's key, or another projection's `$projection_<id>` key for chaining) and the
41
- * function computing its result from their current values, positionally. Declaring `reads`
42
- * up front lets the tree topologically sort projections and reject a dependency cycle at
43
- * construction instead of on first write.
44
- *
45
- * The final entry in `reads` may instead be a wildcard root key (e.g. `"user_*"`, no dot
46
- * nesting) matching every root key with that prefix; each match is read and passed to the
47
- * projection as its own trailing key/value pair. A wildcard read can't be chained off a
48
- * projection's `$projection_<id>` key and can't appear anywhere but last.
49
- */
50
- export type DiffTreeProjectionDefinition = {
51
- reads: string[];
52
- projection: DiffTreeProjectionFunc;
53
- };
54
- /** A map of projection id to its declaration, keyed by the id used in `$projection_<id>`. */
55
- export type DiffTreeProjectionMap = {
56
- [id: string]: DiffTreeProjectionDefinition;
57
- };
58
- /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
59
- export declare const PROJECTION_PREFIX = "$projection_";
60
- export type DiffSpliceResult<T = unknown> = {
61
- /** Array path segments (strings for object keys, numbers for array indices) */
62
- path: (string | number)[];
63
- /** Required by the splice operation **/
64
- start: number;
65
- deleteCount: number;
66
- items: any[];
67
- spliceResult: any[];
68
- diffResult: JsonDiffResult;
69
- };
2
+ import { DiffSpliceResult, DiffTreeProjectionMap } from "./diffTree.types";
70
3
  /**
71
4
  * Diff tree implementation. Maintains root state and computes diffs for path/value updates.
72
5
  * Used directly by `DiffSync`, and is the building block for a `StoreAsync` worker entry
@@ -98,7 +31,7 @@ export declare class DiffTree {
98
31
  DiffPath(path: string, value: any): JsonDiffResult;
99
32
  PatchPath(path: string, patch: any): JsonDiffResult;
100
33
  PushPath(path: string, items: unknown[]): JsonDiffResult;
101
- SplicePath(path: string, start: number, deleteCount: number, items: any[]): DiffSpliceResult<unknown>;
34
+ SplicePath(path: string, start: number, deleteCount: number, items: any[]): DiffSpliceResult;
102
35
  /**
103
36
  * Retrieves the current value at a path.
104
37
  * @param path - Dot-separated path to the value
@@ -1,7 +1,6 @@
1
1
  import { ArraysEqual } from "../../Utils/array";
2
2
  import { JsonDeepClone, JsonDiff, JsonMerge, JsonType } from "../../Utils/json";
3
- /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
4
- export const PROJECTION_PREFIX = "$projection_";
3
+ import { PROJECTION_PREFIX } from "./diffTree.types";
5
4
  /**
6
5
  * Flattens nested objects/arrays, extracting keyed objects to root.
7
6
  * @param root - Root object to store flattened values
@@ -0,0 +1,105 @@
1
+ import type { JsonDiffResult } from "../../Utils/json";
2
+ /**
3
+ * `type` of the message a `DiffTreeWriter` posts on `Commit()`: the batch's diff results,
4
+ * which a `PushStore` applies.
5
+ */
6
+ export declare const DIFF_TREE_EVENT = "DIFF_TREE_EVENT";
7
+ /**
8
+ * `type` of the message a `DiffTreeWriter` posts on `Commit()` when a change in the batch
9
+ * failed, after the `DIFF_TREE_EVENT` carrying the changes that were applied.
10
+ */
11
+ export declare const DIFF_TREE_ERROR = "DIFF_TREE_ERROR";
12
+ /**
13
+ * Interface for diff tree operations.
14
+ */
15
+ export interface IDiffTree {
16
+ /**
17
+ * Computes the diff between a new value and the current value at a path.
18
+ * @param path - Dot-separated path to the value
19
+ * @param value - The new value to compare
20
+ * @returns Diff results showing changes
21
+ */
22
+ DiffPath(path: string, value: any): JsonDiffResult;
23
+ PatchPath(path: string, patch: any): JsonDiffResult;
24
+ SplicePath(path: string, start: number, deleteCount: number | undefined, items: any[]): DiffSpliceResult;
25
+ PushPath(path: string, items: any[]): JsonDiffResult;
26
+ }
27
+ /**
28
+ * Constructor interface for IDiffTree.
29
+ */
30
+ export interface IDiffTreeConstructor {
31
+ /**
32
+ * Creates a new IDiffTree instance.
33
+ * @param keyFunc - Optional function to extract a key from a value
34
+ * @param projections - Optional map of derived-value projections, keyed by id
35
+ */
36
+ new (keyFunc?: {
37
+ (val: any): string;
38
+ }, projections?: DiffTreeProjectionMap): IDiffTree;
39
+ }
40
+ /**
41
+ * A pure function computing a derived value from the current values at `reads`. Called with
42
+ * the resolved read paths (`keys`) first, then each value positionally in the same order as
43
+ * `reads`, so each value parameter can be typed at the call site instead of cast out of a
44
+ * generic read. A trailing wildcard read expands to one key/value pair per matched root key,
45
+ * so `keys` lets the function tell which root each trailing value came from.
46
+ */
47
+ export type DiffTreeProjectionFunc = (keys: string[], ...values: any[]) => any;
48
+ /**
49
+ * A projection's declaration: the dot-separated paths it reads (a plain tree path, an
50
+ * entity's key, or another projection's `$projection_<id>` key for chaining) and the
51
+ * function computing its result from their current values, positionally. Declaring `reads`
52
+ * up front lets the tree topologically sort projections and reject a dependency cycle at
53
+ * construction instead of on first write.
54
+ *
55
+ * The final entry in `reads` may instead be a wildcard root key (e.g. `"user_*"`, no dot
56
+ * nesting) matching every root key with that prefix; each match is read and passed to the
57
+ * projection as its own trailing key/value pair. A wildcard read can't be chained off a
58
+ * projection's `$projection_<id>` key and can't appear anywhere but last.
59
+ */
60
+ export type DiffTreeProjectionDefinition = {
61
+ reads: string[];
62
+ projection: DiffTreeProjectionFunc;
63
+ };
64
+ /** A map of projection id to its declaration, keyed by the id used in `$projection_<id>`. */
65
+ export type DiffTreeProjectionMap = {
66
+ [id: string]: DiffTreeProjectionDefinition;
67
+ };
68
+ /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
69
+ export declare const PROJECTION_PREFIX = "$projection_";
70
+ export type DiffSpliceResult<T = unknown> = {
71
+ /** Array path segments (strings for object keys, numbers for array indices) */
72
+ path: (string | number)[];
73
+ /** Required by the splice operation **/
74
+ start: number;
75
+ deleteCount: number;
76
+ items: T[];
77
+ spliceResult: T[];
78
+ diffResult: JsonDiffResult;
79
+ };
80
+ export type IDiffMethodBatch = IDiffMethod[];
81
+ export type IDiffMethodBatchResult = ({
82
+ type: "diff";
83
+ result: JsonDiffResult;
84
+ } | {
85
+ type: "splice";
86
+ result: DiffSpliceResult;
87
+ })[];
88
+ /**
89
+ * A message a `DiffTreeWriter` posts to its `PushStore`. Both arrive as ordinary
90
+ * `"message"` events, so the channel can carry other traffic too; listeners tell them apart
91
+ * by `type`.
92
+ */
93
+ export type IDiffPushEvent = {
94
+ type: typeof DIFF_TREE_EVENT;
95
+ data: IDiffMethodBatchResult;
96
+ } | {
97
+ type: typeof DIFF_TREE_ERROR;
98
+ data: any;
99
+ };
100
+ export interface IDiffMethod {
101
+ /** The method to call */
102
+ method: "diffpath" | "splicepath" | "pushpath" | "patchpath";
103
+ /** Arguments for the method call */
104
+ arguments: Array<any>;
105
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `type` of the message a `DiffTreeWriter` posts on `Commit()`: the batch's diff results,
3
+ * which a `PushStore` applies.
4
+ */
5
+ export const DIFF_TREE_EVENT = "DIFF_TREE_EVENT";
6
+ /**
7
+ * `type` of the message a `DiffTreeWriter` posts on `Commit()` when a change in the batch
8
+ * failed, after the `DIFF_TREE_EVENT` carrying the changes that were applied.
9
+ */
10
+ export const DIFF_TREE_ERROR = "DIFF_TREE_ERROR";
11
+ /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
12
+ export const PROJECTION_PREFIX = "$projection_";
@@ -1,17 +1,2 @@
1
- import { JsonDiffResult } from "../../Utils/json";
2
- import { DiffSpliceResult, DiffTree } from "./diffTree";
3
- export type IDiffMethodBatch = IDiffMethod[];
4
- export type IDiffMethodBatchResult = ({
5
- type: "diff";
6
- result: JsonDiffResult;
7
- } | {
8
- type: "splice";
9
- result: DiffSpliceResult;
10
- })[];
11
- export interface IDiffMethod {
12
- /** The method to call */
13
- method: "diffpath" | "splicepath" | "pushpath" | "patchpath";
14
- /** Arguments for the method call */
15
- arguments: Array<any>;
16
- }
1
+ import { DiffTree } from "./diffTree";
17
2
  export declare function ConnectWorkerToDiffTree(diffTree: DiffTree, worker: Worker): void;
@@ -0,0 +1,103 @@
1
+ import { JsonDiffResult } from "../../Utils/json";
2
+ import { DiffTree } from "./diffTree";
3
+ import { DiffSpliceResult, DiffTreeProjectionMap } from "./diffTree.types";
4
+ /**
5
+ * A `DiffTree` that pushes its changes to a `PushStore` instead of answering a store's
6
+ * requests. Use it inside a worker that produces data itself (for example, the result of a
7
+ * computation it runs): write with the usual `DiffTree` methods, then call `Commit()` to run
8
+ * projections and post everything since the last commit as one message.
9
+ *
10
+ * - Push only: don't pass a `DiffTreeWriter` to `ConnectWorkerToDiffTree`. It would record
11
+ * the request/response batches too and send them again on the next `Commit()`.
12
+ * - After a change fails, every later change before `Commit()` throws that same error. The
13
+ * changes made before the failure are already in the tree, so `Commit()` still sends them,
14
+ * followed by a `DIFF_TREE_ERROR` message. Projections aren't run for that batch; the next
15
+ * `Commit()` brings them up to date.
16
+ * - A `PushStore` only receives the changes committed after it starts listening, so
17
+ * create it before the writer's first `Commit()`.
18
+ *
19
+ * @example
20
+ * // engine-worker.ts
21
+ * const tree = new DiffTreeWriter(self, undefined, projections);
22
+ * tree.DiffPath("results", computeResults());
23
+ * tree.Commit();
24
+ *
25
+ * // main thread
26
+ * const store = new PushStore(worker);
27
+ *
28
+ * @see PushStore
29
+ */
30
+ export declare class DiffTreeWriter extends DiffTree {
31
+ private worker;
32
+ private uncommittedError;
33
+ private uncommitted;
34
+ /**
35
+ * Creates a DiffTreeWriter.
36
+ * @param worker Where `Commit()` posts its messages: the worker's `self`, or a `MessagePort`.
37
+ * On the same thread as the store, anything whose `postMessage` dispatches a `"message"`
38
+ * event on the store's `EventTarget`; post copies, or construct the store with `cloneData`.
39
+ * @param keyFunc Optional function to extract a key from objects. Pass the same key logic to
40
+ * the `PushStore` that receives the changes.
41
+ * @param projections Optional map of derived-value projections, keyed by id. They run once
42
+ * per `Commit()`, after the batch's changes.
43
+ */
44
+ constructor(worker: {
45
+ postMessage(message: unknown): void;
46
+ }, keyFunc?: {
47
+ (val: any): string;
48
+ }, projections?: DiffTreeProjectionMap);
49
+ /**
50
+ * Not available on a DiffTreeWriter: `Commit()` runs projections once per batch, after its
51
+ * changes. Running them earlier would push a projection result computed from part of a batch.
52
+ * @throws Always.
53
+ */
54
+ RunProjections(): JsonDiffResult;
55
+ /**
56
+ * Computes the diff between a new value and the current value at a path, and records it
57
+ * for the next `Commit()`.
58
+ * @param path - Dot-separated path to the value
59
+ * @param value - The new value to compare
60
+ * @returns Diff results showing changes
61
+ * @throws The change's error, or an earlier uncommitted change's error.
62
+ */
63
+ DiffPath(path: string, value: any): JsonDiffResult;
64
+ /**
65
+ * Merges a patch into the current value at a path, and records the diff for the next
66
+ * `Commit()`.
67
+ * @param path - Dot-separated path to the value
68
+ * @param patch - The value to merge into the current value
69
+ * @returns Diff results showing changes
70
+ * @throws The change's error, or an earlier uncommitted change's error.
71
+ */
72
+ PatchPath(path: string, patch: any): JsonDiffResult;
73
+ /**
74
+ * Appends items to the array at a path, and records the diff for the next `Commit()`.
75
+ * @param path - Dot-separated path to the array
76
+ * @param items - The items to append
77
+ * @returns Diff results showing changes
78
+ * @throws The change's error, or an earlier uncommitted change's error.
79
+ */
80
+ PushPath(path: string, items: unknown[]): JsonDiffResult;
81
+ /**
82
+ * Splices the array at a path, and records the splice for the next `Commit()`.
83
+ * @param path - Dot-separated path to the array
84
+ * @param start - The position at which to start changing the array
85
+ * @param deleteCount - The number of elements to delete
86
+ * @param items - The elements to insert
87
+ * @returns The splice and its diff results
88
+ * @throws The change's error, or an earlier uncommitted change's error.
89
+ */
90
+ SplicePath(path: string, start: number, deleteCount: number, items: any[]): DiffSpliceResult<unknown>;
91
+ /**
92
+ * Runs projections and posts every change since the last commit as one `DIFF_TREE_EVENT`
93
+ * message, which a `PushStore` applies as a unit. Posts nothing when nothing changed.
94
+ *
95
+ * If a change in the batch failed, projections are skipped, the applied changes are still
96
+ * posted, and a `DIFF_TREE_ERROR` message follows with the error; `Commit()` itself doesn't
97
+ * throw, since the failed change already did. If a projection throws, the batch's changes
98
+ * and the error are posted the same way, then `Commit()` rethrows. Either way the next batch
99
+ * starts clean.
100
+ * @throws A projection's error.
101
+ */
102
+ Commit(): void;
103
+ }
@@ -0,0 +1,188 @@
1
+ import { DiffTree } from "./diffTree";
2
+ import { DIFF_TREE_ERROR, DIFF_TREE_EVENT } from "./diffTree.types";
3
+ /**
4
+ * A `DiffTree` that pushes its changes to a `PushStore` instead of answering a store's
5
+ * requests. Use it inside a worker that produces data itself (for example, the result of a
6
+ * computation it runs): write with the usual `DiffTree` methods, then call `Commit()` to run
7
+ * projections and post everything since the last commit as one message.
8
+ *
9
+ * - Push only: don't pass a `DiffTreeWriter` to `ConnectWorkerToDiffTree`. It would record
10
+ * the request/response batches too and send them again on the next `Commit()`.
11
+ * - After a change fails, every later change before `Commit()` throws that same error. The
12
+ * changes made before the failure are already in the tree, so `Commit()` still sends them,
13
+ * followed by a `DIFF_TREE_ERROR` message. Projections aren't run for that batch; the next
14
+ * `Commit()` brings them up to date.
15
+ * - A `PushStore` only receives the changes committed after it starts listening, so
16
+ * create it before the writer's first `Commit()`.
17
+ *
18
+ * @example
19
+ * // engine-worker.ts
20
+ * const tree = new DiffTreeWriter(self, undefined, projections);
21
+ * tree.DiffPath("results", computeResults());
22
+ * tree.Commit();
23
+ *
24
+ * // main thread
25
+ * const store = new PushStore(worker);
26
+ *
27
+ * @see PushStore
28
+ */
29
+ export class DiffTreeWriter extends DiffTree {
30
+ /**
31
+ * Creates a DiffTreeWriter.
32
+ * @param worker Where `Commit()` posts its messages: the worker's `self`, or a `MessagePort`.
33
+ * On the same thread as the store, anything whose `postMessage` dispatches a `"message"`
34
+ * event on the store's `EventTarget`; post copies, or construct the store with `cloneData`.
35
+ * @param keyFunc Optional function to extract a key from objects. Pass the same key logic to
36
+ * the `PushStore` that receives the changes.
37
+ * @param projections Optional map of derived-value projections, keyed by id. They run once
38
+ * per `Commit()`, after the batch's changes.
39
+ */
40
+ constructor(worker, keyFunc, projections) {
41
+ super(keyFunc, projections);
42
+ this.worker = worker;
43
+ this.uncommittedError = null;
44
+ this.uncommitted = [];
45
+ }
46
+ /**
47
+ * Not available on a DiffTreeWriter: `Commit()` runs projections once per batch, after its
48
+ * changes. Running them earlier would push a projection result computed from part of a batch.
49
+ * @throws Always.
50
+ */
51
+ RunProjections() {
52
+ throw new Error("RunProjections is called during Commit()");
53
+ }
54
+ /**
55
+ * Computes the diff between a new value and the current value at a path, and records it
56
+ * for the next `Commit()`.
57
+ * @param path - Dot-separated path to the value
58
+ * @param value - The new value to compare
59
+ * @returns Diff results showing changes
60
+ * @throws The change's error, or an earlier uncommitted change's error.
61
+ */
62
+ DiffPath(path, value) {
63
+ if (this.uncommittedError)
64
+ throw this.uncommittedError;
65
+ try {
66
+ const result = super.DiffPath(path, value);
67
+ result.length > 0 && this.uncommitted.push({
68
+ type: "diff",
69
+ result
70
+ });
71
+ return result;
72
+ }
73
+ catch (err) {
74
+ this.uncommittedError = err;
75
+ throw err;
76
+ }
77
+ }
78
+ /**
79
+ * Merges a patch into the current value at a path, and records the diff for the next
80
+ * `Commit()`.
81
+ * @param path - Dot-separated path to the value
82
+ * @param patch - The value to merge into the current value
83
+ * @returns Diff results showing changes
84
+ * @throws The change's error, or an earlier uncommitted change's error.
85
+ */
86
+ PatchPath(path, patch) {
87
+ if (this.uncommittedError)
88
+ throw this.uncommittedError;
89
+ try {
90
+ const result = super.PatchPath(path, patch);
91
+ result.length > 0 && this.uncommitted.push({
92
+ type: "diff",
93
+ result
94
+ });
95
+ return result;
96
+ }
97
+ catch (err) {
98
+ this.uncommittedError = err;
99
+ throw err;
100
+ }
101
+ }
102
+ /**
103
+ * Appends items to the array at a path, and records the diff for the next `Commit()`.
104
+ * @param path - Dot-separated path to the array
105
+ * @param items - The items to append
106
+ * @returns Diff results showing changes
107
+ * @throws The change's error, or an earlier uncommitted change's error.
108
+ */
109
+ PushPath(path, items) {
110
+ if (this.uncommittedError)
111
+ throw this.uncommittedError;
112
+ try {
113
+ const result = super.PushPath(path, items);
114
+ result.length > 0 && this.uncommitted.push({
115
+ type: "diff",
116
+ result
117
+ });
118
+ return result;
119
+ }
120
+ catch (err) {
121
+ this.uncommittedError = err;
122
+ throw err;
123
+ }
124
+ }
125
+ /**
126
+ * Splices the array at a path, and records the splice for the next `Commit()`.
127
+ * @param path - Dot-separated path to the array
128
+ * @param start - The position at which to start changing the array
129
+ * @param deleteCount - The number of elements to delete
130
+ * @param items - The elements to insert
131
+ * @returns The splice and its diff results
132
+ * @throws The change's error, or an earlier uncommitted change's error.
133
+ */
134
+ SplicePath(path, start, deleteCount, items) {
135
+ if (this.uncommittedError)
136
+ throw this.uncommittedError;
137
+ try {
138
+ const result = super.SplicePath(path, start, deleteCount, items);
139
+ this.uncommitted.push({
140
+ type: "splice",
141
+ result
142
+ });
143
+ return result;
144
+ }
145
+ catch (err) {
146
+ this.uncommittedError = err;
147
+ throw err;
148
+ }
149
+ }
150
+ /**
151
+ * Runs projections and posts every change since the last commit as one `DIFF_TREE_EVENT`
152
+ * message, which a `PushStore` applies as a unit. Posts nothing when nothing changed.
153
+ *
154
+ * If a change in the batch failed, projections are skipped, the applied changes are still
155
+ * posted, and a `DIFF_TREE_ERROR` message follows with the error; `Commit()` itself doesn't
156
+ * throw, since the failed change already did. If a projection throws, the batch's changes
157
+ * and the error are posted the same way, then `Commit()` rethrows. Either way the next batch
158
+ * starts clean.
159
+ * @throws A projection's error.
160
+ */
161
+ Commit() {
162
+ try {
163
+ const result = !this.uncommittedError ? super.RunProjections() : [];
164
+ result.length > 0 && this.uncommitted.push({
165
+ type: "diff",
166
+ result
167
+ });
168
+ }
169
+ catch (err) {
170
+ this.uncommittedError = err;
171
+ throw err;
172
+ }
173
+ finally {
174
+ this.uncommitted.length > 0 && this.worker.postMessage({
175
+ type: DIFF_TREE_EVENT,
176
+ data: this.uncommitted
177
+ });
178
+ if (this.uncommittedError) {
179
+ this.worker.postMessage({
180
+ type: DIFF_TREE_ERROR,
181
+ data: this.uncommittedError
182
+ });
183
+ }
184
+ this.uncommitted = [];
185
+ this.uncommittedError = null;
186
+ }
187
+ }
188
+ }
@@ -0,0 +1 @@
1
+ export * from "./diffTree.types";
@@ -0,0 +1 @@
1
+ export * from "./diffTree.types";
@@ -0,0 +1,57 @@
1
+ import { ReadStore } from "./store";
2
+ /**
3
+ * A read-only store whose changes are pushed to it by a `DiffTreeWriter`. The writer decides
4
+ * when data changes and pushes it with `Commit()`; this store applies each pushed batch as it
5
+ * arrives, and readers update the same way they do for any other store.
6
+ *
7
+ * The source is any `EventTarget` that delivers the writer's messages as `"message"` events:
8
+ * usually a `Worker` or a `MessagePort`, or a plain `EventTarget` when the writer runs on the
9
+ * same thread. The store ignores any message that isn't a `DIFF_TREE_EVENT` or
10
+ * `DIFF_TREE_ERROR`, so the source can carry other traffic too. It never takes over the
11
+ * source's `onmessage` or terminates a worker; that stays with the source's owner. It doesn't
12
+ * listen for the source's `"error"` events either: the writer's errors arrive as
13
+ * `DIFF_TREE_ERROR` messages (see `Error`).
14
+ *
15
+ * Changes are diffs against the writer's tree, so create the store before the writer's
16
+ * first `Commit()`. A store created later misses the earlier changes.
17
+ *
18
+ * @example
19
+ * const worker = new Worker(new URL("./engine-worker.ts", import.meta.url), { type: "module" });
20
+ * const store = new PushStore(worker);
21
+ * store.Get<Results>("results"); // reactive; undefined until the first commit
22
+ * store.Destroy(); // stops listening; doesn't terminate the worker
23
+ *
24
+ * @see DiffTreeWriter
25
+ * @see StoreAsync
26
+ */
27
+ export declare class PushStore extends ReadStore {
28
+ private eventTarget;
29
+ private error;
30
+ private errorScope;
31
+ /**
32
+ * The error the writer reported with its last commit, or null. Reactive. It's cleared when
33
+ * a later commit's changes arrive, so it reflects the most recent push, not a stopped store:
34
+ * the writer's later changes are still applied after an error.
35
+ */
36
+ get Error(): any;
37
+ private messageHandler;
38
+ /**
39
+ * Creates a PushStore and starts listening to the source. A `MessagePort` is started for
40
+ * you, since `addEventListener` doesn't start a port the way assigning `onmessage` does.
41
+ * @param eventTarget The source of the `DiffTreeWriter`'s messages: a `Worker`, a
42
+ * `MessagePort`, or any `EventTarget` that dispatches them as `"message"` events.
43
+ * @param keyFunc Optional function to generate a key for a given data value, used to
44
+ * resolve root objects. Pass the same key logic the `DiffTreeWriter` was constructed
45
+ * with, if any.
46
+ * @param cloneData Whether to deep-clone the values the store adopts (false by default).
47
+ * A `Worker` or `MessagePort` already delivers a structured-clone copy of every message.
48
+ * A same-thread `EventTarget` delivers the writer's own objects, so either the writer
49
+ * posts copies or this is true; otherwise the store and the writer's tree share objects.
50
+ */
51
+ constructor(eventTarget: EventTarget, keyFunc?: (value: any) => string | undefined, cloneData?: boolean);
52
+ /**
53
+ * Stops listening to the source and destroys the error scope. The source stays open (a
54
+ * worker keeps running); closing or terminating it is up to its owner.
55
+ */
56
+ Destroy(): void;
57
+ }
@@ -0,0 +1,96 @@
1
+ import { DIFF_TREE_ERROR, DIFF_TREE_EVENT } from "../Diff/diffTree.types";
2
+ import { ObservableScope } from "../Tree/observableScope";
3
+ import { ReadStore } from "./store";
4
+ /**
5
+ * A read-only store whose changes are pushed to it by a `DiffTreeWriter`. The writer decides
6
+ * when data changes and pushes it with `Commit()`; this store applies each pushed batch as it
7
+ * arrives, and readers update the same way they do for any other store.
8
+ *
9
+ * The source is any `EventTarget` that delivers the writer's messages as `"message"` events:
10
+ * usually a `Worker` or a `MessagePort`, or a plain `EventTarget` when the writer runs on the
11
+ * same thread. The store ignores any message that isn't a `DIFF_TREE_EVENT` or
12
+ * `DIFF_TREE_ERROR`, so the source can carry other traffic too. It never takes over the
13
+ * source's `onmessage` or terminates a worker; that stays with the source's owner. It doesn't
14
+ * listen for the source's `"error"` events either: the writer's errors arrive as
15
+ * `DIFF_TREE_ERROR` messages (see `Error`).
16
+ *
17
+ * Changes are diffs against the writer's tree, so create the store before the writer's
18
+ * first `Commit()`. A store created later misses the earlier changes.
19
+ *
20
+ * @example
21
+ * const worker = new Worker(new URL("./engine-worker.ts", import.meta.url), { type: "module" });
22
+ * const store = new PushStore(worker);
23
+ * store.Get<Results>("results"); // reactive; undefined until the first commit
24
+ * store.Destroy(); // stops listening; doesn't terminate the worker
25
+ *
26
+ * @see DiffTreeWriter
27
+ * @see StoreAsync
28
+ */
29
+ export class PushStore extends ReadStore {
30
+ /**
31
+ * The error the writer reported with its last commit, or null. Reactive. It's cleared when
32
+ * a later commit's changes arrive, so it reflects the most recent push, not a stopped store:
33
+ * the writer's later changes are still applied after an error.
34
+ */
35
+ get Error() {
36
+ return ObservableScope.Value(this.errorScope);
37
+ }
38
+ /**
39
+ * Creates a PushStore and starts listening to the source. A `MessagePort` is started for
40
+ * you, since `addEventListener` doesn't start a port the way assigning `onmessage` does.
41
+ * @param eventTarget The source of the `DiffTreeWriter`'s messages: a `Worker`, a
42
+ * `MessagePort`, or any `EventTarget` that dispatches them as `"message"` events.
43
+ * @param keyFunc Optional function to generate a key for a given data value, used to
44
+ * resolve root objects. Pass the same key logic the `DiffTreeWriter` was constructed
45
+ * with, if any.
46
+ * @param cloneData Whether to deep-clone the values the store adopts (false by default).
47
+ * A `Worker` or `MessagePort` already delivers a structured-clone copy of every message.
48
+ * A same-thread `EventTarget` delivers the writer's own objects, so either the writer
49
+ * posts copies or this is true; otherwise the store and the writer's tree share objects.
50
+ */
51
+ constructor(eventTarget, keyFunc, cloneData = false) {
52
+ super(keyFunc, cloneData);
53
+ this.eventTarget = eventTarget;
54
+ this.error = null;
55
+ this.errorScope = ObservableScope.Basic(() => this.error);
56
+ this.messageHandler = (event) => {
57
+ const messageData = event.data;
58
+ if (messageData && messageData.type === DIFF_TREE_EVENT) {
59
+ const data = messageData.data;
60
+ for (let x = 0; x < data.length; x++) {
61
+ const result = data[x];
62
+ switch (result.type) {
63
+ case "diff": {
64
+ this.UpdateRootMap(result.result);
65
+ break;
66
+ }
67
+ case "splice": {
68
+ const spliceResult = result.result;
69
+ this.UpdateRootMap(spliceResult.diffResult);
70
+ this.SpliceRootObject(spliceResult.path[0], spliceResult.start, spliceResult.deleteCount, spliceResult.items);
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ if (this.error !== null) {
76
+ this.error = null;
77
+ ObservableScope.Update(this.errorScope);
78
+ }
79
+ }
80
+ else if (messageData && messageData.type === DIFF_TREE_ERROR) {
81
+ this.error = messageData.data;
82
+ ObservableScope.Update(this.errorScope);
83
+ }
84
+ };
85
+ eventTarget.addEventListener("message", this.messageHandler);
86
+ eventTarget.start?.();
87
+ }
88
+ /**
89
+ * Stops listening to the source and destroys the error scope. The source stays open (a
90
+ * worker keeps running); closing or terminating it is up to its owner.
91
+ */
92
+ Destroy() {
93
+ this.eventTarget.removeEventListener("message", this.messageHandler);
94
+ ObservableScope.Destroy(this.errorScope);
95
+ }
96
+ }
@@ -1,18 +1,19 @@
1
1
  import { JsonDiffResult } from "../../Utils/json";
2
2
  /**
3
- * Base class for observable data store management.
4
- * Stores root objects as ObservableNode instances and manages updates through diff operations.
3
+ * Base class for reading observable data: holds root objects as ObservableNode instances
4
+ * and applies diff results to them. Subclasses decide where the diffs come from: `Store`
5
+ * adds the write methods, and `PushStore` takes them from a `DiffTreeWriter`.
5
6
  *
6
- * @see StoreSync
7
- * @see StoreAsync
7
+ * @see Store
8
+ * @see PushStore
8
9
  */
9
- export declare abstract class Store {
10
+ export declare abstract class ReadStore {
10
11
  protected keyFunc?: (value: any) => string | undefined;
11
12
  private cloneData;
12
13
  private rootMap;
13
14
  private createNode;
14
15
  /**
15
- * Creates an instance of Store.
16
+ * Creates an instance of ReadStore.
16
17
  * @param keyFunc Optional function to generate a key for a given data value.
17
18
  * When provided, enables alias functionality where values can reference other root objects.
18
19
  */
@@ -33,10 +34,6 @@ export declare abstract class Store {
33
34
  */
34
35
  Get<O>(id: string, defaultValue: O): O;
35
36
  Has(id: string): boolean;
36
- abstract Write(data: unknown, key?: string): Promise<void> | void;
37
- abstract Patch(key: string, patch: unknown): Promise<void> | void;
38
- abstract Push(key: string, ...data: unknown[]): Promise<void> | void;
39
- abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<any[]> | any[];
40
37
  /**
41
38
  * Updates the root map with diff results by grouping changes by root path.
42
39
  * @param results - Array of diff results to apply
@@ -54,3 +51,16 @@ export declare abstract class Store {
54
51
  */
55
52
  private UpdateRootObject;
56
53
  }
54
+ /**
55
+ * Base class for observable data store management.
56
+ * Stores root objects as ObservableNode instances and manages updates through diff operations.
57
+ *
58
+ * @see StoreSync
59
+ * @see StoreAsync
60
+ */
61
+ export declare abstract class Store extends ReadStore {
62
+ abstract Write(data: unknown, key?: string): Promise<void> | void;
63
+ abstract Patch(key: string, patch: unknown): Promise<void> | void;
64
+ abstract Push(key: string, ...data: unknown[]): Promise<void> | void;
65
+ abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<any[]> | any[];
66
+ }
@@ -1,15 +1,16 @@
1
1
  import { JsonDeepClone } from "../../Utils/json";
2
2
  import { ObservableNode } from "../Tree/observableNode";
3
3
  /**
4
- * Base class for observable data store management.
5
- * Stores root objects as ObservableNode instances and manages updates through diff operations.
4
+ * Base class for reading observable data: holds root objects as ObservableNode instances
5
+ * and applies diff results to them. Subclasses decide where the diffs come from: `Store`
6
+ * adds the write methods, and `PushStore` takes them from a `DiffTreeWriter`.
6
7
  *
7
- * @see StoreSync
8
- * @see StoreAsync
8
+ * @see Store
9
+ * @see PushStore
9
10
  */
10
- export class Store {
11
+ export class ReadStore {
11
12
  /**
12
- * Creates an instance of Store.
13
+ * Creates an instance of ReadStore.
13
14
  * @param keyFunc Optional function to generate a key for a given data value.
14
15
  * When provided, enables alias functionality where values can reference other root objects.
15
16
  */
@@ -82,6 +83,15 @@ export class Store {
82
83
  this.rootMap.set(rootPath, newRootObject);
83
84
  return;
84
85
  }
85
- ObservableNode.ApplyDiff(rootObject, results);
86
+ ObservableNode.ApplyDiff(rootObject, results, this.cloneData);
86
87
  }
87
88
  }
89
+ /**
90
+ * Base class for observable data store management.
91
+ * Stores root objects as ObservableNode instances and manages updates through diff operations.
92
+ *
93
+ * @see StoreSync
94
+ * @see StoreAsync
95
+ */
96
+ export class Store extends ReadStore {
97
+ }
@@ -1,4 +1,4 @@
1
- import { DiffTreeProjectionMap } from "../Diff/diffTree";
1
+ import { DiffTreeProjectionMap } from "../Diff/diffTree.types";
2
2
  import { Store } from "./store";
3
3
  /**
4
4
  * StoreSync class extends the base Store class to provide synchronous data management operations.
@@ -70,5 +70,5 @@ export declare class StoreSync extends Store {
70
70
  * @param items Optional elements to insert into the array.
71
71
  * @returns The array of deleted elements.
72
72
  */
73
- Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): any[];
73
+ Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): unknown[];
74
74
  }
@@ -6,8 +6,8 @@ const NODE_PROXY = Symbol("NODE_PROXY");
6
6
  const NODE_SNAPSHOT = Symbol("NODE_SNAPSHOT");
7
7
  const toJSON = "toJSON";
8
8
  const IS_NODE = Symbol("IS_NODE");
9
+ const ARRAY_SCOPE = Symbol("ARRAY_SCOPE");
9
10
  const OBJECT_SCOPE = Symbol("OBJECT_SCOPE");
10
- const ITERATOR_SCOPE = Symbol("ITERATOR_SCOPE");
11
11
  function Identity(value) {
12
12
  return value;
13
13
  }
@@ -34,34 +34,37 @@ function InvalidateSnapshotForValue(value) {
34
34
  InvalidateSnapshotForValue(parent);
35
35
  }
36
36
  function getOwnPropertyDescriptor(target, prop) {
37
+ ObservableScope.Touch(target[OBJECT_SCOPE]);
37
38
  const descriptor = Object.getOwnPropertyDescriptor(target[NODE_VALUE], prop);
38
- return {
39
+ return descriptor && {
39
40
  ...descriptor,
40
41
  configurable: true,
41
42
  };
42
43
  }
43
44
  function getOwnPropertyDescriptorArray(target, prop) {
44
- const descriptor = Object.getOwnPropertyDescriptor(target[NODE_VALUE], prop);
45
- return {
46
- ...descriptor,
47
- configurable: true,
45
+ ObservableScope.Touch(target[ARRAY_SCOPE]);
46
+ const descriptor = Object.getOwnPropertyDescriptor(target, prop);
47
+ const nodeDescriptor = Object.getOwnPropertyDescriptor(target[NODE_VALUE], prop);
48
+ return nodeDescriptor && {
49
+ ...nodeDescriptor,
50
+ configurable: descriptor?.configurable ?? true,
48
51
  };
49
52
  }
50
53
  function has(value, prop) {
51
- ObservableScope.Touch(value[ITERATOR_SCOPE]);
54
+ ObservableScope.Touch(value[OBJECT_SCOPE]);
52
55
  return Object.hasOwn(value[NODE_VALUE], prop);
53
56
  }
54
57
  function hasArray(value, prop) {
55
- ObservableScope.Touch(value[ITERATOR_SCOPE]);
58
+ ObservableScope.Touch(value[ARRAY_SCOPE]);
56
59
  return Object.hasOwn(value[NODE_VALUE], prop);
57
60
  }
58
61
  function ownKeys(value) {
59
- ObservableScope.Touch(value[ITERATOR_SCOPE]);
62
+ ObservableScope.Touch(value[OBJECT_SCOPE]);
60
63
  return Object.keys(value[NODE_VALUE]);
61
64
  }
62
65
  function ownKeysArray(value) {
63
- ObservableScope.Touch(value[ITERATOR_SCOPE]);
64
- return Object.keys(value[NODE_VALUE]);
66
+ ObservableScope.Touch(value[ARRAY_SCOPE]);
67
+ return Reflect.ownKeys(value[NODE_VALUE]);
65
68
  }
66
69
  function UnwrapProxy(value, type = JsonType(value)) {
67
70
  if (type === "value")
@@ -106,20 +109,30 @@ function CloneProxy(value, type = JsonType(value)) {
106
109
  }
107
110
  return value;
108
111
  }
109
- function GetPropertyScope(object, prop) {
110
- return object[prop] ??= ObservableScope.Basic(Property.bind(null, object[NODE_VALUE], prop));
112
+ function GetPropertyScope(object, prop, create = true) {
113
+ if (!Object.hasOwn(object, prop)) {
114
+ if (!create)
115
+ return undefined;
116
+ Object.defineProperty(object, prop, {
117
+ value: ObservableScope.Basic(Property.bind(null, object[NODE_VALUE], prop)),
118
+ writable: true,
119
+ enumerable: true,
120
+ configurable: true,
121
+ });
122
+ }
123
+ return object[prop];
111
124
  }
112
125
  function SetObjectValue(object, prop, value) {
113
126
  const propExists = Object.hasOwn(object[NODE_VALUE], prop);
114
127
  object[NODE_VALUE][prop] = value;
115
128
  InvalidateSnapshotForValue(object[NODE_VALUE]);
116
- ObservableScope.Update(object[prop]);
117
- !propExists && ObservableScope.Update(object[ITERATOR_SCOPE]);
129
+ Object.hasOwn(object, prop) && ObservableScope.Update(object[prop]);
130
+ !propExists && ObservableScope.Update(object[OBJECT_SCOPE]);
118
131
  }
119
132
  function SetArrayValue(object, prop, value) {
120
133
  object[NODE_VALUE][prop] = value;
121
134
  InvalidateSnapshotForValue(object[NODE_VALUE]);
122
- ObservableScope.Update(object[OBJECT_SCOPE]);
135
+ ObservableScope.Update(object[ARRAY_SCOPE]);
123
136
  }
124
137
  function CreateProxyFactory(alias) {
125
138
  /**
@@ -174,7 +187,7 @@ function CreateProxyFactory(alias) {
174
187
  const wrapper = Object.assign([], {
175
188
  [NODE_VALUE]: value,
176
189
  [NODE_PROXY]: null,
177
- [OBJECT_SCOPE]: ObservableScope.Basic(Identity.bind(null, value))
190
+ [ARRAY_SCOPE]: ObservableScope.Basic(Identity.bind(null, value))
178
191
  });
179
192
  const proxy = wrapper[NODE_PROXY] = new Proxy(wrapper, {
180
193
  get: ArrayProxyGetter,
@@ -217,7 +230,7 @@ function CreateProxyFactory(alias) {
217
230
  case NODE_SNAPSHOT:
218
231
  return CreateSnapshot(object[NODE_VALUE]);
219
232
  default: {
220
- const scope = object[OBJECT_SCOPE];
233
+ const scope = object[ARRAY_SCOPE];
221
234
  const array = ObservableScope.Value(scope);
222
235
  const arrayValue = array[prop];
223
236
  if (typeof prop === "symbol")
@@ -254,7 +267,7 @@ function CreateProxyFactory(alias) {
254
267
  const wrapper = {
255
268
  [NODE_VALUE]: value,
256
269
  [NODE_PROXY]: null,
257
- [ITERATOR_SCOPE]: ObservableScope.Basic(Identity.bind(null, value))
270
+ [OBJECT_SCOPE]: ObservableScope.Basic(Identity.bind(null, value))
258
271
  };
259
272
  const proxy = wrapper[NODE_PROXY] = new Proxy(wrapper, {
260
273
  get: ObjectProxyGetter,
@@ -274,7 +287,6 @@ function CreateProxyFactory(alias) {
274
287
  return true;
275
288
  }
276
289
  function ObjectProxyGetter(object, prop) {
277
- ObservableScope.Touch(object[OBJECT_SCOPE]);
278
290
  switch (prop) {
279
291
  case IS_NODE:
280
292
  return true;
@@ -314,7 +326,7 @@ function CreateProxyFactory(alias) {
314
326
  case "array": {
315
327
  const wrapper = wrapperCache.get(value);
316
328
  const proxy = wrapper?.[NODE_PROXY] ?? CreateArrayProxy(value);
317
- ObservableScope.Touch(wrapper?.[OBJECT_SCOPE]);
329
+ ObservableScope.Touch(wrapper?.[ARRAY_SCOPE]);
318
330
  return proxy;
319
331
  }
320
332
  default:
@@ -401,7 +413,7 @@ export var ObservableNode;
401
413
  function Update(value, prop) {
402
414
  const wrapper = wrapperCache.get(value);
403
415
  if (wrapper) {
404
- const scope = Array.isArray(wrapper) ? wrapper[OBJECT_SCOPE] : (prop && wrapper[prop]) ?? wrapper[ITERATOR_SCOPE];
416
+ const scope = Array.isArray(wrapper) ? wrapper[ARRAY_SCOPE] : (prop && GetPropertyScope(wrapper, prop)) ?? wrapper[OBJECT_SCOPE];
405
417
  ObservableScope.Update(scope);
406
418
  }
407
419
  }
@@ -425,7 +437,7 @@ export var ObservableNode;
425
437
  function Touch(value, prop) {
426
438
  const wrapper = wrapperCache.get(value);
427
439
  if (wrapper) {
428
- const scope = prop && GetPropertyScope(wrapper, prop) || wrapper[OBJECT_SCOPE];
440
+ const scope = prop && GetPropertyScope(wrapper, prop) || wrapper[ARRAY_SCOPE] || wrapper[OBJECT_SCOPE];
429
441
  ObservableScope.Touch(scope);
430
442
  }
431
443
  }
@@ -433,7 +445,7 @@ export var ObservableNode;
433
445
  function Read(value, prop) {
434
446
  const wrapper = wrapperCache.get(value);
435
447
  if (wrapper) {
436
- const scope = Array.isArray(wrapper) ? wrapper[OBJECT_SCOPE] : (prop && wrapper[prop]) ?? wrapper[ITERATOR_SCOPE];
448
+ const scope = Array.isArray(wrapper) ? wrapper[ARRAY_SCOPE] : (prop && wrapper[prop]) ?? wrapper[OBJECT_SCOPE];
437
449
  ObservableScope.Update(scope);
438
450
  }
439
451
  }
package/Store/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export { Store } from "./Store/store";
1
+ export { ReadStore, Store } from "./Store/store";
2
2
  export { StoreSync } from "./Store/storeSync";
3
3
  export { StoreAsync } from "./Store/storeAsync";
4
+ export { PushStore } from "./Store/pushStore";
4
5
  export { ObservableScope } from "./Tree/observableScope";
5
6
  export { ObservableNode } from "./Tree/observableNode";
package/Store/index.js CHANGED
@@ -1,5 +1,6 @@
1
- export { Store } from "./Store/store";
1
+ export { ReadStore, Store } from "./Store/store";
2
2
  export { StoreSync } from "./Store/storeSync";
3
3
  export { StoreAsync } from "./Store/storeAsync";
4
+ export { PushStore } from "./Store/pushStore";
4
5
  export { ObservableScope } from "./Tree/observableScope";
5
6
  export { ObservableNode } from "./Tree/observableNode";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "8.0.28",
3
+ "version": "8.1.1",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",