UNPKG

15.3 kBPlain TextView Raw
1'use strict';
2import NativeReanimatedModule from './NativeReanimated';
3import { isWorkletFunction } from './commonTypes';
4import type {
5 ShareableRef,
6 FlatShareableRef,
7 WorkletFunction,
8} from './commonTypes';
9import { shouldBeUseWeb } from './PlatformChecker';
10import { registerWorkletStackDetails } from './errors';
11import { jsVersion } from './platform-specific/jsVersion';
12import {
13 shareableMappingCache,
14 shareableMappingFlag,
15} from './shareableMappingCache';
16
17// for web/chrome debugger/jest environments this file provides a stub implementation
18// where no shareable references are used. Instead, the objects themselves are used
19// instead of shareable references, because of the fact that we don't have to deal with
20// runnning the code on separate VMs.
21const SHOULD_BE_USE_WEB = shouldBeUseWeb();
22
23const MAGIC_KEY = 'REANIMATED_MAGIC_KEY';
24
25function isHostObject(value: NonNullable<object>) {
26 'worklet';
27 // We could use JSI to determine whether an object is a host object, however
28 // the below workaround works well and is way faster than an additional JSI call.
29 // We use the fact that host objects have broken implementation of `hasOwnProperty`
30 // and hence return true for all `in` checks regardless of the key we ask for.
31 return MAGIC_KEY in value;
32}
33
34function isPlainJSObject(object: object) {
35 return Object.getPrototypeOf(object) === Object.prototype;
36}
37
38// The below object is used as a replacement for objects that cannot be transferred
39// as shareable values. In makeShareableCloneRecursive we detect if an object is of
40// a plain Object.prototype and only allow such objects to be transferred. This lets
41// us avoid all sorts of react internals from leaking into the UI runtime. To make it
42// possible to catch errors when someone actually tries to access such object on the UI
43// runtime, we use the below Proxy object which is instantiated on the UI runtime and
44// throws whenever someone tries to access its fields.
45const INACCESSIBLE_OBJECT = {
46 __init: () => {
47 'worklet';
48 return new Proxy(
49 {},
50 {
51 get: (_: unknown, prop: string | symbol) => {
52 if (
53 prop === '_isReanimatedSharedValue' ||
54 prop === '__remoteFunction'
55 ) {
56 // not very happy about this check here, but we need to allow for
57 // "inaccessible" objects to be tested with isSharedValue check
58 // as it is being used in the mappers when extracting inputs recursively
59 // as well as with isRemoteFunction when cloning objects recursively.
60 // Apparently we can't check if a key exists there as HostObjects always
61 // return true for such tests, so the only possibility for us is to
62 // actually access that key and see if it is set to true. We therefore
63 // need to allow for this key to be accessed here.
64 return false;
65 }
66 throw new Error(
67 `[Reanimated] Trying to access property \`${String(
68 prop
69 )}\` of an object which cannot be sent to the UI runtime.`
70 );
71 },
72 set: () => {
73 throw new Error(
74 '[Reanimated] Trying to write to an object which cannot be sent to the UI runtime.'
75 );
76 },
77 }
78 );
79 },
80};
81
82const VALID_ARRAY_VIEWS_NAMES = [
83 'Int8Array',
84 'Uint8Array',
85 'Uint8ClampedArray',
86 'Int16Array',
87 'Uint16Array',
88 'Int32Array',
89 'Uint32Array',
90 'Float32Array',
91 'Float64Array',
92 'BigInt64Array',
93 'BigUint64Array',
94 'DataView',
95];
96
97const DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD = 30;
98// Below variable stores object that we process in makeShareableCloneRecursive at the specified depth.
99// We use it to check if later on the function reenters with the same object
100let processedObjectAtThresholdDepth: unknown;
101
102export function makeShareableCloneRecursive<T>(
103 value: any,
104 shouldPersistRemote = false,
105 depth = 0
106): ShareableRef<T> {
107 if (SHOULD_BE_USE_WEB) {
108 return value;
109 }
110 if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) {
111 // if we reach certain recursion depth we suspect that we are dealing with a cyclic object.
112 // this type of objects are not supported and cannot be trasferred as shareable, so we
113 // implement a simple detection mechanism that remembers the value at a given depth and
114 // tests whether we try reenter this method later on with the same value. If that happens
115 // we throw an appropriate error.
116 if (depth === DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) {
117 processedObjectAtThresholdDepth = value;
118 } else if (value === processedObjectAtThresholdDepth) {
119 throw new Error(
120 '[Reanimated] Trying to convert a cyclic object to a shareable. This is not supported.'
121 );
122 }
123 } else {
124 processedObjectAtThresholdDepth = undefined;
125 }
126 // This one actually may be worth to be moved to c++, we also need similar logic to run on the UI thread
127 const type = typeof value;
128 const isTypeObject = type === 'object';
129 const isTypeFunction = type === 'function';
130 if ((isTypeObject || isTypeFunction) && value !== null) {
131 const cached = shareableMappingCache.get(value);
132 if (cached === shareableMappingFlag) {
133 return value;
134 } else if (cached !== undefined) {
135 return cached as ShareableRef<T>;
136 } else {
137 let toAdapt: any;
138 if (Array.isArray(value)) {
139 toAdapt = value.map((element) =>
140 makeShareableCloneRecursive(element, shouldPersistRemote, depth + 1)
141 );
142 freezeObjectIfDev(value);
143 } else if (isTypeFunction && !isWorkletFunction(value)) {
144 // this is a remote function
145 toAdapt = value;
146 freezeObjectIfDev(value);
147 } else if (isHostObject(value)) {
148 // for host objects we pass the reference to the object as shareable and
149 // then recreate new host object wrapping the same instance on the UI thread.
150 // there is no point of iterating over keys as we do for regular objects.
151 toAdapt = value;
152 } else if (isPlainJSObject(value) || isTypeFunction) {
153 toAdapt = {};
154 if (isWorkletFunction(value)) {
155 if (__DEV__) {
156 const babelVersion = value.__initData.version;
157 if (babelVersion !== undefined && babelVersion !== jsVersion) {
158 throw new Error(`[Reanimated] Mismatch between JavaScript code version and Reanimated Babel plugin version (${jsVersion} vs. ${babelVersion}).
159See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-javascript-code-version-and-reanimated-babel-plugin-version\` for more details.
160Offending code was: \`${getWorkletCode(value)}\``);
161 }
162 registerWorkletStackDetails(
163 value.__workletHash,
164 value.__stackDetails!
165 );
166 }
167 if (value.__stackDetails) {
168 // `Error` type of value cannot be copied to the UI thread, so we
169 // remove it after we handled it in dev mode or delete it to ignore it in production mode.
170 // Not removing this would cause an infinite loop in production mode and it just
171 // seems more elegant to handle it this way.
172 delete value.__stackDetails;
173 }
174 // to save on transferring static __initData field of worklet structure
175 // we request shareable value to persist its UI counterpart. This means
176 // that the __initData field that contains long strings represeting the
177 // worklet code, source map, and location, will always be
178 // serialized/deserialized once.
179 toAdapt.__initData = makeShareableCloneRecursive(
180 value.__initData,
181 true,
182 depth + 1
183 );
184 }
185
186 for (const [key, element] of Object.entries(value)) {
187 if (key === '__initData' && toAdapt.__initData !== undefined) {
188 continue;
189 }
190 toAdapt[key] = makeShareableCloneRecursive(
191 element,
192 shouldPersistRemote,
193 depth + 1
194 );
195 }
196 freezeObjectIfDev(value);
197 } else if (value instanceof RegExp) {
198 const pattern = value.source;
199 const flags = value.flags;
200 const handle = makeShareableCloneRecursive({
201 __init: () => {
202 'worklet';
203 return new RegExp(pattern, flags);
204 },
205 });
206 shareableMappingCache.set(value, handle);
207 return handle as ShareableRef<T>;
208 } else if (value instanceof Error) {
209 const { name, message, stack } = value;
210 const handle = makeShareableCloneRecursive({
211 __init: () => {
212 'worklet';
213 const error = new Error();
214 error.name = name;
215 error.message = message;
216 error.stack = stack;
217 return error;
218 },
219 });
220 shareableMappingCache.set(value, handle);
221 return handle as ShareableRef<T>;
222 } else if (value instanceof ArrayBuffer) {
223 toAdapt = value;
224 } else if (ArrayBuffer.isView(value)) {
225 // typed array (e.g. Int32Array, Uint8ClampedArray) or DataView
226 const buffer = value.buffer;
227 const typeName = value.constructor.name;
228 const handle = makeShareableCloneRecursive({
229 __init: () => {
230 'worklet';
231 if (!VALID_ARRAY_VIEWS_NAMES.includes(typeName)) {
232 throw new Error(
233 `[Reanimated] Invalid array view name \`${typeName}\`.`
234 );
235 }
236 const constructor = global[typeName as keyof typeof global];
237 if (constructor === undefined) {
238 throw new Error(
239 `[Reanimated] Constructor for \`${typeName}\` not found.`
240 );
241 }
242 return new constructor(buffer);
243 },
244 });
245 shareableMappingCache.set(value, handle);
246 return handle as ShareableRef<T>;
247 } else {
248 // This is reached for object types that are not of plain Object.prototype.
249 // We don't support such objects from being transferred as shareables to
250 // the UI runtime and hence we replace them with "inaccessible object"
251 // which is implemented as a Proxy object that throws on any attempt
252 // of accessing its fields. We argue that such objects can sometimes leak
253 // as attributes of objects being captured by worklets but should never
254 // be used on the UI runtime regardless. If they are being accessed, the user
255 // will get an appropriate error message.
256 const inaccessibleObject =
257 makeShareableCloneRecursive<T>(INACCESSIBLE_OBJECT);
258 shareableMappingCache.set(value, inaccessibleObject);
259 return inaccessibleObject;
260 }
261 const adapted = NativeReanimatedModule.makeShareableClone(
262 toAdapt,
263 shouldPersistRemote,
264 value
265 );
266 shareableMappingCache.set(value, adapted);
267 shareableMappingCache.set(adapted);
268 return adapted;
269 }
270 }
271 return NativeReanimatedModule.makeShareableClone(
272 value,
273 shouldPersistRemote,
274 undefined
275 );
276}
277
278const WORKLET_CODE_THRESHOLD = 255;
279
280function getWorkletCode(value: WorkletFunction) {
281 // @ts-ignore this is fine
282 const code = value?.__initData?.code;
283 if (!code) {
284 return 'unknown';
285 }
286 if (code.length > WORKLET_CODE_THRESHOLD) {
287 return `${code.substring(0, WORKLET_CODE_THRESHOLD)}...`;
288 }
289 return code;
290}
291
292type RemoteFunction<T> = {
293 __remoteFunction: FlatShareableRef<T>;
294};
295
296function isRemoteFunction<T>(value: {
297 __remoteFunction?: unknown;
298}): value is RemoteFunction<T> {
299 'worklet';
300 return !!value.__remoteFunction;
301}
302
303/**
304 * We freeze
305 * - arrays,
306 * - remote functions,
307 * - plain JS objects,
308 *
309 * that are transformed to a shareable with a meaningful warning.
310 * This should help detect issues when someone modifies data after it's been converted.
311 * Meaning that they may be doing a faulty assumption in their
312 * code expecting that the updates are going to automatically propagate to
313 * the object sent to the UI thread. If the user really wants some objects
314 * to be mutable they should use shared values instead.
315 */
316function freezeObjectIfDev<T extends object>(value: T) {
317 if (!__DEV__) {
318 return;
319 }
320 Object.entries(value).forEach(([key, element]) => {
321 const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
322 if (!descriptor.configurable) {
323 return;
324 }
325 Object.defineProperty(value, key, {
326 get() {
327 return element;
328 },
329 set() {
330 console.warn(
331 `[Reanimated] Tried to modify key \`${key}\` of an object which has been already passed to a worklet. See
332https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-shareable
333for more details.`
334 );
335 },
336 });
337 });
338 Object.preventExtensions(value);
339}
340
341export function makeShareableCloneOnUIRecursive<T>(
342 value: T
343): FlatShareableRef<T> {
344 'worklet';
345 if (SHOULD_BE_USE_WEB) {
346 // @ts-ignore web is an interesting place where we don't run a secondary VM on the UI thread
347 // see more details in the comment where USE_STUB_IMPLEMENTATION is defined.
348 return value;
349 }
350 // eslint-disable-next-line @typescript-eslint/no-shadow
351 function cloneRecursive(value: T): FlatShareableRef<T> {
352 if (
353 (typeof value === 'object' && value !== null) ||
354 typeof value === 'function'
355 ) {
356 if (isHostObject(value)) {
357 // We call `_makeShareableClone` to wrap the provided HostObject
358 // inside ShareableJSRef.
359 return global._makeShareableClone(
360 value,
361 undefined
362 ) as FlatShareableRef<T>;
363 }
364 if (isRemoteFunction<T>(value)) {
365 // RemoteFunctions are created by us therefore they are
366 // a Shareable out of the box and there is no need to
367 // call `_makeShareableClone`.
368 return value.__remoteFunction;
369 }
370 if (Array.isArray(value)) {
371 return global._makeShareableClone(
372 value.map(cloneRecursive),
373 undefined
374 ) as FlatShareableRef<T>;
375 }
376 const toAdapt: Record<string, FlatShareableRef<T>> = {};
377 for (const [key, element] of Object.entries(value)) {
378 toAdapt[key] = cloneRecursive(element);
379 }
380 return global._makeShareableClone(toAdapt, value) as FlatShareableRef<T>;
381 }
382 return global._makeShareableClone(value, undefined);
383 }
384 return cloneRecursive(value);
385}
386
387function makeShareableJS<T extends object>(value: T): T {
388 return value;
389}
390
391function makeShareableNative<T extends object>(value: T): T {
392 if (shareableMappingCache.get(value)) {
393 return value;
394 }
395 const handle = makeShareableCloneRecursive({
396 __init: () => {
397 'worklet';
398 return value;
399 },
400 });
401 shareableMappingCache.set(value, handle);
402 return value;
403}
404
405/**
406 * This function creates a value on UI with persistent state - changes to it on the UI
407 * thread will be seen by all worklets. Use it when you want to create a value
408 * that is read and written only on the UI thread.
409 */
410export const makeShareable = SHOULD_BE_USE_WEB
411 ? makeShareableJS
412 : makeShareableNative;