From 90b4f7a63d4e4d2db140e8bb3db982f7755f3180 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Sun, 30 Aug 2026 10:49:24 +0200 Subject: [PATCH 1/3] Clone ArrayBuffers and views in structuredClone --- .../__tests__/structuredClone-itest.js | 63 +++++++++++++++++++ .../structuredClone/structuredClone.js | 47 ++++++++++++++ 2 files changed, 110 insertions(+) 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..086bf68be5c2 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,69 @@ 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'); + }, + }); + 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..3c85955f0d8a 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -25,8 +25,23 @@ 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 arrayBufferSlice = ArrayBuffer.prototype.slice; // Technically the memory value should be a parameter in // `structuredCloneInternal` but as an optimization we can reuse the same map @@ -95,6 +110,38 @@ function structuredCloneInternal(value: T): T { // Handles complex types (typeof === 'object'). + if (value instanceof ArrayBuffer) { + const result = arrayBufferSlice.call(value, 0); + 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); From 566f17f28b881c34b9fdb7cbc3c3008712ffbd98 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Sun, 30 Aug 2026 18:03:31 +0700 Subject: [PATCH 2/3] Avoid ArrayBuffer species during cloning --- .../__tests__/structuredClone-itest.js | 6 ++++++ .../webapis/structuredClone/structuredClone.js | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 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 086bf68be5c2..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 @@ -215,6 +215,12 @@ describe('structuredClone', () => { 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', { diff --git a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js index 3c85955f0d8a..431c99a277c9 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -41,7 +41,15 @@ const TYPED_ARRAY_CONSTRUCTORS = [ const ObjectPrototype = Object.prototype; // $FlowFixMe[method-unbinding] this is always called with an explicit receiver. -const arrayBufferSlice = ArrayBuffer.prototype.slice; +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 @@ -111,7 +119,8 @@ function structuredCloneInternal(value: T): T { // Handles complex types (typeof === 'object'). if (value instanceof ArrayBuffer) { - const result = arrayBufferSlice.call(value, 0); + 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; From 70658341cbb04508ff5a4c4b3a38bc235422a845 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Sun, 30 Aug 2026 21:03:30 +0700 Subject: [PATCH 3/3] Fix ArrayBuffer byte length getter type --- .../src/private/webapis/structuredClone/structuredClone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js index 431c99a277c9..500864052659 100644 --- a/packages/react-native/src/private/webapis/structuredClone/structuredClone.js +++ b/packages/react-native/src/private/webapis/structuredClone/structuredClone.js @@ -42,7 +42,7 @@ const TYPED_ARRAY_CONSTRUCTORS = [ const ObjectPrototype = Object.prototype; // $FlowFixMe[method-unbinding] this is always called with an explicit receiver. const uint8ArraySet = Uint8Array.prototype.set; -const arrayBufferByteLengthDescriptor = Object.getOwnPropertyDescriptor( +const arrayBufferByteLengthDescriptor = Object.getOwnPropertyDescriptor( ArrayBuffer.prototype, 'byteLength', );