Async Effect Internals
asyncEffect orchestrates asynchronous work that reacts to signals. It combines the signal subscription model with scheduling helpers (debounce/throttle), abort signals, retries, and cleanup delivery. The implementation lives in lib/src/core/async-effect.ts.
Execution lifecycle
- Subscription - Each dependency signal registers
scheduleExecutionwithsubscribe(). The effect runs immediately on creation and whenever any dependency changes. - Scheduling -
scheduleExecutionincrements a monotonicexecutionId, then applies debounce or throttle rules before invokingexecuteEffect. - Abort + cleanup - The previous cleanup function (if any) runs before each new execution. When
abortableis true, a sharedAbortControlleris aborted prior to cleanup and replaced for the upcoming run. - Effect body - The async callback receives the optional
AbortSignal. It may return a cleanup function (sync or async). VoltX stores it so future runs can dispose the previous work. - Race protection - The awaited result checks whether its
executionIdstill matches the global counter. If dependencies changed mid-flight, the run is considered stale and discarded. - Retry loop - Errors increment a
retryCount. While the counter is belowretries, the effect waits forretryDelay(if provided) and reruns the sameexecutionId. Once retries are exhausted VoltX logs the failure and, whenonErroris defined, passes the error alongside aretry()callback that resets the counter and schedules a new run.
Scheduling helpers
- Debounce clears and reuses a
setTimeout, delaying execution until changes stop foropts.debounce(in ms). - Throttle tracks the last execution timestamp. If the window has not expired it schedules a timer to run later and flips
pendingExecutionso only one trailing invocation is queued. - Both helpers coexist with abort support: any timer-driven execution aborts the previous run before invoking the effect body.
Cleanup guarantees
- Returning a function from the effect body registers it as the cleanup for the next iteration.
- Abortable effects tip off downstream code through the
AbortSignal, but cleanup functions still run even if the consumer ignores the signal. - Disposing the effect (via the returned function) aborts active requests, runs cleanup once, clears pending timers, and unsubscribes from every dependency.
Error handling nuances
- All cleanup functions are wrapped in try/catch to avoid crashing the reactive loop.
- Retry delays use
setTimeoutso they respect fake timers in Vitest. - Stale retries bail immediately if the global
executionIdhas advanced, preventing duplicate work after rapid dependency changes.
Testing Surface
lib/test/core/async-effect.test.ts covers:
- Immediate execution and dependency reactivity.
- Cleanup semantics and disposal.
- Abort controller wiring (abort on change, abort on dispose).
- Race protection to ensure stale responses are ignored.
- Debounce and throttle behavior.
- Retry loops,
onErrorcallbacks, and manual retry invocation.
These tests rely on fake timers, so implementation details intentionally avoid microtasks for debounce/throttle, favoring setTimeout to keep deterministic control over scheduling.