UNPKG

web-worker-helper

Version:

Utilities for running tasks on worker threads

1,147 lines (1,120 loc) 53.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.webWorkerHelper = {})); })(this, (function (exports) { 'use strict'; var version = '0.0.4'; // Replacement for the external assert method to reduce bundle size // Note: We don't use the second "message" argument in calling code, // so no need to support it here /** Throws an `Error` with the optional `message` if `condition` is falsy */ function assert(condition, message) { if (!condition) { throw new Error(message || 'web worker helper assertion failed.'); } } // Purpose: include this in your module to avoids adding dependencies on // micro modules like 'global' and 'is-browser'; /* eslint-disable no-restricted-globals */ var globals = { self: typeof self !== 'undefined' && self, window: typeof window !== 'undefined' && window, document: typeof document !== 'undefined' && document, }; var global_ = globals.self || globals.window || {}; /** true if running on a worker thread */ var isWorker = typeof importScripts === 'function'; /** true if running on a mobile device */ var isMobile = typeof window !== 'undefined' && typeof window.orientation !== 'undefined'; /** * Represents one Job handled by a WorkerPool or WorkerFarm */ var WorkerJob = /** @class */ (function () { function WorkerJob(jobName, workerThread) { var _this = this; this.name = jobName; this.workerThread = workerThread; this.isRunning = true; this.resolve = function () { }; this.reject = function () { }; this.result = new Promise(function (resolve, reject) { _this.resolve = resolve; _this.reject = reject; }); } /** * Send a message to the job's worker thread * @param data any data structure, ideally consisting mostly of transferrable objects */ WorkerJob.prototype.postMessage = function (type, payload) { this.workerThread.postMessage({ source: 'Main thread', // Lets worker ignore unrelated messages type: type, payload: payload, }); }; /** * Call to resolve the `result` Promise with the supplied value */ WorkerJob.prototype.done = function (value) { assert(this.isRunning, 'WorkerJob isRunning false.'); this.isRunning = false; this.resolve(value); }; /** * Call to reject the `result` Promise with the supplied error */ WorkerJob.prototype.error = function (error) { assert(this.isRunning, 'WorkerJob isRunning false.'); this.isRunning = false; this.reject(error); }; return WorkerJob; }()); var workerURLCache = new Map(); /** * Creates a loadable URL from worker source or URL * that can be used to create `Worker` instances. * Due to CORS issues it may be necessary to wrap a URL in a small importScripts * @param props * @param props.source Worker source * @param props.url Worker URL * @returns loadable url */ function getLoadableWorkerURL(props) { assert((props.source && !props.url) || (!props.source && props.url)); // Either source or url must be defined var workerURL = workerURLCache.get(props.source || props.url); if (!workerURL) { // Differentiate worker urls from worker source code if (props.url) { workerURL = getLoadableWorkerURLFromURL(props.url); workerURLCache.set(props.url, workerURL); } if (props.source) { workerURL = getLoadableWorkerURLFromSource(props.source); workerURLCache.set(props.source, workerURL); } } assert(workerURL); return workerURL; } /** * Build a loadable worker URL from worker URL * @param url * @returns loadable URL */ function getLoadableWorkerURLFromURL(url) { // A local script url, we can use it to initialize a Worker directly if (!url.startsWith('http')) { return url; } // A remote script, we need to use `importScripts` to load from different origin var workerSource = buildScriptSource(url); return getLoadableWorkerURLFromSource(workerSource); } /** * Build a loadable worker URL from worker source * @param workerSource * @returns loadable url */ function getLoadableWorkerURLFromSource(workerSource) { // NOTE: webworkify was previously used // const blob = webworkify(workerSource, {bare: true}); var blob = new Blob([workerSource], { type: 'application/javascript' }); return URL.createObjectURL(blob); } /** * Per spec, worker cannot be initialized with a script from a different origin * However a local worker script can still import scripts from other origins, * so we simply build a wrapper script. * * @param workerUrl * @returns source */ function buildScriptSource(workerUrl) { return "try {\n importScripts('".concat(workerUrl, "');\n} catch (error) {\n console.error(error);\n throw error;\n}"); } // NOTE - there is a copy of this function is both in core and loader-utils // core does not need all the utils in loader-utils, just this one. /** * Returns an array of Transferrable objects that can be used with postMessage * https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage * @param object data to be sent via postMessage * @param recursive - not for application use * @param transfers - not for application use * @returns a transfer list that can be passed to postMessage */ function getTransferList(object, recursive, transfers) { if (recursive === void 0) { recursive = true; } // Make sure that items in the transfer list is unique var transfersSet = transfers || new Set(); if (!object) ; else if (isTransferable(object)) { transfersSet.add(object); } else if (isTransferable(object.buffer)) { // Typed array transfersSet.add(object.buffer); } else if (ArrayBuffer.isView(object)) ; else if (recursive && typeof object === 'object') { for (var key in object) { // Avoid perf hit - only go one level deep getTransferList(object[key], recursive, transfersSet); } } // If transfers is defined, is internal recursive call // Otherwise it's called by the user return transfers === undefined ? Array.from(transfersSet) : []; } // https://developer.mozilla.org/en-US/docs/Web/API/Transferable function isTransferable(object) { if (!object) { return false; } if (object instanceof ArrayBuffer) { return true; } if (typeof MessagePort !== 'undefined' && object instanceof MessagePort) { return true; } if (typeof ImageBitmap !== 'undefined' && object instanceof ImageBitmap) { return true; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore if (typeof OffscreenCanvas !== 'undefined' && object instanceof OffscreenCanvas) { return true; } return false; } var NOOP = function () { }; /** * Represents one worker thread */ var WorkerThread = /** @class */ (function () { function WorkerThread(props) { this.terminated = false; this.loadableURL = ''; var name = props.name, source = props.source, url = props.url; assert(source || url); // Either source or url must be defined this.name = name; this.source = source; this.url = url; this.onMessage = NOOP; this.onError = function (error) { return console.log(error); }; this.worker = this.createBrowserWorker(); } WorkerThread.isSupported = function () { return typeof Worker !== 'undefined'; }; /** * Terminate this worker thread * @note Can free up significant memory */ WorkerThread.prototype.destroy = function () { this.onMessage = NOOP; this.onError = NOOP; this.worker.terminate(); this.terminated = true; }; Object.defineProperty(WorkerThread.prototype, "isRunning", { get: function () { // TODO: isRunning return Boolean(this.onMessage); }, enumerable: false, configurable: true }); /** * Send a message to this worker thread * @param data any data structure, ideally consisting mostly of transferrable objects * @param transferList If not supplied, calculated automatically by traversing data */ WorkerThread.prototype.postMessage = function (data, transferList) { transferList = transferList || getTransferList(data); this.worker.postMessage(data, transferList); }; /** * Generate a standard Error from an ErrorEvent * @param {ErrorEvent} event */ WorkerThread.prototype.getErrorFromErrorEvent = function (event) { // Note Error object does not have the expected fields if loading failed completely // https://developer.mozilla.org/en-US/docs/Web/API/Worker#Event_handlers // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent var message = 'Failed to load '; message += "worker ".concat(this.name, " from ").concat(this.url, ". "); if (event.message) { message += "".concat(event.message, " in "); } // const hasFilename = event.filename && !event.filename.startsWith('blob:'); // message += hasFilename ? event.filename : this.source.slice(0, 100); if (event.lineno) { message += ":".concat(event.lineno, ":").concat(event.colno); } return new Error(message); }; /** * Creates a worker thread on the browser */ WorkerThread.prototype.createBrowserWorker = function () { var _this = this; this.loadableURL = getLoadableWorkerURL({ source: this.source, url: this.url }); var worker = new Worker(this.loadableURL, { name: this.name }); worker.onmessage = function (event) { if (!event.data) { _this.onError(new Error('No data received')); } else { _this.onMessage(event.data); } }; // This callback represents an uncaught exception in the worker thread worker.onerror = function (error) { _this.onError(_this.getErrorFromErrorEvent(error)); _this.terminated = true; }; worker.onmessageerror = function (event) { return console.error("worker ".concat(_this.name, ", message error: ").concat(event)); }; return worker; }; return WorkerThread; }()); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __asyncValues(o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Process multiple data messages with small pool of identical workers */ var WorkerPool = /** @class */ (function () { function WorkerPool(props) { this.name = 'unnamed'; this.maxConcurrency = 1; this.maxMobileConcurrency = 1; this.onDebug = function () { }; this.reuseWorkers = true; this.props = {}; this.jobQueue = []; this.idleQueue = []; this.count = 0; this.isDestroyed = false; this.source = props.source; this.url = props.url; this.setProps(props); } /** * Terminates all workers in the pool * @note Can free up significant memory */ WorkerPool.prototype.destroy = function () { // Destroy idle workers, active Workers will be destroyed on completion this.idleQueue.forEach(function (worker) { return worker.destroy(); }); this.isDestroyed = true; }; WorkerPool.prototype.setProps = function (props) { this.props = __assign(__assign({}, this.props), props); if (props.name !== undefined) { this.name = props.name; } if (props.maxConcurrency !== undefined) { this.maxConcurrency = props.maxConcurrency; } if (props.maxMobileConcurrency !== undefined) { this.maxMobileConcurrency = props.maxMobileConcurrency; } if (props.reuseWorkers !== undefined) { this.reuseWorkers = props.reuseWorkers; } if (props.onDebug !== undefined) { this.onDebug = props.onDebug; } }; WorkerPool.prototype.startJob = function (name_1) { return __awaiter(this, arguments, void 0, function (name, onMessage, onError) { var startPromise; var _this = this; if (onMessage === void 0) { onMessage = function (job, type, data) { return job.done(data); }; } if (onError === void 0) { onError = function (job, error) { return job.error(error); }; } return __generator(this, function (_a) { switch (_a.label) { case 0: startPromise = new Promise(function (onStart) { // Promise resolves when thread completes or fails working on this job _this.jobQueue.push({ name: name, onMessage: onMessage, onError: onError, onStart: onStart }); return _this; }); this.startQueuedJob(); return [4 /*yield*/, startPromise]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; /** * Starts first queued job if worker is available or can be created * Called when job is started and whenever a worker returns to the idleQueue */ WorkerPool.prototype.startQueuedJob = function () { return __awaiter(this, void 0, void 0, function () { var workerThread, queuedJob, job_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.jobQueue.length) { return [2 /*return*/]; } workerThread = this.getAvailableWorker(); if (!workerThread) { return [2 /*return*/]; } queuedJob = this.jobQueue.shift(); if (!queuedJob) return [3 /*break*/, 4]; this.onDebug({ message: 'Starting job', name: queuedJob.name, backlog: this.jobQueue.length, workerThread: workerThread, }); job_1 = new WorkerJob(queuedJob.name, workerThread); // Set the worker thread's message handlers workerThread.onMessage = function (data) { return queuedJob.onMessage(job_1, data.type, data.payload); }; workerThread.onError = function (error) { return queuedJob.onError(job_1, error); }; // Resolve the start promise so that the app can start sending messages to worker queuedJob.onStart(job_1); _a.label = 1; case 1: _a.trys.push([1, , 3, 4]); return [4 /*yield*/, job_1.result]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: this.returnWorkerToQueue(workerThread); return [7 /*endfinally*/]; case 4: return [2 /*return*/]; } }); }); }; /** * Returns a worker to the idle queue * Destroys the worker if * - pool is destroyed * - if this pool doesn't reuse workers * - if maxConcurrency has been lowered */ WorkerPool.prototype.returnWorkerToQueue = function (worker) { var shouldDestroyWorker = this.isDestroyed || !this.reuseWorkers || this.count > this.getMaxConcurrency(); if (shouldDestroyWorker) { worker.destroy(); this.count--; } else { this.idleQueue.push(worker); } if (!this.isDestroyed) { this.startQueuedJob(); } }; /** * Returns idle worker or creates new worker if maxConcurrency has not been reached */ WorkerPool.prototype.getAvailableWorker = function () { // If a worker has completed and returned to the queue, it can be used if (this.idleQueue.length > 0) { return this.idleQueue.shift() || null; } // Create fresh worker if we haven't yet created the max amount of worker threads for this worker source if (this.count < this.getMaxConcurrency()) { this.count++; var name_1 = "".concat(this.name.toLowerCase(), " (#").concat(this.count, " of ").concat(this.maxConcurrency, ")"); return new WorkerThread({ name: name_1, source: this.source, url: this.url }); } // No worker available, have to wait return null; }; WorkerPool.prototype.getMaxConcurrency = function () { return isMobile ? this.maxMobileConcurrency : this.maxConcurrency; }; return WorkerPool; }()); var DEFAULT_PROPS = { maxConcurrency: 3, maxMobileConcurrency: 1, onDebug: function () { }, reuseWorkers: true, }; /** * Process multiple jobs with a "farm" of different workers in worker pools. */ var WorkerFarm = /** @class */ (function () { /** get global instance with WorkerFarm.getWorkerFarm() */ function WorkerFarm(props) { this.workerPools = new Map(); this.props = __assign({}, DEFAULT_PROPS); this.setProps(props); this.workerPools = new Map(); } /** Check if Workers are supported */ WorkerFarm.isSupported = function () { return WorkerThread.isSupported(); }; /** Get the singleton instance of the global worker farm */ WorkerFarm.getWorkerFarm = function (props) { if (props === void 0) { props = {}; } WorkerFarm.workerFarm = WorkerFarm.workerFarm || new WorkerFarm({}); WorkerFarm.workerFarm.setProps(props); return WorkerFarm.workerFarm; }; /** * Terminate all workers in the farm * @note Can free up significant memory */ WorkerFarm.prototype.destroy = function () { var e_1, _a; try { for (var _b = __values(this.workerPools.values()), _c = _b.next(); !_c.done; _c = _b.next()) { var workerPool = _c.value; workerPool.destroy(); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } }; /** * Set props used when initializing worker pools * @param props */ WorkerFarm.prototype.setProps = function (props) { var e_2, _a; this.props = __assign(__assign({}, this.props), props); try { // Update worker pool props for (var _b = __values(this.workerPools.values()), _c = _b.next(); !_c.done; _c = _b.next()) { var workerPool = _c.value; workerPool.setProps(this.getWorkerPoolProps()); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } }; /** * Returns a worker pool for the specified worker * @param options - only used first time for a specific worker name * @param options.name - the name of the worker - used to identify worker pool * @param options.url - * @param options.source - * @example * const job = WorkerFarm.getWorkerFarm().getWorkerPool({name, url}).startJob(...); */ WorkerFarm.prototype.getWorkerPool = function (options) { var name = options.name, source = options.source, url = options.url; var workerPool = this.workerPools.get(name); if (!workerPool) { workerPool = new WorkerPool({ name: name, source: source, url: url, }); workerPool.setProps(this.getWorkerPoolProps()); this.workerPools.set(name, workerPool); } return workerPool; }; WorkerFarm.prototype.getWorkerPoolProps = function () { return { maxConcurrency: this.props.maxConcurrency, maxMobileConcurrency: this.props.maxMobileConcurrency, reuseWorkers: this.props.reuseWorkers, onDebug: this.props.onDebug, }; }; return WorkerFarm; }()); var onMessageWrapperMap = new Map(); /** * Type safe wrapper for worker code */ var WorkerBody = /** @class */ (function () { function WorkerBody() { } Object.defineProperty(WorkerBody, "onmessage", { /* * (type: WorkerMessageType, payload: WorkerMessagePayload) => any */ set: function (onMessage) { self.onmessage = function (message) { if (!isKnownMessage(message)) { return; } // Confusingly the message itself also has a 'type' field which is always set to 'message' var _a = message.data, type = _a.type, payload = _a.payload; onMessage(type, payload); }; }, enumerable: false, configurable: true }); WorkerBody.addEventListener = function (onMessage) { var onMessageWrapper = onMessageWrapperMap.get(onMessage); if (!onMessageWrapper) { onMessageWrapper = function (message) { if (!isKnownMessage(message)) { return; } // Confusingly the message itself also has a 'type' field which is always set to 'message' var _a = message.data, type = _a.type, payload = _a.payload; onMessage(type, payload); }; } self.addEventListener('message', onMessageWrapper); }; WorkerBody.removeEventListener = function (onMessage) { var onMessageWrapper = onMessageWrapperMap.get(onMessage); onMessageWrapperMap.delete(onMessage); self.removeEventListener('message', onMessageWrapper); }; /** * Send a message from a worker to creating thread (main thread) * 从 worker 线程发送消息到主线程 * @param type * @param payload */ WorkerBody.postMessage = function (type, payload) { if (self) { var data = { source: 'Worker thread', type: type, payload: payload }; var transferList = getTransferList(payload); // TODO: targetOrigin, transferList // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore self.postMessage(data, transferList); } }; return WorkerBody; }()); // Filter out noise messages sent to workers function isKnownMessage(message) { var type = message.type, data = message.data; return type === 'message' && data && typeof data.source === 'string' && data.source === 'Main thread'; } /** * Safely stringify JSON (drop non serializable values like functions and regexps) * @param value */ function removeNontransferableOptions(object) { // options.log object contains functions which cannot be transferred // TODO - decide how to handle logging on workers // TODO - warn if options stringification is long return JSON.parse(stringifyJSON(object)); } function stringifyJSON(v) { var cache = new Set(); return JSON.stringify(v, function (key, value) { if (typeof value === 'object' && value !== null) { if (cache.has(value)) { // Circular reference found try { // If this value does not reference a parent it can be deduped return JSON.parse(JSON.stringify(value)); } catch (err) { // discard key if value cannot be deduped return undefined; } } // Store value in our set cache.add(value); } return value; }); } /** * Gets worker object's name (for debugging in Chrome thread inspector window) */ function getWorkerName(worker) { return "".concat(worker.name); } /** * Generate a worker URL based on worker object and options * @returns A URL to one of the following: * - a published worker on unpkg CDN * - a local test worker * - a URL provided by the user in options */ function getWorkerURL(worker, options) { if (options === void 0) { options = {}; } var workerOptions = options[worker.id] || {}; var workerFileName = "".concat(worker.name, ".worker.js"); var url = workerOptions.workerUrl; // If URL is test if (options._workerType === 'test') { url = "".concat(worker.module, "/dist/").concat(workerFileName); } // If url override is not provided, generate a URL to published version on npm CDN unpkg.com if (!url) { url = "https://unpkg.com/".concat(worker.module, "/dist/").concat(workerFileName); } assert(url); return url; } /** * Determines if we can parse with worker * @param loader * @param data * @param options */ function canProcessOnWorker(worker, options) { if (!WorkerFarm.isSupported()) { return false; } return worker.worker && (options === null || options === void 0 ? void 0 : options.worker); } /** * This function expects that the worker thread sends certain messages, * Creating such a worker can be automated if the worker is wrapper by a call to createWorker */ function processOnWorker(worker_1, data_1) { return __awaiter(this, arguments, void 0, function (worker, data, options, context) { var name, url, workerFarm, workerPool, jobName, job, transferableOptions, result; if (options === void 0) { options = {}; } if (context === void 0) { context = {}; } return __generator(this, function (_a) { switch (_a.label) { case 0: name = getWorkerName(worker); url = getWorkerURL(worker, options); workerFarm = WorkerFarm.getWorkerFarm(options); workerPool = workerFarm.getWorkerPool({ name: name, url: url }); jobName = options.jobName || worker.name; return [4 /*yield*/, workerPool.startJob(jobName, onMessage.bind(null, context))]; case 1: job = _a.sent(); transferableOptions = removeNontransferableOptions(options); job.postMessage('process', { input: data, options: transferableOptions }); return [4 /*yield*/, job.result]; case 2: result = _a.sent(); return [2 /*return*/, result.result]; } }); }); } /** * Job completes when we receive the result * @param job * @param message */ function onMessage(context, job, type, payload) { return __awaiter(this, void 0, void 0, function () { var _a, id, input, options, result, error_1, message; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = type; switch (_a) { case 'done': return [3 /*break*/, 1]; case 'error': return [3 /*break*/, 2]; case 'process': return [3 /*break*/, 3]; } return [3 /*break*/, 8]; case 1: // Worker is done job.done(payload); return [3 /*break*/, 9]; case 2: // Worker encountered an error job.error(new Error(payload.error)); return [3 /*break*/, 9]; case 3: id = payload.id, input = payload.input, options = payload.options; _b.label = 4; case 4: _b.trys.push([4, 6, , 7]); if (!context.process) { job.postMessage('error', { id: id, error: 'Worker not set up to process on main thread' }); return [2 /*return*/]; } return [4 /*yield*/, context.process(input, options)]; case 5: result = _b.sent(); job.postMessage('done', { id: id, result: result }); return [3 /*break*/, 7]; case 6: error_1 = _b.sent(); message = error_1 instanceof Error ? error_1.message : 'unknown error'; job.postMessage('error', { id: id, error: message }); return [3 /*break*/, 7]; case 7: return [3 /*break*/, 9]; case 8: console.warn("process-on-worker: unknown message ".concat(type)); _b.label = 9; case 9: return [2 /*return*/]; } }); }); } // From https://github.com/rauschma/async-iter-demo/tree/master/src under MIT license // http://2ality.com/2016/10/asynchronous-iteration.html /** * Async Queue * - AsyncIterable: An async iterator can be * - Values can be pushed onto the queue * @example * const asyncQueue = new AsyncQueue(); * setTimeout(() => asyncQueue.enqueue('tick'), 1000); * setTimeout(() => asyncQueue.enqueue(new Error('done')), 10000); * for await (const value of asyncQueue) { * console.log(value); // tick * } */ var AsyncQueue = /** @class */ (function () { function AsyncQueue() { this._values = []; // enqueues > dequeues this._settlers = []; // dequeues > enqueues this._closed = false; } /** Return an async iterator for this queue */ AsyncQueue.prototype[Symbol.asyncIterator] = function () { return this; }; /** Push a new value - the async iterator will yield a promise resolved to this value */ AsyncQueue.prototype.push = function (value) { return this.enqueue(value); }; /** * Push a new value - the async iterator will yield a promise resolved to this value * Add an error - the async iterator will yield a promise rejected with this value */ AsyncQueue.prototype.enqueue = function (value) { if (this._closed) { throw new Error('Closed'); } if (this._settlers.length > 0) { if (this._values.length > 0) { throw new Error('Illegal internal state'); } var settler = this._settlers.shift(); if (value instanceof Error) { settler.reject(value); } else { settler.resolve({ value: value }); } } else { this._values.push(value); } }; /** Indicate that we not waiting for more values - The async iterator will be done */ AsyncQueue.prototype.close = function () { while (this._settlers.length > 0) { var settler = this._settlers.shift(); settler.resolve({ done: true }); } this._closed = true; }; // ITERATOR IMPLEMENTATION /** @returns a Promise for an IteratorResult */ AsyncQueue.prototype.next = function () { var _this = this; // If values in queue, yield the first value if (this._values.length > 0) { var value = this._values.shift(); if (value instanceof Error) { return Promise.reject(value); } return Promise.resolve({ done: false, value: value }); } // If queue is closed, the iterator is done if (this._closed) { if (this._settlers.length > 0) { throw new Error('Illegal internal state'); } return Promise.resolve({ done: true, value: undefined }); } // Yield a promise that waits for new values to be enqueued return new Promise(function (resolve, reject) { _this._settlers.push({ resolve: resolve, reject: reject }); }); }; return AsyncQueue; }()); /** Counter for jobs */ var requestId = 0; // 异步队列 var inputBatches; var options; /** * Set up a WebWorkerGlobalScope to talk with the main thread */ function createWorker(process, processInBatches) { var _this = this; // 检查是否在 worker 线程中 if (typeof self === 'undefined') { return; } var context = { process: processOnMainThread, }; WorkerBody.onmessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () { var _a, result, resultIterator, _b, resultIterator_1, resultIterator_1_1, batch, e_1_1, error_1, message; var _c, e_1, _d, _e; return __generator(this, function (_f) { switch (_f.label) { case 0: _f.trys.push([0, 19, , 20]); _a = type; switch (_a) { case 'process': return [3 /*break*/, 1]; case 'process-in-batches': return [3 /*break*/, 3]; case 'input-batch': return [3 /*break*/, 16]; case 'input-done': return [3 /*break*/, 17]; } return [3 /*break*/, 18]; case 1: if (!process) { throw new Error('Worker does not support atomic processing'); } return [4 /*yield*/, process(payload.input, payload.options || {}, context)]; case 2: result = _f.sent(); WorkerBody.postMessage('done', { result: result }); return [3 /*break*/, 18]; case 3: if (!processInBatches) { throw new Error('Worker does not support batched processing'); } inputBatches = new AsyncQueue(); options = payload.options || {}; resultIterator = processInBatches(inputBatches, options, context); _f.label = 4; case 4: _f.trys.push([4, 9, 10, 15]); _b = true, resultIterator_1 = __asyncValues(resultIterator); _f.label = 5; case 5: return [4 /*yield*/, resultIterator_1.next()]; case 6: if (!(resultIterator_1_1 = _f.sent(), _c = resultIterator_1_1.done, !_c)) return [3 /*break*/, 8]; _e = resultIterator_1_1.value; _b = false; batch = _e; WorkerBody.postMessage('output-batch', { result: batch }); _f.label = 7; case 7: _b = true; return [3 /*break*/, 5]; case 8: return [3 /*break*/, 15]; case 9: e_1_1 = _f.sent(); e_1 = { error: e_1_1 }; return [3 /*break*/, 15]; case 10: _f.trys.push([10, , 13, 14]); if (!(!_b && !_c && (_d = resultIterator_1.return))) return [3 /*break*/, 12]; return [4 /*yield*/, _d.call(resultIterator_1)]; case 11: _f.sent(); _f.label = 12; case 12: return [3 /*break*/, 14]; case 13: if (e_1) throw e_1.error; return [7 /*endfinally*/]; case 14: return [7 /*endfinally*/]; case 15: WorkerBody.postMessage('done', {}); return [3 /*break*/, 18]; case 16: inputBatches.push(payload.input); return [3 /*break*/, 18]; case 17: inputBatches.close(); return [3 /*break*/, 18]; case 18: return [3 /*break*/, 20]; case 19: error_1 = _f.sent(); message = error_1 instanceof Error ? error_1.message : ''; WorkerBody.postMessage('error', { error: message }); return [3 /*break*/, 20]; case 20: return [2 /*return*/]; } }); }); }; } function processOnMainThread(arrayBuffer, options) { if (options === void 0) { options = {}; } return new Promise(function (resolve, reject) { var id = requestId++; /** */ var onMessage = function (type, payload) { if (payload.id !== id) { // not ours return; } switch (type) { case 'done': WorkerBody.removeEventListener(onMessage); resolve(payload.result); break; case 'error': WorkerBody.removeEventListener(onMessage); reject(payload.error); break; // ignore } }; WorkerBody.addEventListener(onMessage); // Ask the main thread to decode data var payload = { id: id, input: arrayBuffer, options: options }; WorkerBody.postMessage('process', payload); }); } var loadLibraryPromises = {}; // promises /** * Dynamically loads a library ("module") * * - wasm library: Array buffer is returned * - js library: Parse JS is returned * * Method depends on environment * - browser - script element is created and installed on document * - worker - eval is called on global context * - node - file is required * * @param libraryUrl * @param moduleName * @param options */ function loadLibrary(libraryUrl_1) { return __awaiter(this, arguments, void 0, function (libraryUrl, moduleName, options) { if (moduleName === void 0) { moduleName = null; } if (options === void 0) { options = {}; } return __generator(this, function (_a) { switch (_a.label) { case 0: if (moduleName) { libraryUrl = getLibraryUrl(libraryUrl, moduleName, options); } // Ensure libraries are only loaded once loadLibraryPromises[libraryUrl] = loadLibraryPromises[libraryUrl] || loadLibraryFromFile(libraryUrl); return [4 /*yield*/, loadLibraryPromises[libraryUrl]]; case 1: return [2 /*return*/, _a.sent()]; } }); }); } // TODO - sort out how to resolve paths for main/worker and dev/prod function getLibraryUrl(library, moduleName, options) { // Check if already a URL if (library.startsWith('http')) { return library; } // Allow application to import and supply libraries through `options.modules` var modules = options.modules || {}; if (modules[library]) { return modules[library]; } // load from external scripts if (options.CDN) { assert(options.CDN.startsWith('http')); return "".concat(options.CDN, "/").concat(moduleName, "/dist/libs/").concat(library); } // TODO - loading inside workers requires paths relative to worker script location... if (isWorker) { return "../src/libs/".concat(library); } return "modules/".concat(moduleName, "/src/libs/").concat(library); } function loadLibraryFromFile(libraryUrl) { return __awaiter(this, void 0, void 0, function () { var response_1, response, scriptSource; return __generator(this, function (_a) { switch (_a.label) {