From 0f27df20260e252030f979ca4556bd02b155f24f Mon Sep 17 00:00:00 2001 From: Josh Field Date: Thu, 12 Oct 2023 12:01:32 +1100 Subject: [PATCH] Refactor error boundary (#9040) * Refactor error boundary * add debug menu system errors and reactor error boundaries * license --------- Co-authored-by: Gheric Speiginer --- .../src/common/components/ErrorBoundary.tsx | 36 +---------- .../src/components/Debug/index.tsx | 12 ++-- .../common/src/utils/createErrorBoundary.ts | 60 +++++++++++++++++++ .../hyperflux/functions/ReactorFunctions.tsx | 23 ++++++- packages/ui/package.json | 2 +- 5 files changed, 92 insertions(+), 41 deletions(-) create mode 100644 packages/common/src/utils/createErrorBoundary.ts diff --git a/packages/client-core/src/common/components/ErrorBoundary.tsx b/packages/client-core/src/common/components/ErrorBoundary.tsx index 3c6e5cd985..755010b0ed 100644 --- a/packages/client-core/src/common/components/ErrorBoundary.tsx +++ b/packages/client-core/src/common/components/ErrorBoundary.tsx @@ -25,41 +25,9 @@ Ethereal Engine. All Rights Reserved. import React from 'react' -type Props = { - children: React.ReactNode -} +import { createErrorBoundary } from '@etherealengine/common/src/utils/createErrorBoundary' -type ErrorHandler = (error: Error, info: React.ErrorInfo) => void -type ErrorHandlingComponent = (props: Props, error?: Error) => React.ReactNode - -type ErrorState = { error?: Error } - -function Catch( - component: ErrorHandlingComponent, - errorHandler?: ErrorHandler -): React.ComponentType { - return class extends React.Component { - state: ErrorState = { - error: undefined - } - - static getDerivedStateFromError(error: Error) { - return { error } - } - - componentDidCatch(error: Error, info: React.ErrorInfo) { - if (errorHandler) { - errorHandler(error, info) - } - } - - render() { - return component(this.props, this.state.error) - } - } -} - -const ErrorBoundary = Catch(function error(props: Props, error?: Error) { +const ErrorBoundary = createErrorBoundary(function error(props, error?: Error) { if (error) { return (
diff --git a/packages/client-core/src/components/Debug/index.tsx b/packages/client-core/src/components/Debug/index.tsx index 76d8e8bed9..d6cfa5b042 100755 --- a/packages/client-core/src/components/Debug/index.tsx +++ b/packages/client-core/src/components/Debug/index.tsx @@ -299,14 +299,13 @@ export const Debug = ({ showingStateRef }: { showingStateRef: React.MutableRefOb data={dag} labelRenderer={(raw, ...keyPath) => { const label = raw[0] - if (label === 'preSystems') return {t('common:debug.preSystems')} - if (label === 'simulation') return {t('common:debug.simulation')} - if (label === 'subSystems') return {t('common:debug.subSystems')} - if (label === 'postSystems') return {t('common:debug.postSystems')} + if (label === 'preSystems' || label === 'simulation' || label === 'subSystems' || label === 'postSystems') + return {t(`common:debug.${label}`)} return {label} }} valueRenderer={(raw, value, ...keyPath) => { const system = SystemDefinitions.get((keyPath[0] === 'enabled' ? keyPath[1] : keyPath[0]) as SystemUUID)! + const systemReactor = system ? Engine.instance.activeSystemReactors.get(system.uuid) : undefined return ( <> + {systemReactor?.error && ( + + {systemReactor.error.name}: {systemReactor.error.message} + + )} ) }} diff --git a/packages/common/src/utils/createErrorBoundary.ts b/packages/common/src/utils/createErrorBoundary.ts new file mode 100644 index 0000000000..66410e682c --- /dev/null +++ b/packages/common/src/utils/createErrorBoundary.ts @@ -0,0 +1,60 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import React from 'react' + +type Props = { + children: React.ReactNode +} + +type ErrorHandler = (error: Error, info: React.ErrorInfo) => void +type ErrorHandlingComponent = (props: Props, error?: Error) => React.ReactNode + +type ErrorState = { error?: Error } + +export function createErrorBoundary

( + component: ErrorHandlingComponent

, + errorHandler?: ErrorHandler +): React.ComponentType

{ + return class extends React.Component { + state: ErrorState = { + error: undefined + } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + if (errorHandler) { + errorHandler(error, info) + } + } + + render() { + return component(this.props, this.state.error) + } + } +} diff --git a/packages/hyperflux/functions/ReactorFunctions.tsx b/packages/hyperflux/functions/ReactorFunctions.tsx index 1e8cbeb430..2d393b9390 100644 --- a/packages/hyperflux/functions/ReactorFunctions.tsx +++ b/packages/hyperflux/functions/ReactorFunctions.tsx @@ -29,6 +29,8 @@ import { ConcurrentRoot, DefaultEventPriority } from 'react-reconciler/constants import { isDev } from '@etherealengine/common/src/config' +import { createErrorBoundary } from '@etherealengine/common/src/utils/createErrorBoundary' + import { HyperFlux } from './StoreFunctions' const ReactorReconciler = Reconciler({ @@ -75,9 +77,11 @@ ReactorReconciler.injectIntoDevTools({ version: '18.2.0' }) -export interface ReactorRoot { +export type ReactorRoot = { fiber: any isRunning: boolean + Reactor: React.FC + error: Error | null promise: Promise cleanupFunctions: Set<() => void> run: (force?: boolean) => Promise @@ -90,6 +94,18 @@ export function useReactorRootContext(): ReactorRoot { return React.useContext(ReactorRootContext) } +export const ReactorErrorBoundary = createErrorBoundary<{ children: React.ReactNode; reactorRoot: ReactorRoot }>( + function error(props, error?: Error) { + if (error) { + props.reactorRoot.error = error + props.reactorRoot.stop() + return null + } else { + return {props.children} + } + } +) + export function startReactor(Reactor: React.FC): ReactorRoot { const isStrictMode = false const concurrentUpdatesByDefaultOverride = true @@ -113,6 +129,7 @@ export function startReactor(Reactor: React.FC): ReactorRoot { fiber: fiberRoot, isRunning: false, Reactor, + error: null as Error | null, promise: null! as Promise, run() { if (reactorRoot.isRunning) return Promise.resolve() @@ -121,7 +138,9 @@ export function startReactor(Reactor: React.FC): ReactorRoot { HyperFlux.store.activeReactors.add(reactorRoot) ReactorReconciler.updateContainer( - + + + , fiberRoot, null, diff --git a/packages/ui/package.json b/packages/ui/package.json index 3a3df83156..b46c03b0d1 100755 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -96,7 +96,7 @@ "jest-scss-transform": "^1.0.3", "path-browserify": "^1.0.1", "postcss": "^8.4.23", - "react": "^18.2.0", + "react": "18.2.0", "react-dom": "18.2.0", "sass": "1.59.3", "storybook": "^7.1.0-alpha.37",