Merge pull request #13882 from Snuffleupagus/PDFWorker-rm-closure

[api-minor] Remove the closure from the `PDFWorker` class, in the `src/display/api.js` file
This commit is contained in:
Tim van der Meij 2021-08-07 19:52:39 +02:00 committed by GitHub
commit 036b81496e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 317 additions and 310 deletions

View File

@ -1938,48 +1938,319 @@ class LoopbackPort {
* the constants from {@link VerbosityLevel} should be used. * the constants from {@link VerbosityLevel} should be used.
*/ */
/** @type {any} */ const PDFWorkerUtil = {
const PDFWorker = (function PDFWorkerClosure() { isWorkerDisabled: false,
const pdfWorkerPorts = new WeakMap(); fallbackWorkerSrc: null,
let isWorkerDisabled = false; fakeWorkerId: 0,
let fallbackWorkerSrc; };
let nextFakeWorkerId = 0; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
let fakeWorkerCapability; // eslint-disable-next-line no-undef
if (isNodeJS && typeof __non_webpack_require__ === "function") {
// Workers aren't supported in Node.js, force-disabling them there.
PDFWorkerUtil.isWorkerDisabled = true;
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) { PDFWorkerUtil.fallbackWorkerSrc = PDFJSDev.test("LIB")
// eslint-disable-next-line no-undef ? "../pdf.worker.js"
if (isNodeJS && typeof __non_webpack_require__ === "function") { : "./pdf.worker.js";
// Workers aren't supported in Node.js, force-disabling them there. } else if (typeof document === "object") {
isWorkerDisabled = true; const pdfjsFilePath = document?.currentScript?.src;
if (pdfjsFilePath) {
fallbackWorkerSrc = PDFJSDev.test("LIB") PDFWorkerUtil.fallbackWorkerSrc = pdfjsFilePath.replace(
? "../pdf.worker.js" /(\.(?:min\.)?js)(\?.*)?$/i,
: "./pdf.worker.js"; ".worker$1$2"
} else if (typeof document === "object") { );
const pdfjsFilePath = document?.currentScript?.src;
if (pdfjsFilePath) {
fallbackWorkerSrc = pdfjsFilePath.replace(
/(\.(?:min\.)?js)(\?.*)?$/i,
".worker$1$2"
);
}
} }
} }
function getWorkerSrc() { PDFWorkerUtil.createCDNWrapper = function (url) {
// We will rely on blob URL's property to specify origin.
// We want this function to fail in case if createObjectURL or Blob do not
// exist or fail for some reason -- our Worker creation will fail anyway.
const wrapper = `importScripts("${url}");`;
return URL.createObjectURL(new Blob([wrapper]));
};
}
/**
* PDF.js web worker abstraction that controls the instantiation of PDF
* documents. Message handlers are used to pass information from the main
* thread to the worker thread and vice versa. If the creation of a web
* worker is not possible, a "fake" worker will be used instead.
*
* @param {PDFWorkerParameters} params - The worker initialization parameters.
*/
class PDFWorker {
static get _workerPorts() {
return shadow(this, "_workerPorts", new WeakMap());
}
constructor({
name = null,
port = null,
verbosity = getVerbosityLevel(),
} = {}) {
if (port && PDFWorker._workerPorts.has(port)) {
throw new Error("Cannot use more than one PDFWorker per port.");
}
this.name = name;
this.destroyed = false;
this.postMessageTransfers = true;
this.verbosity = verbosity;
this._readyCapability = createPromiseCapability();
this._port = null;
this._webWorker = null;
this._messageHandler = null;
if (port) {
PDFWorker._workerPorts.set(port, this);
this._initializeFromPort(port);
return;
}
this._initialize();
}
/**
* Promise for worker initialization completion.
* @type {Promise<void>}
*/
get promise() {
return this._readyCapability.promise;
}
/**
* The current `workerPort`, when it exists.
* @type {Worker}
*/
get port() {
return this._port;
}
/**
* The current MessageHandler-instance.
* @type {MessageHandler}
*/
get messageHandler() {
return this._messageHandler;
}
_initializeFromPort(port) {
this._port = port;
this._messageHandler = new MessageHandler("main", "worker", port);
this._messageHandler.on("ready", function () {
// Ignoring "ready" event -- MessageHandler should already be initialized
// and ready to accept messages.
});
this._readyCapability.resolve();
}
_initialize() {
// If worker support isn't disabled explicit and the browser has worker
// support, create a new web worker and test if it/the browser fulfills
// all requirements to run parts of pdf.js in a web worker.
// Right now, the requirement is, that an Uint8Array is still an
// Uint8Array as it arrives on the worker. (Chrome added this with v.15.)
if (
typeof Worker !== "undefined" &&
!PDFWorkerUtil.isWorkerDisabled &&
!PDFWorker._mainThreadWorkerMessageHandler
) {
let workerSrc = PDFWorker.workerSrc;
try {
// Wraps workerSrc path into blob URL, if the former does not belong
// to the same origin.
if (
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("GENERIC") &&
!isSameOrigin(window.location.href, workerSrc)
) {
workerSrc = PDFWorkerUtil.createCDNWrapper(
new URL(workerSrc, window.location).href
);
}
// Some versions of FF can't create a worker on localhost, see:
// https://bugzilla.mozilla.org/show_bug.cgi?id=683280
const worker = new Worker(workerSrc);
const messageHandler = new MessageHandler("main", "worker", worker);
const terminateEarly = () => {
worker.removeEventListener("error", onWorkerError);
messageHandler.destroy();
worker.terminate();
if (this.destroyed) {
this._readyCapability.reject(new Error("Worker was destroyed"));
} else {
// Fall back to fake worker if the termination is caused by an
// error (e.g. NetworkError / SecurityError).
this._setupFakeWorker();
}
};
const onWorkerError = () => {
if (!this._webWorker) {
// Worker failed to initialize due to an error. Clean up and fall
// back to the fake worker.
terminateEarly();
}
};
worker.addEventListener("error", onWorkerError);
messageHandler.on("test", data => {
worker.removeEventListener("error", onWorkerError);
if (this.destroyed) {
terminateEarly();
return; // worker was destroyed
}
if (data) {
// supportTypedArray
this._messageHandler = messageHandler;
this._port = worker;
this._webWorker = worker;
if (!data.supportTransfers) {
this.postMessageTransfers = false;
}
this._readyCapability.resolve();
// Send global setting, e.g. verbosity level.
messageHandler.send("configure", {
verbosity: this.verbosity,
});
} else {
this._setupFakeWorker();
messageHandler.destroy();
worker.terminate();
}
});
messageHandler.on("ready", data => {
worker.removeEventListener("error", onWorkerError);
if (this.destroyed) {
terminateEarly();
return; // worker was destroyed
}
try {
sendTest();
} catch (e) {
// We need fallback to a faked worker.
this._setupFakeWorker();
}
});
const sendTest = () => {
const testObj = new Uint8Array([this.postMessageTransfers ? 255 : 0]);
// Some versions of Opera throw a DATA_CLONE_ERR on serializing the
// typed array. Also, checking if we can use transfers.
try {
messageHandler.send("test", testObj, [testObj.buffer]);
} catch (ex) {
warn("Cannot use postMessage transfers.");
testObj[0] = 0;
messageHandler.send("test", testObj);
}
};
// It might take time for the worker to initialize. We will try to send
// the "test" message immediately, and once the "ready" message arrives.
// The worker shall process only the first received "test" message.
sendTest();
return;
} catch (e) {
info("The worker has been disabled.");
}
}
// Either workers are disabled, not supported or have thrown an exception.
// Thus, we fallback to a faked worker.
this._setupFakeWorker();
}
_setupFakeWorker() {
if (!PDFWorkerUtil.isWorkerDisabled) {
warn("Setting up fake worker.");
PDFWorkerUtil.isWorkerDisabled = true;
}
PDFWorker._setupFakeWorkerGlobal
.then(WorkerMessageHandler => {
if (this.destroyed) {
this._readyCapability.reject(new Error("Worker was destroyed"));
return;
}
const port = new LoopbackPort();
this._port = port;
// All fake workers use the same port, making id unique.
const id = `fake${PDFWorkerUtil.fakeWorkerId++}`;
// If the main thread is our worker, setup the handling for the
// messages -- the main thread sends to it self.
const workerHandler = new MessageHandler(id + "_worker", id, port);
WorkerMessageHandler.setup(workerHandler, port);
const messageHandler = new MessageHandler(id, id + "_worker", port);
this._messageHandler = messageHandler;
this._readyCapability.resolve();
// Send global setting, e.g. verbosity level.
messageHandler.send("configure", {
verbosity: this.verbosity,
});
})
.catch(reason => {
this._readyCapability.reject(
new Error(`Setting up fake worker failed: "${reason.message}".`)
);
});
}
/**
* Destroys the worker instance.
*/
destroy() {
this.destroyed = true;
if (this._webWorker) {
// We need to terminate only web worker created resource.
this._webWorker.terminate();
this._webWorker = null;
}
PDFWorker._workerPorts.delete(this._port);
this._port = null;
if (this._messageHandler) {
this._messageHandler.destroy();
this._messageHandler = null;
}
}
/**
* @param {PDFWorkerParameters} params - The worker initialization parameters.
*/
static fromPort(params) {
if (!params?.port) {
throw new Error("PDFWorker.fromPort - invalid method signature.");
}
if (this._workerPorts.has(params.port)) {
return this._workerPorts.get(params.port);
}
return new PDFWorker(params);
}
/**
* The current `workerSrc`, when it exists.
* @type {string}
*/
static get workerSrc() {
if (GlobalWorkerOptions.workerSrc) { if (GlobalWorkerOptions.workerSrc) {
return GlobalWorkerOptions.workerSrc; return GlobalWorkerOptions.workerSrc;
} }
if (typeof fallbackWorkerSrc !== "undefined") { if (PDFWorkerUtil.fallbackWorkerSrc !== null) {
if (!isNodeJS) { if (!isNodeJS) {
deprecated('No "GlobalWorkerOptions.workerSrc" specified.'); deprecated('No "GlobalWorkerOptions.workerSrc" specified.');
} }
return fallbackWorkerSrc; return PDFWorkerUtil.fallbackWorkerSrc;
} }
throw new Error('No "GlobalWorkerOptions.workerSrc" specified.'); throw new Error('No "GlobalWorkerOptions.workerSrc" specified.');
} }
function getMainThreadWorkerMessageHandler() { static get _mainThreadWorkerMessageHandler() {
try { try {
return globalThis.pdfjsWorker?.WorkerMessageHandler || null; return globalThis.pdfjsWorker?.WorkerMessageHandler || null;
} catch (ex) { } catch (ex) {
@ -1987,15 +2258,10 @@ const PDFWorker = (function PDFWorkerClosure() {
} }
} }
// Loads worker code into main-thread. // Loads worker code into the main-thread.
function setupFakeWorkerGlobal() { static get _setupFakeWorkerGlobal() {
if (fakeWorkerCapability) { const loader = async () => {
return fakeWorkerCapability.promise; const mainWorkerMessageHandler = this._mainThreadWorkerMessageHandler;
}
fakeWorkerCapability = createPromiseCapability();
const loader = async function () {
const mainWorkerMessageHandler = getMainThreadWorkerMessageHandler();
if (mainWorkerMessageHandler) { if (mainWorkerMessageHandler) {
// The worker was already loaded using e.g. a `<script>` tag. // The worker was already loaded using e.g. a `<script>` tag.
@ -2024,283 +2290,24 @@ const PDFWorker = (function PDFWorkerClosure() {
// the Webpack warnings instead (telling users to ignore them). // the Webpack warnings instead (telling users to ignore them).
// //
// eslint-disable-next-line no-eval // eslint-disable-next-line no-eval
const worker = eval("require")(getWorkerSrc()); const worker = eval("require")(this.workerSrc);
return worker.WorkerMessageHandler; return worker.WorkerMessageHandler;
} }
await loadScript(getWorkerSrc()); await loadScript(this.workerSrc);
return window.pdfjsWorker.WorkerMessageHandler; return window.pdfjsWorker.WorkerMessageHandler;
}; };
loader().then(fakeWorkerCapability.resolve, fakeWorkerCapability.reject);
return fakeWorkerCapability.promise; return shadow(this, "_setupFakeWorkerGlobal", loader());
} }
}
function createCDNWrapper(url) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
// We will rely on blob URL's property to specify origin. PDFWorker.getWorkerSrc = function () {
// We want this function to fail in case if createObjectURL or Blob do not deprecated(
// exist or fail for some reason -- our Worker creation will fail anyway. "`PDFWorker.getWorkerSrc()`, please use `PDFWorker.workerSrc` instead."
const wrapper = "importScripts('" + url + "');"; );
return URL.createObjectURL(new Blob([wrapper])); return this.workerSrc;
} };
}
/**
* PDF.js web worker abstraction that controls the instantiation of PDF
* documents. Message handlers are used to pass information from the main
* thread to the worker thread and vice versa. If the creation of a web
* worker is not possible, a "fake" worker will be used instead.
*/
// eslint-disable-next-line no-shadow
class PDFWorker {
/**
* @param {PDFWorkerParameters} params - Worker initialization parameters.
*/
constructor({
name = null,
port = null,
verbosity = getVerbosityLevel(),
} = {}) {
if (port && pdfWorkerPorts.has(port)) {
throw new Error("Cannot use more than one PDFWorker per port");
}
this.name = name;
this.destroyed = false;
this.postMessageTransfers = true;
this.verbosity = verbosity;
this._readyCapability = createPromiseCapability();
this._port = null;
this._webWorker = null;
this._messageHandler = null;
if (port) {
pdfWorkerPorts.set(port, this);
this._initializeFromPort(port);
return;
}
this._initialize();
}
get promise() {
return this._readyCapability.promise;
}
get port() {
return this._port;
}
get messageHandler() {
return this._messageHandler;
}
_initializeFromPort(port) {
this._port = port;
this._messageHandler = new MessageHandler("main", "worker", port);
this._messageHandler.on("ready", function () {
// Ignoring 'ready' event -- MessageHandler shall be already initialized
// and ready to accept the messages.
});
this._readyCapability.resolve();
}
_initialize() {
// If worker support isn't disabled explicit and the browser has worker
// support, create a new web worker and test if it/the browser fulfills
// all requirements to run parts of pdf.js in a web worker.
// Right now, the requirement is, that an Uint8Array is still an
// Uint8Array as it arrives on the worker. (Chrome added this with v.15.)
if (
typeof Worker !== "undefined" &&
!isWorkerDisabled &&
!getMainThreadWorkerMessageHandler()
) {
let workerSrc = getWorkerSrc();
try {
// Wraps workerSrc path into blob URL, if the former does not belong
// to the same origin.
if (
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("GENERIC") &&
!isSameOrigin(window.location.href, workerSrc)
) {
workerSrc = createCDNWrapper(
new URL(workerSrc, window.location).href
);
}
// Some versions of FF can't create a worker on localhost, see:
// https://bugzilla.mozilla.org/show_bug.cgi?id=683280
const worker = new Worker(workerSrc);
const messageHandler = new MessageHandler("main", "worker", worker);
const terminateEarly = () => {
worker.removeEventListener("error", onWorkerError);
messageHandler.destroy();
worker.terminate();
if (this.destroyed) {
this._readyCapability.reject(new Error("Worker was destroyed"));
} else {
// Fall back to fake worker if the termination is caused by an
// error (e.g. NetworkError / SecurityError).
this._setupFakeWorker();
}
};
const onWorkerError = () => {
if (!this._webWorker) {
// Worker failed to initialize due to an error. Clean up and fall
// back to the fake worker.
terminateEarly();
}
};
worker.addEventListener("error", onWorkerError);
messageHandler.on("test", data => {
worker.removeEventListener("error", onWorkerError);
if (this.destroyed) {
terminateEarly();
return; // worker was destroyed
}
if (data) {
// supportTypedArray
this._messageHandler = messageHandler;
this._port = worker;
this._webWorker = worker;
if (!data.supportTransfers) {
this.postMessageTransfers = false;
}
this._readyCapability.resolve();
// Send global setting, e.g. verbosity level.
messageHandler.send("configure", {
verbosity: this.verbosity,
});
} else {
this._setupFakeWorker();
messageHandler.destroy();
worker.terminate();
}
});
messageHandler.on("ready", data => {
worker.removeEventListener("error", onWorkerError);
if (this.destroyed) {
terminateEarly();
return; // worker was destroyed
}
try {
sendTest();
} catch (e) {
// We need fallback to a faked worker.
this._setupFakeWorker();
}
});
const sendTest = () => {
const testObj = new Uint8Array([
this.postMessageTransfers ? 255 : 0,
]);
// Some versions of Opera throw a DATA_CLONE_ERR on serializing the
// typed array. Also, checking if we can use transfers.
try {
messageHandler.send("test", testObj, [testObj.buffer]);
} catch (ex) {
warn("Cannot use postMessage transfers.");
testObj[0] = 0;
messageHandler.send("test", testObj);
}
};
// It might take time for worker to initialize (especially when AMD
// loader is used). We will try to send test immediately, and then
// when 'ready' message will arrive. The worker shall process only
// first received 'test'.
sendTest();
return;
} catch (e) {
info("The worker has been disabled.");
}
}
// Either workers are disabled, not supported or have thrown an exception.
// Thus, we fallback to a faked worker.
this._setupFakeWorker();
}
_setupFakeWorker() {
if (!isWorkerDisabled) {
warn("Setting up fake worker.");
isWorkerDisabled = true;
}
setupFakeWorkerGlobal()
.then(WorkerMessageHandler => {
if (this.destroyed) {
this._readyCapability.reject(new Error("Worker was destroyed"));
return;
}
const port = new LoopbackPort();
this._port = port;
// All fake workers use the same port, making id unique.
const id = "fake" + nextFakeWorkerId++;
// If the main thread is our worker, setup the handling for the
// messages -- the main thread sends to it self.
const workerHandler = new MessageHandler(id + "_worker", id, port);
WorkerMessageHandler.setup(workerHandler, port);
const messageHandler = new MessageHandler(id, id + "_worker", port);
this._messageHandler = messageHandler;
this._readyCapability.resolve();
// Send global setting, e.g. verbosity level.
messageHandler.send("configure", {
verbosity: this.verbosity,
});
})
.catch(reason => {
this._readyCapability.reject(
new Error(`Setting up fake worker failed: "${reason.message}".`)
);
});
}
/**
* Destroys the worker instance.
*/
destroy() {
this.destroyed = true;
if (this._webWorker) {
// We need to terminate only web worker created resource.
this._webWorker.terminate();
this._webWorker = null;
}
pdfWorkerPorts.delete(this._port);
this._port = null;
if (this._messageHandler) {
this._messageHandler.destroy();
this._messageHandler = null;
}
}
/**
* @param {PDFWorkerParameters} params - The worker initialization
* parameters.
*/
static fromPort(params) {
if (!params || !params.port) {
throw new Error("PDFWorker.fromPort - invalid method signature.");
}
if (pdfWorkerPorts.has(params.port)) {
return pdfWorkerPorts.get(params.port);
}
return new PDFWorker(params);
}
static getWorkerSrc() {
return getWorkerSrc();
}
}
return PDFWorker;
})();
/** /**
* For internal use only. * For internal use only.

View File

@ -501,7 +501,7 @@ describe("api", function () {
pending("Worker is not supported in Node.js."); pending("Worker is not supported in Node.js.");
} }
const workerSrc = PDFWorker.getWorkerSrc(); const workerSrc = PDFWorker.workerSrc;
expect(typeof workerSrc).toEqual("string"); expect(typeof workerSrc).toEqual("string");
expect(workerSrc).toEqual(GlobalWorkerOptions.workerSrc); expect(workerSrc).toEqual(GlobalWorkerOptions.workerSrc);
}); });

View File

@ -2139,7 +2139,7 @@ async function loadFakeWorker() {
window.pdfjsWorker = await import("pdfjs/core/worker.js"); window.pdfjsWorker = await import("pdfjs/core/worker.js");
return undefined; return undefined;
} }
return loadScript(PDFWorker.getWorkerSrc()); return loadScript(PDFWorker.workerSrc);
} }
function loadAndEnablePDFBug(enabledTabs) { function loadAndEnablePDFBug(enabledTabs) {