main
js 2,488 lines 70.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 import fs from 'fs';
14 import os from 'os';
15 import path from 'path';
16 import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
17
18 let clientExports;
19 let webpackMap;
20 let webpackModules;
21 let webpackModuleLoading;
22 let React;
23 let ReactDOMServer;
24 let ReactDOMFizzStatic;
25 let ReactServer;
26 let ReactServerDOMServer;
27 let ReactServerDOMStaticServer;
28 let ReactServerDOMClient;
29 let Stream;
30 let use;
31 let assertConsoleErrorDev;
32 let serverAct;
33
34 // We test pass-through without encoding strings but it should work without it too.
35 const streamOptions = {
36 objectMode: true,
37 };
38
39 describe('ReactFlightDOMNode', () => {
40 beforeEach(() => {
41 jest.resetModules();
42
43 patchSetImmediate();
44 serverAct = require('internal-test-utils').serverAct;
45
46 // Simulate the condition resolution
47 jest.mock('react', () => require('react/react.react-server'));
48 jest.mock('react-server-dom-webpack/server', () =>
49 jest.requireActual('react-server-dom-webpack/server.node'),
50 );
51 ReactServer = require('react');
52 ReactServerDOMServer = require('react-server-dom-webpack/server');
53 jest.mock('react-server-dom-webpack/static', () =>
54 jest.requireActual('react-server-dom-webpack/static.node'),
55 );
56 ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
57
58 const WebpackMock = require('./utils/WebpackMock');
59 clientExports = WebpackMock.clientExports;
60 webpackMap = WebpackMock.webpackMap;
61 webpackModules = WebpackMock.webpackModules;
62 webpackModuleLoading = WebpackMock.moduleLoading;
63
64 jest.resetModules();
65 __unmockReact();
66 jest.unmock('react-server-dom-webpack/server');
67 jest.mock('react-server-dom-webpack/client', () =>
68 jest.requireActual('react-server-dom-webpack/client.node'),
69 );
70
71 React = require('react');
72 ReactDOMServer = require('react-dom/server.node');
73 ReactDOMFizzStatic = require('react-dom/static');
74 ReactServerDOMClient = require('react-server-dom-webpack/client');
75 Stream = require('stream');
76 use = React.use;
77
78 const InternalTestUtils = require('internal-test-utils');
79 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
80 });
81
82 function filterStackFrame(filename, functionName) {
83 return (
84 filename !== '' &&
85 !filename.startsWith('node:') &&
86 !filename.includes('node_modules') &&
87 // Filter out our own internal source code since it'll typically be in node_modules
88 (!filename.includes('/packages/') || filename.includes('/__tests__/')) &&
89 !filename.includes('/build/')
90 );
91 }
92
93 const relativeFilename = path.relative(__dirname, __filename);
94
95 function normalizeCodeLocInfo(str, {preserveLocation = false} = {}) {
96 return (
97 str &&
98 str.replace(
99 /^ +(?:at|in) ([\S]+) ([^\n]*)/gm,
100 function (m, name, location) {
101 return (
102 ' in ' +
103 name +
104 (/:\d+:\d+/.test(m)
105 ? preserveLocation
106 ? ' ' + location.replace(__filename, relativeFilename)
107 : ' (at **)'
108 : '')
109 );
110 },
111 )
112 );
113 }
114
115 /**
116 * Removes all stackframes not pointing into this file
117 */
118 function ignoreListStack(str) {
119 if (!str) {
120 return str;
121 }
122
123 let ignoreListedStack = '';
124 const lines = str.split('\n');
125
126 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
127 for (const line of lines) {
128 if (line.indexOf(__filename) === -1) {
129 } else {
130 ignoreListedStack += '\n' + line.replace(__dirname, '.');
131 }
132 }
133
134 return ignoreListedStack;
135 }
136
137 function readResult(stream) {
138 return new Promise((resolve, reject) => {
139 let buffer = '';
140 const writable = new Stream.PassThrough(streamOptions);
141 writable.setEncoding('utf8');
142 writable.on('data', chunk => {
143 buffer += chunk;
144 });
145 writable.on('error', error => {
146 reject(error);
147 });
148 writable.on('end', () => {
149 resolve(buffer);
150 });
151 stream.pipe(writable);
152 });
153 }
154
155 async function readWebResult(webStream: ReadableStream<Uint8Array>) {
156 const reader = webStream.getReader();
157 let result = '';
158 while (true) {
159 const {done, value} = await reader.read();
160 if (done) {
161 return result;
162 }
163 result += Buffer.from(value).toString('utf8');
164 }
165 }
166
167 async function createBufferedUnclosingStream(
168 stream: ReadableStream<Uint8Array>,
169 ): Promise<ReadableStream<Uint8Array>> {
170 const chunks: Array<Uint8Array> = [];
171 const reader = stream.getReader();
172 while (true) {
173 const {done, value} = await reader.read();
174 if (done) {
175 break;
176 } else {
177 chunks.push(value);
178 }
179 }
180
181 let i = 0;
182 return new ReadableStream({
183 async pull(controller) {
184 if (i < chunks.length) {
185 controller.enqueue(chunks[i++]);
186 }
187 },
188 });
189 }
190
191 function createDelayedStream() {
192 let resolveDelayedStream;
193 const promise = new Promise(resolve => (resolveDelayedStream = resolve));
194 const delayedStream = new Stream.Transform({
195 ...streamOptions,
196 transform(chunk, encoding, callback) {
197 // Artificially delay pushing the chunk.
198 promise.then(() => {
199 this.push(chunk);
200 callback();
201 });
202 },
203 });
204 return {delayedStream, resolveDelayedStream};
205 }
206
207 it('should support web streams in node', async () => {
208 function Text({children}) {
209 return <span>{children}</span>;
210 }
211 // Large strings can get encoded differently so we need to test that.
212 const largeString = 'world'.repeat(1000);
213 function HTML() {
214 return (
215 <div>
216 <Text>hello</Text>
217 <Text>{largeString}</Text>
218 </div>
219 );
220 }
221
222 function App() {
223 const model = {
224 html: <HTML />,
225 };
226 return model;
227 }
228
229 const readable = await serverAct(() =>
230 ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
231 );
232 const response = ReactServerDOMClient.createFromReadableStream(readable, {
233 serverConsumerManifest: {
234 moduleMap: null,
235 moduleLoading: null,
236 },
237 });
238 const model = await response;
239 expect(model).toEqual({
240 html: (
241 <div>
242 <span>hello</span>
243 <span>{largeString}</span>
244 </div>
245 ),
246 });
247 });
248
249 it('should allow an alternative module mapping to be used for SSR', async () => {
250 function ClientComponent() {
251 return <span>Client Component</span>;
252 }
253 // The Client build may not have the same IDs as the Server bundles for the same
254 // component.
255 const ClientComponentOnTheClient = clientExports(
256 ClientComponent,
257 123,
258 'path/to/chunk.js',
259 );
260 const ClientComponentOnTheServer = clientExports(ClientComponent);
261
262 // In the SSR bundle this module won't exist. We simulate this by deleting it.
263 const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
264 delete webpackModules[clientId];
265
266 // Instead, we have to provide a translation from the client meta data to the SSR
267 // meta data.
268 const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
269 const translationMap = {
270 [clientId]: {
271 '*': ssrMetadata,
272 },
273 };
274 const serverConsumerManifest = {
275 moduleMap: translationMap,
276 moduleLoading: webpackModuleLoading,
277 };
278
279 function App() {
280 return <ClientComponentOnTheClient />;
281 }
282
283 const stream = await serverAct(() =>
284 ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
285 );
286 const readable = new Stream.PassThrough(streamOptions);
287 let response;
288
289 stream.pipe(readable);
290
291 function ClientRoot() {
292 if (response) return use(response);
293 response = ReactServerDOMClient.createFromNodeStream(
294 readable,
295 serverConsumerManifest,
296 );
297 return use(response);
298 }
299
300 const ssrStream = await serverAct(() =>
301 ReactDOMServer.renderToPipeableStream(<ClientRoot />),
302 );
303 const result = await readResult(ssrStream);
304 expect(result).toEqual(
305 '<script src="/path/to/chunk.js" async=""></script><span>Client Component</span>',
306 );
307 });
308
309 it('should encode long string in a compact format', async () => {
310 const testString = '"\n\t'.repeat(500) + '🙃';
311
312 const stream = await serverAct(() =>
313 ReactServerDOMServer.renderToPipeableStream({
314 text: testString,
315 }),
316 );
317
318 const readable = new Stream.PassThrough(streamOptions);
319
320 const stringResult = readResult(readable);
321 const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
322 moduleMap: {},
323 moduleLoading: webpackModuleLoading,
324 });
325
326 stream.pipe(readable);
327
328 const serializedContent = await stringResult;
329 // The content should be compact an unescaped
330 expect(serializedContent.length).toBeLessThan(2000);
331 expect(serializedContent).not.toContain('\\n');
332 expect(serializedContent).not.toContain('\\t');
333 expect(serializedContent).not.toContain('\\"');
334 expect(serializedContent).toContain('\t');
335
336 const result = await parsedResult;
337 // Should still match the result when parsed
338 expect(result.text).toBe(testString);
339 });
340
341 it('round-trips long multi-byte strings using true UTF-8 byte length', async () => {
342 // Strings >= 1024 chars are emitted out-of-band with a binary length
343 // prefix (`id:T<byteLength>,`). The client reads exactly that many bytes,
344 // so the prefix must be the true UTF-8 byte length. These are three-byte
345 // characters: byte length is 3x the code unit count. A string.length
346 // shortcut for byteLengthOfChunk would undercount and truncate parsing.
347 const testString = ''.repeat(1100);
348
349 const stream = await serverAct(() =>
350 ReactServerDOMServer.renderToPipeableStream({
351 text: testString,
352 }),
353 );
354
355 const readable = new Stream.PassThrough(streamOptions);
356 const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
357 moduleMap: {},
358 moduleLoading: webpackModuleLoading,
359 });
360 stream.pipe(readable);
361
362 const result = await parsedResult;
363 expect(result.text).toBe(testString);
364 });
365
366 it('should be able to serialize any kind of typed array', async () => {
367 const buffer = new Uint8Array([
368 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
369 ]).buffer;
370 const buffers = [
371 buffer,
372 new Int8Array(buffer, 1),
373 new Uint8Array(buffer, 2),
374 new Uint8ClampedArray(buffer, 2),
375 new Int16Array(buffer, 2),
376 new Uint16Array(buffer, 2),
377 new Int32Array(buffer, 4),
378 new Uint32Array(buffer, 4),
379 new Float32Array(buffer, 4),
380 new Float64Array(buffer, 0),
381 new BigInt64Array(buffer, 0),
382 new BigUint64Array(buffer, 0),
383 new DataView(buffer, 3),
384 ];
385 const stream = await serverAct(() =>
386 ReactServerDOMServer.renderToPipeableStream(buffers),
387 );
388 const readable = new Stream.PassThrough(streamOptions);
389 const promise = ReactServerDOMClient.createFromNodeStream(readable, {
390 moduleMap: {},
391 moduleLoading: webpackModuleLoading,
392 });
393 stream.pipe(readable);
394 const result = await promise;
395 expect(result).toEqual(buffers);
396 });
397
398 it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
399 function ClientComponent() {
400 return <span>Client Component</span>;
401 }
402 // The Client build may not have the same IDs as the Server bundles for the same
403 // component.
404 const ClientComponentOnTheClient = clientExports(
405 ClientComponent,
406 123,
407 'path/to/chunk.js',
408 );
409 const ClientComponentOnTheServer = clientExports(ClientComponent);
410
411 // In the SSR bundle this module won't exist. We simulate this by deleting it.
412 const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
413 delete webpackModules[clientId];
414
415 // Instead, we have to provide a translation from the client meta data to the SSR
416 // meta data.
417 const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
418 const translationMap = {
419 [clientId]: {
420 '*': ssrMetadata,
421 },
422 };
423 const serverConsumerManifest = {
424 moduleMap: translationMap,
425 moduleLoading: webpackModuleLoading,
426 };
427
428 function App() {
429 return <ClientComponentOnTheClient />;
430 }
431
432 const stream = await serverAct(() =>
433 ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
434 );
435 const readable = new Stream.PassThrough(streamOptions);
436 let response;
437
438 stream.pipe(readable);
439
440 function ClientRoot() {
441 if (response) return use(response);
442 response = ReactServerDOMClient.createFromNodeStream(
443 readable,
444 serverConsumerManifest,
445 {
446 nonce: 'r4nd0m',
447 },
448 );
449 return use(response);
450 }
451
452 const ssrStream = await serverAct(() =>
453 ReactDOMServer.renderToPipeableStream(<ClientRoot />),
454 );
455 const result = await readResult(ssrStream);
456 expect(result).toEqual(
457 '<script src="/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
458 );
459 });
460
461 it('should cancel the underlying and transported ReadableStreams when we are cancelled', async () => {
462 let controller;
463 let cancelReason;
464 const s = new ReadableStream({
465 start(c) {
466 controller = c;
467 },
468 cancel(r) {
469 cancelReason = r;
470 },
471 });
472
473 const rscStream = await serverAct(() =>
474 ReactServerDOMServer.renderToPipeableStream(
475 s,
476 {},
477 {
478 onError(error) {
479 return error.message;
480 },
481 },
482 ),
483 );
484
485 const readable = new Stream.PassThrough(streamOptions);
486 rscStream.pipe(readable);
487
488 const result = await ReactServerDOMClient.createFromNodeStream(readable, {
489 moduleMap: {},
490 moduleLoading: webpackModuleLoading,
491 });
492 const reader = result.getReader();
493
494 controller.enqueue('hi');
495
496 await serverAct(async () => {
497 // We should be able to read the part we already emitted before the abort
498 expect(await reader.read()).toEqual({
499 value: 'hi',
500 done: false,
501 });
502 });
503
504 const reason = new Error('aborted');
505 readable.destroy(reason);
506
507 await new Promise(resolve => {
508 readable.on('error', () => {
509 resolve();
510 });
511 });
512
513 expect(cancelReason.message).toBe(
514 'The destination stream errored while writing data.',
515 );
516
517 let error = null;
518 try {
519 await reader.read();
520 } catch (x) {
521 error = x;
522 }
523 expect(error).toBe(reason);
524 });
525
526 it('should cancel the underlying and transported ReadableStreams when we abort', async () => {
527 const errors = [];
528 let controller;
529 let cancelReason;
530 const s = new ReadableStream({
531 start(c) {
532 controller = c;
533 },
534 cancel(r) {
535 cancelReason = r;
536 },
537 });
538 const rscStream = await serverAct(() =>
539 ReactServerDOMServer.renderToPipeableStream(
540 s,
541 {},
542 {
543 onError(x) {
544 errors.push(x);
545 return x.message;
546 },
547 },
548 ),
549 );
550
551 const readable = new Stream.PassThrough(streamOptions);
552 rscStream.pipe(readable);
553
554 const result = await ReactServerDOMClient.createFromNodeStream(readable, {
555 moduleMap: {},
556 moduleLoading: webpackModuleLoading,
557 });
558 const reader = result.getReader();
559 controller.enqueue('hi');
560
561 const reason = new Error('aborted');
562 rscStream.abort(reason);
563
564 expect(cancelReason).toBe(reason);
565
566 let error = null;
567 try {
568 await reader.read();
569 } catch (x) {
570 error = x;
571 }
572 expect(error.digest).toBe('aborted');
573 expect(errors).toEqual([reason]);
574 });
575
576 it('can prerender', async () => {
577 let resolveGreeting;
578 const greetingPromise = new Promise(resolve => {
579 resolveGreeting = resolve;
580 });
581
582 function App() {
583 return (
584 <div>
585 <Greeting />
586 </div>
587 );
588 }
589
590 async function Greeting() {
591 await greetingPromise;
592 return 'hello world';
593 }
594
595 const {pendingResult} = await serverAct(async () => {
596 // destructure trick to avoid the act scope from awaiting the returned value
597 return {
598 pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
599 <App />,
600 webpackMap,
601 ),
602 };
603 });
604
605 resolveGreeting();
606 const {prelude} = await pendingResult;
607
608 function ClientRoot({response}) {
609 return use(response);
610 }
611
612 const response = ReactServerDOMClient.createFromNodeStream(prelude, {
613 serverConsumerManifest: {
614 moduleMap: null,
615 moduleLoading: null,
616 },
617 });
618 // Use the SSR render to resolve any lazy elements
619 const ssrStream = await serverAct(() =>
620 ReactDOMServer.renderToPipeableStream(
621 React.createElement(ClientRoot, {response}),
622 ),
623 );
624 // Should still match the result when parsed
625 const result = await readResult(ssrStream);
626 expect(result).toBe('<div>hello world</div>');
627 });
628
629 it('does not propagate abort reasons errors when aborting a prerender', async () => {
630 let resolveGreeting;
631 const greetingPromise = new Promise(resolve => {
632 resolveGreeting = resolve;
633 });
634
635 function App() {
636 return (
637 <div>
638 <ReactServer.Suspense fallback="loading...">
639 <Greeting />
640 </ReactServer.Suspense>
641 </div>
642 );
643 }
644
645 async function Greeting() {
646 await greetingPromise;
647 return 'hello world';
648 }
649
650 const controller = new AbortController();
651 const errors = [];
652 const {pendingResult} = await serverAct(async () => {
653 // destructure trick to avoid the act scope from awaiting the returned value
654 return {
655 pendingResult: ReactServerDOMStaticServer.prerenderToNodeStream(
656 <App />,
657 webpackMap,
658 {
659 signal: controller.signal,
660 onError(err) {
661 errors.push(err);
662 },
663 },
664 ),
665 };
666 });
667
668 controller.abort('boom');
669 resolveGreeting();
670 const {prelude} = await serverAct(() => pendingResult);
671 expect(errors).toEqual([]);
672
673 function ClientRoot({response}) {
674 return use(response);
675 }
676
677 const response = ReactServerDOMClient.createFromNodeStream(prelude, {
678 serverConsumerManifest: {
679 moduleMap: null,
680 moduleLoading: null,
681 },
682 });
683 errors.length = 0;
684 const ssrStream = await serverAct(() =>
685 ReactDOMServer.renderToPipeableStream(
686 React.createElement(ClientRoot, {response}),
687 {
688 onError(error) {
689 errors.push(error);
690 },
691 },
692 ),
693 );
694 ssrStream.abort('bam');
695 expect(errors).toEqual([new Error('Connection closed.')]);
696 // Should still match the result when parsed
697 const result = await readResult(ssrStream);
698 expect(result).toContain('loading...');
699 });
700
701 it('includes source locations in component and owner stacks for halted components', async () => {
702 async function Component() {
703 await new Promise(() => {});
704 return null;
705 }
706
707 function App() {
708 return ReactServer.createElement(
709 'html',
710 null,
711 ReactServer.createElement(
712 'body',
713 null,
714 ReactServer.createElement(
715 ReactServer.Suspense,
716 {fallback: 'Loading...'},
717 ReactServer.createElement(Component, null),
718 ),
719 ),
720 );
721 }
722
723 const errors = [];
724 const serverAbortController = new AbortController();
725 const {pendingResult} = await serverAct(async () => {
726 // destructure trick to avoid the act scope from awaiting the returned value
727 return {
728 pendingResult: ReactServerDOMStaticServer.prerender(
729 ReactServer.createElement(App, null),
730 webpackMap,
731 {
732 signal: serverAbortController.signal,
733 onError(error) {
734 errors.push(error);
735 },
736 },
737 ),
738 };
739 });
740
741 await await serverAct(
742 async () =>
743 new Promise(resolve => {
744 setImmediate(() => {
745 serverAbortController.abort();
746 resolve();
747 });
748 }),
749 );
750
751 const {prelude} = await pendingResult;
752
753 expect(errors).toEqual([]);
754
755 function ClientRoot({response}) {
756 return use(response);
757 }
758
759 const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
760 await createBufferedUnclosingStream(prelude),
761 {
762 serverConsumerManifest: {
763 moduleMap: null,
764 moduleLoading: null,
765 },
766 },
767 );
768
769 let componentStack;
770 let ownerStack;
771
772 const clientAbortController = new AbortController();
773
774 const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
775 React.createElement(ClientRoot, {response: prerenderResponse}),
776 {
777 signal: clientAbortController.signal,
778 onError(error, errorInfo) {
779 componentStack = errorInfo.componentStack;
780 ownerStack = React.captureOwnerStack
781 ? React.captureOwnerStack()
782 : null;
783 },
784 },
785 );
786
787 await await serverAct(
788 async () =>
789 new Promise(resolve => {
790 setImmediate(() => {
791 clientAbortController.abort();
792 resolve();
793 });
794 }),
795 );
796
797 const fizzPrerenderStream = await fizzPrerenderStreamResult;
798 const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
799
800 expect(prerenderHTML).toContain('Loading...');
801
802 if (__DEV__) {
803 expect(normalizeCodeLocInfo(componentStack)).toBe(
804 '\n' +
805 ' in Component' +
806 (gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)\n' : '\n') +
807 ' in Suspense\n' +
808 ' in body\n' +
809 ' in html\n' +
810 ' in App (at **)\n' +
811 ' in ClientRoot (at **)',
812 );
813 } else {
814 expect(normalizeCodeLocInfo(componentStack)).toBe(
815 '\n in Suspense\n' +
816 ' in body\n' +
817 ' in html\n' +
818 ' in ClientRoot (at **)',
819 );
820 }
821
822 if (__DEV__) {
823 if (gate(flags => flags.enableAsyncDebugInfo)) {
824 expect(normalizeCodeLocInfo(ownerStack)).toBe(
825 '\n in Component (at **)\n in App (at **)',
826 );
827 } else {
828 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
829 }
830 } else {
831 expect(ownerStack).toBeNull();
832 }
833 });
834
835 it('includes source locations in component and owner stacks for halted Client components', async () => {
836 function SharedComponent({p1, p2, p3}) {
837 use(p1);
838 use(p2);
839 use(p3);
840 return <div>Hello, Dave!</div>;
841 }
842 const ClientComponentOnTheServer = clientExports(SharedComponent);
843 const ClientComponentOnTheClient = clientExports(
844 SharedComponent,
845 123,
846 'path/to/chunk.js',
847 );
848
849 let resolvePendingPromise;
850 function ServerComponent() {
851 const p1 = Promise.resolve();
852 const p2 = new Promise(resolve => {
853 resolvePendingPromise = value => {
854 p2.status = 'fulfilled';
855 p2.value = value;
856 resolve(value);
857 };
858 });
859 const p3 = new Promise(() => {});
860 return ReactServer.createElement(ClientComponentOnTheClient, {
861 p1: p1,
862 p2: p2,
863 p3: p3,
864 });
865 }
866
867 function App() {
868 return ReactServer.createElement(
869 'html',
870 null,
871 ReactServer.createElement(
872 'body',
873 null,
874 ReactServer.createElement(
875 ReactServer.Suspense,
876 {fallback: 'Loading...'},
877 ReactServer.createElement(ServerComponent, null),
878 ),
879 ),
880 );
881 }
882
883 const errors = [];
884 const rscStream = await serverAct(() =>
885 ReactServerDOMServer.renderToPipeableStream(
886 ReactServer.createElement(App, null),
887 webpackMap,
888 ),
889 );
890
891 const readable = new Stream.PassThrough(streamOptions);
892 rscStream.pipe(readable);
893
894 function ClientRoot({response}) {
895 return use(response);
896 }
897
898 const serverConsumerManifest = {
899 moduleMap: {
900 [webpackMap[ClientComponentOnTheClient.$$id].id]: {
901 '*': webpackMap[ClientComponentOnTheServer.$$id],
902 },
903 },
904 moduleLoading: webpackModuleLoading,
905 };
906
907 expect(errors).toEqual([]);
908
909 function ClientRoot({response}) {
910 return use(response);
911 }
912
913 const response = ReactServerDOMClient.createFromNodeStream(
914 readable,
915 serverConsumerManifest,
916 );
917
918 let componentStack;
919 let ownerStack;
920
921 const clientAbortController = new AbortController();
922
923 const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
924 React.createElement(ClientRoot, {response}),
925 {
926 signal: clientAbortController.signal,
927 onError(error, errorInfo) {
928 componentStack = errorInfo.componentStack;
929 ownerStack = React.captureOwnerStack
930 ? React.captureOwnerStack()
931 : null;
932 },
933 },
934 );
935
936 resolvePendingPromise('custom-instrum-resolve');
937 await serverAct(
938 async () =>
939 new Promise(resolve => {
940 setImmediate(() => {
941 clientAbortController.abort();
942 resolve();
943 });
944 }),
945 );
946
947 const fizzPrerenderStream = await fizzPrerenderStreamResult;
948 const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
949
950 expect(prerenderHTML).toContain('Loading...');
951
952 if (__DEV__) {
953 expect(normalizeCodeLocInfo(componentStack)).toBe(
954 '\n' +
955 ' in SharedComponent (at **)\n' +
956 ' in ServerComponent' +
957 (gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)' : '') +
958 '\n' +
959 ' in Suspense\n' +
960 ' in body\n' +
961 ' in html\n' +
962 ' in App (at **)\n' +
963 ' in ClientRoot (at **)',
964 );
965 } else {
966 expect(normalizeCodeLocInfo(componentStack)).toBe(
967 '\n' +
968 ' in SharedComponent (at **)\n' +
969 ' in Suspense\n' +
970 ' in body\n' +
971 ' in html\n' +
972 ' in ClientRoot (at **)',
973 );
974 }
975
976 if (__DEV__) {
977 expect(ignoreListStack(ownerStack)).toBe(
978 // eslint-disable-next-line react-internal/safe-string-coercion
979 '' +
980 // The concrete location may change as this test is updated.
981 // Just make sure they still point at React.use(p2)
982 (gate(flags => flags.enableAsyncDebugInfo)
983 ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:838:7)'
984 : '') +
985 '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:860:26)' +
986 '\n at App (file://./ReactFlightDOMNode-test.js:877:25)',
987 );
988 } else {
989 expect(ownerStack).toBeNull();
990 }
991 });
992
993 it('includes deeper location for aborted stacks', async () => {
994 async function getData() {
995 const signal = ReactServer.cacheSignal();
996 await new Promise((resolve, reject) => {
997 signal.addEventListener('abort', () => reject(signal.reason));
998 });
999 }
1000
1001 async function thisShouldNotBeInTheStack() {
1002 await new Promise((resolve, reject) => {
1003 resolve();
1004 });
1005 }
1006
1007 async function Component() {
1008 try {
1009 await getData();
1010 } catch (x) {
1011 await thisShouldNotBeInTheStack(); // This is issued after the rejection so should not be included.
1012 }
1013 return null;
1014 }
1015
1016 function App() {
1017 return ReactServer.createElement(
1018 'html',
1019 null,
1020 ReactServer.createElement(
1021 'body',
1022 null,
1023 ReactServer.createElement(
1024 ReactServer.Suspense,
1025 {fallback: 'Loading...'},
1026 ReactServer.createElement(Component, null),
1027 ),
1028 ),
1029 );
1030 }
1031
1032 const errors = [];
1033 const serverAbortController = new AbortController();
1034 const {pendingResult} = await serverAct(async () => {
1035 // destructure trick to avoid the act scope from awaiting the returned value
1036 return {
1037 pendingResult: ReactServerDOMStaticServer.prerender(
1038 ReactServer.createElement(App, null),
1039 webpackMap,
1040 {
1041 signal: serverAbortController.signal,
1042 onError(error) {
1043 errors.push(error);
1044 },
1045 filterStackFrame,
1046 },
1047 ),
1048 };
1049 });
1050
1051 await serverAct(
1052 () =>
1053 new Promise(resolve => {
1054 setImmediate(() => {
1055 serverAbortController.abort();
1056 resolve();
1057 });
1058 }),
1059 );
1060
1061 const {prelude} = await pendingResult;
1062
1063 expect(errors).toEqual([]);
1064
1065 function ClientRoot({response}) {
1066 return use(response);
1067 }
1068
1069 const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
1070 await createBufferedUnclosingStream(prelude),
1071 {
1072 serverConsumerManifest: {
1073 moduleMap: null,
1074 moduleLoading: null,
1075 },
1076 },
1077 );
1078
1079 let componentStack;
1080 let ownerStack;
1081
1082 const clientAbortController = new AbortController();
1083
1084 const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
1085 React.createElement(ClientRoot, {response: prerenderResponse}),
1086 {
1087 signal: clientAbortController.signal,
1088 onError(error, errorInfo) {
1089 componentStack = errorInfo.componentStack;
1090 ownerStack = React.captureOwnerStack
1091 ? React.captureOwnerStack()
1092 : null;
1093 },
1094 },
1095 );
1096
1097 await await serverAct(
1098 async () =>
1099 new Promise(resolve => {
1100 setImmediate(() => {
1101 clientAbortController.abort();
1102 resolve();
1103 });
1104 }),
1105 );
1106
1107 const fizzPrerenderStream = await fizzPrerenderStreamResult;
1108 const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude);
1109
1110 expect(prerenderHTML).toContain('Loading...');
1111
1112 if (__DEV__) {
1113 expect(normalizeCodeLocInfo(componentStack)).toBe(
1114 '\n' +
1115 ' in Component' +
1116 (gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)\n' : '\n') +
1117 ' in Suspense\n' +
1118 ' in body\n' +
1119 ' in html\n' +
1120 ' in App (at **)\n' +
1121 ' in ClientRoot (at **)',
1122 );
1123 } else {
1124 expect(normalizeCodeLocInfo(componentStack)).toBe(
1125 '\n in Suspense\n' +
1126 ' in body\n' +
1127 ' in html\n' +
1128 ' in ClientRoot (at **)',
1129 );
1130 }
1131
1132 if (__DEV__) {
1133 if (gate(flags => flags.enableAsyncDebugInfo)) {
1134 expect(normalizeCodeLocInfo(ownerStack)).toBe(
1135 '' +
1136 '\n in getData (at **)' +
1137 '\n in Component (at **)' +
1138 '\n in App (at **)',
1139 );
1140 } else {
1141 expect(normalizeCodeLocInfo(ownerStack)).toBe(
1142 '' + '\n in App (at **)',
1143 );
1144 }
1145 } else {
1146 expect(ownerStack).toBeNull();
1147 }
1148 });
1149
1150 it('can handle an empty prelude when prerendering', async () => {
1151 function App() {
1152 return null;
1153 }
1154
1155 const serverAbortController = new AbortController();
1156 serverAbortController.abort();
1157 const errors = [];
1158 const {pendingResult} = await serverAct(async () => {
1159 // destructure trick to avoid the act scope from awaiting the returned value
1160 return {
1161 pendingResult: ReactServerDOMStaticServer.prerender(
1162 ReactServer.createElement(App, null),
1163 webpackMap,
1164 {
1165 signal: serverAbortController.signal,
1166 onError(error) {
1167 errors.push(error);
1168 },
1169 },
1170 ),
1171 };
1172 });
1173
1174 expect(errors).toEqual([]);
1175
1176 const {prelude} = await pendingResult;
1177
1178 const reader = prelude.getReader();
1179 while (true) {
1180 const {done} = await reader.read();
1181 if (done) {
1182 break;
1183 }
1184 }
1185
1186 // We don't really have an assertion other than to make sure
1187 // the stream doesn't hang.
1188 });
1189
1190 // @gate __DEV__
1191 it('can transport debug info through a separate debug channel', async () => {
1192 function Thrower() {
1193 throw new Error('ssr-throw');
1194 }
1195
1196 const ClientComponentOnTheClient = clientExports(
1197 Thrower,
1198 123,
1199 'path/to/chunk.js',
1200 );
1201
1202 const ClientComponentOnTheServer = clientExports(Thrower);
1203
1204 function App() {
1205 return ReactServer.createElement(
1206 ReactServer.Suspense,
1207 null,
1208 ReactServer.createElement(ClientComponentOnTheClient, null),
1209 );
1210 }
1211
1212 const debugReadable = new Stream.PassThrough(streamOptions);
1213
1214 const rscStream = await serverAct(() =>
1215 ReactServerDOMServer.renderToPipeableStream(
1216 ReactServer.createElement(App, null),
1217 webpackMap,
1218 {
1219 debugChannel: new Stream.Writable({
1220 write(chunk, encoding, callback) {
1221 debugReadable.write(chunk, encoding);
1222 callback();
1223 },
1224 final() {
1225 debugReadable.end();
1226 },
1227 }),
1228 },
1229 ),
1230 );
1231
1232 // Create a delayed stream to simulate that the RSC stream might be
1233 // transported slower than the debug channel, which must not lead to a
1234 // `Connection closed` error in the Flight client.
1235 const {delayedStream, resolveDelayedStream} = createDelayedStream();
1236
1237 rscStream.pipe(delayedStream);
1238
1239 function ClientRoot({response}) {
1240 return use(response);
1241 }
1242
1243 const serverConsumerManifest = {
1244 moduleMap: {
1245 [webpackMap[ClientComponentOnTheClient.$$id].id]: {
1246 '*': webpackMap[ClientComponentOnTheServer.$$id],
1247 },
1248 },
1249 moduleLoading: webpackModuleLoading,
1250 };
1251
1252 const response = ReactServerDOMClient.createFromNodeStream(
1253 delayedStream,
1254 serverConsumerManifest,
1255 {debugChannel: debugReadable},
1256 );
1257
1258 setTimeout(resolveDelayedStream);
1259
1260 let ownerStack;
1261
1262 const ssrStream = await serverAct(() =>
1263 ReactDOMServer.renderToPipeableStream(
1264 <ClientRoot response={response} />,
1265 {
1266 onError(err, errorInfo) {
1267 ownerStack = React.captureOwnerStack
1268 ? React.captureOwnerStack()
1269 : null;
1270 },
1271 },
1272 ),
1273 );
1274
1275 const result = await readResult(ssrStream);
1276
1277 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
1278
1279 expect(result).toContain(
1280 'Switched to client rendering because the server rendering errored:\n\nssr-throw',
1281 );
1282 });
1283
1284 // @gate __DEV__
1285 it('can transport debug info through a slow debug channel', async () => {
1286 function Thrower() {
1287 throw new Error('ssr-throw');
1288 }
1289
1290 const ClientComponentOnTheClient = clientExports(
1291 Thrower,
1292 123,
1293 'path/to/chunk.js',
1294 );
1295
1296 const ClientComponentOnTheServer = clientExports(Thrower);
1297
1298 function App() {
1299 return ReactServer.createElement(
1300 ReactServer.Suspense,
1301 null,
1302 ReactServer.createElement(ClientComponentOnTheClient, null),
1303 );
1304 }
1305
1306 // Create a delayed stream to simulate that the debug stream might be
1307 // transported slower than the RSC stream, which must not lead to missing
1308 // debug info.
1309 const {delayedStream, resolveDelayedStream} = createDelayedStream();
1310
1311 const rscStream = await serverAct(() =>
1312 ReactServerDOMServer.renderToPipeableStream(
1313 ReactServer.createElement(App, null),
1314 webpackMap,
1315 {
1316 debugChannel: new Stream.Writable({
1317 write(chunk, encoding, callback) {
1318 delayedStream.write(chunk, encoding);
1319 callback();
1320 },
1321 final() {
1322 delayedStream.end();
1323 },
1324 }),
1325 },
1326 ),
1327 );
1328
1329 const readable = new Stream.PassThrough(streamOptions);
1330
1331 rscStream.pipe(readable);
1332
1333 function ClientRoot({response}) {
1334 return use(response);
1335 }
1336
1337 const serverConsumerManifest = {
1338 moduleMap: {
1339 [webpackMap[ClientComponentOnTheClient.$$id].id]: {
1340 '*': webpackMap[ClientComponentOnTheServer.$$id],
1341 },
1342 },
1343 moduleLoading: webpackModuleLoading,
1344 };
1345
1346 const response = ReactServerDOMClient.createFromNodeStream(
1347 readable,
1348 serverConsumerManifest,
1349 {debugChannel: delayedStream},
1350 );
1351
1352 setTimeout(resolveDelayedStream);
1353
1354 let ownerStack;
1355
1356 const ssrStream = await serverAct(() =>
1357 ReactDOMServer.renderToPipeableStream(
1358 <ClientRoot response={response} />,
1359 {
1360 onError(err, errorInfo) {
1361 ownerStack = React.captureOwnerStack
1362 ? React.captureOwnerStack()
1363 : null;
1364 },
1365 },
1366 ),
1367 );
1368
1369 const result = await readResult(ssrStream);
1370
1371 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
1372
1373 expect(result).toContain(
1374 'Switched to client rendering because the server rendering errored:\n\nssr-throw',
1375 );
1376 });
1377
1378 // This is a regression test for a specific issue where byte Web Streams are
1379 // detaching ArrayBuffers, which caused downstream issues (e.g. "Cannot
1380 // perform Construct on a detached ArrayBuffer") for chunks that are using
1381 // Node's internal Buffer pool.
1382 it('should not corrupt the Node.js Buffer pool by detaching ArrayBuffers when using Web Streams', async () => {
1383 // Create a temp file smaller than 4KB to ensure it uses the Buffer pool.
1384 const file = path.join(os.tmpdir(), 'test.bin');
1385 fs.writeFileSync(file, Buffer.alloc(4095));
1386 const fileChunk = fs.readFileSync(file);
1387 fs.unlinkSync(file);
1388
1389 // Verify this chunk uses the Buffer pool (8192 bytes for files < 4KB).
1390 expect(fileChunk.buffer.byteLength).toBe(8192);
1391
1392 const readable = await serverAct(() =>
1393 ReactServerDOMServer.renderToReadableStream(fileChunk, webpackMap),
1394 );
1395
1396 // Create a Web Streams WritableStream that tries to use Buffer operations.
1397 const writable = new WritableStream({
1398 write(chunk) {
1399 // Only write one byte to ensure Node.js is not creating a new Buffer
1400 // pool. Typically, library code (e.g. a compression middleware) would
1401 // call Buffer.from(chunk) or similar, instead of allocating a new
1402 // Buffer directly. With that, the test file could only be ~2600 bytes.
1403 Buffer.allocUnsafe(1);
1404 },
1405 });
1406
1407 // Must not throw an error.
1408 await readable.pipeTo(writable);
1409 });
1410
1411 describe('with real timers', () => {
1412 // These tests schedule their rendering in a way that requires real timers
1413 // to be used to accurately represent how this interacts with React's
1414 // internal scheduling.
1415
1416 beforeEach(() => {
1417 jest.useRealTimers();
1418 });
1419
1420 afterEach(() => {
1421 jest.useFakeTimers();
1422 });
1423
1424 it('should use late-arriving I/O debug info to enhance component and owner stacks when aborting a prerender', async () => {
1425 let resolveDynamicData1;
1426 let resolveDynamicData2;
1427
1428 async function getDynamicData1() {
1429 return new Promise(resolve => {
1430 resolveDynamicData1 = resolve;
1431 });
1432 }
1433
1434 async function getDynamicData2() {
1435 return new Promise(resolve => {
1436 resolveDynamicData2 = resolve;
1437 });
1438 }
1439
1440 async function Dynamic() {
1441 const data1 = await getDynamicData1();
1442 const data2 = await getDynamicData2();
1443
1444 return ReactServer.createElement('p', null, data1, ' ', data2);
1445 }
1446
1447 function App() {
1448 return ReactServer.createElement(
1449 'html',
1450 null,
1451 ReactServer.createElement(
1452 'body',
1453 null,
1454 ReactServer.createElement(Dynamic),
1455 ),
1456 );
1457 }
1458
1459 let staticEndTime = -1;
1460 const initialChunks = [];
1461 const dynamicChunks = [];
1462
1463 await new Promise(resolve => {
1464 setTimeout(async () => {
1465 const stream = ReactServerDOMServer.renderToPipeableStream(
1466 ReactServer.createElement(App),
1467 webpackMap,
1468 {filterStackFrame},
1469 );
1470
1471 const passThrough = new Stream.PassThrough(streamOptions);
1472 stream.pipe(passThrough);
1473
1474 passThrough.on('data', chunk => {
1475 if (staticEndTime < 0) {
1476 initialChunks.push(chunk);
1477 } else {
1478 dynamicChunks.push(chunk);
1479 }
1480 });
1481
1482 passThrough.on('end', resolve);
1483 });
1484 setTimeout(() => {
1485 staticEndTime = performance.now() + performance.timeOrigin;
1486 resolveDynamicData1('Hi');
1487 setTimeout(() => {
1488 resolveDynamicData2('Josh');
1489 });
1490 });
1491 });
1492
1493 // Create a new Readable and push all initial chunks immediately.
1494 const readable = new Stream.Readable({...streamOptions, read() {}});
1495 for (let i = 0; i < initialChunks.length; i++) {
1496 readable.push(initialChunks[i]);
1497 }
1498
1499 const abortController = new AbortController();
1500
1501 // When prerendering is aborted, push all dynamic chunks. They won't be
1502 // considered for rendering, but they include debug info we want to use.
1503 abortController.signal.addEventListener(
1504 'abort',
1505 () => {
1506 for (let i = 0; i < dynamicChunks.length; i++) {
1507 readable.push(dynamicChunks[i]);
1508 }
1509 },
1510 {once: true},
1511 );
1512
1513 const response = ReactServerDOMClient.createFromNodeStream(
1514 readable,
1515 {
1516 serverConsumerManifest: {
1517 moduleMap: null,
1518 moduleLoading: null,
1519 },
1520 },
1521 {
1522 // Debug info arriving after this end time will be ignored, e.g. the
1523 // I/O info for the second dynamic data.
1524 endTime: staticEndTime,
1525 },
1526 );
1527
1528 function ClientRoot() {
1529 return use(response);
1530 }
1531
1532 let componentStack;
1533 let ownerStack;
1534
1535 const {prelude} = await new Promise(resolve => {
1536 let result;
1537
1538 setTimeout(() => {
1539 result = ReactDOMFizzStatic.prerenderToNodeStream(
1540 React.createElement(ClientRoot),
1541 {
1542 signal: abortController.signal,
1543 onError(error, errorInfo) {
1544 componentStack = errorInfo.componentStack;
1545 ownerStack = React.captureOwnerStack
1546 ? React.captureOwnerStack()
1547 : null;
1548 },
1549 },
1550 );
1551 });
1552
1553 setTimeout(() => {
1554 abortController.abort();
1555 resolve(result);
1556 });
1557 });
1558
1559 const prerenderHTML = await readResult(prelude);
1560
1561 expect(prerenderHTML).toBe('');
1562
1563 if (__DEV__) {
1564 expect(
1565 normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1566 ).toBe(
1567 '\n' +
1568 ' in Dynamic' +
1569 (gate(flags => flags.enableAsyncDebugInfo)
1570 ? ' (file://ReactFlightDOMNode-test.js:1441:27)\n'
1571 : '\n') +
1572 ' in body\n' +
1573 ' in html\n' +
1574 ' in App (file://ReactFlightDOMNode-test.js:1454:25)\n' +
1575 ' in ClientRoot (ReactFlightDOMNode-test.js:1529:16)',
1576 );
1577 } else {
1578 expect(
1579 normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1580 ).toBe(
1581 '\n' +
1582 ' in body\n' +
1583 ' in html\n' +
1584 ' in ClientRoot (ReactFlightDOMNode-test.js:1529:16)',
1585 );
1586 }
1587
1588 if (__DEV__) {
1589 if (gate(flags => flags.enableAsyncDebugInfo)) {
1590 expect(
1591 normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1592 ).toBe(
1593 '\n' +
1594 ' in Dynamic (file://ReactFlightDOMNode-test.js:1441:27)\n' +
1595 ' in App (file://ReactFlightDOMNode-test.js:1454:25)',
1596 );
1597 } else {
1598 expect(
1599 normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1600 ).toBe(
1601 '' +
1602 '\n' +
1603 ' in App (file://ReactFlightDOMNode-test.js:1454:25)',
1604 );
1605 }
1606 } else {
1607 expect(ownerStack).toBeNull();
1608 }
1609 });
1610
1611 it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender', async () => {
1612 let rejectHangingPromise;
1613
1614 async function makeHangingPromise() {
1615 return new Promise((resolve, reject) => {
1616 rejectHangingPromise = reject;
1617 });
1618 }
1619
1620 async function getRoot() {
1621 return {promise: makeHangingPromise()};
1622 }
1623
1624 let staticEndTime = -1;
1625 const staticChunks = [];
1626 const dynamicChunks = [];
1627
1628 const serverAbortController = new AbortController();
1629 await new Promise(resolve => {
1630 setTimeout(async () => {
1631 const stream = ReactServerDOMServer.renderToPipeableStream(
1632 getRoot(),
1633 webpackMap,
1634 {
1635 filterStackFrame,
1636 onError(err) {
1637 if (serverAbortController.signal.aborted) {
1638 return;
1639 }
1640 console.error(err);
1641 },
1642 },
1643 );
1644 serverAbortController.signal.addEventListener(
1645 'abort',
1646 () => {
1647 stream.abort(serverAbortController.signal.reason);
1648
1649 // Only reject the promise after the render is aborted
1650 // so that it's no longer observable
1651 rejectHangingPromise(
1652 new Error(
1653 'Hanging promise was rejected after the prerender finished',
1654 ),
1655 );
1656 },
1657 {once: true},
1658 );
1659
1660 const passThrough = new Stream.PassThrough(streamOptions);
1661 stream.pipe(passThrough);
1662
1663 passThrough.on('data', chunk => {
1664 if (staticEndTime < 0) {
1665 staticChunks.push(chunk);
1666 } else {
1667 dynamicChunks.push(chunk);
1668 }
1669 });
1670
1671 passThrough.on('end', resolve);
1672 });
1673 setTimeout(() => {
1674 staticEndTime = performance.now() + performance.timeOrigin;
1675 serverAbortController.abort();
1676 });
1677 });
1678
1679 const clientAbortController = new AbortController();
1680
1681 const serverStream = createReadableWithLateRelease(
1682 staticChunks,
1683 dynamicChunks,
1684 clientAbortController.signal,
1685 );
1686
1687 const response = await ReactServerDOMClient.createFromNodeStream(
1688 serverStream,
1689 {
1690 serverConsumerManifest: {
1691 moduleMap: null,
1692 moduleLoading: null,
1693 },
1694 },
1695 {
1696 // Debug info arriving after this end time will be ignored, e.g. the
1697 // I/O info for the second dynamic data.
1698 endTime: staticEndTime,
1699 },
1700 );
1701
1702 const resolvedPromise = Promise.resolve('hello');
1703 function ClientDynamic() {
1704 use(resolvedPromise);
1705 use(response.promise); // unresolved ReactPromise (becomes rejected when we abort)
1706 }
1707
1708 function ClientRoot() {
1709 return React.createElement(
1710 'html',
1711 null,
1712 React.createElement(
1713 'body',
1714 null,
1715 React.createElement(
1716 React.Suspense,
1717 {fallback: 'Loading...'},
1718 React.createElement(ClientDynamic),
1719 ),
1720 ),
1721 );
1722 }
1723
1724 let ownerStack;
1725 let componentStack;
1726
1727 const {prelude} = await new Promise(resolve => {
1728 let result;
1729
1730 setTimeout(() => {
1731 result = ReactDOMFizzStatic.prerenderToNodeStream(
1732 React.createElement(ClientRoot),
1733 {
1734 signal: clientAbortController.signal,
1735 onError(error, errorInfo) {
1736 componentStack = errorInfo.componentStack;
1737 ownerStack = React.captureOwnerStack
1738 ? React.captureOwnerStack()
1739 : null;
1740 },
1741 },
1742 );
1743 });
1744
1745 setTimeout(() => {
1746 clientAbortController.abort();
1747 resolve(result);
1748 });
1749 });
1750
1751 const prerenderHTML = await readResult(prelude);
1752
1753 expect(prerenderHTML).toContain('Loading...');
1754
1755 if (__DEV__) {
1756 expect(
1757 normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1758 ).toBe(
1759 '\n' +
1760 ' in ClientDynamic (ReactFlightDOMNode-test.js:1704:9)\n' +
1761 ' in Suspense\n' +
1762 ' in body\n' +
1763 ' in html\n' +
1764 ' in ClientRoot',
1765 );
1766 } else {
1767 expect(
1768 normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1769 ).toBe(
1770 '\n' +
1771 ' in ClientDynamic (ReactFlightDOMNode-test.js:1704:9)\n' +
1772 ' in Suspense\n' +
1773 ' in body\n' +
1774 ' in html\n' +
1775 ' in ClientRoot',
1776 );
1777 }
1778
1779 if (__DEV__) {
1780 expect(ignoreListStack(ownerStack)).toBe(
1781 '\n' +
1782 gate(flags =>
1783 flags.enableAsyncDebugInfo
1784 ? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1705:9)\n'
1785 : '',
1786 ) +
1787 ' at ClientRoot (./ReactFlightDOMNode-test.js:1718:21)',
1788 );
1789 } else {
1790 expect(ownerStack).toBeNull();
1791 }
1792 });
1793
1794 function createReadableWithLateRelease(initialChunks, lateChunks, signal) {
1795 // Create a new Readable and push all initial chunks immediately.
1796 const readable = new Stream.Readable({...streamOptions, read() {}});
1797 for (let i = 0; i < initialChunks.length; i++) {
1798 readable.push(initialChunks[i]);
1799 }
1800
1801 // When prerendering is aborted, push all dynamic chunks. They won't be
1802 // considered for rendering, but they include debug info we want to use.
1803 signal.addEventListener(
1804 'abort',
1805 () => {
1806 for (let i = 0; i < lateChunks.length; i++) {
1807 readable.push(lateChunks[i]);
1808 }
1809 setImmediate(() => {
1810 readable.push(null);
1811 });
1812 },
1813 {once: true},
1814 );
1815
1816 return readable;
1817 }
1818
1819 async function reencodeFlightStream(
1820 staticChunks,
1821 dynamicChunks,
1822 startTime,
1823 serverConsumerManifest,
1824 ) {
1825 let staticEndTime = -1;
1826 const chunks = {
1827 static: [],
1828 dynamic: [],
1829 };
1830 await new Promise(async resolve => {
1831 const renderStageController = new AbortController();
1832
1833 const serverStream = createReadableWithLateRelease(
1834 staticChunks,
1835 dynamicChunks,
1836 renderStageController.signal,
1837 );
1838 const decoded = await ReactServerDOMClient.createFromNodeStream(
1839 serverStream,
1840 serverConsumerManifest,
1841 {
1842 // We're re-encoding the whole stream, so we don't want to filter out any debug info.
1843 endTime: undefined,
1844 },
1845 );
1846
1847 setTimeout(async () => {
1848 const stream = ReactServerDOMServer.renderToPipeableStream(
1849 decoded,
1850 webpackMap,
1851 {
1852 filterStackFrame,
1853 // Pass in the original render's startTime to avoid omitting its IO info.
1854 startTime,
1855 },
1856 );
1857
1858 const passThrough = new Stream.PassThrough(streamOptions);
1859
1860 passThrough.on('data', chunk => {
1861 if (!renderStageController.signal.aborted) {
1862 chunks.static.push(chunk);
1863 } else {
1864 chunks.dynamic.push(chunk);
1865 }
1866 });
1867 passThrough.on('end', resolve);
1868
1869 stream.pipe(passThrough);
1870 });
1871
1872 setTimeout(() => {
1873 staticEndTime = performance.now() + performance.timeOrigin;
1874 renderStageController.abort();
1875 });
1876 });
1877
1878 return {chunks, staticEndTime};
1879 }
1880
1881 // @gate __DEV__
1882 it('can preserve old IO info when decoding and re-encoding a stream with options.startTime', async () => {
1883 let resolveDynamicData;
1884
1885 function getDynamicData() {
1886 return new Promise(resolve => {
1887 resolveDynamicData = resolve;
1888 });
1889 }
1890
1891 async function Dynamic() {
1892 const data = await getDynamicData();
1893 return ReactServer.createElement('p', null, data);
1894 }
1895
1896 function App() {
1897 return ReactServer.createElement(
1898 'html',
1899 null,
1900 ReactServer.createElement(
1901 'body',
1902 null,
1903 ReactServer.createElement(
1904 ReactServer.Suspense,
1905 {fallback: 'Loading...'},
1906 // TODO: having a wrapper <section> here seems load-bearing.
1907 // ReactServer.createElement(ReactServer.createElement(Dynamic)),
1908 ReactServer.createElement(
1909 'section',
1910 null,
1911 ReactServer.createElement(Dynamic),
1912 ),
1913 ),
1914 ),
1915 );
1916 }
1917
1918 const resolveDynamic = () => {
1919 resolveDynamicData('Hi Janka');
1920 };
1921
1922 // 1. Render <App />, dividing the output into static and dynamic content.
1923
1924 let startTime = -1;
1925
1926 let isStatic = true;
1927 const chunks1 = {
1928 static: [],
1929 dynamic: [],
1930 };
1931
1932 await new Promise(resolve => {
1933 setTimeout(async () => {
1934 startTime = performance.now() + performance.timeOrigin;
1935
1936 const stream = ReactServerDOMServer.renderToPipeableStream(
1937 ReactServer.createElement(App),
1938 webpackMap,
1939 {
1940 filterStackFrame,
1941 startTime,
1942 environmentName() {
1943 return isStatic ? 'Prerender' : 'Server';
1944 },
1945 },
1946 );
1947
1948 const passThrough = new Stream.PassThrough(streamOptions);
1949
1950 passThrough.on('data', chunk => {
1951 if (isStatic) {
1952 chunks1.static.push(chunk);
1953 } else {
1954 chunks1.dynamic.push(chunk);
1955 }
1956 });
1957 passThrough.on('end', resolve);
1958
1959 stream.pipe(passThrough);
1960 });
1961 setTimeout(() => {
1962 isStatic = false;
1963 resolveDynamic();
1964 });
1965 });
1966
1967 //===============================================
1968 // 2. Decode the stream from the previous step and render it again.
1969 // This should preserve existing debug info.
1970
1971 const serverConsumerManifest = {
1972 moduleMap: null,
1973 moduleLoading: null,
1974 };
1975
1976 const {chunks: chunks2, staticEndTime: reencodeStaticEndTime} =
1977 await reencodeFlightStream(
1978 chunks1.static,
1979 chunks1.dynamic,
1980 // This is load-bearing. If we don't pass a startTime, IO info
1981 // from the initial render will be skipped (because it finished in the past)
1982 // and we won't get the precise location of the blocking await in the owner stack.
1983 startTime,
1984 serverConsumerManifest,
1985 );
1986
1987 //===============================================
1988 // 3. SSR the stream from the previous step and abort it after the static stage
1989 // (which should trigger `onError` for each "hole" that hasn't resolved yet)
1990
1991 function ClientRoot({response}) {
1992 return use(response);
1993 }
1994
1995 let ssrStream;
1996 let ownerStack;
1997 let componentStack;
1998
1999 await new Promise(async (resolve, reject) => {
2000 const renderController = new AbortController();
2001
2002 const serverStream = createReadableWithLateRelease(
2003 chunks2.static,
2004 chunks2.dynamic,
2005 renderController.signal,
2006 );
2007
2008 const decodedPromise = ReactServerDOMClient.createFromNodeStream(
2009 serverStream,
2010 serverConsumerManifest,
2011 {
2012 endTime: reencodeStaticEndTime,
2013 },
2014 );
2015
2016 setTimeout(() => {
2017 ssrStream = ReactDOMServer.renderToPipeableStream(
2018 React.createElement(ClientRoot, {
2019 response: decodedPromise,
2020 }),
2021 {
2022 onError(err, errorInfo) {
2023 componentStack = errorInfo.componentStack;
2024 ownerStack = React.captureOwnerStack
2025 ? React.captureOwnerStack()
2026 : null;
2027 return null;
2028 },
2029 },
2030 );
2031
2032 renderController.signal.addEventListener(
2033 'abort',
2034 () => {
2035 const {reason} = renderController.signal;
2036 ssrStream.abort(reason);
2037 },
2038 {
2039 once: true,
2040 },
2041 );
2042 });
2043
2044 setTimeout(() => {
2045 renderController.abort(new Error('ssr-abort'));
2046 resolve();
2047 });
2048 });
2049
2050 const result = await readResult(ssrStream);
2051
2052 expect(normalizeCodeLocInfo(componentStack)).toBe(
2053 '\n' +
2054 // TODO:
2055 // when we reencode a stream, the component stack doesn't have server frames for the dynamic content
2056 // (which is what causes the dynamic hole here)
2057 // because Flight delays forwarding debug info for lazies until they resolve.
2058 // (the owner stack is filled in `pushHaltedAwaitOnComponentStack`, so it works fine)
2059 //
2060 // ' in Dynamic (at **)\n'
2061 ' in section\n' +
2062 ' in Suspense\n' +
2063 ' in body\n' +
2064 ' in html\n' +
2065 ' in App (at **)\n' +
2066 ' in ClientRoot (at **)',
2067 );
2068 expect(normalizeCodeLocInfo(ownerStack)).toBe(
2069 '\n' +
2070 gate(flags =>
2071 flags.enableAsyncDebugInfo
2072 ? ' in Dynamic (at **)\n'
2073 : ' in section\n',
2074 ) +
2075 ' in App (at **)',
2076 );
2077
2078 expect(result).toContain(
2079 'Switched to client rendering because the server rendering aborted due to:\\n\\n' +
2080 'ssr-abort',
2081 );
2082 });
2083
2084 // @gate __DEV__
2085 it('filters parsed debug info when the Flight stream errors', async () => {
2086 let resolveInitialData;
2087 const laterDataResolvers = [];
2088
2089 async function getInitialData() {
2090 return new Promise(resolve => {
2091 resolveInitialData = resolve;
2092 });
2093 }
2094
2095 async function loadInitialData() {
2096 return await getInitialData();
2097 }
2098
2099 async function loadLaterData() {
2100 for (let i = 0; i < 40; i++) {
2101 await new Promise(resolve => {
2102 laterDataResolvers[i] = resolve;
2103 });
2104 }
2105 }
2106
2107 async function Dynamic() {
2108 await loadInitialData();
2109 await loadLaterData();
2110 return ReactServer.createElement('p', null, 'Done');
2111 }
2112
2113 function App() {
2114 return ReactServer.createElement(
2115 'html',
2116 null,
2117 ReactServer.createElement(
2118 'body',
2119 null,
2120 ReactServer.createElement(Dynamic),
2121 ),
2122 );
2123 }
2124
2125 let staticEndTime = -1;
2126 const chunks = [];
2127
2128 await new Promise(resolve => {
2129 setTimeout(() => {
2130 const flightStream = ReactServerDOMServer.renderToPipeableStream(
2131 ReactServer.createElement(App),
2132 webpackMap,
2133 {
2134 filterStackFrame,
2135 },
2136 );
2137
2138 const passThrough = new Stream.PassThrough(streamOptions);
2139 flightStream.pipe(passThrough);
2140 passThrough.on('data', chunk => {
2141 chunks.push(chunk);
2142 });
2143 passThrough.on('end', resolve);
2144 });
2145
2146 setTimeout(() => {
2147 staticEndTime = performance.now() + performance.timeOrigin;
2148 resolveInitialData();
2149
2150 let index = 0;
2151 function resolveNext() {
2152 setTimeout(() => {
2153 laterDataResolvers[index++]();
2154 if (index < 40) {
2155 resolveNext();
2156 }
2157 });
2158 }
2159 setTimeout(resolveNext);
2160 });
2161 });
2162
2163 const contentStream = new Stream.Readable({
2164 ...streamOptions,
2165 read() {},
2166 });
2167 const response = ReactServerDOMClient.createFromNodeStream(
2168 contentStream,
2169 {
2170 moduleMap: null,
2171 moduleLoading: null,
2172 serverModuleMap: null,
2173 },
2174 {
2175 endTime: staticEndTime,
2176 },
2177 );
2178 // The final write contains the completed model. The preceding writes
2179 // contain the debug rows produced while rendering it.
2180 for (let i = 0; i < chunks.length - 1; i++) {
2181 contentStream.push(chunks[i]);
2182 }
2183
2184 const decoded = await response;
2185
2186 function ClientRoot() {
2187 return decoded;
2188 }
2189
2190 const flightError = new Error('Flight stream errored');
2191 const fizzAbortController = new AbortController();
2192 let caughtError;
2193 let ownerStack;
2194 const {prelude} = await new Promise(resolve => {
2195 let result;
2196
2197 setTimeout(() => {
2198 result = ReactDOMFizzStatic.prerenderToNodeStream(
2199 React.createElement(ClientRoot),
2200 {
2201 signal: fizzAbortController.signal,
2202 onError(error) {
2203 caughtError = error;
2204 ownerStack = React.captureOwnerStack
2205 ? React.captureOwnerStack()
2206 : null;
2207 },
2208 },
2209 );
2210 });
2211
2212 setTimeout(() => {
2213 contentStream.emit('error', flightError);
2214 contentStream.push(null);
2215 fizzAbortController.abort(new Error('Fizz aborted'));
2216 resolve(result);
2217 });
2218 });
2219
2220 expect(await readResult(prelude)).toBe('');
2221 expect(caughtError).toBe(flightError);
2222 expect(normalizeCodeLocInfo(ownerStack)).toBe(
2223 '\n' +
2224 gate(flags =>
2225 flags.enableAsyncDebugInfo
2226 ? ' in loadInitialData (at **)\n' + ' in Dynamic (at **)\n'
2227 : '',
2228 ) +
2229 ' in App (at **)',
2230 );
2231 });
2232 });
2233
2234 it('warns with a tailored message if eval is not available in dev', async () => {
2235 // eslint-disable-next-line no-eval
2236 const previousEval = globalThis.eval.bind(globalThis);
2237 // eslint-disable-next-line no-eval
2238 globalThis.eval = () => {
2239 throw new Error('eval is disabled');
2240 };
2241
2242 try {
2243 const readable = await serverAct(() =>
2244 ReactServerDOMServer.renderToReadableStream({}, webpackMap),
2245 );
2246
2247 assertConsoleErrorDev([]);
2248
2249 await ReactServerDOMClient.createFromReadableStream(readable, {
2250 serverConsumerManifest: {
2251 moduleMap: null,
2252 moduleLoading: null,
2253 },
2254 });
2255
2256 assertConsoleErrorDev([
2257 'eval() is not supported in this environment. ' +
2258 'This can happen if you started the Node.js process with --disallow-code-generation-from-strings, ' +
2259 'or if `eval` was patched by other means. ' +
2260 'React requires eval() in development mode for various debugging features ' +
2261 'like reconstructing callstacks from a different environment.\n' +
2262 'React will never use eval() in production mode',
2263 ]);
2264 } finally {
2265 // eslint-disable-next-line no-eval
2266 globalThis.eval = previousEval;
2267 }
2268 });
2269
2270 // Shared scenario for the two regression tests below. Both guard against
2271 // the same destination-backpressure bug — emitTextChunk and
2272 // emitTypedArrayChunk each push a [headerChunk, contentChunk] pair into
2273 // completedRegularChunks. Before the fix, a flush that broke between the
2274 // two writes left the content chunk stranded at the head of the queue,
2275 // and the next flush emitted any newly-arrived Import rows ahead of it —
2276 // splicing Import bytes into the position the Flight Client expects to
2277 // read as the row's content.
2278 //
2279 // The scenario embeds `payload` in the model under the key `payload` and
2280 // returns the deserialized model. Each test asserts that result.payload
2281 // round-trips identically to what the Flight Server emitted.
2282 async function runScenarioWithBackpressureBetweenHeaderAndContent(payload) {
2283 function Client1() {
2284 return <span>client1</span>;
2285 }
2286 // Client1's Import row must exceed VIEW_SIZE (4096) so writeStringChunk
2287 // takes its BIG path and calls destination.write directly. That write
2288 // returning false is what triggers the backpressure we want to test.
2289 const Client1Reference = clientExports(
2290 Client1,
2291 1,
2292 '/' + 'a'.repeat(5000),
2293 Promise.resolve(),
2294 );
2295
2296 function Client2() {
2297 return <span>client2</span>;
2298 }
2299 const Client2Reference = clientExports(
2300 Client2,
2301 2,
2302 '/client2.js',
2303 Promise.resolve(),
2304 );
2305
2306 let resolveAsync;
2307 const asyncPromise = new Promise(resolve => {
2308 resolveAsync = resolve;
2309 });
2310
2311 async function AsyncWrapper() {
2312 await asyncPromise;
2313 return <Client2Reference />;
2314 }
2315
2316 const model = {
2317 client: <Client1Reference />,
2318 payload,
2319 async: <AsyncWrapper />,
2320 };
2321
2322 const heldCallbacks = [];
2323 const collectedChunks = [];
2324
2325 // A destination that returns false from every write (highWaterMark: 1 in
2326 // byte mode) and never completes any of them until the test releases the
2327 // stored callback. This gives us deterministic control over when each write
2328 // finishes and when 'drain' fires.
2329 const destination = new Stream.Writable({
2330 highWaterMark: 1,
2331 write(chunk, encoding, callback) {
2332 collectedChunks.push(
2333 Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding),
2334 );
2335 heldCallbacks.push(callback);
2336 },
2337 });
2338
2339 const finished = new Promise((resolve, reject) => {
2340 destination.on('finish', resolve);
2341 destination.on('error', reject);
2342 });
2343
2344 // First flush: Client1's huge Import row hits backpressure, so the flush
2345 // loop reaches the payload row's [headerChunk, contentChunk] pair while
2346 // destinationHasCapacity is already false. Before the fix, this would
2347 // have encoded just the header into currentView, broken the loop, and
2348 // let completeWriting flush the header as its own write — stranding
2349 // the content chunk at the front of completedRegularChunks.
2350 const {pipe} = await serverAct(() =>
2351 ReactServerDOMServer.renderToPipeableStream(model, webpackMap),
2352 );
2353 await serverAct(() => {
2354 pipe(destination);
2355 });
2356
2357 // While the destination is still paused, push Client2's Import row into
2358 // completedImportChunks. No flush runs (request.destination is null after
2359 // the first flush's backpressure break).
2360 await serverAct(() => {
2361 resolveAsync();
2362 });
2363
2364 // Release callbacks one at a time. The drain that empties the writable
2365 // buffer triggers flushCompletedChunks; before the fix, this is where
2366 // Client2's newly-queued Import row would have been emitted ahead of
2367 // the still-orphaned payload content chunk.
2368 while (heldCallbacks.length > 0) {
2369 await serverAct(() => {
2370 const cb = heldCallbacks.shift();
2371 cb();
2372 });
2373 }
2374
2375 await finished;
2376
2377 const readable = new Stream.Readable({read() {}});
2378 for (let i = 0; i < collectedChunks.length; i++) {
2379 readable.push(collectedChunks[i]);
2380 }
2381 readable.push(null);
2382
2383 const response = ReactServerDOMClient.createFromNodeStream(readable, {
2384 moduleMap: null,
2385 moduleLoading: null,
2386 });
2387 return await response;
2388 }
2389
2390 it("keeps a Text row's header and content chunks adjacent when a flush hits backpressure between them", async () => {
2391 // length >= 1024 makes the Flight Server outline this as a Text row via
2392 // serializeLargeTextString, which is where emitTextChunk pushes its
2393 // [headerChunk, textChunk] pair.
2394 const largeText = 'x'.repeat(2048);
2395
2396 const result =
2397 await runScenarioWithBackpressureBetweenHeaderAndContent(largeText);
2398
2399 // Before the fix, the Flight Client would have framed Client2's Import
2400 // row bytes as text-row content, making result.payload `<id>:I[...]...`
2401 // garbage rather than the x's the Flight Server emitted.
2402 expect(result.payload).toBe(largeText);
2403 });
2404
2405 it("keeps a TypedArray row's header and content chunks adjacent when a flush hits backpressure between them", async () => {
2406 // emitTypedArrayChunk pushes the same [headerChunk, contentChunk] pair as
2407 // emitTextChunk. Before the fix, a flush break after the header would have
2408 // stranded the content chunk in exactly the same way.
2409 const binaryData = new Uint8Array(1024);
2410 for (let i = 0; i < binaryData.length; i++) {
2411 binaryData[i] = i % 256;
2412 }
2413
2414 const result =
2415 await runScenarioWithBackpressureBetweenHeaderAndContent(binaryData);
2416
2417 // Before the fix, the typed array's bytes would have been replaced by
2418 // Client2's Import row bytes followed by whatever happened to land in
2419 // the next 1024-byte window.
2420 expect(result.payload).toEqual(binaryData);
2421 });
2422
2423 // A Node.js Buffer carries a `toJSON` method, so Flight serializes it through
2424 // that method instead of as binary, and warns. It is therefore deserialized
2425 // as a plain `{type: 'Buffer', data: [...]}` object rather than a
2426 // Buffer/Uint8Array.
2427 it('serializes a Node Buffer through its toJSON and warns', async () => {
2428 const buffer = Buffer.from([1, 2, 3, 4]);
2429 const stream = await serverAct(() =>
2430 ReactServerDOMServer.renderToPipeableStream({font: buffer}),
2431 );
2432 assertConsoleErrorDev([
2433 'Binary data with a toJSON method, such as a Node.js Buffer, is ' +
2434 'serialized through toJSON instead of as binary. Pass a ' +
2435 'Uint8Array or ArrayBuffer to send binary data.\n' +
2436 ' {font: Uint8Array}\n' +
2437 ' ^^^^^^^^^^',
2438 ]);
2439 const readable = new Stream.PassThrough(streamOptions);
2440 const promise = ReactServerDOMClient.createFromNodeStream(readable, {
2441 moduleMap: {},
2442 moduleLoading: webpackModuleLoading,
2443 });
2444 stream.pipe(readable);
2445 const result = await promise;
2446 expect(Buffer.isBuffer(result.font)).toBe(false);
2447 expect(result.font).toEqual({type: 'Buffer', data: [1, 2, 3, 4]});
2448 });
2449
2450 it('detaches the abort listener from a composite signal once the prerender completes', async () => {
2451 // A composite signal from AbortSignal.any() is retained by the runtime for
2452 // as long as it has an abort listener attached, so a listener left behind
2453 // by a completed render keeps that render reachable for the lifetime of the
2454 // source signals.
2455 //
2456 // React bounds its listener with a lifetime signal, so the runtime removes
2457 // the listener rather than React calling removeEventListener. This test
2458 // observes the registration itself through a Node API, which is the only
2459 // way to see that removal. The suites that run under jsdom assert on the
2460 // lifetime signal instead.
2461 const {getEventListeners} = require('node:events');
2462
2463 const outer = new AbortController();
2464 const timeout = new AbortController();
2465 const composite = AbortSignal.any([outer.signal, timeout.signal]);
2466
2467 function App() {
2468 return <div>hello world</div>;
2469 }
2470
2471 const {prelude} = await serverAct(() =>
2472 ReactServerDOMStaticServer.prerenderToNodeStream(<App />, webpackMap, {
2473 signal: composite,
2474 }),
2475 );
2476 expect(getEventListeners(composite, 'abort')).toHaveLength(1);
2477
2478 await serverAct(
2479 () =>
2480 new Promise(resolve => {
2481 prelude.resume();
2482 prelude.on('end', resolve);
2483 }),
2484 );
2485
2486 expect(getEventListeners(composite, 'abort')).toHaveLength(0);
2487 });
2488 });