j-templates 8.0.26 → 8.0.28

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 13, 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.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.
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.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.
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
 
@@ -102,20 +102,27 @@ Component.Attach(document.getElementById("app")!, counter({}));
102
102
  **Members:**
103
103
 
104
104
  - `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.
105
+ - `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
106
  - `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`.
107
+ - `this.Data`, `this.Templates`, `this.Injector`, `this.VNode` (host vNode; `this.VNode.node` is the host element), `this.IsConnected`, `this.Destroyed`.
108
108
  - Never override the constructor. Use field initializers and `Bound()`.
109
109
 
110
- **Lifecycle (verified order):**
110
+ **Lifecycle (verified order, 8.0.23+):**
111
111
 
112
112
  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.
113
+ 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.
114
+ 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.
115
+ 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
116
 
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.
117
+ By default steps 2–4 happen **after** `Attach` returns; with `SYNC_SCHEDULING=true`, before.
118
+
119
+ 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:
120
+
121
+ - Find and wire up the elements you render in `Bound()`.
122
+ - Put work that needs layout (measuring, sizes), focus, or the document in `Connected()`.
123
+ - 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.
124
+
125
+ `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
126
 
120
127
  **Host element.** The name passed to `ToFunction` is the host's tag. `Template()` output renders inside it:
121
128
 
@@ -352,7 +359,7 @@ Tracked array methods: `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `rev
352
359
  syncFilter(value: FilterType) { this.localFilter = value; }
353
360
  ```
354
361
 
355
- - Fires once in `super.Bound()` with the initial value, then on every change.
362
+ - 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
363
  - Several synchronous writes in one tick produce one call with the latest value (`n = 1; n = 2; n = 3` → one call with `3`).
357
364
  - Without `super.Bound()` it never fires.
358
365
  - 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.
@@ -531,7 +538,7 @@ A store holds reactive data under string keys and diffs every write into existin
531
538
  | --- | --- | --- |
532
539
  | Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` |
533
540
  | Diffing | main thread, synchronous | in the `Worker` you supply |
534
- | Writes | `void`; readable immediately | `Promise`; readable only after `await` |
541
+ | Writes | `void`; readable immediately | `Promise`; batched per tick, readable only after `await` |
535
542
  | `Destroy()` | none | required; terminates the worker |
536
543
  | Backs | nothing (`@Computed` uses `ObservableNode` directly) | `@ComputedAsync` |
537
544
 
@@ -547,7 +554,7 @@ Push(key: string, ...items: unknown[]); // append to the array at key
547
554
  Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]); // returns removed items
548
555
  ```
549
556
 
550
- `StoreAsync` has the same methods; the four writers return promises.
557
+ `StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below).
551
558
 
552
559
  ### keyFunc and flattening
553
560
 
@@ -585,6 +592,13 @@ store.Destroy(); // when done
585
592
 
586
593
  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
594
 
595
+ **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.
596
+
597
+ - 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.
598
+ - 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.
599
+ - `await store.Flush()` waits for every change made before the call. Changes made after it go in a later batch.
600
+ - **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.
601
+
588
602
  ### Projections
589
603
 
590
604
  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,6 +619,8 @@ store.Get<number>(`${PROJECTION_PREFIX}activeCount`);
605
619
  - 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
620
  - A projection has no value until every read resolves. It re-runs when a read's value changes.
607
621
  - A cycle or a read of an undefined projection throws at construction.
622
+ - 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).
608
624
 
609
625
  ## ObservableScope and ObservableNode
610
626
 
@@ -895,7 +911,7 @@ Component.Attach(document.getElementById("app")!, app({}));
895
911
  9. **`@State` deep-tracks plain objects and arrays only**, not class instances, `Map` or `Set`.
896
912
  10. **`@Scope` has no equality check**; consumers re-run on every change of its inputs. `gate()` it if it returns a primitive.
897
913
  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`.
914
+ 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
915
  13. **`@Watch` fires immediately** with the initial value, and coalesces synchronous changes into one call.
900
916
  14. **`scope()`, `gate()` and `mapped()` outside a scope throw**, including in `on:` handlers and inside `peek()` callbacks. `peek()` works anywhere.
901
917
  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,7 +920,7 @@ Component.Attach(document.getElementById("app")!, app({}));
904
920
  18. **Native DOM listeners belong on elements you render**, not the host.
905
921
  19. **`svg()` is HTML-namespace.** Use namespaced components or `innerHTML` for SVG.
906
922
  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).
923
+ 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
924
  22. **The package needs a bundler or TypeScript runner**; plain Node cannot import it.
909
925
 
910
926
  23) **Equal-value writes still notify.** Assigning the current value again re-runs every reader. Guard repeated writes or read through `gate()`.
@@ -927,7 +943,9 @@ Component.Attach(document.getElementById("app")!, app({}));
927
943
  | `gate()` doesn't stop re-runs | gated value is a new object/array each time | gate a primitive, or use `@Computed` |
928
944
  | Async value never resolves (shows a Promise) | callback lacks `async` | `async () => …` |
929
945
  | 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(() => …)` |
946
+ | `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 |
947
+ | Measured size is 0, or `focus()` does nothing, in `Bound()` | the host isn't in the document yet | move the work to `Connected()` |
948
+ | `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
949
  | Manual service scope never updates | `ObservableScope.Create` with no reactive reads | `ObservableScope.Basic` |
932
950
  | Test sees old DOM after a write | asynchronous rendering | `SYNC_SCHEDULING=true` or await a timeout |
933
951
  | `ReferenceError: self is not defined` | importing `defaultDiffTreeWorker` on the main thread | write your own worker entry file |
@@ -935,19 +953,22 @@ Component.Attach(document.getElementById("app")!, app({}));
935
953
  | 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
954
  | `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
955
  | `scope() must be called within a watch context` inside `peek()` | tracking is off inside `peek` | call `scope()`/`gate()` outside the `peek` callback |
956
+ | `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
+ | Every `StoreAsync` change rejects with the same old error | an earlier change failed in the worker | fix that change; replace the store |
958
+ | A test's projection never updates | the test drives a `DiffTree` directly | call `tree.RunProjections()` after each write |
938
959
 
939
960
  ## Corrections from the v5 primer
940
961
 
941
962
  For maintainers comparing against the previous `SYNTAX_PRIMER.md`. Builders can skip this section.
942
963
 
943
- | Previous claim | Observed in 8.0.18 and 8.0.19 |
964
+ | Previous claim | Observed (8.0.18 and 8.0.19 unless a version is named) |
944
965
  | --- | --- |
945
966
  | `Injector` is not exported from any public entry point | exported from `j-templates/Utils` |
946
967
  | `@Computed` returning `this.tasks` won't re-run on item property changes | it re-runs |
947
968
  | a second un-ID'd `scope()`/`gate()` call silently resolves to the first | unconditional duplicates are independent; conditional ones mis-pair |
948
969
  | exceptions in children functions/getters propagate uncaught | children-function and `Template()` exceptions are caught and logged; `props:`/`attrs:`/`data:` exceptions halt all rendering |
949
970
  | 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 |
971
+ | `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
972
  | `gate()` and `@Scope` are incompatible | fine for primitive-returning getters |
952
973
  | `data: this.value` (non-function) in the granular-scopes example | throws `data is not a function` |
953
974
  | `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,10 +1,11 @@
1
+ import { JsonDiffResult } from "../../Utils/json";
1
2
  import { DiffSpliceResult, DiffTreeProjectionMap, IDiffTree } from "./diffTree";
2
3
  /**
3
4
  * Synchronous diff implementation.
4
5
  * Computes diffs immediately without queuing or workers.
5
6
  *
6
7
  * @see StoreSync
7
- * @see DiffAsync
8
+ * @see StoreAsync
8
9
  */
9
10
  export declare class DiffSync implements IDiffTree {
10
11
  private diffTree;
@@ -22,15 +23,14 @@ export declare class DiffSync implements IDiffTree {
22
23
  * @param value - The new value to compare
23
24
  * @returns Diff results showing changes
24
25
  */
25
- DiffPath(path: string, value: any, flatten?: boolean): import("../../Utils/json").JsonDiffResult;
26
- /**
27
- * Computes diffs for a batch of path/value pairs.
28
- * @param data - Array of objects with path and value properties
29
- * @returns Combined diff results
30
- */
31
- DiffBatch(data: Array<{
32
- path: string;
33
- value: any;
34
- }>): import("../../Utils/json").JsonDiffResult;
35
- SplicePath(path: string, start: number, deleteCount: number | undefined, items: any[], flatten?: boolean): DiffSpliceResult;
26
+ DiffPath(path: string, value: any): {
27
+ path: (string | number)[];
28
+ value: unknown;
29
+ }[];
30
+ PatchPath(path: string, patch: any): {
31
+ path: (string | number)[];
32
+ value: unknown;
33
+ }[];
34
+ PushPath(path: string, items: any[]): JsonDiffResult;
35
+ SplicePath(path: string, start: number, deleteCount: number | undefined, items: any[]): DiffSpliceResult;
36
36
  }
@@ -4,7 +4,7 @@ import { DiffTree } from "./diffTree";
4
4
  * Computes diffs immediately without queuing or workers.
5
5
  *
6
6
  * @see StoreSync
7
- * @see DiffAsync
7
+ * @see StoreAsync
8
8
  */
9
9
  export class DiffSync {
10
10
  /**
@@ -21,18 +21,21 @@ export class DiffSync {
21
21
  * @param value - The new value to compare
22
22
  * @returns Diff results showing changes
23
23
  */
24
- DiffPath(path, value, flatten = true) {
25
- return this.diffTree.DiffPath(path, value, flatten);
24
+ DiffPath(path, value) {
25
+ const diff = this.diffTree.DiffPath(path, value);
26
+ return diff.concat(this.diffTree.RunProjections());
26
27
  }
27
- /**
28
- * Computes diffs for a batch of path/value pairs.
29
- * @param data - Array of objects with path and value properties
30
- * @returns Combined diff results
31
- */
32
- DiffBatch(data) {
33
- return this.diffTree.DiffBatch(data);
28
+ PatchPath(path, patch) {
29
+ const diff = this.diffTree.PatchPath(path, patch);
30
+ return diff.concat(this.diffTree.RunProjections());
31
+ }
32
+ PushPath(path, items) {
33
+ const diff = this.diffTree.PushPath(path, items);
34
+ return diff.concat(this.diffTree.RunProjections());
34
35
  }
35
- SplicePath(path, start, deleteCount, items, flatten) {
36
- return this.diffTree.SplicePath(path, start, deleteCount, items, flatten);
36
+ SplicePath(path, start, deleteCount, items) {
37
+ const spliceResult = this.diffTree.SplicePath(path, start, deleteCount, items);
38
+ spliceResult.diffResult.push(...this.diffTree.RunProjections());
39
+ return spliceResult;
37
40
  }
38
41
  }
@@ -3,23 +3,16 @@ import { type JsonDiffResult } from "../../Utils/json";
3
3
  * Interface for diff tree operations.
4
4
  */
5
5
  export interface IDiffTree {
6
- /**
7
- * Computes diffs for a batch of path/value pairs.
8
- * @param data - Array of objects with path and value properties
9
- * @returns Combined diff results
10
- */
11
- DiffBatch(data: Array<{
12
- path: string;
13
- value: any;
14
- }>): JsonDiffResult;
15
6
  /**
16
7
  * Computes the diff between a new value and the current value at a path.
17
8
  * @param path - Dot-separated path to the value
18
9
  * @param value - The new value to compare
19
10
  * @returns Diff results showing changes
20
11
  */
21
- DiffPath(path: string, value: any, flatten?: boolean): JsonDiffResult;
22
- SplicePath(path: string, start: number, deleteCount: number | undefined, items: any[], flatten?: boolean): DiffSpliceResult;
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;
23
16
  }
24
17
  /**
25
18
  * Constructor interface for IDiffTree.
@@ -70,12 +63,13 @@ export type DiffSpliceResult<T = unknown> = {
70
63
  /** Required by the splice operation **/
71
64
  start: number;
72
65
  deleteCount: number;
66
+ items: any[];
73
67
  spliceResult: any[];
74
68
  diffResult: JsonDiffResult;
75
69
  };
76
70
  /**
77
71
  * Diff tree implementation. Maintains root state and computes diffs for path/value updates.
78
- * Used directly by `DiffSync`, and is the building block for a `DiffAsync` worker entry
72
+ * Used directly by `DiffSync`, and is the building block for a `StoreAsync` worker entry
79
73
  * file: construct one with the desired `keyFunc`/`projections` and connect it to the worker
80
74
  * thread with `ConnectWorkerToDiffTree` (see `defaultDiffTreeWorker.ts` for the no-argument
81
75
  * case, or a custom entry file when either is needed).
@@ -94,27 +88,17 @@ export declare class DiffTree {
94
88
  constructor(keyFunc?: {
95
89
  (val: any): string;
96
90
  }, projections?: DiffTreeProjectionMap);
97
- private RunProjections;
98
- /**
99
- * Computes diffs for a batch of path/value pairs.
100
- * @param data - Array of objects with path and value properties
101
- * @returns Combined diff results
102
- */
103
- DiffBatch(data: Array<{
104
- path: string;
105
- value: any;
106
- }>): {
107
- path: (string | number)[];
108
- value: unknown;
109
- }[];
91
+ RunProjections(): JsonDiffResult;
110
92
  /**
111
93
  * Computes the diff between a new value and the current value at a path.
112
94
  * @param path - Dot-separated path to the value
113
95
  * @param value - The new value to compare
114
96
  * @returns Diff results showing changes
115
97
  */
116
- DiffPath(path: string, value: any, flatten?: boolean): JsonDiffResult;
117
- SplicePath(path: string, start: number, deleteCount: number, items: any[], flatten?: boolean): DiffSpliceResult<unknown>;
98
+ DiffPath(path: string, value: any): JsonDiffResult;
99
+ PatchPath(path: string, patch: any): JsonDiffResult;
100
+ PushPath(path: string, items: unknown[]): JsonDiffResult;
101
+ SplicePath(path: string, start: number, deleteCount: number, items: any[]): DiffSpliceResult<unknown>;
118
102
  /**
119
103
  * Retrieves the current value at a path.
120
104
  * @param path - Dot-separated path to the value
@@ -1,5 +1,5 @@
1
1
  import { ArraysEqual } from "../../Utils/array";
2
- import { JsonDeepClone, JsonDiff, JsonType } from "../../Utils/json";
2
+ import { JsonDeepClone, JsonDiff, JsonMerge, JsonType } from "../../Utils/json";
3
3
  /** Reserved key prefix a projection's result is stored under: `$projection_<id>`. */
4
4
  export const PROJECTION_PREFIX = "$projection_";
5
5
  /**
@@ -247,8 +247,8 @@ function RunProjections(source, keyFunc, projections, memo) {
247
247
  memo.set(id, values);
248
248
  // The same diff-and-apply UpdateSource uses for any other write, so a keyed entity
249
249
  // embedded in a projection's result is flattened to its own root like any other write,
250
- // and the diff reported here is unfiltered, the same as DiffPath/DiffBatch return.
251
- diffResult.push(...UpdateSource(source, PROJECTION_PREFIX + id, result, true, keyFunc));
250
+ // and the diff reported here is unfiltered, the same as DiffPath returns.
251
+ diffResult.push(...UpdateSource(source, PROJECTION_PREFIX + id, result, keyFunc));
252
252
  }
253
253
  }
254
254
  }
@@ -294,18 +294,18 @@ function ResolveKeyPath(source, path, keyFunc) {
294
294
  * @param keyFunc - Optional function to extract keys from objects
295
295
  * @returns Diff results showing all changes made
296
296
  */
297
- function UpdateSource(source, path, value, flatten, keyFunc) {
297
+ function UpdateSource(source, path, value, keyFunc) {
298
298
  const diffResult = [];
299
299
  if (keyFunc) {
300
300
  const keyPath = ResolveKeyPath(source, path, keyFunc);
301
301
  if (keyPath !== path) {
302
- const keyDiffResult = UpdateSource(source, keyPath, value, flatten, keyFunc);
302
+ const keyDiffResult = UpdateSource(source, keyPath, value, keyFunc);
303
303
  diffResult.push(...keyDiffResult);
304
304
  }
305
305
  }
306
306
  const [sourceValue] = GetPathValue(source, path);
307
307
  JsonDiff(value, sourceValue, path, diffResult);
308
- if (flatten && keyFunc) {
308
+ if (keyFunc) {
309
309
  let flattened = {};
310
310
  flattened = FlattenValue(flattened, value, keyFunc);
311
311
  flattened = JsonDeepClone(flattened);
@@ -319,7 +319,37 @@ function UpdateSource(source, path, value, flatten, keyFunc) {
319
319
  }
320
320
  return diffResult;
321
321
  }
322
- function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc) {
322
+ function PushSource(source, path, items, keyFunc) {
323
+ const [sourceValue, found] = GetPathValue(source, path);
324
+ if (!found)
325
+ throw `Value not found at path ${path}`;
326
+ if (!Array.isArray(sourceValue))
327
+ throw `Value found at path ${path} is not an array`;
328
+ const results = items.map(function (value, index) {
329
+ return {
330
+ path: `${path}.${sourceValue.length + index}`.split('.'),
331
+ value
332
+ };
333
+ });
334
+ sourceValue.push(...items);
335
+ if (items.length > 0)
336
+ results.push({
337
+ path: `${path}.length`.split('.'),
338
+ value: sourceValue.length
339
+ });
340
+ if (keyFunc) {
341
+ const flattened = JsonDeepClone(FlattenValue({}, items, keyFunc));
342
+ const keys = Object.keys(flattened);
343
+ for (let x = 0; x < keys.length; x++)
344
+ JsonDiff(flattened[keys[x]], source[keys[x]], keys[x], results);
345
+ }
346
+ const filteredDiffResult = results.filter((diff) => diff.value !== undefined);
347
+ for (let x = 0; x < filteredDiffResult.length; x++) {
348
+ SetPathValue(source, filteredDiffResult[x].path, filteredDiffResult[x].value);
349
+ }
350
+ return results;
351
+ }
352
+ function SpliceSource(source, path, start, deleteCount, items, keyFunc) {
323
353
  const [sourceValue, found] = GetPathValue(source, path);
324
354
  if (!found)
325
355
  throw `Value not found at path ${path}`;
@@ -329,13 +359,14 @@ function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc)
329
359
  const spliceResult = {
330
360
  path: path.split("."),
331
361
  start,
362
+ items,
332
363
  deleteCount,
333
364
  spliceResult: null,
334
365
  diffResult: [],
335
366
  };
336
367
  spliceResult.spliceResult = sourceValue.splice(start, deleteCount, ...items);
337
368
  InvalidateSnapshotForValue(sourceValue);
338
- if (flatten && keyFunc) {
369
+ if (keyFunc) {
339
370
  // Register the keyed entities the new items carry, diffed against the roots
340
371
  // already held: a new entity is added whole, one re-inserted unchanged (an undo)
341
372
  // produces no diff, so its readers aren't woken.
@@ -352,7 +383,7 @@ function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc)
352
383
  }
353
384
  /**
354
385
  * Diff tree implementation. Maintains root state and computes diffs for path/value updates.
355
- * Used directly by `DiffSync`, and is the building block for a `DiffAsync` worker entry
386
+ * Used directly by `DiffSync`, and is the building block for a `StoreAsync` worker entry
356
387
  * file: construct one with the desired `keyFunc`/`projections` and connect it to the worker
357
388
  * thread with `ConnectWorkerToDiffTree` (see `defaultDiffTreeWorker.ts` for the no-argument
358
389
  * case, or a custom entry file when either is needed).
@@ -375,33 +406,25 @@ export class DiffTree {
375
406
  return [];
376
407
  return RunProjections(this.rootState, this.keyFunc, this.projectionOrder, this.projectionMemo);
377
408
  }
378
- /**
379
- * Computes diffs for a batch of path/value pairs.
380
- * @param data - Array of objects with path and value properties
381
- * @returns Combined diff results
382
- */
383
- DiffBatch(data) {
384
- const results = data
385
- .map(({ path, value }) => UpdateSource(this.rootState, path, value, true, this.keyFunc))
386
- .flat(1);
387
- results.push(...this.RunProjections());
388
- return results;
389
- }
390
409
  /**
391
410
  * Computes the diff between a new value and the current value at a path.
392
411
  * @param path - Dot-separated path to the value
393
412
  * @param value - The new value to compare
394
413
  * @returns Diff results showing changes
395
414
  */
396
- DiffPath(path, value, flatten = true) {
397
- const results = UpdateSource(this.rootState, path, value, flatten, this.keyFunc);
398
- results.push(...this.RunProjections());
399
- return results;
415
+ DiffPath(path, value) {
416
+ return UpdateSource(this.rootState, path, value, this.keyFunc);
417
+ }
418
+ PatchPath(path, patch) {
419
+ const value = this.Snapshot(path);
420
+ const merged = JsonMerge(value, patch);
421
+ return UpdateSource(this.rootState, path, merged, this.keyFunc);
422
+ }
423
+ PushPath(path, items) {
424
+ return PushSource(this.rootState, path, items, this.keyFunc);
400
425
  }
401
- SplicePath(path, start, deleteCount, items, flatten = true) {
402
- const spliceResult = SpliceSource(this.rootState, path, start, deleteCount, items, flatten, this.keyFunc);
403
- spliceResult.diffResult.push(...this.RunProjections());
404
- return spliceResult;
426
+ SplicePath(path, start, deleteCount, items) {
427
+ return SpliceSource(this.rootState, path, start, deleteCount, items, this.keyFunc);
405
428
  }
406
429
  /**
407
430
  * Retrieves the current value at a path.
@@ -1,7 +1,16 @@
1
- import { DiffTree } from "./diffTree";
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
+ })[];
2
11
  export interface IDiffMethod {
3
12
  /** The method to call */
4
- method: "create" | "diffpath" | "splicepath" | "diffbatch" | "updatepath" | "getpath";
13
+ method: "diffpath" | "splicepath" | "pushpath" | "patchpath";
5
14
  /** Arguments for the method call */
6
15
  arguments: Array<any>;
7
16
  }
@@ -1,27 +1,44 @@
1
1
  export function ConnectWorkerToDiffTree(diffTree, worker) {
2
2
  worker.onmessage = function (event) {
3
3
  const data = event.data;
4
- switch (data.method) {
5
- case "diffpath": {
6
- const diff = diffTree.DiffPath(data.arguments[0], data.arguments[1], data.arguments[2]);
7
- worker.postMessage(diff);
8
- break;
9
- }
10
- case "splicepath": {
11
- const diff = diffTree.SplicePath(data.arguments[0], data.arguments[1], data.arguments[2], data.arguments[3], data.arguments[4]);
12
- worker.postMessage(diff);
13
- break;
14
- }
15
- case "diffbatch": {
16
- const diff = diffTree.DiffBatch(data.arguments[0]);
17
- worker.postMessage(diff);
18
- break;
19
- }
20
- case "getpath": {
21
- const ret = diffTree.GetPath(data.arguments[0]);
22
- worker.postMessage(ret);
23
- break;
4
+ const result = new Array(data.length + 1);
5
+ for (let x = 0; x < data.length; x++) {
6
+ const call = data[x];
7
+ switch (call.method) {
8
+ case "diffpath": {
9
+ result[x] = {
10
+ type: "diff",
11
+ result: diffTree.DiffPath(call.arguments[0], call.arguments[1])
12
+ };
13
+ break;
14
+ }
15
+ case "splicepath": {
16
+ result[x] = {
17
+ type: "splice",
18
+ result: diffTree.SplicePath(call.arguments[0], call.arguments[1], call.arguments[2], call.arguments[3])
19
+ };
20
+ break;
21
+ }
22
+ case "pushpath": {
23
+ result[x] = {
24
+ type: "diff",
25
+ result: diffTree.PushPath(call.arguments[0], call.arguments[1])
26
+ };
27
+ break;
28
+ }
29
+ case "patchpath": {
30
+ result[x] = {
31
+ type: "diff",
32
+ result: diffTree.PatchPath(call.arguments[0], call.arguments[1])
33
+ };
34
+ break;
35
+ }
24
36
  }
25
37
  }
38
+ result[result.length - 1] = {
39
+ type: "diff",
40
+ result: diffTree.RunProjections()
41
+ };
42
+ worker.postMessage(result);
26
43
  };
27
44
  }