Skip to content

Commit 3fe51c9

Browse files
authored
[Flight] Use more robust web socket implementation in fixture (#34338)
The `WebSocketStream` implementation seems to be a bit unreliable. We've seen `Cannot close a ERRORED writable stream` errors when expanding the logged deep object, for example. And when reducing the fixture to a minimal app, we even get `Connection closed` errors, because the web socket connection is closed before all debug chunks are sent. We can improve the reliability of the web socket connection by using a normal `WebSocket` instance on the client, along with manually creating a `WritableStream` and a `ReadableStream` for processing the messages. As an additional benefit, the debug channel now also works in Firefox and Safari. On the server, we're simplifying the integration with the Express server a bit by utilizing the `server` property for `WebSocket.Server`, instead of the `noServer` property with the manual upgrade handling.
1 parent 4082b0e commit 3fe51c9

File tree

2 files changed

+61
-40
lines changed

2 files changed

+61
-40
lines changed

fixtures/flight/server/region.js

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,7 @@ function getDebugChannel(req) {
7474
return activeDebugChannels.get(requestId);
7575
}
7676

77-
async function renderApp(
78-
res,
79-
returnValue,
80-
formState,
81-
noCache,
82-
promiseForDebugChannel
83-
) {
77+
async function renderApp(res, returnValue, formState, noCache, debugChannel) {
8478
const {renderToPipeableStream} = await import(
8579
'react-server-dom-webpack/server'
8680
);
@@ -132,7 +126,7 @@ async function renderApp(
132126
// For client-invoked server actions we refresh the tree and return a return value.
133127
const payload = {root, returnValue, formState};
134128
const {pipe} = renderToPipeableStream(payload, moduleMap, {
135-
debugChannel: await promiseForDebugChannel,
129+
debugChannel,
136130
filterStackFrame,
137131
});
138132
pipe(res);
@@ -385,23 +379,20 @@ app.on('error', function (error) {
385379
if (process.env.NODE_ENV === 'development') {
386380
// Open a websocket server for Debug information
387381
const WebSocket = require('ws');
388-
const webSocketServer = new WebSocket.Server({noServer: true});
389-
390-
httpServer.on('upgrade', (request, socket, head) => {
391-
const DEBUG_CHANNEL_PATH = '/debug-channel?';
392-
if (request.url.startsWith(DEBUG_CHANNEL_PATH)) {
393-
const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length);
394-
const promiseForWs = new Promise(resolve => {
395-
webSocketServer.handleUpgrade(request, socket, head, ws => {
396-
ws.on('close', () => {
397-
activeDebugChannels.delete(requestId);
398-
});
399-
resolve(ws);
400-
});
401-
});
402-
activeDebugChannels.set(requestId, promiseForWs);
403-
} else {
404-
socket.destroy();
405-
}
382+
383+
const webSocketServer = new WebSocket.Server({
384+
server: httpServer,
385+
path: '/debug-channel',
386+
});
387+
388+
webSocketServer.on('connection', (ws, req) => {
389+
const url = new URL(req.url, `http://${req.headers.host}`);
390+
const requestId = url.searchParams.get('id');
391+
392+
activeDebugChannels.set(requestId, ws);
393+
394+
ws.on('close', (code, reason) => {
395+
activeDebugChannels.delete(requestId);
396+
});
406397
});
407398
}

fixtures/flight/src/index.js

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,52 @@ function findSourceMapURL(fileName) {
1414
);
1515
}
1616

17+
async function createWebSocketStream(url) {
18+
const ws = new WebSocket(url);
19+
ws.binaryType = 'arraybuffer';
20+
21+
await new Promise((resolve, reject) => {
22+
ws.addEventListener('open', resolve, {once: true});
23+
ws.addEventListener('error', reject, {once: true});
24+
});
25+
26+
const writable = new WritableStream({
27+
write(chunk) {
28+
ws.send(chunk);
29+
},
30+
close() {
31+
ws.close();
32+
},
33+
abort(reason) {
34+
ws.close(1000, reason && String(reason));
35+
},
36+
});
37+
38+
const readable = new ReadableStream({
39+
start(controller) {
40+
ws.addEventListener('message', event => {
41+
controller.enqueue(event.data);
42+
});
43+
ws.addEventListener('close', () => {
44+
controller.close();
45+
});
46+
ws.addEventListener('error', err => {
47+
controller.error(err);
48+
});
49+
},
50+
});
51+
52+
return {readable, writable};
53+
}
54+
1755
let updateRoot;
1856
async function callServer(id, args) {
1957
let response;
20-
if (
21-
process.env.NODE_ENV === 'development' &&
22-
typeof WebSocketStream === 'function'
23-
) {
58+
if (process.env.NODE_ENV === 'development') {
2459
const requestId = crypto.randomUUID();
25-
const wss = new WebSocketStream(
26-
'ws://localhost:3001/debug-channel?' + requestId
60+
const debugChannel = await createWebSocketStream(
61+
`ws://localhost:3001/debug-channel?id=${requestId}`
2762
);
28-
const debugChannel = await wss.opened;
2963
response = createFromFetch(
3064
fetch('/', {
3165
method: 'POST',
@@ -74,15 +108,11 @@ function Shell({data}) {
74108

75109
async function hydrateApp() {
76110
let response;
77-
if (
78-
process.env.NODE_ENV === 'development' &&
79-
typeof WebSocketStream === 'function'
80-
) {
111+
if (process.env.NODE_ENV === 'development') {
81112
const requestId = crypto.randomUUID();
82-
const wss = new WebSocketStream(
83-
'ws://localhost:3001/debug-channel?' + requestId
113+
const debugChannel = await createWebSocketStream(
114+
`ws://localhost:3001/debug-channel?id=${requestId}`
84115
);
85-
const debugChannel = await wss.opened;
86116
response = createFromFetch(
87117
fetch('/', {
88118
headers: {

0 commit comments

Comments
 (0)