Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _events / useDriveEventManager.tsx
blobceaae9d7e3a5bcfc1acd73f326c15e082a0240a0
1 import { createContext, useContext, useMemo, useRef } from 'react';
3 import { useApi, useEventManager } from '@proton/components';
4 import metrics from '@proton/metrics';
5 import { queryLatestVolumeEvent, queryVolumeEvents } from '@proton/shared/lib/api/drive/volume';
6 import type { EventManager } from '@proton/shared/lib/eventManager/eventManager';
7 import createEventManager from '@proton/shared/lib/eventManager/eventManager';
8 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
9 import type { Api } from '@proton/shared/lib/interfaces';
10 import type { DriveEventsResult } from '@proton/shared/lib/interfaces/drive/events';
11 import generateUID from '@proton/utils/generateUID';
13 import { isIgnoredErrorForReporting, logError } from '../../utils/errorHandling';
14 import { UserAvailabilityTypes } from '../../utils/metrics/types/userSuccessMetricsTypes';
15 import { userSuccessMetrics } from '../../utils/metrics/userSuccessMetrics';
16 import { driveEventsResultToDriveEvents } from '../_api';
17 import type { VolumeType } from '../_volumes';
18 import { EventsMetrics, countEventsPerType, getErrorCategory } from './driveEventsMetrics';
19 import type { DriveCoreEvent, DriveEvent, EventHandler } from './interface';
21 const DRIVE_EVENT_HANDLER_ID_PREFIX = 'drive-event-handler';
23 const DRIVE_EVENT_MANAGER_FUNCTIONS_STUB = {
24     getSubscriptionIds: () => [],
25     clear: () => undefined,
27     eventHandlers: {
28         register: () => 'id',
29         unregister: () => false,
30         subscribeToCore: () => () => {},
31     },
33     volumes: {
34         startSubscription: () => {
35             throw Error('Usage of uninitialized DriveEventManager!');
36         },
37         pauseSubscription: () => {},
38         unsubscribe: () => {},
39     },
41     pollEvents: {
42         volumes: () => Promise.resolve(),
43         driveEvents: () => Promise.resolve(),
44     },
47 export function useDriveEventManagerProvider(api: Api, generalEventManager: EventManager) {
48     const isPollingManually = useRef(false);
49     const eventHandlers = useRef(new Map<string, EventHandler>());
50     const eventManagers = useRef(new Map<string, EventManager>());
51     const eventsMetrics = useMemo(() => new EventsMetrics(), [api, generalEventManager]);
53     const genericHandler = (volumeId: string, type: VolumeType, driveEvents: DriveEventsResult) => {
54         countEventsPerType(type, driveEvents);
56         if (!driveEvents.Events?.length) {
57             return;
58         }
60         eventsMetrics.batchStart(volumeId, driveEvents);
62         const handlerPromises: unknown[] = [];
63         eventHandlers.current.forEach((handler) => {
64             handlerPromises.push(
65                 handler(volumeId, driveEventsResultToDriveEvents(driveEvents), (eventId: string, event: DriveEvent) => {
66                     // Our app is depending on the events but it could avoid it
67                     // completely if we update local state after receiving OK
68                     // from the backend. Web polls extensively for this reason
69                     // and any poll should be ignored from processed events
70                     // because those are the events we could technically avoid.
71                     // We want to know how many such events it is.
72                     if (!isPollingManually.current) {
73                         eventsMetrics.processed(eventId, event);
74                     }
75                 })
76             );
77         });
79         /*
80             forcing .poll function's returned Promise to be resolved
81             *after* event processin is finished
82         */
83         return Promise.all(handlerPromises).then(() => {
84             eventsMetrics.batchCompleted(volumeId, driveEvents.EventID, type);
85         });
86     };
88     const createVolumeEventManager = async (volumeId: string, volumeType: VolumeType) => {
89         try {
90             const { EventID } = await api<{ EventID: string }>(queryLatestVolumeEvent(volumeId));
92             const eventManager = createEventManager({
93                 api,
94                 eventID: EventID,
95                 query: (eventId: string) => queryVolumeEvents(volumeId, eventId),
96             });
98             eventManagers.current.set(volumeId, eventManager);
100             return eventManager;
101         } catch (e) {
102             // TODO: DRVWEB-4319 Implement sync errors & sync erroring users
103             // This metric will have to be redone
104             metrics.drive_sync_errors_total.increment({
105                 type: getErrorCategory(e),
106                 // This is in fact volumeType but since the metric is old we haven't updated yet and are using shareType as volumeType
107                 shareType: volumeType,
108             });
109             if (!isIgnoredErrorForReporting(e)) {
110                 userSuccessMetrics.mark(UserAvailabilityTypes.coreFeatureError);
111             }
112             throw e;
113         }
114     };
116     /**
117      * Creates event manager for a specified volume and starts interval polling of event.
118      */
119     const subscribeToVolume = async (volumeId: string, type: VolumeType) => {
120         const eventManager = await createVolumeEventManager(volumeId, type);
121         eventManager.subscribe((payload: DriveEventsResult) => genericHandler(volumeId, type, payload));
122         eventManagers.current.set(volumeId, eventManager);
123     };
125     /**
126      * Subscribe to core events from the general event manager
127      */
128     const subscribeToCoreEvents = (listener: (event: DriveCoreEvent) => void) => {
129         return generalEventManager.subscribe((event) => {
130             listener(event);
131         });
132     };
134     /**
135      * Creates an event manager for a specified volume if doesn't exist,
136      * and starts event polling
137      */
138     const startVolumeSubscription = async (volumeId: string, type: VolumeType) => {
139         if (!eventManagers.current.get(volumeId)) {
140             await subscribeToVolume(volumeId, type);
141         }
142         eventManagers.current.get(volumeId)!.start();
143     };
145     /**
146      * Pauses event polling for the volume. Returns false if there's no event manager
147      * associated with the volumeId
148      */
149     const pauseVolumeSubscription = (volumeId: string): boolean => {
150         const volumeSubscription = eventManagers.current.get(volumeId);
151         if (volumeSubscription) {
152             volumeSubscription.stop();
153             return true;
154         }
156         return false;
157     };
159     /**
160      * Stops event listening, empties handlers and clears reference to the event manager
161      */
162     const unsubscribeFromVolume = (volumeId: string): boolean => {
163         eventManagers.current.get(volumeId)?.reset();
164         return eventManagers.current.delete(volumeId);
165     };
167     /**
168      * Polls drive events for a volume
169      * @private
170      */
171     const pollVolume = async (volumeId: string): Promise<void> => {
172         const eventManager = eventManagers.current.get(volumeId);
174         if (!eventManager) {
175             captureMessage('Trying to call non-existing event manager');
176             return;
177         }
179         await eventManager.call().catch(logError);
180     };
182     /**
183      * Polls events for specified list of volumes
184      */
185     const pollVolumeEvents = async (
186         volumeIds: string | string[],
187         params: { includeCommon: boolean } = { includeCommon: false }
188     ) => {
189         isPollingManually.current = true;
191         const volumeIdsArray = Array.isArray(volumeIds) ? volumeIds : [volumeIds];
192         const pollingTasks = [];
194         if (params.includeCommon) {
195             pollingTasks.push(generalEventManager.call());
196         }
198         pollingTasks.push(...volumeIdsArray.map((volumeId) => pollVolume(volumeId)));
200         await Promise.all(pollingTasks)
201             .catch(logError)
202             .finally(() => {
203                 isPollingManually.current = false;
204             });
205     };
207     /**
208      *  Polls drive events for all subscribed volumes
209      */
210     const pollDriveEvents = async (params: { includeCommon: boolean } = { includeCommon: false }): Promise<void> => {
211         isPollingManually.current = true;
213         const pollingPromises: Promise<unknown>[] = [];
214         if (params.includeCommon) {
215             pollingPromises.push(generalEventManager.call());
216         }
217         eventManagers.current.forEach((eventManager) => {
218             pollingPromises.push(eventManager.call());
219         });
221         await Promise.all(pollingPromises)
222             .catch(logError)
223             .finally(() => {
224                 isPollingManually.current = false;
225             });
226     };
228     /**
229      * Registers passed event handler to process currenlty active share subscriptions by specific id
230      */
231     const registerEventHandlerById = (id: string, callback: EventHandler): string => {
232         eventHandlers.current.set(id, callback);
233         return id;
234     };
236     /**
237      * Registers passed event handler to process currenlty active share subscriptions
238      */
239     const registerEventHandler = (callback: EventHandler): string => {
240         const callbackUID = generateUID(DRIVE_EVENT_HANDLER_ID_PREFIX);
241         return registerEventHandlerById(callbackUID, callback);
242     };
244     /**
245      * Removes event handler
246      */
247     const unregisterEventHandler = (callbackId: string): boolean => {
248         return eventHandlers.current.delete(callbackId);
249     };
251     /**
252      * List share ids which event manager subscribed to
253      */
254     const getSubscriptionIds = (): string[] => {
255         return Array.from(eventManagers.current.keys());
256     };
258     /**
259      * Cancels all ongoing requests, clears timeout and references to all listeners
260      * event managers and handlers
261      */
262     const clear = () => {
263         // clear timeouts and listeners
264         eventManagers.current.forEach((_, key) => {
265             unsubscribeFromVolume(key);
266         });
267         // clear references to event managers
268         eventManagers.current.clear();
269         // clear event handlers
270         eventHandlers.current.clear();
271     };
273     return {
274         getSubscriptionIds,
275         clear,
277         volumes: {
278             startSubscription: startVolumeSubscription,
279             pauseSubscription: pauseVolumeSubscription,
280             unsubscribe: unsubscribeFromVolume,
281         },
283         eventHandlers: {
284             register: registerEventHandler,
285             unregister: unregisterEventHandler,
286             subscribeToCore: subscribeToCoreEvents,
287         },
289         pollEvents: {
290             volumes: pollVolumeEvents,
291             driveEvents: pollDriveEvents,
292         },
293     };
296 const DriveEventManagerContext = createContext<ReturnType<typeof useDriveEventManagerProvider> | null>(null);
298 export function DriveEventManagerProvider({ children }: { children: React.ReactNode }) {
299     const api = useApi();
300     const generalEventManager = useEventManager();
301     const driveEventManager = useDriveEventManagerProvider(api, generalEventManager);
303     return <DriveEventManagerContext.Provider value={driveEventManager}>{children}</DriveEventManagerContext.Provider>;
306 export const useDriveEventManager = () => {
307     const state = useContext(DriveEventManagerContext);
308     if (!state) {
309         // DriveEventManager might be uninitialized in some cases.
310         // For example, public shares do not have this implemented yet.
311         // Better would be to not have event manager as required automatic
312         // dependency, but that requires bigger changes. In the end, this
313         // situation is just because of how React hooks work. One day, once
314         // this all is shifted to worker instead, we can make it nicer.
315         return DRIVE_EVENT_MANAGER_FUNCTIONS_STUB;
316     }
317     return state;