@kuindji/reactive 1.1.0 → 1.3.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 +140 -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/listenerOptionsEqual.d.ts +14 -0
- package/dist/react/listenerOptionsEqual.js +26 -0
- package/dist/react/useActionBusStatus.d.ts +13 -0
- package/dist/react/useActionBusStatus.js +23 -0
- package/dist/react/useAsyncAction.d.ts +26 -0
- package/dist/react/useAsyncAction.js +61 -0
- package/dist/react/useReconciledListener.js +11 -2
- package/dist/react/useStoreSelector.d.ts +35 -0
- package/dist/react/useStoreSelector.js +153 -0
- package/dist/react/useStoreState.js +22 -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 +437 -75
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -10,20 +10,90 @@ 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
|
-
const _set = (name, value, triggerChange = true) => {
|
|
83
|
+
const _set = (name, value, triggerChange = true, runBeforeChange = true) => {
|
|
21
84
|
var _a, _b, _c, _d, _e;
|
|
22
85
|
const prev = data.get(name);
|
|
23
86
|
if (prev !== value) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
87
|
+
// A computed recompute skips the beforeChange veto (runBeforeChange
|
|
88
|
+
// false): the initial computed seed() bypasses beforeChange for the
|
|
89
|
+
// same reason, so allowing a veto here would leave the derived value
|
|
90
|
+
// stale and no longer equal to fn(deps) — internally inconsistent
|
|
91
|
+
// with the seeded value. beforeChange still gates ordinary sets.
|
|
92
|
+
if (runBeforeChange) {
|
|
93
|
+
const beforeChangeResults = control.all(BeforeChangeEventName, name, value);
|
|
94
|
+
if (beforeChangeResults.some((result) => result === false)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
27
97
|
}
|
|
28
98
|
const pipeArgs = [value];
|
|
29
99
|
let newValue;
|
|
@@ -98,12 +168,16 @@ export function createStore(initialData = {}) {
|
|
|
98
168
|
effectKeys = [];
|
|
99
169
|
return true;
|
|
100
170
|
}
|
|
171
|
+
// Clear before propagating: an unhandled throw here would
|
|
172
|
+
// otherwise leave the cascade's collected keys dirty for the
|
|
173
|
+
// next _set, which would report them as spuriously changed.
|
|
174
|
+
effectKeys = [];
|
|
101
175
|
throw error;
|
|
102
176
|
}
|
|
103
177
|
}
|
|
104
178
|
if (triggerChange) {
|
|
105
179
|
try {
|
|
106
|
-
control.trigger(ChangeEventName, [name, ...effectKeys]);
|
|
180
|
+
control.trigger(ChangeEventName, dedupe([name, ...effectKeys]));
|
|
107
181
|
if (!control.isIntercepting()) {
|
|
108
182
|
effectKeys = [];
|
|
109
183
|
}
|
|
@@ -121,6 +195,9 @@ export function createStore(initialData = {}) {
|
|
|
121
195
|
effectKeys = [];
|
|
122
196
|
return true;
|
|
123
197
|
}
|
|
198
|
+
// Clear before propagating (see the effect-trigger catch
|
|
199
|
+
// above): a leaked effectKeys would taint the next _set.
|
|
200
|
+
effectKeys = [];
|
|
124
201
|
throw error;
|
|
125
202
|
}
|
|
126
203
|
}
|
|
@@ -129,7 +206,28 @@ export function createStore(initialData = {}) {
|
|
|
129
206
|
return false;
|
|
130
207
|
};
|
|
131
208
|
function asyncSet(name, value) {
|
|
132
|
-
|
|
209
|
+
// Validate computed keys synchronously, mirroring set(). Deferring the
|
|
210
|
+
// check to the timer callback would turn a misuse into an uncaught
|
|
211
|
+
// exception escaping the timer (crashing Node / surfacing as an uncaught
|
|
212
|
+
// browser error) instead of a catchable throw at the call site.
|
|
213
|
+
if (typeof name === "string") {
|
|
214
|
+
if (computedKeys.has(name)) {
|
|
215
|
+
throw new Error(`Cannot set computed property "${name}"`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (typeof name === "object" && name !== null) {
|
|
219
|
+
for (const k of Object.keys(name)) {
|
|
220
|
+
if (computedKeys.has(k)) {
|
|
221
|
+
throw new Error(`Cannot set computed property "${k}"`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
pendingTimers.delete(timer);
|
|
227
|
+
// The store may have been destroyed between scheduling and firing.
|
|
228
|
+
if (destroyed) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
133
231
|
if (typeof name === "string") {
|
|
134
232
|
set(name, value);
|
|
135
233
|
}
|
|
@@ -137,65 +235,235 @@ export function createStore(initialData = {}) {
|
|
|
137
235
|
set(name);
|
|
138
236
|
}
|
|
139
237
|
}, 0);
|
|
238
|
+
pendingTimers.add(timer);
|
|
140
239
|
}
|
|
141
|
-
|
|
240
|
+
// Coalesce a change log per key and replay it as one onChange each, keeping
|
|
241
|
+
// the first entry's pre-cascade `prev` and the last entry's settled `value`
|
|
242
|
+
// and dropping keys whose net value is unchanged. Store-change errors route
|
|
243
|
+
// to the error event; an unhandled one propagates unless the surrounding
|
|
244
|
+
// callback already failed (`hasCallbackError`), in which case it is swallowed
|
|
245
|
+
// so the original callback error is the one that ultimately surfaces. Shared
|
|
246
|
+
// by batch() and the single-set cascade wrapper so both coalesce identically.
|
|
247
|
+
const replayCoalescedLog = (log, hasCallbackError) => {
|
|
248
|
+
var _a;
|
|
249
|
+
const coalesced = new Map();
|
|
250
|
+
for (const [propName, value, prev] of log) {
|
|
251
|
+
const existing = coalesced.get(propName);
|
|
252
|
+
if (existing) {
|
|
253
|
+
existing.value = value;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
coalesced.set(propName, { value, prev });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [propName, { value, prev }] of coalesced) {
|
|
260
|
+
if (value === prev) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const changeArgs = [
|
|
264
|
+
value,
|
|
265
|
+
prev,
|
|
266
|
+
];
|
|
267
|
+
try {
|
|
268
|
+
changes.trigger(propName, ...changeArgs);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
control.trigger(ErrorEventName, {
|
|
272
|
+
error: error instanceof Error
|
|
273
|
+
? error
|
|
274
|
+
: new Error(String(error)),
|
|
275
|
+
args: changeArgs,
|
|
276
|
+
type: "store-change",
|
|
277
|
+
name: propName,
|
|
278
|
+
});
|
|
279
|
+
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (hasCallbackError) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
// Replay a coalesced cascade log and, if the driving callback failed, rethrow
|
|
290
|
+
// its error last (after every surviving onChange has fired).
|
|
291
|
+
const replayCoalescedChanges = (log, hasCallbackError, callbackError) => {
|
|
292
|
+
replayCoalescedLog(log, hasCallbackError);
|
|
293
|
+
if (hasCallbackError) {
|
|
294
|
+
throw callbackError;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
// Run `fn` with onChange emissions intercepted and coalesced. The control
|
|
298
|
+
// change-key collection (effectKeys / effectInterceptor ordering) inside
|
|
299
|
+
// `fn` is untouched, so only the per-key onChange stream is deduped.
|
|
300
|
+
const withChangeCoalescing = (fn, liveKey) => {
|
|
301
|
+
const log = [];
|
|
302
|
+
let liveDelivered = false;
|
|
303
|
+
const logger = function (propName, args) {
|
|
304
|
+
// Deliver the directly-set key's onChange live (it fires inside _set
|
|
305
|
+
// before the effect cascade) so plain onChange listeners still run
|
|
306
|
+
// ahead of effect listeners; only the cascade's emissions are
|
|
307
|
+
// coalesced. Returning a non-false value lets the interceptor pass
|
|
308
|
+
// the event through to its listeners.
|
|
309
|
+
if (!liveDelivered && liveKey !== undefined && propName === liveKey) {
|
|
310
|
+
liveDelivered = true;
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
log.push([propName, args[0], args[1]]);
|
|
314
|
+
return false;
|
|
315
|
+
};
|
|
316
|
+
changes.intercept(logger);
|
|
317
|
+
let callbackError;
|
|
318
|
+
let hasCallbackError = false;
|
|
319
|
+
try {
|
|
320
|
+
fn();
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
callbackError = error;
|
|
324
|
+
hasCallbackError = true;
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
changes.stopIntercepting();
|
|
328
|
+
}
|
|
329
|
+
replayCoalescedChanges(log, hasCallbackError, callbackError);
|
|
330
|
+
};
|
|
331
|
+
// The write path shared by set() and its coalescing wrapper. Computed-key
|
|
332
|
+
// validation happens in set() before this runs.
|
|
333
|
+
const applySet = (name, value) => {
|
|
142
334
|
var _a, _b;
|
|
143
335
|
if (typeof name === "string") {
|
|
144
336
|
_set(name, value);
|
|
337
|
+
return;
|
|
145
338
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
339
|
+
const changedKeys = [];
|
|
340
|
+
const isIntercepting = control.isIntercepting();
|
|
341
|
+
const hasEffectListener = (_a = control.get(EffectEventName)) === null || _a === void 0 ? void 0 : _a.hasListener();
|
|
342
|
+
const shouldInterceptEffects = hasEffectListener && !isIntercepting;
|
|
343
|
+
let controlError = null;
|
|
344
|
+
if (shouldInterceptEffects) {
|
|
345
|
+
control.intercept(effectInterceptor);
|
|
346
|
+
computedBatch = new Map();
|
|
347
|
+
}
|
|
348
|
+
let allChangedKeys = [];
|
|
349
|
+
try {
|
|
350
|
+
// Phase 1: write every base value with effect emission deferred,
|
|
351
|
+
// so computed values do not recompute against a half-updated
|
|
352
|
+
// state mid-loop.
|
|
152
353
|
if (shouldInterceptEffects) {
|
|
153
|
-
|
|
354
|
+
deferEffects = true;
|
|
154
355
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
356
|
+
const entries = Object.entries(name);
|
|
357
|
+
entries.forEach(([k, v]) => {
|
|
358
|
+
if (_set(k, v, false)) {
|
|
359
|
+
changedKeys.push(k);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
// Phase 2: with all base values final, replay the effect once per
|
|
363
|
+
// changed key. `computedBatch` skips a computed whose dependency
|
|
364
|
+
// values are unchanged since its last recompute, while still
|
|
365
|
+
// letting chained computeds re-settle when an upstream updates.
|
|
366
|
+
if (shouldInterceptEffects) {
|
|
367
|
+
deferEffects = false;
|
|
368
|
+
changedKeys.forEach((k) => {
|
|
369
|
+
var _a;
|
|
370
|
+
// Mirror _set's effect error contract: a throwing effect
|
|
371
|
+
// listener routes to the error event (and is swallowed if
|
|
372
|
+
// a handler exists) rather than aborting the whole set.
|
|
166
373
|
try {
|
|
167
|
-
control.trigger(
|
|
374
|
+
control.trigger(EffectEventName, k, data.get(k));
|
|
168
375
|
}
|
|
169
376
|
catch (error) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
377
|
+
control.trigger(ErrorEventName, {
|
|
378
|
+
error: error instanceof Error
|
|
379
|
+
? error
|
|
380
|
+
: new Error(String(error)),
|
|
381
|
+
args: [k],
|
|
382
|
+
type: "store-control",
|
|
383
|
+
name: k,
|
|
384
|
+
});
|
|
385
|
+
if (!((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener())) {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
173
388
|
}
|
|
174
|
-
}
|
|
389
|
+
});
|
|
175
390
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
391
|
+
allChangedKeys = dedupe([
|
|
392
|
+
...changedKeys,
|
|
393
|
+
...effectKeys,
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
if (shouldInterceptEffects) {
|
|
398
|
+
effectKeys = [];
|
|
399
|
+
deferEffects = false;
|
|
400
|
+
computedBatch = null;
|
|
401
|
+
control.stopIntercepting();
|
|
181
402
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
403
|
+
}
|
|
404
|
+
// Fire the outer change AFTER intercepting stops; otherwise the
|
|
405
|
+
// effectInterceptor (active during the loop to fold computed/effect
|
|
406
|
+
// writes into effectKeys) would swallow this trigger too.
|
|
407
|
+
if (allChangedKeys.length > 0) {
|
|
408
|
+
try {
|
|
409
|
+
control.trigger(ChangeEventName, allChangedKeys);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
controlError = error instanceof Error
|
|
413
|
+
? error
|
|
414
|
+
: new Error(String(error));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (controlError) {
|
|
418
|
+
control.trigger(ErrorEventName, {
|
|
419
|
+
error: controlError,
|
|
420
|
+
args: [name],
|
|
421
|
+
type: "store-control",
|
|
422
|
+
});
|
|
423
|
+
if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
throw controlError;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
function set(name, value) {
|
|
430
|
+
assertAlive();
|
|
431
|
+
if (typeof name === "string") {
|
|
432
|
+
if (computedKeys.has(name)) {
|
|
433
|
+
throw new Error(`Cannot set computed property "${name}"`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else if (typeof name === "object") {
|
|
437
|
+
// Validate all keys before any write so a computed key in the patch
|
|
438
|
+
// throws without partially applying the others.
|
|
439
|
+
for (const k of Object.keys(name)) {
|
|
440
|
+
if (computedKeys.has(k)) {
|
|
441
|
+
throw new Error(`Cannot set computed property "${k}"`);
|
|
190
442
|
}
|
|
191
|
-
throw controlError;
|
|
192
443
|
}
|
|
193
444
|
}
|
|
194
445
|
else {
|
|
195
446
|
throw new Error(`Invalid key: ${String(name)}`);
|
|
196
447
|
}
|
|
448
|
+
// A cascade-triggering set coalesces its onChange emissions: a computed
|
|
449
|
+
// touched several times during the cascade (e.g. a diamond sink
|
|
450
|
+
// recomputed once per upstream) fires onChange once with its settled
|
|
451
|
+
// value and the real pre-set prev, instead of leaking an internally
|
|
452
|
+
// inconsistent intermediate (b_new + c_old) with a wrong prev. The
|
|
453
|
+
// control change-key collection is left untouched, so change-key
|
|
454
|
+
// ordering is unaffected.
|
|
455
|
+
if (canCoalesceCascade()) {
|
|
456
|
+
// For a single-key set, deliver that key's onChange live (before the
|
|
457
|
+
// effect cascade) and coalesce only the downstream computed-key
|
|
458
|
+
// emissions. A multi-key (object) set is an explicit batch, so all
|
|
459
|
+
// of its onChange emissions remain coalesced.
|
|
460
|
+
withChangeCoalescing(() => applySet(name, value), typeof name === "string" ? name : undefined);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
applySet(name, value);
|
|
197
464
|
}
|
|
198
465
|
const get = (key) => {
|
|
466
|
+
assertAlive();
|
|
199
467
|
if (typeof key === "string") {
|
|
200
468
|
const value = data.get(key);
|
|
201
469
|
return value;
|
|
@@ -223,7 +491,7 @@ export function createStore(initialData = {}) {
|
|
|
223
491
|
};
|
|
224
492
|
let batching = false;
|
|
225
493
|
const batch = (fn) => {
|
|
226
|
-
var _a
|
|
494
|
+
var _a;
|
|
227
495
|
if (batching) {
|
|
228
496
|
throw new Error("Nested batch() calls are not supported");
|
|
229
497
|
}
|
|
@@ -257,45 +525,30 @@ export function createStore(initialData = {}) {
|
|
|
257
525
|
changes.stopIntercepting();
|
|
258
526
|
batching = false;
|
|
259
527
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
528
|
+
// Coalesce the log per key before replaying: a key written multiple
|
|
529
|
+
// times in the batch (e.g. a computed recomputing once per base-key
|
|
530
|
+
// write) must fire onChange once with its final value, not once per
|
|
531
|
+
// intermediate value. Drop keys whose net value is unchanged from before
|
|
532
|
+
// the batch. The callback error (if any) is deferred and rethrown at the
|
|
533
|
+
// end so the partial-write control change event below still fires.
|
|
534
|
+
replayCoalescedLog(log, hasCallbackError);
|
|
535
|
+
// Dedupe so the control change event lists each key once, matching the
|
|
536
|
+
// non-batch path (which dedupes via `dedupe([name, ...effectKeys])`).
|
|
537
|
+
// A computed touched by several base-key writes would otherwise repeat.
|
|
538
|
+
const dedupedChangedKeys = dedupe(allChangedKeys);
|
|
539
|
+
if (dedupedChangedKeys.length > 0) {
|
|
265
540
|
try {
|
|
266
|
-
|
|
541
|
+
control.trigger(ChangeEventName, dedupedChangedKeys);
|
|
267
542
|
}
|
|
268
543
|
catch (error) {
|
|
269
544
|
control.trigger(ErrorEventName, {
|
|
270
545
|
error: error instanceof Error
|
|
271
546
|
? error
|
|
272
547
|
: new Error(String(error)),
|
|
273
|
-
args:
|
|
274
|
-
type: "store-change",
|
|
275
|
-
name: propName,
|
|
276
|
-
});
|
|
277
|
-
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
if (hasCallbackError) {
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
throw error;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (allChangedKeys.length > 0) {
|
|
287
|
-
try {
|
|
288
|
-
control.trigger(ChangeEventName, allChangedKeys);
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
control.trigger(ErrorEventName, {
|
|
292
|
-
error: error instanceof Error
|
|
293
|
-
? error
|
|
294
|
-
: new Error(String(error)),
|
|
295
|
-
args: [allChangedKeys],
|
|
548
|
+
args: [dedupedChangedKeys],
|
|
296
549
|
type: "store-control",
|
|
297
550
|
});
|
|
298
|
-
if ((
|
|
551
|
+
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
|
|
299
552
|
if (hasCallbackError) {
|
|
300
553
|
throw callbackError;
|
|
301
554
|
}
|
|
@@ -313,16 +566,125 @@ export function createStore(initialData = {}) {
|
|
|
313
566
|
};
|
|
314
567
|
const reset = () => {
|
|
315
568
|
data.clear();
|
|
569
|
+
// Recompute computed keys from the cleared deps (registration order, so a
|
|
570
|
+
// base computed re-seeds before a dependent in a chain) and re-seed them
|
|
571
|
+
// silently, keeping each consistent with fn(deps) instead of stale.
|
|
572
|
+
computedReseeders.forEach((reseed) => {
|
|
573
|
+
reseed();
|
|
574
|
+
});
|
|
316
575
|
control.trigger(ResetEventName);
|
|
317
576
|
};
|
|
577
|
+
// One-call teardown: destroy the underlying change/pipe/control buses and
|
|
578
|
+
// drop all data. Post-destroy set/get throw rather than silently no-op.
|
|
579
|
+
const destroy = () => {
|
|
580
|
+
pendingTimers.forEach((timer) => clearTimeout(timer));
|
|
581
|
+
pendingTimers.clear();
|
|
582
|
+
changes.destroy();
|
|
583
|
+
pipe.destroy();
|
|
584
|
+
control.destroy();
|
|
585
|
+
data.clear();
|
|
586
|
+
computedKeys.clear();
|
|
587
|
+
computingKeys.clear();
|
|
588
|
+
computedReseeders.clear();
|
|
589
|
+
computedEffectListeners.clear();
|
|
590
|
+
effectKeys = [];
|
|
591
|
+
destroyed = true;
|
|
592
|
+
};
|
|
593
|
+
const isDestroyed = () => destroyed;
|
|
594
|
+
// Registers `key` as a derived value recomputed from `deps`. Built as sugar
|
|
595
|
+
// over the `effect` control event: recompute writes via the internal `_set`
|
|
596
|
+
// (triggerChange = true) so the change folds into the same outer `change`
|
|
597
|
+
// batch via `effectKeys`. Computed keys flow transparently through
|
|
598
|
+
// get/getData/onChange/useStoreState/useStoreSelector.
|
|
599
|
+
//
|
|
600
|
+
// Recompute is registration-order, not topologically sorted, so a dependent
|
|
601
|
+
// (chain or diamond) may recompute from a stale upstream first. The internal
|
|
602
|
+
// intermediate recompute is invisible to consumers: set() coalesces the
|
|
603
|
+
// onChange stream for a cascade, so each computed fires onChange once with
|
|
604
|
+
// its settled value and the real pre-set prev (`computedBatch` also dedupes
|
|
605
|
+
// redundant recomputes within a multi-key set). The final get()/onChange
|
|
606
|
+
// value is always correct.
|
|
607
|
+
const computed = (key, deps, fn) => {
|
|
608
|
+
// Bail before running user code or seeding data: on a destroyed store
|
|
609
|
+
// the control bus throws only when the recompute listener is attached,
|
|
610
|
+
// by which point fn() has already run and the initial value has been
|
|
611
|
+
// written, repopulating cleared state that getData() would then expose.
|
|
612
|
+
assertAlive();
|
|
613
|
+
const readDeps = () => deps.map((d) => data.get(d));
|
|
614
|
+
// Compute the initial value BEFORE committing any registration state.
|
|
615
|
+
// If `fn` throws here, nothing has been mutated, so the key does not
|
|
616
|
+
// become a permanently read-only computed with no listener installed.
|
|
617
|
+
const initialValue = fn(...readDeps());
|
|
618
|
+
// Seed silently (no change emitted at setup time) but through pipe, so
|
|
619
|
+
// the value read right after computed()/reset() matches what every
|
|
620
|
+
// later _set-driven recompute produces; otherwise a piped key silently
|
|
621
|
+
// changes shape on the first dependency change. beforeChange is skipped
|
|
622
|
+
// on purpose: a silent seed has no change to veto, and a computed key
|
|
623
|
+
// must always hold a value.
|
|
624
|
+
const seed = (raw) => {
|
|
625
|
+
const pipeArgs = [raw];
|
|
626
|
+
const piped = pipe.pipe(key, ...pipeArgs);
|
|
627
|
+
data.set(key, piped !== undefined ? piped : raw);
|
|
628
|
+
};
|
|
629
|
+
seed(initialValue);
|
|
630
|
+
computedKeys.add(key);
|
|
631
|
+
// reset() re-runs this to recompute the value from the cleared deps. It
|
|
632
|
+
// seeds `data` silently (no change emitted), matching this setup path.
|
|
633
|
+
computedReseeders.set(key, () => {
|
|
634
|
+
seed(fn(...readDeps()));
|
|
635
|
+
});
|
|
636
|
+
// Detach a prior recompute closure for this key (re-registration)
|
|
637
|
+
// before installing the new one, so the old fn does not keep running on
|
|
638
|
+
// every dependency change.
|
|
639
|
+
const prevEffect = computedEffectListeners.get(key);
|
|
640
|
+
if (prevEffect) {
|
|
641
|
+
control.removeListener(EffectEventName, prevEffect);
|
|
642
|
+
}
|
|
643
|
+
const effectListener = (changedName) => {
|
|
644
|
+
if (deps.indexOf(changedName) === -1) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const depValues = readDeps();
|
|
648
|
+
// Within a single multi-key set, skip the recompute only when this
|
|
649
|
+
// computed's dependency values are unchanged since its last
|
|
650
|
+
// recompute in the batch. That dedupes redundant dep changes while
|
|
651
|
+
// still re-settling a chain when an upstream computed updates.
|
|
652
|
+
if (computedBatch) {
|
|
653
|
+
const lastDepValues = computedBatch.get(key);
|
|
654
|
+
if (lastDepValues
|
|
655
|
+
&& arraysShallowEqual(lastDepValues, depValues)) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
computedBatch.set(key, depValues);
|
|
659
|
+
}
|
|
660
|
+
if (computingKeys.has(key)) {
|
|
661
|
+
throw new Error(`Cyclic computed dependency detected at "${key}"`);
|
|
662
|
+
}
|
|
663
|
+
computingKeys.add(key);
|
|
664
|
+
try {
|
|
665
|
+
// runBeforeChange=false: a computed key is derived and must
|
|
666
|
+
// always hold fn(deps); a beforeChange veto here would strand a
|
|
667
|
+
// stale value (see _set and the seed() rationale).
|
|
668
|
+
_set(key, fn(...depValues), true, false);
|
|
669
|
+
}
|
|
670
|
+
finally {
|
|
671
|
+
computingKeys.delete(key);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
control.addListener(EffectEventName, effectListener);
|
|
675
|
+
computedEffectListeners.set(key, effectListener);
|
|
676
|
+
};
|
|
318
677
|
const api = {
|
|
319
678
|
set,
|
|
320
679
|
get,
|
|
321
680
|
getData,
|
|
322
681
|
batch,
|
|
323
682
|
asyncSet,
|
|
683
|
+
computed,
|
|
324
684
|
isEmpty,
|
|
325
685
|
reset,
|
|
686
|
+
destroy,
|
|
687
|
+
isDestroyed,
|
|
326
688
|
onChange: changes.addListener,
|
|
327
689
|
removeOnChange: changes.removeListener,
|
|
328
690
|
updateOnChangeOptions: changes.updateListenerOptions,
|