Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / store / optimistic / with-optimistic.ts
blobbed9eaa3b47fe429abc49466692269f88a040c40
1 import type { Selector } from '@reduxjs/toolkit';
2 import type { Action, Reducer } from 'redux';
4 import type { MaybeArray } from '@proton/pass/types';
6 import { commitReducer } from './reducers/commit';
7 import { failReducer } from './reducers/fail';
8 import { initiateReducer } from './reducers/initiate';
9 import { revertReducer } from './reducers/revert';
10 import { asIfNotFailedSubSelector } from './selectors/select-is-failed';
11 import { asIfNotOptimisticSubSelector } from './selectors/select-is-optimistic';
12 import type {
13     OptimisticMatcher,
14     OptimisticMatchers,
15     OptimisticState,
16     WithOptimisticReducer,
17     WrappedOptimisticState,
18 } from './types';
19 import { HistoryFlag } from './types';
20 import { getActionFromHistoryItem, unwrapOptimisticState } from './utils/transformers';
21 import { withHistoryAction } from './utils/with-history-action';
23 export const withInitialOptimisticState = <T extends object>(state: T): WrappedOptimisticState<T> => {
24     const initialOptimistic: { optimistic: OptimisticState<T> } = {
25         optimistic: {
26             checkpoint: undefined,
27             history: [],
28         },
29     };
31     return {
32         ...state,
33         ...initialOptimistic,
34     };
37 const applyOptimisticMatcher = (action: Action) => (matcher: OptimisticMatcher) =>
38     typeof matcher === 'string' ? matcher === action.type : matcher(action);
40 const withOptimistic = <T extends object>(
41     matchersList: OptimisticMatchers[],
42     reducer: Reducer<T>,
43     options?: { sanitizeAction: (action: Action) => Action }
44 ): {
45     reducer: WithOptimisticReducer<T>;
46     selectors: {
47         asIfNotFailed: Selector<WrappedOptimisticState<T>, WrappedOptimisticState<T>>;
48         asIfNotOptimistic: Selector<WrappedOptimisticState<T>, WrappedOptimisticState<T>>;
49     };
50 } => {
51     const initialState = reducer(undefined, { type: '__OPTIMISTIC_INIT__' });
52     const initialOptimisticState = withInitialOptimisticState(initialState);
54     const wrappedReducer: WithOptimisticReducer<T> = (outer = initialOptimisticState, optimisticAction: Action) => {
55         const action = options?.sanitizeAction?.(optimisticAction) ?? optimisticAction;
57         const inner = unwrapOptimisticState(outer);
58         const nextInner = reducer(inner, action);
60         const { optimistic } = outer;
62         for (const [index, matchers] of Object.entries(matchersList)) {
63             for (const [key, matcher] of Object.entries(matchers) as [string, MaybeArray<OptimisticMatcher>][]) {
64                 const match = Array.isArray(matcher)
65                     ? matcher.map(applyOptimisticMatcher(action)).find(Boolean)
66                     : applyOptimisticMatcher(action)(matcher);
68                 if (match) {
69                     const id = typeof match === 'string' ? match : index;
71                     switch (key as keyof OptimisticMatchers) {
72                         case 'initiate': {
73                             const nextOptimistic = initiateReducer(inner, reducer, action, optimistic, id);
75                             return {
76                                 ...nextOptimistic.history
77                                     .map(getActionFromHistoryItem)
78                                     .reduce(reducer, nextOptimistic.checkpoint!),
79                                 optimistic: nextOptimistic,
80                             };
81                         }
83                         case 'commit': {
84                             return {
85                                 ...nextInner,
86                                 optimistic: withHistoryAction(
87                                     {
88                                         id,
89                                         action,
90                                         type: HistoryFlag.OPTIMISTIC_EFFECT,
91                                     },
92                                     commitReducer(reducer, optimistic, id)
93                                 ),
94                             };
95                         }
97                         case 'fail': {
98                             return {
99                                 ...nextInner,
100                                 optimistic: withHistoryAction(
101                                     {
102                                         id,
103                                         action,
104                                         type: HistoryFlag.OPTIMISTIC_EFFECT,
105                                     },
106                                     failReducer(optimistic, id)
107                                 ),
108                             };
109                         }
111                         case 'revert': {
112                             const [revertedInner, nextOptimistic] = revertReducer(inner, reducer, optimistic, id);
114                             return {
115                                 ...reducer(revertedInner, action),
116                                 optimistic: withHistoryAction(
117                                     {
118                                         id,
119                                         action,
120                                         type: HistoryFlag.OPTIMISTIC_EFFECT,
121                                     },
122                                     nextOptimistic
123                                 ),
124                             };
125                         }
126                     }
127                 }
128             }
129         }
131         if (inner === nextInner) {
132             return outer;
133         }
135         return {
136             ...nextInner,
137             optimistic: withHistoryAction({ type: HistoryFlag.DETERMINISTIC, action }, outer.optimistic),
138         };
139     };
141     wrappedReducer.innerReducer = reducer;
143     return {
144         reducer: wrappedReducer,
145         selectors: {
146             asIfNotFailed: asIfNotFailedSubSelector(wrappedReducer),
147             asIfNotOptimistic: asIfNotOptimisticSubSelector(wrappedReducer),
148         },
149     };
152 export default withOptimistic;