@samitouri / QOS-React-1 / commits / eb7f8b42c9

[Flight] Add Separate Outgoing Debug Channel (#33754)

This lets us pass a writable on the server side and readable on the client side to send debug info through a separate channel so that it doesn't interfere with the main payload as much. The main payload refers to chunks defined in the debug info which means it's still blocked on it though. This ensures that the debug data has loaded by the time the value is rendered so that the next step can forward the data. This will be a bit fragile to race conditions until #33665 lands. Another follow up needed is the ability to skip the debug channel on the receiving side. Right now it'll block forever if you don't provide one since we're blocking on the debug data.

Sebastian Markbåge committed Jul 10, 2025 at 16:22 UTC eb7f8b42c92ed804bbf7f700d2bdda276d591007
25 files changed +997 -240
packages/react-client/src/ReactFlightClient.js
+38 -28
@@ -342,11 +342,6 @@ type Response = {
342 _chunks: Map<number, SomeChunk<any>>,
343 _fromJSON: (key: string, value: JSONValue) => any,
344 _stringDecoder: StringDecoder,
345 - _rowState: RowParserState,
346 - _rowID: number, // parts of a row ID parsed so far
347 - _rowTag: number, // 0 indicates that we're currently parsing the row ID
348 - _rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
349 - _buffer: Array<Uint8Array>, // chunks received so far as part of this row
345 _closed: boolean,
346 _closedReason: mixed,
347 _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
@@ -2154,11 +2149,6 @@ function ResponseInstance(
2149 this._chunks = chunks;
2150 this._stringDecoder = createStringDecoder();
2151 this._fromJSON = (null: any);
2157 - this._rowState = 0;
2158 - this._rowID = 0;
2159 - this._rowTag = 0;
2160 - this._rowLength = 0;
2161 - this._buffer = [];
2152 this._closed = false;
2153 this._closedReason = null;
2154 this._tempRefs = temporaryReferences;
@@ -2259,6 +2249,24 @@ export function createResponse(
2249 );
2250 }
2251
2252 +export type StreamState = {
2253 + _rowState: RowParserState,
2254 + _rowID: number, // parts of a row ID parsed so far
2255 + _rowTag: number, // 0 indicates that we're currently parsing the row ID
2256 + _rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
2257 + _buffer: Array<Uint8Array>, // chunks received so far as part of this row
2258 +};
2259 +
2260 +export function createStreamState(): StreamState {
2261 + return {
2262 + _rowState: 0,
2263 + _rowID: 0,
2264 + _rowTag: 0,
2265 + _rowLength: 0,
2266 + _buffer: [],
2267 + };
2268 +}
2269 +
2270 function resolveDebugHalt(response: Response, id: number): void {
2271 const chunks = response._chunks;
2272 let chunk = chunks.get(id);
@@ -3995,6 +4003,7 @@ function processFullStringRow(
4003
4004 export function processBinaryChunk(
4005 weakResponse: WeakResponse,
4006 + streamState: StreamState,
4007 chunk: Uint8Array,
4008 ): void {
4009 if (hasGCedResponse(weakResponse)) {
@@ -4003,11 +4012,11 @@ export function processBinaryChunk(
4012 }
4013 const response = unwrapWeakResponse(weakResponse);
4014 let i = 0;
4006 - let rowState = response._rowState;
4007 - let rowID = response._rowID;
4008 - let rowTag = response._rowTag;
4009 - let rowLength = response._rowLength;
4010 - const buffer = response._buffer;
4015 + let rowState = streamState._rowState;
4016 + let rowID = streamState._rowID;
4017 + let rowTag = streamState._rowTag;
4018 + let rowLength = streamState._rowLength;
4019 + const buffer = streamState._buffer;
4020 const chunkLength = chunk.length;
4021 while (i < chunkLength) {
4022 let lastIdx = -1;
@@ -4112,14 +4121,15 @@ export function processBinaryChunk(
4121 break;
4122 }
4123 }
4115 - response._rowState = rowState;
4116 - response._rowID = rowID;
4117 - response._rowTag = rowTag;
4118 - response._rowLength = rowLength;
4124 + streamState._rowState = rowState;
4125 + streamState._rowID = rowID;
4126 + streamState._rowTag = rowTag;
4127 + streamState._rowLength = rowLength;
4128 }
4129
4130 export function processStringChunk(
4131 weakResponse: WeakResponse,
4132 + streamState: StreamState,
4133 chunk: string,
4134 ): void {
4135 if (hasGCedResponse(weakResponse)) {
@@ -4136,11 +4146,11 @@ export function processStringChunk(
4146 // here. Basically, only if Flight Server gave you this string as a chunk,
4147 // you can use it here.
4148 let i = 0;
4139 - let rowState = response._rowState;
4140 - let rowID = response._rowID;
4141 - let rowTag = response._rowTag;
4142 - let rowLength = response._rowLength;
4143 - const buffer = response._buffer;
4149 + let rowState = streamState._rowState;
4150 + let rowID = streamState._rowID;
4151 + let rowTag = streamState._rowTag;
4152 + let rowLength = streamState._rowLength;
4153 + const buffer = streamState._buffer;
4154 const chunkLength = chunk.length;
4155 while (i < chunkLength) {
4156 let lastIdx = -1;
@@ -4264,10 +4274,10 @@ export function processStringChunk(
4274 );
4275 }
4276 }
4267 - response._rowState = rowState;
4268 - response._rowID = rowID;
4269 - response._rowTag = rowTag;
4270 - response._rowLength = rowLength;
4277 + streamState._rowState = rowState;
4278 + streamState._rowID = rowID;
4279 + streamState._rowTag = rowTag;
4280 + streamState._rowLength = rowLength;
4281 }
4282
4283 function parseModel<T>(response: Response, json: UninitializedModel): T {
packages/react-markup/src/ReactMarkupServer.js
+3 -1
@@ -25,6 +25,7 @@ import {
25
26 import {
27 createResponse as createFlightResponse,
28 + createStreamState as createFlightStreamState,
29 getRoot as getFlightRoot,
30 processStringChunk as processFlightStringChunk,
31 close as closeFlight,
@@ -80,10 +81,11 @@ export function experimental_renderToHTML(
81 options?: MarkupOptions,
82 ): Promise<string> {
83 return new Promise((resolve, reject) => {
84 + const streamState = createFlightStreamState();
85 const flightDestination = {
86 push(chunk: string | null): boolean {
87 if (chunk !== null) {
86 - processFlightStringChunk(flightResponse, chunk);
88 + processFlightStringChunk(flightResponse, streamState, chunk);
89 } else {
90 closeFlight(flightResponse);
91 }
packages/react-noop-renderer/src/ReactNoopFlightClient.js
+32 -30
@@ -24,35 +24,36 @@ type Source = Array<Uint8Array>;
24
25 const decoderOptions = {stream: true};
26
27 -const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({
28 - createStringDecoder() {
29 - return new TextDecoder();
30 - },
31 - readPartialStringChunk(decoder: TextDecoder, buffer: Uint8Array): string {
32 - return decoder.decode(buffer, decoderOptions);
33 - },
34 - readFinalStringChunk(decoder: TextDecoder, buffer: Uint8Array): string {
35 - return decoder.decode(buffer);
36 - },
37 - resolveClientReference(bundlerConfig: null, idx: string) {
38 - return idx;
39 - },
40 - prepareDestinationForModule(moduleLoading: null, metadata: string) {},
41 - preloadModule(idx: string) {},
42 - requireModule(idx: string) {
43 - return readModule(idx);
44 - },
45 - parseModel(response: Response, json) {
46 - return JSON.parse(json, response._fromJSON);
47 - },
48 - bindToConsole(methodName, args, badgeName) {
49 - return Function.prototype.bind.apply(
50 - // eslint-disable-next-line react-internal/no-production-logging
51 - console[methodName],
52 - [console].concat(args),
53 - );
54 - },
55 -});
27 +const {createResponse, createStreamState, processBinaryChunk, getRoot, close} =
28 + ReactFlightClient({
29 + createStringDecoder() {
30 + return new TextDecoder();
31 + },
32 + readPartialStringChunk(decoder: TextDecoder, buffer: Uint8Array): string {
33 + return decoder.decode(buffer, decoderOptions);
34 + },
35 + readFinalStringChunk(decoder: TextDecoder, buffer: Uint8Array): string {
36 + return decoder.decode(buffer);
37 + },
38 + resolveClientReference(bundlerConfig: null, idx: string) {
39 + return idx;
40 + },
41 + prepareDestinationForModule(moduleLoading: null, metadata: string) {},
42 + preloadModule(idx: string) {},
43 + requireModule(idx: string) {
44 + return readModule(idx);
45 + },
46 + parseModel(response: Response, json) {
47 + return JSON.parse(json, response._fromJSON);
48 + },
49 + bindToConsole(methodName, args, badgeName) {
50 + return Function.prototype.bind.apply(
51 + // eslint-disable-next-line react-internal/no-production-logging
52 + console[methodName],
53 + [console].concat(args),
54 + );
55 + },
56 + });
57
58 type ReadOptions = {|
59 findSourceMapURL?: FindSourceMapURLCallback,
@@ -76,8 +77,9 @@ function read<T>(source: Source, options: ReadOptions): Thenable<T> {
77 ? options.debugChannel.onMessage
78 : undefined,
79 );
80 + const streamState = createStreamState();
81 for (let i = 0; i < source.length; i++) {
80 - processBinaryChunk(response, source[i], 0);
82 + processBinaryChunk(response, streamState, source[i], 0);
83 }
84 if (options !== undefined && options.close) {
85 close(response);
packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js
+73 -6
@@ -19,9 +19,11 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
19
20 import {
21 createResponse,
22 + createStreamState,
23 getRoot,
24 reportGlobalError,
25 processBinaryChunk,
26 + processStringChunk,
27 close,
28 injectIntoDevTools,
29 } from 'react-client/src/ReactFlightClient';
@@ -44,7 +46,7 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
46 export type Options = {
47 moduleBaseURL?: string,
48 callServer?: CallServerCallback,
47 - debugChannel?: {writable?: WritableStream, ...},
49 + debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...},
50 temporaryReferences?: TemporaryReferenceSet,
51 findSourceMapURL?: FindSourceMapURLCallback,
52 replayConsoleLogs?: boolean,
@@ -96,10 +98,50 @@ function createResponseFromOptions(options: void | Options) {
98 );
99 }
100
101 +function startReadingFromUniversalStream(
102 + response: FlightResponse,
103 + stream: ReadableStream,
104 +): void {
105 + // This is the same as startReadingFromStream except this allows WebSocketStreams which
106 + // return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
107 + // always allow streams with variable chunk types.
108 + const streamState = createStreamState();
109 + const reader = stream.getReader();
110 + function progress({
111 + done,
112 + value,
113 + }: {
114 + done: boolean,
115 + value: any,
116 + ...
117 + }): void | Promise<void> {
118 + if (done) {
119 + close(response);
120 + return;
121 + }
122 + if (value instanceof ArrayBuffer) {
123 + // WebSockets can produce ArrayBuffer values in ReadableStreams.
124 + processBinaryChunk(response, streamState, new Uint8Array(value));
125 + } else if (typeof value === 'string') {
126 + // WebSockets can produce string values in ReadableStreams.
127 + processStringChunk(response, streamState, value);
128 + } else {
129 + processBinaryChunk(response, streamState, value);
130 + }
131 + return reader.read().then(progress).catch(error);
132 + }
133 + function error(e: any) {
134 + reportGlobalError(response, e);
135 + }
136 + reader.read().then(progress).catch(error);
137 +}
138 +
139 function startReadingFromStream(
140 response: FlightResponse,
141 stream: ReadableStream,
142 + isSecondaryStream: boolean,
143 ): void {
144 + const streamState = createStreamState();
145 const reader = stream.getReader();
146 function progress({
147 done,
@@ -110,11 +152,14 @@ function startReadingFromStream(
152 ...
153 }): void | Promise<void> {
154 if (done) {
113 - close(response);
155 + // If we're the secondary stream, then we don't close the response until the debug channel closes.
156 + if (!isSecondaryStream) {
157 + close(response);
158 + }
159 return;
160 }
161 const buffer: Uint8Array = (value: any);
117 - processBinaryChunk(response, buffer);
162 + processBinaryChunk(response, streamState, buffer);
163 return reader.read().then(progress).catch(error);
164 }
165 function error(e: any) {
@@ -122,13 +167,22 @@ function startReadingFromStream(
167 }
168 reader.read().then(progress).catch(error);
169 }
125 -
170 function createFromReadableStream<T>(
171 stream: ReadableStream,
172 options?: Options,
173 ): Thenable<T> {
174 const response: FlightResponse = createResponseFromOptions(options);
131 - startReadingFromStream(response, stream);
175 + if (
176 + __DEV__ &&
177 + options &&
178 + options.debugChannel &&
179 + options.debugChannel.readable
180 + ) {
181 + startReadingFromUniversalStream(response, options.debugChannel.readable);
182 + startReadingFromStream(response, stream, true);
183 + } else {
184 + startReadingFromStream(response, stream, false);
185 + }
186 return getRoot(response);
187 }
188
@@ -139,7 +193,20 @@ function createFromFetch<T>(
193 const response: FlightResponse = createResponseFromOptions(options);
194 promiseForResponse.then(
195 function (r) {
142 - startReadingFromStream(response, (r.body: any));
196 + if (
197 + __DEV__ &&
198 + options &&
199 + options.debugChannel &&
200 + options.debugChannel.readable
201 + ) {
202 + startReadingFromUniversalStream(
203 + response,
204 + options.debugChannel.readable,
205 + );
206 + startReadingFromStream(response, (r.body: any), true);
207 + } else {
208 + startReadingFromStream(response, (r.body: any), false);
209 + }
210 },
211 function (e) {
212 reportGlobalError(response, e);
packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js
+8 -1
@@ -18,8 +18,10 @@ import type {Readable} from 'stream';
18
19 import {
20 createResponse,
21 + createStreamState,
22 getRoot,
23 reportGlobalError,
24 + processStringChunk,
25 processBinaryChunk,
26 close,
27 } from 'react-client/src/ReactFlightClient';
@@ -78,8 +80,13 @@ function createFromNodeStream<T>(
80 ? options.environmentName
81 : undefined,
82 );
83 + const streamState = createStreamState();
84 stream.on('data', chunk => {
82 - processBinaryChunk(response, chunk);
85 + if (typeof chunk === 'string') {
86 + processStringChunk(response, streamState, chunk);
87 + } else {
88 + processBinaryChunk(response, streamState, chunk);
89 + }
90 });
91 stream.on('error', error => {
92 reportGlobalError(response, error);
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+54 -7
@@ -27,6 +27,7 @@ import {
27 createPrerenderRequest,
28 startWork,
29 startFlowing,
30 + startFlowingDebug,
31 stopFlowing,
32 abort,
33 resolveDebugMessage,
@@ -139,7 +140,7 @@ function startReadingFromDebugChannelReadable(
140 }
141
142 type Options = {
142 - debugChannel?: Readable | Duplex | WebSocket,
143 + debugChannel?: Readable | Writable | Duplex | WebSocket,
144 environmentName?: string | (() => string),
145 filterStackFrame?: (url: string, functionName: string) => boolean,
146 onError?: (error: mixed) => void,
@@ -159,6 +160,24 @@ function renderToPipeableStream(
160 options?: Options,
161 ): PipeableStream {
162 const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
163 + const debugChannelReadable: void | Readable | WebSocket =
164 + __DEV__ &&
165 + debugChannel !== undefined &&
166 + // $FlowFixMe[method-unbinding]
167 + (typeof debugChannel.read === 'function' ||
168 + typeof debugChannel.readyState === 'number')
169 + ? (debugChannel: any)
170 + : undefined;
171 + const debugChannelWritable: void | Writable =
172 + __DEV__ && debugChannel !== undefined
173 + ? // $FlowFixMe[method-unbinding]
174 + typeof debugChannel.write === 'function'
175 + ? (debugChannel: any)
176 + : // $FlowFixMe[method-unbinding]
177 + typeof debugChannel.send === 'function'
178 + ? createFakeWritableFromWebSocket((debugChannel: any))
179 + : undefined
180 + : undefined;
181 const request = createRequest(
182 model,
183 moduleBasePath,
@@ -172,8 +191,11 @@ function renderToPipeableStream(
191 );
192 let hasStartedFlowing = false;
193 startWork(request);
175 - if (debugChannel !== undefined) {
176 - startReadingFromDebugChannelReadable(request, debugChannel);
194 + if (debugChannelWritable !== undefined) {
195 + startFlowingDebug(request, debugChannelWritable);
196 + }
197 + if (debugChannelReadable !== undefined) {
198 + startReadingFromDebugChannelReadable(request, debugChannelReadable);
199 }
200 return {
201 pipe<T: Writable>(destination: T): T {
@@ -192,10 +214,13 @@ function renderToPipeableStream(
214 'The destination stream errored while writing data.',
215 ),
216 );
195 - destination.on(
196 - 'close',
197 - createCancelHandler(request, 'The destination stream closed early.'),
198 - );
217 + // We don't close until the debug channel closes.
218 + if (!__DEV__ || debugChannelReadable === undefined) {
219 + destination.on(
220 + 'close',
221 + createCancelHandler(request, 'The destination stream closed early.'),
222 + );
223 + }
224 return destination;
225 },
226 abort(reason: mixed) {
@@ -204,6 +229,28 @@ function renderToPipeableStream(
229 };
230 }
231
232 +function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
233 + return ({
234 + write(chunk: string | Uint8Array) {
235 + webSocket.send((chunk: any));
236 + return true;
237 + },
238 + end() {
239 + webSocket.close();
240 + },
241 + destroy(reason) {
242 + if (typeof reason === 'object' && reason !== null) {
243 + reason = reason.message;
244 + }
245 + if (typeof reason === 'string') {
246 + webSocket.close(1011, reason);
247 + } else {
248 + webSocket.close(1011);
249 + }
250 + },
251 + }: any);
252 +}
253 +
254 function createFakeWritable(readable: any): Writable {
255 // The current host config expects a Writable so we create
256 // a fake writable for now to push into the Readable.
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js
+73 -5
@@ -17,9 +17,11 @@ import type {ServerReferenceId} from '../client/ReactFlightClientConfigBundlerPa
17
18 import {
19 createResponse,
20 + createStreamState,
21 getRoot,
22 reportGlobalError,
23 processBinaryChunk,
24 + processStringChunk,
25 close,
26 injectIntoDevTools,
27 } from 'react-client/src/ReactFlightClient';
@@ -97,10 +99,50 @@ function createDebugCallbackFromWritableStream(
99 };
100 }
101
102 +function startReadingFromUniversalStream(
103 + response: FlightResponse,
104 + stream: ReadableStream,
105 +): void {
106 + // This is the same as startReadingFromStream except this allows WebSocketStreams which
107 + // return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
108 + // always allow streams with variable chunk types.
109 + const streamState = createStreamState();
110 + const reader = stream.getReader();
111 + function progress({
112 + done,
113 + value,
114 + }: {
115 + done: boolean,
116 + value: any,
117 + ...
118 + }): void | Promise<void> {
119 + if (done) {
120 + close(response);
121 + return;
122 + }
123 + if (value instanceof ArrayBuffer) {
124 + // WebSockets can produce ArrayBuffer values in ReadableStreams.
125 + processBinaryChunk(response, streamState, new Uint8Array(value));
126 + } else if (typeof value === 'string') {
127 + // WebSockets can produce string values in ReadableStreams.
128 + processStringChunk(response, streamState, value);
129 + } else {
130 + processBinaryChunk(response, streamState, value);
131 + }
132 + return reader.read().then(progress).catch(error);
133 + }
134 + function error(e: any) {
135 + reportGlobalError(response, e);
136 + }
137 + reader.read().then(progress).catch(error);
138 +}
139 +
140 function startReadingFromStream(
141 response: FlightResponse,
142 stream: ReadableStream,
143 + isSecondaryStream: boolean,
144 ): void {
145 + const streamState = createStreamState();
146 const reader = stream.getReader();
147 function progress({
148 done,
@@ -111,11 +153,14 @@ function startReadingFromStream(
153 ...
154 }): void | Promise<void> {
155 if (done) {
114 - close(response);
156 + // If we're the secondary stream, then we don't close the response until the debug channel closes.
157 + if (!isSecondaryStream) {
158 + close(response);
159 + }
160 return;
161 }
162 const buffer: Uint8Array = (value: any);
118 - processBinaryChunk(response, buffer);
163 + processBinaryChunk(response, streamState, buffer);
164 return reader.read().then(progress).catch(error);
165 }
166 function error(e: any) {
@@ -125,7 +170,7 @@ function startReadingFromStream(
170 }
171
172 export type Options = {
128 - debugChannel?: {writable?: WritableStream, ...},
173 + debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...},
174 temporaryReferences?: TemporaryReferenceSet,
175 replayConsoleLogs?: boolean,
176 environmentName?: string,
@@ -157,7 +202,17 @@ export function createFromReadableStream<T>(
202 ? createDebugCallbackFromWritableStream(options.debugChannel.writable)
203 : undefined,
204 );
160 - startReadingFromStream(response, stream);
205 + if (
206 + __DEV__ &&
207 + options &&
208 + options.debugChannel &&
209 + options.debugChannel.readable
210 + ) {
211 + startReadingFromUniversalStream(response, options.debugChannel.readable);
212 + startReadingFromStream(response, stream, true);
213 + } else {
214 + startReadingFromStream(response, stream, false);
215 + }
216 return getRoot(response);
217 }
218
@@ -189,7 +244,20 @@ export function createFromFetch<T>(
244 );
245 promiseForResponse.then(
246 function (r) {
192 - startReadingFromStream(response, (r.body: any));
247 + if (
248 + __DEV__ &&
249 + options &&
250 + options.debugChannel &&
251 + options.debugChannel.readable
252 + ) {
253 + startReadingFromUniversalStream(
254 + response,
255 + options.debugChannel.readable,
256 + );
257 + startReadingFromStream(response, (r.body: any), true);
258 + } else {
259 + startReadingFromStream(response, (r.body: any), false);
260 + }
261 },
262 function (e) {
263 reportGlobalError(response, e);
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js
+3 -1
@@ -14,6 +14,7 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
14
15 import {
16 createResponse,
17 + createStreamState,
18 getRoot,
19 reportGlobalError,
20 processBinaryChunk,
@@ -100,6 +101,7 @@ function startReadingFromStream(
101 response: FlightResponse,
102 stream: ReadableStream,
103 ): void {
104 + const streamState = createStreamState();
105 const reader = stream.getReader();
106 function progress({
107 done,
@@ -114,7 +116,7 @@ function startReadingFromStream(
116 return;
117 }
118 const buffer: Uint8Array = (value: any);
117 - processBinaryChunk(response, buffer);
119 + processBinaryChunk(response, streamState, buffer);
120 return reader.read().then(progress).catch(error);
121 }
122 function error(e: any) {
packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js
+8 -1
@@ -13,8 +13,10 @@ import type {Readable} from 'stream';
13
14 import {
15 createResponse,
16 + createStreamState,
17 getRoot,
18 reportGlobalError,
19 + processStringChunk,
20 processBinaryChunk,
21 close,
22 } from 'react-client/src/ReactFlightClient';
@@ -70,8 +72,13 @@ export function createFromNodeStream<T>(
72 ? options.environmentName
73 : undefined,
74 );
75 + const streamState = createStreamState();
76 stream.on('data', chunk => {
74 - processBinaryChunk(response, chunk);
77 + if (typeof chunk === 'string') {
78 + processStringChunk(response, streamState, chunk);
79 + } else {
80 + processBinaryChunk(response, streamState, chunk);
81 + }
82 });
83 stream.on('error', error => {
84 reportGlobalError(response, error);
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
+19 -1
@@ -25,6 +25,7 @@ import {
25 createPrerenderRequest,
26 startWork,
27 startFlowing,
28 + startFlowingDebug,
29 stopFlowing,
30 abort,
31 resolveDebugMessage,
@@ -59,7 +60,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
60 export type {TemporaryReferenceSet};
61
62 type Options = {
62 - debugChannel?: {readable?: ReadableStream, ...},
63 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
64 environmentName?: string | (() => string),
65 filterStackFrame?: (url: string, functionName: string) => boolean,
66 identifierPrefix?: string,
@@ -118,6 +119,10 @@ export function renderToReadableStream(
119 __DEV__ && options && options.debugChannel
120 ? options.debugChannel.readable
121 : undefined;
122 + const debugChannelWritable =
123 + __DEV__ && options && options.debugChannel
124 + ? options.debugChannel.writable
125 + : undefined;
126 const request = createRequest(
127 model,
128 null,
@@ -141,6 +146,19 @@ export function renderToReadableStream(
146 signal.addEventListener('abort', listener);
147 }
148 }
149 + if (debugChannelWritable !== undefined) {
150 + const debugStream = new ReadableStream(
151 + {
152 + type: 'bytes',
153 + pull: (controller): ?Promise<void> => {
154 + startFlowingDebug(request, controller);
155 + },
156 + },
157 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
158 + {highWaterMark: 0},
159 + );
160 + debugStream.pipeTo(debugChannelWritable);
161 + }
162 if (debugChannelReadable !== undefined) {
163 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
164 }
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
+19 -1
@@ -27,6 +27,7 @@ import {
27 createPrerenderRequest,
28 startWork,
29 startFlowing,
30 + startFlowingDebug,
31 stopFlowing,
32 abort,
33 resolveDebugMessage,
@@ -64,7 +65,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
65 export type {TemporaryReferenceSet};
66
67 type Options = {
67 - debugChannel?: {readable?: ReadableStream, ...},
68 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
69 environmentName?: string | (() => string),
70 filterStackFrame?: (url: string, functionName: string) => boolean,
71 identifierPrefix?: string,
@@ -123,6 +124,10 @@ export function renderToReadableStream(
124 __DEV__ && options && options.debugChannel
125 ? options.debugChannel.readable
126 : undefined;
127 + const debugChannelWritable =
128 + __DEV__ && options && options.debugChannel
129 + ? options.debugChannel.writable
130 + : undefined;
131 const request = createRequest(
132 model,
133 null,
@@ -146,6 +151,19 @@ export function renderToReadableStream(
151 signal.addEventListener('abort', listener);
152 }
153 }
154 + if (debugChannelWritable !== undefined) {
155 + const debugStream = new ReadableStream(
156 + {
157 + type: 'bytes',
158 + pull: (controller): ?Promise<void> => {
159 + startFlowingDebug(request, controller);
160 + },
161 + },
162 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
163 + {highWaterMark: 0},
164 + );
165 + debugStream.pipeTo(debugChannelWritable);
166 + }
167 if (debugChannelReadable !== undefined) {
168 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
169 }
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
+77 -8
@@ -31,6 +31,7 @@ import {
31 createPrerenderRequest,
32 startWork,
33 startFlowing,
34 + startFlowingDebug,
35 stopFlowing,
36 abort,
37 resolveDebugMessage,
@@ -152,7 +153,7 @@ function startReadingFromDebugChannelReadable(
153 }
154
155 type Options = {
155 - debugChannel?: Readable | Duplex | WebSocket,
156 + debugChannel?: Readable | Writable | Duplex | WebSocket,
157 environmentName?: string | (() => string),
158 filterStackFrame?: (url: string, functionName: string) => boolean,
159 onError?: (error: mixed) => void,
@@ -171,6 +172,24 @@ export function renderToPipeableStream(
172 options?: Options,
173 ): PipeableStream {
174 const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
175 + const debugChannelReadable: void | Readable | WebSocket =
176 + __DEV__ &&
177 + debugChannel !== undefined &&
178 + // $FlowFixMe[method-unbinding]
179 + (typeof debugChannel.read === 'function' ||
180 + typeof debugChannel.readyState === 'number')
181 + ? (debugChannel: any)
182 + : undefined;
183 + const debugChannelWritable: void | Writable =
184 + __DEV__ && debugChannel !== undefined
185 + ? // $FlowFixMe[method-unbinding]
186 + typeof debugChannel.write === 'function'
187 + ? (debugChannel: any)
188 + : // $FlowFixMe[method-unbinding]
189 + typeof debugChannel.send === 'function'
190 + ? createFakeWritableFromWebSocket((debugChannel: any))
191 + : undefined
192 + : undefined;
193 const request = createRequest(
194 model,
195 null,
@@ -184,8 +203,11 @@ export function renderToPipeableStream(
203 );
204 let hasStartedFlowing = false;
205 startWork(request);
187 - if (debugChannel !== undefined) {
188 - startReadingFromDebugChannelReadable(request, debugChannel);
206 + if (debugChannelWritable !== undefined) {
207 + startFlowingDebug(request, debugChannelWritable);
208 + }
209 + if (debugChannelReadable !== undefined) {
210 + startReadingFromDebugChannelReadable(request, debugChannelReadable);
211 }
212 return {
213 pipe<T: Writable>(destination: T): T {
@@ -204,10 +226,13 @@ export function renderToPipeableStream(
226 'The destination stream errored while writing data.',
227 ),
228 );
207 - destination.on(
208 - 'close',
209 - createCancelHandler(request, 'The destination stream closed early.'),
210 - );
229 + // We don't close until the debug channel closes.
230 + if (!__DEV__ || debugChannelReadable === undefined) {
231 + destination.on(
232 + 'close',
233 + createCancelHandler(request, 'The destination stream closed early.'),
234 + );
235 + }
236 return destination;
237 },
238 abort(reason: mixed) {
@@ -216,6 +241,28 @@ export function renderToPipeableStream(
241 };
242 }
243
244 +function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
245 + return ({
246 + write(chunk: string | Uint8Array) {
247 + webSocket.send((chunk: any));
248 + return true;
249 + },
250 + end() {
251 + webSocket.close();
252 + },
253 + destroy(reason) {
254 + if (typeof reason === 'object' && reason !== null) {
255 + reason = reason.message;
256 + }
257 + if (typeof reason === 'string') {
258 + webSocket.close(1011, reason);
259 + } else {
260 + webSocket.close(1011);
261 + }
262 + },
263 + }: any);
264 +}
265 +
266 function createFakeWritableFromReadableStreamController(
267 controller: ReadableStreamController,
268 ): Writable {
@@ -289,7 +336,7 @@ function startReadingFromDebugChannelReadableStream(
336 export function renderToReadableStream(
337 model: ReactClientValue,
338 options?: Omit<Options, 'debugChannel'> & {
292 - debugChannel?: {readable?: ReadableStream, ...},
339 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
340 signal?: AbortSignal,
341 },
342 ): ReadableStream {
@@ -297,6 +344,10 @@ export function renderToReadableStream(
344 __DEV__ && options && options.debugChannel
345 ? options.debugChannel.readable
346 : undefined;
347 + const debugChannelWritable =
348 + __DEV__ && options && options.debugChannel
349 + ? options.debugChannel.writable
350 + : undefined;
351 const request = createRequest(
352 model,
353 null,
@@ -320,6 +371,24 @@ export function renderToReadableStream(
371 signal.addEventListener('abort', listener);
372 }
373 }
374 + if (debugChannelWritable !== undefined) {
375 + let debugWritable: Writable;
376 + const debugStream = new ReadableStream(
377 + {
378 + type: 'bytes',
379 + start: (controller): ?Promise<void> => {
380 + debugWritable =
381 + createFakeWritableFromReadableStreamController(controller);
382 + },
383 + pull: (controller): ?Promise<void> => {
384 + startFlowingDebug(request, debugWritable);
385 + },
386 + },
387 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
388 + {highWaterMark: 0},
389 + );
390 + debugStream.pipeTo(debugChannelWritable);
391 + }
392 if (debugChannelReadable !== undefined) {
393 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
394 }
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js
+73 -5
@@ -19,9 +19,11 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
19
20 import {
21 createResponse,
22 + createStreamState,
23 getRoot,
24 reportGlobalError,
25 processBinaryChunk,
26 + processStringChunk,
27 close,
28 injectIntoDevTools,
29 } from 'react-client/src/ReactFlightClient';
@@ -43,7 +45,7 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
45
46 export type Options = {
47 callServer?: CallServerCallback,
46 - debugChannel?: {writable?: WritableStream, ...},
48 + debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...},
49 temporaryReferences?: TemporaryReferenceSet,
50 findSourceMapURL?: FindSourceMapURLCallback,
51 replayConsoleLogs?: boolean,
@@ -95,10 +97,50 @@ function createResponseFromOptions(options: void | Options) {
97 );
98 }
99
100 +function startReadingFromUniversalStream(
101 + response: FlightResponse,
102 + stream: ReadableStream,
103 +): void {
104 + // This is the same as startReadingFromStream except this allows WebSocketStreams which
105 + // return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
106 + // always allow streams with variable chunk types.
107 + const streamState = createStreamState();
108 + const reader = stream.getReader();
109 + function progress({
110 + done,
111 + value,
112 + }: {
113 + done: boolean,
114 + value: any,
115 + ...
116 + }): void | Promise<void> {
117 + if (done) {
118 + close(response);
119 + return;
120 + }
121 + if (value instanceof ArrayBuffer) {
122 + // WebSockets can produce ArrayBuffer values in ReadableStreams.
123 + processBinaryChunk(response, streamState, new Uint8Array(value));
124 + } else if (typeof value === 'string') {
125 + // WebSockets can produce string values in ReadableStreams.
126 + processStringChunk(response, streamState, value);
127 + } else {
128 + processBinaryChunk(response, streamState, value);
129 + }
130 + return reader.read().then(progress).catch(error);
131 + }
132 + function error(e: any) {
133 + reportGlobalError(response, e);
134 + }
135 + reader.read().then(progress).catch(error);
136 +}
137 +
138 function startReadingFromStream(
139 response: FlightResponse,
140 stream: ReadableStream,
141 + isSecondaryStream: boolean,
142 ): void {
143 + const streamState = createStreamState();
144 const reader = stream.getReader();
145 function progress({
146 done,
@@ -109,11 +151,14 @@ function startReadingFromStream(
151 ...
152 }): void | Promise<void> {
153 if (done) {
112 - close(response);
154 + // If we're the secondary stream, then we don't close the response until the debug channel closes.
155 + if (!isSecondaryStream) {
156 + close(response);
157 + }
158 return;
159 }
160 const buffer: Uint8Array = (value: any);
116 - processBinaryChunk(response, buffer);
161 + processBinaryChunk(response, streamState, buffer);
162 return reader.read().then(progress).catch(error);
163 }
164 function error(e: any) {
@@ -127,7 +172,17 @@ function createFromReadableStream<T>(
172 options?: Options,
173 ): Thenable<T> {
174 const response: FlightResponse = createResponseFromOptions(options);
130 - startReadingFromStream(response, stream);
175 + if (
176 + __DEV__ &&
177 + options &&
178 + options.debugChannel &&
179 + options.debugChannel.readable
180 + ) {
181 + startReadingFromUniversalStream(response, options.debugChannel.readable);
182 + startReadingFromStream(response, stream, true);
183 + } else {
184 + startReadingFromStream(response, stream, false);
185 + }
186 return getRoot(response);
187 }
188
@@ -138,7 +193,20 @@ function createFromFetch<T>(
193 const response: FlightResponse = createResponseFromOptions(options);
194 promiseForResponse.then(
195 function (r) {
141 - startReadingFromStream(response, (r.body: any));
196 + if (
197 + __DEV__ &&
198 + options &&
199 + options.debugChannel &&
200 + options.debugChannel.readable
201 + ) {
202 + startReadingFromUniversalStream(
203 + response,
204 + options.debugChannel.readable,
205 + );
206 + startReadingFromStream(response, (r.body: any), true);
207 + } else {
208 + startReadingFromStream(response, (r.body: any), false);
209 + }
210 },
211 function (e) {
212 reportGlobalError(response, e);
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js
+3 -1
@@ -30,6 +30,7 @@ type ServerConsumerManifest = {
30
31 import {
32 createResponse,
33 + createStreamState,
34 getRoot,
35 reportGlobalError,
36 processBinaryChunk,
@@ -104,6 +105,7 @@ function startReadingFromStream(
105 response: FlightResponse,
106 stream: ReadableStream,
107 ): void {
108 + const streamState = createStreamState();
109 const reader = stream.getReader();
110 function progress({
111 done,
@@ -118,7 +120,7 @@ function startReadingFromStream(
120 return;
121 }
122 const buffer: Uint8Array = (value: any);
121 - processBinaryChunk(response, buffer);
123 + processBinaryChunk(response, streamState, buffer);
124 return reader.read().then(progress).catch(error);
125 }
126 function error(e: any) {
packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js
+8 -1
@@ -30,8 +30,10 @@ import type {Readable} from 'stream';
30
31 import {
32 createResponse,
33 + createStreamState,
34 getRoot,
35 reportGlobalError,
36 + processStringChunk,
37 processBinaryChunk,
38 close,
39 } from 'react-client/src/ReactFlightClient';
@@ -80,8 +82,13 @@ function createFromNodeStream<T>(
82 ? options.environmentName
83 : undefined,
84 );
85 + const streamState = createStreamState();
86 stream.on('data', chunk => {
84 - processBinaryChunk(response, chunk);
87 + if (typeof chunk === 'string') {
88 + processStringChunk(response, streamState, chunk);
89 + } else {
90 + processBinaryChunk(response, streamState, chunk);
91 + }
92 });
93 stream.on('error', error => {
94 reportGlobalError(response, error);
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+19 -1
@@ -20,6 +20,7 @@ import {
20 createPrerenderRequest,
21 startWork,
22 startFlowing,
23 + startFlowingDebug,
24 stopFlowing,
25 abort,
26 resolveDebugMessage,
@@ -56,7 +57,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
57 export type {TemporaryReferenceSet};
58
59 type Options = {
59 - debugChannel?: {readable?: ReadableStream, ...},
60 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
61 environmentName?: string | (() => string),
62 filterStackFrame?: (url: string, functionName: string) => boolean,
63 identifierPrefix?: string,
@@ -116,6 +117,10 @@ function renderToReadableStream(
117 __DEV__ && options && options.debugChannel
118 ? options.debugChannel.readable
119 : undefined;
120 + const debugChannelWritable =
121 + __DEV__ && options && options.debugChannel
122 + ? options.debugChannel.writable
123 + : undefined;
124 const request = createRequest(
125 model,
126 turbopackMap,
@@ -139,6 +144,19 @@ function renderToReadableStream(
144 signal.addEventListener('abort', listener);
145 }
146 }
147 + if (debugChannelWritable !== undefined) {
148 + const debugStream = new ReadableStream(
149 + {
150 + type: 'bytes',
151 + pull: (controller): ?Promise<void> => {
152 + startFlowingDebug(request, controller);
153 + },
154 + },
155 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
156 + {highWaterMark: 0},
157 + );
158 + debugStream.pipeTo(debugChannelWritable);
159 + }
160 if (debugChannelReadable !== undefined) {
161 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
162 }
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+19 -1
@@ -22,6 +22,7 @@ import {
22 createPrerenderRequest,
23 startWork,
24 startFlowing,
25 + startFlowingDebug,
26 stopFlowing,
27 abort,
28 resolveDebugMessage,
@@ -61,7 +62,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
62 export type {TemporaryReferenceSet};
63
64 type Options = {
64 - debugChannel?: {readable?: ReadableStream, ...},
65 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
66 environmentName?: string | (() => string),
67 filterStackFrame?: (url: string, functionName: string) => boolean,
68 identifierPrefix?: string,
@@ -121,6 +122,10 @@ function renderToReadableStream(
122 __DEV__ && options && options.debugChannel
123 ? options.debugChannel.readable
124 : undefined;
125 + const debugChannelWritable =
126 + __DEV__ && options && options.debugChannel
127 + ? options.debugChannel.writable
128 + : undefined;
129 const request = createRequest(
130 model,
131 turbopackMap,
@@ -144,6 +149,19 @@ function renderToReadableStream(
149 signal.addEventListener('abort', listener);
150 }
151 }
152 + if (debugChannelWritable !== undefined) {
153 + const debugStream = new ReadableStream(
154 + {
155 + type: 'bytes',
156 + pull: (controller): ?Promise<void> => {
157 + startFlowingDebug(request, controller);
158 + },
159 + },
160 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
161 + {highWaterMark: 0},
162 + );
163 + debugStream.pipeTo(debugChannelWritable);
164 + }
165 if (debugChannelReadable !== undefined) {
166 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
167 }
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+77 -8
@@ -29,6 +29,7 @@ import {
29 createPrerenderRequest,
30 startWork,
31 startFlowing,
32 + startFlowingDebug,
33 stopFlowing,
34 abort,
35 resolveDebugMessage,
@@ -145,7 +146,7 @@ function startReadingFromDebugChannelReadable(
146 }
147
148 type Options = {
148 - debugChannel?: Readable | Duplex | WebSocket,
149 + debugChannel?: Readable | Writable | Duplex | WebSocket,
150 environmentName?: string | (() => string),
151 filterStackFrame?: (url: string, functionName: string) => boolean,
152 onError?: (error: mixed) => void,
@@ -165,6 +166,24 @@ function renderToPipeableStream(
166 options?: Options,
167 ): PipeableStream {
168 const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
169 + const debugChannelReadable: void | Readable | WebSocket =
170 + __DEV__ &&
171 + debugChannel !== undefined &&
172 + // $FlowFixMe[method-unbinding]
173 + (typeof debugChannel.read === 'function' ||
174 + typeof debugChannel.readyState === 'number')
175 + ? (debugChannel: any)
176 + : undefined;
177 + const debugChannelWritable: void | Writable =
178 + __DEV__ && debugChannel !== undefined
179 + ? // $FlowFixMe[method-unbinding]
180 + typeof debugChannel.write === 'function'
181 + ? (debugChannel: any)
182 + : // $FlowFixMe[method-unbinding]
183 + typeof debugChannel.send === 'function'
184 + ? createFakeWritableFromWebSocket((debugChannel: any))
185 + : undefined
186 + : undefined;
187 const request = createRequest(
188 model,
189 turbopackMap,
@@ -178,8 +197,11 @@ function renderToPipeableStream(
197 );
198 let hasStartedFlowing = false;
199 startWork(request);
181 - if (debugChannel !== undefined) {
182 - startReadingFromDebugChannelReadable(request, debugChannel);
200 + if (debugChannelWritable !== undefined) {
201 + startFlowingDebug(request, debugChannelWritable);
202 + }
203 + if (debugChannelReadable !== undefined) {
204 + startReadingFromDebugChannelReadable(request, debugChannelReadable);
205 }
206 return {
207 pipe<T: Writable>(destination: T): T {
@@ -198,10 +220,13 @@ function renderToPipeableStream(
220 'The destination stream errored while writing data.',
221 ),
222 );
201 - destination.on(
202 - 'close',
203 - createCancelHandler(request, 'The destination stream closed early.'),
204 - );
223 + // We don't close until the debug channel closes.
224 + if (!__DEV__ || debugChannelReadable === undefined) {
225 + destination.on(
226 + 'close',
227 + createCancelHandler(request, 'The destination stream closed early.'),
228 + );
229 + }
230 return destination;
231 },
232 abort(reason: mixed) {
@@ -210,6 +235,28 @@ function renderToPipeableStream(
235 };
236 }
237
238 +function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
239 + return ({
240 + write(chunk: string | Uint8Array) {
241 + webSocket.send((chunk: any));
242 + return true;
243 + },
244 + end() {
245 + webSocket.close();
246 + },
247 + destroy(reason) {
248 + if (typeof reason === 'object' && reason !== null) {
249 + reason = reason.message;
250 + }
251 + if (typeof reason === 'string') {
252 + webSocket.close(1011, reason);
253 + } else {
254 + webSocket.close(1011);
255 + }
256 + },
257 + }: any);
258 +}
259 +
260 function createFakeWritableFromReadableStreamController(
261 controller: ReadableStreamController,
262 ): Writable {
@@ -284,7 +331,7 @@ function renderToReadableStream(
331 model: ReactClientValue,
332 turbopackMap: ClientManifest,
333 options?: Omit<Options, 'debugChannel'> & {
287 - debugChannel?: {readable?: ReadableStream, ...},
334 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
335 signal?: AbortSignal,
336 },
337 ): ReadableStream {
@@ -292,6 +339,10 @@ function renderToReadableStream(
339 __DEV__ && options && options.debugChannel
340 ? options.debugChannel.readable
341 : undefined;
342 + const debugChannelWritable =
343 + __DEV__ && options && options.debugChannel
344 + ? options.debugChannel.writable
345 + : undefined;
346 const request = createRequest(
347 model,
348 turbopackMap,
@@ -315,6 +366,24 @@ function renderToReadableStream(
366 signal.addEventListener('abort', listener);
367 }
368 }
369 + if (debugChannelWritable !== undefined) {
370 + let debugWritable: Writable;
371 + const debugStream = new ReadableStream(
372 + {
373 + type: 'bytes',
374 + start: (controller): ?Promise<void> => {
375 + debugWritable =
376 + createFakeWritableFromReadableStreamController(controller);
377 + },
378 + pull: (controller): ?Promise<void> => {
379 + startFlowingDebug(request, debugWritable);
380 + },
381 + },
382 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
383 + {highWaterMark: 0},
384 + );
385 + debugStream.pipeTo(debugChannelWritable);
386 + }
387 if (debugChannelReadable !== undefined) {
388 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
389 }
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
+73 -5
@@ -19,9 +19,11 @@ import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
19
20 import {
21 createResponse,
22 + createStreamState,
23 getRoot,
24 reportGlobalError,
25 processBinaryChunk,
26 + processStringChunk,
27 close,
28 injectIntoDevTools,
29 } from 'react-client/src/ReactFlightClient';
@@ -43,7 +45,7 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
45
46 export type Options = {
47 callServer?: CallServerCallback,
46 - debugChannel?: {writable?: WritableStream, ...},
48 + debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...},
49 temporaryReferences?: TemporaryReferenceSet,
50 findSourceMapURL?: FindSourceMapURLCallback,
51 replayConsoleLogs?: boolean,
@@ -95,10 +97,50 @@ function createResponseFromOptions(options: void | Options) {
97 );
98 }
99
100 +function startReadingFromUniversalStream(
101 + response: FlightResponse,
102 + stream: ReadableStream,
103 +): void {
104 + // This is the same as startReadingFromStream except this allows WebSocketStreams which
105 + // return ArrayBuffer and string chunks instead of Uint8Array chunks. We could potentially
106 + // always allow streams with variable chunk types.
107 + const streamState = createStreamState();
108 + const reader = stream.getReader();
109 + function progress({
110 + done,
111 + value,
112 + }: {
113 + done: boolean,
114 + value: any,
115 + ...
116 + }): void | Promise<void> {
117 + if (done) {
118 + close(response);
119 + return;
120 + }
121 + if (value instanceof ArrayBuffer) {
122 + // WebSockets can produce ArrayBuffer values in ReadableStreams.
123 + processBinaryChunk(response, streamState, new Uint8Array(value));
124 + } else if (typeof value === 'string') {
125 + // WebSockets can produce string values in ReadableStreams.
126 + processStringChunk(response, streamState, value);
127 + } else {
128 + processBinaryChunk(response, streamState, value);
129 + }
130 + return reader.read().then(progress).catch(error);
131 + }
132 + function error(e: any) {
133 + reportGlobalError(response, e);
134 + }
135 + reader.read().then(progress).catch(error);
136 +}
137 +
138 function startReadingFromStream(
139 response: FlightResponse,
140 stream: ReadableStream,
141 + isSecondaryStream: boolean,
142 ): void {
143 + const streamState = createStreamState();
144 const reader = stream.getReader();
145 function progress({
146 done,
@@ -109,11 +151,14 @@ function startReadingFromStream(
151 ...
152 }): void | Promise<void> {
153 if (done) {
112 - close(response);
154 + // If we're the secondary stream, then we don't close the response until the debug channel closes.
155 + if (!isSecondaryStream) {
156 + close(response);
157 + }
158 return;
159 }
160 const buffer: Uint8Array = (value: any);
116 - processBinaryChunk(response, buffer);
161 + processBinaryChunk(response, streamState, buffer);
162 return reader.read().then(progress).catch(error);
163 }
164 function error(e: any) {
@@ -127,7 +172,17 @@ function createFromReadableStream<T>(
172 options?: Options,
173 ): Thenable<T> {
174 const response: FlightResponse = createResponseFromOptions(options);
130 - startReadingFromStream(response, stream);
175 + if (
176 + __DEV__ &&
177 + options &&
178 + options.debugChannel &&
179 + options.debugChannel.readable
180 + ) {
181 + startReadingFromUniversalStream(response, options.debugChannel.readable);
182 + startReadingFromStream(response, stream, true);
183 + } else {
184 + startReadingFromStream(response, stream, false);
185 + }
186 return getRoot(response);
187 }
188
@@ -138,7 +193,20 @@ function createFromFetch<T>(
193 const response: FlightResponse = createResponseFromOptions(options);
194 promiseForResponse.then(
195 function (r) {
141 - startReadingFromStream(response, (r.body: any));
196 + if (
197 + __DEV__ &&
198 + options &&
199 + options.debugChannel &&
200 + options.debugChannel.readable
201 + ) {
202 + startReadingFromUniversalStream(
203 + response,
204 + options.debugChannel.readable,
205 + );
206 + startReadingFromStream(response, (r.body: any), true);
207 + } else {
208 + startReadingFromStream(response, (r.body: any), false);
209 + }
210 },
211 function (e) {
212 reportGlobalError(response, e);
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js
+3 -1
@@ -30,6 +30,7 @@ type ServerConsumerManifest = {
30
31 import {
32 createResponse,
33 + createStreamState,
34 getRoot,
35 reportGlobalError,
36 processBinaryChunk,
@@ -104,6 +105,7 @@ function startReadingFromStream(
105 response: FlightResponse,
106 stream: ReadableStream,
107 ): void {
108 + const streamState = createStreamState();
109 const reader = stream.getReader();
110 function progress({
111 done,
@@ -118,7 +120,7 @@ function startReadingFromStream(
120 return;
121 }
122 const buffer: Uint8Array = (value: any);
121 - processBinaryChunk(response, buffer);
123 + processBinaryChunk(response, streamState, buffer);
124 return reader.read().then(progress).catch(error);
125 }
126 function error(e: any) {
packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js
+4 -2
@@ -30,6 +30,7 @@ import type {Readable} from 'stream';
30
31 import {
32 createResponse,
33 + createStreamState,
34 getRoot,
35 reportGlobalError,
36 processStringChunk,
@@ -81,11 +82,12 @@ function createFromNodeStream<T>(
82 ? options.environmentName
83 : undefined,
84 );
85 + const streamState = createStreamState();
86 stream.on('data', chunk => {
87 if (typeof chunk === 'string') {
86 - processStringChunk(response, chunk);
88 + processStringChunk(response, streamState, chunk);
89 } else {
88 - processBinaryChunk(response, chunk);
90 + processBinaryChunk(response, streamState, chunk);
91 }
92 });
93 stream.on('error', error => {
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+19 -1
@@ -20,6 +20,7 @@ import {
20 createPrerenderRequest,
21 startWork,
22 startFlowing,
23 + startFlowingDebug,
24 stopFlowing,
25 abort,
26 resolveDebugMessage,
@@ -56,7 +57,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
57 export type {TemporaryReferenceSet};
58
59 type Options = {
59 - debugChannel?: {readable?: ReadableStream, ...},
60 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
61 environmentName?: string | (() => string),
62 filterStackFrame?: (url: string, functionName: string) => boolean,
63 identifierPrefix?: string,
@@ -116,6 +117,10 @@ function renderToReadableStream(
117 __DEV__ && options && options.debugChannel
118 ? options.debugChannel.readable
119 : undefined;
120 + const debugChannelWritable =
121 + __DEV__ && options && options.debugChannel
122 + ? options.debugChannel.writable
123 + : undefined;
124 const request = createRequest(
125 model,
126 webpackMap,
@@ -139,6 +144,19 @@ function renderToReadableStream(
144 signal.addEventListener('abort', listener);
145 }
146 }
147 + if (debugChannelWritable !== undefined) {
148 + const debugStream = new ReadableStream(
149 + {
150 + type: 'bytes',
151 + pull: (controller): ?Promise<void> => {
152 + startFlowingDebug(request, controller);
153 + },
154 + },
155 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
156 + {highWaterMark: 0},
157 + );
158 + debugStream.pipeTo(debugChannelWritable);
159 + }
160 if (debugChannelReadable !== undefined) {
161 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
162 }
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+19 -1
@@ -22,6 +22,7 @@ import {
22 createPrerenderRequest,
23 startWork,
24 startFlowing,
25 + startFlowingDebug,
26 stopFlowing,
27 abort,
28 resolveDebugMessage,
@@ -61,7 +62,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
62 export type {TemporaryReferenceSet};
63
64 type Options = {
64 - debugChannel?: {readable?: ReadableStream, ...},
65 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
66 environmentName?: string | (() => string),
67 filterStackFrame?: (url: string, functionName: string) => boolean,
68 identifierPrefix?: string,
@@ -121,6 +122,10 @@ function renderToReadableStream(
122 __DEV__ && options && options.debugChannel
123 ? options.debugChannel.readable
124 : undefined;
125 + const debugChannelWritable =
126 + __DEV__ && options && options.debugChannel
127 + ? options.debugChannel.writable
128 + : undefined;
129 const request = createRequest(
130 model,
131 webpackMap,
@@ -144,6 +149,19 @@ function renderToReadableStream(
149 signal.addEventListener('abort', listener);
150 }
151 }
152 + if (debugChannelWritable !== undefined) {
153 + const debugStream = new ReadableStream(
154 + {
155 + type: 'bytes',
156 + pull: (controller): ?Promise<void> => {
157 + startFlowingDebug(request, controller);
158 + },
159 + },
160 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
161 + {highWaterMark: 0},
162 + );
163 + debugStream.pipeTo(debugChannelWritable);
164 + }
165 if (debugChannelReadable !== undefined) {
166 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
167 }
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+78 -9
@@ -29,6 +29,7 @@ import {
29 createPrerenderRequest,
30 startWork,
31 startFlowing,
32 + startFlowingDebug,
33 stopFlowing,
34 abort,
35 resolveDebugMessage,
@@ -145,7 +146,7 @@ function startReadingFromDebugChannelReadable(
146 }
147
148 type Options = {
148 - debugChannel?: Readable | Duplex | WebSocket,
149 + debugChannel?: Readable | Writable | Duplex | WebSocket,
150 environmentName?: string | (() => string),
151 filterStackFrame?: (url: string, functionName: string) => boolean,
152 onError?: (error: mixed) => void,
@@ -165,6 +166,24 @@ function renderToPipeableStream(
166 options?: Options,
167 ): PipeableStream {
168 const debugChannel = __DEV__ && options ? options.debugChannel : undefined;
169 + const debugChannelReadable: void | Readable | WebSocket =
170 + __DEV__ &&
171 + debugChannel !== undefined &&
172 + // $FlowFixMe[method-unbinding]
173 + (typeof debugChannel.read === 'function' ||
174 + typeof debugChannel.readyState === 'number')
175 + ? (debugChannel: any)
176 + : undefined;
177 + const debugChannelWritable: void | Writable =
178 + __DEV__ && debugChannel !== undefined
179 + ? // $FlowFixMe[method-unbinding]
180 + typeof debugChannel.write === 'function'
181 + ? (debugChannel: any)
182 + : // $FlowFixMe[method-unbinding]
183 + typeof debugChannel.send === 'function'
184 + ? createFakeWritableFromWebSocket((debugChannel: any))
185 + : undefined
186 + : undefined;
187 const request = createRequest(
188 model,
189 webpackMap,
@@ -174,12 +193,15 @@ function renderToPipeableStream(
193 options ? options.temporaryReferences : undefined,
194 __DEV__ && options ? options.environmentName : undefined,
195 __DEV__ && options ? options.filterStackFrame : undefined,
177 - debugChannel !== undefined,
196 + debugChannelReadable !== undefined,
197 );
198 let hasStartedFlowing = false;
199 startWork(request);
181 - if (debugChannel !== undefined) {
182 - startReadingFromDebugChannelReadable(request, debugChannel);
200 + if (debugChannelWritable !== undefined) {
201 + startFlowingDebug(request, debugChannelWritable);
202 + }
203 + if (debugChannelReadable !== undefined) {
204 + startReadingFromDebugChannelReadable(request, debugChannelReadable);
205 }
206 return {
207 pipe<T: Writable>(destination: T): T {
@@ -198,10 +220,13 @@ function renderToPipeableStream(
220 'The destination stream errored while writing data.',
221 ),
222 );
201 - destination.on(
202 - 'close',
203 - createCancelHandler(request, 'The destination stream closed early.'),
204 - );
223 + // We don't close until the debug channel closes.
224 + if (!__DEV__ || debugChannelReadable === undefined) {
225 + destination.on(
226 + 'close',
227 + createCancelHandler(request, 'The destination stream closed early.'),
228 + );
229 + }
230 return destination;
231 },
232 abort(reason: mixed) {
@@ -210,6 +235,28 @@ function renderToPipeableStream(
235 };
236 }
237
238 +function createFakeWritableFromWebSocket(webSocket: WebSocket): Writable {
239 + return ({
240 + write(chunk: string | Uint8Array) {
241 + webSocket.send((chunk: any));
242 + return true;
243 + },
244 + end() {
245 + webSocket.close();
246 + },
247 + destroy(reason) {
248 + if (typeof reason === 'object' && reason !== null) {
249 + reason = reason.message;
250 + }
251 + if (typeof reason === 'string') {
252 + webSocket.close(1011, reason);
253 + } else {
254 + webSocket.close(1011);
255 + }
256 + },
257 + }: any);
258 +}
259 +
260 function createFakeWritableFromReadableStreamController(
261 controller: ReadableStreamController,
262 ): Writable {
@@ -284,7 +331,7 @@ function renderToReadableStream(
331 model: ReactClientValue,
332 webpackMap: ClientManifest,
333 options?: Omit<Options, 'debugChannel'> & {
287 - debugChannel?: {readable?: ReadableStream, ...},
334 + debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...},
335 signal?: AbortSignal,
336 },
337 ): ReadableStream {
@@ -292,6 +339,10 @@ function renderToReadableStream(
339 __DEV__ && options && options.debugChannel
340 ? options.debugChannel.readable
341 : undefined;
342 + const debugChannelWritable =
343 + __DEV__ && options && options.debugChannel
344 + ? options.debugChannel.writable
345 + : undefined;
346 const request = createRequest(
347 model,
348 webpackMap,
@@ -315,6 +366,24 @@ function renderToReadableStream(
366 signal.addEventListener('abort', listener);
367 }
368 }
369 + if (debugChannelWritable !== undefined) {
370 + let debugWritable: Writable;
371 + const debugStream = new ReadableStream(
372 + {
373 + type: 'bytes',
374 + start: (controller): ?Promise<void> => {
375 + debugWritable =
376 + createFakeWritableFromReadableStreamController(controller);
377 + },
378 + pull: (controller): ?Promise<void> => {
379 + startFlowingDebug(request, debugWritable);
380 + },
381 + },
382 + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
383 + {highWaterMark: 0},
384 + );
385 + debugStream.pipeTo(debugChannelWritable);
386 + }
387 if (debugChannelReadable !== undefined) {
388 startReadingFromDebugChannelReadableStream(request, debugChannelReadable);
389 }
packages/react-server/src/ReactFlightServer.js
+195 -114
@@ -519,6 +519,7 @@ export type Request = {
519 // DEV-only
520 pendingDebugChunks: number,
521 completedDebugChunks: Array<Chunk | BinaryChunk>,
522 + debugDestination: null | Destination,
523 environmentName: () => string,
524 filterStackFrame: (
525 url: string,
@@ -639,6 +640,7 @@ function RequestInstance(
640 if (__DEV__) {
641 this.pendingDebugChunks = 0;
642 this.completedDebugChunks = ([]: Array<Chunk>);
643 + this.debugDestination = null;
644 this.environmentName =
645 environmentName === undefined
646 ? () => 'Server'
@@ -1519,7 +1521,7 @@ function renderFunctionComponent<Props>(
1521 const componentName =
1522 (Component: any).displayName || Component.name || '';
1523 const componentEnv = (0, request.environmentName)();
1522 - request.pendingDebugChunks++;
1524 + request.pendingChunks++;
1525 componentDebugInfo = ({
1526 name: componentName,
1527 env: componentEnv,
@@ -2274,7 +2276,7 @@ function visitAsyncNode(
2276 const env = (0, request.environmentName)();
2277 advanceTaskTime(request, task, startTime);
2278 // Then emit a reference to us awaiting it in the current task.
2277 - request.pendingDebugChunks++;
2279 + request.pendingChunks++;
2280 emitDebugChunk(request, task.id, {
2281 awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
2282 env: env,
@@ -2334,7 +2336,7 @@ function emitAsyncSequence(
2336 } else if (awaitedNode !== null) {
2337 // Nothing in user space (unfiltered stack) awaited this.
2338 serializeIONode(request, awaitedNode, awaitedNode.promise);
2337 - request.pendingDebugChunks++;
2339 + request.pendingChunks++;
2340 // We log the environment at the time when we ping which may be later than what the
2341 // environment was when we actually started awaiting.
2342 const env = (0, request.environmentName)();
@@ -4021,9 +4023,19 @@ function emitDebugChunk(
4023 }
4024
4025 const json: string = serializeDebugModel(request, 500, debugInfo);
4024 - const row = serializeRowHeader('D', id) + json + '\n';
4025 - const processedChunk = stringToChunk(row);
4026 - request.completedDebugChunks.push(processedChunk);
4026 + if (request.debugDestination !== null) {
4027 + // Outline the actual timing information to the debug channel.
4028 + const outlinedId = request.nextChunkId++;
4029 + const debugRow = outlinedId.toString(16) + ':' + json + '\n';
4030 + request.pendingDebugChunks++;
4031 + request.completedDebugChunks.push(stringToChunk(debugRow));
4032 + const row =
4033 + serializeRowHeader('D', id) + '"$' + outlinedId.toString(16) + '"\n';
4034 + request.completedRegularChunks.push(stringToChunk(row));
4035 + } else {
4036 + const row = serializeRowHeader('D', id) + json + '\n';
4037 + request.completedRegularChunks.push(stringToChunk(row));
4038 + }
4039 }
4040
4041 function outlineComponentInfo(
@@ -4941,7 +4953,7 @@ function forwardDebugInfo(
4953 // being no references to this as an owner.
4954 outlineComponentInfo(request, (info: any));
4955 // Emit a reference to the outlined one.
4944 - request.pendingDebugChunks++;
4956 + request.pendingChunks++;
4957 emitDebugChunk(request, id, info);
4958 } else if (info.awaited) {
4959 const ioInfo = info.awaited;
@@ -4982,11 +4994,11 @@ function forwardDebugInfo(
4994 // $FlowFixMe[cannot-write]
4995 debugAsyncInfo.stack = debugStack;
4996 }
4985 - request.pendingDebugChunks++;
4997 + request.pendingChunks++;
4998 emitDebugChunk(request, id, debugAsyncInfo);
4999 }
5000 } else {
4989 - request.pendingDebugChunks++;
5001 + request.pendingChunks++;
5002 emitDebugChunk(request, id, info);
5003 }
5004 }
@@ -5088,7 +5100,7 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
5100 // complete in time before aborting.
5101 // The best we can do is try to emit the stack of where this Promise was created.
5102 serializeIONode(request, node, null);
5091 - request.pendingDebugChunks++;
5103 + request.pendingChunks++;
5104 const env = (0, request.environmentName)();
5105 const asyncInfo: ReactAsyncInfo = {
5106 awaited: ((node: any): ReactIOInfo), // This is deduped by this reference.
@@ -5117,13 +5129,22 @@ function emitTimingChunk(
5129 if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
5130 return;
5131 }
5120 - request.pendingDebugChunks++;
5132 + request.pendingChunks++;
5133 const relativeTimestamp = timestamp - request.timeOrigin;
5122 - const row =
5123 - serializeRowHeader('D', id) + '{"time":' + relativeTimestamp + '}\n';
5124 - const processedChunk = stringToChunk(row);
5125 - // TODO: Move to its own priority queue.
5126 - request.completedDebugChunks.push(processedChunk);
5134 + const json = '{"time":' + relativeTimestamp + '}';
5135 + if (request.debugDestination !== null) {
5136 + // Outline the actual timing information to the debug channel.
5137 + const outlinedId = request.nextChunkId++;
5138 + const debugRow = outlinedId.toString(16) + ':' + json + '\n';
5139 + request.pendingDebugChunks++;
5140 + request.completedDebugChunks.push(stringToChunk(debugRow));
5141 + const row =
5142 + serializeRowHeader('D', id) + '"$' + outlinedId.toString(16) + '"\n';
5143 + request.completedRegularChunks.push(stringToChunk(row));
5144 + } else {
5145 + const row = serializeRowHeader('D', id) + json + '\n';
5146 + request.completedRegularChunks.push(stringToChunk(row));
5147 + }
5148 }
5149
5150 function advanceTaskTime(
@@ -5329,7 +5350,7 @@ function retryTask(request: Request, task: Task): void {
5350 if (__DEV__) {
5351 const currentEnv = (0, request.environmentName)();
5352 if (currentEnv !== task.environmentName) {
5332 - request.pendingDebugChunks++;
5353 + request.pendingChunks++;
5354 // The environment changed since we last emitted any debug information for this
5355 // task. We emit an entry that just includes the environment name change.
5356 emitDebugChunk(request, task.id, {env: currentEnv});
@@ -5444,9 +5465,7 @@ function performWork(request: Request): void {
5465 const task = pingedTasks[i];
5466 retryTask(request, task);
5467 }
5447 - if (request.destination !== null) {
5448 - flushCompletedChunks(request, request.destination);
5449 - }
5468 + flushCompletedChunks(request);
5469 } catch (error) {
5470 logRecoverableError(request, error, null);
5471 fatalError(request, error);
@@ -5507,50 +5526,49 @@ function finishHaltedTask(task: Task, request: Request): void {
5526 request.pendingChunks--;
5527 }
5528
5510 -function flushCompletedChunks(
5511 - request: Request,
5512 - destination: Destination,
5513 -): void {
5514 - beginWriting(destination);
5515 - try {
5516 - // We emit module chunks first in the stream so that
5517 - // they can be preloaded as early as possible.
5518 - const importsChunks = request.completedImportChunks;
5519 - let i = 0;
5520 - for (; i < importsChunks.length; i++) {
5521 - request.pendingChunks--;
5522 - const chunk = importsChunks[i];
5523 - const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5524 - if (!keepWriting) {
5525 - request.destination = null;
5526 - i++;
5527 - break;
5529 +function flushCompletedChunks(request: Request): void {
5530 + if (__DEV__ && request.debugDestination !== null) {
5531 + const debugDestination = request.debugDestination;
5532 + beginWriting(debugDestination);
5533 + try {
5534 + const debugChunks = request.completedDebugChunks;
5535 + let i = 0;
5536 + for (; i < debugChunks.length; i++) {
5537 + request.pendingDebugChunks--;
5538 + const chunk = debugChunks[i];
5539 + writeChunkAndReturn(debugDestination, chunk);
5540 }
5541 + debugChunks.splice(0, i);
5542 + } finally {
5543 + completeWriting(debugDestination);
5544 }
5530 - importsChunks.splice(0, i);
5531 -
5532 - // Next comes hints.
5533 - const hintChunks = request.completedHintChunks;
5534 - i = 0;
5535 - for (; i < hintChunks.length; i++) {
5536 - const chunk = hintChunks[i];
5537 - const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5538 - if (!keepWriting) {
5539 - request.destination = null;
5540 - i++;
5541 - break;
5545 + flushBuffered(debugDestination);
5546 + }
5547 + const destination = request.destination;
5548 + if (destination !== null) {
5549 + beginWriting(destination);
5550 + try {
5551 + // We emit module chunks first in the stream so that
5552 + // they can be preloaded as early as possible.
5553 + const importsChunks = request.completedImportChunks;
5554 + let i = 0;
5555 + for (; i < importsChunks.length; i++) {
5556 + request.pendingChunks--;
5557 + const chunk = importsChunks[i];
5558 + const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5559 + if (!keepWriting) {
5560 + request.destination = null;
5561 + i++;
5562 + break;
5563 + }
5564 }
5543 - }
5544 - hintChunks.splice(0, i);
5565 + importsChunks.splice(0, i);
5566
5546 - // Debug meta data comes before the model data because it will often end up blocking the model from
5547 - // completing since the JSX will reference the debug data.
5548 - if (__DEV__) {
5549 - const debugChunks = request.completedDebugChunks;
5567 + // Next comes hints.
5568 + const hintChunks = request.completedHintChunks;
5569 i = 0;
5551 - for (; i < debugChunks.length; i++) {
5552 - request.pendingDebugChunks--;
5553 - const chunk = debugChunks[i];
5570 + for (; i < hintChunks.length; i++) {
5571 + const chunk = hintChunks[i];
5572 const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5573 if (!keepWriting) {
5574 request.destination = null;
@@ -5558,49 +5576,89 @@ function flushCompletedChunks(
5576 break;
5577 }
5578 }
5561 - debugChunks.splice(0, i);
5562 - }
5579 + hintChunks.splice(0, i);
5580 +
5581 + // Debug meta data comes before the model data because it will often end up blocking the model from
5582 + // completing since the JSX will reference the debug data.
5583 + if (__DEV__ && request.debugDestination === null) {
5584 + const debugChunks = request.completedDebugChunks;
5585 + i = 0;
5586 + for (; i < debugChunks.length; i++) {
5587 + request.pendingDebugChunks--;
5588 + const chunk = debugChunks[i];
5589 + const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5590 + if (!keepWriting) {
5591 + request.destination = null;
5592 + i++;
5593 + break;
5594 + }
5595 + }
5596 + debugChunks.splice(0, i);
5597 + }
5598
5564 - // Next comes model data.
5565 - const regularChunks = request.completedRegularChunks;
5566 - i = 0;
5567 - for (; i < regularChunks.length; i++) {
5568 - request.pendingChunks--;
5569 - const chunk = regularChunks[i];
5570 - const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5571 - if (!keepWriting) {
5572 - request.destination = null;
5573 - i++;
5574 - break;
5599 + // Next comes model data.
5600 + const regularChunks = request.completedRegularChunks;
5601 + i = 0;
5602 + for (; i < regularChunks.length; i++) {
5603 + request.pendingChunks--;
5604 + const chunk = regularChunks[i];
5605 + const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5606 + if (!keepWriting) {
5607 + request.destination = null;
5608 + i++;
5609 + break;
5610 + }
5611 }
5576 - }
5577 - regularChunks.splice(0, i);
5578 -
5579 - // Finally, errors are sent. The idea is that it's ok to delay
5580 - // any error messages and prioritize display of other parts of
5581 - // the page.
5582 - const errorChunks = request.completedErrorChunks;
5583 - i = 0;
5584 - for (; i < errorChunks.length; i++) {
5585 - request.pendingChunks--;
5586 - const chunk = errorChunks[i];
5587 - const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5588 - if (!keepWriting) {
5589 - request.destination = null;
5590 - i++;
5591 - break;
5612 + regularChunks.splice(0, i);
5613 +
5614 + // Finally, errors are sent. The idea is that it's ok to delay
5615 + // any error messages and prioritize display of other parts of
5616 + // the page.
5617 + const errorChunks = request.completedErrorChunks;
5618 + i = 0;
5619 + for (; i < errorChunks.length; i++) {
5620 + request.pendingChunks--;
5621 + const chunk = errorChunks[i];
5622 + const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
5623 + if (!keepWriting) {
5624 + request.destination = null;
5625 + i++;
5626 + break;
5627 + }
5628 }
5629 + errorChunks.splice(0, i);
5630 + } finally {
5631 + request.flushScheduled = false;
5632 + completeWriting(destination);
5633 }
5594 - errorChunks.splice(0, i);
5595 - } finally {
5596 - request.flushScheduled = false;
5597 - completeWriting(destination);
5634 + flushBuffered(destination);
5635 }
5599 - flushBuffered(destination);
5600 - if (
5601 - request.pendingChunks === 0 &&
5602 - (!__DEV__ || request.pendingDebugChunks === 0)
5603 - ) {
5636 + if (request.pendingChunks === 0) {
5637 + if (__DEV__) {
5638 + const debugDestination = request.debugDestination;
5639 + if (request.pendingDebugChunks === 0) {
5640 + // Continue fully closing both streams.
5641 + if (debugDestination !== null) {
5642 + close(debugDestination);
5643 + request.debugDestination = null;
5644 + }
5645 + } else {
5646 + // We still have debug information to write.
5647 + if (debugDestination === null) {
5648 + // We'll continue writing on this stream so nothing closes.
5649 + return;
5650 + } else {
5651 + // We'll close the main stream but keep the debug stream open.
5652 + // TODO: If this destination is not currently flowing we'll not close it when it resumes flowing.
5653 + // We should keep a separate status for this.
5654 + if (request.destination !== null) {
5655 + close(request.destination);
5656 + request.destination = null;
5657 + }
5658 + return;
5659 + }
5660 + }
5661 + }
5662 // We're done.
5663 if (enableTaint) {
5664 cleanupTaintQueue(request);
@@ -5612,8 +5670,14 @@ function flushCompletedChunks(
5670 request.cacheController.abort(abortReason);
5671 }
5672 request.status = CLOSED;
5615 - close(destination);
5616 - request.destination = null;
5673 + if (request.destination !== null) {
5674 + close(request.destination);
5675 + request.destination = null;
5676 + }
5677 + if (__DEV__ && request.debugDestination !== null) {
5678 + close(request.debugDestination);
5679 + request.debugDestination = null;
5680 + }
5681 }
5682 }
5683
@@ -5640,17 +5704,15 @@ function enqueueFlush(request: Request): void {
5704 request.pingedTasks.length === 0 &&
5705 // If there is no destination there is nothing we can flush to. A flush will
5706 // happen when we start flowing again
5643 - request.destination !== null
5707 + (request.destination !== null ||
5708 + (__DEV__ && request.debugDestination !== null))
5709 ) {
5710 request.flushScheduled = true;
5711 // Unlike startWork and pingTask we intetionally use scheduleWork
5712 // here even during prerenders to allow as much batching as possible
5713 scheduleWork(() => {
5714 request.flushScheduled = false;
5650 - const destination = request.destination;
5651 - if (destination) {
5652 - flushCompletedChunks(request, destination);
5653 - }
5715 + flushCompletedChunks(request);
5716 });
5717 }
5718 }
@@ -5677,7 +5739,32 @@ export function startFlowing(request: Request, destination: Destination): void {
5739 }
5740 request.destination = destination;
5741 try {
5680 - flushCompletedChunks(request, destination);
5742 + flushCompletedChunks(request);
5743 + } catch (error) {
5744 + logRecoverableError(request, error, null);
5745 + fatalError(request, error);
5746 + }
5747 +}
5748 +
5749 +export function startFlowingDebug(
5750 + request: Request,
5751 + debugDestination: Destination,
5752 +): void {
5753 + if (request.status === CLOSING) {
5754 + request.status = CLOSED;
5755 + closeWithError(debugDestination, request.fatalError);
5756 + return;
5757 + }
5758 + if (request.status === CLOSED) {
5759 + return;
5760 + }
5761 + if (request.debugDestination !== null) {
5762 + // We're already flowing.
5763 + return;
5764 + }
5765 + request.debugDestination = debugDestination;
5766 + try {
5767 + flushCompletedChunks(request);
5768 } catch (error) {
5769 logRecoverableError(request, error, null);
5770 fatalError(request, error);
@@ -5693,9 +5780,7 @@ function finishHalt(request: Request, abortedTasks: Set<Task>): void {
5780 abortedTasks.forEach(task => finishHaltedTask(task, request));
5781 const onAllReady = request.onAllReady;
5782 onAllReady();
5696 - if (request.destination !== null) {
5697 - flushCompletedChunks(request, request.destination);
5698 - }
5783 + flushCompletedChunks(request);
5784 } catch (error) {
5785 logRecoverableError(request, error, null);
5786 fatalError(request, error);
@@ -5711,9 +5796,7 @@ function finishAbort(
5796 abortedTasks.forEach(task => finishAbortedTask(task, request, errorId));
5797 const onAllReady = request.onAllReady;
5798 onAllReady();
5714 - if (request.destination !== null) {
5715 - flushCompletedChunks(request, request.destination);
5716 - }
5799 + flushCompletedChunks(request);
5800 } catch (error) {
5801 logRecoverableError(request, error, null);
5802 fatalError(request, error);
@@ -5780,9 +5863,7 @@ export function abort(request: Request, reason: mixed): void {
5863 } else {
5864 const onAllReady = request.onAllReady;
5865 onAllReady();
5783 - if (request.destination !== null) {
5784 - flushCompletedChunks(request, request.destination);
5785 - }
5866 + flushCompletedChunks(request);
5867 }
5868 } catch (error) {
5869 logRecoverableError(request, error, null);