@kuindji/reactive 1.0.24 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -14
- package/dist/action.d.ts +31 -10
- package/dist/action.js +156 -23
- package/dist/actionBus.d.ts +13 -4
- package/dist/actionBus.js +201 -5
- package/dist/actionMap.d.ts +26 -19
- package/dist/actionMap.js +10 -4
- package/dist/event.d.ts +37 -3
- package/dist/event.js +345 -78
- package/dist/eventBus.d.ts +7 -3
- package/dist/eventBus.js +194 -34
- package/dist/index.d.ts +7 -7
- package/dist/index.js +7 -7
- package/dist/lib/actionMapInternal.d.ts +8 -0
- package/dist/lib/actionMapInternal.js +8 -0
- package/dist/lib/isPromiseLike.d.ts +1 -0
- package/dist/lib/isPromiseLike.js +5 -0
- package/dist/lib/normalizeEventOptions.d.ts +13 -0
- package/dist/lib/normalizeEventOptions.js +21 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/react/ErrorBoundary.d.ts +1 -1
- package/dist/react/listenerOptionsEqual.d.ts +27 -0
- package/dist/react/listenerOptionsEqual.js +121 -0
- package/dist/react/useAction.d.ts +3 -3
- package/dist/react/useAction.js +10 -7
- package/dist/react/useActionBus.d.ts +4 -4
- package/dist/react/useActionBus.js +32 -2
- package/dist/react/useActionBusStatus.d.ts +13 -0
- package/dist/react/useActionBusStatus.js +26 -0
- package/dist/react/useActionMap.d.ts +4 -4
- package/dist/react/useActionMap.js +40 -7
- package/dist/react/useAsyncAction.d.ts +20 -0
- package/dist/react/useAsyncAction.js +53 -0
- package/dist/react/useEvent.d.ts +2 -2
- package/dist/react/useEvent.js +18 -2
- package/dist/react/useEventBus.d.ts +2 -2
- package/dist/react/useEventBus.js +14 -10
- package/dist/react/useListenToAction.d.ts +1 -1
- package/dist/react/useListenToAction.js +17 -38
- package/dist/react/useListenToActionBus.d.ts +3 -3
- package/dist/react/useListenToActionBus.js +15 -9
- package/dist/react/useListenToEvent.d.ts +2 -2
- package/dist/react/useListenToEvent.js +8 -6
- package/dist/react/useListenToEventBus.d.ts +3 -3
- package/dist/react/useListenToEventBus.js +9 -7
- package/dist/react/useListenToStoreChanges.d.ts +3 -3
- package/dist/react/useListenToStoreChanges.js +9 -7
- package/dist/react/useReconciledListener.d.ts +33 -0
- package/dist/react/useReconciledListener.js +44 -0
- package/dist/react/useStore.d.ts +2 -2
- package/dist/react/useStore.js +71 -19
- package/dist/react/useStoreSelector.d.ts +35 -0
- package/dist/react/useStoreSelector.js +144 -0
- package/dist/react/useStoreState.d.ts +2 -2
- package/dist/react/useStoreState.js +26 -21
- package/dist/react.d.ts +16 -13
- package/dist/react.js +16 -13
- package/dist/store.d.ts +12 -8
- package/dist/store.js +473 -39
- package/package.json +13 -3
|
@@ -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>>;
|
package/dist/react/useAction.js
CHANGED
|
@@ -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
|
|
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 (
|
|
29
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
+
}
|
package/dist/react/useEvent.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/react/useEvent.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo, useRef } from "react";
|
|
2
|
-
import { createEvent } from "../event";
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (committedOptionsRef.current === eventBusOptions) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!areEventBusOptionsEqual(committedOptionsRef.current, eventBusOptions)) {
|
|
32
|
+
eventBus.setOptions(eventBusOptions);
|
|
29
33
|
}
|
|
30
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
action.addListener(genericHandler);
|
|
11
|
+
return () => {
|
|
12
|
+
action.removeListener(genericHandler);
|
|
13
|
+
};
|
|
14
|
+
}, [action, genericHandler]);
|
|
17
15
|
useEffect(() => {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 (
|
|
20
|
+
return (_a = beforeActionListenerRef.current) === null || _a === void 0 ? void 0 : _a.call(beforeActionListenerRef, ...args);
|
|
20
21
|
}, []);
|
|
21
|
-
// Main listener + beforeAction listener -
|
|
22
|
-
|
|
23
|
-
actionBus
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
actionBus.
|
|
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
|
-
|
|
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) {
|