personal web client for Bluesky
typescript
solidjs
bluesky
atcute
1// https://github.com/bluesky-social/social-app/blob/019aae5f01cb7b503d242917ae0092c2818f3b71/src/lib/hooks/useToggleMutationQueue.ts
2
3interface Task<TState> {
4 on: boolean;
5 res: (state: TState) => void;
6 rej: (e: unknown) => void;
7}
8
9interface TaskQueue<TState> {
10 curr: Task<TState> | null;
11 next: Task<TState> | null;
12}
13
14export interface ToggleMutationOptions<TState> {
15 initialState: () => TState;
16 mutate: (prevState: TState, nextIsOn: boolean) => Promise<TState>;
17 finalize: (finalState: TState) => void;
18}
19
20export class AbortError extends Error {
21 name = 'AbortError';
22}
23
24export const createToggleMutationQueue = <TState>({
25 initialState,
26 mutate,
27 finalize,
28}: ToggleMutationOptions<TState>) => {
29 const queue: TaskQueue<TState> = {
30 curr: null,
31 next: null,
32 };
33
34 const process = async () => {
35 if (queue.curr) {
36 // There is another active processQueue call iterating over tasks.
37 // It will handle any newly added tasks, so we should exit early.
38 return;
39 }
40
41 // To avoid relying on the rendered state, capture it once at the start.
42 // From that point on, and until the queue is drained, we'll use the real server state.
43 let confirmedState: TState = initialState();
44 try {
45 while (queue.next) {
46 const prev = queue.curr;
47 const next = queue.next;
48 queue.curr = next;
49 queue.next = null;
50
51 if (prev?.on === next.on) {
52 // Skip multiple requests to update to the same value in a row.
53 prev.rej(new (AbortError as any)());
54 continue;
55 }
56
57 try {
58 // The state received from the server feeds into the next task.
59 // This lets us queue deletions of not-yet-created resources.
60 confirmedState = await mutate(confirmedState, next.on);
61 next.res(confirmedState);
62 } catch (e) {
63 next.rej(e);
64 }
65 }
66 } finally {
67 finalize(confirmedState);
68 queue.curr = null;
69 queue.next = null;
70 }
71 };
72
73 const queueToggle = (isOn: boolean): Promise<TState> => {
74 return new Promise((resolve, reject) => {
75 // This is a toggle, so the next queued value can safely replace the queued one.
76 if (queue.next) {
77 queue.next.rej(new (AbortError as any)());
78 }
79
80 queue.next = { on: isOn, res: resolve, rej: reject };
81 process();
82 });
83 };
84
85 return queueToggle;
86};