worker.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. /**
  2. * workerpool.js
  3. * https://github.com/josdejong/workerpool
  4. *
  5. * Offload tasks to a pool of workers on node.js and in the browser.
  6. *
  7. * @version 9.3.4
  8. * @date 2025-09-10
  9. *
  10. * @license
  11. * Copyright (C) 2014-2022 Jos de Jong <wjosdejong@gmail.com>
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  14. * use this file except in compliance with the License. You may obtain a copy
  15. * of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  21. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  22. * License for the specific language governing permissions and limitations under
  23. * the License.
  24. */
  25. (function (global, factory) {
  26. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  27. typeof define === 'function' && define.amd ? define(factory) :
  28. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.worker = factory());
  29. })(this, (function () { 'use strict';
  30. function _typeof(o) {
  31. "@babel/helpers - typeof";
  32. return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
  33. return typeof o;
  34. } : function (o) {
  35. return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
  36. }, _typeof(o);
  37. }
  38. function getDefaultExportFromCjs (x) {
  39. return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
  40. }
  41. var worker$1 = {};
  42. /**
  43. * The helper class for transferring data from the worker to the main thread.
  44. *
  45. * @param {Object} message The object to deliver to the main thread.
  46. * @param {Object[]} transfer An array of transferable Objects to transfer ownership of.
  47. */
  48. function Transfer(message, transfer) {
  49. this.message = message;
  50. this.transfer = transfer;
  51. }
  52. var transfer = Transfer;
  53. var _Promise = {};
  54. /**
  55. * Promise
  56. *
  57. * Inspired by https://gist.github.com/RubaXa/8501359 from RubaXa <trash@rubaxa.org>
  58. * @template T
  59. * @template [E=Error]
  60. * @param {Function} handler Called as handler(resolve: Function, reject: Function)
  61. * @param {Promise} [parent] Parent promise for propagation of cancel and timeout
  62. */
  63. function Promise$1(handler, parent) {
  64. var me = this;
  65. if (!(this instanceof Promise$1)) {
  66. throw new SyntaxError('Constructor must be called with the new operator');
  67. }
  68. if (typeof handler !== 'function') {
  69. throw new SyntaxError('Function parameter handler(resolve, reject) missing');
  70. }
  71. var _onSuccess = [];
  72. var _onFail = [];
  73. // status
  74. /**
  75. * @readonly
  76. */
  77. this.resolved = false;
  78. /**
  79. * @readonly
  80. */
  81. this.rejected = false;
  82. /**
  83. * @readonly
  84. */
  85. this.pending = true;
  86. /**
  87. * @readonly
  88. */
  89. this[Symbol.toStringTag] = 'Promise';
  90. /**
  91. * Process onSuccess and onFail callbacks: add them to the queue.
  92. * Once the promise is resolved, the function _promise is replace.
  93. * @param {Function} onSuccess
  94. * @param {Function} onFail
  95. * @private
  96. */
  97. var _process = function _process(onSuccess, onFail) {
  98. _onSuccess.push(onSuccess);
  99. _onFail.push(onFail);
  100. };
  101. /**
  102. * Add an onSuccess callback and optionally an onFail callback to the Promise
  103. * @template TT
  104. * @template [TE=never]
  105. * @param {(r: T) => TT | PromiseLike<TT>} onSuccess
  106. * @param {(r: E) => TE | PromiseLike<TE>} [onFail]
  107. * @returns {Promise<TT | TE, any>} promise
  108. */
  109. this.then = function (onSuccess, onFail) {
  110. return new Promise$1(function (resolve, reject) {
  111. var s = onSuccess ? _then(onSuccess, resolve, reject) : resolve;
  112. var f = onFail ? _then(onFail, resolve, reject) : reject;
  113. _process(s, f);
  114. }, me);
  115. };
  116. /**
  117. * Resolve the promise
  118. * @param {*} result
  119. * @type {Function}
  120. */
  121. var _resolve2 = function _resolve(result) {
  122. // update status
  123. me.resolved = true;
  124. me.rejected = false;
  125. me.pending = false;
  126. _onSuccess.forEach(function (fn) {
  127. fn(result);
  128. });
  129. _process = function _process(onSuccess, onFail) {
  130. onSuccess(result);
  131. };
  132. _resolve2 = _reject2 = function _reject() {};
  133. return me;
  134. };
  135. /**
  136. * Reject the promise
  137. * @param {Error} error
  138. * @type {Function}
  139. */
  140. var _reject2 = function _reject(error) {
  141. // update status
  142. me.resolved = false;
  143. me.rejected = true;
  144. me.pending = false;
  145. _onFail.forEach(function (fn) {
  146. fn(error);
  147. });
  148. _process = function _process(onSuccess, onFail) {
  149. onFail(error);
  150. };
  151. _resolve2 = _reject2 = function _reject() {};
  152. return me;
  153. };
  154. /**
  155. * Cancel the promise. This will reject the promise with a CancellationError
  156. * @returns {this} self
  157. */
  158. this.cancel = function () {
  159. if (parent) {
  160. parent.cancel();
  161. } else {
  162. _reject2(new CancellationError());
  163. }
  164. return me;
  165. };
  166. /**
  167. * Set a timeout for the promise. If the promise is not resolved within
  168. * the time, the promise will be cancelled and a TimeoutError is thrown.
  169. * If the promise is resolved in time, the timeout is removed.
  170. * @param {number} delay Delay in milliseconds
  171. * @returns {this} self
  172. */
  173. this.timeout = function (delay) {
  174. if (parent) {
  175. parent.timeout(delay);
  176. } else {
  177. var timer = setTimeout(function () {
  178. _reject2(new TimeoutError('Promise timed out after ' + delay + ' ms'));
  179. }, delay);
  180. me.always(function () {
  181. clearTimeout(timer);
  182. });
  183. }
  184. return me;
  185. };
  186. // attach handler passing the resolve and reject functions
  187. handler(function (result) {
  188. _resolve2(result);
  189. }, function (error) {
  190. _reject2(error);
  191. });
  192. }
  193. /**
  194. * Execute given callback, then call resolve/reject based on the returned result
  195. * @param {Function} callback
  196. * @param {Function} resolve
  197. * @param {Function} reject
  198. * @returns {Function}
  199. * @private
  200. */
  201. function _then(callback, resolve, reject) {
  202. return function (result) {
  203. try {
  204. var res = callback(result);
  205. if (res && typeof res.then === 'function' && typeof res['catch'] === 'function') {
  206. // method returned a promise
  207. res.then(resolve, reject);
  208. } else {
  209. resolve(res);
  210. }
  211. } catch (error) {
  212. reject(error);
  213. }
  214. };
  215. }
  216. /**
  217. * Add an onFail callback to the Promise
  218. * @template TT
  219. * @param {(error: E) => TT | PromiseLike<TT>} onFail
  220. * @returns {Promise<T | TT>} promise
  221. */
  222. Promise$1.prototype['catch'] = function (onFail) {
  223. return this.then(null, onFail);
  224. };
  225. // TODO: add support for Promise.catch(Error, callback)
  226. // TODO: add support for Promise.catch(Error, Error, callback)
  227. /**
  228. * Execute given callback when the promise either resolves or rejects.
  229. * @template TT
  230. * @param {() => Promise<TT>} fn
  231. * @returns {Promise<TT>} promise
  232. */
  233. Promise$1.prototype.always = function (fn) {
  234. return this.then(fn, fn);
  235. };
  236. /**
  237. * Execute given callback when the promise either resolves or rejects.
  238. * Same semantics as Node's Promise.finally()
  239. * @param {Function | null | undefined} [fn]
  240. * @returns {Promise} promise
  241. */
  242. Promise$1.prototype.finally = function (fn) {
  243. var me = this;
  244. var final = function final() {
  245. return new Promise$1(function (resolve) {
  246. return resolve();
  247. }).then(fn).then(function () {
  248. return me;
  249. });
  250. };
  251. return this.then(final, final);
  252. };
  253. /**
  254. * Create a promise which resolves when all provided promises are resolved,
  255. * and fails when any of the promises resolves.
  256. * @param {Promise[]} promises
  257. * @returns {Promise<any[], any>} promise
  258. */
  259. Promise$1.all = function (promises) {
  260. return new Promise$1(function (resolve, reject) {
  261. var remaining = promises.length,
  262. results = [];
  263. if (remaining) {
  264. promises.forEach(function (p, i) {
  265. p.then(function (result) {
  266. results[i] = result;
  267. remaining--;
  268. if (remaining == 0) {
  269. resolve(results);
  270. }
  271. }, function (error) {
  272. remaining = 0;
  273. reject(error);
  274. });
  275. });
  276. } else {
  277. resolve(results);
  278. }
  279. });
  280. };
  281. /**
  282. * Create a promise resolver
  283. * @returns {{promise: Promise, resolve: Function, reject: Function}} resolver
  284. */
  285. Promise$1.defer = function () {
  286. var resolver = {};
  287. resolver.promise = new Promise$1(function (resolve, reject) {
  288. resolver.resolve = resolve;
  289. resolver.reject = reject;
  290. });
  291. return resolver;
  292. };
  293. /**
  294. * Create a cancellation error
  295. * @param {String} [message]
  296. * @extends Error
  297. */
  298. function CancellationError(message) {
  299. this.message = message || 'promise cancelled';
  300. this.stack = new Error().stack;
  301. }
  302. CancellationError.prototype = new Error();
  303. CancellationError.prototype.constructor = Error;
  304. CancellationError.prototype.name = 'CancellationError';
  305. Promise$1.CancellationError = CancellationError;
  306. /**
  307. * Create a timeout error
  308. * @param {String} [message]
  309. * @extends Error
  310. */
  311. function TimeoutError(message) {
  312. this.message = message || 'timeout exceeded';
  313. this.stack = new Error().stack;
  314. }
  315. TimeoutError.prototype = new Error();
  316. TimeoutError.prototype.constructor = Error;
  317. TimeoutError.prototype.name = 'TimeoutError';
  318. Promise$1.TimeoutError = TimeoutError;
  319. _Promise.Promise = Promise$1;
  320. (function (exports) {
  321. var Transfer = transfer;
  322. /**
  323. * worker must handle async cleanup handlers. Use custom Promise implementation.
  324. */
  325. var Promise = _Promise.Promise;
  326. /**
  327. * Special message sent by parent which causes the worker to terminate itself.
  328. * Not a "message object"; this string is the entire message.
  329. */
  330. var TERMINATE_METHOD_ID = '__workerpool-terminate__';
  331. /**
  332. * Special message by parent which causes a child process worker to perform cleaup
  333. * steps before determining if the child process worker should be terminated.
  334. */
  335. var CLEANUP_METHOD_ID = '__workerpool-cleanup__';
  336. // var nodeOSPlatform = require('./environment').nodeOSPlatform;
  337. var TIMEOUT_DEFAULT = 1000;
  338. // create a worker API for sending and receiving messages which works both on
  339. // node.js and in the browser
  340. var worker = {
  341. exit: function exit() {}
  342. };
  343. // api for in worker communication with parent process
  344. // works in both node.js and the browser
  345. var publicWorker = {
  346. /**
  347. * Registers listeners which will trigger when a task is timed out or cancled. If all listeners resolve, the worker executing the given task will not be terminated.
  348. * *Note*: If there is a blocking operation within a listener, the worker will be terminated.
  349. * @param {() => Promise<void>} listener
  350. */
  351. addAbortListener: function addAbortListener(listener) {
  352. worker.abortListeners.push(listener);
  353. },
  354. /**
  355. * Emit an event from the worker thread to the main thread.
  356. * @param {any} payload
  357. */
  358. emit: worker.emit
  359. };
  360. if (typeof self !== 'undefined' && typeof postMessage === 'function' && typeof addEventListener === 'function') {
  361. // worker in the browser
  362. worker.on = function (event, callback) {
  363. addEventListener(event, function (message) {
  364. callback(message.data);
  365. });
  366. };
  367. worker.send = function (message, transfer) {
  368. transfer ? postMessage(message, transfer) : postMessage(message);
  369. };
  370. } else if (typeof process !== 'undefined') {
  371. // node.js
  372. var WorkerThreads;
  373. try {
  374. WorkerThreads = require('worker_threads');
  375. } catch (error) {
  376. if (_typeof(error) === 'object' && error !== null && error.code === 'MODULE_NOT_FOUND') ; else {
  377. throw error;
  378. }
  379. }
  380. if (WorkerThreads && /* if there is a parentPort, we are in a WorkerThread */
  381. WorkerThreads.parentPort !== null) {
  382. var parentPort = WorkerThreads.parentPort;
  383. worker.send = parentPort.postMessage.bind(parentPort);
  384. worker.on = parentPort.on.bind(parentPort);
  385. worker.exit = process.exit.bind(process);
  386. } else {
  387. worker.on = process.on.bind(process);
  388. // ignore transfer argument since it is not supported by process
  389. worker.send = function (message) {
  390. process.send(message);
  391. };
  392. // register disconnect handler only for subprocess worker to exit when parent is killed unexpectedly
  393. worker.on('disconnect', function () {
  394. process.exit(1);
  395. });
  396. worker.exit = process.exit.bind(process);
  397. }
  398. } else {
  399. throw new Error('Script must be executed as a worker');
  400. }
  401. function convertError(error) {
  402. if (error && error.toJSON) {
  403. return JSON.parse(JSON.stringify(error));
  404. }
  405. // turn a class like Error (having non-enumerable properties) into a plain object
  406. return JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)));
  407. }
  408. /**
  409. * Test whether a value is a Promise via duck typing.
  410. * @param {*} value
  411. * @returns {boolean} Returns true when given value is an object
  412. * having functions `then` and `catch`.
  413. */
  414. function isPromise(value) {
  415. return value && typeof value.then === 'function' && typeof value.catch === 'function';
  416. }
  417. // functions available externally
  418. worker.methods = {};
  419. /**
  420. * Execute a function with provided arguments
  421. * @param {String} fn Stringified function
  422. * @param {Array} [args] Function arguments
  423. * @returns {*}
  424. */
  425. worker.methods.run = function run(fn, args) {
  426. var f = new Function('return (' + fn + ').apply(this, arguments);');
  427. f.worker = publicWorker;
  428. return f.apply(f, args);
  429. };
  430. /**
  431. * Get a list with methods available on this worker
  432. * @return {String[]} methods
  433. */
  434. worker.methods.methods = function methods() {
  435. return Object.keys(worker.methods);
  436. };
  437. /**
  438. * Custom handler for when the worker is terminated.
  439. */
  440. worker.terminationHandler = undefined;
  441. worker.abortListenerTimeout = TIMEOUT_DEFAULT;
  442. /**
  443. * Abort handlers for resolving errors which may cause a timeout or cancellation
  444. * to occur from a worker context
  445. */
  446. worker.abortListeners = [];
  447. /**
  448. * Cleanup and exit the worker.
  449. * @param {Number} code
  450. * @returns {Promise<void>}
  451. */
  452. worker.terminateAndExit = function (code) {
  453. var _exit = function _exit() {
  454. worker.exit(code);
  455. };
  456. if (!worker.terminationHandler) {
  457. return _exit();
  458. }
  459. var result = worker.terminationHandler(code);
  460. if (isPromise(result)) {
  461. result.then(_exit, _exit);
  462. return result;
  463. } else {
  464. _exit();
  465. return new Promise(function (_resolve, reject) {
  466. reject(new Error("Worker terminating"));
  467. });
  468. }
  469. };
  470. /**
  471. * Called within the worker message handler to run abort handlers if registered to perform cleanup operations.
  472. * @param {Integer} [requestId] id of task which is currently executing in the worker
  473. * @return {Promise<void>}
  474. */
  475. worker.cleanup = function (requestId) {
  476. if (!worker.abortListeners.length) {
  477. worker.send({
  478. id: requestId,
  479. method: CLEANUP_METHOD_ID,
  480. error: convertError(new Error('Worker terminating'))
  481. });
  482. // If there are no handlers registered, reject the promise with an error as we want the handler to be notified
  483. // that cleanup should begin and the handler should be GCed.
  484. return new Promise(function (resolve) {
  485. resolve();
  486. });
  487. }
  488. var _exit = function _exit() {
  489. worker.exit();
  490. };
  491. var _abort = function _abort() {
  492. if (!worker.abortListeners.length) {
  493. worker.abortListeners = [];
  494. }
  495. };
  496. var promises = worker.abortListeners.map(function (listener) {
  497. return listener();
  498. });
  499. var timerId;
  500. var timeoutPromise = new Promise(function (_resolve, reject) {
  501. timerId = setTimeout(function () {
  502. reject(new Error('Timeout occured waiting for abort handler, killing worker'));
  503. }, worker.abortListenerTimeout);
  504. });
  505. // Once a promise settles we need to clear the timeout to prevet fulfulling the promise twice
  506. var settlePromise = Promise.all(promises).then(function () {
  507. clearTimeout(timerId);
  508. _abort();
  509. }, function () {
  510. clearTimeout(timerId);
  511. _exit();
  512. });
  513. // Returns a promise which will result in one of the following cases
  514. // - Resolve once all handlers resolve
  515. // - Reject if one or more handlers exceed the 'abortListenerTimeout' interval
  516. // - Reject if one or more handlers reject
  517. // Upon one of the above cases a message will be sent to the handler with the result of the handler execution
  518. // which will either kill the worker if the result contains an error, or keep it in the pool if the result
  519. // does not contain an error.
  520. return new Promise(function (resolve, reject) {
  521. settlePromise.then(resolve, reject);
  522. timeoutPromise.then(resolve, reject);
  523. }).then(function () {
  524. worker.send({
  525. id: requestId,
  526. method: CLEANUP_METHOD_ID,
  527. error: null
  528. });
  529. }, function (err) {
  530. worker.send({
  531. id: requestId,
  532. method: CLEANUP_METHOD_ID,
  533. error: err ? convertError(err) : null
  534. });
  535. });
  536. };
  537. var currentRequestId = null;
  538. worker.on('message', function (request) {
  539. if (request === TERMINATE_METHOD_ID) {
  540. return worker.terminateAndExit(0);
  541. }
  542. if (request.method === CLEANUP_METHOD_ID) {
  543. return worker.cleanup(request.id);
  544. }
  545. try {
  546. var method = worker.methods[request.method];
  547. if (method) {
  548. currentRequestId = request.id;
  549. // execute the function
  550. var result = method.apply(method, request.params);
  551. if (isPromise(result)) {
  552. // promise returned, resolve this and then return
  553. result.then(function (result) {
  554. if (result instanceof Transfer) {
  555. worker.send({
  556. id: request.id,
  557. result: result.message,
  558. error: null
  559. }, result.transfer);
  560. } else {
  561. worker.send({
  562. id: request.id,
  563. result: result,
  564. error: null
  565. });
  566. }
  567. currentRequestId = null;
  568. }).catch(function (err) {
  569. worker.send({
  570. id: request.id,
  571. result: null,
  572. error: convertError(err)
  573. });
  574. currentRequestId = null;
  575. });
  576. } else {
  577. // immediate result
  578. if (result instanceof Transfer) {
  579. worker.send({
  580. id: request.id,
  581. result: result.message,
  582. error: null
  583. }, result.transfer);
  584. } else {
  585. worker.send({
  586. id: request.id,
  587. result: result,
  588. error: null
  589. });
  590. }
  591. currentRequestId = null;
  592. }
  593. } else {
  594. throw new Error('Unknown method "' + request.method + '"');
  595. }
  596. } catch (err) {
  597. worker.send({
  598. id: request.id,
  599. result: null,
  600. error: convertError(err)
  601. });
  602. }
  603. });
  604. /**
  605. * Register methods to the worker
  606. * @param {Object} [methods]
  607. * @param {import('./types.js').WorkerRegisterOptions} [options]
  608. */
  609. worker.register = function (methods, options) {
  610. if (methods) {
  611. for (var name in methods) {
  612. if (methods.hasOwnProperty(name)) {
  613. worker.methods[name] = methods[name];
  614. worker.methods[name].worker = publicWorker;
  615. }
  616. }
  617. }
  618. if (options) {
  619. worker.terminationHandler = options.onTerminate;
  620. // register listener timeout or default to 1 second
  621. worker.abortListenerTimeout = options.abortListenerTimeout || TIMEOUT_DEFAULT;
  622. }
  623. worker.send('ready');
  624. };
  625. worker.emit = function (payload) {
  626. if (currentRequestId) {
  627. if (payload instanceof Transfer) {
  628. worker.send({
  629. id: currentRequestId,
  630. isEvent: true,
  631. payload: payload.message
  632. }, payload.transfer);
  633. return;
  634. }
  635. worker.send({
  636. id: currentRequestId,
  637. isEvent: true,
  638. payload: payload
  639. });
  640. }
  641. };
  642. {
  643. exports.add = worker.register;
  644. exports.emit = worker.emit;
  645. }
  646. })(worker$1);
  647. var worker = /*@__PURE__*/getDefaultExportFromCjs(worker$1);
  648. return worker;
  649. }));
  650. //# sourceMappingURL=worker.js.map