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/Store/Diff/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export * from "./diffTree.types";
|
|
2
2
|
export * from "./utils/diffTree.types";
|
|
3
3
|
export { PROJECTION_PREFIX } from "./utils/paths";
|
|
4
|
+
export { FollowRoutes } from "./utils/routes";
|
|
5
|
+
export * from "./diffTreeEndpoint.types";
|
|
6
|
+
export { DiffTreeEndpoint } from "./diffTreeEndpoint";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { JsonDiffResult } from "../../Utils/json";
|
|
2
|
+
import { DiffTree } from "./diffTree";
|
|
3
|
+
import { IDiffPost, IObservableDiffTreeConfig, ISnapshotUpdate, ISubscribeOptions } from "./diffTree.types";
|
|
4
|
+
import type { IDiffTreeEndpoint } from "./diffTreeEndpoint.types";
|
|
5
|
+
import type { IKeyFunc } from "./utils/diffTree.types";
|
|
6
|
+
/**
|
|
7
|
+
* A `DiffTree` that delivers every `Commit()` to its subscribers: the tree, observable.
|
|
8
|
+
*
|
|
9
|
+
* - **`Commit()` diffs and delivers; it doesn't run projections.** Call `RunProjections()` first
|
|
10
|
+
* when the change needs them. A commit without it delivers the changed roots with the projection
|
|
11
|
+
* outputs as they were, which is how to send a small change (a flag) ahead of a long write.
|
|
12
|
+
* - **Each subscription names its roots and form** (`ISubscribeOptions`). The diff is computed
|
|
13
|
+
* once per commit and shaped for each: diff records, or whole changed roots. A commit with no
|
|
14
|
+
* changes delivers nothing.
|
|
15
|
+
* - **A new subscription gets the committed state first**, inside `Subscribe`, then every commit.
|
|
16
|
+
* Uncommitted writes arrive with the next commit, like everyone else's.
|
|
17
|
+
* - **Delivery is synchronous**, inside `Commit()`. A subscriber may write to another tree, but
|
|
18
|
+
* a change to this one, or a commit, during its delivery throws. Values are shared with the
|
|
19
|
+
* tree, never cloned: treat them as read-only.
|
|
20
|
+
* - When a subscriber throws, the others still get the commit, and `Commit()` throws the first error.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const tree = new ObservableDiffTree({ keyFunc, routes });
|
|
24
|
+
* const stop = tree.Subscribe({ roots: ["loading", "$projection_*"] }, post => store.Apply(post.diff));
|
|
25
|
+
* tree.WritePath("loading", true);
|
|
26
|
+
* tree.Commit();
|
|
27
|
+
*/
|
|
28
|
+
export declare class ObservableDiffTree extends DiffTree {
|
|
29
|
+
private subscriptions;
|
|
30
|
+
private seq;
|
|
31
|
+
/** How many deliveries are running: changes and commits throw while it isn't 0 */
|
|
32
|
+
private delivering;
|
|
33
|
+
/** The endpoint the tree was built for, which `DiffTreeEndpoint.Host` checks */
|
|
34
|
+
readonly Endpoint?: IDiffTreeEndpoint;
|
|
35
|
+
/** How the tree keys its data: its config's, or its endpoint's. A `StoreSync` given the tree takes it. */
|
|
36
|
+
readonly KeyFunc?: IKeyFunc;
|
|
37
|
+
/**
|
|
38
|
+
* @param config The tree's endpoint, keyFunc, routes and opaque keys (`IObservableDiffTreeConfig`).
|
|
39
|
+
* @throws When a keyFunc is given with an endpoint (the endpoint's is the tree's), or a route of
|
|
40
|
+
* the tree's own is one of the endpoint's follow routes.
|
|
41
|
+
*/
|
|
42
|
+
constructor(config?: IObservableDiffTreeConfig);
|
|
43
|
+
/** The number of the last commit that changed something; 0 before the first */
|
|
44
|
+
get Seq(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Diffs the tree since the last commit and delivers the changes to every subscription whose
|
|
47
|
+
* roots they touch.
|
|
48
|
+
* @returns The diff.
|
|
49
|
+
* @throws During a delivery from this tree, or the first error a subscriber threw.
|
|
50
|
+
*/
|
|
51
|
+
Commit(): JsonDiffResult;
|
|
52
|
+
/**
|
|
53
|
+
* Subscribes to the tree's commits. The committed state of the matching roots is delivered
|
|
54
|
+
* first, before this returns, in the subscription's form: a `write` record per root, or every
|
|
55
|
+
* root; nothing when no root matches. Every later commit that touches them follows. A
|
|
56
|
+
* subscriber that held roots from an earlier subscription clears them before subscribing again.
|
|
57
|
+
* @returns A function that ends the subscription.
|
|
58
|
+
* @throws The first delivery's error, after ending the subscription.
|
|
59
|
+
*/
|
|
60
|
+
Subscribe(options: ISubscribeOptions & {
|
|
61
|
+
form: "snapshot";
|
|
62
|
+
}, callback: (update: ISnapshotUpdate) => void): () => void;
|
|
63
|
+
Subscribe(options: ISubscribeOptions & {
|
|
64
|
+
form?: "diff";
|
|
65
|
+
}, callback: (post: IDiffPost) => void): () => void;
|
|
66
|
+
/**
|
|
67
|
+
* Waits for the first delivery in which every key has a committed value: at once when they all
|
|
68
|
+
* have one, otherwise the commit that adds the last. The values come from one commit.
|
|
69
|
+
* @param keys Exact root keys; no patterns.
|
|
70
|
+
* @param options `signal` cancels the wait, rejecting with its reason.
|
|
71
|
+
* @returns Each key's committed value.
|
|
72
|
+
*/
|
|
73
|
+
Once(keys: readonly string[], options?: {
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}>;
|
|
78
|
+
/** Ends every subscription made with this `group`; see `ISubscribeOptions.group` */
|
|
79
|
+
Disconnect(group: unknown): void;
|
|
80
|
+
/** How many subscriptions name a root key exactly now; see `SubscriptionManager.SubscriberCount` */
|
|
81
|
+
SubscriberCount(key: string): number;
|
|
82
|
+
/**
|
|
83
|
+
* Calls back when a root key matching a pattern loses its last exact subscription. It only
|
|
84
|
+
* reports: whether and when to delete the key is the caller's. It runs inside the unsubscribe,
|
|
85
|
+
* which may be inside a delivery, so a callback that deletes the key does it later (a timer).
|
|
86
|
+
* @param pattern An exact key or a pattern ending in `*`.
|
|
87
|
+
* @returns A function that removes the callback.
|
|
88
|
+
*/
|
|
89
|
+
OnReleased(pattern: string, callback: (key: string) => void): () => void;
|
|
90
|
+
private Deliver;
|
|
91
|
+
/** A subscriber can't change or commit the tree delivering to it */
|
|
92
|
+
protected BeforeChange(): void;
|
|
93
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { SubscriptionManager } from "../../Utils/subscriptionManager";
|
|
2
|
+
import { DiffTree } from "./diffTree";
|
|
3
|
+
import { FollowRoutes } from "./utils/routes";
|
|
4
|
+
/**
|
|
5
|
+
* A `DiffTree` that delivers every `Commit()` to its subscribers: the tree, observable.
|
|
6
|
+
*
|
|
7
|
+
* - **`Commit()` diffs and delivers; it doesn't run projections.** Call `RunProjections()` first
|
|
8
|
+
* when the change needs them. A commit without it delivers the changed roots with the projection
|
|
9
|
+
* outputs as they were, which is how to send a small change (a flag) ahead of a long write.
|
|
10
|
+
* - **Each subscription names its roots and form** (`ISubscribeOptions`). The diff is computed
|
|
11
|
+
* once per commit and shaped for each: diff records, or whole changed roots. A commit with no
|
|
12
|
+
* changes delivers nothing.
|
|
13
|
+
* - **A new subscription gets the committed state first**, inside `Subscribe`, then every commit.
|
|
14
|
+
* Uncommitted writes arrive with the next commit, like everyone else's.
|
|
15
|
+
* - **Delivery is synchronous**, inside `Commit()`. A subscriber may write to another tree, but
|
|
16
|
+
* a change to this one, or a commit, during its delivery throws. Values are shared with the
|
|
17
|
+
* tree, never cloned: treat them as read-only.
|
|
18
|
+
* - When a subscriber throws, the others still get the commit, and `Commit()` throws the first error.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const tree = new ObservableDiffTree({ keyFunc, routes });
|
|
22
|
+
* const stop = tree.Subscribe({ roots: ["loading", "$projection_*"] }, post => store.Apply(post.diff));
|
|
23
|
+
* tree.WritePath("loading", true);
|
|
24
|
+
* tree.Commit();
|
|
25
|
+
*/
|
|
26
|
+
export class ObservableDiffTree extends DiffTree {
|
|
27
|
+
/**
|
|
28
|
+
* @param config The tree's endpoint, keyFunc, routes and opaque keys (`IObservableDiffTreeConfig`).
|
|
29
|
+
* @throws When a keyFunc is given with an endpoint (the endpoint's is the tree's), or a route of
|
|
30
|
+
* the tree's own is one of the endpoint's follow routes.
|
|
31
|
+
*/
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
const { endpoint, routes = {} } = config;
|
|
34
|
+
if (endpoint && config.keyFunc)
|
|
35
|
+
throw new Error(`ObservableDiffTree: the endpoint ${endpoint.name} gives the tree its keyFunc; leave it out of the config.`);
|
|
36
|
+
const follow = endpoint ? FollowRoutes(Object.fromEntries(endpoint.remotes.map(remote => [remote.source, remote.keyFunc]))) : {};
|
|
37
|
+
for (const route of Object.keys(follow))
|
|
38
|
+
if (route in routes)
|
|
39
|
+
throw new Error(`ObservableDiffTree: the route ${route} is ${endpoint.name}'s follow route for a source; rename it.`);
|
|
40
|
+
const keyFunc = endpoint ? endpoint.keyFunc : config.keyFunc;
|
|
41
|
+
super(keyFunc, { ...routes, ...follow });
|
|
42
|
+
this.subscriptions = new SubscriptionManager();
|
|
43
|
+
this.seq = 0;
|
|
44
|
+
/** How many deliveries are running: changes and commits throw while it isn't 0 */
|
|
45
|
+
this.delivering = 0;
|
|
46
|
+
this.Endpoint = endpoint;
|
|
47
|
+
this.KeyFunc = keyFunc;
|
|
48
|
+
config.opaque && this.MarkOpaque(config.opaque);
|
|
49
|
+
}
|
|
50
|
+
/** The number of the last commit that changed something; 0 before the first */
|
|
51
|
+
get Seq() {
|
|
52
|
+
return this.seq;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Diffs the tree since the last commit and delivers the changes to every subscription whose
|
|
56
|
+
* roots they touch.
|
|
57
|
+
* @returns The diff.
|
|
58
|
+
* @throws During a delivery from this tree, or the first error a subscriber threw.
|
|
59
|
+
*/
|
|
60
|
+
Commit() {
|
|
61
|
+
this.BeforeChange();
|
|
62
|
+
const diff = super.Commit();
|
|
63
|
+
if (diff.length === 0)
|
|
64
|
+
return diff;
|
|
65
|
+
const seq = ++this.seq;
|
|
66
|
+
const changed = new Set();
|
|
67
|
+
const deleted = new Set();
|
|
68
|
+
for (let x = 0; x < diff.length; x++) {
|
|
69
|
+
const record = diff[x];
|
|
70
|
+
const key = record.path[0];
|
|
71
|
+
changed.add(key);
|
|
72
|
+
record.type === "delete" && record.path.length === 1 && deleted.add(key);
|
|
73
|
+
}
|
|
74
|
+
const errors = [];
|
|
75
|
+
for (const { id, subscription, keys } of this.subscriptions.Matching(changed)) {
|
|
76
|
+
// An earlier subscriber's delivery may have ended this one
|
|
77
|
+
if (!this.subscriptions.Has(id))
|
|
78
|
+
continue;
|
|
79
|
+
let delivery;
|
|
80
|
+
if (subscription.form === "diff") {
|
|
81
|
+
const matched = this.subscriptions.MatchesAll(id) ? diff : FilterDiff(diff, new Set(keys));
|
|
82
|
+
delivery = { seq, diff: matched };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const roots = {};
|
|
86
|
+
const gone = [];
|
|
87
|
+
for (const key of keys)
|
|
88
|
+
deleted.has(key) ? gone.push(key) : roots[key] = this.Committed(key);
|
|
89
|
+
delivery = { seq, roots, deleted: gone };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
this.Deliver(subscription, delivery);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
errors.push(err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (errors.length > 0)
|
|
99
|
+
throw errors[0];
|
|
100
|
+
return diff;
|
|
101
|
+
}
|
|
102
|
+
Subscribe(options, callback) {
|
|
103
|
+
const subscription = { form: options.form ?? "diff", callback };
|
|
104
|
+
const id = this.subscriptions.Subscribe(options.roots, subscription, { group: options.group });
|
|
105
|
+
const keys = this.subscriptions.MatchingKeys(id, this.CommittedKeys());
|
|
106
|
+
const delivery = subscription.form === "diff"
|
|
107
|
+
? { seq: this.seq, diff: keys.map(key => ({ type: "write", path: [key], value: this.Committed(key) })) }
|
|
108
|
+
: { seq: this.seq, roots: Object.fromEntries(keys.map(key => [key, this.Committed(key)])), deleted: [] };
|
|
109
|
+
try {
|
|
110
|
+
keys.length > 0 && this.Deliver(subscription, delivery);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
this.subscriptions.Unsubscribe(id);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
return () => { this.subscriptions.Unsubscribe(id); };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Waits for the first delivery in which every key has a committed value: at once when they all
|
|
120
|
+
* have one, otherwise the commit that adds the last. The values come from one commit.
|
|
121
|
+
* @param keys Exact root keys; no patterns.
|
|
122
|
+
* @param options `signal` cancels the wait, rejecting with its reason.
|
|
123
|
+
* @returns Each key's committed value.
|
|
124
|
+
*/
|
|
125
|
+
Once(keys, options = {}) {
|
|
126
|
+
const { signal } = options;
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
if (keys.some(key => key.endsWith("*")))
|
|
129
|
+
return reject(new Error(`Once takes exact keys, not patterns: ${keys.join(", ")}`));
|
|
130
|
+
if (signal?.aborted)
|
|
131
|
+
return reject(signal.reason);
|
|
132
|
+
let done = false;
|
|
133
|
+
let stop;
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
finish();
|
|
136
|
+
reject(signal.reason);
|
|
137
|
+
};
|
|
138
|
+
const finish = () => {
|
|
139
|
+
done = true;
|
|
140
|
+
stop?.();
|
|
141
|
+
signal?.removeEventListener("abort", onAbort);
|
|
142
|
+
};
|
|
143
|
+
stop = this.Subscribe({ roots: keys, form: "snapshot" }, () => {
|
|
144
|
+
if (done)
|
|
145
|
+
return;
|
|
146
|
+
const values = {};
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
const value = this.Committed(key);
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
return;
|
|
151
|
+
values[key] = value;
|
|
152
|
+
}
|
|
153
|
+
finish();
|
|
154
|
+
resolve(values);
|
|
155
|
+
});
|
|
156
|
+
// Resolved by the first delivery, before `stop` was set
|
|
157
|
+
if (done)
|
|
158
|
+
stop();
|
|
159
|
+
else
|
|
160
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** Ends every subscription made with this `group`; see `ISubscribeOptions.group` */
|
|
164
|
+
Disconnect(group) {
|
|
165
|
+
this.subscriptions.Disconnect(group);
|
|
166
|
+
}
|
|
167
|
+
/** How many subscriptions name a root key exactly now; see `SubscriptionManager.SubscriberCount` */
|
|
168
|
+
SubscriberCount(key) {
|
|
169
|
+
return this.subscriptions.SubscriberCount(key);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Calls back when a root key matching a pattern loses its last exact subscription. It only
|
|
173
|
+
* reports: whether and when to delete the key is the caller's. It runs inside the unsubscribe,
|
|
174
|
+
* which may be inside a delivery, so a callback that deletes the key does it later (a timer).
|
|
175
|
+
* @param pattern An exact key or a pattern ending in `*`.
|
|
176
|
+
* @returns A function that removes the callback.
|
|
177
|
+
*/
|
|
178
|
+
OnReleased(pattern, callback) {
|
|
179
|
+
return this.subscriptions.OnReleased(pattern, callback);
|
|
180
|
+
}
|
|
181
|
+
Deliver(subscription, delivery) {
|
|
182
|
+
this.delivering++;
|
|
183
|
+
try {
|
|
184
|
+
subscription.callback(delivery);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
this.delivering--;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** A subscriber can't change or commit the tree delivering to it */
|
|
191
|
+
BeforeChange() {
|
|
192
|
+
if (this.delivering > 0)
|
|
193
|
+
throw new Error("ObservableDiffTree: a subscriber can't write to or commit the tree delivering to it.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** The records whose root is one of `keys` */
|
|
197
|
+
function FilterDiff(diff, keys) {
|
|
198
|
+
return diff.filter(record => keys.has(record.path[0]));
|
|
199
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IChannel } from "./diffTree.types";
|
|
2
|
+
type ConnectHandler = (port: MessagePort) => (() => void) | void;
|
|
3
|
+
/**
|
|
4
|
+
* Handles a worker's ports to a source it reads from, given by a `DiffTreeHub` (`Register(name,
|
|
5
|
+
* factory, { sources })`). The handler runs for each port: when the worker starts, and again when the source
|
|
6
|
+
* restarts. Before it runs for a new port, the cleanup it returned for the last one runs and that
|
|
7
|
+
* port is closed. A port that arrives before the handler is registered is kept for it, once the
|
|
8
|
+
* worker listens: `HostPublisher` starts listening, and so does the first `OnConnect`.
|
|
9
|
+
*
|
|
10
|
+
* The source must be one the worker was registered with (`sources`). The hub tells the worker its
|
|
11
|
+
* sources when it starts: `OnConnect` for another source throws then, or, when it was called
|
|
12
|
+
* before they arrived (while the worker's module first runs), throws as the worker's uncaught
|
|
13
|
+
* error when they do, which the main thread sees as the `Worker`'s `error` event.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // trace worker
|
|
17
|
+
* OnConnect("engine", port => {
|
|
18
|
+
* tree.DeleteRoute("/engine/");
|
|
19
|
+
* return Subscribe(port, { roots: ["index"], form: "snapshot" }, update => { ... });
|
|
20
|
+
* });
|
|
21
|
+
* @param source The source worker's registered name.
|
|
22
|
+
* @param handler Subscribes over the port; may return a function that ends what it started.
|
|
23
|
+
* @param scope The worker's own channel; `self` when omitted.
|
|
24
|
+
* @returns A function that runs the current cleanup and removes the handler.
|
|
25
|
+
* @throws When the hub has told the worker its sources, and `source` isn't one.
|
|
26
|
+
*/
|
|
27
|
+
export declare function OnConnect(source: string, handler: ConnectHandler, scope?: IChannel): () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Starts keeping the `jt-connect` ports that arrive on a worker's channel, so a port that arrives
|
|
30
|
+
* before its `OnConnect` handler is registered is kept for it. `HostPublisher` calls it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ListenForConnections(scope: IChannel): void;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const registries = new WeakMap();
|
|
2
|
+
/**
|
|
3
|
+
* Handles a worker's ports to a source it reads from, given by a `DiffTreeHub` (`Register(name,
|
|
4
|
+
* factory, { sources })`). The handler runs for each port: when the worker starts, and again when the source
|
|
5
|
+
* restarts. Before it runs for a new port, the cleanup it returned for the last one runs and that
|
|
6
|
+
* port is closed. A port that arrives before the handler is registered is kept for it, once the
|
|
7
|
+
* worker listens: `HostPublisher` starts listening, and so does the first `OnConnect`.
|
|
8
|
+
*
|
|
9
|
+
* The source must be one the worker was registered with (`sources`). The hub tells the worker its
|
|
10
|
+
* sources when it starts: `OnConnect` for another source throws then, or, when it was called
|
|
11
|
+
* before they arrived (while the worker's module first runs), throws as the worker's uncaught
|
|
12
|
+
* error when they do, which the main thread sees as the `Worker`'s `error` event.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // trace worker
|
|
16
|
+
* OnConnect("engine", port => {
|
|
17
|
+
* tree.DeleteRoute("/engine/");
|
|
18
|
+
* return Subscribe(port, { roots: ["index"], form: "snapshot" }, update => { ... });
|
|
19
|
+
* });
|
|
20
|
+
* @param source The source worker's registered name.
|
|
21
|
+
* @param handler Subscribes over the port; may return a function that ends what it started.
|
|
22
|
+
* @param scope The worker's own channel; `self` when omitted.
|
|
23
|
+
* @returns A function that runs the current cleanup and removes the handler.
|
|
24
|
+
* @throws When the hub has told the worker its sources, and `source` isn't one.
|
|
25
|
+
*/
|
|
26
|
+
export function OnConnect(source, handler, scope = globalThis) {
|
|
27
|
+
const connections = Connections(scope);
|
|
28
|
+
if (connections.declared && !connections.declared.has(source))
|
|
29
|
+
throw UndeclaredError([source], connections.declared);
|
|
30
|
+
connections.cleanups.get(source)?.();
|
|
31
|
+
connections.cleanups.delete(source);
|
|
32
|
+
connections.handlers.set(source, handler);
|
|
33
|
+
const port = connections.ports.get(source);
|
|
34
|
+
port && Run(connections, source, port);
|
|
35
|
+
return () => {
|
|
36
|
+
if (connections.handlers.get(source) !== handler)
|
|
37
|
+
return;
|
|
38
|
+
connections.cleanups.get(source)?.();
|
|
39
|
+
connections.cleanups.delete(source);
|
|
40
|
+
connections.handlers.delete(source);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Starts keeping the `jt-connect` ports that arrive on a worker's channel, so a port that arrives
|
|
45
|
+
* before its `OnConnect` handler is registered is kept for it. `HostPublisher` calls it.
|
|
46
|
+
*/
|
|
47
|
+
export function ListenForConnections(scope) {
|
|
48
|
+
Connections(scope);
|
|
49
|
+
}
|
|
50
|
+
function Connections(scope) {
|
|
51
|
+
let connections = registries.get(scope);
|
|
52
|
+
if (!connections) {
|
|
53
|
+
const created = connections = { ports: new Map(), handlers: new Map(), cleanups: new Map() };
|
|
54
|
+
registries.set(scope, created);
|
|
55
|
+
scope.addEventListener("message", (event) => {
|
|
56
|
+
const message = event.data;
|
|
57
|
+
if (message?.type === "jt-sources") {
|
|
58
|
+
const declared = created.declared = new Set(message.sources);
|
|
59
|
+
const undeclared = Array.from(created.handlers.keys()).filter(source => !declared.has(source));
|
|
60
|
+
if (undeclared.length > 0)
|
|
61
|
+
throw UndeclaredError(undeclared, declared);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (message?.type !== "jt-connect")
|
|
65
|
+
return;
|
|
66
|
+
const { source, port } = message;
|
|
67
|
+
created.cleanups.get(source)?.();
|
|
68
|
+
created.cleanups.delete(source);
|
|
69
|
+
created.ports.get(source)?.close();
|
|
70
|
+
created.ports.set(source, port);
|
|
71
|
+
created.handlers.has(source) && Run(created, source, port);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return connections;
|
|
75
|
+
}
|
|
76
|
+
function UndeclaredError(sources, declared) {
|
|
77
|
+
const list = declared.size > 0 ? Array.from(declared).join(", ") : "none";
|
|
78
|
+
return new Error(`OnConnect: ${sources.join(", ")} isn't a source this worker was registered with (sources: ${list}); add it to the worker's sources in DiffTreeHub.Register.`);
|
|
79
|
+
}
|
|
80
|
+
function Run(connections, source, port) {
|
|
81
|
+
const cleanup = connections.handlers.get(source)(port);
|
|
82
|
+
cleanup && connections.cleanups.set(source, cleanup);
|
|
83
|
+
}
|
|
@@ -95,6 +95,13 @@ export type IRouteDefinition = {
|
|
|
95
95
|
projections: IProjectionDefinitions;
|
|
96
96
|
keyFunc?: IKeyFunc;
|
|
97
97
|
routes?: IRouteDefinitions;
|
|
98
|
+
/**
|
|
99
|
+
* A follow route: it holds another tree's roots, written only by `ApplyDiff` from a subscription
|
|
100
|
+
* to that tree, under the source's own keys. It has no projections of its own, so a source's
|
|
101
|
+
* projection output (`$projection_<key>`) is held as it is, and any other write to it throws.
|
|
102
|
+
* Projections elsewhere read it as any route. See `FollowRoutes`.
|
|
103
|
+
*/
|
|
104
|
+
follow?: boolean;
|
|
98
105
|
};
|
|
99
106
|
export type IRouteDefinitions = {
|
|
100
107
|
[route: string]: IRouteDefinition;
|
|
@@ -110,4 +117,6 @@ export type IRoute = {
|
|
|
110
117
|
childPartCounts: number[];
|
|
111
118
|
keyFunc?: IKeyFunc;
|
|
112
119
|
routeDefinitions?: IRouteDefinitions;
|
|
120
|
+
/** A follow route, written only by `ApplyDiff`; see `IRouteDefinition.follow` */
|
|
121
|
+
follow: boolean;
|
|
113
122
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JsonDiffResult } from "../../../Utils/json";
|
|
1
2
|
import { ISnapshotState } from "../../Tree/snapshot";
|
|
2
3
|
type KeyFunc = {
|
|
3
4
|
(value: any): string | undefined;
|
|
@@ -43,4 +44,15 @@ export declare function ApplyValueToPath(context: WriteContext, path: string, va
|
|
|
43
44
|
* @throws When there's no array at the path
|
|
44
45
|
*/
|
|
45
46
|
export declare function SpliceValueAtPath(context: WriteContext, path: string, start: number, deleteCount: number, items: readonly unknown[]): void;
|
|
47
|
+
/**
|
|
48
|
+
* Applies another tree's diff records to a route, in order, as writes: a `write` as
|
|
49
|
+
* ApplyValueToPath writes it, a `splice` as SpliceValueAtPath splices, its inserted values
|
|
50
|
+
* flattened, and a `delete` by removing the property its path ends with, a root key's from the
|
|
51
|
+
* route's state. The records' paths are parts, relative to the route, so a key holding `.` or `/`
|
|
52
|
+
* is applied as it is. Records from a tree with the same keyFunc never write inside a slot: the
|
|
53
|
+
* change to an entity comes as its own root's records.
|
|
54
|
+
* @param route A full route path, e.g. `/` or `/engine/`
|
|
55
|
+
* @throws When a record's parent doesn't exist, or a splice's path holds no array
|
|
56
|
+
*/
|
|
57
|
+
export declare function ApplyRecords(context: WriteContext, records: JsonDiffResult, route: string): void;
|
|
46
58
|
export {};
|
|
@@ -74,6 +74,46 @@ export function SpliceValueAtPath(context, path, start, deleteCount, items) {
|
|
|
74
74
|
JsonApplySplices(array, [{ index, delete: actualDelete, insert }], KeepValue);
|
|
75
75
|
Snapshot.Invalidate(context.snapshot, array);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Applies another tree's diff records to a route, in order, as writes: a `write` as
|
|
79
|
+
* ApplyValueToPath writes it, a `splice` as SpliceValueAtPath splices, its inserted values
|
|
80
|
+
* flattened, and a `delete` by removing the property its path ends with, a root key's from the
|
|
81
|
+
* route's state. The records' paths are parts, relative to the route, so a key holding `.` or `/`
|
|
82
|
+
* is applied as it is. Records from a tree with the same keyFunc never write inside a slot: the
|
|
83
|
+
* change to an entity comes as its own root's records.
|
|
84
|
+
* @param route A full route path, e.g. `/` or `/engine/`
|
|
85
|
+
* @throws When a record's parent doesn't exist, or a splice's path holds no array
|
|
86
|
+
*/
|
|
87
|
+
export function ApplyRecords(context, records, route) {
|
|
88
|
+
const routeRoot = RouteState(context.root, route, true);
|
|
89
|
+
const keyFunc = context.routeKeyFunc(route);
|
|
90
|
+
for (let x = 0; x < records.length; x++) {
|
|
91
|
+
const record = records[x];
|
|
92
|
+
const parts = record.path.map(String);
|
|
93
|
+
if (record.type === "write") {
|
|
94
|
+
WriteValue(context, route, parts, record.value);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (record.type === "delete") {
|
|
98
|
+
const parent = ReadPathParts(routeRoot, parts, parts.length - 1);
|
|
99
|
+
if (JsonType(parent) !== "value" && Object.hasOwn(parent, parts[parts.length - 1]))
|
|
100
|
+
Remove(context.snapshot, parent, parts[parts.length - 1]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const array = ReadPathParts(routeRoot, parts, parts.length);
|
|
104
|
+
if (!Array.isArray(array))
|
|
105
|
+
throw new Error(`Unable to apply a splice at ${parts.join(PROP_DELIMETER)}: no array there`);
|
|
106
|
+
const targetPath = JoinPaths(route, parts.join(PROP_DELIMETER));
|
|
107
|
+
const splices = record.splices.map(splice => ({
|
|
108
|
+
...splice,
|
|
109
|
+
insert: splice.insert.map(item => item.type === SPLICE_ACTION.INSERT
|
|
110
|
+
? { type: SPLICE_ACTION.INSERT, value: Flatten(context, keyFunc, route, targetPath, item.value) }
|
|
111
|
+
: item)
|
|
112
|
+
}));
|
|
113
|
+
JsonApplySplices(array, splices, KeepValue);
|
|
114
|
+
Snapshot.Invalidate(context.snapshot, array);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
77
117
|
/** A written path's route from the root (`/` for the root route) and its property path */
|
|
78
118
|
function WriteRoute(path) {
|
|
79
119
|
const [route, property] = SplitRouteAndPath(path);
|
|
@@ -25,3 +25,21 @@ export declare class RouteInstances {
|
|
|
25
25
|
*/
|
|
26
26
|
Delete(route: string): void;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The follow routes for the trees a tree follows, one per source by name: `/<name>/`, holding the
|
|
30
|
+
* source's roots under their own keys, each keyed entity at its home in the route (its key is
|
|
31
|
+
* joined to the route, so it can't meet the tree's own). Give each source's keyFunc: the trees that
|
|
32
|
+
* pass data identify it the same way, or a slot holding an entity elsewhere in the source keeps its
|
|
33
|
+
* first value here. Merge them into the tree's route definitions; a tree built from an endpoint
|
|
34
|
+
* (`IObservableDiffTreeConfig.endpoint`) has its sources' added for it.
|
|
35
|
+
* @example
|
|
36
|
+
* const tree = new ObservableDiffTree({ routes: {
|
|
37
|
+
* "/": { projections: { ... } },
|
|
38
|
+
* ...FollowRoutes({ project: entityKey, engine: undefined }),
|
|
39
|
+
* } });
|
|
40
|
+
* // a subscription to the project's tree applies its posts at /project/
|
|
41
|
+
* subscription.Connect(port); // callback: post => tree.ApplyDiff(post.diff, "/project/")
|
|
42
|
+
*/
|
|
43
|
+
export declare function FollowRoutes(sources: {
|
|
44
|
+
[name: string]: IKeyFunc | undefined;
|
|
45
|
+
}): IRouteDefinitions;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IsWildcardPath, JoinPaths, PathsOverlap, PROJECTION_PREFIX, ROOT_ROUTE, RouteSegments, ValidateWildcardPath } from "./paths";
|
|
1
|
+
import { IsWildcardPath, JoinPaths, PathsOverlap, PROJECTION_PREFIX, ROOT_ROUTE, ROUTE_DELIMETER, RouteSegments, ValidateWildcardPath } from "./paths";
|
|
2
2
|
/**
|
|
3
3
|
* A DiffTree's route instances: one per route path a definition matches, created from the
|
|
4
4
|
* definition on first use, each with its own projections and keyFunc.
|
|
@@ -124,6 +124,9 @@ function MatchRouteDefinition(definitions, segments, index) {
|
|
|
124
124
|
* @param defaultKeyFunc The tree's keyFunc, used when the definition doesn't define one
|
|
125
125
|
*/
|
|
126
126
|
function CreateRoute(parts, routePath, definition, defaultKeyFunc) {
|
|
127
|
+
const follow = definition.follow === true;
|
|
128
|
+
if (follow && Object.keys(definition.projections).length > 0)
|
|
129
|
+
throw new Error(`Invalid follow route ${routePath}: a follow route holds another tree's roots and has no projections of its own.`);
|
|
127
130
|
const projections = [];
|
|
128
131
|
for (const key in definition.projections) {
|
|
129
132
|
const projectionDefinition = definition.projections[key];
|
|
@@ -156,9 +159,37 @@ function CreateRoute(parts, routePath, definition, defaultKeyFunc) {
|
|
|
156
159
|
childRoutes: new Map(),
|
|
157
160
|
childPartCounts: PartCounts(definition.routes),
|
|
158
161
|
keyFunc: definition.keyFunc ?? defaultKeyFunc,
|
|
159
|
-
routeDefinitions: definition.routes
|
|
162
|
+
routeDefinitions: definition.routes,
|
|
163
|
+
follow
|
|
160
164
|
};
|
|
161
165
|
}
|
|
166
|
+
/** A source's keyFunc when it has none: a follow route doesn't take the tree's. */
|
|
167
|
+
const NoKey = () => undefined;
|
|
168
|
+
/**
|
|
169
|
+
* The follow routes for the trees a tree follows, one per source by name: `/<name>/`, holding the
|
|
170
|
+
* source's roots under their own keys, each keyed entity at its home in the route (its key is
|
|
171
|
+
* joined to the route, so it can't meet the tree's own). Give each source's keyFunc: the trees that
|
|
172
|
+
* pass data identify it the same way, or a slot holding an entity elsewhere in the source keeps its
|
|
173
|
+
* first value here. Merge them into the tree's route definitions; a tree built from an endpoint
|
|
174
|
+
* (`IObservableDiffTreeConfig.endpoint`) has its sources' added for it.
|
|
175
|
+
* @example
|
|
176
|
+
* const tree = new ObservableDiffTree({ routes: {
|
|
177
|
+
* "/": { projections: { ... } },
|
|
178
|
+
* ...FollowRoutes({ project: entityKey, engine: undefined }),
|
|
179
|
+
* } });
|
|
180
|
+
* // a subscription to the project's tree applies its posts at /project/
|
|
181
|
+
* subscription.Connect(port); // callback: post => tree.ApplyDiff(post.diff, "/project/")
|
|
182
|
+
*/
|
|
183
|
+
export function FollowRoutes(sources) {
|
|
184
|
+
const definitions = {};
|
|
185
|
+
for (const name of Object.keys(sources)) {
|
|
186
|
+
if (name === "" || name.includes(ROUTE_DELIMETER) || IsParam(name))
|
|
187
|
+
throw new Error(`Invalid follow route name ${name}: one route segment, not a parameter.`);
|
|
188
|
+
// A route without a keyFunc takes the tree's: a source without one is given one that keys nothing
|
|
189
|
+
definitions[`${ROUTE_DELIMETER}${name}`] = { projections: {}, keyFunc: sources[name] ?? NoKey, follow: true };
|
|
190
|
+
}
|
|
191
|
+
return definitions;
|
|
192
|
+
}
|
|
162
193
|
/** Whether a projection definition is a keyed projection: it has no `reads` */
|
|
163
194
|
function IsKeyedDefinition(definition) {
|
|
164
195
|
return definition.reads === undefined;
|
package/Store/Store/store.d.ts
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
import { JsonDiffResult } from "../../Utils/json";
|
|
2
|
+
import type { IKeyFunc } from "../Diff/utils/diffTree.types";
|
|
3
|
+
/** Options shared by `StoreSync` and `StoreAsync` */
|
|
4
|
+
export interface IStoreOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Gives a value its key, so the store resolves an object written with an id to its own root. For a
|
|
7
|
+
* store whose tree has none to give: a `StoreAsync` over a plain channel (pass the worker tree's
|
|
8
|
+
* key logic), or a `StoreSync` with a tree of its own. A lease's keyFunc is the endpoint's, and a
|
|
9
|
+
* tree's its own; giving one there too throws.
|
|
10
|
+
*/
|
|
11
|
+
keyFunc?: IKeyFunc;
|
|
12
|
+
/**
|
|
13
|
+
* Root keys the store holds whole, as an `Opaque`: a written root or a projection's
|
|
14
|
+
* `$projection_<key>`. `Get` returns such a root's raw value, never proxied, the same reference
|
|
15
|
+
* until the root is written again, and only that write updates its readers. Treat the value as
|
|
16
|
+
* read-only: a change made inside it notifies no one. An opaque root can't be read through a
|
|
17
|
+
* key (`keyFunc`). Its tree must write each one whole instead of diffing inside it
|
|
18
|
+
* (`DiffTree.MarkOpaque`): a store with a tree of its own marks it; for a hub's tree, give the
|
|
19
|
+
* tree the same keys (`IObservableDiffTreeConfig.opaque`) from a file both import. Write the raw
|
|
20
|
+
* value: the store boxes it, and an `Opaque` written to a store isn't opened.
|
|
21
|
+
*/
|
|
22
|
+
opaque?: readonly string[];
|
|
23
|
+
}
|
|
2
24
|
/**
|
|
3
25
|
* Base class for reading observable data: holds root objects as ObservableNode instances
|
|
4
|
-
* and applies diff results to them. Subclasses decide where the diffs come from: `
|
|
5
|
-
*
|
|
26
|
+
* and applies diff results to them. Subclasses decide where the diffs come from: `StoreSync`
|
|
27
|
+
* and `StoreAsync` apply what an `ObservableDiffTree` delivers to them.
|
|
6
28
|
*
|
|
7
29
|
* @see Store
|
|
8
|
-
* @see PushStore
|
|
9
30
|
*/
|
|
10
31
|
export declare abstract class ReadStore {
|
|
11
32
|
protected keyFunc?: (value: any) => string | undefined;
|
|
12
33
|
private rootMap;
|
|
13
34
|
private createNode;
|
|
35
|
+
/** Root keys held whole, see `IStoreOptions.opaque` */
|
|
36
|
+
protected readonly opaque: ReadonlySet<string | number>;
|
|
14
37
|
/**
|
|
15
38
|
* Creates an instance of ReadStore.
|
|
16
39
|
* @param keyFunc Optional function to generate a key for a given data value.
|
|
17
40
|
* When provided, enables alias functionality where values can reference other root objects.
|
|
41
|
+
* @param opaque Root keys held whole, see `IStoreOptions.opaque`.
|
|
18
42
|
*/
|
|
19
|
-
constructor(keyFunc?: (value: any) => string | undefined);
|
|
43
|
+
constructor(keyFunc?: (value: any) => string | undefined, opaque?: readonly string[]);
|
|
20
44
|
/**
|
|
21
45
|
* Retrieves a value from the store by id.
|
|
22
46
|
* @template O - The type of the value to retrieve
|
|
@@ -45,6 +69,14 @@ export declare abstract class ReadStore {
|
|
|
45
69
|
* @protected
|
|
46
70
|
*/
|
|
47
71
|
protected UpdateRootMap(results: JsonDiffResult): void;
|
|
72
|
+
/**
|
|
73
|
+
* Applies whole-root writes by diffing each one into the root the store already holds, so
|
|
74
|
+
* readers of an unchanged part aren't notified and keep its objects. Anything else, an opaque
|
|
75
|
+
* root, or a root the store doesn't hold, is applied as it is.
|
|
76
|
+
* @param results - Diff results, e.g. a tree's first state after reconnecting
|
|
77
|
+
* @protected
|
|
78
|
+
*/
|
|
79
|
+
protected UpdateRootMapInPlace(results: JsonDiffResult): void;
|
|
48
80
|
/**
|
|
49
81
|
* Updates a specific root object with diff results.
|
|
50
82
|
* Creates the root object if it doesn't exist.
|