react-native-worklets
Version:
The React Native multithreading library
631 lines (613 loc) • 24.1 kB
JavaScript
'use strict';
import { registerWorkletStackDetails } from '../debug/errors';
import { jsVersion } from '../debug/jsVersion';
import { logger } from '../debug/logger';
import { WorkletsError } from '../debug/WorkletsError';
import { getRuntimeKind, RuntimeKind } from '../runtimeKind';
import { isWorkletFunction } from '../workletFunction';
import { WorkletsModule } from '../WorkletsModule/NativeWorklets';
import { isSynchronizable } from './isSynchronizable';
import { serializableMappingCache, serializableMappingFlag } from './serializableMappingCache';
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;
}
export function isSerializableRef(value) {
'worklet';
return typeof value === 'object' && value !== null && '__serializableRef' in value && value.__serializableRef === true;
}
function isPlainJSObject(object) {
'worklet';
return Object.getPrototypeOf(object) === Object.prototype;
}
function isTurboModuleLike(object) {
return isHostObject(Object.getPrototypeOf(object));
}
function getFromCache(value) {
const cached = serializableMappingCache.get(value);
if (cached === serializableMappingFlag) {
// 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 serializable values. In createSerializable 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' || prop === '__synchronizableRef') {
// not very happy about this check here, but we need to allow for
// "inaccessible" objects to be tested with isSerializableRef 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 WorkletsError(`Trying to access property \`${String(prop)}\` of an object which cannot be sent to the UI runtime.`);
},
set: () => {
throw new WorkletsError('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 createSerializable at the specified depth.
// We use it to check if later on the function reenters with the same object
let processedObjectAtThresholdDepth;
export function createSerializable(value, shouldPersistRemote = false, depth = 0) {
detectCyclicObject(value, depth);
const isObject = typeof value === 'object';
const isFunction = typeof value === 'function';
if (typeof value === 'string') {
return cloneString(value);
}
if (typeof value === 'number') {
return cloneNumber(value);
}
if (typeof value === 'boolean') {
return cloneBoolean(value);
}
if (typeof value === 'bigint') {
return cloneBigInt(value);
}
if (value === undefined) {
return cloneUndefined();
}
if (value === null) {
return cloneNull();
}
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 (globalThis._WORKLETS_BUNDLE_MODE && isFunction && value.__bundleData) {
return cloneImport(value);
}
if (isFunction && !isWorkletFunction(value)) {
return cloneRemoteFunction(value);
}
// RN has introduced a new representation of TurboModules as a JS object whose prototype is the host object
// More details: https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp#L182
if (isTurboModuleLike(value)) {
return cloneTurboModuleLike(value, shouldPersistRemote, depth);
}
if (isHostObject(value)) {
return cloneHostObject(value);
}
if (isPlainJSObject(value) && value.__init) {
return cloneInitializer(value, shouldPersistRemote, depth);
}
if (isPlainJSObject(value) && value.__workletContextObjectFactory) {
return cloneContextObject(value);
}
if ((isPlainJSObject(value) || isFunction) && isWorkletFunction(value)) {
return cloneWorklet(value, shouldPersistRemote, depth);
}
if (isSynchronizable(value)) {
return cloneSynchronizable(value);
}
if (isPlainJSObject(value) || isFunction) {
return clonePlainJSObject(value, shouldPersistRemote, depth);
}
if (value instanceof Set) {
return cloneSet(value);
}
if (value instanceof Map) {
return cloneMap(value);
}
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);
}
for (let i = 0; i < customSerializationRegistry.length; i++) {
const {
determine,
pack
} = customSerializationRegistry[i];
if (determine(value)) {
return cloneCustom(value, pack, i);
}
}
return inaccessibleObject(value);
}
if (globalThis._WORKLETS_BUNDLE_MODE) {
// TODO: Do it programmatically.
createSerializable.__bundleData = {
imported: 'createSerializable',
source: require.resolveWeak('react-native-worklets')
};
}
if (!globalThis.__customSerializationRegistry) {
globalThis.__customSerializationRegistry = [];
}
const customSerializationRegistry = globalThis.__customSerializationRegistry;
/**
* `registerCustomSerializable` lets you register your own pre-serialization and
* post-deserialization logic. This is necessary for objects with prototypes
* different than just `Object.prototype` or some other built-in prototypes like
* `Map` etc. Worklets can't handle such objects by default to convert into
* [Serializables](https://docs.swmansion.com/react-native-worklets/docs/memory/serializable)
* hence you need to register them as **Custom Serializables**. This way you can
* tell Worklets how to transfer your custom data structures between different
* Runtimes without manually serializing and deserializing them every time.
*
* @param registrationData - The registration data for the custom serializable -
* {@link RegistrationData}
* @see https://docs.swmansion.com/react-native-worklets/docs/memory/registerCustomSerializable/
*/
export function registerCustomSerializable(registrationData) {
if (__DEV__ && getRuntimeKind() !== RuntimeKind.ReactNative) {
throw new WorkletsError('registerCustomSerializable can be used only on React Native runtime.');
}
const {
name,
determine,
pack,
unpack
} = registrationData;
if (__DEV__) {
verifyRegistrationData(determine, pack, unpack);
}
if (customSerializationRegistry.some(data => data.name === name)) {
if (__DEV__) {
console.warn(`Custom serializable with name "${name}" is already registered. Duplicate registration is ignored.`);
}
return;
}
customSerializationRegistry.push(registrationData);
WorkletsModule.registerCustomSerializable(createSerializable(determine), createSerializable(pack), createSerializable(unpack), customSerializationRegistry.length - 1);
}
function verifyRegistrationData(determine, pack, unpack) {
if (!isWorkletFunction(determine)) {
throw new WorkletsError('The "determine" function provided to registerCustomSerializable must be a worklet.');
}
if (!isWorkletFunction(pack)) {
throw new WorkletsError('The "pack" function provided to registerCustomSerializable must be a worklet.');
}
if (!isWorkletFunction(unpack)) {
throw new WorkletsError('The "unpack" function provided to registerCustomSerializable must be a worklet.');
}
}
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 serializable, 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 WorkletsError('Trying to convert a cyclic object to a serializable. This is not supported.');
}
} else {
processedObjectAtThresholdDepth = undefined;
}
}
function clonePrimitive(value, shouldPersistRemote) {
return WorkletsModule.createSerializable(value, shouldPersistRemote);
}
function cloneString(value) {
return WorkletsModule.createSerializableString(value);
}
function cloneNumber(value) {
return WorkletsModule.createSerializableNumber(value);
}
function cloneBoolean(value) {
return WorkletsModule.createSerializableBoolean(value);
}
function cloneBigInt(value) {
return WorkletsModule.createSerializableBigInt(value);
}
function cloneUndefined() {
return WorkletsModule.createSerializableUndefined();
}
function cloneNull() {
return WorkletsModule.createSerializableNull();
}
function cloneObjectProperties(value, shouldPersistRemote, depth) {
const clonedProps = {};
for (const [key, element] of Object.entries(value)) {
// We don't need to clone __initData field as it contains long strings
// representing the worklet code, source map, and location, and we will
// serialize/deserialize it once.
if (key === '__initData' && clonedProps.__initData !== undefined) {
continue;
}
clonedProps[key] = createSerializable(element, shouldPersistRemote, depth + 1);
}
return clonedProps;
}
function cloneInitializer(value, shouldPersistRemote = false, depth = 0) {
const clonedProps = cloneObjectProperties(value, shouldPersistRemote, depth);
return WorkletsModule.createSerializableInitializer(clonedProps);
}
function cloneArray(value, shouldPersistRemote, depth) {
const clonedElements = value.map(element => createSerializable(element, shouldPersistRemote, depth + 1));
const clone = WorkletsModule.createSerializableArray(clonedElements, shouldPersistRemote);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
freezeObjectInDev(value);
return clone;
}
function cloneRemoteFunction(value) {
const clone = WorkletsModule.createSerializableFunction(value);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
freezeObjectInDev(value);
return clone;
}
function cloneHostObject(value) {
// for host objects we pass the reference to the object as serializable 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.createSerializableHostObject(value);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
return clone;
}
function cloneWorklet(value, shouldPersistRemote, depth) {
if (__DEV__) {
const babelVersion = value.__pluginVersion;
if (babelVersion !== undefined && babelVersion !== jsVersion) {
throw new WorkletsError(`Mismatch between JavaScript code version and Worklets Babel plugin version (${jsVersion} vs. ${babelVersion}).
See \`https://docs.swmansion.com/react-native-worklets/docs/guides/troubleshooting#mismatch-between-javascript-code-version-and-worklets-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;
}
const clonedProps = cloneObjectProperties(value, true, depth);
// to save on transferring static __initData field of worklet structure
// we request serializable value to persist its UI counterpart. This means
// that the __initData field that contains long strings representing the
// worklet code, source map, and location, will always be
// serialized/deserialized once.
clonedProps.__initData = createSerializable(value.__initData, true, depth + 1);
const clone = WorkletsModule.createSerializableWorklet(clonedProps,
// TODO: Check after refactor if we can remove shouldPersistRemote parameter (imho it's redundant here since worklets are always persistent)
// retain all worklets
true);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
freezeObjectInDev(value);
return clone;
}
/**
* TurboModuleLike objects are JS objects that have a TurboModule as their
* prototype.
*/
function cloneTurboModuleLike(value, shouldPersistRemote, depth) {
const proto = Object.getPrototypeOf(value);
const clonedProps = cloneObjectProperties(value, shouldPersistRemote, depth);
const clone = WorkletsModule.createSerializableTurboModuleLike(clonedProps, proto);
return clone;
}
function cloneContextObject(value) {
const workletContextObjectFactory = value.__workletContextObjectFactory;
const handle = cloneInitializer({
__init: () => {
'worklet';
return workletContextObjectFactory();
}
});
serializableMappingCache.set(value, handle);
return handle;
}
function clonePlainJSObject(value, shouldPersistRemote, depth) {
const clonedProps = cloneObjectProperties(value, shouldPersistRemote, depth);
const clone = WorkletsModule.createSerializableObject(clonedProps, shouldPersistRemote, value);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
freezeObjectInDev(value);
return clone;
}
function cloneMap(value) {
const clonedKeys = [];
const clonedValues = [];
for (const [key, element] of value.entries()) {
clonedKeys.push(createSerializable(key));
clonedValues.push(createSerializable(element));
}
const clone = WorkletsModule.createSerializableMap(clonedKeys, clonedValues);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
freezeObjectInDev(value);
return clone;
}
function cloneSet(value) {
const clonedElements = [];
for (const element of value) {
clonedElements.push(createSerializable(element));
}
const clone = WorkletsModule.createSerializableSet(clonedElements);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
freezeObjectInDev(value);
return clone;
}
function cloneRegExp(value) {
const pattern = value.source;
const flags = value.flags;
const handle = cloneInitializer({
__init: () => {
'worklet';
return new RegExp(pattern, flags);
}
});
serializableMappingCache.set(value, handle);
return handle;
}
function cloneError(value) {
const {
name,
message,
stack
} = value;
const handle = cloneInitializer({
__init: () => {
'worklet';
// eslint-disable-next-line reanimated/use-worklets-error
const error = new Error();
error.name = name;
error.message = message;
error.stack = stack;
return error;
}
});
serializableMappingCache.set(value, handle);
return handle;
}
function cloneArrayBuffer(value, shouldPersistRemote) {
const clone = WorkletsModule.createSerializable(value, shouldPersistRemote, value);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
return clone;
}
function cloneArrayBufferView(value) {
const buffer = value.buffer;
const typeName = value.constructor.name;
const handle = cloneInitializer({
__init: () => {
'worklet';
if (!VALID_ARRAY_VIEWS_NAMES.includes(typeName)) {
throw new WorkletsError(`Invalid array view name \`${typeName}\`.`);
}
const constructor = global[typeName];
if (constructor === undefined) {
throw new WorkletsError(`Constructor for \`${typeName}\` not found.`);
}
return new constructor(buffer);
}
});
serializableMappingCache.set(value, handle);
return handle;
}
function cloneSynchronizable(value) {
serializableMappingCache.set(value);
return value;
}
function cloneImport(value) {
const {
source,
imported
} = value.__bundleData;
const clone = WorkletsModule.createSerializableImport(source, imported);
serializableMappingCache.set(value, clone);
serializableMappingCache.set(clone);
return clone;
}
function cloneCustom(data, pack, typeId) {
const packedData = pack(data);
const serialized = createSerializable(packedData);
return WorkletsModule.createCustomSerializable(serialized, typeId);
}
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 serializables 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 = createSerializable(INACCESSIBLE_OBJECT);
serializableMappingCache.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 serializable 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__ || globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative) {
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-serializable
for more details.`);
}
});
});
Object.preventExtensions(value);
}
function makeShareableCloneOnUIRecursiveLEGACY(value) {
'worklet';
// 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 `_createSerializableClone` to wrap the provided HostObject
// inside SerializableJSRef.
return global._createSerializableHostObject(value);
}
if (isRemoteFunction(value)) {
// RemoteFunctions are created by us therefore they are
// a Serializable out of the box and there is no need to
// call `_createSerializableClone`.
return value.__remoteFunction;
}
if (Array.isArray(value)) {
return global._createSerializableArray(value.map(cloneRecursive));
}
if (value.__synchronizableRef) {
return global._createSerializableSynchronizable(value);
}
if (Object.getPrototypeOf(value) !== Object.prototype) {
const length = globalThis.__customSerializationRegistry.length;
for (let i = 0; i < length; i++) {
const {
determine,
pack
} = globalThis.__customSerializationRegistry[i];
if (determine(value)) {
const packedData = pack(value);
return globalThis.__workletsModuleProxy?.createCustomSerializable(cloneRecursive(packedData), i);
}
}
}
const toAdapt = {};
for (const [key, element] of Object.entries(value)) {
toAdapt[key] = cloneRecursive(element);
}
return global._createSerializable(toAdapt, value);
}
if (typeof value === 'string') {
return global._createSerializableString(value);
}
if (typeof value === 'number') {
return global._createSerializableNumber(value);
}
if (typeof value === 'boolean') {
return global._createSerializableBoolean(value);
}
if (typeof value === 'bigint') {
return global._createSerializableBigInt(value);
}
if (value === undefined) {
return global._createSerializableUndefined();
}
if (value === null) {
return global._createSerializableNull();
}
return global._createSerializable(value, undefined);
}
return cloneRecursive(value);
}
/** @deprecated This function is no longer supported. */
export const makeShareableCloneOnUIRecursive = globalThis._WORKLETS_BUNDLE_MODE ? createSerializable : makeShareableCloneOnUIRecursiveLEGACY;
/**
* 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.
*
* @deprecated This function is no longer supported.
*/
export function makeShareable(value) {
if (serializableMappingCache.get(value)) {
return value;
}
const handle = createSerializable({
__init: () => {
'worklet';
return value;
}
});
serializableMappingCache.set(value, handle);
return value;
}
//# sourceMappingURL=serializable.native.js.map