From 0492e4328f6600b30d0a061dcef15241ec470e28 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 22 Jun 2025 14:14:03 -0400 Subject: [PATCH 1/6] Add Debug Channel to Server --- .../__tests__/ReactFlightDebugChannel-test.js | 142 ++++++++++++++++ .../react-markup/src/ReactMarkupServer.js | 1 + .../src/ReactNoopFlightServer.js | 7 + .../src/server/ReactFlightDOMServerNode.js | 2 + .../src/server/ReactFlightDOMServerBrowser.js | 2 + .../src/server/ReactFlightDOMServerEdge.js | 2 + .../src/server/ReactFlightDOMServerNode.js | 4 + .../src/server/ReactFlightDOMServerBrowser.js | 2 + .../src/server/ReactFlightDOMServerEdge.js | 2 + .../src/server/ReactFlightDOMServerNode.js | 4 + .../src/server/ReactFlightDOMServerBrowser.js | 2 + .../src/server/ReactFlightDOMServerEdge.js | 2 + .../src/server/ReactFlightDOMServerNode.js | 4 + .../react-server/src/ReactFlightServer.js | 156 ++++++++++++++++-- scripts/error-codes/codes.json | 4 +- 15 files changed, 323 insertions(+), 13 deletions(-) create mode 100644 packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js 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..07ef07b0c7169 --- /dev/null +++ b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js @@ -0,0 +1,142 @@ +/** + * 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); + 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); + }); + + await act(async () => { + // Ask for more data + debugChannel.onMessage('Q:7'); + }); + + 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/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/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index a9c978b493012..5a75f7213ba4c 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js @@ -95,6 +95,7 @@ function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); let hasStartedFlowing = false; startWork(request); @@ -184,6 +185,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; diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js index 73a8741618213..f1e0738820d6c 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js @@ -70,6 +70,7 @@ export function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -144,6 +145,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..beab7ed986273 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js @@ -75,6 +75,7 @@ export function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -149,6 +150,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..bcc076ef3acdb 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js @@ -106,6 +106,7 @@ export function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); let hasStartedFlowing = false; startWork(request); @@ -183,6 +184,7 @@ export function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -275,6 +277,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; @@ -338,6 +341,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/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index 11dbe1a7c1358..1a6e9a3fdda27 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js @@ -68,6 +68,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -143,6 +144,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..3d15f3ee4755f 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -73,6 +73,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -148,6 +149,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..98fc5347d183d 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -101,6 +101,7 @@ function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); let hasStartedFlowing = false; startWork(request); @@ -178,6 +179,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -271,6 +273,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 +337,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/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index 7954417b95a25..ca5346332a7c0 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js @@ -68,6 +68,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -143,6 +144,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..65d4995fe467a 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -73,6 +73,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -145,6 +146,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..1687829a275a0 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -101,6 +101,7 @@ function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); let hasStartedFlowing = false; startWork(request); @@ -178,6 +179,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, + false, // TODO ); if (options && options.signal) { const signal = options.signal; @@ -271,6 +273,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 +337,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..cabbd92fc73b5 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 retain 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 thiswhen 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 thiswhen 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 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 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..ef1bfbf574d3b 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 not be called for a Request that wasn't kept alive. This is a bug in React." } From da28bd9166175432b53a38f8b4c7bc909ee2edb6 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 22 Jun 2025 23:33:34 -0400 Subject: [PATCH 2/6] Add Debug Channel to Client For now it just immediately releases objects that are decoded. --- .../react-client/src/ReactFlightClient.js | 36 +++++++++++++++---- .../__tests__/ReactFlightDebugChannel-test.js | 9 ++--- .../src/ReactNoopFlightClient.js | 4 +++ 3 files changed, 37 insertions(+), 12 deletions(-) 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 index 07ef07b0c7169..e9428c3ba4074 100644 --- a/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js +++ b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js @@ -124,7 +124,9 @@ describe('ReactFlight', () => { ); await act(async () => { - const rootModel = await ReactNoopFlightClient.read(transport); + const rootModel = await ReactNoopFlightClient.read(transport, { + debugChannel, + }); const root = rootModel.root; const children = getDebugInfo(root)[1].props.children; expect(children.type).toBe('div'); @@ -132,11 +134,6 @@ describe('ReactFlight', () => { ReactNoop.render(root); }); - await act(async () => { - // Ask for more data - debugChannel.onMessage('Q:7'); - }); - expect(ReactNoop).toMatchRenderedOutput(
not using props
); }); }); 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); From 89f0014cfdc5153859d2ee702944ae4acb5a3639 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 22 Jun 2025 23:57:34 -0400 Subject: [PATCH 3/6] Wire up bindings for Servers The API is prepared to accept duplex streams to allow debug info to be sent through this channel. Add WebSocket support to the Server Bindings This is a slightly different protocol than Duplex but likely to be commonly used. --- .../src/server/ReactFlightDOMServerNode.js | 83 ++++++++++- .../src/server/ReactFlightDOMServerBrowser.js | 67 ++++++++- .../src/server/ReactFlightDOMServerEdge.js | 67 ++++++++- .../src/server/ReactFlightDOMServerNode.js | 136 +++++++++++++++++- .../src/server/ReactFlightDOMServerBrowser.js | 67 ++++++++- .../src/server/ReactFlightDOMServerEdge.js | 67 ++++++++- .../src/server/ReactFlightDOMServerNode.js | 131 ++++++++++++++++- .../src/server/ReactFlightDOMServerBrowser.js | 67 ++++++++- .../src/server/ReactFlightDOMServerEdge.js | 64 ++++++++- .../src/server/ReactFlightDOMServerNode.js | 131 ++++++++++++++++- 10 files changed, 838 insertions(+), 42 deletions(-) diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index 5a75f7213ba4c..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,10 +168,13 @@ function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannel !== undefined, ); let hasStartedFlowing = false; startWork(request); + if (debugChannel !== undefined) { + startReadingFromDebugChannelReadable(request, debugChannel); + } return { pipe(destination: T): T { if (hasStartedFlowing) { @@ -127,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() { @@ -289,8 +366,8 @@ function decodeReply( export { renderToPipeableStream, prerenderToNodeStream, - decodeReplyFromBusboy, decodeReply, + decodeReplyFromBusboy, decodeAction, decodeFormState, }; diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js index f1e0738820d6c..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,7 +127,7 @@ export function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -84,6 +141,9 @@ export function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } const stream = new ReadableStream( { type: 'bytes', @@ -118,9 +178,6 @@ export function prerender( const stream = new ReadableStream( { type: 'bytes', - start: (controller): ?Promise => { - startWork(request); - }, pull: (controller): ?Promise => { startFlowing(request, controller); }, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js index beab7ed986273..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,7 +132,7 @@ export function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -89,6 +146,9 @@ export function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } const stream = new ReadableStream( { type: 'bytes', @@ -123,9 +183,6 @@ export function prerender( const stream = new ReadableStream( { type: 'bytes', - start: (controller): ?Promise => { - startWork(request); - }, pull: (controller): ?Promise => { startFlowing(request, controller); }, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js index bcc076ef3acdb..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,10 +180,13 @@ export function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannel !== undefined, ); let hasStartedFlowing = false; startWork(request); + if (debugChannel !== undefined) { + startReadingFromDebugChannelReadable(request, debugChannel); + } return { pipe(destination: T): T { if (hasStartedFlowing) { @@ -150,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() { @@ -168,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, @@ -184,7 +306,7 @@ export function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -198,6 +320,9 @@ export function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } let writable: Writable; const stream = new ReadableStream( { @@ -299,7 +424,6 @@ export function prerenderToNodeStream( export function prerender( model: ReactClientValue, - options?: Options & { signal?: AbortSignal, }, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index 1a6e9a3fdda27..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,7 +125,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -82,6 +139,9 @@ function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } const stream = new ReadableStream( { type: 'bytes', @@ -117,9 +177,6 @@ function prerender( const stream = new ReadableStream( { type: 'bytes', - start: (controller): ?Promise => { - startWork(request); - }, pull: (controller): ?Promise => { startFlowing(request, controller); }, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index 3d15f3ee4755f..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,7 +130,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -87,6 +144,9 @@ function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } const stream = new ReadableStream( { type: 'bytes', @@ -122,9 +182,6 @@ function prerender( const stream = new ReadableStream( { type: 'bytes', - start: (controller): ?Promise => { - startWork(request); - }, pull: (controller): ?Promise => { startFlowing(request, controller); }, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index 98fc5347d183d..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,10 +174,13 @@ function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannel !== undefined, ); let hasStartedFlowing = false; startWork(request); + if (debugChannel !== undefined) { + startReadingFromDebugChannelReadable(request, debugChannel); + } return { pipe(destination: T): T { if (hasStartedFlowing) { @@ -163,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, @@ -179,7 +301,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -193,6 +315,9 @@ function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } let writable: Writable; const stream = new ReadableStream( { diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index ca5346332a7c0..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,7 +125,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -82,6 +139,9 @@ function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } const stream = new ReadableStream( { type: 'bytes', @@ -117,9 +177,6 @@ function prerender( const stream = new ReadableStream( { type: 'bytes', - start: (controller): ?Promise => { - startWork(request); - }, pull: (controller): ?Promise => { startFlowing(request, controller); }, diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 65d4995fe467a..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,7 +130,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -87,6 +144,9 @@ function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } const stream = new ReadableStream( { type: 'bytes', diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index 1687829a275a0..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,10 +174,13 @@ function renderToPipeableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannel !== undefined, ); let hasStartedFlowing = false; startWork(request); + if (debugChannel !== undefined) { + startReadingFromDebugChannelReadable(request, debugChannel); + } return { pipe(destination: T): T { if (hasStartedFlowing) { @@ -163,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, @@ -179,7 +301,7 @@ function renderToReadableStream( options ? options.temporaryReferences : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, - false, // TODO + debugChannelReadable !== undefined, ); if (options && options.signal) { const signal = options.signal; @@ -193,6 +315,9 @@ function renderToReadableStream( signal.addEventListener('abort', listener); } } + if (debugChannelReadable !== undefined) { + startReadingFromDebugChannelReadableStream(request, debugChannelReadable); + } let writable: Writable; const stream = new ReadableStream( { From c6c224de512bcdf8e866e168f4b15bb7f7285f72 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 23 Jun 2025 01:27:31 -0400 Subject: [PATCH 4/6] Wire up bindings for Browser Clients --- .../src/client/ReactFlightDOMClientBrowser.js | 26 ++++++++++++++ .../src/client/ReactFlightDOMClientBrowser.js | 36 ++++++++++++++++++- .../src/client/ReactFlightDOMClientBrowser.js | 26 ++++++++++++++ .../src/client/ReactFlightDOMClientBrowser.js | 26 ++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) 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-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-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-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, ); } From 87861550ffda60666145729ab31e005b68ca8ba2 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 23 Jun 2025 15:03:07 -0400 Subject: [PATCH 5/6] Wire up in fixture --- fixtures/flight/server/global.js | 3 ++ fixtures/flight/server/region.js | 60 ++++++++++++++++++++++++++++---- fixtures/flight/src/App.js | 1 - fixtures/flight/src/index.js | 48 +++++++++++++++++++------ 4 files changed, 93 insertions(+), 19 deletions(-) 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, From 598ff083a964969c742868b5769ebb3b21d6ff78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 24 Jun 2025 10:37:39 -0400 Subject: [PATCH 6/6] Typos Co-authored-by: Hendrik Liebau --- packages/react-server/src/ReactFlightServer.js | 10 +++++----- scripts/error-codes/codes.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index cabbd92fc73b5..df309ac856b25 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2358,7 +2358,7 @@ function serializeDeferredObject( 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 retain it. + // This keeps the connection alive until we ask for it or release it. request.pendingChunks++; const id = request.nextChunkId++; deferredDebugObjects.existing.set(value, id); @@ -4091,7 +4091,7 @@ 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 or lazy load thiswhen accessed by the console. + // as a marker so that the client can error or lazy load this when accessed by the console. return serializeDeferredObject(request, value); } @@ -4284,7 +4284,7 @@ function renderDebugModel( // 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 thiswhen accessed by the console. + // as a marker so that the client can error or lazy load this when accessed by the console. return serializeDeferredObject(request, value); } counter.objectLimit--; @@ -5323,7 +5323,7 @@ export function resolveDebugMessage(request: Request, message: string): void { const deferredDebugObjects = request.deferredDebugObjects; if (deferredDebugObjects === null) { throw new Error( - "resolveDebugMessage/closeDebugChannel not be called for a Request that wasn't kept alive. This is a bug in React.", + "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. @@ -5376,7 +5376,7 @@ export function closeDebugChannel(request: Request): void { const deferredDebugObjects = request.deferredDebugObjects; if (deferredDebugObjects === null) { throw new Error( - "resolveDebugMessage/closeDebugChannel not be called for a Request that wasn't kept alive. This is a bug in React.", + "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) => { diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index ef1bfbf574d3b..a9867c154c66c 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -550,5 +550,5 @@ "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.", "564": "Unknown command. The debugChannel was not wired up properly.", - "565": "resolveDebugMessage/closeDebugChannel not be called for a Request that wasn't kept alive. This is a bug in React." + "565": "resolveDebugMessage/closeDebugChannel should not be called for a Request that wasn't kept alive. This is a bug in React." }