1 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
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
20 * useTransfersView provides data for transfer manager.
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(() => {
30 total: transfers.filter(isTransferError).length,
31 downloads: downloads.filter(isTransferError).length,
32 uploads: uploads.filter(isTransferError).length,
34 }, [transfers, downloads, uploads]);
36 const getTransferProgresses = useCallback(() => {
38 ...getDownloadsProgresses(),
39 ...getUploadsProgresses(),
41 }, [getDownloadsProgresses, getUploadsProgresses]);
42 const stats = useStats(transfers, getTransferProgresses);
44 const clearAllTransfers = useCallback(() => {
47 }, [clearDownloads, clearUploads]);
53 numberOfFailedTransfer,
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) {
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)
79 return sum / statsHistory.length;
82 const getStats = (transferId: string): TransferStats => ({
83 progress: statsHistory[0]?.stats[transferId]?.progress || 0,
84 averageSpeed: calculateAverageSpeed(transferId),
87 return Object.fromEntries(Object.keys(getTransferProgresses()).map((id) => [id, getStats(id)]));
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]) => ({
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)
117 active: getTransfer(id)?.state === TransferState.Progress,
121 {} as { [id: string]: TransferHistoryStats }
124 return [{ stats, timestamp }, ...prev.slice(0, SPEED_SNAPSHOTS - 1)];
128 const runUpdateStatsJob = useRunPeriodicJobOnce(updateStats, PROGRESS_UPDATE_INTERVAL);
131 const transfersInProgress = transfers.filter(isTransferProgress);
132 if (!transfersInProgress.length) {
136 const stop = runUpdateStatsJob();
138 // When transfer is paused, progress is updated a bit later.
139 // Therefore we need to update stats even few ms after nothing
141 setTimeout(stop, PROGRESS_UPDATE_INTERVAL);
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;
160 numOfCallers.current++;
161 if (numOfCallers.current === 1) {
162 const timer = setInterval(() => {
163 if (numOfCallers.current === 0) {
164 clearInterval(timer);
172 numOfCallers.current--;