Merge branch 'comment-composer' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _uploads / UploadProvider / useUploadControl.ts
blob6c17fdcf0bb7ceed3b9fd5999e532b44febdd748
1 import { useCallback, useRef } from 'react';
3 import { FILE_CHUNK_SIZE } from '@proton/shared/lib/drive/constants';
5 import type { TransferProgresses } from '../../../components/TransferManager/transfer';
6 import { TransferState } from '../../../components/TransferManager/transfer';
7 import { isTransferActive, isTransferFinalizing, isTransferPending, isTransferProgress } from '../../../utils/transfer';
8 import { MAX_BLOCKS_PER_UPLOAD } from '../constants';
9 import type { UploadFileControls, UploadFolderControls } from '../interface';
10 import type { FileUpload, UpdateCallback, UpdateFilter, UpdateState } from './interface';
12 export default function useUploadControl(
13     fileUploads: FileUpload[],
14     updateWithCallback: (idOrFilter: UpdateFilter, newState: UpdateState, callback: UpdateCallback) => void,
15     removeFromQueue: (idOrFilter: UpdateFilter, callback: UpdateCallback) => void,
16     clearQueue: () => void
17 ) {
18     // Controls keep references to ongoing uploads to have ability
19     // to pause or cancel them.
20     const controls = useRef<{ [id: string]: UploadFileControls | UploadFolderControls }>({});
21     const progresses = useRef<TransferProgresses>({});
23     const add = (id: string, uploadControls: UploadFileControls | UploadFolderControls) => {
24         controls.current[id] = uploadControls;
25         progresses.current[id] = 0;
26     };
28     const remove = (id: string) => {
29         delete controls.current[id];
30         delete progresses.current[id];
31     };
33     const updateProgress = (id: string, increment: number) => {
34         // Progress might be updated even when transfer is already finished and
35         // thus progress is not here anymore. In such case it is OK to simply
36         // ignore the call to not crash.
37         if (progresses.current[id] === undefined) {
38             return;
39         }
40         progresses.current[id] += increment;
41         // Because increment can be float, some aritmetic operation can result
42         // in -0.0000000001 which would be then displayed as -0 after rounding.
43         if (progresses.current[id] < 0) {
44             progresses.current[id] = 0;
45         }
46     };
48     const getProgresses = () => ({ ...progresses.current });
50     const getProgress = (id: string) => progresses.current[id];
52     /**
53      * calculateRemainingUploadBytes returns based on progresses of ongoing
54      * uploads how many data is planned to be uploaded to properly count the
55      * available space for next batch of files to be uploaded.
56      */
57     const calculateRemainingUploadBytes = (): number => {
58         return fileUploads.reduce((sum, upload) => {
59             if (!isTransferActive(upload) || !upload.file.size) {
60                 return sum;
61             }
62             // uploadedChunksSize counts only fully uploaded blocks. Fully
63             // uploaded blocks are counted into used space returned by API.
64             // The algorithm is not precise as file is uploaded in parallel,
65             // but this is what we can do without introducing complex
66             // computation. If better precision is needed, we need to keep
67             // track of each block, not the whole file.
68             const uploadedChunksSize =
69                 progresses.current[upload.id] - (progresses.current[upload.id] % FILE_CHUNK_SIZE) || 0;
70             return sum + upload.file.size - uploadedChunksSize;
71         }, 0);
72     };
74     /**
75      * calculateFileUploadLoad returns how many blocks are being currently
76      * uploaded by all ongoing uploads, considering into account the real
77      * state using the progresses.
78      */
79     const calculateFileUploadLoad = (): number => {
80         // Count both in-progress and finalizing transfers as the ones still
81         // running the worker and taking up some load. Without counting finalizing
82         // state and with the API being slow, we can keep around too many workers.
83         return fileUploads
84             .filter((transfer) => isTransferProgress(transfer) || isTransferFinalizing(transfer))
85             .reduce((load, upload) => {
86                 const remainingSize = (upload.file.size || 0) - (progresses.current[upload.id] || 0);
87                 // Even if the file is empty, keep the minimum of blocks to 1,
88                 // otherwise it would start too many threads.
89                 const chunks = Math.max(Math.ceil(remainingSize / FILE_CHUNK_SIZE), 1);
90                 const loadIncrease = Math.min(MAX_BLOCKS_PER_UPLOAD, chunks);
91                 return load + loadIncrease;
92             }, 0);
93     };
95     const pauseUploads = useCallback(
96         (idOrFilter: UpdateFilter) => {
97             updateWithCallback(idOrFilter, TransferState.Paused, ({ id, state }) => {
98                 if (isTransferProgress({ state }) || isTransferPending({ state })) {
99                     (controls.current[id] as UploadFileControls)?.pause?.();
100                 }
101             });
102         },
103         [updateWithCallback]
104     );
106     const resumeUploads = useCallback(
107         (idOrFilter: UpdateFilter) => {
108             updateWithCallback(
109                 idOrFilter,
110                 ({ resumeState, parentId }) => {
111                     // If the parent folder was created during the pause,
112                     // go back to pending, not initializing state.
113                     if (parentId && resumeState === TransferState.Initializing) {
114                         return TransferState.Pending;
115                     }
116                     return resumeState || TransferState.Progress;
117                 },
118                 ({ id }) => {
119                     (controls.current[id] as UploadFileControls)?.resume?.();
120                 }
121             );
122         },
123         [updateWithCallback]
124     );
126     const cancelUploads = useCallback(
127         (idOrFilter: UpdateFilter) => {
128             updateWithCallback(idOrFilter, TransferState.Canceled, ({ id }) => {
129                 controls.current[id]?.cancel();
130             });
131         },
132         [updateWithCallback]
133     );
135     const removeUploads = useCallback(
136         (idOrFilter: UpdateFilter) => {
137             // We should never simply remove uploads, but cancel it first, so
138             // it does not continue on background without our knowledge.
139             cancelUploads(idOrFilter);
140             removeFromQueue(idOrFilter, ({ id }) => remove(id));
141         },
142         [removeFromQueue]
143     );
145     const clearUploads = useCallback(() => {
146         Object.entries(controls.current).map(([, uploadControls]) => uploadControls.cancel());
147         controls.current = {};
148         progresses.current = {};
149         clearQueue();
150     }, [clearQueue]);
152     return {
153         add,
154         remove,
155         updateProgress,
156         getProgresses,
157         getProgress,
158         calculateRemainingUploadBytes,
159         calculateFileUploadLoad,
160         pauseUploads,
161         resumeUploads,
162         cancelUploads,
163         removeUploads,
164         clearUploads,
165     };