@kuindji/reactive 1.1.0 → 1.2.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/README.md +128 -5
- package/dist/action.d.ts +19 -0
- package/dist/action.js +137 -14
- package/dist/actionBus.d.ts +5 -0
- package/dist/actionBus.js +168 -4
- package/dist/actionMap.d.ts +5 -0
- package/dist/event.d.ts +34 -1
- package/dist/event.js +256 -43
- package/dist/eventBus.d.ts +2 -0
- package/dist/eventBus.js +106 -3
- package/dist/lib/types.d.ts +1 -1
- package/dist/react/useActionBusStatus.d.ts +13 -0
- package/dist/react/useActionBusStatus.js +26 -0
- package/dist/react/useAsyncAction.d.ts +20 -0
- package/dist/react/useAsyncAction.js +53 -0
- package/dist/react/useStoreSelector.d.ts +35 -0
- package/dist/react/useStoreSelector.js +144 -0
- package/dist/react/useStoreState.js +13 -1
- package/dist/react.d.ts +3 -0
- package/dist/react.js +3 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +410 -43
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -10,11 +10,74 @@ export function createStore(initialData = {}) {
|
|
|
10
10
|
const pipe = createEventBus();
|
|
11
11
|
const control = createEventBus();
|
|
12
12
|
let effectKeys = [];
|
|
13
|
+
// Computed keys are read-only via the public `set` and recompute via the
|
|
14
|
+
// `effect` control event. `computingKeys` is a per-key re-entrancy guard so
|
|
15
|
+
// a cyclic computed throws instead of looping forever.
|
|
16
|
+
const computedKeys = new Set();
|
|
17
|
+
const computingKeys = new Set();
|
|
18
|
+
// Re-seed closures keyed in registration order so reset() can recompute each
|
|
19
|
+
// computed value from the (now cleared) deps. Without this, reset() clears
|
|
20
|
+
// `data` but leaves computed keys stale/undefined while still marked
|
|
21
|
+
// read-only, so they no longer equal fn(deps).
|
|
22
|
+
const computedReseeders = new Map();
|
|
23
|
+
// The effect listener installed for each computed key, so re-registering the
|
|
24
|
+
// same key detaches the previous recompute closure instead of leaving it
|
|
25
|
+
// attached (which would run a stale fn on every dependency change).
|
|
26
|
+
const computedEffectListeners = new Map();
|
|
27
|
+
// Multi-key object sets write every base value first with effect emission
|
|
28
|
+
// deferred (`deferEffects`), then replay effects once so computed values
|
|
29
|
+
// recompute from the final state instead of once per intermediate write.
|
|
30
|
+
// `computedBatch`, when active, records the dependency values each computed
|
|
31
|
+
// last recomputed from in this batch. A computed is skipped only when its
|
|
32
|
+
// deps are unchanged since then — so a redundant dep change is a no-op, but
|
|
33
|
+
// a dependent in a computed chain still recomputes once its upstream
|
|
34
|
+
// computed updates (rather than settling on a stale early value).
|
|
35
|
+
let deferEffects = false;
|
|
36
|
+
let computedBatch = null;
|
|
37
|
+
const arraysShallowEqual = (a, b) => {
|
|
38
|
+
if (a.length !== b.length) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
for (let i = 0; i < a.length; i++) {
|
|
42
|
+
if (a[i] !== b[i]) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
let destroyed = false;
|
|
49
|
+
// Timers scheduled by asyncSet, tracked so destroy() can cancel them.
|
|
50
|
+
// Otherwise a pending callback fires after teardown and throws "Store is
|
|
51
|
+
// destroyed" from inside the timer.
|
|
52
|
+
const pendingTimers = new Set();
|
|
53
|
+
const assertAlive = () => {
|
|
54
|
+
if (destroyed) {
|
|
55
|
+
throw new Error("Store is destroyed");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const dedupe = (keys) => Array.from(new Set(keys));
|
|
59
|
+
// A public set() can trigger a computed cascade. Routing it through batch()
|
|
60
|
+
// makes that cascade glitch-free (one coalesced onChange per affected key).
|
|
61
|
+
// Only do so at the top level: if already batching or intercepting, the
|
|
62
|
+
// surrounding operation coalesces; with no effect listener there is no
|
|
63
|
+
// cascade to coalesce.
|
|
64
|
+
const canCoalesceCascade = () => {
|
|
65
|
+
var _a;
|
|
66
|
+
return !batching
|
|
67
|
+
&& !changes.isIntercepting()
|
|
68
|
+
&& !control.isIntercepting()
|
|
69
|
+
&& !!((_a = control.get(EffectEventName)) === null || _a === void 0 ? void 0 : _a.hasListener());
|
|
70
|
+
};
|
|
13
71
|
const effectInterceptor = (name, args) => {
|
|
14
72
|
if (name === ChangeEventName) {
|
|
15
73
|
effectKeys.push(...args[0]);
|
|
16
74
|
return false;
|
|
17
75
|
}
|
|
76
|
+
// While the multi-key loop writes base values, swallow effect emissions;
|
|
77
|
+
// they are replayed once afterwards against the final state.
|
|
78
|
+
if (name === EffectEventName && deferEffects) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
18
81
|
return true;
|
|
19
82
|
};
|
|
20
83
|
const _set = (name, value, triggerChange = true) => {
|
|
@@ -98,12 +161,16 @@ export function createStore(initialData = {}) {
|
|
|
98
161
|
effectKeys = [];
|
|
99
162
|
return true;
|
|
100
163
|
}
|
|
164
|
+
// Clear before propagating: an unhandled throw here would
|
|
165
|
+
// otherwise leave the cascade's collected keys dirty for the
|
|
166
|
+
// next _set, which would report them as spuriously changed.
|
|
167
|
+
effectKeys = [];
|
|
101
168
|
throw error;
|
|
102
169
|
}
|
|
103
170
|
}
|
|
104
171
|
if (triggerChange) {
|
|
105
172
|
try {
|
|
106
|
-
control.trigger(ChangeEventName, [name, ...effectKeys]);
|
|
173
|
+
control.trigger(ChangeEventName, dedupe([name, ...effectKeys]));
|
|
107
174
|
if (!control.isIntercepting()) {
|
|
108
175
|
effectKeys = [];
|
|
109
176
|
}
|
|
@@ -121,6 +188,9 @@ export function createStore(initialData = {}) {
|
|
|
121
188
|
effectKeys = [];
|
|
122
189
|
return true;
|
|
123
190
|
}
|
|
191
|
+
// Clear before propagating (see the effect-trigger catch
|
|
192
|
+
// above): a leaked effectKeys would taint the next _set.
|
|
193
|
+
effectKeys = [];
|
|
124
194
|
throw error;
|
|
125
195
|
}
|
|
126
196
|
}
|
|
@@ -129,7 +199,12 @@ export function createStore(initialData = {}) {
|
|
|
129
199
|
return false;
|
|
130
200
|
};
|
|
131
201
|
function asyncSet(name, value) {
|
|
132
|
-
setTimeout(() => {
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
pendingTimers.delete(timer);
|
|
204
|
+
// The store may have been destroyed between scheduling and firing.
|
|
205
|
+
if (destroyed) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
133
208
|
if (typeof name === "string") {
|
|
134
209
|
set(name, value);
|
|
135
210
|
}
|
|
@@ -137,65 +212,228 @@ export function createStore(initialData = {}) {
|
|
|
137
212
|
set(name);
|
|
138
213
|
}
|
|
139
214
|
}, 0);
|
|
215
|
+
pendingTimers.add(timer);
|
|
140
216
|
}
|
|
141
|
-
|
|
217
|
+
// Replay a coalesced change log: one onChange per key, keeping the first
|
|
218
|
+
// entry's pre-cascade `prev` and the last entry's settled `value`, dropping
|
|
219
|
+
// keys whose net value is unchanged. Mirrors batch()'s replay (including its
|
|
220
|
+
// store-change error routing) so a computed touched several times during a
|
|
221
|
+
// cascade emits a single, internally-consistent onChange.
|
|
222
|
+
const replayCoalescedChanges = (log, hasCallbackError, callbackError) => {
|
|
223
|
+
var _a;
|
|
224
|
+
const coalesced = new Map();
|
|
225
|
+
for (const [propName, value, prev] of log) {
|
|
226
|
+
const existing = coalesced.get(propName);
|
|
227
|
+
if (existing) {
|
|
228
|
+
existing.value = value;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
coalesced.set(propName, { value, prev });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const [propName, { value, prev }] of coalesced) {
|
|
235
|
+
if (value === prev) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const changeArgs = [
|
|
239
|
+
value,
|
|
240
|
+
prev,
|
|
241
|
+
];
|
|
242
|
+
try {
|
|
243
|
+
changes.trigger(propName, ...changeArgs);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
control.trigger(ErrorEventName, {
|
|
247
|
+
error: error instanceof Error
|
|
248
|
+
? error
|
|
249
|
+
: new Error(String(error)),
|
|
250
|
+
args: changeArgs,
|
|
251
|
+
type: "store-change",
|
|
252
|
+
name: propName,
|
|
253
|
+
});
|
|
254
|
+
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (hasCallbackError) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (hasCallbackError) {
|
|
264
|
+
throw callbackError;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
// Run `fn` with onChange emissions intercepted and coalesced. The control
|
|
268
|
+
// change-key collection (effectKeys / effectInterceptor ordering) inside
|
|
269
|
+
// `fn` is untouched, so only the per-key onChange stream is deduped.
|
|
270
|
+
const withChangeCoalescing = (fn, liveKey) => {
|
|
271
|
+
const log = [];
|
|
272
|
+
let liveDelivered = false;
|
|
273
|
+
const logger = function (propName, args) {
|
|
274
|
+
// Deliver the directly-set key's onChange live (it fires inside _set
|
|
275
|
+
// before the effect cascade) so plain onChange listeners still run
|
|
276
|
+
// ahead of effect listeners; only the cascade's emissions are
|
|
277
|
+
// coalesced. Returning a non-false value lets the interceptor pass
|
|
278
|
+
// the event through to its listeners.
|
|
279
|
+
if (!liveDelivered && liveKey !== undefined && propName === liveKey) {
|
|
280
|
+
liveDelivered = true;
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
log.push([propName, args[0], args[1]]);
|
|
284
|
+
return false;
|
|
285
|
+
};
|
|
286
|
+
changes.intercept(logger);
|
|
287
|
+
let callbackError;
|
|
288
|
+
let hasCallbackError = false;
|
|
289
|
+
try {
|
|
290
|
+
fn();
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
callbackError = error;
|
|
294
|
+
hasCallbackError = true;
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
changes.stopIntercepting();
|
|
298
|
+
}
|
|
299
|
+
replayCoalescedChanges(log, hasCallbackError, callbackError);
|
|
300
|
+
};
|
|
301
|
+
// The write path shared by set() and its coalescing wrapper. Computed-key
|
|
302
|
+
// validation happens in set() before this runs.
|
|
303
|
+
const applySet = (name, value) => {
|
|
142
304
|
var _a, _b;
|
|
143
305
|
if (typeof name === "string") {
|
|
144
306
|
_set(name, value);
|
|
307
|
+
return;
|
|
145
308
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
309
|
+
const changedKeys = [];
|
|
310
|
+
const isIntercepting = control.isIntercepting();
|
|
311
|
+
const hasEffectListener = (_a = control.get(EffectEventName)) === null || _a === void 0 ? void 0 : _a.hasListener();
|
|
312
|
+
const shouldInterceptEffects = hasEffectListener && !isIntercepting;
|
|
313
|
+
let controlError = null;
|
|
314
|
+
if (shouldInterceptEffects) {
|
|
315
|
+
control.intercept(effectInterceptor);
|
|
316
|
+
computedBatch = new Map();
|
|
317
|
+
}
|
|
318
|
+
let allChangedKeys = [];
|
|
319
|
+
try {
|
|
320
|
+
// Phase 1: write every base value with effect emission deferred,
|
|
321
|
+
// so computed values do not recompute against a half-updated
|
|
322
|
+
// state mid-loop.
|
|
152
323
|
if (shouldInterceptEffects) {
|
|
153
|
-
|
|
324
|
+
deferEffects = true;
|
|
154
325
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
326
|
+
const entries = Object.entries(name);
|
|
327
|
+
entries.forEach(([k, v]) => {
|
|
328
|
+
if (_set(k, v, false)) {
|
|
329
|
+
changedKeys.push(k);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
// Phase 2: with all base values final, replay the effect once per
|
|
333
|
+
// changed key. `computedBatch` skips a computed whose dependency
|
|
334
|
+
// values are unchanged since its last recompute, while still
|
|
335
|
+
// letting chained computeds re-settle when an upstream updates.
|
|
336
|
+
if (shouldInterceptEffects) {
|
|
337
|
+
deferEffects = false;
|
|
338
|
+
changedKeys.forEach((k) => {
|
|
339
|
+
var _a;
|
|
340
|
+
// Mirror _set's effect error contract: a throwing effect
|
|
341
|
+
// listener routes to the error event (and is swallowed if
|
|
342
|
+
// a handler exists) rather than aborting the whole set.
|
|
166
343
|
try {
|
|
167
|
-
control.trigger(
|
|
344
|
+
control.trigger(EffectEventName, k, data.get(k));
|
|
168
345
|
}
|
|
169
346
|
catch (error) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
347
|
+
control.trigger(ErrorEventName, {
|
|
348
|
+
error: error instanceof Error
|
|
349
|
+
? error
|
|
350
|
+
: new Error(String(error)),
|
|
351
|
+
args: [k],
|
|
352
|
+
type: "store-control",
|
|
353
|
+
name: k,
|
|
354
|
+
});
|
|
355
|
+
if (!((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener())) {
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
173
358
|
}
|
|
174
|
-
}
|
|
359
|
+
});
|
|
175
360
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
361
|
+
allChangedKeys = dedupe([
|
|
362
|
+
...changedKeys,
|
|
363
|
+
...effectKeys,
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
if (shouldInterceptEffects) {
|
|
368
|
+
effectKeys = [];
|
|
369
|
+
deferEffects = false;
|
|
370
|
+
computedBatch = null;
|
|
371
|
+
control.stopIntercepting();
|
|
181
372
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
373
|
+
}
|
|
374
|
+
// Fire the outer change AFTER intercepting stops; otherwise the
|
|
375
|
+
// effectInterceptor (active during the loop to fold computed/effect
|
|
376
|
+
// writes into effectKeys) would swallow this trigger too.
|
|
377
|
+
if (allChangedKeys.length > 0) {
|
|
378
|
+
try {
|
|
379
|
+
control.trigger(ChangeEventName, allChangedKeys);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
controlError = error instanceof Error
|
|
383
|
+
? error
|
|
384
|
+
: new Error(String(error));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (controlError) {
|
|
388
|
+
control.trigger(ErrorEventName, {
|
|
389
|
+
error: controlError,
|
|
390
|
+
args: [name],
|
|
391
|
+
type: "store-control",
|
|
392
|
+
});
|
|
393
|
+
if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
throw controlError;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function set(name, value) {
|
|
400
|
+
assertAlive();
|
|
401
|
+
if (typeof name === "string") {
|
|
402
|
+
if (computedKeys.has(name)) {
|
|
403
|
+
throw new Error(`Cannot set computed property "${name}"`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else if (typeof name === "object") {
|
|
407
|
+
// Validate all keys before any write so a computed key in the patch
|
|
408
|
+
// throws without partially applying the others.
|
|
409
|
+
for (const k of Object.keys(name)) {
|
|
410
|
+
if (computedKeys.has(k)) {
|
|
411
|
+
throw new Error(`Cannot set computed property "${k}"`);
|
|
190
412
|
}
|
|
191
|
-
throw controlError;
|
|
192
413
|
}
|
|
193
414
|
}
|
|
194
415
|
else {
|
|
195
416
|
throw new Error(`Invalid key: ${String(name)}`);
|
|
196
417
|
}
|
|
418
|
+
// A cascade-triggering set coalesces its onChange emissions: a computed
|
|
419
|
+
// touched several times during the cascade (e.g. a diamond sink
|
|
420
|
+
// recomputed once per upstream) fires onChange once with its settled
|
|
421
|
+
// value and the real pre-set prev, instead of leaking an internally
|
|
422
|
+
// inconsistent intermediate (b_new + c_old) with a wrong prev. The
|
|
423
|
+
// control change-key collection is left untouched, so change-key
|
|
424
|
+
// ordering is unaffected.
|
|
425
|
+
if (canCoalesceCascade()) {
|
|
426
|
+
// For a single-key set, deliver that key's onChange live (before the
|
|
427
|
+
// effect cascade) and coalesce only the downstream computed-key
|
|
428
|
+
// emissions. A multi-key (object) set is an explicit batch, so all
|
|
429
|
+
// of its onChange emissions remain coalesced.
|
|
430
|
+
withChangeCoalescing(() => applySet(name, value), typeof name === "string" ? name : undefined);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
applySet(name, value);
|
|
197
434
|
}
|
|
198
435
|
const get = (key) => {
|
|
436
|
+
assertAlive();
|
|
199
437
|
if (typeof key === "string") {
|
|
200
438
|
const value = data.get(key);
|
|
201
439
|
return value;
|
|
@@ -257,7 +495,26 @@ export function createStore(initialData = {}) {
|
|
|
257
495
|
changes.stopIntercepting();
|
|
258
496
|
batching = false;
|
|
259
497
|
}
|
|
498
|
+
// Coalesce the log per key before replaying: a key written multiple
|
|
499
|
+
// times in the batch (e.g. a computed recomputing once per base-key
|
|
500
|
+
// write) must fire onChange once with its final value, not once per
|
|
501
|
+
// intermediate value. Keep first-occurrence order, the pre-batch `prev`
|
|
502
|
+
// from the first entry, and the final `value` from the last entry; drop
|
|
503
|
+
// keys whose net value is unchanged from before the batch.
|
|
504
|
+
const coalesced = new Map();
|
|
260
505
|
for (const [propName, value, prev] of log) {
|
|
506
|
+
const existing = coalesced.get(propName);
|
|
507
|
+
if (existing) {
|
|
508
|
+
existing.value = value;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
coalesced.set(propName, { value, prev });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const [propName, { value, prev }] of coalesced) {
|
|
515
|
+
if (value === prev) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
261
518
|
const changeArgs = [
|
|
262
519
|
value,
|
|
263
520
|
prev,
|
|
@@ -283,16 +540,20 @@ export function createStore(initialData = {}) {
|
|
|
283
540
|
throw error;
|
|
284
541
|
}
|
|
285
542
|
}
|
|
286
|
-
|
|
543
|
+
// Dedupe so the control change event lists each key once, matching the
|
|
544
|
+
// non-batch path (which dedupes via `dedupe([name, ...effectKeys])`).
|
|
545
|
+
// A computed touched by several base-key writes would otherwise repeat.
|
|
546
|
+
const dedupedChangedKeys = dedupe(allChangedKeys);
|
|
547
|
+
if (dedupedChangedKeys.length > 0) {
|
|
287
548
|
try {
|
|
288
|
-
control.trigger(ChangeEventName,
|
|
549
|
+
control.trigger(ChangeEventName, dedupedChangedKeys);
|
|
289
550
|
}
|
|
290
551
|
catch (error) {
|
|
291
552
|
control.trigger(ErrorEventName, {
|
|
292
553
|
error: error instanceof Error
|
|
293
554
|
? error
|
|
294
555
|
: new Error(String(error)),
|
|
295
|
-
args: [
|
|
556
|
+
args: [dedupedChangedKeys],
|
|
296
557
|
type: "store-control",
|
|
297
558
|
});
|
|
298
559
|
if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) {
|
|
@@ -313,16 +574,122 @@ export function createStore(initialData = {}) {
|
|
|
313
574
|
};
|
|
314
575
|
const reset = () => {
|
|
315
576
|
data.clear();
|
|
577
|
+
// Recompute computed keys from the cleared deps (registration order, so a
|
|
578
|
+
// base computed re-seeds before a dependent in a chain) and re-seed them
|
|
579
|
+
// silently, keeping each consistent with fn(deps) instead of stale.
|
|
580
|
+
computedReseeders.forEach((reseed) => {
|
|
581
|
+
reseed();
|
|
582
|
+
});
|
|
316
583
|
control.trigger(ResetEventName);
|
|
317
584
|
};
|
|
585
|
+
// One-call teardown: destroy the underlying change/pipe/control buses and
|
|
586
|
+
// drop all data. Post-destroy set/get throw rather than silently no-op.
|
|
587
|
+
const destroy = () => {
|
|
588
|
+
pendingTimers.forEach((timer) => clearTimeout(timer));
|
|
589
|
+
pendingTimers.clear();
|
|
590
|
+
changes.destroy();
|
|
591
|
+
pipe.destroy();
|
|
592
|
+
control.destroy();
|
|
593
|
+
data.clear();
|
|
594
|
+
computedKeys.clear();
|
|
595
|
+
computingKeys.clear();
|
|
596
|
+
computedReseeders.clear();
|
|
597
|
+
computedEffectListeners.clear();
|
|
598
|
+
effectKeys = [];
|
|
599
|
+
destroyed = true;
|
|
600
|
+
};
|
|
601
|
+
const isDestroyed = () => destroyed;
|
|
602
|
+
// Registers `key` as a derived value recomputed from `deps`. Built as sugar
|
|
603
|
+
// over the `effect` control event: recompute writes via the internal `_set`
|
|
604
|
+
// (triggerChange = true) so the change folds into the same outer `change`
|
|
605
|
+
// batch via `effectKeys`. Computed keys flow transparently through
|
|
606
|
+
// get/getData/onChange/useStoreState/useStoreSelector.
|
|
607
|
+
//
|
|
608
|
+
// Recompute is registration-order, not topologically sorted, so a dependent
|
|
609
|
+
// (chain or diamond) may recompute from a stale upstream first. The internal
|
|
610
|
+
// intermediate recompute is invisible to consumers: set() coalesces the
|
|
611
|
+
// onChange stream for a cascade, so each computed fires onChange once with
|
|
612
|
+
// its settled value and the real pre-set prev (`computedBatch` also dedupes
|
|
613
|
+
// redundant recomputes within a multi-key set). The final get()/onChange
|
|
614
|
+
// value is always correct.
|
|
615
|
+
const computed = (key, deps, fn) => {
|
|
616
|
+
// Bail before running user code or seeding data: on a destroyed store
|
|
617
|
+
// the control bus throws only when the recompute listener is attached,
|
|
618
|
+
// by which point fn() has already run and the initial value has been
|
|
619
|
+
// written, repopulating cleared state that getData() would then expose.
|
|
620
|
+
assertAlive();
|
|
621
|
+
const readDeps = () => deps.map((d) => data.get(d));
|
|
622
|
+
// Compute the initial value BEFORE committing any registration state.
|
|
623
|
+
// If `fn` throws here, nothing has been mutated, so the key does not
|
|
624
|
+
// become a permanently read-only computed with no listener installed.
|
|
625
|
+
const initialValue = fn(...readDeps());
|
|
626
|
+
// Seed silently (no change emitted at setup time) but through pipe, so
|
|
627
|
+
// the value read right after computed()/reset() matches what every
|
|
628
|
+
// later _set-driven recompute produces; otherwise a piped key silently
|
|
629
|
+
// changes shape on the first dependency change. beforeChange is skipped
|
|
630
|
+
// on purpose: a silent seed has no change to veto, and a computed key
|
|
631
|
+
// must always hold a value.
|
|
632
|
+
const seed = (raw) => {
|
|
633
|
+
const pipeArgs = [raw];
|
|
634
|
+
const piped = pipe.pipe(key, ...pipeArgs);
|
|
635
|
+
data.set(key, piped !== undefined ? piped : raw);
|
|
636
|
+
};
|
|
637
|
+
seed(initialValue);
|
|
638
|
+
computedKeys.add(key);
|
|
639
|
+
// reset() re-runs this to recompute the value from the cleared deps. It
|
|
640
|
+
// seeds `data` silently (no change emitted), matching this setup path.
|
|
641
|
+
computedReseeders.set(key, () => {
|
|
642
|
+
seed(fn(...readDeps()));
|
|
643
|
+
});
|
|
644
|
+
// Detach a prior recompute closure for this key (re-registration)
|
|
645
|
+
// before installing the new one, so the old fn does not keep running on
|
|
646
|
+
// every dependency change.
|
|
647
|
+
const prevEffect = computedEffectListeners.get(key);
|
|
648
|
+
if (prevEffect) {
|
|
649
|
+
control.removeListener(EffectEventName, prevEffect);
|
|
650
|
+
}
|
|
651
|
+
const effectListener = (changedName) => {
|
|
652
|
+
if (deps.indexOf(changedName) === -1) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const depValues = readDeps();
|
|
656
|
+
// Within a single multi-key set, skip the recompute only when this
|
|
657
|
+
// computed's dependency values are unchanged since its last
|
|
658
|
+
// recompute in the batch. That dedupes redundant dep changes while
|
|
659
|
+
// still re-settling a chain when an upstream computed updates.
|
|
660
|
+
if (computedBatch) {
|
|
661
|
+
const lastDepValues = computedBatch.get(key);
|
|
662
|
+
if (lastDepValues
|
|
663
|
+
&& arraysShallowEqual(lastDepValues, depValues)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
computedBatch.set(key, depValues);
|
|
667
|
+
}
|
|
668
|
+
if (computingKeys.has(key)) {
|
|
669
|
+
throw new Error(`Cyclic computed dependency detected at "${key}"`);
|
|
670
|
+
}
|
|
671
|
+
computingKeys.add(key);
|
|
672
|
+
try {
|
|
673
|
+
_set(key, fn(...depValues));
|
|
674
|
+
}
|
|
675
|
+
finally {
|
|
676
|
+
computingKeys.delete(key);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
control.addListener(EffectEventName, effectListener);
|
|
680
|
+
computedEffectListeners.set(key, effectListener);
|
|
681
|
+
};
|
|
318
682
|
const api = {
|
|
319
683
|
set,
|
|
320
684
|
get,
|
|
321
685
|
getData,
|
|
322
686
|
batch,
|
|
323
687
|
asyncSet,
|
|
688
|
+
computed,
|
|
324
689
|
isEmpty,
|
|
325
690
|
reset,
|
|
691
|
+
destroy,
|
|
692
|
+
isDestroyed,
|
|
326
693
|
onChange: changes.addListener,
|
|
327
694
|
removeOnChange: changes.removeListener,
|
|
328
695
|
updateOnChangeOptions: changes.updateListenerOptions,
|