From 58b2f9bbc901d9f57c1fa5afa348f520e08f05a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 29 Sep 2026 08:37:54 -0700 Subject: [PATCH] Expand structuredClone compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Compare React Native structuredClone behavior with ungap/structured-clone and port the applicable upstream coverage. Add support for ArrayBuffer, DataView, typed arrays, boxed BigInt values, Error subclasses, deeply cloned causes, and safe __proto__ data properties. Serialization-only and JSON/lossy tests remain out of scope because React Native exposes a single-step cloning API. The comparison dependency and benchmark cases were used only to collect the results below and are not shipped with React Native. Before — React Native implementation before this change: ### structuredClone (mode 🚀) ### | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | ------------------------ | ----------------- | ---------------- | ---------------------- | ---------------------- | ------- | | 0 | 'clone a string' | '338.62 ± 0.05%' | '330.00 ± 9.00' | '3014547 ± 0.05%' | '3030303 ± 84962' | 2953184 | | 1 | 'clone a basic array' | '1587.6 ± 10.36%' | '1353.0 ± 19.00' | '734465 ± 0.42%' | '739098 ± 10235' | 638616 | | 2 | 'clone a basic object' | '1344.2 ± 8.81%' | '1172.0 ± 20.00' | '828917 ± 0.33%' | '853242 ± 14813' | 743963 | | 3 | 'clone a complex object' | '3370.6 ± 0.60%' | '3255.0 ± 40.00' | '304183 ± 0.02%' | '307220 ± 3822' | 296680 | After — React Native compared with ungap/structured-clone: ### structuredClone (mode 🚀) ### | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | -------------------------------------- | ----------------- | ----------------- | ---------------------- | ---------------------- | ------- | | 0 | 'React Native: clone a string' | '348.44 ± 0.08%' | '331.00 ± 1.00' | '2927181 ± 0.04%' | '3021148 ± 9155' | 2869970 | | 1 | 'ungap: clone a string' | '2037.4 ± 11.17%' | '1592.0 ± 50.00' | '617833 ± 0.09%' | '628141 ± 19947' | 490832 | | 2 | 'React Native: clone a basic array' | '1460.2 ± 8.39%' | '1282.0 ± 20.00' | '775137 ± 0.42%' | '780031 ± 12362' | 684824 | | 3 | 'ungap: clone a basic array' | '5112.8 ± 0.81%' | '4718.0 ± 111.00' | '205586 ± 0.08%' | '211954 ± 5107' | 195589 | | 4 | 'React Native: clone a basic object' | '1373.8 ± 8.58%' | '1152.0 ± 20.00' | '842837 ± 0.46%' | '868056 ± 15337' | 727910 | | 5 | 'ungap: clone a basic object' | '8853.2 ± 0.76%' | '8253.0 ± 141.00' | '117562 ± 0.05%' | '121168 ± 2106' | 112954 | | 6 | 'React Native: clone a complex object' | '3539.4 ± 0.55%' | '3456.0 ± 50.00' | '288368 ± 0.02%' | '289352 ± 4248' | 282537 | | 7 | 'ungap: clone a complex object' | '21498 ± 0.72%' | '20080 ± 301.00' | '48208 ± 0.09%' | '49801 ± 755' | 46516 | The benchmark shows no material regression in the existing implementation and React Native remains faster than ungap/structured-clone across all scenarios. Changelog: [General][Added] - Add ArrayBuffer, DataView, typed array, boxed BigInt, and Error subclass support to structuredClone Reviewed By: javache Differential Revision: D122156629 --- .../__tests__/structuredClone-itest.js | 106 ++++++++ .../structuredClone/structuredClone.js | 255 +++++++++++++++--- 2 files changed, 319 insertions(+), 42 deletions(-) diff --git a/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-itest.js b/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-itest.js index 0bede29eb067..357ba6bfb55c 100644 --- a/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-itest.js +++ b/packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-itest.js @@ -67,6 +67,7 @@ describe('structuredClone', () => { expect(structuredClone(0)).toBe(0); expect(structuredClone(1)).toBe(1); + expect(Object.is(structuredClone(-0), -0)).toBe(true); expect(structuredClone(0n)).toBe(0n); expect(structuredClone(1n)).toBe(1n); @@ -99,6 +100,12 @@ describe('structuredClone', () => { expect(booleanClone).not.toBe(booleanValue); expect(booleanClone).toBeInstanceOf(Boolean); expect(booleanClone.valueOf()).toBe(true); + + const bigintValue = Object(1n); + const bigintClone = structuredClone(bigintValue); + expect(bigintClone).not.toBe(bigintValue); + expect(bigintClone).toBeInstanceOf(BigInt); + expect(bigintClone.valueOf()).toBe(1n); }); it('throws with symbols, functions, WeakMap, WeakSet, Promise', () => { @@ -223,6 +230,58 @@ describe('structuredClone', () => { expect(clone).toEqual(value); }); + it('clones invalid dates', () => { + const value = new Date(NaN); + const clone = structuredClone(value); + expect(clone).not.toBe(value); + expect(clone).toBeInstanceOf(Date); + expect(Number.isNaN(clone.getTime())).toBe(true); + }); + + it('clones ArrayBuffer', () => { + const value = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]).buffer; + const clone = structuredClone(value); + expect(clone).not.toBe(value); + expect(clone).toBeInstanceOf(ArrayBuffer); + expect([...new Uint8Array(clone)]).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }); + + it('clones DataView', () => { + const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]).buffer; + const value = new DataView(buffer); + const clone = structuredClone(value); + expect(clone).not.toBe(value); + expect(clone).toBeInstanceOf(DataView); + expect([...new Uint8Array(clone.buffer)]).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }); + + it('clones typed arrays', () => { + const value = new Uint32Array([1, 2, 3]); + const clone = structuredClone(value); + expect(clone).not.toBe(value); + expect(clone).toBeInstanceOf(Uint32Array); + expect([...clone]).toEqual([1, 2, 3]); + }); + + it('preserves shared buffers between views', () => { + const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]).buffer; + const value = { + typedArray: new Uint16Array(buffer, 2, 2), + dataView: new DataView(buffer, 1, 6), + buffer, + }; + + const clone = structuredClone(value); + expect(clone.buffer).not.toBe(buffer); + expect(clone.dataView.buffer).toBe(clone.buffer); + expect(clone.typedArray.buffer).toBe(clone.buffer); + expect(clone.dataView.byteOffset).toBe(value.dataView.byteOffset); + expect(clone.dataView.byteLength).toBe(value.dataView.byteLength); + expect(clone.typedArray.byteOffset).toBe(value.typedArray.byteOffset); + expect(clone.typedArray.length).toBe(value.typedArray.length); + expect([...clone.typedArray]).toEqual([...value.typedArray]); + }); + it('clones errors', () => { const cause = new Error('cause message'); const value = new Error('error message', {cause}); @@ -261,6 +320,53 @@ describe('structuredClone', () => { expect(structuredClone(value).name).toBe('Error'); }); + it('preserves error subclasses', () => { + expect(structuredClone(new Error('boom'))).toBeInstanceOf(Error); + expect(structuredClone(new EvalError('boom'))).toBeInstanceOf(EvalError); + expect(structuredClone(new RangeError('boom'))).toBeInstanceOf(RangeError); + expect(structuredClone(new ReferenceError('boom'))).toBeInstanceOf( + ReferenceError, + ); + expect(structuredClone(new SyntaxError('boom'))).toBeInstanceOf( + SyntaxError, + ); + expect(structuredClone(new TypeError('boom'))).toBeInstanceOf(TypeError); + expect(structuredClone(new URIError('boom'))).toBeInstanceOf(URIError); + }); + + it('clones aggregate errors', () => { + const innerError = new TypeError('inner'); + const value = new AggregateError([innerError, {foo: 'bar'}], 'outer'); + const clone = structuredClone(value); + const clonedErrors = Array.from(clone.errors); + const clonedInnerError = ensureInstance(clonedErrors[0], TypeError); + + expect(clone).not.toBe(value); + expect(clone).toBeInstanceOf(AggregateError); + expect(clone.message).toBe(value.message); + expect(clone.stack).toBe(value.stack); + expect(clone.errors).not.toBe(value.errors); + expect(clonedInnerError).not.toBe(innerError); + expect(clonedInnerError.message).toBe(innerError.message); + expect(clonedErrors[1]).toEqual({foo: 'bar'}); + }); + + it('clones __proto__ as an own property', () => { + const value = JSON.parse('{"__proto__":{"foo":"bar"}}'); + const clone = structuredClone(value); + const clonedValue = nullthrows( + Object.getOwnPropertyDescriptor(clone, '__proto__'), + ).value; + const originalValue = nullthrows( + Object.getOwnPropertyDescriptor(value, '__proto__'), + ).value; + + expect(clone.foo).toBeUndefined(); + expect(Object.hasOwn(clone, '__proto__')).toBe(true); + expect(clonedValue).not.toBe(originalValue); + expect(clonedValue).toEqual({foo: 'bar'}); + }); + it('clones values deeply', () => { const value = { obj: { diff --git a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js index 89a7f90725be..636043dbe4ef 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -14,16 +14,6 @@ import { isPlatformObject, } from '../webidl/PlatformObjects'; -const VALID_ERROR_NAMES = new Set([ - 'Error', - 'EvalError', - 'RangeError', - 'ReferenceError', - 'SyntaxError', - 'TypeError', - 'URIError', -]); - const BASIC_CONSTRUCTORS = [Number, String, Boolean, Date]; const ObjectPrototype = Object.prototype; @@ -35,7 +25,7 @@ const ObjectPrototype = Object.prototype; // any given point we only have one memory object alive anyway. const memory: Map = new Map(); -function structuredCloneInternal(value: T): T { +function structuredCloneInternal(value: unknown): unknown { // Handles `null` and `undefined`. if (value == null) { return value; @@ -60,21 +50,33 @@ function structuredCloneInternal(value: T): T { } // Handles circular references. - if (memory.has(value)) { - // $FlowExpectedError[incompatible-type] we know memory.get(value) is T - return memory.get(value); + const existingClone = memory.get(value); + if (existingClone !== undefined) { + return existingClone; } // Handles arrays. if (Array.isArray(value)) { - const result = []; + const result: Array = []; memory.set(value, result); - - for (const key of Object.keys(value)) { - result[key] = structuredCloneInternal(value[key]); + const keys = Object.keys(value); + + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + const innerValue = value[key]; + switch (typeof innerValue) { + case 'undefined': + case 'boolean': + case 'number': + case 'string': + case 'bigint': + result[key] = innerValue; + break; + default: + result[key] = structuredCloneInternal(innerValue); + } } - // $FlowExpectedError[incompatible-type] we know result is T return result; } @@ -83,23 +85,73 @@ function structuredCloneInternal(value: T): T { if (Object.getPrototypeOf(value) === ObjectPrototype) { const result = {}; memory.set(value, result); - - for (const key of Object.keys(value)) { - // $FlowExpectedError[prop-missing] - result[key] = structuredCloneInternal(value[key]); + const keys = Object.keys(value); + + if (!Object.hasOwn(value, '__proto__')) { + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + const innerValue = value[key]; + switch (typeof innerValue) { + case 'undefined': + case 'boolean': + case 'number': + case 'string': + case 'bigint': + // $FlowExpectedError[prop-missing] + result[key] = innerValue; + break; + default: + // $FlowExpectedError[prop-missing] + result[key] = structuredCloneInternal(innerValue); + } + } + } else { + for (const key of keys) { + const clonedValue = structuredCloneInternal(value[key]); + if (key === '__proto__') { + defineEnumerableProperty(result, key, clonedValue); + } else { + // $FlowExpectedError[prop-missing] + result[key] = clonedValue; + } + } } - // $FlowExpectedError[incompatible-type] we know result is T return result; } // Handles complex types (typeof === 'object'). + if (value instanceof ArrayBuffer) { + return cloneArrayBuffer(value); + } + + if (value instanceof DataView) { + const result = new DataView( + cloneArrayBuffer(value.buffer), + value.byteOffset, + value.byteLength, + ); + memory.set(value, result); + return result; + } + + if (isTypedArray(value)) { + const result = cloneTypedArray(value); + memory.set(value, result); + return result; + } + + if (value instanceof BigInt) { + const result = Object(value.valueOf()); + memory.set(value, result); + return result; + } + for (const Cls of BASIC_CONSTRUCTORS) { if (value instanceof Cls) { const result = new Cls(value); memory.set(value, result); - // $FlowExpectedError[incompatible-type] we know result is T return result; } } @@ -115,7 +167,6 @@ function structuredCloneInternal(value: T): T { ); } - // $FlowExpectedError[incompatible-type] we know result is T return result; } @@ -127,7 +178,6 @@ function structuredCloneInternal(value: T): T { result.add(structuredCloneInternal(innerValue)); } - // $FlowExpectedError[incompatible-type] we know result is T return result; } @@ -135,7 +185,6 @@ function structuredCloneInternal(value: T): T { const result = new RegExp(value.source, value.flags); memory.set(value, result); - // $FlowExpectedError[incompatible-type] we know result is T return result; } @@ -148,21 +197,25 @@ function structuredCloneInternal(value: T): T { return result; } - if (value instanceof Error) { - const result = value.cause - ? new Error(value.message, {cause: value.cause}) - : new Error(value.message); + if (value instanceof AggregateError) { + const result = new AggregateError([], value.message); memory.set(value, result); - if (VALID_ERROR_NAMES.has(value.name)) { - result.name = value.name; - } else { - result.name = 'Error'; + const errors = structuredCloneInternal(value.errors); + if (!Array.isArray(errors)) { + throw new TypeError('AggregateError errors must be an array'); } + result.errors = errors; + + cloneErrorProperties(value, result); + return result; + } - result.stack = value.stack; + if (value instanceof Error) { + const result = createErrorClone(value); + memory.set(value, result); - // $FlowExpectedError[incompatible-type] we know result is T + cloneErrorProperties(value, result); return result; } @@ -177,18 +230,43 @@ function structuredCloneInternal(value: T): T { // Arbitrary object slow path const result = {}; memory.set(value, result); + const keys = Object.keys(value); // We need to use Object.keys instead of iterating by indices because we // also need to copy arbitrary fields set in the array. - for (const key of Object.keys(value)) { - // $FlowExpectedError[prop-missing] - result[key] = structuredCloneInternal(value[key]); + if (Object.hasOwn(value, '__proto__')) { + for (const key of keys) { + const clonedValue = structuredCloneInternal(value[key]); + if (key === '__proto__') { + defineEnumerableProperty(result, key, clonedValue); + } else { + // $FlowExpectedError[prop-missing] + result[key] = clonedValue; + } + } + } else { + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + // $FlowExpectedError[prop-missing] + result[key] = structuredCloneInternal(value[key]); + } } - // $FlowExpectedError[incompatible-type] we know result is T return result; } +function cloneErrorProperties(value: Error, result: Error): void { + if (Object.hasOwn(value, 'cause')) { + Object.defineProperty(result, 'cause', { + configurable: true, + value: structuredCloneInternal(value.cause), + writable: true, + }); + } + + result.stack = value.stack; +} + /** * Basic implementation of `structuredClone`. * See: @@ -209,6 +287,7 @@ function structuredCloneInternal(value: T): T { */ export default function structuredClone(value: T): T { try { + // $FlowExpectedError[incompatible-type] structured cloning preserves the value's serializable type. return structuredCloneInternal(value); } finally { memory.clear(); @@ -217,6 +296,98 @@ export default function structuredClone(value: T): T { const NON_SERIALIZABLE_OBJECT_KEY = Symbol('nonSerializableObject'); +function cloneArrayBuffer(value: ArrayBuffer): ArrayBuffer { + const existingClone = memory.get(value); + if (existingClone instanceof ArrayBuffer) { + return existingClone; + } + + const result = value.slice(0); + memory.set(value, result); + return result; +} + +function cloneTypedArray(value: $TypedArray): $TypedArray { + const buffer = cloneArrayBuffer(value.buffer); + const {byteOffset, length} = value; + + if (value instanceof Int8Array) { + return new Int8Array(buffer, byteOffset, length); + } + if (value instanceof Uint8Array) { + return new Uint8Array(buffer, byteOffset, length); + } + if (value instanceof Uint8ClampedArray) { + return new Uint8ClampedArray(buffer, byteOffset, length); + } + if (value instanceof Int16Array) { + return new Int16Array(buffer, byteOffset, length); + } + if (value instanceof Uint16Array) { + return new Uint16Array(buffer, byteOffset, length); + } + if (value instanceof Int32Array) { + return new Int32Array(buffer, byteOffset, length); + } + if (value instanceof Uint32Array) { + return new Uint32Array(buffer, byteOffset, length); + } + if (typeof Float16Array !== 'undefined' && value instanceof Float16Array) { + return new Float16Array(buffer, byteOffset, length); + } + if (value instanceof Float32Array) { + return new Float32Array(buffer, byteOffset, length); + } + if (value instanceof Float64Array) { + return new Float64Array(buffer, byteOffset, length); + } + if (value instanceof BigInt64Array) { + return new BigInt64Array(buffer, byteOffset, length); + } + if (value instanceof BigUint64Array) { + return new BigUint64Array(buffer, byteOffset, length); + } + + throw new TypeError('Unsupported typed array'); +} + +function defineEnumerableProperty( + target: interface {}, + key: string, + value: unknown, +): void { + Object.defineProperty(target, key, { + configurable: true, + enumerable: true, + value, + writable: true, + }); +} + +declare function isTypedArray(value: unknown): implies value is $TypedArray; +function isTypedArray(value: unknown): boolean { + return ArrayBuffer.isView(value) && !(value instanceof DataView); +} + +function createErrorClone(value: Error): Error { + switch (value.name) { + case 'EvalError': + return new EvalError(value.message); + case 'RangeError': + return new RangeError(value.message); + case 'ReferenceError': + return new ReferenceError(value.message); + case 'SyntaxError': + return new SyntaxError(value.message); + case 'TypeError': + return new TypeError(value.message); + case 'URIError': + return new URIError(value.message); + default: + return new Error(value.message); + } +} + function isNonSerializableObject(obj: T): boolean { // $FlowExpectedError[invalid-in-lhs] return NON_SERIALIZABLE_OBJECT_KEY in obj;