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.
This commit is contained in:
Calixte Denizet 2026-04-27 16:06:45 +02:00
parent 8d3d370daa
commit cffb19e266

View File

@ -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) {