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,
29 unregister: () => false,
30 subscribeToCore: () => () => {},
34 startSubscription: () => {
35 throw Error('Usage of uninitialized DriveEventManager!');
37 pauseSubscription: () => {},
38 unsubscribe: () => {},
42 volumes: () => Promise.resolve(),
43 driveEvents: () => Promise.resolve(),
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) {
60 eventsMetrics.batchStart(volumeId, driveEvents);
62 const handlerPromises: unknown[] = [];
63 eventHandlers.current.forEach((handler) => {
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);
80 forcing .poll function's returned Promise to be resolved
81 *after* event processin is finished
83 return Promise.all(handlerPromises).then(() => {
84 eventsMetrics.batchCompleted(volumeId, driveEvents.EventID, type);
88 const createVolumeEventManager = async (volumeId: string, volumeType: VolumeType) => {
90 const { EventID } = await api<{ EventID: string }>(queryLatestVolumeEvent(volumeId));
92 const eventManager = createEventManager({
95 query: (eventId: string) => queryVolumeEvents(volumeId, eventId),
98 eventManagers.current.set(volumeId, eventManager);
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,
109 if (!isIgnoredErrorForReporting(e)) {
110 userSuccessMetrics.mark(UserAvailabilityTypes.coreFeatureError);
117 * Creates event manager for a specified volume and starts interval polling of event.
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);
126 * Subscribe to core events from the general event manager
128 const subscribeToCoreEvents = (listener: (event: DriveCoreEvent) => void) => {
129 return generalEventManager.subscribe((event) => {
135 * Creates an event manager for a specified volume if doesn't exist,
136 * and starts event polling
138 const startVolumeSubscription = async (volumeId: string, type: VolumeType) => {
139 if (!eventManagers.current.get(volumeId)) {
140 await subscribeToVolume(volumeId, type);
142 eventManagers.current.get(volumeId)!.start();
146 * Pauses event polling for the volume. Returns false if there's no event manager
147 * associated with the volumeId
149 const pauseVolumeSubscription = (volumeId: string): boolean => {
150 const volumeSubscription = eventManagers.current.get(volumeId);
151 if (volumeSubscription) {
152 volumeSubscription.stop();
160 * Stops event listening, empties handlers and clears reference to the event manager
162 const unsubscribeFromVolume = (volumeId: string): boolean => {
163 eventManagers.current.get(volumeId)?.reset();
164 return eventManagers.current.delete(volumeId);
168 * Polls drive events for a volume
171 const pollVolume = async (volumeId: string): Promise<void> => {
172 const eventManager = eventManagers.current.get(volumeId);
175 captureMessage('Trying to call non-existing event manager');
179 await eventManager.call().catch(logError);
183 * Polls events for specified list of volumes
185 const pollVolumeEvents = async (
186 volumeIds: string | string[],
187 params: { includeCommon: boolean } = { includeCommon: false }
189 isPollingManually.current = true;
191 const volumeIdsArray = Array.isArray(volumeIds) ? volumeIds : [volumeIds];
192 const pollingTasks = [];
194 if (params.includeCommon) {
195 pollingTasks.push(generalEventManager.call());
198 pollingTasks.push(...volumeIdsArray.map((volumeId) => pollVolume(volumeId)));
200 await Promise.all(pollingTasks)
203 isPollingManually.current = false;
208 * Polls drive events for all subscribed volumes
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());
217 eventManagers.current.forEach((eventManager) => {
218 pollingPromises.push(eventManager.call());
221 await Promise.all(pollingPromises)
224 isPollingManually.current = false;
229 * Registers passed event handler to process currenlty active share subscriptions by specific id
231 const registerEventHandlerById = (id: string, callback: EventHandler): string => {
232 eventHandlers.current.set(id, callback);
237 * Registers passed event handler to process currenlty active share subscriptions
239 const registerEventHandler = (callback: EventHandler): string => {
240 const callbackUID = generateUID(DRIVE_EVENT_HANDLER_ID_PREFIX);
241 return registerEventHandlerById(callbackUID, callback);
245 * Removes event handler
247 const unregisterEventHandler = (callbackId: string): boolean => {
248 return eventHandlers.current.delete(callbackId);
252 * List share ids which event manager subscribed to
254 const getSubscriptionIds = (): string[] => {
255 return Array.from(eventManagers.current.keys());
259 * Cancels all ongoing requests, clears timeout and references to all listeners
260 * event managers and handlers
262 const clear = () => {
263 // clear timeouts and listeners
264 eventManagers.current.forEach((_, key) => {
265 unsubscribeFromVolume(key);
267 // clear references to event managers
268 eventManagers.current.clear();
269 // clear event handlers
270 eventHandlers.current.clear();
278 startSubscription: startVolumeSubscription,
279 pauseSubscription: pauseVolumeSubscription,
280 unsubscribe: unsubscribeFromVolume,
284 register: registerEventHandler,
285 unregister: unregisterEventHandler,
286 subscribeToCore: subscribeToCoreEvents,
290 volumes: pollVolumeEvents,
291 driveEvents: pollDriveEvents,
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);
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;