Merge branch 'fix/sentry-issue' into 'main'
[ProtonMail-WebClient.git] / packages / pass / store / request / flow.ts
blob48a60a7095b010f59d1b23614f91eff954b66bf6
1 import { createAction } from '@reduxjs/toolkit';
3 import { type ActionCallback, withCallback } from '@proton/pass/store/actions/enhancers/callback';
4 import type { TagMatch, Tagged } from '@proton/pass/types';
5 import { pipe } from '@proton/pass/utils/fp/pipe';
7 import { withRequest, withRequestFailure, withRequestSuccess } from './enhancers';
8 import type { RequestConfig } from './types';
10 type RequestPrepareAction<P extends any[], R> = (...params: P) => R;
11 type RequestKeyPrepator<T> = (dto: T) => string;
12 type RequestKeyDefault = () => string;
13 type RequestKeyPreparators<T> = RequestKeyDefault | RequestKeyPrepator<T>;
15 type Payload<T = any> = { payload: T };
17 export type RequestFlow<I, S, F> = ReturnType<ReturnType<typeof requestActionsFactory<I, S, F>>>;
18 export type RequestIntent<T extends RequestFlow<any, any, any>> = T extends RequestFlow<infer U, any, any> ? U : never;
19 export type RequestSuccess<T extends RequestFlow<any, any, any>> = T extends RequestFlow<any, infer U, any> ? U : never;
21 type CreateRequestActionsOptions<
22     IntentDTO,
23     IntentPrepared extends Payload,
24     IntentData extends any,
25     SuccessDTO,
26     SuccessPrepared extends Payload,
27     SuccessData extends any,
28     FailureDTO,
29     FailurePrepared extends Payload,
30     FailureData extends any,
31     RequestKey extends RequestKeyPrepator<IntentDTO>,
32 > = {
33     /** Key option determines requestID generation:
34      * - When provided: ${namespace}::${key(dto)} requiring IntentDTO param
35      * - When omitted: uses action namespace & `requestID` function becomes `() => string` */
36     key?: RequestKey;
37     /** Intent action configuration */
38     intent?: {
39         /** Defaults to `false` - set to `true` to track "intent" request data */
40         config?: RequestConfig<'start', IntentData>;
41         /** Defaults to `(intent: IntentDTO) => ({ payload: IntentDTO })`  */
42         prepare?: RequestPrepareAction<[intent: IntentDTO], IntentPrepared>;
43     };
44     /** Success action configuration */
45     success?: {
46         /** Defaults to `false` - set to `true` to track "success" request data */
47         config?: RequestConfig<'success', SuccessData>;
48         /** Defaults to `(success: SuccessDTO) => ({ payload: SuccessDTO })`  */
49         prepare?: RequestPrepareAction<[success: SuccessDTO], SuccessPrepared>;
50     };
51     /** Failure action configuration */
52     failure?: {
53         /** Defaults to `false` - set to `true` to track "failure" request data */
54         config?: RequestConfig<'failure', FailureData>;
55         /** Defaults to `(error: unknown, failure: FailureDTO) => ({ payload: FailureDTO, error: unknown })`  */
56         prepare?: RequestPrepareAction<[error: unknown, failure: FailureDTO], FailurePrepared>;
57     };
60 /** Creates action creators for each stage of a request sequence:
61  * intent, success, and error. These action creators facilitate the
62  * dispatching of actions to represent the initiation of a request,
63  * successful completion of a request, and handling of errors that
64  * occur during the request process. */
65 export const requestActionsFactory =
66     <IntentDTO, SuccessDTO, FailureDTO = void>(namespace: string) =>
67     /** All generics include sensible defaults allowing partial `options`:
68      * - Prepared types default to `Payload<DTO>` maintaining the payload structure
69      * - Data flags default to false (no request tracking)
70      * - `RequestKey` defaults to `() => string` when key option is omitted */
71     <
72         IntentPrepared extends Payload = Payload<IntentDTO>,
73         SuccessPrepared extends Payload = Payload<SuccessDTO>,
74         FailurePrepared extends Payload = Payload<FailureDTO>,
75         IntentData extends any = never,
76         SuccessData extends any = never,
77         FailureData extends any = never,
78         /* Tags default key preparator with 'fallback' for type discrimination
79          * while preserving RequestKey type parameter constraints. */
80         RequestKey extends RequestKeyPrepator<IntentDTO> = Tagged<RequestKeyPreparators<IntentDTO>, 'fallback'>,
81     >(
82         options: CreateRequestActionsOptions<
83             IntentDTO,
84             IntentPrepared,
85             IntentData,
86             SuccessDTO,
87             SuccessPrepared,
88             SuccessData,
89             FailureDTO,
90             FailurePrepared,
91             FailureData,
92             RequestKey
93         > = {}
94     ) => {
95         type IntentPA = RequestPrepareAction<[intent: IntentDTO], IntentPrepared>;
96         type SuccessPA = RequestPrepareAction<[success: SuccessDTO], SuccessPrepared>;
97         type FailurePA = RequestPrepareAction<[error: unknown, failure: FailureDTO], FailurePrepared>;
98         type RequestPA = TagMatch<RequestKey, 'fallback', RequestKeyDefault, RequestKey>;
100         const toPayload = (payload: unknown) => ({ payload });
101         const toPayloadWithError = (error: unknown, payload: unknown) => ({ payload, error });
103         const intentPA = (options.intent?.prepare ?? toPayload) as IntentPA;
104         const successPA = (options.success?.prepare ?? toPayload) as SuccessPA;
105         const failurePA = (options.failure?.prepare ?? toPayloadWithError) as FailurePA;
106         const requestID = (dto: IntentDTO) =>
107             'key' in options && options.key ? `${namespace}::${options.key(dto)}` : namespace;
109         return {
110             requestID: requestID as RequestPA,
111             intent: createAction(`${namespace}::intent`, (dto: IntentDTO, callback?: ActionCallback) =>
112                 pipe(
113                     withRequest({
114                         status: 'start',
115                         id: requestID(dto),
116                         ...(options.intent?.config ?? {}),
117                     }),
118                     withCallback(callback)
119                 )(intentPA(dto))
120             ),
121             success: createAction(`${namespace}::success`, withRequestSuccess(successPA, options.success?.config)),
122             failure: createAction(`${namespace}::failure`, withRequestFailure(failurePA, options.failure?.config)),
123         } as const;
124     };