[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);