UNPKG

react-native-reanimated

Version:

More powerful alternative to Animated library for React Native.

317 lines (300 loc) • 12.9 kB
import NativeReanimatedModule from './NativeReanimated'; import type { ShareableRef, FlatShareableRef, __WorkletFunction, } from './commonTypes'; import { shouldBeUseWeb } from './PlatformChecker'; import { registerWorkletStackDetails } from './errors'; import { jsVersion } from './platform-specific/jsVersion'; // 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 // runnning the code on separate VMs. const USE_STUB_IMPLEMENTATION = shouldBeUseWeb(); const _shareableCache = new WeakMap< Record<string, unknown>, ShareableRef<any> | symbol >(); // the below symbol is used to represent a mapping from the value to itself // this is used to allow for a converted shareable to be passed to makeShareableClone const _shareableFlag = Symbol('shareable flag'); const MAGIC_KEY = 'REANIMATED_MAGIC_KEY'; function isHostObject(value: NonNullable<object>) { '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; } export function registerShareableMapping( shareable: any, shareableRef?: ShareableRef<any> ): void { if (USE_STUB_IMPLEMENTATION) { return; } _shareableCache.set(shareable, shareableRef || _shareableFlag); } function isPlainJSObject(object: object) { return Object.getPrototypeOf(object) === Object.prototype; } // 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: (_: any, prop: string | symbol) => { if (prop === '_isReanimatedSharedValue') { // 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 extracing inputs 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 Error( `[Reanimated] Trying to access property \`${String( prop )}\` of an object which cannot be sent to the UI runtime.` ); }, set: () => { throw new Error( '[Reanimated] Trying to write to an object which cannot be sent to the UI runtime.' ); }, } ); }, }; 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: any; export function makeShareableCloneRecursive<T>( value: any, shouldPersistRemote = false, depth = 0 ): ShareableRef<T> { if (USE_STUB_IMPLEMENTATION) { return value; } 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 trasferred 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 Error( '[Reanimated] Trying to convert a cyclic object to a shareable. This is not supported.' ); } } else { processedObjectAtThresholdDepth = undefined; } // This one actually may be worth to be moved to c++, we also need similar logic to run on the UI thread const type = typeof value; const isTypeObject = type === 'object'; const isTypeFunction = type === 'function'; if ((isTypeObject || isTypeFunction) && value !== null) { const cached = _shareableCache.get(value); if (cached === _shareableFlag) { return value; } else if (cached !== undefined) { return cached as ShareableRef<T>; } else { let toAdapt: any; if (Array.isArray(value)) { toAdapt = value.map((element) => makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1) ); } else if (isTypeFunction && value.__workletHash === undefined) { // this is a remote function toAdapt = value; } else if (isHostObject(value)) { // 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. toAdapt = value; } else if (isPlainJSObject(value) || isTypeFunction) { toAdapt = {}; if (value.__workletHash !== undefined) { // we are converting a worklet if (__DEV__) { const babelVersion = value.__initData.version; if (babelVersion === undefined) { throw new Error(`[Reanimated] Unknown version of Reanimated Babel plugin. See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#unknown-version-of-reanimated-babel-plugin\` for more details. Offending code was: \`${getWorkletCode(value)}\``); } else if (babelVersion !== jsVersion) { throw new Error(`[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 ); delete value.__stackDetails; } else if (value.__stackDetails) { // Detected debug version of the worklet in release bundle. This // might lead to unexpected issues or errors. Probably one of user // dependencies provided transpiled code with debug version of the // Reanimated plugin. throw new Error( `[Reanimated] Using dev bundle in a release app build is not supported. See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#using-dev-bundle-in-a-release-app-build-is-not-supported\` for more details.` ); } // 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. toAdapt.__initData = makeShareableCloneRecursive( value.__initData, true, depth + 1 ); } for (const [key, element] of Object.entries(value)) { toAdapt[key] = makeShareableCloneRecursive( element, shouldPersistRemote, depth + 1 ); } } else if (value instanceof RegExp) { const pattern = value.source; const flags = value.flags; const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; return new RegExp(pattern, flags); }, }); registerShareableMapping(value, handle); return handle as ShareableRef<T>; } else { // 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 inaccessibleObject = makeShareableCloneRecursive<T>(INACCESSIBLE_OBJECT); _shareableCache.set(value, inaccessibleObject); return inaccessibleObject; } if (__DEV__) { // we freeze objects that are transformed to shareable. This should help // detect issues when someone modifies data after it's been converted to // shareable. Meaning that they may be doing a faulty assumption in their // code expecting that the updates are going to automatically populate to // the object sent to the UI thread. If the user really wants some objects // to be mutable they should use shared values instead. Object.freeze(value); } const adopted = NativeReanimatedModule.makeShareableClone( toAdapt, shouldPersistRemote ); _shareableCache.set(value, adopted); _shareableCache.set(adopted, _shareableFlag); return adopted; } } return NativeReanimatedModule.makeShareableClone(value, shouldPersistRemote); } const WORKLET_CODE_THRESHOLD = 255; function getWorkletCode(value: __WorkletFunction) { // @ts-ignore this is fine const code = value?.__initData?.code; if (!code) { return 'unknown'; } if (code.length > WORKLET_CODE_THRESHOLD) { return `${code.substring(0, WORKLET_CODE_THRESHOLD)}...`; } return code; } type RemoteFunction<T> = { __remoteFunction: FlatShareableRef<T>; }; function isRemoteFunction<T>(value: { __remoteFunction?: unknown; }): value is RemoteFunction<T> { 'worklet'; return !!value.__remoteFunction; } export function makeShareableCloneOnUIRecursive<T>( value: T ): FlatShareableRef<T> { 'worklet'; if (USE_STUB_IMPLEMENTATION) { // @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; } function cloneRecursive<T>(value: T): FlatShareableRef<T> { if ( (typeof value === 'object' && value !== null) || typeof value === 'function' ) { if (isHostObject(value)) { // We call `_makeShareableClone` to wrap the provided HostObject // inside ShareableJSRef. return _makeShareableClone(value) as FlatShareableRef<T>; } if (isRemoteFunction<T>(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 _makeShareableClone( value.map(cloneRecursive) ) as FlatShareableRef<T>; } const toAdapt: Record<string, FlatShareableRef<T>> = {}; for (const [key, element] of Object.entries(value)) { toAdapt[key] = cloneRecursive<T>(element); } return _makeShareableClone(toAdapt) as FlatShareableRef<T>; } return _makeShareableClone(value); } return cloneRecursive(value); } export function makeShareable<T>(value: T): T { if (USE_STUB_IMPLEMENTATION) { return value; } const handle = makeShareableCloneRecursive({ __init: () => { 'worklet'; return value; }, }); registerShareableMapping(value, handle); return value; }