@kuindji/reactive
Advanced tools
+40
-6
@@ -17,3 +17,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
| let actionFn = action; | ||
| const { trigger, addListener, removeAllListeners, removeListener, updateListenerOptions, promise, destroy: destroyResponseEvent, } = createEvent(); | ||
| const { trigger, addListener, removeAllListeners, removeListener, updateListenerOptions, promise, addErrorListener: addResponseErrorListener, destroy: destroyResponseEvent, } = createEvent(); | ||
| const { all: triggerBeforeAction, addListener: addBeforeActionListener, removeAllListeners: removeAllBeforeActionListeners, removeListener: removeBeforeActionListener, promise: beforeActionPromise, destroy: destroyBeforeEvent, } = createEvent(); | ||
@@ -86,2 +86,36 @@ const { trigger: triggerError, addListener: addErrorListener, removeAllListeners: removeAllErrorListeners, removeListener: removeErrorListener, promise: errorPromise, hasListener: hasErrorListeners, destroy: destroyErrorEvent, } = createEvent(); | ||
| const getStatus = () => currentStatus; | ||
| // Emit onto the action error event without letting a throwing error | ||
| // listener escape into the invoke lifecycle: an un-isolated throw here | ||
| // would (depending on the call site) reject an already-settled invoke with | ||
| // the wrong error, or skip the inFlight decrement and strand pending:true. | ||
| // Mirrors the isolation already applied to status-listener failures. | ||
| const reportError = (error, args, type = "action") => { | ||
| if (destroyed) { | ||
| return; | ||
| } | ||
| try { | ||
| triggerError({ | ||
| error: error instanceof Error ? error : new Error(String(error)), | ||
| args, | ||
| type, | ||
| }); | ||
| } | ||
| catch (_a) { | ||
| // A throwing error listener must not corrupt the invoke lifecycle; | ||
| // there is nothing left to route it to. | ||
| } | ||
| }; | ||
| // A throwing or rejecting response (result) listener must not corrupt the | ||
| // invoke that emitted it: a successful invocation must still resolve with | ||
| // its result, and an async rejection must not surface as an unhandled | ||
| // rejection. Registering an error listener on the response event makes the | ||
| // event catch its listeners' failures (instead of re-throwing synchronously | ||
| // or leaving a floating rejected promise) and routes them to the action | ||
| // error event with a distinct "action-listener" type so consumers can tell | ||
| // a failed result listener apart from a failed action. | ||
| addResponseErrorListener((errorResponse) => { | ||
| var _a; | ||
| const settled = errorResponse.args[0]; | ||
| reportError(errorResponse.error, (_a = settled === null || settled === void 0 ? void 0 : settled.args) !== null && _a !== void 0 ? _a : [], "action-listener"); | ||
| }); | ||
| const invoke = (...args) => __awaiter(this, void 0, void 0, function* () { | ||
@@ -168,7 +202,7 @@ if (destroyed) { | ||
| trigger(response); | ||
| triggerError({ | ||
| error: lastError, | ||
| args: args, | ||
| type: "action", | ||
| }); | ||
| // Isolated: a throwing error listener must not re-escape here | ||
| // (the failure is already handled, so invoke must resolve with | ||
| // the error response rather than reject with the listener's | ||
| // error). reportError swallows a throwing error listener. | ||
| reportError(lastError, args, "action"); | ||
| } | ||
@@ -175,0 +209,0 @@ return response; |
@@ -148,4 +148,2 @@ import { createAction } from "./action.js"; | ||
| } | ||
| options = options || {}; | ||
| options.limit = 1; | ||
| const action = get(name); | ||
@@ -155,3 +153,6 @@ if (!action) { | ||
| } | ||
| return action.addListener(handler, options); | ||
| // Spread rather than mutate: assigning limit onto the caller's options | ||
| // object would leak `limit: 1` into a shared options object reused for | ||
| // other subscriptions. Mirrors once() in event.ts / eventBus.ts. | ||
| return action.addListener(handler, Object.assign(Object.assign({}, (options || {})), { limit: 1 })); | ||
| }; | ||
@@ -158,0 +159,0 @@ const un = (name, handler, context, tag) => { |
@@ -47,4 +47,4 @@ export type MapKey = string; | ||
| name?: MapKey; | ||
| type: "action" | "action-status" | "event" | "store-change" | "store-pipe" | "store-control"; | ||
| type: "action" | "action-status" | "action-listener" | "event" | "store-change" | "store-pipe" | "store-control"; | ||
| }; | ||
| export type ErrorListenerSignature<Arguments extends any[] = any[]> = (errorResponse: ErrorResponse<Arguments>) => void; |
@@ -27,2 +27,10 @@ import { useCallback, useEffect, useRef } from "react"; | ||
| subscribe: (opts) => { | ||
| // Unlike an eventBus/store target (which auto-creates on access), an | ||
| // ActionBus action can be absent — never registered, or removed at | ||
| // runtime while this component stays mounted. addListener/get would | ||
| // throw ("Action <name> not found" / a TypeError on undefined) out | ||
| // of the commit-phase effect. Skip when absent instead of crashing. | ||
| if (!actionBus.has(actionName)) { | ||
| return; | ||
| } | ||
| actionBus.addListener(actionName, genericHandler, opts !== null && opts !== void 0 ? opts : undefined); | ||
@@ -32,2 +40,8 @@ actionBus.get(actionName).addBeforeActionListener(genericBeforeActionHandler); | ||
| unsubscribe: (ctx) => { | ||
| // The action may have been removed while mounted; its listeners were | ||
| // dropped with it, so there is nothing to detach and get() would | ||
| // throw. Skip when absent (matches the subscribe guard above). | ||
| if (!actionBus.has(actionName)) { | ||
| return; | ||
| } | ||
| actionBus.removeListener(actionName, genericHandler, ctx); | ||
@@ -34,0 +48,0 @@ actionBus.get(actionName).removeBeforeActionListener(genericBeforeActionHandler); |
@@ -32,7 +32,5 @@ import { useCallback, useSyncExternalStore } from "react"; | ||
| if (typeof value === "function") { | ||
| // The cast is required by tsc (the typeof-narrowed `value` is | ||
| // `Setter | (ValueType & Function)`, not all callable), even | ||
| // though no-unnecessary-type-assertion disagrees on this TS | ||
| // version. | ||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
| // The cast is required by tsc: the typeof-narrowed `value` is | ||
| // `Setter | (ValueType & Function)`, which is not uniformly | ||
| // callable without narrowing to Setter. | ||
| store.set(key, value(store.get(key))); | ||
@@ -39,0 +37,0 @@ } |
+18
-4
@@ -125,2 +125,12 @@ import { createEventBus } from "./eventBus.js"; | ||
| ]; | ||
| // A throwing onChange listener must NOT abort the computed recompute | ||
| // cascade below: aborting would leave a computed derived from `name` | ||
| // stale and no longer equal to fn(deps), while `data` already holds | ||
| // the new value. Route the error to the error event and, when | ||
| // unhandled, defer the throw until after the cascade + control | ||
| // change have run so derived state is consistent before it | ||
| // propagates. (Previously this returned/threw here, before the | ||
| // cascade, silently stranding dependents.) | ||
| let deferredChangeError = null; | ||
| let hasDeferredChangeError = false; | ||
| try { | ||
@@ -138,7 +148,6 @@ changes.trigger(name, ...changeArgs); | ||
| }); | ||
| if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) { | ||
| effectKeys = []; | ||
| return true; | ||
| if (!((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener())) { | ||
| deferredChangeError = error; | ||
| hasDeferredChangeError = true; | ||
| } | ||
| throw error; | ||
| } | ||
@@ -206,2 +215,7 @@ if ((_c = control.get(EffectEventName)) === null || _c === void 0 ? void 0 : _c.hasListener()) { | ||
| } | ||
| // Propagate an unhandled onChange-listener error now that the | ||
| // cascade and control change have run and derived state is settled. | ||
| if (hasDeferredChangeError) { | ||
| throw deferredChangeError; | ||
| } | ||
| return true; | ||
@@ -208,0 +222,0 @@ } |
+1
-1
| { | ||
| "name": "@kuindji/reactive", | ||
| "version": "1.3.0", | ||
| "version": "1.3.1", | ||
| "author": "Ivan Kuindzhi", | ||
@@ -5,0 +5,0 @@ "type": "module", |
268500
1.47%4900
1.26%