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.
+
+
+ )}
>
)
}