feat: Re-throw error on JS side instead of just logging on native side (#2366)

* feat: Re-throw error on JS side instead of just logging on native side

* fix: Fix proxy

* fix: Fix app crash by only logging error

* fix: Use `global.ErrorUtils` (from reanimated)
This commit is contained in:
Marc Rousavy 2024-01-11 17:23:38 +01:00 committed by GitHub
parent eb14aa1402
commit 34c5b11927
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 30 deletions

View File

@ -36,22 +36,12 @@ void JFrameProcessor::callWithFrameHostObject(const std::shared_ptr<FrameHostObj
// Call the Frame Processor on the Worklet Runtime // Call the Frame Processor on the Worklet Runtime
jsi::Runtime& runtime = _workletContext->getWorkletRuntime(); jsi::Runtime& runtime = _workletContext->getWorkletRuntime();
try { // Wrap HostObject as JSI Value
// Wrap HostObject as JSI Value auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject);
auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); jsi::Value jsValue(std::move(argument));
jsi::Value jsValue(std::move(argument));
// Call the Worklet with the Frame JS Host Object as an argument // Call the Worklet with the Frame JS Host Object as an argument
_workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); _workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1);
} catch (jsi::JSError& jsError) {
// JS Error occured, print it to console.
const std::string& message = jsError.getMessage();
_workletContext->invokeOnJsThread([message](jsi::Runtime& jsRuntime) {
auto logFn = jsRuntime.global().getPropertyAsObject(jsRuntime, "console").getPropertyAsFunction(jsRuntime, "error");
logFn.call(jsRuntime, jsi::String::createFromUtf8(jsRuntime, "Frame Processor threw an error: " + message));
});
}
} }
void JFrameProcessor::call(jni::alias_ref<JFrame::javaobject> frame) { void JFrameProcessor::call(jni::alias_ref<JFrame::javaobject> frame) {

View File

@ -34,22 +34,12 @@ using namespace facebook;
// Call the Frame Processor on the Worklet Runtime // Call the Frame Processor on the Worklet Runtime
jsi::Runtime& runtime = _workletContext->getWorkletRuntime(); jsi::Runtime& runtime = _workletContext->getWorkletRuntime();
try { // Wrap HostObject as JSI Value
// Wrap HostObject as JSI Value auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject);
auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); jsi::Value jsValue(std::move(argument));
jsi::Value jsValue(std::move(argument));
// Call the Worklet with the Frame JS Host Object as an argument // Call the Worklet with the Frame JS Host Object as an argument
_workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); _workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1);
} catch (jsi::JSError& jsError) {
// JS Error occured, print it to console.
auto message = jsError.getMessage();
_workletContext->invokeOnJsThread([message](jsi::Runtime& jsRuntime) {
auto logFn = jsRuntime.global().getPropertyAsObject(jsRuntime, "console").getPropertyAsFunction(jsRuntime, "error");
logFn.call(jsRuntime, jsi::String::createFromUtf8(jsRuntime, "Frame Processor threw an error: " + message));
});
}
} }
- (void)call:(Frame* _Nonnull)frame { - (void)call:(Frame* _Nonnull)frame {

View File

@ -35,6 +35,10 @@ interface TVisionCameraProxy {
* ``` * ```
*/ */
initFrameProcessorPlugin: (name: string, options?: Record<string, ParameterType>) => FrameProcessorPlugin | undefined initFrameProcessorPlugin: (name: string, options?: Record<string, ParameterType>) => FrameProcessorPlugin | undefined
/**
* Throws the given error.
*/
throwJSError: (error: unknown) => void
} }
const errorMessage = 'Frame Processors are not available, react-native-worklets-core is not installed!' const errorMessage = 'Frame Processors are not available, react-native-worklets-core is not installed!'
@ -44,6 +48,9 @@ let isAsyncContextBusy = { value: false }
let runOnAsyncContext = (_frame: Frame, _func: () => void): void => { let runOnAsyncContext = (_frame: Frame, _func: () => void): void => {
throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage) throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage)
} }
let throwJSError = (error: unknown): void => {
throw error
}
try { try {
assertJSIAvailable() assertJSIAvailable()
@ -51,6 +58,24 @@ try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const { Worklets } = require('react-native-worklets-core') as typeof TWorklets const { Worklets } = require('react-native-worklets-core') as typeof TWorklets
const throwErrorOnJS = Worklets.createRunInJsFn((message: string, stack: string | undefined) => {
const error = new Error()
error.message = message
error.stack = stack
error.name = 'Frame Processor Error'
// @ts-expect-error this is react-native specific
error.jsEngine = 'VisionCamera'
// From react-native:
// @ts-ignore the reportFatalError method is an internal method of ErrorUtils not exposed in the type definitions
global.ErrorUtils.reportFatalError(error)
})
throwJSError = (error) => {
'worklet'
const safeError = error as Error | undefined
const message = safeError != null && 'message' in safeError ? safeError.message : 'Frame Processor threw an error.'
throwErrorOnJS(message, safeError?.stack)
}
isAsyncContextBusy = Worklets.createSharedValue(false) isAsyncContextBusy = Worklets.createSharedValue(false)
const asyncContext = Worklets.createContext('VisionCamera.async') const asyncContext = Worklets.createContext('VisionCamera.async')
runOnAsyncContext = Worklets.createRunInContextFn((frame: Frame, func: () => void) => { runOnAsyncContext = Worklets.createRunInContextFn((frame: Frame, func: () => void) => {
@ -58,6 +83,9 @@ try {
try { try {
// Call long-running function // Call long-running function
func() func()
} catch (e) {
// Re-throw error on JS Thread
throwJSError(e)
} finally { } finally {
// Potentially delete Frame if we were the last ref // Potentially delete Frame if we were the last ref
const internal = frame as FrameInternal const internal = frame as FrameInternal
@ -81,6 +109,7 @@ let proxy: TVisionCameraProxy = {
setFrameProcessor: () => { setFrameProcessor: () => {
throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage) throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage)
}, },
throwJSError: throwJSError,
} }
if (hasWorklets) { if (hasWorklets) {
// Install native Frame Processor Runtime Manager // Install native Frame Processor Runtime Manager
@ -103,6 +132,7 @@ export const VisionCameraProxy: TVisionCameraProxy = {
initFrameProcessorPlugin: proxy.initFrameProcessorPlugin, initFrameProcessorPlugin: proxy.initFrameProcessorPlugin,
removeFrameProcessor: proxy.removeFrameProcessor, removeFrameProcessor: proxy.removeFrameProcessor,
setFrameProcessor: proxy.setFrameProcessor, setFrameProcessor: proxy.setFrameProcessor,
throwJSError: throwJSError,
// TODO: Remove this in the next version // TODO: Remove this in the next version
// @ts-expect-error // @ts-expect-error
getFrameProcessorPlugin: (name, options) => { getFrameProcessorPlugin: (name, options) => {
@ -116,6 +146,12 @@ export const VisionCameraProxy: TVisionCameraProxy = {
declare global { declare global {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var __frameProcessorRunAtTargetFpsMap: Record<string, number | undefined> | undefined var __frameProcessorRunAtTargetFpsMap: Record<string, number | undefined> | undefined
// eslint-disable-next-line no-var
var __ErrorUtils:
| {
reportFatalError: (error: unknown) => void
}
| undefined
} }
function getLastFrameProcessorCall(frameProcessorFuncId: string): number { function getLastFrameProcessorCall(frameProcessorFuncId: string): number {

View File

@ -1,6 +1,7 @@
import { DependencyList, useMemo } from 'react' import { DependencyList, useMemo } from 'react'
import type { Frame, FrameInternal } from '../Frame' import type { Frame, FrameInternal } from '../Frame'
import { FrameProcessor } from '../CameraProps' import { FrameProcessor } from '../CameraProps'
import { VisionCameraProxy } from '../FrameProcessorPlugins'
/** /**
* Create a new Frame Processor function which you can pass to the `<Camera>`. * Create a new Frame Processor function which you can pass to the `<Camera>`.
@ -20,6 +21,9 @@ export function createFrameProcessor(frameProcessor: FrameProcessor['frameProces
try { try {
// Call sync frame processor // Call sync frame processor
frameProcessor(frame) frameProcessor(frame)
} catch (e) {
// Re-throw error on JS Thread
VisionCameraProxy.throwJSError(e)
} finally { } finally {
// Potentially delete Frame if we were the last ref (no runAsync) // Potentially delete Frame if we were the last ref (no runAsync)
internal.decrementRefCount() internal.decrementRefCount()