@kuindji/reactive
Advanced tools
| import type { TriggerReturnType } from "./types.js"; | ||
| export declare const EventTriggerInternal: unique symbol; | ||
| export type EventTriggerInternalApi = { | ||
| [EventTriggerInternal]: (args: any[], returnType?: TriggerReturnType | null, tags?: string[] | null) => unknown; | ||
| }; |
| // Private bridge used by EventBus to observe the result of Event.trigger() | ||
| // without changing Event's public fire-and-forget return type. | ||
| export const EventTriggerInternal = Symbol("eventTriggerInternal"); |
+6
-3
@@ -18,3 +18,3 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
| 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(); | ||
| const { raw: triggerBeforeAction, addListener: addBeforeActionListener, removeAllListeners: removeAllBeforeActionListeners, removeListener: removeBeforeActionListener, promise: beforeActionPromise, destroy: destroyBeforeEvent, } = createEvent(); | ||
| const { trigger: triggerError, addListener: addErrorListener, removeAllListeners: removeAllErrorListeners, removeListener: removeErrorListener, promise: errorPromise, hasListener: hasErrorListeners, destroy: destroyErrorEvent, } = createEvent(); | ||
@@ -134,4 +134,7 @@ // Status is a side channel over the invoke lifecycle: a dedicated event so | ||
| const beforeResponse = triggerBeforeAction(...args); | ||
| const beforeResults = isPromiseLike(beforeResponse) | ||
| ? yield Promise.resolve(beforeResponse) | ||
| // raw() always preserves the array container. Await its entries only | ||
| // when needed so synchronous before listeners and the action itself | ||
| // retain their existing same-tick execution timing. | ||
| const beforeResults = beforeResponse.some(isPromiseLike) | ||
| ? yield Promise.all(beforeResponse.map((result) => Promise.resolve(result))) | ||
| : beforeResponse; | ||
@@ -138,0 +141,0 @@ for (const before of beforeResults) { |
+27
-38
| import asyncCall from "./lib/asyncCall.js"; | ||
| import { EventTriggerInternal } from "./lib/eventInternal.js"; | ||
| import isPromiseLike from "./lib/isPromiseLike.js"; | ||
@@ -370,2 +371,19 @@ import listenerSorter from "./lib/listenerSorter.js"; | ||
| }; | ||
| const handleListenerError = (error, args) => { | ||
| const response = { | ||
| error: error instanceof Error ? error : new Error(String(error)), | ||
| args, | ||
| type: "event", | ||
| }; | ||
| // Snapshot both the listeners and handledness before dispatch. A handler | ||
| // may remove itself; that must not skip the next handler or turn an error | ||
| // that was handled into an unhandled one after the fact. | ||
| const handlers = errorListeners.slice(); | ||
| if (handlers.length === 0) { | ||
| throw error; | ||
| } | ||
| for (const errorListener of handlers) { | ||
| errorListener.handler.call(errorListener.context, response); | ||
| } | ||
| }; | ||
| const _listenerCall = (listener, args, resolve = null) => { | ||
@@ -388,14 +406,3 @@ let isAsync = listener.async; | ||
| const handledResult = Promise.resolve(result).catch((error) => { | ||
| for (const errorListener of errorListeners) { | ||
| errorListener.handler({ | ||
| error: error instanceof Error | ||
| ? error | ||
| : new Error(error), | ||
| args: args, | ||
| type: "event", | ||
| }); | ||
| } | ||
| if (errorListeners.length === 0) { | ||
| throw error; | ||
| } | ||
| handleListenerError(error, args); | ||
| return undefined; | ||
@@ -414,14 +421,3 @@ }); | ||
| catch (error) { | ||
| for (const errorListener of errorListeners) { | ||
| errorListener.handler({ | ||
| error: error instanceof Error | ||
| ? error | ||
| : new Error(error), | ||
| args: args, | ||
| type: "event", | ||
| }); | ||
| } | ||
| if (errorListeners.length === 0) { | ||
| throw error; | ||
| } | ||
| handleListenerError(error, args); | ||
| return undefined; | ||
@@ -632,10 +628,6 @@ } | ||
| case TriggerReturnType.ALL: { | ||
| return hasPromises | ||
| ? Promise.all(results) | ||
| : results; | ||
| return results; | ||
| } | ||
| case TriggerReturnType.CONCAT: { | ||
| return hasPromises | ||
| ? Promise.all(results).then((r) => r.flat()) | ||
| : results.flat(); | ||
| return results.flat(); | ||
| } | ||
@@ -706,6 +698,3 @@ case TriggerReturnType.MERGE: { | ||
| const response = _trigger(args, TriggerReturnType.ALL); | ||
| if (isPromiseLike(response)) { | ||
| return Promise.resolve(response); | ||
| } | ||
| return Promise.resolve(response); | ||
| return Promise.all(response.map((item) => Promise.resolve(item))); | ||
| }; | ||
@@ -737,6 +726,3 @@ const last = (...args) => { | ||
| const response = _trigger(args, TriggerReturnType.CONCAT); | ||
| if (isPromiseLike(response)) { | ||
| return Promise.resolve(response); | ||
| } | ||
| return Promise.resolve(response); | ||
| return Promise.all(response.map((item) => Promise.resolve(item))).then((results) => results.flat()); | ||
| }; | ||
@@ -835,3 +821,6 @@ const firstNonEmpty = (...args) => { | ||
| }; | ||
| Object.defineProperty(api, EventTriggerInternal, { | ||
| value: _trigger, | ||
| }); | ||
| return api; | ||
| } |
+28
-20
| import { createEvent } from "./event.js"; | ||
| import { EventTriggerInternal, } from "./lib/eventInternal.js"; | ||
| import isPromiseLike from "./lib/isPromiseLike.js"; | ||
@@ -85,4 +86,22 @@ import { normalizeEventOptions } from "./lib/normalizeEventOptions.js"; | ||
| const relays = []; | ||
| const errorEvent = createEvent(); | ||
| const handleError = (name, error, args) => { | ||
| // Capture handledness before dispatch: a one-shot or self-removing error | ||
| // listener still handled this error even though it is gone afterwards. | ||
| const handled = errorEvent.hasListener(); | ||
| errorEvent.trigger({ | ||
| name, | ||
| error: error instanceof Error ? error : new Error(String(error)), | ||
| args, | ||
| type: "event", | ||
| }); | ||
| if (handled) { | ||
| return undefined; | ||
| } | ||
| throw error; | ||
| }; | ||
| const asterisk = createEvent(); | ||
| const errorEvent = createEvent(); | ||
| const triggerEvent = (event, args) => { | ||
| return event[EventTriggerInternal](args); | ||
| }; | ||
| const _getProxyListener = ({ remoteEventName, localEventName, returnType, resolve, localEventNamePrefix, }) => { | ||
@@ -270,16 +289,3 @@ let listener = proxyListeners.find((listener) => listener.returnType === returnType | ||
| const e = _getOrAddEvent(name); | ||
| const handleError = (error) => { | ||
| errorEvent.trigger({ | ||
| name, | ||
| error: error instanceof Error | ||
| ? error | ||
| : new Error(String(error)), | ||
| args, | ||
| type: "event", | ||
| }); | ||
| if (errorEvent.hasListener()) { | ||
| return undefined; | ||
| } | ||
| throw error; | ||
| }; | ||
| const handleTriggerError = (error) => handleError(name, error, args); | ||
| const runner = () => { | ||
@@ -327,4 +333,3 @@ let result; | ||
| default: | ||
| e.trigger(...args); | ||
| return undefined; | ||
| return triggerEvent(e, args); | ||
| } | ||
@@ -341,5 +346,8 @@ return result; | ||
| } | ||
| asterisk.trigger(name, args, currentTagsFilter); | ||
| const allEventsResult = triggerEvent(asterisk, [name, args, currentTagsFilter]); | ||
| if (isPromiseLike(allEventsResult)) { | ||
| void Promise.resolve(allEventsResult).catch(handleTriggerError); | ||
| } | ||
| if (isPromiseLike(result)) { | ||
| return Promise.resolve(result).catch(handleError); | ||
| return Promise.resolve(result).catch(handleTriggerError); | ||
| } | ||
@@ -349,3 +357,3 @@ return result; | ||
| catch (error) { | ||
| return handleError(error); | ||
| return handleTriggerError(error); | ||
| } | ||
@@ -352,0 +360,0 @@ }; |
@@ -1,2 +0,2 @@ | ||
| import { useContext, useEffect, useMemo, useRef } from "react"; | ||
| import { useContext, useEffect, useLayoutEffect, useMemo, useRef, } from "react"; | ||
| import { createAction } from "../action.js"; | ||
@@ -31,3 +31,6 @@ import { ErrorBoundaryContext } from "./ErrorBoundary.js"; | ||
| // initial render). | ||
| useEffect(() => { | ||
| // Commit the replacement before descendant passive effects can invoke the | ||
| // action, while keeping abandoned renders from leaking a new implementation | ||
| // into the currently committed action. | ||
| useLayoutEffect(() => { | ||
| if (actionSignatureRef.current !== actionSignature) { | ||
@@ -34,0 +37,0 @@ action.setAction(actionSignature); |
@@ -1,2 +0,2 @@ | ||
| import { useContext, useEffect, useMemo, useRef } from "react"; | ||
| import { useContext, useEffect, useLayoutEffect, useMemo, useRef, } from "react"; | ||
| import { createActionBus } from "../actionBus.js"; | ||
@@ -21,17 +21,17 @@ import { ErrorBoundaryContext } from "./ErrorBoundary.js"; | ||
| const appliedActionsRef = useRef(Object.assign({}, initialActions)); | ||
| const nextActions = (initialActions !== null && initialActions !== void 0 ? initialActions : {}); | ||
| // Add newly-introduced actions during render (not in an effect): React runs | ||
| // child passive effects BEFORE parent passive effects, so a child rendered | ||
| // in the same pass that subscribes to a new action would otherwise throw | ||
| // "Action <name> not found". Parent render precedes child render, and | ||
| // add() is idempotent (a no-op if the action already exists). | ||
| for (const key in nextActions) { | ||
| actionBus.add(key, nextActions[key]); | ||
| } | ||
| // Replacements and removals can be deferred to a passive effect: a replaced | ||
| // action keeps its identity/listeners (so subscriptions are unaffected by | ||
| // timing), and removing late is harmless. | ||
| useEffect(() => { | ||
| // Reconcile only after this render commits, so a suspended/abandoned render | ||
| // cannot leak actions into the live bus. Layout effects finish before | ||
| // descendant passive effects, so newly-added actions are still present | ||
| // before children subscribe or invoke them and replacements are never stale. | ||
| useLayoutEffect(() => { | ||
| const next = (initialActions !== null && initialActions !== void 0 ? initialActions : {}); | ||
| const prev = appliedActionsRef.current; | ||
| for (const key in next) { | ||
| if (!(key in prev)) { | ||
| actionBus.add(key, next[key]); | ||
| } | ||
| else if (next[key] !== prev[key]) { | ||
| actionBus.replace(key, next[key]); | ||
| } | ||
| } | ||
| for (const key in prev) { | ||
@@ -42,7 +42,2 @@ if (!(key in next)) { | ||
| } | ||
| for (const key in next) { | ||
| if (key in prev && next[key] !== prev[key]) { | ||
| actionBus.replace(key, next[key]); | ||
| } | ||
| } | ||
| appliedActionsRef.current = Object.assign({}, next); | ||
@@ -49,0 +44,0 @@ }); |
@@ -1,2 +0,2 @@ | ||
| import { useContext, useEffect, useMemo, useRef } from "react"; | ||
| import { useContext, useLayoutEffect, useMemo, useRef, } from "react"; | ||
| import { createActionMap } from "../actionMap.js"; | ||
@@ -22,3 +22,5 @@ import { ActionMapSetErrorListeners } from "../lib/actionMapInternal.js"; | ||
| // keeps a defensive throw. | ||
| useEffect(() => { | ||
| // Reconcile during commit before descendant passive effects can invoke an | ||
| // entry, without exposing functions from abandoned renders. | ||
| useLayoutEffect(() => { | ||
| const next = actions; | ||
@@ -25,0 +27,0 @@ const prev = committedActionsRef.current; |
@@ -1,2 +0,2 @@ | ||
| import { useCallback, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react"; | ||
| import { useCallback, useInsertionEffect, useMemo, useRef, useSyncExternalStore, } from "react"; | ||
| import { createAction } from "../action.js"; | ||
@@ -28,10 +28,10 @@ /** | ||
| // passive effects — which a useEffect+setAction swap would miss, invoking | ||
| // the previous fn. The ref is updated in a layout effect (commit phase), | ||
| // the previous fn. The ref is updated in an insertion effect (commit phase), | ||
| // not during render: a render-phase mutation would leak a fn from a | ||
| // suspended or abandoned concurrent render that never commits into the | ||
| // currently committed UI. Layout effects run only for committed renders, | ||
| // and this one runs before any consumer layout effect declared after the | ||
| // hook call, so consumers still observe the latest fn. | ||
| // currently committed UI. Insertion effects run only for committed renders | ||
| // and finish before descendant layout effects, so every consumer observes | ||
| // the latest committed function. | ||
| const fnRef = useRef(fn); | ||
| useLayoutEffect(() => { | ||
| useInsertionEffect(() => { | ||
| fnRef.current = fn; | ||
@@ -38,0 +38,0 @@ }); |
+28
-22
@@ -58,2 +58,11 @@ import { createEventBus } from "./eventBus.js"; | ||
| }; | ||
| const reportStoreError = (response) => { | ||
| var _a; | ||
| // Handledness belongs to the dispatch that is about to happen. A | ||
| // self-removing/one-shot error listener must still count as having | ||
| // handled this error after it removes itself during the callback. | ||
| const handled = !!((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()); | ||
| control.trigger(ErrorEventName, response); | ||
| return handled; | ||
| }; | ||
| const dedupe = (keys) => Array.from(new Set(keys)); | ||
@@ -85,3 +94,3 @@ // A public set() can trigger a computed cascade. Routing it through batch() | ||
| const _set = (name, value, triggerChange = true, runBeforeChange = true) => { | ||
| var _a, _b, _c, _d, _e; | ||
| var _a; | ||
| const prev = data.get(name); | ||
@@ -106,3 +115,3 @@ if (prev !== value) { | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -115,3 +124,3 @@ ? error | ||
| }); | ||
| if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) { | ||
| if (handled) { | ||
| return false; | ||
@@ -143,3 +152,3 @@ } | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -152,3 +161,3 @@ ? error | ||
| }); | ||
| if (!((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener())) { | ||
| if (!handled) { | ||
| deferredChangeError = error; | ||
@@ -158,3 +167,3 @@ hasDeferredChangeError = true; | ||
| } | ||
| if ((_c = control.get(EffectEventName)) === null || _c === void 0 ? void 0 : _c.hasListener()) { | ||
| if ((_a = control.get(EffectEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) { | ||
| try { | ||
@@ -175,3 +184,3 @@ const isIntercepting = control.isIntercepting(); | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -184,3 +193,3 @@ ? error | ||
| }); | ||
| if ((_d = control.get(ErrorEventName)) === null || _d === void 0 ? void 0 : _d.hasListener()) { | ||
| if (handled) { | ||
| effectKeys = []; | ||
@@ -204,3 +213,3 @@ return true; | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -213,3 +222,3 @@ ? error | ||
| }); | ||
| if ((_e = control.get(ErrorEventName)) === null || _e === void 0 ? void 0 : _e.hasListener()) { | ||
| if (handled) { | ||
| effectKeys = []; | ||
@@ -273,3 +282,2 @@ return true; | ||
| const replayCoalescedLog = (log, hasCallbackError) => { | ||
| var _a; | ||
| const coalesced = new Map(); | ||
@@ -297,3 +305,3 @@ for (const [propName, value, prev] of log) { | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -306,3 +314,3 @@ ? error | ||
| }); | ||
| if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) { | ||
| if (handled) { | ||
| continue; | ||
@@ -362,3 +370,3 @@ } | ||
| const applySet = (name, value) => { | ||
| var _a, _b; | ||
| var _a; | ||
| if (typeof name === "string") { | ||
@@ -398,3 +406,2 @@ _set(name, value); | ||
| changedKeys.forEach((k) => { | ||
| var _a; | ||
| // Mirror _set's effect error contract: a throwing effect | ||
@@ -407,3 +414,3 @@ // listener routes to the error event (and is swallowed if | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -416,3 +423,3 @@ ? error | ||
| }); | ||
| if (!((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener())) { | ||
| if (!handled) { | ||
| throw error; | ||
@@ -450,3 +457,3 @@ } | ||
| if (controlError) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: controlError, | ||
@@ -456,3 +463,3 @@ args: [name], | ||
| }); | ||
| if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) { | ||
| if (handled) { | ||
| return; | ||
@@ -528,3 +535,2 @@ } | ||
| const batch = (fn) => { | ||
| var _a; | ||
| if (batching) { | ||
@@ -579,3 +585,3 @@ throw new Error("Nested batch() calls are not supported"); | ||
| catch (error) { | ||
| control.trigger(ErrorEventName, { | ||
| const handled = reportStoreError({ | ||
| error: error instanceof Error | ||
@@ -587,3 +593,3 @@ ? error | ||
| }); | ||
| if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) { | ||
| if (handled) { | ||
| if (hasCallbackError) { | ||
@@ -590,0 +596,0 @@ throw callbackError; |
+1
-1
| { | ||
| "name": "@kuindji/reactive", | ||
| "version": "1.3.1", | ||
| "version": "1.3.2", | ||
| "author": "Ivan Kuindzhi", | ||
@@ -5,0 +5,0 @@ "type": "module", |
269317
0.3%70
2.94%4914
0.29%