@samitouri / QOS-React-2 / commits / bbc13fa17b

[Flight] Add Debug Channel option for stateful connection to the backend in DEV (#33627)

This adds plumbing for opening a stream from the Flight Client to the Flight Server so it can ask for more data on-demand. In this mode, the Flight Server keeps the connection open as long as the client is still alive and there's more objects to load. It retains any depth limited objects so that they can be asked for later. In this first PR it just releases the object when it's discovered on the server and doesn't actually lazy load it yet. That's coming in a follow up. This strategy is built on the model that each request has its own channel for this. Instead of some global registry. That ensures that referential identity is preserved within a Request and the Request can refer to previously written objects by reference. The fixture implements a WebSocket per request but it doesn't have to be done that way. It can be multiplexed through an existing WebSocket for example. The current protocol is just a Readable(Stream) on the server and WritableStream on the client. It could even be sent through a HTTP request body if browsers implemented full duplex (which they don't). This PR only implements the direction of messages from Client to Server. However, I also plan on adding Debug Channel in the other direction to allow debug info (optionally) be sent from Server to Client through this channel instead of through the main RSC request. So the `debugChannel` option will be able to take writable or readable or both. --------- Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>

Sebastian Markbåge committed Jun 24, 2025 at 11:16 UTC bbc13fa17be8eebef3e6ee47f48c76c0c44e2f36
25 files changed +1385 -68
fixtures/flight/server/global.js
+3
@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
104 if (req.headers['cache-control']) {
105 proxiedHeaders['Cache-Control'] = req.get('cache-control');
106 }
107 + if (req.get('rsc-request-id')) {
108 + proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
109 + }
110
111 const requestsPrerender = req.path === '/prerender';
112
fixtures/flight/server/region.js
+53 -7
@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
50
51 const React = require('react');
52
53 -async function renderApp(res, returnValue, formState, noCache) {
53 +const activeDebugChannels =
54 + process.env.NODE_ENV === 'development' ? new Map() : null;
55 +
56 +function getDebugChannel(req) {
57 + if (process.env.NODE_ENV !== 'development') {
58 + return undefined;
59 + }
60 + const requestId = req.get('rsc-request-id');
61 + if (!requestId) {
62 + return undefined;
63 + }
64 + return activeDebugChannels.get(requestId);
65 +}
66 +
67 +async function renderApp(
68 + res,
69 + returnValue,
70 + formState,
71 + noCache,
72 + promiseForDebugChannel
73 +) {
74 const {renderToPipeableStream} = await import(
75 'react-server-dom-webpack/server'
76 );
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
121 );
122 // For client-invoked server actions we refresh the tree and return a return value.
123 const payload = {root, returnValue, formState};
104 - const {pipe} = renderToPipeableStream(payload, moduleMap);
124 + const {pipe} = renderToPipeableStream(payload, moduleMap, {
125 + debugChannel: await promiseForDebugChannel,
126 + });
127 pipe(res);
128 }
129
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
188 if ('prerender' in req.query) {
189 await prerenderApp(res, null, null, noCache);
190 } else {
169 - await renderApp(res, null, null, noCache);
191 + await renderApp(res, null, null, noCache, getDebugChannel(req));
192 }
193 });
194
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
226 // We handle the error on the client
227 }
228 // Refresh the client and return the value
207 - renderApp(res, result, null, noCache);
229 + renderApp(res, result, null, noCache, getDebugChannel(req));
230 } else {
231 // This is the progressive enhancement case
232 const UndiciRequest = require('undici').Request;
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
242 // Wait for any mutations
243 const result = await action();
244 const formState = decodeFormState(result, formData);
223 - renderApp(res, null, formState, noCache);
245 + renderApp(res, null, formState, noCache, undefined);
246 } catch (x) {
247 const {setServerState} = await import('../src/ServerState.js');
248 setServerState('Error: ' + x.message);
227 - renderApp(res, null, null, noCache);
249 + renderApp(res, null, null, noCache, undefined);
250 }
251 }
252 });
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
346 });
347 }
348
327 -app.listen(3001, () => {
349 +const httpServer = app.listen(3001, () => {
350 console.log('Regional Flight Server listening on port 3001...');
351 });
352
@@ -346,3 +368,27 @@ app.on('error', function (error) {
368 throw error;
369 }
370 });
371 +
372 +if (process.env.NODE_ENV === 'development') {
373 + // Open a websocket server for Debug information
374 + const WebSocket = require('ws');
375 + const webSocketServer = new WebSocket.Server({noServer: true});
376 +
377 + httpServer.on('upgrade', (request, socket, head) => {
378 + const DEBUG_CHANNEL_PATH = '/debug-channel?';
379 + if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
380 + const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
381 + const promiseForWs = new Promise(resolve => {
382 + webSocketServer.handleUpgrade(request, socket, head, ws => {
383 + ws.on('close', () => {
384 + activeDebugChannels.delete(requestId);
385 + });
386 + resolve(ws);
387 + });
388 + });
389 + activeDebugChannels.set(requestId, promiseForWs);
390 + } else {
391 + socket.destroy();
392 + }
393 + });
394 +}
fixtures/flight/src/App.js
-1
@@ -123,7 +123,6 @@ async function ServerComponent({noCache}) {
123 export default async function App({prerender, noCache}) {
124 const res = await fetch('http://localhost:3001/todos');
125 const todos = await res.json();
126 - console.log(res);
126
127 const dedupedChild = <ServerComponent noCache={noCache} />;
128 const message = getServerState();
fixtures/flight/src/index.js
+37 -11
@@ -42,17 +42,43 @@ function Shell({data}) {
42 }
43
44 async function hydrateApp() {
45 - const {root, returnValue, formState} = await createFromFetch(
46 - fetch('/', {
47 - headers: {
48 - Accept: 'text/x-component',
49 - },
50 - }),
51 - {
52 - callServer,
53 - findSourceMapURL,
54 - }
55 - );
45 + let response;
46 + if (
47 + process.env.NODE_ENV === 'development' &&
48 + typeof WebSocketStream === 'function'
49 + ) {
50 + const requestId = crypto.randomUUID();
51 + const wss = new WebSocketStream(
52 + 'ws://localhost:3001/debug-channel?' + requestId
53 + );
54 + const debugChannel = await wss.opened;
55 + response = createFromFetch(
56 + fetch('/', {
57 + headers: {
58 + Accept: 'text/x-component',
59 + 'rsc-request-id': requestId,
60 + },
61 + }),
62 + {
63 + callServer,
64 + debugChannel,
65 + findSourceMapURL,
66 + }
67 + );
68 + } else {
69 + response = createFromFetch(
70 + fetch('/', {
71 + headers: {
72 + Accept: 'text/x-component',
73 + },
74 + }),
75 + {
76 + callServer,
77 + findSourceMapURL,
78 + }
79 + );
80 + }
81 + const {root, returnValue, formState} = await response;
82
83 ReactDOM.hydrateRoot(
84 document,
packages/react-client/src/ReactFlightClient.js
+30 -6
@@ -328,6 +328,8 @@ export type FindSourceMapURLCallback = (
328 environmentName: string,
329 ) => null | string;
330
331 +export type DebugChannelCallback = (message: string) => void;
332 +
333 export type Response = {
334 _bundlerConfig: ServerConsumerModuleMap,
335 _serverReferenceConfig: null | ServerManifest,
@@ -351,6 +353,7 @@ export type Response = {
353 _debugRootStack?: null | Error, // DEV-only
354 _debugRootTask?: null | ConsoleTask, // DEV-only
355 _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
356 + _debugChannel?: void | DebugChannelCallback, // DEV-only
357 _replayConsole: boolean, // DEV-only
358 _rootEnvironmentName: string, // DEV-only, the requested environment name.
359 };
@@ -687,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
690 triggerErrorOnChunk(chunk, error);
691 }
692 });
693 + if (__DEV__) {
694 + const debugChannel = response._debugChannel;
695 + if (debugChannel !== undefined) {
696 + // If we don't have any more ways of reading data, we don't have to send any
697 + // more neither. So we close the writable side.
698 + debugChannel('');
699 + response._debugChannel = undefined;
700 + }
701 + }
702 if (enableProfilerTimer && enableComponentPerformanceTrack) {
703 markAllTracksInOrder();
704 flushComponentPerformance(
@@ -1667,6 +1679,14 @@ function parseModelString(
1679 }
1680 case 'Y': {
1681 if (__DEV__) {
1682 + if (value.length > 2) {
1683 + const debugChannel = response._debugChannel;
1684 + if (debugChannel) {
1685 + const ref = value.slice(2);
1686 + debugChannel('R:' + ref); // Release this reference immediately
1687 + }
1688 + }
1689 +
1690 // In DEV mode we encode omitted objects in logs as a getter that throws
1691 // so that when you try to access it on the client, you know why that
1692 // happened.
@@ -1730,9 +1750,10 @@ function ResponseInstance(
1750 encodeFormAction: void | EncodeFormActionCallback,
1751 nonce: void | string,
1752 temporaryReferences: void | TemporaryReferenceSet,
1733 - findSourceMapURL: void | FindSourceMapURLCallback,
1734 - replayConsole: boolean,
1735 - environmentName: void | string,
1753 + findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
1754 + replayConsole: boolean, // DEV-only
1755 + environmentName: void | string, // DEV-only
1756 + debugChannel: void | DebugChannelCallback, // DEV-only
1757 ) {
1758 const chunks: Map<number, SomeChunk<any>> = new Map();
1759 this._bundlerConfig = bundlerConfig;
@@ -1787,6 +1808,7 @@ function ResponseInstance(
1808 );
1809 }
1810 this._debugFindSourceMapURL = findSourceMapURL;
1811 + this._debugChannel = debugChannel;
1812 this._replayConsole = replayConsole;
1813 this._rootEnvironmentName = rootEnv;
1814 }
@@ -1802,9 +1824,10 @@ export function createResponse(
1824 encodeFormAction: void | EncodeFormActionCallback,
1825 nonce: void | string,
1826 temporaryReferences: void | TemporaryReferenceSet,
1805 - findSourceMapURL: void | FindSourceMapURLCallback,
1806 - replayConsole: boolean,
1807 - environmentName: void | string,
1827 + findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
1828 + replayConsole: boolean, // DEV-only
1829 + environmentName: void | string, // DEV-only
1830 + debugChannel: void | DebugChannelCallback, // DEV-only
1831 ): Response {
1832 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
1833 return new ResponseInstance(
@@ -1818,6 +1841,7 @@ export function createResponse(
1841 findSourceMapURL,
1842 replayConsole,
1843 environmentName,
1844 + debugChannel,
1845 );
1846 }
1847
packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js new
+139
@@ -0,0 +1,139 @@
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 +if (typeof Blob === 'undefined') {
14 + global.Blob = require('buffer').Blob;
15 +}
16 +if (typeof File === 'undefined' || typeof FormData === 'undefined') {
17 + global.File = require('undici').File;
18 + global.FormData = require('undici').FormData;
19 +}
20 +
21 +function formatV8Stack(stack) {
22 + let v8StyleStack = '';
23 + if (stack) {
24 + for (let i = 0; i < stack.length; i++) {
25 + const [name] = stack[i];
26 + if (v8StyleStack !== '') {
27 + v8StyleStack += '\n';
28 + }
29 + v8StyleStack += ' in ' + name + ' (at **)';
30 + }
31 + }
32 + return v8StyleStack;
33 +}
34 +
35 +function normalizeComponentInfo(debugInfo) {
36 + if (Array.isArray(debugInfo.stack)) {
37 + const {debugTask, debugStack, ...copy} = debugInfo;
38 + copy.stack = formatV8Stack(debugInfo.stack);
39 + if (debugInfo.owner) {
40 + copy.owner = normalizeComponentInfo(debugInfo.owner);
41 + }
42 + return copy;
43 + } else {
44 + return debugInfo;
45 + }
46 +}
47 +
48 +function getDebugInfo(obj) {
49 + const debugInfo = obj._debugInfo;
50 + if (debugInfo) {
51 + const copy = [];
52 + for (let i = 0; i < debugInfo.length; i++) {
53 + copy.push(normalizeComponentInfo(debugInfo[i]));
54 + }
55 + return copy;
56 + }
57 + return debugInfo;
58 +}
59 +
60 +let act;
61 +let React;
62 +let ReactNoop;
63 +let ReactNoopFlightServer;
64 +let ReactNoopFlightClient;
65 +
66 +describe('ReactFlight', () => {
67 + beforeEach(() => {
68 + // Mock performance.now for timing tests
69 + let time = 10;
70 + const now = jest.fn().mockImplementation(() => {
71 + return time++;
72 + });
73 + Object.defineProperty(performance, 'timeOrigin', {
74 + value: time,
75 + configurable: true,
76 + });
77 + Object.defineProperty(performance, 'now', {
78 + value: now,
79 + configurable: true,
80 + });
81 +
82 + jest.resetModules();
83 + jest.mock('react', () => require('react/react.react-server'));
84 + ReactNoopFlightServer = require('react-noop-renderer/flight-server');
85 + // This stores the state so we need to preserve it
86 + const flightModules = require('react-noop-renderer/flight-modules');
87 + jest.resetModules();
88 + __unmockReact();
89 + jest.mock('react-noop-renderer/flight-modules', () => flightModules);
90 + React = require('react');
91 + ReactNoop = require('react-noop-renderer');
92 + ReactNoopFlightClient = require('react-noop-renderer/flight-client');
93 + act = require('internal-test-utils').act;
94 + });
95 +
96 + afterEach(() => {
97 + jest.restoreAllMocks();
98 + });
99 +
100 + // @gate __DEV__ && enableComponentPerformanceTrack
101 + it('can render deep but cut off JSX in debug info', async () => {
102 + function createDeepJSX(n) {
103 + if (n <= 0) {
104 + return null;
105 + }
106 + return <div>{createDeepJSX(n - 1)}</div>;
107 + }
108 +
109 + function ServerComponent(props) {
110 + return <div>not using props</div>;
111 + }
112 +
113 + const debugChannel = {onMessage(message) {}};
114 +
115 + const transport = ReactNoopFlightServer.render(
116 + {
117 + root: (
118 + <ServerComponent>
119 + {createDeepJSX(100) /* deper than objectLimit */}
120 + </ServerComponent>
121 + ),
122 + },
123 + {debugChannel},
124 + );
125 +
126 + await act(async () => {
127 + const rootModel = await ReactNoopFlightClient.read(transport, {
128 + debugChannel,
129 + });
130 + const root = rootModel.root;
131 + const children = getDebugInfo(root)[1].props.children;
132 + expect(children.type).toBe('div');
133 + expect(children.props.children.type).toBe('div');
134 + ReactNoop.render(root);
135 + });
136 +
137 + expect(ReactNoop).toMatchRenderedOutput(<div>not using props</div>);
138 + });
139 +});
packages/react-markup/src/ReactMarkupServer.js
+1
@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
171 undefined,
172 'Markup',
173 undefined,
174 + false,
175 );
176 const flightResponse = createFlightResponse(
177 null,
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+4
@@ -56,6 +56,7 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
56
57 type ReadOptions = {|
58 findSourceMapURL?: FindSourceMapURLCallback,
59 + debugChannel?: {onMessage: (message: string) => void},
60 close?: boolean,
61 |};
62
@@ -71,6 +72,9 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
72 options !== undefined ? options.findSourceMapURL : undefined,
73 true,
74 undefined,
75 + __DEV__ && options !== undefined && options.debugChannel !== undefined
76 + ? options.debugChannel.onMessage
77 + : undefined,
78 );
79 for (let i = 0; i < source.length; i++) {
80 processBinaryChunk(response, source[i], 0);
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+7
@@ -71,6 +71,7 @@ type Options = {
71 filterStackFrame?: (url: string, functionName: string) => boolean,
72 identifierPrefix?: string,
73 signal?: AbortSignal,
74 + debugChannel?: {onMessage?: (message: string) => void},
75 onError?: (error: mixed) => void,
76 onPostpone?: (reason: string) => void,
77 };
@@ -87,6 +88,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
88 undefined,
89 __DEV__ && options ? options.environmentName : undefined,
90 __DEV__ && options ? options.filterStackFrame : undefined,
91 + __DEV__ && options && options.debugChannel !== undefined,
92 );
93 const signal = options ? options.signal : undefined;
94 if (signal) {
@@ -100,6 +102,11 @@ function render(model: ReactClientValue, options?: Options): Destination {
102 signal.addEventListener('abort', listener);
103 }
104 }
105 + if (__DEV__ && options && options.debugChannel !== undefined) {
106 + options.debugChannel.onMessage = message => {
107 + ReactNoopFlightServer.resolveDebugMessage(request, message);
108 + };
109 + }
110 ReactNoopFlightServer.startWork(request);
111 ReactNoopFlightServer.startFlowing(request, destination);
112 return destination;
packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
+26
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
12 import type {
13 Response as FlightResponse,
14 FindSourceMapURLCallback,
15 + DebugChannelCallback,
16 } from 'react-client/src/ReactFlightClient';
17
18 import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -43,12 +44,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
44 export type Options = {
45 moduleBaseURL?: string,
46 callServer?: CallServerCallback,
47 + debugChannel?: {writable?: WritableStream, ...},
48 temporaryReferences?: TemporaryReferenceSet,
49 findSourceMapURL?: FindSourceMapURLCallback,
50 replayConsoleLogs?: boolean,
51 environmentName?: string,
52 };
53
54 +function createDebugCallbackFromWritableStream(
55 + debugWritable: WritableStream,
56 +): DebugChannelCallback {
57 + const textEncoder = new TextEncoder();
58 + const writer = debugWritable.getWriter();
59 + return message => {
60 + if (message === '') {
61 + writer.close();
62 + } else {
63 + // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
64 + // Therefore, we can't report errors from this write back to the Response object.
65 + if (__DEV__) {
66 + writer.write(textEncoder.encode(message + '\n')).catch(console.error);
67 + }
68 + }
69 + };
70 +}
71 +
72 function createResponseFromOptions(options: void | Options) {
73 return createResponse(
74 options && options.moduleBaseURL ? options.moduleBaseURL : '',
@@ -67,6 +87,12 @@ function createResponseFromOptions(options: void | Options) {
87 __DEV__ && options && options.environmentName
88 ? options.environmentName
89 : undefined,
90 + __DEV__ &&
91 + options &&
92 + options.debugChannel !== undefined &&
93 + options.debugChannel.writable !== undefined
94 + ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
95 + : undefined,
96 );
97 }
98
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+81 -2
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
19 import type {Thenable} from 'shared/ReactTypes';
20
21 +import type {Duplex} from 'stream';
22 +
23 import {Readable} from 'stream';
24
25 import {
@@ -27,6 +29,8 @@ import {
29 startFlowing,
30 stopFlowing,
31 abort,
32 + resolveDebugMessage,
33 + closeDebugChannel,
34 } from 'react-server/src/ReactFlightServer';
35
36 import {
@@ -50,6 +54,12 @@ export {
54 registerClientReference,
55 } from '../ReactFlightESMReferences';
56
57 +import {
58 + createStringDecoder,
59 + readPartialStringChunk,
60 + readFinalStringChunk,
61 +} from 'react-client/src/ReactFlightClientStreamConfigNode';
62 +
63 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
64
65 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -67,7 +77,69 @@ function createCancelHandler(request: Request, reason: string) {
77 };
78 }
79
80 +function startReadingFromDebugChannelReadable(
81 + request: Request,
82 + stream: Readable | WebSocket,
83 +): void {
84 + const stringDecoder = createStringDecoder();
85 + let lastWasPartial = false;
86 + let stringBuffer = '';
87 + function onData(chunk: string | Uint8Array) {
88 + if (typeof chunk === 'string') {
89 + if (lastWasPartial) {
90 + stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
91 + lastWasPartial = false;
92 + }
93 + stringBuffer += chunk;
94 + } else {
95 + const buffer: Uint8Array = (chunk: any);
96 + stringBuffer += readPartialStringChunk(stringDecoder, buffer);
97 + lastWasPartial = true;
98 + }
99 + const messages = stringBuffer.split('\n');
100 + for (let i = 0; i < messages.length - 1; i++) {
101 + resolveDebugMessage(request, messages[i]);
102 + }
103 + stringBuffer = messages[messages.length - 1];
104 + }
105 + function onError(error: mixed) {
106 + abort(
107 + request,
108 + new Error('Lost connection to the Debug Channel.', {
109 + cause: error,
110 + }),
111 + );
112 + }
113 + function onClose() {
114 + closeDebugChannel(request);
115 + }
116 + if (
117 + // $FlowFixMe[method-unbinding]
118 + typeof stream.addEventListener === 'function' &&
119 + // $FlowFixMe[method-unbinding]
120 + typeof stream.binaryType === 'string'
121 + ) {
122 + const ws: WebSocket = (stream: any);
123 + ws.binaryType = 'arraybuffer';
124 + ws.addEventListener('message', event => {
125 + // $FlowFixMe
126 + onData(event.data);
127 + });
128 + ws.addEventListener('error', event => {
129 + // $FlowFixMe
130 + onError(event.error);
131 + });
132 + ws.addEventListener('close', onClose);
133 + } else {
134 + const readable: Readable = (stream: any);
135 + readable.on('data', onData);
136 + readable.on('error', onError);
137 + readable.on('end', onClose);
138 + }
139 +}
140 +
141 type Options = {
142 + debugChannel?: Readable | Duplex | WebSocket,
143 environmentName?: string | (() => string),
144 filterStackFrame?: (url: string, functionName: string) => boolean,
145 onError?: (error: mixed) => void,
@@ -86,6 +158,7 @@ function renderToPipeableStream(
158 moduleBasePath: ClientManifest,
159 options?: Options,
160 ): PipeableStream {
161 + const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
162 const request = createRequest(
163 model,
164 moduleBasePath,
@@ -95,9 +168,13 @@ function renderToPipeableStream(
168 options ? options.temporaryReferences : undefined,
169 __DEV__ && options ? options.environmentName : undefined,
170 __DEV__ && options ? options.filterStackFrame : undefined,
171 + debugChannel !== undefined,
172 );
173 let hasStartedFlowing = false;
174 startWork(request);
175 + if (debugChannel !== undefined) {
176 + startReadingFromDebugChannelReadable(request, debugChannel);
177 + }
178 return {
179 pipe<T: Writable>(destination: T): T {
180 if (hasStartedFlowing) {
@@ -126,11 +203,12 @@ function renderToPipeableStream(
203 },
204 };
205 }
206 +
207 function createFakeWritable(readable: any): Writable {
208 // The current host config expects a Writable so we create
209 // a fake writable for now to push into the Readable.
210 return ({
133 - write(chunk) {
211 + write(chunk: string | Uint8Array) {
212 return readable.push(chunk);
213 },
214 end() {
@@ -184,6 +262,7 @@ function prerenderToNodeStream(
262 options ? options.temporaryReferences : undefined,
263 __DEV__ && options ? options.environmentName : undefined,
264 __DEV__ && options ? options.filterStackFrame : undefined,
265 + false,
266 );
267 if (options && options.signal) {
268 const signal = options.signal;
@@ -287,8 +366,8 @@ function decodeReply<T>(
366 export {
367 renderToPipeableStream,
368 prerenderToNodeStream,
290 - decodeReplyFromBusboy,
369 decodeReply,
370 + decodeReplyFromBusboy,
371 decodeAction,
372 decodeFormState,
373 };
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
+35 -1
@@ -8,7 +8,10 @@
8 */
9
10 import type {Thenable} from 'shared/ReactTypes.js';
11 -import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
11 +import type {
12 + Response as FlightResponse,
13 + DebugChannelCallback,
14 +} from 'react-client/src/ReactFlightClient';
15 import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
16 import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
17
@@ -76,6 +79,24 @@ export function createServerReference<A: Iterable<any>, T>(
79 );
80 }
81
82 +function createDebugCallbackFromWritableStream(
83 + debugWritable: WritableStream,
84 +): DebugChannelCallback {
85 + const textEncoder = new TextEncoder();
86 + const writer = debugWritable.getWriter();
87 + return message => {
88 + if (message === '') {
89 + writer.close();
90 + } else {
91 + // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
92 + // Therefore, we can't report errors from this write back to the Response object.
93 + if (__DEV__) {
94 + writer.write(textEncoder.encode(message + '\n')).catch(console.error);
95 + }
96 + }
97 + };
98 +}
99 +
100 function startReadingFromStream(
101 response: FlightResponse,
102 stream: ReadableStream,
@@ -104,6 +125,7 @@ function startReadingFromStream(
125 }
126
127 export type Options = {
128 + debugChannel?: {writable?: WritableStream, ...},
129 temporaryReferences?: TemporaryReferenceSet,
130 replayConsoleLogs?: boolean,
131 environmentName?: string,
@@ -128,6 +150,12 @@ export function createFromReadableStream<T>(
150 __DEV__ && options && options.environmentName
151 ? options.environmentName
152 : undefined,
153 + __DEV__ &&
154 + options &&
155 + options.debugChannel !== undefined &&
156 + options.debugChannel.writable !== undefined
157 + ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
158 + : undefined,
159 );
160 startReadingFromStream(response, stream);
161 return getRoot(response);
@@ -152,6 +180,12 @@ export function createFromFetch<T>(
180 __DEV__ && options && options.environmentName
181 ? options.environmentName
182 : undefined,
183 + __DEV__ &&
184 + options &&
185 + options.debugChannel !== undefined &&
186 + options.debugChannel.writable !== undefined
187 + ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
188 + : undefined,
189 );
190 promiseForResponse.then(
191 function (r) {
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
+63 -4
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
10 +import type {
11 + Request,
12 + ReactClientValue,
13 +} from 'react-server/src/ReactFlightServer';
14 import type {ReactFormState, Thenable} from 'shared/ReactTypes';
15 import {
16 preloadModule,
@@ -24,6 +27,8 @@ import {
27 startFlowing,
28 stopFlowing,
29 abort,
30 + resolveDebugMessage,
31 + closeDebugChannel,
32 } from 'react-server/src/ReactFlightServer';
33
34 import {
@@ -42,12 +47,19 @@ export {
47 registerServerReference,
48 } from '../ReactFlightParcelReferences';
49
50 +import {
51 + createStringDecoder,
52 + readPartialStringChunk,
53 + readFinalStringChunk,
54 +} from 'react-client/src/ReactFlightClientStreamConfigWeb';
55 +
56 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
57
58 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
59 export type {TemporaryReferenceSet};
60
61 type Options = {
62 + debugChannel?: {readable?: ReadableStream, ...},
63 environmentName?: string | (() => string),
64 filterStackFrame?: (url: string, functionName: string) => boolean,
65 identifierPrefix?: string,
@@ -57,10 +69,55 @@ type Options = {
69 onPostpone?: (reason: string) => void,
70 };
71
72 +function startReadingFromDebugChannelReadableStream(
73 + request: Request,
74 + stream: ReadableStream,
75 +): void {
76 + const reader = stream.getReader();
77 + const stringDecoder = createStringDecoder();
78 + let stringBuffer = '';
79 + function progress({
80 + done,
81 + value,
82 + }: {
83 + done: boolean,
84 + value: ?any,
85 + ...
86 + }): void | Promise<void> {
87 + const buffer: Uint8Array = (value: any);
88 + stringBuffer += done
89 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
90 + : readPartialStringChunk(stringDecoder, buffer);
91 + const messages = stringBuffer.split('\n');
92 + for (let i = 0; i < messages.length - 1; i++) {
93 + resolveDebugMessage(request, messages[i]);
94 + }
95 + stringBuffer = messages[messages.length - 1];
96 + if (done) {
97 + closeDebugChannel(request);
98 + return;
99 + }
100 + return reader.read().then(progress).catch(error);
101 + }
102 + function error(e: any) {
103 + abort(
104 + request,
105 + new Error('Lost connection to the Debug Channel.', {
106 + cause: e,
107 + }),
108 + );
109 + }
110 + reader.read().then(progress).catch(error);
111 +}
112 +
113 export function renderToReadableStream(
114 model: ReactClientValue,
115 options?: Options,
116 ): ReadableStream {
117 + const debugChannelReadable =
118 + __DEV__ && options && options.debugChannel
119 + ? options.debugChannel.readable
120 + : undefined;
121 const request = createRequest(
122 model,
123 null,
@@ -70,6 +127,7 @@ export function renderToReadableStream(
127 options ? options.temporaryReferences : undefined,
128 __DEV__ && options ? options.environmentName : undefined,
129 __DEV__ && options ? options.filterStackFrame : undefined,
130 + debugChannelReadable !== undefined,
131 );
132 if (options && options.signal) {
133 const signal = options.signal;
@@ -83,6 +141,9 @@ export function renderToReadableStream(
141 signal.addEventListener('abort', listener);
142 }
143 }
144 + if (debugChannelReadable !== undefined) {
145 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
146 + }
147 const stream = new ReadableStream(
148 {
149 type: 'bytes',
@@ -117,9 +178,6 @@ export function prerender(
178 const stream = new ReadableStream(
179 {
180 type: 'bytes',
120 - start: (controller): ?Promise<void> => {
121 - startWork(request);
122 - },
181 pull: (controller): ?Promise<void> => {
182 startFlowing(request, controller);
183 },
@@ -144,6 +202,7 @@ export function prerender(
202 options ? options.temporaryReferences : undefined,
203 __DEV__ && options ? options.environmentName : undefined,
204 __DEV__ && options ? options.filterStackFrame : undefined,
205 + false,
206 );
207 if (options && options.signal) {
208 const signal = options.signal;
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
+63 -4
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
10 +import type {
11 + Request,
12 + ReactClientValue,
13 +} from 'react-server/src/ReactFlightServer';
14 import type {ReactFormState, Thenable} from 'shared/ReactTypes';
15 import {
16 preloadModule,
@@ -26,6 +29,8 @@ import {
29 startFlowing,
30 stopFlowing,
31 abort,
32 + resolveDebugMessage,
33 + closeDebugChannel,
34 } from 'react-server/src/ReactFlightServer';
35
36 import {
@@ -47,12 +52,19 @@ export {
52 registerServerReference,
53 } from '../ReactFlightParcelReferences';
54
55 +import {
56 + createStringDecoder,
57 + readPartialStringChunk,
58 + readFinalStringChunk,
59 +} from 'react-client/src/ReactFlightClientStreamConfigWeb';
60 +
61 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
62
63 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
64 export type {TemporaryReferenceSet};
65
66 type Options = {
67 + debugChannel?: {readable?: ReadableStream, ...},
68 environmentName?: string | (() => string),
69 filterStackFrame?: (url: string, functionName: string) => boolean,
70 identifierPrefix?: string,
@@ -62,10 +74,55 @@ type Options = {
74 onPostpone?: (reason: string) => void,
75 };
76
77 +function startReadingFromDebugChannelReadableStream(
78 + request: Request,
79 + stream: ReadableStream,
80 +): void {
81 + const reader = stream.getReader();
82 + const stringDecoder = createStringDecoder();
83 + let stringBuffer = '';
84 + function progress({
85 + done,
86 + value,
87 + }: {
88 + done: boolean,
89 + value: ?any,
90 + ...
91 + }): void | Promise<void> {
92 + const buffer: Uint8Array = (value: any);
93 + stringBuffer += done
94 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
95 + : readPartialStringChunk(stringDecoder, buffer);
96 + const messages = stringBuffer.split('\n');
97 + for (let i = 0; i < messages.length - 1; i++) {
98 + resolveDebugMessage(request, messages[i]);
99 + }
100 + stringBuffer = messages[messages.length - 1];
101 + if (done) {
102 + closeDebugChannel(request);
103 + return;
104 + }
105 + return reader.read().then(progress).catch(error);
106 + }
107 + function error(e: any) {
108 + abort(
109 + request,
110 + new Error('Lost connection to the Debug Channel.', {
111 + cause: e,
112 + }),
113 + );
114 + }
115 + reader.read().then(progress).catch(error);
116 +}
117 +
118 export function renderToReadableStream(
119 model: ReactClientValue,
120 options?: Options,
121 ): ReadableStream {
122 + const debugChannelReadable =
123 + __DEV__ && options && options.debugChannel
124 + ? options.debugChannel.readable
125 + : undefined;
126 const request = createRequest(
127 model,
128 null,
@@ -75,6 +132,7 @@ export function renderToReadableStream(
132 options ? options.temporaryReferences : undefined,
133 __DEV__ && options ? options.environmentName : undefined,
134 __DEV__ && options ? options.filterStackFrame : undefined,
135 + debugChannelReadable !== undefined,
136 );
137 if (options && options.signal) {
138 const signal = options.signal;
@@ -88,6 +146,9 @@ export function renderToReadableStream(
146 signal.addEventListener('abort', listener);
147 }
148 }
149 + if (debugChannelReadable !== undefined) {
150 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
151 + }
152 const stream = new ReadableStream(
153 {
154 type: 'bytes',
@@ -122,9 +183,6 @@ export function prerender(
183 const stream = new ReadableStream(
184 {
185 type: 'bytes',
125 - start: (controller): ?Promise<void> => {
126 - startWork(request);
127 - },
186 pull: (controller): ?Promise<void> => {
187 startFlowing(request, controller);
188 },
@@ -149,6 +207,7 @@ export function prerender(
207 options ? options.temporaryReferences : undefined,
208 __DEV__ && options ? options.environmentName : undefined,
209 __DEV__ && options ? options.filterStackFrame : undefined,
210 + false,
211 );
212 if (options && options.signal) {
213 const signal = options.signal;
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
+132 -4
@@ -20,6 +20,8 @@ import type {
20 ServerReferenceId,
21 } from '../client/ReactFlightClientConfigBundlerParcel';
22
23 +import type {Duplex} from 'stream';
24 +
25 import {Readable} from 'stream';
26
27 import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -31,6 +33,8 @@ import {
33 startFlowing,
34 stopFlowing,
35 abort,
36 + resolveDebugMessage,
37 + closeDebugChannel,
38 } from 'react-server/src/ReactFlightServer';
39
40 import {
@@ -49,6 +53,7 @@ import {
53 decodeAction as decodeActionImpl,
54 decodeFormState as decodeFormStateImpl,
55 } from 'react-server/src/ReactFlightActionServer';
56 +
57 import {
58 preloadModule,
59 requireModule,
@@ -60,6 +65,12 @@ export {
65 registerServerReference,
66 } from '../ReactFlightParcelReferences';
67
68 +import {
69 + createStringDecoder,
70 + readPartialStringChunk,
71 + readFinalStringChunk,
72 +} from 'react-client/src/ReactFlightClientStreamConfigNode';
73 +
74 import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
75
76 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -79,7 +90,69 @@ function createCancelHandler(request: Request, reason: string) {
90 };
91 }
92
93 +function startReadingFromDebugChannelReadable(
94 + request: Request,
95 + stream: Readable | WebSocket,
96 +): void {
97 + const stringDecoder = createStringDecoder();
98 + let lastWasPartial = false;
99 + let stringBuffer = '';
100 + function onData(chunk: string | Uint8Array) {
101 + if (typeof chunk === 'string') {
102 + if (lastWasPartial) {
103 + stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
104 + lastWasPartial = false;
105 + }
106 + stringBuffer += chunk;
107 + } else {
108 + const buffer: Uint8Array = (chunk: any);
109 + stringBuffer += readPartialStringChunk(stringDecoder, buffer);
110 + lastWasPartial = true;
111 + }
112 + const messages = stringBuffer.split('\n');
113 + for (let i = 0; i < messages.length - 1; i++) {
114 + resolveDebugMessage(request, messages[i]);
115 + }
116 + stringBuffer = messages[messages.length - 1];
117 + }
118 + function onError(error: mixed) {
119 + abort(
120 + request,
121 + new Error('Lost connection to the Debug Channel.', {
122 + cause: error,
123 + }),
124 + );
125 + }
126 + function onClose() {
127 + closeDebugChannel(request);
128 + }
129 + if (
130 + // $FlowFixMe[method-unbinding]
131 + typeof stream.addEventListener === 'function' &&
132 + // $FlowFixMe[method-unbinding]
133 + typeof stream.binaryType === 'string'
134 + ) {
135 + const ws: WebSocket = (stream: any);
136 + ws.binaryType = 'arraybuffer';
137 + ws.addEventListener('message', event => {
138 + // $FlowFixMe
139 + onData(event.data);
140 + });
141 + ws.addEventListener('error', event => {
142 + // $FlowFixMe
143 + onError(event.error);
144 + });
145 + ws.addEventListener('close', onClose);
146 + } else {
147 + const readable: Readable = (stream: any);
148 + readable.on('data', onData);
149 + readable.on('error', onError);
150 + readable.on('end', onClose);
151 + }
152 +}
153 +
154 type Options = {
155 + debugChannel?: Readable | Duplex | WebSocket,
156 environmentName?: string | (() => string),
157 filterStackFrame?: (url: string, functionName: string) => boolean,
158 onError?: (error: mixed) => void,
@@ -97,6 +170,7 @@ export function renderToPipeableStream(
170 model: ReactClientValue,
171 options?: Options,
172 ): PipeableStream {
173 + const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
174 const request = createRequest(
175 model,
176 null,
@@ -106,9 +180,13 @@ export function renderToPipeableStream(
180 options ? options.temporaryReferences : undefined,
181 __DEV__ && options ? options.environmentName : undefined,
182 __DEV__ && options ? options.filterStackFrame : undefined,
183 + debugChannel !== undefined,
184 );
185 let hasStartedFlowing = false;
186 startWork(request);
187 + if (debugChannel !== undefined) {
188 + startReadingFromDebugChannelReadable(request, debugChannel);
189 + }
190 return {
191 pipe<T: Writable>(destination: T): T {
192 if (hasStartedFlowing) {
@@ -149,7 +227,7 @@ function createFakeWritableFromReadableStreamController(
227 chunk = textEncoder.encode(chunk);
228 }
229 controller.enqueue(chunk);
152 - // in web streams there is no backpressure so we can alwas write more
230 + // in web streams there is no backpressure so we can always write more
231 return true;
232 },
233 end() {
@@ -167,13 +245,58 @@ function createFakeWritableFromReadableStreamController(
245 }: any);
246 }
247
248 +function startReadingFromDebugChannelReadableStream(
249 + request: Request,
250 + stream: ReadableStream,
251 +): void {
252 + const reader = stream.getReader();
253 + const stringDecoder = createStringDecoder();
254 + let stringBuffer = '';
255 + function progress({
256 + done,
257 + value,
258 + }: {
259 + done: boolean,
260 + value: ?any,
261 + ...
262 + }): void | Promise<void> {
263 + const buffer: Uint8Array = (value: any);
264 + stringBuffer += done
265 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
266 + : readPartialStringChunk(stringDecoder, buffer);
267 + const messages = stringBuffer.split('\n');
268 + for (let i = 0; i < messages.length - 1; i++) {
269 + resolveDebugMessage(request, messages[i]);
270 + }
271 + stringBuffer = messages[messages.length - 1];
272 + if (done) {
273 + closeDebugChannel(request);
274 + return;
275 + }
276 + return reader.read().then(progress).catch(error);
277 + }
278 + function error(e: any) {
279 + abort(
280 + request,
281 + new Error('Lost connection to the Debug Channel.', {
282 + cause: e,
283 + }),
284 + );
285 + }
286 + reader.read().then(progress).catch(error);
287 +}
288 +
289 export function renderToReadableStream(
290 model: ReactClientValue,
172 -
173 - options?: Options & {
291 + options?: Omit<Options, 'debugChannel'> & {
292 + debugChannel?: {readable?: ReadableStream, ...},
293 signal?: AbortSignal,
294 },
295 ): ReadableStream {
296 + const debugChannelReadable =
297 + __DEV__ && options && options.debugChannel
298 + ? options.debugChannel.readable
299 + : undefined;
300 const request = createRequest(
301 model,
302 null,
@@ -183,6 +306,7 @@ export function renderToReadableStream(
306 options ? options.temporaryReferences : undefined,
307 __DEV__ && options ? options.environmentName : undefined,
308 __DEV__ && options ? options.filterStackFrame : undefined,
309 + debugChannelReadable !== undefined,
310 );
311 if (options && options.signal) {
312 const signal = options.signal;
@@ -196,6 +320,9 @@ export function renderToReadableStream(
320 signal.addEventListener('abort', listener);
321 }
322 }
323 + if (debugChannelReadable !== undefined) {
324 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
325 + }
326 let writable: Writable;
327 const stream = new ReadableStream(
328 {
@@ -275,6 +402,7 @@ export function prerenderToNodeStream(
402 options ? options.temporaryReferences : undefined,
403 __DEV__ && options ? options.environmentName : undefined,
404 __DEV__ && options ? options.filterStackFrame : undefined,
405 + false,
406 );
407 if (options && options.signal) {
408 const signal = options.signal;
@@ -296,7 +424,6 @@ export function prerenderToNodeStream(
424
425 export function prerender(
426 model: ReactClientValue,
299 -
427 options?: Options & {
428 signal?: AbortSignal,
429 },
@@ -338,6 +465,7 @@ export function prerender(
465 options ? options.temporaryReferences : undefined,
466 __DEV__ && options ? options.environmentName : undefined,
467 __DEV__ && options ? options.filterStackFrame : undefined,
468 + false,
469 );
470 if (options && options.signal) {
471 const signal = options.signal;
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
+26
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
12 import type {
13 Response as FlightResponse,
14 FindSourceMapURLCallback,
15 + DebugChannelCallback,
16 } from 'react-client/src/ReactFlightClient';
17
18 import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
43
44 export type Options = {
45 callServer?: CallServerCallback,
46 + debugChannel?: {writable?: WritableStream, ...},
47 temporaryReferences?: TemporaryReferenceSet,
48 findSourceMapURL?: FindSourceMapURLCallback,
49 replayConsoleLogs?: boolean,
50 environmentName?: string,
51 };
52
53 +function createDebugCallbackFromWritableStream(
54 + debugWritable: WritableStream,
55 +): DebugChannelCallback {
56 + const textEncoder = new TextEncoder();
57 + const writer = debugWritable.getWriter();
58 + return message => {
59 + if (message === '') {
60 + writer.close();
61 + } else {
62 + // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
63 + // Therefore, we can't report errors from this write back to the Response object.
64 + if (__DEV__) {
65 + writer.write(textEncoder.encode(message + '\n')).catch(console.error);
66 + }
67 + }
68 + };
69 +}
70 +
71 function createResponseFromOptions(options: void | Options) {
72 return createResponse(
73 null,
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
86 __DEV__ && options && options.environmentName
87 ? options.environmentName
88 : undefined,
89 + __DEV__ &&
90 + options &&
91 + options.debugChannel !== undefined &&
92 + options.debugChannel.writable !== undefined
93 + ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
94 + : undefined,
95 );
96 }
97
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+63 -4
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
10 +import type {
11 + Request,
12 + ReactClientValue,
13 +} from 'react-server/src/ReactFlightServer';
14 import type {Thenable} from 'shared/ReactTypes';
15 import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
16 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -19,6 +22,8 @@ import {
22 startFlowing,
23 stopFlowing,
24 abort,
25 + resolveDebugMessage,
26 + closeDebugChannel,
27 } from 'react-server/src/ReactFlightServer';
28
29 import {
@@ -38,6 +43,12 @@ export {
43 createClientModuleProxy,
44 } from '../ReactFlightTurbopackReferences';
45
46 +import {
47 + createStringDecoder,
48 + readPartialStringChunk,
49 + readFinalStringChunk,
50 +} from 'react-client/src/ReactFlightClientStreamConfigWeb';
51 +
52 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
53
54 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
56 export type {TemporaryReferenceSet};
57
58 type Options = {
59 + debugChannel?: {readable?: ReadableStream, ...},
60 environmentName?: string | (() => string),
61 filterStackFrame?: (url: string, functionName: string) => boolean,
62 identifierPrefix?: string,
@@ -54,11 +66,56 @@ type Options = {
66 onPostpone?: (reason: string) => void,
67 };
68
69 +function startReadingFromDebugChannelReadableStream(
70 + request: Request,
71 + stream: ReadableStream,
72 +): void {
73 + const reader = stream.getReader();
74 + const stringDecoder = createStringDecoder();
75 + let stringBuffer = '';
76 + function progress({
77 + done,
78 + value,
79 + }: {
80 + done: boolean,
81 + value: ?any,
82 + ...
83 + }): void | Promise<void> {
84 + const buffer: Uint8Array = (value: any);
85 + stringBuffer += done
86 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
87 + : readPartialStringChunk(stringDecoder, buffer);
88 + const messages = stringBuffer.split('\n');
89 + for (let i = 0; i < messages.length - 1; i++) {
90 + resolveDebugMessage(request, messages[i]);
91 + }
92 + stringBuffer = messages[messages.length - 1];
93 + if (done) {
94 + closeDebugChannel(request);
95 + return;
96 + }
97 + return reader.read().then(progress).catch(error);
98 + }
99 + function error(e: any) {
100 + abort(
101 + request,
102 + new Error('Lost connection to the Debug Channel.', {
103 + cause: e,
104 + }),
105 + );
106 + }
107 + reader.read().then(progress).catch(error);
108 +}
109 +
110 function renderToReadableStream(
111 model: ReactClientValue,
112 turbopackMap: ClientManifest,
113 options?: Options,
114 ): ReadableStream {
115 + const debugChannelReadable =
116 + __DEV__ && options && options.debugChannel
117 + ? options.debugChannel.readable
118 + : undefined;
119 const request = createRequest(
120 model,
121 turbopackMap,
@@ -68,6 +125,7 @@ function renderToReadableStream(
125 options ? options.temporaryReferences : undefined,
126 __DEV__ && options ? options.environmentName : undefined,
127 __DEV__ && options ? options.filterStackFrame : undefined,
128 + debugChannelReadable !== undefined,
129 );
130 if (options && options.signal) {
131 const signal = options.signal;
@@ -81,6 +139,9 @@ function renderToReadableStream(
139 signal.addEventListener('abort', listener);
140 }
141 }
142 + if (debugChannelReadable !== undefined) {
143 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
144 + }
145 const stream = new ReadableStream(
146 {
147 type: 'bytes',
@@ -116,9 +177,6 @@ function prerender(
177 const stream = new ReadableStream(
178 {
179 type: 'bytes',
119 - start: (controller): ?Promise<void> => {
120 - startWork(request);
121 - },
180 pull: (controller): ?Promise<void> => {
181 startFlowing(request, controller);
182 },
@@ -143,6 +201,7 @@ function prerender(
201 options ? options.temporaryReferences : undefined,
202 __DEV__ && options ? options.environmentName : undefined,
203 __DEV__ && options ? options.filterStackFrame : undefined,
204 + false,
205 );
206 if (options && options.signal) {
207 const signal = options.signal;
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+63 -4
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
10 +import type {
11 + Request,
12 + ReactClientValue,
13 +} from 'react-server/src/ReactFlightServer';
14 import type {Thenable} from 'shared/ReactTypes';
15 import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
16 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -21,6 +24,8 @@ import {
24 startFlowing,
25 stopFlowing,
26 abort,
27 + resolveDebugMessage,
28 + closeDebugChannel,
29 } from 'react-server/src/ReactFlightServer';
30
31 import {
@@ -43,6 +48,12 @@ export {
48 createClientModuleProxy,
49 } from '../ReactFlightTurbopackReferences';
50
51 +import {
52 + createStringDecoder,
53 + readPartialStringChunk,
54 + readFinalStringChunk,
55 +} from 'react-client/src/ReactFlightClientStreamConfigWeb';
56 +
57 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
58
59 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
61 export type {TemporaryReferenceSet};
62
63 type Options = {
64 + debugChannel?: {readable?: ReadableStream, ...},
65 environmentName?: string | (() => string),
66 filterStackFrame?: (url: string, functionName: string) => boolean,
67 identifierPrefix?: string,
@@ -59,11 +71,56 @@ type Options = {
71 onPostpone?: (reason: string) => void,
72 };
73
74 +function startReadingFromDebugChannelReadableStream(
75 + request: Request,
76 + stream: ReadableStream,
77 +): void {
78 + const reader = stream.getReader();
79 + const stringDecoder = createStringDecoder();
80 + let stringBuffer = '';
81 + function progress({
82 + done,
83 + value,
84 + }: {
85 + done: boolean,
86 + value: ?any,
87 + ...
88 + }): void | Promise<void> {
89 + const buffer: Uint8Array = (value: any);
90 + stringBuffer += done
91 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
92 + : readPartialStringChunk(stringDecoder, buffer);
93 + const messages = stringBuffer.split('\n');
94 + for (let i = 0; i < messages.length - 1; i++) {
95 + resolveDebugMessage(request, messages[i]);
96 + }
97 + stringBuffer = messages[messages.length - 1];
98 + if (done) {
99 + closeDebugChannel(request);
100 + return;
101 + }
102 + return reader.read().then(progress).catch(error);
103 + }
104 + function error(e: any) {
105 + abort(
106 + request,
107 + new Error('Lost connection to the Debug Channel.', {
108 + cause: e,
109 + }),
110 + );
111 + }
112 + reader.read().then(progress).catch(error);
113 +}
114 +
115 function renderToReadableStream(
116 model: ReactClientValue,
117 turbopackMap: ClientManifest,
118 options?: Options,
119 ): ReadableStream {
120 + const debugChannelReadable =
121 + __DEV__ && options && options.debugChannel
122 + ? options.debugChannel.readable
123 + : undefined;
124 const request = createRequest(
125 model,
126 turbopackMap,
@@ -73,6 +130,7 @@ function renderToReadableStream(
130 options ? options.temporaryReferences : undefined,
131 __DEV__ && options ? options.environmentName : undefined,
132 __DEV__ && options ? options.filterStackFrame : undefined,
133 + debugChannelReadable !== undefined,
134 );
135 if (options && options.signal) {
136 const signal = options.signal;
@@ -86,6 +144,9 @@ function renderToReadableStream(
144 signal.addEventListener('abort', listener);
145 }
146 }
147 + if (debugChannelReadable !== undefined) {
148 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
149 + }
150 const stream = new ReadableStream(
151 {
152 type: 'bytes',
@@ -121,9 +182,6 @@ function prerender(
182 const stream = new ReadableStream(
183 {
184 type: 'bytes',
124 - start: (controller): ?Promise<void> => {
125 - startWork(request);
126 - },
185 pull: (controller): ?Promise<void> => {
186 startFlowing(request, controller);
187 },
@@ -148,6 +206,7 @@ function prerender(
206 options ? options.temporaryReferences : undefined,
207 __DEV__ && options ? options.environmentName : undefined,
208 __DEV__ && options ? options.filterStackFrame : undefined,
209 + false,
210 );
211 if (options && options.signal) {
212 const signal = options.signal;
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+130 -1
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
19 import type {Thenable} from 'shared/ReactTypes';
20
21 +import type {Duplex} from 'stream';
22 +
23 import {Readable} from 'stream';
24
25 import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -29,6 +31,8 @@ import {
31 startFlowing,
32 stopFlowing,
33 abort,
34 + resolveDebugMessage,
35 + closeDebugChannel,
36 } from 'react-server/src/ReactFlightServer';
37
38 import {
@@ -54,6 +58,12 @@ export {
58 createClientModuleProxy,
59 } from '../ReactFlightTurbopackReferences';
60
61 +import {
62 + createStringDecoder,
63 + readPartialStringChunk,
64 + readFinalStringChunk,
65 +} from 'react-client/src/ReactFlightClientStreamConfigNode';
66 +
67 import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
68
69 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
83 };
84 }
85
86 +function startReadingFromDebugChannelReadable(
87 + request: Request,
88 + stream: Readable | WebSocket,
89 +): void {
90 + const stringDecoder = createStringDecoder();
91 + let lastWasPartial = false;
92 + let stringBuffer = '';
93 + function onData(chunk: string | Uint8Array) {
94 + if (typeof chunk === 'string') {
95 + if (lastWasPartial) {
96 + stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
97 + lastWasPartial = false;
98 + }
99 + stringBuffer += chunk;
100 + } else {
101 + const buffer: Uint8Array = (chunk: any);
102 + stringBuffer += readPartialStringChunk(stringDecoder, buffer);
103 + lastWasPartial = true;
104 + }
105 + const messages = stringBuffer.split('\n');
106 + for (let i = 0; i < messages.length - 1; i++) {
107 + resolveDebugMessage(request, messages[i]);
108 + }
109 + stringBuffer = messages[messages.length - 1];
110 + }
111 + function onError(error: mixed) {
112 + abort(
113 + request,
114 + new Error('Lost connection to the Debug Channel.', {
115 + cause: error,
116 + }),
117 + );
118 + }
119 + function onClose() {
120 + closeDebugChannel(request);
121 + }
122 + if (
123 + // $FlowFixMe[method-unbinding]
124 + typeof stream.addEventListener === 'function' &&
125 + // $FlowFixMe[method-unbinding]
126 + typeof stream.binaryType === 'string'
127 + ) {
128 + const ws: WebSocket = (stream: any);
129 + ws.binaryType = 'arraybuffer';
130 + ws.addEventListener('message', event => {
131 + // $FlowFixMe
132 + onData(event.data);
133 + });
134 + ws.addEventListener('error', event => {
135 + // $FlowFixMe
136 + onError(event.error);
137 + });
138 + ws.addEventListener('close', onClose);
139 + } else {
140 + const readable: Readable = (stream: any);
141 + readable.on('data', onData);
142 + readable.on('error', onError);
143 + readable.on('end', onClose);
144 + }
145 +}
146 +
147 type Options = {
148 + debugChannel?: Readable | Duplex | WebSocket,
149 environmentName?: string | (() => string),
150 filterStackFrame?: (url: string, functionName: string) => boolean,
151 onError?: (error: mixed) => void,
@@ -92,6 +164,7 @@ function renderToPipeableStream(
164 turbopackMap: ClientManifest,
165 options?: Options,
166 ): PipeableStream {
167 + const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
168 const request = createRequest(
169 model,
170 turbopackMap,
@@ -101,9 +174,13 @@ function renderToPipeableStream(
174 options ? options.temporaryReferences : undefined,
175 __DEV__ && options ? options.environmentName : undefined,
176 __DEV__ && options ? options.filterStackFrame : undefined,
177 + debugChannel !== undefined,
178 );
179 let hasStartedFlowing = false;
180 startWork(request);
181 + if (debugChannel !== undefined) {
182 + startReadingFromDebugChannelReadable(request, debugChannel);
183 + }
184 return {
185 pipe<T: Writable>(destination: T): T {
186 if (hasStartedFlowing) {
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
239 }: any);
240 }
241
242 +function startReadingFromDebugChannelReadableStream(
243 + request: Request,
244 + stream: ReadableStream,
245 +): void {
246 + const reader = stream.getReader();
247 + const stringDecoder = createStringDecoder();
248 + let stringBuffer = '';
249 + function progress({
250 + done,
251 + value,
252 + }: {
253 + done: boolean,
254 + value: ?any,
255 + ...
256 + }): void | Promise<void> {
257 + const buffer: Uint8Array = (value: any);
258 + stringBuffer += done
259 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
260 + : readPartialStringChunk(stringDecoder, buffer);
261 + const messages = stringBuffer.split('\n');
262 + for (let i = 0; i < messages.length - 1; i++) {
263 + resolveDebugMessage(request, messages[i]);
264 + }
265 + stringBuffer = messages[messages.length - 1];
266 + if (done) {
267 + closeDebugChannel(request);
268 + return;
269 + }
270 + return reader.read().then(progress).catch(error);
271 + }
272 + function error(e: any) {
273 + abort(
274 + request,
275 + new Error('Lost connection to the Debug Channel.', {
276 + cause: e,
277 + }),
278 + );
279 + }
280 + reader.read().then(progress).catch(error);
281 +}
282 +
283 function renderToReadableStream(
284 model: ReactClientValue,
285 turbopackMap: ClientManifest,
168 - options?: Options & {
286 + options?: Omit<Options, 'debugChannel'> & {
287 + debugChannel?: {readable?: ReadableStream, ...},
288 signal?: AbortSignal,
289 },
290 ): ReadableStream {
291 + const debugChannelReadable =
292 + __DEV__ && options && options.debugChannel
293 + ? options.debugChannel.readable
294 + : undefined;
295 const request = createRequest(
296 model,
297 turbopackMap,
@@ -178,6 +301,7 @@ function renderToReadableStream(
301 options ? options.temporaryReferences : undefined,
302 __DEV__ && options ? options.environmentName : undefined,
303 __DEV__ && options ? options.filterStackFrame : undefined,
304 + debugChannelReadable !== undefined,
305 );
306 if (options && options.signal) {
307 const signal = options.signal;
@@ -191,6 +315,9 @@ function renderToReadableStream(
315 signal.addEventListener('abort', listener);
316 }
317 }
318 + if (debugChannelReadable !== undefined) {
319 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
320 + }
321 let writable: Writable;
322 const stream = new ReadableStream(
323 {
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
398 options ? options.temporaryReferences : undefined,
399 __DEV__ && options ? options.environmentName : undefined,
400 __DEV__ && options ? options.filterStackFrame : undefined,
401 + false,
402 );
403 if (options && options.signal) {
404 const signal = options.signal;
@@ -334,6 +462,7 @@ function prerender(
462 options ? options.temporaryReferences : undefined,
463 __DEV__ && options ? options.environmentName : undefined,
464 __DEV__ && options ? options.filterStackFrame : undefined,
465 + false,
466 );
467 if (options && options.signal) {
468 const signal = options.signal;
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
+26
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
12 import type {
13 Response as FlightResponse,
14 FindSourceMapURLCallback,
15 + DebugChannelCallback,
16 } from 'react-client/src/ReactFlightClient';
17
18 import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -42,12 +43,31 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
43
44 export type Options = {
45 callServer?: CallServerCallback,
46 + debugChannel?: {writable?: WritableStream, ...},
47 temporaryReferences?: TemporaryReferenceSet,
48 findSourceMapURL?: FindSourceMapURLCallback,
49 replayConsoleLogs?: boolean,
50 environmentName?: string,
51 };
52
53 +function createDebugCallbackFromWritableStream(
54 + debugWritable: WritableStream,
55 +): DebugChannelCallback {
56 + const textEncoder = new TextEncoder();
57 + const writer = debugWritable.getWriter();
58 + return message => {
59 + if (message === '') {
60 + writer.close();
61 + } else {
62 + // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
63 + // Therefore, we can't report errors from this write back to the Response object.
64 + if (__DEV__) {
65 + writer.write(textEncoder.encode(message + '\n')).catch(console.error);
66 + }
67 + }
68 + };
69 +}
70 +
71 function createResponseFromOptions(options: void | Options) {
72 return createResponse(
73 null,
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
86 __DEV__ && options && options.environmentName
87 ? options.environmentName
88 : undefined,
89 + __DEV__ &&
90 + options &&
91 + options.debugChannel !== undefined &&
92 + options.debugChannel.writable !== undefined
93 + ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
94 + : undefined,
95 );
96 }
97
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+63 -4
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
10 +import type {
11 + Request,
12 + ReactClientValue,
13 +} from 'react-server/src/ReactFlightServer';
14 import type {Thenable} from 'shared/ReactTypes';
15 import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
16 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -19,6 +22,8 @@ import {
22 startFlowing,
23 stopFlowing,
24 abort,
25 + resolveDebugMessage,
26 + closeDebugChannel,
27 } from 'react-server/src/ReactFlightServer';
28
29 import {
@@ -38,6 +43,12 @@ export {
43 createClientModuleProxy,
44 } from '../ReactFlightWebpackReferences';
45
46 +import {
47 + createStringDecoder,
48 + readPartialStringChunk,
49 + readFinalStringChunk,
50 +} from 'react-client/src/ReactFlightClientStreamConfigWeb';
51 +
52 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
53
54 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
56 export type {TemporaryReferenceSet};
57
58 type Options = {
59 + debugChannel?: {readable?: ReadableStream, ...},
60 environmentName?: string | (() => string),
61 filterStackFrame?: (url: string, functionName: string) => boolean,
62 identifierPrefix?: string,
@@ -54,11 +66,56 @@ type Options = {
66 onPostpone?: (reason: string) => void,
67 };
68
69 +function startReadingFromDebugChannelReadableStream(
70 + request: Request,
71 + stream: ReadableStream,
72 +): void {
73 + const reader = stream.getReader();
74 + const stringDecoder = createStringDecoder();
75 + let stringBuffer = '';
76 + function progress({
77 + done,
78 + value,
79 + }: {
80 + done: boolean,
81 + value: ?any,
82 + ...
83 + }): void | Promise<void> {
84 + const buffer: Uint8Array = (value: any);
85 + stringBuffer += done
86 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
87 + : readPartialStringChunk(stringDecoder, buffer);
88 + const messages = stringBuffer.split('\n');
89 + for (let i = 0; i < messages.length - 1; i++) {
90 + resolveDebugMessage(request, messages[i]);
91 + }
92 + stringBuffer = messages[messages.length - 1];
93 + if (done) {
94 + closeDebugChannel(request);
95 + return;
96 + }
97 + return reader.read().then(progress).catch(error);
98 + }
99 + function error(e: any) {
100 + abort(
101 + request,
102 + new Error('Lost connection to the Debug Channel.', {
103 + cause: e,
104 + }),
105 + );
106 + }
107 + reader.read().then(progress).catch(error);
108 +}
109 +
110 function renderToReadableStream(
111 model: ReactClientValue,
112 webpackMap: ClientManifest,
113 options?: Options,
114 ): ReadableStream {
115 + const debugChannelReadable =
116 + __DEV__ && options && options.debugChannel
117 + ? options.debugChannel.readable
118 + : undefined;
119 const request = createRequest(
120 model,
121 webpackMap,
@@ -68,6 +125,7 @@ function renderToReadableStream(
125 options ? options.temporaryReferences : undefined,
126 __DEV__ && options ? options.environmentName : undefined,
127 __DEV__ && options ? options.filterStackFrame : undefined,
128 + debugChannelReadable !== undefined,
129 );
130 if (options && options.signal) {
131 const signal = options.signal;
@@ -81,6 +139,9 @@ function renderToReadableStream(
139 signal.addEventListener('abort', listener);
140 }
141 }
142 + if (debugChannelReadable !== undefined) {
143 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
144 + }
145 const stream = new ReadableStream(
146 {
147 type: 'bytes',
@@ -116,9 +177,6 @@ function prerender(
177 const stream = new ReadableStream(
178 {
179 type: 'bytes',
119 - start: (controller): ?Promise<void> => {
120 - startWork(request);
121 - },
180 pull: (controller): ?Promise<void> => {
181 startFlowing(request, controller);
182 },
@@ -143,6 +201,7 @@ function prerender(
201 options ? options.temporaryReferences : undefined,
202 __DEV__ && options ? options.environmentName : undefined,
203 __DEV__ && options ? options.filterStackFrame : undefined,
204 + false,
205 );
206 if (options && options.signal) {
207 const signal = options.signal;
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+63 -1
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
10 +import type {
11 + Request,
12 + ReactClientValue,
13 +} from 'react-server/src/ReactFlightServer';
14 import type {Thenable} from 'shared/ReactTypes';
15 import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
16 import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -21,6 +24,8 @@ import {
24 startFlowing,
25 stopFlowing,
26 abort,
27 + resolveDebugMessage,
28 + closeDebugChannel,
29 } from 'react-server/src/ReactFlightServer';
30
31 import {
@@ -43,6 +48,12 @@ export {
48 createClientModuleProxy,
49 } from '../ReactFlightWebpackReferences';
50
51 +import {
52 + createStringDecoder,
53 + readPartialStringChunk,
54 + readFinalStringChunk,
55 +} from 'react-client/src/ReactFlightClientStreamConfigWeb';
56 +
57 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
58
59 export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
61 export type {TemporaryReferenceSet};
62
63 type Options = {
64 + debugChannel?: {readable?: ReadableStream, ...},
65 environmentName?: string | (() => string),
66 filterStackFrame?: (url: string, functionName: string) => boolean,
67 identifierPrefix?: string,
@@ -59,11 +71,56 @@ type Options = {
71 onPostpone?: (reason: string) => void,
72 };
73
74 +function startReadingFromDebugChannelReadableStream(
75 + request: Request,
76 + stream: ReadableStream,
77 +): void {
78 + const reader = stream.getReader();
79 + const stringDecoder = createStringDecoder();
80 + let stringBuffer = '';
81 + function progress({
82 + done,
83 + value,
84 + }: {
85 + done: boolean,
86 + value: ?any,
87 + ...
88 + }): void | Promise<void> {
89 + const buffer: Uint8Array = (value: any);
90 + stringBuffer += done
91 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
92 + : readPartialStringChunk(stringDecoder, buffer);
93 + const messages = stringBuffer.split('\n');
94 + for (let i = 0; i < messages.length - 1; i++) {
95 + resolveDebugMessage(request, messages[i]);
96 + }
97 + stringBuffer = messages[messages.length - 1];
98 + if (done) {
99 + closeDebugChannel(request);
100 + return;
101 + }
102 + return reader.read().then(progress).catch(error);
103 + }
104 + function error(e: any) {
105 + abort(
106 + request,
107 + new Error('Lost connection to the Debug Channel.', {
108 + cause: e,
109 + }),
110 + );
111 + }
112 + reader.read().then(progress).catch(error);
113 +}
114 +
115 function renderToReadableStream(
116 model: ReactClientValue,
117 webpackMap: ClientManifest,
118 options?: Options,
119 ): ReadableStream {
120 + const debugChannelReadable =
121 + __DEV__ && options && options.debugChannel
122 + ? options.debugChannel.readable
123 + : undefined;
124 const request = createRequest(
125 model,
126 webpackMap,
@@ -73,6 +130,7 @@ function renderToReadableStream(
130 options ? options.temporaryReferences : undefined,
131 __DEV__ && options ? options.environmentName : undefined,
132 __DEV__ && options ? options.filterStackFrame : undefined,
133 + debugChannelReadable !== undefined,
134 );
135 if (options && options.signal) {
136 const signal = options.signal;
@@ -86,6 +144,9 @@ function renderToReadableStream(
144 signal.addEventListener('abort', listener);
145 }
146 }
147 + if (debugChannelReadable !== undefined) {
148 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
149 + }
150 const stream = new ReadableStream(
151 {
152 type: 'bytes',
@@ -145,6 +206,7 @@ function prerender(
206 options ? options.temporaryReferences : undefined,
207 __DEV__ && options ? options.environmentName : undefined,
208 __DEV__ && options ? options.filterStackFrame : undefined,
209 + false,
210 );
211 if (options && options.signal) {
212 const signal = options.signal;
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+130 -1
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
18 import type {Writable} from 'stream';
19 import type {Thenable} from 'shared/ReactTypes';
20
21 +import type {Duplex} from 'stream';
22 +
23 import {Readable} from 'stream';
24
25 import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -29,6 +31,8 @@ import {
31 startFlowing,
32 stopFlowing,
33 abort,
34 + resolveDebugMessage,
35 + closeDebugChannel,
36 } from 'react-server/src/ReactFlightServer';
37
38 import {
@@ -54,6 +58,12 @@ export {
58 createClientModuleProxy,
59 } from '../ReactFlightWebpackReferences';
60
61 +import {
62 + createStringDecoder,
63 + readPartialStringChunk,
64 + readFinalStringChunk,
65 +} from 'react-client/src/ReactFlightClientStreamConfigNode';
66 +
67 import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
68
69 import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
83 };
84 }
85
86 +function startReadingFromDebugChannelReadable(
87 + request: Request,
88 + stream: Readable | WebSocket,
89 +): void {
90 + const stringDecoder = createStringDecoder();
91 + let lastWasPartial = false;
92 + let stringBuffer = '';
93 + function onData(chunk: string | Uint8Array) {
94 + if (typeof chunk === 'string') {
95 + if (lastWasPartial) {
96 + stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
97 + lastWasPartial = false;
98 + }
99 + stringBuffer += chunk;
100 + } else {
101 + const buffer: Uint8Array = (chunk: any);
102 + stringBuffer += readPartialStringChunk(stringDecoder, buffer);
103 + lastWasPartial = true;
104 + }
105 + const messages = stringBuffer.split('\n');
106 + for (let i = 0; i < messages.length - 1; i++) {
107 + resolveDebugMessage(request, messages[i]);
108 + }
109 + stringBuffer = messages[messages.length - 1];
110 + }
111 + function onError(error: mixed) {
112 + abort(
113 + request,
114 + new Error('Lost connection to the Debug Channel.', {
115 + cause: error,
116 + }),
117 + );
118 + }
119 + function onClose() {
120 + closeDebugChannel(request);
121 + }
122 + if (
123 + // $FlowFixMe[method-unbinding]
124 + typeof stream.addEventListener === 'function' &&
125 + // $FlowFixMe[method-unbinding]
126 + typeof stream.binaryType === 'string'
127 + ) {
128 + const ws: WebSocket = (stream: any);
129 + ws.binaryType = 'arraybuffer';
130 + ws.addEventListener('message', event => {
131 + // $FlowFixMe
132 + onData(event.data);
133 + });
134 + ws.addEventListener('error', event => {
135 + // $FlowFixMe
136 + onError(event.error);
137 + });
138 + ws.addEventListener('close', onClose);
139 + } else {
140 + const readable: Readable = (stream: any);
141 + readable.on('data', onData);
142 + readable.on('error', onError);
143 + readable.on('end', onClose);
144 + }
145 +}
146 +
147 type Options = {
148 + debugChannel?: Readable | Duplex | WebSocket,
149 environmentName?: string | (() => string),
150 filterStackFrame?: (url: string, functionName: string) => boolean,
151 onError?: (error: mixed) => void,
@@ -92,6 +164,7 @@ function renderToPipeableStream(
164 webpackMap: ClientManifest,
165 options?: Options,
166 ): PipeableStream {
167 + const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
168 const request = createRequest(
169 model,
170 webpackMap,
@@ -101,9 +174,13 @@ function renderToPipeableStream(
174 options ? options.temporaryReferences : undefined,
175 __DEV__ && options ? options.environmentName : undefined,
176 __DEV__ && options ? options.filterStackFrame : undefined,
177 + debugChannel !== undefined,
178 );
179 let hasStartedFlowing = false;
180 startWork(request);
181 + if (debugChannel !== undefined) {
182 + startReadingFromDebugChannelReadable(request, debugChannel);
183 + }
184 return {
185 pipe<T: Writable>(destination: T): T {
186 if (hasStartedFlowing) {
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
239 }: any);
240 }
241
242 +function startReadingFromDebugChannelReadableStream(
243 + request: Request,
244 + stream: ReadableStream,
245 +): void {
246 + const reader = stream.getReader();
247 + const stringDecoder = createStringDecoder();
248 + let stringBuffer = '';
249 + function progress({
250 + done,
251 + value,
252 + }: {
253 + done: boolean,
254 + value: ?any,
255 + ...
256 + }): void | Promise<void> {
257 + const buffer: Uint8Array = (value: any);
258 + stringBuffer += done
259 + ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
260 + : readPartialStringChunk(stringDecoder, buffer);
261 + const messages = stringBuffer.split('\n');
262 + for (let i = 0; i < messages.length - 1; i++) {
263 + resolveDebugMessage(request, messages[i]);
264 + }
265 + stringBuffer = messages[messages.length - 1];
266 + if (done) {
267 + closeDebugChannel(request);
268 + return;
269 + }
270 + return reader.read().then(progress).catch(error);
271 + }
272 + function error(e: any) {
273 + abort(
274 + request,
275 + new Error('Lost connection to the Debug Channel.', {
276 + cause: e,
277 + }),
278 + );
279 + }
280 + reader.read().then(progress).catch(error);
281 +}
282 +
283 function renderToReadableStream(
284 model: ReactClientValue,
285 webpackMap: ClientManifest,
168 - options?: Options & {
286 + options?: Omit<Options, 'debugChannel'> & {
287 + debugChannel?: {readable?: ReadableStream, ...},
288 signal?: AbortSignal,
289 },
290 ): ReadableStream {
291 + const debugChannelReadable =
292 + __DEV__ && options && options.debugChannel
293 + ? options.debugChannel.readable
294 + : undefined;
295 const request = createRequest(
296 model,
297 webpackMap,
@@ -178,6 +301,7 @@ function renderToReadableStream(
301 options ? options.temporaryReferences : undefined,
302 __DEV__ && options ? options.environmentName : undefined,
303 __DEV__ && options ? options.filterStackFrame : undefined,
304 + debugChannelReadable !== undefined,
305 );
306 if (options && options.signal) {
307 const signal = options.signal;
@@ -191,6 +315,9 @@ function renderToReadableStream(
315 signal.addEventListener('abort', listener);
316 }
317 }
318 + if (debugChannelReadable !== undefined) {
319 + startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
320 + }
321 let writable: Writable;
322 const stream = new ReadableStream(
323 {
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
398 options ? options.temporaryReferences : undefined,
399 __DEV__ && options ? options.environmentName : undefined,
400 __DEV__ && options ? options.filterStackFrame : undefined,
401 + false,
402 );
403 if (options && options.signal) {
404 const signal = options.signal;
@@ -334,6 +462,7 @@ function prerender(
462 options ? options.temporaryReferences : undefined,
463 __DEV__ && options ? options.environmentName : undefined,
464 __DEV__ && options ? options.filterStackFrame : undefined,
465 + false,
466 );
467 if (options && options.signal) {
468 const signal = options.signal;
packages/react-server/src/ReactFlightServer.js
+144 -12
@@ -404,6 +404,13 @@ type Task = {
404
405 interface Reference {}
406
407 +type ReactClientReference = Reference & ReactClientValue;
408 +
409 +type DeferredDebugStore = {
410 + retained: Map<number, ReactClientReference | string>,
411 + existing: Map<ReactClientReference | string, number>,
412 +};
413 +
414 const OPENING = 10;
415 const OPEN = 11;
416 const ABORTING = 12;
@@ -451,6 +458,7 @@ export type Request = {
458 filterStackFrame: (url: string, functionName: string) => boolean,
459 didWarnForKey: null | WeakSet<ReactComponentInfo>,
460 writtenDebugObjects: WeakMap<Reference, string>,
461 + deferredDebugObjects: null | DeferredDebugStore,
462 };
463
464 const {
@@ -495,13 +503,14 @@ function RequestInstance(
503 model: ReactClientValue,
504 bundlerConfig: ClientManifest,
505 onError: void | ((error: mixed) => ?string),
498 - identifierPrefix?: string,
506 onPostpone: void | ((reason: string) => void),
507 + onAllReady: () => void,
508 + onFatalError: (error: mixed) => void,
509 + identifierPrefix?: string,
510 temporaryReferences: void | TemporaryReferenceSet,
511 environmentName: void | string | (() => string), // DEV-only
512 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
503 - onAllReady: () => void,
504 - onFatalError: (error: mixed) => void,
513 + keepDebugAlive: boolean, // DEV-only
514 ) {
515 if (
516 ReactSharedInternals.A !== null &&
@@ -571,6 +580,12 @@ function RequestInstance(
580 : filterStackFrame;
581 this.didWarnForKey = null;
582 this.writtenDebugObjects = new WeakMap();
583 + this.deferredDebugObjects = keepDebugAlive
584 + ? {
585 + retained: new Map(),
586 + existing: new Map(),
587 + }
588 + : null;
589 }
590
591 let timeOrigin: number;
@@ -615,6 +630,7 @@ export function createRequest(
630 temporaryReferences: void | TemporaryReferenceSet,
631 environmentName: void | string | (() => string), // DEV-only
632 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
633 + keepDebugAlive: boolean, // DEV-only
634 ): Request {
635 if (__DEV__) {
636 resetOwnerStackLimit();
@@ -626,13 +642,14 @@ export function createRequest(
642 model,
643 bundlerConfig,
644 onError,
629 - identifierPrefix,
645 onPostpone,
646 + noop,
647 + noop,
648 + identifierPrefix,
649 temporaryReferences,
650 environmentName,
651 filterStackFrame,
634 - noop,
635 - noop,
652 + keepDebugAlive,
653 );
654 }
655
@@ -647,6 +664,7 @@ export function createPrerenderRequest(
664 temporaryReferences: void | TemporaryReferenceSet,
665 environmentName: void | string | (() => string), // DEV-only
666 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
667 + keepDebugAlive: boolean, // DEV-only
668 ): Request {
669 if (__DEV__) {
670 resetOwnerStackLimit();
@@ -658,13 +676,14 @@ export function createPrerenderRequest(
676 model,
677 bundlerConfig,
678 onError,
661 - identifierPrefix,
679 onPostpone,
680 + onAllReady,
681 + onFatalError,
682 + identifierPrefix,
683 temporaryReferences,
684 environmentName,
685 filterStackFrame,
666 - onAllReady,
667 - onFatalError,
686 + keepDebugAlive,
687 );
688 }
689
@@ -2331,7 +2350,21 @@ function serializeSymbolReference(name: string): string {
2350 return '$S' + name;
2351 }
2352
2334 -function serializeLimitedObject(): string {
2353 +function serializeDeferredObject(
2354 + request: Request,
2355 + value: ReactClientReference | string,
2356 +): string {
2357 + const deferredDebugObjects = request.deferredDebugObjects;
2358 + if (deferredDebugObjects !== null) {
2359 + // This client supports a long lived connection. We can assign this object
2360 + // an ID to be lazy loaded later.
2361 + // This keeps the connection alive until we ask for it or release it.
2362 + request.pendingChunks++;
2363 + const id = request.nextChunkId++;
2364 + deferredDebugObjects.existing.set(value, id);
2365 + deferredDebugObjects.retained.set(id, value);
2366 + return '$Y' + id.toString(16);
2367 + }
2368 return '$Y';
2369 }
2370
@@ -4058,12 +4091,25 @@ function renderDebugModel(
4091
4092 if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
4093 // We've reached our max number of objects to serialize across the wire so we serialize this
4061 - // as a marker so that the client can error when this is accessed by the console.
4062 - return serializeLimitedObject();
4094 + // as a marker so that the client can error or lazy load this when accessed by the console.
4095 + return serializeDeferredObject(request, value);
4096 }
4097
4098 counter.objectLimit--;
4099
4100 + const deferredDebugObjects = request.deferredDebugObjects;
4101 + if (deferredDebugObjects !== null) {
4102 + const deferredId = deferredDebugObjects.existing.get(value);
4103 + // We earlier deferred this same object. We're now going to eagerly emit it so let's emit it
4104 + // at the same ID that we already used to refer to it.
4105 + if (deferredId !== undefined) {
4106 + deferredDebugObjects.existing.delete(value);
4107 + deferredDebugObjects.retained.delete(deferredId);
4108 + emitOutlinedDebugModelChunk(request, deferredId, counter, value);
4109 + return serializeByValueID(deferredId);
4110 + }
4111 + }
4112 +
4113 switch ((value: any).$$typeof) {
4114 case REACT_ELEMENT_TYPE: {
4115 const element: ReactElement = (value: any);
@@ -4235,6 +4281,13 @@ function renderDebugModel(
4281 }
4282 }
4283 if (value.length >= 1024) {
4284 + // Large strings are counted towards the object limit.
4285 + if (counter.objectLimit <= 0) {
4286 + // We've reached our max number of objects to serialize across the wire so we serialize this
4287 + // as a marker so that the client can error or lazy load this when accessed by the console.
4288 + return serializeDeferredObject(request, value);
4289 + }
4290 + counter.objectLimit--;
4291 // For large strings, we encode them outside the JSON payload so that we
4292 // don't have to double encode and double parse the strings. This can also
4293 // be more compact in case the string has a lot of escaped characters.
@@ -5254,3 +5307,82 @@ export function abort(request: Request, reason: mixed): void {
5307 fatalError(request, error);
5308 }
5309 }
5310 +
5311 +function fromHex(str: string): number {
5312 + return parseInt(str, 16);
5313 +}
5314 +
5315 +export function resolveDebugMessage(request: Request, message: string): void {
5316 + if (!__DEV__) {
5317 + // These errors should never make it into a build so we don't need to encode them in codes.json
5318 + // eslint-disable-next-line react-internal/prod-error-codes
5319 + throw new Error(
5320 + 'resolveDebugMessage should never be called in production mode. This is a bug in React.',
5321 + );
5322 + }
5323 + const deferredDebugObjects = request.deferredDebugObjects;
5324 + if (deferredDebugObjects === null) {
5325 + throw new Error(
5326 + "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
5327 + );
5328 + }
5329 + // This function lets the client ask for more data lazily through the debug channel.
5330 + const command = message.charCodeAt(0);
5331 + const ids = message.slice(2).split(',').map(fromHex);
5332 + switch (command) {
5333 + case 82 /* "R" */:
5334 + // Release IDs
5335 + for (let i = 0; i < ids.length; i++) {
5336 + const id = ids[i];
5337 + const retainedValue = deferredDebugObjects.retained.get(id);
5338 + if (retainedValue !== undefined) {
5339 + // We're no longer blocked on this. We won't emit it.
5340 + request.pendingChunks--;
5341 + deferredDebugObjects.retained.delete(id);
5342 + deferredDebugObjects.existing.delete(retainedValue);
5343 + enqueueFlush(request);
5344 + }
5345 + }
5346 + break;
5347 + case 81 /* "Q" */:
5348 + // Query IDs
5349 + for (let i = 0; i < ids.length; i++) {
5350 + const id = ids[i];
5351 + const retainedValue = deferredDebugObjects.retained.get(id);
5352 + if (retainedValue !== undefined) {
5353 + // If we still have this object, and haven't emitted it before, emit it on the stream.
5354 + const counter = {objectLimit: 10};
5355 + emitOutlinedDebugModelChunk(request, id, counter, retainedValue);
5356 + enqueueFlush(request);
5357 + }
5358 + }
5359 + break;
5360 + default:
5361 + throw new Error(
5362 + 'Unknown command. The debugChannel was not wired up properly.',
5363 + );
5364 + }
5365 +}
5366 +
5367 +export function closeDebugChannel(request: Request): void {
5368 + if (!__DEV__) {
5369 + // These errors should never make it into a build so we don't need to encode them in codes.json
5370 + // eslint-disable-next-line react-internal/prod-error-codes
5371 + throw new Error(
5372 + 'closeDebugChannel should never be called in production mode. This is a bug in React.',
5373 + );
5374 + }
5375 + // This clears all remaining deferred objects, potentially resulting in the completion of the Request.
5376 + const deferredDebugObjects = request.deferredDebugObjects;
5377 + if (deferredDebugObjects === null) {
5378 + throw new Error(
5379 + "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
5380 + );
5381 + }
5382 + deferredDebugObjects.retained.forEach((value, id) => {
5383 + request.pendingChunks--;
5384 + deferredDebugObjects.retained.delete(id);
5385 + deferredDebugObjects.existing.delete(value);
5386 + });
5387 + enqueueFlush(request);
5388 +}
scripts/error-codes/codes.json
+3 -1
@@ -548,5 +548,7 @@
548 "560": "Cannot use a startGestureTransition() with a comment node root.",
549 "561": "This rendered a large document (>%s kB) without any Suspense boundaries around most of it. That can delay initial paint longer than necessary. To improve load performance, add a <Suspense> or <SuspenseList> around the content you expect to be below the header or below the fold. In the meantime, the content will deopt to paint arbitrary incomplete pieces of HTML.",
550 "562": "The render was aborted due to a fatal error.",
551 - "563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources."
551 + "563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
552 + "564": "Unknown command. The debugChannel was not wired up properly.",
553 + "565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React."
554 }