Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _views / useTransfersView.tsx
blob46ccc283ccfa4c7fbcc92c8f4c531d4e70f910b9
1 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3 import type {
4     Transfer,
5     TransferHistoryStats,
6     TransferProgresses,
7     TransferStats,
8     TransfersHistoryStats,
9     TransfersStats,
10 } from '../../components/TransferManager/transfer';
11 import { TransferState } from '../../components/TransferManager/transfer';
12 import { isTransferError, isTransferFinished, isTransferProgress } from '../../utils/transfer';
13 import { useDownloadProvider } from '../_downloads';
14 import { useUpload } from '../_uploads';
16 const PROGRESS_UPDATE_INTERVAL = 500;
17 const SPEED_SNAPSHOTS = 10; // How many snapshots should the speed be average of
19 /**
20  * useTransfersView provides data for transfer manager.
21  */
22 export default function useTransfersView() {
23     const { downloads, getDownloadsProgresses, clearDownloads } = useDownloadProvider();
24     const { uploads, getUploadsProgresses, clearUploads } = useUpload();
26     const transfers = useMemo(() => [...downloads, ...uploads], [downloads, uploads]);
27     const hasActiveTransfer = useMemo(() => !transfers.every(isTransferFinished), [transfers]);
28     const numberOfFailedTransfer = useMemo(() => {
29         return {
30             total: transfers.filter(isTransferError).length,
31             downloads: downloads.filter(isTransferError).length,
32             uploads: uploads.filter(isTransferError).length,
33         };
34     }, [transfers, downloads, uploads]);
36     const getTransferProgresses = useCallback(() => {
37         return {
38             ...getDownloadsProgresses(),
39             ...getUploadsProgresses(),
40         };
41     }, [getDownloadsProgresses, getUploadsProgresses]);
42     const stats = useStats(transfers, getTransferProgresses);
44     const clearAllTransfers = useCallback(() => {
45         clearDownloads();
46         clearUploads();
47     }, [clearDownloads, clearUploads]);
49     return {
50         downloads,
51         uploads,
52         hasActiveTransfer,
53         numberOfFailedTransfer,
54         stats,
55         clearAllTransfers,
56     };
59 function useStats(transfers: Transfer[], getTransferProgresses: () => TransferProgresses) {
60     const statsHistory = useStatsHistory(transfers, getTransferProgresses);
62     return useMemo((): TransfersStats => {
63         const calculateAverageSpeed = (transferId: string) => {
64             if (!statsHistory.length) {
65                 return 0;
66             }
68             let sum = 0;
70             for (let i = 0; i < statsHistory.length; i++) {
71                 const stats = statsHistory[i].stats[transferId];
73                 if (!stats?.active || stats.speed < 0) {
74                     break; // Only take most recent progress (e.g. after pause or progress reset)
75                 }
76                 sum += stats.speed;
77             }
79             return sum / statsHistory.length;
80         };
82         const getStats = (transferId: string): TransferStats => ({
83             progress: statsHistory[0]?.stats[transferId]?.progress || 0,
84             averageSpeed: calculateAverageSpeed(transferId),
85         });
87         return Object.fromEntries(Object.keys(getTransferProgresses()).map((id) => [id, getStats(id)]));
88     }, [statsHistory]);
91 function useStatsHistory(transfers: Transfer[], getTransferProgresses: () => TransferProgresses) {
92     const [statsHistory, setStatsHistory] = useState<TransfersHistoryStats[]>([]);
94     const getTransfer = useCallback((id: string) => transfers.find((transfer) => transfer.id === id), [transfers]);
96     const updateStats = () => {
97         const timestamp = new Date();
98         const transferProgresses = getTransferProgresses();
100         setStatsHistory((prev) => {
101             const lastStats = (id: string) => prev[0]?.stats[id] || {};
103             // With a lot of uploads the interval is not called in precise
104             // time and to compute correct speed we need to have accurate
105             // difference from the last check.
106             const lastTimestamp = prev[0]?.timestamp || new Date();
107             const intervalSinceLastCheck = timestamp.getTime() - lastTimestamp.getTime();
109             const stats = Object.entries(transferProgresses).reduce(
110                 (stats, [id, progress]) => ({
111                     ...stats,
112                     [id]: {
113                         // get speed snapshot based on bytes uploaded/downloaded since last update
114                         speed: lastStats(id).active
115                             ? (transferProgresses[id] - lastStats(id).progress) * (1000 / intervalSinceLastCheck)
116                             : 0,
117                         active: getTransfer(id)?.state === TransferState.Progress,
118                         progress,
119                     },
120                 }),
121                 {} as { [id: string]: TransferHistoryStats }
122             );
124             return [{ stats, timestamp }, ...prev.slice(0, SPEED_SNAPSHOTS - 1)];
125         });
126     };
128     const runUpdateStatsJob = useRunPeriodicJobOnce(updateStats, PROGRESS_UPDATE_INTERVAL);
130     useEffect(() => {
131         const transfersInProgress = transfers.filter(isTransferProgress);
132         if (!transfersInProgress.length) {
133             return;
134         }
136         const stop = runUpdateStatsJob();
137         return () => {
138             // When transfer is paused, progress is updated a bit later.
139             // Therefore we need to update stats even few ms after nothing
140             // is in progress.
141             setTimeout(stop, PROGRESS_UPDATE_INTERVAL);
142         };
143     }, [transfers]);
145     return statsHistory;
148 function useRunPeriodicJobOnce(job: () => void, interval: number): () => () => void {
149     // The job will be running until there is any caller requesting it.
150     const numOfCallers = useRef(0);
152     // Make reference to latest callback to always call the newest one.
153     // Probably cleaner solution is to clear the timer and start the new
154     // one with new callback, but that is harder to guarantee it will be
155     // called in specified interval.
156     const jobRef = useRef(job);
157     jobRef.current = job;
159     return () => {
160         numOfCallers.current++;
161         if (numOfCallers.current === 1) {
162             const timer = setInterval(() => {
163                 if (numOfCallers.current === 0) {
164                     clearInterval(timer);
165                     return;
166                 }
167                 jobRef.current();
168             }, interval);
169             jobRef.current();
170         }
171         return () => {
172             numOfCallers.current--;
173         };
174     };