mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useState, useRef, useEffect, useCallback} from 'react' 2 3type Task<TServerState> = { 4 isOn: boolean 5 resolve: (serverState: TServerState) => void 6 reject: (e: unknown) => void 7} 8 9type TaskQueue<TServerState> = { 10 activeTask: Task<TServerState> | null 11 queuedTask: Task<TServerState> | null 12} 13 14function AbortError() { 15 const e = new Error() 16 e.name = 'AbortError' 17 return e 18} 19 20export function useToggleMutationQueue<TServerState>({ 21 initialState, 22 runMutation, 23 onSuccess, 24}: { 25 initialState: TServerState 26 runMutation: ( 27 prevState: TServerState, 28 nextIsOn: boolean, 29 ) => Promise<TServerState> 30 onSuccess: (finalState: TServerState) => void 31}) { 32 // We use the queue as a mutable object. 33 // This is safe becuase it is not used for rendering. 34 const [queue] = useState<TaskQueue<TServerState>>({ 35 activeTask: null, 36 queuedTask: null, 37 }) 38 39 async function processQueue() { 40 if (queue.activeTask) { 41 // There is another active processQueue call iterating over tasks. 42 // It will handle any newly added tasks, so we should exit early. 43 return 44 } 45 // To avoid relying on the rendered state, capture it once at the start. 46 // From that point on, and until the queue is drained, we'll use the real server state. 47 let confirmedState: TServerState = initialState 48 try { 49 while (queue.queuedTask) { 50 const prevTask = queue.activeTask 51 const nextTask = queue.queuedTask 52 queue.activeTask = nextTask 53 queue.queuedTask = null 54 if (prevTask?.isOn === nextTask.isOn) { 55 // Skip multiple requests to update to the same value in a row. 56 prevTask.reject(new (AbortError as any)()) 57 continue 58 } 59 try { 60 // The state received from the server feeds into the next task. 61 // This lets us queue deletions of not-yet-created resources. 62 confirmedState = await runMutation(confirmedState, nextTask.isOn) 63 nextTask.resolve(confirmedState) 64 } catch (e) { 65 nextTask.reject(e) 66 } 67 } 68 } finally { 69 onSuccess(confirmedState) 70 queue.activeTask = null 71 queue.queuedTask = null 72 } 73 } 74 75 function queueToggle(isOn: boolean): Promise<TServerState> { 76 return new Promise((resolve, reject) => { 77 // This is a toggle, so the next queued value can safely replace the queued one. 78 if (queue.queuedTask) { 79 queue.queuedTask.reject(new (AbortError as any)()) 80 } 81 queue.queuedTask = {isOn, resolve, reject} 82 processQueue() 83 }) 84 } 85 86 const queueToggleRef = useRef(queueToggle) 87 useEffect(() => { 88 queueToggleRef.current = queueToggle 89 }) 90 const queueToggleStable = useCallback( 91 (isOn: boolean): Promise<TServerState> => { 92 const queueToggleLatest = queueToggleRef.current 93 return queueToggleLatest(isOn) 94 }, 95 [], 96 ) 97 return queueToggleStable 98}