UNPKG

mortice

Version:

Isomorphic read/write lock that works in single processes, node clusters and web workers

945 lines (924 loc) 31.8 kB
"use strict"; (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/eventemitter3/index.js var require_eventemitter3 = __commonJS({ "node_modules/eventemitter3/index.js"(exports, module) { "use strict"; var has = Object.prototype.hasOwnProperty; var prefix = "~"; function Events() { } if (Object.create) { Events.prototype = /* @__PURE__ */ Object.create(null); if (!new Events().__proto__) prefix = false; } function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } function addListener(emitter, event, fn, context, once) { if (typeof fn !== "function") { throw new TypeError("The listener must be a function"); } var listener = new EE(fn, context || emitter, once), evt = prefix ? prefix + event : event; if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); else emitter._events[evt] = [emitter._events[evt], listener]; return emitter; } function clearEvent(emitter, evt) { if (--emitter._eventsCount === 0) emitter._events = new Events(); else delete emitter._events[evt]; } function EventEmitter2() { this._events = new Events(); this._eventsCount = 0; } EventEmitter2.prototype.eventNames = function eventNames() { var names = [], events2, name; if (this._eventsCount === 0) return names; for (name in events2 = this._events) { if (has.call(events2, name)) names.push(prefix ? name.slice(1) : name); } if (Object.getOwnPropertySymbols) { return names.concat(Object.getOwnPropertySymbols(events2)); } return names; }; EventEmitter2.prototype.listeners = function listeners(event) { var evt = prefix ? prefix + event : event, handlers = this._events[evt]; if (!handlers) return []; if (handlers.fn) return [handlers.fn]; for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { ee[i] = handlers[i].fn; } return ee; }; EventEmitter2.prototype.listenerCount = function listenerCount(event) { var evt = prefix ? prefix + event : event, listeners = this._events[evt]; if (!listeners) return 0; if (listeners.fn) return 1; return listeners.length; }; EventEmitter2.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return false; var listeners = this._events[evt], len = arguments.length, args, i; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, void 0, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len - 1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length, j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, void 0, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; default: if (!args) for (j = 1, args = new Array(len - 1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); } } } return true; }; EventEmitter2.prototype.on = function on(event, fn, context) { return addListener(this, event, fn, context, false); }; EventEmitter2.prototype.once = function once(event, fn, context) { return addListener(this, event, fn, context, true); }; EventEmitter2.prototype.removeListener = function removeListener(event, fn, context, once) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return this; if (!fn) { clearEvent(this, evt); return this; } var listeners = this._events[evt]; if (listeners.fn) { if (listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context)) { clearEvent(this, evt); } } else { for (var i = 0, events2 = [], length = listeners.length; i < length; i++) { if (listeners[i].fn !== fn || once && !listeners[i].once || context && listeners[i].context !== context) { events2.push(listeners[i]); } } if (events2.length) this._events[evt] = events2.length === 1 ? events2[0] : events2; else clearEvent(this, evt); } return this; }; EventEmitter2.prototype.removeAllListeners = function removeAllListeners(event) { var evt; if (event) { evt = prefix ? prefix + event : event; if (this._events[evt]) clearEvent(this, evt); } else { this._events = new Events(); this._eventsCount = 0; } return this; }; EventEmitter2.prototype.off = EventEmitter2.prototype.removeListener; EventEmitter2.prototype.addListener = EventEmitter2.prototype.on; EventEmitter2.prefixed = prefix; EventEmitter2.EventEmitter = EventEmitter2; if ("undefined" !== typeof module) { module.exports = EventEmitter2; } } }); // node_modules/eventemitter3/index.mjs var import_index = __toESM(require_eventemitter3(), 1); // node_modules/p-timeout/index.js var TimeoutError = class extends Error { constructor(message) { super(message); this.name = "TimeoutError"; } }; var AbortError = class extends Error { constructor(message) { super(); this.name = "AbortError"; this.message = message; } }; var getDOMException = (errorMessage) => globalThis.DOMException === void 0 ? new AbortError(errorMessage) : new DOMException(errorMessage); var getAbortedReason = (signal) => { const reason = signal.reason === void 0 ? getDOMException("This operation was aborted.") : signal.reason; return reason instanceof Error ? reason : getDOMException(reason); }; function pTimeout(promise, options) { const { milliseconds, fallback, message, customTimers = { setTimeout, clearTimeout } } = options; let timer; const wrappedPromise = new Promise((resolve, reject) => { if (typeof milliseconds !== "number" || Math.sign(milliseconds) !== 1) { throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``); } if (options.signal) { const { signal } = options; if (signal.aborted) { reject(getAbortedReason(signal)); } const abortHandler = () => { reject(getAbortedReason(signal)); }; signal.addEventListener("abort", abortHandler, { once: true }); promise.finally(() => { signal.removeEventListener("abort", abortHandler); }); } if (milliseconds === Number.POSITIVE_INFINITY) { promise.then(resolve, reject); return; } const timeoutError = new TimeoutError(); timer = customTimers.setTimeout.call(void 0, () => { if (fallback) { try { resolve(fallback()); } catch (error) { reject(error); } return; } if (typeof promise.cancel === "function") { promise.cancel(); } if (message === false) { resolve(); } else if (message instanceof Error) { reject(message); } else { timeoutError.message = message ?? `Promise timed out after ${milliseconds} milliseconds`; reject(timeoutError); } }, milliseconds); (async () => { try { resolve(await promise); } catch (error) { reject(error); } })(); }); const cancelablePromise = wrappedPromise.finally(() => { cancelablePromise.clear(); }); cancelablePromise.clear = () => { customTimers.clearTimeout.call(void 0, timer); timer = void 0; }; return cancelablePromise; } // node_modules/p-queue/dist/lower-bound.js function lowerBound(array, value, comparator) { let first = 0; let count = array.length; while (count > 0) { const step = Math.trunc(count / 2); let it = first + step; if (comparator(array[it], value) <= 0) { first = ++it; count -= step + 1; } else { count = step; } } return first; } // node_modules/p-queue/dist/priority-queue.js var PriorityQueue = class { #queue = []; enqueue(run2, options) { options = { priority: 0, ...options }; const element = { priority: options.priority, run: run2 }; if (this.size && this.#queue[this.size - 1].priority >= options.priority) { this.#queue.push(element); return; } const index = lowerBound(this.#queue, element, (a, b) => b.priority - a.priority); this.#queue.splice(index, 0, element); } dequeue() { const item = this.#queue.shift(); return item?.run; } filter(options) { return this.#queue.filter((element) => element.priority === options.priority).map((element) => element.run); } get size() { return this.#queue.length; } }; // node_modules/p-queue/dist/index.js var PQueue = class extends import_index.default { #carryoverConcurrencyCount; #isIntervalIgnored; #intervalCount = 0; #intervalCap; #interval; #intervalEnd = 0; #intervalId; #timeoutId; #queue; #queueClass; #pending = 0; // The `!` is needed because of https://github.com/microsoft/TypeScript/issues/32194 #concurrency; #isPaused; #throwOnTimeout; /** Per-operation timeout in milliseconds. Operations fulfill once `timeout` elapses if they haven't already. Applies to each future operation. */ timeout; // TODO: The `throwOnTimeout` option should affect the return types of `add()` and `addAll()` constructor(options) { super(); options = { carryoverConcurrencyCount: false, intervalCap: Number.POSITIVE_INFINITY, interval: 0, concurrency: Number.POSITIVE_INFINITY, autoStart: true, queueClass: PriorityQueue, ...options }; if (!(typeof options.intervalCap === "number" && options.intervalCap >= 1)) { throw new TypeError(`Expected \`intervalCap\` to be a number from 1 and up, got \`${options.intervalCap?.toString() ?? ""}\` (${typeof options.intervalCap})`); } if (options.interval === void 0 || !(Number.isFinite(options.interval) && options.interval >= 0)) { throw new TypeError(`Expected \`interval\` to be a finite number >= 0, got \`${options.interval?.toString() ?? ""}\` (${typeof options.interval})`); } this.#carryoverConcurrencyCount = options.carryoverConcurrencyCount; this.#isIntervalIgnored = options.intervalCap === Number.POSITIVE_INFINITY || options.interval === 0; this.#intervalCap = options.intervalCap; this.#interval = options.interval; this.#queue = new options.queueClass(); this.#queueClass = options.queueClass; this.concurrency = options.concurrency; this.timeout = options.timeout; this.#throwOnTimeout = options.throwOnTimeout === true; this.#isPaused = options.autoStart === false; } get #doesIntervalAllowAnother() { return this.#isIntervalIgnored || this.#intervalCount < this.#intervalCap; } get #doesConcurrentAllowAnother() { return this.#pending < this.#concurrency; } #next() { this.#pending--; this.#tryToStartAnother(); this.emit("next"); } #onResumeInterval() { this.#onInterval(); this.#initializeIntervalIfNeeded(); this.#timeoutId = void 0; } get #isIntervalPaused() { const now = Date.now(); if (this.#intervalId === void 0) { const delay2 = this.#intervalEnd - now; if (delay2 < 0) { this.#intervalCount = this.#carryoverConcurrencyCount ? this.#pending : 0; } else { if (this.#timeoutId === void 0) { this.#timeoutId = setTimeout(() => { this.#onResumeInterval(); }, delay2); } return true; } } return false; } #tryToStartAnother() { if (this.#queue.size === 0) { if (this.#intervalId) { clearInterval(this.#intervalId); } this.#intervalId = void 0; this.emit("empty"); if (this.#pending === 0) { this.emit("idle"); } return false; } if (!this.#isPaused) { const canInitializeInterval = !this.#isIntervalPaused; if (this.#doesIntervalAllowAnother && this.#doesConcurrentAllowAnother) { const job = this.#queue.dequeue(); if (!job) { return false; } this.emit("active"); job(); if (canInitializeInterval) { this.#initializeIntervalIfNeeded(); } return true; } } return false; } #initializeIntervalIfNeeded() { if (this.#isIntervalIgnored || this.#intervalId !== void 0) { return; } this.#intervalId = setInterval(() => { this.#onInterval(); }, this.#interval); this.#intervalEnd = Date.now() + this.#interval; } #onInterval() { if (this.#intervalCount === 0 && this.#pending === 0 && this.#intervalId) { clearInterval(this.#intervalId); this.#intervalId = void 0; } this.#intervalCount = this.#carryoverConcurrencyCount ? this.#pending : 0; this.#processQueue(); } /** Executes all queued functions until it reaches the limit. */ #processQueue() { while (this.#tryToStartAnother()) { } } get concurrency() { return this.#concurrency; } set concurrency(newConcurrency) { if (!(typeof newConcurrency === "number" && newConcurrency >= 1)) { throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${newConcurrency}\` (${typeof newConcurrency})`); } this.#concurrency = newConcurrency; this.#processQueue(); } async #throwOnAbort(signal) { return new Promise((_resolve, reject) => { signal.addEventListener("abort", () => { reject(signal.reason); }, { once: true }); }); } async add(function_, options = {}) { options = { timeout: this.timeout, throwOnTimeout: this.#throwOnTimeout, ...options }; return new Promise((resolve, reject) => { this.#queue.enqueue(async () => { this.#pending++; this.#intervalCount++; try { options.signal?.throwIfAborted(); let operation = function_({ signal: options.signal }); if (options.timeout) { operation = pTimeout(Promise.resolve(operation), { milliseconds: options.timeout }); } if (options.signal) { operation = Promise.race([operation, this.#throwOnAbort(options.signal)]); } const result = await operation; resolve(result); this.emit("completed", result); } catch (error) { if (error instanceof TimeoutError && !options.throwOnTimeout) { resolve(); return; } reject(error); this.emit("error", error); } finally { this.#next(); } }, options); this.emit("add"); this.#tryToStartAnother(); }); } async addAll(functions, options) { return Promise.all(functions.map(async (function_) => this.add(function_, options))); } /** Start (or resume) executing enqueued tasks within concurrency limit. No need to call this if queue is not paused (via `options.autoStart = false` or by `.pause()` method.) */ start() { if (!this.#isPaused) { return this; } this.#isPaused = false; this.#processQueue(); return this; } /** Put queue execution on hold. */ pause() { this.#isPaused = true; } /** Clear the queue. */ clear() { this.#queue = new this.#queueClass(); } /** Can be called multiple times. Useful if you for example add additional items at a later time. @returns A promise that settles when the queue becomes empty. */ async onEmpty() { if (this.#queue.size === 0) { return; } await this.#onEvent("empty"); } /** @returns A promise that settles when the queue size is less than the given limit: `queue.size < limit`. If you want to avoid having the queue grow beyond a certain size you can `await queue.onSizeLessThan()` before adding a new item. Note that this only limits the number of items waiting to start. There could still be up to `concurrency` jobs already running that this call does not include in its calculation. */ async onSizeLessThan(limit) { if (this.#queue.size < limit) { return; } await this.#onEvent("next", () => this.#queue.size < limit); } /** The difference with `.onEmpty` is that `.onIdle` guarantees that all work from the queue has finished. `.onEmpty` merely signals that the queue is empty, but it could mean that some promises haven't completed yet. @returns A promise that settles when the queue becomes empty, and all promises have completed; `queue.size === 0 && queue.pending === 0`. */ async onIdle() { if (this.#pending === 0 && this.#queue.size === 0) { return; } await this.#onEvent("idle"); } async #onEvent(event, filter) { return new Promise((resolve) => { const listener = () => { if (filter && !filter()) { return; } this.off(event, listener); resolve(); }; this.on(event, listener); }); } /** Size of the queue, the number of queued items waiting to run. */ get size() { return this.#queue.size; } /** Size of the queue, filtered by the given options. For example, this can be used to find the number of items remaining in the queue with a specific priority level. */ sizeBy(options) { return this.#queue.filter(options).length; } /** Number of running items (no longer in the queue). */ get pending() { return this.#pending; } /** Whether the queue is currently paused. */ get isPaused() { return this.#isPaused; } }; // node_modules/observable-webworkers/dist/src/index.js var events = {}; var observable = (worker) => { worker.addEventListener("message", (event) => { observable.dispatchEvent("message", worker, event); }); if (worker.port != null) { worker.port.addEventListener("message", (event) => { observable.dispatchEvent("message", worker, event); }); } }; observable.addEventListener = (type, fn) => { if (events[type] == null) { events[type] = []; } events[type].push(fn); }; observable.removeEventListener = (type, fn) => { if (events[type] == null) { return; } events[type] = events[type].filter((listener) => listener === fn); }; observable.dispatchEvent = function(type, worker, event) { if (events[type] == null) { return; } events[type].forEach((fn) => fn(worker, event)); }; var src_default = observable; // src/constants.ts var WORKER_REQUEST_READ_LOCK = "lock:worker:request-read"; var WORKER_RELEASE_READ_LOCK = "lock:worker:release-read"; var MASTER_GRANT_READ_LOCK = "lock:master:grant-read"; var WORKER_REQUEST_WRITE_LOCK = "lock:worker:request-write"; var WORKER_RELEASE_WRITE_LOCK = "lock:worker:release-write"; var MASTER_GRANT_WRITE_LOCK = "lock:master:grant-write"; // src/utils.ts var nanoid = (size = 21) => { return Math.random().toString().substring(2); }; // src/browser.ts var handleWorkerLockRequest = (emitter, masterEvent, requestType, releaseType, grantType) => { return (worker, event) => { if (event.data.type !== requestType) { return; } const requestEvent = { type: event.data.type, name: event.data.name, identifier: event.data.identifier }; emitter.dispatchEvent(new MessageEvent(masterEvent, { data: { name: requestEvent.name, handler: async () => { worker.postMessage({ type: grantType, name: requestEvent.name, identifier: requestEvent.identifier }); await new Promise((resolve) => { const releaseEventListener = (event2) => { if (event2?.data == null) { return; } const releaseEvent = { type: event2.data.type, name: event2.data.name, identifier: event2.data.identifier }; if (releaseEvent.type === releaseType && releaseEvent.identifier === requestEvent.identifier) { worker.removeEventListener("message", releaseEventListener); resolve(); } }; worker.addEventListener("message", releaseEventListener); }); } } })); }; }; var makeWorkerLockRequest = (name, requestType, grantType, releaseType) => { return async () => { const id = nanoid(); globalThis.postMessage({ type: requestType, identifier: id, name }); return new Promise((resolve) => { const listener = (event) => { if (event?.data == null) { return; } const responseEvent = { type: event.data.type, identifier: event.data.identifier }; if (responseEvent.type === grantType && responseEvent.identifier === id) { globalThis.removeEventListener("message", listener); resolve(() => { globalThis.postMessage({ type: releaseType, identifier: id, name }); }); } }; globalThis.addEventListener("message", listener); }); }; }; var defaultOptions = { singleProcess: false }; var browser_default = (options) => { options = Object.assign({}, defaultOptions, options); const isPrimary = Boolean(globalThis.document) || options.singleProcess; if (isPrimary) { const emitter = new EventTarget(); src_default.addEventListener("message", handleWorkerLockRequest(emitter, "requestReadLock", WORKER_REQUEST_READ_LOCK, WORKER_RELEASE_READ_LOCK, MASTER_GRANT_READ_LOCK)); src_default.addEventListener("message", handleWorkerLockRequest(emitter, "requestWriteLock", WORKER_REQUEST_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK)); return emitter; } return { isWorker: true, readLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_READ_LOCK, MASTER_GRANT_READ_LOCK, WORKER_RELEASE_READ_LOCK), writeLock: (name) => makeWorkerLockRequest(name, WORKER_REQUEST_WRITE_LOCK, MASTER_GRANT_WRITE_LOCK, WORKER_RELEASE_WRITE_LOCK) }; }; // src/index.ts var mutexes = {}; var implementation; async function createReleaseable(queue, options) { let res; const p = new Promise((resolve) => { res = resolve; }); void queue.add(async () => pTimeout((async () => { await new Promise((resolve) => { res(() => { resolve(); }); }); })(), { milliseconds: options.timeout })); return p; } var createMutex = (name, options) => { if (implementation.isWorker === true) { return { readLock: implementation.readLock(name, options), writeLock: implementation.writeLock(name, options) }; } const masterQueue = new PQueue({ concurrency: 1 }); let readQueue; return { async readLock() { if (readQueue != null) { return createReleaseable(readQueue, options); } readQueue = new PQueue({ concurrency: options.concurrency, autoStart: false }); const localReadQueue = readQueue; const readPromise = createReleaseable(readQueue, options); void masterQueue.add(async () => { localReadQueue.start(); await localReadQueue.onIdle().then(() => { if (readQueue === localReadQueue) { readQueue = null; } }); }); return readPromise; }, async writeLock() { readQueue = null; return createReleaseable(masterQueue, options); } }; }; var defaultOptions2 = { name: "lock", concurrency: Infinity, timeout: 846e5, singleProcess: false }; function createMortice(options) { const opts = Object.assign({}, defaultOptions2, options); if (implementation == null) { implementation = browser_default(opts); if (implementation.isWorker !== true) { implementation.addEventListener("requestReadLock", (event) => { if (mutexes[event.data.name] == null) { return; } void mutexes[event.data.name].readLock().then(async (release) => event.data.handler().finally(() => { release(); })); }); implementation.addEventListener("requestWriteLock", async (event) => { if (mutexes[event.data.name] == null) { return; } void mutexes[event.data.name].writeLock().then(async (release) => event.data.handler().finally(() => { release(); })); }); } } if (mutexes[opts.name] == null) { mutexes[opts.name] = createMutex(opts.name, opts); } return mutexes[opts.name]; } // node_modules/delay/index.js var createAbortError = () => { const error = new Error("Delay aborted"); error.name = "AbortError"; return error; }; var clearMethods = /* @__PURE__ */ new WeakMap(); function createDelay({ clearTimeout: defaultClear, setTimeout: defaultSet } = {}) { return (milliseconds, { value, signal } = {}) => { if (signal?.aborted) { return Promise.reject(createAbortError()); } let timeoutId; let settle; let rejectFunction; const clear = defaultClear ?? clearTimeout; const signalListener = () => { clear(timeoutId); rejectFunction(createAbortError()); }; const cleanup = () => { if (signal) { signal.removeEventListener("abort", signalListener); } }; const delayPromise = new Promise((resolve, reject) => { settle = () => { cleanup(); resolve(value); }; rejectFunction = reject; timeoutId = (defaultSet ?? setTimeout)(settle, milliseconds); }); if (signal) { signal.addEventListener("abort", signalListener, { once: true }); } clearMethods.set(delayPromise, () => { clear(timeoutId); timeoutId = null; settle(); }); return delayPromise; }; } var delay = createDelay(); var delay_default = delay; // test/fixtures/lock.ts async function lock(type, muxex, counts, result, timeout = 0) { counts[type]++; const index = counts[type]; result.push(`${type} ${index} waiting`); const release = await muxex[`${type}Lock`](); result.push(`${type} ${index} start`); if (timeout > 0) { await delay_default(timeout); } result.push(`${type} ${index} complete`); release(); } // test/fixtures/worker-single-thread.ts async function run() { const mutex = createMortice({ singleProcess: true }); const counts = { read: 0, write: 0 }; const result = []; void lock("write", mutex, counts, result); void lock("read", mutex, counts, result); void lock("read", mutex, counts, result); void lock("read", mutex, counts, result, 500); void lock("write", mutex, counts, result); await lock("read", mutex, counts, result); return result; } run().then((result = []) => { globalThis.postMessage({ type: "done", result }); }, (err) => { globalThis.postMessage({ type: "error", error: { message: err.message, stack: err.stack } }); }); })();