main
js 3,057 lines 88.9 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 ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12
13 // Patch for Edge environments for global scope
14 global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
15
16 let serverExports;
17 let clientExports;
18 let webpackMap;
19 let webpackServerMap;
20 let webpackModules;
21 let webpackModuleLoading;
22 let React;
23 let ReactServer;
24 let ReactDOMServer;
25 let ReactDOMFizzStatic;
26 let ReactServerDOMServer;
27 let ReactServerDOMStaticServer;
28 let ReactServerDOMClient;
29 let use;
30 let serverAct;
31 let assertConsoleErrorDev;
32
33 function normalizeCodeLocInfo(str) {
34 return (
35 str &&
36 str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
37 return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
38 })
39 );
40 }
41
42 function normalizeSerializedContent(str) {
43 return str.replaceAll(__REACT_ROOT_PATH_TEST__, '**');
44 }
45
46 describe('ReactFlightDOMEdge', () => {
47 beforeEach(() => {
48 // Mock performance.now for timing tests
49 let time = 10;
50 jest.spyOn(performance, 'timeOrigin', 'get').mockReturnValue(time);
51 jest.spyOn(performance, 'now').mockImplementation(() => {
52 return time++;
53 });
54
55 jest.resetModules();
56
57 serverAct = require('internal-test-utils').serverAct;
58 assertConsoleErrorDev =
59 require('internal-test-utils').assertConsoleErrorDev;
60
61 // Simulate the condition resolution
62 jest.mock('react', () => require('react/react.react-server'));
63 jest.mock('react-server-dom-webpack/server', () =>
64 require('react-server-dom-webpack/server.edge'),
65 );
66
67 const WebpackMock = require('./utils/WebpackMock');
68
69 serverExports = WebpackMock.serverExports;
70 clientExports = WebpackMock.clientExports;
71 webpackMap = WebpackMock.webpackMap;
72 webpackServerMap = WebpackMock.webpackServerMap;
73 webpackModules = WebpackMock.webpackModules;
74 webpackModuleLoading = WebpackMock.moduleLoading;
75
76 ReactServer = require('react');
77 ReactServerDOMServer = require('react-server-dom-webpack/server');
78 jest.mock('react-server-dom-webpack/static', () =>
79 require('react-server-dom-webpack/static.edge'),
80 );
81 ReactServerDOMStaticServer = require('react-server-dom-webpack/static');
82
83 jest.resetModules();
84 __unmockReact();
85 jest.unmock('react-server-dom-webpack/server');
86 jest.mock('react-server-dom-webpack/client', () =>
87 require('react-server-dom-webpack/client.edge'),
88 );
89 React = require('react');
90 ReactDOMServer = require('react-dom/server.edge');
91 ReactDOMFizzStatic = require('react-dom/static.edge');
92 ReactServerDOMClient = require('react-server-dom-webpack/client');
93 use = React.use;
94 });
95
96 function passThrough(stream) {
97 // Simulate more realistic network by splitting up and rejoining some chunks.
98 // This lets us test that we don't accidentally rely on particular bounds of the chunks.
99 return new ReadableStream({
100 async start(controller) {
101 const reader = stream.getReader();
102 let prevChunk = new Uint8Array(0);
103 function push() {
104 reader.read().then(({done, value}) => {
105 if (done) {
106 controller.enqueue(prevChunk);
107 prevChunk = new Uint8Array(0);
108 controller.close();
109 return;
110 }
111 const chunk = new Uint8Array(prevChunk.length + value.length);
112 chunk.set(prevChunk, 0);
113 chunk.set(value, prevChunk.length);
114 if (chunk.length > 50) {
115 // Copy the part we're keeping (prevChunk) to avoid buffer
116 // transfer. When we enqueue the partial chunk below, downstream
117 // consumers (like byte streams in the Flight Client) may detach
118 // the underlying buffer. Since prevChunk would share the same
119 // buffer, we copy it first so it has its own independent buffer.
120 // TODO: Should we just use {type: 'bytes'} for this stream to
121 // always transfer ownership, and not only "accidentally" when we
122 // enqueue in the Flight Client?
123 prevChunk = chunk.slice(chunk.length - 50);
124 controller.enqueue(chunk.subarray(0, chunk.length - 50));
125 } else {
126 // Wait to see if we get some more bytes to join in.
127 prevChunk = chunk;
128 // Flush if we don't get any more.
129 (async function flushAfterAFewTasks() {
130 for (let i = 0; i < 10; i++) {
131 await i;
132 }
133 if (prevChunk.byteLength > 0) {
134 controller.enqueue(prevChunk);
135 }
136 prevChunk = new Uint8Array(0);
137 })();
138 }
139 push();
140 });
141 }
142 push();
143 },
144 });
145 }
146
147 function dripStream(input) {
148 const reader = input.getReader();
149 let nextDrop = 0;
150 let controller = null;
151 let streamDone = false;
152 const buffer = [];
153 function flush() {
154 if (controller === null || nextDrop === 0) {
155 return;
156 }
157 while (buffer.length > 0 && nextDrop > 0) {
158 const nextChunk = buffer[0];
159 if (nextChunk.byteLength <= nextDrop) {
160 nextDrop -= nextChunk.byteLength;
161 controller.enqueue(nextChunk);
162 buffer.shift();
163 if (streamDone && buffer.length === 0) {
164 controller.done();
165 }
166 } else {
167 controller.enqueue(nextChunk.subarray(0, nextDrop));
168 buffer[0] = nextChunk.subarray(nextDrop);
169 nextDrop = 0;
170 }
171 }
172 }
173 const output = new ReadableStream({
174 start(c) {
175 controller = c;
176 async function pump() {
177 for (;;) {
178 const {value, done} = await reader.read();
179 if (done) {
180 streamDone = true;
181 break;
182 }
183 buffer.push(value);
184 flush();
185 }
186 }
187 pump();
188 },
189 pull() {},
190 cancel(reason) {
191 reader.cancel(reason);
192 },
193 });
194 function drip(n) {
195 nextDrop += n;
196 flush();
197 }
198
199 return [output, drip];
200 }
201
202 async function readResult(stream) {
203 const reader = stream.getReader();
204 let result = '';
205 while (true) {
206 const {done, value} = await reader.read();
207 if (done) {
208 return result;
209 }
210 result += Buffer.from(value).toString('utf8');
211 }
212 }
213
214 async function readByteLength(stream) {
215 const reader = stream.getReader();
216 let length = 0;
217 while (true) {
218 const {done, value} = await reader.read();
219 if (done) {
220 return length;
221 }
222 length += value.byteLength;
223 }
224 }
225
226 async function createBufferedUnclosingStream(
227 stream: ReadableStream<Uint8Array>,
228 ): Promise<ReadableStream<Uint8Array>> {
229 const chunks: Array<Uint8Array> = [];
230 const reader = stream.getReader();
231 while (true) {
232 const {done, value} = await reader.read();
233 if (done) {
234 break;
235 } else {
236 chunks.push(value);
237 }
238 }
239
240 let i = 0;
241 return new ReadableStream({
242 async pull(controller) {
243 if (i < chunks.length) {
244 controller.enqueue(chunks[i++]);
245 }
246 },
247 });
248 }
249
250 function createDelayedStream(
251 stream: ReadableStream<Uint8Array>,
252 ): ReadableStream<Uint8Array> {
253 return new ReadableStream({
254 async start(controller) {
255 const reader = stream.getReader();
256 while (true) {
257 const {done, value} = await reader.read();
258 if (done) {
259 controller.close();
260 } else {
261 // Artificially delay between enqueuing chunks.
262 await new Promise(resolve => setTimeout(resolve));
263 controller.enqueue(value);
264 }
265 }
266 },
267 });
268 }
269
270 it('should allow an alternative module mapping to be used for SSR', async () => {
271 function ClientComponent() {
272 return <span>Client Component</span>;
273 }
274 // The Client build may not have the same IDs as the Server bundles for the same
275 // component.
276 const ClientComponentOnTheClient = clientExports(ClientComponent);
277 const ClientComponentOnTheServer = clientExports(ClientComponent);
278
279 // In the SSR bundle this module won't exist. We simulate this by deleting it.
280 const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
281 delete webpackModules[clientId];
282
283 // Instead, we have to provide a translation from the client meta data to the SSR
284 // meta data.
285 const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
286 const translationMap = {
287 [clientId]: {
288 '*': ssrMetadata,
289 },
290 };
291
292 function App() {
293 return <ClientComponentOnTheClient />;
294 }
295
296 const stream = await serverAct(() =>
297 ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
298 );
299 const response = ReactServerDOMClient.createFromReadableStream(stream, {
300 serverConsumerManifest: {
301 moduleMap: translationMap,
302 moduleLoading: webpackModuleLoading,
303 },
304 });
305
306 function ClientRoot() {
307 return use(response);
308 }
309
310 const ssrStream = await serverAct(() =>
311 ReactDOMServer.renderToReadableStream(<ClientRoot />),
312 );
313 const result = await readResult(ssrStream);
314 expect(result).toEqual('<span>Client Component</span>');
315 });
316
317 it('should resolve cyclic references in client component props after two rounds of serialization and deserialization', async () => {
318 const ClientComponent = clientExports(function ClientComponent({data}) {
319 return (
320 <div>{data.self === data ? 'Cycle resolved' : 'Cycle broken'}</div>
321 );
322 });
323 const clientModuleMetadata = webpackMap[ClientComponent.$$id];
324 const consumerModuleId = 'consumer-' + clientModuleMetadata.id;
325 const clientReference = Object.defineProperties(ClientComponent, {
326 $$typeof: {value: Symbol.for('react.client.reference')},
327 $$id: {value: ClientComponent.$$id},
328 });
329 webpackModules[consumerModuleId] = clientReference;
330
331 const cyclic = {self: null};
332 cyclic.self = cyclic;
333
334 const stream1 = ReactServerDOMServer.renderToReadableStream(
335 <React.Fragment key="this-key-is-important-to-repro-a-prior-cycle-serialization-bug">
336 <ClientComponent data={cyclic} />
337 </React.Fragment>,
338 webpackMap,
339 );
340
341 const promise = ReactServerDOMClient.createFromReadableStream(stream1, {
342 serverConsumerManifest: {
343 moduleMap: {
344 [clientModuleMetadata.id]: {
345 '*': {
346 id: consumerModuleId,
347 chunks: [],
348 name: '*',
349 },
350 },
351 },
352 moduleLoading: webpackModuleLoading,
353 serverModuleMap: null,
354 },
355 });
356
357 const errors = [];
358 const stream2 = await serverAct(() =>
359 ReactServerDOMServer.renderToReadableStream(promise, webpackMap, {
360 onError(error) {
361 errors.push(error);
362 },
363 }),
364 );
365
366 expect(errors).toEqual([]);
367
368 const element = await serverAct(() =>
369 ReactServerDOMClient.createFromReadableStream(stream2, {
370 serverConsumerManifest: {
371 moduleMap: null,
372 moduleLoading: null,
373 },
374 }),
375 );
376
377 const ssrStream = await serverAct(() =>
378 ReactDOMServer.renderToReadableStream(element),
379 );
380 const result = await readResult(ssrStream);
381
382 expect(result).toBe('<div>Cycle resolved</div>');
383 });
384
385 it('should be able to load a server reference on a consuming server if a mapping exists', async () => {
386 function greet(name) {
387 return 'hi, ' + name;
388 }
389 const ServerModule = serverExports({
390 greet,
391 });
392
393 const stream = await serverAct(() =>
394 ReactServerDOMServer.renderToReadableStream(
395 {
396 method: ServerModule.greet,
397 boundMethod: ServerModule.greet.bind(null, 'there'),
398 },
399 webpackMap,
400 ),
401 );
402 const response = ReactServerDOMClient.createFromReadableStream(stream, {
403 serverConsumerManifest: {
404 moduleMap: webpackMap,
405 serverModuleMap: webpackServerMap,
406 moduleLoading: webpackModuleLoading,
407 },
408 });
409
410 const result = await response;
411
412 expect(result.method).toBe(greet);
413 expect(result.boundMethod()).toBe('hi, there');
414 });
415
416 it('should be able to load a server reference on a consuming server if a mapping exists (async)', async () => {
417 let resolve;
418 const chunkPromise = new Promise(r => (resolve = r));
419
420 function greet(name) {
421 return 'hi, ' + name;
422 }
423 const ServerModule = serverExports(
424 {
425 greet,
426 },
427 chunkPromise,
428 );
429
430 const stream = await serverAct(() =>
431 ReactServerDOMServer.renderToReadableStream(
432 {
433 method: ServerModule.greet,
434 boundMethod: ServerModule.greet.bind(null, 'there'),
435 },
436 webpackMap,
437 ),
438 );
439 const response = ReactServerDOMClient.createFromReadableStream(stream, {
440 serverConsumerManifest: {
441 moduleMap: webpackMap,
442 serverModuleMap: webpackServerMap,
443 moduleLoading: webpackModuleLoading,
444 },
445 });
446
447 await resolve();
448
449 const result = await response;
450
451 expect(result.method).toBe(greet);
452 expect(result.boundMethod()).toBe('hi, there');
453 });
454
455 it('should load a server reference on a consuming server and pass it back', async () => {
456 function greet(name) {
457 return 'hi, ' + name;
458 }
459 const ServerModule = serverExports({
460 greet,
461 });
462
463 // Registering the server reference also with the client must not break
464 // subsequent `.bind` calls.
465 ReactServerDOMClient.registerServerReference(
466 ServerModule.greet,
467 ServerModule.greet.$$id,
468 );
469
470 const stream = await serverAct(() =>
471 ReactServerDOMServer.renderToReadableStream(
472 {
473 method: ServerModule.greet,
474 boundMethod: ServerModule.greet.bind(null, 'there'),
475 },
476 webpackMap,
477 ),
478 );
479 const response = ReactServerDOMClient.createFromReadableStream(stream, {
480 serverConsumerManifest: {
481 moduleMap: webpackMap,
482 serverModuleMap: webpackServerMap,
483 moduleLoading: webpackModuleLoading,
484 },
485 });
486
487 const result = await response;
488
489 expect(result.method).toBe(greet);
490 expect(result.boundMethod()).toBe('hi, there');
491
492 const body = await ReactServerDOMClient.encodeReply({
493 method: result.method,
494 boundMethod: result.boundMethod,
495 });
496 const replyResult = await ReactServerDOMServer.decodeReply(
497 body,
498 webpackServerMap,
499 );
500 expect(replyResult.method).toBe(greet);
501 expect(replyResult.boundMethod()).toBe('hi, there');
502 });
503
504 it('should encode long string in a compact format', async () => {
505 const testString = '"\n\t'.repeat(500) + '🙃';
506 const testString2 = 'hello'.repeat(400);
507
508 const stream = await serverAct(() =>
509 ReactServerDOMServer.renderToReadableStream({
510 text: testString,
511 text2: testString2,
512 }),
513 );
514 const [stream1, stream2] = passThrough(stream).tee();
515
516 const serializedContent = await readResult(stream1);
517 // The content should be compact an unescaped
518 expect(serializedContent.length).toBeLessThan(4000);
519 expect(serializedContent).not.toContain('\\n');
520 expect(serializedContent).not.toContain('\\t');
521 expect(serializedContent).not.toContain('\\"');
522 expect(serializedContent).toContain('\t');
523
524 const result = await ReactServerDOMClient.createFromReadableStream(
525 stream2,
526 {
527 serverConsumerManifest: {
528 moduleMap: null,
529 moduleLoading: null,
530 },
531 },
532 );
533 // Should still match the result when parsed
534 expect(result.text).toBe(testString);
535 expect(result.text2).toBe(testString2);
536 });
537
538 it('should encode repeated objects in a compact format by deduping', async () => {
539 const obj = {
540 this: {is: 'a large objected'},
541 with: {many: 'properties in it'},
542 };
543 const props = {root: <div>{new Array(30).fill(obj)}</div>};
544 const stream = await serverAct(() =>
545 ReactServerDOMServer.renderToReadableStream(props),
546 );
547 const [stream1, stream2] = passThrough(stream).tee();
548
549 const serializedContent = normalizeSerializedContent(
550 await readResult(stream1),
551 );
552 expect(serializedContent.length).toBeLessThan(1075);
553
554 const result = await ReactServerDOMClient.createFromReadableStream(
555 stream2,
556 {
557 serverConsumerManifest: {
558 moduleMap: null,
559 moduleLoading: null,
560 },
561 },
562 );
563 // TODO: Cyclic references currently cause a Lazy wrapper which is not ideal.
564 const resultElement = result.root._init(result.root._payload);
565 // Should still match the result when parsed
566 expect(resultElement).toEqual(props.root);
567 expect(resultElement.props.children[5]).toBe(
568 resultElement.props.children[10],
569 ); // two random items are the same instance
570 });
571
572 it('should execute repeated server components only once', async () => {
573 const str = 'this is a long return value';
574 let timesRendered = 0;
575 function ServerComponent() {
576 timesRendered++;
577 return str;
578 }
579 const element = <ServerComponent />;
580 // Hardcoded list to avoid the key warning
581 const children = (
582 <>
583 {element}
584 {element}
585 {element}
586 {element}
587 {element}
588 {element}
589 {element}
590 {element}
591 {element}
592 {element}
593 {element}
594 {element}
595 {element}
596 {element}
597 {element}
598 {element}
599 {element}
600 {element}
601 {element}
602 {element}
603 {element}
604 {element}
605 {element}
606 {element}
607 {element}
608 {element}
609 {element}
610 {element}
611 {element}
612 {element}
613 </>
614 );
615 const resolvedChildren = new Array(30).fill(str);
616 const stream = await serverAct(() =>
617 ReactServerDOMServer.renderToReadableStream(children),
618 );
619 const [stream1, stream2] = passThrough(stream).tee();
620
621 const serializedContent = normalizeSerializedContent(
622 await readResult(stream1),
623 );
624
625 expect(serializedContent.length).toBeLessThan(465);
626 expect(timesRendered).toBeLessThan(5);
627
628 const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
629 serverConsumerManifest: {
630 moduleMap: null,
631 moduleLoading: null,
632 },
633 });
634
635 // Use the SSR render to resolve any lazy elements
636 const ssrStream = await serverAct(() =>
637 ReactDOMServer.renderToReadableStream(model),
638 );
639 // Should still match the result when parsed
640 const result = await readResult(ssrStream);
641 expect(result).toEqual(resolvedChildren.join('<!-- -->'));
642 });
643
644 it('should execute repeated host components only once', async () => {
645 const div = <div>this is a long return value</div>;
646 let timesRendered = 0;
647 function ServerComponent() {
648 timesRendered++;
649 return div;
650 }
651 const element = <ServerComponent />;
652 // Hardcoded list to avoid the key warning
653 const children = (
654 <>
655 {element}
656 {element}
657 {element}
658 {element}
659 {element}
660 {element}
661 {element}
662 {element}
663 {element}
664 {element}
665 {element}
666 {element}
667 {element}
668 {element}
669 {element}
670 {element}
671 {element}
672 {element}
673 {element}
674 {element}
675 {element}
676 {element}
677 {element}
678 {element}
679 {element}
680 {element}
681 {element}
682 {element}
683 {element}
684 {element}
685 </>
686 );
687 const resolvedChildren = new Array(30).fill(
688 '<div>this is a long return value</div>',
689 );
690 const stream = await serverAct(() =>
691 ReactServerDOMServer.renderToReadableStream(children),
692 );
693 const [stream1, stream2] = passThrough(stream).tee();
694
695 const serializedContent = normalizeSerializedContent(
696 await readResult(stream1),
697 );
698 expect(serializedContent.length).toBeLessThan(__DEV__ ? 630 : 400);
699 expect(timesRendered).toBeLessThan(5);
700
701 const model = await serverAct(() =>
702 ReactServerDOMClient.createFromReadableStream(stream2, {
703 serverConsumerManifest: {
704 moduleMap: null,
705 moduleLoading: null,
706 },
707 }),
708 );
709
710 // Use the SSR render to resolve any lazy elements
711 const ssrStream = await serverAct(() =>
712 ReactDOMServer.renderToReadableStream(model),
713 );
714 // Should still match the result when parsed
715 const result = await readResult(ssrStream);
716 expect(result).toEqual(resolvedChildren.join(''));
717 });
718
719 it('should execute repeated server components in a compact form', async () => {
720 async function ServerComponent({recurse}) {
721 if (recurse > 0) {
722 return <ServerComponent recurse={recurse - 1} />;
723 }
724 return <div>Fin</div>;
725 }
726 const stream = await serverAct(() =>
727 ReactServerDOMServer.renderToReadableStream(
728 <ServerComponent recurse={20} />,
729 ),
730 );
731 const serializedContent = normalizeSerializedContent(
732 await readResult(stream),
733 );
734 const expectedDebugInfoSize = __DEV__ ? 295 * 20 : 0;
735 expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
736 });
737
738 it('should break up large sync components by outlining into streamable elements', async () => {
739 const paragraphs = [];
740 for (let i = 0; i < 20; i++) {
741 const text =
742 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris' +
743 'porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.' +
744 'Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,' +
745 'aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras' +
746 'facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet' +
747 'tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante' +
748 'sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius' +
749 'aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.' +
750 'Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.' +
751 'Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed' +
752 'ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum' +
753 'fermentum quam id aliquam. Donec porta risus vitae pretium posuere.' +
754 'Fusce facilisis eros in lacus tincidunt congue.' +
755 i; /* trick dedupe */
756 paragraphs.push(<p key={i}>{text}</p>);
757 }
758
759 const stream = await serverAct(() =>
760 ReactServerDOMServer.renderToReadableStream(paragraphs),
761 );
762
763 const [stream2, drip] = dripStream(stream);
764
765 // Allow some of the content through.
766 drip(__DEV__ ? 7500 : 5000);
767
768 const result = await ReactServerDOMClient.createFromReadableStream(
769 stream2,
770 {
771 serverConsumerManifest: {
772 moduleMap: null,
773 moduleLoading: null,
774 },
775 },
776 );
777
778 // We should have resolved enough to be able to get the array even though some
779 // of the items inside are still lazy.
780 expect(result.length).toBe(20);
781
782 // Unblock the rest
783 drip(Infinity);
784
785 // Use the SSR render to resolve any lazy elements
786 const ssrStream = await serverAct(() =>
787 ReactDOMServer.renderToReadableStream(result),
788 );
789 const html = await readResult(ssrStream);
790
791 const ssrStream2 = await serverAct(() =>
792 ReactDOMServer.renderToReadableStream(paragraphs),
793 );
794 const html2 = await readResult(ssrStream2);
795
796 expect(html).toBe(html2);
797 });
798
799 it('regression: should not leak serialized size', async () => {
800 const MAX_ROW_SIZE = 3200;
801 // This test case is a bit convoluted and may no longer trigger the original bug.
802 // Originally, the size of `promisedText` was not cleaned up so the sync portion
803 // ended up being deferred immediately when we called `renderToReadableStream` again
804 // i.e. `result2.syncText` became a Lazy element on the second request.
805 const longText = 'd'.repeat(MAX_ROW_SIZE);
806 const promisedText = Promise.resolve(longText);
807 const model = {syncText: <p>{longText}</p>, promisedText};
808
809 const stream = await serverAct(() =>
810 ReactServerDOMServer.renderToReadableStream(model),
811 );
812
813 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
814 serverConsumerManifest: {
815 moduleMap: null,
816 moduleLoading: null,
817 },
818 });
819
820 const stream2 = await serverAct(() =>
821 ReactServerDOMServer.renderToReadableStream(model),
822 );
823
824 const result2 = await ReactServerDOMClient.createFromReadableStream(
825 stream2,
826 {
827 serverConsumerManifest: {
828 moduleMap: null,
829 moduleLoading: null,
830 },
831 },
832 );
833
834 expect(result2.syncText).toEqual(result.syncText);
835 });
836
837 it('should be able to serialize any kind of typed array', async () => {
838 const buffer = new Uint8Array([
839 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
840 ]).buffer;
841 const buffers = [
842 buffer,
843 new Int8Array(buffer, 1),
844 new Uint8Array(buffer, 2),
845 new Uint8ClampedArray(buffer, 2),
846 new Int16Array(buffer, 2),
847 new Uint16Array(buffer, 2),
848 new Int32Array(buffer, 4),
849 new Uint32Array(buffer, 4),
850 new Float32Array(buffer, 4),
851 new Float64Array(buffer, 0),
852 new BigInt64Array(buffer, 0),
853 new BigUint64Array(buffer, 0),
854 new DataView(buffer, 3),
855 ];
856 const stream = await serverAct(() =>
857 passThrough(ReactServerDOMServer.renderToReadableStream(buffers)),
858 );
859 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
860 serverConsumerManifest: {
861 moduleMap: null,
862 moduleLoading: null,
863 },
864 });
865 expect(result).toEqual(buffers);
866 });
867
868 it('should be able to serialize a blob', async () => {
869 const bytes = new Uint8Array([
870 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
871 ]);
872 const blob = new Blob([bytes, bytes], {
873 type: 'application/x-test',
874 });
875 const stream = await serverAct(() =>
876 passThrough(ReactServerDOMServer.renderToReadableStream(blob)),
877 );
878 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
879 serverConsumerManifest: {
880 moduleMap: null,
881 moduleLoading: null,
882 },
883 });
884 expect(result instanceof Blob).toBe(true);
885 expect(result.size).toBe(bytes.length * 2);
886 expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
887 });
888
889 it('can transport FormData (blobs)', async () => {
890 const bytes = new Uint8Array([
891 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
892 ]);
893 const blob = new Blob([bytes, bytes], {
894 type: 'application/x-test',
895 });
896
897 const formData = new FormData();
898 formData.append('hi', 'world');
899 formData.append('file', blob, 'filename.test');
900
901 expect(formData.get('file') instanceof File).toBe(true);
902 expect(formData.get('file').name).toBe('filename.test');
903
904 const stream = await serverAct(() =>
905 passThrough(ReactServerDOMServer.renderToReadableStream(formData)),
906 );
907 const result = await ReactServerDOMClient.createFromReadableStream(stream, {
908 serverConsumerManifest: {
909 moduleMap: null,
910 moduleLoading: null,
911 },
912 });
913
914 expect(result instanceof FormData).toBe(true);
915 expect(result.get('hi')).toBe('world');
916 const resultBlob = result.get('file');
917 expect(resultBlob instanceof Blob).toBe(true);
918 expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
919 expect(resultBlob.size).toBe(bytes.length * 2);
920 expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
921 });
922
923 it('can pass an async import that resolves later to an outline object like a Map', async () => {
924 let resolve;
925 const promise = new Promise(r => (resolve = r));
926
927 const asyncClient = clientExports(promise);
928
929 // We await the value on the servers so it's an async value that the client should wait for
930 const awaitedValue = await asyncClient;
931
932 const map = new Map();
933 map.set('value', awaitedValue);
934
935 const stream = await serverAct(() =>
936 passThrough(ReactServerDOMServer.renderToReadableStream(map, webpackMap)),
937 );
938
939 // Parsing the root blocks because the module hasn't loaded yet
940 const resultPromise = ReactServerDOMClient.createFromReadableStream(
941 stream,
942 {
943 serverConsumerManifest: {
944 moduleMap: null,
945 moduleLoading: null,
946 },
947 },
948 );
949
950 // Afterwards we finally resolve the module value so it's available on the client
951 resolve('hello');
952
953 const result = await resultPromise;
954 expect(result instanceof Map).toBe(true);
955 expect(result.get('value')).toBe('hello');
956 });
957
958 it('can pass an async import to a ReadableStream while enqueuing in order', async () => {
959 let resolve;
960 const promise = new Promise(r => (resolve = r));
961
962 const asyncClient = clientExports(promise);
963
964 // We await the value on the servers so it's an async value that the client should wait for
965 const awaitedValue = await asyncClient;
966
967 const s = new ReadableStream({
968 start(c) {
969 c.enqueue('hello');
970 c.enqueue(awaitedValue);
971 c.enqueue('!');
972 c.close();
973 },
974 });
975
976 const stream = await serverAct(() =>
977 passThrough(ReactServerDOMServer.renderToReadableStream(s, webpackMap)),
978 );
979
980 const result = await serverAct(() =>
981 ReactServerDOMClient.createFromReadableStream(stream, {
982 serverConsumerManifest: {
983 moduleMap: null,
984 moduleLoading: null,
985 },
986 }),
987 );
988
989 const reader = result.getReader();
990
991 expect(await reader.read()).toEqual({value: 'hello', done: false});
992
993 const readPromise = reader.read();
994 // We resolve this after we've already received the '!' row.
995 await resolve('world');
996
997 expect(await readPromise).toEqual({value: 'world', done: false});
998 expect(await reader.read()).toEqual({value: '!', done: false});
999 expect(await reader.read()).toEqual({value: undefined, done: true});
1000 });
1001
1002 it('can pass an async import a AsyncIterable while allowing peaking at future values', async () => {
1003 let resolve;
1004 const promise = new Promise(r => (resolve = r));
1005
1006 const asyncClient = clientExports(promise);
1007
1008 const multiShotIterable = {
1009 async *[Symbol.asyncIterator]() {
1010 yield 'hello';
1011 // We await the value on the servers so it's an async value that the client should wait for
1012 yield await asyncClient;
1013 yield '!';
1014 },
1015 };
1016
1017 const stream = await serverAct(() =>
1018 passThrough(
1019 ReactServerDOMServer.renderToReadableStream(
1020 multiShotIterable,
1021 webpackMap,
1022 ),
1023 ),
1024 );
1025
1026 // Parsing the root blocks because the module hasn't loaded yet
1027 const result = await serverAct(() =>
1028 ReactServerDOMClient.createFromReadableStream(stream, {
1029 serverConsumerManifest: {
1030 moduleMap: null,
1031 moduleLoading: null,
1032 },
1033 }),
1034 );
1035
1036 const iterator = result[Symbol.asyncIterator]();
1037
1038 expect(await iterator.next()).toEqual({value: 'hello', done: false});
1039
1040 const readPromise = iterator.next();
1041
1042 // While the previous promise didn't resolve yet, we should be able to peak at the next value
1043 // by iterating past it.
1044 expect(await iterator.next()).toEqual({value: '!', done: false});
1045
1046 // We resolve the previous row after we've already received the '!' row.
1047 await resolve('world');
1048 expect(await readPromise).toEqual({value: 'world', done: false});
1049
1050 expect(await iterator.next()).toEqual({value: undefined, done: true});
1051 });
1052
1053 it('should ideally dedupe objects inside async iterables but does not yet', async () => {
1054 const obj = {
1055 this: {is: 'a large objected'},
1056 with: {many: 'properties in it'},
1057 };
1058 const iterable = {
1059 async *[Symbol.asyncIterator]() {
1060 for (let i = 0; i < 30; i++) {
1061 yield obj;
1062 }
1063 },
1064 };
1065
1066 const stream = await serverAct(() =>
1067 ReactServerDOMServer.renderToReadableStream({
1068 iterable,
1069 }),
1070 );
1071 const [stream1, stream2] = passThrough(stream).tee();
1072
1073 const serializedContent = await readResult(stream1);
1074 // TODO: Ideally streams should dedupe objects but because we never outline the objects
1075 // they end up not having a row to reference them nor any of its nested objects.
1076 // expect(serializedContent.length).toBeLessThan(400);
1077 expect(serializedContent.length).toBeGreaterThan(400);
1078
1079 const result = await ReactServerDOMClient.createFromReadableStream(
1080 stream2,
1081 {
1082 serverConsumerManifest: {
1083 moduleMap: null,
1084 moduleLoading: null,
1085 },
1086 },
1087 );
1088
1089 const items = [];
1090 const iterator = result.iterable[Symbol.asyncIterator]();
1091 let entry;
1092 while (!(entry = await iterator.next()).done) {
1093 items.push(entry.value);
1094 }
1095
1096 // Should still match the result when parsed
1097 expect(items.length).toBe(30);
1098 // TODO: These should be the same
1099 // expect(items[5]).toBe(items[10]); // two random items are the same instance
1100 expect(items[5]).toEqual(items[10]);
1101 });
1102
1103 function clientComponent(name, chunkFilename) {
1104 return clientExports(
1105 function Client() {
1106 return <span>{name}</span>;
1107 },
1108 'chunk-' + name,
1109 chunkFilename,
1110 Promise.resolve(),
1111 );
1112 }
1113
1114 async function renderClients(chunkFilenames) {
1115 const Clients = chunkFilenames.map(chunkFilename =>
1116 clientComponent('Client', chunkFilename),
1117 );
1118 const stream = await serverAct(() =>
1119 ReactServerDOMServer.renderToReadableStream(
1120 <div>
1121 {Clients.map((Client, i) => (
1122 <Client key={i} />
1123 ))}
1124 </div>,
1125 webpackMap,
1126 ),
1127 );
1128 const [stream1, stream2] = passThrough(stream).tee();
1129 const payload = await readResult(stream1);
1130 const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
1131 serverConsumerManifest: {
1132 moduleMap: null,
1133 moduleLoading: null,
1134 },
1135 });
1136 const ssrStream = await serverAct(() =>
1137 ReactDOMServer.renderToReadableStream(model),
1138 );
1139 expect(await readResult(ssrStream)).toBe(
1140 '<div>' + '<span>Client</span>'.repeat(Clients.length) + '</div>',
1141 );
1142 return payload;
1143 }
1144
1145 it('should dedupe strings inside client reference metadata', async () => {
1146 // Bundlers repeat the same chunk in the metadata of every client reference
1147 // that needs it.
1148 const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
1149 const shared = await renderClients(new Array(10).fill(chunk));
1150 // The same length, so sharing is the only difference between the two.
1151 const distinct = await renderClients(
1152 Array.from(
1153 {length: 10},
1154 (_, i) => 'unique/hashed-chunk-' + ('' + i).padStart(16, '0') + '.js',
1155 ),
1156 );
1157
1158 // However many references there are, the chunk goes on the wire once, as a
1159 // row that every import row points at.
1160 expect(shared.split(chunk).length - 1).toBe(1);
1161 expect(distinct.length - shared.length).toBeGreaterThan(8 * chunk.length);
1162
1163 // The client resolves a client reference while parsing its row, so the
1164 // outlined copy has to arrive before every row that points at it. The
1165 // chunk id is too short to be outlined, so it counts the rows.
1166 const beforeOutlinedCopy = shared.slice(0, shared.indexOf(chunk));
1167 expect(beforeOutlinedCopy).not.toContain('chunk-Client');
1168 });
1169
1170 it('should escape strings inside client reference metadata', async () => {
1171 // A leading $ has to be escaped whether the string gets outlined or not.
1172 const outlinedChunk = '$shared/hashed-chunk-0f1e2d3c4b5a6978.js';
1173 const inlineChunk = '$chunk.js';
1174 const outlined = await renderClients(new Array(3).fill(outlinedChunk));
1175 const inline = await renderClients(new Array(3).fill(inlineChunk));
1176
1177 expect(outlined.split('$' + outlinedChunk).length - 1).toBe(1);
1178 expect(inline.split('$' + inlineChunk).length - 1).toBe(3);
1179 });
1180
1181 it('should not dedupe import strings below the size limit', async () => {
1182 // A short string costs more to reference than to repeat.
1183 const shortChunk = 'abc/chunk-15.js';
1184 const longChunk = 'abcd/chunk-16.js';
1185 const short = await renderClients(new Array(10).fill(shortChunk));
1186 const long = await renderClients(new Array(10).fill(longChunk));
1187
1188 expect(short.split(shortChunk).length - 1).toBe(10);
1189 expect(long.split(longChunk).length - 1).toBe(1);
1190 // The longer chunk is the one that produces the smaller payload.
1191 expect(long.length).toBeLessThan(short.length);
1192 });
1193
1194 it('should stop tracking new import strings once the budget is spent', async () => {
1195 // Every chunk is outlined the first time it's seen, so chunks that never
1196 // repeat spend budget too. 32 fillers of 1 KiB fill the 32 KiB budget.
1197 const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
1198 const repeats = new Array(10).fill(chunk);
1199 const filler = (count, length) =>
1200 Array.from({length: count}, (_, i) =>
1201 ('filler/chunk-' + i + '-').padEnd(length - 3, 'x').concat('.js'),
1202 );
1203 const fitsInTheRest = await renderClients(filler(31, 1024).concat(repeats));
1204 const findsItSpent = await renderClients(filler(32, 1024).concat(repeats));
1205
1206 expect(fitsInTheRest.split(chunk).length - 1).toBe(1);
1207 expect(findsItSpent.split(chunk).length - 1).toBe(10);
1208
1209 // A string outlined before the budget is spent keeps deduping after.
1210 const lastFiller = filler(1, 32768 - 31 * 1024 - chunk.length);
1211 const trackedBefore = await renderClients(
1212 [chunk].concat(filler(31, 1024), lastFiller, repeats),
1213 );
1214
1215 expect(trackedBefore.split(chunk).length - 1).toBe(1);
1216
1217 const bigChunk = 'path/to/' + 'a'.repeat(40000) + '.js';
1218 const big = await renderClients(new Array(3).fill(bigChunk));
1219
1220 expect(big.split(bigChunk).length - 1).toBe(3);
1221 });
1222
1223 it('should dedupe import strings produced by toJSON', async () => {
1224 const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
1225 const payload = await renderClients(
1226 Array.from({length: 3}, () => ({
1227 toJSON() {
1228 return chunk;
1229 },
1230 })),
1231 );
1232
1233 expect(payload.split(chunk).length - 1).toBe(1);
1234 });
1235
1236 it('should error on circular client reference metadata', async () => {
1237 const circular = [];
1238 circular.push(circular);
1239 const Client = clientComponent('Client', circular);
1240
1241 const errors = [];
1242 const stream = await serverAct(() =>
1243 ReactServerDOMServer.renderToReadableStream(<Client />, webpackMap, {
1244 onError(error) {
1245 errors.push(error.message);
1246 },
1247 }),
1248 );
1249 await readResult(stream);
1250
1251 expect(errors).toEqual([
1252 expect.stringContaining('Converting circular structure to JSON'),
1253 ]);
1254 });
1255
1256 it('should not dedupe strings in the model', async () => {
1257 // Only import metadata is deduped. Keying a map on arbitrary model strings
1258 // would hold them in memory for the rest of the request.
1259 const text = 'a repeated model string well past the import threshold';
1260 const model = new Array(10).fill(text);
1261
1262 const stream = await serverAct(() =>
1263 ReactServerDOMServer.renderToReadableStream(model),
1264 );
1265 const [stream1, stream2] = passThrough(stream).tee();
1266
1267 const payload = await readResult(stream1);
1268 expect(payload.split(text).length - 1).toBe(10);
1269
1270 const result = await ReactServerDOMClient.createFromReadableStream(
1271 stream2,
1272 {
1273 serverConsumerManifest: {
1274 moduleMap: null,
1275 moduleLoading: null,
1276 },
1277 },
1278 );
1279 expect(result).toEqual(model);
1280 });
1281
1282 // @gate __DEV__
1283 it('should not dedupe import metadata on the debug channel', async () => {
1284 // The debug channel is a separate transport, so a row it emits can't be
1285 // referenced from the main stream and vice versa.
1286 const chunk = 'shared/hashed-chunk-0f1e2d3c4b5a6978.js';
1287 const A = clientComponent('Client', chunk);
1288 const B = clientComponent('Client', chunk);
1289 const C = clientComponent('Client', chunk);
1290
1291 function Server({a, b, c}) {
1292 return ReactServer.createElement('div', null, a, b, c);
1293 }
1294
1295 let debugContent = '';
1296 const debugChannel = {
1297 writable: new WritableStream({
1298 write(value) {
1299 debugContent += Buffer.from(value).toString('utf8');
1300 },
1301 }),
1302 };
1303
1304 const stream = await serverAct(() =>
1305 ReactServerDOMServer.renderToReadableStream(
1306 // We can't use JSX here because it'll use the Client React.
1307 ReactServer.createElement(Server, {
1308 a: ReactServer.createElement(A),
1309 b: ReactServer.createElement(B),
1310 c: ReactServer.createElement(C),
1311 }),
1312 webpackMap,
1313 {debugChannel},
1314 ),
1315 );
1316 const payload = await readResult(stream);
1317
1318 // The main stream dedupes as usual.
1319 expect(payload.split(chunk).length - 1).toBe(1);
1320
1321 // The debug channel can't point at that row, so every import row it writes
1322 // spells the chunk out. The chunk id is too short to be outlined, so it
1323 // counts those rows.
1324 expect(debugContent).toContain('chunk-Client');
1325 expect(debugContent.split(chunk).length).toBe(
1326 debugContent.split('chunk-Client').length,
1327 );
1328 });
1329
1330 it('warns if passing a this argument to bind() of a server reference', async () => {
1331 const ServerModule = serverExports({
1332 greet: function () {},
1333 });
1334
1335 const ServerModuleImportedOnClient = {
1336 greet: ReactServerDOMClient.createServerReference(
1337 ServerModule.greet.$$id,
1338 async function (ref, args) {},
1339 ),
1340 };
1341
1342 ServerModule.greet.bind({}, 'hi');
1343 assertConsoleErrorDev([
1344 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
1345 ]);
1346
1347 ServerModuleImportedOnClient.greet.bind({}, 'hi');
1348 assertConsoleErrorDev([
1349 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
1350 ]);
1351 });
1352
1353 it('should supports ReadableStreams with typed arrays', async () => {
1354 const buffer = new Uint8Array([
1355 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
1356 ]).buffer;
1357 const buffers = [
1358 buffer,
1359 new Int8Array(buffer, 1),
1360 new Uint8Array(buffer, 2),
1361 new Uint8ClampedArray(buffer, 2),
1362 new Int16Array(buffer, 2),
1363 new Uint16Array(buffer, 2),
1364 new Int32Array(buffer, 4),
1365 new Uint32Array(buffer, 4),
1366 new Float32Array(buffer, 4),
1367 new Float64Array(buffer, 0),
1368 new BigInt64Array(buffer, 0),
1369 new BigUint64Array(buffer, 0),
1370 new DataView(buffer, 3),
1371 ];
1372
1373 // This is not a binary stream, it's a stream that contain binary chunks.
1374 const s = new ReadableStream({
1375 start(c) {
1376 for (let i = 0; i < buffers.length; i++) {
1377 c.enqueue(buffers[i]);
1378 }
1379 c.close();
1380 },
1381 });
1382
1383 const stream = await serverAct(() =>
1384 ReactServerDOMServer.renderToReadableStream(s, {}),
1385 );
1386
1387 const [stream1, stream2] = passThrough(stream).tee();
1388
1389 const result = await ReactServerDOMClient.createFromReadableStream(
1390 stream1,
1391 {
1392 serverConsumerManifest: {
1393 moduleMap: null,
1394 moduleLoading: null,
1395 },
1396 },
1397 );
1398
1399 expect(await readByteLength(stream2)).toBeLessThan(300);
1400
1401 const streamedBuffers = [];
1402 const reader = result.getReader();
1403 let entry;
1404 while (!(entry = await reader.read()).done) {
1405 streamedBuffers.push(entry.value);
1406 }
1407
1408 expect(streamedBuffers).toEqual(buffers);
1409 });
1410
1411 it('should support binary ReadableStreams', async () => {
1412 const encoder = new TextEncoder();
1413 const words = ['Hello', 'streaming', 'world'];
1414
1415 const stream = new ReadableStream({
1416 type: 'bytes',
1417 async start(controller) {
1418 for (let i = 0; i < words.length; i++) {
1419 const chunk = encoder.encode(words[i] + ' ');
1420 controller.enqueue(chunk);
1421 }
1422 controller.close();
1423 },
1424 });
1425
1426 const rscStream = await serverAct(() =>
1427 ReactServerDOMServer.renderToReadableStream(stream, {}),
1428 );
1429
1430 const result = await ReactServerDOMClient.createFromReadableStream(
1431 rscStream,
1432 {
1433 serverConsumerManifest: {
1434 moduleMap: null,
1435 moduleLoading: null,
1436 },
1437 },
1438 );
1439
1440 const reader = result.getReader();
1441 const decoder = new TextDecoder();
1442
1443 let text = '';
1444 let entry;
1445 while (!(entry = await reader.read()).done) {
1446 text += decoder.decode(entry.value);
1447 }
1448
1449 expect(text).toBe('Hello streaming world ');
1450 });
1451
1452 it('should support large binary ReadableStreams', async () => {
1453 const chunkCount = 100;
1454 const chunkSize = 1024;
1455 const expectedBytes = [];
1456
1457 const stream = new ReadableStream({
1458 type: 'bytes',
1459 start(controller) {
1460 for (let i = 0; i < chunkCount; i++) {
1461 const chunk = new Uint8Array(chunkSize);
1462 for (let j = 0; j < chunkSize; j++) {
1463 chunk[j] = (i + j) % 256;
1464 }
1465 expectedBytes.push(...Array.from(chunk));
1466 controller.enqueue(chunk);
1467 }
1468 controller.close();
1469 },
1470 });
1471
1472 const rscStream = await serverAct(() =>
1473 ReactServerDOMServer.renderToReadableStream(stream, {}),
1474 );
1475
1476 const result = await ReactServerDOMClient.createFromReadableStream(
1477 // Use passThrough to split and rejoin chunks at arbitrary boundaries.
1478 passThrough(rscStream),
1479 {
1480 serverConsumerManifest: {
1481 moduleMap: null,
1482 moduleLoading: null,
1483 },
1484 },
1485 );
1486
1487 const reader = result.getReader();
1488 const receivedBytes = [];
1489 let entry;
1490 while (!(entry = await reader.read()).done) {
1491 expect(entry.value instanceof Uint8Array).toBe(true);
1492 receivedBytes.push(...Array.from(entry.value));
1493 }
1494
1495 expect(receivedBytes).toEqual(expectedBytes);
1496 });
1497
1498 it('should support BYOB binary ReadableStreams', async () => {
1499 const sourceBytes = [
1500 123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
1501 ];
1502
1503 // Create separate buffers for each typed array to avoid ArrayBuffer
1504 // transfer issues. Each view needs its own buffer because enqueue()
1505 // transfers ownership.
1506 const buffers = [
1507 new Int8Array(sourceBytes.slice(1)),
1508 new Uint8Array(sourceBytes.slice(2)),
1509 new Uint8ClampedArray(sourceBytes.slice(2)),
1510 new Int16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
1511 new Uint16Array(new Uint8Array(sourceBytes.slice(2)).buffer),
1512 new Int32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
1513 new Uint32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
1514 new Float32Array(new Uint8Array(sourceBytes.slice(4)).buffer),
1515 new Float64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
1516 new BigInt64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
1517 new BigUint64Array(new Uint8Array(sourceBytes.slice(0)).buffer),
1518 new DataView(new Uint8Array(sourceBytes.slice(3)).buffer),
1519 ];
1520
1521 // Save expected bytes before enqueueing (which will detach the buffers).
1522 const expectedBytes = buffers.flatMap(c =>
1523 Array.from(new Uint8Array(c.buffer, c.byteOffset, c.byteLength)),
1524 );
1525
1526 // This a binary stream where each chunk ends up as Uint8Array.
1527 const s = new ReadableStream({
1528 type: 'bytes',
1529 start(c) {
1530 for (let i = 0; i < buffers.length; i++) {
1531 c.enqueue(buffers[i]);
1532 }
1533 c.close();
1534 },
1535 });
1536
1537 const stream = await serverAct(() =>
1538 ReactServerDOMServer.renderToReadableStream(s, {}),
1539 );
1540
1541 const [stream1, stream2] = passThrough(stream).tee();
1542
1543 const result = await ReactServerDOMClient.createFromReadableStream(
1544 stream1,
1545 {
1546 serverConsumerManifest: {
1547 moduleMap: null,
1548 moduleLoading: null,
1549 },
1550 },
1551 );
1552
1553 expect(await readByteLength(stream2)).toBeLessThan(300);
1554
1555 const streamedBuffers = [];
1556 const reader = result.getReader({mode: 'byob'});
1557 let entry;
1558 while (!(entry = await reader.read(new Uint8Array(10))).done) {
1559 expect(entry.value instanceof Uint8Array).toBe(true);
1560 streamedBuffers.push(entry.value);
1561 }
1562
1563 // The streamed buffers might be in different chunks and in Uint8Array form but
1564 // the concatenated bytes should be the same.
1565 expect(streamedBuffers.flatMap(t => Array.from(t))).toEqual(expectedBytes);
1566 });
1567
1568 // @gate !__DEV__ || enableComponentPerformanceTrack
1569 it('supports async server component debug info as the element owner in DEV', async () => {
1570 function Container({children}) {
1571 return children;
1572 }
1573
1574 const promise = Promise.resolve(true);
1575 async function Greeting({firstName}) {
1576 // We can't use JSX here because it'll use the Client React.
1577 const child = ReactServer.createElement(
1578 'span',
1579 null,
1580 'Hello, ' + firstName,
1581 );
1582 // Yield the synchronous pass
1583 await promise;
1584 // We should still be able to track owner using AsyncLocalStorage.
1585 return ReactServer.createElement(Container, null, child);
1586 }
1587
1588 const model = {
1589 greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
1590 };
1591
1592 const stream = await serverAct(() =>
1593 ReactServerDOMServer.renderToReadableStream(model, webpackMap),
1594 );
1595
1596 const rootModel = await serverAct(() =>
1597 ReactServerDOMClient.createFromReadableStream(stream, {
1598 serverConsumerManifest: {
1599 moduleMap: null,
1600 moduleLoading: null,
1601 },
1602 }),
1603 );
1604
1605 const ssrStream = await serverAct(() =>
1606 ReactDOMServer.renderToReadableStream(rootModel.greeting),
1607 );
1608 const result = await readResult(ssrStream);
1609 expect(result).toEqual('<span>Hello, Seb</span>');
1610
1611 // Resolve the React Lazy wrapper which must have resolved by now.
1612 const lazyWrapper = rootModel.greeting;
1613 const greeting = lazyWrapper._init(lazyWrapper._payload);
1614
1615 // We've rendered down to the span.
1616 expect(greeting.type).toBe('span');
1617 if (__DEV__) {
1618 const greetInfo = expect.objectContaining({
1619 name: 'Greeting',
1620 env: 'Server',
1621 });
1622 if (gate(flags => flags.enableAsyncDebugInfo)) {
1623 expect(greeting._debugInfo).toEqual([
1624 {time: 12},
1625 greetInfo,
1626 {time: 13},
1627 expect.objectContaining({
1628 name: 'Container',
1629 env: 'Server',
1630 owner: greetInfo,
1631 }),
1632 {time: 14},
1633 ]);
1634 }
1635 // The owner that created the span was the outer server component.
1636 // We expect the debug info to be referentially equal to the owner.
1637 expect(greeting._owner).toBe(greeting._debugInfo[1]);
1638 } else {
1639 expect(lazyWrapper._debugInfo).toBe(undefined);
1640 expect(greeting._owner).toBe(undefined);
1641 }
1642 });
1643
1644 // @gate __DEV__
1645 it('can get the component owner stacks asynchronously', async () => {
1646 let stack;
1647
1648 function Foo() {
1649 return ReactServer.createElement(Bar, null);
1650 }
1651 function Bar() {
1652 return ReactServer.createElement(
1653 'div',
1654 null,
1655 ReactServer.createElement(Baz, null),
1656 );
1657 }
1658
1659 const promise = Promise.resolve(0);
1660
1661 async function Baz() {
1662 await promise;
1663 stack = ReactServer.captureOwnerStack();
1664 return ReactServer.createElement('span', null, 'hi');
1665 }
1666
1667 const stream = await serverAct(() =>
1668 ReactServerDOMServer.renderToReadableStream(
1669 ReactServer.createElement(
1670 'div',
1671 null,
1672 ReactServer.createElement(Foo, null),
1673 ),
1674 webpackMap,
1675 ),
1676 );
1677 await readResult(stream);
1678
1679 expect(normalizeCodeLocInfo(stack)).toBe(
1680 '\n in Bar (at **)' + '\n in Foo (at **)',
1681 );
1682 });
1683
1684 it('supports server components in ssr component stacks', async () => {
1685 let reject;
1686 const promise = new Promise((_, r) => (reject = r));
1687 async function Erroring() {
1688 await promise;
1689 return 'should not render';
1690 }
1691
1692 const model = {
1693 root: ReactServer.createElement(Erroring),
1694 };
1695
1696 const stream = await serverAct(() =>
1697 ReactServerDOMServer.renderToReadableStream(model, webpackMap, {
1698 onError() {},
1699 }),
1700 );
1701
1702 const rootModel = await serverAct(() =>
1703 ReactServerDOMClient.createFromReadableStream(stream, {
1704 serverConsumerManifest: {
1705 moduleMap: null,
1706 moduleLoading: null,
1707 },
1708 }),
1709 );
1710
1711 const errors = [];
1712 const result = serverAct(() =>
1713 ReactDOMServer.renderToReadableStream(<div>{rootModel.root}</div>, {
1714 onError(error, {componentStack}) {
1715 errors.push({
1716 error,
1717 componentStack: normalizeCodeLocInfo(componentStack),
1718 });
1719 },
1720 }),
1721 );
1722
1723 const theError = new Error('my error');
1724 reject(theError);
1725
1726 const expectedMessage = __DEV__
1727 ? 'my error'
1728 : 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.';
1729
1730 try {
1731 await result;
1732 } catch (x) {
1733 expect(x).toEqual(
1734 expect.objectContaining({
1735 message: expectedMessage,
1736 }),
1737 );
1738 }
1739
1740 expect(errors).toEqual([
1741 {
1742 error: expect.objectContaining({
1743 message: expectedMessage,
1744 }),
1745 componentStack: (__DEV__ ? '\n in Erroring' : '') + '\n in div',
1746 },
1747 ]);
1748 });
1749
1750 it('can prerender', async () => {
1751 let resolveGreeting;
1752 const greetingPromise = new Promise(resolve => {
1753 resolveGreeting = resolve;
1754 });
1755
1756 function App() {
1757 return (
1758 <div>
1759 <Greeting />
1760 </div>
1761 );
1762 }
1763
1764 async function Greeting() {
1765 await greetingPromise;
1766 return 'hello world';
1767 }
1768
1769 const {pendingResult} = await serverAct(async () => {
1770 // destructure trick to avoid the act scope from awaiting the returned value
1771 return {
1772 pendingResult: ReactServerDOMStaticServer.prerender(
1773 <App />,
1774 webpackMap,
1775 ),
1776 };
1777 });
1778
1779 resolveGreeting();
1780 const {prelude} = await pendingResult;
1781
1782 function ClientRoot({response}) {
1783 return use(response);
1784 }
1785
1786 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1787 serverConsumerManifest: {
1788 moduleMap: null,
1789 moduleLoading: null,
1790 },
1791 });
1792 // Use the SSR render to resolve any lazy elements
1793 const ssrStream = await serverAct(() =>
1794 ReactDOMServer.renderToReadableStream(
1795 React.createElement(ClientRoot, {response}),
1796 ),
1797 );
1798 // Should still match the result when parsed
1799 const result = await readResult(ssrStream);
1800 expect(result).toBe('<div>hello world</div>');
1801 });
1802
1803 it('does not propagate abort reasons errors when aborting a prerender', async () => {
1804 let resolveGreeting;
1805 const greetingPromise = new Promise(resolve => {
1806 resolveGreeting = resolve;
1807 });
1808
1809 function App() {
1810 return (
1811 <div>
1812 <ReactServer.Suspense fallback="loading...">
1813 <Greeting />
1814 </ReactServer.Suspense>
1815 </div>
1816 );
1817 }
1818
1819 async function Greeting() {
1820 await greetingPromise;
1821 return 'hello world';
1822 }
1823
1824 const controller = new AbortController();
1825 const errors = [];
1826 const {pendingResult} = await serverAct(async () => {
1827 // destructure trick to avoid the act scope from awaiting the returned value
1828 return {
1829 pendingResult: ReactServerDOMStaticServer.prerender(
1830 <App />,
1831 webpackMap,
1832 {
1833 signal: controller.signal,
1834 onError(err) {
1835 errors.push(err);
1836 },
1837 },
1838 ),
1839 };
1840 });
1841
1842 await serverAct(() => {
1843 controller.abort('boom');
1844 });
1845 resolveGreeting();
1846 const {prelude} = await pendingResult;
1847
1848 expect(errors).toEqual([]);
1849
1850 function ClientRoot({response}) {
1851 return use(response);
1852 }
1853
1854 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1855 serverConsumerManifest: {
1856 moduleMap: null,
1857 moduleLoading: null,
1858 },
1859 });
1860 const fizzController = new AbortController();
1861 errors.length = 0;
1862 const ssrStream = await serverAct(() =>
1863 ReactDOMServer.renderToReadableStream(
1864 React.createElement(ClientRoot, {response}),
1865 {
1866 signal: fizzController.signal,
1867 onError(error) {
1868 errors.push(error);
1869 },
1870 },
1871 ),
1872 );
1873 fizzController.abort('bam');
1874 expect(errors).toEqual([new Error('Connection closed.')]);
1875 // Should still match the result when parsed
1876 const result = await readResult(ssrStream);
1877 const div = document.createElement('div');
1878 div.innerHTML = result;
1879 expect(div.textContent).toBe('loading...');
1880 });
1881
1882 it('should abort parsing an incomplete prerender payload', async () => {
1883 const infinitePromise = new Promise(() => {});
1884 const controller = new AbortController();
1885 const errors = [];
1886 const {pendingResult} = await serverAct(async () => {
1887 // destructure trick to avoid the act scope from awaiting the returned value
1888 return {
1889 pendingResult: ReactServerDOMStaticServer.prerender(
1890 {promise: infinitePromise},
1891 webpackMap,
1892 {
1893 signal: controller.signal,
1894 onError(err) {
1895 errors.push(err);
1896 },
1897 },
1898 ),
1899 };
1900 });
1901
1902 controller.abort();
1903 const {prelude} = await serverAct(() => pendingResult);
1904
1905 expect(errors).toEqual([]);
1906
1907 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1908 serverConsumerManifest: {
1909 moduleMap: {},
1910 moduleLoading: {},
1911 },
1912 });
1913
1914 // Wait for the stream to finish and therefore abort before we try to .then the response.
1915 await 0;
1916
1917 const result = await response;
1918
1919 let error = null;
1920 try {
1921 await result.promise;
1922 } catch (x) {
1923 error = x;
1924 }
1925 expect(error).not.toBe(null);
1926 expect(error.message).toBe('Connection closed.');
1927 });
1928
1929 it('should be able to handle a rejected promise in prerender', async () => {
1930 const expectedError = new Error('Bam!');
1931 const errors = [];
1932
1933 const {prelude} = await ReactServerDOMStaticServer.prerender(
1934 Promise.reject(expectedError),
1935 webpackMap,
1936 {
1937 onError(err) {
1938 errors.push(err);
1939 },
1940 },
1941 );
1942
1943 expect(errors).toEqual([expectedError]);
1944
1945 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1946 serverConsumerManifest: {
1947 moduleMap: {},
1948 moduleLoading: {},
1949 },
1950 });
1951
1952 let error = null;
1953 try {
1954 await response;
1955 } catch (x) {
1956 error = x;
1957 }
1958
1959 const expectedMessage = __DEV__
1960 ? expectedError.message
1961 : 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.';
1962
1963 expect(error).not.toBe(null);
1964 expect(error.message).toBe(expectedMessage);
1965 });
1966
1967 it('should be able to handle an erroring async iterable in prerender', async () => {
1968 const expectedError = new Error('Bam!');
1969 const errors = [];
1970
1971 const {prelude} = await ReactServerDOMStaticServer.prerender(
1972 {
1973 async *[Symbol.asyncIterator]() {
1974 await serverAct(() => {
1975 throw expectedError;
1976 });
1977 },
1978 },
1979 webpackMap,
1980 {
1981 onError(err) {
1982 errors.push(err);
1983 },
1984 },
1985 );
1986
1987 expect(errors).toEqual([expectedError]);
1988
1989 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
1990 serverConsumerManifest: {
1991 moduleMap: {},
1992 moduleLoading: {},
1993 },
1994 });
1995
1996 let error = null;
1997 try {
1998 const result = await response;
1999 const iterator = result[Symbol.asyncIterator]();
2000 await iterator.next();
2001 } catch (x) {
2002 error = x;
2003 }
2004
2005 const expectedMessage = __DEV__
2006 ? expectedError.message
2007 : 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.';
2008
2009 expect(error).not.toBe(null);
2010 expect(error.message).toBe(expectedMessage);
2011 });
2012
2013 it('should be able to handle an erroring readable stream in prerender', async () => {
2014 const expectedError = new Error('Bam!');
2015 const errors = [];
2016
2017 let streamController;
2018 const erroringStream = new ReadableStream({
2019 start(controller) {
2020 streamController = controller;
2021 },
2022 });
2023
2024 const {pendingResult} = await serverAct(async () => {
2025 // destructure trick to avoid the act scope from awaiting the returned value
2026 return {
2027 pendingResult: ReactServerDOMStaticServer.prerender(
2028 erroringStream,
2029 webpackMap,
2030 {
2031 onError(err) {
2032 errors.push(err);
2033 },
2034 },
2035 ),
2036 };
2037 });
2038
2039 await serverAct(() => {
2040 streamController.error(expectedError);
2041 });
2042
2043 const {prelude} = await pendingResult;
2044
2045 expect(errors).toEqual([expectedError]);
2046
2047 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
2048 serverConsumerManifest: {
2049 moduleMap: {},
2050 moduleLoading: {},
2051 },
2052 });
2053
2054 let error = null;
2055 try {
2056 const stream = await response;
2057 await stream.getReader().read();
2058 } catch (x) {
2059 error = x;
2060 }
2061
2062 const expectedMessage = __DEV__
2063 ? expectedError.message
2064 : 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.';
2065
2066 expect(error).not.toBe(null);
2067 expect(error.message).toBe(expectedMessage);
2068 });
2069
2070 it('can prerender an async iterable', async () => {
2071 const errors = [];
2072
2073 const {prelude} = await ReactServerDOMStaticServer.prerender(
2074 {
2075 async *[Symbol.asyncIterator]() {
2076 yield 'hello';
2077 yield ' ';
2078 yield 'world';
2079 },
2080 },
2081 webpackMap,
2082 {
2083 onError(err) {
2084 errors.push(err);
2085 },
2086 },
2087 );
2088
2089 expect(errors).toEqual([]);
2090
2091 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
2092 serverConsumerManifest: {
2093 moduleMap: {},
2094 moduleLoading: {},
2095 },
2096 });
2097
2098 let text = '';
2099 const result = await response;
2100 const iterator = result[Symbol.asyncIterator]();
2101
2102 while (true) {
2103 const {done, value} = await iterator.next();
2104 if (done) {
2105 break;
2106 }
2107 text += value;
2108 }
2109
2110 expect(text).toBe('hello world');
2111 });
2112
2113 it('can prerender a readable stream', async () => {
2114 const errors = [];
2115
2116 const {prelude} = await ReactServerDOMStaticServer.prerender(
2117 new ReadableStream({
2118 start(controller) {
2119 controller.enqueue('hello world');
2120 controller.close();
2121 },
2122 }),
2123 webpackMap,
2124 {
2125 onError(err) {
2126 errors.push(err);
2127 },
2128 },
2129 );
2130
2131 expect(errors).toEqual([]);
2132
2133 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
2134 serverConsumerManifest: {
2135 moduleMap: {},
2136 moduleLoading: {},
2137 },
2138 });
2139
2140 const stream = await response;
2141 const result = await readResult(stream);
2142
2143 expect(result).toBe('hello world');
2144 });
2145
2146 it('does not return a prerender prelude early when an error is emitted and there are still pending tasks', async () => {
2147 let rejectPromise;
2148 const rejectingPromise = new Promise(
2149 (resolve, reject) => (rejectPromise = reject),
2150 );
2151 const expectedError = new Error('Boom!');
2152 const errors = [];
2153
2154 const {prelude} = await ReactServerDOMStaticServer.prerender(
2155 [
2156 rejectingPromise,
2157 {
2158 async *[Symbol.asyncIterator]() {
2159 yield 'hello';
2160 yield ' ';
2161 await serverAct(() => {
2162 rejectPromise(expectedError);
2163 });
2164 yield 'world';
2165 },
2166 },
2167 ],
2168 webpackMap,
2169 {
2170 onError(err) {
2171 errors.push(err);
2172 },
2173 },
2174 );
2175
2176 expect(errors).toEqual([expectedError]);
2177
2178 const response = ReactServerDOMClient.createFromReadableStream(prelude, {
2179 serverConsumerManifest: {
2180 moduleMap: {},
2181 moduleLoading: {},
2182 },
2183 });
2184
2185 let text = '';
2186 const [promise, iterable] = await response;
2187 const iterator = iterable[Symbol.asyncIterator]();
2188
2189 while (true) {
2190 const {done, value} = await iterator.next();
2191 if (done) {
2192 break;
2193 }
2194 text += value;
2195 }
2196
2197 expect(text).toBe('hello world');
2198
2199 let error = null;
2200 try {
2201 await promise;
2202 } catch (x) {
2203 error = x;
2204 }
2205
2206 const expectedMessage = __DEV__
2207 ? expectedError.message
2208 : 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.';
2209
2210 expect(error).not.toBe(null);
2211 expect(error.message).toBe(expectedMessage);
2212 });
2213
2214 it('does not include source locations in component stacks for halted components', async () => {
2215 // We only support adding source locations for halted components in the Node.js builds.
2216
2217 async function Component() {
2218 await new Promise(() => {});
2219 return null;
2220 }
2221
2222 function App() {
2223 return ReactServer.createElement(
2224 'html',
2225 null,
2226 ReactServer.createElement(
2227 'body',
2228 null,
2229 ReactServer.createElement(
2230 ReactServer.Suspense,
2231 {fallback: 'Loading...'},
2232 ReactServer.createElement(Component, null),
2233 ),
2234 ),
2235 );
2236 }
2237
2238 const serverAbortController = new AbortController();
2239 const errors = [];
2240 const {pendingResult} = await serverAct(async () => {
2241 // destructure trick to avoid the act scope from awaiting the returned value
2242 return {
2243 pendingResult: ReactServerDOMStaticServer.prerender(
2244 ReactServer.createElement(App, null),
2245 webpackMap,
2246 {
2247 signal: serverAbortController.signal,
2248 onError(err) {
2249 errors.push(err);
2250 },
2251 },
2252 ),
2253 };
2254 });
2255
2256 await serverAct(
2257 () =>
2258 new Promise(resolve => {
2259 setImmediate(() => {
2260 serverAbortController.abort();
2261 resolve();
2262 });
2263 }),
2264 );
2265
2266 const {prelude} = await pendingResult;
2267
2268 expect(errors).toEqual([]);
2269
2270 function ClientRoot({response}) {
2271 return use(response);
2272 }
2273
2274 const prerenderResponse = ReactServerDOMClient.createFromReadableStream(
2275 await createBufferedUnclosingStream(prelude),
2276 {
2277 serverConsumerManifest: {
2278 moduleMap: null,
2279 moduleLoading: null,
2280 },
2281 },
2282 );
2283
2284 let componentStack;
2285 let ownerStack;
2286
2287 const clientAbortController = new AbortController();
2288
2289 const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
2290 React.createElement(ClientRoot, {response: prerenderResponse}),
2291 {
2292 signal: clientAbortController.signal,
2293 onError(error, errorInfo) {
2294 componentStack = errorInfo.componentStack;
2295 ownerStack = React.captureOwnerStack
2296 ? React.captureOwnerStack()
2297 : null;
2298 },
2299 },
2300 );
2301
2302 await serverAct(
2303 () =>
2304 new Promise(resolve => {
2305 setImmediate(() => {
2306 clientAbortController.abort();
2307 resolve();
2308 });
2309 }),
2310 );
2311
2312 const fizzPrerenderStream = await fizzPrerenderStreamResult;
2313 const prerenderHTML = await readResult(fizzPrerenderStream.prelude);
2314
2315 expect(prerenderHTML).toContain('Loading...');
2316
2317 if (__DEV__) {
2318 expect(normalizeCodeLocInfo(componentStack)).toBe(
2319 '\n in Component\n' +
2320 ' in Suspense\n' +
2321 ' in body\n' +
2322 ' in html\n' +
2323 ' in App (at **)\n' +
2324 ' in ClientRoot (at **)',
2325 );
2326 } else {
2327 expect(normalizeCodeLocInfo(componentStack)).toBe(
2328 '\n in Suspense\n' +
2329 ' in body\n' +
2330 ' in html\n' +
2331 ' in ClientRoot (at **)',
2332 );
2333 }
2334
2335 if (__DEV__) {
2336 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
2337 } else {
2338 expect(ownerStack).toBeNull();
2339 }
2340 });
2341
2342 it('can pass an async import that resolves later as a prop to a null component', async () => {
2343 let resolveClientComponentChunk;
2344 const client = clientExports(
2345 {
2346 foo: 'bar',
2347 },
2348 '42',
2349 '/test.js',
2350 new Promise(resolve => (resolveClientComponentChunk = resolve)),
2351 );
2352
2353 function ServerComponent(props) {
2354 return null;
2355 }
2356
2357 function App() {
2358 return (
2359 <div>
2360 <ServerComponent client={client} />
2361 </div>
2362 );
2363 }
2364
2365 const stream = await serverAct(() =>
2366 passThrough(
2367 ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
2368 ),
2369 );
2370
2371 // Parsing the root blocks because the module hasn't loaded yet
2372 const response = ReactServerDOMClient.createFromReadableStream(stream, {
2373 serverConsumerManifest: {
2374 moduleMap: null,
2375 moduleLoading: null,
2376 },
2377 });
2378
2379 function ClientRoot() {
2380 return use(response);
2381 }
2382
2383 // Initialize to be blocked.
2384 response.then(() => {});
2385 // Unblock.
2386 resolveClientComponentChunk();
2387
2388 const ssrStream = await serverAct(() =>
2389 ReactDOMServer.renderToReadableStream(<ClientRoot />),
2390 );
2391 const result = await readResult(ssrStream);
2392 expect(result).toEqual('<div></div>');
2393 });
2394
2395 // @gate __DEV__
2396 it('can transport debug info through a separate debug channel', async () => {
2397 function Thrower() {
2398 throw new Error('ssr-throw');
2399 }
2400
2401 const ClientComponentOnTheClient = clientExports(
2402 Thrower,
2403 123,
2404 'path/to/chunk.js',
2405 );
2406
2407 const ClientComponentOnTheServer = clientExports(Thrower);
2408
2409 function App() {
2410 return ReactServer.createElement(
2411 ReactServer.Suspense,
2412 null,
2413 ReactServer.createElement(ClientComponentOnTheClient, null),
2414 );
2415 }
2416
2417 let debugReadableStreamController;
2418
2419 const debugReadableStream = new ReadableStream({
2420 start(controller) {
2421 debugReadableStreamController = controller;
2422 },
2423 });
2424
2425 const rscStream = await serverAct(() =>
2426 passThrough(
2427 ReactServerDOMServer.renderToReadableStream(
2428 ReactServer.createElement(App, null),
2429 webpackMap,
2430 {
2431 debugChannel: {
2432 writable: new WritableStream({
2433 write(chunk) {
2434 debugReadableStreamController.enqueue(chunk);
2435 },
2436 close() {
2437 debugReadableStreamController.close();
2438 },
2439 }),
2440 },
2441 },
2442 ),
2443 ),
2444 );
2445
2446 function ClientRoot({response}) {
2447 return use(response);
2448 }
2449
2450 const serverConsumerManifest = {
2451 moduleMap: {
2452 [webpackMap[ClientComponentOnTheClient.$$id].id]: {
2453 '*': webpackMap[ClientComponentOnTheServer.$$id],
2454 },
2455 },
2456 moduleLoading: webpackModuleLoading,
2457 };
2458
2459 const response = ReactServerDOMClient.createFromReadableStream(
2460 // Create a delayed stream to simulate that the RSC stream might be
2461 // transported slower than the debug channel, which must not lead to a
2462 // `Connection closed` error in the Flight client.
2463 createDelayedStream(rscStream),
2464 {
2465 serverConsumerManifest,
2466 debugChannel: {readable: debugReadableStream},
2467 },
2468 );
2469
2470 let ownerStack;
2471
2472 const ssrStream = await serverAct(() =>
2473 ReactDOMServer.renderToReadableStream(
2474 <ClientRoot response={response} />,
2475 {
2476 onError(err, errorInfo) {
2477 ownerStack = React.captureOwnerStack
2478 ? React.captureOwnerStack()
2479 : null;
2480 },
2481 },
2482 ),
2483 );
2484
2485 const result = await readResult(ssrStream);
2486
2487 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
2488
2489 expect(result).toContain(
2490 'Switched to client rendering because the server rendering errored:\n\nssr-throw',
2491 );
2492 });
2493
2494 // @gate __DEV__
2495 it('can transport debug info through a slow debug channel', async () => {
2496 function Thrower() {
2497 throw new Error('ssr-throw');
2498 }
2499
2500 const ClientComponentOnTheClient = clientExports(
2501 Thrower,
2502 123,
2503 'path/to/chunk.js',
2504 );
2505
2506 const ClientComponentOnTheServer = clientExports(Thrower);
2507
2508 function App() {
2509 return ReactServer.createElement(
2510 ReactServer.Suspense,
2511 null,
2512 ReactServer.createElement(ClientComponentOnTheClient, null),
2513 );
2514 }
2515
2516 let debugReadableStreamController;
2517
2518 const debugReadableStream = new ReadableStream({
2519 start(controller) {
2520 debugReadableStreamController = controller;
2521 },
2522 });
2523
2524 const rscStream = await serverAct(() =>
2525 passThrough(
2526 ReactServerDOMServer.renderToReadableStream(
2527 ReactServer.createElement(App, null),
2528 webpackMap,
2529 {
2530 debugChannel: {
2531 writable: new WritableStream({
2532 write(chunk) {
2533 debugReadableStreamController.enqueue(chunk);
2534 },
2535 close() {
2536 debugReadableStreamController.close();
2537 },
2538 }),
2539 },
2540 },
2541 ),
2542 ),
2543 );
2544
2545 function ClientRoot({response}) {
2546 return use(response);
2547 }
2548
2549 const serverConsumerManifest = {
2550 moduleMap: {
2551 [webpackMap[ClientComponentOnTheClient.$$id].id]: {
2552 '*': webpackMap[ClientComponentOnTheServer.$$id],
2553 },
2554 },
2555 moduleLoading: webpackModuleLoading,
2556 };
2557
2558 const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
2559 serverConsumerManifest,
2560 debugChannel: {
2561 readable:
2562 // Create a delayed stream to simulate that the debug stream might be
2563 // transported slower than the RSC stream, which must not lead to
2564 // missing debug info.
2565 createDelayedStream(debugReadableStream),
2566 },
2567 });
2568
2569 let ownerStack;
2570
2571 const ssrStream = await serverAct(() =>
2572 ReactDOMServer.renderToReadableStream(
2573 <ClientRoot response={response} />,
2574 {
2575 onError(err, errorInfo) {
2576 ownerStack = React.captureOwnerStack
2577 ? React.captureOwnerStack()
2578 : null;
2579 },
2580 },
2581 ),
2582 );
2583
2584 const result = await readResult(ssrStream);
2585
2586 expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)');
2587
2588 expect(result).toContain(
2589 'Switched to client rendering because the server rendering errored:\n\nssr-throw',
2590 );
2591 });
2592
2593 async function renderThroughDebugChannel(chunkFilename) {
2594 const Client = clientComponent('Client', chunkFilename);
2595 // The client reference shows up in the owner's props on the debug channel.
2596 function Server({component}) {
2597 return ReactServer.createElement(component, null);
2598 }
2599
2600 let debugReadableStreamController;
2601 const debugReadableStream = new ReadableStream({
2602 start(controller) {
2603 debugReadableStreamController = controller;
2604 },
2605 });
2606
2607 const stream = await serverAct(() =>
2608 ReactServerDOMServer.renderToReadableStream(
2609 ReactServer.createElement(Server, {component: Client}),
2610 webpackMap,
2611 {
2612 debugChannel: {
2613 writable: new WritableStream({
2614 write(chunk) {
2615 debugReadableStreamController.enqueue(chunk);
2616 },
2617 close() {
2618 debugReadableStreamController.close();
2619 },
2620 }),
2621 },
2622 },
2623 ),
2624 );
2625
2626 const response = ReactServerDOMClient.createFromReadableStream(stream, {
2627 serverConsumerManifest: {moduleMap: null, moduleLoading: null},
2628 debugChannel: {readable: debugReadableStream},
2629 });
2630
2631 function ClientRoot() {
2632 return use(response);
2633 }
2634
2635 const ssrStream = await serverAct(() =>
2636 ReactDOMServer.renderToReadableStream(<ClientRoot />),
2637 );
2638 return readResult(ssrStream);
2639 }
2640
2641 it('can resolve a client reference while debug info is still blocked', async () => {
2642 const result = await renderThroughDebugChannel('path/to/chunk.js');
2643
2644 expect(result).toBe('<span>Client</span>');
2645 });
2646
2647 it('should escape strings in import metadata on the debug channel', async () => {
2648 const result = await renderThroughDebugChannel('$path/to/chunk.js');
2649
2650 expect(result).toBe('<span>Client</span>');
2651 });
2652
2653 it('should properly resolve with deduped objects', async () => {
2654 const obj = {foo: 'hi'};
2655
2656 function Test(props) {
2657 return props.obj.foo;
2658 }
2659
2660 const root = {
2661 obj: obj,
2662 node: <Test obj={obj} />,
2663 };
2664
2665 const stream = ReactServerDOMServer.renderToReadableStream(root);
2666
2667 const response = ReactServerDOMClient.createFromReadableStream(stream, {
2668 serverConsumerManifest: {
2669 moduleMap: null,
2670 moduleLoading: null,
2671 },
2672 });
2673
2674 const result = await response;
2675 expect(result).toEqual({obj: obj, node: 'hi'});
2676 });
2677
2678 it('does not leak the server reference code', async () => {
2679 function foo() {
2680 return 'foo';
2681 }
2682
2683 const bar = () => {
2684 return 'bar';
2685 };
2686
2687 const anonymous = (
2688 () => () =>
2689 'anonymous'
2690 )();
2691
2692 expect(
2693 ReactServerDOMServer.registerServerReference(foo, 'foo-id').toString(),
2694 ).toBe('function () { [omitted code] }');
2695
2696 expect(
2697 ReactServerDOMServer.registerServerReference(bar, 'bar-id').toString(),
2698 ).toBe('function () { [omitted code] }');
2699
2700 expect(
2701 ReactServerDOMServer.registerServerReference(
2702 anonymous,
2703 'anonymous-id',
2704 ).toString(),
2705 ).toBe('function () { [omitted code] }');
2706 });
2707
2708 // A thenable with status 'pending_weak' doesn't keep the Flight stream
2709 // open. If it settles before the stream closes for other reasons its value
2710 // is emitted like a normal pending thenable; otherwise its reference is
2711 // left unfulfilled and stays forever pending on the client.
2712 //
2713 // A framework-style tracker for whether a page accessed its search params
2714 // during a render. The params object is instrumented so that the first
2715 // access settles the usedSearchParams thenable. It settles synchronously
2716 // at the access point, so an access is guaranteed to be encoded before
2717 // the response closes.
2718 function createSearchParams(values) {
2719 const listeners = [];
2720 const usedSearchParams = {
2721 status: 'pending_weak',
2722 value: undefined,
2723 then(onFulfill) {
2724 if (usedSearchParams.status === 'fulfilled') {
2725 onFulfill(usedSearchParams.value);
2726 } else {
2727 listeners.push(onFulfill);
2728 }
2729 },
2730 };
2731 const searchParams = new Proxy(values, {
2732 get(target, key) {
2733 if (usedSearchParams.status === 'pending_weak') {
2734 usedSearchParams.status = 'fulfilled';
2735 usedSearchParams.value = true;
2736 for (let i = 0; i < listeners.length; i++) {
2737 listeners[i](true);
2738 }
2739 listeners.length = 0;
2740 }
2741 return target[key];
2742 },
2743 });
2744 return {searchParams, usedSearchParams};
2745 }
2746
2747 it('emits the value of a weak-pending thenable that settles during the render', async () => {
2748 const {searchParams, usedSearchParams} = createSearchParams({q: 'react'});
2749
2750 function Page() {
2751 return <div>{'Results for ' + searchParams.q}</div>;
2752 }
2753
2754 let response;
2755 await serverAct(() => {
2756 const stream = ReactServerDOMServer.renderToReadableStream({
2757 usedSearchParams,
2758 root: <Page />,
2759 });
2760 // Start consuming immediately, like a server that pipes the response
2761 // while it renders.
2762 response = ReactServerDOMClient.createFromReadableStream(stream, {
2763 serverConsumerManifest: {
2764 moduleMap: null,
2765 moduleLoading: null,
2766 },
2767 });
2768 });
2769
2770 const result = await response;
2771 expect(await result.usedSearchParams).toBe(true);
2772
2773 const ssrStream = await serverAct(() =>
2774 ReactDOMServer.renderToReadableStream(result.root),
2775 );
2776 expect(await readResult(ssrStream)).toBe('<div>Results for react</div>');
2777 });
2778
2779 // @gate enableFlightWeakThenables
2780 it('completes the response without waiting for a weak-pending thenable that never settles', async () => {
2781 const {searchParams, usedSearchParams} = createSearchParams({q: 'react'});
2782
2783 function Page() {
2784 return <div>Static content</div>;
2785 }
2786
2787 const stream = await serverAct(() =>
2788 ReactServerDOMServer.renderToReadableStream({
2789 usedSearchParams,
2790 root: <Page />,
2791 }),
2792 );
2793 const [stream1, stream2] = stream.tee();
2794
2795 let content = null;
2796 const readPromise = readResult(stream1).then(c => (content = c));
2797 await serverAct(async () => {});
2798 // The response completed even though the weak thenable never settled.
2799 expect(content).not.toBe(null);
2800 await readPromise;
2801
2802 const result = await ReactServerDOMClient.createFromReadableStream(
2803 stream2,
2804 {
2805 serverConsumerManifest: {
2806 moduleMap: null,
2807 moduleLoading: null,
2808 },
2809 },
2810 );
2811
2812 // Accessing the params after the response already completed doesn't do
2813 // anything.
2814 expect(searchParams.q).toBe('react');
2815
2816 // The reference is left forever pending, without erroring.
2817 const raced = await Promise.race([
2818 result.usedSearchParams,
2819 Promise.resolve('never accessed'),
2820 ]);
2821 expect(raced).toBe('never accessed');
2822 });
2823
2824 it('emits the value of a weak-pending thenable that settles while the response is still streaming', async () => {
2825 const {searchParams, usedSearchParams} = createSearchParams({q: 'react'});
2826
2827 let resolveData;
2828 const data = new Promise(res => (resolveData = res));
2829 async function Results() {
2830 const filter = await data;
2831 return <div>{'Results for ' + searchParams[filter]}</div>;
2832 }
2833
2834 const stream = await serverAct(() =>
2835 ReactServerDOMServer.renderToReadableStream({
2836 usedSearchParams,
2837 root: <Results />,
2838 }),
2839 );
2840 const [stream1, stream2] = stream.tee();
2841
2842 let content = null;
2843 const readPromise = readResult(stream1).then(c => (content = c));
2844
2845 // The response stays open while the data is loading — because of the
2846 // async component, not because of the unresolved weak thenable.
2847 await serverAct(async () => {});
2848 expect(content).toBe(null);
2849
2850 // The data resolves, the component accesses the search params, and the
2851 // response completes.
2852 await serverAct(() => resolveData('q'));
2853 await serverAct(async () => {});
2854 expect(content).not.toBe(null);
2855 await readPromise;
2856
2857 const result = await ReactServerDOMClient.createFromReadableStream(
2858 stream2,
2859 {
2860 serverConsumerManifest: {
2861 moduleMap: null,
2862 moduleLoading: null,
2863 },
2864 },
2865 );
2866 expect(await result.usedSearchParams).toBe(true);
2867
2868 const ssrStream = await serverAct(() =>
2869 ReactDOMServer.renderToReadableStream(result.root),
2870 );
2871 expect(await readResult(ssrStream)).toBe('<div>Results for react</div>');
2872 });
2873
2874 // @gate !enableFlightWeakThenables
2875 it('treats a weak-pending thenable like a normal pending thenable when the flag is off', async () => {
2876 const {searchParams, usedSearchParams} = createSearchParams({q: 'react'});
2877
2878 function Page() {
2879 return <div>Static content</div>;
2880 }
2881
2882 const stream = await serverAct(() =>
2883 ReactServerDOMServer.renderToReadableStream({
2884 usedSearchParams,
2885 root: <Page />,
2886 }),
2887 );
2888 const [stream1, stream2] = stream.tee();
2889
2890 let content = null;
2891 const readPromise = readResult(stream1).then(c => (content = c));
2892
2893 // Without the flag, the unknown thenable status is treated as an
2894 // ordinary pending thenable, which keeps the response open.
2895 await serverAct(async () => {});
2896 expect(content).toBe(null);
2897
2898 // Accessing the params settles the thenable and lets the response
2899 // complete.
2900 await serverAct(() => {
2901 expect(searchParams.q).toBe('react');
2902 });
2903 await serverAct(async () => {});
2904 expect(content).not.toBe(null);
2905 await readPromise;
2906
2907 const result = await ReactServerDOMClient.createFromReadableStream(
2908 stream2,
2909 {
2910 serverConsumerManifest: {
2911 moduleMap: null,
2912 moduleLoading: null,
2913 },
2914 },
2915 );
2916 expect(await result.usedSearchParams).toBe(true);
2917 });
2918
2919 // @gate enableFlightWeakThenables
2920 it('supports linked lists of weak-pending thenables', async () => {
2921 // Weak thenables compose recursively: the value that a weak-pending
2922 // thenable settles with can itself contain more weak-pending thenables.
2923 // A linked list of them forms an async sequence that never blocks the
2924 // response from completing. Modeled here as a framework tracking which
2925 // params a page accessed during a dynamic render, encoded into the
2926 // response itself as WeakThenable<{value: T, next: WeakThenable<...>}>.
2927 function instrumentParams(params) {
2928 function createWeakNode() {
2929 const listeners = [];
2930 const node = {
2931 status: 'pending_weak',
2932 value: undefined,
2933 then(onFulfill) {
2934 if (node.status === 'fulfilled') {
2935 onFulfill(node.value);
2936 } else {
2937 listeners.push(onFulfill);
2938 }
2939 },
2940 };
2941 return {node, listeners};
2942 }
2943 let tail = createWeakNode();
2944 const head = tail.node;
2945 const accessed = new Set();
2946 const instrumentedParams = new Proxy(params, {
2947 get(target, name) {
2948 if (
2949 typeof name === 'string' &&
2950 name in target &&
2951 !accessed.has(name)
2952 ) {
2953 accessed.add(name);
2954 const settledTail = tail;
2955 tail = createWeakNode();
2956 // Settle the tail of the list synchronously at the access point
2957 // so it's guaranteed to be encoded before the response closes.
2958 const result = {value: name, next: tail.node};
2959 settledTail.node.status = 'fulfilled';
2960 settledTail.node.value = result;
2961 for (let i = 0; i < settledTail.listeners.length; i++) {
2962 settledTail.listeners[i](result);
2963 }
2964 settledTail.listeners.length = 0;
2965 }
2966 return target[name];
2967 },
2968 });
2969 return {params: instrumentedParams, accessedParams: head};
2970 }
2971
2972 const {params, accessedParams} = instrumentParams({
2973 a: 'value-of-a',
2974 b: 'value-of-b',
2975 c: 'value-of-c',
2976 });
2977
2978 function Page() {
2979 // The page reads param a during the render.
2980 return 'Accessed: ' + params.a;
2981 }
2982
2983 let resolveNormal;
2984 const pending = new Promise(res => {
2985 resolveNormal = res;
2986 });
2987
2988 const stream = await serverAct(() =>
2989 ReactServerDOMServer.renderToReadableStream({
2990 accessedParams,
2991 page: <Page />,
2992 pending,
2993 }),
2994 );
2995 const [stream1, stream2] = stream.tee();
2996
2997 let content = null;
2998 const readPromise = readResult(stream1).then(c => (content = c));
2999
3000 // While the normal pending promise holds the stream open, param c is
3001 // accessed, settling the next node of the list.
3002 await serverAct(() => {
3003 expect(params.c).toBe('value-of-c');
3004 });
3005 await serverAct(async () => {});
3006 expect(content).toBe(null);
3007
3008 // Param b is never accessed, so the tail of the list stays unsettled.
3009 // It doesn't keep the response open: once the normal promise resolves,
3010 // the response completes.
3011 await serverAct(() => {
3012 resolveNormal('done');
3013 });
3014 await serverAct(async () => {});
3015 expect(content).not.toBe(null);
3016 await readPromise;
3017
3018 const result = await ReactServerDOMClient.createFromReadableStream(
3019 stream2,
3020 {
3021 serverConsumerManifest: {
3022 moduleMap: null,
3023 moduleLoading: null,
3024 },
3025 },
3026 );
3027 expect(result.page).toBe('Accessed: value-of-a');
3028
3029 // Wait until the full response has been processed.
3030 await serverAct(async () => {});
3031
3032 // Read the accessed params off the list, synchronously. A node that
3033 // was never settled by the server stays forever pending, without
3034 // erroring, which marks the end of the accessed params.
3035 function readNode(node) {
3036 // Attach a no-op listener to force Flight to synchronously unwrap a
3037 // node that was received but not yet initialized.
3038 node.then(() => {});
3039 if (node.status !== 'fulfilled') {
3040 return null;
3041 }
3042 return node.value;
3043 }
3044
3045 const accessed = [];
3046 let node = result.accessedParams;
3047 while (node !== null) {
3048 const entry = readNode(node);
3049 if (entry === null) {
3050 break;
3051 }
3052 accessed.push(entry.value);
3053 node = entry.next;
3054 }
3055 expect(accessed).toEqual(['a', 'c']);
3056 });
3057 });