j-templates 9.0.1 → 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 (54) hide show
  1. package/SYNTAX_PRIMER.md +181 -101
  2. package/Store/Diff/channelClient.d.ts +106 -0
  3. package/Store/Diff/channelClient.js +228 -0
  4. package/Store/Diff/defaultDiffTreeWorker.js +3 -4
  5. package/Store/Diff/diffTree.d.ts +56 -0
  6. package/Store/Diff/diffTree.js +110 -4
  7. package/Store/Diff/diffTree.types.d.ts +191 -21
  8. package/Store/Diff/diffTree.types.js +2 -10
  9. package/Store/Diff/diffTreeEndpoint.d.ts +101 -0
  10. package/Store/Diff/diffTreeEndpoint.js +173 -0
  11. package/Store/Diff/diffTreeEndpoint.types.d.ts +71 -0
  12. package/Store/Diff/diffTreeEndpoint.types.js +1 -0
  13. package/Store/Diff/diffTreeHub.d.ts +139 -0
  14. package/Store/Diff/diffTreeHub.js +346 -0
  15. package/Store/Diff/hostPublisher.d.ts +39 -0
  16. package/Store/Diff/hostPublisher.js +151 -0
  17. package/Store/Diff/index.d.ts +3 -0
  18. package/Store/Diff/index.js +3 -0
  19. package/Store/Diff/observableDiffTree.d.ts +93 -0
  20. package/Store/Diff/observableDiffTree.js +199 -0
  21. package/Store/Diff/onConnect.d.ts +33 -0
  22. package/Store/Diff/onConnect.js +83 -0
  23. package/Store/Diff/utils/diffTree.types.d.ts +9 -0
  24. package/Store/Diff/utils/homes.d.ts +12 -0
  25. package/Store/Diff/utils/homes.js +40 -0
  26. package/Store/Diff/utils/routes.d.ts +18 -0
  27. package/Store/Diff/utils/routes.js +33 -2
  28. package/Store/Store/store.d.ts +36 -4
  29. package/Store/Store/store.js +47 -6
  30. package/Store/Store/storeAsync.d.ts +80 -25
  31. package/Store/Store/storeAsync.js +102 -33
  32. package/Store/Store/storeSync.d.ts +76 -11
  33. package/Store/Store/storeSync.js +105 -12
  34. package/Store/Tree/observableNode.js +68 -13
  35. package/Store/Tree/opaque.d.ts +30 -0
  36. package/Store/Tree/opaque.js +35 -0
  37. package/Store/Tree/snapshot.js +5 -4
  38. package/Store/index.d.ts +4 -1
  39. package/Store/index.js +1 -1
  40. package/Utils/decorators.d.ts +0 -98
  41. package/Utils/decorators.js +0 -156
  42. package/Utils/index.d.ts +2 -0
  43. package/Utils/index.js +1 -0
  44. package/Utils/subscriptionManager.d.ts +78 -0
  45. package/Utils/subscriptionManager.js +162 -0
  46. package/package.json +1 -1
  47. package/Store/Diff/diffTreeWorker.d.ts +0 -6
  48. package/Store/Diff/diffTreeWorker.js +0 -22
  49. package/Store/Diff/diffTreeWriter.d.ts +0 -92
  50. package/Store/Diff/diffTreeWriter.js +0 -137
  51. package/Store/Diff/workerQueue.d.ts +0 -7
  52. package/Store/Diff/workerQueue.js +0 -29
  53. package/Store/Store/pushStore.d.ts +0 -53
  54. package/Store/Store/pushStore.js +0 -77
@@ -1,27 +1,29 @@
1
1
  import type { JsonDiffResult } from "../../Utils/json";
2
+ import type { ObservableDiffTree } from "./observableDiffTree";
3
+ import type { IDiffTreeEndpoint } from "./diffTreeEndpoint.types";
4
+ import type { IKeyFunc, IRouteDefinitions } from "./utils/diffTree.types";
2
5
  /**
3
- * `type` of the message a `DiffTreeWriter` posts on `Commit()`: the diff since the last commit,
4
- * which a `PushStore` applies.
6
+ * An `ObservableDiffTree`'s configuration, in one place.
7
+ *
8
+ * Config shared with the stores opened on the tree, `opaque` say, belongs in a file both sides
9
+ * import, spread into this and into each store's options: the tree lives in its worker, and a
10
+ * store on the main thread sees only what it imports.
5
11
  */
6
- export declare const DIFF_TREE_EVENT = "DIFF_TREE_EVENT";
7
- /**
8
- * `type` of the message a `DiffTreeWriter` posts on `Commit()` when a change in the batch
9
- * failed, after the `DIFF_TREE_EVENT` carrying the changes that were applied.
10
- */
11
- export declare const DIFF_TREE_ERROR = "DIFF_TREE_ERROR";
12
- /**
13
- * A message a `DiffTreeWriter` posts to its `PushStore`. Both arrive as ordinary
14
- * `"message"` events, so the channel can carry other traffic too; listeners tell them apart
15
- * by `type`.
16
- */
17
- export type IDiffPushEvent = {
18
- type: typeof DIFF_TREE_EVENT;
19
- data: JsonDiffResult;
20
- } | {
21
- type: typeof DIFF_TREE_ERROR;
22
- data: any;
23
- };
24
- /** A `DiffTree` method call sent to a worker connected with `ConnectWorkerToDiffTree` */
12
+ export interface IObservableDiffTreeConfig {
13
+ /**
14
+ * The endpoint the tree is hosted as (`DiffTreeEndpoint.Host`). It gives the tree its keyFunc
15
+ * and a follow route per source (`FollowRoutes`). `Host` refuses a tree built from another
16
+ * endpoint, or from none when the endpoint follows sources.
17
+ */
18
+ endpoint?: IDiffTreeEndpoint;
19
+ /** How the tree's data is keyed, for a tree without an endpoint; with one, the endpoint's (giving both throws). */
20
+ keyFunc?: IKeyFunc;
21
+ /** The tree's own routes and projections. The endpoint's follow routes are added to them. */
22
+ routes?: IRouteDefinitions;
23
+ /** Root keys written whole (`DiffTree.MarkOpaque`). A store holding a key whole needs it here. */
24
+ opaque?: readonly string[];
25
+ }
26
+ /** An `ObservableDiffTree` method call in a `jt-batch` */
25
27
  export interface IDiffMethod {
26
28
  /** The method to call */
27
29
  method: "writepath" | "patchpath" | "pushpath" | "splicepath";
@@ -29,3 +31,171 @@ export interface IDiffMethod {
29
31
  arguments: Array<any>;
30
32
  }
31
33
  export type IDiffMethodBatch = IDiffMethod[];
34
+ /**
35
+ * How an `ObservableDiffTree` delivers a commit to a subscription:
36
+ * - `"diff"`: the commit's diff records for the matching roots (`IDiffPost`), which a store applies.
37
+ * - `"snapshot"`: each matching root that changed, whole, as last committed (`ISnapshotUpdate`), which
38
+ * another tree writes.
39
+ */
40
+ export type DeliveryForm = "diff" | "snapshot";
41
+ /** A subscription to an `ObservableDiffTree` */
42
+ export interface ISubscribeOptions {
43
+ /**
44
+ * The root keys it receives: exact keys, and patterns ending in `*` (`task_*`). Omitted, every
45
+ * root. With a `keyFunc`, include the entity roots the received roots refer to.
46
+ */
47
+ roots?: readonly string[];
48
+ /** How it's delivered; `"diff"` when omitted */
49
+ form?: DeliveryForm;
50
+ /** A group `ObservableDiffTree.Disconnect` ends it with, e.g. the channel it came from */
51
+ group?: unknown;
52
+ }
53
+ /** A `"diff"` delivery */
54
+ export interface IDiffPost {
55
+ /** The commit it comes from; on the delivery made on subscribing, the last commit's */
56
+ seq: number;
57
+ /** The commit's records for the matching roots; on subscribing, a `write` per matching committed root */
58
+ diff: JsonDiffResult;
59
+ }
60
+ /** A `"snapshot"` delivery */
61
+ export interface ISnapshotUpdate {
62
+ /** The commit it comes from; on the delivery made on subscribing, the last commit's */
63
+ seq: number;
64
+ /** Each matching root that changed, as committed, or on subscribing every matching committed root: shared with the tree, so read-only */
65
+ roots: {
66
+ [key: string]: unknown;
67
+ };
68
+ /** Matching roots deleted by the commit */
69
+ deleted: string[];
70
+ }
71
+ /**
72
+ * A channel between threads: a `Worker`, a `MessagePort`, or a worker's own `self`. Messages are
73
+ * read with `addEventListener`, never `onmessage`, so other messages can share the channel.
74
+ */
75
+ export interface IChannel {
76
+ postMessage(message: any, transfer?: Transferable[]): void;
77
+ addEventListener(type: "message", listener: (event: any) => void): void;
78
+ removeEventListener(type: "message", listener: (event: any) => void): void;
79
+ /** A `MessagePort` delivers nothing to `addEventListener` until it's started */
80
+ start?(): void;
81
+ /** A `MessagePort` is closed when it's no longer served */
82
+ close?(): void;
83
+ }
84
+ /** A `jt-` message a client (a `StoreAsync`, `Subscribe`, `Once`) sends to a `HostPublisher` */
85
+ export type IHostRequest =
86
+ /** Runs the calls, then projections, commits, and answers with a `jt-ack` */
87
+ {
88
+ type: "jt-batch";
89
+ id: number;
90
+ calls: IDiffMethodBatch;
91
+ }
92
+ /** Subscribes; the first state is posted, then `jt-subscribed` */
93
+ | {
94
+ type: "jt-subscribe";
95
+ sub: number;
96
+ roots?: readonly string[];
97
+ form: DeliveryForm;
98
+ } | {
99
+ type: "jt-unsubscribe";
100
+ sub: number;
101
+ }
102
+ /** Ends every subscription on the channel */
103
+ | {
104
+ type: "jt-disconnect";
105
+ }
106
+ /** See `DiffTree.MarkOpaque` */
107
+ | {
108
+ type: "jt-markopaque";
109
+ keys: readonly string[];
110
+ }
111
+ /** From a `DiffTreeHub`, on the worker's own channel: serve `port`, a lease's channel, as `id` */
112
+ | {
113
+ type: "jt-serve";
114
+ id: number;
115
+ port: MessagePort;
116
+ }
117
+ /** From a `DiffTreeHub`: stop serving lease `id`, ending its subscriptions */
118
+ | {
119
+ type: "jt-unserve";
120
+ id: number;
121
+ };
122
+ /**
123
+ * From a `DiffTreeHub` to a tree that follows `source`, on its own channel: a port to `source`'s tree,
124
+ * for its `OnConnect` handler. Sent when the follower starts, and again when either restarts.
125
+ */
126
+ export interface IConnectMessage {
127
+ type: "jt-connect";
128
+ source: string;
129
+ port: MessagePort;
130
+ }
131
+ /**
132
+ * From a `DiffTreeHub` to a tree it started, on the tree's own channel, first on every start: the
133
+ * sources it follows (its endpoint's `sources`). `OnConnect` throws for a source that isn't listed.
134
+ */
135
+ export interface ISourcesMessage {
136
+ type: "jt-sources";
137
+ sources: readonly string[];
138
+ }
139
+ /**
140
+ * A lease on a worker's tree, from `DiffTreeHub.Open`: a port to it, served by the worker's
141
+ * `HostPublisher`. The worker runs while it has leases. Give it to a `StoreAsync`, which owns it from
142
+ * then on and releases it when it's destroyed.
143
+ */
144
+ export interface IWorkerLease {
145
+ /** The endpoint's name */
146
+ readonly name: string;
147
+ /** The endpoint the tree is registered as */
148
+ readonly endpoint: IDiffTreeEndpoint;
149
+ /** The current port to the worker; replaced when the worker restarts */
150
+ readonly port: MessagePort;
151
+ readonly released: boolean;
152
+ /**
153
+ * Calls back with the new port each time the worker restarts.
154
+ * @returns A function that removes the callback.
155
+ */
156
+ OnPort(callback: (port: MessagePort) => void): () => void;
157
+ /** Ends the lease: its subscriptions end and its port closes. The last one releases the worker. */
158
+ Release(): void;
159
+ }
160
+ /**
161
+ * A lease on a tree on this thread, from `DiffTreeHub.OpenLocal`: the tree itself. The tree runs while
162
+ * it has leases. Give it to a `StoreSync`, which owns it from then on and releases it when it's
163
+ * destroyed.
164
+ */
165
+ export interface ILocalLease {
166
+ /** The endpoint's name */
167
+ readonly name: string;
168
+ /** The endpoint the tree is registered as */
169
+ readonly endpoint: IDiffTreeEndpoint;
170
+ /** The current tree; replaced when it restarts */
171
+ readonly tree: ObservableDiffTree;
172
+ readonly released: boolean;
173
+ /**
174
+ * Calls back with the new tree each time it restarts.
175
+ * @returns A function that removes the callback.
176
+ */
177
+ OnTree(callback: (tree: ObservableDiffTree) => void): () => void;
178
+ /** Ends the lease. The last one ends the tree, and the leases it holds on its sources. */
179
+ Release(): void;
180
+ }
181
+ /** A `jt-` message a `HostPublisher` posts to its clients */
182
+ export type IHostMessage =
183
+ /** A delivery for subscription `sub` */
184
+ ({
185
+ type: "jt-post";
186
+ sub: number;
187
+ } & (IDiffPost | ISnapshotUpdate))
188
+ /** Batch `id` was applied and committed, after the commit's posts; `error` when it failed */
189
+ | {
190
+ type: "jt-ack";
191
+ id: number;
192
+ error?: unknown;
193
+ }
194
+ /** Subscription `sub`'s first state has been posted (none when no root matched); `error` when subscribing failed */
195
+ | {
196
+ type: "jt-subscribed";
197
+ sub: number;
198
+ error?: unknown;
199
+ };
200
+ /** The prefix of every message the framework sends on a channel */
201
+ export declare const MESSAGE_PREFIX = "jt-";
@@ -1,10 +1,2 @@
1
- /**
2
- * `type` of the message a `DiffTreeWriter` posts on `Commit()`: the diff since the last commit,
3
- * which a `PushStore` applies.
4
- */
5
- export const DIFF_TREE_EVENT = "DIFF_TREE_EVENT";
6
- /**
7
- * `type` of the message a `DiffTreeWriter` posts on `Commit()` when a change in the batch
8
- * failed, after the `DIFF_TREE_EVENT` carrying the changes that were applied.
9
- */
10
- export const DIFF_TREE_ERROR = "DIFF_TREE_ERROR";
1
+ /** The prefix of every message the framework sends on a channel */
2
+ export const MESSAGE_PREFIX = "jt-";
@@ -0,0 +1,101 @@
1
+ import type { IChannel } from "./diffTree.types";
2
+ import type { ObservableDiffTree } from "./observableDiffTree";
3
+ import type { DiffTreeEndpointRemoteFunc, IDiffTreeEndpoint, IDiffTreeEndpointHostOptions, IDiffTreeEndpointOptions } from "./diffTreeEndpoint.types";
4
+ /**
5
+ * DiffTree endpoints: an endpoint is a key in a `DiffTreeHub`, and one tree in the app's dependency graph.
6
+ * `Define` describes it once, as plain data, and everything attached to it is derived from that: the
7
+ * hub's registration (`DiffTreeHub.Register`), the tree built from it (its keyFunc and follow routes,
8
+ * `IObservableDiffTreeConfig.endpoint`), its hosting (`Host`), and the stores opened on it
9
+ * (`StoreAsync`, `StoreSync`).
10
+ *
11
+ * @example
12
+ * // ledger-endpoint.ts and totals-endpoint.ts: imported by the main thread and the workers
13
+ * export const ledgerEndpoint = DiffTreeEndpoint.Define("ledger");
14
+ * export const totalsEndpoint = DiffTreeEndpoint.Define("totals", remote => ({
15
+ * ledger: remote(ledgerEndpoint, ["entries"]) // { entries: "/ledger/entries" }
16
+ * }));
17
+ *
18
+ * // totals-tree.ts
19
+ * export function newTotalsTree() {
20
+ * return new ObservableDiffTree({
21
+ * endpoint: totalsEndpoint,
22
+ * routes: { "/": { projections: { totals: { reads: [totalsEndpoint.config.ledger.entries], projection: categoryTotals } } } }
23
+ * });
24
+ * }
25
+ *
26
+ * // totals-worker.ts
27
+ * DiffTreeEndpoint.Host(totalsEndpoint, newTotalsTree(), self);
28
+ */
29
+ export declare namespace DiffTreeEndpoint {
30
+ /**
31
+ * Defines an endpoint. `remote(source, roots)`, given to `build`, records a connection when it's
32
+ * called and returns each root's path in the following tree. It takes the source's definition,
33
+ * so an endpoint can only follow one defined before it, and the source's `keyFunc` comes with it.
34
+ * Roots are the source's tree keys as they are: `"$projection_summary"` follows a projection
35
+ * output, and `"o_*"` follows keyed entities.
36
+ *
37
+ * @example
38
+ * const totalsEndpoint = DiffTreeEndpoint.Define("totals", remote => ({
39
+ * ledger: remote(ledgerEndpoint, ["entries"]) // { entries: "/ledger/entries" }
40
+ * }));
41
+ * hub.Register(totalsEndpoint, () => new Worker(new URL("./totals-worker.ts", import.meta.url), { type: "module" }));
42
+ * @param name The endpoint's name: its key in the `DiffTreeHub`, and the segment of its follow route
43
+ * in a tree that follows it.
44
+ * @param build Returns the endpoint's config, the paths `remote` gave included.
45
+ * @param options `keyFunc`: how the data published here is keyed.
46
+ * @returns The endpoint's definition: plain data.
47
+ * @throws When `name` isn't one route segment (or is a parameter, `{id}`), or the endpoint follows itself.
48
+ */
49
+ function Define<C extends object = {}>(name: string, build?: (remote: DiffTreeEndpointRemoteFunc) => C, options?: IDiffTreeEndpointOptions): IDiffTreeEndpoint<C>;
50
+ /**
51
+ * Where a source's roots are held in a tree that follows it: `/<source>/`. A tree built from the
52
+ * endpoint (`IObservableDiffTreeConfig.endpoint`) has a follow route at `/<source>` for each source.
53
+ * @param source The source endpoint's name.
54
+ */
55
+ function FollowRoute(source: string): string;
56
+ /**
57
+ * Hosts an endpoint's tree on a channel: serves it to stores and followers (`HostPublisher`), and
58
+ * follows the endpoint's sources into it (`Connect`). Build the tree from the endpoint
59
+ * (`IObservableDiffTreeConfig.endpoint`), which gives it its keyFunc and follow routes. A
60
+ * worker entry calls it on its own channel; a `DiffTreeHub` calls it for a tree on the main thread,
61
+ * on a channel it creates. So every tree is hosted the same way, and a tree file only builds the
62
+ * tree.
63
+ *
64
+ * @example
65
+ * // totals-worker.ts
66
+ * DiffTreeEndpoint.Host(totalsEndpoint, newTotalsTree(), self);
67
+ *
68
+ * // a tree that acts on what it follows at the end of the turn, and commits then
69
+ * DiffTreeEndpoint.Host(traceEndpoint, tree.tree, self, { onFollow: () => tree.received() });
70
+ * @param endpoint The endpoint the tree is registered as.
71
+ * @param tree The tree, built from `endpoint`. One built from none (a `StoreSync`'s own, say) is
72
+ * taken for an endpoint with no sources.
73
+ * @param scope The channel the hub posts to: the worker's own; `globalThis` when omitted.
74
+ * @param options `onFollow`: what runs after each source's post is applied. `onConnect`: what
75
+ * runs with each new port to a source.
76
+ * @returns A function that stops following the sources and serving the tree.
77
+ * @throws When the tree was built from another endpoint, or from none while `endpoint` follows
78
+ * sources (it has no follow routes).
79
+ */
80
+ function Host(endpoint: IDiffTreeEndpoint, tree: ObservableDiffTree, scope?: IChannel, options?: IDiffTreeEndpointHostOptions): () => void;
81
+ /**
82
+ * The following part of `Host`: follows each of an endpoint's sources into `tree`, over the ports
83
+ * the `DiffTreeHub` posts to `scope` (`OnConnect`). A `ChannelSubscription` per source applies each
84
+ * post at the source's follow route, then `onFollow` runs. When a source restarts, the hub gives a
85
+ * new port, and the subscription moves to it: the roots the new source doesn't hold are deleted.
86
+ * Call it alone only for a tree served some other way.
87
+ *
88
+ * @example
89
+ * const tree = new ObservableDiffTree({ endpoint, routes: { "/": { projections } } });
90
+ * HostPublisher(tree, self);
91
+ * DiffTreeEndpoint.Connect(endpoint, tree, self);
92
+ * @param endpoint The endpoint whose sources are followed.
93
+ * @param tree The tree, with the endpoint's follow routes (built from it).
94
+ * @param scope The channel the hub posts to: the worker's own; `globalThis` when omitted.
95
+ * @param options `onFollow`: what runs after each post is applied; by default the projections run
96
+ * and the tree commits. `onConnect`: what runs with each new port to a source, after the
97
+ * follow has moved to it.
98
+ * @returns A function that stops following every source.
99
+ */
100
+ function Connect(endpoint: IDiffTreeEndpoint, tree: ObservableDiffTree, scope?: IChannel, options?: IDiffTreeEndpointHostOptions): () => void;
101
+ }
@@ -0,0 +1,173 @@
1
+ import { ChannelSubscription } from "./channelClient";
2
+ import { HostPublisher } from "./hostPublisher";
3
+ import { OnConnect } from "./onConnect";
4
+ // An endpoint on a hub, defined once, and the function that hosts its tree: `Host` serves it and
5
+ // connects it to the endpoint's sources, the same for a worker's tree (the worker calls it) and one on the main thread (`DiffTreeHub` calls it). Imports only
6
+ // the Diff modules, never the Store barrel (which reads `window`), so every side imports the same
7
+ // definition: the main thread to register it and open stores on it, the tree that publishes it, and
8
+ // the endpoints that follow it.
9
+ /**
10
+ * DiffTree endpoints: an endpoint is a key in a `DiffTreeHub`, and one tree in the app's dependency graph.
11
+ * `Define` describes it once, as plain data, and everything attached to it is derived from that: the
12
+ * hub's registration (`DiffTreeHub.Register`), the tree built from it (its keyFunc and follow routes,
13
+ * `IObservableDiffTreeConfig.endpoint`), its hosting (`Host`), and the stores opened on it
14
+ * (`StoreAsync`, `StoreSync`).
15
+ *
16
+ * @example
17
+ * // ledger-endpoint.ts and totals-endpoint.ts: imported by the main thread and the workers
18
+ * export const ledgerEndpoint = DiffTreeEndpoint.Define("ledger");
19
+ * export const totalsEndpoint = DiffTreeEndpoint.Define("totals", remote => ({
20
+ * ledger: remote(ledgerEndpoint, ["entries"]) // { entries: "/ledger/entries" }
21
+ * }));
22
+ *
23
+ * // totals-tree.ts
24
+ * export function newTotalsTree() {
25
+ * return new ObservableDiffTree({
26
+ * endpoint: totalsEndpoint,
27
+ * routes: { "/": { projections: { totals: { reads: [totalsEndpoint.config.ledger.entries], projection: categoryTotals } } } }
28
+ * });
29
+ * }
30
+ *
31
+ * // totals-worker.ts
32
+ * DiffTreeEndpoint.Host(totalsEndpoint, newTotalsTree(), self);
33
+ */
34
+ export var DiffTreeEndpoint;
35
+ (function (DiffTreeEndpoint) {
36
+ /**
37
+ * Defines an endpoint. `remote(source, roots)`, given to `build`, records a connection when it's
38
+ * called and returns each root's path in the following tree. It takes the source's definition,
39
+ * so an endpoint can only follow one defined before it, and the source's `keyFunc` comes with it.
40
+ * Roots are the source's tree keys as they are: `"$projection_summary"` follows a projection
41
+ * output, and `"o_*"` follows keyed entities.
42
+ *
43
+ * @example
44
+ * const totalsEndpoint = DiffTreeEndpoint.Define("totals", remote => ({
45
+ * ledger: remote(ledgerEndpoint, ["entries"]) // { entries: "/ledger/entries" }
46
+ * }));
47
+ * hub.Register(totalsEndpoint, () => new Worker(new URL("./totals-worker.ts", import.meta.url), { type: "module" }));
48
+ * @param name The endpoint's name: its key in the `DiffTreeHub`, and the segment of its follow route
49
+ * in a tree that follows it.
50
+ * @param build Returns the endpoint's config, the paths `remote` gave included.
51
+ * @param options `keyFunc`: how the data published here is keyed.
52
+ * @returns The endpoint's definition: plain data.
53
+ * @throws When `name` isn't one route segment (or is a parameter, `{id}`), or the endpoint follows itself.
54
+ */
55
+ function Define(name, build = () => ({}), options = {}) {
56
+ if (name === "" || name.includes("/") || (name.startsWith("{") && name.endsWith("}")))
57
+ throw new Error(`DiffTreeEndpoint: invalid name ${name}, one route segment, not a parameter.`);
58
+ const remotes = new Map();
59
+ const remote = (source, roots) => {
60
+ if (source.name === name)
61
+ throw new Error(`DiffTreeEndpoint: ${name} can't follow itself.`);
62
+ let spec = remotes.get(source.name);
63
+ if (!spec) {
64
+ spec = { source: source.name, keyFunc: source.keyFunc, roots: [] };
65
+ remotes.set(source.name, spec);
66
+ }
67
+ for (let x = 0; x < roots.length; x++)
68
+ spec.roots.includes(roots[x]) || spec.roots.push(roots[x]);
69
+ const route = FollowRoute(source.name);
70
+ return Object.fromEntries(roots.map(root => [root, route + root]));
71
+ };
72
+ const config = build(remote);
73
+ return {
74
+ name,
75
+ keyFunc: options.keyFunc,
76
+ config,
77
+ remotes: Array.from(remotes.values()),
78
+ sources: Array.from(remotes.keys())
79
+ };
80
+ }
81
+ DiffTreeEndpoint.Define = Define;
82
+ /**
83
+ * Where a source's roots are held in a tree that follows it: `/<source>/`. A tree built from the
84
+ * endpoint (`IObservableDiffTreeConfig.endpoint`) has a follow route at `/<source>` for each source.
85
+ * @param source The source endpoint's name.
86
+ */
87
+ function FollowRoute(source) {
88
+ return `/${source}/`;
89
+ }
90
+ DiffTreeEndpoint.FollowRoute = FollowRoute;
91
+ /**
92
+ * Hosts an endpoint's tree on a channel: serves it to stores and followers (`HostPublisher`), and
93
+ * follows the endpoint's sources into it (`Connect`). Build the tree from the endpoint
94
+ * (`IObservableDiffTreeConfig.endpoint`), which gives it its keyFunc and follow routes. A
95
+ * worker entry calls it on its own channel; a `DiffTreeHub` calls it for a tree on the main thread,
96
+ * on a channel it creates. So every tree is hosted the same way, and a tree file only builds the
97
+ * tree.
98
+ *
99
+ * @example
100
+ * // totals-worker.ts
101
+ * DiffTreeEndpoint.Host(totalsEndpoint, newTotalsTree(), self);
102
+ *
103
+ * // a tree that acts on what it follows at the end of the turn, and commits then
104
+ * DiffTreeEndpoint.Host(traceEndpoint, tree.tree, self, { onFollow: () => tree.received() });
105
+ * @param endpoint The endpoint the tree is registered as.
106
+ * @param tree The tree, built from `endpoint`. One built from none (a `StoreSync`'s own, say) is
107
+ * taken for an endpoint with no sources.
108
+ * @param scope The channel the hub posts to: the worker's own; `globalThis` when omitted.
109
+ * @param options `onFollow`: what runs after each source's post is applied. `onConnect`: what
110
+ * runs with each new port to a source.
111
+ * @returns A function that stops following the sources and serving the tree.
112
+ * @throws When the tree was built from another endpoint, or from none while `endpoint` follows
113
+ * sources (it has no follow routes).
114
+ */
115
+ function Host(endpoint, tree, scope = globalThis, options = {}) {
116
+ if (tree.Endpoint ? tree.Endpoint !== endpoint : endpoint.sources.length > 0)
117
+ throw new Error(`DiffTreeEndpoint: the tree hosted as ${endpoint.name} was built from ${tree.Endpoint ? tree.Endpoint.name : "no endpoint, so it has no follow routes"}; build it with that endpoint (IObservableDiffTreeConfig.endpoint).`);
118
+ const host = HostPublisher(tree, scope);
119
+ const stop = Connect(endpoint, tree, scope, options);
120
+ return () => {
121
+ stop();
122
+ host.Stop();
123
+ };
124
+ }
125
+ DiffTreeEndpoint.Host = Host;
126
+ /**
127
+ * The following part of `Host`: follows each of an endpoint's sources into `tree`, over the ports
128
+ * the `DiffTreeHub` posts to `scope` (`OnConnect`). A `ChannelSubscription` per source applies each
129
+ * post at the source's follow route, then `onFollow` runs. When a source restarts, the hub gives a
130
+ * new port, and the subscription moves to it: the roots the new source doesn't hold are deleted.
131
+ * Call it alone only for a tree served some other way.
132
+ *
133
+ * @example
134
+ * const tree = new ObservableDiffTree({ endpoint, routes: { "/": { projections } } });
135
+ * HostPublisher(tree, self);
136
+ * DiffTreeEndpoint.Connect(endpoint, tree, self);
137
+ * @param endpoint The endpoint whose sources are followed.
138
+ * @param tree The tree, with the endpoint's follow routes (built from it).
139
+ * @param scope The channel the hub posts to: the worker's own; `globalThis` when omitted.
140
+ * @param options `onFollow`: what runs after each post is applied; by default the projections run
141
+ * and the tree commits. `onConnect`: what runs with each new port to a source, after the
142
+ * follow has moved to it.
143
+ * @returns A function that stops following every source.
144
+ */
145
+ function Connect(endpoint, tree, scope = globalThis, options = {}) {
146
+ const onFollow = options.onFollow ?? Commit;
147
+ const stops = endpoint.remotes.map(remote => {
148
+ const follow = Follower(remote, tree, onFollow);
149
+ return OnConnect(remote.source, port => {
150
+ const stopFollow = follow.Connect(port);
151
+ const stopMore = options.onConnect?.(tree, remote.source, port);
152
+ return () => {
153
+ typeof stopMore === "function" && stopMore();
154
+ stopFollow();
155
+ };
156
+ }, scope);
157
+ });
158
+ return () => stops.forEach(stop => stop());
159
+ }
160
+ DiffTreeEndpoint.Connect = Connect;
161
+ /** Applies a source's posts at its follow route, over the ports `Connect` gives it */
162
+ function Follower(remote, tree, onFollow) {
163
+ const route = FollowRoute(remote.source);
164
+ return new ChannelSubscription({ roots: remote.roots }, (post) => {
165
+ tree.ApplyDiff(post.diff, route);
166
+ onFollow(tree, remote.source);
167
+ });
168
+ }
169
+ function Commit(tree) {
170
+ tree.RunProjections();
171
+ tree.Commit();
172
+ }
173
+ })(DiffTreeEndpoint || (DiffTreeEndpoint = {}));
@@ -0,0 +1,71 @@
1
+ import type { ObservableDiffTree } from "./observableDiffTree";
2
+ import type { IKeyFunc } from "./utils/diffTree.types";
3
+ /** A connection to another endpoint, recorded by the `remote` given to `DiffTreeEndpoint.Define` */
4
+ export interface IDiffTreeEndpointRemote {
5
+ /** The source's name: its name in the `DiffTreeHub`, and its follow route's segment */
6
+ source: string;
7
+ /** The source's keyFunc, for its follow route */
8
+ keyFunc?: IKeyFunc;
9
+ /** The source's root keys followed, as the source's tree holds them; patterns included */
10
+ roots: string[];
11
+ }
12
+ /**
13
+ * Follows `roots` of `source`. Recorded on the endpoint being defined when it's called.
14
+ * @returns Each root's path in the following tree, e.g. `{ entries: "/ledger/entries" }`.
15
+ */
16
+ export type DiffTreeEndpointRemoteFunc = <const K extends string>(source: IDiffTreeEndpoint<object>, roots: readonly K[]) => {
17
+ readonly [P in K]: string;
18
+ };
19
+ /** `DiffTreeEndpoint.Define` options */
20
+ export interface IDiffTreeEndpointOptions {
21
+ /**
22
+ * How the data published here is keyed: the tree built from the endpoint takes it, the stores
23
+ * opened on it, and the follow routes of the endpoints that follow this one.
24
+ */
25
+ keyFunc?: IKeyFunc;
26
+ }
27
+ /**
28
+ * An endpoint on a `DiffTreeHub`, defined: its name, how its data is keyed and the endpoints it
29
+ * follows. Plain data that drives the creation of what's attached to it: the hub's registration, the
30
+ * tree that publishes it (`IObservableDiffTreeConfig.endpoint`), the stores opened on it. What the
31
+ * tree and its stores otherwise share, `opaque` say, is theirs: a shared file each imports.
32
+ */
33
+ export interface IDiffTreeEndpoint<C extends object = object> {
34
+ readonly name: string;
35
+ readonly keyFunc?: IKeyFunc;
36
+ /** What `Define`'s callback returned: the paths `remote` gave, and anything else kept there */
37
+ readonly config: C;
38
+ /** One per source, in the order first followed */
39
+ readonly remotes: readonly IDiffTreeEndpointRemote[];
40
+ /** The sources' names, in the same order */
41
+ readonly sources: readonly string[];
42
+ }
43
+ /**
44
+ * Runs after a source's post is applied at its follow route in the following tree.
45
+ * @param tree The following tree.
46
+ * @param source The name of the source the post came from.
47
+ */
48
+ export type DiffTreeEndpointFollowFunc = (tree: ObservableDiffTree, source: string) => void;
49
+ /**
50
+ * Runs with each new port to a source, after the follow has moved to it: when the tree starts,
51
+ * and again when the source restarts.
52
+ * @param tree The following tree.
53
+ * @param source The source's name.
54
+ * @param port The port to the source, for reads besides the follow (a one-off `Once`, say).
55
+ * @returns Optionally, what ends what it started; it runs before the next port's call.
56
+ */
57
+ export type DiffTreeEndpointConnectFunc = (tree: ObservableDiffTree, source: string, port: MessagePort) => (() => void) | void;
58
+ /** `DiffTreeEndpoint.Host` and `DiffTreeEndpoint.Connect` options */
59
+ export interface IDiffTreeEndpointHostOptions {
60
+ /**
61
+ * Runs after each post from a source is applied. Default: run the projections and commit, so the
62
+ * tree's own subscribers get the change at once. A tree that acts later (at the end of the turn,
63
+ * say) does its own committing here instead.
64
+ */
65
+ onFollow?: DiffTreeEndpointFollowFunc;
66
+ /**
67
+ * Runs with each new port to a source, after the follow has moved to it. For a tree that reads
68
+ * from a source besides what it follows; the port stays the follow's, so don't close it.
69
+ */
70
+ onConnect?: DiffTreeEndpointConnectFunc;
71
+ }
@@ -0,0 +1 @@
1
+ export {};