From cffb19e266ebe82c629d1651878411f78cf3b232 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 27 Apr 2026 16:06:45 +0200 Subject: [PATCH] Send 'Terminate' to the worker when destroy races the load-time handshake (bug 1942304) If `PDFDocumentLoadingTask.destroy` ran while `workerIdPromise` was pending, the inner `.then` in `getDocument` threw "Loading aborted" before `WorkerTransport` was constructed, so `_transport` was never set and the "Terminate" message was never posted. --- src/display/api.js | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 8b8977bd4..1f5f6d9bc 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -424,9 +424,6 @@ function getDocument(src = {}) { Promise.all([worker.promise, gpuPromise]) .then(function ([, hasGPU]) { - if (task.destroyed) { - throw new Error("Loading aborted"); - } if (worker.destroyed) { throw new Error("Worker was destroyed"); } @@ -469,9 +466,6 @@ function getDocument(src = {}) { } return workerIdPromise.then(workerId => { - if (task.destroyed) { - throw new Error("Loading aborted"); - } if (worker.destroyed) { throw new Error("Worker was destroyed"); } @@ -486,10 +480,18 @@ function getDocument(src = {}) { pagesMapper ); task._transport = transport; + + if (task.destroyed) { + // `destroy()` was called during the worker handshake; the orderly + // shutdown (including the "Terminate" message) will be issued + // through the transport once destroy resumes. + throw new Error("Loading aborted"); + } messageHandler.send("Ready", null); }); }) - .catch(task._capability.reject); + .catch(task._capability.reject) + .finally(task._setupCapability.resolve); return task; } @@ -515,6 +517,14 @@ class PDFDocumentLoadingTask { */ _capability = Promise.withResolvers(); + /** + * Resolves once the load-time setup chain has settled, regardless of + * outcome; used by `destroy()` to wait until `_transport` is either set + * or definitely never going to be. + * @private + */ + _setupCapability = Promise.withResolvers(); + /** * @private */ @@ -568,11 +578,25 @@ class PDFDocumentLoadingTask { */ async destroy() { this.destroyed = true; + // The setup chain rejects `_capability` with "Loading aborted" once the + // load-time chain unwinds (see `getDocument`). Claim that rejection + // here so it isn't reported as unhandled during the awaits below; + // callers awaiting `task.promise` still see it. + this._capability.promise.catch(() => {}); try { + // `_pendingDestroy` must be set synchronously, before any `await`, + // so subsequent `PDFWorker.create()` calls on the shared `workerPort` + // observe it and throw (see issue 16777). if (this._worker?.port) { this._worker._pendingDestroy = true; } + // Wait for the load-time setup chain to settle so `_transport` is set + // (when applicable) before we tear down. This is what guarantees the + // "Terminate" message gets sent through `WorkerTransport.destroy` if + // `destroy` races with the initial worker handshake. + await this._setupCapability.promise; + await this._transport?.destroy(); } catch (ex) { if (this._worker?.port) {