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 402e055048da..975320d27851 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 @@ -206,6 +206,75 @@ describe('structuredClone', () => { expect(clone).toEqual(value); }); + it('clones ArrayBuffers and views while preserving shared buffers', () => { + const buffer = new ArrayBuffer(6); + new Uint8Array(buffer).set([0, 1, 2, 3, 4, 5]); + // $FlowExpectedError[cannot-write] this intentionally shadows the method. + Object.defineProperty(buffer, 'slice', { + value() { + throw new Error('The clone must use the ArrayBuffer intrinsic'); + }, + }); + // $FlowExpectedError[cannot-write] this intentionally shadows the constructor. + Object.defineProperty(buffer, 'constructor', { + get() { + throw new Error('The clone must not consult ArrayBuffer species'); + }, + }); + const typedArray = new Uint8Array(buffer, 1, 3); + // $FlowExpectedError[prop-missing] this intentionally shadows the constructor. + Object.defineProperty(typedArray, 'constructor', { + value() { + throw new Error('The clone must use a typed array intrinsic'); + }, + }); + const value = { + buffer, + typedArray, + dataView: new DataView(buffer, 2, 2), + }; + + const clone = structuredClone(value); + + expect(clone.buffer).not.toBe(buffer); + expect(clone.buffer).toBeInstanceOf(ArrayBuffer); + expect(clone.typedArray).toBeInstanceOf(Uint8Array); + expect(clone.dataView).toBeInstanceOf(DataView); + expect(clone.typedArray.buffer).toBe(clone.buffer); + expect(clone.dataView.buffer).toBe(clone.buffer); + expect(Array.from(new Uint8Array(clone.buffer))).toEqual([ + 0, 1, 2, 3, 4, 5, + ]); + expect(Array.from(clone.typedArray)).toEqual([1, 2, 3]); + expect(clone.typedArray.byteOffset).toBe(1); + expect(clone.dataView.byteOffset).toBe(2); + expect(clone.dataView.byteLength).toBe(2); + }); + + it('clones every typed array kind', () => { + const values: Array<$FlowFixMe> = [ + new Int8Array([-1, 2]), + new Uint8Array([1, 2]), + new Uint8ClampedArray([-1, 300]), + new Int16Array([-300, 300]), + new Uint16Array([0, 65535]), + new Int32Array([-100000, 100000]), + new Uint32Array([0, 4000000000]), + new Float32Array([1.5, -2.25]), + new Float64Array([Math.PI, -Math.E]), + new BigInt64Array([-1n, 2n]), + new BigUint64Array([1n, 2n]), + ]; + + for (const value of values) { + const clone = structuredClone(value); + + expect(clone).not.toBe(value); + expect(clone.constructor).toBe(value.constructor); + expect(Array.from(clone)).toEqual(Array.from(value)); + } + }); + it('clones errors', () => { const cause = new Error('cause message'); const value = new Error('error message', {cause}); diff --git a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js index 89a7f90725be..500864052659 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -25,8 +25,31 @@ const VALID_ERROR_NAMES = new Set([ ]); const BASIC_CONSTRUCTORS = [Number, String, Boolean, Date]; +const TYPED_ARRAY_CONSTRUCTORS = [ + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + BigInt64Array, + BigUint64Array, +]; const ObjectPrototype = Object.prototype; +// $FlowFixMe[method-unbinding] this is always called with an explicit receiver. +const uint8ArraySet = Uint8Array.prototype.set; +const arrayBufferByteLengthDescriptor = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + 'byteLength', +); +if (arrayBufferByteLengthDescriptor?.get == null) { + throw new Error('ArrayBuffer byteLength getter is required'); +} +const arrayBufferByteLengthGetter = arrayBufferByteLengthDescriptor.get; // Technically the memory value should be a parameter in // `structuredCloneInternal` but as an optimization we can reuse the same map @@ -95,6 +118,39 @@ function structuredCloneInternal(value: T): T { // Handles complex types (typeof === 'object'). + if (value instanceof ArrayBuffer) { + const result = new ArrayBuffer(arrayBufferByteLengthGetter.call(value)); + uint8ArraySet.call(new Uint8Array(result), new Uint8Array(value)); + memory.set(value, result); + // $FlowExpectedError[incompatible-type] we know result is T + return result; + } + + if (ArrayBuffer.isView(value)) { + const view = value as $FlowFixMe; + const buffer = structuredCloneInternal(view.buffer); + let result; + if (view instanceof DataView) { + result = new DataView(buffer, view.byteOffset, view.byteLength); + } else { + for (const Cls of TYPED_ARRAY_CONSTRUCTORS) { + if (view instanceof Cls) { + result = new Cls(buffer, view.byteOffset, view.length); + break; + } + } + if (result == null) { + throw new DOMException( + "Failed to execute 'structuredClone' on 'Window': ArrayBuffer view could not be cloned.", + 'DataCloneError', + ); + } + } + memory.set(value, result); + // $FlowExpectedError[incompatible-type] we know result is T + return result; + } + for (const Cls of BASIC_CONSTRUCTORS) { if (value instanceof Cls) { const result = new Cls(value);