Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / redux-utilities / asyncModelThunk / creator.ts
blobcca904676efa4ef2b6db4074d1b0c305e43f6945
1 import type { ActionReducerMapBuilder, Draft, ThunkDispatch } from '@reduxjs/toolkit';
2 import { type Action, createAction, miniSerializeError } from '@reduxjs/toolkit';
3 import type { ThunkAction } from 'redux-thunk';
5 import { getFetchedAt as getDefaultFetchedAt, getFetchedEphemeral } from './fetchedAt';
6 import type { ReducerValue, ThunkOptions } from './interface';
7 import { cacheHelper, createPromiseStore } from './promiseStore';
9 export const createAsyncModelThunk = <Returned, State, Extra, ThunkArg = void>(
10     prefix: string,
11     {
12         expiry,
13         miss,
14         previous,
15     }: {
16         expiry?: number;
17         miss: (extra: {
18             dispatch: ThunkDispatch<State, Extra, Action>;
19             getState: () => State;
20             extraArgument: Extra;
21             options?: ThunkOptions<ThunkArg>;
22         }) => Promise<Returned>;
23         previous: (extra: {
24             dispatch: ThunkDispatch<State, Extra, Action>;
25             getState: () => State;
26             extraArgument: Extra;
27             options?: ThunkOptions<ThunkArg>;
28         }) => ReducerValue<Returned> | undefined;
29     }
30 ) => {
31     const pending = createAction(`${prefix}/pending`, () => ({
32         payload: undefined,
33     }));
35     const fulfilled = createAction(`${prefix}/fulfilled`, (payload: Returned) => ({
36         payload,
37     }));
39     const rejected = createAction(`${prefix}/failed`, (payload: any) => ({
40         payload,
41     }));
43     const promiseStore = createPromiseStore<Returned>();
45     const thunk = (
46         options?: ThunkOptions<ThunkArg>
47     ): ThunkAction<
48         Promise<Returned>,
49         State,
50         Extra,
51         ReturnType<typeof pending> | ReturnType<typeof fulfilled> | ReturnType<typeof rejected>
52     > => {
53         return (dispatch, getState, extraArgument) => {
54             const select = () => {
55                 return previous({ dispatch, getState, extraArgument, options });
56             };
57             const cb = async () => {
58                 try {
59                     dispatch(pending());
60                     const value = await miss({ dispatch, getState, extraArgument, options });
61                     dispatch(fulfilled(value));
62                     return value;
63                 } catch (error) {
64                     dispatch(rejected(miniSerializeError(error)));
65                     throw error;
66                 }
67             };
68             return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache, expiry });
69         };
70     };
72     return {
73         pending,
74         fulfilled,
75         rejected,
76         thunk,
77     };
80 export const handleAsyncModel = <Returned, State, Extra, Options>(
81     builder: ActionReducerMapBuilder<ReducerValue<Returned>>,
82     cases: ReturnType<typeof createAsyncModelThunk<Returned, State, Extra, Options>>,
83     {
84         getFetchedAt,
85     }: {
86         getFetchedAt: typeof getDefaultFetchedAt;
87     } = { getFetchedAt: getDefaultFetchedAt }
88 ) => {
89     return builder
90         .addCase(cases.pending, (state) => {
91             state.error = undefined;
92         })
93         .addCase(cases.fulfilled, (state, action) => {
94             state.value = action.payload as Draft<Returned> | undefined;
95             state.error = undefined;
96             state.meta.fetchedAt = getFetchedAt();
97             state.meta.fetchedEphemeral = getFetchedEphemeral();
98         })
99         .addCase(cases.rejected, (state, action) => {
100             state.error = action.payload;
101             state.meta.fetchedAt = getFetchedAt();
102         });
105 export const previousSelector =
106     <Returned, State, Extra, ThunkArg = void>(
107         selector: (state: State) => ReducerValue<Returned>
108     ): ((extra: {
109         dispatch: ThunkDispatch<State, Extra, Action>;
110         getState: () => State;
111         extraArgument: Extra;
112         options?: ThunkOptions<ThunkArg>;
113     }) => ReducerValue<Returned> | undefined) =>
114     ({ getState }) => {
115         return selector(getState());
116     };
118 export const selectPersistModel = <T>(state: ReducerValue<T>) => {
119     if (state.error || state.value === undefined) {
120         return undefined;
121     }
122     return state;