diff options
author | dan <dan.abramov@gmail.com> | 2023-11-16 22:01:01 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-16 14:01:01 -0800 |
commit | 8475312422579c2c7d2798fcd0ff58009c366690 (patch) | |
tree | a2576ed4e7766aac25d16a74ef802866786ea12e /src/lib/hooks/useToggleMutationQueue.ts | |
parent | 54faa7e176ed2f8644ef4941c8a65522107a84c1 (diff) | |
download | voidsky-8475312422579c2c7d2798fcd0ff58009c366690.tar.zst |
Add a mutation queue to fix race conditions in toggles (#1933)
* Prototype a queue * Track both current and pending actions * Skip unnecessary actions * Commit last confirmed state to shadow * Thread state through actions over time * Fix the logic to skip redundant mutations * Track status * Extract an abstraction * Fix standalone mutations * Add types * Move to another file * Return stable function * Clean up * Use queue for muting * Use queue for blocking * Convert other follow buttons * Don't export non-queue mutations * Properly handle canceled tasks * Fix copy paste
Diffstat (limited to 'src/lib/hooks/useToggleMutationQueue.ts')
-rw-r--r-- | src/lib/hooks/useToggleMutationQueue.ts | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/src/lib/hooks/useToggleMutationQueue.ts b/src/lib/hooks/useToggleMutationQueue.ts new file mode 100644 index 000000000..28ae86142 --- /dev/null +++ b/src/lib/hooks/useToggleMutationQueue.ts @@ -0,0 +1,98 @@ +import {useState, useRef, useEffect, useCallback} from 'react' + +type Task<TServerState> = { + isOn: boolean + resolve: (serverState: TServerState) => void + reject: (e: unknown) => void +} + +type TaskQueue<TServerState> = { + activeTask: Task<TServerState> | null + queuedTask: Task<TServerState> | null +} + +function AbortError() { + const e = new Error() + e.name = 'AbortError' + return e +} + +export function useToggleMutationQueue<TServerState>({ + initialState, + runMutation, + onSuccess, +}: { + initialState: TServerState + runMutation: ( + prevState: TServerState, + nextIsOn: boolean, + ) => Promise<TServerState> + onSuccess: (finalState: TServerState) => void +}) { + // We use the queue as a mutable object. + // This is safe becuase it is not used for rendering. + const [queue] = useState<TaskQueue<TServerState>>({ + activeTask: null, + queuedTask: null, + }) + + async function processQueue() { + if (queue.activeTask) { + // There is another active processQueue call iterating over tasks. + // It will handle any newly added tasks, so we should exit early. + return + } + // To avoid relying on the rendered state, capture it once at the start. + // From that point on, and until the queue is drained, we'll use the real server state. + let confirmedState: TServerState = initialState + try { + while (queue.queuedTask) { + const prevTask = queue.activeTask + const nextTask = queue.queuedTask + queue.activeTask = nextTask + queue.queuedTask = null + if (prevTask?.isOn === nextTask.isOn) { + // Skip multiple requests to update to the same value in a row. + prevTask.reject(new (AbortError as any)()) + continue + } + try { + // The state received from the server feeds into the next task. + // This lets us queue deletions of not-yet-created resources. + confirmedState = await runMutation(confirmedState, nextTask.isOn) + nextTask.resolve(confirmedState) + } catch (e) { + nextTask.reject(e) + } + } + } finally { + onSuccess(confirmedState) + queue.activeTask = null + queue.queuedTask = null + } + } + + function queueToggle(isOn: boolean): Promise<TServerState> { + return new Promise((resolve, reject) => { + // This is a toggle, so the next queued value can safely replace the queued one. + if (queue.queuedTask) { + queue.queuedTask.reject(new (AbortError as any)()) + } + queue.queuedTask = {isOn, resolve, reject} + processQueue() + }) + } + + const queueToggleRef = useRef(queueToggle) + useEffect(() => { + queueToggleRef.current = queueToggle + }) + const queueToggleStable = useCallback( + (isOn: boolean): Promise<TServerState> => { + const queueToggleLatest = queueToggleRef.current + return queueToggleLatest(isOn) + }, + [], + ) + return queueToggleStable +} |