1 import type { Maybe } from '@proton/pass/types';
2 import type { Api } from '@proton/pass/types/api';
3 import { logger } from '@proton/pass/utils/logger';
4 import { FIBONACCI_LIST } from '@proton/shared/lib/constants';
5 import createListeners from '@proton/shared/lib/helpers/listeners';
6 import { onceWithQueue } from '@proton/shared/lib/helpers/onceWithQueue';
8 import { ACTIVE_POLLING_TIMEOUT } from './constants';
10 export type EventManagerEvent<T extends {}> = T | { error: unknown };
11 export type EventCursor = { EventID: string; More: boolean };
13 export const NOOP_EVENT = '*';
15 export type EventManagerConfig<T extends {}> = {
16 api: Api /* Function to call the API */;
17 interval?: number /* Maximum interval time to wait between each call */;
18 initialEventID: string;
19 query: (eventID: string) => {} /* Event polling endpoint override */;
20 getCursor: (event: T) => EventCursor;
21 getLatestEventID?: () => Promise<string> | string;
24 export type EventManager<T extends {}> = {
25 state: EventManagerState;
26 setEventID: (eventID: string) => void;
27 getEventID: () => Maybe<string>;
30 call: () => Promise<void>;
32 setInterval: (interval: number) => void;
33 subscribe: (listener: (event: EventManagerEvent<T>) => void) => () => void;
36 type EventManagerState = {
41 abortController?: AbortController;
44 export const eventManager = <T extends {}>({
46 interval = ACTIVE_POLLING_TIMEOUT,
51 }: EventManagerConfig<T>): EventManager<T> => {
52 const listeners = createListeners<[EventManagerEvent<T>]>();
54 const state: EventManagerState = { interval, retryIndex: 0, lastEventID: initialEventID };
56 const setInterval = (nextInterval: number) => (state.interval = nextInterval);
57 const setEventID = (eventID: string) => (state.lastEventID = eventID);
58 const getEventID = () => (state.lastEventID ? state.lastEventID : undefined);
59 const setRetryIndex = (index: number) => (state.retryIndex = index);
60 const getRetryIndex = () => state.retryIndex;
62 /* Increase the retry index when the call fails to not spam */
63 const increaseRetryIndex = () => {
64 const index = getRetryIndex();
65 if (index < FIBONACCI_LIST.length - 1) setRetryIndex(index + 1);
68 /* Start the event manager, does nothing if it is already started */
69 const start = (callFn: () => Promise<void>) => {
70 if (!state.timeoutHandle) {
71 const ms = state.interval * FIBONACCI_LIST[state.retryIndex];
72 state.timeoutHandle = setTimeout(callFn, ms);
76 /* Stop the event manager, does nothing if it's already stopped */
78 if (state.abortController) {
79 state.abortController.abort();
80 delete state.abortController;
83 if (state.timeoutHandle) {
84 clearTimeout(state.timeoutHandle);
85 delete state.timeoutHandle;
89 /* Stop the event manager and reset its state */
94 state.interval = interval;
95 delete state.abortController;
96 delete state.lastEventID;
97 delete state.timeoutHandle;
102 /* Call the event manager. Either does it immediately, or queues
103 * the call until after the current call has finished */
104 const call = onceWithQueue(async () => {
108 const abortController = new AbortController();
109 state.abortController = abortController;
112 const eventID = getEventID() ?? (await getLatestEventID?.());
115 logger.warn('No valid `EventID` provided');
119 const result = await api<T>({ ...query(eventID), signal: abortController.signal, silence: true });
122 await Promise.all(listeners.notify(result));
123 const { More, EventID: nextEventID } = getCursor(result);
125 setEventID(nextEventID);
131 delete state.abortController;
133 } catch (error: any) {
134 /* ⚠️ if the request failed due to a locked or inactive session :
135 * do not restart the event-manager. For any other type of error,
136 * we can safely increase the retry index and retry.. */
137 const { appVersionBad, sessionInactive, sessionLocked } = api.getState();
138 if (error.name === 'AbortError' || appVersionBad || sessionInactive || sessionLocked) return;
140 delete state.abortController;
141 increaseRetryIndex();
144 listeners.notify({ error });
153 start: () => start(call),
157 subscribe: listeners.subscribe,