j-templates 10.0.0 → 10.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SYNTAX_PRIMER.md +1 -1
- package/Store/Diff/channelClient.d.ts +0 -2
- package/Store/Diff/channelClient.js +2 -5
- package/Store/Diff/diffTree.types.d.ts +0 -5
- package/Store/Diff/hostPublisher.d.ts +0 -1
- package/Store/Diff/hostPublisher.js +0 -4
- package/Store/Diff/observableDiffTree.js +2 -1
- package/Store/Diff/utils/paths.d.ts +2 -1
- package/Store/Diff/utils/paths.js +3 -2
- package/Store/Diff/utils/state.js +4 -3
- package/Store/Store/store.d.ts +8 -3
- package/Store/Store/store.js +17 -3
- package/Store/Store/storeAsync.d.ts +56 -2
- package/Store/Store/storeAsync.js +139 -6
- package/Store/Store/storeSync.d.ts +17 -2
- package/Store/Store/storeSync.js +5 -3
- package/Store/Tree/observableNode.d.ts +3 -0
- package/Store/Tree/observableNode.js +19 -0
- package/Store/Tree/observableScope.d.ts +3 -1
- package/Store/Tree/observableScope.js +12 -0
- package/Utils/emitter.d.ts +11 -4
- package/Utils/emitter.js +25 -2
- package/Utils/pattern.d.ts +14 -0
- package/Utils/pattern.js +20 -0
- package/Utils/subscriptionManager.js +3 -6
- package/package.json +1 -1
package/SYNTAX_PRIMER.md
CHANGED
|
@@ -626,7 +626,7 @@ const store = new StoreSync(new ObservableDiffTree({ keyFunc, routes, ...layoutC
|
|
|
626
626
|
const layout = store.Get<Layout>("$projection_layout"); // the raw value: no proxies inside it
|
|
627
627
|
```
|
|
628
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`
|
|
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. A store never marks a tree: a `StoreAsync` needs its worker's tree to have the keys in its config too. 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
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
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
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 }`).
|
|
@@ -18,8 +18,6 @@ export declare class ChannelClient {
|
|
|
18
18
|
Batch(calls: IDiffMethodBatch): Promise<void>;
|
|
19
19
|
/** Rejects the batches waiting for an ack with `error`; this client stops waiting for them */
|
|
20
20
|
RejectBatches(error: unknown): void;
|
|
21
|
-
/** See `DiffTree.MarkOpaque` */
|
|
22
|
-
MarkOpaque(keys: readonly string[]): void;
|
|
23
21
|
/**
|
|
24
22
|
* Subscribes to the host's tree. Deliveries arrive as the host posts them.
|
|
25
23
|
* @param onSubscribed Called once the host has posted the first state, with its error when
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IsPattern } from "../../Utils/pattern";
|
|
1
2
|
/** Ids are unique across clients, so several can share one channel */
|
|
2
3
|
let nextId = 1;
|
|
3
4
|
/**
|
|
@@ -32,10 +33,6 @@ export class ChannelClient {
|
|
|
32
33
|
this.batches.clear();
|
|
33
34
|
batches.forEach(batch => batch.reject(error));
|
|
34
35
|
}
|
|
35
|
-
/** See `DiffTree.MarkOpaque` */
|
|
36
|
-
MarkOpaque(keys) {
|
|
37
|
-
this.channel.postMessage({ type: "jt-markopaque", keys });
|
|
38
|
-
}
|
|
39
36
|
/**
|
|
40
37
|
* Subscribes to the host's tree. Deliveries arrive as the host posts them.
|
|
41
38
|
* @param onSubscribed Called once the host has posted the first state, with its error when
|
|
@@ -194,7 +191,7 @@ export function Subscribe(channel, options, callback) {
|
|
|
194
191
|
export function Once(channel, keys, options = {}) {
|
|
195
192
|
const { signal } = options;
|
|
196
193
|
return new Promise((resolve, reject) => {
|
|
197
|
-
if (keys.some(
|
|
194
|
+
if (keys.some(IsPattern))
|
|
198
195
|
return reject(new Error(`Once takes exact keys, not patterns: ${keys.join(", ")}`));
|
|
199
196
|
if (signal?.aborted)
|
|
200
197
|
return reject(signal.reason);
|
|
@@ -103,11 +103,6 @@ export type IHostRequest =
|
|
|
103
103
|
| {
|
|
104
104
|
type: "jt-disconnect";
|
|
105
105
|
}
|
|
106
|
-
/** See `DiffTree.MarkOpaque` */
|
|
107
|
-
| {
|
|
108
|
-
type: "jt-markopaque";
|
|
109
|
-
keys: readonly string[];
|
|
110
|
-
}
|
|
111
106
|
/** From a `DiffTreeHub`, on the worker's own channel: serve `port`, a lease's channel, as `id` */
|
|
112
107
|
| {
|
|
113
108
|
type: "jt-serve";
|
|
@@ -22,7 +22,6 @@ export interface IHost {
|
|
|
22
22
|
* - `jt-subscribe` / `jt-unsubscribe`: a subscription for the channel. Its first state and every
|
|
23
23
|
* commit are posted as `jt-post`, then `jt-subscribed` once the first state is out.
|
|
24
24
|
* - `jt-disconnect`: ends every subscription on the channel.
|
|
25
|
-
* - `jt-markopaque`: see `DiffTree.MarkOpaque`.
|
|
26
25
|
* - `jt-serve` / `jt-unserve`: from a `DiffTreeHub`, a lease's port to serve, or to stop serving.
|
|
27
26
|
*
|
|
28
27
|
* The worker's own work commits on the same publisher, and its commits reach every subscriber
|
|
@@ -12,7 +12,6 @@ import { ListenForConnections } from "./onConnect";
|
|
|
12
12
|
* - `jt-subscribe` / `jt-unsubscribe`: a subscription for the channel. Its first state and every
|
|
13
13
|
* commit are posted as `jt-post`, then `jt-subscribed` once the first state is out.
|
|
14
14
|
* - `jt-disconnect`: ends every subscription on the channel.
|
|
15
|
-
* - `jt-markopaque`: see `DiffTree.MarkOpaque`.
|
|
16
15
|
* - `jt-serve` / `jt-unserve`: from a `DiffTreeHub`, a lease's port to serve, or to stop serving.
|
|
17
16
|
*
|
|
18
17
|
* The worker's own work commits on the same publisher, and its commits reach every subscriber
|
|
@@ -65,9 +64,6 @@ export function HostPublisher(publisher, scope) {
|
|
|
65
64
|
publisher.Disconnect(channel);
|
|
66
65
|
subscriptions.clear();
|
|
67
66
|
break;
|
|
68
|
-
case "jt-markopaque":
|
|
69
|
-
publisher.MarkOpaque(message.keys);
|
|
70
|
-
break;
|
|
71
67
|
case "jt-serve":
|
|
72
68
|
leases.set(message.id, Serve(message.port));
|
|
73
69
|
break;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IsPattern } from "../../Utils/pattern";
|
|
1
2
|
import { SubscriptionManager } from "../../Utils/subscriptionManager";
|
|
2
3
|
import { DiffTree } from "./diffTree";
|
|
3
4
|
import { FollowRoutes } from "./utils/routes";
|
|
@@ -125,7 +126,7 @@ export class ObservableDiffTree extends DiffTree {
|
|
|
125
126
|
Once(keys, options = {}) {
|
|
126
127
|
const { signal } = options;
|
|
127
128
|
return new Promise((resolve, reject) => {
|
|
128
|
-
if (keys.some(
|
|
129
|
+
if (keys.some(IsPattern))
|
|
129
130
|
return reject(new Error(`Once takes exact keys, not patterns: ${keys.join(", ")}`));
|
|
130
131
|
if (signal?.aborted)
|
|
131
132
|
return reject(signal.reason);
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* path `value.item` in the route `/channel/1/`. A path without a route (`value.item`) is in the
|
|
4
4
|
* root route.
|
|
5
5
|
*/
|
|
6
|
+
import { WILDCARD } from "../../../Utils/pattern";
|
|
6
7
|
export declare const ROOT_ROUTE = "/";
|
|
7
8
|
export declare const PROJECTION_PREFIX = "$projection_";
|
|
8
9
|
export declare const ROUTE_DELIMETER = "/";
|
|
9
10
|
export declare const PROP_DELIMETER = ".";
|
|
10
|
-
export
|
|
11
|
+
export { WILDCARD };
|
|
11
12
|
export declare function SplitRouteAndPath(path: string): readonly [string, string];
|
|
12
13
|
/**
|
|
13
14
|
* A path's route and property path. Only a path starting with `/` has a route: any other path
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* path `value.item` in the route `/channel/1/`. A path without a route (`value.item`) is in the
|
|
4
4
|
* root route.
|
|
5
5
|
*/
|
|
6
|
+
import { PatternMatches, WILDCARD } from "../../../Utils/pattern";
|
|
6
7
|
export const ROOT_ROUTE = "/";
|
|
7
8
|
export const PROJECTION_PREFIX = "$projection_";
|
|
8
9
|
export const ROUTE_DELIMETER = "/";
|
|
9
10
|
export const PROP_DELIMETER = ".";
|
|
10
|
-
export
|
|
11
|
+
export { WILDCARD };
|
|
11
12
|
export function SplitRouteAndPath(path) {
|
|
12
13
|
const lastSlash = path.lastIndexOf(ROUTE_DELIMETER);
|
|
13
14
|
const route = path.slice(0, lastSlash + 1);
|
|
@@ -56,7 +57,7 @@ export function RouteMatches(pattern, route) {
|
|
|
56
57
|
if (patternParts.length !== routeParts.length)
|
|
57
58
|
return false;
|
|
58
59
|
for (let x = 0; x < patternParts.length; x++)
|
|
59
|
-
if (
|
|
60
|
+
if (!PatternMatches(patternParts[x], routeParts[x]))
|
|
60
61
|
return false;
|
|
61
62
|
return true;
|
|
62
63
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JsonType } from "../../../Utils/json";
|
|
2
2
|
import { Snapshot } from "../../Tree/snapshot";
|
|
3
|
-
import { PROP_DELIMETER, ROUTE_DELIMETER, RouteAndProperty, SplitRouteAndPath
|
|
3
|
+
import { PROP_DELIMETER, ROUTE_DELIMETER, RouteAndProperty, SplitRouteAndPath } from "./paths";
|
|
4
|
+
import { IsPattern, PatternPrefix } from "../../../Utils/pattern";
|
|
4
5
|
/**
|
|
5
6
|
* The raw state of a DiffTree's routes. A route's state hangs off its parent route's state under
|
|
6
7
|
* `ROUTE_STATE`, keyed by segment, so `Object.keys` of a route's state lists only its own values.
|
|
@@ -141,8 +142,8 @@ function InvalidateState(snapshot, value) {
|
|
|
141
142
|
export function ExpandWildcardPath(root, path) {
|
|
142
143
|
const [route, property] = SplitRouteAndPath(path);
|
|
143
144
|
const parts = route.split(ROUTE_DELIMETER);
|
|
144
|
-
const index = parts.findIndex(part => part
|
|
145
|
-
const prefix = parts[index]
|
|
145
|
+
const index = parts.findIndex(part => IsPattern(part));
|
|
146
|
+
const prefix = PatternPrefix(parts[index]);
|
|
146
147
|
const parent = RouteState(root, parts.slice(0, index).join(ROUTE_DELIMETER) + ROUTE_DELIMETER);
|
|
147
148
|
const children = parent?.[ROUTE_STATE] ?? {};
|
|
148
149
|
const paths = {};
|
package/Store/Store/store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { JsonDiffResult } from "../../Utils/json";
|
|
2
2
|
import type { IKeyFunc } from "../Diff/utils/diffTree.types";
|
|
3
|
+
import { EmitterEvent } from "../../Utils/emitter";
|
|
3
4
|
/** Options shared by `StoreSync` and `StoreAsync` */
|
|
4
5
|
export interface IStoreOptions {
|
|
5
6
|
/**
|
|
@@ -15,8 +16,9 @@ export interface IStoreOptions {
|
|
|
15
16
|
* until the root is written again, and only that write updates its readers. Treat the value as
|
|
16
17
|
* read-only: a change made inside it notifies no one. An opaque root can't be read through a
|
|
17
18
|
* 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;
|
|
19
|
-
*
|
|
19
|
+
* (`DiffTree.MarkOpaque`): a store with a tree of its own marks it; any other tree, a hub's or a
|
|
20
|
+
* worker's, takes the same keys in its config (`IObservableDiffTreeConfig.opaque`), from a file
|
|
21
|
+
* both import. A store never marks a tree: opaque is local. Write the raw
|
|
20
22
|
* value: the store boxes it, and an `Opaque` written to a store isn't opened.
|
|
21
23
|
*/
|
|
22
24
|
opaque?: readonly string[];
|
|
@@ -34,13 +36,15 @@ export declare abstract class ReadStore {
|
|
|
34
36
|
private createNode;
|
|
35
37
|
/** Root keys held whole, see `IStoreOptions.opaque` */
|
|
36
38
|
protected readonly opaque: ReadonlySet<string | number>;
|
|
39
|
+
protected readonly watch: readonly string[];
|
|
40
|
+
protected readonly watchCallback?: (rootProperty: string, event: EmitterEvent) => void;
|
|
37
41
|
/**
|
|
38
42
|
* Creates an instance of ReadStore.
|
|
39
43
|
* @param keyFunc Optional function to generate a key for a given data value.
|
|
40
44
|
* When provided, enables alias functionality where values can reference other root objects.
|
|
41
45
|
* @param opaque Root keys held whole, see `IStoreOptions.opaque`.
|
|
42
46
|
*/
|
|
43
|
-
constructor(keyFunc?: (value: any) => string | undefined, opaque?: readonly string[]);
|
|
47
|
+
constructor(keyFunc?: (value: any) => string | undefined, opaque?: readonly string[], watch?: readonly string[], watchCallback?: (rootKey: string, event: EmitterEvent) => void);
|
|
44
48
|
/**
|
|
45
49
|
* Retrieves a value from the store by id.
|
|
46
50
|
* @template O - The type of the value to retrieve
|
|
@@ -86,6 +90,7 @@ export declare abstract class ReadStore {
|
|
|
86
90
|
* @private
|
|
87
91
|
*/
|
|
88
92
|
private UpdateRootObject;
|
|
93
|
+
private CreateRootNode;
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
91
96
|
* Base class for observable data store management.
|
package/Store/Store/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JsonDeepClone, JsonDiff, JsonType } from "../../Utils/json";
|
|
2
2
|
import { ObservableNode } from "../Tree/observableNode";
|
|
3
3
|
import { Opaque } from "../Tree/opaque";
|
|
4
|
+
import { PatternMatches } from "../../Utils/pattern";
|
|
4
5
|
/**
|
|
5
6
|
* Base class for reading observable data: holds root objects as ObservableNode instances
|
|
6
7
|
* and applies diff results to them. Subclasses decide where the diffs come from: `StoreSync`
|
|
@@ -15,10 +16,14 @@ export class ReadStore {
|
|
|
15
16
|
* When provided, enables alias functionality where values can reference other root objects.
|
|
16
17
|
* @param opaque Root keys held whole, see `IStoreOptions.opaque`.
|
|
17
18
|
*/
|
|
18
|
-
constructor(keyFunc, opaque = []) {
|
|
19
|
+
constructor(keyFunc, opaque = [], watch = [], watchCallback) {
|
|
19
20
|
this.keyFunc = keyFunc;
|
|
20
21
|
this.rootMap = new Map();
|
|
21
22
|
this.opaque = new Set(opaque);
|
|
23
|
+
this.watch = watch;
|
|
24
|
+
if (this.watch.length > 0 && !watchCallback)
|
|
25
|
+
throw new Error("watchCallback must be provided when passing watch properties");
|
|
26
|
+
this.watchCallback = watchCallback;
|
|
22
27
|
const aliasFunc = keyFunc &&
|
|
23
28
|
((value, reactive = true) => {
|
|
24
29
|
const key = keyFunc(value);
|
|
@@ -43,7 +48,7 @@ export class ReadStore {
|
|
|
43
48
|
Get(id, defaultValue) {
|
|
44
49
|
let result = this.rootMap.get(id);
|
|
45
50
|
if (result === undefined) {
|
|
46
|
-
result = this.
|
|
51
|
+
result = this.CreateRootNode(id, defaultValue);
|
|
47
52
|
this.rootMap.set(id, result);
|
|
48
53
|
}
|
|
49
54
|
return result[id];
|
|
@@ -109,7 +114,7 @@ export class ReadStore {
|
|
|
109
114
|
if (results[0].type !== "write" || results[0].path.length > 1)
|
|
110
115
|
throw `Unable to initialize root path ${rootPath} with initial path ${results[0].path}`;
|
|
111
116
|
const rootData = JsonDeepClone(results[0].value);
|
|
112
|
-
const newRootObject = this.
|
|
117
|
+
const newRootObject = this.CreateRootNode(rootPath, rootData, false);
|
|
113
118
|
this.rootMap.set(rootPath, newRootObject);
|
|
114
119
|
// Later entries for the same root (e.g. route summaries written one instance at a time)
|
|
115
120
|
// apply to the root just created
|
|
@@ -119,6 +124,15 @@ export class ReadStore {
|
|
|
119
124
|
}
|
|
120
125
|
ObservableNode.ApplyDiff(rootObject, results);
|
|
121
126
|
}
|
|
127
|
+
CreateRootNode(rootPath, data, unwrap) {
|
|
128
|
+
const result = this.createNode({ [rootPath]: this.opaque.has(rootPath) ? Box(data) : data }, unwrap);
|
|
129
|
+
const rootPathStr = rootPath.toString();
|
|
130
|
+
if (this.watch.some(pattern => PatternMatches(pattern, rootPathStr)))
|
|
131
|
+
ObservableNode.WatchProperty(result, rootPath, (event) => {
|
|
132
|
+
this.watchCallback(rootPathStr, event);
|
|
133
|
+
});
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
122
136
|
}
|
|
123
137
|
/**
|
|
124
138
|
* Base class for observable data store management.
|
|
@@ -8,6 +8,16 @@ export interface IStoreAsyncOptions extends IStoreOptions {
|
|
|
8
8
|
* `keyFunc`, include the entity roots the received roots refer to.
|
|
9
9
|
*/
|
|
10
10
|
roots?: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Root keys the store receives only while something reads them: exact keys and patterns ending
|
|
13
|
+
* in `*`. A demand root is subscribed on its own once a reactive scope reads it with `Get` (or
|
|
14
|
+
* `Hold` holds it), and released `demandDebounce` ms after its last reader goes: unsubscribed and
|
|
15
|
+
* deleted from the store. Until its first post arrives it reads as its default. Needs `roots`:
|
|
16
|
+
* a store that receives every root has none left to demand. A key `roots` names is a `roots` root.
|
|
17
|
+
*/
|
|
18
|
+
demand?: readonly string[];
|
|
19
|
+
/** How long a demand root no one reads stays subscribed, in ms; default 1000 */
|
|
20
|
+
demandDebounce?: number;
|
|
11
21
|
}
|
|
12
22
|
/**
|
|
13
23
|
* StoreAsync class extends the base Store class to provide asynchronous data management operations.
|
|
@@ -23,6 +33,11 @@ export interface IStoreAsyncOptions extends IStoreOptions {
|
|
|
23
33
|
* posts, so the change has been applied when it resolves.
|
|
24
34
|
* - A failed batch rejects only its own changes; later batches carry on.
|
|
25
35
|
*
|
|
36
|
+
* **Demand roots** (`IStoreAsyncOptions.demand`) are received while they're read: the first reactive
|
|
37
|
+
* read of one subscribes to it alone, and its first post brings the tree's state of it; a grace
|
|
38
|
+
* period after the last reader goes, it's unsubscribed and deleted from the store. A read outside
|
|
39
|
+
* a scope neither subscribes nor keeps one; `Hold` does, for code that reads it that way.
|
|
40
|
+
*
|
|
26
41
|
* **Over a lease** (`DiffTreeHub.Open`), the store takes its keyFunc from the lease's endpoint, owns
|
|
27
42
|
* the lease, and releases it when it's destroyed. When the hub restarts the worker,
|
|
28
43
|
* the store starts over with the new tree, as when it first subscribed: the tree is the source of
|
|
@@ -76,6 +91,18 @@ export declare class StoreAsync extends Store {
|
|
|
76
91
|
private currentBatchPromise;
|
|
77
92
|
/** Batches not yet acknowledged, for `Flush` */
|
|
78
93
|
private inFlight;
|
|
94
|
+
/** Demand roots a reactive scope reads now, from their watchers */
|
|
95
|
+
private readonly demandRead;
|
|
96
|
+
/** `Hold`s, by demand root */
|
|
97
|
+
private readonly demandHolds;
|
|
98
|
+
/** A subscription for each demand root the store holds */
|
|
99
|
+
private readonly demandSubscriptions;
|
|
100
|
+
/** The release timers of demand roots no one reads, by key */
|
|
101
|
+
private readonly demandReleasing;
|
|
102
|
+
/** Demand roots whose readers or holds changed since the last flush */
|
|
103
|
+
private readonly demandChanged;
|
|
104
|
+
private demandFlushQueued;
|
|
105
|
+
private destroyed;
|
|
79
106
|
/**
|
|
80
107
|
* Creates an instance of StoreAsync and subscribes to the tree.
|
|
81
108
|
* @param channel A channel to an `ObservableDiffTree` served by `HostPublisher`: a `Worker` (e.g. a
|
|
@@ -85,8 +112,8 @@ export declare class StoreAsync extends Store {
|
|
|
85
112
|
* (`DiffTreeHub.OpenLocal`) is for a `StoreSync`, and doesn't compile here.
|
|
86
113
|
* @param options `keyFunc`, `roots` and `opaque` (`IStoreAsyncOptions`). `keyFunc` is for a plain
|
|
87
114
|
* channel: pass the key logic the worker's tree was built with. A lease's is the endpoint's.
|
|
88
|
-
* `opaque` is
|
|
89
|
-
* (`IObservableDiffTreeConfig.opaque`) from a file both import.
|
|
115
|
+
* `opaque` is the store's own: the tree must write those keys whole too, from its config
|
|
116
|
+
* (`IObservableDiffTreeConfig.opaque`), so share them from a file both import.
|
|
90
117
|
* @throws When `keyFunc` is given with a lease, whose endpoint has its own.
|
|
91
118
|
*/
|
|
92
119
|
constructor(channel: IChannel | IWorkerLease, options?: IStoreAsyncOptions);
|
|
@@ -99,6 +126,33 @@ export declare class StoreAsync extends Store {
|
|
|
99
126
|
*/
|
|
100
127
|
private Restarted;
|
|
101
128
|
private Connect;
|
|
129
|
+
/**
|
|
130
|
+
* Holds a demand root without reading it reactively, for code that reads it outside a scope: the
|
|
131
|
+
* store subscribes to it as for a reader, and keeps it until every hold is released. Its first
|
|
132
|
+
* post arrives later; until then it reads as its default.
|
|
133
|
+
* @returns What releases this hold; calling it again does nothing.
|
|
134
|
+
* @throws When the key is neither a demand root nor one of the store's `roots`. A `roots` root is
|
|
135
|
+
* always received: its hold does nothing.
|
|
136
|
+
*/
|
|
137
|
+
Hold(key: string): () => void;
|
|
138
|
+
private IsRootsKey;
|
|
139
|
+
private IsDemandKey;
|
|
140
|
+
/**
|
|
141
|
+
* A demand root's watcher: it gained its first reactive reader or lost its last. This runs inside
|
|
142
|
+
* a scope's update, so it only records the change for the next flush.
|
|
143
|
+
*/
|
|
144
|
+
private DemandWatched;
|
|
145
|
+
private DemandChanged;
|
|
146
|
+
/**
|
|
147
|
+
* Subscribes the demand roots read or held now and not yet subscribed, and starts the release of
|
|
148
|
+
* the ones no one reads or holds. A root read and dropped since the last flush is never subscribed.
|
|
149
|
+
*/
|
|
150
|
+
private FlushDemand;
|
|
151
|
+
private IsDemanded;
|
|
152
|
+
private SubscribeDemand;
|
|
153
|
+
private ReleaseLater;
|
|
154
|
+
/** Unsubscribes a demand root and deletes it from the store, so no later reader sees it stale */
|
|
155
|
+
private ReleaseDemand;
|
|
102
156
|
/** Applies a post. A tree's first state is diffed into the roots in place, its deletes with it. */
|
|
103
157
|
private Apply;
|
|
104
158
|
private Schedule;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AsyncQueue } from "../../Utils/asyncQueue";
|
|
2
|
+
import { EmitterEvent } from "../../Utils/emitter";
|
|
3
|
+
import { PatternMatches } from "../../Utils/pattern";
|
|
4
|
+
import { _queueMicrotask, _setTimeout } from "../../Utils/scheduling";
|
|
2
5
|
import { ChannelClient, ChannelSubscription } from "../Diff/channelClient";
|
|
3
6
|
import { RootOf, Store } from "./store";
|
|
7
|
+
/** How long a demand root no one reads stays subscribed, by default */
|
|
8
|
+
const DEMAND_GRACE = 1000;
|
|
4
9
|
/**
|
|
5
10
|
* StoreAsync class extends the base Store class to provide asynchronous data management operations.
|
|
6
11
|
* This class handles writing, patching, pushing, and splicing data in an asynchronous manner.
|
|
@@ -15,6 +20,11 @@ import { RootOf, Store } from "./store";
|
|
|
15
20
|
* posts, so the change has been applied when it resolves.
|
|
16
21
|
* - A failed batch rejects only its own changes; later batches carry on.
|
|
17
22
|
*
|
|
23
|
+
* **Demand roots** (`IStoreAsyncOptions.demand`) are received while they're read: the first reactive
|
|
24
|
+
* read of one subscribes to it alone, and its first post brings the tree's state of it; a grace
|
|
25
|
+
* period after the last reader goes, it's unsubscribed and deleted from the store. A read outside
|
|
26
|
+
* a scope neither subscribes nor keeps one; `Hold` does, for code that reads it that way.
|
|
27
|
+
*
|
|
18
28
|
* **Over a lease** (`DiffTreeHub.Open`), the store takes its keyFunc from the lease's endpoint, owns
|
|
19
29
|
* the lease, and releases it when it's destroyed. When the hub restarts the worker,
|
|
20
30
|
* the store starts over with the new tree, as when it first subscribed: the tree is the source of
|
|
@@ -61,22 +71,36 @@ export class StoreAsync extends Store {
|
|
|
61
71
|
* (`DiffTreeHub.OpenLocal`) is for a `StoreSync`, and doesn't compile here.
|
|
62
72
|
* @param options `keyFunc`, `roots` and `opaque` (`IStoreAsyncOptions`). `keyFunc` is for a plain
|
|
63
73
|
* channel: pass the key logic the worker's tree was built with. A lease's is the endpoint's.
|
|
64
|
-
* `opaque` is
|
|
65
|
-
* (`IObservableDiffTreeConfig.opaque`) from a file both import.
|
|
74
|
+
* `opaque` is the store's own: the tree must write those keys whole too, from its config
|
|
75
|
+
* (`IObservableDiffTreeConfig.opaque`), so share them from a file both import.
|
|
66
76
|
* @throws When `keyFunc` is given with a lease, whose endpoint has its own.
|
|
67
77
|
*/
|
|
68
78
|
constructor(channel, options = {}) {
|
|
69
79
|
const lease = IsLease(channel) ? channel : undefined;
|
|
70
80
|
if (lease && options.keyFunc)
|
|
71
81
|
throw new Error(`StoreAsync: the endpoint ${lease.name} gives the store its keyFunc; leave it out of the options.`);
|
|
82
|
+
if (options.demand && options.demand.length > 0 && !options.roots)
|
|
83
|
+
throw new Error("StoreAsync: demand roots need roots: a store that receives every root has none left to demand.");
|
|
72
84
|
const keyFunc = lease ? lease.endpoint.keyFunc : options.keyFunc;
|
|
73
|
-
super(keyFunc, options.opaque);
|
|
85
|
+
super(keyFunc, options.opaque, options.demand ?? [], (key, event) => this.DemandWatched(key, event));
|
|
74
86
|
this.writeKeySet = new Set();
|
|
75
87
|
this.asyncQueue = new AsyncQueue();
|
|
76
88
|
this.currentBatch = [];
|
|
77
89
|
this.currentBatchPromise = Promise.resolve();
|
|
78
90
|
/** Batches not yet acknowledged, for `Flush` */
|
|
79
91
|
this.inFlight = new Set();
|
|
92
|
+
/** Demand roots a reactive scope reads now, from their watchers */
|
|
93
|
+
this.demandRead = new Set();
|
|
94
|
+
/** `Hold`s, by demand root */
|
|
95
|
+
this.demandHolds = new Map();
|
|
96
|
+
/** A subscription for each demand root the store holds */
|
|
97
|
+
this.demandSubscriptions = new Map();
|
|
98
|
+
/** The release timers of demand roots no one reads, by key */
|
|
99
|
+
this.demandReleasing = new Map();
|
|
100
|
+
/** Demand roots whose readers or holds changed since the last flush */
|
|
101
|
+
this.demandChanged = new Set();
|
|
102
|
+
this.demandFlushQueued = false;
|
|
103
|
+
this.destroyed = false;
|
|
80
104
|
this.options = options;
|
|
81
105
|
this.lease = lease;
|
|
82
106
|
if (lease)
|
|
@@ -97,11 +121,113 @@ export class StoreAsync extends Store {
|
|
|
97
121
|
this.client.Destroy();
|
|
98
122
|
this.client = this.Connect(port);
|
|
99
123
|
this.subscription.Connect(this.client);
|
|
124
|
+
for (const subscription of this.demandSubscriptions.values())
|
|
125
|
+
subscription.Connect(this.client);
|
|
100
126
|
}
|
|
101
127
|
Connect(channel) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
return new ChannelClient(channel);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Holds a demand root without reading it reactively, for code that reads it outside a scope: the
|
|
132
|
+
* store subscribes to it as for a reader, and keeps it until every hold is released. Its first
|
|
133
|
+
* post arrives later; until then it reads as its default.
|
|
134
|
+
* @returns What releases this hold; calling it again does nothing.
|
|
135
|
+
* @throws When the key is neither a demand root nor one of the store's `roots`. A `roots` root is
|
|
136
|
+
* always received: its hold does nothing.
|
|
137
|
+
*/
|
|
138
|
+
Hold(key) {
|
|
139
|
+
if (this.IsRootsKey(key))
|
|
140
|
+
return () => { };
|
|
141
|
+
if (!this.IsDemandKey(key))
|
|
142
|
+
throw new Error(`StoreAsync: ${key} isn't a demand root or one of the store's roots.`);
|
|
143
|
+
this.demandHolds.set(key, (this.demandHolds.get(key) ?? 0) + 1);
|
|
144
|
+
this.DemandChanged(key);
|
|
145
|
+
let held = true;
|
|
146
|
+
return () => {
|
|
147
|
+
if (!held)
|
|
148
|
+
return;
|
|
149
|
+
held = false;
|
|
150
|
+
const holds = (this.demandHolds.get(key) ?? 1) - 1;
|
|
151
|
+
holds > 0 ? this.demandHolds.set(key, holds) : this.demandHolds.delete(key);
|
|
152
|
+
this.DemandChanged(key);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
IsRootsKey(key) {
|
|
156
|
+
return !this.options.roots || this.options.roots.some(pattern => PatternMatches(pattern, key));
|
|
157
|
+
}
|
|
158
|
+
IsDemandKey(key) {
|
|
159
|
+
return !this.IsRootsKey(key) && (this.options.demand ?? []).some(pattern => PatternMatches(pattern, key));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A demand root's watcher: it gained its first reactive reader or lost its last. This runs inside
|
|
163
|
+
* a scope's update, so it only records the change for the next flush.
|
|
164
|
+
*/
|
|
165
|
+
DemandWatched(key, event) {
|
|
166
|
+
if (this.IsRootsKey(key))
|
|
167
|
+
return;
|
|
168
|
+
event === EmitterEvent.SUBSCRIBED ? this.demandRead.add(key) : this.demandRead.delete(key);
|
|
169
|
+
this.DemandChanged(key);
|
|
170
|
+
}
|
|
171
|
+
DemandChanged(key) {
|
|
172
|
+
this.demandChanged.add(key);
|
|
173
|
+
if (this.demandFlushQueued)
|
|
174
|
+
return;
|
|
175
|
+
this.demandFlushQueued = true;
|
|
176
|
+
_queueMicrotask(() => this.FlushDemand());
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Subscribes the demand roots read or held now and not yet subscribed, and starts the release of
|
|
180
|
+
* the ones no one reads or holds. A root read and dropped since the last flush is never subscribed.
|
|
181
|
+
*/
|
|
182
|
+
FlushDemand() {
|
|
183
|
+
this.demandFlushQueued = false;
|
|
184
|
+
if (this.destroyed)
|
|
185
|
+
return;
|
|
186
|
+
const keys = Array.from(this.demandChanged);
|
|
187
|
+
this.demandChanged.clear();
|
|
188
|
+
for (const key of keys) {
|
|
189
|
+
if (this.IsDemanded(key)) {
|
|
190
|
+
const timer = this.demandReleasing.get(key);
|
|
191
|
+
if (timer !== undefined) {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
this.demandReleasing.delete(key);
|
|
194
|
+
}
|
|
195
|
+
!this.demandSubscriptions.has(key) && this.SubscribeDemand(key);
|
|
196
|
+
}
|
|
197
|
+
else if (this.demandSubscriptions.has(key) && !this.demandReleasing.has(key))
|
|
198
|
+
this.ReleaseLater(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
IsDemanded(key) {
|
|
202
|
+
return this.demandRead.has(key) || this.demandHolds.has(key);
|
|
203
|
+
}
|
|
204
|
+
SubscribeDemand(key) {
|
|
205
|
+
// A post for a key released since is dropped: an unsubscribe and a commit can cross on the channel
|
|
206
|
+
const subscription = new ChannelSubscription({ roots: [key] }, (post, first) => this.demandSubscriptions.get(key) === subscription && this.Apply(post, first));
|
|
207
|
+
this.demandSubscriptions.set(key, subscription);
|
|
208
|
+
subscription.Connect(this.client);
|
|
209
|
+
}
|
|
210
|
+
ReleaseLater(key) {
|
|
211
|
+
const grace = this.options.demandDebounce ?? DEMAND_GRACE;
|
|
212
|
+
if (grace <= 0)
|
|
213
|
+
return this.ReleaseDemand(key);
|
|
214
|
+
let fired = false;
|
|
215
|
+
const timer = _setTimeout(() => {
|
|
216
|
+
fired = true;
|
|
217
|
+
this.demandReleasing.delete(key);
|
|
218
|
+
!this.destroyed && !this.IsDemanded(key) && this.ReleaseDemand(key);
|
|
219
|
+
}, grace);
|
|
220
|
+
// A timer that ran at once (synchronous scheduling) has nothing left to cancel
|
|
221
|
+
!fired && this.demandReleasing.set(key, timer);
|
|
222
|
+
}
|
|
223
|
+
/** Unsubscribes a demand root and deletes it from the store, so no later reader sees it stale */
|
|
224
|
+
ReleaseDemand(key) {
|
|
225
|
+
const subscription = this.demandSubscriptions.get(key);
|
|
226
|
+
if (!subscription)
|
|
227
|
+
return;
|
|
228
|
+
this.demandSubscriptions.delete(key);
|
|
229
|
+
subscription.Stop();
|
|
230
|
+
this.Has(key) && this.UpdateRootMap([{ type: "delete", path: [key] }]);
|
|
105
231
|
}
|
|
106
232
|
/** Applies a post. A tree's first state is diffed into the roots in place, its deletes with it. */
|
|
107
233
|
Apply(post, firstState) {
|
|
@@ -221,9 +347,16 @@ export class StoreAsync extends Store {
|
|
|
221
347
|
* channel stays open: whoever created the worker terminates it.
|
|
222
348
|
*/
|
|
223
349
|
Destroy() {
|
|
350
|
+
this.destroyed = true;
|
|
224
351
|
this.stopLease?.();
|
|
225
352
|
this.asyncQueue.Stop();
|
|
226
353
|
this.subscription.Stop();
|
|
354
|
+
for (const subscription of this.demandSubscriptions.values())
|
|
355
|
+
subscription.Stop();
|
|
356
|
+
this.demandSubscriptions.clear();
|
|
357
|
+
for (const timer of this.demandReleasing.values())
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
this.demandReleasing.clear();
|
|
227
360
|
this.client.Destroy();
|
|
228
361
|
this.lease?.Release();
|
|
229
362
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EmitterEvent } from "../../Utils/emitter";
|
|
1
2
|
import type { ILocalLease } from "../Diff/diffTree.types";
|
|
2
3
|
import { ObservableDiffTree } from "../Diff/observableDiffTree";
|
|
3
4
|
import { IStoreOptions, Store } from "./store";
|
|
@@ -8,6 +9,18 @@ export interface IStoreSyncOptions extends IStoreOptions {
|
|
|
8
9
|
* root. With a `keyFunc`, include the entity roots the held roots refer to.
|
|
9
10
|
*/
|
|
10
11
|
roots?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Root keys whose readers the store reports to `watchCallback`: exact keys and patterns ending in
|
|
14
|
+
* `*`. A watched root reports when a reactive scope first reads it with `Get`, and when its last
|
|
15
|
+
* reader stops; a read outside a scope reports nothing.
|
|
16
|
+
*/
|
|
17
|
+
watch?: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* Told when a watched root gains its first reader or loses its last, with the root's key. It runs
|
|
20
|
+
* inside the scope update that caused it: record the change and act on it later, without reading
|
|
21
|
+
* or writing the store. Needed with `watch`.
|
|
22
|
+
*/
|
|
23
|
+
watchCallback?: (rootKey: string, event: EmitterEvent) => void;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
26
|
* StoreSync class extends the base Store class to provide synchronous data management operations.
|
|
@@ -81,11 +94,13 @@ export declare class StoreSync extends Store {
|
|
|
81
94
|
* A projection in the root route (`"/"`) is readable through `Get` at `$projection_<key>`. The
|
|
82
95
|
* store doesn't own it: `Destroy` stops listening.
|
|
83
96
|
* - Left out, the store creates an empty tree of its own, with the store's `keyFunc` and `opaque`.
|
|
84
|
-
* @param options `keyFunc`, `roots` and `
|
|
97
|
+
* @param options `keyFunc`, `roots`, `opaque`, `watch` and `watchCallback` (`IStoreSyncOptions`).
|
|
98
|
+
* `keyFunc` is for the tree the
|
|
85
99
|
* store creates: a leased tree's is the endpoint's, and a given tree's its own. A tree given or
|
|
86
100
|
* leased must write the `opaque` keys whole already (`IObservableDiffTreeConfig.opaque`): share
|
|
87
101
|
* them from one file.
|
|
88
|
-
* @throws When `keyFunc` is given with a tree or a lease, which has its own
|
|
102
|
+
* @throws When `keyFunc` is given with a tree or a lease, which has its own; or `watch` without
|
|
103
|
+
* `watchCallback`.
|
|
89
104
|
*/
|
|
90
105
|
constructor(tree?: ObservableDiffTree | ILocalLease, options?: IStoreSyncOptions);
|
|
91
106
|
/**
|
package/Store/Store/storeSync.js
CHANGED
|
@@ -60,11 +60,13 @@ export class StoreSync extends Store {
|
|
|
60
60
|
* A projection in the root route (`"/"`) is readable through `Get` at `$projection_<key>`. The
|
|
61
61
|
* store doesn't own it: `Destroy` stops listening.
|
|
62
62
|
* - Left out, the store creates an empty tree of its own, with the store's `keyFunc` and `opaque`.
|
|
63
|
-
* @param options `keyFunc`, `roots` and `
|
|
63
|
+
* @param options `keyFunc`, `roots`, `opaque`, `watch` and `watchCallback` (`IStoreSyncOptions`).
|
|
64
|
+
* `keyFunc` is for the tree the
|
|
64
65
|
* store creates: a leased tree's is the endpoint's, and a given tree's its own. A tree given or
|
|
65
66
|
* leased must write the `opaque` keys whole already (`IObservableDiffTreeConfig.opaque`): share
|
|
66
67
|
* them from one file.
|
|
67
|
-
* @throws When `keyFunc` is given with a tree or a lease, which has its own
|
|
68
|
+
* @throws When `keyFunc` is given with a tree or a lease, which has its own; or `watch` without
|
|
69
|
+
* `watchCallback`.
|
|
68
70
|
*/
|
|
69
71
|
constructor(tree, options = {}) {
|
|
70
72
|
const lease = tree && !(tree instanceof ObservableDiffTree) ? tree : undefined;
|
|
@@ -72,7 +74,7 @@ export class StoreSync extends Store {
|
|
|
72
74
|
throw new Error(lease
|
|
73
75
|
? `StoreSync: the endpoint ${lease.name} gives the store its keyFunc; leave it out of the options.`
|
|
74
76
|
: "StoreSync: the tree has its keyFunc, which the store takes; leave it out of the options.");
|
|
75
|
-
super(lease ? lease.endpoint.keyFunc : tree ? tree.KeyFunc : options.keyFunc, options.opaque);
|
|
77
|
+
super(lease ? lease.endpoint.keyFunc : tree ? tree.KeyFunc : options.keyFunc, options.opaque, options.watch, options.watchCallback);
|
|
76
78
|
/** Over a lease: the roots delivered and not deleted, to delete the ones a restarted tree doesn't hold */
|
|
77
79
|
this.delivered = new Set();
|
|
78
80
|
this.roots = options.roots;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EmitterEventCallback } from "../../Utils/emitter";
|
|
1
2
|
import { JsonDiffResult } from "../../Utils/json";
|
|
2
3
|
export declare namespace ObservableNode {
|
|
3
4
|
/**
|
|
@@ -39,6 +40,8 @@ export declare namespace ObservableNode {
|
|
|
39
40
|
* @returns A proxied version of the value that emits change events.
|
|
40
41
|
*/
|
|
41
42
|
function Create<T>(value: T, unwrapValue?: boolean): T;
|
|
43
|
+
function WatchProperty<T>(proxy: T, prop: keyof T, callback: EmitterEventCallback): void;
|
|
44
|
+
function UnwatchProperty<T>(proxy: T, prop: keyof T, callback: EmitterEventCallback): void;
|
|
42
45
|
/**
|
|
43
46
|
* Returns an immutable, cached snapshot of an observable node's current value (plain
|
|
44
47
|
* objects/arrays, with keyed children read through their root). The same snapshot is
|
|
@@ -416,6 +416,25 @@ export var ObservableNode;
|
|
|
416
416
|
return DefaultCreateProxy(value, unwrapValue);
|
|
417
417
|
}
|
|
418
418
|
ObservableNode.Create = Create;
|
|
419
|
+
function WatchProperty(proxy, prop, callback) {
|
|
420
|
+
if (!proxy[IS_NODE])
|
|
421
|
+
throw new Error("Parameter proxy is not an ObservableNode");
|
|
422
|
+
const wrapper = proxy[NODE_WRAPPER];
|
|
423
|
+
if (Array.isArray(wrapper))
|
|
424
|
+
throw new Error("Unable to watch individual property on array proxy");
|
|
425
|
+
const scope = GetPropertyScope(wrapper, prop);
|
|
426
|
+
ObservableScope.WatchSubscribers(scope, callback);
|
|
427
|
+
}
|
|
428
|
+
ObservableNode.WatchProperty = WatchProperty;
|
|
429
|
+
function UnwatchProperty(proxy, prop, callback) {
|
|
430
|
+
if (!proxy[IS_NODE])
|
|
431
|
+
throw new Error("Parameter proxy is not an ObservableNode");
|
|
432
|
+
const wrapper = proxy[NODE_WRAPPER];
|
|
433
|
+
if (Array.isArray(wrapper))
|
|
434
|
+
return;
|
|
435
|
+
Object.hasOwn(wrapper, prop) && ObservableScope.UnwatchSubscribers(wrapper[prop], callback);
|
|
436
|
+
}
|
|
437
|
+
ObservableNode.UnwatchProperty = UnwatchProperty;
|
|
419
438
|
/**
|
|
420
439
|
* Returns an immutable, cached snapshot of an observable node's current value (plain
|
|
421
440
|
* objects/arrays, with keyed children read through their root). The same snapshot is
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Emitter, EmitterCallback } from "../../Utils/emitter";
|
|
1
|
+
import { Emitter, EmitterCallback, EmitterEventCallback } from "../../Utils/emitter";
|
|
2
2
|
/**
|
|
3
3
|
* Represents a static (non-reactive) observable scope.
|
|
4
4
|
* Static scopes are immutable and do not track dependencies or emit updates.
|
|
@@ -189,6 +189,8 @@ export declare namespace ObservableScope {
|
|
|
189
189
|
* @param callback The callback function to remove.
|
|
190
190
|
*/
|
|
191
191
|
function Unwatch<T>(scope: IObservableScope<T> | IBasicObservableScope<T>, callback: (scope: IObservableScope<T>) => void): void;
|
|
192
|
+
function WatchSubscribers<T = any>(scope: IObservableScope<T> | IBasicObservableScope<T>, callback: EmitterEventCallback): void;
|
|
193
|
+
function UnwatchSubscribers<T = any>(scope: IObservableScope<T> | IBasicObservableScope<T>, callback: EmitterEventCallback): void;
|
|
192
194
|
/**
|
|
193
195
|
* Registers a callback to be invoked when the scope's value is updated.
|
|
194
196
|
* Only applies to dynamic scopes; no-op for static scopes.
|
|
@@ -639,6 +639,18 @@ export var ObservableScope;
|
|
|
639
639
|
Emitter.Remove(scope.emitter, callback);
|
|
640
640
|
}
|
|
641
641
|
ObservableScope.Unwatch = Unwatch;
|
|
642
|
+
function WatchSubscribers(scope, callback) {
|
|
643
|
+
if (!scope || scope.destroyed || scope.type === "static")
|
|
644
|
+
return;
|
|
645
|
+
Emitter.Watch(scope.emitter, callback);
|
|
646
|
+
}
|
|
647
|
+
ObservableScope.WatchSubscribers = WatchSubscribers;
|
|
648
|
+
function UnwatchSubscribers(scope, callback) {
|
|
649
|
+
if (!scope || scope.destroyed || scope.type === "static")
|
|
650
|
+
return;
|
|
651
|
+
Emitter.Unwatch(scope.emitter, callback);
|
|
652
|
+
}
|
|
653
|
+
ObservableScope.UnwatchSubscribers = UnwatchSubscribers;
|
|
642
654
|
/**
|
|
643
655
|
* Registers a callback to be invoked when the scope's value is updated.
|
|
644
656
|
* Only applies to dynamic scopes; no-op for static scopes.
|
package/Utils/emitter.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
export declare enum EmitterEvent {
|
|
2
|
+
SUBSCRIBED = 0,
|
|
3
|
+
UNSUBSCRIBED = 1
|
|
4
|
+
}
|
|
5
|
+
export type EmitterEventCallback = (event: EmitterEvent, emitter: Emitter) => void;
|
|
1
6
|
export type EmitterCallback<T extends readonly any[] = any[]> = (...args: T) => void;
|
|
2
|
-
export type Emitter = [[number, number], ...EmitterCallback[]];
|
|
7
|
+
export type Emitter<T extends readonly any[] = any[]> = [[number, number, Emitter<[EmitterEvent, Emitter]> | undefined], ...EmitterCallback<T>[]];
|
|
3
8
|
export declare namespace Emitter {
|
|
4
|
-
function Create(): Emitter
|
|
5
|
-
function On(emitter: Emitter
|
|
6
|
-
function
|
|
9
|
+
function Create<T extends any[] = any[]>(): Emitter<T>;
|
|
10
|
+
function On<T extends any[]>(emitter: Emitter<T>, callback: EmitterCallback<T>): void;
|
|
11
|
+
function Watch(emitter: Emitter, callback: EmitterCallback<[EmitterEvent, Emitter]>): void;
|
|
12
|
+
function Unwatch(emitter: Emitter, callback: EmitterCallback<[EmitterEvent, Emitter]>): void;
|
|
13
|
+
function Emit<T extends any[]>(emitter: Emitter<T>, ...args: T): void;
|
|
7
14
|
function Remove(emitter: Emitter, callback: EmitterCallback): void;
|
|
8
15
|
function Clear(emitter: Emitter): void;
|
|
9
16
|
}
|
package/Utils/emitter.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { _requestIdleCallback } from "./scheduling";
|
|
2
|
+
export var EmitterEvent;
|
|
3
|
+
(function (EmitterEvent) {
|
|
4
|
+
EmitterEvent[EmitterEvent["SUBSCRIBED"] = 0] = "SUBSCRIBED";
|
|
5
|
+
EmitterEvent[EmitterEvent["UNSUBSCRIBED"] = 1] = "UNSUBSCRIBED";
|
|
6
|
+
})(EmitterEvent || (EmitterEvent = {}));
|
|
2
7
|
const pendingCompactEmitters = new Set();
|
|
3
8
|
function Compact(emitter) {
|
|
4
9
|
if (emitter.length === 1 || emitter[0][0] === 0)
|
|
@@ -29,13 +34,26 @@ function CompactEmitter(emitter) {
|
|
|
29
34
|
export var Emitter;
|
|
30
35
|
(function (Emitter) {
|
|
31
36
|
function Create() {
|
|
32
|
-
return [[0, -1]];
|
|
37
|
+
return [[0, -1, undefined]];
|
|
33
38
|
}
|
|
34
39
|
Emitter.Create = Create;
|
|
35
40
|
function On(emitter, callback) {
|
|
36
41
|
emitter.push(callback);
|
|
42
|
+
if (emitter.length - emitter[0][0] === 2)
|
|
43
|
+
emitter[0][2] && Emitter.Emit(emitter[0][2], EmitterEvent.SUBSCRIBED, emitter);
|
|
37
44
|
}
|
|
38
45
|
Emitter.On = On;
|
|
46
|
+
function Watch(emitter, callback) {
|
|
47
|
+
emitter[0][2] ??= Emitter.Create();
|
|
48
|
+
On(emitter[0][2], callback);
|
|
49
|
+
}
|
|
50
|
+
Emitter.Watch = Watch;
|
|
51
|
+
function Unwatch(emitter, callback) {
|
|
52
|
+
if (!emitter[0][2])
|
|
53
|
+
return;
|
|
54
|
+
Remove(emitter[0][2], callback);
|
|
55
|
+
}
|
|
56
|
+
Emitter.Unwatch = Unwatch;
|
|
39
57
|
// Re-entry during emit is not supported and considered a bug in client code
|
|
40
58
|
function Emit(emitter, ...args) {
|
|
41
59
|
if (emitter[0][1] !== -1)
|
|
@@ -73,11 +91,16 @@ export var Emitter;
|
|
|
73
91
|
RemoveFromBeginning(emitter, index);
|
|
74
92
|
else
|
|
75
93
|
RemoveFromEnd(emitter, index);
|
|
94
|
+
if (emitter.length - emitter[0][0] === 1)
|
|
95
|
+
emitter[0][2] && Emitter.Emit(emitter[0][2], EmitterEvent.UNSUBSCRIBED, emitter);
|
|
76
96
|
}
|
|
77
97
|
Emitter.Remove = Remove;
|
|
78
98
|
function Clear(emitter) {
|
|
79
|
-
emitter.length
|
|
99
|
+
if (emitter.length - emitter[0][0] === 1)
|
|
100
|
+
return;
|
|
101
|
+
emitter.splice(1);
|
|
80
102
|
emitter[0][0] = 0;
|
|
103
|
+
emitter[0][2] && Emitter.Emit(emitter[0][2], EmitterEvent.UNSUBSCRIBED, emitter);
|
|
81
104
|
}
|
|
82
105
|
Emitter.Clear = Clear;
|
|
83
106
|
})(Emitter || (Emitter = {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key patterns: an exact key, or a pattern ending in `*` that matches every key starting with the
|
|
3
|
+
* text before it (`task_*` matches `task_1`, and `task_` itself). A lone `*` matches every key.
|
|
4
|
+
* Store roots, subscriptions' key filters and route segments all match this way. The text is
|
|
5
|
+
* compared as it is: no character but a final `*` is special.
|
|
6
|
+
*/
|
|
7
|
+
/** A pattern's wildcard, its last character */
|
|
8
|
+
export declare const WILDCARD = "*";
|
|
9
|
+
/** Whether `pattern` is a pattern rather than an exact key: it ends in `*` */
|
|
10
|
+
export declare function IsPattern(pattern: string): boolean;
|
|
11
|
+
/** The text a pattern's keys start with: the pattern without its `*` */
|
|
12
|
+
export declare function PatternPrefix(pattern: string): string;
|
|
13
|
+
/** Whether `key` matches `pattern`: it is the exact key, or starts with the pattern's prefix */
|
|
14
|
+
export declare function PatternMatches(pattern: string, key: string): boolean;
|
package/Utils/pattern.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key patterns: an exact key, or a pattern ending in `*` that matches every key starting with the
|
|
3
|
+
* text before it (`task_*` matches `task_1`, and `task_` itself). A lone `*` matches every key.
|
|
4
|
+
* Store roots, subscriptions' key filters and route segments all match this way. The text is
|
|
5
|
+
* compared as it is: no character but a final `*` is special.
|
|
6
|
+
*/
|
|
7
|
+
/** A pattern's wildcard, its last character */
|
|
8
|
+
export const WILDCARD = "*";
|
|
9
|
+
/** Whether `pattern` is a pattern rather than an exact key: it ends in `*` */
|
|
10
|
+
export function IsPattern(pattern) {
|
|
11
|
+
return pattern.endsWith(WILDCARD);
|
|
12
|
+
}
|
|
13
|
+
/** The text a pattern's keys start with: the pattern without its `*` */
|
|
14
|
+
export function PatternPrefix(pattern) {
|
|
15
|
+
return pattern.slice(0, -WILDCARD.length);
|
|
16
|
+
}
|
|
17
|
+
/** Whether `key` matches `pattern`: it is the exact key, or starts with the pattern's prefix */
|
|
18
|
+
export function PatternMatches(pattern, key) {
|
|
19
|
+
return IsPattern(pattern) ? key.startsWith(PatternPrefix(pattern)) : pattern === key;
|
|
20
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { IsPattern, PatternMatches, PatternPrefix } from "./pattern";
|
|
2
2
|
/**
|
|
3
3
|
* Subscriptions to keys, for any use: it matches keys to subscriptions and counts who holds a
|
|
4
4
|
* key, and knows nothing about what a key's value is or how it's delivered.
|
|
@@ -34,8 +34,8 @@ export class SubscriptionManager {
|
|
|
34
34
|
if (keys)
|
|
35
35
|
for (let x = 0; x < keys.length; x++) {
|
|
36
36
|
const key = keys[x];
|
|
37
|
-
if (key
|
|
38
|
-
entry.prefixes.push(key
|
|
37
|
+
if (IsPattern(key))
|
|
38
|
+
entry.prefixes.push(PatternPrefix(key));
|
|
39
39
|
else
|
|
40
40
|
entry.exact.add(key);
|
|
41
41
|
}
|
|
@@ -153,9 +153,6 @@ function Matches(entry, key) {
|
|
|
153
153
|
return true;
|
|
154
154
|
return false;
|
|
155
155
|
}
|
|
156
|
-
function PatternMatches(pattern, key) {
|
|
157
|
-
return pattern.endsWith(WILDCARD) ? key.startsWith(pattern.slice(0, -1)) : pattern === key;
|
|
158
|
-
}
|
|
159
156
|
function Remove(list, item) {
|
|
160
157
|
const index = list.indexOf(item);
|
|
161
158
|
index >= 0 && list.splice(index, 1);
|