diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index bb3aee80..8aabe0aa 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -6,16 +6,26 @@ import React, { ReactElement } from 'react' interface Props { linearProgressProps?: LinearProgressProps value: number + indeterminate?: boolean + label?: string } -export function LinearProgressWithLabel(props: Props): ReactElement { +export function LinearProgressWithLabel({ indeterminate, value, linearProgressProps, label }: Props): ReactElement { + const displayLabel = label ?? (indeterminate ? 'Syncing...' : `${Math.round(value)}%`) + return ( - + - {`${Math.round(props.value)}%`} + + {displayLabel} + ) diff --git a/src/pages/files/AssetSyncing.tsx b/src/pages/files/AssetSyncing.tsx index 65129695..31457382 100644 --- a/src/pages/files/AssetSyncing.tsx +++ b/src/pages/files/AssetSyncing.tsx @@ -1,6 +1,5 @@ -import { Tag } from '@ethersphere/bee-js' import { Box } from '@mui/material' -import { ReactElement, useCallback, useContext, useEffect, useRef, useState } from 'react' +import { ReactElement, useContext, useEffect, useState } from 'react' import { DocumentationText } from '../../components/DocumentationText' import { LinearProgressWithLabel } from '../../components/ProgressBar' @@ -10,88 +9,66 @@ interface Props { reference?: string } -const SYNC_CHECK_INTERVAL_MS = 2000 +const PROBE_RETRY_DELAY_MS = 500 +const PROBE_TIMEOUT_MS = 10 * 1000 export function AssetSyncing({ reference }: Props): ReactElement { const { beeApi } = useContext(SettingsContext) - const syncTimer = useRef | null>(null) - const retrieveCheckRef = useRef(false) - const [isRetrieveChecking, setIsRetrieveChecking] = useState(false) const [syncProgress, setSyncProgress] = useState(0) + const [probeFailed, setProbeFailed] = useState(false) + + useEffect(() => { + setSyncProgress(0) + setProbeFailed(false) - const syncCheck = useCallback(async () => { if (!beeApi || !reference) return - let allTags: Tag[] = [] - let offset = 0 - const limit = 1000 - let tagsBatch: Tag[] - - do { - tagsBatch = await beeApi.getAllTags({ limit, offset }) - allTags = allTags.concat(tagsBatch) - offset += limit - } while (tagsBatch.length === limit) // Continue if the batch is full, stop if fewer than the limit - - const tag = allTags.find(t => t.address === reference) - - if (tag && tag.split > 0) { - const progress = ((tag.seen + tag.synced) / tag.split) * 100 - setSyncProgress(progress) - } else if (!tag && !retrieveCheckRef.current) { - // Direct (non-deferred) uploads do not create a tag on the Bee node, - // so verify network availability with the stewardship endpoint instead - retrieveCheckRef.current = true + let isMounted = true + let retryTimer: ReturnType | null = null + let currentAbortController: AbortController | null = null + + // deferred: false already guarantees the upload was pushed to and acknowledged by the + // network before the upload call resolved. This is just a cheap local sanity check + // (HEAD on the root chunk, served from local storage) - not a network-wide verification. + const check = async (isRetry: boolean) => { + // fetch() ignores requestOptions.timeout, so bound the request explicitly - otherwise a + // hung request never resolves or rejects, and neither the retry nor the failure state + // would ever trigger. + const abortController = new AbortController() + currentAbortController = abortController + const abortTimer = setTimeout(() => abortController.abort(), PROBE_TIMEOUT_MS) + try { - if (await beeApi.isReferenceRetrievable(reference)) { - setSyncProgress(100) - } + await beeApi.probeData(reference, { signal: abortController.signal }) + + if (isMounted) setSyncProgress(100) } catch { - // Transient error, the next interval tick will retry + // Bail out entirely once unmounted/reference changed, so a rejection from an + // in-flight first attempt can't schedule an unnecessary extra retry request. + if (!isMounted) return + + if (!isRetry) { + retryTimer = setTimeout(() => check(true), PROBE_RETRY_DELAY_MS) + } else { + setProbeFailed(true) + } } finally { - retrieveCheckRef.current = false + clearTimeout(abortTimer) } } - }, [beeApi, reference]) - useEffect(() => { - syncTimer.current = setInterval(syncCheck, SYNC_CHECK_INTERVAL_MS) + check(false) return () => { - if (syncTimer.current) { - clearInterval(syncTimer.current) - syncTimer.current = null - } - } - }, [reference, syncCheck]) + isMounted = false + currentAbortController?.abort() - useEffect(() => { - if (syncProgress === 100 && syncTimer.current) { - clearInterval(syncTimer.current) - syncTimer.current = null - } - }, [syncProgress]) - - useEffect(() => { - /* - There are instances when it seems that the content isn't synchronized, despite being already available. - To ensure it's not due to invalid synchronization data, - verify availability from at least 70% using one of the stewardship endpoints. - */ - if (beeApi && reference && !isRetrieveChecking && syncProgress > 10 && syncProgress < 100) { - // It's a long running task make sure only one run occurs at a time. - setIsRetrieveChecking(true) - - beeApi.isReferenceRetrievable(reference).then(isRetriavable => { - if (isRetriavable) { - setSyncProgress(100) - } - - setIsRetrieveChecking(false) - }) + if (retryTimer) { + clearTimeout(retryTimer) + } } - }, [syncProgress, isRetrieveChecking, beeApi, reference]) + }, [beeApi, reference]) return ( <> @@ -104,8 +81,20 @@ export function AssetSyncing({ reference }: Props): ReactElement { - + + {probeFailed && ( + + + Upload succeeded, but we couldn't confirm it locally. Try refreshing this page — your file has very + likely still been uploaded successfully. + + + )} ) }