From fb9404fd44c6e3511506a71e20187cd446216692 Mon Sep 17 00:00:00 2001 From: turbocrime Date: Thu, 11 Jul 2024 22:33:28 -0700 Subject: [PATCH] use listener --- .../src/components/penumbra-provider.tsx | 201 +++++++++++------- 1 file changed, 119 insertions(+), 82 deletions(-) diff --git a/packages/react/src/components/penumbra-provider.tsx b/packages/react/src/components/penumbra-provider.tsx index b549a454a3..9839ba6b8e 100644 --- a/packages/react/src/components/penumbra-provider.tsx +++ b/packages/react/src/components/penumbra-provider.tsx @@ -1,8 +1,12 @@ -import { PenumbraInjection, PenumbraInjectionState } from '@penumbra-zone/client'; +import { + isPenumbraInjectionStateEvent, + PenumbraInjection, + PenumbraInjectionState, +} from '@penumbra-zone/client'; import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; -import { PenumbraManifest } from '../manifest'; -import { PenumbraContext, penumbraContext } from '../penumbra-context'; -import { assertManifestOrigin, injectionOfKey, keyOfInjection } from '../util'; +import { PenumbraManifest } from '../manifest.js'; +import { PenumbraContext, penumbraContext } from '../penumbra-context.js'; +import { assertManifestOrigin, injectionOfKey, keyOfInjection } from '../util.js'; type PenumbraProviderProps = { children?: ReactNode; @@ -22,82 +26,67 @@ export const PenumbraProvider = ({ const [providerState, setProviderState] = useState(providerInjection?.state()); const [providerConnected, setProviderConnected] = useState(providerInjection?.isConnected()); - const updateProviderState = useCallback(() => { - // skip uninitialized state - if ( - providerState === undefined && - providerConnected === undefined && - providerInjection === undefined - ) - return; - - // skip final states - if ( - providerConnected === false && - (providerState === PenumbraInjectionState.Failed || - providerState === PenumbraInjectionState.Disconnected) - ) - return; - - setProviderState(providerInjection?.state()); - setProviderConnected(providerInjection?.isConnected()); - }, [providerInjection, providerState, providerConnected, setProviderState, setProviderConnected]); const [failure, setFailureError] = useState(); const setFailureUnknown = useCallback( - (cause: unknown) => { - if (failure) - console.error('Not replacing existing PenumbraProvider failure', { failure, cause }); - else - setFailureError(cause instanceof Error ? cause : new Error('Unknown failure', { cause })); - }, + (cause: unknown) => + failure + ? console.error('Not replacing existing PenumbraProvider failure', { failure, cause }) + : setFailureError(cause instanceof Error ? cause : new Error('Unknown failure', { cause })), [failure, setFailureError], ); const [providerPort, setProviderPort] = useState(); const [manifest, setManifest] = useState(); - const createdContext: PenumbraContext = useMemo( - () => ({ - failure, - manifest, - origin: providerOrigin, + // force destruction of provider on failure + useEffect(() => { + if (failure) { + setProviderState(PenumbraInjectionState.Failed); + setProviderConnected(false); + setProviderPort(undefined); + } + }, [failure]); - // require manifest to forward state - state: manifest && providerState, + // attach state event listener + useEffect(() => { + // require manifest, no failures + if (!manifest || failure) { + return; + } - // require manifest and no failures to forward injected methods - ...(manifest && !failure - ? { - port: providerConnected && providerPort, - connect: providerInjection?.connect, - request: providerInjection?.request, - disconnect: providerInjection?.disconnect, - } - : {}), - }), - [ - failure, - manifest, - providerPort, - providerInjection?.connect, - providerInjection?.connect, - providerInjection?.disconnect, - providerOrigin, - providerState, - ], - ); + const listener = (evt: Event) => { + if (isPenumbraInjectionStateEvent(evt)) { + if (!providerInjection) { + setFailureError(new Error('State change event without injection')); + } else if (evt.detail.origin !== providerOrigin) { + setFailureError(new Error('State change from unexpected origin')); + } else if (evt.detail.state !== providerInjection.state()) { + console.warn('State change not verifiable'); + } else { + setProviderState(providerInjection.state()); + setProviderConnected(providerInjection.isConnected()); + } + } + }; - useEffect(() => updateProviderState()); + const ac = new AbortController(); + providerInjection?.addEventListener('penumbrastate', listener, { + signal: ac.signal, + }); + return () => ac.abort(); + }, [providerInjection, providerInjection?.addEventListener, manifest, failure]); // fetch manifest to confirm presence of provider useEffect(() => { // require provider - if (!providerOrigin || !providerInjection) return; - // don't repeat - if (manifest) return; - // unnecessary if failed - if (failure) return; + if (!providerOrigin || !providerInjection) { + return; + } + // don't repeat, unnecessary if failed + if (!!manifest || failure) { + return; + } // sync assertion try { @@ -107,34 +96,46 @@ export const PenumbraProvider = ({ return; } - // async fetch + // abortable fetch const ac = new AbortController(); - void fetch(providerInjection.manifest, { signal: ac.signal }) - .then( - async res => { + const fetchManifest = fetch(providerInjection.manifest, { signal: ac.signal }).catch( + (noAbortError: unknown) => { + // abort is not a failure + if (noAbortError instanceof Error && noAbortError.name === 'AbortError') { + return; + } else { + throw noAbortError; + } + }, + ); + + // async handle response + void fetchManifest + .then(async res => { + const manifestJson: unknown = await res?.json(); + if (manifestJson) { // this cast is fairly safe coming from an extension manifest, where // schema is enforced by chrome store. - const manifestJson = (await res.json()) as PenumbraManifest; - setManifest(manifestJson); - }, - (noAbortError: unknown) => { - // abort is not a failure - if (noAbortError instanceof Error && noAbortError.name === 'AbortError') return; - else throw noAbortError; - }, - ) + setManifest(manifestJson as PenumbraManifest); + } + }) .catch(setFailureUnknown); - // useEffect cleanup return () => ac.abort(); }, [providerOrigin, providerInjection, manifest, setManifest]); // request effect useEffect(() => { - if (!manifest || failure) return; + // require manifest, no failures + if (!manifest || failure) { + return; + } + switch (providerState) { case PenumbraInjectionState.Present: - if (makeApprovalRequest) void providerInjection?.request().catch(setFailureUnknown); + if (makeApprovalRequest) { + void providerInjection?.request().catch(setFailureUnknown); + } break; default: break; @@ -143,14 +144,19 @@ export const PenumbraProvider = ({ // connect effect useEffect(() => { - if (!manifest || failure) return; + // require manifest, no failures + if (!manifest || failure) { + return; + } + switch (providerState) { case PenumbraInjectionState.Present: - if (!makeApprovalRequest) + if (!makeApprovalRequest) { void providerInjection ?.connect() .then(p => setProviderPort(p)) .catch(setFailureUnknown); + } break; case PenumbraInjectionState.Requested: void providerInjection @@ -163,5 +169,36 @@ export const PenumbraProvider = ({ } }, [makeApprovalRequest, providerState, providerInjection?.connect, manifest, failure]); + const createdContext: PenumbraContext = useMemo( + () => ({ + failure, + manifest, + origin: providerOrigin, + + // require manifest to forward state + state: manifest && providerState, + + // require manifest and no failures to forward injected methods + ...(manifest && !failure + ? { + port: providerConnected && providerPort, + connect: providerInjection?.connect, + request: providerInjection?.request, + disconnect: providerInjection?.disconnect, + } + : {}), + }), + [ + failure, + manifest, + providerPort, + providerInjection?.connect, + providerInjection?.connect, + providerInjection?.disconnect, + providerOrigin, + providerState, + ], + ); + return {children}; };