@kuindji/reactive 1.0.24 → 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 +160 -14
- package/dist/action.d.ts +31 -10
- package/dist/action.js +156 -23
- package/dist/actionBus.d.ts +13 -4
- package/dist/actionBus.js +201 -5
- package/dist/actionMap.d.ts +26 -19
- package/dist/actionMap.js +10 -4
- package/dist/event.d.ts +37 -3
- package/dist/event.js +345 -78
- package/dist/eventBus.d.ts +7 -3
- package/dist/eventBus.js +194 -34
- package/dist/index.d.ts +7 -7
- package/dist/index.js +7 -7
- package/dist/lib/actionMapInternal.d.ts +8 -0
- package/dist/lib/actionMapInternal.js +8 -0
- package/dist/lib/isPromiseLike.d.ts +1 -0
- package/dist/lib/isPromiseLike.js +5 -0
- package/dist/lib/normalizeEventOptions.d.ts +13 -0
- package/dist/lib/normalizeEventOptions.js +21 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/react/ErrorBoundary.d.ts +1 -1
- package/dist/react/listenerOptionsEqual.d.ts +27 -0
- package/dist/react/listenerOptionsEqual.js +121 -0
- package/dist/react/useAction.d.ts +3 -3
- package/dist/react/useAction.js +10 -7
- package/dist/react/useActionBus.d.ts +4 -4
- package/dist/react/useActionBus.js +32 -2
- package/dist/react/useActionBusStatus.d.ts +13 -0
- package/dist/react/useActionBusStatus.js +26 -0
- package/dist/react/useActionMap.d.ts +4 -4
- package/dist/react/useActionMap.js +40 -7
- package/dist/react/useAsyncAction.d.ts +20 -0
- package/dist/react/useAsyncAction.js +53 -0
- package/dist/react/useEvent.d.ts +2 -2
- package/dist/react/useEvent.js +18 -2
- package/dist/react/useEventBus.d.ts +2 -2
- package/dist/react/useEventBus.js +14 -10
- package/dist/react/useListenToAction.d.ts +1 -1
- package/dist/react/useListenToAction.js +17 -38
- package/dist/react/useListenToActionBus.d.ts +3 -3
- package/dist/react/useListenToActionBus.js +15 -9
- package/dist/react/useListenToEvent.d.ts +2 -2
- package/dist/react/useListenToEvent.js +8 -6
- package/dist/react/useListenToEventBus.d.ts +3 -3
- package/dist/react/useListenToEventBus.js +9 -7
- package/dist/react/useListenToStoreChanges.d.ts +3 -3
- package/dist/react/useListenToStoreChanges.js +9 -7
- package/dist/react/useReconciledListener.d.ts +33 -0
- package/dist/react/useReconciledListener.js +44 -0
- package/dist/react/useStore.d.ts +2 -2
- package/dist/react/useStore.js +71 -19
- package/dist/react/useStoreSelector.d.ts +35 -0
- package/dist/react/useStoreSelector.js +144 -0
- package/dist/react/useStoreState.d.ts +2 -2
- package/dist/react/useStoreState.js +26 -21
- package/dist/react.d.ts +16 -13
- package/dist/react.js +16 -13
- package/dist/store.d.ts +12 -8
- package/dist/store.js +473 -39
- package/package.json +13 -3
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EventBusDefinitionHelper } from "./eventBus";
|
|
2
|
-
import type { ApiType, ErrorListenerSignature, KeyOf, MapKey } from "./lib/types";
|
|
1
|
+
import { EventBusDefinitionHelper } from "./eventBus.js";
|
|
2
|
+
import type { ApiType, ErrorListenerSignature, KeyOf, MapKey } from "./lib/types.js";
|
|
3
3
|
export interface BasePropMap {
|
|
4
4
|
[key: MapKey]: any;
|
|
5
5
|
}
|
|
@@ -42,14 +42,18 @@ export declare function createStore<PropMap extends BasePropMap = BasePropMap>(i
|
|
|
42
42
|
<K extends KeyOf<PropMap>>(key: K, value: PropMap[K] | undefined): void;
|
|
43
43
|
(key: Partial<PropMap>): void;
|
|
44
44
|
};
|
|
45
|
+
readonly computed: <K extends KeyOf<PropMap>, const D extends readonly KeyOf<PropMap>[]>(key: K, deps: D, fn: (...values: { [I in keyof D]: PropMap[D[I]] | undefined; }) => PropMap[K]) => void;
|
|
45
46
|
readonly isEmpty: () => boolean;
|
|
46
47
|
readonly reset: () => void;
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
50
|
-
readonly
|
|
51
|
-
readonly
|
|
52
|
-
readonly
|
|
48
|
+
readonly destroy: () => void;
|
|
49
|
+
readonly isDestroyed: () => boolean;
|
|
50
|
+
readonly onChange: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StoreChangeEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StoreChangeEvents<PropMap>>[K]["signature"]>(name: K, handler: H, options?: import("./event.js").ListenerOptions) => void;
|
|
51
|
+
readonly removeOnChange: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StoreChangeEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StoreChangeEvents<PropMap>>[K]["signature"]>(name: K, handler: H, context?: object | null, tag?: string | null) => void;
|
|
52
|
+
readonly updateOnChangeOptions: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StoreChangeEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StoreChangeEvents<PropMap>>[K]["signature"]>(name: K, handler: H, context?: object | null, nextOptions?: import("./event.js").ListenerOptions) => boolean;
|
|
53
|
+
readonly control: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StoreControlEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StoreControlEvents<PropMap>>[K]["signature"]>(name: K, handler: H, options?: import("./event.js").ListenerOptions) => void;
|
|
54
|
+
readonly removeControl: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StoreControlEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StoreControlEvents<PropMap>>[K]["signature"]>(name: K, handler: H, context?: object | null, tag?: string | null) => void;
|
|
55
|
+
readonly pipe: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StorePipeEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StorePipeEvents<PropMap>>[K]["signature"]>(name: K, handler: H, options?: import("./event.js").ListenerOptions) => void;
|
|
56
|
+
readonly removePipe: <K extends KeyOf<import("./eventBus.js").GetEventsMap<StorePipeEvents<PropMap>>>, H extends import("./eventBus.js").GetEventsMap<StorePipeEvents<PropMap>>[K]["signature"]>(name: K, handler: H, context?: object | null, tag?: string | null) => void;
|
|
53
57
|
}>;
|
|
54
58
|
export type BaseStoreDefinition = StoreDefinitionHelper<BasePropMap>;
|
|
55
59
|
export type BaseStore = ReturnType<typeof createStore<any>>;
|
package/dist/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEventBus } from "./eventBus";
|
|
1
|
+
import { createEventBus } from "./eventBus.js";
|
|
2
2
|
export const BeforeChangeEventName = "before";
|
|
3
3
|
export const ChangeEventName = "change";
|
|
4
4
|
export const ResetEventName = "reset";
|
|
@@ -10,19 +10,82 @@ 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) => {
|
|
21
84
|
var _a, _b, _c, _d, _e;
|
|
22
85
|
const prev = data.get(name);
|
|
23
86
|
if (prev !== value) {
|
|
24
|
-
|
|
25
|
-
|
|
87
|
+
const beforeChangeResults = control.all(BeforeChangeEventName, name, value);
|
|
88
|
+
if (beforeChangeResults.some((result) => result === false)) {
|
|
26
89
|
return;
|
|
27
90
|
}
|
|
28
91
|
const pipeArgs = [value];
|
|
@@ -73,12 +136,16 @@ export function createStore(initialData = {}) {
|
|
|
73
136
|
if ((_c = control.get(EffectEventName)) === null || _c === void 0 ? void 0 : _c.hasListener()) {
|
|
74
137
|
try {
|
|
75
138
|
const isIntercepting = control.isIntercepting();
|
|
76
|
-
|
|
77
|
-
|
|
139
|
+
try {
|
|
140
|
+
if (!isIntercepting) {
|
|
141
|
+
control.intercept(effectInterceptor);
|
|
142
|
+
}
|
|
143
|
+
control.trigger(EffectEventName, name, value);
|
|
78
144
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
145
|
+
finally {
|
|
146
|
+
if (!isIntercepting) {
|
|
147
|
+
control.stopIntercepting();
|
|
148
|
+
}
|
|
82
149
|
}
|
|
83
150
|
}
|
|
84
151
|
catch (error) {
|
|
@@ -94,12 +161,16 @@ export function createStore(initialData = {}) {
|
|
|
94
161
|
effectKeys = [];
|
|
95
162
|
return true;
|
|
96
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 = [];
|
|
97
168
|
throw error;
|
|
98
169
|
}
|
|
99
170
|
}
|
|
100
171
|
if (triggerChange) {
|
|
101
172
|
try {
|
|
102
|
-
control.trigger(ChangeEventName, [name, ...effectKeys]);
|
|
173
|
+
control.trigger(ChangeEventName, dedupe([name, ...effectKeys]));
|
|
103
174
|
if (!control.isIntercepting()) {
|
|
104
175
|
effectKeys = [];
|
|
105
176
|
}
|
|
@@ -117,6 +188,9 @@ export function createStore(initialData = {}) {
|
|
|
117
188
|
effectKeys = [];
|
|
118
189
|
return true;
|
|
119
190
|
}
|
|
191
|
+
// Clear before propagating (see the effect-trigger catch
|
|
192
|
+
// above): a leaked effectKeys would taint the next _set.
|
|
193
|
+
effectKeys = [];
|
|
120
194
|
throw error;
|
|
121
195
|
}
|
|
122
196
|
}
|
|
@@ -125,7 +199,12 @@ export function createStore(initialData = {}) {
|
|
|
125
199
|
return false;
|
|
126
200
|
};
|
|
127
201
|
function asyncSet(name, value) {
|
|
128
|
-
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
|
+
}
|
|
129
208
|
if (typeof name === "string") {
|
|
130
209
|
set(name, value);
|
|
131
210
|
}
|
|
@@ -133,53 +212,228 @@ export function createStore(initialData = {}) {
|
|
|
133
212
|
set(name);
|
|
134
213
|
}
|
|
135
214
|
}, 0);
|
|
215
|
+
pendingTimers.add(timer);
|
|
136
216
|
}
|
|
137
|
-
|
|
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) => {
|
|
138
304
|
var _a, _b;
|
|
139
305
|
if (typeof name === "string") {
|
|
140
306
|
_set(name, value);
|
|
307
|
+
return;
|
|
141
308
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
323
|
+
if (shouldInterceptEffects) {
|
|
324
|
+
deferEffects = true;
|
|
148
325
|
}
|
|
149
|
-
Object.entries(name)
|
|
326
|
+
const entries = Object.entries(name);
|
|
327
|
+
entries.forEach(([k, v]) => {
|
|
150
328
|
if (_set(k, v, false)) {
|
|
151
329
|
changedKeys.push(k);
|
|
152
330
|
}
|
|
153
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.
|
|
343
|
+
try {
|
|
344
|
+
control.trigger(EffectEventName, k, data.get(k));
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
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
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
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();
|
|
372
|
+
}
|
|
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) {
|
|
154
378
|
try {
|
|
155
|
-
control.trigger(ChangeEventName,
|
|
156
|
-
...changedKeys,
|
|
157
|
-
...effectKeys,
|
|
158
|
-
]);
|
|
159
|
-
if (hasEffectListener && !isIntercepting) {
|
|
160
|
-
effectKeys = [];
|
|
161
|
-
control.stopIntercepting();
|
|
162
|
-
}
|
|
379
|
+
control.trigger(ChangeEventName, allChangedKeys);
|
|
163
380
|
}
|
|
164
381
|
catch (error) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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}"`);
|
|
174
412
|
}
|
|
175
|
-
throw error;
|
|
176
413
|
}
|
|
177
414
|
}
|
|
178
415
|
else {
|
|
179
416
|
throw new Error(`Invalid key: ${String(name)}`);
|
|
180
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);
|
|
181
434
|
}
|
|
182
435
|
const get = (key) => {
|
|
436
|
+
assertAlive();
|
|
183
437
|
if (typeof key === "string") {
|
|
184
438
|
const value = data.get(key);
|
|
185
439
|
return value;
|
|
@@ -207,6 +461,7 @@ export function createStore(initialData = {}) {
|
|
|
207
461
|
};
|
|
208
462
|
let batching = false;
|
|
209
463
|
const batch = (fn) => {
|
|
464
|
+
var _a, _b;
|
|
210
465
|
if (batching) {
|
|
211
466
|
throw new Error("Nested batch() calls are not supported");
|
|
212
467
|
}
|
|
@@ -226,39 +481,218 @@ export function createStore(initialData = {}) {
|
|
|
226
481
|
};
|
|
227
482
|
changes.intercept(changeInterceptor);
|
|
228
483
|
control.intercept(controlInterceptor);
|
|
484
|
+
let callbackError;
|
|
485
|
+
let hasCallbackError = false;
|
|
229
486
|
try {
|
|
230
487
|
fn();
|
|
231
488
|
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
callbackError = error;
|
|
491
|
+
hasCallbackError = true;
|
|
492
|
+
}
|
|
232
493
|
finally {
|
|
233
494
|
control.stopIntercepting();
|
|
234
495
|
changes.stopIntercepting();
|
|
235
496
|
batching = false;
|
|
236
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();
|
|
237
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
|
+
}
|
|
238
518
|
const changeArgs = [
|
|
239
519
|
value,
|
|
240
520
|
prev,
|
|
241
521
|
];
|
|
242
|
-
|
|
522
|
+
try {
|
|
523
|
+
changes.trigger(propName, ...changeArgs);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
control.trigger(ErrorEventName, {
|
|
527
|
+
error: error instanceof Error
|
|
528
|
+
? error
|
|
529
|
+
: new Error(String(error)),
|
|
530
|
+
args: changeArgs,
|
|
531
|
+
type: "store-change",
|
|
532
|
+
name: propName,
|
|
533
|
+
});
|
|
534
|
+
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (hasCallbackError) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
243
542
|
}
|
|
244
|
-
|
|
245
|
-
|
|
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) {
|
|
548
|
+
try {
|
|
549
|
+
control.trigger(ChangeEventName, dedupedChangedKeys);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
control.trigger(ErrorEventName, {
|
|
553
|
+
error: error instanceof Error
|
|
554
|
+
? error
|
|
555
|
+
: new Error(String(error)),
|
|
556
|
+
args: [dedupedChangedKeys],
|
|
557
|
+
type: "store-control",
|
|
558
|
+
});
|
|
559
|
+
if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) {
|
|
560
|
+
if (hasCallbackError) {
|
|
561
|
+
throw callbackError;
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (hasCallbackError) {
|
|
566
|
+
throw callbackError;
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (hasCallbackError) {
|
|
572
|
+
throw callbackError;
|
|
246
573
|
}
|
|
247
574
|
};
|
|
248
575
|
const reset = () => {
|
|
249
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
|
+
});
|
|
250
583
|
control.trigger(ResetEventName);
|
|
251
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
|
+
};
|
|
252
682
|
const api = {
|
|
253
683
|
set,
|
|
254
684
|
get,
|
|
255
685
|
getData,
|
|
256
686
|
batch,
|
|
257
687
|
asyncSet,
|
|
688
|
+
computed,
|
|
258
689
|
isEmpty,
|
|
259
690
|
reset,
|
|
691
|
+
destroy,
|
|
692
|
+
isDestroyed,
|
|
260
693
|
onChange: changes.addListener,
|
|
261
694
|
removeOnChange: changes.removeListener,
|
|
695
|
+
updateOnChangeOptions: changes.updateListenerOptions,
|
|
262
696
|
control: control.addListener,
|
|
263
697
|
removeControl: control.removeListener,
|
|
264
698
|
pipe: pipe.addListener,
|