j-templates 8.0.28 → 8.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SYNTAX_PRIMER.md +71 -16
- package/Store/Diff/diffSync.d.ts +1 -1
- package/Store/Diff/diffTree.d.ts +2 -69
- package/Store/Diff/diffTree.js +1 -2
- package/Store/Diff/diffTree.types.d.ts +105 -0
- package/Store/Diff/diffTree.types.js +12 -0
- package/Store/Diff/diffTreeWorker.d.ts +1 -16
- package/Store/Diff/diffTreeWriter.d.ts +103 -0
- package/Store/Diff/diffTreeWriter.js +188 -0
- package/Store/Diff/index.d.ts +1 -0
- package/Store/Diff/index.js +1 -0
- package/Store/Store/pushStore.d.ts +57 -0
- package/Store/Store/pushStore.js +96 -0
- package/Store/Store/store.d.ts +20 -10
- package/Store/Store/store.js +17 -7
- package/Store/Store/storeSync.d.ts +2 -2
- package/Store/Tree/observableNode.js +36 -24
- package/Store/index.d.ts +2 -1
- package/Store/index.js +2 -1
- package/package.json +1 -1
package/SYNTAX_PRIMER.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# j-templates Syntax Primer
|
|
2
2
|
|
|
3
|
-
Oct
|
|
3
|
+
Oct 17, 2026 · @Jay Landrum
|
|
4
4
|
|
|
5
|
-
Reference for building apps with j-templates 8.0.
|
|
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.
|
|
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
|
|
62
|
+
**Deep imports used below:** `j-templates/Store/Diff/diffTree` (`DiffTree`), `j-templates/Store/Diff/diffTreeWorker` (`ConnectWorkerToDiffTree`), `j-templates/Store/Diff/diffTreeWriter` (`DiffTreeWriter`), `j-templates/Node/vNode` (`vNode.Destroy`). `j-templates/Store/Diff/defaultDiffTreeWorker` runs worker code on import and throws outside a worker (`self is not defined`).
|
|
62
63
|
|
|
63
64
|
**Element functions** (all verified present):
|
|
64
65
|
|
|
@@ -532,15 +533,15 @@ class Injector {
|
|
|
532
533
|
|
|
533
534
|
## Store
|
|
534
535
|
|
|
535
|
-
A store holds reactive data under string keys and diffs every write into existing objects in place. Use `StoreSync` unless diffing large data blocks the main thread.
|
|
536
|
+
A store holds reactive data under string keys and diffs every write into existing objects in place. Use `StoreSync` unless diffing large data blocks the main thread. Use `PushStore` when a worker produces the data itself, such as the result of a computation it runs.
|
|
536
537
|
|
|
537
|
-
| | `StoreSync` | `StoreAsync` |
|
|
538
|
-
| --- | --- | --- |
|
|
539
|
-
| Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` |
|
|
540
|
-
| Diffing | main thread, synchronous | in the `Worker` you supply |
|
|
541
|
-
| Writes | `void`; readable immediately | `Promise`; batched per tick, readable only after `await` |
|
|
542
|
-
| `Destroy()` | none | required; terminates the worker |
|
|
543
|
-
| Backs | nothing (`@Computed` uses `ObservableNode` directly) | `@ComputedAsync` |
|
|
538
|
+
| | `StoreSync` | `StoreAsync` | `PushStore` |
|
|
539
|
+
| --- | --- | --- | --- |
|
|
540
|
+
| Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` | `new PushStore(source, keyFunc?, cloneData?)` |
|
|
541
|
+
| Diffing | main thread, synchronous | in the `Worker` you supply | in the `DiffTreeWriter` that pushes to it |
|
|
542
|
+
| Writes | `void`; readable immediately | `Promise`; batched per tick, readable only after `await` | none; read-only. Changes arrive when the writer calls `Commit()` |
|
|
543
|
+
| `Destroy()` | none | required; terminates the worker | required; stops listening, leaves the worker running |
|
|
544
|
+
| Backs | nothing (`@Computed` uses `ObservableNode` directly) | `@ComputedAsync` | nothing |
|
|
544
545
|
|
|
545
546
|
### API
|
|
546
547
|
|
|
@@ -554,7 +555,7 @@ Push(key: string, ...items: unknown[]); // append to the array at key
|
|
|
554
555
|
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]); // returns removed items
|
|
555
556
|
```
|
|
556
557
|
|
|
557
|
-
`StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below).
|
|
558
|
+
`StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below). `PushStore` has only `Get` and `Has`, plus `Error` (see PushStore and DiffTreeWriter).
|
|
558
559
|
|
|
559
560
|
### keyFunc and flattening
|
|
560
561
|
|
|
@@ -604,7 +605,7 @@ Data crosses the worker boundary by structured clone. Class instances arrive as
|
|
|
604
605
|
A projection is a derived value the store computes and stores under `$projection_<name>`. Pass them as `StoreSync`'s second argument (for `StoreAsync`, to the worker's `DiffTree`).
|
|
605
606
|
|
|
606
607
|
```typescript
|
|
607
|
-
import { PROJECTION_PREFIX } from "j-templates/Store/Diff
|
|
608
|
+
import { PROJECTION_PREFIX } from "j-templates/Store/Diff"; // "$projection_"
|
|
608
609
|
|
|
609
610
|
const store = new StoreSync((v: any) => v?.id, {
|
|
610
611
|
activeCount: { reads: ["todos"], projection: (keys, todos) => todos.filter((t) => !t.done).length },
|
|
@@ -620,7 +621,38 @@ store.Get<number>(`${PROJECTION_PREFIX}activeCount`);
|
|
|
620
621
|
- A projection has no value until every read resolves. It re-runs when a read's value changes.
|
|
621
622
|
- A cycle or a read of an undefined projection throws at construction.
|
|
622
623
|
- A dotted read can pass through a keyed entity (`"doc.owner.name"`, where `owner` has an id). It reads the entity's own root, so a `Patch` to the entity reruns the projection with the new value (verified; before 8.0.24 such a read returned the stale embedded copy).
|
|
623
|
-
- `StoreSync` and the `StoreAsync` worker run projections for you
|
|
624
|
+
- `StoreSync` and the `StoreAsync` worker run projections for you, and a `DiffTreeWriter` runs them in `Commit()`. `DiffTree.DiffPath`, `PatchPath`, `PushPath` and `SplicePath` don't (8.0.27): code that drives a `DiffTree` directly, such as a test, must call `tree.RunProjections()` after its writes (verified).
|
|
625
|
+
|
|
626
|
+
### PushStore and DiffTreeWriter
|
|
627
|
+
|
|
628
|
+
`StoreAsync` only changes when the main thread writes. When a worker produces the data itself, the worker owns the tree instead: it writes into a `DiffTreeWriter` and calls `Commit()`, which runs projections and posts every change since the last commit as one message. A `PushStore` on the main thread applies each message as it arrives (8.1.0).
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// engine-worker.ts (bundled as its own worker entry)
|
|
632
|
+
import { DiffTreeWriter } from "j-templates/Store/Diff/diffTreeWriter";
|
|
633
|
+
const tree = new DiffTreeWriter(self, (v: any) => v?.id, projections);
|
|
634
|
+
self.onmessage = async (e) => {
|
|
635
|
+
const results = await compute(e.data); // the worker's own work
|
|
636
|
+
tree.DiffPath("results", results);
|
|
637
|
+
tree.DiffPath("meta", { finishedAt: Date.now() });
|
|
638
|
+
tree.Commit(); // one message: both writes and the projections
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// main thread
|
|
642
|
+
const worker = new Worker(new URL("./engine-worker.ts", import.meta.url), { type: "module" });
|
|
643
|
+
const store = new PushStore(worker, (v: any) => v?.id); // before the first Commit()
|
|
644
|
+
store.Get("results"); // reactive; undefined until the first commit
|
|
645
|
+
store.Destroy(); // stops listening; terminate the worker yourself
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
- The writer is a `DiffTree`: write with `DiffPath`, `PatchPath`, `PushPath` and `SplicePath`. Nothing reaches the store until `Commit()`, and a `Commit()` with no changes posts nothing (verified).
|
|
649
|
+
- Projections run once per `Commit()`, after the batch's writes, and their results arrive in the same message, so readers never see a projection out of step with the writes it came from (verified). Calling `RunProjections()` on a writer throws (from source).
|
|
650
|
+
- Use the same `keyFunc` on both sides. Keyed entities resolve through their own roots, as in any store (verified).
|
|
651
|
+
- `source` is any `EventTarget` that delivers the writer's messages as `"message"` events: a `Worker`, a `MessagePort` (started for you), or a plain `EventTarget` on the same thread. The store ignores messages whose `type` isn't `DIFF_TREE_EVENT` or `DIFF_TREE_ERROR` (verified). It listens with `addEventListener` and never sets `onmessage` (from source). The worker can carry its own requests and replies on the same channel.
|
|
652
|
+
- **Create the store before the first `Commit()`.** Messages are diffs against the writer's tree, so a store created later misses the earlier changes and holds partial roots (from source). There is no catch-up message.
|
|
653
|
+
- **A failed change doesn't stop either side.** After a write throws, every later write before `Commit()` throws the same error. `Commit()` still sends the writes made before the failure, skips projections, then posts `DIFF_TREE_ERROR`. The store applies the changes and sets `Error` (verified). The next commit brings projections up to date.
|
|
654
|
+
- `store.Error` is reactive: the last commit's error, or `null`. It's cleared when a later commit's changes arrive (verified).
|
|
655
|
+
- `cloneData` (default `false`): a `Worker` or `MessagePort` already delivers a structured-clone copy, so no copy is made. A same-thread `EventTarget` delivers the writer's own objects; post copies, or pass `true`. The flag only covers new roots and splices; changes to an existing root are always copied (from source).
|
|
624
656
|
|
|
625
657
|
## ObservableScope and ObservableNode
|
|
626
658
|
|
|
@@ -738,6 +770,23 @@ function fakeWorker(tree = new DiffTree()) {
|
|
|
738
770
|
const store = new StoreAsync(fakeWorker(new DiffTree((v: any) => v?.id)), (v: any) => v?.id);
|
|
739
771
|
```
|
|
740
772
|
|
|
773
|
+
4. For `PushStore`, use a plain `EventTarget` as the channel. Deliver copies a tick later, as a worker would, and await a timeout before reading:
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
import { DiffTreeWriter } from "j-templates/Store/Diff/diffTreeWriter";
|
|
777
|
+
|
|
778
|
+
const target = new EventTarget();
|
|
779
|
+
const port = { postMessage: (d: unknown) => setTimeout(() =>
|
|
780
|
+
target.dispatchEvent(new MessageEvent("message", { data: structuredClone(d) }))) };
|
|
781
|
+
const writer = new DiffTreeWriter(port);
|
|
782
|
+
const store = new PushStore(target);
|
|
783
|
+
|
|
784
|
+
writer.DiffPath("a", { value: 1 });
|
|
785
|
+
writer.Commit();
|
|
786
|
+
await new Promise((r) => setTimeout(r));
|
|
787
|
+
store.Get<any>("a").value; // 1
|
|
788
|
+
```
|
|
789
|
+
|
|
741
790
|
jsdom has no `innerText`; use `textContent` in tests.
|
|
742
791
|
|
|
743
792
|
## Worked example: Smart Tasks
|
|
@@ -926,6 +975,7 @@ Component.Attach(document.getElementById("app")!, app({}));
|
|
|
926
975
|
23) **Equal-value writes still notify.** Assigning the current value again re-runs every reader. Guard repeated writes or read through `gate()`.
|
|
927
976
|
24) **`JSON.stringify(node)` and `ObservableNode.Snapshot(node)` don't subscribe.** Read properties directly, or `ObservableNode.Clone(node)` to track everything.
|
|
928
977
|
25) **Don't update a scope from its own emit.** Calling `ObservableScope.Update(s)` from a `Watch(s, …)` callback, or any write that re-emits the emitter currently notifying, throws `Emitter.Emit: re-entry during emit is not supported` (verified). Defer the write (`queueMicrotask`) or restructure.
|
|
978
|
+
26) **A `PushStore` sees only what's committed after it exists.** Create it before the writer's first `Commit()`, and call `Commit()` after every batch of writes.
|
|
929
979
|
|
|
930
980
|
## Debugging
|
|
931
981
|
|
|
@@ -956,6 +1006,11 @@ Component.Attach(document.getElementById("app")!, app({}));
|
|
|
956
1006
|
| `Key not found in store` from `StoreAsync.Patch` right after a `Write` | the key is an entity id flattened out of a write that hasn't been applied yet | await the `Write` first |
|
|
957
1007
|
| Every `StoreAsync` change rejects with the same old error | an earlier change failed in the worker | fix that change; replace the store |
|
|
958
1008
|
| A test's projection never updates | the test drives a `DiffTree` directly | call `tree.RunProjections()` after each write |
|
|
1009
|
+
| `PushStore` never gets the writer's data | the worker wrote but didn't call `Commit()` | call `Commit()` after each batch |
|
|
1010
|
+
| `PushStore` holds partial or missing roots that the writer has | the store was created after the writer's first `Commit()` | create the store with the worker, before any commit |
|
|
1011
|
+
| Every `DiffTreeWriter` write throws the same old error | an earlier write in the batch failed | call `Commit()`: it sends the applied writes and the error, and the next batch starts clean |
|
|
1012
|
+
| `RunProjections` throws on a `DiffTreeWriter` | projections run in `Commit()` | call `Commit()` instead |
|
|
1013
|
+
| `PROJECTION_PREFIX` is `undefined`, projection keys read `"undefined<id>"` | imported from `j-templates/Store/Diff/diffTree`, its path before 8.1.0 | import it from `j-templates/Store/Diff` |
|
|
959
1014
|
|
|
960
1015
|
## Corrections from the v5 primer
|
|
961
1016
|
|
package/Store/Diff/diffSync.d.ts
CHANGED
|
@@ -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.
|
package/Store/Diff/diffTree.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/Store/Diff/diffTree.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ArraysEqual } from "../../Utils/array";
|
|
2
2
|
import { JsonDeepClone, JsonDiff, JsonMerge, JsonType } from "../../Utils/json";
|
|
3
|
-
|
|
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 {
|
|
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
|
+
}
|
package/Store/Store/store.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { JsonDiffResult } from "../../Utils/json";
|
|
2
2
|
/**
|
|
3
|
-
* Base class for observable data
|
|
4
|
-
*
|
|
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
|
|
7
|
-
* @see
|
|
7
|
+
* @see Store
|
|
8
|
+
* @see PushStore
|
|
8
9
|
*/
|
|
9
|
-
export declare abstract class
|
|
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
|
|
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
|
+
}
|
package/Store/Store/store.js
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
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
|
|
8
|
-
* @see
|
|
8
|
+
* @see Store
|
|
9
|
+
* @see PushStore
|
|
9
10
|
*/
|
|
10
|
-
export class
|
|
11
|
+
export class ReadStore {
|
|
11
12
|
/**
|
|
12
|
-
* Creates an instance of
|
|
13
|
+
* Creates an instance of ReadStore.
|
|
13
14
|
* @param keyFunc Optional function to generate a key for a given data value.
|
|
14
15
|
* When provided, enables alias functionality where values can reference other root objects.
|
|
15
16
|
*/
|
|
@@ -82,6 +83,15 @@ export class Store {
|
|
|
82
83
|
this.rootMap.set(rootPath, newRootObject);
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
ObservableNode.ApplyDiff(rootObject, results);
|
|
86
|
+
ObservableNode.ApplyDiff(rootObject, results, this.cloneData);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Base class for observable data store management.
|
|
91
|
+
* Stores root objects as ObservableNode instances and manages updates through diff operations.
|
|
92
|
+
*
|
|
93
|
+
* @see StoreSync
|
|
94
|
+
* @see StoreAsync
|
|
95
|
+
*/
|
|
96
|
+
export class Store extends ReadStore {
|
|
97
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DiffTreeProjectionMap } from "../Diff/diffTree";
|
|
1
|
+
import { DiffTreeProjectionMap } from "../Diff/diffTree.types";
|
|
2
2
|
import { Store } from "./store";
|
|
3
3
|
/**
|
|
4
4
|
* StoreSync class extends the base Store class to provide synchronous data management operations.
|
|
@@ -70,5 +70,5 @@ export declare class StoreSync extends Store {
|
|
|
70
70
|
* @param items Optional elements to insert into the array.
|
|
71
71
|
* @returns The array of deleted elements.
|
|
72
72
|
*/
|
|
73
|
-
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]):
|
|
73
|
+
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): unknown[];
|
|
74
74
|
}
|
|
@@ -6,8 +6,8 @@ const NODE_PROXY = Symbol("NODE_PROXY");
|
|
|
6
6
|
const NODE_SNAPSHOT = Symbol("NODE_SNAPSHOT");
|
|
7
7
|
const toJSON = "toJSON";
|
|
8
8
|
const IS_NODE = Symbol("IS_NODE");
|
|
9
|
+
const ARRAY_SCOPE = Symbol("ARRAY_SCOPE");
|
|
9
10
|
const OBJECT_SCOPE = Symbol("OBJECT_SCOPE");
|
|
10
|
-
const ITERATOR_SCOPE = Symbol("ITERATOR_SCOPE");
|
|
11
11
|
function Identity(value) {
|
|
12
12
|
return value;
|
|
13
13
|
}
|
|
@@ -34,34 +34,37 @@ function InvalidateSnapshotForValue(value) {
|
|
|
34
34
|
InvalidateSnapshotForValue(parent);
|
|
35
35
|
}
|
|
36
36
|
function getOwnPropertyDescriptor(target, prop) {
|
|
37
|
+
ObservableScope.Touch(target[OBJECT_SCOPE]);
|
|
37
38
|
const descriptor = Object.getOwnPropertyDescriptor(target[NODE_VALUE], prop);
|
|
38
|
-
return {
|
|
39
|
+
return descriptor && {
|
|
39
40
|
...descriptor,
|
|
40
41
|
configurable: true,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
function getOwnPropertyDescriptorArray(target, prop) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
ObservableScope.Touch(target[ARRAY_SCOPE]);
|
|
46
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
|
|
47
|
+
const nodeDescriptor = Object.getOwnPropertyDescriptor(target[NODE_VALUE], prop);
|
|
48
|
+
return nodeDescriptor && {
|
|
49
|
+
...nodeDescriptor,
|
|
50
|
+
configurable: descriptor?.configurable ?? true,
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
function has(value, prop) {
|
|
51
|
-
ObservableScope.Touch(value[
|
|
54
|
+
ObservableScope.Touch(value[OBJECT_SCOPE]);
|
|
52
55
|
return Object.hasOwn(value[NODE_VALUE], prop);
|
|
53
56
|
}
|
|
54
57
|
function hasArray(value, prop) {
|
|
55
|
-
ObservableScope.Touch(value[
|
|
58
|
+
ObservableScope.Touch(value[ARRAY_SCOPE]);
|
|
56
59
|
return Object.hasOwn(value[NODE_VALUE], prop);
|
|
57
60
|
}
|
|
58
61
|
function ownKeys(value) {
|
|
59
|
-
ObservableScope.Touch(value[
|
|
62
|
+
ObservableScope.Touch(value[OBJECT_SCOPE]);
|
|
60
63
|
return Object.keys(value[NODE_VALUE]);
|
|
61
64
|
}
|
|
62
65
|
function ownKeysArray(value) {
|
|
63
|
-
ObservableScope.Touch(value[
|
|
64
|
-
return
|
|
66
|
+
ObservableScope.Touch(value[ARRAY_SCOPE]);
|
|
67
|
+
return Reflect.ownKeys(value[NODE_VALUE]);
|
|
65
68
|
}
|
|
66
69
|
function UnwrapProxy(value, type = JsonType(value)) {
|
|
67
70
|
if (type === "value")
|
|
@@ -106,20 +109,30 @@ function CloneProxy(value, type = JsonType(value)) {
|
|
|
106
109
|
}
|
|
107
110
|
return value;
|
|
108
111
|
}
|
|
109
|
-
function GetPropertyScope(object, prop) {
|
|
110
|
-
|
|
112
|
+
function GetPropertyScope(object, prop, create = true) {
|
|
113
|
+
if (!Object.hasOwn(object, prop)) {
|
|
114
|
+
if (!create)
|
|
115
|
+
return undefined;
|
|
116
|
+
Object.defineProperty(object, prop, {
|
|
117
|
+
value: ObservableScope.Basic(Property.bind(null, object[NODE_VALUE], prop)),
|
|
118
|
+
writable: true,
|
|
119
|
+
enumerable: true,
|
|
120
|
+
configurable: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return object[prop];
|
|
111
124
|
}
|
|
112
125
|
function SetObjectValue(object, prop, value) {
|
|
113
126
|
const propExists = Object.hasOwn(object[NODE_VALUE], prop);
|
|
114
127
|
object[NODE_VALUE][prop] = value;
|
|
115
128
|
InvalidateSnapshotForValue(object[NODE_VALUE]);
|
|
116
|
-
ObservableScope.Update(object[prop]);
|
|
117
|
-
!propExists && ObservableScope.Update(object[
|
|
129
|
+
Object.hasOwn(object, prop) && ObservableScope.Update(object[prop]);
|
|
130
|
+
!propExists && ObservableScope.Update(object[OBJECT_SCOPE]);
|
|
118
131
|
}
|
|
119
132
|
function SetArrayValue(object, prop, value) {
|
|
120
133
|
object[NODE_VALUE][prop] = value;
|
|
121
134
|
InvalidateSnapshotForValue(object[NODE_VALUE]);
|
|
122
|
-
ObservableScope.Update(object[
|
|
135
|
+
ObservableScope.Update(object[ARRAY_SCOPE]);
|
|
123
136
|
}
|
|
124
137
|
function CreateProxyFactory(alias) {
|
|
125
138
|
/**
|
|
@@ -174,7 +187,7 @@ function CreateProxyFactory(alias) {
|
|
|
174
187
|
const wrapper = Object.assign([], {
|
|
175
188
|
[NODE_VALUE]: value,
|
|
176
189
|
[NODE_PROXY]: null,
|
|
177
|
-
[
|
|
190
|
+
[ARRAY_SCOPE]: ObservableScope.Basic(Identity.bind(null, value))
|
|
178
191
|
});
|
|
179
192
|
const proxy = wrapper[NODE_PROXY] = new Proxy(wrapper, {
|
|
180
193
|
get: ArrayProxyGetter,
|
|
@@ -217,7 +230,7 @@ function CreateProxyFactory(alias) {
|
|
|
217
230
|
case NODE_SNAPSHOT:
|
|
218
231
|
return CreateSnapshot(object[NODE_VALUE]);
|
|
219
232
|
default: {
|
|
220
|
-
const scope = object[
|
|
233
|
+
const scope = object[ARRAY_SCOPE];
|
|
221
234
|
const array = ObservableScope.Value(scope);
|
|
222
235
|
const arrayValue = array[prop];
|
|
223
236
|
if (typeof prop === "symbol")
|
|
@@ -254,7 +267,7 @@ function CreateProxyFactory(alias) {
|
|
|
254
267
|
const wrapper = {
|
|
255
268
|
[NODE_VALUE]: value,
|
|
256
269
|
[NODE_PROXY]: null,
|
|
257
|
-
[
|
|
270
|
+
[OBJECT_SCOPE]: ObservableScope.Basic(Identity.bind(null, value))
|
|
258
271
|
};
|
|
259
272
|
const proxy = wrapper[NODE_PROXY] = new Proxy(wrapper, {
|
|
260
273
|
get: ObjectProxyGetter,
|
|
@@ -274,7 +287,6 @@ function CreateProxyFactory(alias) {
|
|
|
274
287
|
return true;
|
|
275
288
|
}
|
|
276
289
|
function ObjectProxyGetter(object, prop) {
|
|
277
|
-
ObservableScope.Touch(object[OBJECT_SCOPE]);
|
|
278
290
|
switch (prop) {
|
|
279
291
|
case IS_NODE:
|
|
280
292
|
return true;
|
|
@@ -314,7 +326,7 @@ function CreateProxyFactory(alias) {
|
|
|
314
326
|
case "array": {
|
|
315
327
|
const wrapper = wrapperCache.get(value);
|
|
316
328
|
const proxy = wrapper?.[NODE_PROXY] ?? CreateArrayProxy(value);
|
|
317
|
-
ObservableScope.Touch(wrapper?.[
|
|
329
|
+
ObservableScope.Touch(wrapper?.[ARRAY_SCOPE]);
|
|
318
330
|
return proxy;
|
|
319
331
|
}
|
|
320
332
|
default:
|
|
@@ -401,7 +413,7 @@ export var ObservableNode;
|
|
|
401
413
|
function Update(value, prop) {
|
|
402
414
|
const wrapper = wrapperCache.get(value);
|
|
403
415
|
if (wrapper) {
|
|
404
|
-
const scope = Array.isArray(wrapper) ? wrapper[
|
|
416
|
+
const scope = Array.isArray(wrapper) ? wrapper[ARRAY_SCOPE] : (prop && GetPropertyScope(wrapper, prop)) ?? wrapper[OBJECT_SCOPE];
|
|
405
417
|
ObservableScope.Update(scope);
|
|
406
418
|
}
|
|
407
419
|
}
|
|
@@ -425,7 +437,7 @@ export var ObservableNode;
|
|
|
425
437
|
function Touch(value, prop) {
|
|
426
438
|
const wrapper = wrapperCache.get(value);
|
|
427
439
|
if (wrapper) {
|
|
428
|
-
const scope = prop && GetPropertyScope(wrapper, prop) || wrapper[OBJECT_SCOPE];
|
|
440
|
+
const scope = prop && GetPropertyScope(wrapper, prop) || wrapper[ARRAY_SCOPE] || wrapper[OBJECT_SCOPE];
|
|
429
441
|
ObservableScope.Touch(scope);
|
|
430
442
|
}
|
|
431
443
|
}
|
|
@@ -433,7 +445,7 @@ export var ObservableNode;
|
|
|
433
445
|
function Read(value, prop) {
|
|
434
446
|
const wrapper = wrapperCache.get(value);
|
|
435
447
|
if (wrapper) {
|
|
436
|
-
const scope = Array.isArray(wrapper) ? wrapper[
|
|
448
|
+
const scope = Array.isArray(wrapper) ? wrapper[ARRAY_SCOPE] : (prop && wrapper[prop]) ?? wrapper[OBJECT_SCOPE];
|
|
437
449
|
ObservableScope.Update(scope);
|
|
438
450
|
}
|
|
439
451
|
}
|
package/Store/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { Store } from "./Store/store";
|
|
1
|
+
export { ReadStore, Store } from "./Store/store";
|
|
2
2
|
export { StoreSync } from "./Store/storeSync";
|
|
3
3
|
export { StoreAsync } from "./Store/storeAsync";
|
|
4
|
+
export { PushStore } from "./Store/pushStore";
|
|
4
5
|
export { ObservableScope } from "./Tree/observableScope";
|
|
5
6
|
export { ObservableNode } from "./Tree/observableNode";
|
package/Store/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { Store } from "./Store/store";
|
|
1
|
+
export { ReadStore, Store } from "./Store/store";
|
|
2
2
|
export { StoreSync } from "./Store/storeSync";
|
|
3
3
|
export { StoreAsync } from "./Store/storeAsync";
|
|
4
|
+
export { PushStore } from "./Store/pushStore";
|
|
4
5
|
export { ObservableScope } from "./Tree/observableScope";
|
|
5
6
|
export { ObservableNode } from "./Tree/observableNode";
|