j-templates 8.0.15 → 8.0.17
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 +76 -25
- package/Store/Diff/diffAsync.d.ts +4 -5
- package/Store/Diff/diffAsync.js +4 -5
- package/Store/Diff/diffTree.d.ts +16 -7
- package/Store/Diff/diffTree.js +21 -8
- package/Store/Store/storeAsync.d.ts +12 -8
- package/Store/Store/storeAsync.js +26 -14
- package/Store/Tree/observableNode.d.ts +20 -0
- package/Store/Tree/observableNode.js +20 -0
- package/Store/Tree/observableScope.d.ts +3 -1
- package/Store/Tree/observableScope.js +3 -1
- package/package.json +1 -1
package/SYNTAX_PRIMER.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# j-templates Syntax Primer —
|
|
1
|
+
# j-templates Syntax Primer — v5
|
|
2
2
|
|
|
3
|
-
Complete reference for the **j-templates** framework syntax. This documents **j-templates
|
|
3
|
+
Complete reference for the **j-templates** framework syntax. This documents **j-templates v8.0.16** (see `package.json`). For pattern-oriented guides, see `docs/patterns/`; for step-by-step tutorials, see `docs/tutorials/`.
|
|
4
4
|
|
|
5
5
|
> **Core concepts:** Components define UI via `Template()`. State decorators (`@Value`, `@State`, `@Computed`) enable reactivity. DOM functions (`div()`, `button()`) create virtual nodes. No compile step, minimal dependencies.
|
|
6
6
|
|
|
@@ -1339,42 +1339,59 @@ class Child extends Component {
|
|
|
1339
1339
|
|
|
1340
1340
|
Both stores share the same API and flattening model, but differ fundamentally in where and how diffing occurs.
|
|
1341
1341
|
|
|
1342
|
-
**StoreSync** computes diffs synchronously on the main thread. Writes are immediate and consistent — a value written is readable in the same tick. It has no worker overhead
|
|
1342
|
+
**StoreSync** computes diffs synchronously on the main thread. Writes are immediate and consistent — a value written is readable in the same tick. It has no worker overhead and no `Destroy()` requirement. Use StoreSync for the vast majority of application state: user data, UI state, app config, form data, and any dataset where diff computation is not a bottleneck. `@Computed` uses `StoreSync` internally.
|
|
1343
1343
|
|
|
1344
|
-
**StoreAsync**
|
|
1344
|
+
**StoreAsync** runs diff computation on a `Worker` you supply, off the main thread, so large dataset operations don't block rendering or input. Use StoreAsync when diffing genuinely large or deeply nested datasets — message feeds, large tables, real-time data. `@ComputedAsync` uses `StoreAsync` internally, with a shared prebuilt worker.
|
|
1345
1345
|
|
|
1346
1346
|
**If you are unsure which to use, start with StoreSync.** StoreAsync introduces meaningful constraints (see below) that are only worth accepting when the dataset size justifies off-thread diffing.
|
|
1347
1347
|
|
|
1348
1348
|
| Aspect | StoreSync | StoreAsync |
|
|
1349
1349
|
|--------|-----------|------------|
|
|
1350
|
-
| Diff execution | Main thread, synchronous |
|
|
1350
|
+
| Diff execution | Main thread, synchronous | Caller-supplied `Worker`, asynchronous |
|
|
1351
1351
|
| Write consistency | Immediate — readable same tick | Eventual — must `await` before reading |
|
|
1352
|
-
|
|
|
1353
|
-
|
|
|
1354
|
-
|
|
|
1352
|
+
| Construction | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(diffWorker, keyFunc?)` — the worker is built separately (see below) |
|
|
1353
|
+
| `keyFunc`/projections used for diffing | Passed straight to the constructor, can close over outer scope | Must be baked into the worker's own entry file — the constructor's `keyFunc` only resolves aliases locally, on the main thread |
|
|
1354
|
+
| Data constraint | Any JS value | **JSON-serialisable only** — no class instances, methods, `Date`, `Map`, `Set`, circular refs (carried over `postMessage`'s structured clone) |
|
|
1355
|
+
| `Destroy()` required | No | Yes — terminates the worker and queue |
|
|
1355
1356
|
| Best for | Most app state | Large / real-time datasets |
|
|
1356
1357
|
| Decorator backend | `@Computed` | `@ComputedAsync` |
|
|
1357
1358
|
|
|
1358
|
-
### StoreAsync
|
|
1359
|
+
### StoreAsync: Supplying the Worker
|
|
1359
1360
|
|
|
1360
|
-
StoreAsync'
|
|
1361
|
+
`StoreAsync` doesn't build or serialize a worker itself anymore — you construct a real `Worker` (a bundler-built module, not a Blob) and hand it in. Internally, a worker becomes usable by connecting a `DiffTree` to it with `ConnectWorkerToDiffTree`; that's what a worker *entry file* does.
|
|
1361
1362
|
|
|
1362
|
-
**
|
|
1363
|
+
**Most portable option — write your own tiny entry file.** This always works regardless of bundler, because it's a relative import inside your own source tree, exactly like any other bundler-built worker:
|
|
1363
1364
|
|
|
1364
|
-
|
|
1365
|
+
```typescript
|
|
1366
|
+
// diff-worker.ts — a separate entry file; your bundler compiles it as its own chunk
|
|
1367
|
+
import { DiffTree } from "j-templates/Store/Diff/diffTree";
|
|
1368
|
+
import { ConnectWorkerToDiffTree } from "j-templates/Store/Diff/diffTreeWorker";
|
|
1369
|
+
|
|
1370
|
+
const diffTree = new DiffTree(/* keyFunc, projections — see below */);
|
|
1371
|
+
ConnectWorkerToDiffTree(diffTree, self as any as Worker);
|
|
1372
|
+
```
|
|
1365
1373
|
|
|
1366
1374
|
```typescript
|
|
1367
|
-
//
|
|
1368
|
-
|
|
1369
|
-
|
|
1375
|
+
// wherever you construct the store
|
|
1376
|
+
import { StoreAsync } from "j-templates/Store";
|
|
1377
|
+
|
|
1378
|
+
const worker = new Worker(new URL("./diff-worker.ts", import.meta.url), { type: "module" });
|
|
1379
|
+
const store = new StoreAsync(worker, keyFunc); // pass the same keyFunc for local alias resolution
|
|
1380
|
+
```
|
|
1370
1381
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1382
|
+
**No custom `keyFunc`/projections needed?** The package ships a ready no-argument entry file, `defaultDiffTreeWorker.js`, which is what `@ComputedAsync` builds internally (see `src/Utils/decorators.ts`) via a relative import from inside the package itself:
|
|
1383
|
+
|
|
1384
|
+
```typescript
|
|
1385
|
+
const worker = new Worker(
|
|
1386
|
+
new URL("../Store/Diff/defaultDiffTreeWorker.js", import.meta.url),
|
|
1387
|
+
{ type: "module" },
|
|
1388
|
+
);
|
|
1389
|
+
const store = new StoreAsync(worker);
|
|
1373
1390
|
```
|
|
1374
1391
|
|
|
1375
|
-
|
|
1392
|
+
That relative path only resolves correctly from *inside* the installed package (it's relative to the file doing the importing), so from your own app code, writing your own entry file (above) is the reliable choice.
|
|
1376
1393
|
|
|
1377
|
-
|
|
1394
|
+
Because the worker module runs in its own realm, its `keyFunc`/projections can only close over things defined in that file — there's no shared runtime state with the main thread, and nothing is serialised with `.toString()`/`eval` (unlike older versions of this library). Data crossing the worker boundary, via `postMessage`, still goes through the structured clone algorithm, so it must be JSON-serialisable:
|
|
1378
1395
|
|
|
1379
1396
|
```typescript
|
|
1380
1397
|
// ❌ Methods and class instances are stripped by structured clone
|
|
@@ -1392,6 +1409,7 @@ class Store {
|
|
|
1392
1409
|
constructor(keyFunc?: (value: any) => string | undefined);
|
|
1393
1410
|
Get<O>(id: string): O | undefined; // Returns undefined if not found
|
|
1394
1411
|
Get<O>(id: string, defaultValue: O): O; // Creates and returns default if not found
|
|
1412
|
+
Has(id: string): boolean; // Whether a root object exists for id
|
|
1395
1413
|
}
|
|
1396
1414
|
```
|
|
1397
1415
|
|
|
@@ -1399,6 +1417,7 @@ class Store {
|
|
|
1399
1417
|
|
|
1400
1418
|
```typescript
|
|
1401
1419
|
class StoreSync extends Store {
|
|
1420
|
+
constructor(keyFunc?: (value: any) => string | undefined, projections?: DiffTreeProjectionMap);
|
|
1402
1421
|
Write(data: unknown, key?: string): void;
|
|
1403
1422
|
Patch(key: string, patch: unknown): void; // Deep merge; throws if key not found
|
|
1404
1423
|
Push(key: string, ...data: unknown[]): void;
|
|
@@ -1411,14 +1430,39 @@ class StoreSync extends Store {
|
|
|
1411
1430
|
|
|
1412
1431
|
```typescript
|
|
1413
1432
|
class StoreAsync extends Store {
|
|
1433
|
+
constructor(diffWorker: Worker, keyFunc?: (value: any) => string | undefined);
|
|
1414
1434
|
async Write(data: unknown, key?: string): Promise<void>;
|
|
1415
1435
|
async Patch(key: string, patch: unknown): Promise<void>; // Deep merge; throws if key not found
|
|
1416
1436
|
async Push(key: string, ...data: unknown[]): Promise<void>;
|
|
1417
1437
|
async Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<unknown[]>;
|
|
1418
|
-
Destroy(): void; // Always call when service is destroyed — terminates worker
|
|
1438
|
+
Destroy(): void; // Always call when service is destroyed — terminates the worker
|
|
1419
1439
|
}
|
|
1420
1440
|
```
|
|
1421
1441
|
|
|
1442
|
+
### Projections — Derived Values Computed Inside the Store
|
|
1443
|
+
|
|
1444
|
+
A projection is a derived value the store computes for you and stores alongside your written data, readable through `Get` like anything else. Declare a map of them at construction (`StoreSync`'s second argument; for `StoreAsync`, baked into the worker's `DiffTree` as shown above):
|
|
1445
|
+
|
|
1446
|
+
```typescript
|
|
1447
|
+
import { StoreSync } from "j-templates/Store";
|
|
1448
|
+
import { PROJECTION_PREFIX } from "j-templates/Store/Diff/diffTree";
|
|
1449
|
+
|
|
1450
|
+
const store = new StoreSync((value: any) => value?.id, {
|
|
1451
|
+
activeCount: {
|
|
1452
|
+
reads: ["todos"],
|
|
1453
|
+
projection: (keys: string[], todos: Todo[]) => todos.filter((t) => !t.completed).length,
|
|
1454
|
+
},
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
store.Write(todos, "todos");
|
|
1458
|
+
store.Get<number>(`${PROJECTION_PREFIX}activeCount`); // "$projection_activeCount"
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
- `reads` is a list of dot-separated paths: a plain tree path (`"todos"`), a keyed entity's id (whatever `keyFunc` returns for it), or another projection's output via `` `${PROJECTION_PREFIX}otherId` `` (i.e. `"$projection_otherId"`) — chaining. The dependency graph is topologically sorted at construction, so a cycle (or a read of an unregistered id) throws immediately rather than on first write.
|
|
1462
|
+
- `projection` is called as `(keys, ...values)` — every resolved read path, then each value positionally in the same order as `reads`.
|
|
1463
|
+
- The final `reads` entry may instead be a wildcard root prefix (e.g. `"task_*"`, no dot nesting) — every root key with that prefix is read and appended as its own trailing key/value pair, so `projection` receives one extra `(key, value)` pair per match. Only the last read may be a wildcard, and it can't target a projection's `$projection_<id>` key.
|
|
1464
|
+
- A projection only runs once every one of its `reads` resolves — a path never written, an entity never seen, or a projection that hasn't produced a value yet blocks it (and blocks anything chaining off it). It re-runs whenever a read's resolved value changes, and its result is diffed and applied in place like any other write, so only the fields that actually changed reach reactive readers.
|
|
1465
|
+
|
|
1422
1466
|
### keyFunc and Automatic Flattening
|
|
1423
1467
|
|
|
1424
1468
|
`keyFunc` teaches the store how to extract an ID from any object. On `Write` and `Push`, the store eagerly recurses the entire object tree, calling `keyFunc` on each node. Any node that returns a valid ID is registered as an independently addressable entry in the flat internal map. The original shape is preserved because stored objects are `ObservableNode` proxies — nested objects are virtual getters into the flat map rather than copies, which is what allows `Get` to return the original structure while `Patch` operates on individual entries.
|
|
@@ -1462,7 +1506,7 @@ If an object has no `id` property (or `keyFunc` returns `undefined`), it is stor
|
|
|
1462
1506
|
| Splice | sync | async (Promise) |
|
|
1463
1507
|
| Get | sync | sync |
|
|
1464
1508
|
| Destroy | N/A | `Destroy(): void` — required |
|
|
1465
|
-
| keyFunc
|
|
1509
|
+
| Construction | `keyFunc`/`projections` args | `diffWorker` (required) + local `keyFunc` |
|
|
1466
1510
|
| Data constraint | Any JS value | JSON-serialisable only |
|
|
1467
1511
|
|
|
1468
1512
|
---
|
|
@@ -1499,9 +1543,12 @@ scope whose value doesn't derive from reactive state, use `Basic` instead, which
|
|
|
1499
1543
|
### Service Patterns
|
|
1500
1544
|
|
|
1501
1545
|
```typescript
|
|
1502
|
-
// Derived state in services
|
|
1546
|
+
// Derived state in services — worker built once, e.g. in a module-level helper
|
|
1547
|
+
// (see "StoreAsync: Supplying the Worker" above for how diff-worker.ts is written)
|
|
1548
|
+
const worker = new Worker(new URL("./diff-worker.ts", import.meta.url), { type: "module" });
|
|
1549
|
+
|
|
1503
1550
|
class DataService implements IDestroyable {
|
|
1504
|
-
private store = new StoreAsync((value) => value.id);
|
|
1551
|
+
private store = new StoreAsync(worker, (value) => value.id);
|
|
1505
1552
|
private derived = ObservableScope.Create(() => {
|
|
1506
1553
|
const items = this.store.Get<Item[]>("items", []);
|
|
1507
1554
|
return ObservableNode.Unwrap(items).filter(i => i.active);
|
|
@@ -1531,6 +1578,7 @@ namespace ObservableNode {
|
|
|
1531
1578
|
Create<T>(value: T): T; // Wrap in reactive proxy
|
|
1532
1579
|
Unwrap<T>(value: T): T; // Get raw value from proxy
|
|
1533
1580
|
Clone<T>(value: T): T; // Strip proxies into plain data (mutates plain objects in place)
|
|
1581
|
+
Snapshot<T>(proxy: T): T | undefined; // Immutable cached plain-value snapshot of a node
|
|
1534
1582
|
Update(value: unknown, prop?: string | number): void; // Manually trigger change on a node/property
|
|
1535
1583
|
Apply(rootNode: any, value: any): void; // Merge a full value in-place, preserving identity (public)
|
|
1536
1584
|
ApplyDiff(rootNode: any, diffResult: JsonDiffResult): void; // Apply diff in-place (internal — used by Store/@Computed)
|
|
@@ -1540,6 +1588,8 @@ namespace ObservableNode {
|
|
|
1540
1588
|
|
|
1541
1589
|
**`Apply` vs `ApplyDiff`:** `Apply(rootNode, value)` merges a full replacement value into an observable node in-place, preserving the node's reference (so `===` checks and DOM reuse stay stable). Use this when you want to update an observable node in place — assigning a property directly on an observable node does **not** generate a diff. `ApplyDiff` is internal-only (used by `StoreSync`/`StoreAsync`/`@Computed`) — ignore it.
|
|
1542
1590
|
|
|
1591
|
+
**`Snapshot` and `toJSON`.** `ObservableNode.Snapshot(proxy)` returns an immutable plain copy of a node's current value (arrays/objects only; keyed children are read through their root). The same snapshot is returned on repeated calls until a write invalidates it, and unchanged nested parts are shared between snapshots, so `===` holds for anything that hasn't changed — useful for cheap change detection or handing data to non-reactive code. Calling `.toJSON()` on an observable node (e.g. via `JSON.stringify`) returns this same cached snapshot.
|
|
1592
|
+
|
|
1543
1593
|
**Array operations on ObservableNode proxies:** `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` — all trigger reactive updates.
|
|
1544
1594
|
|
|
1545
1595
|
---
|
|
@@ -1568,7 +1618,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
|
|
|
1568
1618
|
10. **`@Computed` only tracks what the getter touches.** Returning `this.tasks` without reading item properties won't re-trigger on per-item mutations. Touch every property you track.
|
|
1569
1619
|
11. **`scope()`/`gate()`/`peek()`/`mapped()` throw outside a watch context.** They must be called inside a children function, `data:`/`props:`/`attrs:` function, `@Scope`/`@Computed`/`@ComputedAsync` getter, or `@Watch` callback — not inside `on:` event handlers.
|
|
1570
1620
|
12. **`Injector` is not publicly exported.** Use `@Inject` and `this.Injector` on components.
|
|
1571
|
-
13. **StoreAsync data must be JSON-serialisable** and `
|
|
1621
|
+
13. **StoreAsync data must be JSON-serialisable** (carried over `postMessage`'s structured clone), and `StoreAsync` now takes an already-running `Worker` as its first constructor argument — any `keyFunc`/projections needed for diffing must be baked into that worker's own entry file, not passed to `StoreAsync` directly. Always `await` StoreAsync writes before reading.
|
|
1572
1622
|
14. **Two-way binding needs reactive props** (`props: () => ({ value })`). A static `props: { value }` object causes input focus loss.
|
|
1573
1623
|
15. **The Destructuring Trap.** Reading a scope or `this.Data` at the top of `Template()` subscribes the whole component — the React instinct to hoist state reads is backwards here. Read scopes inside children functions or `data:` bindings for fine-grained updates. See Mental Model.
|
|
1574
1624
|
16. **`scope()`/`gate()`/`peek()` ID collisions are per-scope.** Multiple calls to the same helper in one watch context without IDs silently resolve to the first scope. Provide distinct IDs when calling the same helper more than once in a single ObservableScope definition.
|
|
@@ -1645,12 +1695,13 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
|
|
|
1645
1695
|
| **ObservableNode** | A reactive proxy wrapper around objects/arrays that tracks property-level mutations |
|
|
1646
1696
|
| **Injector** | Scoped dependency injection container with parent-chain resolution (not publicly exported) |
|
|
1647
1697
|
| **keyFunc** | A function passed to Store that extracts an ID from objects for automatic flattening |
|
|
1698
|
+
| **Projection** | A derived value declared at Store construction (`reads` + a computing function), stored and read like any other data at `$projection_<id>` |
|
|
1648
1699
|
|
|
1649
1700
|
---
|
|
1650
1701
|
|
|
1651
1702
|
## References
|
|
1652
1703
|
|
|
1653
|
-
- **Source of truth:** `src/` (this primer documents `j-templates`
|
|
1704
|
+
- **Source of truth:** `src/` (this primer documents `j-templates` v8.0.16).
|
|
1654
1705
|
- **Pattern guides:** `docs/patterns/01-components.md`, `docs/patterns/02-reactivity.md`, `docs/patterns/03-templates-and-data.md`, `docs/patterns/04-dependency-injection.md`.
|
|
1655
1706
|
- **Tutorials:** `docs/tutorials/` (01-getting-started through 08-building-complete-app).
|
|
1656
1707
|
- **Worked example:** `examples/smart-tasks/src/` (the Smart Tasks app used above).
|
|
@@ -16,11 +16,10 @@ type IDiffTreeAsync = {
|
|
|
16
16
|
export declare class DiffAsync implements IDiffTreeAsync {
|
|
17
17
|
private workerQueue;
|
|
18
18
|
/**
|
|
19
|
-
* Creates a DiffAsync instance
|
|
20
|
-
* @param
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* function in the worker, the same mechanic used for `keyFunc`.
|
|
19
|
+
* Creates a DiffAsync instance around an already-running worker.
|
|
20
|
+
* @param diffWorker - A `Worker` running a `DiffTree` connected via
|
|
21
|
+
* `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry, or a custom
|
|
22
|
+
* entry file for a non-default `keyFunc`/`projections` baked into the worker thread).
|
|
24
23
|
*/
|
|
25
24
|
constructor(diffWorker: Worker);
|
|
26
25
|
/**
|
package/Store/Diff/diffAsync.js
CHANGED
|
@@ -8,11 +8,10 @@ import { WorkerQueue } from "./workerQueue";
|
|
|
8
8
|
*/
|
|
9
9
|
export class DiffAsync {
|
|
10
10
|
/**
|
|
11
|
-
* Creates a DiffAsync instance
|
|
12
|
-
* @param
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* function in the worker, the same mechanic used for `keyFunc`.
|
|
11
|
+
* Creates a DiffAsync instance around an already-running worker.
|
|
12
|
+
* @param diffWorker - A `Worker` running a `DiffTree` connected via
|
|
13
|
+
* `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry, or a custom
|
|
14
|
+
* entry file for a non-default `keyFunc`/`projections` baked into the worker thread).
|
|
16
15
|
*/
|
|
17
16
|
constructor(diffWorker) {
|
|
18
17
|
this.workerQueue = new WorkerQueue(diffWorker);
|
package/Store/Diff/diffTree.d.ts
CHANGED
|
@@ -35,17 +35,24 @@ export interface IDiffTreeConstructor {
|
|
|
35
35
|
}, projections?: DiffTreeProjectionMap): IDiffTree;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* A pure function computing a derived value from the current values at `reads
|
|
39
|
-
*
|
|
40
|
-
* cast out of a
|
|
38
|
+
* A pure function computing a derived value from the current values at `reads`. Called with
|
|
39
|
+
* the resolved read paths (`keys`) first, then each value positionally in the same order as
|
|
40
|
+
* `reads`, so each value parameter can be typed at the call site instead of cast out of a
|
|
41
|
+
* generic read. A trailing wildcard read expands to one key/value pair per matched root key,
|
|
42
|
+
* so `keys` lets the function tell which root each trailing value came from.
|
|
41
43
|
*/
|
|
42
|
-
export type DiffTreeProjectionFunc = (...values: any[]) => any;
|
|
44
|
+
export type DiffTreeProjectionFunc = (keys: string[], ...values: any[]) => any;
|
|
43
45
|
/**
|
|
44
46
|
* A projection's declaration: the dot-separated paths it reads (a plain tree path, an
|
|
45
47
|
* entity's key, or another projection's `$projection_<id>` key for chaining) and the
|
|
46
48
|
* function computing its result from their current values, positionally. Declaring `reads`
|
|
47
49
|
* up front lets the tree topologically sort projections and reject a dependency cycle at
|
|
48
50
|
* construction instead of on first write.
|
|
51
|
+
*
|
|
52
|
+
* The final entry in `reads` may instead be a wildcard root key (e.g. `"user_*"`, no dot
|
|
53
|
+
* nesting) matching every root key with that prefix; each match is read and passed to the
|
|
54
|
+
* projection as its own trailing key/value pair. A wildcard read can't be chained off a
|
|
55
|
+
* projection's `$projection_<id>` key and can't appear anywhere but last.
|
|
49
56
|
*/
|
|
50
57
|
export type DiffTreeProjectionDefinition = {
|
|
51
58
|
reads: string[];
|
|
@@ -67,9 +74,11 @@ export type DiffSpliceResult<T = unknown> = {
|
|
|
67
74
|
diffResult: JsonDiffResult;
|
|
68
75
|
};
|
|
69
76
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
77
|
+
* Diff tree implementation. Maintains root state and computes diffs for path/value updates.
|
|
78
|
+
* Used directly by `DiffSync`, and is the building block for a `DiffAsync` worker entry
|
|
79
|
+
* file: construct one with the desired `keyFunc`/`projections` and connect it to the worker
|
|
80
|
+
* thread with `ConnectWorkerToDiffTree` (see `defaultDiffTreeWorker.ts` for the no-argument
|
|
81
|
+
* case, or a custom entry file when either is needed).
|
|
73
82
|
*/
|
|
74
83
|
export declare class DiffTree {
|
|
75
84
|
private keyFunc?;
|
package/Store/Diff/diffTree.js
CHANGED
|
@@ -146,6 +146,9 @@ function ChildSnapshot(source, keyFunc, parent, child) {
|
|
|
146
146
|
* sequentially: a read of another projection's `$projection_<id>` output is a dependency
|
|
147
147
|
* edge, so that projection is sorted earlier. Runs once, at construction, so a cycle (or a
|
|
148
148
|
* read of an id that isn't registered) is rejected immediately instead of on first write.
|
|
149
|
+
* Also validates each projection's wildcard reads here, at construction: a wildcard must be
|
|
150
|
+
* the final read, must not have dot nesting, and can't target a projection's
|
|
151
|
+
* `$projection_<id>` key.
|
|
149
152
|
*/
|
|
150
153
|
function BuildProjectionOrder(projections) {
|
|
151
154
|
const order = [];
|
|
@@ -199,6 +202,11 @@ function BuildProjectionOrder(projections) {
|
|
|
199
202
|
* makes a blocked dependency propagate for free: a projection that never runs never writes
|
|
200
203
|
* `$projection_<id>`, so anything chaining off it stays blocked in turn.
|
|
201
204
|
*
|
|
205
|
+
* A trailing wildcard read is expanded against the current root keys before the projection
|
|
206
|
+
* runs: every root key matching the prefix is read and appended as its own key/value pair,
|
|
207
|
+
* so the projection is called with `(keys, ...values)` where `keys` is every resolved read
|
|
208
|
+
* path (fixed reads first, then each matched wildcard key) positional with `values`.
|
|
209
|
+
*
|
|
202
210
|
* @param source - The root tree object
|
|
203
211
|
* @param keyFunc - Optional function to extract a key from objects, for reading through roots
|
|
204
212
|
* @param projections - Dependency-sorted projection entries, from `BuildProjectionOrder`
|
|
@@ -211,27 +219,30 @@ function RunProjections(source, keyFunc, projections, memo) {
|
|
|
211
219
|
for (let x = 0; x < projections.length; x++) {
|
|
212
220
|
const { id, reads, projection } = projections[x];
|
|
213
221
|
const values = [];
|
|
222
|
+
const keys = [];
|
|
214
223
|
let allPathsExist = true;
|
|
215
224
|
let y = 0;
|
|
216
225
|
for (; allPathsExist && y < reads.length && !reads[y].endsWith("*"); y++) {
|
|
217
226
|
const read = reads[y];
|
|
218
227
|
const [value, found] = GetPathValue(source, read);
|
|
219
228
|
allPathsExist = found;
|
|
220
|
-
|
|
229
|
+
keys.push(read);
|
|
230
|
+
values.push((found && CreateSnapshot(source, keyFunc, value)) || value);
|
|
221
231
|
}
|
|
222
232
|
if (y < reads.length && reads[y].endsWith("*")) {
|
|
223
233
|
const prefix = reads[y].slice(0, reads[y].length - 1);
|
|
224
|
-
const
|
|
225
|
-
for (let z = 0; z <
|
|
226
|
-
const [value, found] = GetPathValue(source,
|
|
234
|
+
const matchedKeys = Object.keys(source).filter(key => key.startsWith(prefix));
|
|
235
|
+
for (let z = 0; z < matchedKeys.length; z++) {
|
|
236
|
+
const [value, found] = GetPathValue(source, matchedKeys[z]);
|
|
227
237
|
allPathsExist = allPathsExist && found;
|
|
238
|
+
keys.push(matchedKeys[z]);
|
|
228
239
|
values.push((found && CreateSnapshot(source, keyFunc, value)) || value);
|
|
229
240
|
}
|
|
230
241
|
}
|
|
231
242
|
if (allPathsExist) {
|
|
232
243
|
const priorValues = memo.get(id);
|
|
233
244
|
if (!ArraysEqual(priorValues, values)) {
|
|
234
|
-
const result = projection(...values);
|
|
245
|
+
const result = projection(keys, ...values);
|
|
235
246
|
memo.set(id, values);
|
|
236
247
|
// The same diff-and-apply UpdateSource uses for any other write, so a keyed entity
|
|
237
248
|
// embedded in a projection's result is flattened to its own root like any other write,
|
|
@@ -333,9 +344,11 @@ function SpliceSource(source, path, start, deleteCount, items, flatten, keyFunc)
|
|
|
333
344
|
return spliceResult;
|
|
334
345
|
}
|
|
335
346
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
347
|
+
* Diff tree implementation. Maintains root state and computes diffs for path/value updates.
|
|
348
|
+
* Used directly by `DiffSync`, and is the building block for a `DiffAsync` worker entry
|
|
349
|
+
* file: construct one with the desired `keyFunc`/`projections` and connect it to the worker
|
|
350
|
+
* thread with `ConnectWorkerToDiffTree` (see `defaultDiffTreeWorker.ts` for the no-argument
|
|
351
|
+
* case, or a custom entry file when either is needed).
|
|
339
352
|
*/
|
|
340
353
|
export class DiffTree {
|
|
341
354
|
/**
|
|
@@ -9,8 +9,9 @@ import { Store } from "./store";
|
|
|
9
9
|
* without blocking the main thread.
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
|
-
* // Creating a StoreAsync instance
|
|
13
|
-
* const
|
|
12
|
+
* // Creating a StoreAsync instance from a bundler-built worker entry file
|
|
13
|
+
* const worker = new Worker(new URL("./Store/Diff/defaultDiffTreeWorker.js", import.meta.url), { type: "module" });
|
|
14
|
+
* const store = new StoreAsync(worker);
|
|
14
15
|
*
|
|
15
16
|
* // Writing data to the store asynchronously
|
|
16
17
|
* await store.Write({ name: "John", age: 30 }, "user");
|
|
@@ -44,18 +45,21 @@ export declare class StoreAsync extends Store {
|
|
|
44
45
|
private queue;
|
|
45
46
|
/**
|
|
46
47
|
* Creates an instance of StoreAsync.
|
|
47
|
-
* @param
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
* @param diffWorker A `Worker` running a `DiffTree` connected via
|
|
49
|
+
* `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry). Any
|
|
50
|
+
* `keyFunc`/projections the diff needs must be baked into that worker's own entry file,
|
|
51
|
+
* since the worker now runs standard bundled code rather than a serialized function.
|
|
52
|
+
* @param keyFunc Optional function to generate a key for a given data value, used locally
|
|
53
|
+
* by this store to resolve root objects. Pass the same key logic the worker's `DiffTree`
|
|
54
|
+
* was constructed with, if any.
|
|
53
55
|
*/
|
|
54
56
|
constructor(diffWorker: Worker, keyFunc?: (value: any) => string | undefined);
|
|
55
57
|
/**
|
|
56
58
|
* Returns a promise that resolve once all current queued writes have completed
|
|
57
59
|
*/
|
|
58
60
|
Flush(): Promise<void>;
|
|
61
|
+
private nextWritePromise;
|
|
62
|
+
private nextWriteBatch;
|
|
59
63
|
/**
|
|
60
64
|
* Writes data to the store asynchronously.
|
|
61
65
|
* This method ensures that write operations are queued and executed in a non-blocking manner.
|
|
@@ -12,8 +12,9 @@ import { Store } from "./store";
|
|
|
12
12
|
* without blocking the main thread.
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
|
-
* // Creating a StoreAsync instance
|
|
16
|
-
* const
|
|
15
|
+
* // Creating a StoreAsync instance from a bundler-built worker entry file
|
|
16
|
+
* const worker = new Worker(new URL("./Store/Diff/defaultDiffTreeWorker.js", import.meta.url), { type: "module" });
|
|
17
|
+
* const store = new StoreAsync(worker);
|
|
17
18
|
*
|
|
18
19
|
* // Writing data to the store asynchronously
|
|
19
20
|
* await store.Write({ name: "John", age: 30 }, "user");
|
|
@@ -37,12 +38,13 @@ import { Store } from "./store";
|
|
|
37
38
|
export class StoreAsync extends Store {
|
|
38
39
|
/**
|
|
39
40
|
* Creates an instance of StoreAsync.
|
|
40
|
-
* @param
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* @param diffWorker A `Worker` running a `DiffTree` connected via
|
|
42
|
+
* `ConnectWorkerToDiffTree` (e.g. a bundled `defaultDiffTreeWorker` entry). Any
|
|
43
|
+
* `keyFunc`/projections the diff needs must be baked into that worker's own entry file,
|
|
44
|
+
* since the worker now runs standard bundled code rather than a serialized function.
|
|
45
|
+
* @param keyFunc Optional function to generate a key for a given data value, used locally
|
|
46
|
+
* by this store to resolve root objects. Pass the same key logic the worker's `DiffTree`
|
|
47
|
+
* was constructed with, if any.
|
|
46
48
|
*/
|
|
47
49
|
constructor(diffWorker, keyFunc) {
|
|
48
50
|
super(keyFunc, false);
|
|
@@ -51,6 +53,8 @@ export class StoreAsync extends Store {
|
|
|
51
53
|
* @private
|
|
52
54
|
*/
|
|
53
55
|
this.queue = new AsyncQueue();
|
|
56
|
+
this.nextWritePromise = null;
|
|
57
|
+
this.nextWriteBatch = null;
|
|
54
58
|
this.diff = new DiffAsync(diffWorker);
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
@@ -67,13 +71,21 @@ export class StoreAsync extends Store {
|
|
|
67
71
|
* @throws Will throw an error if no key is provided for the data.
|
|
68
72
|
*/
|
|
69
73
|
async Write(data, key) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.UpdateRootMap(diffResult);
|
|
74
|
+
const nextWriteBatch = this.nextWriteBatch ??= [];
|
|
75
|
+
const init = nextWriteBatch.length === 0;
|
|
76
|
+
nextWriteBatch.push({
|
|
77
|
+
path: key || this.keyFunc?.(data),
|
|
78
|
+
value: data
|
|
76
79
|
});
|
|
80
|
+
if (init) {
|
|
81
|
+
this.nextWritePromise = this.queue.Next(async () => {
|
|
82
|
+
await Promise.resolve();
|
|
83
|
+
this.nextWriteBatch = null;
|
|
84
|
+
const diffResult = await this.diff.DiffBatch(nextWriteBatch);
|
|
85
|
+
this.UpdateRootMap(diffResult);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return this.nextWritePromise;
|
|
77
89
|
}
|
|
78
90
|
/**
|
|
79
91
|
* Patches an existing value in the store with new data asynchronously.
|
|
@@ -39,6 +39,14 @@ export declare namespace ObservableNode {
|
|
|
39
39
|
* @returns A proxied version of the value that emits change events.
|
|
40
40
|
*/
|
|
41
41
|
function Create<T>(value: T): T;
|
|
42
|
+
/**
|
|
43
|
+
* Returns an immutable, cached snapshot of an observable node's current value (plain
|
|
44
|
+
* objects/arrays, with keyed children read through their root). The same snapshot is
|
|
45
|
+
* returned until a write invalidates it, and unchanged nested parts are shared between
|
|
46
|
+
* snapshots, so `===` holds for any part that hasn't changed.
|
|
47
|
+
* @param proxy The observable node to snapshot.
|
|
48
|
+
* @returns The snapshot, or `undefined` if `proxy` is not an observable node.
|
|
49
|
+
*/
|
|
42
50
|
function Snapshot<T>(proxy: T): T | undefined;
|
|
43
51
|
/**
|
|
44
52
|
* Marks an observable node or its property as changed, triggering reactive updates.
|
|
@@ -69,12 +77,24 @@ export declare namespace ObservableNode {
|
|
|
69
77
|
* @remarks No-op when `value` deep-equals the node's current value (empty diff).
|
|
70
78
|
*/
|
|
71
79
|
function Apply(rootNode: any, value: any): void;
|
|
80
|
+
/**
|
|
81
|
+
* Splices an observable node's underlying array in-place, invalidating its snapshot and
|
|
82
|
+
* touching its scope so reactive readers update.
|
|
83
|
+
* @param rootNode The observable array node to splice.
|
|
84
|
+
* @param start Index at which to start changing the array.
|
|
85
|
+
* @param deleteCount Number of elements to remove starting at `start`.
|
|
86
|
+
* @param items Elements to insert at `start`.
|
|
87
|
+
* @param cloneData Whether to deep-clone `items` before inserting them (true by default,
|
|
88
|
+
* so the store isn't left holding a reference the caller can still mutate).
|
|
89
|
+
*/
|
|
72
90
|
function ApplySplice(rootNode: any, start: number, deleteCount: number, items: any[], cloneData?: boolean): void;
|
|
73
91
|
/**
|
|
74
92
|
* Applies a JSON diff result to an observable node, efficiently updating only changed properties.
|
|
75
93
|
* Optimizes nested object updates by computing paths incrementally and touching modified properties.
|
|
76
94
|
* @param rootNode The observable node to apply the diff to.
|
|
77
95
|
* @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
|
|
96
|
+
* @param cloneData Whether to deep-clone each diff value before writing it in (true by
|
|
97
|
+
* default), so the store isn't left holding a reference the caller can still mutate.
|
|
78
98
|
*/
|
|
79
99
|
function ApplyDiff(rootNode: any, diffResult: JsonDiffResult, cloneData?: boolean): void;
|
|
80
100
|
/**
|
|
@@ -377,6 +377,14 @@ export var ObservableNode;
|
|
|
377
377
|
return DefaultCreateProxy(value);
|
|
378
378
|
}
|
|
379
379
|
ObservableNode.Create = Create;
|
|
380
|
+
/**
|
|
381
|
+
* Returns an immutable, cached snapshot of an observable node's current value (plain
|
|
382
|
+
* objects/arrays, with keyed children read through their root). The same snapshot is
|
|
383
|
+
* returned until a write invalidates it, and unchanged nested parts are shared between
|
|
384
|
+
* snapshots, so `===` holds for any part that hasn't changed.
|
|
385
|
+
* @param proxy The observable node to snapshot.
|
|
386
|
+
* @returns The snapshot, or `undefined` if `proxy` is not an observable node.
|
|
387
|
+
*/
|
|
380
388
|
function Snapshot(proxy) {
|
|
381
389
|
if (proxy[IS_NODE])
|
|
382
390
|
return proxy[NODE_SNAPSHOT];
|
|
@@ -453,6 +461,16 @@ export var ObservableNode;
|
|
|
453
461
|
ApplyDiff(rootNode, diff);
|
|
454
462
|
}
|
|
455
463
|
ObservableNode.Apply = Apply;
|
|
464
|
+
/**
|
|
465
|
+
* Splices an observable node's underlying array in-place, invalidating its snapshot and
|
|
466
|
+
* touching its scope so reactive readers update.
|
|
467
|
+
* @param rootNode The observable array node to splice.
|
|
468
|
+
* @param start Index at which to start changing the array.
|
|
469
|
+
* @param deleteCount Number of elements to remove starting at `start`.
|
|
470
|
+
* @param items Elements to insert at `start`.
|
|
471
|
+
* @param cloneData Whether to deep-clone `items` before inserting them (true by default,
|
|
472
|
+
* so the store isn't left holding a reference the caller can still mutate).
|
|
473
|
+
*/
|
|
456
474
|
function ApplySplice(rootNode, start, deleteCount, items, cloneData = true) {
|
|
457
475
|
const root = rootNode[NODE_VALUE];
|
|
458
476
|
InvalidateSnapshotForValue(rootNode[NODE_VALUE]);
|
|
@@ -466,6 +484,8 @@ export var ObservableNode;
|
|
|
466
484
|
* Optimizes nested object updates by computing paths incrementally and touching modified properties.
|
|
467
485
|
* @param rootNode The observable node to apply the diff to.
|
|
468
486
|
* @param diffResult The diff result from JsonDiff containing path-value pairs of changes.
|
|
487
|
+
* @param cloneData Whether to deep-clone each diff value before writing it in (true by
|
|
488
|
+
* default), so the store isn't left holding a reference the caller can still mutate.
|
|
469
489
|
*/
|
|
470
490
|
function ApplyDiff(rootNode, diffResult, cloneData = true) {
|
|
471
491
|
const root = rootNode[NODE_VALUE];
|
|
@@ -140,7 +140,9 @@ export declare namespace ObservableScope {
|
|
|
140
140
|
* Creates a lightweight basic scope that stores a value directly without a proxy.
|
|
141
141
|
* Used by `@Value` for primitives. Basic scopes do not automatically track dependencies —
|
|
142
142
|
* after creation, `ObservableScope.Update` must be called for the scope to emit. They also do
|
|
143
|
-
* not cache a value internally: the valueFunction is invoked on every read
|
|
143
|
+
* not cache a value internally: the valueFunction is invoked on every read, with dependency
|
|
144
|
+
* tracking suspended, so any reactive value it reads is never registered as a dependency of
|
|
145
|
+
* whichever scope happened to be executing when the read occurred.
|
|
144
146
|
* @template T The type of value stored in the scope.
|
|
145
147
|
* @param valueFunction Function that returns the scope's value.
|
|
146
148
|
* @returns A new basic observable scope.
|
|
@@ -565,7 +565,9 @@ export var ObservableScope;
|
|
|
565
565
|
* Creates a lightweight basic scope that stores a value directly without a proxy.
|
|
566
566
|
* Used by `@Value` for primitives. Basic scopes do not automatically track dependencies —
|
|
567
567
|
* after creation, `ObservableScope.Update` must be called for the scope to emit. They also do
|
|
568
|
-
* not cache a value internally: the valueFunction is invoked on every read
|
|
568
|
+
* not cache a value internally: the valueFunction is invoked on every read, with dependency
|
|
569
|
+
* tracking suspended, so any reactive value it reads is never registered as a dependency of
|
|
570
|
+
* whichever scope happened to be executing when the read occurred.
|
|
569
571
|
* @template T The type of value stored in the scope.
|
|
570
572
|
* @param valueFunction Function that returns the scope's value.
|
|
571
573
|
* @returns A new basic observable scope.
|