diff --git a/packages/react-hooks/src/useCall.ts b/packages/react-hooks/src/useCall.ts index 8cc68640dd..1563c54969 100644 --- a/packages/react-hooks/src/useCall.ts +++ b/packages/react-hooks/src/useCall.ts @@ -9,7 +9,7 @@ import type { MountedRef } from './useIsMountedRef'; import { useEffect, useRef, useState } from 'react'; -import { isNull, isUndefined } from '@polkadot/util'; +import { isFunction, isNull, isUndefined } from '@polkadot/util'; import { useIsMountedRef } from './useIsMountedRef'; @@ -45,10 +45,12 @@ type CallFn = (...params: unknown[]) => Promise; export type TrackFn = PromiseResult | QueryFn; export interface Tracker { + error: Error | null; fn: TrackFn | undefined | null | false; isActive: boolean; serialized: string | null; subscriber: TrackFnResult | null; + type: 'useCall' | 'useCallMulti'; } interface TrackerRef { @@ -64,6 +66,10 @@ function isMapFn (fn: unknown): fn is QueryMapFn { return !!(fn as QueryTrackFn).meta?.type?.isMap; } +function isQuery (fn: unknown): fn is QueryableStorageEntry<'promise', []> { + return !!fn && !isUndefined((fn as QueryableStorageEntry<'promise', []>).creator); +} + // extract the serialized and mapped params, all ready for use in our call function extractParams (fn: unknown, params: unknown[], { paramMap = transformIdentity }: CallOptions = {}): [string, CallParams | null] { return [ @@ -74,20 +80,28 @@ function extractParams (fn: unknown, params: unknown[], { paramMap = transfo ]; } +export function handleError (error: Error, tracker: TrackerRef, fn?: unknown): void { + console.error( + tracker.current.error = new Error(`${tracker.current.type}(${ + isQuery(fn) + ? `${fn.creator.section}.${fn.creator.method}` + : '...' + }):: ${error.message}:: ${error.stack || ''}`) + ); +} + // unsubscribe and remove from the tracker export function unsubscribe (tracker: TrackerRef): void { tracker.current.isActive = false; if (tracker.current.subscriber) { - tracker.current.subscriber.then((u) => (u as VoidFn)()).catch(console.error); + tracker.current.subscriber + .then((u) => isFunction(u) && (u as VoidFn)()) + .catch((e) => handleError(e as Error, tracker)); tracker.current.subscriber = null; } } -function isQuery (fn: TrackFn): fn is QueryableStorageEntry<'promise', []> { - return !isUndefined((fn as QueryableStorageEntry<'promise', []>).creator); -} - // subscribe, trying to play nice with the browser threads function subscribe (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: any) => void, { transform = transformIdentity, withParams, withParamsTransform }: CallOptions = {}): void { const validParams = params.filter((p) => !isUndefined(p)); @@ -117,16 +131,10 @@ function subscribe (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn : transform(value) ); } catch (error) { - let extra = '...'; - - if (isQuery(fn)) { - extra = `${fn.creator.section}.${fn.creator.method}`; - } - - throw new Error(`useCall(${extra}):: ${(error as Error).message}:: ${(error as Error).stack || ''}`); + handleError(error as Error, tracker, fn); } } - }); + }).catch((error) => handleError(error as Error, tracker, fn)); } else { tracker.current.subscriber = null; } @@ -134,6 +142,16 @@ function subscribe (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn }, 0); } +export function throwOnError (tracker: Tracker): void { + if (tracker.error) { + const error = tracker.error; + + tracker.error = null; + + throw error; + } +} + // tracks a stream, typically an api.* call (derive, rpc, query) that // - returns a promise with an unsubscribe function // - has a callback to set the value @@ -141,7 +159,7 @@ function subscribe (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn // FIXME This is generic, we cannot really use createNamedHook export function useCall (fn: TrackFn | undefined | null | false, params?: CallParams | null, options?: CallOptions): T | undefined { const mountedRef = useIsMountedRef(); - const tracker = useRef({ fn: null, isActive: false, serialized: null, subscriber: null }); + const tracker = useRef({ error: null, fn: null, isActive: false, serialized: null, subscriber: null, type: 'useCall' }); const [value, setValue] = useState((options || {}).defaultValue); // initial effect, we need an un-subscription @@ -164,5 +182,7 @@ export function useCall (fn: TrackFn | undefined | null | false, params?: Ca } }, [fn, options, mountedRef, params]); + // throwOnError(tracker.current); + return value; } diff --git a/packages/react-hooks/src/useCallMulti.ts b/packages/react-hooks/src/useCallMulti.ts index adefd4826d..052f1ea274 100644 --- a/packages/react-hooks/src/useCallMulti.ts +++ b/packages/react-hooks/src/useCallMulti.ts @@ -9,7 +9,7 @@ import type { MountedRef } from './useIsMountedRef'; import { useEffect, useRef, useState } from 'react'; import { useApi } from './useApi'; -import { transformIdentity, unsubscribe } from './useCall'; +import { handleError, transformIdentity, unsubscribe } from './useCall'; import { useIsMountedRef } from './useIsMountedRef'; interface TrackerRef { @@ -33,7 +33,6 @@ function subscribe (api: ApiPromise, mountedRef: MountedRef, tracker: Tracke if (filtered.length) { // swap to active mode tracker.current.isActive = true; - tracker.current.subscriber = api.queryMulti(filtered, (value): void => { // we use the isActive flag here since .subscriber may not be set on immediate callback) if (mountedRef.current && tracker.current.isActive) { @@ -51,11 +50,11 @@ function subscribe (api: ApiPromise, mountedRef: MountedRef, tracker: Tracke ) ); } catch (error) { - throw new Error(`useCallMulti(...):: ${(error as Error).message}:: ${(error as Error).stack || ''}`); + handleError(error as Error, tracker); } } } - }); + }).catch((error) => handleError(error as Error, tracker)); } else { tracker.current.subscriber = null; } @@ -68,7 +67,7 @@ function subscribe (api: ApiPromise, mountedRef: MountedRef, tracker: Tracke export function useCallMulti (calls?: QueryableStorageMultiArg<'promise'>[] | null | false, options?: CallOptions): T { const { api } = useApi(); const mountedRef = useIsMountedRef(); - const tracker = useRef({ fn: null, isActive: false, serialized: null, subscriber: null }); + const tracker = useRef({ error: null, fn: null, isActive: false, serialized: null, subscriber: null, type: 'useCallMulti' }); const [value, setValue] = useState(() => (options || {}).defaultValue || [] as unknown as T); // initial effect, we need an un-subscription @@ -90,5 +89,7 @@ export function useCallMulti (calls?: QueryableStorageMultiArg<'promise'>[] } }, [api, calls, options, mountedRef]); + // throwOnError(tracker.current); + return value; }