diff --git a/speedtest.js b/speedtest.js index f2ac516..22cafb3 100755 --- a/speedtest.js +++ b/speedtest.js @@ -336,6 +336,13 @@ Speedtest.prototype = { const isCurrentRun = function() { return this._activeRun === run && this.worker === worker; }.bind(this); + const armAbortTimeout = function() { + if (run.abortTimeout !== null) clearTimeout(run.abortTimeout); + run.abortTimeout = setTimeout(function() { + if (this._activeRun === run && this.worker === run.worker) + run.finish(true); + }.bind(this), 1000); + }.bind(this); const finish = function(aborted) { if (run.ended) return; run.ended = true; @@ -362,14 +369,18 @@ Speedtest.prototype = { } }.bind(this); + run.armAbortTimeout = armAbortTimeout; run.finish = finish; this._activeRun = run; this.worker = worker; worker.onmessage = function(e) { if (!isCurrentRun() || run.ended) return; + const data = JSON.parse(e.data); + // While an aborted run is submitting telemetry, each status response + // proves that the worker is still responsive, so keep its watchdog alive. + if (data.abortPending && run.abortTimeout !== null) armAbortTimeout(); if (e.data === run.prevData) return; else run.prevData = e.data; - const data = JSON.parse(e.data); try { if (this.onupdate) this.onupdate(data); } catch (e) { @@ -415,10 +426,7 @@ Speedtest.prototype = { if (this._state < 4 && run && this.worker === run.worker) { run.worker.postMessage("abort"); if (!run.ended && run.abortTimeout === null) { - run.abortTimeout = setTimeout(function() { - if (this._activeRun === run && this.worker === run.worker) - run.finish(true); - }.bind(this), 1000); + run.armAbortTimeout(); } } } diff --git a/speedtest_worker.js b/speedtest_worker.js index dc99686..b24717c 100755 --- a/speedtest_worker.js +++ b/speedtest_worker.js @@ -16,6 +16,7 @@ let dlProgress = 0; //progress of download test 0-1 let ulProgress = 0; //progress of upload test 0-1 let pingProgress = 0; //progress of ping+jitter test 0-1 let testId = null; //test ID (sent back by telemetry if used, null otherwise) +let abortPending = false; // true while aborted-run telemetry is being submitted let log = ""; //telemetry log function tlog(s) { @@ -102,7 +103,8 @@ this.addEventListener("message", function(e) { dlProgress: dlProgress, ulProgress: ulProgress, pingProgress: pingProgress, - testId: testId + testId: testId, + abortPending: abortPending }) ); } @@ -246,13 +248,12 @@ this.addEventListener("message", function(e) { } if (params[0] === "abort") { // abort command - if (testState >= 4) return; + if (testState >= 4 || abortPending) return; tlog("manually aborted"); clearRequests(); // stop all xhr activity runNextTest = null; if (interval) clearInterval(interval); // clear timer if present - if (settings.telemetry_level > 1) sendTelemetry(function() {}); - testState = 5; //set test as aborted + abortPending = settings.telemetry_level > 1; dlStatus = ""; ulStatus = ""; pingStatus = ""; @@ -261,6 +262,12 @@ this.addEventListener("message", function(e) { dlProgress = 0; ulProgress = 0; pingProgress = 0; + const completeAbort = function() { + abortPending = false; + testState = 5; //set test as aborted + }; + if (abortPending) sendTelemetry(completeAbort); + else completeAbort(); } }); // stops all XHR activity, aggressively diff --git a/tests/e2e/worker-lifecycle.spec.js b/tests/e2e/worker-lifecycle.spec.js index 1ccd659..206ccb4 100644 --- a/tests/e2e/worker-lifecycle.spec.js +++ b/tests/e2e/worker-lifecycle.spec.js @@ -182,6 +182,37 @@ test.describe("Speedtest worker lifecycle", () => { expect(await page.evaluate(() => window.__endCalls)).toBe(1); }); + test("keeps a responsive worker alive while aborted-run telemetry is pending", async ({ page }) => { + await page.evaluate(() => { + window.__speedtest = new Speedtest(); + window.__speedtest.start(); + window.__speedtest.abort(); + window.__worker = window.__workers[0]; + window.__telemetryStatus = setInterval( + () => window.__worker.emit({ testState: 3, abortPending: true }), + 100 + ); + }); + + await page.waitForTimeout(1100); + await page.evaluate(() => clearInterval(window.__telemetryStatus)); + expect( + await page.evaluate(() => ({ + state: window.__speedtest.getState(), + terminated: window.__worker.terminated + })) + ).toEqual({ state: 3, terminated: false }); + + const result = await page.evaluate((data) => { + window.__worker.emit(data); + return { + state: window.__speedtest.getState(), + terminated: window.__worker.terminated + }; + }, terminalState(5)); + expect(result).toEqual({ state: 4, terminated: true }); + }); + test("ignores delayed events and an old abort timeout after a new run starts", async ({ page }) => { await page.evaluate((data) => { window.__speedtest = new Speedtest();