@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
@@ -0,0 +1,121 @@
1
+ function normalizeAsync(value) {
2
+ if (value === undefined || value === null) {
3
+ return null;
4
+ }
5
+ if (value === true) {
6
+ return 1;
7
+ }
8
+ return value;
9
+ }
10
+ /**
11
+ * Order-insensitive set comparison for listener tags.
12
+ * `undefined`, `[]` and missing all compare equal. Order and duplicates do
13
+ * not matter because the core only ever uses tags via membership and
14
+ * intersection checks.
15
+ */
16
+ export function areTagsEqual(a, b) {
17
+ const aa = a !== null && a !== void 0 ? a : [];
18
+ const bb = b !== null && b !== void 0 ? b : [];
19
+ if (aa.length === 0 && bb.length === 0) {
20
+ return true;
21
+ }
22
+ const sa = new Set(aa);
23
+ const sb = new Set(bb);
24
+ if (sa.size !== sb.size) {
25
+ return false;
26
+ }
27
+ for (const tag of sa) {
28
+ if (!sb.has(tag)) {
29
+ return false;
30
+ }
31
+ }
32
+ return true;
33
+ }
34
+ /**
35
+ * Domain-specific comparator for {@link ListenerOptions}. Avoids generic deep
36
+ * equality: primitives compare after default semantics, `context`/`extraData`
37
+ * compare by reference, and `tags` use order-insensitive set comparison.
38
+ */
39
+ export function areListenerOptionsEqual(a, b) {
40
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
41
+ const aa = a !== null && a !== void 0 ? a : {};
42
+ const bb = b !== null && b !== void 0 ? b : {};
43
+ if (((_a = aa.limit) !== null && _a !== void 0 ? _a : 0) !== ((_b = bb.limit) !== null && _b !== void 0 ? _b : 0)) {
44
+ return false;
45
+ }
46
+ if (((_c = aa.start) !== null && _c !== void 0 ? _c : 1) !== ((_d = bb.start) !== null && _d !== void 0 ? _d : 1)) {
47
+ return false;
48
+ }
49
+ if (((_e = aa.first) !== null && _e !== void 0 ? _e : false) !== ((_f = bb.first) !== null && _f !== void 0 ? _f : false)) {
50
+ return false;
51
+ }
52
+ if (((_g = aa.alwaysFirst) !== null && _g !== void 0 ? _g : false) !== ((_h = bb.alwaysFirst) !== null && _h !== void 0 ? _h : false)) {
53
+ return false;
54
+ }
55
+ if (((_j = aa.alwaysLast) !== null && _j !== void 0 ? _j : false) !== ((_k = bb.alwaysLast) !== null && _k !== void 0 ? _k : false)) {
56
+ return false;
57
+ }
58
+ if (normalizeAsync(aa.async) !== normalizeAsync(bb.async)) {
59
+ return false;
60
+ }
61
+ if (((_l = aa.context) !== null && _l !== void 0 ? _l : null) !== ((_m = bb.context) !== null && _m !== void 0 ? _m : null)) {
62
+ return false;
63
+ }
64
+ // reference equality only; do not deep compare arbitrary values
65
+ if (aa.extraData !== bb.extraData) {
66
+ return false;
67
+ }
68
+ if (!areTagsEqual(aa.tags, bb.tags)) {
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+ /**
74
+ * Domain-specific comparator for {@link EventOptions}. Primitives compare after
75
+ * default semantics; `filter`/`filterContext` compare by reference.
76
+ */
77
+ export function areEventOptionsEqual(a, b) {
78
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
79
+ const aa = a !== null && a !== void 0 ? a : {};
80
+ const bb = b !== null && b !== void 0 ? b : {};
81
+ if (normalizeAsync(aa.async) !== normalizeAsync(bb.async)) {
82
+ return false;
83
+ }
84
+ if (((_a = aa.limit) !== null && _a !== void 0 ? _a : null) !== ((_b = bb.limit) !== null && _b !== void 0 ? _b : null)) {
85
+ return false;
86
+ }
87
+ if (((_c = aa.autoTrigger) !== null && _c !== void 0 ? _c : null) !== ((_d = bb.autoTrigger) !== null && _d !== void 0 ? _d : null)) {
88
+ return false;
89
+ }
90
+ if (((_e = aa.maxListeners) !== null && _e !== void 0 ? _e : 0) !== ((_f = bb.maxListeners) !== null && _f !== void 0 ? _f : 0)) {
91
+ return false;
92
+ }
93
+ // reference equality
94
+ if (((_g = aa.filter) !== null && _g !== void 0 ? _g : null) !== ((_h = bb.filter) !== null && _h !== void 0 ? _h : null)) {
95
+ return false;
96
+ }
97
+ if (((_j = aa.filterContext) !== null && _j !== void 0 ? _j : null) !== ((_k = bb.filterContext) !== null && _k !== void 0 ? _k : null)) {
98
+ return false;
99
+ }
100
+ return true;
101
+ }
102
+ /**
103
+ * Compares the per-event `eventOptions` maps of two {@link EventBusOptions}.
104
+ * Equal when every event name present in either map has semantically equal
105
+ * {@link EventOptions} (missing entries compare as default options).
106
+ */
107
+ export function areEventBusOptionsEqual(a, b) {
108
+ var _a, _b;
109
+ const aMap = (_a = a === null || a === void 0 ? void 0 : a.eventOptions) !== null && _a !== void 0 ? _a : {};
110
+ const bMap = (_b = b === null || b === void 0 ? void 0 : b.eventOptions) !== null && _b !== void 0 ? _b : {};
111
+ const names = new Set([
112
+ ...Object.keys(aMap),
113
+ ...Object.keys(bMap),
114
+ ]);
115
+ for (const name of names) {
116
+ if (!areEventOptionsEqual(aMap[name], bMap[name])) {
117
+ return false;
118
+ }
119
+ }
120
+ return true;
121
+ }
@@ -1,5 +1,5 @@
1
- import { createAction } from "../action";
2
- import type { ActionResponse, BeforeActionSignature, ListenerSignature } from "../action";
3
- import type { BaseHandler, ErrorListenerSignature, ErrorResponse } from "../lib/types";
1
+ import { createAction } from "../action.js";
2
+ import type { ActionResponse, BeforeActionSignature, ListenerSignature } from "../action.js";
3
+ import type { BaseHandler, ErrorListenerSignature, ErrorResponse } from "../lib/types.js";
4
4
  export type { ActionResponse, BaseHandler, ErrorListenerSignature, ErrorResponse, ListenerSignature, };
5
5
  export declare function useAction<ActionSignature extends BaseHandler, Listener extends ListenerSignature<ActionSignature>, ErrorListener extends ErrorListenerSignature<Parameters<ActionSignature>>, BeforeActionListener extends BeforeActionSignature<ActionSignature>>(actionSignature: ActionSignature, listener?: Listener | null, errorListener?: ErrorListener | null, beforeActionListener?: BeforeActionListener | null): ReturnType<typeof createAction<ActionSignature>>;
@@ -1,9 +1,9 @@
1
1
  import { useContext, useEffect, useMemo, useRef } from "react";
2
- import { createAction } from "../action";
3
- import { ErrorBoundaryContext } from "./ErrorBoundary";
2
+ import { createAction } from "../action.js";
3
+ import { ErrorBoundaryContext } from "./ErrorBoundary.js";
4
4
  export function useAction(actionSignature, listener, errorListener, beforeActionListener) {
5
5
  const boundaryErrorListener = useContext(ErrorBoundaryContext);
6
- const updateRef = useRef(0);
6
+ const actionSignatureRef = useRef(actionSignature);
7
7
  const listenerRef = useRef(listener);
8
8
  const errorListenerRef = useRef(errorListener);
9
9
  const boundaryErrorListenerRef = useRef(boundaryErrorListener);
@@ -24,11 +24,15 @@ export function useAction(actionSignature, listener, errorListener, beforeAction
24
24
  }
25
25
  return action;
26
26
  }, []);
27
+ // Replace the action function in place when its reference changes,
28
+ // preserving all listeners and the action identity. A changed function
29
+ // must keep a compatible signature (TypeScript fixes the generic from the
30
+ // initial render).
27
31
  useEffect(() => {
28
- if (updateRef.current > 0) {
29
- throw new Error("Action cannot be updated");
32
+ if (actionSignatureRef.current !== actionSignature) {
33
+ action.setAction(actionSignature);
34
+ actionSignatureRef.current = actionSignature;
30
35
  }
31
- updateRef.current++;
32
36
  }, [actionSignature]);
33
37
  useEffect(() => {
34
38
  if (listenerRef.current !== listener) {
@@ -91,7 +95,6 @@ export function useAction(actionSignature, listener, errorListener, beforeAction
91
95
  action.removeAllListeners();
92
96
  action.removeAllBeforeActionListeners();
93
97
  action.removeAllErrorListeners();
94
- updateRef.current = 0;
95
98
  };
96
99
  }, []);
97
100
  return action;
@@ -1,6 +1,6 @@
1
- import type { ActionResponse, ListenerSignature } from "../action";
2
- import type { BaseActionsMap } from "../actionBus";
3
- import { createActionBus } from "../actionBus";
4
- import type { ErrorListenerSignature, ErrorResponse } from "../lib/types";
1
+ import type { ActionResponse, ListenerSignature } from "../action.js";
2
+ import type { BaseActionsMap } from "../actionBus.js";
3
+ import { createActionBus } from "../actionBus.js";
4
+ import type { ErrorListenerSignature, ErrorResponse } from "../lib/types.js";
5
5
  export type { ActionResponse, BaseActionsMap, ErrorListenerSignature, ErrorResponse, ListenerSignature, };
6
6
  export declare function useActionBus<ActionsMap extends BaseActionsMap = BaseActionsMap>(initialActions?: ActionsMap, errorListener?: ErrorListenerSignature<any[]>): ReturnType<typeof createActionBus<ActionsMap>>;
@@ -1,6 +1,6 @@
1
1
  import { useContext, useEffect, useMemo, useRef } from "react";
2
- import { createActionBus } from "../actionBus";
3
- import { ErrorBoundaryContext } from "./ErrorBoundary";
2
+ import { createActionBus } from "../actionBus.js";
3
+ import { ErrorBoundaryContext } from "./ErrorBoundary.js";
4
4
  export function useActionBus(initialActions, errorListener) {
5
5
  const boundaryErrorListener = useContext(ErrorBoundaryContext);
6
6
  const errorListenerRef = useRef(errorListener);
@@ -15,6 +15,36 @@ export function useActionBus(initialActions, errorListener) {
15
15
  }
16
16
  return actionBus;
17
17
  }, []);
18
+ // Reconcile the actions map every render. Functions are compared by
19
+ // reference and never invoked.
20
+ const appliedActionsRef = useRef(Object.assign({}, initialActions));
21
+ const nextActions = (initialActions !== null && initialActions !== void 0 ? initialActions : {});
22
+ // Add newly-introduced actions during render (not in an effect): React runs
23
+ // child passive effects BEFORE parent passive effects, so a child rendered
24
+ // in the same pass that subscribes to a new action would otherwise throw
25
+ // "Action <name> not found". Parent render precedes child render, and
26
+ // add() is idempotent (a no-op if the action already exists).
27
+ for (const key in nextActions) {
28
+ actionBus.add(key, nextActions[key]);
29
+ }
30
+ // Replacements and removals can be deferred to a passive effect: a replaced
31
+ // action keeps its identity/listeners (so subscriptions are unaffected by
32
+ // timing), and removing late is harmless.
33
+ useEffect(() => {
34
+ const next = (initialActions !== null && initialActions !== void 0 ? initialActions : {});
35
+ const prev = appliedActionsRef.current;
36
+ for (const key in prev) {
37
+ if (!(key in next)) {
38
+ actionBus.removeAction(key);
39
+ }
40
+ }
41
+ for (const key in next) {
42
+ if (key in prev && next[key] !== prev[key]) {
43
+ actionBus.replace(key, next[key]);
44
+ }
45
+ }
46
+ appliedActionsRef.current = Object.assign({}, next);
47
+ });
18
48
  useEffect(() => {
19
49
  if (errorListenerRef.current !== errorListener) {
20
50
  if (errorListenerRef.current) {
@@ -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"]>;
@@ -0,0 +1,26 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ /**
3
+ * Subscribes to the status of a named action on an ActionBus and returns
4
+ * `{ loading, error, response }` for driving `loading`/`disabled` UI. This is
5
+ * the primary path for apps that route mutations through one shared ActionBus.
6
+ *
7
+ * An unregistered name reports an idle status and is safe to subscribe to.
8
+ */
9
+ export function useActionBusStatus(bus, name) {
10
+ const subscribe = useCallback((onChange) => {
11
+ const listener = () => {
12
+ onChange();
13
+ };
14
+ bus.onStatusChange(name, listener);
15
+ return () => {
16
+ bus.removeStatusListener(name, listener);
17
+ };
18
+ }, [bus, name]);
19
+ const getSnapshot = useCallback(() => bus.getStatus(name), [bus, name]);
20
+ const status = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
21
+ return {
22
+ loading: status.pending,
23
+ error: status.error,
24
+ response: status.response,
25
+ };
26
+ }
@@ -1,6 +1,6 @@
1
- import type { ActionResponse, ListenerSignature } from "../action";
2
- import type { BaseActionsMap } from "../actionBus";
3
- import { createActionMap } from "../actionMap";
4
- import type { ErrorListenerSignature, ErrorResponse } from "../lib/types";
1
+ import type { ActionResponse, ListenerSignature } from "../action.js";
2
+ import type { BaseActionsMap } from "../actionBus.js";
3
+ import { createActionMap } from "../actionMap.js";
4
+ import type { ErrorListenerSignature, ErrorResponse } from "../lib/types.js";
5
5
  export type { ActionResponse, BaseActionsMap, ErrorListenerSignature, ErrorResponse, ListenerSignature, };
6
6
  export declare function useActionMap<M extends BaseActionsMap>(actions: M, errorListener?: ErrorListenerSignature<any[]>): ReturnType<typeof createActionMap<M>>;
@@ -1,9 +1,12 @@
1
1
  import { useContext, useEffect, useMemo, useRef } from "react";
2
- import { createActionMap } from "../actionMap";
3
- import { ErrorBoundaryContext } from "./ErrorBoundary";
2
+ import { createActionMap } from "../actionMap.js";
3
+ import { ActionMapSetErrorListeners } from "../lib/actionMapInternal.js";
4
+ import { ErrorBoundaryContext } from "./ErrorBoundary.js";
4
5
  export function useActionMap(actions, errorListener) {
5
6
  const boundaryErrorListener = useContext(ErrorBoundaryContext);
6
- const changeRef = useRef(0);
7
+ const committedActionsRef = useRef(actions);
8
+ const committedErrorListenerRef = useRef(errorListener !== null && errorListener !== void 0 ? errorListener : null);
9
+ const committedBoundaryErrorListenerRef = useRef(boundaryErrorListener !== null && boundaryErrorListener !== void 0 ? boundaryErrorListener : null);
7
10
  const actionMap = useMemo(() => {
8
11
  const errorListeners = [
9
12
  ...(errorListener ? [errorListener] : []),
@@ -12,11 +15,41 @@ export function useActionMap(actions, errorListener) {
12
15
  const actionMap = createActionMap(actions, errorListeners);
13
16
  return actionMap;
14
17
  }, []);
18
+ // The action map TYPE fixes the available keys, so the key set is static:
19
+ // reconcile values only (in-place setAction) and the forwarded error
20
+ // listeners. A runtime key-set change is a type-contract violation and
21
+ // keeps a defensive throw.
15
22
  useEffect(() => {
16
- if (changeRef.current > 0) {
17
- throw new Error("useActionMap() does not support changing actions or errorListener");
23
+ const next = actions;
24
+ const prev = committedActionsRef.current;
25
+ const prevKeys = Object.keys(prev);
26
+ const nextKeys = Object.keys(next);
27
+ if (prevKeys.length !== nextKeys.length
28
+ || nextKeys.some((key) => !(key in prev))) {
29
+ throw new Error("useActionMap() does not support changing the set of action keys");
18
30
  }
19
- changeRef.current++;
20
- }, [actions, errorListener !== null && errorListener !== void 0 ? errorListener : null, boundaryErrorListener !== null && boundaryErrorListener !== void 0 ? boundaryErrorListener : null]);
31
+ for (const key of nextKeys) {
32
+ if (next[key] !== prev[key]) {
33
+ actionMap[key].setAction(next[key]);
34
+ }
35
+ }
36
+ committedActionsRef.current = next;
37
+ const nextErrorListener = errorListener !== null && errorListener !== void 0 ? errorListener : null;
38
+ const nextBoundaryErrorListener = boundaryErrorListener !== null && boundaryErrorListener !== void 0 ? boundaryErrorListener : null;
39
+ if (committedErrorListenerRef.current !== nextErrorListener
40
+ || committedBoundaryErrorListenerRef.current
41
+ !== nextBoundaryErrorListener) {
42
+ const errorListeners = [
43
+ ...(nextErrorListener ? [nextErrorListener] : []),
44
+ ...(nextBoundaryErrorListener
45
+ ? [nextBoundaryErrorListener]
46
+ : []),
47
+ ];
48
+ actionMap[ActionMapSetErrorListeners](errorListeners);
49
+ committedErrorListenerRef.current = nextErrorListener;
50
+ committedBoundaryErrorListenerRef.current =
51
+ nextBoundaryErrorListener;
52
+ }
53
+ });
21
54
  return actionMap;
22
55
  }
@@ -0,0 +1,20 @@
1
+ import type { ActionResponse, ActionStatus } from "../action.js";
2
+ import type { BaseHandler } from "../lib/types.js";
3
+ export type { ActionResponse, ActionStatus };
4
+ export type AsyncActionState<Response = any> = {
5
+ loading: boolean;
6
+ error: Error | null;
7
+ response: Response | null;
8
+ };
9
+ /**
10
+ * Wraps a function in an action and exposes its in-flight status, so a
11
+ * component can drive `loading`/`disabled` without a hand-rolled
12
+ * `useState(false)`. Returns `[invoke, { loading, error, response }]`.
13
+ *
14
+ * For the common app pattern (one shared ActionBus) prefer
15
+ * `useActionBusStatus`. This hook is for a standalone, component-local action.
16
+ */
17
+ export declare function useAsyncAction<Fn extends BaseHandler>(fn: Fn): readonly [
18
+ (...args: Parameters<Fn>) => Promise<ActionResponse<Awaited<ReturnType<Fn>>, Parameters<Fn>>>,
19
+ AsyncActionState<Awaited<ReturnType<Fn>>>
20
+ ];
@@ -0,0 +1,53 @@
1
+ import { useCallback, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react";
2
+ import { createAction } from "../action.js";
3
+ /**
4
+ * Wraps a function in an action and exposes its in-flight status, so a
5
+ * component can drive `loading`/`disabled` without a hand-rolled
6
+ * `useState(false)`. Returns `[invoke, { loading, error, response }]`.
7
+ *
8
+ * For the common app pattern (one shared ActionBus) prefer
9
+ * `useActionBusStatus`. This hook is for a standalone, component-local action.
10
+ */
11
+ export function useAsyncAction(fn) {
12
+ // Keep the latest fn in a ref. The action wraps a stable indirection that
13
+ // always calls fnRef.current, so it invokes the current fn even from a
14
+ // consumer layout effect that runs after a rerender but before this hook's
15
+ // passive effects — which a useEffect+setAction swap would miss, invoking
16
+ // the previous fn. The ref is updated in a layout effect (commit phase),
17
+ // not during render: a render-phase mutation would leak a fn from a
18
+ // suspended or abandoned concurrent render that never commits into the
19
+ // currently committed UI. Layout effects run only for committed renders,
20
+ // and this one runs before any consumer layout effect declared after the
21
+ // hook call, so consumers still observe the latest fn.
22
+ const fnRef = useRef(fn);
23
+ useLayoutEffect(() => {
24
+ fnRef.current = fn;
25
+ });
26
+ const action = useMemo(() => {
27
+ const action = createAction(((...args) => fnRef.current(...args)));
28
+ // Without an error listener a throwing fn re-throws out of invoke
29
+ // (an unhandled rejection) instead of surfacing through status.
30
+ action.addErrorListener(() => { });
31
+ return action;
32
+ }, []);
33
+ const subscribe = useCallback((onChange) => {
34
+ const listener = () => {
35
+ onChange();
36
+ };
37
+ action.onStatusChange(listener);
38
+ return () => {
39
+ action.removeStatusListener(listener);
40
+ };
41
+ }, [action]);
42
+ const getSnapshot = useCallback(() => action.getStatus(), [action]);
43
+ const status = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
44
+ const invoke = useCallback((...args) => action.invoke(...args), [action]);
45
+ return [
46
+ invoke,
47
+ {
48
+ loading: status.pending,
49
+ error: status.error,
50
+ response: status.response,
51
+ },
52
+ ];
53
+ }
@@ -1,4 +1,4 @@
1
- import { createEvent, type EventOptions } from "../event";
2
- import type { BaseHandler, ErrorListenerSignature, ErrorResponse } from "../lib/types";
1
+ import { createEvent, type EventOptions } from "../event.js";
2
+ import type { BaseHandler, ErrorListenerSignature, ErrorResponse } from "../lib/types.js";
3
3
  export type { BaseHandler, ErrorListenerSignature, ErrorResponse, EventOptions, };
4
4
  export declare function useEvent<Listener extends BaseHandler = BaseHandler, ErrorListener extends ErrorListenerSignature<Parameters<Listener>> = ErrorListenerSignature<Parameters<Listener>>>(eventOptions?: EventOptions<Listener>, listener?: Listener | null, errorListener?: ErrorListener | null): ReturnType<typeof createEvent<Listener>>;
@@ -1,6 +1,8 @@
1
1
  import { useContext, useEffect, useMemo, useRef } from "react";
2
- import { createEvent } from "../event";
3
- import { ErrorBoundaryContext } from "./ErrorBoundary";
2
+ import { createEvent } from "../event.js";
3
+ import { normalizeEventOptions } from "../lib/normalizeEventOptions.js";
4
+ import { ErrorBoundaryContext } from "./ErrorBoundary.js";
5
+ import { areEventOptionsEqual } from "./listenerOptionsEqual.js";
4
6
  export function useEvent(eventOptions = {}, listener, errorListener) {
5
7
  const boundaryErrorListener = useContext(ErrorBoundaryContext);
6
8
  const listenerRef = useRef(listener);
@@ -19,6 +21,20 @@ export function useEvent(eventOptions = {}, listener, errorListener) {
19
21
  }
20
22
  return event;
21
23
  }, []);
24
+ // Reconcile event options across renders without relying on object
25
+ // identity. Applied in place via setOptions; triggered count is preserved.
26
+ const committedEventOptionsRef = useRef(eventOptions);
27
+ useEffect(() => {
28
+ if (committedEventOptionsRef.current === eventOptions) {
29
+ return;
30
+ }
31
+ if (!areEventOptionsEqual(committedEventOptionsRef.current, eventOptions)) {
32
+ // Normalize so fields removed since the last render reset to their
33
+ // defaults (event.setOptions merges, it does not reset).
34
+ event.setOptions(normalizeEventOptions(eventOptions));
35
+ }
36
+ committedEventOptionsRef.current = eventOptions;
37
+ });
22
38
  useEffect(() => {
23
39
  if (listenerRef.current !== listener) {
24
40
  if (listenerRef.current) {
@@ -1,4 +1,4 @@
1
- import { BaseEventMap, createEventBus, DefaultEventMap, EventBusOptions } from "../eventBus";
2
- import type { BaseHandler, ErrorListenerSignature, ErrorResponse } from "../lib/types";
1
+ import { BaseEventMap, createEventBus, DefaultEventMap, EventBusOptions } from "../eventBus.js";
2
+ import type { BaseHandler, ErrorListenerSignature, ErrorResponse } from "../lib/types.js";
3
3
  export type { BaseEventMap, BaseHandler, ErrorListenerSignature, ErrorResponse, EventBusOptions, };
4
4
  export declare function useEventBus<EventsMap extends BaseEventMap = DefaultEventMap>(eventBusOptions?: EventBusOptions<EventsMap>, allEventsListener?: BaseHandler, errorListener?: ErrorListenerSignature<any[]>): ReturnType<typeof createEventBus<EventsMap>>;
@@ -1,9 +1,10 @@
1
1
  import { useContext, useEffect, useMemo, useRef } from "react";
2
- import { createEventBus, } from "../eventBus";
3
- import { ErrorBoundaryContext } from "./ErrorBoundary";
2
+ import { createEventBus, } from "../eventBus.js";
3
+ import { ErrorBoundaryContext } from "./ErrorBoundary.js";
4
+ import { areEventBusOptionsEqual } from "./listenerOptionsEqual.js";
4
5
  export function useEventBus(eventBusOptions, allEventsListener, errorListener) {
5
6
  const boundaryErrorListener = useContext(ErrorBoundaryContext);
6
- const updateRef = useRef(0);
7
+ const committedOptionsRef = useRef(eventBusOptions);
7
8
  const errorListenerRef = useRef(errorListener || null);
8
9
  const allEventsListenerRef = useRef(allEventsListener || null);
9
10
  const boundaryErrorListenerRef = useRef(boundaryErrorListener || null);
@@ -20,14 +21,18 @@ export function useEventBus(eventBusOptions, allEventsListener, errorListener) {
20
21
  }
21
22
  return eventBus;
22
23
  }, []);
24
+ // Reconcile event bus options across renders instead of throwing. Present
25
+ // entries are applied via event.setOptions; a removed event-name entry
26
+ // leaves the existing event unchanged.
23
27
  useEffect(() => {
24
- if (eventBusOptions) {
25
- if (updateRef.current > 0) {
26
- throw new Error("EventBus options can't be updated");
27
- }
28
- updateRef.current++;
28
+ if (committedOptionsRef.current === eventBusOptions) {
29
+ return;
30
+ }
31
+ if (!areEventBusOptionsEqual(committedOptionsRef.current, eventBusOptions)) {
32
+ eventBus.setOptions(eventBusOptions);
29
33
  }
30
- }, [eventBusOptions]);
34
+ committedOptionsRef.current = eventBusOptions;
35
+ });
31
36
  useEffect(() => {
32
37
  if (allEventsListenerRef.current !== allEventsListener) {
33
38
  if (allEventsListenerRef.current) {
@@ -76,7 +81,6 @@ export function useEventBus(eventBusOptions, allEventsListener, errorListener) {
76
81
  eventBus.removeErrorListener(boundaryErrorListenerRef.current);
77
82
  boundaryErrorListenerRef.current = null;
78
83
  }
79
- updateRef.current = 0;
80
84
  };
81
85
  }, []);
82
86
  return eventBus;
@@ -1,3 +1,3 @@
1
- import type { BaseAction } from "../action";
1
+ import type { BaseAction } from "../action.js";
2
2
  export type { BaseAction };
3
3
  export declare function useListenToAction<TAction extends BaseAction, TListenerSignature extends TAction["__type"]["listenerSignature"] = TAction["__type"]["listenerSignature"], TErrorListenerSignature extends TAction["__type"]["errorListenerSignature"] = TAction["__type"]["errorListenerSignature"], TBeforeActionListenerSignature extends TAction["__type"]["beforeActionSignature"] = TAction["__type"]["beforeActionSignature"]>(action: TAction, listener: TListenerSignature | null, errorListener?: TErrorListenerSignature | null, beforeActionListener?: TBeforeActionListenerSignature | null): void;
@@ -1,52 +1,31 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
2
  export function useListenToAction(action, listener, errorListener, beforeActionListener) {
3
3
  const listenerRef = useRef(listener);
4
- const actionRef = useRef(action);
5
- const errorListenerRef = useRef(null);
6
- const beforeActionListenerRef = useRef(null);
7
4
  listenerRef.current = listener;
8
5
  const genericHandler = useCallback((arg) => {
9
6
  var _a;
10
7
  (_a = listenerRef.current) === null || _a === void 0 ? void 0 : _a.call(listenerRef, arg);
11
8
  }, []);
12
9
  useEffect(() => {
13
- actionRef.current.removeListener(genericHandler);
14
- actionRef.current = action;
15
- actionRef.current.addListener(genericHandler);
16
- }, [action]);
10
+ action.addListener(genericHandler);
11
+ return () => {
12
+ action.removeListener(genericHandler);
13
+ };
14
+ }, [action, genericHandler]);
17
15
  useEffect(() => {
18
- if (errorListenerRef.current !== errorListener) {
19
- if (errorListenerRef.current) {
20
- actionRef.current.removeErrorListener(errorListenerRef.current);
21
- }
22
- errorListenerRef.current = errorListener || null;
23
- if (errorListener) {
24
- actionRef.current.addErrorListener(errorListener);
25
- }
16
+ if (errorListener) {
17
+ action.addErrorListener(errorListener);
18
+ return () => {
19
+ action.removeErrorListener(errorListener);
20
+ };
26
21
  }
27
- }, [errorListener]);
22
+ }, [action, errorListener]);
28
23
  useEffect(() => {
29
- if (beforeActionListenerRef.current !== beforeActionListener) {
30
- if (beforeActionListenerRef.current) {
31
- actionRef.current.removeBeforeActionListener(beforeActionListenerRef.current);
32
- }
33
- beforeActionListenerRef.current = beforeActionListener || null;
34
- if (beforeActionListener) {
35
- actionRef.current.addBeforeActionListener(beforeActionListener);
36
- }
24
+ if (beforeActionListener) {
25
+ action.addBeforeActionListener(beforeActionListener);
26
+ return () => {
27
+ action.removeBeforeActionListener(beforeActionListener);
28
+ };
37
29
  }
38
- }, [beforeActionListener]);
39
- useEffect(() => {
40
- return () => {
41
- actionRef.current.removeListener(genericHandler);
42
- if (errorListenerRef.current) {
43
- actionRef.current.removeErrorListener(errorListenerRef.current);
44
- errorListenerRef.current = null;
45
- }
46
- if (beforeActionListenerRef.current) {
47
- actionRef.current.removeBeforeActionListener(beforeActionListenerRef.current);
48
- beforeActionListenerRef.current = null;
49
- }
50
- };
51
- }, []);
30
+ }, [action, beforeActionListener]);
52
31
  }
@@ -1,6 +1,6 @@
1
- import type { BaseActionBus } from "../actionBus";
2
- import type { ListenerOptions } from "../event";
3
- import type { ErrorListenerSignature, KeyOf } from "../lib/types";
1
+ import type { BaseActionBus } from "../actionBus.js";
2
+ import type { ListenerOptions } from "../event.js";
3
+ import type { ErrorListenerSignature, KeyOf } from "../lib/types.js";
4
4
  export type { BaseActionBus, ErrorListenerSignature, ListenerOptions };
5
5
  export declare function useListenToActionBus<TActionBus extends BaseActionBus, TKey extends KeyOf<TActionBus["__type"]["actions"]>, TListener extends TActionBus["__type"]["actions"][TKey]["listenerSignature"], TBeforeActionListener extends TActionBus["__type"]["actions"][TKey]["beforeActionSignature"]>(actionBus: TActionBus, actionName: TKey, listener?: TListener | null | {
6
6
  listener?: TListener;
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
+ import { useReconciledListener } from "./useReconciledListener.js";
2
3
  export function useListenToActionBus(actionBus, actionName, listener, options, errorListener, beforeActionListener) {
3
4
  if (listener && typeof listener !== "function") {
4
5
  options = listener.options;
@@ -16,17 +17,22 @@ export function useListenToActionBus(actionBus, actionName, listener, options, e
16
17
  }, []);
17
18
  const genericBeforeActionHandler = useCallback((...args) => {
18
19
  var _a;
19
- return ((_a = beforeActionListenerRef.current) === null || _a === void 0 ? void 0 : _a.call(beforeActionListenerRef, ...args)) || undefined;
20
+ return (_a = beforeActionListenerRef.current) === null || _a === void 0 ? void 0 : _a.call(beforeActionListenerRef, ...args);
20
21
  }, []);
21
- // Main listener + beforeAction listener - tied to actionName
22
- useEffect(() => {
23
- actionBus.addListener(actionName, genericHandler, options || undefined);
24
- actionBus.get(actionName).addBeforeActionListener(genericBeforeActionHandler);
25
- return () => {
26
- actionBus.removeListener(actionName, genericHandler);
22
+ // Main listener + beforeAction listener - reconciled across changes
23
+ useReconciledListener({
24
+ keyDeps: [actionBus, actionName],
25
+ options: options !== null && options !== void 0 ? options : undefined,
26
+ subscribe: (opts) => {
27
+ actionBus.addListener(actionName, genericHandler, opts !== null && opts !== void 0 ? opts : undefined);
28
+ actionBus.get(actionName).addBeforeActionListener(genericBeforeActionHandler);
29
+ },
30
+ unsubscribe: (ctx) => {
31
+ actionBus.removeListener(actionName, genericHandler, ctx);
27
32
  actionBus.get(actionName).removeBeforeActionListener(genericBeforeActionHandler);
28
- };
29
- }, [actionBus, actionName, genericHandler, genericBeforeActionHandler]);
33
+ },
34
+ update: (ctx, opts) => actionBus.updateListenerOptions(actionName, genericHandler, ctx, opts !== null && opts !== void 0 ? opts : undefined),
35
+ });
30
36
  // Error listener - bus level
31
37
  useEffect(() => {
32
38
  if (errorListener) {