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 {
12 CALENDAR = 'CalendarModelEventID',
15 type EventResponse = {
16 [key in EVENT_ID_KEYS]: string;
21 interface EventManagerConfig {
22 /** Function to call the API */
24 /** Initial event ID to begin from */
26 /** Maximum interval time to wait between each call */
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;
40 call: () => Promise<void>;
42 subscribe: SubscribeFn;
46 * Create the event manager process.
48 const createEventManager = ({
50 eventID: initialEventID,
51 interval = INTERVAL_EVENT_TIMER,
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.');
64 timeoutHandle?: ReturnType<typeof setTimeout>;
65 abortController?: AbortController;
68 lastEventID: initialEventID,
69 timeoutHandle: undefined,
70 abortController: undefined,
73 const setEventID = (eventID: string) => {
74 STATE.lastEventID = eventID;
77 const getEventID = () => {
78 return STATE.lastEventID;
81 const setRetryIndex = (index: number) => {
82 STATE.retryIndex = index;
85 const getRetryIndex = () => {
86 return STATE.retryIndex;
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);
98 * Start the event manager, does nothing if it is already started.
100 const start = () => {
101 const { timeoutHandle, retryIndex } = STATE;
107 const ms = interval * FIBONACCI_LIST[retryIndex];
108 // eslint-disable-next-line
109 STATE.timeoutHandle = setTimeout(call, ms);
113 * Stop the event manager, does nothing if it's already stopped.
116 const { timeoutHandle, abortController } = STATE;
118 if (abortController) {
119 abortController.abort();
120 delete STATE.abortController;
124 clearTimeout(timeoutHandle);
125 delete STATE.timeoutHandle;
130 * Stop the event manager and reset its state.
132 const reset = () => {
134 STATE = { retryIndex: 0 };
139 * Call the event manager. Either does it immediately, or queues the call until after the current call has finished.
141 const call = onceWithQueue(async () => {
145 const abortController = new AbortController();
146 STATE.abortController = abortController;
149 const eventID = getEventID();
152 throw new Error('EventID undefined');
155 let result: EventResponse;
157 result = await api<EventResponse>({
159 signal: abortController.signal,
162 } catch (error: any) {
163 if (error.name === 'AbortError') {
169 await Promise.all(listeners.notify(result)).catch(noop);
171 const { More, [eventIDKey]: nextEventID } = result;
172 setEventID(nextEventID);
179 delete STATE.abortController;
181 } catch (error: any) {
182 delete STATE.abortController;
183 increaseRetryIndex();
196 subscribe: listeners.subscribe as SubscribeFn,
200 export default createEventManager;