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';
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,>() => {
17 const enqueue = (value: T) => {
18 return queue.push(value);
20 const consume = (cb: (value: T) => void) => {
25 const resetId = () => {
26 symbol = Symbol('debug');
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);
45 state.current = createQueue();
48 const queue = state.current;
55 const dispatchAll = () => {
56 queue.consume((thunk: any) => {
57 store.dispatch(thunk());
62 let cancel: () => void;
63 const consume = () => {
69 // Not supported on safari
70 if (!!globalThis.requestIdleCallback) {
71 const handle = requestIdleCallback(
79 cancelIdleCallback(handle);
82 const handle = setTimeout(() => {
92 const originalEnqueue = queue.enqueue;
93 queue.enqueue = (newThunk: any) => {
94 const result = originalEnqueue(newThunk);
100 queue.enqueue = originalEnqueue;
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 }
114 const useGet = (): ((arg?: ThunkArg) => Promise<Returned>) => {
115 const dispatch = useDispatch<ThunkDispatch<State, Extra, Action>>();
116 return useCallback((arg?: ThunkArg) => dispatch(thunk(arg)), []);
119 let queueRef: { state: boolean; queue: Queue | null; id: null | any; once: boolean } = {
123 // Should the hook trigger the thunk periodically. For now 'periodic' means once per page load.
124 once: !options.periodic,
127 const hookSelector = createSelector(selector, (result): [Returned | undefined, boolean] => {
129 return [undefined, true];
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;
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;
146 if (queueRef.state && queueRef.queue?.getId() !== queueRef.id) {
147 queueRef.state = false;
150 if ((value === undefined || !queueRef.once) && !queueRef.state && queueRef.queue) {
151 queueRef.state = true;
152 queueRef.once = true;
153 queueRef.queue.enqueue(thunk);
156 const loading = value === undefined;
157 return [value, loading];
160 const useValue = (): [Returned | undefined, boolean] => {
161 queueRef.queue = useContext(ModelQueueContext);
162 return useSelector(hookSelector);