1 | 'use strict';
|
2 | import NativeReanimatedModule from './NativeReanimated';
|
3 | import { isWorkletFunction } from './commonTypes';
|
4 | import type {
|
5 | ShareableRef,
|
6 | FlatShareableRef,
|
7 | WorkletFunction,
|
8 | } from './commonTypes';
|
9 | import { shouldBeUseWeb } from './PlatformChecker';
|
10 | import { registerWorkletStackDetails } from './errors';
|
11 | import { jsVersion } from './platform-specific/jsVersion';
|
12 | import {
|
13 | shareableMappingCache,
|
14 | shareableMappingFlag,
|
15 | } from './shareableMappingCache';
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | const SHOULD_BE_USE_WEB = shouldBeUseWeb();
|
22 |
|
23 | const MAGIC_KEY = 'REANIMATED_MAGIC_KEY';
|
24 |
|
25 | function isHostObject(value: NonNullable<object>) {
|
26 | 'worklet';
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | return MAGIC_KEY in value;
|
32 | }
|
33 |
|
34 | function isPlainJSObject(object: object) {
|
35 | return Object.getPrototypeOf(object) === Object.prototype;
|
36 | }
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | const 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 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
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 |
|
82 | const 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 |
|
97 | const DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD = 30;
|
98 |
|
99 |
|
100 | let processedObjectAtThresholdDepth: unknown;
|
101 |
|
102 | export 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 |
|
112 |
|
113 |
|
114 |
|
115 |
|
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 |
|
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 |
|
145 | toAdapt = value;
|
146 | freezeObjectIfDev(value);
|
147 | } else if (isHostObject(value)) {
|
148 |
|
149 |
|
150 |
|
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}).
|
159 | See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-javascript-code-version-and-reanimated-babel-plugin-version\` for more details.
|
160 | Offending code was: \`${getWorkletCode(value)}\``);
|
161 | }
|
162 | registerWorkletStackDetails(
|
163 | value.__workletHash,
|
164 | value.__stackDetails!
|
165 | );
|
166 | }
|
167 | if (value.__stackDetails) {
|
168 |
|
169 |
|
170 |
|
171 |
|
172 | delete value.__stackDetails;
|
173 | }
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
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 |
|
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 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
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 |
|
278 | const WORKLET_CODE_THRESHOLD = 255;
|
279 |
|
280 | function getWorkletCode(value: WorkletFunction) {
|
281 |
|
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 |
|
292 | type RemoteFunction<T> = {
|
293 | __remoteFunction: FlatShareableRef<T>;
|
294 | };
|
295 |
|
296 | function isRemoteFunction<T>(value: {
|
297 | __remoteFunction?: unknown;
|
298 | }): value is RemoteFunction<T> {
|
299 | 'worklet';
|
300 | return !!value.__remoteFunction;
|
301 | }
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | function 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
|
332 | https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-shareable
|
333 | for more details.`
|
334 | );
|
335 | },
|
336 | });
|
337 | });
|
338 | Object.preventExtensions(value);
|
339 | }
|
340 |
|
341 | export function makeShareableCloneOnUIRecursive<T>(
|
342 | value: T
|
343 | ): FlatShareableRef<T> {
|
344 | 'worklet';
|
345 | if (SHOULD_BE_USE_WEB) {
|
346 |
|
347 |
|
348 | return value;
|
349 | }
|
350 |
|
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 |
|
358 |
|
359 | return global._makeShareableClone(
|
360 | value,
|
361 | undefined
|
362 | ) as FlatShareableRef<T>;
|
363 | }
|
364 | if (isRemoteFunction<T>(value)) {
|
365 |
|
366 |
|
367 |
|
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 |
|
387 | function makeShareableJS<T extends object>(value: T): T {
|
388 | return value;
|
389 | }
|
390 |
|
391 | function 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 |
|
407 |
|
408 |
|
409 |
|
410 | export const makeShareable = SHOULD_BE_USE_WEB
|
411 | ? makeShareableJS
|
412 | : makeShareableNative;
|