diff --git a/public/locales/en/explore.json b/public/locales/en/explore.json index 4c7ff5e8..82a61c7d 100644 --- a/public/locales/en/explore.json +++ b/public/locales/en/explore.json @@ -66,5 +66,8 @@ "paragraph1": "A visual representation of the node and its children." } } + }, + "errors": { + "BlockFetchTimeoutError": "Failed to fetch content in {timeout}s. Please refresh the page to retry or try a different CID." } } diff --git a/src/bundles/explore.js b/src/bundles/explore.js index ad5aee05..34bd4b40 100644 --- a/src/bundles/explore.js +++ b/src/bundles/explore.js @@ -52,7 +52,7 @@ const makeBundle = () => { } } catch (error) { console.warn('Failed to resolve path', path, error) - return { path, error: error.toString() } + return { path, error } } }, staleAfter: Infinity, diff --git a/src/components/ExplorePage.jsx b/src/components/ExplorePage.jsx index 1a70d709..fecd12e2 100644 --- a/src/components/ExplorePage.jsx +++ b/src/components/ExplorePage.jsx @@ -6,6 +6,7 @@ import { connect } from 'redux-bundler-react' import CidInfo from './cid-info/CidInfo' import ErrorBoundary from './error/ErrorBoundary' +import { IpldExploreErrorComponent } from './explore/IpldExploreErrorComponent' import IpldGraph from './graph/LoadableIpldGraph' import GraphCrumb from './graph-crumb/GraphCrumb' import ComponentLoader from './loader/ComponentLoader' @@ -51,13 +52,7 @@ export class ExplorePage extends React.Component {
- {error - ? ( -
- {error} -
- ) - : null} + {targetNode ? ( diff --git a/src/components/explore/IpldExploreErrorComponent.tsx b/src/components/explore/IpldExploreErrorComponent.tsx new file mode 100644 index 00000000..341a9fc7 --- /dev/null +++ b/src/components/explore/IpldExploreErrorComponent.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' + +import type IpldExploreError from '../../lib/errors' + +export interface IpldExploreErrorComponentProps { + error?: IpldExploreError +} + +export function IpldExploreErrorComponent ({ error }: IpldExploreErrorComponentProps): JSX.Element | null { + const { t } = useTranslation('explore', { keyPrefix: 'errors' }) + if (error == null) return null + + return ( +
+
{error.toString(t)}
+
+ ) +} diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 00000000..785a6089 --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,24 @@ +import { type TFunction } from 'i18next' + +export default class IpldExploreError extends Error { + constructor (private readonly options: Record) { + super() + this.name = this.constructor.name + } + + /** + * See IpldExploreError for usage. + * You must pass a t function that is registered with namespace 'explore' and keyPrefix 'errors'. + * + * @param t - the i18next-react t function + * @returns the translated string + * @example + * const {t} = useTranslation('explore', { keyPrefix: 'errors' }) + * t('NameOfErrorClassThatExtendsIpldExploreError') + */ + toString (t: TFunction<'translation', undefined, 'translation'>): string { + return t(this.name, this.options) + } +} + +export class BlockFetchTimeoutError extends IpldExploreError {} diff --git a/src/lib/get-raw-block.ts b/src/lib/get-raw-block.ts index 128cd082..a6a6c053 100644 --- a/src/lib/get-raw-block.ts +++ b/src/lib/get-raw-block.ts @@ -3,6 +3,7 @@ import { type IPFSHTTPClient } from 'kubo-rpc-client' import { CID } from 'multiformats' import type { Version as CIDVersion } from 'multiformats/cid' +import { BlockFetchTimeoutError } from './errors' import getHasherForCode from './hash-importer.js' async function getCidFromBytes (bytes: Uint8Array, cidVersion: CIDVersion, codecCode: number, multihashCode: number): Promise { @@ -130,6 +131,10 @@ export async function getRawBlock (helia: Helia, kuboClient: IPFSHTTPClient, cid return rawBlock } catch (err) { console.error('unable to get raw block', err) + if (abortController.signal.aborted) { + // if we timed out, we want to throw a timeout error, not a Promise.any error + throw new BlockFetchTimeoutError({ timeout: timeout / 1000 }) + } throw err } }