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/Store/storeSync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ObservableDiffTree } from "../Diff/observableDiffTree";
|
|
2
2
|
import { Store } from "./store";
|
|
3
3
|
/**
|
|
4
4
|
* StoreSync class extends the base Store class to provide synchronous data management operations.
|
|
@@ -24,21 +24,74 @@ import { Store } from "./store";
|
|
|
24
24
|
* // Splicing an array
|
|
25
25
|
* store.Splice("user.array", 0, 1, { id: 3 });
|
|
26
26
|
*
|
|
27
|
+
* The store subscribes to an `ObservableDiffTree` and applies every commit the tree delivers, so
|
|
28
|
+
* anything else on the thread can subscribe to the same tree (`Tree`), and a commit made on the
|
|
29
|
+
* tree directly reaches the store too. A change runs the tree's projections and commits
|
|
30
|
+
* before it returns.
|
|
31
|
+
*
|
|
32
|
+
* - **Its own tree** (`new StoreSync()`, `new StoreSync(undefined, { keyFunc })`): the store creates an
|
|
33
|
+
* empty tree, and it lives as long as the store.
|
|
34
|
+
* - **A tree it's given** (`new StoreSync(new ObservableDiffTree({ keyFunc, routes }))`): for a tree
|
|
35
|
+
* with routes and projections, whose keyFunc the store takes. Anything else on the thread may hold it too.
|
|
36
|
+
* - **A hub's tree** (`new StoreSync(hub.OpenLocal(name))`): a tree on this thread a `DiffTreeHub` runs
|
|
37
|
+
* for an endpoint, shared by every store opened on it and followed into by the hub. A change from
|
|
38
|
+
* any of them reaches all of them, and the trees that follow it, before it returns. The store
|
|
39
|
+
* owns the lease and releases it when it's destroyed. When the hub restarts the tree, the store
|
|
40
|
+
* moves to the new one, as when it first subscribed.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // A store on a hub's tree: it lives as long as the component that holds it
|
|
44
|
+
* @Inject(DiffTreeHub) hub!: DiffTreeHub;
|
|
45
|
+
* @Destroy() budget = new StoreSync(this.hub.OpenLocal("budget"));
|
|
46
|
+
*
|
|
27
47
|
* @see Store
|
|
28
48
|
* @see StoreAsync
|
|
29
|
-
* @see
|
|
49
|
+
* @see ObservableDiffTree
|
|
30
50
|
*/
|
|
31
51
|
export class StoreSync extends Store {
|
|
32
52
|
/**
|
|
33
|
-
* Creates
|
|
34
|
-
* @param
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
53
|
+
* Creates a store and subscribes it to its tree: the tree's state is the store's when this returns.
|
|
54
|
+
* @param tree The tree:
|
|
55
|
+
* - A lease from `DiffTreeHub.OpenLocal`, on a hub's tree on this thread. The store owns the
|
|
56
|
+
* lease from then on, and releases it when it's destroyed, and follows the tree across
|
|
57
|
+
* restarts. A lease on a worker's tree (`DiffTreeHub.Open`) is for a `StoreAsync`, and doesn't
|
|
58
|
+
* compile here.
|
|
59
|
+
* - An `ObservableDiffTree`, for a tree with routes (`new ObservableDiffTree({ keyFunc, routes })`).
|
|
60
|
+
* A projection in the root route (`"/"`) is readable through `Get` at `$projection_<key>`. The
|
|
61
|
+
* store doesn't own it: `Destroy` stops listening.
|
|
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 `opaque` (`IStoreSyncOptions`). `keyFunc` is for the tree the
|
|
64
|
+
* store creates: a leased tree's is the endpoint's, and a given tree's its own. A tree given or
|
|
65
|
+
* leased must write the `opaque` keys whole already (`IObservableDiffTreeConfig.opaque`): share
|
|
66
|
+
* them from one file.
|
|
67
|
+
* @throws When `keyFunc` is given with a tree or a lease, which has its own.
|
|
68
|
+
*/
|
|
69
|
+
constructor(tree, options = {}) {
|
|
70
|
+
const lease = tree && !(tree instanceof ObservableDiffTree) ? tree : undefined;
|
|
71
|
+
if (tree && options.keyFunc)
|
|
72
|
+
throw new Error(lease
|
|
73
|
+
? `StoreSync: the endpoint ${lease.name} gives the store its keyFunc; leave it out of the options.`
|
|
74
|
+
: "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);
|
|
76
|
+
/** Over a lease: the roots delivered and not deleted, to delete the ones a restarted tree doesn't hold */
|
|
77
|
+
this.delivered = new Set();
|
|
78
|
+
this.roots = options.roots;
|
|
79
|
+
if (lease) {
|
|
80
|
+
this.lease = lease;
|
|
81
|
+
this.Attach(lease.tree, false);
|
|
82
|
+
this.stopLease = lease.OnTree(tree => this.Attach(tree, true));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.diff = tree ?? new ObservableDiffTree({ keyFunc: options.keyFunc, opaque: options.opaque });
|
|
86
|
+
this.unsubscribe = this.diff.Subscribe({ roots: this.roots }, post => this.UpdateRootMap(post.diff));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The store's tree: its own, or the hub's. Subscribe to it to receive the
|
|
90
|
+
* same commits the store applies; a commit made on it reaches the store too. A subscriber can't
|
|
91
|
+
* write to the store during a delivery.
|
|
38
92
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.diff = new DiffTree(keyFunc, routes);
|
|
93
|
+
get Tree() {
|
|
94
|
+
return this.diff;
|
|
42
95
|
}
|
|
43
96
|
/**
|
|
44
97
|
* Writes data to the store synchronously.
|
|
@@ -91,11 +144,51 @@ export class StoreSync extends Store {
|
|
|
91
144
|
this.Sync();
|
|
92
145
|
}
|
|
93
146
|
/**
|
|
94
|
-
*
|
|
147
|
+
* Stops applying the tree's commits. Over a lease, the store then releases it: the last lease on
|
|
148
|
+
* a hub's tree ends it. The store's values stay readable, and stop changing.
|
|
149
|
+
*/
|
|
150
|
+
Destroy() {
|
|
151
|
+
this.stopLease?.();
|
|
152
|
+
this.stopLease = undefined;
|
|
153
|
+
this.unsubscribe?.();
|
|
154
|
+
this.unsubscribe = undefined;
|
|
155
|
+
this.lease?.Release();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Subscribes to a hub's tree in place of the last. A restarted tree's state is applied in place, so
|
|
159
|
+
* readers keep their objects, with a delete for each root the last tree had and this one hasn't, in
|
|
160
|
+
* one update. The first delivery arrives during `Subscribe`.
|
|
161
|
+
*/
|
|
162
|
+
Attach(tree, again) {
|
|
163
|
+
this.unsubscribe?.();
|
|
164
|
+
this.diff = tree;
|
|
165
|
+
const previous = this.delivered;
|
|
166
|
+
this.delivered = new Set();
|
|
167
|
+
let subscribing = true;
|
|
168
|
+
let first = [];
|
|
169
|
+
this.unsubscribe = tree.Subscribe({ roots: this.roots }, post => {
|
|
170
|
+
this.Track(post.diff);
|
|
171
|
+
subscribing ? (first = post.diff) : this.UpdateRootMap(post.diff);
|
|
172
|
+
});
|
|
173
|
+
subscribing = false;
|
|
174
|
+
const diff = [...first];
|
|
175
|
+
for (const key of previous)
|
|
176
|
+
this.delivered.has(key) || diff.push({ type: "delete", path: [key] });
|
|
177
|
+
diff.length > 0 && (again ? this.UpdateRootMapInPlace(diff) : this.UpdateRootMap(diff));
|
|
178
|
+
}
|
|
179
|
+
Track(diff) {
|
|
180
|
+
for (const record of diff) {
|
|
181
|
+
const key = String(record.path[0]);
|
|
182
|
+
record.type === "delete" && record.path.length === 1 ? this.delivered.delete(key) : this.delivered.add(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Runs projections and commits: the commit's delivery applies everything changed since the
|
|
187
|
+
* last commit to the store.
|
|
95
188
|
* @private
|
|
96
189
|
*/
|
|
97
190
|
Sync() {
|
|
98
191
|
this.diff.RunProjections();
|
|
99
|
-
this.
|
|
192
|
+
this.diff.Commit();
|
|
100
193
|
}
|
|
101
194
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JsonApplyDiff, JsonDeepClone, JsonDiff } from "../../Utils/json";
|
|
2
2
|
import { JsonType } from "../../Utils/json";
|
|
3
3
|
import { ObservableScope } from "./observableScope";
|
|
4
|
+
import { Opaque } from "./opaque";
|
|
4
5
|
import { Snapshot } from "./snapshot";
|
|
5
6
|
const NODE_VALUE = Symbol("NODE_VALUE");
|
|
6
7
|
const NODE_PROXY = Symbol("NODE_PROXY");
|
|
@@ -142,6 +143,45 @@ function AssignNodeValue(obj, prop, value, wrapper) {
|
|
|
142
143
|
obj[prop] = value;
|
|
143
144
|
NodeValueSet(obj, prop, added, wrapper);
|
|
144
145
|
}
|
|
146
|
+
/** `Array.prototype.sort`'s order when it's given no compare function: by each value as a string */
|
|
147
|
+
function DefaultCompare(a, b) {
|
|
148
|
+
const x = String(a);
|
|
149
|
+
const y = String(b);
|
|
150
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Runs a mutating array method on a node's stored array. Values passed in are stored as
|
|
154
|
+
* `UnwrapProxy` leaves them, values handed out (removed elements, a sort's compared pair) are read
|
|
155
|
+
* as `read` gives them, so nothing already stored is rewritten. Returns the array itself for `sort`
|
|
156
|
+
* and `reverse`, which the caller hands out as the array's proxy.
|
|
157
|
+
*/
|
|
158
|
+
function MutateArray(array, prop, args, read) {
|
|
159
|
+
switch (prop) {
|
|
160
|
+
case "push":
|
|
161
|
+
case "unshift":
|
|
162
|
+
return array[prop](...args.map(StoredValue));
|
|
163
|
+
case "splice": {
|
|
164
|
+
// Spread so a call without a delete count still deletes to the end
|
|
165
|
+
const stored = args.length > 2 ? [args[0], args[1], ...args.slice(2).map(StoredValue)] : args;
|
|
166
|
+
return array.splice(...stored).map(read);
|
|
167
|
+
}
|
|
168
|
+
case "pop":
|
|
169
|
+
case "shift": {
|
|
170
|
+
// An empty array returns undefined, which reads as itself
|
|
171
|
+
return read(array[prop]());
|
|
172
|
+
}
|
|
173
|
+
case "sort": {
|
|
174
|
+
const compare = args[0] ?? DefaultCompare;
|
|
175
|
+
return array.sort((a, b) => compare(read(a), read(b)));
|
|
176
|
+
}
|
|
177
|
+
case "reverse":
|
|
178
|
+
return array.reverse();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** A value as a node stores it: proxies removed, an Opaque kept as its box */
|
|
182
|
+
function StoredValue(value) {
|
|
183
|
+
return UnwrapProxy(value);
|
|
184
|
+
}
|
|
145
185
|
function CreateProxyFactory(alias) {
|
|
146
186
|
// Keyed children are read through their root. Every factory shares the module-level
|
|
147
187
|
// snapshotState, matching wrapperCache; only the resolver differs per factory.
|
|
@@ -150,6 +190,20 @@ function CreateProxyFactory(alias) {
|
|
|
150
190
|
return Snapshot.Get(snapshotState, value, resolveSnapshot);
|
|
151
191
|
}
|
|
152
192
|
const readOnly = alias !== undefined;
|
|
193
|
+
/**
|
|
194
|
+
* A descriptor trap whose value is what a read gives: a proxy for an object or array, an
|
|
195
|
+
* Opaque's raw value, never the stored value itself
|
|
196
|
+
*/
|
|
197
|
+
function ReadDescriptor(trap) {
|
|
198
|
+
return function (target, prop) {
|
|
199
|
+
const descriptor = trap(target, prop);
|
|
200
|
+
if (descriptor && "value" in descriptor)
|
|
201
|
+
descriptor.value = CreateProxyFromValue(descriptor.value);
|
|
202
|
+
return descriptor;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const readObjectDescriptor = ReadDescriptor(getOwnPropertyDescriptor);
|
|
206
|
+
const readArrayDescriptor = ReadDescriptor(getOwnPropertyDescriptorArray);
|
|
153
207
|
function CreateArrayProxy(value) {
|
|
154
208
|
const wrapper = Object.assign([], {
|
|
155
209
|
[NODE_VALUE]: value,
|
|
@@ -161,7 +215,7 @@ function CreateProxyFactory(alias) {
|
|
|
161
215
|
set: ArrayProxySetter,
|
|
162
216
|
has: hasArray,
|
|
163
217
|
ownKeys: ownKeysArray,
|
|
164
|
-
getOwnPropertyDescriptor:
|
|
218
|
+
getOwnPropertyDescriptor: readArrayDescriptor,
|
|
165
219
|
});
|
|
166
220
|
wrapperCache.set(value, wrapper);
|
|
167
221
|
return proxy;
|
|
@@ -206,10 +260,6 @@ function CreateProxyFactory(alias) {
|
|
|
206
260
|
return arrayValue;
|
|
207
261
|
if (typeof arrayValue === "function")
|
|
208
262
|
return function ArrayFunction(...args) {
|
|
209
|
-
const proxyArray = prop === "slice" ? array.slice(...args).map(CreateProxyFromValue) : array.map(CreateProxyFromValue);
|
|
210
|
-
let result = prop === "slice"
|
|
211
|
-
? proxyArray
|
|
212
|
-
: proxyArray[prop](...args);
|
|
213
263
|
switch (prop) {
|
|
214
264
|
case "push":
|
|
215
265
|
case "unshift":
|
|
@@ -217,15 +267,19 @@ function CreateProxyFactory(alias) {
|
|
|
217
267
|
case "pop":
|
|
218
268
|
case "shift":
|
|
219
269
|
case "sort":
|
|
220
|
-
case "reverse":
|
|
221
|
-
array
|
|
222
|
-
|
|
223
|
-
|
|
270
|
+
case "reverse": {
|
|
271
|
+
// Run on the stored array, so what it holds (an Opaque's box included) stays as
|
|
272
|
+
// it is: arguments go in as stored values, results come out as read values
|
|
273
|
+
const result = MutateArray(array, prop, args, CreateProxyFromValue);
|
|
224
274
|
InvalidateSnapshotForValue(object[NODE_VALUE]);
|
|
225
275
|
ObservableScope.Update(scope);
|
|
226
|
-
|
|
276
|
+
return result === array ? object[NODE_PROXY] : result;
|
|
277
|
+
}
|
|
227
278
|
}
|
|
228
|
-
|
|
279
|
+
const proxyArray = prop === "slice" ? array.slice(...args).map(CreateProxyFromValue) : array.map(CreateProxyFromValue);
|
|
280
|
+
return prop === "slice"
|
|
281
|
+
? proxyArray
|
|
282
|
+
: proxyArray[prop](...args);
|
|
229
283
|
};
|
|
230
284
|
const proxy = CreateProxyFromValue(arrayValue);
|
|
231
285
|
return proxy;
|
|
@@ -243,7 +297,7 @@ function CreateProxyFactory(alias) {
|
|
|
243
297
|
set: ObjectProxySetter,
|
|
244
298
|
has,
|
|
245
299
|
ownKeys,
|
|
246
|
-
getOwnPropertyDescriptor,
|
|
300
|
+
getOwnPropertyDescriptor: readObjectDescriptor,
|
|
247
301
|
});
|
|
248
302
|
wrapperCache.set(value, wrapper);
|
|
249
303
|
return proxy;
|
|
@@ -301,7 +355,8 @@ function CreateProxyFactory(alias) {
|
|
|
301
355
|
return proxy;
|
|
302
356
|
}
|
|
303
357
|
default:
|
|
304
|
-
|
|
358
|
+
// An Opaque is stored as its box and read as its raw value
|
|
359
|
+
return Opaque.Open(value);
|
|
305
360
|
}
|
|
306
361
|
}
|
|
307
362
|
return function CreateProxy(value, unwrapValue = true) {
|
|
@@ -207,6 +207,7 @@ export declare namespace ObservableScope {
|
|
|
207
207
|
function OnDestroyed<T>(scope: IObservableScope<T>, callback: {
|
|
208
208
|
(lastValue: T): void;
|
|
209
209
|
}): void;
|
|
210
|
+
function IsDestroyed(scope: IObservableScope<unknown>): boolean;
|
|
210
211
|
/**
|
|
211
212
|
* Marks a scope as dirty, triggering recomputation on next access or batch.
|
|
212
213
|
* No-op for static scopes (created when a `Create` valueFunction reads no reactive
|
|
@@ -663,6 +663,10 @@ export var ObservableScope;
|
|
|
663
663
|
Emitter.On(scope.onDestroyed, callback);
|
|
664
664
|
}
|
|
665
665
|
ObservableScope.OnDestroyed = OnDestroyed;
|
|
666
|
+
function IsDestroyed(scope) {
|
|
667
|
+
return scope.destroyed;
|
|
668
|
+
}
|
|
669
|
+
ObservableScope.IsDestroyed = IsDestroyed;
|
|
666
670
|
/**
|
|
667
671
|
* Marks a scope as dirty, triggering recomputation on next access or batch.
|
|
668
672
|
* No-op for static scopes (created when a `Create` valueFunction reads no reactive
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value an ObservableNode holds whole: it's never proxied, diffed into or cloned. A node stores
|
|
3
|
+
* the box and reads open it, so a reader gets the raw value, the same reference until the box is
|
|
4
|
+
* replaced. The property holding the box is the only thing observed: replacing the box updates its
|
|
5
|
+
* readers, and a change made inside the raw value notifies no one, so treat it as read-only.
|
|
6
|
+
*
|
|
7
|
+
* - Write one into a node by assigning it (`node.view = new Opaque(view)`), pushing or splicing it
|
|
8
|
+
* into a node's array, or putting it in a value given to `ObservableNode.Create` or returned from
|
|
9
|
+
* a `@Computed` getter.
|
|
10
|
+
* - A store holds a root whole when the root's key is in the store's `opaque` option. The store
|
|
11
|
+
* boxes the root itself; don't pass an `Opaque` to a store's `Write`, `Patch`, `Push` or `Splice`.
|
|
12
|
+
* - Reads, array methods, property descriptors, snapshots and `JSON.stringify` give the raw value,
|
|
13
|
+
* so an object composed from a node's reads holds no box. `ObservableNode.Unwrap` gives a node's
|
|
14
|
+
* storage, boxes included, and `ObservableNode.Clone` keeps a box it's given: writing either back
|
|
15
|
+
* into a node keeps the value opaque, but don't pass them to a store.
|
|
16
|
+
*
|
|
17
|
+
* Reading a box's value through a node and writing it somewhere else writes the raw value, which is
|
|
18
|
+
* then plain data again: box it again (`new Opaque(value)`) to keep it whole.
|
|
19
|
+
*/
|
|
20
|
+
export declare class Opaque<T = unknown> {
|
|
21
|
+
readonly value: T;
|
|
22
|
+
constructor(value: T);
|
|
23
|
+
/** The raw value, so `JSON.stringify` writes the value, not the box */
|
|
24
|
+
toJSON(): T;
|
|
25
|
+
/**
|
|
26
|
+
* The raw value of a box, or the value itself when it isn't one.
|
|
27
|
+
* @param value Any value.
|
|
28
|
+
*/
|
|
29
|
+
static Open<V>(value: V | Opaque<V>): V;
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value an ObservableNode holds whole: it's never proxied, diffed into or cloned. A node stores
|
|
3
|
+
* the box and reads open it, so a reader gets the raw value, the same reference until the box is
|
|
4
|
+
* replaced. The property holding the box is the only thing observed: replacing the box updates its
|
|
5
|
+
* readers, and a change made inside the raw value notifies no one, so treat it as read-only.
|
|
6
|
+
*
|
|
7
|
+
* - Write one into a node by assigning it (`node.view = new Opaque(view)`), pushing or splicing it
|
|
8
|
+
* into a node's array, or putting it in a value given to `ObservableNode.Create` or returned from
|
|
9
|
+
* a `@Computed` getter.
|
|
10
|
+
* - A store holds a root whole when the root's key is in the store's `opaque` option. The store
|
|
11
|
+
* boxes the root itself; don't pass an `Opaque` to a store's `Write`, `Patch`, `Push` or `Splice`.
|
|
12
|
+
* - Reads, array methods, property descriptors, snapshots and `JSON.stringify` give the raw value,
|
|
13
|
+
* so an object composed from a node's reads holds no box. `ObservableNode.Unwrap` gives a node's
|
|
14
|
+
* storage, boxes included, and `ObservableNode.Clone` keeps a box it's given: writing either back
|
|
15
|
+
* into a node keeps the value opaque, but don't pass them to a store.
|
|
16
|
+
*
|
|
17
|
+
* Reading a box's value through a node and writing it somewhere else writes the raw value, which is
|
|
18
|
+
* then plain data again: box it again (`new Opaque(value)`) to keep it whole.
|
|
19
|
+
*/
|
|
20
|
+
export class Opaque {
|
|
21
|
+
constructor(value) {
|
|
22
|
+
this.value = value;
|
|
23
|
+
}
|
|
24
|
+
/** The raw value, so `JSON.stringify` writes the value, not the box */
|
|
25
|
+
toJSON() {
|
|
26
|
+
return this.value;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The raw value of a box, or the value itself when it isn't one.
|
|
30
|
+
* @param value Any value.
|
|
31
|
+
*/
|
|
32
|
+
static Open(value) {
|
|
33
|
+
return value instanceof Opaque ? value.value : value;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/Store/Tree/snapshot.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JsonType } from "../../Utils/json";
|
|
2
|
+
import { Opaque } from "./opaque";
|
|
2
3
|
function CreateSnapshotLink(state, child, parent) {
|
|
3
4
|
let parents = state.hierarchy.get(child);
|
|
4
5
|
if (!parents)
|
|
@@ -21,15 +22,15 @@ function InvalidateValue(state, value) {
|
|
|
21
22
|
InvalidateValue(state, parent);
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
|
-
* An immutable snapshot of a raw value (arrays and plain objects;
|
|
25
|
-
* returned as they are). A snapshot is cached per raw value and returned as-is until a
|
|
25
|
+
* An immutable snapshot of a raw value (arrays and plain objects; an Opaque gives its raw
|
|
26
|
+
* value, and other values are returned as they are). A snapshot is cached per raw value and returned as-is until a
|
|
26
27
|
* write invalidates it. A rebuild makes new containers but reuses the cached snapshots of
|
|
27
28
|
* unchanged children, so a snapshot handed out never changes and unchanged parts are
|
|
28
29
|
* shared between snapshots. Keyed children are read through their root.
|
|
29
30
|
*/
|
|
30
31
|
function CreateSnapshot(state, resolve, value) {
|
|
31
32
|
if (JsonType(value) === "value")
|
|
32
|
-
return value;
|
|
33
|
+
return Opaque.Open(value);
|
|
33
34
|
const cached = state.rawToSnapshotCache.get(value);
|
|
34
35
|
if (cached !== undefined)
|
|
35
36
|
return cached;
|
|
@@ -57,7 +58,7 @@ function CreateSnapshot(state, resolve, value) {
|
|
|
57
58
|
function ChildSnapshot(state, resolve, parent, key) {
|
|
58
59
|
const child = parent[key];
|
|
59
60
|
if (JsonType(child) === "value")
|
|
60
|
-
return child;
|
|
61
|
+
return Opaque.Open(child);
|
|
61
62
|
CreateSnapshotLink(state, child, parent);
|
|
62
63
|
const resolved = resolve?.(child);
|
|
63
64
|
if (resolved !== undefined && resolved !== child) {
|
package/Store/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { ReadStore, Store } from "./Store/store";
|
|
2
|
+
export type { IStoreOptions } from "./Store/store";
|
|
3
|
+
export type { IStoreAsyncOptions } from "./Store/storeAsync";
|
|
4
|
+
export type { IStoreSyncOptions } from "./Store/storeSync";
|
|
2
5
|
export { StoreSync } from "./Store/storeSync";
|
|
3
6
|
export { StoreAsync } from "./Store/storeAsync";
|
|
4
|
-
export { PushStore } from "./Store/pushStore";
|
|
5
7
|
export { ObservableScope } from "./Tree/observableScope";
|
|
6
8
|
export { ObservableNode } from "./Tree/observableNode";
|
|
9
|
+
export { Opaque } from "./Tree/opaque";
|
package/Store/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ReadStore, Store } from "./Store/store";
|
|
2
2
|
export { StoreSync } from "./Store/storeSync";
|
|
3
3
|
export { StoreAsync } from "./Store/storeAsync";
|
|
4
|
-
export { PushStore } from "./Store/pushStore";
|
|
5
4
|
export { ObservableScope } from "./Tree/observableScope";
|
|
6
5
|
export { ObservableNode } from "./Tree/observableNode";
|
|
6
|
+
export { Opaque } from "./Tree/opaque";
|
package/Utils/decorators.d.ts
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 |
|
|
@@ -157,104 +155,10 @@ import { Injector } from "./injector";
|
|
|
157
155
|
* - Use @Scope() for simple computations where object identity doesn't matter
|
|
158
156
|
* - The diff computation overhead is justified when you need object reuse
|
|
159
157
|
* @see {@link Scope} for simple getter-based reactive properties (caches but new reference)
|
|
160
|
-
* @see {@link ComputedAsync} for async computed properties
|
|
161
158
|
* @see {@link ObservableNode.Apply} for how diffs are applied in-place to maintain object identity
|
|
162
159
|
* @see {@link ObservableScope} for the Gated scope mechanism (batched updates)
|
|
163
|
-
* @see {@link StoreSync} for the store-backed variant used by @ComputedAsync
|
|
164
160
|
*/
|
|
165
161
|
export declare function Computed<T extends WeakKey, K extends keyof T, D extends T[K]>(): (target: T, propertyKey: K, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
166
|
-
/**
|
|
167
|
-
* ComputedAsync decorator factory for creating synchronous computed properties with StoreAsync caching.
|
|
168
|
-
* A computed property is derived from other properties and automatically updates when its dependencies change.
|
|
169
|
-
*
|
|
170
|
-
* IMPORTANT: Despite the name, @ComputedAsync expects a SYNC getter, NOT an async function.
|
|
171
|
-
* The "Async" refers to the internal StoreAsync caching mechanism, not the getter signature.
|
|
172
|
-
*
|
|
173
|
-
* Use @ComputedAsync when you need synchronous caching with StoreAsync backend.
|
|
174
|
-
* For direct async operations (no object reuse), use ObservableScope.Create(async () => ...) instead.
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* ```typescript
|
|
178
|
-
* // ✅ Good: Synchronous getter with StoreAsync caching
|
|
179
|
-
* @Value()
|
|
180
|
-
* userId: string = "123";
|
|
181
|
-
*
|
|
182
|
-
* @ComputedAsync(null)
|
|
183
|
-
* get userData(): User | null {
|
|
184
|
-
* // Sync getter - returns value directly
|
|
185
|
-
* return getUserSync(this.userId);
|
|
186
|
-
* }
|
|
187
|
-
* ```
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* ```typescript
|
|
191
|
-
* // ❌ WRONG - Do NOT use async keyword or return Promise
|
|
192
|
-
* @Value()
|
|
193
|
-
* userId: string = "123";
|
|
194
|
-
*
|
|
195
|
-
* @ComputedAsync(null)
|
|
196
|
-
* async getUser(): Promise<User> { // ERROR: getter must be synchronous!
|
|
197
|
-
* return await fetch(`/api/users/${this.userId}`);
|
|
198
|
-
* }
|
|
199
|
-
*
|
|
200
|
-
* // ✅ CORRECT - Use ObservableScope.Create for direct async operations
|
|
201
|
-
* import { ObservableScope } from "j-templates/Store";
|
|
202
|
-
*
|
|
203
|
-
* @Value()
|
|
204
|
-
* userId: string = "123";
|
|
205
|
-
*
|
|
206
|
-
* private userDataScope = ObservableScope.Create(async () => {
|
|
207
|
-
* return await fetch(`/api/users/${this.userId}`); // Async supported here!
|
|
208
|
-
* });
|
|
209
|
-
*
|
|
210
|
-
* get userData(): User | null {
|
|
211
|
-
* return ObservableScope.Value(this.userDataScope); // null while loading
|
|
212
|
-
* }
|
|
213
|
-
* ```
|
|
214
|
-
*
|
|
215
|
-
* @param defaultValue The default value to be used if the computed property is not defined.
|
|
216
|
-
* @returns A property decorator that can be applied to a getter method.
|
|
217
|
-
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
218
|
-
* @remarks
|
|
219
|
-
* The @ComputedAsync decorator uses StoreAsync for caching. The getter must be a synchronous function.
|
|
220
|
-
* Unlike @Computed which uses StoreSync, @ComputedAsync uses StoreAsync internally for caching.
|
|
221
|
-
*
|
|
222
|
-
* **Initialization**: @ComputedAsync uses lazy initialization - the scopes are created on first access:
|
|
223
|
-
* ```typescript
|
|
224
|
-
* @ComputedAsync(null)
|
|
225
|
-
* get userData(): User | null {
|
|
226
|
-
* return getUserSync(this.userId);
|
|
227
|
-
* }
|
|
228
|
-
*
|
|
229
|
-
* // Scope created here, on first access:
|
|
230
|
-
* const user = this.userData;
|
|
231
|
-
* ```
|
|
232
|
-
*
|
|
233
|
-
* **Caching**: Like @Computed, @ComputedAsync caches the result and maintains object identity
|
|
234
|
-
* through diff-based updates. The getter only runs when dependencies change.
|
|
235
|
-
*
|
|
236
|
-
* **Object Reuse**: Like @Computed, the result maintains its identity across updates.
|
|
237
|
-
* Only changed properties are modified in-place, preserving object references.
|
|
238
|
-
*
|
|
239
|
-
* **Key difference from @Computed**: Uses StoreAsync instead of StoreSync for caching.
|
|
240
|
-
* Both use synchronous getters and provide object identity preservation.
|
|
241
|
-
*
|
|
242
|
-
* **Comparison**:
|
|
243
|
-
* | Aspect | @Scope | @Computed | @ComputedAsync | scope(async) + @Scope |
|
|
244
|
-
* |--------|--------|-----------|----------------|----------------------|
|
|
245
|
-
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (memoized) |
|
|
246
|
-
* | Getter type | Sync | Sync | Sync | Async supported |
|
|
247
|
-
* | Object identity | ❌ New | ✅ Same | ✅ Same | ❌ New |
|
|
248
|
-
* | Store backend | Single scope | StoreSync | StoreAsync | Direct scope |
|
|
249
|
-
* | Initial value | First access | First access | defaultValue | Promise |
|
|
250
|
-
* | Best for | Simple sync, async | Complex sync | StoreAsync sync | Component async ops |
|
|
251
|
-
*
|
|
252
|
-
* @see {@link Computed} for synchronous computed properties with StoreSync caching
|
|
253
|
-
* @see {@link Scope} for simple getter-based reactive properties (caches, new reference)
|
|
254
|
-
* @see {@link StoreAsync} for async store implementation
|
|
255
|
-
* @see {@link ObservableScope.Create} for async function support
|
|
256
|
-
*/
|
|
257
|
-
export declare function ComputedAsync<T extends WeakKey, K extends keyof T, V extends T[K]>(defaultValue: V): (target: T, propertyKey: K, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
258
162
|
/**
|
|
259
163
|
* State decorator factory for creating reactive properties backed by ObservableNode proxies.
|
|
260
164
|
*
|
|
@@ -490,7 +394,6 @@ export declare function Value(): any;
|
|
|
490
394
|
* | Best for | Primitives, cheap ops, async ops | Complex objects, expensive ops |
|
|
491
395
|
*
|
|
492
396
|
* @see {@link Computed} for cached computed properties with object reuse
|
|
493
|
-
* @see {@link ComputedAsync} for sync getters with StoreAsync backend
|
|
494
397
|
* @see {@link ObservableNode.Apply} for how @Computed maintains object identity
|
|
495
398
|
* @see {@link ObservableScope} for the scope-based reactivity system
|
|
496
399
|
* @see {@link scope} for inline computed scopes within @Scope
|
|
@@ -797,7 +700,6 @@ export declare namespace Destroy {
|
|
|
797
700
|
* - All @Value-decorated property scopes
|
|
798
701
|
* - All @Scope-decorated property scopes
|
|
799
702
|
* - All @Computed-decorated property scopes (both getter and property scopes)
|
|
800
|
-
* - All @ComputedAsync-decorated property scopes (including StoreAsync)
|
|
801
703
|
* - All @Destroy-marked properties (calls .Destroy() method)
|
|
802
704
|
* - All @Watch subscriptions (via scope destruction)
|
|
803
705
|
*
|