Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / eventManager / eventManager.ts
blob0a187a4312897bcbc57bc76757c998ee05249ee5
1 import noop from '@proton/utils/noop';
3 import { getEvents } from '../api/events';
4 import { FIBONACCI_LIST, INTERVAL_EVENT_TIMER } from '../constants';
5 import type { Listener } from '../helpers/listeners';
6 import createListeners from '../helpers/listeners';
7 import { onceWithQueue } from '../helpers/onceWithQueue';
8 import type { Api } from '../interfaces';
10 export enum EVENT_ID_KEYS {
11     DEFAULT = 'EventID',
12     CALENDAR = 'CalendarModelEventID',
15 type EventResponse = {
16     [key in EVENT_ID_KEYS]: string;
17 } & {
18     More: 0 | 1;
21 interface EventManagerConfig {
22     /** Function to call the API */
23     api: Api;
24     /** Initial event ID to begin from */
25     eventID: string;
26     /** Maximum interval time to wait between each call */
27     interval?: number;
28     /** Event polling endpoint override */
29     query?: (eventID: string) => object;
30     eventIDKey?: EVENT_ID_KEYS;
33 export type SubscribeFn = <A extends any[], R = void>(listener: Listener<A, R>) => () => void;
35 export interface EventManager {
36     setEventID: (eventID: string) => void;
37     getEventID: () => string | undefined;
38     start: () => void;
39     stop: () => void;
40     call: () => Promise<void>;
41     reset: () => void;
42     subscribe: SubscribeFn;
45 /**
46  * Create the event manager process.
47  */
48 const createEventManager = ({
49     api,
50     eventID: initialEventID,
51     interval = INTERVAL_EVENT_TIMER,
52     query = getEvents,
53     eventIDKey = EVENT_ID_KEYS.DEFAULT,
54 }: EventManagerConfig): EventManager => {
55     const listeners = createListeners<[EventResponse]>();
57     if (!initialEventID) {
58         throw new Error('eventID must be provided.');
59     }
61     let STATE: {
62         retryIndex: number;
63         lastEventID?: string;
64         timeoutHandle?: ReturnType<typeof setTimeout>;
65         abortController?: AbortController;
66     } = {
67         retryIndex: 0,
68         lastEventID: initialEventID,
69         timeoutHandle: undefined,
70         abortController: undefined,
71     };
73     const setEventID = (eventID: string) => {
74         STATE.lastEventID = eventID;
75     };
77     const getEventID = () => {
78         return STATE.lastEventID;
79     };
81     const setRetryIndex = (index: number) => {
82         STATE.retryIndex = index;
83     };
85     const getRetryIndex = () => {
86         return STATE.retryIndex;
87     };
89     const increaseRetryIndex = () => {
90         const index = getRetryIndex();
91         // Increase the retry index when the call fails to not spam.
92         if (index < FIBONACCI_LIST.length - 1) {
93             setRetryIndex(index + 1);
94         }
95     };
97     /**
98      * Start the event manager, does nothing if it is already started.
99      */
100     const start = () => {
101         const { timeoutHandle, retryIndex } = STATE;
103         if (timeoutHandle) {
104             return;
105         }
107         const ms = interval * FIBONACCI_LIST[retryIndex];
108         // eslint-disable-next-line
109         STATE.timeoutHandle = setTimeout(call, ms);
110     };
112     /**
113      * Stop the event manager, does nothing if it's already stopped.
114      */
115     const stop = () => {
116         const { timeoutHandle, abortController } = STATE;
118         if (abortController) {
119             abortController.abort();
120             delete STATE.abortController;
121         }
123         if (timeoutHandle) {
124             clearTimeout(timeoutHandle);
125             delete STATE.timeoutHandle;
126         }
127     };
129     /**
130      * Stop the event manager and reset its state.
131      */
132     const reset = () => {
133         stop();
134         STATE = { retryIndex: 0 };
135         listeners.clear();
136     };
138     /**
139      * Call the event manager. Either does it immediately, or queues the call until after the current call has finished.
140      */
141     const call = onceWithQueue(async () => {
142         try {
143             stop();
145             const abortController = new AbortController();
146             STATE.abortController = abortController;
148             for (;;) {
149                 const eventID = getEventID();
151                 if (!eventID) {
152                     throw new Error('EventID undefined');
153                 }
155                 let result: EventResponse;
156                 try {
157                     result = await api<EventResponse>({
158                         ...query(eventID),
159                         signal: abortController.signal,
160                         silence: true,
161                     });
162                 } catch (error: any) {
163                     if (error.name === 'AbortError') {
164                         return;
165                     }
166                     throw error;
167                 }
169                 await Promise.all(listeners.notify(result)).catch(noop);
171                 const { More, [eventIDKey]: nextEventID } = result;
172                 setEventID(nextEventID);
173                 setRetryIndex(0);
175                 if (!More) {
176                     break;
177                 }
178             }
179             delete STATE.abortController;
180             start();
181         } catch (error: any) {
182             delete STATE.abortController;
183             increaseRetryIndex();
184             start();
185             throw error;
186         }
187     });
189     return {
190         setEventID,
191         getEventID,
192         start,
193         stop,
194         call,
195         reset,
196         subscribe: listeners.subscribe as SubscribeFn,
197     };
200 export default createEventManager;