a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
1# Async Effect Internals 2 3`asyncEffect` orchestrates asynchronous work that reacts to signals. 4It combines the signal subscription model with scheduling helpers (debounce/throttle), abort signals, retries, and cleanup delivery. 5The implementation lives in `lib/src/core/async-effect.ts`. 6 7## Execution lifecycle 8 91. **Subscription** - Each dependency signal registers `scheduleExecution` with `subscribe()`. 10 The effect runs immediately on creation and whenever any dependency changes. 112. **Scheduling** - `scheduleExecution` increments a monotonic `executionId`, then applies debounce or throttle rules before invoking `executeEffect`. 123. **Abort + cleanup** - The previous cleanup function (if any) runs before each new execution. 13 When `abortable` is true, a shared `AbortController` is aborted prior to cleanup and replaced for the upcoming run. 144. **Effect body** - The async callback receives the optional `AbortSignal`. 15 It may return a cleanup function (sync or async). 16 VoltX stores it so future runs can dispose the previous work. 175. **Race protection** - The awaited result checks whether its `executionId` still matches the global counter. 18 If dependencies changed mid-flight, the run is considered stale and discarded. 196. **Retry loop** - Errors increment a `retryCount`. 20 While the counter is below `retries`, the effect waits for `retryDelay` (if provided) and reruns the same `executionId`. 21 Once retries are exhausted VoltX logs the failure and, when `onError` is defined, passes the error alongside a `retry()` callback that resets the counter and schedules a new run. 22 23## Scheduling helpers 24 25- **Debounce** clears and reuses a `setTimeout`, delaying execution until changes stop for `opts.debounce` (in ms). 26- **Throttle** tracks the last execution timestamp. 27 If the window has not expired it schedules a timer to run later and flips `pendingExecution` so only one trailing invocation is queued. 28- Both helpers coexist with abort support: any timer-driven execution aborts the previous run before invoking the effect body. 29 30## Cleanup guarantees 31 32- Returning a function from the effect body registers it as the cleanup for the next iteration. 33- Abortable effects tip off downstream code through the `AbortSignal`, but cleanup functions still run even if the consumer ignores the signal. 34- Disposing the effect (via the returned function) aborts active requests, runs cleanup once, clears pending timers, and unsubscribes from every dependency. 35 36## Error handling nuances 37 38- All cleanup functions are wrapped in try/catch to avoid crashing the reactive loop. 39- Retry delays use `setTimeout` so they respect fake timers in Vitest. 40- Stale retries bail immediately if the global `executionId` has advanced, preventing duplicate work after rapid dependency changes. 41 42## Testing Surface 43 44`lib/test/core/async-effect.test.ts` covers: 45 46- Immediate execution and dependency reactivity. 47- Cleanup semantics and disposal. 48- Abort controller wiring (abort on change, abort on dispose). 49- Race protection to ensure stale responses are ignored. 50- Debounce and throttle behavior. 51- Retry loops, `onError` callbacks, and manual retry invocation. 52 53These tests rely on fake timers, so implementation details intentionally avoid microtasks for debounce/throttle, favoring `setTimeout` to keep deterministic control over scheduling.