UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

415 lines (401 loc) • 16.4 kB
'use strict'; import { isWorkletFunction } from "./commonTypes.js"; import { ReanimatedError, registerWorkletStackDetails } from "./errors.js"; import { logger } from "./logger/index.js"; import { jsVersion } from "./platform-specific/jsVersion.js"; import { shouldBeUseWeb } from "./PlatformChecker.js"; import { shareableMappingCache, shareableMappingFlag } from "./shareableMappingCache.js"; import { WorkletsModule } from "./worklets/index.js"; // for web/chrome debugger/jest environments this file provides a stub implementation // where no shareable references are used. Instead, the objects themselves are used // instead of shareable references, because of the fact that we don't have to deal with // running the code on separate VMs. const SHOULD_BE_USE_WEB = shouldBeUseWeb(); const MAGIC_KEY = 'REANIMATED_MAGIC_KEY'; function isHostObject(value) { 'worklet'; // We could use JSI to determine whether an object is a host object, however // the below workaround works well and is way faster than an additional JSI call. // We use the fact that host objects have broken implementation of `hasOwnProperty` // and hence return true for all `in` checks regardless of the key we ask for. return MAGIC_KEY in value; } function isPlainJSObject(object) { return Object.getPrototypeOf(object) === Object.prototype; } function getFromCache(value) { const cached = shareableMappingCache.get(value); if (cached === shareableMappingFlag) { // This means that `value` was already a clone and we should return it as is. return value; } return cached; } // The below object is used as a replacement for objects that cannot be transferred // as shareable values. In makeShareableCloneRecursive we detect if an object is of // a plain Object.prototype and only allow such objects to be transferred. This lets // us avoid all sorts of react internals from leaking into the UI runtime. To make it // possible to catch errors when someone actually tries to access such object on the UI // runtime, we use the below Proxy object which is instantiated on the UI runtime and // throws whenever someone tries to access its fields. const INACCESSIBLE_OBJECT = { __init: () => { 'worklet'; return new Proxy({}, { get: (_, prop) => { if (prop === '_isReanimatedSharedValue' || prop === '__remoteFunction') { // not very happy about this check here, but we need to allow for // "inaccessible" objects to be tested with isSharedValue check // as it is being used in the mappers when extracting inputs recursively // as well as with isRemoteFunction when cloning objects recursively. // Apparently we can't check if a key exists there as HostObjects always // return true for such tests, so the only possibility for us is to // actually access that key and see if it is set to true. We therefore // need to allow for this key to be accessed here. return false; } throw new ReanimatedError(`Trying to access property \`${String(prop)}\` of an object which cannot be sent to the UI runtime.`); }, set: () => { throw new ReanimatedError('Trying to write to an object which cannot be sent to the UI runtime.'); } }); } }; const VALID_ARRAY_VIEWS_NAMES = ['Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'DataView']; const DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD = 30; // Below variable stores object that we process in makeShareableCloneRecursive at the specified depth. // We use it to check if later on the function reenters with the same object let processedObjectAtThresholdDepth; function makeShareableCloneRecursiveWeb(value) { return value; } function makeShareableCloneRecursiveNative(value, shouldPersistRemote = false, depth = 0) { detectCyclicObject(value, depth); const isObject = typeof value === 'object'; const isFunction = typeof value === 'function'; if (!isObject && !isFunction || value === null) { return clonePrimitive(value, shouldPersistRemote); } const cached = getFromCache(value); if (cached !== undefined) { return cached; } if (Array.isArray(value)) { return cloneArray(value, shouldPersistRemote, depth); } if (isFunction && !isWorkletFunction(value)) { return cloneRemoteFunction(value, shouldPersistRemote); } if (isHostObject(value)) { return cloneHostObject(value, shouldPersistRemote); } if (isPlainJSObject(value) && value.__workletContextObjectFactory) { return cloneContextObject(value); } if ((isPlainJSObject(value) || isFunction) && isWorkletFunction(value)) { return cloneWorklet(value, shouldPersistRemote, depth); } if (isPlainJSObject(value) || isFunction) { return clonePlainJSObject(value, shouldPersistRemote, depth); } if (value instanceof RegExp) { return cloneRegExp(value); } if (value instanceof Error) { return cloneError(value); } if (value instanceof ArrayBuffer) { return cloneArrayBuffer(value, shouldPersistRemote); } if (ArrayBuffer.isView(value)) { // typed array (e.g. Int32Array, Uint8ClampedArray) or DataView return cloneArrayBufferView(value); } return inaccessibleObject(value); } export const makeShareableCloneRecursive = SHOULD_BE_USE_WEB ? makeShareableCloneRecursiveWeb : makeShareableCloneRecursiveNative; function detectCyclicObject(value, depth) { if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) { // if we reach certain recursion depth we suspect that we are dealing with a cyclic object. // this type of objects are not supported and cannot be transferred as shareable, so we // implement a simple detection mechanism that remembers the value at a given depth and // tests whether we try reenter this method later on with the same value. If that happens // we throw an appropriate error. if (depth === DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) { processedObjectAtThresholdDepth = value; } else if (value === processedObjectAtThresholdDepth) { throw new ReanimatedError('Trying to convert a cyclic object to a shareable. This is not supported.'); } } else { processedObjectAtThresholdDepth = undefined; } } function clonePrimitive(value, shouldPersistRemote) { return WorkletsModule.makeShareableClone(value, shouldPersistRemote); } function cloneArray(value, shouldPersistRemote, depth) { const clonedElements = value.map(element => makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1)); const clone = WorkletsModule.makeShareableClone(clonedElements, shouldPersistRemote, value); shareableMappingCache.set(value, clone); shareableMappingCache.set(clone); freezeObjectInDev(value); return clone; } function cloneRemoteFunction(value, shouldPersistRemote) { const clone = WorkletsModule.makeShareableClone(value, shouldPersistRemote, value); shareableMappingCache.set(value, clone); shareableMappingCache.set(clone); freezeObjectInDev(value); return clone; } function cloneHostObject(value, shouldPersistRemote) { // for host objects we pass the reference to the object as shareable and // then recreate new host object wrapping the same instance on the UI thread. // there is no point of iterating over keys as we do for regular objects. const clone = WorkletsModule.makeShareableClone(value, shouldPersistRemote, value); shareableMappingCache.set(value, clone); shareableMappingCache.set(clone); return clone; } function cloneWorklet(value, shouldPersistRemote, depth) { if (__DEV__) { const babelVersion = value.__initData.version; if (babelVersion !== undefined && babelVersion !== jsVersion) { throw new ReanimatedError(`[Reanimated] Mismatch between JavaScript code version and Reanimated Babel plugin version (${jsVersion} vs. ${babelVersion}). See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-javascript-code-version-and-reanimated-babel-plugin-version\` for more details. Offending code was: \`${getWorkletCode(value)}\``); } registerWorkletStackDetails(value.__workletHash, value.__stackDetails); } if (value.__stackDetails) { // `Error` type of value cannot be copied to the UI thread, so we // remove it after we handled it in dev mode or delete it to ignore it in production mode. // Not removing this would cause an infinite loop in production mode and it just // seems more elegant to handle it this way. delete value.__stackDetails; } // to save on transferring static __initData field of worklet structure // we request shareable value to persist its UI counterpart. This means // that the __initData field that contains long strings represeting the // worklet code, source map, and location, will always be // serialized/deserialized once. const clonedProps = {}; clonedProps.__initData = makeShareableCloneRecursive(value.__initData, true, depth + 1); for (const [key, element] of Object.entries(value)) { if (key === '__initData' && clonedProps.__initData !== undefined) { continue; } clonedProps[key] = makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1); } const clone = WorkletsModule.makeShareableClone(clonedProps, // retain all worklets true, value); shareableMappingCache.set(value, clone); shareableMappingCache.set(clone); freezeObjectInDev(value); return clone; } function cloneContextObject(value) { const workletContextObjectFactory = value.__workletContextObjectFactory; const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; return workletContextObjectFactory(); } }); shareableMappingCache.set(value, handle); return handle; } function clonePlainJSObject(value, shouldPersistRemote, depth) { const clonedProps = {}; for (const [key, element] of Object.entries(value)) { if (key === '__initData' && clonedProps.__initData !== undefined) { continue; } clonedProps[key] = makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1); } const clone = WorkletsModule.makeShareableClone(clonedProps, shouldPersistRemote, value); shareableMappingCache.set(value, clone); shareableMappingCache.set(clone); freezeObjectInDev(value); return clone; } function cloneRegExp(value) { const pattern = value.source; const flags = value.flags; const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; return new RegExp(pattern, flags); } }); shareableMappingCache.set(value, handle); return handle; } function cloneError(value) { const { name, message, stack } = value; const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; // eslint-disable-next-line reanimated/use-reanimated-error const error = new Error(); error.name = name; error.message = message; error.stack = stack; return error; } }); shareableMappingCache.set(value, handle); return handle; } function cloneArrayBuffer(value, shouldPersistRemote) { const clone = WorkletsModule.makeShareableClone(value, shouldPersistRemote, value); shareableMappingCache.set(value, clone); shareableMappingCache.set(clone); return clone; } function cloneArrayBufferView(value) { const buffer = value.buffer; const typeName = value.constructor.name; const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; if (!VALID_ARRAY_VIEWS_NAMES.includes(typeName)) { throw new ReanimatedError(`[Reanimated] Invalid array view name \`${typeName}\`.`); } const constructor = global[typeName]; if (constructor === undefined) { throw new ReanimatedError(`[Reanimated] Constructor for \`${typeName}\` not found.`); } return new constructor(buffer); } }); shareableMappingCache.set(value, handle); return handle; } function inaccessibleObject(value) { // This is reached for object types that are not of plain Object.prototype. // We don't support such objects from being transferred as shareables to // the UI runtime and hence we replace them with "inaccessible object" // which is implemented as a Proxy object that throws on any attempt // of accessing its fields. We argue that such objects can sometimes leak // as attributes of objects being captured by worklets but should never // be used on the UI runtime regardless. If they are being accessed, the user // will get an appropriate error message. const clone = makeShareableCloneRecursive(INACCESSIBLE_OBJECT); shareableMappingCache.set(value, clone); return clone; } const WORKLET_CODE_THRESHOLD = 255; function getWorkletCode(value) { const code = value?.__initData?.code; if (!code) { return 'unknown'; } if (code.length > WORKLET_CODE_THRESHOLD) { return `${code.substring(0, WORKLET_CODE_THRESHOLD)}...`; } return code; } function isRemoteFunction(value) { 'worklet'; return !!value.__remoteFunction; } /** * We freeze * * - Arrays, * - Remote functions, * - Plain JS objects, * * That are transformed to a shareable with a meaningful warning. This should * help detect issues when someone modifies data after it's been converted. * Meaning that they may be doing a faulty assumption in their code expecting * that the updates are going to automatically propagate to the object sent to * the UI thread. If the user really wants some objects to be mutable they * should use shared values instead. */ function freezeObjectInDev(value) { if (!__DEV__) { return; } Object.entries(value).forEach(([key, element]) => { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor.configurable) { return; } Object.defineProperty(value, key, { get() { return element; }, set() { logger.warn(`Tried to modify key \`${key}\` of an object which has been already passed to a worklet. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-shareable for more details.`); } }); }); Object.preventExtensions(value); } export function makeShareableCloneOnUIRecursive(value) { 'worklet'; if (SHOULD_BE_USE_WEB) { // @ts-ignore web is an interesting place where we don't run a secondary VM on the UI thread // see more details in the comment where USE_STUB_IMPLEMENTATION is defined. return value; } // eslint-disable-next-line @typescript-eslint/no-shadow function cloneRecursive(value) { if (typeof value === 'object' && value !== null || typeof value === 'function') { if (isHostObject(value)) { // We call `_makeShareableClone` to wrap the provided HostObject // inside ShareableJSRef. return global._makeShareableClone(value, undefined); } if (isRemoteFunction(value)) { // RemoteFunctions are created by us therefore they are // a Shareable out of the box and there is no need to // call `_makeShareableClone`. return value.__remoteFunction; } if (Array.isArray(value)) { return global._makeShareableClone(value.map(cloneRecursive), undefined); } const toAdapt = {}; for (const [key, element] of Object.entries(value)) { toAdapt[key] = cloneRecursive(element); } return global._makeShareableClone(toAdapt, value); } return global._makeShareableClone(value, undefined); } return cloneRecursive(value); } function makeShareableJS(value) { return value; } function makeShareableNative(value) { if (shareableMappingCache.get(value)) { return value; } const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; return value; } }); shareableMappingCache.set(value, handle); return value; } /** * This function creates a value on UI with persistent state - changes to it on * the UI thread will be seen by all worklets. Use it when you want to create a * value that is read and written only on the UI thread. */ export const makeShareable = SHOULD_BE_USE_WEB ? makeShareableJS : makeShareableNative; //# sourceMappingURL=shareables.js.map