j-templates 8.0.27 → 8.1.0

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 3, 2026 · @Jay Landrum
3
+ Oct 17, 2026 · @Jay Landrum
4
4
 
5
- Reference for building apps with j-templates 8.0.21. 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.21. 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; those sections were re-verified against 8.0.21 in jsdom.
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
 
@@ -102,20 +103,27 @@ Component.Attach(document.getElementById("app")!, counter({}));
102
103
  **Members:**
103
104
 
104
105
  - `Template(): vNode | vNode[]` — override to render. May return an array. Not overriding renders an empty host.
105
- - `Bound()` / `Destroy()` — optional overrides. If you override either, call `super.Bound()` / `super.Destroy()`. `super.Bound()` starts `@Watch`; `super.Destroy()` tears down scopes and runs `@Destroy` properties.
106
+ - `Bound()` / `Connected()` / `Destroy()` — optional overrides. If you override `Bound()` or `Destroy()`, call `super.Bound()` / `super.Destroy()`. `super.Bound()` starts `@Watch`; `super.Destroy()` tears down scopes and runs `@Destroy` properties. The base `Connected()` is empty.
106
107
  - `Fire(event, payload?)` — sends a component event to the parent's `on:` handler. The payload is optional.
107
- - `this.Data`, `this.Templates`, `this.Injector`, `this.VNode` (host vNode; `this.VNode.node` is the host element), `this.Destroyed`.
108
+ - `this.Data`, `this.Templates`, `this.Injector`, `this.VNode` (host vNode; `this.VNode.node` is the host element), `this.IsConnected`, `this.Destroyed`.
108
109
  - Never override the constructor. Use field initializers and `Bound()`.
109
110
 
110
- **Lifecycle (verified order):**
111
+ **Lifecycle (verified order, 8.0.23+):**
111
112
 
112
113
  1. Field initializers run.
113
- 2. `Bound()` runs. The host element exists but is **not in the document and has no children**. `@Watch` handlers fire here with their initial values.
114
- 3. `Template()` runs.
115
- 4. Children functions run and build the DOM. By default this happens **after** `Attach` returns; with `SYNC_SCHEDULING=true`, before.
116
- 5. The host is in the document.
114
+ 2. `Template()` runs and its children functions build the DOM. Child components go through their own steps 1–3 here, so a child's `Bound()` runs before its parent's.
115
+ 3. `Bound()` runs once the initial subtree is in the host: `querySelector` on the host finds rendered elements, including a child component's content. `@Watch` handlers fire inside `super.Bound()` with their initial values, so they can use those elements too. Code placed before `super.Bound()` runs before the first `@Watch` call.
116
+ 4. `Connected()` runs once the host is first in the document, after `Bound()`. It runs parent first, then children. It fires once per component instance; a component that is removed and rendered again is a new instance and fires again.
117
117
 
118
- To touch rendered DOM from `Bound()`, defer with `requestAnimationFrame` or `setTimeout` (verified: host connected and children present by then). Attach native listeners to an element you render, not to the host.
118
+ By default steps 2–4 happen **after** `Attach` returns; with `SYNC_SCHEDULING=true`, before.
119
+
120
+ The host is in the document during `Bound()` only for the component passed to `Component.Attach(document.body, …)`. A child's host joins the document when its parent's children are placed, after the child's `Bound()`. So:
121
+
122
+ - Find and wire up the elements you render in `Bound()`.
123
+ - Put work that needs layout (measuring, sizes), focus, or the document in `Connected()`.
124
+ - An element that a later change renders isn't there at `Bound()` or `Connected()`. Reach it from the code that renders it, or watch for it.
125
+
126
+ `Connected()` only fires when j-templates itself puts a node in the document. A tree attached to a detached element never gets `Connected()`, even after you append that element to the document (verified). Attach native listeners to an element you render, not to the host.
119
127
 
120
128
  **Host element.** The name passed to `ToFunction` is the host's tag. `Template()` output renders inside it:
121
129
 
@@ -352,7 +360,7 @@ Tracked array methods: `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `rev
352
360
  syncFilter(value: FilterType) { this.localFilter = value; }
353
361
  ```
354
362
 
355
- - Fires once in `super.Bound()` with the initial value, then on every change.
363
+ - Fires once in `super.Bound()` with the initial value, then on every change. Since `Bound()` runs after the first render, that first call can use rendered elements.
356
364
  - Several synchronous writes in one tick produce one call with the latest value (`n = 1; n = 2; n = 3` → one call with `3`).
357
365
  - Without `super.Bound()` it never fires.
358
366
  - Use it for side effects and for copying parent `Data` into local `@Value` state. Don't use it to drive rendering; reads in templates are already reactive.
@@ -525,15 +533,15 @@ class Injector {
525
533
 
526
534
  ## Store
527
535
 
528
- 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.
529
537
 
530
- | | `StoreSync` | `StoreAsync` |
531
- | --- | --- | --- |
532
- | Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` |
533
- | Diffing | main thread, synchronous | in the `Worker` you supply |
534
- | Writes | `void`; readable immediately | `Promise`; readable only after `await` |
535
- | `Destroy()` | none | required; terminates the worker |
536
- | 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 |
537
545
 
538
546
  ### API
539
547
 
@@ -547,7 +555,7 @@ Push(key: string, ...items: unknown[]); // append to the array at key
547
555
  Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]); // returns removed items
548
556
  ```
549
557
 
550
- `StoreAsync` has the same methods; the four writers return promises.
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).
551
559
 
552
560
  ### keyFunc and flattening
553
561
 
@@ -585,12 +593,19 @@ store.Destroy(); // when done
585
593
 
586
594
  Data crosses the worker boundary by structured clone. Class instances arrive as plain objects without their methods; functions cannot be sent. Store plain data.
587
595
 
596
+ **Batching (8.0.27).** Every `Write`, `Patch`, `Push` and `Splice` made in the same tick goes to the worker as one message and is applied in call order. The worker runs projections once per batch, after the last change, not after each one. Verified: a `Write`, a `Push` and a `Patch` in one tick sent one message and ran a projection once.
597
+
598
+ - You don't have to await a change before making the next one. `Patch`, `Push` and `Splice` accept a key whose `Write` is still pending, and `Patch` merges in the worker, on top of everything before it.
599
+ - The pending-key check covers only the key passed to `Write`. Entities that `keyFunc` will flatten out of that write don't exist yet: `Patch("a", …)` right after an un-awaited `Write([{ id: "a" }], "todos")` rejects with `Key not found in store` (verified). Await the write first.
600
+ - `await store.Flush()` waits for every change made before the call. Changes made after it go in a later batch.
601
+ - **A failed change stops the store.** After any change fails in the worker (for example `Splice` on a value that isn't an array), every later `Write`, `Patch`, `Push`, `Splice` and `Flush` rejects with that same error (verified). Validate before writing; a store in this state has to be replaced.
602
+
588
603
  ### Projections
589
604
 
590
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`).
591
606
 
592
607
  ```typescript
593
- import { PROJECTION_PREFIX } from "j-templates/Store/Diff/diffTree"; // "$projection_"
608
+ import { PROJECTION_PREFIX } from "j-templates/Store/Diff"; // "$projection_"
594
609
 
595
610
  const store = new StoreSync((v: any) => v?.id, {
596
611
  activeCount: { reads: ["todos"], projection: (keys, todos) => todos.filter((t) => !t.done).length },
@@ -605,6 +620,39 @@ store.Get<number>(`${PROJECTION_PREFIX}activeCount`);
605
620
  - The last read may be a root wildcard such as `"task_*"`: each matching root's key is added to `keys` and its value appended (verified: `[["task_1","task_2"], v1, v2]`).
606
621
  - A projection has no value until every read resolves. It re-runs when a read's value changes.
607
622
  - A cycle or a read of an undefined projection throws at construction.
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).
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).
608
656
 
609
657
  ## ObservableScope and ObservableNode
610
658
 
@@ -722,6 +770,23 @@ function fakeWorker(tree = new DiffTree()) {
722
770
  const store = new StoreAsync(fakeWorker(new DiffTree((v: any) => v?.id)), (v: any) => v?.id);
723
771
  ```
724
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
+
725
790
  jsdom has no `innerText`; use `textContent` in tests.
726
791
 
727
792
  ## Worked example: Smart Tasks
@@ -895,7 +960,7 @@ Component.Attach(document.getElementById("app")!, app({}));
895
960
  9. **`@State` deep-tracks plain objects and arrays only**, not class instances, `Map` or `Set`.
896
961
  10. **`@Scope` has no equality check**; consumers re-run on every change of its inputs. `gate()` it if it returns a primitive.
897
962
  11. **`@Computed` returns a copy**, not the object the getter returned. Its identity changes only when the result's type changes or an object result loses a key.
898
- 12. **`Bound()` runs before `Template()`**, with the host detached and empty. Defer DOM access with `requestAnimationFrame`. Forgetting `super.Bound()` silences `@Watch`.
963
+ 12. **`Bound()` runs after the first render**, but a child's host isn't in the document yet. Do layout and focus work in `Connected()`. Forgetting `super.Bound()` silences `@Watch`.
899
964
  13. **`@Watch` fires immediately** with the initial value, and coalesces synchronous changes into one call.
900
965
  14. **`scope()`, `gate()` and `mapped()` outside a scope throw**, including in `on:` handlers and inside `peek()` callbacks. `peek()` works anywhere.
901
966
  15. **Conditional `scope()`/`gate()` calls need ids**, or a later call takes a skipped call's scope. Callbacks are fixed at first run: don't close over locals.
@@ -904,12 +969,13 @@ Component.Attach(document.getElementById("app")!, app({}));
904
969
  18. **Native DOM listeners belong on elements you render**, not the host.
905
970
  19. **`svg()` is HTML-namespace.** Use namespaced components or `innerHTML` for SVG.
906
971
  20. **`ObservableScope.Create` with no reactive reads is static** and ignores `Update()`. Use `Basic`.
907
- 21. **`StoreAsync` writes must be awaited**; data crosses by structured clone (class methods are lost).
972
+ 21. **`StoreAsync` writes must be awaited** before reading; data crosses by structured clone (class methods are lost). Changes in one tick are batched, and one failed change makes every later change reject.
908
973
  22. **The package needs a bundler or TypeScript runner**; plain Node cannot import it.
909
974
 
910
975
  23) **Equal-value writes still notify.** Assigning the current value again re-runs every reader. Guard repeated writes or read through `gate()`.
911
976
  24) **`JSON.stringify(node)` and `ObservableNode.Snapshot(node)` don't subscribe.** Read properties directly, or `ObservableNode.Clone(node)` to track everything.
912
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.
913
979
 
914
980
  ## Debugging
915
981
 
@@ -927,7 +993,9 @@ Component.Attach(document.getElementById("app")!, app({}));
927
993
  | `gate()` doesn't stop re-runs | gated value is a new object/array each time | gate a primitive, or use `@Computed` |
928
994
  | Async value never resolves (shows a Promise) | callback lacks `async` | `async () => …` |
929
995
  | Async value doesn't refresh | reactive read after `await` | read before the first `await` |
930
- | `querySelector` in `Bound()` finds nothing | children not built yet | `requestAnimationFrame(() => …)` |
996
+ | `querySelector` in `Bound()` finds nothing | the element is rendered later by a change, not by the first render | reach it where it's rendered, or watch for it |
997
+ | Measured size is 0, or `focus()` does nothing, in `Bound()` | the host isn't in the document yet | move the work to `Connected()` |
998
+ | `Connected()` never fires | the tree was attached to a detached element and appended later | attach to an element that's already in the document |
931
999
  | Manual service scope never updates | `ObservableScope.Create` with no reactive reads | `ObservableScope.Basic` |
932
1000
  | Test sees old DOM after a write | asynchronous rendering | `SYNC_SCHEDULING=true` or await a timeout |
933
1001
  | `ReferenceError: self is not defined` | importing `defaultDiffTreeWorker` on the main thread | write your own worker entry file |
@@ -935,19 +1003,27 @@ Component.Attach(document.getElementById("app")!, app({}));
935
1003
  | A watcher or scope over JSON.stringify/Snapshot never fires | snapshot reads register no dependencies | read properties directly, or JSON.stringify(ObservableNode.Clone(node)) |
936
1004
  | `Emitter.Emit: re-entry during emit is not supported` | a `Watch`/`OnUpdated` callback updates the scope or value that is notifying it | defer the write, or move it out of the callback |
937
1005
  | `scope() must be called within a watch context` inside `peek()` | tracking is off inside `peek` | call `scope()`/`gate()` outside the `peek` callback |
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 |
1007
+ | Every `StoreAsync` change rejects with the same old error | an earlier change failed in the worker | fix that change; replace the store |
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` |
938
1014
 
939
1015
  ## Corrections from the v5 primer
940
1016
 
941
1017
  For maintainers comparing against the previous `SYNTAX_PRIMER.md`. Builders can skip this section.
942
1018
 
943
- | Previous claim | Observed in 8.0.18 and 8.0.19 |
1019
+ | Previous claim | Observed (8.0.18 and 8.0.19 unless a version is named) |
944
1020
  | --- | --- |
945
1021
  | `Injector` is not exported from any public entry point | exported from `j-templates/Utils` |
946
1022
  | `@Computed` returning `this.tasks` won't re-run on item property changes | it re-runs |
947
1023
  | a second un-ID'd `scope()`/`gate()` call silently resolves to the first | unconditional duplicates are independent; conditional ones mis-pair |
948
1024
  | exceptions in children functions/getters propagate uncaught | children-function and `Template()` exceptions are caught and logged; `props:`/`attrs:`/`data:` exceptions halt all rendering |
949
1025
  | children functions can't return `null`/`undefined` | allowed as the sole return value; forbidden inside arrays |
950
- | `Bound()` runs with the DOM attached, after the template renders | runs before `Template()`, host detached and empty |
1026
+ | `Bound()` runs with the DOM attached, after the template renders | before 8.0.23: ran before `Template()`, host detached and empty. Since 8.0.23: runs after the first render; the host is in the document only for the attached root, and `Connected()` marks entry into the document |
951
1027
  | `gate()` and `@Scope` are incompatible | fine for primitive-returning getters |
952
1028
  | `data: this.value` (non-function) in the granular-scopes example | throws `data is not a function` |
953
1029
  | `StoreAsync` data must be JSON-serialisable (no `Date`, `Map`, `Set`) | transport is structured clone: `Date`, `Map`, `Set` survive; class methods and functions do not |
@@ -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
  */
@@ -85,3 +86,12 @@ export class Store {
85
86
  ObservableNode.ApplyDiff(rootObject, results);
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
  }
@@ -188,7 +188,7 @@ export declare namespace ObservableScope {
188
188
  * @param scope The scope to stop watching.
189
189
  * @param callback The callback function to remove.
190
190
  */
191
- function Unwatch<T>(scope: IObservableScope<T>, callback: (scope: IObservableScope<T>) => void): void;
191
+ function Unwatch<T>(scope: IObservableScope<T> | IBasicObservableScope<T>, callback: (scope: IObservableScope<T>) => void): void;
192
192
  /**
193
193
  * Registers a callback to be invoked when the scope's value is updated.
194
194
  * Only applies to dynamic scopes; no-op for static scopes.
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.27",
3
+ "version": "8.1.0",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",