diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js
index d81dc2c038c12..f097378056a46 100644
--- a/fixtures/flight/server/global.js
+++ b/fixtures/flight/server/global.js
@@ -104,6 +104,9 @@ async function renderApp(req, res, next) {
if (req.headers['cache-control']) {
proxiedHeaders['Cache-Control'] = req.get('cache-control');
}
+ if (req.get('rsc-request-id')) {
+ proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id');
+ }
const requestsPrerender = req.path === '/prerender';
diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js
index a352d34ee6b79..3cc15c01cabb5 100644
--- a/fixtures/flight/server/region.js
+++ b/fixtures/flight/server/region.js
@@ -50,7 +50,27 @@ const {readFile} = require('fs').promises;
const React = require('react');
-async function renderApp(res, returnValue, formState, noCache) {
+const activeDebugChannels =
+ process.env.NODE_ENV === 'development' ? new Map() : null;
+
+function getDebugChannel(req) {
+ if (process.env.NODE_ENV !== 'development') {
+ return undefined;
+ }
+ const requestId = req.get('rsc-request-id');
+ if (!requestId) {
+ return undefined;
+ }
+ return activeDebugChannels.get(requestId);
+}
+
+async function renderApp(
+ res,
+ returnValue,
+ formState,
+ noCache,
+ promiseForDebugChannel
+) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@@ -101,7 +121,9 @@ async function renderApp(res, returnValue, formState, noCache) {
);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
- const {pipe} = renderToPipeableStream(payload, moduleMap);
+ const {pipe} = renderToPipeableStream(payload, moduleMap, {
+ debugChannel: await promiseForDebugChannel,
+ });
pipe(res);
}
@@ -166,7 +188,7 @@ app.get('/', async function (req, res) {
if ('prerender' in req.query) {
await prerenderApp(res, null, null, noCache);
} else {
- await renderApp(res, null, null, noCache);
+ await renderApp(res, null, null, noCache, getDebugChannel(req));
}
});
@@ -204,7 +226,7 @@ app.post('/', bodyParser.text(), async function (req, res) {
// We handle the error on the client
}
// Refresh the client and return the value
- renderApp(res, result, null, noCache);
+ renderApp(res, result, null, noCache, getDebugChannel(req));
} else {
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
@@ -220,11 +242,11 @@ app.post('/', bodyParser.text(), async function (req, res) {
// Wait for any mutations
const result = await action();
const formState = decodeFormState(result, formData);
- renderApp(res, null, formState, noCache);
+ renderApp(res, null, formState, noCache, undefined);
} catch (x) {
const {setServerState} = await import('../src/ServerState.js');
setServerState('Error: ' + x.message);
- renderApp(res, null, null, noCache);
+ renderApp(res, null, null, noCache, undefined);
}
}
});
@@ -324,7 +346,7 @@ if (process.env.NODE_ENV === 'development') {
});
}
-app.listen(3001, () => {
+const httpServer = app.listen(3001, () => {
console.log('Regional Flight Server listening on port 3001...');
});
@@ -346,3 +368,27 @@ app.on('error', function (error) {
throw error;
}
});
+
+if (process.env.NODE_ENV === 'development') {
+ // Open a websocket server for Debug information
+ const WebSocket = require('ws');
+ const webSocketServer = new WebSocket.Server({noServer: true});
+
+ httpServer.on('upgrade', (request, socket, head) => {
+ const DEBUG_CHANNEL_PATH = '/debug-channel?';
+ if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
+ const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
+ const promiseForWs = new Promise(resolve => {
+ webSocketServer.handleUpgrade(request, socket, head, ws => {
+ ws.on('close', () => {
+ activeDebugChannels.delete(requestId);
+ });
+ resolve(ws);
+ });
+ });
+ activeDebugChannels.set(requestId, promiseForWs);
+ } else {
+ socket.destroy();
+ }
+ });
+}
diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js
index 5657b040ffa2b..b73162847d612 100644
--- a/fixtures/flight/src/App.js
+++ b/fixtures/flight/src/App.js
@@ -123,7 +123,6 @@ async function ServerComponent({noCache}) {
export default async function App({prerender, noCache}) {
const res = await fetch('http://localhost:3001/todos');
const todos = await res.json();
- console.log(res);
const dedupedChild = ;
const message = getServerState();
diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js
index f08f7a110bf61..3fd921a1bb5b4 100644
--- a/fixtures/flight/src/index.js
+++ b/fixtures/flight/src/index.js
@@ -42,17 +42,43 @@ function Shell({data}) {
}
async function hydrateApp() {
- const {root, returnValue, formState} = await createFromFetch(
- fetch('/', {
- headers: {
- Accept: 'text/x-component',
- },
- }),
- {
- callServer,
- findSourceMapURL,
- }
- );
+ let response;
+ if (
+ process.env.NODE_ENV === 'development' &&
+ typeof WebSocketStream === 'function'
+ ) {
+ const requestId = crypto.randomUUID();
+ const wss = new WebSocketStream(
+ 'ws://localhost:3001/debug-channel?' + requestId
+ );
+ const debugChannel = await wss.opened;
+ response = createFromFetch(
+ fetch('/', {
+ headers: {
+ Accept: 'text/x-component',
+ 'rsc-request-id': requestId,
+ },
+ }),
+ {
+ callServer,
+ debugChannel,
+ findSourceMapURL,
+ }
+ );
+ } else {
+ response = createFromFetch(
+ fetch('/', {
+ headers: {
+ Accept: 'text/x-component',
+ },
+ }),
+ {
+ callServer,
+ findSourceMapURL,
+ }
+ );
+ }
+ const {root, returnValue, formState} = await response;
ReactDOM.hydrateRoot(
document,
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index cb19755734078..9d338c581f39d 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -328,6 +328,8 @@ export type FindSourceMapURLCallback = (
environmentName: string,
) => null | string;
+export type DebugChannelCallback = (message: string) => void;
+
export type Response = {
_bundlerConfig: ServerConsumerModuleMap,
_serverReferenceConfig: null | ServerManifest,
@@ -351,6 +353,7 @@ export type Response = {
_debugRootStack?: null | Error, // DEV-only
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
+ _debugChannel?: void | DebugChannelCallback, // DEV-only
_replayConsole: boolean, // DEV-only
_rootEnvironmentName: string, // DEV-only, the requested environment name.
};
@@ -687,6 +690,15 @@ export function reportGlobalError(response: Response, error: Error): void {
triggerErrorOnChunk(chunk, error);
}
});
+ if (__DEV__) {
+ const debugChannel = response._debugChannel;
+ if (debugChannel !== undefined) {
+ // If we don't have any more ways of reading data, we don't have to send any
+ // more neither. So we close the writable side.
+ debugChannel('');
+ response._debugChannel = undefined;
+ }
+ }
if (enableProfilerTimer && enableComponentPerformanceTrack) {
markAllTracksInOrder();
flushComponentPerformance(
@@ -1667,6 +1679,14 @@ function parseModelString(
}
case 'Y': {
if (__DEV__) {
+ if (value.length > 2) {
+ const debugChannel = response._debugChannel;
+ if (debugChannel) {
+ const ref = value.slice(2);
+ debugChannel('R:' + ref); // Release this reference immediately
+ }
+ }
+
// In DEV mode we encode omitted objects in logs as a getter that throws
// so that when you try to access it on the client, you know why that
// happened.
@@ -1730,9 +1750,10 @@ function ResponseInstance(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
- findSourceMapURL: void | FindSourceMapURLCallback,
- replayConsole: boolean,
- environmentName: void | string,
+ findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
+ replayConsole: boolean, // DEV-only
+ environmentName: void | string, // DEV-only
+ debugChannel: void | DebugChannelCallback, // DEV-only
) {
const chunks: Map> = new Map();
this._bundlerConfig = bundlerConfig;
@@ -1787,6 +1808,7 @@ function ResponseInstance(
);
}
this._debugFindSourceMapURL = findSourceMapURL;
+ this._debugChannel = debugChannel;
this._replayConsole = replayConsole;
this._rootEnvironmentName = rootEnv;
}
@@ -1802,9 +1824,10 @@ export function createResponse(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
- findSourceMapURL: void | FindSourceMapURLCallback,
- replayConsole: boolean,
- environmentName: void | string,
+ findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only
+ replayConsole: boolean, // DEV-only
+ environmentName: void | string, // DEV-only
+ debugChannel: void | DebugChannelCallback, // DEV-only
): Response {
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
return new ResponseInstance(
@@ -1818,6 +1841,7 @@ export function createResponse(
findSourceMapURL,
replayConsole,
environmentName,
+ debugChannel,
);
}
diff --git a/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
new file mode 100644
index 0000000000000..e9428c3ba4074
--- /dev/null
+++ b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ * @jest-environment node
+ */
+
+'use strict';
+
+if (typeof Blob === 'undefined') {
+ global.Blob = require('buffer').Blob;
+}
+if (typeof File === 'undefined' || typeof FormData === 'undefined') {
+ global.File = require('undici').File;
+ global.FormData = require('undici').FormData;
+}
+
+function formatV8Stack(stack) {
+ let v8StyleStack = '';
+ if (stack) {
+ for (let i = 0; i < stack.length; i++) {
+ const [name] = stack[i];
+ if (v8StyleStack !== '') {
+ v8StyleStack += '\n';
+ }
+ v8StyleStack += ' in ' + name + ' (at **)';
+ }
+ }
+ return v8StyleStack;
+}
+
+function normalizeComponentInfo(debugInfo) {
+ if (Array.isArray(debugInfo.stack)) {
+ const {debugTask, debugStack, ...copy} = debugInfo;
+ copy.stack = formatV8Stack(debugInfo.stack);
+ if (debugInfo.owner) {
+ copy.owner = normalizeComponentInfo(debugInfo.owner);
+ }
+ return copy;
+ } else {
+ return debugInfo;
+ }
+}
+
+function getDebugInfo(obj) {
+ const debugInfo = obj._debugInfo;
+ if (debugInfo) {
+ const copy = [];
+ for (let i = 0; i < debugInfo.length; i++) {
+ copy.push(normalizeComponentInfo(debugInfo[i]));
+ }
+ return copy;
+ }
+ return debugInfo;
+}
+
+let act;
+let React;
+let ReactNoop;
+let ReactNoopFlightServer;
+let ReactNoopFlightClient;
+
+describe('ReactFlight', () => {
+ beforeEach(() => {
+ // Mock performance.now for timing tests
+ let time = 10;
+ const now = jest.fn().mockImplementation(() => {
+ return time++;
+ });
+ Object.defineProperty(performance, 'timeOrigin', {
+ value: time,
+ configurable: true,
+ });
+ Object.defineProperty(performance, 'now', {
+ value: now,
+ configurable: true,
+ });
+
+ jest.resetModules();
+ jest.mock('react', () => require('react/react.react-server'));
+ ReactNoopFlightServer = require('react-noop-renderer/flight-server');
+ // This stores the state so we need to preserve it
+ const flightModules = require('react-noop-renderer/flight-modules');
+ jest.resetModules();
+ __unmockReact();
+ jest.mock('react-noop-renderer/flight-modules', () => flightModules);
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ ReactNoopFlightClient = require('react-noop-renderer/flight-client');
+ act = require('internal-test-utils').act;
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ // @gate __DEV__ && enableComponentPerformanceTrack
+ it('can render deep but cut off JSX in debug info', async () => {
+ function createDeepJSX(n) {
+ if (n <= 0) {
+ return null;
+ }
+ return {createDeepJSX(n - 1)}
;
+ }
+
+ function ServerComponent(props) {
+ return not using props
;
+ }
+
+ const debugChannel = {onMessage(message) {}};
+
+ const transport = ReactNoopFlightServer.render(
+ {
+ root: (
+
+ {createDeepJSX(100) /* deper than objectLimit */}
+
+ ),
+ },
+ {debugChannel},
+ );
+
+ await act(async () => {
+ const rootModel = await ReactNoopFlightClient.read(transport, {
+ debugChannel,
+ });
+ const root = rootModel.root;
+ const children = getDebugInfo(root)[1].props.children;
+ expect(children.type).toBe('div');
+ expect(children.props.children.type).toBe('div');
+ ReactNoop.render(root);
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput(not using props
);
+ });
+});
diff --git a/packages/react-markup/src/ReactMarkupServer.js b/packages/react-markup/src/ReactMarkupServer.js
index d3950c568f7ce..f144211711d75 100644
--- a/packages/react-markup/src/ReactMarkupServer.js
+++ b/packages/react-markup/src/ReactMarkupServer.js
@@ -171,6 +171,7 @@ export function experimental_renderToHTML(
undefined,
'Markup',
undefined,
+ false,
);
const flightResponse = createFlightResponse(
null,
diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js
index c2be176b1a8b1..d4e6e6ea7936e 100644
--- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js
+++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js
@@ -56,6 +56,7 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
type ReadOptions = {|
findSourceMapURL?: FindSourceMapURLCallback,
+ debugChannel?: {onMessage: (message: string) => void},
close?: boolean,
|};
@@ -71,6 +72,9 @@ function read(source: Source, options: ReadOptions): Thenable {
options !== undefined ? options.findSourceMapURL : undefined,
true,
undefined,
+ __DEV__ && options !== undefined && options.debugChannel !== undefined
+ ? options.debugChannel.onMessage
+ : undefined,
);
for (let i = 0; i < source.length; i++) {
processBinaryChunk(response, source[i], 0);
diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js
index 7c3790e9cf052..d54ebe30c89da 100644
--- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js
+++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js
@@ -71,6 +71,7 @@ type Options = {
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
signal?: AbortSignal,
+ debugChannel?: {onMessage?: (message: string) => void},
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
};
@@ -87,6 +88,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ __DEV__ && options && options.debugChannel !== undefined,
);
const signal = options ? options.signal : undefined;
if (signal) {
@@ -100,6 +102,11 @@ function render(model: ReactClientValue, options?: Options): Destination {
signal.addEventListener('abort', listener);
}
}
+ if (__DEV__ && options && options.debugChannel !== undefined) {
+ options.debugChannel.onMessage = message => {
+ ReactNoopFlightServer.resolveDebugMessage(request, message);
+ };
+ }
ReactNoopFlightServer.startWork(request);
ReactNoopFlightServer.startFlowing(request, destination);
return destination;
diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
index 9ae47e3b5512d..9e4a9efb58cca 100644
--- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
+++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
+ DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -43,12 +44,31 @@ type CallServerCallback = (string, args: A) => Promise;
export type Options = {
moduleBaseURL?: string,
callServer?: CallServerCallback,
+ debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
};
+function createDebugCallbackFromWritableStream(
+ debugWritable: WritableStream,
+): DebugChannelCallback {
+ const textEncoder = new TextEncoder();
+ const writer = debugWritable.getWriter();
+ return message => {
+ if (message === '') {
+ writer.close();
+ } else {
+ // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
+ // Therefore, we can't report errors from this write back to the Response object.
+ if (__DEV__) {
+ writer.write(textEncoder.encode(message + '\n')).catch(console.error);
+ }
+ }
+ };
+}
+
function createResponseFromOptions(options: void | Options) {
return createResponse(
options && options.moduleBaseURL ? options.moduleBaseURL : '',
@@ -67,6 +87,12 @@ function createResponseFromOptions(options: void | Options) {
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
+ __DEV__ &&
+ options &&
+ options.debugChannel !== undefined &&
+ options.debugChannel.writable !== undefined
+ ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
+ : undefined,
);
}
diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
index a9c978b493012..6f530dd2b6fd7 100644
--- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
+import type {Duplex} from 'stream';
+
import {Readable} from 'stream';
import {
@@ -27,6 +29,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -50,6 +54,12 @@ export {
registerClientReference,
} from '../ReactFlightESMReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigNode';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -67,7 +77,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
+function startReadingFromDebugChannelReadable(
+ request: Request,
+ stream: Readable | WebSocket,
+): void {
+ const stringDecoder = createStringDecoder();
+ let lastWasPartial = false;
+ let stringBuffer = '';
+ function onData(chunk: string | Uint8Array) {
+ if (typeof chunk === 'string') {
+ if (lastWasPartial) {
+ stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
+ lastWasPartial = false;
+ }
+ stringBuffer += chunk;
+ } else {
+ const buffer: Uint8Array = (chunk: any);
+ stringBuffer += readPartialStringChunk(stringDecoder, buffer);
+ lastWasPartial = true;
+ }
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ }
+ function onError(error: mixed) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: error,
+ }),
+ );
+ }
+ function onClose() {
+ closeDebugChannel(request);
+ }
+ if (
+ // $FlowFixMe[method-unbinding]
+ typeof stream.addEventListener === 'function' &&
+ // $FlowFixMe[method-unbinding]
+ typeof stream.binaryType === 'string'
+ ) {
+ const ws: WebSocket = (stream: any);
+ ws.binaryType = 'arraybuffer';
+ ws.addEventListener('message', event => {
+ // $FlowFixMe
+ onData(event.data);
+ });
+ ws.addEventListener('error', event => {
+ // $FlowFixMe
+ onError(event.error);
+ });
+ ws.addEventListener('close', onClose);
+ } else {
+ const readable: Readable = (stream: any);
+ readable.on('data', onData);
+ readable.on('error', onError);
+ readable.on('end', onClose);
+ }
+}
+
type Options = {
+ debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -86,6 +158,7 @@ function renderToPipeableStream(
moduleBasePath: ClientManifest,
options?: Options,
): PipeableStream {
+ const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
moduleBasePath,
@@ -95,9 +168,13 @@ function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
+ if (debugChannel !== undefined) {
+ startReadingFromDebugChannelReadable(request, debugChannel);
+ }
return {
pipe(destination: T): T {
if (hasStartedFlowing) {
@@ -126,11 +203,12 @@ function renderToPipeableStream(
},
};
}
+
function createFakeWritable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
- write(chunk) {
+ write(chunk: string | Uint8Array) {
return readable.push(chunk);
},
end() {
@@ -184,6 +262,7 @@ function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -287,8 +366,8 @@ function decodeReply(
export {
renderToPipeableStream,
prerenderToNodeStream,
- decodeReplyFromBusboy,
decodeReply,
+ decodeReplyFromBusboy,
decodeAction,
decodeFormState,
};
diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
index 3aca4a355dce7..47098d94902de 100644
--- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
+++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
@@ -8,7 +8,10 @@
*/
import type {Thenable} from 'shared/ReactTypes.js';
-import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
+import type {
+ Response as FlightResponse,
+ DebugChannelCallback,
+} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerParcel';
@@ -76,6 +79,24 @@ export function createServerReference, T>(
);
}
+function createDebugCallbackFromWritableStream(
+ debugWritable: WritableStream,
+): DebugChannelCallback {
+ const textEncoder = new TextEncoder();
+ const writer = debugWritable.getWriter();
+ return message => {
+ if (message === '') {
+ writer.close();
+ } else {
+ // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
+ // Therefore, we can't report errors from this write back to the Response object.
+ if (__DEV__) {
+ writer.write(textEncoder.encode(message + '\n')).catch(console.error);
+ }
+ }
+ };
+}
+
function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
@@ -104,6 +125,7 @@ function startReadingFromStream(
}
export type Options = {
+ debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
replayConsoleLogs?: boolean,
environmentName?: string,
@@ -128,6 +150,12 @@ export function createFromReadableStream(
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
+ __DEV__ &&
+ options &&
+ options.debugChannel !== undefined &&
+ options.debugChannel.writable !== undefined
+ ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
+ : undefined,
);
startReadingFromStream(response, stream);
return getRoot(response);
@@ -152,6 +180,12 @@ export function createFromFetch(
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
+ __DEV__ &&
+ options &&
+ options.debugChannel !== undefined &&
+ options.debugChannel.writable !== undefined
+ ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
+ : undefined,
);
promiseForResponse.then(
function (r) {
diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
index 73a8741618213..988f5628a919a 100644
--- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
@@ -7,7 +7,10 @@
* @flow
*/
-import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
import {
preloadModule,
@@ -24,6 +27,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -42,12 +47,19 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigWeb';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
type Options = {
+ debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -57,10 +69,55 @@ type Options = {
onPostpone?: (reason: string) => void,
};
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
export function renderToReadableStream(
model: ReactClientValue,
options?: Options,
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
null,
@@ -70,6 +127,7 @@ export function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -83,6 +141,9 @@ export function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
const stream = new ReadableStream(
{
type: 'bytes',
@@ -117,9 +178,6 @@ export function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
- start: (controller): ?Promise => {
- startWork(request);
- },
pull: (controller): ?Promise => {
startFlowing(request, controller);
},
@@ -144,6 +202,7 @@ export function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
index 2a365993a7cfb..54d9a78c5f92a 100644
--- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
@@ -7,7 +7,10 @@
* @flow
*/
-import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
import type {ReactFormState, Thenable} from 'shared/ReactTypes';
import {
preloadModule,
@@ -26,6 +29,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -47,12 +52,19 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigWeb';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export type {TemporaryReferenceSet};
type Options = {
+ debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -62,10 +74,55 @@ type Options = {
onPostpone?: (reason: string) => void,
};
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
export function renderToReadableStream(
model: ReactClientValue,
options?: Options,
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
null,
@@ -75,6 +132,7 @@ export function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -88,6 +146,9 @@ export function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
const stream = new ReadableStream(
{
type: 'bytes',
@@ -122,9 +183,6 @@ export function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
- start: (controller): ?Promise => {
- startWork(request);
- },
pull: (controller): ?Promise => {
startFlowing(request, controller);
},
@@ -149,6 +207,7 @@ export function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
index 9ce1d43fa718d..abdd6452793dd 100644
--- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
@@ -20,6 +20,8 @@ import type {
ServerReferenceId,
} from '../client/ReactFlightClientConfigBundlerParcel';
+import type {Duplex} from 'stream';
+
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -31,6 +33,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -49,6 +53,7 @@ import {
decodeAction as decodeActionImpl,
decodeFormState as decodeFormStateImpl,
} from 'react-server/src/ReactFlightActionServer';
+
import {
preloadModule,
requireModule,
@@ -60,6 +65,12 @@ export {
registerServerReference,
} from '../ReactFlightParcelReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigNode';
+
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -79,7 +90,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
+function startReadingFromDebugChannelReadable(
+ request: Request,
+ stream: Readable | WebSocket,
+): void {
+ const stringDecoder = createStringDecoder();
+ let lastWasPartial = false;
+ let stringBuffer = '';
+ function onData(chunk: string | Uint8Array) {
+ if (typeof chunk === 'string') {
+ if (lastWasPartial) {
+ stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
+ lastWasPartial = false;
+ }
+ stringBuffer += chunk;
+ } else {
+ const buffer: Uint8Array = (chunk: any);
+ stringBuffer += readPartialStringChunk(stringDecoder, buffer);
+ lastWasPartial = true;
+ }
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ }
+ function onError(error: mixed) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: error,
+ }),
+ );
+ }
+ function onClose() {
+ closeDebugChannel(request);
+ }
+ if (
+ // $FlowFixMe[method-unbinding]
+ typeof stream.addEventListener === 'function' &&
+ // $FlowFixMe[method-unbinding]
+ typeof stream.binaryType === 'string'
+ ) {
+ const ws: WebSocket = (stream: any);
+ ws.binaryType = 'arraybuffer';
+ ws.addEventListener('message', event => {
+ // $FlowFixMe
+ onData(event.data);
+ });
+ ws.addEventListener('error', event => {
+ // $FlowFixMe
+ onError(event.error);
+ });
+ ws.addEventListener('close', onClose);
+ } else {
+ const readable: Readable = (stream: any);
+ readable.on('data', onData);
+ readable.on('error', onError);
+ readable.on('end', onClose);
+ }
+}
+
type Options = {
+ debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -97,6 +170,7 @@ export function renderToPipeableStream(
model: ReactClientValue,
options?: Options,
): PipeableStream {
+ const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
null,
@@ -106,9 +180,13 @@ export function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
+ if (debugChannel !== undefined) {
+ startReadingFromDebugChannelReadable(request, debugChannel);
+ }
return {
pipe(destination: T): T {
if (hasStartedFlowing) {
@@ -149,7 +227,7 @@ function createFakeWritableFromReadableStreamController(
chunk = textEncoder.encode(chunk);
}
controller.enqueue(chunk);
- // in web streams there is no backpressure so we can alwas write more
+ // in web streams there is no backpressure so we can always write more
return true;
},
end() {
@@ -167,13 +245,58 @@ function createFakeWritableFromReadableStreamController(
}: any);
}
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
export function renderToReadableStream(
model: ReactClientValue,
-
- options?: Options & {
+ options?: Omit & {
+ debugChannel?: {readable?: ReadableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
null,
@@ -183,6 +306,7 @@ export function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -196,6 +320,9 @@ export function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
let writable: Writable;
const stream = new ReadableStream(
{
@@ -275,6 +402,7 @@ export function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -296,7 +424,6 @@ export function prerenderToNodeStream(
export function prerender(
model: ReactClientValue,
-
options?: Options & {
signal?: AbortSignal,
},
@@ -338,6 +465,7 @@ export function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
index ee319beca18ef..0a3b6cedc8224 100644
--- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
+++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
+ DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -42,12 +43,31 @@ type CallServerCallback = (string, args: A) => Promise;
export type Options = {
callServer?: CallServerCallback,
+ debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
};
+function createDebugCallbackFromWritableStream(
+ debugWritable: WritableStream,
+): DebugChannelCallback {
+ const textEncoder = new TextEncoder();
+ const writer = debugWritable.getWriter();
+ return message => {
+ if (message === '') {
+ writer.close();
+ } else {
+ // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
+ // Therefore, we can't report errors from this write back to the Response object.
+ if (__DEV__) {
+ writer.write(textEncoder.encode(message + '\n')).catch(console.error);
+ }
+ }
+ };
+}
+
function createResponseFromOptions(options: void | Options) {
return createResponse(
null,
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
+ __DEV__ &&
+ options &&
+ options.debugChannel !== undefined &&
+ options.debugChannel.writable !== undefined
+ ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
+ : undefined,
);
}
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
index 11dbe1a7c1358..0f09f82b90d74 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
@@ -7,7 +7,10 @@
* @flow
*/
-import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -19,6 +22,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -38,6 +43,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigWeb';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
+ debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -54,11 +66,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options,
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
turbopackMap,
@@ -68,6 +125,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -81,6 +139,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
const stream = new ReadableStream(
{
type: 'bytes',
@@ -116,9 +177,6 @@ function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
- start: (controller): ?Promise => {
- startWork(request);
- },
pull: (controller): ?Promise => {
startFlowing(request, controller);
},
@@ -143,6 +201,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
index e8256767fa5b1..07ed44059f070 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
@@ -7,7 +7,10 @@
* @flow
*/
-import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -21,6 +24,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -43,6 +48,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigWeb';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
+ debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -59,11 +71,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options,
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
turbopackMap,
@@ -73,6 +130,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -86,6 +144,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
const stream = new ReadableStream(
{
type: 'bytes',
@@ -121,9 +182,6 @@ function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
- start: (controller): ?Promise => {
- startWork(request);
- },
pull: (controller): ?Promise => {
startFlowing(request, controller);
},
@@ -148,6 +206,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
index 10d39e67a8169..8e18f71909856 100644
--- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
+import type {Duplex} from 'stream';
+
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -29,6 +31,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -54,6 +58,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightTurbopackReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigNode';
+
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
+function startReadingFromDebugChannelReadable(
+ request: Request,
+ stream: Readable | WebSocket,
+): void {
+ const stringDecoder = createStringDecoder();
+ let lastWasPartial = false;
+ let stringBuffer = '';
+ function onData(chunk: string | Uint8Array) {
+ if (typeof chunk === 'string') {
+ if (lastWasPartial) {
+ stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
+ lastWasPartial = false;
+ }
+ stringBuffer += chunk;
+ } else {
+ const buffer: Uint8Array = (chunk: any);
+ stringBuffer += readPartialStringChunk(stringDecoder, buffer);
+ lastWasPartial = true;
+ }
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ }
+ function onError(error: mixed) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: error,
+ }),
+ );
+ }
+ function onClose() {
+ closeDebugChannel(request);
+ }
+ if (
+ // $FlowFixMe[method-unbinding]
+ typeof stream.addEventListener === 'function' &&
+ // $FlowFixMe[method-unbinding]
+ typeof stream.binaryType === 'string'
+ ) {
+ const ws: WebSocket = (stream: any);
+ ws.binaryType = 'arraybuffer';
+ ws.addEventListener('message', event => {
+ // $FlowFixMe
+ onData(event.data);
+ });
+ ws.addEventListener('error', event => {
+ // $FlowFixMe
+ onError(event.error);
+ });
+ ws.addEventListener('close', onClose);
+ } else {
+ const readable: Readable = (stream: any);
+ readable.on('data', onData);
+ readable.on('error', onError);
+ readable.on('end', onClose);
+ }
+}
+
type Options = {
+ debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -92,6 +164,7 @@ function renderToPipeableStream(
turbopackMap: ClientManifest,
options?: Options,
): PipeableStream {
+ const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
turbopackMap,
@@ -101,9 +174,13 @@ function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
+ if (debugChannel !== undefined) {
+ startReadingFromDebugChannelReadable(request, debugChannel);
+ }
return {
pipe(destination: T): T {
if (hasStartedFlowing) {
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
}: any);
}
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
function renderToReadableStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
- options?: Options & {
+ options?: Omit & {
+ debugChannel?: {readable?: ReadableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
turbopackMap,
@@ -178,6 +301,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -191,6 +315,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
let writable: Writable;
const stream = new ReadableStream(
{
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -334,6 +462,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
index ee319beca18ef..0a3b6cedc8224 100644
--- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
+++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes.js';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
+ DebugChannelCallback,
} from 'react-client/src/ReactFlightClient';
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
@@ -42,12 +43,31 @@ type CallServerCallback = (string, args: A) => Promise;
export type Options = {
callServer?: CallServerCallback,
+ debugChannel?: {writable?: WritableStream, ...},
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
};
+function createDebugCallbackFromWritableStream(
+ debugWritable: WritableStream,
+): DebugChannelCallback {
+ const textEncoder = new TextEncoder();
+ const writer = debugWritable.getWriter();
+ return message => {
+ if (message === '') {
+ writer.close();
+ } else {
+ // Note: It's important that this function doesn't close over the Response object or it can't be GC:ed.
+ // Therefore, we can't report errors from this write back to the Response object.
+ if (__DEV__) {
+ writer.write(textEncoder.encode(message + '\n')).catch(console.error);
+ }
+ }
+ };
+}
+
function createResponseFromOptions(options: void | Options) {
return createResponse(
null,
@@ -66,6 +86,12 @@ function createResponseFromOptions(options: void | Options) {
__DEV__ && options && options.environmentName
? options.environmentName
: undefined,
+ __DEV__ &&
+ options &&
+ options.debugChannel !== undefined &&
+ options.debugChannel.writable !== undefined
+ ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
+ : undefined,
);
}
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
index 7954417b95a25..e2576eafecc19 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
@@ -7,7 +7,10 @@
* @flow
*/
-import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -19,6 +22,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -38,6 +43,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigWeb';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -45,6 +56,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
+ debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -54,11 +66,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options,
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
webpackMap,
@@ -68,6 +125,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -81,6 +139,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
const stream = new ReadableStream(
{
type: 'bytes',
@@ -116,9 +177,6 @@ function prerender(
const stream = new ReadableStream(
{
type: 'bytes',
- start: (controller): ?Promise => {
- startWork(request);
- },
pull: (controller): ?Promise => {
startFlowing(request, controller);
},
@@ -143,6 +201,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
index 46cb61fc4c0e9..e871cfb9e9edb 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
@@ -7,7 +7,10 @@
* @flow
*/
-import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
+import type {
+ Request,
+ ReactClientValue,
+} from 'react-server/src/ReactFlightServer';
import type {Thenable} from 'shared/ReactTypes';
import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler';
import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig';
@@ -21,6 +24,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -43,6 +48,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigWeb';
+
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -50,6 +61,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
export type {TemporaryReferenceSet};
type Options = {
+ debugChannel?: {readable?: ReadableStream, ...},
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
identifierPrefix?: string,
@@ -59,11 +71,56 @@ type Options = {
onPostpone?: (reason: string) => void,
};
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
options?: Options,
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
webpackMap,
@@ -73,6 +130,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -86,6 +144,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
const stream = new ReadableStream(
{
type: 'bytes',
@@ -145,6 +206,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
index cede8a46d69ad..7bac80292fe5e 100644
--- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
@@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';
+import type {Duplex} from 'stream';
+
import {Readable} from 'stream';
import {ASYNC_ITERATOR} from 'shared/ReactSymbols';
@@ -29,6 +31,8 @@ import {
startFlowing,
stopFlowing,
abort,
+ resolveDebugMessage,
+ closeDebugChannel,
} from 'react-server/src/ReactFlightServer';
import {
@@ -54,6 +58,12 @@ export {
createClientModuleProxy,
} from '../ReactFlightWebpackReferences';
+import {
+ createStringDecoder,
+ readPartialStringChunk,
+ readFinalStringChunk,
+} from 'react-client/src/ReactFlightClientStreamConfigNode';
+
import {textEncoder} from 'react-server/src/ReactServerStreamConfigNode';
import type {TemporaryReferenceSet} from 'react-server/src/ReactFlightServerTemporaryReferences';
@@ -73,7 +83,69 @@ function createCancelHandler(request: Request, reason: string) {
};
}
+function startReadingFromDebugChannelReadable(
+ request: Request,
+ stream: Readable | WebSocket,
+): void {
+ const stringDecoder = createStringDecoder();
+ let lastWasPartial = false;
+ let stringBuffer = '';
+ function onData(chunk: string | Uint8Array) {
+ if (typeof chunk === 'string') {
+ if (lastWasPartial) {
+ stringBuffer += readFinalStringChunk(stringDecoder, new Uint8Array(0));
+ lastWasPartial = false;
+ }
+ stringBuffer += chunk;
+ } else {
+ const buffer: Uint8Array = (chunk: any);
+ stringBuffer += readPartialStringChunk(stringDecoder, buffer);
+ lastWasPartial = true;
+ }
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ }
+ function onError(error: mixed) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: error,
+ }),
+ );
+ }
+ function onClose() {
+ closeDebugChannel(request);
+ }
+ if (
+ // $FlowFixMe[method-unbinding]
+ typeof stream.addEventListener === 'function' &&
+ // $FlowFixMe[method-unbinding]
+ typeof stream.binaryType === 'string'
+ ) {
+ const ws: WebSocket = (stream: any);
+ ws.binaryType = 'arraybuffer';
+ ws.addEventListener('message', event => {
+ // $FlowFixMe
+ onData(event.data);
+ });
+ ws.addEventListener('error', event => {
+ // $FlowFixMe
+ onError(event.error);
+ });
+ ws.addEventListener('close', onClose);
+ } else {
+ const readable: Readable = (stream: any);
+ readable.on('data', onData);
+ readable.on('error', onError);
+ readable.on('end', onClose);
+ }
+}
+
type Options = {
+ debugChannel?: Readable | Duplex | WebSocket,
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
@@ -92,6 +164,7 @@ function renderToPipeableStream(
webpackMap: ClientManifest,
options?: Options,
): PipeableStream {
+ const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
const request = createRequest(
model,
webpackMap,
@@ -101,9 +174,13 @@ function renderToPipeableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannel !== undefined,
);
let hasStartedFlowing = false;
startWork(request);
+ if (debugChannel !== undefined) {
+ startReadingFromDebugChannelReadable(request, debugChannel);
+ }
return {
pipe(destination: T): T {
if (hasStartedFlowing) {
@@ -162,13 +239,59 @@ function createFakeWritableFromReadableStreamController(
}: any);
}
+function startReadingFromDebugChannelReadableStream(
+ request: Request,
+ stream: ReadableStream,
+): void {
+ const reader = stream.getReader();
+ const stringDecoder = createStringDecoder();
+ let stringBuffer = '';
+ function progress({
+ done,
+ value,
+ }: {
+ done: boolean,
+ value: ?any,
+ ...
+ }): void | Promise {
+ const buffer: Uint8Array = (value: any);
+ stringBuffer += done
+ ? readFinalStringChunk(stringDecoder, new Uint8Array(0))
+ : readPartialStringChunk(stringDecoder, buffer);
+ const messages = stringBuffer.split('\n');
+ for (let i = 0; i < messages.length - 1; i++) {
+ resolveDebugMessage(request, messages[i]);
+ }
+ stringBuffer = messages[messages.length - 1];
+ if (done) {
+ closeDebugChannel(request);
+ return;
+ }
+ return reader.read().then(progress).catch(error);
+ }
+ function error(e: any) {
+ abort(
+ request,
+ new Error('Lost connection to the Debug Channel.', {
+ cause: e,
+ }),
+ );
+ }
+ reader.read().then(progress).catch(error);
+}
+
function renderToReadableStream(
model: ReactClientValue,
webpackMap: ClientManifest,
- options?: Options & {
+ options?: Omit & {
+ debugChannel?: {readable?: ReadableStream, ...},
signal?: AbortSignal,
},
): ReadableStream {
+ const debugChannelReadable =
+ __DEV__ && options && options.debugChannel
+ ? options.debugChannel.readable
+ : undefined;
const request = createRequest(
model,
webpackMap,
@@ -178,6 +301,7 @@ function renderToReadableStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ debugChannelReadable !== undefined,
);
if (options && options.signal) {
const signal = options.signal;
@@ -191,6 +315,9 @@ function renderToReadableStream(
signal.addEventListener('abort', listener);
}
}
+ if (debugChannelReadable !== undefined) {
+ startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
+ }
let writable: Writable;
const stream = new ReadableStream(
{
@@ -271,6 +398,7 @@ function prerenderToNodeStream(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
@@ -334,6 +462,7 @@ function prerender(
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
+ false,
);
if (options && options.signal) {
const signal = options.signal;
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index f07e3c6304582..df309ac856b25 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -404,6 +404,13 @@ type Task = {
interface Reference {}
+type ReactClientReference = Reference & ReactClientValue;
+
+type DeferredDebugStore = {
+ retained: Map,
+ existing: Map,
+};
+
const OPENING = 10;
const OPEN = 11;
const ABORTING = 12;
@@ -451,6 +458,7 @@ export type Request = {
filterStackFrame: (url: string, functionName: string) => boolean,
didWarnForKey: null | WeakSet,
writtenDebugObjects: WeakMap,
+ deferredDebugObjects: null | DeferredDebugStore,
};
const {
@@ -495,13 +503,14 @@ function RequestInstance(
model: ReactClientValue,
bundlerConfig: ClientManifest,
onError: void | ((error: mixed) => ?string),
- identifierPrefix?: string,
onPostpone: void | ((reason: string) => void),
+ onAllReady: () => void,
+ onFatalError: (error: mixed) => void,
+ identifierPrefix?: string,
temporaryReferences: void | TemporaryReferenceSet,
environmentName: void | string | (() => string), // DEV-only
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
- onAllReady: () => void,
- onFatalError: (error: mixed) => void,
+ keepDebugAlive: boolean, // DEV-only
) {
if (
ReactSharedInternals.A !== null &&
@@ -571,6 +580,12 @@ function RequestInstance(
: filterStackFrame;
this.didWarnForKey = null;
this.writtenDebugObjects = new WeakMap();
+ this.deferredDebugObjects = keepDebugAlive
+ ? {
+ retained: new Map(),
+ existing: new Map(),
+ }
+ : null;
}
let timeOrigin: number;
@@ -615,6 +630,7 @@ export function createRequest(
temporaryReferences: void | TemporaryReferenceSet,
environmentName: void | string | (() => string), // DEV-only
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
+ keepDebugAlive: boolean, // DEV-only
): Request {
if (__DEV__) {
resetOwnerStackLimit();
@@ -626,13 +642,14 @@ export function createRequest(
model,
bundlerConfig,
onError,
- identifierPrefix,
onPostpone,
+ noop,
+ noop,
+ identifierPrefix,
temporaryReferences,
environmentName,
filterStackFrame,
- noop,
- noop,
+ keepDebugAlive,
);
}
@@ -647,6 +664,7 @@ export function createPrerenderRequest(
temporaryReferences: void | TemporaryReferenceSet,
environmentName: void | string | (() => string), // DEV-only
filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
+ keepDebugAlive: boolean, // DEV-only
): Request {
if (__DEV__) {
resetOwnerStackLimit();
@@ -658,13 +676,14 @@ export function createPrerenderRequest(
model,
bundlerConfig,
onError,
- identifierPrefix,
onPostpone,
+ onAllReady,
+ onFatalError,
+ identifierPrefix,
temporaryReferences,
environmentName,
filterStackFrame,
- onAllReady,
- onFatalError,
+ keepDebugAlive,
);
}
@@ -2331,7 +2350,21 @@ function serializeSymbolReference(name: string): string {
return '$S' + name;
}
-function serializeLimitedObject(): string {
+function serializeDeferredObject(
+ request: Request,
+ value: ReactClientReference | string,
+): string {
+ const deferredDebugObjects = request.deferredDebugObjects;
+ if (deferredDebugObjects !== null) {
+ // This client supports a long lived connection. We can assign this object
+ // an ID to be lazy loaded later.
+ // This keeps the connection alive until we ask for it or release it.
+ request.pendingChunks++;
+ const id = request.nextChunkId++;
+ deferredDebugObjects.existing.set(value, id);
+ deferredDebugObjects.retained.set(id, value);
+ return '$Y' + id.toString(16);
+ }
return '$Y';
}
@@ -4058,12 +4091,25 @@ function renderDebugModel(
if (counter.objectLimit <= 0 && !doNotLimit.has(value)) {
// We've reached our max number of objects to serialize across the wire so we serialize this
- // as a marker so that the client can error when this is accessed by the console.
- return serializeLimitedObject();
+ // as a marker so that the client can error or lazy load this when accessed by the console.
+ return serializeDeferredObject(request, value);
}
counter.objectLimit--;
+ const deferredDebugObjects = request.deferredDebugObjects;
+ if (deferredDebugObjects !== null) {
+ const deferredId = deferredDebugObjects.existing.get(value);
+ // We earlier deferred this same object. We're now going to eagerly emit it so let's emit it
+ // at the same ID that we already used to refer to it.
+ if (deferredId !== undefined) {
+ deferredDebugObjects.existing.delete(value);
+ deferredDebugObjects.retained.delete(deferredId);
+ emitOutlinedDebugModelChunk(request, deferredId, counter, value);
+ return serializeByValueID(deferredId);
+ }
+ }
+
switch ((value: any).$$typeof) {
case REACT_ELEMENT_TYPE: {
const element: ReactElement = (value: any);
@@ -4235,6 +4281,13 @@ function renderDebugModel(
}
}
if (value.length >= 1024) {
+ // Large strings are counted towards the object limit.
+ if (counter.objectLimit <= 0) {
+ // We've reached our max number of objects to serialize across the wire so we serialize this
+ // as a marker so that the client can error or lazy load this when accessed by the console.
+ return serializeDeferredObject(request, value);
+ }
+ counter.objectLimit--;
// For large strings, we encode them outside the JSON payload so that we
// don't have to double encode and double parse the strings. This can also
// 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 {
fatalError(request, error);
}
}
+
+function fromHex(str: string): number {
+ return parseInt(str, 16);
+}
+
+export function resolveDebugMessage(request: Request, message: string): void {
+ if (!__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'resolveDebugMessage should never be called in production mode. This is a bug in React.',
+ );
+ }
+ const deferredDebugObjects = request.deferredDebugObjects;
+ if (deferredDebugObjects === null) {
+ throw new Error(
+ "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
+ );
+ }
+ // This function lets the client ask for more data lazily through the debug channel.
+ const command = message.charCodeAt(0);
+ const ids = message.slice(2).split(',').map(fromHex);
+ switch (command) {
+ case 82 /* "R" */:
+ // Release IDs
+ for (let i = 0; i < ids.length; i++) {
+ const id = ids[i];
+ const retainedValue = deferredDebugObjects.retained.get(id);
+ if (retainedValue !== undefined) {
+ // We're no longer blocked on this. We won't emit it.
+ request.pendingChunks--;
+ deferredDebugObjects.retained.delete(id);
+ deferredDebugObjects.existing.delete(retainedValue);
+ enqueueFlush(request);
+ }
+ }
+ break;
+ case 81 /* "Q" */:
+ // Query IDs
+ for (let i = 0; i < ids.length; i++) {
+ const id = ids[i];
+ const retainedValue = deferredDebugObjects.retained.get(id);
+ if (retainedValue !== undefined) {
+ // If we still have this object, and haven't emitted it before, emit it on the stream.
+ const counter = {objectLimit: 10};
+ emitOutlinedDebugModelChunk(request, id, counter, retainedValue);
+ enqueueFlush(request);
+ }
+ }
+ break;
+ default:
+ throw new Error(
+ 'Unknown command. The debugChannel was not wired up properly.',
+ );
+ }
+}
+
+export function closeDebugChannel(request: Request): void {
+ if (!__DEV__) {
+ // These errors should never make it into a build so we don't need to encode them in codes.json
+ // eslint-disable-next-line react-internal/prod-error-codes
+ throw new Error(
+ 'closeDebugChannel should never be called in production mode. This is a bug in React.',
+ );
+ }
+ // This clears all remaining deferred objects, potentially resulting in the completion of the Request.
+ const deferredDebugObjects = request.deferredDebugObjects;
+ if (deferredDebugObjects === null) {
+ throw new Error(
+ "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React.",
+ );
+ }
+ deferredDebugObjects.retained.forEach((value, id) => {
+ request.pendingChunks--;
+ deferredDebugObjects.retained.delete(id);
+ deferredDebugObjects.existing.delete(value);
+ });
+ enqueueFlush(request);
+}
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index ef0bad4c09833..a9867c154c66c 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -548,5 +548,7 @@
"560": "Cannot use a startGestureTransition() with a comment node root.",
"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 or 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.",
"562": "The render was aborted due to a fatal error.",
- "563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources."
+ "563": "This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.",
+ "564": "Unknown command. The debugChannel was not wired up properly.",
+ "565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React."
}