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.
Files changed (57) hide show
  1. package/Node/vNode.js +2 -0
  2. package/SYNTAX_PRIMER.md +181 -101
  3. package/Store/Diff/channelClient.d.ts +106 -0
  4. package/Store/Diff/channelClient.js +228 -0
  5. package/Store/Diff/defaultDiffTreeWorker.js +3 -4
  6. package/Store/Diff/diffTree.d.ts +56 -0
  7. package/Store/Diff/diffTree.js +110 -4
  8. package/Store/Diff/diffTree.types.d.ts +191 -21
  9. package/Store/Diff/diffTree.types.js +2 -10
  10. package/Store/Diff/diffTreeEndpoint.d.ts +101 -0
  11. package/Store/Diff/diffTreeEndpoint.js +173 -0
  12. package/Store/Diff/diffTreeEndpoint.types.d.ts +71 -0
  13. package/Store/Diff/diffTreeEndpoint.types.js +1 -0
  14. package/Store/Diff/diffTreeHub.d.ts +139 -0
  15. package/Store/Diff/diffTreeHub.js +346 -0
  16. package/Store/Diff/hostPublisher.d.ts +39 -0
  17. package/Store/Diff/hostPublisher.js +151 -0
  18. package/Store/Diff/index.d.ts +3 -0
  19. package/Store/Diff/index.js +3 -0
  20. package/Store/Diff/observableDiffTree.d.ts +93 -0
  21. package/Store/Diff/observableDiffTree.js +199 -0
  22. package/Store/Diff/onConnect.d.ts +33 -0
  23. package/Store/Diff/onConnect.js +83 -0
  24. package/Store/Diff/utils/diffTree.types.d.ts +9 -0
  25. package/Store/Diff/utils/homes.d.ts +12 -0
  26. package/Store/Diff/utils/homes.js +40 -0
  27. package/Store/Diff/utils/routes.d.ts +18 -0
  28. package/Store/Diff/utils/routes.js +33 -2
  29. package/Store/Store/store.d.ts +36 -4
  30. package/Store/Store/store.js +47 -6
  31. package/Store/Store/storeAsync.d.ts +80 -25
  32. package/Store/Store/storeAsync.js +102 -33
  33. package/Store/Store/storeSync.d.ts +76 -11
  34. package/Store/Store/storeSync.js +105 -12
  35. package/Store/Tree/observableNode.js +68 -13
  36. package/Store/Tree/observableScope.d.ts +1 -0
  37. package/Store/Tree/observableScope.js +4 -0
  38. package/Store/Tree/opaque.d.ts +30 -0
  39. package/Store/Tree/opaque.js +35 -0
  40. package/Store/Tree/snapshot.js +5 -4
  41. package/Store/index.d.ts +4 -1
  42. package/Store/index.js +1 -1
  43. package/Utils/decorators.d.ts +0 -98
  44. package/Utils/decorators.js +0 -156
  45. package/Utils/index.d.ts +2 -0
  46. package/Utils/index.js +1 -0
  47. package/Utils/subscriptionManager.d.ts +78 -0
  48. package/Utils/subscriptionManager.js +162 -0
  49. package/package.json +1 -1
  50. package/Store/Diff/diffTreeWorker.d.ts +0 -6
  51. package/Store/Diff/diffTreeWorker.js +0 -22
  52. package/Store/Diff/diffTreeWriter.d.ts +0 -92
  53. package/Store/Diff/diffTreeWriter.js +0 -137
  54. package/Store/Diff/workerQueue.d.ts +0 -7
  55. package/Store/Diff/workerQueue.js +0 -29
  56. package/Store/Store/pushStore.d.ts +0 -53
  57. 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`, `ComputedAsync`, `Scope`, `Watch`, `Inject`, `Destroy`, `Bound`, `Animation`, `AnimationType`, `Injector`; type `IDestroyable` |
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. |
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/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
+ **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 `PushStore` when a worker produces the data itself, such as the result of a computation it runs.
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` | `PushStore` |
539
- | --- | --- | --- | --- |
540
- | Constructor | `new StoreSync(keyFunc?, projections?)` | `new StoreAsync(worker, keyFunc?)` | `new PushStore(source, keyFunc?)` |
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 |
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). `PushStore` has only `Get` and `Has`, plus `Error` (see PushStore and DiffTreeWriter).
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 running `Worker` whose entry file connects a `DiffTree`. Any `keyFunc` or projections used for diffing must live in that file; the constructor's `keyFunc` only resolves ids on the main thread.
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 { DiffTree } from "j-templates/Store/Diff/diffTree";
583
- import { ConnectWorkerToDiffTree } from "j-templates/Store/Diff/diffTreeWorker";
584
- ConnectWorkerToDiffTree(new DiffTree((v: any) => v?.id), self as any as Worker);
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"); // only now defined
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
- **Batching (8.0.27).** Every `Write`, `Patch`, `Push` and `Splice` made in the same tick goes to the worker as one message and is applied in call order. The worker runs projections once per batch, after the last change, not after each one. Verified: a `Write`, a `Push` and a `Patch` in one tick sent one message and ran a projection once.
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 change made before the call. Changes made after it go in a later batch.
601
- - **A failed change stops the store.** After any change fails in the worker (for example `Splice` on a value that isn't an array), every later `Write`, `Patch`, `Push`, `Splice` and `Flush` rejects with that same error (verified). Validate before writing; a store in this state has to be replaced.
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>`. Pass them as `StoreSync`'s second argument (for `StoreAsync`, to the worker's `DiffTree`).
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
- const store = new StoreSync((v: any) => v?.id, {
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 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).
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
- ### PushStore and DiffTreeWriter
660
+ ### A worker's own commits
627
661
 
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).
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
- 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
- };
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 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
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
- - 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
- - The store deep-clones every value it applies, so even a same-thread `EventTarget` that delivers the writer's own objects never leaves the store sharing them with the writer's tree (from source).
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 `@ComputedAsync` or `StoreAsync`, stub `Worker` with an in-process shim:
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 { DiffTree } from "j-templates/Store/Diff/diffTree";
758
- import { ConnectWorkerToDiffTree } from "j-templates/Store/Diff/diffTreeWorker";
759
-
760
- function fakeWorker(tree = new DiffTree()) {
761
- const inner: any = { onmessage: null,
762
- postMessage: (d: any) => queueMicrotask(() => outer.onmessage?.({ data: structuredClone(d) })) };
763
- const outer: any = { onmessage: null, onerror: null, terminate() {},
764
- postMessage: (d: any) => queueMicrotask(() => inner.onmessage?.({ data: structuredClone(d) })) };
765
- ConnectWorkerToDiffTree(tree, inner);
766
- return outer;
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
- (globalThis as any).Worker = class { constructor() { return fakeWorker(); } }; // for @ComputedAsync
769
- const store = new StoreAsync(fakeWorker(new DiffTree((v: any) => v?.id)), (v: any) => v?.id);
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
- writer.DiffPath("a", { value: 1 });
784
- writer.Commit();
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 one failed change makes every later change reject.
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 `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.
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 rejects with the same old error | an earlier change failed in the worker | fix that change; replace the store |
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
- | `PushStore` never gets the writer's data | the worker wrote but didn't call `Commit()` | call `Commit()` after each batch |
1009
- | `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 |
1010
- | 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 |
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
+ }>;