Skip to content

Commit dabf0a8

Browse files
committed
Fix unnecessary admin server shutdown due to brief disconnections
Without this, when the device goes to sleep, in many cases the server stops, and the client disconnects, then attempts and fails to reconnect, before the machine restarts, resulting in full disconnection and a self-shutdown of the whole session. This causes problems in HTTP Toolkit on Windows, where it can be triggered easily in normal usage. We now retry multiple times, with a brief delay after the first retry.
1 parent 19f3445 commit dabf0a8

File tree

1 file changed

+37
-22
lines changed

1 file changed

+37
-22
lines changed

src/client/admin-client.ts

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { print } from 'graphql';
1212
import { DEFAULT_ADMIN_SERVER_PORT } from "../types";
1313

1414
import { MaybePromise, RequireProps } from '../util/type-utils';
15-
import { isNode } from '../util/util';
15+
import { delay, isNode } from '../util/util';
1616
import { isErrorLike } from '../util/error';
1717
import { getDeferred } from '../util/promise';
1818

@@ -219,7 +219,7 @@ export class AdminClient<Plugins extends { [key: string]: AdminPlugin<any, any>
219219
private attachStreamWebsocket(adminSessionBaseUrl: string, targetStream: Duplex): Duplex {
220220
const adminSessionBaseWSUrl = adminSessionBaseUrl.replace(/^http/, 'ws');
221221
const wsStream = connectWebSocketStream(`${adminSessionBaseWSUrl}/stream`, {
222-
headers: this.adminClientOptions?.requestOptions?.headers // Only used in Node.js (via WS)
222+
headers: this.adminClientOptions.requestOptions?.headers // Only used in Node.js (via WS)
223223
});
224224

225225
let streamConnected = false;
@@ -246,26 +246,7 @@ export class AdminClient<Plugins extends { [key: string]: AdminPlugin<any, any>
246246
targetStream.emit('server-shutdown');
247247
} else if (streamConnected && (await this.running) === true) {
248248
console.warn('Admin client stream unexpectedly disconnected', closeEvent);
249-
250-
this.emit('stream-reconnecting');
251-
252-
// Unclean shutdown means something has gone wrong somewhere. Try to reconnect.
253-
const newStream = this.attachStreamWebsocket(adminSessionBaseUrl, targetStream);
254-
255-
new Promise((resolve, reject) => {
256-
newStream.once('connect', resolve);
257-
newStream.once('error', reject);
258-
}).then(() => {
259-
// On a successful connect, business resumes as normal.
260-
console.warn('Admin client stream reconnected');
261-
this.emit('stream-reconnected');
262-
}).catch((err) => {
263-
// On a failed reconnect, we just shut down completely.
264-
console.warn('Admin client stream reconnection failed, shutting down:', err.message);
265-
if (this.debug) console.warn(err);
266-
this.emit('stream-reconnect-failed', err);
267-
targetStream.emit('server-shutdown');
268-
});
249+
this.tryToReconnectStream(adminSessionBaseUrl, targetStream);
269250
}
270251
// If never connected successfully, we do nothing.
271252
});
@@ -280,6 +261,40 @@ export class AdminClient<Plugins extends { [key: string]: AdminPlugin<any, any>
280261
return wsStream;
281262
}
282263

264+
/**
265+
* Attempt to recreate a stream after disconnection, up to a limited number of retries. This is
266+
* different to normal connection setup, as it assumes the target stream is otherwise already
267+
* set up and active.
268+
*/
269+
private async tryToReconnectStream(adminSessionBaseUrl: string, targetStream: Duplex, retries = 3) {
270+
this.emit('stream-reconnecting');
271+
272+
// Unclean shutdown means something has gone wrong somewhere. Try to reconnect.
273+
const newStream = this.attachStreamWebsocket(adminSessionBaseUrl, targetStream);
274+
275+
new Promise((resolve, reject) => {
276+
newStream.once('connect', resolve);
277+
newStream.once('error', reject);
278+
}).then(() => {
279+
// On a successful connect, business resumes as normal.
280+
console.warn('Admin client stream reconnected');
281+
this.emit('stream-reconnected');
282+
}).catch(async (err) => {
283+
if (retries > 0) {
284+
// We delay re-retrying briefly - this helps to handle cases like the computer going
285+
// to sleep (where the server & client pause in parallel, but race to do so).
286+
await delay(50);
287+
return this.tryToReconnectStream(adminSessionBaseUrl, targetStream, retries - 1);
288+
}
289+
290+
// Otherwise, once retries have failed, we give up entirely:
291+
console.warn('Admin client stream reconnection failed, shutting down:', err.message);
292+
if (this.debug) console.warn(err);
293+
this.emit('stream-reconnect-failed', err);
294+
targetStream.emit('server-shutdown');
295+
});
296+
}
297+
283298
private openStreamToMockServer(adminSessionBaseUrl: string): Promise<Duplex> {
284299
// To allow reconnects, we need to not end the client stream when an individual web socket ends.
285300
// To make that work, we return a separate stream, which isn't directly connected to the websocket

0 commit comments

Comments
 (0)