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 +40 -19
- package/Store/Diff/diffSync.d.ts +12 -12
- package/Store/Diff/diffSync.js +15 -12
- package/Store/Diff/diffTree.d.ts +11 -27
- package/Store/Diff/diffTree.js +52 -29
- package/Store/Diff/diffTreeWorker.d.ts +11 -2
- package/Store/Diff/diffTreeWorker.js +37 -20
- package/Store/Store/storeAsync.d.ts +41 -34
- package/Store/Store/storeAsync.js +100 -79
- package/Store/Store/storeSync.js +3 -17
- package/Store/Tree/observableScope.d.ts +1 -1
- package/Utils/asyncQueue.d.ts +3 -0
- package/Utils/asyncQueue.js +22 -4
- package/package.json +1 -1
- package/Store/Diff/diffAsync.d.ts +0 -47
- package/Store/Diff/diffAsync.js +0 -54
- package/Store/Diff/diffTree_v2.d.ts +0 -135
- package/Store/Diff/diffTree_v2.js +0 -445
package/SYNTAX_PRIMER.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# j-templates Syntax Primer
|
|
2
2
|
|
|
3
|
-
Oct
|
|
3
|
+
Oct 13, 2026 · @Jay Landrum
|
|
4
4
|
|
|
5
|
-
Reference for building apps with j-templates 8.0.
|
|
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.
|
|
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
|
|
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. `
|
|
114
|
-
3. `
|
|
115
|
-
4.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
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 |
|
|
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 |
|
package/Store/Diff/diffSync.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
}
|
package/Store/Diff/diffSync.js
CHANGED
|
@@ -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
|
|
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
|
|
25
|
-
|
|
24
|
+
DiffPath(path, value) {
|
|
25
|
+
const diff = this.diffTree.DiffPath(path, value);
|
|
26
|
+
return diff.concat(this.diffTree.RunProjections());
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return this.diffTree.
|
|
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
|
|
36
|
-
|
|
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
|
}
|
package/Store/Diff/diffTree.d.ts
CHANGED
|
@@ -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
|
|
22
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
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
|
package/Store/Diff/diffTree.js
CHANGED
|
@@ -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
|
|
251
|
-
diffResult.push(...UpdateSource(source, PROJECTION_PREFIX + id, result,
|
|
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,
|
|
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,
|
|
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 (
|
|
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
|
|
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 (
|
|
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 `
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
402
|
-
|
|
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 {
|
|
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: "
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|