Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / redux-utilities / asyncModelThunk / hooks.tsx
blob491a39b96a38e9a7bddcb4a3006d5e1848b5a4a0
1 import { type ReactNode, createContext, useCallback, useContext, useEffect, useRef } from 'react';
3 import type { Action, ThunkDispatch } from '@reduxjs/toolkit';
4 import { createSelector } from '@reduxjs/toolkit';
5 import type { ThunkAction } from 'redux-thunk';
7 import {
8     baseUseDispatch as useDispatch,
9     baseUseSelector as useSelector,
10     baseUseStore as useStore,
11 } from '@proton/react-redux-store';
13 import type { ReducerValue } from './interface';
15 const createQueue = <T,>() => {
16     let queue: T[] = [];
17     const enqueue = (value: T) => {
18         return queue.push(value);
19     };
20     const consume = (cb: (value: T) => void) => {
21         queue.forEach(cb);
22         queue.length = 0;
23     };
24     let symbol: Symbol;
25     const resetId = () => {
26         symbol = Symbol('debug');
27     };
28     resetId();
29     return {
30         enqueue,
31         consume,
32         resetId,
33         getId: () => symbol,
34     };
37 export type Queue = ReturnType<typeof createQueue>;
38 export const ModelQueueContext = createContext<Queue | null>(null);
40 export const ModelThunkDispatcher = ({ children }: { children: ReactNode }) => {
41     const store = useStore();
42     const state = useRef<Queue | null>(null);
44     if (!state.current) {
45         state.current = createQueue();
46     }
48     const queue = state.current;
50     useEffect(() => {
51         if (!queue) {
52             return;
53         }
55         const dispatchAll = () => {
56             queue.consume((thunk: any) => {
57                 store.dispatch(thunk());
58             });
59         };
61         let queued = false;
62         let cancel: () => void;
63         const consume = () => {
64             if (queued) {
65                 return;
66             }
68             queued = true;
69             // Not supported on safari
70             if (!!globalThis.requestIdleCallback) {
71                 const handle = requestIdleCallback(
72                     () => {
73                         queued = false;
74                         dispatchAll();
75                     },
76                     { timeout: 100 }
77                 );
78                 cancel = () => {
79                     cancelIdleCallback(handle);
80                 };
81             } else {
82                 const handle = setTimeout(() => {
83                     queued = false;
84                     dispatchAll();
85                 }, 10);
86                 cancel = () => {
87                     clearTimeout(handle);
88                 };
89             }
90         };
92         const originalEnqueue = queue.enqueue;
93         queue.enqueue = (newThunk: any) => {
94             const result = originalEnqueue(newThunk);
95             consume();
96             return result;
97         };
98         consume();
99         return () => {
100             queue.enqueue = originalEnqueue;
101             queue.resetId();
102             cancel();
103         };
104     }, [store, queue]);
106     return <ModelQueueContext.Provider value={queue}>{children}</ModelQueueContext.Provider>;
109 export const createHooks = <State, Extra, Returned, ThunkArg = void>(
110     thunk: (arg?: ThunkArg) => ThunkAction<Promise<Returned>, State, Extra, Action>,
111     selector: (state: State) => ReducerValue<Returned>,
112     options: { periodic: boolean } = { periodic: true }
113 ) => {
114     const useGet = (): ((arg?: ThunkArg) => Promise<Returned>) => {
115         const dispatch = useDispatch<ThunkDispatch<State, Extra, Action>>();
116         return useCallback((arg?: ThunkArg) => dispatch(thunk(arg)), []);
117     };
119     let queueRef: { state: boolean; queue: Queue | null; id: null | any; once: boolean } = {
120         state: false,
121         queue: null,
122         id: null,
123         // Should the hook trigger the thunk periodically. For now 'periodic' means once per page load.
124         once: !options.periodic,
125     };
127     const hookSelector = createSelector(selector, (result): [Returned | undefined, boolean] => {
128         if (!result) {
129             return [undefined, true];
130         }
132         const { error, value } = result;
134         if ((error !== undefined || value !== undefined) && queueRef.state) {
135             // Reset the queued state when the thunk has resolved.
136             queueRef.state = false;
137         }
139         if (error && value === undefined) {
140             const thrownError = new Error(error.message);
141             thrownError.name = error.name || thrownError.name;
142             thrownError.stack = error.stack || thrownError.stack;
143             throw thrownError;
144         }
146         if (queueRef.state && queueRef.queue?.getId() !== queueRef.id) {
147             queueRef.state = false;
148         }
150         if ((value === undefined || !queueRef.once) && !queueRef.state && queueRef.queue) {
151             queueRef.state = true;
152             queueRef.once = true;
153             queueRef.queue.enqueue(thunk);
154         }
156         const loading = value === undefined;
157         return [value, loading];
158     });
160     const useValue = (): [Returned | undefined, boolean] => {
161         queueRef.queue = useContext(ModelQueueContext);
162         return useSelector(hookSelector);
163     };
165     return {
166         useGet,
167         useValue,
168     };