11diff --git a/dist/Server.js b/dist/Server.js
2- index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b4eaf1bba 100644
2+ index ad9debe2086ab9b96e97a69aec966da8114ad102..c8f4e2a1b9d3f6e8a7c5d4b2e1f0a9c8b7d6e5f4 100644
33--- a/dist/Server.js
44+++ b/dist/Server.js
5- @@ -130,6 +130,17 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
5+ @@ -73,6 +73,8 @@
6+ /** The options to send to the next connecting running client */
7+ this.clientOptions = {};
8+ this._listening = false;
9+ + this._clientResetTimer = null;
10+ + this._awaitingInitialClientRun = false;
11+ this.handleConnection = (ws, req) => {
12+ this.debug("Client connected");
13+ // Check that the protocol matches
14+ @@ -88,6 +90,11 @@
15+ ws.close(1002, `Expected "${expectedProtocol}" protocol got "${ws.protocol}"`);
16+ return;
17+ }
18+ + if (this._clientResetTimer) {
19+ + clearTimeout(this._clientResetTimer);
20+ + this._clientResetTimer = null;
21+ + console.warn(`[mocha-remote-ws] reconnect_recovered preserving_runner`);
22+ + }
23+ if (this.client) {
24+ this.debug("A client was already connected");
25+ this.client.close(1013 /* try again later */, "Got a connection from another client");
26+ @@ -97,14 +104,20 @@
27+ // Hang onto the client
28+ this.client = ws;
29+ this.client.on("message", this.handleMessage.bind(this, this.client));
30+ - this.client.once("close", this.handleReset);
31+ + this.client.once("close", (code) => this.handleClientDisconnect(code));
32+ // If we already have a runner, it can run now that we have a client
33+ if (this.runner) {
34+ - if (this.clientOptions) {
35+ - this.send({ action: "run", options: this.clientOptions });
36+ + if (this._awaitingInitialClientRun) {
37+ + if (this.clientOptions) {
38+ + this.send({ action: "run", options: this.clientOptions });
39+ + this._awaitingInitialClientRun = false;
40+ + }
41+ + else {
42+ + throw new Error("Internal error: Expected a clientOptions");
43+ + }
44+ }
45+ else {
46+ - throw new Error("Internal error: Expected a clientOptions");
47+ + this.debug("Client reconnected while runner active; resuming without re-run");
48+ }
49+ }
50+ else if (this.config.autoRun) {
51+ @@ -130,6 +143,17 @@
652 throw new Error("Received a message from the client, but server wasn't running");
753 }
854 }
@@ -20,3 +66,58 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b
2066 else if (msg.action === "error") {
2167 if (typeof msg.message !== "string") {
2268 throw new Error("Expected 'error' action to have an error argument with a message");
69+ @@ -149,13 +173,46 @@
70+ }
71+ else {
72+ throw err;
73+ + }
74+ + }
75+ + };
76+ + this.handleClientDisconnect = (code) => {
77+ + const transientCodes = [1006, 1001];
78+ + const graceMs = this.config.reconnectGraceMs ?? 15000;
79+ + if (this.runner && transientCodes.includes(code)) {
80+ + if (this._clientResetTimer) {
81+ + clearTimeout(this._clientResetTimer);
82+ }
83+ + console.warn(`[mocha-remote-ws] transient_disconnect code=${code} grace_ms=${graceMs} preserving_runner`);
84+ + const client = this.client;
85+ + delete this.client;
86+ + if (client) {
87+ + client.removeAllListeners();
88+ + }
89+ + this._clientResetTimer = setTimeout(() => {
90+ + this._clientResetTimer = null;
91+ + if (!this.client) {
92+ + console.error(`[mocha-remote-ws] fatal_disconnect code=${code} grace_expired_ms=${graceMs}`);
93+ + this.handleReset();
94+ + }
95+ + }, graceMs);
96+ + return;
97+ }
98+ + if (this._clientResetTimer) {
99+ + clearTimeout(this._clientResetTimer);
100+ + this._clientResetTimer = null;
101+ + }
102+ + this.handleReset();
103+ };
104+ /**
105+ * Resets the server for another test run.
106+ */
107+ this.handleReset = () => {
108+ + if (this._clientResetTimer) {
109+ + clearTimeout(this._clientResetTimer);
110+ + this._clientResetTimer = null;
111+ + }
112+ + this._awaitingInitialClientRun = false;
113+ // Forget everything about the runner and the client
114+ const { runner, client } = this;
115+ delete this.runner;
116+ @@ -263,6 +320,7 @@
117+ // this.runner = new Mocha.Runner(this.suite, this.options.delay || false);
118+ // TODO: Stub this to match the Runner's interface even better
119+ this.runner = new FakeRunner_1.FakeRunner();
120+ + this._awaitingInitialClientRun = true;
121+ // Attach event listeners to update stats
122+ (0, stats_collector_1.createStatsCollector)(this.runner);
123+ // Set the client options, to be passed to the next running client
0 commit comments