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.