@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.
Files changed (60) hide show
  1. package/README.md +160 -14
  2. package/dist/action.d.ts +31 -10
  3. package/dist/action.js +156 -23
  4. package/dist/actionBus.d.ts +13 -4
  5. package/dist/actionBus.js +201 -5
  6. package/dist/actionMap.d.ts +26 -19
  7. package/dist/actionMap.js +10 -4
  8. package/dist/event.d.ts +37 -3
  9. package/dist/event.js +345 -78
  10. package/dist/eventBus.d.ts +7 -3
  11. package/dist/eventBus.js +194 -34
  12. package/dist/index.d.ts +7 -7
  13. package/dist/index.js +7 -7
  14. package/dist/lib/actionMapInternal.d.ts +8 -0
  15. package/dist/lib/actionMapInternal.js +8 -0
  16. package/dist/lib/isPromiseLike.d.ts +1 -0
  17. package/dist/lib/isPromiseLike.js +5 -0
  18. package/dist/lib/normalizeEventOptions.d.ts +13 -0
  19. package/dist/lib/normalizeEventOptions.js +21 -0
  20. package/dist/lib/types.d.ts +1 -1
  21. package/dist/react/ErrorBoundary.d.ts +1 -1
  22. package/dist/react/listenerOptionsEqual.d.ts +27 -0
  23. package/dist/react/listenerOptionsEqual.js +121 -0
  24. package/dist/react/useAction.d.ts +3 -3
  25. package/dist/react/useAction.js +10 -7
  26. package/dist/react/useActionBus.d.ts +4 -4
  27. package/dist/react/useActionBus.js +32 -2
  28. package/dist/react/useActionBusStatus.d.ts +13 -0
  29. package/dist/react/useActionBusStatus.js +26 -0
  30. package/dist/react/useActionMap.d.ts +4 -4
  31. package/dist/react/useActionMap.js +40 -7
  32. package/dist/react/useAsyncAction.d.ts +20 -0
  33. package/dist/react/useAsyncAction.js +53 -0
  34. package/dist/react/useEvent.d.ts +2 -2
  35. package/dist/react/useEvent.js +18 -2
  36. package/dist/react/useEventBus.d.ts +2 -2
  37. package/dist/react/useEventBus.js +14 -10
  38. package/dist/react/useListenToAction.d.ts +1 -1
  39. package/dist/react/useListenToAction.js +17 -38
  40. package/dist/react/useListenToActionBus.d.ts +3 -3
  41. package/dist/react/useListenToActionBus.js +15 -9
  42. package/dist/react/useListenToEvent.d.ts +2 -2
  43. package/dist/react/useListenToEvent.js +8 -6
  44. package/dist/react/useListenToEventBus.d.ts +3 -3
  45. package/dist/react/useListenToEventBus.js +9 -7
  46. package/dist/react/useListenToStoreChanges.d.ts +3 -3
  47. package/dist/react/useListenToStoreChanges.js +9 -7
  48. package/dist/react/useReconciledListener.d.ts +33 -0
  49. package/dist/react/useReconciledListener.js +44 -0
  50. package/dist/react/useStore.d.ts +2 -2
  51. package/dist/react/useStore.js +71 -19
  52. package/dist/react/useStoreSelector.d.ts +35 -0
  53. package/dist/react/useStoreSelector.js +144 -0
  54. package/dist/react/useStoreState.d.ts +2 -2
  55. package/dist/react/useStoreState.js +26 -21
  56. package/dist/react.d.ts +16 -13
  57. package/dist/react.js +16 -13
  58. package/dist/store.d.ts +12 -8
  59. package/dist/store.js +473 -39
  60. package/package.json +13 -3
package/dist/event.js CHANGED
@@ -1,29 +1,45 @@
1
- import asyncCall from "./lib/asyncCall";
2
- import listenerSorter from "./lib/listenerSorter";
3
- import tagsIntersect from "./lib/tagsIntersect";
4
- import { TriggerReturnType } from "./lib/types";
1
+ import asyncCall from "./lib/asyncCall.js";
2
+ import isPromiseLike from "./lib/isPromiseLike.js";
3
+ import listenerSorter from "./lib/listenerSorter.js";
4
+ import tagsIntersect from "./lib/tagsIntersect.js";
5
+ import { TriggerReturnType } from "./lib/types.js";
5
6
  export function createEvent(eventOptions = {}) {
6
7
  let listeners = [];
7
8
  const errorListeners = [];
8
9
  let queue = [];
9
10
  let suspended = false;
10
11
  let queued = false;
12
+ let destroyed = false;
11
13
  let triggered = 0;
12
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;
13
20
  let sortListeners = false;
14
21
  let currentTagsFilter = null;
15
22
  const options = Object.assign({ async: null, limit: null, autoTrigger: null, filter: null, filterContext: null, maxListeners: 0 }, eventOptions);
16
23
  const addListener = (handler, listenerOptions = {}) => {
24
+ var _a, _b;
25
+ if (destroyed) {
26
+ throw new Error("Event is destroyed");
27
+ }
17
28
  if (!handler) {
18
29
  return;
19
30
  }
20
- if (listeners.find((l) => l.handler === handler && l.context === listenerOptions.context)) {
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;
36
+ if (listeners.find((l) => l.handler === handler && l.context === listenerContext)) {
21
37
  return;
22
38
  }
23
39
  if (options.maxListeners && listeners.length >= options.maxListeners) {
24
40
  throw new Error(`Max listeners (${options.maxListeners}) reached`);
25
41
  }
26
- 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 });
27
43
  if (listener.async === true) {
28
44
  listener.async = 1;
29
45
  }
@@ -44,21 +60,32 @@ export function createEvent(eventOptions = {}) {
44
60
  || (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.alwaysLast) === true) {
45
61
  sortListeners = true;
46
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
+ }
47
72
  if (options.autoTrigger
48
- && lastTrigger !== null
73
+ && autoTriggerArgs !== null
49
74
  && !suspended) {
50
- const prevFilter = options.filter;
51
- options.filter = (args, l) => {
52
- if (l && l.handler === handler) {
53
- return prevFilter ? prevFilter(args, l) !== false : true;
54
- }
55
- return false;
56
- };
57
- _trigger(lastTrigger);
58
- options.filter = prevFilter;
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
+ });
59
85
  }
60
86
  };
61
87
  const removeListener = (handler, context, tag) => {
88
+ var _a;
62
89
  const inx = listeners.findIndex((l) => {
63
90
  if (l.handler !== handler) {
64
91
  return false;
@@ -77,7 +104,92 @@ export function createEvent(eventOptions = {}) {
77
104
  if (inx === -1) {
78
105
  return false;
79
106
  }
80
- 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);
109
+ return true;
110
+ };
111
+ const updateListenerOptions = (handler, context = null, nextOptions = {}) => {
112
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
113
+ const listenerContext = context !== null && context !== void 0 ? context : null;
114
+ const listener = listeners.find((l) => l.handler === handler && l.context === listenerContext);
115
+ if (!listener) {
116
+ return false;
117
+ }
118
+ const prevAlwaysFirst = listener.alwaysFirst;
119
+ const prevAlwaysLast = listener.alwaysLast;
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
+ }
149
+ // Re-sort if ordering hints changed. Unlike addListener we do NOT
150
+ // rewrite each listener's index here: the existing indices hold the
151
+ // original insertion order, and preserving them lets sorting restore
152
+ // that order when alwaysFirst/alwaysLast is cleared.
153
+ if (listener.alwaysFirst !== prevAlwaysFirst
154
+ || listener.alwaysLast !== prevAlwaysLast) {
155
+ if (listener.alwaysFirst === true || listener.alwaysLast === true) {
156
+ sortListeners = true;
157
+ }
158
+ if (sortListeners) {
159
+ listeners.sort((l1, l2) => listenerSorter(l1, l2));
160
+ }
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
+ }
187
+ // The core auto-remove check is a strict `called === limit`, so a
188
+ // listener whose `called` already exceeds the new limit would never
189
+ // auto-remove. Remove it immediately in that case.
190
+ if (listener.limit !== 0 && listener.called >= listener.limit) {
191
+ removeListener(listener.handler, listener.context);
192
+ }
81
193
  return true;
82
194
  };
83
195
  const hasListener = (handler, context, tag) => {
@@ -105,10 +217,16 @@ export function createEvent(eventOptions = {}) {
105
217
  const removeAllListeners = (tag) => {
106
218
  if (tag) {
107
219
  listeners = listeners.filter((l) => {
108
- return !l.tags || l.tags.indexOf(tag) === -1;
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;
109
226
  });
110
227
  }
111
228
  else {
229
+ listeners.forEach((l) => { var _a; return (_a = l.abortCleanup) === null || _a === void 0 ? void 0 : _a.call(l); });
112
230
  listeners = [];
113
231
  }
114
232
  };
@@ -137,7 +255,7 @@ export function createEvent(eventOptions = {}) {
137
255
  queued = false;
138
256
  if (queue.length > 0) {
139
257
  for (let i = 0, l = queue.length; i < l; i++) {
140
- _trigger(queue[i][0], queue[i][1]);
258
+ _trigger(queue[i][0], queue[i][1], queue[i][2]);
141
259
  }
142
260
  queue = [];
143
261
  }
@@ -147,7 +265,98 @@ export function createEvent(eventOptions = {}) {
147
265
  };
148
266
  const isSuspended = () => suspended;
149
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
+ };
150
358
  const reset = () => {
359
+ listeners.forEach((l) => { var _a; return (_a = l.abortCleanup) === null || _a === void 0 ? void 0 : _a.call(l); });
151
360
  listeners.length = 0;
152
361
  errorListeners.length = 0;
153
362
  queue.length = 0;
@@ -155,8 +364,8 @@ export function createEvent(eventOptions = {}) {
155
364
  queued = false;
156
365
  triggered = 0;
157
366
  lastTrigger = null;
367
+ autoTriggerArgs = null;
158
368
  sortListeners = false;
159
- cachedPromise = null;
160
369
  };
161
370
  const _listenerCall = (listener, args, resolve = null) => {
162
371
  let isAsync = listener.async;
@@ -173,26 +382,29 @@ export function createEvent(eventOptions = {}) {
173
382
  const result = isAsync !== false
174
383
  ? asyncCall(listener.handler, listener.context, args, isAsync)
175
384
  : listener.handler.bind(listener.context)(...args);
176
- if (resolve !== null) {
177
- if (result instanceof Promise) {
178
- void result.then(resolve).catch((error) => {
179
- for (const errorListener of errorListeners) {
180
- errorListener.handler({
181
- error: error instanceof Error
182
- ? error
183
- : new Error(error),
184
- args: args,
185
- type: "event",
186
- });
187
- }
188
- if (errorListeners.length === 0) {
189
- throw error;
190
- }
191
- });
192
- }
193
- else {
194
- resolve(result);
385
+ if (isPromiseLike(result)) {
386
+ const handledResult = Promise.resolve(result).catch((error) => {
387
+ for (const errorListener of errorListeners) {
388
+ errorListener.handler({
389
+ error: error instanceof Error
390
+ ? error
391
+ : new Error(error),
392
+ args: args,
393
+ type: "event",
394
+ });
395
+ }
396
+ if (errorListeners.length === 0) {
397
+ throw error;
398
+ }
399
+ return undefined;
400
+ });
401
+ if (resolve !== null) {
402
+ void handledResult.then(resolve);
195
403
  }
404
+ return handledResult;
405
+ }
406
+ if (resolve !== null) {
407
+ resolve(result);
196
408
  }
197
409
  return result;
198
410
  }
@@ -214,6 +426,12 @@ export function createEvent(eventOptions = {}) {
214
426
  };
215
427
  const _listenerCallWPrev = (listener, args, prevValue, returnType) => {
216
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
+ }
217
435
  args[0] = prevValue;
218
436
  // since we don't user listener's arg transformer,
219
437
  // we don't need to prepare args
@@ -235,20 +453,49 @@ export function createEvent(eventOptions = {}) {
235
453
  }
236
454
  return _listenerCall(listener, args);
237
455
  };
238
- const _trigger = (args, returnType = null, tags) => {
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
+ }
239
466
  if (queued) {
240
- queue.push([args, returnType]);
467
+ queue.push([
468
+ args,
469
+ returnType,
470
+ (_b = (_a = (tags || currentTagsFilter)) === null || _a === void 0 ? void 0 : _a.slice()) !== null && _b !== void 0 ? _b : null,
471
+ ]);
241
472
  return;
242
473
  }
243
474
  if (suspended) {
244
475
  return;
245
476
  }
246
- if (options.limit && triggered >= options.limit) {
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) {
247
480
  return;
248
481
  }
249
- triggered++;
250
- if (options.autoTrigger) {
251
- lastTrigger = args.slice();
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
+ }
252
499
  }
253
500
  // in pipe mode if there is no listeners,
254
501
  // we just return piped value
@@ -279,6 +526,13 @@ export function createEvent(eventOptions = {}) {
279
526
  if (!listener) {
280
527
  continue;
281
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
+ }
282
536
  if (options.filter
283
537
  && options.filter.call(options.filterContext, args, listener)
284
538
  === false) {
@@ -300,12 +554,20 @@ export function createEvent(eventOptions = {}) {
300
554
  && listener.count < listener.start) {
301
555
  continue;
302
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
+ }
303
567
  if (isConsequent && results.length > 0) {
304
568
  const prev = results[results.length - 1];
305
569
  if (hasPromises) {
306
- const prevPromise = prev instanceof Promise
307
- ? prev
308
- : Promise.resolve(prev);
570
+ const prevPromise = Promise.resolve(prev);
309
571
  listenerResult = prevPromise.then(((listener, args, returnType) => (value) => {
310
572
  return _listenerCallWPrev(listener, args, value, returnType);
311
573
  })(listener, args, returnType));
@@ -319,10 +581,6 @@ export function createEvent(eventOptions = {}) {
319
581
  else {
320
582
  listenerResult = _listenerCall(listener, args);
321
583
  }
322
- listener.called++;
323
- if (listener.called === listener.limit) {
324
- removeListener(listener.handler, listener.context);
325
- }
326
584
  if (returnType === TriggerReturnType.FIRST) {
327
585
  return listenerResult;
328
586
  }
@@ -342,7 +600,7 @@ export function createEvent(eventOptions = {}) {
342
600
  }
343
601
  case TriggerReturnType.FIRST_NON_EMPTY: {
344
602
  if (!hasPromises
345
- && !(listenerResult instanceof Promise)
603
+ && !isPromiseLike(listenerResult)
346
604
  && listenerResult !== null
347
605
  && listenerResult !== undefined) {
348
606
  return listenerResult;
@@ -351,7 +609,7 @@ export function createEvent(eventOptions = {}) {
351
609
  }
352
610
  }
353
611
  }
354
- if (!hasPromises && listenerResult instanceof Promise) {
612
+ if (!hasPromises && isPromiseLike(listenerResult)) {
355
613
  hasPromises = true;
356
614
  }
357
615
  results.push(listenerResult);
@@ -405,33 +663,34 @@ export function createEvent(eventOptions = {}) {
405
663
  _trigger(args);
406
664
  };
407
665
  const withTags = (tags, callback) => {
666
+ const prevTagsFilter = currentTagsFilter;
408
667
  currentTagsFilter = tags;
409
668
  try {
410
669
  return callback();
411
670
  }
412
671
  finally {
413
- currentTagsFilter = null;
672
+ currentTagsFilter = prevTagsFilter;
414
673
  }
415
674
  };
416
- let cachedPromise = null;
675
+ const once = (handler, listenerOptions = {}) => {
676
+ return addListener(handler, Object.assign(Object.assign({}, listenerOptions), { limit: 1 }));
677
+ };
417
678
  const promise = (options) => {
418
- return cachedPromise = cachedPromise
419
- || new Promise((resolve) => {
420
- options = Object.assign(Object.assign({}, (options || {})), { limit: 1 });
421
- const l = ((...args) => {
422
- resolve(args);
423
- cachedPromise = null;
424
- });
425
- addListener(l, options);
679
+ return new Promise((resolve) => {
680
+ options = Object.assign(Object.assign({}, (options || {})), { limit: 1 });
681
+ const l = ((...args) => {
682
+ resolve(args);
426
683
  });
684
+ addListener(l, options);
685
+ });
427
686
  };
428
687
  const first = (...args) => {
429
688
  return _trigger(args, TriggerReturnType.FIRST);
430
689
  };
431
690
  const resolveFirst = (...args) => {
432
691
  const response = _trigger(args, TriggerReturnType.FIRST);
433
- if (response instanceof Promise) {
434
- return response;
692
+ if (isPromiseLike(response)) {
693
+ return Promise.resolve(response);
435
694
  }
436
695
  return Promise.resolve(response);
437
696
  };
@@ -440,8 +699,8 @@ export function createEvent(eventOptions = {}) {
440
699
  };
441
700
  const resolveAll = (...args) => {
442
701
  const response = _trigger(args, TriggerReturnType.ALL);
443
- if (response instanceof Promise) {
444
- return response;
702
+ if (isPromiseLike(response)) {
703
+ return Promise.resolve(response);
445
704
  }
446
705
  return Promise.resolve(response);
447
706
  };
@@ -450,8 +709,8 @@ export function createEvent(eventOptions = {}) {
450
709
  };
451
710
  const resolveLast = (...args) => {
452
711
  const response = _trigger(args, TriggerReturnType.LAST);
453
- if (response instanceof Promise) {
454
- return response;
712
+ if (isPromiseLike(response)) {
713
+ return Promise.resolve(response);
455
714
  }
456
715
  return Promise.resolve(response);
457
716
  };
@@ -460,8 +719,8 @@ export function createEvent(eventOptions = {}) {
460
719
  };
461
720
  const resolveMerge = (...args) => {
462
721
  const response = _trigger(args, TriggerReturnType.MERGE);
463
- if (response instanceof Promise) {
464
- return response;
722
+ if (isPromiseLike(response)) {
723
+ return Promise.resolve(response);
465
724
  }
466
725
  return Promise.resolve(response);
467
726
  };
@@ -470,8 +729,8 @@ export function createEvent(eventOptions = {}) {
470
729
  };
471
730
  const resolveConcat = (...args) => {
472
731
  const response = _trigger(args, TriggerReturnType.CONCAT);
473
- if (response instanceof Promise) {
474
- return response;
732
+ if (isPromiseLike(response)) {
733
+ return Promise.resolve(response);
475
734
  }
476
735
  return Promise.resolve(response);
477
736
  };
@@ -480,8 +739,8 @@ export function createEvent(eventOptions = {}) {
480
739
  };
481
740
  const resolveFirstNonEmpty = (...args) => {
482
741
  const response = _trigger(args, TriggerReturnType.FIRST_NON_EMPTY);
483
- if (response instanceof Promise) {
484
- return response;
742
+ if (isPromiseLike(response)) {
743
+ return Promise.resolve(response);
485
744
  }
486
745
  return Promise.resolve(response);
487
746
  };
@@ -496,8 +755,8 @@ export function createEvent(eventOptions = {}) {
496
755
  };
497
756
  const resolvePipe = (...args) => {
498
757
  const response = _trigger(args, TriggerReturnType.PIPE);
499
- if (response instanceof Promise) {
500
- return response;
758
+ if (isPromiseLike(response)) {
759
+ return Promise.resolve(response);
501
760
  }
502
761
  return Promise.resolve(response);
503
762
  };
@@ -512,7 +771,9 @@ export function createEvent(eventOptions = {}) {
512
771
  listen: addListener,
513
772
  /** @alias addListener */
514
773
  subscribe: addListener,
774
+ once,
515
775
  removeListener,
776
+ updateListenerOptions,
516
777
  /** @alias removeListener */
517
778
  un: removeListener,
518
779
  /** @alias removeListener */
@@ -536,8 +797,14 @@ export function createEvent(eventOptions = {}) {
536
797
  resume,
537
798
  setOptions,
538
799
  reset,
800
+ destroy,
801
+ isDestroyed,
539
802
  isSuspended,
540
803
  isQueued,
804
+ listenerCount,
805
+ triggeredCount,
806
+ lastTriggerArgs,
807
+ getListeners,
541
808
  withTags,
542
809
  promise,
543
810
  first,