@hyrfilm/workhorse
Version:
Transactional task queue for the browser with support for OPFS, localStorage, sessionStorage, in-memory
1 lines β’ 1.14 MB
Source Map (JSON)
{"version":3,"file":"workhorse.cjs","sources":["../src/types.ts","../src/util/time.ts","../node_modules/eventemitter3/index.js","../src/events/emitter.ts","../src/events/eventTypes.ts","../src/events/taskHelpers.ts","../node_modules/loglevel/lib/loglevel.js","../src/util/logging.ts","../src/plugins/TaskMonitor.ts","../src/config.ts","../node_modules/proxy-target/esm/types.js","../node_modules/coincident/esm/channel.js","../node_modules/proxy-target/esm/traps.js","../node_modules/coincident/esm/bridge.js","../node_modules/coincident/esm/index.js","../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs","../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs","../node_modules/sqlocal/dist/lib/create-mutex.js","../node_modules/sqlocal/dist/lib/exec-on-db.js","../node_modules/sqlocal/dist/lib/parse-database-path.js","../node_modules/sqlocal/dist/lib/normalize-database-file.js","../node_modules/sqlocal/dist/processor.js","../node_modules/sqlocal/dist/lib/sql-tag.js","../node_modules/sqlocal/dist/lib/convert-rows-to-objects.js","../node_modules/sqlocal/dist/lib/normalize-statement.js","../node_modules/sqlocal/dist/lib/get-query-key.js","../node_modules/sqlocal/dist/lib/normalize-sql.js","../node_modules/sqlocal/dist/lib/mutation-lock.js","../node_modules/sqlocal/dist/client.js","../src/queue/db/sql.ts","../src/queue/db/runQuery.ts","../src/queue/db/createDatabase.ts","../src/errors.ts","../src/queue/TaskQueue.ts","../src/executor/hooks.ts","../src/util/backoff.ts","../node_modules/xstate/dev/dist/xstate-dev.esm.js","../node_modules/xstate/dist/raise-c17ec2bc.esm.js","../node_modules/xstate/actors/dist/xstate-actors.esm.js","../node_modules/xstate/dist/log-3d9d72a9.esm.js","../node_modules/xstate/dist/xstate.esm.js","../src/executor/TaskExecutor.ts","../src/executor/TaskExecutorPool.ts","../src/defaultFactories.ts","../src/dispatcher.ts","../src/util/periodic.ts","../src/pluginHandler.ts","../src/workhorse.ts","../src/plugins/PauseWhenOffline.ts"],"sourcesContent":["import { InspectionEvent, Observer } from 'xstate';\nimport { SubscriptionEvents, WorkhorseEventMap } from '@/events/eventTypes.ts';\n\ntype LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';\n\ntype SqlExecutor = (\n queryTemplate: TemplateStringsArray | string,\n ...params: unknown[]\n) => Promise<QueryResult[]>;\ntype QueryResult = Record<string, string | number | null>[];\ntype RunQuery = (query: string, ...values: unknown[]) => Promise<QueryResult[]>;\ntype SubscriptionHandler<K extends SubscriptionEvents> = (payload: WorkhorseEventMap[K]) => void;\n\ninterface Workhorse {\n subscribe<K extends SubscriptionEvents>(event: K, handler: SubscriptionHandler<K>): () => void;\n queue: (taskId: string, payload: Payload) => Promise<void>;\n run: (taskId: string, payload: Payload) => Promise<unknown>;\n getStatus: () => Promise<QueueStatus>;\n startPoller: () => void;\n stopPoller: () => void;\n poll: () => Promise<void>;\n requeue: () => Promise<void>;\n shutdown: () => Promise<QueueStatus>;\n}\n\ninterface CommandDispatcher {\n getStatus: () => Promise<QueueStatus>;\n queue: (taskId: string, payload: Payload) => Promise<QueueStatus>;\n requeue: () => Promise<QueueStatus>;\n startExecutors: () => Promise<QueueStatus>;\n stopExecutors: () => Promise<QueueStatus>;\n poll: () => Promise<QueueStatus>;\n log: (s: string) => void;\n shutdown: () => Promise<QueueStatus>;\n}\n\ninterface WorkhorsePlugin {\n name: string;\n onStart(): void;\n onStop(): void;\n}\n\nexport type Inspector =\n | Observer<InspectionEvent>\n | ((inspectionEvent: InspectionEvent) => void)\n | undefined;\n\ninterface WorkhorseConfig {\n dbPath?: string;\n backoff: BackoffSettings;\n duplicates: DuplicateStrategy;\n concurrency: number;\n taskExecution: TaskExecutorStrategy;\n logLevel: LogLevel;\n\n poll: {\n auto: boolean;\n interval: number;\n pre: {\n wait: 'ready';\n timeout?: number;\n };\n };\n defaultPlugins: WorkhorsePlugin[];\n plugins: WorkhorsePlugin[];\n}\n\ninterface Factories {\n createDatabase: (config: WorkhorseConfig) => Promise<RunQuery>;\n createTaskQueue: (config: WorkhorseConfig, runQuery: RunQuery) => TaskQueue;\n createExecutorHooks: (config: WorkhorseConfig, queue: TaskQueue, run: RunTask) => TaskHooks;\n createTaskExecutor: (\n config: WorkhorseConfig,\n taskRunner: TaskHooks,\n inspect?: Inspector\n ) => SingleTaskExecutor;\n createExecutorPool: (\n config: WorkhorseConfig,\n executors: SingleTaskExecutor[],\n inspect?: Inspector\n ) => TaskExecutorPool;\n}\n\nenum TaskState {\n queued = 1,\n executing = 2,\n successful = 3,\n failed = 4,\n}\n\ntype RowId = number;\n\ninterface TaskRow {\n id: RowId;\n taskId: string;\n payload: Payload;\n}\n\ntype WorkhorseStatus = QueueStatus;\n\ninterface TaskQueue {\n addTask(taskId: string, payload: Payload): Promise<void>;\n reserveTask(): Promise<TaskRow | undefined>;\n taskSuccessful(taskRow: TaskRow): Promise<void>;\n taskFailed(taskRow: TaskRow): Promise<void>;\n requeue: () => Promise<void>;\n queryTaskCount(status: TaskState): Promise<number>;\n getStatus(): Promise<QueueStatus>;\n}\n\nenum DuplicateStrategy {\n // if a task is added with an id that already exist ignore it\n IGNORE = 'ignore',\n // if a task is added with an id that already throw an error\n FORBID = 'forbid',\n}\n\nenum TaskExecutorStrategy {\n // loops over each executor, waits until its ready - and then performs a poll.\n SERIAL = 'serial',\n // like 'serial', but stores each wait-promise - and performs a Promise.all()\n PARALLEL = 'parallel',\n // does not wait, sends a poll message to executor and immediately returns\n DETACHED = 'detached',\n}\n\nenum RequeueStrategy {\n IMMEDIATE = 'immediate',\n DEFERRED = 'deferred',\n}\n\ninterface PollOptions {\n pollInterval?: number;\n requeuing?: RequeueStrategy;\n}\n\n// Holds TaskExecutors, determined by config.concurrency\ninterface TaskExecutorPool {\n startAll(): Promise<void>;\n stopAll(): Promise<void>;\n pollAll(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\n// Interface for the state machine that reserves taks in db, executes them and writes the result back\ninterface SingleTaskExecutor {\n start(): void;\n stop(): void;\n poll(): void;\n waitFor(tag: 'ready' | 'busy' | 'canStop' | 'stopped' | 'executed'): Promise<void>;\n waitIf(tag: 'busy' | 'executing'): Promise<void>;\n getStatus(): 'stopped' | 'started' | 'critical'; //TODO: Move the types from the machine into this file to make more DRY\n}\n\ninterface TaskHooks {\n executeHook: () => Promise<void>;\n successHook: () => Promise<void>;\n reserveHook: () => Promise<void>;\n failureHook: () => Promise<void>;\n}\n\ntype JSONPrimitive = string | number | boolean | null | undefined;\ntype JSONObject = { [key: string]: JSONValue };\ntype JSONArray = JSONValue[];\ntype JSONValue = JSONPrimitive | JSONObject | JSONArray;\n\ntype Payload = JSONValue;\ntype TaskResult = Payload | undefined;\n\n// TODO: Use these for building up the type for Payload as well,\n// TODO: we don't want to allow naked primitives or null values\ntype JsonValue = string | number | boolean | JsonObject | JsonArray;\ntype JsonObject = { [key: string]: JsonValue | null };\ntype JsonArray = JsonValue[];\ntype EventPayload = JsonObject;\n\ntype RunTask = (taskId: string, payload: Payload) => Promise<TaskResult>;\n\ninterface BackoffSettings {\n initial: number;\n multiplier: number;\n maxTime: number;\n}\n\ninterface QueueStatus {\n queued: number;\n executing: number;\n successful: number;\n failed: number;\n}\n\ninterface TaskQueueRow extends Record<string, number | string> {\n id: number;\n task_id: string;\n task_payload: string;\n}\n\nfunction assertTaskQueueRow(maybeTaskQueueRow: unknown): asserts maybeTaskQueueRow is TaskQueueRow {\n if (typeof maybeTaskQueueRow !== 'object' || maybeTaskQueueRow == null)\n throw new Error('Invalid TaskQueue row - is null');\n const row = maybeTaskQueueRow as Record<string, unknown>;\n if (typeof row.id !== 'number') throw new Error(`Invalid row TaskQueue row - missing \"id\"`);\n if (typeof row.task_id !== 'string')\n throw new Error(`Invalid row TaskQueue row - missing \"task_id\"`);\n if (typeof row.task_payload !== 'string')\n throw new Error(`Invalid row TaskQueue row - missing \"payload\"`);\n}\n\nfunction assertTaskRow(\n maybeTaskRow: object | Record<string, string | number> | undefined\n): asserts maybeTaskRow is TaskRow {\n if (maybeTaskRow == undefined) {\n throw new Error('Invalid TaskRow (missing)');\n }\n if ('id' in maybeTaskRow && typeof maybeTaskRow.id !== 'number') {\n throw new Error(`Invalid row - id: ${maybeTaskRow.id}`);\n }\n if ('taskId' in maybeTaskRow && typeof maybeTaskRow.taskId !== 'string') {\n throw new Error(`Invalid row - taskId: ${maybeTaskRow.taskId}`);\n }\n if ('payload' in maybeTaskRow && typeof maybeTaskRow.payload !== 'object') {\n throw new Error(`Invalid row - payload: ${maybeTaskRow.payload}`);\n }\n}\n\nfunction assertNonPrimitive(payload: Payload): asserts payload is JSONObject {\n if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {\n throw new TypeError('Payload must be a non-primitive JSON object.');\n }\n}\n\nexport type {\n SqlExecutor,\n QueryResult,\n RunQuery,\n RowId,\n TaskRow,\n Workhorse,\n WorkhorseStatus,\n TaskQueue,\n QueueStatus,\n Payload,\n RunTask,\n TaskResult,\n TaskHooks,\n TaskExecutorPool,\n SingleTaskExecutor,\n WorkhorseConfig,\n BackoffSettings,\n LogLevel,\n PollOptions,\n CommandDispatcher,\n WorkhorsePlugin,\n Factories,\n EventPayload,\n};\nexport {\n TaskState,\n DuplicateStrategy,\n TaskExecutorStrategy,\n RequeueStrategy,\n assertTaskQueueRow,\n assertTaskRow,\n assertNonPrimitive,\n};\n","type Milliseconds = number;\n\nconst millisec = (ms: number): Milliseconds => ms;\nconst seconds = (sec: number): Milliseconds => millisec(1000) * sec;\nconst minutes = (min: number): Milliseconds => seconds(60) * min;\nconst hours = (hr: number): Milliseconds => minutes(60) * hr;\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport { millisec, millisec as ms, seconds, minutes, hours, sleep };\n","'use strict';\n\nvar has = Object.prototype.hasOwnProperty\n , prefix = '~';\n\n/**\n * Constructor to create a storage for our `EE` objects.\n * An `Events` instance is a plain object whose properties are event names.\n *\n * @constructor\n * @private\n */\nfunction Events() {}\n\n//\n// We try to not inherit from `Object.prototype`. In some engines creating an\n// instance in this way is faster than calling `Object.create(null)` directly.\n// If `Object.create(null)` is not supported we prefix the event names with a\n// character to make sure that the built-in object properties are not\n// overridden or used as an attack vector.\n//\nif (Object.create) {\n Events.prototype = Object.create(null);\n\n //\n // This hack is needed because the `__proto__` property is still inherited in\n // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.\n //\n if (!new Events().__proto__) prefix = false;\n}\n\n/**\n * Representation of a single event listener.\n *\n * @param {Function} fn The listener function.\n * @param {*} context The context to invoke the listener with.\n * @param {Boolean} [once=false] Specify if the listener is a one-time listener.\n * @constructor\n * @private\n */\nfunction EE(fn, context, once) {\n this.fn = fn;\n this.context = context;\n this.once = once || false;\n}\n\n/**\n * Add a listener for a given event.\n *\n * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} context The context to invoke the listener with.\n * @param {Boolean} once Specify if the listener is a one-time listener.\n * @returns {EventEmitter}\n * @private\n */\nfunction addListener(emitter, event, fn, context, once) {\n if (typeof fn !== 'function') {\n throw new TypeError('The listener must be a function');\n }\n\n var listener = new EE(fn, context || emitter, once)\n , evt = prefix ? prefix + event : event;\n\n if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;\n else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);\n else emitter._events[evt] = [emitter._events[evt], listener];\n\n return emitter;\n}\n\n/**\n * Clear event by name.\n *\n * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.\n * @param {(String|Symbol)} evt The Event name.\n * @private\n */\nfunction clearEvent(emitter, evt) {\n if (--emitter._eventsCount === 0) emitter._events = new Events();\n else delete emitter._events[evt];\n}\n\n/**\n * Minimal `EventEmitter` interface that is molded against the Node.js\n * `EventEmitter` interface.\n *\n * @constructor\n * @public\n */\nfunction EventEmitter() {\n this._events = new Events();\n this._eventsCount = 0;\n}\n\n/**\n * Return an array listing the events for which the emitter has registered\n * listeners.\n *\n * @returns {Array}\n * @public\n */\nEventEmitter.prototype.eventNames = function eventNames() {\n var names = []\n , events\n , name;\n\n if (this._eventsCount === 0) return names;\n\n for (name in (events = this._events)) {\n if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);\n }\n\n if (Object.getOwnPropertySymbols) {\n return names.concat(Object.getOwnPropertySymbols(events));\n }\n\n return names;\n};\n\n/**\n * Return the listeners registered for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Array} The registered listeners.\n * @public\n */\nEventEmitter.prototype.listeners = function listeners(event) {\n var evt = prefix ? prefix + event : event\n , handlers = this._events[evt];\n\n if (!handlers) return [];\n if (handlers.fn) return [handlers.fn];\n\n for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {\n ee[i] = handlers[i].fn;\n }\n\n return ee;\n};\n\n/**\n * Return the number of listeners listening to a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Number} The number of listeners.\n * @public\n */\nEventEmitter.prototype.listenerCount = function listenerCount(event) {\n var evt = prefix ? prefix + event : event\n , listeners = this._events[evt];\n\n if (!listeners) return 0;\n if (listeners.fn) return 1;\n return listeners.length;\n};\n\n/**\n * Calls each of the listeners registered for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Boolean} `true` if the event had listeners, else `false`.\n * @public\n */\nEventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {\n var evt = prefix ? prefix + event : event;\n\n if (!this._events[evt]) return false;\n\n var listeners = this._events[evt]\n , len = arguments.length\n , args\n , i;\n\n if (listeners.fn) {\n if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);\n\n switch (len) {\n case 1: return listeners.fn.call(listeners.context), true;\n case 2: return listeners.fn.call(listeners.context, a1), true;\n case 3: return listeners.fn.call(listeners.context, a1, a2), true;\n case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;\n case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;\n case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;\n }\n\n for (i = 1, args = new Array(len -1); i < len; i++) {\n args[i - 1] = arguments[i];\n }\n\n listeners.fn.apply(listeners.context, args);\n } else {\n var length = listeners.length\n , j;\n\n for (i = 0; i < length; i++) {\n if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);\n\n switch (len) {\n case 1: listeners[i].fn.call(listeners[i].context); break;\n case 2: listeners[i].fn.call(listeners[i].context, a1); break;\n case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;\n case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;\n default:\n if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {\n args[j - 1] = arguments[j];\n }\n\n listeners[i].fn.apply(listeners[i].context, args);\n }\n }\n }\n\n return true;\n};\n\n/**\n * Add a listener for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} [context=this] The context to invoke the listener with.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.on = function on(event, fn, context) {\n return addListener(this, event, fn, context, false);\n};\n\n/**\n * Add a one-time listener for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} [context=this] The context to invoke the listener with.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.once = function once(event, fn, context) {\n return addListener(this, event, fn, context, true);\n};\n\n/**\n * Remove the listeners of a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn Only remove the listeners that match this function.\n * @param {*} context Only remove the listeners that have this context.\n * @param {Boolean} once Only remove one-time listeners.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {\n var evt = prefix ? prefix + event : event;\n\n if (!this._events[evt]) return this;\n if (!fn) {\n clearEvent(this, evt);\n return this;\n }\n\n var listeners = this._events[evt];\n\n if (listeners.fn) {\n if (\n listeners.fn === fn &&\n (!once || listeners.once) &&\n (!context || listeners.context === context)\n ) {\n clearEvent(this, evt);\n }\n } else {\n for (var i = 0, events = [], length = listeners.length; i < length; i++) {\n if (\n listeners[i].fn !== fn ||\n (once && !listeners[i].once) ||\n (context && listeners[i].context !== context)\n ) {\n events.push(listeners[i]);\n }\n }\n\n //\n // Reset the array, or remove it completely if we have no more listeners.\n //\n if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;\n else clearEvent(this, evt);\n }\n\n return this;\n};\n\n/**\n * Remove all listeners, or those of the specified event.\n *\n * @param {(String|Symbol)} [event] The event name.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {\n var evt;\n\n if (event) {\n evt = prefix ? prefix + event : event;\n if (this._events[evt]) clearEvent(this, evt);\n } else {\n this._events = new Events();\n this._eventsCount = 0;\n }\n\n return this;\n};\n\n//\n// Alias methods names because people roll like that.\n//\nEventEmitter.prototype.off = EventEmitter.prototype.removeListener;\nEventEmitter.prototype.addListener = EventEmitter.prototype.on;\n\n//\n// Expose the prefix.\n//\nEventEmitter.prefixed = prefix;\n\n//\n// Allow `EventEmitter` to be imported as module namespace.\n//\nEventEmitter.EventEmitter = EventEmitter;\n\n//\n// Expose the module.\n//\nif ('undefined' !== typeof module) {\n module.exports = EventEmitter;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { EventEmitter } from 'eventemitter3';\nimport type { WorkhorseEventMap } from './eventTypes';\n\ninterface TypedEmitter<Events extends Record<string, any>> {\n on<K extends keyof Events>(event: K, fn: (payload: Events[K]) => void): void;\n once<K extends keyof Events>(event: K, fn: (payload: Events[K]) => void): void;\n off<K extends keyof Events>(event: K, fn: (payload: Events[K]) => void): void;\n emit<K extends keyof Events>(event: K, payload: Events[K]): void;\n}\n\nfunction createTypedEmitter<Events extends Record<string, any>>(): TypedEmitter<Events> {\n const ee = new EventEmitter();\n\n return {\n on(event, fn) {\n ee.on(event as string, fn as (...args: any[]) => void);\n },\n once(event, fn) {\n ee.once(event as string, fn as (...args: any[]) => void);\n },\n off(event, fn) {\n ee.off(event as string, fn as (...args: any[]) => void);\n },\n emit(event, payload) {\n ee.emit(event as string, payload);\n },\n };\n}\n\nconst Emitter: TypedEmitter<WorkhorseEventMap> = createTypedEmitter();\n\nexport { Emitter, createTypedEmitter };\nexport type { TypedEmitter };\n","// events received from workhorse\nconst Notifications = {\n /*\n Workhorse: {\n Config: 'workhorse.config',\n Starting: 'workhorse.starting',\n Started: 'workhorse.started',\n Stopping: 'workhorse.stopping',\n },\n Pool: {\n Starting: 'poll.starting',\n Started: 'poll.started',\n Stopping: 'poll.stopping',\n },\n Executor: {\n Started: 'executor.started',\n Stopped: 'executor.stopped',\n },\n */\n\n // taskId passed as a parameter\n Task: {\n Added: 'task.added',\n //Reserved: 'task.reserved',\n //Executing: 'task.executing',\n Success: 'task.success',\n Failure: 'task.failure',\n },\n // used by workhorse.run()\n // taskId as suffix of the event name eg 'Task.Completed.[taskId]'\n TaskId: {\n Success: 'taskid.success.',\n Failure: 'taskid.failure.',\n },\n /*\n Poller: {\n Started: 'poller.started',\n Stopped: 'poller.Stopped',\n },\n */\n} as const;\n\n// events sent to workhorse\nconst Actions = {\n Log: 'log',\n Executors: {\n Start: 'Executors.Start',\n Stop: 'Executors.Stop',\n },\n Poller: {\n Pause: 'Poller.Pause',\n Resume: 'Poller.Resume',\n },\n} as const;\n\nconst Subscriptions = {\n TaskMonitor: {\n Updated: 'TaskMonitor.Updated',\n },\n} as const;\n\n// If T is an object, it maps over its keys and recursively applies DeepValue to each value.\n// Finally, it indexes into the resulting mapped type with [keyof T] to form a union of all values.\n// If T is not an object (i.e. itβs a string), it just returns T.\ntype DeepValue<T> = T extends object ? { [K in keyof T]: DeepValue<T[K]> }[keyof T] : T;\n\ntype NotificationEvents = DeepValue<typeof Notifications>;\ntype ActionEvents = DeepValue<typeof Actions>;\ntype SubscriptionEvents = DeepValue<typeof Subscriptions>;\n\ntype WorkhorseEvent = NotificationEvents | ActionEvents | SubscriptionEvents;\n\ninterface WorkhorseEventMap {\n // Task notifications\n 'task.added': { taskId: string };\n 'task.success': { taskId: string };\n 'task.failure': { taskId: string };\n\n // Actions\n 'Executors.Start': [];\n 'Executors.Stop': [];\n\n 'Poller.Pause': [];\n 'Poller.Resume': [];\n\n log: { message: string };\n\n // Subscriptions\n 'TaskMonitor.Updated': {\n total: number;\n remaining: number;\n progress: number;\n };\n}\n\nexport { Notifications, Actions, Subscriptions };\nexport type { WorkhorseEvent, SubscriptionEvents, WorkhorseEventMap };\n","import { Notifications } from '@/events/eventTypes.ts';\nimport { EventEmitter } from 'eventemitter3';\n\nconst InternalEmitter = createInternalEmitter();\n\nconst taskIdSuccess = (taskId: string): string => {\n return `${Notifications.TaskId.Success}${taskId}`;\n};\n\n//TODO: Is there a use-case for this?\n/*\nconst taskIdFailure = (taskId: string): string => {\n return `${Notifications.TaskId.Failure}${taskId}`;\n};\n*/\n\nconst emitReturnValue = (taskId: string, returnValue: unknown): void => {\n InternalEmitter.emitAnything(taskIdSuccess(taskId), returnValue);\n};\n\nconst waitForReturnValue = (taskId: string): Promise<unknown> => {\n return new Promise((resolve) => {\n InternalEmitter.once(taskIdSuccess(taskId), (args) => {\n resolve(args);\n });\n });\n};\n\n// Note: don't use this object.\n// If you do, things may break in surprising ways.\n// Its sole unexcited purpose is to facilitate the functions in this file.\n// The reason this object exists is to handle the scenario when someone subscribes to the\n// result of a task, and wants to know the exact return value.\n// And the person creating the task can decide to return literally anything.\n// If that anything isn't serializable to JSON, the result of the task will not be persisted.\n// But this allows one to at least receive the result as the task finishes its execution.\nfunction createInternalEmitter() {\n const eventEmitter = new EventEmitter();\n type callback = (args: unknown[]) => void;\n return {\n once: (event: string, fn: callback) => {\n eventEmitter.once(event, fn);\n },\n emitAnything: (event: string, ...args: unknown[]) => {\n eventEmitter.emit(event, ...args);\n },\n } as const;\n}\n\nexport { emitReturnValue, waitForReturnValue };\n","/*\n* loglevel - https://github.com/pimterry/loglevel\n*\n* Copyright (c) 2013 Tim Perry\n* Licensed under the MIT license.\n*/\n(function (root, definition) {\n \"use strict\";\n if (typeof define === 'function' && define.amd) {\n define(definition);\n } else if (typeof module === 'object' && module.exports) {\n module.exports = definition();\n } else {\n root.log = definition();\n }\n}(this, function () {\n \"use strict\";\n\n // Slightly dubious tricks to cut down minimized file size\n var noop = function() {};\n var undefinedType = \"undefined\";\n var isIE = (typeof window !== undefinedType) && (typeof window.navigator !== undefinedType) && (\n /Trident\\/|MSIE /.test(window.navigator.userAgent)\n );\n\n var logMethods = [\n \"trace\",\n \"debug\",\n \"info\",\n \"warn\",\n \"error\"\n ];\n\n var _loggersByName = {};\n var defaultLogger = null;\n\n // Cross-browser bind equivalent that works at least back to IE6\n function bindMethod(obj, methodName) {\n var method = obj[methodName];\n if (typeof method.bind === 'function') {\n return method.bind(obj);\n } else {\n try {\n return Function.prototype.bind.call(method, obj);\n } catch (e) {\n // Missing bind shim or IE8 + Modernizr, fallback to wrapping\n return function() {\n return Function.prototype.apply.apply(method, [obj, arguments]);\n };\n }\n }\n }\n\n // Trace() doesn't print the message in IE, so for that case we need to wrap it\n function traceForIE() {\n if (console.log) {\n if (console.log.apply) {\n console.log.apply(console, arguments);\n } else {\n // In old IE, native console methods themselves don't have apply().\n Function.prototype.apply.apply(console.log, [console, arguments]);\n }\n }\n if (console.trace) console.trace();\n }\n\n // Build the best logging method possible for this env\n // Wherever possible we want to bind, not wrap, to preserve stack traces\n function realMethod(methodName) {\n if (methodName === 'debug') {\n methodName = 'log';\n }\n\n if (typeof console === undefinedType) {\n return false; // No method possible, for now - fixed later by enableLoggingWhenConsoleArrives\n } else if (methodName === 'trace' && isIE) {\n return traceForIE;\n } else if (console[methodName] !== undefined) {\n return bindMethod(console, methodName);\n } else if (console.log !== undefined) {\n return bindMethod(console, 'log');\n } else {\n return noop;\n }\n }\n\n // These private functions always need `this` to be set properly\n\n function replaceLoggingMethods() {\n /*jshint validthis:true */\n var level = this.getLevel();\n\n // Replace the actual methods.\n for (var i = 0; i < logMethods.length; i++) {\n var methodName = logMethods[i];\n this[methodName] = (i < level) ?\n noop :\n this.methodFactory(methodName, level, this.name);\n }\n\n // Define log.log as an alias for log.debug\n this.log = this.debug;\n\n // Return any important warnings.\n if (typeof console === undefinedType && level < this.levels.SILENT) {\n return \"No console available for logging\";\n }\n }\n\n // In old IE versions, the console isn't present until you first open it.\n // We build realMethod() replacements here that regenerate logging methods\n function enableLoggingWhenConsoleArrives(methodName) {\n return function () {\n if (typeof console !== undefinedType) {\n replaceLoggingMethods.call(this);\n this[methodName].apply(this, arguments);\n }\n };\n }\n\n // By default, we use closely bound real methods wherever possible, and\n // otherwise we wait for a console to appear, and then try again.\n function defaultMethodFactory(methodName, _level, _loggerName) {\n /*jshint validthis:true */\n return realMethod(methodName) ||\n enableLoggingWhenConsoleArrives.apply(this, arguments);\n }\n\n function Logger(name, factory) {\n // Private instance variables.\n var self = this;\n /**\n * The level inherited from a parent logger (or a global default). We\n * cache this here rather than delegating to the parent so that it stays\n * in sync with the actual logging methods that we have installed (the\n * parent could change levels but we might not have rebuilt the loggers\n * in this child yet).\n * @type {number}\n */\n var inheritedLevel;\n /**\n * The default level for this logger, if any. If set, this overrides\n * `inheritedLevel`.\n * @type {number|null}\n */\n var defaultLevel;\n /**\n * A user-specific level for this logger. If set, this overrides\n * `defaultLevel`.\n * @type {number|null}\n */\n var userLevel;\n\n var storageKey = \"loglevel\";\n if (typeof name === \"string\") {\n storageKey += \":\" + name;\n } else if (typeof name === \"symbol\") {\n storageKey = undefined;\n }\n\n function persistLevelIfPossible(levelNum) {\n var levelName = (logMethods[levelNum] || 'silent').toUpperCase();\n\n if (typeof window === undefinedType || !storageKey) return;\n\n // Use localStorage if available\n try {\n window.localStorage[storageKey] = levelName;\n return;\n } catch (ignore) {}\n\n // Use session cookie as fallback\n try {\n window.document.cookie =\n encodeURIComponent(storageKey) + \"=\" + levelName + \";\";\n } catch (ignore) {}\n }\n\n function getPersistedLevel() {\n var storedLevel;\n\n if (typeof window === undefinedType || !storageKey) return;\n\n try {\n storedLevel = window.localStorage[storageKey];\n } catch (ignore) {}\n\n // Fallback to cookies if local storage gives us nothing\n if (typeof storedLevel === undefinedType) {\n try {\n var cookie = window.document.cookie;\n var cookieName = encodeURIComponent(storageKey);\n var location = cookie.indexOf(cookieName + \"=\");\n if (location !== -1) {\n storedLevel = /^([^;]+)/.exec(\n cookie.slice(location + cookieName.length + 1)\n )[1];\n }\n } catch (ignore) {}\n }\n\n // If the stored level is not valid, treat it as if nothing was stored.\n if (self.levels[storedLevel] === undefined) {\n storedLevel = undefined;\n }\n\n return storedLevel;\n }\n\n function clearPersistedLevel() {\n if (typeof window === undefinedType || !storageKey) return;\n\n // Use localStorage if available\n try {\n window.localStorage.removeItem(storageKey);\n } catch (ignore) {}\n\n // Use session cookie as fallback\n try {\n window.document.cookie =\n encodeURIComponent(storageKey) + \"=; expires=Thu, 01 Jan 1970 00:00:00 UTC\";\n } catch (ignore) {}\n }\n\n function normalizeLevel(input) {\n var level = input;\n if (typeof level === \"string\" && self.levels[level.toUpperCase()] !== undefined) {\n level = self.levels[level.toUpperCase()];\n }\n if (typeof level === \"number\" && level >= 0 && level <= self.levels.SILENT) {\n return level;\n } else {\n throw new TypeError(\"log.setLevel() called with invalid level: \" + input);\n }\n }\n\n /*\n *\n * Public logger API - see https://github.com/pimterry/loglevel for details\n *\n */\n\n self.name = name;\n\n self.levels = { \"TRACE\": 0, \"DEBUG\": 1, \"INFO\": 2, \"WARN\": 3,\n \"ERROR\": 4, \"SILENT\": 5};\n\n self.methodFactory = factory || defaultMethodFactory;\n\n self.getLevel = function () {\n if (userLevel != null) {\n return userLevel;\n } else if (defaultLevel != null) {\n return defaultLevel;\n } else {\n return inheritedLevel;\n }\n };\n\n self.setLevel = function (level, persist) {\n userLevel = normalizeLevel(level);\n if (persist !== false) { // defaults to true\n persistLevelIfPossible(userLevel);\n }\n\n // NOTE: in v2, this should call rebuild(), which updates children.\n return replaceLoggingMethods.call(self);\n };\n\n self.setDefaultLevel = function (level) {\n defaultLevel = normalizeLevel(level);\n if (!getPersistedLevel()) {\n self.setLevel(level, false);\n }\n };\n\n self.resetLevel = function () {\n userLevel = null;\n clearPersistedLevel();\n replaceLoggingMethods.call(self);\n };\n\n self.enableAll = function(persist) {\n self.setLevel(self.levels.TRACE, persist);\n };\n\n self.disableAll = function(persist) {\n self.setLevel(self.levels.SILENT, persist);\n };\n\n self.rebuild = function () {\n if (defaultLogger !== self) {\n inheritedLevel = normalizeLevel(defaultLogger.getLevel());\n }\n replaceLoggingMethods.call(self);\n\n if (defaultLogger === self) {\n for (var childName in _loggersByName) {\n _loggersByName[childName].rebuild();\n }\n }\n };\n\n // Initialize all the internal levels.\n inheritedLevel = normalizeLevel(\n defaultLogger ? defaultLogger.getLevel() : \"WARN\"\n );\n var initialLevel = getPersistedLevel();\n if (initialLevel != null) {\n userLevel = normalizeLevel(initialLevel);\n }\n replaceLoggingMethods.call(self);\n }\n\n /*\n *\n * Top-level API\n *\n */\n\n defaultLogger = new Logger();\n\n defaultLogger.getLogger = function getLogger(name) {\n if ((typeof name !== \"symbol\" && typeof name !== \"string\") || name === \"\") {\n throw new TypeError(\"You must supply a name when creating a logger.\");\n }\n\n var logger = _loggersByName[name];\n if (!logger) {\n logger = _loggersByName[name] = new Logger(\n name,\n defaultLogger.methodFactory\n );\n }\n return logger;\n };\n\n // Grab the current global log variable in case of overwrite\n var _log = (typeof window !== undefinedType) ? window.log : undefined;\n defaultLogger.noConflict = function() {\n if (typeof window !== undefinedType &&\n window.log === defaultLogger) {\n window.log = _log;\n }\n\n return defaultLogger;\n };\n\n defaultLogger.getLoggers = function getLoggers() {\n return _loggersByName;\n };\n\n // ES6 default export, for compatibility\n defaultLogger['default'] = defaultLogger;\n\n return defaultLogger;\n}));\n","import { LogLevel } from '@types';\nimport loglevel from 'loglevel';\n\nconst prefix = '[workhorse]: ';\n\nconst setLogLevel = (level: LogLevel): void => {\n loglevel.setLevel(level);\n};\n\nconst debug = (...s: string[]): void => {\n loglevel.debug(prefix, ...s);\n};\n\nconst log = (...s: string[]): void => {\n loglevel.info(prefix, ...s);\n};\n\nconst error = (...s: string[]): void => {\n loglevel.error(prefix, 'ERROR - ', ...s);\n};\n\nexport { log, error, debug, setLogLevel };\nexport type { LogLevel };\n","import { WorkhorsePlugin, EventPayload } from '@/types';\nimport { Emitter, Notifications } from '@events';\nimport { Subscriptions } from '@/events/eventTypes.ts';\nimport { debug } from '@/util/logging.ts';\n\nclass TaskMonitor implements WorkhorsePlugin {\n public name = 'TaskMonitor';\n private allTaskIds = new Set();\n private remainingTaskIds = new Set();\n private notify = () => {\n const total = this.allTaskIds.size;\n const remaining = this.remainingTaskIds.size;\n let progress = 0;\n if (total > 0) {\n progress = (total - remaining) / total;\n }\n Emitter.emit(Subscriptions.TaskMonitor.Updated, { total, remaining, progress });\n };\n\n public add = (event: EventPayload): void => {\n this.allTaskIds.add(event.taskId);\n this.remainingTaskIds.add(event.taskId);\n this.notify();\n };\n\n public remove = (event: EventPayload): void => {\n this.remainingTaskIds.delete(event.taskId);\n this.notify();\n };\n\n onStart(): void {\n debug(this.name, ' starting');\n Emitter.on(Notifications.Task.Added, this.add);\n Emitter.on(Notifications.Task.Success, this.remove);\n }\n\n onStop(): void {\n Emitter.off(Notifications.Task.Added, this.add);\n Emitter.off(Notifications.Task.Success, this.remove);\n }\n}\n\nexport { TaskMonitor };\n","import { DuplicateStrategy, TaskExecutorStrategy, WorkhorseConfig } from './types';\nimport { millisec, minutes, seconds } from './util/time';\nimport { TaskMonitor } from '@/plugins/TaskMonitor.ts';\n\nconst defaults: WorkhorseConfig = {\n concurrency: 1,\n taskExecution: TaskExecutorStrategy.SERIAL,\n logLevel: 'info',\n\n poll: {\n auto: false,\n interval: millisec(100),\n\n //TODO: Call these something else these names are not clear\n pre: {\n wait: 'ready',\n },\n },\n backoff: {\n initial: seconds(0.5),\n multiplier: 2.5,\n maxTime: minutes(15),\n },\n duplicates: DuplicateStrategy.IGNORE,\n defaultPlugins: [new TaskMonitor()],\n plugins: [],\n};\n\nconst defaultOptions = (): WorkhorseConfig => {\n return defaults;\n};\n\nexport { defaultOptions };\n","export const ARRAY = 'array';\nexport const BIGINT = 'bigint';\nexport const BOOLEAN = 'boolean';\nexport const FUNCTION = 'function';\nexport const NULL = 'null';\nexport const NUMBER = 'number';\nexport const OBJECT = 'object';\nexport const STRING = 'string';\nexport const SYMBOL = 'symbol';\nexport const UNDEFINED = 'undefined';\n","// β οΈ AUTOMATICALLY GENERATED - DO NOT CHANGE\nexport const CHANNEL = '64e10b34-2bf7-4616-9668-f99de5aa046e';\n\nexport const MAIN = 'M' + CHANNEL;\nexport const THREAD = 'T' + CHANNEL;\n","export const APPLY = 'apply';\nexport const CONSTRUCT = 'construct';\nexport const DEFINE_PROPERTY = 'defineProperty';\nexport const DELETE_PROPERTY = 'deleteProperty';\nexport const GET = 'get';\nexport const GET_OWN_PROPERTY_DESCRIPTOR = 'getOwnPropertyDescriptor';\nexport const GET_PROTOTYPE_OF = 'getPrototypeOf';\nexport const HAS = 'has';\nexport const IS_EXTENSIBLE = 'isExtensible';\nexport const OWN_KEYS = 'ownKeys';\nexport const PREVENT_EXTENSION = 'preventExtensions';\nexport const SET = 'set';\nexport const SET_PROTOTYPE_OF = 'setPrototypeOf';\n","// The goal of this file is to normalize SAB\n// at least in main -> worker() use cases.\n// This still cannot possibly solve the sync\n// worker -> main() use case if SharedArrayBuffer\n// is not available or usable.\n\nimport {CHANNEL} from './channel.js';\n\nconst {isArray} = Array;\n\nlet {SharedArrayBuffer, window} = globalThis;\nlet {notify, wait, waitAsync} = Atomics;\nlet postPatched = null;\n\n// This is needed for some version of Firefox\nif (!waitAsync) {\n waitAsync = buffer => ({\n value: new Promise(onmessage => {\n // encodeURIComponent('onmessage=({data:b})=>(Atomics.wait(b,0),postMessage(0))')\n let w = new Worker('data:application/javascript,onmessage%3D(%7Bdata%3Ab%7D)%3D%3E(Atomics.wait(b%2C0)%2CpostMessage(0))');\n w.onmessage = onmessage;\n w.postMessage(buffer);\n })\n });\n}\n\n// Monkey-patch SharedArrayBuffer if needed\ntry {\n new SharedArrayBuffer(4);\n}\ncatch (_) {\n SharedArrayBuffer = ArrayBuffer;\n\n const ids = new WeakMap;\n // patch only main -> worker():async use case\n if (window) {\n const resolvers = new Map;\n const {prototype: {postMessage}} = Worker;\n\n const listener = event => {\n const details = event.data?.[CHANNEL];\n if (!isArray(details)) {\n event.stopImmediatePropagation();\n const { id, sb } = details;\n resolvers.get(id)(sb);\n }\n };\n\n postPatched = function (data, ...rest) {\n const details = data?.[CHANNEL];\n if (isArray(details)) {\n const [id, sb] = details;\n ids.set(sb, id);\n this.addEventListener('message', listener);\n }\n return postMessage.call(this, data, ...rest);\n };\n\n waitAsync = sb => ({\n value: new Promise(resolve => {\n resolvers.set(ids.get(sb), resolve);\n }).then(buff => {\n resolvers.delete(ids.get(sb));\n ids.delete(sb);\n for (let i = 0; i < buff.length; i++) sb[i] = buff[i];\n return 'ok';\n })\n });\n }\n else {\n const as = (id, sb) => ({[CHANNEL]: { id, sb }});\n\n notify = sb => {\n postMessage(as(ids.get(sb), sb));\n };\n\n addEventListener('message', event => {\n const details = event.data?.[CHANNEL];\n if (isArray(details)) {\n const [id, sb] = details;\n ids.set(sb, id);\n }\n });\n }\n}\n\nexport {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync};\n","/*! (c) Andrea Giammarchi - ISC */\n\nimport {FUNCTION} from 'proxy-target/types';\n\nimport {CHANNEL} from './channel.js';\nimport {GET, HAS, SET} from './shared/traps.js';\n\nimport {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync} from './bridge.js';\n\n// just minifier friendly for Blob Workers' cases\nconst {Int32Array, Map, Uint16Array} = globalThis;\n\n// common constants / utilities for repeated operations\nconst {BYTES_PER_ELEMENT: I32_BYTES} = Int32Array;\nconst {BYTES_PER_ELEMENT: UI16_BYTES} = Uint16Array;\n\nconst waitInterrupt = (sb, delay, handler) => {\n while (wait(sb, 0, 0, delay) === 'timed-out')\n handler();\n};\n\n// retain buffers to transfer\nconst buffers = new WeakSet;\n\n// retain either main threads or workers global context\nconst context = new WeakMap;\n\nconst syncResult = {value: {then: fn => fn()}};\n\n// used to generate a unique `id` per each worker `postMessage` \"transaction\"\nlet uid = 0;\n\n/**\n * @typedef {Object} Interrupt used to sanity-check interrupts while waiting synchronously.\n * @prop {function} [handler] a callback invoked every `delay` milliseconds.\n * @prop {number} [delay=42] define `handler` invokes in terms of milliseconds.\n */\n\n/**\n * Create once a `Proxy` able to orchestrate synchronous `postMessage` out of the box.\n * @param {globalThis | Worker} self the context in which code should run\n * @param {{parse: (serialized: string) => any, stringify: (serializable: any) => string, transform?: (value:any) => any, interrupt?: () => void | Interrupt}} [JSON] an optional `JSON` like interface to `parse` or `stringify` content with extra `transform` ability.\n * @returns {ProxyHandler<globalThis> | ProxyHandler<Worker>}\n */\nconst coincident = (self, {parse = JSON.parse, stringify = JSON.stringify, transform, interrupt} = JSON) => {\n // create a Proxy once for the given context (globalThis or Worker instance)\n if (!context.has(self)) {\n // ensure no SAB gets a chance to pass through this call\n const sendMessage = postPatched || self.postMessage;\n // ensure the CHANNEL and data are posted correctly\n const post = (transfer, ...args) => sendMessage.call(self, {[CHANNEL]: args}, {transfer});\n\n const handler = typeof interrupt === FUNCTION ? interrupt : interrupt?.handler;\n const delay = interrupt?.delay || 42;\n const decoder = new TextDecoder('utf-16');\n\n // automatically uses sync wait (worker -> main)\n // or fallback to async wait (main -> worker)\n const waitFor = (isAsync, sb) => isAsync ?\n waitAsync(sb, 0) :\n ((handler ? waitInterrupt(sb, delay, handler) : wait(sb, 0)), syncResult);\n\n // prevent Harakiri https://github.com/WebReflection/coincident/issues/18\n let seppuku = false;\n\n context.set(self, new Proxy(new Map, {\n // there is very little point in checking prop in proxy for this very specific case\n // and I don't want to orchestrate a whole roundtrip neither, as stuff would fail\n // regardless if from Worker we access non existent Main callback, and vice-versa.\n // This is here mostly to guarantee that if such check is performed, at least the\n // get trap goes through and then it's up to developers guarantee they are accessing\n // stuff that actually exists elsewhere.\n [HAS]: (_, action) => typeof action === 'string' && !action.startsWith('_'),\n\n // worker related: get any utility that should be available on the main thread\n [GET]: (_, action) => action === 'then' ? null : ((...args) => {\n // transaction id\n const id = uid++;\n\n // first contact: just ask for how big the buffer should be\n // the value would be stored at index [1] while [0] is just control\n let sb = new Int32Array(new SharedArrayBuffer(I32_BYTES * 2));\n\n // if a transfer list has been passed, drop it from args\n let transfer = [];\n if (buffers.has(args.at(-1) || transfer))\n buffers.delete(transfer = args.pop());\n\n // ask for invoke with arguments and wait for it\n post(transfer, id, sb, action, transform ? args.map(transform) : args);\n\n // helps deciding how to wait for results\n const isAsync = self !== globalThis;\n\n // warn users about possible deadlock still allowing them\n // to explicitly `proxy.invoke().then(...)` without blocking\n let deadlock = 0;\n if (seppuku && isAsync)\n deadlock = setTimeout(console.warn, 1000, `ππ - Possible deadlock if proxy.${action}(...args) is awaited`);\n\n return waitFor(isAsync, sb).value.then(() => {\n clearTimeout(deadlock);\n\n // commit transaction using the returned / needed buffer length\n const length = sb[1];\n\n // filter undefined results\n if (!length) return;\n\n // calculate the needed ui16 bytes length to store the result string\n const bytes = UI16_BYTES * length;\n\n // round up to the next amount of bytes divided by 4 to allow i32 operations\n sb = new Int32Array(new SharedArrayBuffer(bytes + (bytes % I32_BYTES)));\n\n // ask for results and wait for it\n post([], id, sb);\n return waitFor(isAsync, sb).value.then(() => parse(\n decoder.decode(new Uint16Array(sb.buffer).slice(0, length)))\n );\n });\n }),\n\n // main thread related: react to any utility a worker is asking for\n [SET](actions, action, callback) {\n const type = typeof callback;\n if (type !== FUNCTION)\n throw new Error(`Unable to assign ${action} as ${type}`);\n // lazy event listener and logic handling, triggered once by setters actions\n if (!actions.size) {\n // maps results by `id` as they are asked for\n const results = new Map;\n // add the event listener once (first defined setter, all others work the same)\n self.addEventListener('message', async (event) => {\n // grub the very same library CHANNEL; ignore otherwise\n const details = event.data?.[CHANNEL];\n if (isArray(details)) {\n // if early enough, avoid leaking data to other listeners\n event.stopImmediatePropagation();\n const [id, sb, ...rest] = details;\n let error;\n // action available: it must be defined/known on the main thread\n if (rest.length) {\n