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.
- 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/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/Utils/decorators.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* @module Utils/Decorators
|
|
6
6
|
* @see ObservableScope
|
|
7
7
|
* @see ObservableNode
|
|
8
|
-
* @see StoreAsync
|
|
9
8
|
* @see StoreSync
|
|
10
9
|
* @see Component
|
|
11
10
|
*
|
|
@@ -20,7 +19,6 @@
|
|
|
20
19
|
* | arrays (User[], Todo[]) | @State | Array mutations tracked |
|
|
21
20
|
* | getters only (expensive) | @Computed | Cached + object reuse via Store |
|
|
22
21
|
* | getters only (simple) | @Scope | Cached, but new reference on update |
|
|
23
|
-
* | getters only (sync, StoreAsync) | @ComputedAsync | Sync getter, StoreAsync caching + object reuse |
|
|
24
22
|
* | async functions (direct scope) | ObservableScope.Create(async) | Direct async, new reference, initial null |
|
|
25
23
|
* | subscribe to changes | @Watch | Calls method when scope value changes |
|
|
26
24
|
* | dependency injection | @Inject | Gets value from component injector |
|
|
@@ -58,7 +56,6 @@
|
|
|
58
56
|
*/
|
|
59
57
|
import { ObservableScope, PeekCallback, } from "../Store/Tree/observableScope";
|
|
60
58
|
import { ObservableNode } from "../Store/Tree/observableNode";
|
|
61
|
-
import { StoreAsync } from "../Store";
|
|
62
59
|
/**
|
|
63
60
|
* WeakMap for storing node instances and their associated properties.
|
|
64
61
|
* @private
|
|
@@ -122,27 +119,6 @@ function GetDestroyArrayForPrototype(prototype, create = true) {
|
|
|
122
119
|
}
|
|
123
120
|
return array;
|
|
124
121
|
}
|
|
125
|
-
function CreateStoreScope(getter, store, defaultValue) {
|
|
126
|
-
const getterScope = ObservableScope.Gated(function () {
|
|
127
|
-
const value = getter();
|
|
128
|
-
return ObservableNode.Clone(value);
|
|
129
|
-
});
|
|
130
|
-
ObservableScope.Watch(getterScope, (scope) => {
|
|
131
|
-
const data = ObservableScope.Peek(scope);
|
|
132
|
-
store.Write(data, "root");
|
|
133
|
-
});
|
|
134
|
-
const data = ObservableScope.Peek(getterScope);
|
|
135
|
-
store.Write(data, "root");
|
|
136
|
-
const propertyScope = ObservableScope.Create(() => {
|
|
137
|
-
return store.Get("root", defaultValue);
|
|
138
|
-
});
|
|
139
|
-
ObservableScope.OnDestroyed(propertyScope, function () {
|
|
140
|
-
ObservableScope.Destroy(getterScope);
|
|
141
|
-
if (store instanceof StoreAsync)
|
|
142
|
-
store.Destroy();
|
|
143
|
-
});
|
|
144
|
-
return propertyScope;
|
|
145
|
-
}
|
|
146
122
|
/**
|
|
147
123
|
* Creates an observable scope that exposes a getter's value through a persistent
|
|
148
124
|
* observable node, preserving the value's object identity across updates.
|
|
@@ -281,10 +257,8 @@ function CreateNodeScope(getter) {
|
|
|
281
257
|
* - Use @Scope() for simple computations where object identity doesn't matter
|
|
282
258
|
* - The diff computation overhead is justified when you need object reuse
|
|
283
259
|
* @see {@link Scope} for simple getter-based reactive properties (caches but new reference)
|
|
284
|
-
* @see {@link ComputedAsync} for async computed properties
|
|
285
260
|
* @see {@link ObservableNode.Apply} for how diffs are applied in-place to maintain object identity
|
|
286
261
|
* @see {@link ObservableScope} for the Gated scope mechanism (batched updates)
|
|
287
|
-
* @see {@link StoreSync} for the store-backed variant used by @ComputedAsync
|
|
288
262
|
*/
|
|
289
263
|
export function Computed() {
|
|
290
264
|
return function (target, propertyKey, descriptor) {
|
|
@@ -320,134 +294,6 @@ function ComputedDecorator(target, prop, descriptor) {
|
|
|
320
294
|
},
|
|
321
295
|
};
|
|
322
296
|
}
|
|
323
|
-
/**
|
|
324
|
-
* ComputedAsync decorator factory for creating synchronous computed properties with StoreAsync caching.
|
|
325
|
-
* A computed property is derived from other properties and automatically updates when its dependencies change.
|
|
326
|
-
*
|
|
327
|
-
* IMPORTANT: Despite the name, @ComputedAsync expects a SYNC getter, NOT an async function.
|
|
328
|
-
* The "Async" refers to the internal StoreAsync caching mechanism, not the getter signature.
|
|
329
|
-
*
|
|
330
|
-
* Use @ComputedAsync when you need synchronous caching with StoreAsync backend.
|
|
331
|
-
* For direct async operations (no object reuse), use ObservableScope.Create(async () => ...) instead.
|
|
332
|
-
*
|
|
333
|
-
* @example
|
|
334
|
-
* ```typescript
|
|
335
|
-
* // ✅ Good: Synchronous getter with StoreAsync caching
|
|
336
|
-
* @Value()
|
|
337
|
-
* userId: string = "123";
|
|
338
|
-
*
|
|
339
|
-
* @ComputedAsync(null)
|
|
340
|
-
* get userData(): User | null {
|
|
341
|
-
* // Sync getter - returns value directly
|
|
342
|
-
* return getUserSync(this.userId);
|
|
343
|
-
* }
|
|
344
|
-
* ```
|
|
345
|
-
*
|
|
346
|
-
* @example
|
|
347
|
-
* ```typescript
|
|
348
|
-
* // ❌ WRONG - Do NOT use async keyword or return Promise
|
|
349
|
-
* @Value()
|
|
350
|
-
* userId: string = "123";
|
|
351
|
-
*
|
|
352
|
-
* @ComputedAsync(null)
|
|
353
|
-
* async getUser(): Promise<User> { // ERROR: getter must be synchronous!
|
|
354
|
-
* return await fetch(`/api/users/${this.userId}`);
|
|
355
|
-
* }
|
|
356
|
-
*
|
|
357
|
-
* // ✅ CORRECT - Use ObservableScope.Create for direct async operations
|
|
358
|
-
* import { ObservableScope } from "j-templates/Store";
|
|
359
|
-
*
|
|
360
|
-
* @Value()
|
|
361
|
-
* userId: string = "123";
|
|
362
|
-
*
|
|
363
|
-
* private userDataScope = ObservableScope.Create(async () => {
|
|
364
|
-
* return await fetch(`/api/users/${this.userId}`); // Async supported here!
|
|
365
|
-
* });
|
|
366
|
-
*
|
|
367
|
-
* get userData(): User | null {
|
|
368
|
-
* return ObservableScope.Value(this.userDataScope); // null while loading
|
|
369
|
-
* }
|
|
370
|
-
* ```
|
|
371
|
-
*
|
|
372
|
-
* @param defaultValue The default value to be used if the computed property is not defined.
|
|
373
|
-
* @returns A property decorator that can be applied to a getter method.
|
|
374
|
-
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
375
|
-
* @remarks
|
|
376
|
-
* The @ComputedAsync decorator uses StoreAsync for caching. The getter must be a synchronous function.
|
|
377
|
-
* Unlike @Computed which uses StoreSync, @ComputedAsync uses StoreAsync internally for caching.
|
|
378
|
-
*
|
|
379
|
-
* **Initialization**: @ComputedAsync uses lazy initialization - the scopes are created on first access:
|
|
380
|
-
* ```typescript
|
|
381
|
-
* @ComputedAsync(null)
|
|
382
|
-
* get userData(): User | null {
|
|
383
|
-
* return getUserSync(this.userId);
|
|
384
|
-
* }
|
|
385
|
-
*
|
|
386
|
-
* // Scope created here, on first access:
|
|
387
|
-
* const user = this.userData;
|
|
388
|
-
* ```
|
|
389
|
-
*
|
|
390
|
-
* **Caching**: Like @Computed, @ComputedAsync caches the result and maintains object identity
|
|
391
|
-
* through diff-based updates. The getter only runs when dependencies change.
|
|
392
|
-
*
|
|
393
|
-
* **Object Reuse**: Like @Computed, the result maintains its identity across updates.
|
|
394
|
-
* Only changed properties are modified in-place, preserving object references.
|
|
395
|
-
*
|
|
396
|
-
* **Key difference from @Computed**: Uses StoreAsync instead of StoreSync for caching.
|
|
397
|
-
* Both use synchronous getters and provide object identity preservation.
|
|
398
|
-
*
|
|
399
|
-
* **Comparison**:
|
|
400
|
-
* | Aspect | @Scope | @Computed | @ComputedAsync | scope(async) + @Scope |
|
|
401
|
-
* |--------|--------|-----------|----------------|----------------------|
|
|
402
|
-
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (memoized) |
|
|
403
|
-
* | Getter type | Sync | Sync | Sync | Async supported |
|
|
404
|
-
* | Object identity | ❌ New | ✅ Same | ✅ Same | ❌ New |
|
|
405
|
-
* | Store backend | Single scope | StoreSync | StoreAsync | Direct scope |
|
|
406
|
-
* | Initial value | First access | First access | defaultValue | Promise |
|
|
407
|
-
* | Best for | Simple sync, async | Complex sync | StoreAsync sync | Component async ops |
|
|
408
|
-
*
|
|
409
|
-
* @see {@link Computed} for synchronous computed properties with StoreSync caching
|
|
410
|
-
* @see {@link Scope} for simple getter-based reactive properties (caches, new reference)
|
|
411
|
-
* @see {@link StoreAsync} for async store implementation
|
|
412
|
-
* @see {@link ObservableScope.Create} for async function support
|
|
413
|
-
*/
|
|
414
|
-
export function ComputedAsync(defaultValue) {
|
|
415
|
-
return function (target, propertyKey, descriptor) {
|
|
416
|
-
return ComputedAsyncDecorator(target, propertyKey, descriptor, defaultValue);
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* ComputedAsync decorator implementation for creating asynchronous computed properties.
|
|
421
|
-
* @private
|
|
422
|
-
* @param target The target object.
|
|
423
|
-
* @param prop The property key.
|
|
424
|
-
* @param descriptor The property descriptor.
|
|
425
|
-
* @param defaultValue The default value to be used if the computed property is not defined.
|
|
426
|
-
* @returns A property descriptor that replaces the original descriptor with a computed implementation.
|
|
427
|
-
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
428
|
-
*/
|
|
429
|
-
function ComputedAsyncDecorator(target, prop, descriptor, defaultValue) {
|
|
430
|
-
const propertyKey = prop;
|
|
431
|
-
if (!(descriptor && descriptor.get))
|
|
432
|
-
throw "Computed decorator requires a getter";
|
|
433
|
-
if (descriptor && descriptor.set)
|
|
434
|
-
throw "Computed decorator does not support setters";
|
|
435
|
-
const getter = descriptor.get;
|
|
436
|
-
return {
|
|
437
|
-
configurable: false,
|
|
438
|
-
enumerable: true,
|
|
439
|
-
get: function () {
|
|
440
|
-
const scopeMap = GetScopeMapForInstance(this);
|
|
441
|
-
if (scopeMap[propertyKey] === undefined) {
|
|
442
|
-
const worker = new Worker(new URL("../Store/Diff/defaultDiffTreeWorker.js", import.meta.url), { type: "module" });
|
|
443
|
-
const storeAsync = new StoreAsync(worker);
|
|
444
|
-
const propertyScope = CreateStoreScope(getter.bind(this), storeAsync, defaultValue);
|
|
445
|
-
scopeMap[propertyKey] = [propertyScope, undefined];
|
|
446
|
-
}
|
|
447
|
-
return ObservableScope.Value(scopeMap[propertyKey][0]);
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
297
|
/**
|
|
452
298
|
* State decorator factory for creating reactive properties backed by ObservableNode proxies.
|
|
453
299
|
*
|
|
@@ -749,7 +595,6 @@ function ValueDecorator(target, propertyKey) {
|
|
|
749
595
|
* | Best for | Primitives, cheap ops, async ops | Complex objects, expensive ops |
|
|
750
596
|
*
|
|
751
597
|
* @see {@link Computed} for cached computed properties with object reuse
|
|
752
|
-
* @see {@link ComputedAsync} for sync getters with StoreAsync backend
|
|
753
598
|
* @see {@link ObservableNode.Apply} for how @Computed maintains object identity
|
|
754
599
|
* @see {@link ObservableScope} for the scope-based reactivity system
|
|
755
600
|
* @see {@link scope} for inline computed scopes within @Scope
|
|
@@ -1118,7 +963,6 @@ export var Bound;
|
|
|
1118
963
|
* - All @Value-decorated property scopes
|
|
1119
964
|
* - All @Scope-decorated property scopes
|
|
1120
965
|
* - All @Computed-decorated property scopes (both getter and property scopes)
|
|
1121
|
-
* - All @ComputedAsync-decorated property scopes (including StoreAsync)
|
|
1122
966
|
* - All @Destroy-marked properties (calls .Destroy() method)
|
|
1123
967
|
* - All @Watch subscriptions (via scope destruction)
|
|
1124
968
|
*
|
package/Utils/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export * from "./decorators";
|
|
|
2
2
|
export * from "./animation";
|
|
3
3
|
export { Injector } from "./injector";
|
|
4
4
|
export { IDestroyable } from "./utils.types";
|
|
5
|
+
export { SubscriptionManager } from "./subscriptionManager";
|
|
6
|
+
export type { KeyFilter, ISubscriptionMatch } from "./subscriptionManager";
|
package/Utils/index.js
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A key filter: exact keys, and patterns ending in `*` that match every key starting with the text
|
|
3
|
+
* before it (`task_*` matches `task_1`). Undefined matches every key; an empty array matches none.
|
|
4
|
+
*/
|
|
5
|
+
export type KeyFilter = readonly string[] | undefined;
|
|
6
|
+
/** A subscription that matched some changed keys, and the keys it matched */
|
|
7
|
+
export interface ISubscriptionMatch<S> {
|
|
8
|
+
id: number;
|
|
9
|
+
subscription: S;
|
|
10
|
+
keys: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Subscriptions to keys, for any use: it matches keys to subscriptions and counts who holds a
|
|
14
|
+
* key, and knows nothing about what a key's value is or how it's delivered.
|
|
15
|
+
*
|
|
16
|
+
* - **Exact keys hold a key; patterns don't.** `SubscriberCount(key)` counts the subscriptions
|
|
17
|
+
* naming the key exactly, and `OnReleased` reports when the last of them ends. A pattern
|
|
18
|
+
* subscription matches keys without holding them, or a `whatif_*` subscriber would keep every key.
|
|
19
|
+
* - **Groups end together.** A subscription can name a group (to a host, a channel), and
|
|
20
|
+
* `Disconnect(group)` ends every subscription in it.
|
|
21
|
+
* - Callbacks run synchronously, inside the call that caused them.
|
|
22
|
+
*
|
|
23
|
+
* @template S What a subscription carries, e.g. its callback and form.
|
|
24
|
+
*/
|
|
25
|
+
export declare class SubscriptionManager<S> {
|
|
26
|
+
private entries;
|
|
27
|
+
private nextId;
|
|
28
|
+
/** Exact key -> the subscriptions naming it */
|
|
29
|
+
private counts;
|
|
30
|
+
private groups;
|
|
31
|
+
private released;
|
|
32
|
+
/**
|
|
33
|
+
* Adds a subscription.
|
|
34
|
+
* @param keys The keys it matches, see `KeyFilter`.
|
|
35
|
+
* @param subscription What the subscription carries.
|
|
36
|
+
* @param options `group`: the group `Disconnect` ends it with.
|
|
37
|
+
* @returns Its id.
|
|
38
|
+
*/
|
|
39
|
+
Subscribe(keys: KeyFilter, subscription: S, options?: {
|
|
40
|
+
group?: unknown;
|
|
41
|
+
}): number;
|
|
42
|
+
/**
|
|
43
|
+
* Ends a subscription. Each exact key it was the last to name is released.
|
|
44
|
+
* @returns Whether it existed.
|
|
45
|
+
*/
|
|
46
|
+
Unsubscribe(id: number): boolean;
|
|
47
|
+
/** Ends every subscription in a group */
|
|
48
|
+
Disconnect(group: unknown): void;
|
|
49
|
+
/** Whether a subscription exists */
|
|
50
|
+
Has(id: number): boolean;
|
|
51
|
+
/** What a subscription carries, or undefined when it doesn't exist */
|
|
52
|
+
Get(id: number): S | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The keys a subscription matches, in the order given.
|
|
55
|
+
* @returns The keys, or an empty array when the subscription doesn't exist.
|
|
56
|
+
*/
|
|
57
|
+
MatchingKeys(id: number, keys: Iterable<string>): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Each subscription that matches any of the keys, with the keys it matches, in subscription
|
|
60
|
+
* order. Subscriptions that match none are left out.
|
|
61
|
+
*/
|
|
62
|
+
Matching(keys: Iterable<string>): ISubscriptionMatch<S>[];
|
|
63
|
+
/**
|
|
64
|
+
* Whether a subscription matches every key, with no filter, so a caller can hand it the whole
|
|
65
|
+
* of something without filtering it.
|
|
66
|
+
*/
|
|
67
|
+
MatchesAll(id: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Calls back when a key matching a pattern loses its last exact subscription, inside the
|
|
70
|
+
* `Unsubscribe` or `Disconnect` that ended it. A key never named exactly is never released.
|
|
71
|
+
* @param pattern An exact key or a pattern ending in `*`.
|
|
72
|
+
* @returns A function that removes the callback.
|
|
73
|
+
*/
|
|
74
|
+
OnReleased(pattern: string, callback: (key: string) => void): () => void;
|
|
75
|
+
/** How many subscriptions name a key exactly now; patterns don't count */
|
|
76
|
+
SubscriberCount(key: string): number;
|
|
77
|
+
private Release;
|
|
78
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const WILDCARD = "*";
|
|
2
|
+
/**
|
|
3
|
+
* Subscriptions to keys, for any use: it matches keys to subscriptions and counts who holds a
|
|
4
|
+
* key, and knows nothing about what a key's value is or how it's delivered.
|
|
5
|
+
*
|
|
6
|
+
* - **Exact keys hold a key; patterns don't.** `SubscriberCount(key)` counts the subscriptions
|
|
7
|
+
* naming the key exactly, and `OnReleased` reports when the last of them ends. A pattern
|
|
8
|
+
* subscription matches keys without holding them, or a `whatif_*` subscriber would keep every key.
|
|
9
|
+
* - **Groups end together.** A subscription can name a group (to a host, a channel), and
|
|
10
|
+
* `Disconnect(group)` ends every subscription in it.
|
|
11
|
+
* - Callbacks run synchronously, inside the call that caused them.
|
|
12
|
+
*
|
|
13
|
+
* @template S What a subscription carries, e.g. its callback and form.
|
|
14
|
+
*/
|
|
15
|
+
export class SubscriptionManager {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.entries = new Map();
|
|
18
|
+
this.nextId = 1;
|
|
19
|
+
/** Exact key -> the subscriptions naming it */
|
|
20
|
+
this.counts = new Map();
|
|
21
|
+
this.groups = new Map();
|
|
22
|
+
this.released = [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Adds a subscription.
|
|
26
|
+
* @param keys The keys it matches, see `KeyFilter`.
|
|
27
|
+
* @param subscription What the subscription carries.
|
|
28
|
+
* @param options `group`: the group `Disconnect` ends it with.
|
|
29
|
+
* @returns Its id.
|
|
30
|
+
*/
|
|
31
|
+
Subscribe(keys, subscription, options = {}) {
|
|
32
|
+
const id = this.nextId++;
|
|
33
|
+
const entry = { subscription, all: keys === undefined, exact: new Set(), prefixes: [], group: options.group };
|
|
34
|
+
if (keys)
|
|
35
|
+
for (let x = 0; x < keys.length; x++) {
|
|
36
|
+
const key = keys[x];
|
|
37
|
+
if (key.endsWith(WILDCARD))
|
|
38
|
+
entry.prefixes.push(key.slice(0, -1));
|
|
39
|
+
else
|
|
40
|
+
entry.exact.add(key);
|
|
41
|
+
}
|
|
42
|
+
this.entries.set(id, entry);
|
|
43
|
+
for (const key of entry.exact)
|
|
44
|
+
this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
|
|
45
|
+
if (options.group !== undefined) {
|
|
46
|
+
let ids = this.groups.get(options.group);
|
|
47
|
+
ids || this.groups.set(options.group, ids = new Set());
|
|
48
|
+
ids.add(id);
|
|
49
|
+
}
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ends a subscription. Each exact key it was the last to name is released.
|
|
54
|
+
* @returns Whether it existed.
|
|
55
|
+
*/
|
|
56
|
+
Unsubscribe(id) {
|
|
57
|
+
const entry = this.entries.get(id);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return false;
|
|
60
|
+
this.entries.delete(id);
|
|
61
|
+
if (entry.group !== undefined) {
|
|
62
|
+
const ids = this.groups.get(entry.group);
|
|
63
|
+
ids.delete(id);
|
|
64
|
+
ids.size === 0 && this.groups.delete(entry.group);
|
|
65
|
+
}
|
|
66
|
+
const releasedKeys = [];
|
|
67
|
+
for (const key of entry.exact) {
|
|
68
|
+
const count = this.counts.get(key) - 1;
|
|
69
|
+
if (count > 0)
|
|
70
|
+
this.counts.set(key, count);
|
|
71
|
+
else {
|
|
72
|
+
this.counts.delete(key);
|
|
73
|
+
releasedKeys.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const key of releasedKeys)
|
|
77
|
+
this.Release(key);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** Ends every subscription in a group */
|
|
81
|
+
Disconnect(group) {
|
|
82
|
+
const ids = this.groups.get(group);
|
|
83
|
+
if (ids)
|
|
84
|
+
for (const id of Array.from(ids))
|
|
85
|
+
this.Unsubscribe(id);
|
|
86
|
+
}
|
|
87
|
+
/** Whether a subscription exists */
|
|
88
|
+
Has(id) {
|
|
89
|
+
return this.entries.has(id);
|
|
90
|
+
}
|
|
91
|
+
/** What a subscription carries, or undefined when it doesn't exist */
|
|
92
|
+
Get(id) {
|
|
93
|
+
return this.entries.get(id)?.subscription;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The keys a subscription matches, in the order given.
|
|
97
|
+
* @returns The keys, or an empty array when the subscription doesn't exist.
|
|
98
|
+
*/
|
|
99
|
+
MatchingKeys(id, keys) {
|
|
100
|
+
const entry = this.entries.get(id);
|
|
101
|
+
const matched = [];
|
|
102
|
+
if (entry)
|
|
103
|
+
for (const key of keys)
|
|
104
|
+
Matches(entry, key) && matched.push(key);
|
|
105
|
+
return matched;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Each subscription that matches any of the keys, with the keys it matches, in subscription
|
|
109
|
+
* order. Subscriptions that match none are left out.
|
|
110
|
+
*/
|
|
111
|
+
Matching(keys) {
|
|
112
|
+
const list = Array.isArray(keys) ? keys : Array.from(keys);
|
|
113
|
+
const matches = [];
|
|
114
|
+
for (const [id, entry] of this.entries) {
|
|
115
|
+
const matched = entry.all ? list : list.filter(key => Matches(entry, key));
|
|
116
|
+
matched.length > 0 && matches.push({ id, subscription: entry.subscription, keys: matched });
|
|
117
|
+
}
|
|
118
|
+
return matches;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Whether a subscription matches every key, with no filter, so a caller can hand it the whole
|
|
122
|
+
* of something without filtering it.
|
|
123
|
+
*/
|
|
124
|
+
MatchesAll(id) {
|
|
125
|
+
return this.entries.get(id)?.all === true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Calls back when a key matching a pattern loses its last exact subscription, inside the
|
|
129
|
+
* `Unsubscribe` or `Disconnect` that ended it. A key never named exactly is never released.
|
|
130
|
+
* @param pattern An exact key or a pattern ending in `*`.
|
|
131
|
+
* @returns A function that removes the callback.
|
|
132
|
+
*/
|
|
133
|
+
OnReleased(pattern, callback) {
|
|
134
|
+
const entry = { pattern, callback };
|
|
135
|
+
this.released.push(entry);
|
|
136
|
+
return () => Remove(this.released, entry);
|
|
137
|
+
}
|
|
138
|
+
/** How many subscriptions name a key exactly now; patterns don't count */
|
|
139
|
+
SubscriberCount(key) {
|
|
140
|
+
return this.counts.get(key) ?? 0;
|
|
141
|
+
}
|
|
142
|
+
Release(key) {
|
|
143
|
+
for (const { pattern, callback } of this.released.slice())
|
|
144
|
+
if (PatternMatches(pattern, key))
|
|
145
|
+
callback(key);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function Matches(entry, key) {
|
|
149
|
+
if (entry.all || entry.exact.has(key))
|
|
150
|
+
return true;
|
|
151
|
+
for (let x = 0; x < entry.prefixes.length; x++)
|
|
152
|
+
if (key.startsWith(entry.prefixes[x]))
|
|
153
|
+
return true;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
function PatternMatches(pattern, key) {
|
|
157
|
+
return pattern.endsWith(WILDCARD) ? key.startsWith(pattern.slice(0, -1)) : pattern === key;
|
|
158
|
+
}
|
|
159
|
+
function Remove(list, item) {
|
|
160
|
+
const index = list.indexOf(item);
|
|
161
|
+
index >= 0 && list.splice(index, 1);
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { DiffTree } from "./diffTree";
|
|
2
|
-
/**
|
|
3
|
-
* Runs each batch of method calls a `StoreAsync` sends on the tree, then its projections, and
|
|
4
|
-
* posts back the tree's `Commit()` diff for the store to apply.
|
|
5
|
-
*/
|
|
6
|
-
export declare function ConnectWorkerToDiffTree(diffTree: DiffTree, worker: Worker): void;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runs each batch of method calls a `StoreAsync` sends on the tree, then its projections, and
|
|
3
|
-
* posts back the tree's `Commit()` diff for the store to apply.
|
|
4
|
-
*/
|
|
5
|
-
export function ConnectWorkerToDiffTree(diffTree, worker) {
|
|
6
|
-
worker.onmessage = function (event) {
|
|
7
|
-
const data = event.data;
|
|
8
|
-
for (let x = 0; x < data.length; x++)
|
|
9
|
-
RunMethod(diffTree, data[x]);
|
|
10
|
-
diffTree.RunProjections();
|
|
11
|
-
worker.postMessage(diffTree.Commit());
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function RunMethod(diffTree, call) {
|
|
15
|
-
const args = call.arguments;
|
|
16
|
-
switch (call.method) {
|
|
17
|
-
case "writepath": return diffTree.WritePath(args[0], args[1]);
|
|
18
|
-
case "splicepath": return diffTree.SplicePath(args[0], args[1], args[2], args[3]);
|
|
19
|
-
case "pushpath": return diffTree.PushPath(args[0], args[1]);
|
|
20
|
-
case "patchpath": return diffTree.PatchPath(args[0], args[1]);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { JsonDiffResult } from "../../Utils/json";
|
|
2
|
-
import { DiffTree } from "./diffTree";
|
|
3
|
-
import { IKeyFunc, IRouteDefinitions } from "./utils/diffTree.types";
|
|
4
|
-
/**
|
|
5
|
-
* A `DiffTree` that pushes its changes to a `PushStore` instead of answering a store's
|
|
6
|
-
* requests. Use it inside a worker that produces data itself (for example, the result of a
|
|
7
|
-
* computation it runs): write with the usual `DiffTree` methods, then call `Commit()` to run
|
|
8
|
-
* projections and post everything since the last commit as one message.
|
|
9
|
-
*
|
|
10
|
-
* - Push only: don't pass a `DiffTreeWriter` to `ConnectWorkerToDiffTree`, which commits
|
|
11
|
-
* every batch itself and would post it to the wrong place.
|
|
12
|
-
* - After a change fails, every later change before `Commit()` throws that same error. The
|
|
13
|
-
* changes made before the failure are already in the tree, so `Commit()` still sends them,
|
|
14
|
-
* followed by a `DIFF_TREE_ERROR` message. Projections aren't run for that batch; the next
|
|
15
|
-
* `Commit()` brings them up to date.
|
|
16
|
-
* - A `PushStore` only receives the changes committed after it starts listening, so
|
|
17
|
-
* create it before the writer's first `Commit()`.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // engine-worker.ts
|
|
21
|
-
* const tree = new DiffTreeWriter(self, undefined, { "/": { projections } });
|
|
22
|
-
* tree.WritePath("results", computeResults());
|
|
23
|
-
* tree.Commit();
|
|
24
|
-
*
|
|
25
|
-
* // main thread
|
|
26
|
-
* const store = new PushStore(worker);
|
|
27
|
-
*
|
|
28
|
-
* @see PushStore
|
|
29
|
-
*/
|
|
30
|
-
export declare class DiffTreeWriter extends DiffTree {
|
|
31
|
-
private worker;
|
|
32
|
-
private uncommittedError;
|
|
33
|
-
/**
|
|
34
|
-
* Creates a DiffTreeWriter.
|
|
35
|
-
* @param worker Where `Commit()` posts its messages: the worker's `self`, or a `MessagePort`.
|
|
36
|
-
* On the same thread as the store, anything whose `postMessage` dispatches a `"message"`
|
|
37
|
-
* event on the store's `EventTarget`. The store clones every value it applies, so it never
|
|
38
|
-
* shares objects with the writer's tree.
|
|
39
|
-
* @param keyFunc Optional function to extract a key from objects. Pass the same key logic to
|
|
40
|
-
* the `PushStore` that receives the changes.
|
|
41
|
-
* @param routes Optional route definitions. Projections run once per `Commit()`, after the
|
|
42
|
-
* batch's changes.
|
|
43
|
-
*/
|
|
44
|
-
constructor(worker: {
|
|
45
|
-
postMessage(message: unknown): void;
|
|
46
|
-
}, keyFunc?: IKeyFunc, routes?: IRouteDefinitions);
|
|
47
|
-
/**
|
|
48
|
-
* Not available on a DiffTreeWriter: `Commit()` runs projections once per batch, after its
|
|
49
|
-
* changes. Running them earlier would push a projection result computed from part of a batch.
|
|
50
|
-
* @throws Always.
|
|
51
|
-
*/
|
|
52
|
-
RunProjections(): void;
|
|
53
|
-
/**
|
|
54
|
-
* Writes a value at a path.
|
|
55
|
-
* @throws The change's error, or an earlier uncommitted change's error.
|
|
56
|
-
*/
|
|
57
|
-
WritePath(path: string, value: any): void;
|
|
58
|
-
/**
|
|
59
|
-
* Merges a patch into the current value at a path.
|
|
60
|
-
* @throws The change's error, or an earlier uncommitted change's error.
|
|
61
|
-
*/
|
|
62
|
-
PatchPath(path: string, patch: any): void;
|
|
63
|
-
/**
|
|
64
|
-
* Appends items to the array at a path.
|
|
65
|
-
* @throws The change's error, or an earlier uncommitted change's error.
|
|
66
|
-
*/
|
|
67
|
-
PushPath(path: string, items: unknown[]): void;
|
|
68
|
-
/**
|
|
69
|
-
* Splices the array at a path.
|
|
70
|
-
* @throws The change's error, or an earlier uncommitted change's error.
|
|
71
|
-
*/
|
|
72
|
-
SplicePath(path: string, start: number, deleteCount: number, items: any[]): void;
|
|
73
|
-
/**
|
|
74
|
-
* Deletes a route, its child routes and its route instances.
|
|
75
|
-
* @throws The change's error, or an earlier uncommitted change's error.
|
|
76
|
-
*/
|
|
77
|
-
DeleteRoute(routePath: string): void;
|
|
78
|
-
/**
|
|
79
|
-
* Runs projections and posts every change since the last commit as one `DIFF_TREE_EVENT`
|
|
80
|
-
* message, which a `PushStore` applies as a unit. Posts nothing when nothing changed.
|
|
81
|
-
*
|
|
82
|
-
* If a change in the batch failed, projections are skipped, the applied changes are still
|
|
83
|
-
* posted, and a `DIFF_TREE_ERROR` message follows with the error; `Commit()` itself doesn't
|
|
84
|
-
* throw, since the failed change already did. If a projection throws, the batch's changes
|
|
85
|
-
* and the error are posted the same way, then `Commit()` rethrows. Either way the next batch
|
|
86
|
-
* starts clean.
|
|
87
|
-
* @returns The diff that was posted.
|
|
88
|
-
* @throws A projection's error.
|
|
89
|
-
*/
|
|
90
|
-
Commit(): JsonDiffResult;
|
|
91
|
-
private Guard;
|
|
92
|
-
}
|