j-templates 9.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Node/vNode.js +2 -0
- package/SYNTAX_PRIMER.md +181 -101
- package/Store/Diff/channelClient.d.ts +106 -0
- package/Store/Diff/channelClient.js +228 -0
- package/Store/Diff/defaultDiffTreeWorker.js +3 -4
- package/Store/Diff/diffTree.d.ts +56 -0
- package/Store/Diff/diffTree.js +110 -4
- package/Store/Diff/diffTree.types.d.ts +191 -21
- package/Store/Diff/diffTree.types.js +2 -10
- package/Store/Diff/diffTreeEndpoint.d.ts +101 -0
- package/Store/Diff/diffTreeEndpoint.js +173 -0
- package/Store/Diff/diffTreeEndpoint.types.d.ts +71 -0
- package/Store/Diff/diffTreeEndpoint.types.js +1 -0
- package/Store/Diff/diffTreeHub.d.ts +139 -0
- package/Store/Diff/diffTreeHub.js +346 -0
- package/Store/Diff/hostPublisher.d.ts +39 -0
- package/Store/Diff/hostPublisher.js +151 -0
- package/Store/Diff/index.d.ts +3 -0
- package/Store/Diff/index.js +3 -0
- package/Store/Diff/observableDiffTree.d.ts +93 -0
- package/Store/Diff/observableDiffTree.js +199 -0
- package/Store/Diff/onConnect.d.ts +33 -0
- package/Store/Diff/onConnect.js +83 -0
- package/Store/Diff/utils/diffTree.types.d.ts +9 -0
- package/Store/Diff/utils/homes.d.ts +12 -0
- package/Store/Diff/utils/homes.js +40 -0
- package/Store/Diff/utils/routes.d.ts +18 -0
- package/Store/Diff/utils/routes.js +33 -2
- package/Store/Store/store.d.ts +36 -4
- package/Store/Store/store.js +47 -6
- package/Store/Store/storeAsync.d.ts +80 -25
- package/Store/Store/storeAsync.js +102 -33
- package/Store/Store/storeSync.d.ts +76 -11
- package/Store/Store/storeSync.js +105 -12
- package/Store/Tree/observableNode.js +68 -13
- package/Store/Tree/observableScope.d.ts +1 -0
- package/Store/Tree/observableScope.js +4 -0
- package/Store/Tree/opaque.d.ts +30 -0
- package/Store/Tree/opaque.js +35 -0
- package/Store/Tree/snapshot.js +5 -4
- package/Store/index.d.ts +4 -1
- package/Store/index.js +1 -1
- package/Utils/decorators.d.ts +0 -98
- package/Utils/decorators.js +0 -156
- package/Utils/index.d.ts +2 -0
- package/Utils/index.js +1 -0
- package/Utils/subscriptionManager.d.ts +78 -0
- package/Utils/subscriptionManager.js +162 -0
- package/package.json +1 -1
- package/Store/Diff/diffTreeWorker.d.ts +0 -6
- package/Store/Diff/diffTreeWorker.js +0 -22
- package/Store/Diff/diffTreeWriter.d.ts +0 -92
- package/Store/Diff/diffTreeWriter.js +0 -137
- package/Store/Diff/workerQueue.d.ts +0 -7
- package/Store/Diff/workerQueue.js +0 -29
- package/Store/Store/pushStore.d.ts +0 -53
- package/Store/Store/pushStore.js +0 -77
package/Node/vNode.js
CHANGED
|
@@ -312,6 +312,8 @@ function ScheduledAssignment(assign) {
|
|
|
312
312
|
return;
|
|
313
313
|
scheduled = true;
|
|
314
314
|
NodeConfig.scheduleUpdate(function () {
|
|
315
|
+
if (ObservableScope.IsDestroyed(scope))
|
|
316
|
+
return;
|
|
315
317
|
scheduled = false;
|
|
316
318
|
const value = ObservableScope.Peek(scope);
|
|
317
319
|
assign(value);
|
package/SYNTAX_PRIMER.md
CHANGED
|
@@ -8,7 +8,7 @@ Reference for building apps with j-templates 8.1.0. Every behavioral claim was c
|
|
|
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.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.
|
|
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`. Unreleased (this branch): `ObservableDiffTree`, `HostPublisher` and a `StoreAsync` that applies the tree's commits replace `PushStore`, `DiffTreeWriter`, `WorkerQueue` and `ConnectWorkerToDiffTree`; those sections were verified against the test suite. 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
|
|
|
@@ -55,11 +55,11 @@ npm install j-templates
|
|
|
55
55
|
| --- | --- |
|
|
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
|
-
| `j-templates/Utils` | `Value`, `State`, `Computed`, `
|
|
59
|
-
| `j-templates/Store` | `ReadStore`, `Store`, `StoreSync`, `StoreAsync`, `
|
|
60
|
-
| `j-templates/Store/Diff` | `PROJECTION_PREFIX`, `
|
|
58
|
+
| `j-templates/Utils` | `Value`, `State`, `Computed`, `Scope`, `Watch`, `Inject`, `Destroy`, `Bound`, `Animation`, `AnimationType`, `Injector`, `SubscriptionManager`; types `IDestroyable`, `KeyFilter`, `ISubscriptionMatch` |
|
|
59
|
+
| `j-templates/Store` | `ReadStore`, `Store`, `StoreSync`, `StoreAsync`, `ObservableScope`, `ObservableNode`, `Opaque`; types `IStoreOptions`, `IStoreAsyncOptions`, `IStoreSyncOptions` |
|
|
60
|
+
| `j-templates/Store/Diff` | `PROJECTION_PREFIX`, `MESSAGE_PREFIX`; types `DiffTreeProjectionMap`, `DiffTreeProjectionDefinition`, `DiffTreeProjectionFunc`, `DiffSpliceResult`, `IDiffTree`, the batch types, the tree's `IObservableDiffTreeConfig`, `DeliveryForm`, `ISubscribeOptions`, `IDiffPost`, `ISnapshotUpdate`, and the channel's `IChannel`, `IHostRequest`, `IHostMessage`, `IConnectMessage`, `IWorkerLease`, `ILocalLease`, and the endpoint's `IDiffTreeEndpoint` and its option types; `FollowRoutes`, and `DiffTreeEndpoint`. Nothing that reads `window`, so a worker bundle can import it. |
|
|
61
61
|
|
|
62
|
-
**Deep imports used below:** `j-templates/Store/Diff/diffTree` (`DiffTree`), `j-templates/Store/Diff/
|
|
62
|
+
**Deep imports used below:** `j-templates/Store/Diff/diffTree` (`DiffTree`), `j-templates/Store/Diff/observableDiffTree` (`ObservableDiffTree`), `j-templates/Store/Diff/hostPublisher` (`HostPublisher`), `j-templates/Store/Diff/channelClient` (`Subscribe`, `Once`, `ChannelClient`), `j-templates/Store/Diff/diffTreeHub` (`DiffTreeHub`), `j-templates/Store/Diff/diffTreeEndpoint` (`DiffTreeEndpoint`), `j-templates/Store/Diff/onConnect` (`OnConnect`), `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`).
|
|
63
63
|
|
|
64
64
|
**Element functions** (all verified present):
|
|
65
65
|
|
|
@@ -283,7 +283,6 @@ Decorators work on component classes only. Services use `ObservableScope`, `Obse
|
|
|
283
283
|
| `@State()` | plain objects and arrays mutated in place | a deep reactive proxy | on any tracked write |
|
|
284
284
|
| `@Scope()` getter | cheap derivations; filters/sorts of existing objects | whatever the getter returns (new object each run if it builds one) | lazily, on the next read after a change |
|
|
285
285
|
| `@Computed()` getter | building new objects consumed in several places | the **same** object every time, updated in place | eagerly, when a dependency changes |
|
|
286
|
-
| `@ComputedAsync(default)` getter | as `@Computed`, diffed off-thread | same object every time; `default` until first result | eagerly |
|
|
287
286
|
| `@Watch(sel)` method | side effects on change | — | — |
|
|
288
287
|
| `@Inject(Type)` field | dependency injection | — | — |
|
|
289
288
|
| `@Destroy()` field | calling `.Destroy()` at teardown | — | — |
|
|
@@ -340,19 +339,6 @@ Tracked array methods: `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `rev
|
|
|
340
339
|
- The copy step reads every property of the result, so all of them become dependencies. Returning a `@State` array therefore re-runs the getter when any item property changes (verified).
|
|
341
340
|
- No arguments. Prefer `@Scope` or a plain getter for cheap values.
|
|
342
341
|
|
|
343
|
-
### @ComputedAsync
|
|
344
|
-
|
|
345
|
-
```typescript
|
|
346
|
-
@ComputedAsync(null) get user(): User | null {
|
|
347
|
-
return { id: this.userId, name: lookupName(this.userId) }; // must be synchronous
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
- The getter must be synchronous. "Async" refers to the backend: results are diffed in a `StoreAsync` worker.
|
|
352
|
-
- Returns `default` first, then the computed object, with the same reference across updates (verified).
|
|
353
|
-
- Each instance creates `new Worker(new URL("../Store/Diff/defaultDiffTreeWorker.js", import.meta.url), { type: "module" })` (from source). It needs a real `Worker` and a bundler that handles `new URL(…, import.meta.url)`. In jsdom tests, stub `globalThis.Worker` (see Scheduling, errors and testing).
|
|
354
|
-
- For genuinely async data, use `@Scope()` + `scope(async () => …)` or `ObservableScope.Create(async () => …)`.
|
|
355
|
-
|
|
356
342
|
### @Watch
|
|
357
343
|
|
|
358
344
|
```typescript
|
|
@@ -533,15 +519,16 @@ class Injector {
|
|
|
533
519
|
|
|
534
520
|
## Store
|
|
535
521
|
|
|
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 `
|
|
522
|
+
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 `StoreAsync` when the tree lives in a worker, whether the main thread writes it, the worker produces the data itself, or both.
|
|
537
523
|
|
|
538
|
-
| | `StoreSync` | `StoreAsync` |
|
|
539
|
-
| --- | --- | --- |
|
|
540
|
-
| Constructor | `new StoreSync(
|
|
541
|
-
|
|
|
542
|
-
|
|
|
543
|
-
|
|
|
544
|
-
|
|
|
524
|
+
| | `StoreSync` | `StoreAsync` |
|
|
525
|
+
| --- | --- | --- |
|
|
526
|
+
| Constructor | `new StoreSync(tree?, options?)`: `tree` is `hub.OpenLocal(name)`, an `ObservableDiffTree` (for routes), or left out for an empty tree of its own | `new StoreAsync(channel, options?)`; the channel may be `hub.Open(name)` |
|
|
527
|
+
| Options | `{ keyFunc?, roots?, opaque? }`. `keyFunc` only for a tree with none to give: a lease's is the endpoint's and a given tree's its own, and giving one there too throws | `{ keyFunc?, roots?, opaque? }`. `keyFunc` only over a plain channel; a lease's is the endpoint's, and giving one too throws |
|
|
528
|
+
| Diffing | main thread, synchronous | in the worker's `ObservableDiffTree`, served by `HostPublisher` |
|
|
529
|
+
| Writes | `void`; readable immediately | `Promise`; batched per tick, readable once it resolves. Optional: without writes it's a read-only view of the tree |
|
|
530
|
+
| Changes from elsewhere | commits made on `store.Tree` | every commit the worker's tree makes, whatever caused it |
|
|
531
|
+
| `Destroy()` | stops applying commits; over a lease, releases it | required; unsubscribes and stops listening. Over a lease, releases it; over a channel, leaves the worker running |
|
|
545
532
|
|
|
546
533
|
### API
|
|
547
534
|
|
|
@@ -555,14 +542,14 @@ Push(key: string, ...items: unknown[]); // append to the array at key
|
|
|
555
542
|
Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]); // returns removed items
|
|
556
543
|
```
|
|
557
544
|
|
|
558
|
-
`StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below).
|
|
545
|
+
`StoreAsync` has the same methods; the four writers return promises. It also has `Flush()` (see below).
|
|
559
546
|
|
|
560
547
|
### keyFunc and flattening
|
|
561
548
|
|
|
562
549
|
`keyFunc(value)` returns an id for any object (or `undefined`). On `Write`/`Push`, every nested object with an id is also stored under that id, so it can be patched directly while `Get` still returns the original shape.
|
|
563
550
|
|
|
564
551
|
```typescript
|
|
565
|
-
const store = new StoreSync((v: any) => v?.id);
|
|
552
|
+
const store = new StoreSync(undefined, { keyFunc: (v: any) => v?.id });
|
|
566
553
|
store.Write([{ id: "a", done: false }, { id: "b", done: false }], "todos");
|
|
567
554
|
store.Patch("a", { done: true }); // targets the nested item by id
|
|
568
555
|
store.Get<Todo[]>("todos", [])[0].done; // true
|
|
@@ -575,42 +562,48 @@ store.Get<Todo[]>("todos", [])[0].done; // true
|
|
|
575
562
|
|
|
576
563
|
### StoreAsync and its worker
|
|
577
564
|
|
|
578
|
-
Supply a
|
|
565
|
+
Supply a channel to a worker whose entry file hosts an `ObservableDiffTree`: a `Worker`, or a `MessagePort` to one. Any `keyFunc` or projections used for diffing must live in that file; the constructor's `keyFunc` only resolves ids on the main thread.
|
|
579
566
|
|
|
580
567
|
```typescript
|
|
581
568
|
// diff-worker.ts (bundled as its own worker entry)
|
|
582
|
-
import {
|
|
583
|
-
import {
|
|
584
|
-
|
|
569
|
+
import { ObservableDiffTree } from "j-templates/Store/Diff/observableDiffTree";
|
|
570
|
+
import { HostPublisher } from "j-templates/Store/Diff/hostPublisher";
|
|
571
|
+
HostPublisher(new ObservableDiffTree({ keyFunc: (v: any) => v?.id }), self as any);
|
|
585
572
|
|
|
586
573
|
// main thread
|
|
587
574
|
const worker = new Worker(new URL("./diff-worker.ts", import.meta.url), { type: "module" });
|
|
588
|
-
const store = new StoreAsync(worker, (v: any) => v?.id);
|
|
575
|
+
const store = new StoreAsync(worker, { keyFunc: (v: any) => v?.id });
|
|
589
576
|
await store.Write(todos, "todos");
|
|
590
|
-
store.Get("todos"); //
|
|
577
|
+
store.Get("todos"); // defined once the write resolves
|
|
591
578
|
store.Destroy(); // when done
|
|
579
|
+
worker.terminate(); // the worker is yours to end
|
|
592
580
|
```
|
|
593
581
|
|
|
594
582
|
Data crosses the worker boundary by structured clone. Class instances arrive as plain objects without their methods; functions cannot be sent. Store plain data.
|
|
595
583
|
|
|
596
|
-
**
|
|
584
|
+
**The store subscribes to the tree.** On construction it sends `jt-subscribe` and applies every commit the tree posts, in arrival order: its own batches, other writers', and the worker's own work. A store created after the tree has committed starts from the committed state. `options.roots` limits the roots it receives (exact keys and `task_*` patterns); a root it writes but doesn't receive is never read back. The channel can carry the app's own messages too: `HostPublisher` and the store use `addEventListener` and handle only `jt-` messages.
|
|
585
|
+
|
|
586
|
+
**Batching (8.0.27).** Every `Write`, `Patch`, `Push` and `Splice` made in the same tick goes to the worker as one `jt-batch` and is applied in call order. The worker runs projections once per batch, after the last change, and commits. At most one batch waits behind the one in flight.
|
|
597
587
|
|
|
588
|
+
- A change's promise resolves on its batch's `jt-ack`, which the host sends after the commit's posts, so the change has been applied when it resolves. A commit the worker makes while the batch is in flight is applied too, but doesn't resolve it.
|
|
598
589
|
- You don't have to await a change before making the next one. `Patch`, `Push` and `Splice` accept a key whose `Write` is still pending, and `Patch` merges in the worker, on top of everything before it.
|
|
599
590
|
- The pending-key check covers only the key passed to `Write`. Entities that `keyFunc` will flatten out of that write don't exist yet: `Patch("a", …)` right after an un-awaited `Write([{ id: "a" }], "todos")` rejects with `Key not found in store` (verified). Await the write first.
|
|
600
|
-
- `await store.Flush()` waits for every
|
|
601
|
-
- **A failed
|
|
591
|
+
- `await store.Flush()` waits for every batch sent before the call to be acknowledged, and resolves at once when none is in flight. It rejects with the first error among those batches. Changes made after it go in a later batch.
|
|
592
|
+
- **A failed batch rejects only itself.** When a change fails in the worker (for example `Splice` on a value that isn't an array), the calls after it and the batch's projections are skipped, what the batch applied before it is committed, and every change in that batch rejects with the error. Later batches carry on, and the next one's projections catch up. A projection that throws fails its batch the same way.
|
|
602
593
|
|
|
603
594
|
### Projections
|
|
604
595
|
|
|
605
|
-
A projection is a derived value the store computes and stores under `$projection_<name>`.
|
|
596
|
+
A projection is a derived value the store computes and stores under `$projection_<name>`. Give them to the tree's root route, and the tree to `StoreSync` (for `StoreAsync`, to the worker's tree). The store takes the tree's keyFunc.
|
|
606
597
|
|
|
607
598
|
```typescript
|
|
608
599
|
import { PROJECTION_PREFIX } from "j-templates/Store/Diff"; // "$projection_"
|
|
609
600
|
|
|
610
|
-
|
|
601
|
+
import { ObservableDiffTree } from "j-templates/Store/Diff/observableDiffTree";
|
|
602
|
+
|
|
603
|
+
const store = new StoreSync(new ObservableDiffTree({ keyFunc: (v: any) => v?.id, routes: { "/": { projections: {
|
|
611
604
|
activeCount: { reads: ["todos"], projection: (keys, todos) => todos.filter((t) => !t.done).length },
|
|
612
605
|
doubled: { reads: [`${PROJECTION_PREFIX}activeCount`], projection: (keys, n) => n * 2 },
|
|
613
|
-
});
|
|
606
|
+
} } } }));
|
|
614
607
|
store.Write(todos, "todos");
|
|
615
608
|
store.Get<number>(`${PROJECTION_PREFIX}activeCount`);
|
|
616
609
|
```
|
|
@@ -621,38 +614,136 @@ store.Get<number>(`${PROJECTION_PREFIX}activeCount`);
|
|
|
621
614
|
- A projection has no value until every read resolves. It re-runs when a read's value changes.
|
|
622
615
|
- A cycle or a read of an undefined projection throws at construction.
|
|
623
616
|
- A dotted read can pass through a keyed entity (`"doc.owner.name"`, where `owner` has an id). It reads the entity's own root, so a `Patch` to the entity reruns the projection with the new value (verified; before 8.0.24 such a read returned the stale embedded copy).
|
|
624
|
-
- `StoreSync` and the `StoreAsync` worker run projections for
|
|
617
|
+
- `StoreSync` and the `StoreAsync` worker run projections for each batch. An `ObservableDiffTree`'s `Commit()` doesn't run them. `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).
|
|
618
|
+
|
|
619
|
+
### Opaque roots
|
|
620
|
+
|
|
621
|
+
A root that only grows with the data, such as a drawing of every row, is expensive to diff field by field and to read through proxies. List its key in the store's `opaque` option to hold it whole:
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
const layoutConfig = { opaque: ["$projection_layout"] }; // the tree writes it whole, the store holds it whole
|
|
625
|
+
const store = new StoreSync(new ObservableDiffTree({ keyFunc, routes, ...layoutConfig }), layoutConfig);
|
|
626
|
+
const layout = store.Get<Layout>("$projection_layout"); // the raw value: no proxies inside it
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
- The key can be a written root or a projection's `$projection_<key>`. The tree writes it whole whenever it changes (`DiffTree.MarkOpaque`): a `StoreSync` marks the empty tree it creates (`new StoreSync(undefined, { opaque })`), and a tree it's given has the keys in its config. `StoreAsync` sends the keys to its worker ahead of the first change. A store on a hub's tree needs the tree to write the key whole too: give both the same keys from a file both import (`new ObservableDiffTree({ endpoint, routes, ...searchConfig })`, `new StoreAsync(hub.Open("search"), searchConfig)`).
|
|
630
|
+
- `Get` returns the same reference until the root changes, and only that change updates its readers. A write that changes nothing updates no one.
|
|
631
|
+
- The value is the store's own: treat it as read-only. A change made inside it notifies no one, and under `StoreSync` it is also the `DiffTree`'s committed snapshot.
|
|
632
|
+
- Write the raw value. Don't pass an `Opaque` to `Write`, `Patch`, `Push` or `Splice`, at any depth: the store boxes the root itself, and a box written to a store isn't opened (under `StoreAsync` it arrives as a plain `{ value }`).
|
|
633
|
+
- An opaque root can't be read through a key: a `keyFunc` alias that resolves to one throws.
|
|
634
|
+
|
|
635
|
+
### ObservableDiffTree
|
|
636
|
+
|
|
637
|
+
An `ObservableDiffTree` is a `DiffTree` that delivers every `Commit()` to its subscribers: the tree's whole API (`WritePath`, `PatchPath`, `PushPath`, `SplicePath`, `ApplyDiff`, `DeleteRoot`, `DeleteRoute`, `RunProjections`, `MarkOpaque`, `ReadPath`, `Snapshot`, `Committed`, `CommittedKeys`), with `Commit()` delivering what it diffs. `StoreSync` subscribes to one it creates, exposed as `store.Tree`.
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { ObservableDiffTree } from "j-templates/Store/Diff/observableDiffTree";
|
|
641
|
+
const tree = new ObservableDiffTree({ keyFunc, routes }); // and `endpoint`, `opaque`: IObservableDiffTreeConfig
|
|
642
|
+
const stop = tree.Subscribe({ roots: ["loading", "$projection_*"] }, post => { /* post.diff, post.seq */ });
|
|
643
|
+
tree.Subscribe({ roots: ["index", "task_*"], form: "snapshot" }, update => { /* update.roots, update.deleted */ });
|
|
644
|
+
|
|
645
|
+
tree.WritePath("loading", true);
|
|
646
|
+
tree.Commit(); // delivered now, before the work below
|
|
647
|
+
tree.WritePath("results", computeResults());
|
|
648
|
+
tree.RunProjections(); // Commit() doesn't run them
|
|
649
|
+
tree.Commit();
|
|
650
|
+
|
|
651
|
+
const { index, revision } = await tree.Once(["index", "revision"]); // the first commit holding both
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
- A subscription first gets the committed state of the roots it names, inside `Subscribe` and in the same shape as every later delivery, so one handler applies them all; nothing is sent when no root matches. Then every commit that touches them follows. Uncommitted writes arrive with the next commit. To subscribe again, clear what the earlier subscription delivered first.
|
|
655
|
+
- `roots` takes exact keys and patterns ending in `*`; omitted, every root. With a `keyFunc`, name the entity roots too (`task_*`): editing an entity commits only the entity's root, not the roots that refer to it.
|
|
656
|
+
- `"diff"` (the default) delivers diff records, which a store applies. `"snapshot"` delivers each changed root whole, the tree's committed value itself: read-only.
|
|
657
|
+
- Delivery is synchronous. A subscriber may write to another tree, but writing to or committing the tree delivering to it throws. A subscriber's error doesn't stop the others; `Commit()` throws it after.
|
|
658
|
+
- `SubscriberCount(key)` counts the subscriptions naming a key exactly, and `OnReleased(pattern, callback)` reports when a key loses its last one. Deleting the key is the caller's (`DeleteRoot` and `Commit`, from a timer: the callback can run inside a delivery).
|
|
625
659
|
|
|
626
|
-
###
|
|
660
|
+
### A worker's own commits
|
|
627
661
|
|
|
628
|
-
|
|
662
|
+
A worker that produces data itself (a computation's result, data it fetched) writes to its hosted tree and commits. The commit reaches every subscriber, the main thread's `StoreAsync` included, with no round trip. A store that never writes is a read-only view of the tree.
|
|
629
663
|
|
|
630
664
|
```typescript
|
|
631
665
|
// engine-worker.ts (bundled as its own worker entry)
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
self.
|
|
635
|
-
|
|
636
|
-
tree.
|
|
637
|
-
tree.
|
|
638
|
-
tree.
|
|
639
|
-
|
|
666
|
+
const tree = new ObservableDiffTree({ keyFunc: (v: any) => v?.id, routes });
|
|
667
|
+
HostPublisher(tree, self as any);
|
|
668
|
+
self.addEventListener("message", async (e) => {
|
|
669
|
+
if (e.data?.type !== "run") return; // the app's own message, beside the jt- ones
|
|
670
|
+
tree.WritePath("status", "running");
|
|
671
|
+
tree.Commit(); // posted now; projections not rerun
|
|
672
|
+
tree.WritePath("results", await compute(e.data));
|
|
673
|
+
tree.WritePath("status", "done");
|
|
674
|
+
tree.RunProjections();
|
|
675
|
+
tree.Commit();
|
|
676
|
+
});
|
|
640
677
|
|
|
641
678
|
// main thread
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
store.Get("results"); // reactive; undefined until the first commit
|
|
645
|
-
store.Destroy(); // stops listening; terminate the worker yourself
|
|
679
|
+
const store = new StoreAsync(worker, { keyFunc: (v: any) => v?.id }); // created whenever: it starts from the committed state
|
|
680
|
+
store.Get("results"); // reactive; undefined until a commit holds it
|
|
646
681
|
```
|
|
647
682
|
|
|
648
|
-
-
|
|
649
|
-
-
|
|
650
|
-
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
683
|
+
- Nothing reaches subscribers until `Commit()`, and a commit with no changes posts nothing. Errors in the worker's own writes throw to the worker's code; nothing is posted for them.
|
|
684
|
+
- `HostPublisher(tree, self)` serves the worker's own channel; `host.Serve(port)` serves a `MessagePort` it's given, so another worker can subscribe.
|
|
685
|
+
- Another worker subscribes over a port with `Subscribe(port, { roots, form: "snapshot" }, update => …)` and writes the roots it receives under a route of its own tree, which is never posted on. `Once(port, keys, { signal })` waits for the first commit that holds every key. Values arrive as structured-cloned copies.
|
|
686
|
+
|
|
687
|
+
### DiffTreeHub: trees by endpoint
|
|
688
|
+
|
|
689
|
+
On the main thread, a `DiffTreeHub` owns the app's trees. One key is one tree in the app's dependency graph, running in a worker or on the main thread. The hub creates each tree from a factory when it's first needed, hands out leases on it, connects each tree to the ones it follows, re-creates one on request, and ends each one when its last lease ends. It never carries data.
|
|
690
|
+
|
|
691
|
+
An **endpoint** describes a key once, as plain data: its name, what it follows and its keyFunc. The main thread registers it, the tree is built from it, and the trees that follow it read their paths from it. It imports nothing that reads `window`, so workers import the same definitions. The tree's own configuration (routes, projections, `opaque`) is one object given to its constructor, in a file only its worker imports; config its stores share with it, `opaque` say, goes in a file both import.
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
// endpoints.ts: imported by the main thread and the workers
|
|
695
|
+
import { DiffTreeEndpoint } from "j-templates/Store/Diff/diffTreeEndpoint";
|
|
696
|
+
export const engineEndpoint = DiffTreeEndpoint.Define("engine", undefined, { keyFunc });
|
|
697
|
+
export const engineConfig = { opaque: ["view"] }; // shared by the engine's tree and its stores
|
|
698
|
+
export const traceEndpoint = DiffTreeEndpoint.Define("trace", remote => ({
|
|
699
|
+
engine: remote(engineEndpoint, ["index", "task_*"]), // { index: "/engine/index", "task_*": "/engine/task_*" }
|
|
700
|
+
}));
|
|
701
|
+
export const filterEndpoint = DiffTreeEndpoint.Define("filter", remote => ({
|
|
702
|
+
trace: remote(traceEndpoint, ["$projection_count"]),
|
|
703
|
+
}));
|
|
704
|
+
|
|
705
|
+
// trace-tree.ts: builds the tree; the endpoint gives its keyFunc and a follow route per source
|
|
706
|
+
export const newTraceTree = () => new ObservableDiffTree({
|
|
707
|
+
endpoint: traceEndpoint,
|
|
708
|
+
routes: { "/": { projections: { count: { reads: [traceEndpoint.config.engine.index], projection: countOf } } } },
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// trace-worker.ts: serves the tree and follows its sources into it
|
|
712
|
+
DiffTreeEndpoint.Host(traceEndpoint, newTraceTree(), self as any);
|
|
713
|
+
|
|
714
|
+
// main thread
|
|
715
|
+
import { DiffTreeHub } from "j-templates/Store/Diff/diffTreeHub";
|
|
716
|
+
const hub = new DiffTreeHub();
|
|
717
|
+
hub.Register(engineEndpoint, () => {
|
|
718
|
+
const worker = new Worker(new URL("./engine-worker.ts", import.meta.url), { type: "module" });
|
|
719
|
+
worker.postMessage({ type: "load", project }); // starting intent: runs on every start
|
|
720
|
+
return worker;
|
|
721
|
+
});
|
|
722
|
+
hub.Register(traceEndpoint, () => new Worker(new URL("./trace-worker.ts", import.meta.url), { type: "module" }), {
|
|
723
|
+
releaseDebounce: 5000,
|
|
724
|
+
});
|
|
725
|
+
hub.Register(filterEndpoint, newFilterTree); // returns the tree: it runs on this thread
|
|
726
|
+
|
|
727
|
+
const engine = new StoreAsync(hub.Open("engine"), engineConfig); // a worker's tree, over a port; held for the app's lifetime
|
|
728
|
+
const trace = new StoreAsync(hub.Open("trace")); // starts trace; its lease on its source keeps engine open
|
|
729
|
+
const filter = new StoreSync(hub.OpenLocal("filter")); // a tree on this thread, directly
|
|
730
|
+
// teardown: each store releases its lease
|
|
731
|
+
trace.Destroy(); // trace ends 5 s later unless reopened
|
|
732
|
+
hub.Restart("engine"); // a new engine; every store on it starts over with it
|
|
733
|
+
hub.Destroy(); // every tree ended
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
- `Register(endpoint, factory)` creates nothing; the first lease does. The endpoint's sources come from its definition, and each must be registered before it. While a tree runs, the hub holds a lease on each source and gives the tree a port to it, again whenever either restarts.
|
|
737
|
+
- The factory returns a worker, or an `ObservableDiffTree` to run on this thread, which the hub hosts with the same `DiffTreeEndpoint.Host` on a channel of its own. The first start fixes the key's kind. A factory that returns neither, or the other kind on a restart, throws.
|
|
738
|
+
- `Open` returns an `IWorkerLease`, for `StoreAsync`; `OpenLocal` returns an `ILocalLease`, for `StoreSync`. A store given the other kind's lease doesn't compile, and opening a key as the other kind throws. Any number of stores may be opened on one tree. A store owns its lease, takes its keyFunc from the endpoint, and releases the lease on `Destroy()`.
|
|
739
|
+
- A `StoreSync` on a hub's tree shares it: a change from any store on it, and from the trees it follows, reaches every one of them before it returns.
|
|
740
|
+
- `DiffTreeEndpoint.Host(endpoint, tree, scope, { onFollow })` throws for a tree built from another endpoint, or from none when `endpoint` follows sources: build it with `new ObservableDiffTree({ endpoint })`. It applies each source's post at `/<source>/` and then, by default, runs the projections and commits. An `onFollow(tree, source)` replaces that, for a tree that acts at the end of the turn. An `onConnect(tree, source, port)` runs with each new port to a source, for a tree that also reads from the source outside the follow (a one-off `Once`); it may return a cleanup, which runs before the next port's call.
|
|
741
|
+
- **The tree is the source of truth.** `Restart` is the app choosing to start a tree over; the hub doesn't restart on `error`. Every store on the tree starts over with the new one, as when it first subscribed: its state is applied in place, so readers keep their objects, and the roots it lacks are deleted. Nothing a store wrote is written back, and changes the old tree hadn't acknowledged reject. State that must outlive a restart goes in a tree of its own, which the restarted tree follows.
|
|
742
|
+
- A tree is released when its last lease ends: at once, or after `releaseDebounce` ms, unless a lease is opened meanwhile. Releasing ends it and its leases on its sources. Keep a store open on a tree that must outlive every view.
|
|
743
|
+
- The hub tells each tree its sources first, on every start. `OnConnect` for a source the endpoint doesn't follow throws: at once if the list has arrived, otherwise when it does, as the worker's uncaught error (the `Worker`'s `error` event on the main thread). The factory runs again on `Restart` and on the next lease after a release, so it should send whatever the worker needs to start.
|
|
744
|
+
- `Unregister(name)` ends one tree, and throws while another registered endpoint follows it; `Destroy()` ends all of them, the last registered first. `KindOf(name)` says where a key's tree runs, once it has started.
|
|
745
|
+
- Keep a worker's factory a literal `new Worker(new URL(...), { type: "module" })` at registration: bundlers compile only that form into a worker bundle.
|
|
746
|
+
- A tree that wires its connections by hand serves itself with `HostPublisher` and follows with `OnConnect(source, port => …)` and a `ChannelSubscription`; `DiffTreeEndpoint.Host` does this for every source.
|
|
656
747
|
|
|
657
748
|
## ObservableScope and ObservableNode
|
|
658
749
|
|
|
@@ -691,7 +782,7 @@ A service deriving from a store:
|
|
|
691
782
|
|
|
692
783
|
```typescript
|
|
693
784
|
class TodoService implements IDestroyable {
|
|
694
|
-
private store = new StoreSync((v: any) => v?.id);
|
|
785
|
+
private store = new StoreSync(undefined, { keyFunc: (v: any) => v?.id });
|
|
695
786
|
private active = ObservableScope.Create(() =>
|
|
696
787
|
this.store.Get<Todo[]>("todos", []).filter((t) => !t.done));
|
|
697
788
|
get Active() { return ObservableScope.Value(this.active); }
|
|
@@ -712,6 +803,7 @@ ObservableNode.CreateFactory(alias?) // proxy factory with aliasing (used by st
|
|
|
712
803
|
|
|
713
804
|
- `Snapshot` returns the same object on repeated calls until a write. After a write, unchanged nested parts are shared (`===`) with the previous snapshot. Snapshots are **not frozen**; treat them as read-only.
|
|
714
805
|
- `JSON.stringify(node)` and `node.toJSON()` use the cached snapshot.
|
|
806
|
+
- **`Opaque`** (`j-templates/Store`) holds a value whole inside a node. Assign one (`node.view = new Opaque(view)`), push or splice it into a node's array, or return it inside a `@Computed` result. The node stores the box and every read opens it, so readers get the raw value, never proxied, and re-run only when the box is replaced. Array callbacks, `pop`, `shift`, `splice`'s removed items and a `sort` comparator get raw values too. Spread, `Object.assign`, property descriptors, snapshots and `JSON.stringify` give the raw value too, so an object composed from reads holds no box. `Unwrap` gives the node's storage, boxes included (`Create(Unwrap(node))` is `node` itself), and `Clone` keeps a box it's given: fine to write back into a node, not to a store.
|
|
715
807
|
- `ApplyDiff`, `ApplySplice` and `Touch` also exist; they are internal to the stores. `ApplyDiff` and `ApplySplice` deep-clone what they write, so the node never shares objects with the caller.
|
|
716
808
|
|
|
717
809
|
**Snapshot reads do not subscribe.** `JSON.stringify(node)`, `node.toJSON()` and `ObservableNode.Snapshot(node)` register no dependencies, so a scope that reads only through them never updates (verified). Read properties directly, or use `ObservableNode.Clone(node)`, which tracks every nested field and leaves the source untouched:
|
|
@@ -751,38 +843,26 @@ Verification used tsx with jsdom; Vitest with jsdom should behave the same. Requ
|
|
|
751
843
|
|
|
752
844
|
1. Create `window`, `document` and the DOM classes as globals **before** importing j-templates.
|
|
753
845
|
2. Set `SYNC_SCHEDULING=true` for synchronous assertions, or await a timeout (100–150 ms was reliable in testing) after each action.
|
|
754
|
-
3. For
|
|
846
|
+
3. For `StoreAsync`, stub `Worker` with an in-process shim: two `EventTarget`s that deliver structured-cloned copies a tick later, with a `HostPublisher` on one side. Await a timeout before reading a commit the worker made on its own:
|
|
755
847
|
|
|
756
848
|
```typescript
|
|
757
|
-
import {
|
|
758
|
-
import {
|
|
759
|
-
|
|
760
|
-
function fakeWorker(tree = new
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return
|
|
849
|
+
import { ObservableDiffTree } from "j-templates/Store/Diff/observableDiffTree";
|
|
850
|
+
import { HostPublisher } from "j-templates/Store/Diff/hostPublisher";
|
|
851
|
+
|
|
852
|
+
function fakeWorker(tree = new ObservableDiffTree()) {
|
|
853
|
+
const main: any = new EventTarget(), inner: any = new EventTarget();
|
|
854
|
+
const send = (to: EventTarget) => (d: unknown) =>
|
|
855
|
+
setTimeout(() => to.dispatchEvent(new MessageEvent("message", { data: structuredClone(d) })));
|
|
856
|
+
main.postMessage = send(inner); inner.postMessage = send(main); main.terminate = () => {};
|
|
857
|
+
HostPublisher(tree, inner);
|
|
858
|
+
return main;
|
|
767
859
|
}
|
|
768
|
-
|
|
769
|
-
const store = new StoreAsync(fakeWorker(
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
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:
|
|
773
|
-
|
|
774
|
-
```typescript
|
|
775
|
-
import { DiffTreeWriter } from "j-templates/Store/Diff/diffTreeWriter";
|
|
776
|
-
|
|
777
|
-
const target = new EventTarget();
|
|
778
|
-
const port = { postMessage: (d: unknown) => setTimeout(() =>
|
|
779
|
-
target.dispatchEvent(new MessageEvent("message", { data: structuredClone(d) }))) };
|
|
780
|
-
const writer = new DiffTreeWriter(port);
|
|
781
|
-
const store = new PushStore(target);
|
|
860
|
+
const tree = new ObservableDiffTree({ keyFunc: (v: any) => v?.id });
|
|
861
|
+
const store = new StoreAsync(fakeWorker(tree), { keyFunc: (v: any) => v?.id });
|
|
782
862
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
await new Promise((r) => setTimeout(r));
|
|
863
|
+
tree.WritePath("a", { value: 1 });
|
|
864
|
+
tree.Commit();
|
|
865
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
786
866
|
store.Get<any>("a").value; // 1
|
|
787
867
|
```
|
|
788
868
|
|
|
@@ -968,13 +1048,15 @@ Component.Attach(document.getElementById("app")!, app({}));
|
|
|
968
1048
|
18. **Native DOM listeners belong on elements you render**, not the host.
|
|
969
1049
|
19. **`svg()` is HTML-namespace.** Use namespaced components or `innerHTML` for SVG.
|
|
970
1050
|
20. **`ObservableScope.Create` with no reactive reads is static** and ignores `Update()`. Use `Basic`.
|
|
971
|
-
21. **`StoreAsync` writes must be awaited** before reading; data crosses by structured clone (class methods are lost). Changes in one tick are batched, and
|
|
1051
|
+
21. **`StoreAsync` writes must be awaited** before reading; data crosses by structured clone (class methods are lost). Changes in one tick are batched, and a failed change rejects every change in its batch, and only those.
|
|
972
1052
|
22. **The package needs a bundler or TypeScript runner**; plain Node cannot import it.
|
|
973
1053
|
|
|
974
1054
|
23) **Equal-value writes still notify.** Assigning the current value again re-runs every reader. Guard repeated writes or read through `gate()`.
|
|
975
1055
|
24) **`JSON.stringify(node)` and `ObservableNode.Snapshot(node)` don't subscribe.** Read properties directly, or `ObservableNode.Clone(node)` to track everything.
|
|
976
1056
|
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.
|
|
977
|
-
26) **A
|
|
1057
|
+
26) **A worker's own writes reach no one until `Commit()`, and `Commit()` doesn't run projections.** Call `RunProjections()` before it when the change should update them.
|
|
1058
|
+
27) **An opaque value read and written elsewhere is plain data again.** A read opens the box, so writing the value into another node or returning it from `@Computed` proxies it there, and a `@Computed` result merges into it in place. Build from it, or box it again (`new Opaque(value)`). Never mutate it.
|
|
1059
|
+
28) **Don't write to an `ObservableDiffTree` (or its `StoreSync`) from its own delivery.** A subscriber that writes to, commits or runs projections on the tree delivering to it throws `ObservableDiffTree: a subscriber can't write to or commit the tree delivering to it.` Write to another tree, or defer the write.
|
|
978
1060
|
|
|
979
1061
|
## Debugging
|
|
980
1062
|
|
|
@@ -1003,13 +1085,11 @@ Component.Attach(document.getElementById("app")!, app({}));
|
|
|
1003
1085
|
| `Emitter.Emit: re-entry during emit is not supported` | a `Watch`/`OnUpdated` callback updates the scope or value that is notifying it | defer the write, or move it out of the callback |
|
|
1004
1086
|
| `scope() must be called within a watch context` inside `peek()` | tracking is off inside `peek` | call `scope()`/`gate()` outside the `peek` callback |
|
|
1005
1087
|
| `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 |
|
|
1006
|
-
| Every `StoreAsync` change
|
|
1088
|
+
| Every `StoreAsync` change in one batch rejects | one change in the batch failed in the worker, or a projection threw | fix that change; later batches apply as usual |
|
|
1007
1089
|
| A test's projection never updates | the test drives a `DiffTree` directly | call `tree.RunProjections()` after each write |
|
|
1008
|
-
| `
|
|
1009
|
-
|
|
|
1010
|
-
|
|
|
1011
|
-
| `RunProjections` throws on a `DiffTreeWriter` | projections run in `Commit()` | call `Commit()` instead |
|
|
1012
|
-
| `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` |
|
|
1090
|
+
| `StoreAsync` never gets the worker's own data | the worker wrote but didn't call `Commit()` | call `Commit()` after the work |
|
|
1091
|
+
| A worker commit arrives but projections are stale | `Commit()` doesn't run projections | call `RunProjections()` before `Commit()` |
|
|
1092
|
+
| `StoreAsync.Destroy()` leaves the worker running | by design: the creator terminates it | call `worker.terminate()` |
|
|
1013
1093
|
|
|
1014
1094
|
## Corrections from the v5 primer
|
|
1015
1095
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { IChannel, IDiffMethodBatch, IDiffPost, ISnapshotUpdate, ISubscribeOptions } from "./diffTree.types";
|
|
2
|
+
/**
|
|
3
|
+
* The client side of a `HostPublisher` channel: it sends batches and subscriptions, and routes
|
|
4
|
+
* the host's posts and acks back by id. Messages for ids it didn't send are left alone, so
|
|
5
|
+
* other clients and the app's own messages share the channel.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ChannelClient {
|
|
8
|
+
private channel;
|
|
9
|
+
private subscriptions;
|
|
10
|
+
private batches;
|
|
11
|
+
private listener;
|
|
12
|
+
constructor(channel: IChannel);
|
|
13
|
+
/**
|
|
14
|
+
* Sends a batch of calls.
|
|
15
|
+
* @returns A promise that resolves on the batch's `jt-ack`, after the commit's posts have been
|
|
16
|
+
* delivered, or rejects with the batch's error.
|
|
17
|
+
*/
|
|
18
|
+
Batch(calls: IDiffMethodBatch): Promise<void>;
|
|
19
|
+
/** Rejects the batches waiting for an ack with `error`; this client stops waiting for them */
|
|
20
|
+
RejectBatches(error: unknown): void;
|
|
21
|
+
/** See `DiffTree.MarkOpaque` */
|
|
22
|
+
MarkOpaque(keys: readonly string[]): void;
|
|
23
|
+
/**
|
|
24
|
+
* Subscribes to the host's tree. Deliveries arrive as the host posts them.
|
|
25
|
+
* @param onSubscribed Called once the host has posted the first state, with its error when
|
|
26
|
+
* subscribing failed.
|
|
27
|
+
* @returns The subscription's id.
|
|
28
|
+
*/
|
|
29
|
+
Subscribe(options: ISubscribeOptions, callback: (delivery: any) => void, onSubscribed?: (error?: unknown) => void): number;
|
|
30
|
+
Unsubscribe(sub: number): void;
|
|
31
|
+
/**
|
|
32
|
+
* Ends this client's subscriptions and stops listening. Batches waiting for an ack are left
|
|
33
|
+
* unsettled. The channel stays open.
|
|
34
|
+
*/
|
|
35
|
+
Destroy(): void;
|
|
36
|
+
private Receive;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A subscription to a tree served on a channel that outlives the channel: when the tree is
|
|
40
|
+
* replaced (a restarted worker, given on a new port), `Connect` subscribes to the new one, and its
|
|
41
|
+
* first state also deletes the roots this subscription delivered that the new tree doesn't hold. A
|
|
42
|
+
* subscriber that applies every delivery, a store or a tree, then holds the new tree's state, and
|
|
43
|
+
* a wildcard read of it reads nothing the old tree left behind.
|
|
44
|
+
*
|
|
45
|
+
* - The first state after a reconnect is one delivery: the new tree's roots, then a `delete` record
|
|
46
|
+
* (in the `"diff"` form) or a `deleted` key (in the `"snapshot"` form) for each root gone, so
|
|
47
|
+
* nothing applied in between holds both. With no roots in the new tree, it's the deletes alone.
|
|
48
|
+
* - A root is delivered while it was written by the last delivery that touched it, and gone once a
|
|
49
|
+
* delivery deletes it, as a `HostPublisher`'s deliveries say.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const engine = new ChannelSubscription({ roots: ["run", "$projection_*"] }, (post, first) => {
|
|
53
|
+
* tree.ApplyDiff(post.diff, "/engine/");
|
|
54
|
+
* tree.RunProjections();
|
|
55
|
+
* tree.Commit();
|
|
56
|
+
* });
|
|
57
|
+
* OnConnect("engine", port => engine.Connect(port));
|
|
58
|
+
*/
|
|
59
|
+
export declare class ChannelSubscription {
|
|
60
|
+
private options;
|
|
61
|
+
private callback;
|
|
62
|
+
/** The source's root keys delivered and not deleted */
|
|
63
|
+
private delivered;
|
|
64
|
+
private client?;
|
|
65
|
+
private ownClient;
|
|
66
|
+
private sub?;
|
|
67
|
+
/**
|
|
68
|
+
* @param options The roots, and the form, as `ObservableDiffTree.Subscribe` takes them.
|
|
69
|
+
* @param callback Each delivery, and whether it's the first state of a `Connect`.
|
|
70
|
+
*/
|
|
71
|
+
constructor(options: ISubscribeOptions, callback: (delivery: any, first: boolean) => void);
|
|
72
|
+
/**
|
|
73
|
+
* Subscribes over a channel, or a client of one, in place of the last: the first state arrives
|
|
74
|
+
* as one delivery, with the deletes for the roots the last tree had and this one hasn't.
|
|
75
|
+
* @param onSubscribed Called once the first state is out, with the error when subscribing failed.
|
|
76
|
+
* @returns What ends this connection's subscription.
|
|
77
|
+
*/
|
|
78
|
+
Connect(channel: IChannel | ChannelClient, onSubscribed?: (error?: unknown) => void): () => void;
|
|
79
|
+
/** Ends the subscription on the current channel; what it delivered is kept for the next `Connect` */
|
|
80
|
+
Stop(): void;
|
|
81
|
+
private Track;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Subscribes to the tree a `HostPublisher` serves on a channel, such as a port to another worker.
|
|
85
|
+
* The deliveries are the same as `ObservableDiffTree.Subscribe`'s, but they arrive asynchronously:
|
|
86
|
+
* the first state as the host's first post, then each commit. Values are structured-cloned copies.
|
|
87
|
+
* @returns A function that ends the subscription.
|
|
88
|
+
*/
|
|
89
|
+
export declare function Subscribe(channel: IChannel, options: ISubscribeOptions & {
|
|
90
|
+
form: "snapshot";
|
|
91
|
+
}, callback: (update: ISnapshotUpdate) => void): () => void;
|
|
92
|
+
export declare function Subscribe(channel: IChannel, options: ISubscribeOptions & {
|
|
93
|
+
form?: "diff";
|
|
94
|
+
}, callback: (post: IDiffPost) => void): () => void;
|
|
95
|
+
/**
|
|
96
|
+
* Waits on a channel for the first delivery in which every key has a value: at once when they
|
|
97
|
+
* all have one, otherwise the commit that adds the last. See `ObservableDiffTree.Once`.
|
|
98
|
+
* @param keys Exact root keys; no patterns.
|
|
99
|
+
* @param options `signal` cancels the wait, rejecting with its reason.
|
|
100
|
+
* @returns Each key's value.
|
|
101
|
+
*/
|
|
102
|
+
export declare function Once(channel: IChannel, keys: readonly string[], options?: {
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
}>;
|