@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/event.js
CHANGED
|
@@ -9,24 +9,37 @@ export function createEvent(eventOptions = {}) {
|
|
|
9
9
|
let queue = [];
|
|
10
10
|
let suspended = false;
|
|
11
11
|
let queued = false;
|
|
12
|
+
let destroyed = false;
|
|
12
13
|
let triggered = 0;
|
|
13
14
|
let lastTrigger = null;
|
|
15
|
+
// The args replayed to a late autoTrigger listener. Recorded only while
|
|
16
|
+
// autoTrigger is enabled, so enabling it *after* a trigger does not replay
|
|
17
|
+
// that earlier (pre-enablement) trigger. Kept separate from `lastTrigger`,
|
|
18
|
+
// which is recorded on every trigger purely for `lastTriggerArgs`.
|
|
19
|
+
let autoTriggerArgs = null;
|
|
14
20
|
let sortListeners = false;
|
|
15
21
|
let currentTagsFilter = null;
|
|
16
22
|
const options = Object.assign({ async: null, limit: null, autoTrigger: null, filter: null, filterContext: null, maxListeners: 0 }, eventOptions);
|
|
17
23
|
const addListener = (handler, listenerOptions = {}) => {
|
|
18
|
-
var _a;
|
|
24
|
+
var _a, _b;
|
|
25
|
+
if (destroyed) {
|
|
26
|
+
throw new Error("Event is destroyed");
|
|
27
|
+
}
|
|
19
28
|
if (!handler) {
|
|
20
29
|
return;
|
|
21
30
|
}
|
|
22
|
-
const
|
|
31
|
+
const signal = (_a = listenerOptions.signal) !== null && _a !== void 0 ? _a : null;
|
|
32
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const listenerContext = (_b = listenerOptions.context) !== null && _b !== void 0 ? _b : null;
|
|
23
36
|
if (listeners.find((l) => l.handler === handler && l.context === listenerContext)) {
|
|
24
37
|
return;
|
|
25
38
|
}
|
|
26
39
|
if (options.maxListeners && listeners.length >= options.maxListeners) {
|
|
27
40
|
throw new Error(`Max listeners (${options.maxListeners}) reached`);
|
|
28
41
|
}
|
|
29
|
-
const listener = Object.assign({ handler, called: 0, count: 0, index: listeners.length, start: 1, context: null, tags: [], extraData: null, first: false, alwaysFirst: false, alwaysLast: false, limit: 0, async: null }, listenerOptions);
|
|
42
|
+
const listener = Object.assign(Object.assign({ handler, called: 0, count: 0, index: listeners.length, start: 1, context: null, tags: [], extraData: null, first: false, alwaysFirst: false, alwaysLast: false, limit: 0, async: null }, listenerOptions), { abortCleanup: null });
|
|
30
43
|
if (listener.async === true) {
|
|
31
44
|
listener.async = 1;
|
|
32
45
|
}
|
|
@@ -47,25 +60,32 @@ export function createEvent(eventOptions = {}) {
|
|
|
47
60
|
|| (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.alwaysLast) === true) {
|
|
48
61
|
sortListeners = true;
|
|
49
62
|
}
|
|
63
|
+
if (signal) {
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
removeListener(handler, listenerContext);
|
|
66
|
+
};
|
|
67
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
68
|
+
listener.abortCleanup = () => {
|
|
69
|
+
signal.removeEventListener("abort", onAbort);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
50
72
|
if (options.autoTrigger
|
|
51
|
-
&&
|
|
73
|
+
&& autoTriggerArgs !== null
|
|
52
74
|
&& !suspended) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
options.filter = prevFilter;
|
|
65
|
-
}
|
|
75
|
+
// Replay the last enabled trigger, but only into the listener just
|
|
76
|
+
// added. The replay target is passed explicitly to `_trigger` (not
|
|
77
|
+
// via shared closure/`options.filter` state) so that a real trigger
|
|
78
|
+
// fired synchronously by the replayed handler is an ordinary real
|
|
79
|
+
// trigger — full bookkeeping, limit enforcement, and delivery to all
|
|
80
|
+
// listeners — rather than inheriting this replay's suppression.
|
|
81
|
+
_trigger(autoTriggerArgs, null, null, {
|
|
82
|
+
handler,
|
|
83
|
+
context: listenerContext !== null && listenerContext !== void 0 ? listenerContext : null,
|
|
84
|
+
});
|
|
66
85
|
}
|
|
67
86
|
};
|
|
68
87
|
const removeListener = (handler, context, tag) => {
|
|
88
|
+
var _a;
|
|
69
89
|
const inx = listeners.findIndex((l) => {
|
|
70
90
|
if (l.handler !== handler) {
|
|
71
91
|
return false;
|
|
@@ -84,11 +104,12 @@ export function createEvent(eventOptions = {}) {
|
|
|
84
104
|
if (inx === -1) {
|
|
85
105
|
return false;
|
|
86
106
|
}
|
|
87
|
-
listeners.splice(inx, 1);
|
|
107
|
+
const [removed] = listeners.splice(inx, 1);
|
|
108
|
+
(_a = removed === null || removed === void 0 ? void 0 : removed.abortCleanup) === null || _a === void 0 ? void 0 : _a.call(removed);
|
|
88
109
|
return true;
|
|
89
110
|
};
|
|
90
111
|
const updateListenerOptions = (handler, context = null, nextOptions = {}) => {
|
|
91
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
112
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
92
113
|
const listenerContext = context !== null && context !== void 0 ? context : null;
|
|
93
114
|
const listener = listeners.find((l) => l.handler === handler && l.context === listenerContext);
|
|
94
115
|
if (!listener) {
|
|
@@ -96,19 +117,35 @@ export function createEvent(eventOptions = {}) {
|
|
|
96
117
|
}
|
|
97
118
|
const prevAlwaysFirst = listener.alwaysFirst;
|
|
98
119
|
const prevAlwaysLast = listener.alwaysLast;
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
// Partial update: only fields explicitly present in nextOptions change;
|
|
121
|
+
// any omitted field keeps its current value (a caller changing one
|
|
122
|
+
// option does not silently reset the others). Pass a field explicitly
|
|
123
|
+
// (e.g. limit: 0, signal: null) to clear it.
|
|
124
|
+
if ("limit" in nextOptions) {
|
|
125
|
+
listener.limit = (_a = nextOptions.limit) !== null && _a !== void 0 ? _a : 0;
|
|
126
|
+
}
|
|
127
|
+
if ("start" in nextOptions) {
|
|
128
|
+
listener.start = (_b = nextOptions.start) !== null && _b !== void 0 ? _b : 1;
|
|
129
|
+
}
|
|
130
|
+
if ("tags" in nextOptions) {
|
|
131
|
+
listener.tags = (_c = nextOptions.tags) !== null && _c !== void 0 ? _c : [];
|
|
132
|
+
}
|
|
133
|
+
if ("extraData" in nextOptions) {
|
|
134
|
+
listener.extraData = (_d = nextOptions.extraData) !== null && _d !== void 0 ? _d : null;
|
|
135
|
+
}
|
|
136
|
+
if ("alwaysFirst" in nextOptions) {
|
|
137
|
+
listener.alwaysFirst = (_e = nextOptions.alwaysFirst) !== null && _e !== void 0 ? _e : false;
|
|
138
|
+
}
|
|
139
|
+
if ("alwaysLast" in nextOptions) {
|
|
140
|
+
listener.alwaysLast = (_f = nextOptions.alwaysLast) !== null && _f !== void 0 ? _f : false;
|
|
141
|
+
}
|
|
142
|
+
if ("async" in nextOptions) {
|
|
143
|
+
let nextAsync = (_g = nextOptions.async) !== null && _g !== void 0 ? _g : null;
|
|
144
|
+
if (nextAsync === true) {
|
|
145
|
+
nextAsync = 1;
|
|
146
|
+
}
|
|
147
|
+
listener.async = nextAsync;
|
|
148
|
+
}
|
|
112
149
|
// Re-sort if ordering hints changed. Unlike addListener we do NOT
|
|
113
150
|
// rewrite each listener's index here: the existing indices hold the
|
|
114
151
|
// original insertion order, and preserving them lets sorting restore
|
|
@@ -122,6 +159,31 @@ export function createEvent(eventOptions = {}) {
|
|
|
122
159
|
listeners.sort((l1, l2) => listenerSorter(l1, l2));
|
|
123
160
|
}
|
|
124
161
|
}
|
|
162
|
+
// Rebind the AbortSignal only when `signal` is explicitly present:
|
|
163
|
+
// detach any previous wiring so the old controller can no longer remove
|
|
164
|
+
// this listener, then attach the new signal. Omitting the field leaves
|
|
165
|
+
// the existing binding intact (partial-update convention); pass
|
|
166
|
+
// signal: null to clear it. An already-aborted new signal removes the
|
|
167
|
+
// listener now, mirroring addListener's "do not keep an aborted-signal
|
|
168
|
+
// listener".
|
|
169
|
+
if ("signal" in nextOptions) {
|
|
170
|
+
(_h = listener.abortCleanup) === null || _h === void 0 ? void 0 : _h.call(listener);
|
|
171
|
+
listener.abortCleanup = null;
|
|
172
|
+
const nextSignal = (_j = nextOptions.signal) !== null && _j !== void 0 ? _j : null;
|
|
173
|
+
if (nextSignal) {
|
|
174
|
+
if (nextSignal.aborted) {
|
|
175
|
+
removeListener(listener.handler, listenerContext);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
const onAbort = () => {
|
|
179
|
+
removeListener(listener.handler, listenerContext);
|
|
180
|
+
};
|
|
181
|
+
nextSignal.addEventListener("abort", onAbort, { once: true });
|
|
182
|
+
listener.abortCleanup = () => {
|
|
183
|
+
nextSignal.removeEventListener("abort", onAbort);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
125
187
|
// The core auto-remove check is a strict `called === limit`, so a
|
|
126
188
|
// listener whose `called` already exceeds the new limit would never
|
|
127
189
|
// auto-remove. Remove it immediately in that case.
|
|
@@ -155,10 +217,16 @@ export function createEvent(eventOptions = {}) {
|
|
|
155
217
|
const removeAllListeners = (tag) => {
|
|
156
218
|
if (tag) {
|
|
157
219
|
listeners = listeners.filter((l) => {
|
|
158
|
-
|
|
220
|
+
var _a;
|
|
221
|
+
const keep = !l.tags || l.tags.indexOf(tag) === -1;
|
|
222
|
+
if (!keep) {
|
|
223
|
+
(_a = l.abortCleanup) === null || _a === void 0 ? void 0 : _a.call(l);
|
|
224
|
+
}
|
|
225
|
+
return keep;
|
|
159
226
|
});
|
|
160
227
|
}
|
|
161
228
|
else {
|
|
229
|
+
listeners.forEach((l) => { var _a; return (_a = l.abortCleanup) === null || _a === void 0 ? void 0 : _a.call(l); });
|
|
162
230
|
listeners = [];
|
|
163
231
|
}
|
|
164
232
|
};
|
|
@@ -197,7 +265,98 @@ export function createEvent(eventOptions = {}) {
|
|
|
197
265
|
};
|
|
198
266
|
const isSuspended = () => suspended;
|
|
199
267
|
const isQueued = () => queued;
|
|
268
|
+
// One-call teardown: drop all listeners (unwinding their abort handlers via
|
|
269
|
+
// reset) and mark the event dead. Post-destroy trigger/addListener throw
|
|
270
|
+
// rather than silently no-op, surfacing use-after-free.
|
|
271
|
+
const destroy = () => {
|
|
272
|
+
reset();
|
|
273
|
+
destroyed = true;
|
|
274
|
+
};
|
|
275
|
+
const isDestroyed = () => destroyed;
|
|
276
|
+
const listenerCount = (tag) => {
|
|
277
|
+
if (tag) {
|
|
278
|
+
return listeners.filter((l) => l.tags && l.tags.indexOf(tag) !== -1).length;
|
|
279
|
+
}
|
|
280
|
+
return listeners.length;
|
|
281
|
+
};
|
|
282
|
+
const triggeredCount = () => triggered;
|
|
283
|
+
// Return a copy: handing back the internal `lastTrigger` reference would let
|
|
284
|
+
// a caller mutate it, corrupting both the recorded snapshot and the values
|
|
285
|
+
// replayed to autoTrigger listeners.
|
|
286
|
+
const lastTriggerArgs = () => lastTrigger ? lastTrigger.slice() : null;
|
|
287
|
+
// Deep-copy extraData so the read-only projection cannot mutate internal
|
|
288
|
+
// listener metadata (which filters can read) at any depth; a shallow copy
|
|
289
|
+
// still shares nested containers by reference. `seen` carries already-cloned
|
|
290
|
+
// containers so a cyclic graph reuses its clone instead of recursing forever
|
|
291
|
+
// (a plain recursive clone throws RangeError on cycles). Arrays, plain
|
|
292
|
+
// objects, Date, Map and Set are cloned. Truly opaque values (functions,
|
|
293
|
+
// class instances) are returned as-is — copying their enumerable keys would
|
|
294
|
+
// not faithfully reproduce them — as are primitives.
|
|
295
|
+
const projectExtraDataDeep = (value, seen) => {
|
|
296
|
+
if (value === null || typeof value !== "object") {
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
const existing = seen.get(value);
|
|
300
|
+
if (existing !== undefined) {
|
|
301
|
+
return existing;
|
|
302
|
+
}
|
|
303
|
+
if (value instanceof Date) {
|
|
304
|
+
return new Date(value.getTime());
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
const copy = [];
|
|
308
|
+
seen.set(value, copy);
|
|
309
|
+
for (const v of value) {
|
|
310
|
+
copy.push(projectExtraDataDeep(v, seen));
|
|
311
|
+
}
|
|
312
|
+
return copy;
|
|
313
|
+
}
|
|
314
|
+
if (value instanceof Map) {
|
|
315
|
+
const copy = new Map();
|
|
316
|
+
seen.set(value, copy);
|
|
317
|
+
value.forEach((v, k) => {
|
|
318
|
+
copy.set(projectExtraDataDeep(k, seen), projectExtraDataDeep(v, seen));
|
|
319
|
+
});
|
|
320
|
+
return copy;
|
|
321
|
+
}
|
|
322
|
+
if (value instanceof Set) {
|
|
323
|
+
const copy = new Set();
|
|
324
|
+
seen.set(value, copy);
|
|
325
|
+
value.forEach((v) => {
|
|
326
|
+
copy.add(projectExtraDataDeep(v, seen));
|
|
327
|
+
});
|
|
328
|
+
return copy;
|
|
329
|
+
}
|
|
330
|
+
const proto = Object.getPrototypeOf(value);
|
|
331
|
+
if (proto === Object.prototype || proto === null) {
|
|
332
|
+
const copy = {};
|
|
333
|
+
seen.set(value, copy);
|
|
334
|
+
for (const k of Object.keys(value)) {
|
|
335
|
+
copy[k] = projectExtraDataDeep(value[k], seen);
|
|
336
|
+
}
|
|
337
|
+
return copy;
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
};
|
|
341
|
+
const projectExtraData = (value) => projectExtraDataDeep(value, new WeakMap());
|
|
342
|
+
const getListeners = () => {
|
|
343
|
+
return listeners.map((l) => ({
|
|
344
|
+
handler: l.handler,
|
|
345
|
+
context: l.context,
|
|
346
|
+
tags: l.tags ? l.tags.slice() : [],
|
|
347
|
+
limit: l.limit,
|
|
348
|
+
start: l.start,
|
|
349
|
+
called: l.called,
|
|
350
|
+
count: l.count,
|
|
351
|
+
async: l.async,
|
|
352
|
+
first: l.first,
|
|
353
|
+
alwaysFirst: l.alwaysFirst,
|
|
354
|
+
alwaysLast: l.alwaysLast,
|
|
355
|
+
extraData: projectExtraData(l.extraData),
|
|
356
|
+
}));
|
|
357
|
+
};
|
|
200
358
|
const reset = () => {
|
|
359
|
+
listeners.forEach((l) => { var _a; return (_a = l.abortCleanup) === null || _a === void 0 ? void 0 : _a.call(l); });
|
|
201
360
|
listeners.length = 0;
|
|
202
361
|
errorListeners.length = 0;
|
|
203
362
|
queue.length = 0;
|
|
@@ -205,6 +364,7 @@ export function createEvent(eventOptions = {}) {
|
|
|
205
364
|
queued = false;
|
|
206
365
|
triggered = 0;
|
|
207
366
|
lastTrigger = null;
|
|
367
|
+
autoTriggerArgs = null;
|
|
208
368
|
sortListeners = false;
|
|
209
369
|
};
|
|
210
370
|
const _listenerCall = (listener, args, resolve = null) => {
|
|
@@ -266,6 +426,12 @@ export function createEvent(eventOptions = {}) {
|
|
|
266
426
|
};
|
|
267
427
|
const _listenerCallWPrev = (listener, args, prevValue, returnType) => {
|
|
268
428
|
if (returnType === TriggerReturnType.PIPE) {
|
|
429
|
+
// Copy-on-write: preserve the pre-pipe lastTrigger snapshot before
|
|
430
|
+
// mutating args[0] in place for the pipe chain (lastTrigger stores
|
|
431
|
+
// the args reference rather than an eager per-trigger copy).
|
|
432
|
+
if (lastTrigger === args) {
|
|
433
|
+
lastTrigger = args.slice();
|
|
434
|
+
}
|
|
269
435
|
args[0] = prevValue;
|
|
270
436
|
// since we don't user listener's arg transformer,
|
|
271
437
|
// we don't need to prepare args
|
|
@@ -287,8 +453,16 @@ export function createEvent(eventOptions = {}) {
|
|
|
287
453
|
}
|
|
288
454
|
return _listenerCall(listener, args);
|
|
289
455
|
};
|
|
290
|
-
const _trigger = (args, returnType = null, tags
|
|
291
|
-
|
|
456
|
+
const _trigger = (args, returnType = null, tags,
|
|
457
|
+
// When set, this call is an autoTrigger replay: it must not bump
|
|
458
|
+
// `triggered`/`lastTrigger` or be gated by the trigger `limit`, and it is
|
|
459
|
+
// delivered only to the listener identified here.
|
|
460
|
+
replayTo) => {
|
|
461
|
+
var _a, _b, _c;
|
|
462
|
+
const replaying = !!replayTo;
|
|
463
|
+
if (destroyed) {
|
|
464
|
+
throw new Error("Event is destroyed");
|
|
465
|
+
}
|
|
292
466
|
if (queued) {
|
|
293
467
|
queue.push([
|
|
294
468
|
args,
|
|
@@ -300,12 +474,28 @@ export function createEvent(eventOptions = {}) {
|
|
|
300
474
|
if (suspended) {
|
|
301
475
|
return;
|
|
302
476
|
}
|
|
303
|
-
|
|
477
|
+
// The trigger `limit` bounds real triggers; an autoTrigger replay is an
|
|
478
|
+
// internal redelivery and must always reach the new listener.
|
|
479
|
+
if (options.limit && triggered >= options.limit && !replaying) {
|
|
304
480
|
return;
|
|
305
481
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
482
|
+
if (!replaying) {
|
|
483
|
+
triggered++;
|
|
484
|
+
// Record the last trigger arguments for introspection
|
|
485
|
+
// (`lastTriggerArgs`). Store the reference rather than eagerly
|
|
486
|
+
// copying on every trigger (a hot path even when nothing ever reads
|
|
487
|
+
// it): `args` is a fresh per-call array the caller cannot reach, and
|
|
488
|
+
// listeners receive it spread (never the array itself). The snapshot
|
|
489
|
+
// is copied lazily — when handed out by lastTriggerArgs(), and
|
|
490
|
+
// copy-on-write before PIPE mode mutates args[0] in place — so the
|
|
491
|
+
// recorded snapshot stays the pre-pipe arguments.
|
|
492
|
+
lastTrigger = args;
|
|
493
|
+
// Record the replay source only while autoTrigger is enabled, so a
|
|
494
|
+
// late listener added after autoTrigger is turned on replays the
|
|
495
|
+
// most recent *enabled* trigger, never an earlier disabled one.
|
|
496
|
+
if (options.autoTrigger) {
|
|
497
|
+
autoTriggerArgs = args.slice();
|
|
498
|
+
}
|
|
309
499
|
}
|
|
310
500
|
// in pipe mode if there is no listeners,
|
|
311
501
|
// we just return piped value
|
|
@@ -336,6 +526,13 @@ export function createEvent(eventOptions = {}) {
|
|
|
336
526
|
if (!listener) {
|
|
337
527
|
continue;
|
|
338
528
|
}
|
|
529
|
+
// An autoTrigger replay is delivered only to the newly added
|
|
530
|
+
// listener identified by `replayTo`.
|
|
531
|
+
if (replayTo
|
|
532
|
+
&& (listener.handler !== replayTo.handler
|
|
533
|
+
|| ((_c = listener.context) !== null && _c !== void 0 ? _c : null) !== replayTo.context)) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
339
536
|
if (options.filter
|
|
340
537
|
&& options.filter.call(options.filterContext, args, listener)
|
|
341
538
|
=== false) {
|
|
@@ -357,6 +554,16 @@ export function createEvent(eventOptions = {}) {
|
|
|
357
554
|
&& listener.count < listener.start) {
|
|
358
555
|
continue;
|
|
359
556
|
}
|
|
557
|
+
// Count the call and exhaust the limit BEFORE invoking the handler.
|
|
558
|
+
// If the handler re-triggers this same event, the nested _trigger
|
|
559
|
+
// snapshots `listeners` AFTER the removal below, so an exhausted
|
|
560
|
+
// (e.g. once()) listener is not invoked a second time. Doing this
|
|
561
|
+
// after the call would let a re-entrant trigger see the still-live
|
|
562
|
+
// listener and run it again past its limit.
|
|
563
|
+
listener.called++;
|
|
564
|
+
if (listener.called === listener.limit) {
|
|
565
|
+
removeListener(listener.handler, listener.context);
|
|
566
|
+
}
|
|
360
567
|
if (isConsequent && results.length > 0) {
|
|
361
568
|
const prev = results[results.length - 1];
|
|
362
569
|
if (hasPromises) {
|
|
@@ -374,10 +581,6 @@ export function createEvent(eventOptions = {}) {
|
|
|
374
581
|
else {
|
|
375
582
|
listenerResult = _listenerCall(listener, args);
|
|
376
583
|
}
|
|
377
|
-
listener.called++;
|
|
378
|
-
if (listener.called === listener.limit) {
|
|
379
|
-
removeListener(listener.handler, listener.context);
|
|
380
|
-
}
|
|
381
584
|
if (returnType === TriggerReturnType.FIRST) {
|
|
382
585
|
return listenerResult;
|
|
383
586
|
}
|
|
@@ -469,6 +672,9 @@ export function createEvent(eventOptions = {}) {
|
|
|
469
672
|
currentTagsFilter = prevTagsFilter;
|
|
470
673
|
}
|
|
471
674
|
};
|
|
675
|
+
const once = (handler, listenerOptions = {}) => {
|
|
676
|
+
return addListener(handler, Object.assign(Object.assign({}, listenerOptions), { limit: 1 }));
|
|
677
|
+
};
|
|
472
678
|
const promise = (options) => {
|
|
473
679
|
return new Promise((resolve) => {
|
|
474
680
|
options = Object.assign(Object.assign({}, (options || {})), { limit: 1 });
|
|
@@ -565,6 +771,7 @@ export function createEvent(eventOptions = {}) {
|
|
|
565
771
|
listen: addListener,
|
|
566
772
|
/** @alias addListener */
|
|
567
773
|
subscribe: addListener,
|
|
774
|
+
once,
|
|
568
775
|
removeListener,
|
|
569
776
|
updateListenerOptions,
|
|
570
777
|
/** @alias removeListener */
|
|
@@ -590,8 +797,14 @@ export function createEvent(eventOptions = {}) {
|
|
|
590
797
|
resume,
|
|
591
798
|
setOptions,
|
|
592
799
|
reset,
|
|
800
|
+
destroy,
|
|
801
|
+
isDestroyed,
|
|
593
802
|
isSuspended,
|
|
594
803
|
isQueued,
|
|
804
|
+
listenerCount,
|
|
805
|
+
triggeredCount,
|
|
806
|
+
lastTriggerArgs,
|
|
807
|
+
getListeners,
|
|
595
808
|
withTags,
|
|
596
809
|
promise,
|
|
597
810
|
first,
|
package/dist/eventBus.d.ts
CHANGED
|
@@ -91,6 +91,8 @@ export declare function createEventBus<EventsMap extends BaseEventMap = DefaultE
|
|
|
91
91
|
readonly stopIntercepting: () => void;
|
|
92
92
|
readonly isIntercepting: () => boolean;
|
|
93
93
|
readonly reset: () => void;
|
|
94
|
+
readonly destroy: () => void;
|
|
95
|
+
readonly isDestroyed: () => boolean;
|
|
94
96
|
readonly suspendAll: (withQueue?: boolean) => void;
|
|
95
97
|
readonly resumeAll: () => void;
|
|
96
98
|
readonly relay: ({ eventSource, remoteEventName, localEventName, proxyType, localEventNamePrefix, }: {
|
package/dist/eventBus.js
CHANGED
|
@@ -78,6 +78,10 @@ export function createEventBus(eventBusOptions) {
|
|
|
78
78
|
let interceptor = null;
|
|
79
79
|
const proxyListeners = [];
|
|
80
80
|
const eventSources = [];
|
|
81
|
+
let destroyed = false;
|
|
82
|
+
// Registry of active relays so destroy() can unwind the external listeners
|
|
83
|
+
// they attach (reset()/destroy() otherwise leave them dangling).
|
|
84
|
+
const relays = [];
|
|
81
85
|
const asterisk = createEvent();
|
|
82
86
|
const errorEvent = createEvent();
|
|
83
87
|
const _getProxyListener = ({ remoteEventName, localEventName, returnType, resolve, localEventNamePrefix, }) => {
|
|
@@ -131,6 +135,9 @@ export function createEventBus(eventBusOptions) {
|
|
|
131
135
|
return listener;
|
|
132
136
|
};
|
|
133
137
|
const add = (name, options) => {
|
|
138
|
+
if (destroyed) {
|
|
139
|
+
throw new Error("EventBus is destroyed");
|
|
140
|
+
}
|
|
134
141
|
if (!events.has(name)) {
|
|
135
142
|
events.set(name, createEvent(options));
|
|
136
143
|
}
|
|
@@ -173,9 +180,15 @@ export function createEventBus(eventBusOptions) {
|
|
|
173
180
|
return interceptor !== null;
|
|
174
181
|
};
|
|
175
182
|
const get = (name) => {
|
|
183
|
+
if (destroyed) {
|
|
184
|
+
throw new Error("EventBus is destroyed");
|
|
185
|
+
}
|
|
176
186
|
return _getOrAddEvent(name);
|
|
177
187
|
};
|
|
178
188
|
const on = (name, handler, options) => {
|
|
189
|
+
if (destroyed) {
|
|
190
|
+
throw new Error("EventBus is destroyed");
|
|
191
|
+
}
|
|
179
192
|
const e = _getOrAddEvent(name);
|
|
180
193
|
eventSources.forEach((evs) => {
|
|
181
194
|
if (evs.eventSource.accepts === false
|
|
@@ -199,11 +212,12 @@ export function createEventBus(eventBusOptions) {
|
|
|
199
212
|
return e.addListener(handler, options);
|
|
200
213
|
};
|
|
201
214
|
const once = (name, handler, options) => {
|
|
202
|
-
|
|
203
|
-
options.limit = 1;
|
|
204
|
-
return on(name, handler, options);
|
|
215
|
+
return on(name, handler, Object.assign(Object.assign({}, (options || {})), { limit: 1 }));
|
|
205
216
|
};
|
|
206
217
|
const promise = (name, options) => {
|
|
218
|
+
if (destroyed) {
|
|
219
|
+
throw new Error("EventBus is destroyed");
|
|
220
|
+
}
|
|
207
221
|
const e = _getOrAddEvent(name);
|
|
208
222
|
return e.promise(options);
|
|
209
223
|
};
|
|
@@ -239,6 +253,9 @@ export function createEventBus(eventBusOptions) {
|
|
|
239
253
|
return e.updateListenerOptions(handler, context !== null && context !== void 0 ? context : null, nextOptions);
|
|
240
254
|
};
|
|
241
255
|
const _trigger = (name, args, returnType, resolve) => {
|
|
256
|
+
if (destroyed) {
|
|
257
|
+
throw new Error("EventBus is destroyed");
|
|
258
|
+
}
|
|
242
259
|
if (name === "*") {
|
|
243
260
|
return;
|
|
244
261
|
}
|
|
@@ -393,18 +410,70 @@ export function createEventBus(eventBusOptions) {
|
|
|
393
410
|
}
|
|
394
411
|
};
|
|
395
412
|
const reset = () => {
|
|
413
|
+
// Detach relays BEFORE clearing proxyListeners: unrelay() resolves the
|
|
414
|
+
// external listener via _getProxyListener, which depends on the original
|
|
415
|
+
// entry still being present. Clearing proxyListeners first would lose the
|
|
416
|
+
// callback identity, leaving the external subscription dangling so a
|
|
417
|
+
// later destroy() could never remove it.
|
|
418
|
+
relays.slice().forEach((r) => {
|
|
419
|
+
unrelay({
|
|
420
|
+
eventSource: r.eventSource,
|
|
421
|
+
remoteEventName: r.remoteEventName,
|
|
422
|
+
localEventName: r.localEventName,
|
|
423
|
+
proxyType: r.proxyType,
|
|
424
|
+
localEventNamePrefix: r.localEventNamePrefix,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
396
427
|
if (eventSources.length > 0) {
|
|
397
428
|
eventSources.slice().forEach((evs) => {
|
|
398
429
|
removeEventSource(evs.eventSource);
|
|
399
430
|
});
|
|
400
431
|
}
|
|
432
|
+
// Reset each owned event before dropping it: clearing the map alone
|
|
433
|
+
// leaves listener AbortSignal handlers attached to their signals, which
|
|
434
|
+
// retains the orphaned events (and their listeners) until the signal
|
|
435
|
+
// aborts. reset() detaches those handlers and clears the listeners.
|
|
436
|
+
events.forEach((event) => {
|
|
437
|
+
event.reset();
|
|
438
|
+
});
|
|
401
439
|
events.clear();
|
|
402
440
|
interceptor = null;
|
|
403
441
|
currentTagsFilter = null;
|
|
404
442
|
asterisk.reset();
|
|
405
443
|
proxyListeners.length = 0;
|
|
406
444
|
eventSources.length = 0;
|
|
445
|
+
relays.length = 0;
|
|
446
|
+
};
|
|
447
|
+
// One-call teardown: unwind external attachments (relays + event sources)
|
|
448
|
+
// that reset() leaves dangling, destroy every owned event, and mark the bus
|
|
449
|
+
// dead. Post-destroy trigger/addListener throw rather than silently no-op.
|
|
450
|
+
const destroy = () => {
|
|
451
|
+
relays.slice().forEach((r) => {
|
|
452
|
+
unrelay({
|
|
453
|
+
eventSource: r.eventSource,
|
|
454
|
+
remoteEventName: r.remoteEventName,
|
|
455
|
+
localEventName: r.localEventName,
|
|
456
|
+
proxyType: r.proxyType,
|
|
457
|
+
localEventNamePrefix: r.localEventNamePrefix,
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
eventSources.slice().forEach((evs) => {
|
|
461
|
+
removeEventSource(evs.eventSource);
|
|
462
|
+
});
|
|
463
|
+
events.forEach((event) => {
|
|
464
|
+
event.destroy();
|
|
465
|
+
});
|
|
466
|
+
events.clear();
|
|
467
|
+
asterisk.destroy();
|
|
468
|
+
errorEvent.destroy();
|
|
469
|
+
proxyListeners.length = 0;
|
|
470
|
+
eventSources.length = 0;
|
|
471
|
+
relays.length = 0;
|
|
472
|
+
interceptor = null;
|
|
473
|
+
currentTagsFilter = null;
|
|
474
|
+
destroyed = true;
|
|
407
475
|
};
|
|
476
|
+
const isDestroyed = () => destroyed;
|
|
408
477
|
const suspendAll = (withQueue = false) => {
|
|
409
478
|
events.forEach((event) => {
|
|
410
479
|
event.suspend(withQueue);
|
|
@@ -416,6 +485,12 @@ export function createEventBus(eventBusOptions) {
|
|
|
416
485
|
});
|
|
417
486
|
};
|
|
418
487
|
const relay = ({ eventSource, remoteEventName, localEventName, proxyType, localEventNamePrefix, }) => {
|
|
488
|
+
// Like the other registration methods, refuse on a dead bus: attaching
|
|
489
|
+
// the proxy listener here would leave a dangling subscription that
|
|
490
|
+
// throws "EventBus is destroyed" the next time the source fires.
|
|
491
|
+
if (destroyed) {
|
|
492
|
+
throw new Error("EventBus is destroyed");
|
|
493
|
+
}
|
|
419
494
|
const { returnType, resolve } = proxyReturnTypeToTriggerReturnType(proxyType || ProxyType.TRIGGER);
|
|
420
495
|
const listener = _getProxyListener({
|
|
421
496
|
localEventName: localEventName || null,
|
|
@@ -430,6 +505,18 @@ export function createEventBus(eventBusOptions) {
|
|
|
430
505
|
else {
|
|
431
506
|
eventSource.on(remoteEventName, listener.listener);
|
|
432
507
|
}
|
|
508
|
+
relays.push({
|
|
509
|
+
eventSource,
|
|
510
|
+
remoteEventName,
|
|
511
|
+
localEventName: localEventName || null,
|
|
512
|
+
// Store the resolved proxyType (undefined resolves to TRIGGER) so the
|
|
513
|
+
// registry key matches how the proxy listener is actually resolved.
|
|
514
|
+
// Otherwise unrelay({ proxyType: TRIGGER }) for a relay({}) (undefined)
|
|
515
|
+
// detaches the listener but leaves a stale registry entry, which a
|
|
516
|
+
// later reset()/destroy() then unrelays a second time.
|
|
517
|
+
proxyType: proxyType || ProxyType.TRIGGER,
|
|
518
|
+
localEventNamePrefix: localEventNamePrefix || null,
|
|
519
|
+
});
|
|
433
520
|
};
|
|
434
521
|
const unrelay = ({ eventSource, remoteEventName, localEventName, proxyType, localEventNamePrefix, }) => {
|
|
435
522
|
const { returnType, resolve } = proxyReturnTypeToTriggerReturnType(proxyType || ProxyType.TRIGGER);
|
|
@@ -448,8 +535,22 @@ export function createEventBus(eventBusOptions) {
|
|
|
448
535
|
eventSource.un(remoteEventName, listener.listener);
|
|
449
536
|
}
|
|
450
537
|
}
|
|
538
|
+
const inx = relays.findIndex((r) => r.eventSource === eventSource
|
|
539
|
+
&& r.remoteEventName === remoteEventName
|
|
540
|
+
&& r.localEventName === (localEventName || null)
|
|
541
|
+
// Compare on the resolved proxyType so an undefined relay matches an
|
|
542
|
+
// explicit TRIGGER unrelay (and vice versa), mirroring how the proxy
|
|
543
|
+
// listener itself resolves equivalent types.
|
|
544
|
+
&& r.proxyType === (proxyType || ProxyType.TRIGGER)
|
|
545
|
+
&& r.localEventNamePrefix === (localEventNamePrefix || null));
|
|
546
|
+
if (inx !== -1) {
|
|
547
|
+
relays.splice(inx, 1);
|
|
548
|
+
}
|
|
451
549
|
};
|
|
452
550
|
const addEventSource = (eventSource) => {
|
|
551
|
+
if (destroyed) {
|
|
552
|
+
throw new Error("EventBus is destroyed");
|
|
553
|
+
}
|
|
453
554
|
if (eventSources.find((evs) => evs.eventSource.name === eventSource.name)) {
|
|
454
555
|
return;
|
|
455
556
|
}
|
|
@@ -549,6 +650,8 @@ export function createEventBus(eventBusOptions) {
|
|
|
549
650
|
stopIntercepting,
|
|
550
651
|
isIntercepting,
|
|
551
652
|
reset,
|
|
653
|
+
destroy,
|
|
654
|
+
isDestroyed,
|
|
552
655
|
suspendAll,
|
|
553
656
|
resumeAll,
|
|
554
657
|
relay,
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -44,6 +44,6 @@ export type ErrorResponse<Arguments extends any[] = any[]> = {
|
|
|
44
44
|
error: Error;
|
|
45
45
|
args: Arguments;
|
|
46
46
|
name?: MapKey;
|
|
47
|
-
type: "action" | "event" | "store-change" | "store-pipe" | "store-control";
|
|
47
|
+
type: "action" | "action-status" | "event" | "store-change" | "store-pipe" | "store-control";
|
|
48
48
|
};
|
|
49
49
|
export type ErrorListenerSignature<Arguments extends any[] = any[]> = (errorResponse: ErrorResponse<Arguments>) => void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ActionStatus } from "../action.js";
|
|
2
|
+
import type { BaseActionBus } from "../actionBus.js";
|
|
3
|
+
import type { KeyOf } from "../lib/types.js";
|
|
4
|
+
import type { AsyncActionState } from "./useAsyncAction.js";
|
|
5
|
+
export type { ActionStatus, AsyncActionState };
|
|
6
|
+
/**
|
|
7
|
+
* Subscribes to the status of a named action on an ActionBus and returns
|
|
8
|
+
* `{ loading, error, response }` for driving `loading`/`disabled` UI. This is
|
|
9
|
+
* the primary path for apps that route mutations through one shared ActionBus.
|
|
10
|
+
*
|
|
11
|
+
* An unregistered name reports an idle status and is safe to subscribe to.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useActionBusStatus<TBus extends BaseActionBus, TName extends KeyOf<TBus["__type"]["actions"]>>(bus: TBus, name: TName): AsyncActionState<TBus["__type"]["actions"][TName]["actionReturnType"]>;
|