react-native-worklets
Version:
The React Native multithreading library
254 lines (224 loc) • 8.35 kB
text/typescript
;
import { bundleValueUnpacker } from './bundleUnpacker';
import { setupCallGuard } from './callGuard';
import { registerReportFatalRemoteError } from './errors';
import { IS_JEST, SHOULD_BE_USE_WEB } from './PlatformChecker';
import { mockedRequestAnimationFrame } from './runLoop/mockedRequestAnimationFrame';
import { setupRequestAnimationFrame } from './runLoop/requestAnimationFrame';
import { setupSetImmediate } from './runLoop/setImmediatePolyfill';
import { setupSetInterval } from './runLoop/setIntervalPolyfill';
import { setupSetTimeout } from './runLoop/setTimeoutPolyfill';
import { executeOnUIRuntimeSync, runOnJS, setupMicrotasks } from './threads';
import { isWorkletFunction } from './workletFunction';
import { registerWorkletsError, WorkletsError } from './WorkletsError';
import { WorkletsModule } from './WorkletsModule';
import type { ValueUnpacker } from './workletTypes';
let capturableConsole: typeof console;
/**
* Currently there seems to be a bug in the JSI layer which causes a crash when
* we try to copy some of the console methods, i.e. `clear` or `dirxml`.
*
* The crash happens only in React Native 0.75. It's not reproducible in neither
* 0.76 nor 0.74. It also happens only in the configuration of a debug app and
* production bundle.
*
* I haven't yet discovered what exactly causes the crash. It's tied to the
* console methods sometimes being `HostFunction`s. Therefore, as a workaround
* we don't copy the methods as they are in the original console object, we copy
* JavaScript wrappers instead.
*/
export function getMemorySafeCapturableConsole(): typeof console {
if (capturableConsole) {
return capturableConsole;
}
const consoleCopy = Object.fromEntries(
Object.entries(console).map(([methodName, method]) => {
const methodWrapper = function methodWrapper(...args: unknown[]) {
return method(...args);
};
if (method.name) {
/**
* Set the original method name as the wrapper name if available.
*
* It might be unnecessary but if we want to fully mimic the console
* object we should take into the account the fact some code might rely
* on the method name.
*/
Object.defineProperty(methodWrapper, 'name', {
value: method.name,
writable: false,
});
}
return [methodName, methodWrapper];
})
);
capturableConsole = consoleCopy as unknown as typeof console;
return consoleCopy as unknown as typeof console;
}
export function setupConsole(boundCapturableConsole: typeof console) {
'worklet';
// @ts-ignore TypeScript doesn't like that there are missing methods in console object, but we don't provide all the methods for the UI runtime console version
globalThis.console = {
assert: runOnJS(boundCapturableConsole.assert),
debug: runOnJS(boundCapturableConsole.debug),
log: runOnJS(boundCapturableConsole.log),
warn: runOnJS(boundCapturableConsole.warn),
error: runOnJS(boundCapturableConsole.error),
info: runOnJS(boundCapturableConsole.info),
};
}
let initialized = false;
export function init() {
if (initialized) {
return;
}
initialized = true;
initializeRuntime();
if (SHOULD_BE_USE_WEB) {
initializeRuntimeOnWeb();
}
if (globalThis._WORKLET) {
initializeWorkletRuntime();
} else {
initializeRNRuntime();
if (!SHOULD_BE_USE_WEB) {
installRNBindingsOnUIRuntime();
}
}
}
/** A function that should be run on any kind of runtime. */
function initializeRuntime() {
if (globalThis._WORKLETS_BUNDLE_MODE) {
globalThis.__valueUnpacker = bundleValueUnpacker as ValueUnpacker;
}
}
/** A function that should be run only on React Native runtime. */
function initializeRNRuntime() {
if (__DEV__) {
const testWorklet = () => {
'worklet';
};
if (!isWorkletFunction(testWorklet)) {
throw new WorkletsError(
`Failed to create a worklet. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#failed-to-create-a-worklet for more details.`
);
}
}
registerReportFatalRemoteError();
}
/** A function that should be run only on Worklet runtimes. */
function initializeWorkletRuntime() {
if (globalThis._WORKLETS_BUNDLE_MODE) {
setupCallGuard();
if (__DEV__) {
/*
* Temporary workaround for Metro bundler. We must implement a dummy
* Refresh module to prevent Metro from throwing irrelevant errors.
*/
const Refresh = new Proxy(
{},
{
get() {
return () => {};
},
}
);
globalThis.__r.Refresh = Refresh;
/* Gracefully handle unwanted imports from React Native. */
// @ts-expect-error type not exposed by Metro
const modules = require.getModules();
// @ts-expect-error type not exposed by Metro
const ReactNativeModuleId = require.resolveWeak('react-native');
const factory = function (
_global: unknown,
_require: unknown,
_importDefault: unknown,
_importAll: unknown,
module: Record<string, unknown>,
_exports: unknown,
_dependencyMap: unknown
) {
module.exports = new Proxy(
{},
{
get: function get(_target, prop) {
globalThis.console.warn(
`You tried to import '${String(prop)}' from 'react-native' module on a Worklet Runtime. Using 'react-native' module on a Worklet Runtime is not allowed.`
);
return {
get() {
return undefined;
},
};
},
}
);
};
const mod = {
dependencyMap: [],
factory,
hasError: false,
importedAll: {},
importedDefault: {},
isInitialized: false,
publicModule: {
exports: {},
},
};
modules.set(ReactNativeModuleId, mod);
}
}
}
/** A function that should be run only on RN Runtime in web implementation. */
function initializeRuntimeOnWeb() {
globalThis._WORKLET = false;
globalThis._log = console.log;
globalThis._getAnimationTimestamp = () => performance.now();
if (IS_JEST) {
// requestAnimationFrame react-native jest's setup is incorrect as it polyfills
// the method directly using setTimeout, therefore the callback doesn't get the
// expected timestamp as the only argument: https://github.com/facebook/react-native/blob/main/packages/react-native/jest/setup.js#L28
// We override this setup here to make sure that callbacks get the proper timestamps
// when executed. For non-jest environments we define requestAnimationFrame in setupRequestAnimationFrame
// @ts-ignore TypeScript uses Node definition for rAF, setTimeout, etc which returns a Timeout object rather than a number
globalThis.requestAnimationFrame = mockedRequestAnimationFrame;
}
}
/**
* A function that should be run on the RN Runtime to configure the UI Runtime
* with callback bindings.
*/
function installRNBindingsOnUIRuntime() {
if (!WorkletsModule) {
throw new WorkletsError(
'Worklets are trying to initialize the UI runtime without a valid WorkletsModule'
);
}
const runtimeBoundCapturableConsole = getMemorySafeCapturableConsole();
if (!globalThis._WORKLETS_BUNDLE_MODE) {
/** In bundle mode Runtimes setup their callGuard themselves. */
executeOnUIRuntimeSync(setupCallGuard)();
/**
* Register WorkletsError in the UI runtime global scope. (we are using
* `executeOnUIRuntimeSync` here to make sure that the changes are applied
* before any async operations are executed on the UI runtime).
*
* There's no need to register the error in bundle mode.
*/
executeOnUIRuntimeSync(registerWorkletsError)();
}
executeOnUIRuntimeSync(() => {
'worklet';
setupConsole(runtimeBoundCapturableConsole);
/**
* TODO: Move `setupMicrotasks` and `setupRequestAnimationFrame` to a
* separate function once we have a better way to distinguish between
* Worklet Runtimes.
*/
setupMicrotasks();
setupRequestAnimationFrame();
setupSetTimeout();
setupSetImmediate();
setupSetInterval();
})();
}