802 lines
25 KiB
TypeScript
802 lines
25 KiB
TypeScript
'use strict';
|
|
import { registerWorkletStackDetails } from './errors';
|
|
import { isSynchronizable } from './isSynchronizable';
|
|
import { logger } from './logger';
|
|
import { SHOULD_BE_USE_WEB } from './PlatformChecker';
|
|
import {
|
|
serializableMappingCache,
|
|
serializableMappingFlag,
|
|
} from './serializableMappingCache';
|
|
import type { Synchronizable } from './synchronizable';
|
|
import { jsVersion } from './utils/jsVersion';
|
|
import { isWorkletFunction } from './workletFunction';
|
|
import { WorkletsError } from './WorkletsError';
|
|
import { WorkletsModule } from './WorkletsModule';
|
|
import type {
|
|
FlatSerializableRef,
|
|
SerializableRef,
|
|
WorkletFunction,
|
|
WorkletImport,
|
|
} from './workletTypes';
|
|
|
|
// for web and jest environments this file provides a stub implementation
|
|
// where no serializable references are used. Instead, the objects themselves are used
|
|
// instead of serializable references, because of the fact that we don't have to deal with
|
|
// running the code on separate VMs.
|
|
|
|
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 isSerializableRef(value: unknown): value is SerializableRef {
|
|
return (
|
|
typeof value === 'object' && value !== null && '__serializableRef' in value
|
|
);
|
|
}
|
|
|
|
function isPlainJSObject(object: object): object is Record<string, unknown> {
|
|
'worklet';
|
|
return Object.getPrototypeOf(object) === Object.prototype;
|
|
}
|
|
|
|
function isTurboModuleLike(object: object): object is Record<string, unknown> {
|
|
return isHostObject(Object.getPrototypeOf(object));
|
|
}
|
|
|
|
function getFromCache(value: object) {
|
|
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: (_: unknown, prop: string | symbol) => {
|
|
if (
|
|
prop === '_isReanimatedSharedValue' ||
|
|
prop === '__remoteFunction'
|
|
) {
|
|
// 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: unknown;
|
|
|
|
function createSerializableWeb<T>(value: T): SerializableRef<T> {
|
|
return value as SerializableRef<T>;
|
|
}
|
|
|
|
function createSerializableNative<T>(
|
|
value: T,
|
|
shouldPersistRemote = false,
|
|
depth = 0
|
|
): SerializableRef<T> {
|
|
detectCyclicObject(value, depth);
|
|
|
|
const isObject = typeof value === 'object';
|
|
const isFunction = typeof value === 'function';
|
|
|
|
if (typeof value === 'string') {
|
|
return cloneString(value) as SerializableRef<T>;
|
|
}
|
|
|
|
if (typeof value === 'number') {
|
|
return cloneNumber(value) as SerializableRef<T>;
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return cloneBoolean(value) as SerializableRef<T>;
|
|
}
|
|
|
|
if (typeof value === 'bigint') {
|
|
return cloneBigInt(value) as SerializableRef<T>;
|
|
}
|
|
|
|
if (value === undefined) {
|
|
return cloneUndefined() as SerializableRef<T>;
|
|
}
|
|
|
|
if (value === null) {
|
|
return cloneNull() as SerializableRef<T>;
|
|
}
|
|
|
|
if ((!isObject && !isFunction) || value === null) {
|
|
return clonePrimitive(value, shouldPersistRemote);
|
|
}
|
|
|
|
const cached = getFromCache(value);
|
|
if (cached !== undefined) {
|
|
return cached as SerializableRef<T>;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return cloneArray(value, shouldPersistRemote, depth);
|
|
}
|
|
if (
|
|
globalThis._WORKLETS_BUNDLE_MODE &&
|
|
isFunction &&
|
|
(value as WorkletImport).__bundleData
|
|
) {
|
|
return cloneImport(value as WorkletImport) as SerializableRef<T>;
|
|
}
|
|
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
|
|
) as SerializableRef<T>;
|
|
}
|
|
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) as SerializableRef<T>;
|
|
}
|
|
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);
|
|
}
|
|
return inaccessibleObject(value);
|
|
}
|
|
|
|
if (globalThis._WORKLETS_BUNDLE_MODE) {
|
|
// TODO: Do it programatically.
|
|
createSerializableNative.__bundleData = {
|
|
imported: 'createSerializable',
|
|
// @ts-expect-error resolveWeak is defined by Metro
|
|
source: require.resolveWeak('./index'),
|
|
};
|
|
}
|
|
|
|
interface CreateSerializable {
|
|
<T>(value: T): SerializableRef<T>;
|
|
<T>(
|
|
value: T,
|
|
shouldPersistRemote: boolean,
|
|
depth: number
|
|
): SerializableRef<T>;
|
|
}
|
|
|
|
export const createSerializable: CreateSerializable = SHOULD_BE_USE_WEB
|
|
? createSerializableWeb
|
|
: createSerializableNative;
|
|
|
|
function detectCyclicObject(value: unknown, depth: number) {
|
|
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<T>(
|
|
value: T,
|
|
shouldPersistRemote: boolean
|
|
): SerializableRef<T> {
|
|
return WorkletsModule.createSerializable(value, shouldPersistRemote);
|
|
}
|
|
|
|
function cloneString(value: string): SerializableRef<string> {
|
|
return WorkletsModule.createSerializableString(value);
|
|
}
|
|
|
|
function cloneNumber(value: number): SerializableRef<number> {
|
|
return WorkletsModule.createSerializableNumber(value);
|
|
}
|
|
|
|
function cloneBoolean(value: boolean): SerializableRef<boolean> {
|
|
return WorkletsModule.createSerializableBoolean(value);
|
|
}
|
|
|
|
function cloneBigInt(value: bigint): SerializableRef<bigint> {
|
|
return WorkletsModule.createSerializableBigInt(value);
|
|
}
|
|
|
|
function cloneUndefined(): SerializableRef<undefined> {
|
|
return WorkletsModule.createSerializableUndefined();
|
|
}
|
|
|
|
function cloneNull(): SerializableRef<null> {
|
|
return WorkletsModule.createSerializableNull();
|
|
}
|
|
|
|
function cloneObjectProperties<T extends object>(
|
|
value: T,
|
|
shouldPersistRemote: boolean,
|
|
depth: number
|
|
): Record<string, unknown> {
|
|
const clonedProps: Record<string, unknown> = {};
|
|
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: object,
|
|
shouldPersistRemote = false,
|
|
depth = 0
|
|
): SerializableRef<object> {
|
|
const clonedProps: Record<string, unknown> = cloneObjectProperties(
|
|
value,
|
|
shouldPersistRemote,
|
|
depth
|
|
);
|
|
return WorkletsModule.createSerializableInitializer(clonedProps);
|
|
}
|
|
|
|
function cloneArray<T extends unknown[]>(
|
|
value: T,
|
|
shouldPersistRemote: boolean,
|
|
depth: number
|
|
): SerializableRef<T> {
|
|
const clonedElements = value.map((element) =>
|
|
createSerializable(element, shouldPersistRemote, depth + 1)
|
|
);
|
|
const clone = WorkletsModule.createSerializableArray(
|
|
clonedElements,
|
|
shouldPersistRemote
|
|
) as SerializableRef<T>;
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
freezeObjectInDev(value);
|
|
return clone;
|
|
}
|
|
|
|
function cloneRemoteFunction<TArgs extends unknown[], TReturn>(
|
|
value: (...args: TArgs) => TReturn
|
|
): SerializableRef<TReturn> {
|
|
const clone = WorkletsModule.createSerializableFunction(value);
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
freezeObjectInDev(value);
|
|
return clone;
|
|
}
|
|
|
|
function cloneHostObject<T extends object>(value: T): SerializableRef<T> {
|
|
// 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<T extends WorkletFunction>(
|
|
value: T,
|
|
shouldPersistRemote: boolean,
|
|
depth: number
|
|
): SerializableRef<T> {
|
|
if (__DEV__) {
|
|
const babelVersion = (value as WorkletFunction).__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 as WorkletFunction).__stackDetails!
|
|
);
|
|
}
|
|
if ((value as WorkletFunction).__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 as WorkletFunction).__stackDetails;
|
|
}
|
|
const clonedProps: Record<string, unknown> = 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
|
|
) as SerializableRef<T>;
|
|
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<T extends object>(
|
|
value: T,
|
|
shouldPersistRemote: boolean,
|
|
depth: number
|
|
): SerializableRef<T> {
|
|
const proto = Object.getPrototypeOf(value);
|
|
const clonedProps = cloneObjectProperties(value, shouldPersistRemote, depth);
|
|
const clone = WorkletsModule.createSerializableTurboModuleLike(
|
|
clonedProps,
|
|
proto
|
|
) as SerializableRef<T>;
|
|
return clone;
|
|
}
|
|
|
|
function cloneContextObject<T extends object>(value: T): SerializableRef<T> {
|
|
const workletContextObjectFactory = (value as Record<string, unknown>)
|
|
.__workletContextObjectFactory as () => T;
|
|
const handle = cloneInitializer({
|
|
__init: () => {
|
|
'worklet';
|
|
return workletContextObjectFactory();
|
|
},
|
|
});
|
|
serializableMappingCache.set(value, handle);
|
|
return handle as SerializableRef<T>;
|
|
}
|
|
|
|
function clonePlainJSObject<T extends object>(
|
|
value: T,
|
|
shouldPersistRemote: boolean,
|
|
depth: number
|
|
): SerializableRef<T> {
|
|
const clonedProps: Record<string, unknown> = cloneObjectProperties(
|
|
value,
|
|
shouldPersistRemote,
|
|
depth
|
|
);
|
|
const clone = WorkletsModule.createSerializableObject(
|
|
clonedProps,
|
|
shouldPersistRemote,
|
|
value
|
|
) as SerializableRef<T>;
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
freezeObjectInDev(value);
|
|
return clone;
|
|
}
|
|
|
|
function cloneMap<T extends Map<unknown, unknown>>(
|
|
value: T
|
|
): SerializableRef<T> {
|
|
const clonedKeys: unknown[] = [];
|
|
const clonedValues: unknown[] = [];
|
|
for (const [key, element] of value.entries()) {
|
|
clonedKeys.push(createSerializable(key));
|
|
clonedValues.push(createSerializable(element));
|
|
}
|
|
const clone = WorkletsModule.createSerializableMap(
|
|
clonedKeys,
|
|
clonedValues
|
|
) as SerializableRef<T>;
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
freezeObjectInDev(value);
|
|
return clone;
|
|
}
|
|
|
|
function cloneSet<T extends Set<unknown>>(value: T): SerializableRef<T> {
|
|
const clonedElements: unknown[] = [];
|
|
for (const element of value) {
|
|
clonedElements.push(createSerializable(element));
|
|
}
|
|
const clone = WorkletsModule.createSerializableSet(
|
|
clonedElements
|
|
) as SerializableRef<T>;
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
freezeObjectInDev(value);
|
|
return clone;
|
|
}
|
|
|
|
function cloneRegExp<T extends RegExp>(value: T): SerializableRef<T> {
|
|
const pattern = value.source;
|
|
const flags = value.flags;
|
|
const handle = cloneInitializer({
|
|
__init: () => {
|
|
'worklet';
|
|
return new RegExp(pattern, flags);
|
|
},
|
|
}) as unknown as SerializableRef<T>;
|
|
serializableMappingCache.set(value, handle);
|
|
|
|
return handle;
|
|
}
|
|
|
|
function cloneError<T extends Error>(value: T): SerializableRef<T> {
|
|
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 as unknown as SerializableRef<T>;
|
|
}
|
|
|
|
function cloneArrayBuffer<T extends ArrayBuffer>(
|
|
value: T,
|
|
shouldPersistRemote: boolean
|
|
): SerializableRef<T> {
|
|
const clone = WorkletsModule.createSerializable(
|
|
value,
|
|
shouldPersistRemote,
|
|
value
|
|
);
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
return clone;
|
|
}
|
|
|
|
function cloneArrayBufferView<T extends ArrayBufferView>(
|
|
value: T
|
|
): SerializableRef<T> {
|
|
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 as keyof typeof global];
|
|
if (constructor === undefined) {
|
|
throw new WorkletsError(`Constructor for \`${typeName}\` not found.`);
|
|
}
|
|
return new constructor(buffer);
|
|
},
|
|
}) as unknown as SerializableRef<T>;
|
|
serializableMappingCache.set(value, handle);
|
|
|
|
return handle;
|
|
}
|
|
|
|
function cloneSynchronizable<TValue>(
|
|
value: Synchronizable<TValue>
|
|
): SerializableRef<TValue> {
|
|
serializableMappingCache.set(value);
|
|
return value;
|
|
}
|
|
|
|
function cloneImport<TValue extends WorkletImport>(
|
|
value: TValue
|
|
): SerializableRef<TValue> {
|
|
const { source, imported } = value.__bundleData;
|
|
const clone = WorkletsModule.createSerializableImport(source, imported);
|
|
|
|
serializableMappingCache.set(value, clone);
|
|
serializableMappingCache.set(clone);
|
|
|
|
return clone as SerializableRef<TValue>;
|
|
}
|
|
|
|
function inaccessibleObject<T extends object>(value: T): SerializableRef<T> {
|
|
// 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<T>(INACCESSIBLE_OBJECT as T);
|
|
serializableMappingCache.set(value, clone);
|
|
return clone;
|
|
}
|
|
|
|
const WORKLET_CODE_THRESHOLD = 255;
|
|
|
|
function getWorkletCode(value: WorkletFunction) {
|
|
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: FlatSerializableRef<T>;
|
|
};
|
|
|
|
function isRemoteFunction<T>(value: {
|
|
__remoteFunction?: unknown;
|
|
}): value is RemoteFunction<T> {
|
|
'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<T extends object>(value: T) {
|
|
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-serializable
|
|
for more details.`
|
|
);
|
|
},
|
|
});
|
|
});
|
|
Object.preventExtensions(value);
|
|
}
|
|
|
|
function makeShareableCloneOnUIRecursiveLEGACY<T>(
|
|
value: T
|
|
): FlatSerializableRef<T> {
|
|
'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: T): FlatSerializableRef<T> {
|
|
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
|
|
) as FlatSerializableRef<T>;
|
|
}
|
|
if (isRemoteFunction<T>(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)
|
|
) as FlatSerializableRef<T>;
|
|
}
|
|
if ((value as Record<string, unknown>).__synchronizableRef) {
|
|
return global._createSerializableSynchronizable(
|
|
value
|
|
) as FlatSerializableRef<T>;
|
|
}
|
|
const toAdapt: Record<string, FlatSerializableRef<T>> = {};
|
|
for (const [key, element] of Object.entries(value)) {
|
|
toAdapt[key] = cloneRecursive(element);
|
|
}
|
|
return global._createSerializable(
|
|
toAdapt,
|
|
value
|
|
) as FlatSerializableRef<T>;
|
|
}
|
|
|
|
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
|
|
) as typeof makeShareableCloneOnUIRecursiveLEGACY;
|
|
|
|
function makeShareableJS<T>(value: T): T {
|
|
return value;
|
|
}
|
|
|
|
function makeShareableNative<T extends object>(value: T): T {
|
|
if (serializableMappingCache.get(value)) {
|
|
return value;
|
|
}
|
|
const handle = createSerializable({
|
|
__init: () => {
|
|
'worklet';
|
|
return value;
|
|
},
|
|
});
|
|
serializableMappingCache.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.
|
|
*/
|
|
/** @deprecated This function is no longer supported. */
|
|
export const makeShareable = SHOULD_BE_USE_WEB
|
|
? makeShareableJS
|
|
: makeShareableNative;
|