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}