Skip to content

Commit 5dd4891

Browse files
rubennortefacebook-github-bot
authored andcommitted
Handle platform objects in structuredClone (#51257)
Summary: Pull Request resolved: #51257 Changelog: [internal] Fixes the semantics for `structruredClone` when dealing with platform objects (cloning the supported ones and throwing exceptions for the rest). Reviewed By: hoxyq Differential Revision: D74574857 fbshipit-source-id: 71b99e9659cf3fb4bed8f431e9b54d2f9f514706
1 parent 6454d2f commit 5dd4891

2 files changed

Lines changed: 174 additions & 10 deletions

File tree

packages/react-native/src/private/webapis/structuredClone/__tests__/structuredClone-itest.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,24 @@
1111

1212
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1313

14+
import type {HostInstance} from 'react-native';
15+
16+
import ensureInstance from '../../../__tests__/utilities/ensureInstance';
17+
import * as Fantom from '@react-native/fantom';
18+
import * as React from 'react';
19+
import {View} from 'react-native';
20+
import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver';
21+
import setUpMutationObserver from 'react-native/src/private/setup/setUpMutationObserver';
22+
import EventTarget from 'react-native/src/private/webapis/dom/events/EventTarget';
23+
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
1424
import DOMException from 'react-native/src/private/webapis/errors/DOMException';
25+
import IntersectionObserver from 'react-native/src/private/webapis/intersectionobserver/IntersectionObserver';
26+
import MutationObserver from 'react-native/src/private/webapis/mutationobserver/MutationObserver';
1527
import structuredClone from 'react-native/src/private/webapis/structuredClone/structuredClone';
1628

29+
setUpIntersectionObserver();
30+
setUpMutationObserver();
31+
1732
function expectDataCloneError(fn: () => mixed) {
1833
try {
1934
fn();
@@ -289,4 +304,141 @@ describe('structuredClone', () => {
289304
// $FlowExpectedError[prop-missing]
290305
expect([...clone.map.get('set')][0]).toBe(clone.map);
291306
});
307+
308+
describe('platform objects', () => {
309+
describe('serializable platform objects', () => {
310+
it('clones DOMRectReadOnly', () => {
311+
let value = new DOMRectReadOnly(1, 2, 3, 4);
312+
let clone = structuredClone(value);
313+
expect(clone).not.toBe(value);
314+
expect(clone).toBeInstanceOf(DOMRectReadOnly);
315+
expect(clone).toEqual(value);
316+
});
317+
318+
it('clones DOMRect', () => {
319+
let value = new DOMRect(1, 2, 3, 4);
320+
let clone = structuredClone(value);
321+
expect(clone).not.toBe(value);
322+
expect(clone).toBeInstanceOf(DOMRect);
323+
expect(clone).toEqual(value);
324+
});
325+
326+
it('clones DOMException', () => {
327+
const value = new DOMException('error message', 'Error');
328+
const clone = structuredClone(value);
329+
expect(clone).not.toBe(value);
330+
expect(clone).toBeInstanceOf(DOMException);
331+
expect(clone.name).toEqual(value.name);
332+
expect(clone.message).toEqual(value.message);
333+
});
334+
});
335+
336+
describe('non-serializable platform objects', () => {
337+
it('does NOT clone ReadOnlyNode', () => {
338+
const ref = React.createRef<HostInstance>();
339+
const root = Fantom.createRoot();
340+
Fantom.runTask(() => {
341+
root.render(<View ref={ref} />);
342+
});
343+
expect(ref.current).not.toBe(null);
344+
expectDataCloneError(() => structuredClone(ref.current));
345+
});
346+
347+
it('does NOT clone EventTarget', () => {
348+
expectDataCloneError(() => structuredClone(new EventTarget()));
349+
});
350+
351+
it('does NOT clone XMLHttpRequest', () => {
352+
const xhr = new XMLHttpRequest();
353+
expectDataCloneError(() => structuredClone(xhr));
354+
});
355+
356+
it('does NOT clone performance', () => {
357+
expectDataCloneError(() => structuredClone(performance));
358+
});
359+
360+
it('does NOT clone performance.memory', () => {
361+
// $FlowExpectedError[prop-missing]
362+
expectDataCloneError(() => structuredClone(performance.memory));
363+
});
364+
365+
it('does NOT clone performance.rnStartupTiming', () => {
366+
expectDataCloneError(() =>
367+
// $FlowExpectedError[prop-missing]
368+
structuredClone(performance.rnStartupTiming),
369+
);
370+
});
371+
372+
it('does NOT clone PerformanceEntry', () => {
373+
// $FlowExpectedError[prop-missing]
374+
expectDataCloneError(() => structuredClone(performance.mark('foo')));
375+
});
376+
377+
it('does NOT clone IntersectionObserver', () => {
378+
expectDataCloneError(() =>
379+
structuredClone(new IntersectionObserver(() => {})),
380+
);
381+
});
382+
383+
it('does NOT clone IntersectionObserverEntry', () => {
384+
const ref = React.createRef<HostInstance>();
385+
const root = Fantom.createRoot();
386+
Fantom.runTask(() => {
387+
root.render(<View ref={ref} />);
388+
});
389+
expect(ref.current).not.toBe(null);
390+
391+
const entries: Array<mixed> = [];
392+
Fantom.runTask(() => {
393+
const observer = new IntersectionObserver(e => {
394+
entries.push(...e);
395+
});
396+
397+
observer.observe(ensureInstance(ref.current, ReactNativeElement));
398+
399+
Fantom.scheduleTask(() => {
400+
observer.disconnect();
401+
});
402+
});
403+
404+
expectDataCloneError(() => structuredClone(entries[0]));
405+
});
406+
407+
it('does NOT clone MutationObserver', () => {
408+
expectDataCloneError(() =>
409+
structuredClone(new MutationObserver(() => {})),
410+
);
411+
});
412+
413+
it('does NOT clone MutationRecord', () => {
414+
const ref = React.createRef<HostInstance>();
415+
const root = Fantom.createRoot();
416+
Fantom.runTask(() => {
417+
root.render(<View ref={ref} />);
418+
});
419+
expect(ref.current).not.toBe(null);
420+
421+
const records: Array<mixed> = [];
422+
Fantom.runTask(() => {
423+
const observer = new MutationObserver(e => {
424+
records.push(...e);
425+
});
426+
427+
observer.observe(ensureInstance(ref.current, ReactNativeElement), {
428+
childList: true,
429+
});
430+
});
431+
432+
Fantom.runTask(() => {
433+
root.render(
434+
<View>
435+
<View />
436+
</View>,
437+
);
438+
});
439+
440+
expectDataCloneError(() => structuredClone(records[0]));
441+
});
442+
});
443+
});
292444
});

packages/react-native/src/private/webapis/structuredClone/structuredClone.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
*/
1111

1212
import DOMException from '../errors/DOMException';
13+
import {
14+
getPlatformObjectClone,
15+
isPlatformObject,
16+
} from '../webidl/PlatformObjects';
1317

1418
const VALID_ERROR_NAMES = new Set([
1519
'Error',
@@ -128,6 +132,23 @@ function structuredCloneInternal<T>(value: T): T {
128132
return result;
129133
}
130134

135+
if (value instanceof RegExp) {
136+
const result = new RegExp(value.source, value.flags);
137+
memory.set(value, result);
138+
139+
// $FlowExpectedError[incompatible-return] we know result is T
140+
return result;
141+
}
142+
143+
// We need to check platform objects before `Error` because `DOMException`
144+
// is a platform object AND an `Error` subclass.
145+
const clone = getPlatformObjectClone(value);
146+
if (clone != null) {
147+
const result = clone(value);
148+
memory.set(value, result);
149+
return result;
150+
}
151+
131152
if (value instanceof Error) {
132153
const result = value.cause
133154
? new Error(value.message, {cause: value.cause})
@@ -146,16 +167,8 @@ function structuredCloneInternal<T>(value: T): T {
146167
return result;
147168
}
148169

149-
if (value instanceof RegExp) {
150-
const result = new RegExp(value.source, value.flags);
151-
memory.set(value, result);
152-
153-
// $FlowExpectedError[incompatible-return] we know result is T
154-
return result;
155-
}
156-
157170
// Known non-serializable objects.
158-
if (isNonSerializableObject(value)) {
171+
if (isNonSerializableObject(value) || isPlatformObject(value)) {
159172
throw new DOMException(
160173
`Failed to execute 'structuredClone' on 'Window': ${String(value)} could not be cloned.`,
161174
'DataCloneError',
@@ -194,7 +207,6 @@ function structuredCloneInternal<T>(value: T): T {
194207
*
195208
* Known limitations:
196209
* - It does not support transfering values.
197-
* - it does not support cloning platform objects like `DOMRect` and `DOMException`.
198210
*/
199211
export default function structuredClone<T>(value: T): T {
200212
try {

0 commit comments

Comments
 (0)