@kuindji/reactive 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -5
- package/dist/action.d.ts +19 -0
- package/dist/action.js +137 -14
- package/dist/actionBus.d.ts +5 -0
- package/dist/actionBus.js +168 -4
- package/dist/actionMap.d.ts +5 -0
- package/dist/event.d.ts +34 -1
- package/dist/event.js +256 -43
- package/dist/eventBus.d.ts +2 -0
- package/dist/eventBus.js +106 -3
- package/dist/lib/types.d.ts +1 -1
- package/dist/react/useActionBusStatus.d.ts +13 -0
- package/dist/react/useActionBusStatus.js +26 -0
- package/dist/react/useAsyncAction.d.ts +20 -0
- package/dist/react/useAsyncAction.js +53 -0
- package/dist/react/useStoreSelector.d.ts +35 -0
- package/dist/react/useStoreSelector.js +144 -0
- package/dist/react/useStoreState.js +13 -1
- package/dist/react.d.ts +3 -0
- package/dist/react.js +3 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +410 -43
- package/package.json +1 -1
package/dist/actionBus.js
CHANGED
|
@@ -3,16 +3,44 @@ import { createEvent } from "./event.js";
|
|
|
3
3
|
export function createActionBus(initialActions = {}, errorListener) {
|
|
4
4
|
const actions = new Map();
|
|
5
5
|
const errorEvent = createEvent();
|
|
6
|
+
let destroyed = false;
|
|
7
|
+
// The error-forwarding listener attached to each action's error event is
|
|
8
|
+
// retained per name so removeAction can detach it. Otherwise a held action
|
|
9
|
+
// reference keeps forwarding errors into the bus after removal, and — since
|
|
10
|
+
// the forwarder counts as an error listener — its invoke resolves with an
|
|
11
|
+
// error response instead of rejecting.
|
|
12
|
+
const errorForwarders = new Map();
|
|
13
|
+
// Status subscriptions for actions that are not registered yet. They are
|
|
14
|
+
// recorded here so a later add()/replace() can attach them — otherwise a
|
|
15
|
+
// hook subscribing before registration (e.g. useActionBusStatus) would stay
|
|
16
|
+
// unsubscribed forever. Kept after attach so a re-added action reattaches.
|
|
17
|
+
const pendingStatusListeners = new Map();
|
|
6
18
|
if (errorListener) {
|
|
7
19
|
errorEvent.addListener(errorListener);
|
|
8
20
|
}
|
|
9
21
|
const add = (name, action) => {
|
|
22
|
+
if (destroyed) {
|
|
23
|
+
throw new Error("ActionBus is destroyed");
|
|
24
|
+
}
|
|
10
25
|
if (!actions.has(name)) {
|
|
11
26
|
const a = createAction(action);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
// Preserve the original error type (e.g. "action-status") rather
|
|
28
|
+
// than hardcoding "action", so consumers can distinguish a failed
|
|
29
|
+
// invocation from a throwing status listener.
|
|
30
|
+
const forwarder = ({ error, args, type }) => {
|
|
31
|
+
errorEvent.emit({ name, error, args, type });
|
|
32
|
+
};
|
|
33
|
+
errorForwarders.set(name, forwarder);
|
|
34
|
+
a.addErrorListener(forwarder);
|
|
15
35
|
actions.set(name, a);
|
|
36
|
+
// Attach any status subscriptions that were registered before this
|
|
37
|
+
// action existed.
|
|
38
|
+
const pending = pendingStatusListeners.get(name);
|
|
39
|
+
if (pending) {
|
|
40
|
+
pending.forEach((handler) => {
|
|
41
|
+
a.onStatusChange(handler);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
16
44
|
}
|
|
17
45
|
};
|
|
18
46
|
Object.entries(initialActions).forEach(([name, action]) => {
|
|
@@ -28,6 +56,9 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
28
56
|
// its listeners and the bus error-forwarding listener, which is attached to
|
|
29
57
|
// the action's error event, not the function); otherwise add it.
|
|
30
58
|
const replace = (name, action) => {
|
|
59
|
+
if (destroyed) {
|
|
60
|
+
throw new Error("ActionBus is destroyed");
|
|
61
|
+
}
|
|
31
62
|
const existing = actions.get(name);
|
|
32
63
|
if (existing) {
|
|
33
64
|
existing.setAction(action);
|
|
@@ -40,9 +71,60 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
40
71
|
// removeListener. The removed action's listeners and its error-forwarding
|
|
41
72
|
// listener are dropped with it (they lived on the action's own events).
|
|
42
73
|
const removeAction = (name) => {
|
|
43
|
-
actions.
|
|
74
|
+
const action = actions.get(name);
|
|
75
|
+
const existed = actions.delete(name);
|
|
76
|
+
// Detach the bus error-forwarding listener from the removed action so a
|
|
77
|
+
// held reference no longer feeds the bus error event (and so its invoke
|
|
78
|
+
// rejects rather than resolving via the forwarder acting as an error
|
|
79
|
+
// listener). A later re-add() installs a fresh forwarder.
|
|
80
|
+
const forwarder = errorForwarders.get(name);
|
|
81
|
+
if (forwarder) {
|
|
82
|
+
action === null || action === void 0 ? void 0 : action.removeErrorListener(forwarder);
|
|
83
|
+
errorForwarders.delete(name);
|
|
84
|
+
}
|
|
85
|
+
// getStatus() now reports idle for this name, but status subscribers
|
|
86
|
+
// (e.g. useActionBusStatus via useSyncExternalStore) were attached to
|
|
87
|
+
// the removed action's own status event and will never be notified of
|
|
88
|
+
// the drop. Detach each retained subscription from the removed action
|
|
89
|
+
// (otherwise invoking a held action reference keeps notifying a
|
|
90
|
+
// listener that bus.removeStatusListener can no longer reach), then push
|
|
91
|
+
// an idle status so they re-read and clear stale state. Subscriptions
|
|
92
|
+
// stay in pendingStatusListeners so a later re-add() reattaches them.
|
|
93
|
+
if (existed) {
|
|
94
|
+
const pending = pendingStatusListeners.get(name);
|
|
95
|
+
pending === null || pending === void 0 ? void 0 : pending.forEach((handler) => {
|
|
96
|
+
action === null || action === void 0 ? void 0 : action.removeStatusListener(handler);
|
|
97
|
+
// Isolate each notify: one throwing subscriber must not abort
|
|
98
|
+
// the loop and leave the remaining subscribers attached and
|
|
99
|
+
// un-notified. Route the failure to the bus error event.
|
|
100
|
+
try {
|
|
101
|
+
handler(idleStatus);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// The error forwarding must not re-escape either: a throwing
|
|
105
|
+
// bus error listener would abort the loop and leave later
|
|
106
|
+
// subscribers attached and un-notified.
|
|
107
|
+
try {
|
|
108
|
+
errorEvent.emit({
|
|
109
|
+
name,
|
|
110
|
+
error: error instanceof Error
|
|
111
|
+
? error
|
|
112
|
+
: new Error(String(error)),
|
|
113
|
+
args: [],
|
|
114
|
+
type: "action-status",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (_a) {
|
|
118
|
+
// Nothing left to route to; swallow to keep the loop going.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
44
123
|
};
|
|
45
124
|
const invoke = (name, ...args) => {
|
|
125
|
+
if (destroyed) {
|
|
126
|
+
throw new Error("ActionBus is destroyed");
|
|
127
|
+
}
|
|
46
128
|
const action = get(name);
|
|
47
129
|
if (!action) {
|
|
48
130
|
throw new Error(`Action ${name} not found`);
|
|
@@ -50,6 +132,9 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
50
132
|
return action.invoke(...args);
|
|
51
133
|
};
|
|
52
134
|
const on = (name, handler, options) => {
|
|
135
|
+
if (destroyed) {
|
|
136
|
+
throw new Error("ActionBus is destroyed");
|
|
137
|
+
}
|
|
53
138
|
const action = get(name);
|
|
54
139
|
if (!action) {
|
|
55
140
|
throw new Error(`Action ${name} not found`);
|
|
@@ -57,6 +142,9 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
57
142
|
return action.addListener(handler, options);
|
|
58
143
|
};
|
|
59
144
|
const once = (name, handler, options) => {
|
|
145
|
+
if (destroyed) {
|
|
146
|
+
throw new Error("ActionBus is destroyed");
|
|
147
|
+
}
|
|
60
148
|
options = options || {};
|
|
61
149
|
options.limit = 1;
|
|
62
150
|
const action = get(name);
|
|
@@ -66,6 +154,9 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
66
154
|
return action.addListener(handler, options);
|
|
67
155
|
};
|
|
68
156
|
const un = (name, handler, context, tag) => {
|
|
157
|
+
if (destroyed) {
|
|
158
|
+
throw new Error("ActionBus is destroyed");
|
|
159
|
+
}
|
|
69
160
|
const action = get(name);
|
|
70
161
|
if (!action) {
|
|
71
162
|
throw new Error(`Action ${name} not found`);
|
|
@@ -73,12 +164,80 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
73
164
|
return action.removeListener(handler, context, tag);
|
|
74
165
|
};
|
|
75
166
|
const updateListenerOptions = (name, handler, context, nextOptions) => {
|
|
167
|
+
if (destroyed) {
|
|
168
|
+
throw new Error("ActionBus is destroyed");
|
|
169
|
+
}
|
|
76
170
|
const action = get(name);
|
|
77
171
|
if (!action) {
|
|
78
172
|
return false;
|
|
79
173
|
}
|
|
80
174
|
return action.updateListenerOptions(handler, context, nextOptions);
|
|
81
175
|
};
|
|
176
|
+
// Frozen and shared: getStatus() hands this single object to every missing
|
|
177
|
+
// action, so a consumer mutating it would corrupt all future idle snapshots
|
|
178
|
+
// (e.g. leaving a hook reporting false loading). Matches action status,
|
|
179
|
+
// whose snapshots are also frozen.
|
|
180
|
+
const idleStatus = Object.freeze({
|
|
181
|
+
pending: false,
|
|
182
|
+
error: null,
|
|
183
|
+
response: null,
|
|
184
|
+
});
|
|
185
|
+
// Status lives on the underlying action (the single in-flight point);
|
|
186
|
+
// the bus just delegates per name. An unregistered name reports idle and
|
|
187
|
+
// is a no-op to (un)subscribe.
|
|
188
|
+
const getStatus = (name) => {
|
|
189
|
+
const action = get(name);
|
|
190
|
+
if (!action) {
|
|
191
|
+
return idleStatus;
|
|
192
|
+
}
|
|
193
|
+
return action.getStatus();
|
|
194
|
+
};
|
|
195
|
+
const onStatusChange = (name, handler) => {
|
|
196
|
+
if (destroyed) {
|
|
197
|
+
throw new Error("ActionBus is destroyed");
|
|
198
|
+
}
|
|
199
|
+
// Record the subscription so it survives (re)registration, then attach
|
|
200
|
+
// to the action now if it already exists.
|
|
201
|
+
let pending = pendingStatusListeners.get(name);
|
|
202
|
+
if (!pending) {
|
|
203
|
+
pending = new Set();
|
|
204
|
+
pendingStatusListeners.set(name, pending);
|
|
205
|
+
}
|
|
206
|
+
pending.add(handler);
|
|
207
|
+
const action = get(name);
|
|
208
|
+
if (!action) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
return action.onStatusChange(handler);
|
|
212
|
+
};
|
|
213
|
+
const removeStatusListener = (name, handler) => {
|
|
214
|
+
const pending = pendingStatusListeners.get(name);
|
|
215
|
+
if (pending) {
|
|
216
|
+
pending.delete(handler);
|
|
217
|
+
if (pending.size === 0) {
|
|
218
|
+
pendingStatusListeners.delete(name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const action = get(name);
|
|
222
|
+
if (!action) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
return action.removeStatusListener(handler);
|
|
226
|
+
};
|
|
227
|
+
// One-call teardown: destroy each owned action and the error event, then
|
|
228
|
+
// drop them all. Post-destroy invoke/addListener throw rather than silently
|
|
229
|
+
// no-op.
|
|
230
|
+
const destroy = () => {
|
|
231
|
+
actions.forEach((action) => {
|
|
232
|
+
action.destroy();
|
|
233
|
+
});
|
|
234
|
+
actions.clear();
|
|
235
|
+
pendingStatusListeners.clear();
|
|
236
|
+
errorForwarders.clear();
|
|
237
|
+
errorEvent.destroy();
|
|
238
|
+
destroyed = true;
|
|
239
|
+
};
|
|
240
|
+
const isDestroyed = () => destroyed;
|
|
82
241
|
const api = {
|
|
83
242
|
add,
|
|
84
243
|
replace,
|
|
@@ -86,6 +245,11 @@ export function createActionBus(initialActions = {}, errorListener) {
|
|
|
86
245
|
has,
|
|
87
246
|
get,
|
|
88
247
|
invoke,
|
|
248
|
+
destroy,
|
|
249
|
+
isDestroyed,
|
|
250
|
+
getStatus,
|
|
251
|
+
onStatusChange,
|
|
252
|
+
removeStatusListener,
|
|
89
253
|
addListener: on,
|
|
90
254
|
/** @alias addListener */
|
|
91
255
|
on,
|
package/dist/actionMap.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export type { ActionResponse, BaseActionsMap, ErrorListenerSignature, ErrorRespo
|
|
|
5
5
|
export declare function createActionMap<M extends BaseActionsMap>(actions: M, onAnyError?: ErrorListenerSignature<any[]> | ErrorListenerSignature<any[]>[]): { [key in KeyOf<M>]: {
|
|
6
6
|
readonly invoke: (...args: Parameters<M[key]>) => Promise<ActionResponse<Awaited<ReturnType<M[key]>>, Parameters<M[key]>>>;
|
|
7
7
|
readonly setAction: (nextAction: M[key]) => void;
|
|
8
|
+
readonly destroy: () => void;
|
|
9
|
+
readonly isDestroyed: () => boolean;
|
|
10
|
+
readonly getStatus: () => import("./action.js").ActionStatus<Awaited<ReturnType<M[key]>>>;
|
|
11
|
+
readonly onStatusChange: (handler: import("./action.js").StatusListenerSignature<M[key]>, listenerOptions?: import("./event.js").ListenerOptions) => void;
|
|
12
|
+
readonly removeStatusListener: (handler: import("./action.js").StatusListenerSignature<M[key]>, context?: object | null, tag?: string | null) => boolean;
|
|
8
13
|
readonly addListener: (handler: import("./action.js").ListenerSignature<M[key]>, listenerOptions?: import("./event.js").ListenerOptions) => void;
|
|
9
14
|
readonly on: (handler: import("./action.js").ListenerSignature<M[key]>, listenerOptions?: import("./event.js").ListenerOptions) => void;
|
|
10
15
|
readonly subscribe: (handler: import("./action.js").ListenerSignature<M[key]>, listenerOptions?: import("./event.js").ListenerOptions) => void;
|
package/dist/event.d.ts
CHANGED
|
@@ -44,13 +44,39 @@ export interface ListenerOptions extends BaseOptions {
|
|
|
44
44
|
* You can pass any additional fields here. They will be passed back to TriggerFilter
|
|
45
45
|
*/
|
|
46
46
|
extraData?: any;
|
|
47
|
+
/**
|
|
48
|
+
* When provided, the listener is auto-removed once the signal aborts. If the
|
|
49
|
+
* signal is already aborted the listener is not added at all.
|
|
50
|
+
*/
|
|
51
|
+
signal?: AbortSignal | null;
|
|
47
52
|
}
|
|
48
|
-
interface ListenerPrototype<Handler extends BaseHandler> extends Required<ListenerOptions
|
|
53
|
+
interface ListenerPrototype<Handler extends BaseHandler> extends Required<Omit<ListenerOptions, "signal">> {
|
|
49
54
|
handler: Handler;
|
|
50
55
|
called: number;
|
|
51
56
|
count: number;
|
|
52
57
|
index: number;
|
|
53
58
|
start: number;
|
|
59
|
+
abortCleanup: (() => void) | null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read-only projection of a registered listener, returned by `getListeners()`.
|
|
63
|
+
* Carries the listener's options plus its live `called`/`count` counters but
|
|
64
|
+
* none of the mutable internals — mutating this object does not affect the
|
|
65
|
+
* event.
|
|
66
|
+
*/
|
|
67
|
+
export interface ListenerInfo<Handler extends BaseHandler = BaseHandler> {
|
|
68
|
+
handler: Handler;
|
|
69
|
+
context: object | null;
|
|
70
|
+
tags: string[];
|
|
71
|
+
limit: number;
|
|
72
|
+
start: number;
|
|
73
|
+
called: number;
|
|
74
|
+
count: number;
|
|
75
|
+
async: boolean | number | null;
|
|
76
|
+
first: boolean;
|
|
77
|
+
alwaysFirst: boolean;
|
|
78
|
+
alwaysLast: boolean;
|
|
79
|
+
extraData: any;
|
|
54
80
|
}
|
|
55
81
|
export interface EventOptions<ListenerSignature extends BaseHandler> extends BaseOptions {
|
|
56
82
|
/**
|
|
@@ -92,6 +118,7 @@ export declare function createEvent<ListenerSignature extends BaseHandler>(event
|
|
|
92
118
|
readonly listen: (handler: ListenerSignature, listenerOptions?: ListenerOptions) => void;
|
|
93
119
|
/** @alias addListener */
|
|
94
120
|
readonly subscribe: (handler: ListenerSignature, listenerOptions?: ListenerOptions) => void;
|
|
121
|
+
readonly once: (handler: ListenerSignature, listenerOptions?: ListenerOptions) => void;
|
|
95
122
|
readonly removeListener: (handler: ListenerSignature, context?: object | null, tag?: string | null) => boolean;
|
|
96
123
|
readonly updateListenerOptions: (handler: ListenerSignature, context?: object | null, nextOptions?: ListenerOptions) => boolean;
|
|
97
124
|
/** @alias removeListener */
|
|
@@ -117,8 +144,14 @@ export declare function createEvent<ListenerSignature extends BaseHandler>(event
|
|
|
117
144
|
readonly resume: () => void;
|
|
118
145
|
readonly setOptions: (eventOptions: Partial<EventOptions<ListenerSignature>>) => void;
|
|
119
146
|
readonly reset: () => void;
|
|
147
|
+
readonly destroy: () => void;
|
|
148
|
+
readonly isDestroyed: () => boolean;
|
|
120
149
|
readonly isSuspended: () => boolean;
|
|
121
150
|
readonly isQueued: () => boolean;
|
|
151
|
+
readonly listenerCount: (tag?: string | null) => number;
|
|
152
|
+
readonly triggeredCount: () => number;
|
|
153
|
+
readonly lastTriggerArgs: () => Parameters<ListenerSignature> | null;
|
|
154
|
+
readonly getListeners: () => ListenerInfo<ListenerSignature>[];
|
|
122
155
|
readonly withTags: <R>(tags: string[], callback: () => R) => R;
|
|
123
156
|
readonly promise: (options?: ListenerOptions) => Promise<Parameters<ListenerSignature>>;
|
|
124
157
|
readonly first: (...args: Parameters<ListenerSignature>) => ReturnType<ListenerSignature> | undefined;
|