UNPKG

@loaders.gl/worker-utils

Version:

Utilities for running tasks on worker threads

4 lines 77.4 kB
{ "version": 3, "sources": ["index.js", "lib/env-utils/version.js", "lib/env-utils/assert.js", "lib/env-utils/globals.js", "lib/worker-farm/worker-job.js", "lib/node/worker_threads-browser.js", "lib/worker-utils/get-loadable-worker-url.js", "lib/worker-utils/get-transfer-list.js", "lib/worker-farm/worker-thread.js", "lib/worker-farm/worker-pool.js", "lib/worker-farm/worker-farm.js", "lib/worker-farm/worker-body.js", "lib/worker-api/get-worker-url.js", "lib/worker-api/process-on-worker.js", "lib/async-queue/async-queue.js", "lib/worker-api/create-worker.js", "lib/worker-api/validate-worker-version.js", "lib/library-utils/library-utils.js", "lib/process-utils/child-process-proxy.js", "lib/process-utils/process-utils.js"], "sourcesContent": ["// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { VERSION } from \"./lib/env-utils/version.js\";\n// GENERAL UTILS\nexport { assert } from \"./lib/env-utils/assert.js\";\nexport { isBrowser, isWorker } from \"./lib/env-utils/globals.js\";\n// WORKER UTILS - TYPES\nexport { default as WorkerJob } from \"./lib/worker-farm/worker-job.js\";\nexport { default as WorkerThread } from \"./lib/worker-farm/worker-thread.js\";\n// WORKER FARMS\nexport { default as WorkerFarm } from \"./lib/worker-farm/worker-farm.js\";\nexport { default as WorkerPool } from \"./lib/worker-farm/worker-pool.js\";\nexport { default as WorkerBody } from \"./lib/worker-farm/worker-body.js\";\nexport { processOnWorker, canProcessOnWorker } from \"./lib/worker-api/process-on-worker.js\";\nexport { createWorker } from \"./lib/worker-api/create-worker.js\";\n// WORKER UTILS - EXPORTS\nexport { getWorkerURL } from \"./lib/worker-api/get-worker-url.js\";\nexport { validateWorkerVersion } from \"./lib/worker-api/validate-worker-version.js\";\nexport { getTransferList, getTransferListForWriter } from \"./lib/worker-utils/get-transfer-list.js\";\n// LIBRARY UTILS\nexport { getLibraryUrl, loadLibrary } from \"./lib/library-utils/library-utils.js\";\n// PARSER UTILS\nexport { default as AsyncQueue } from \"./lib/async-queue/async-queue.js\";\n// PROCESS UTILS\nexport { default as ChildProcessProxy } from \"./lib/process-utils/child-process-proxy.js\";\n// WORKER OBJECTS\n/** A null worker to test that worker processing is functional */\nexport const NullWorker = {\n id: 'null',\n name: 'null',\n module: 'worker-utils',\n version: VERSION,\n options: {\n null: {}\n }\n};\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// Version constant cannot be imported, it needs to correspond to the build version of **this** module.\n/**\n * TODO - unpkg.com doesn't seem to have a `latest` specifier for alpha releases...\n * 'beta' on beta branch, 'latest' on prod branch\n */\nexport const NPM_TAG = 'latest';\nfunction getVersion() {\n if (!globalThis._loadersgl_?.version) {\n globalThis._loadersgl_ = globalThis._loadersgl_ || {};\n // __VERSION__ is injected by babel-plugin-version-inline\n if (typeof \"4.3.2\" === 'undefined') {\n // eslint-disable-next-line\n console.warn('loaders.gl: The __VERSION__ variable is not injected using babel plugin. Latest unstable workers would be fetched from the CDN.');\n globalThis._loadersgl_.version = NPM_TAG;\n }\n else {\n globalThis._loadersgl_.version = \"4.3.2\";\n }\n }\n return globalThis._loadersgl_.version;\n}\nexport const VERSION = getVersion();\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// Replacement for the external assert method to reduce bundle size\n// Note: We don't use the second \"message\" argument in calling code,\n// so no need to support it here\n/** Throws an `Error` with the optional `message` if `condition` is falsy */\nexport function assert(condition, message) {\n if (!condition) {\n throw new Error(message || 'loaders.gl assertion failed.');\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// Purpose: include this in your module to avoids adding dependencies on\n// micro modules like 'global' and 'is-browser';\n/* eslint-disable no-restricted-globals */\nconst globals = {\n self: typeof self !== 'undefined' && self,\n window: typeof window !== 'undefined' && window,\n global: typeof global !== 'undefined' && global,\n document: typeof document !== 'undefined' && document\n};\nconst self_ = globals.self || globals.window || globals.global || {};\nconst window_ = globals.window || globals.self || globals.global || {};\nconst global_ = globals.global || globals.self || globals.window || {};\nconst document_ = globals.document || {};\nexport { self_ as self, window_ as window, global_ as global, document_ as document };\n/** true if running in the browser, false if running in Node.js */\nexport const isBrowser = \n// @ts-ignore process.browser\ntypeof process !== 'object' || String(process) !== '[object process]' || process.browser;\n/** true if running on a worker thread */\nexport const isWorker = typeof importScripts === 'function';\n/** true if running on a mobile device */\nexport const isMobile = typeof window !== 'undefined' && typeof window.orientation !== 'undefined';\n// Extract node major version\nconst matches = typeof process !== 'undefined' && process.version && /v([0-9]*)/.exec(process.version);\n/** Version of Node.js if running under Node, otherwise 0 */\nexport const nodeVersion = (matches && parseFloat(matches[1])) || 0;\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { assert } from \"../env-utils/assert.js\";\n/**\n * Represents one Job handled by a WorkerPool or WorkerFarm\n */\nexport default class WorkerJob {\n name;\n workerThread;\n isRunning = true;\n /** Promise that resolves when Job is done */\n result;\n _resolve = () => { };\n _reject = () => { };\n constructor(jobName, workerThread) {\n this.name = jobName;\n this.workerThread = workerThread;\n this.result = new Promise((resolve, reject) => {\n this._resolve = resolve;\n this._reject = reject;\n });\n }\n /**\n * Send a message to the job's worker thread\n * @param data any data structure, ideally consisting mostly of transferrable objects\n */\n postMessage(type, payload) {\n this.workerThread.postMessage({\n source: 'loaders.gl', // Lets worker ignore unrelated messages\n type,\n payload\n });\n }\n /**\n * Call to resolve the `result` Promise with the supplied value\n */\n done(value) {\n assert(this.isRunning);\n this.isRunning = false;\n this._resolve(value);\n }\n /**\n * Call to reject the `result` Promise with the supplied error\n */\n error(error) {\n assert(this.isRunning);\n this.isRunning = false;\n this._reject(error);\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n/** Browser polyfill for Node.js built-in `worker_threads` module.\n * These fills are non-functional, and just intended to ensure that\n * `import 'worker_threads` doesn't break browser builds.\n * The replacement is done in package.json browser field\n */\nexport class NodeWorker {\n terminate() { }\n}\nexport const parentPort = null;\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { assert } from \"../env-utils/assert.js\";\nconst workerURLCache = new Map();\n/**\n * Creates a loadable URL from worker source or URL\n * that can be used to create `Worker` instances.\n * Due to CORS issues it may be necessary to wrap a URL in a small importScripts\n * @param props\n * @param props.source Worker source\n * @param props.url Worker URL\n * @returns loadable url\n */\nexport function getLoadableWorkerURL(props) {\n assert((props.source && !props.url) || (!props.source && props.url)); // Either source or url must be defined\n let workerURL = workerURLCache.get(props.source || props.url);\n if (!workerURL) {\n // Differentiate worker urls from worker source code\n if (props.url) {\n workerURL = getLoadableWorkerURLFromURL(props.url);\n workerURLCache.set(props.url, workerURL);\n }\n if (props.source) {\n workerURL = getLoadableWorkerURLFromSource(props.source);\n workerURLCache.set(props.source, workerURL);\n }\n }\n assert(workerURL);\n return workerURL;\n}\n/**\n * Build a loadable worker URL from worker URL\n * @param url\n * @returns loadable URL\n */\nfunction getLoadableWorkerURLFromURL(url) {\n // A local script url, we can use it to initialize a Worker directly\n if (!url.startsWith('http')) {\n return url;\n }\n // A remote script, we need to use `importScripts` to load from different origin\n const workerSource = buildScriptSource(url);\n return getLoadableWorkerURLFromSource(workerSource);\n}\n/**\n * Build a loadable worker URL from worker source\n * @param workerSource\n * @returns loadable url\n */\nfunction getLoadableWorkerURLFromSource(workerSource) {\n const blob = new Blob([workerSource], { type: 'application/javascript' });\n return URL.createObjectURL(blob);\n}\n/**\n * Per spec, worker cannot be initialized with a script from a different origin\n * However a local worker script can still import scripts from other origins,\n * so we simply build a wrapper script.\n *\n * @param workerUrl\n * @returns source\n */\nfunction buildScriptSource(workerUrl) {\n return `\\\ntry {\n importScripts('${workerUrl}');\n} catch (error) {\n console.error(error);\n throw error;\n}`;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// NOTE - there is a copy of this function is both in core and loader-utils\n// core does not need all the utils in loader-utils, just this one.\n/**\n * Returns an array of Transferrable objects that can be used with postMessage\n * https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage\n * @param object data to be sent via postMessage\n * @param recursive - not for application use\n * @param transfers - not for application use\n * @returns a transfer list that can be passed to postMessage\n */\nexport function getTransferList(object, recursive = true, transfers) {\n // Make sure that items in the transfer list is unique\n const transfersSet = transfers || new Set();\n if (!object) {\n // ignore\n }\n else if (isTransferable(object)) {\n transfersSet.add(object);\n }\n else if (isTransferable(object.buffer)) {\n // Typed array\n transfersSet.add(object.buffer);\n }\n else if (ArrayBuffer.isView(object)) {\n // object is a TypeArray viewing into a SharedArrayBuffer (not transferable)\n // Do not iterate through the content in this case\n }\n else if (recursive && typeof object === 'object') {\n for (const key in object) {\n // Avoid perf hit - only go one level deep\n getTransferList(object[key], recursive, transfersSet);\n }\n }\n // If transfers is defined, is internal recursive call\n // Otherwise it's called by the user\n return transfers === undefined ? Array.from(transfersSet) : [];\n}\n// https://developer.mozilla.org/en-US/docs/Web/API/Transferable\nfunction isTransferable(object) {\n if (!object) {\n return false;\n }\n if (object instanceof ArrayBuffer) {\n return true;\n }\n if (typeof MessagePort !== 'undefined' && object instanceof MessagePort) {\n return true;\n }\n if (typeof ImageBitmap !== 'undefined' && object instanceof ImageBitmap) {\n return true;\n }\n // @ts-ignore\n if (typeof OffscreenCanvas !== 'undefined' && object instanceof OffscreenCanvas) {\n return true;\n }\n return false;\n}\n/**\n * Recursively drop non serializable values like functions and regexps.\n * @param object\n */\nexport function getTransferListForWriter(object) {\n if (object === null) {\n return {};\n }\n const clone = Object.assign({}, object);\n Object.keys(clone).forEach((key) => {\n // Typed Arrays and Arrays are passed with no change\n if (typeof object[key] === 'object' &&\n !ArrayBuffer.isView(object[key]) &&\n !(object[key] instanceof Array)) {\n clone[key] = getTransferListForWriter(object[key]);\n }\n else if (typeof clone[key] === 'function' || clone[key] instanceof RegExp) {\n clone[key] = {};\n }\n else {\n clone[key] = object[key];\n }\n });\n return clone;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { NodeWorker } from \"../node/worker_threads.js\";\nimport { isBrowser } from \"../env-utils/globals.js\";\nimport { assert } from \"../env-utils/assert.js\";\nimport { getLoadableWorkerURL } from \"../worker-utils/get-loadable-worker-url.js\";\nimport { getTransferList } from \"../worker-utils/get-transfer-list.js\";\nconst NOOP = () => { };\n/**\n * Represents one worker thread\n */\nexport default class WorkerThread {\n name;\n source;\n url;\n terminated = false;\n worker;\n onMessage;\n onError;\n _loadableURL = '';\n /** Checks if workers are supported on this platform */\n static isSupported() {\n return ((typeof Worker !== 'undefined' && isBrowser) ||\n (typeof NodeWorker !== 'undefined' && !isBrowser));\n }\n constructor(props) {\n const { name, source, url } = props;\n assert(source || url); // Either source or url must be defined\n this.name = name;\n this.source = source;\n this.url = url;\n this.onMessage = NOOP;\n this.onError = (error) => console.log(error); // eslint-disable-line\n this.worker = isBrowser ? this._createBrowserWorker() : this._createNodeWorker();\n }\n /**\n * Terminate this worker thread\n * @note Can free up significant memory\n */\n destroy() {\n this.onMessage = NOOP;\n this.onError = NOOP;\n this.worker.terminate(); // eslint-disable-line @typescript-eslint/no-floating-promises\n this.terminated = true;\n }\n get isRunning() {\n return Boolean(this.onMessage);\n }\n /**\n * Send a message to this worker thread\n * @param data any data structure, ideally consisting mostly of transferrable objects\n * @param transferList If not supplied, calculated automatically by traversing data\n */\n postMessage(data, transferList) {\n transferList = transferList || getTransferList(data);\n // @ts-ignore\n this.worker.postMessage(data, transferList);\n }\n // PRIVATE\n /**\n * Generate a standard Error from an ErrorEvent\n * @param event\n */\n _getErrorFromErrorEvent(event) {\n // Note Error object does not have the expected fields if loading failed completely\n // https://developer.mozilla.org/en-US/docs/Web/API/Worker#Event_handlers\n // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent\n let message = 'Failed to load ';\n message += `worker ${this.name} from ${this.url}. `;\n if (event.message) {\n message += `${event.message} in `;\n }\n // const hasFilename = event.filename && !event.filename.startsWith('blob:');\n // message += hasFilename ? event.filename : this.source.slice(0, 100);\n if (event.lineno) {\n message += `:${event.lineno}:${event.colno}`;\n }\n return new Error(message);\n }\n /**\n * Creates a worker thread on the browser\n */\n _createBrowserWorker() {\n this._loadableURL = getLoadableWorkerURL({ source: this.source, url: this.url });\n const worker = new Worker(this._loadableURL, { name: this.name });\n worker.onmessage = (event) => {\n if (!event.data) {\n this.onError(new Error('No data received'));\n }\n else {\n this.onMessage(event.data);\n }\n };\n // This callback represents an uncaught exception in the worker thread\n worker.onerror = (error) => {\n this.onError(this._getErrorFromErrorEvent(error));\n this.terminated = true;\n };\n // TODO - not clear when this would be called, for now just log in case it happens\n worker.onmessageerror = (event) => console.error(event); // eslint-disable-line\n return worker;\n }\n /**\n * Creates a worker thread in node.js\n * @todo https://nodejs.org/api/async_hooks.html#async-resource-worker-pool\n */\n _createNodeWorker() {\n let worker;\n if (this.url) {\n // Make sure relative URLs start with './'\n const absolute = this.url.includes(':/') || this.url.startsWith('/');\n const url = absolute ? this.url : `./${this.url}`;\n // console.log('Starting work from', url);\n worker = new NodeWorker(url, { eval: false });\n }\n else if (this.source) {\n worker = new NodeWorker(this.source, { eval: true });\n }\n else {\n throw new Error('no worker');\n }\n worker.on('message', (data) => {\n // console.error('message', data);\n this.onMessage(data);\n });\n worker.on('error', (error) => {\n // console.error('error', error);\n this.onError(error);\n });\n worker.on('exit', (code) => {\n // console.error('exit', code);\n });\n return worker;\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { isMobile, isBrowser } from \"../env-utils/globals.js\";\nimport WorkerThread from \"./worker-thread.js\";\nimport WorkerJob from \"./worker-job.js\";\n/**\n * Process multiple data messages with small pool of identical workers\n */\nexport default class WorkerPool {\n name = 'unnamed';\n source; // | Function;\n url;\n maxConcurrency = 1;\n maxMobileConcurrency = 1;\n onDebug = () => { };\n reuseWorkers = true;\n props = {};\n jobQueue = [];\n idleQueue = [];\n count = 0;\n isDestroyed = false;\n /** Checks if workers are supported on this platform */\n static isSupported() {\n return WorkerThread.isSupported();\n }\n /**\n * @param processor - worker function\n * @param maxConcurrency - max count of workers\n */\n constructor(props) {\n this.source = props.source;\n this.url = props.url;\n this.setProps(props);\n }\n /**\n * Terminates all workers in the pool\n * @note Can free up significant memory\n */\n destroy() {\n // Destroy idle workers, active Workers will be destroyed on completion\n this.idleQueue.forEach((worker) => worker.destroy());\n this.isDestroyed = true;\n }\n setProps(props) {\n this.props = { ...this.props, ...props };\n if (props.name !== undefined) {\n this.name = props.name;\n }\n if (props.maxConcurrency !== undefined) {\n this.maxConcurrency = props.maxConcurrency;\n }\n if (props.maxMobileConcurrency !== undefined) {\n this.maxMobileConcurrency = props.maxMobileConcurrency;\n }\n if (props.reuseWorkers !== undefined) {\n this.reuseWorkers = props.reuseWorkers;\n }\n if (props.onDebug !== undefined) {\n this.onDebug = props.onDebug;\n }\n }\n async startJob(name, onMessage = (job, type, data) => job.done(data), onError = (job, error) => job.error(error)) {\n // Promise resolves when thread starts working on this job\n const startPromise = new Promise((onStart) => {\n // Promise resolves when thread completes or fails working on this job\n this.jobQueue.push({ name, onMessage, onError, onStart });\n return this;\n });\n this._startQueuedJob(); // eslint-disable-line @typescript-eslint/no-floating-promises\n return await startPromise;\n }\n // PRIVATE\n /**\n * Starts first queued job if worker is available or can be created\n * Called when job is started and whenever a worker returns to the idleQueue\n */\n async _startQueuedJob() {\n if (!this.jobQueue.length) {\n return;\n }\n const workerThread = this._getAvailableWorker();\n if (!workerThread) {\n return;\n }\n // We have a worker, dequeue and start the job\n const queuedJob = this.jobQueue.shift();\n if (queuedJob) {\n // Emit a debug event\n // @ts-ignore\n this.onDebug({\n message: 'Starting job',\n name: queuedJob.name,\n workerThread,\n backlog: this.jobQueue.length\n });\n // Create a worker job to let the app access thread and manage job completion\n const job = new WorkerJob(queuedJob.name, workerThread);\n // Set the worker thread's message handlers\n workerThread.onMessage = (data) => queuedJob.onMessage(job, data.type, data.payload);\n workerThread.onError = (error) => queuedJob.onError(job, error);\n // Resolve the start promise so that the app can start sending messages to worker\n queuedJob.onStart(job);\n // Wait for the app to signal that the job is complete, then return worker to queue\n try {\n await job.result;\n }\n catch (error) {\n // eslint-disable-next-line no-console\n console.error(`Worker exception: ${error}`);\n }\n finally {\n this.returnWorkerToQueue(workerThread);\n }\n }\n }\n /**\n * Returns a worker to the idle queue\n * Destroys the worker if\n * - pool is destroyed\n * - if this pool doesn't reuse workers\n * - if maxConcurrency has been lowered\n * @param worker\n */\n returnWorkerToQueue(worker) {\n const shouldDestroyWorker = \n // Workers on Node.js prevent the process from exiting.\n // Until we figure out how to close them before exit, we always destroy them\n !isBrowser ||\n // If the pool is destroyed, there is no reason to keep the worker around\n this.isDestroyed ||\n // If the app has disabled worker reuse, any completed workers should be destroyed\n !this.reuseWorkers ||\n // If concurrency has been lowered, this worker might be surplus to requirements\n this.count > this._getMaxConcurrency();\n if (shouldDestroyWorker) {\n worker.destroy();\n this.count--;\n }\n else {\n this.idleQueue.push(worker);\n }\n if (!this.isDestroyed) {\n this._startQueuedJob(); // eslint-disable-line @typescript-eslint/no-floating-promises\n }\n }\n /**\n * Returns idle worker or creates new worker if maxConcurrency has not been reached\n */\n _getAvailableWorker() {\n // If a worker has completed and returned to the queue, it can be used\n if (this.idleQueue.length > 0) {\n return this.idleQueue.shift() || null;\n }\n // Create fresh worker if we haven't yet created the max amount of worker threads for this worker source\n if (this.count < this._getMaxConcurrency()) {\n this.count++;\n const name = `${this.name.toLowerCase()} (#${this.count} of ${this.maxConcurrency})`;\n return new WorkerThread({ name, source: this.source, url: this.url });\n }\n // No worker available, have to wait\n return null;\n }\n _getMaxConcurrency() {\n return isMobile ? this.maxMobileConcurrency : this.maxConcurrency;\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport WorkerPool from \"./worker-pool.js\";\nimport WorkerThread from \"./worker-thread.js\";\nconst DEFAULT_PROPS = {\n maxConcurrency: 3,\n maxMobileConcurrency: 1,\n reuseWorkers: true,\n onDebug: () => { }\n};\n/**\n * Process multiple jobs with a \"farm\" of different workers in worker pools.\n */\nexport default class WorkerFarm {\n props;\n workerPools = new Map();\n // singleton\n static _workerFarm;\n /** Checks if workers are supported on this platform */\n static isSupported() {\n return WorkerThread.isSupported();\n }\n /** Get the singleton instance of the global worker farm */\n static getWorkerFarm(props = {}) {\n WorkerFarm._workerFarm = WorkerFarm._workerFarm || new WorkerFarm({});\n WorkerFarm._workerFarm.setProps(props);\n return WorkerFarm._workerFarm;\n }\n /** get global instance with WorkerFarm.getWorkerFarm() */\n constructor(props) {\n this.props = { ...DEFAULT_PROPS };\n this.setProps(props);\n /** @type Map<string, WorkerPool>} */\n this.workerPools = new Map();\n }\n /**\n * Terminate all workers in the farm\n * @note Can free up significant memory\n */\n destroy() {\n for (const workerPool of this.workerPools.values()) {\n workerPool.destroy();\n }\n this.workerPools = new Map();\n }\n /**\n * Set props used when initializing worker pools\n * @param props\n */\n setProps(props) {\n this.props = { ...this.props, ...props };\n // Update worker pool props\n for (const workerPool of this.workerPools.values()) {\n workerPool.setProps(this._getWorkerPoolProps());\n }\n }\n /**\n * Returns a worker pool for the specified worker\n * @param options - only used first time for a specific worker name\n * @param options.name - the name of the worker - used to identify worker pool\n * @param options.url -\n * @param options.source -\n * @example\n * const job = WorkerFarm.getWorkerFarm().getWorkerPool({name, url}).startJob(...);\n */\n getWorkerPool(options) {\n const { name, source, url } = options;\n let workerPool = this.workerPools.get(name);\n if (!workerPool) {\n workerPool = new WorkerPool({\n name,\n source,\n url\n });\n workerPool.setProps(this._getWorkerPoolProps());\n this.workerPools.set(name, workerPool);\n }\n return workerPool;\n }\n _getWorkerPoolProps() {\n return {\n maxConcurrency: this.props.maxConcurrency,\n maxMobileConcurrency: this.props.maxMobileConcurrency,\n reuseWorkers: this.props.reuseWorkers,\n onDebug: this.props.onDebug\n };\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { getTransferList } from \"../worker-utils/get-transfer-list.js\";\n// import type {TransferListItem} from '../node/worker_threads';\nimport { parentPort } from \"../node/worker_threads.js\";\n/** Vile hack to defeat over-zealous bundlers from stripping out the require */\nasync function getParentPort() {\n // const isNode = globalThis.process;\n // let parentPort;\n // try {\n // // prettier-ignore\n // eval('globalThis.parentPort = require(\\'worker_threads\\').parentPort'); // eslint-disable-line no-eval\n // parentPort = globalThis.parentPort;\n // } catch {\n // try {\n // // prettier-ignore\n // eval('globalThis.workerThreadsPromise = import(\\'worker_threads\\')'); // eslint-disable-line no-eval\n // const workerThreads = await globalThis.workerThreadsPromise;\n // parentPort = workerThreads.parentPort;\n // } catch (error) {\n // console.error((error as Error).message); // eslint-disable-line no-console\n // }\n // }\n return parentPort;\n}\nconst onMessageWrapperMap = new Map();\n/**\n * Type safe wrapper for worker code\n */\nexport default class WorkerBody {\n /** Check that we are actually in a worker thread */\n static async inWorkerThread() {\n return typeof self !== 'undefined' || Boolean(await getParentPort());\n }\n /*\n * (type: WorkerMessageType, payload: WorkerMessagePayload) => any\n */\n static set onmessage(onMessage) {\n async function handleMessage(message) {\n const parentPort = await getParentPort();\n // Confusingly the message itself also has a 'type' field which is always set to 'message'\n const { type, payload } = parentPort ? message : message.data;\n // if (!isKnownMessage(message)) {\n // return;\n // }\n onMessage(type, payload);\n }\n getParentPort().then((parentPort) => {\n if (parentPort) {\n parentPort.on('message', (message) => {\n handleMessage(message);\n });\n // if (message == 'exit') { parentPort.unref(); }\n // eslint-disable-next-line\n parentPort.on('exit', () => console.debug('Node worker closing'));\n }\n else {\n // eslint-disable-next-line no-restricted-globals\n globalThis.onmessage = handleMessage;\n }\n });\n }\n static async addEventListener(onMessage) {\n let onMessageWrapper = onMessageWrapperMap.get(onMessage);\n if (!onMessageWrapper) {\n onMessageWrapper = async (message) => {\n if (!isKnownMessage(message)) {\n return;\n }\n const parentPort = await getParentPort();\n // Confusingly in the browser, the message itself also has a 'type' field which is always set to 'message'\n const { type, payload } = parentPort ? message : message.data;\n onMessage(type, payload);\n };\n }\n const parentPort = await getParentPort();\n if (parentPort) {\n console.error('not implemented'); // eslint-disable-line\n }\n else {\n globalThis.addEventListener('message', onMessageWrapper);\n }\n }\n static async removeEventListener(onMessage) {\n const onMessageWrapper = onMessageWrapperMap.get(onMessage);\n onMessageWrapperMap.delete(onMessage);\n const parentPort = await getParentPort();\n if (parentPort) {\n console.error('not implemented'); // eslint-disable-line\n }\n else {\n globalThis.removeEventListener('message', onMessageWrapper);\n }\n }\n /**\n * Send a message from a worker to creating thread (main thread)\n * @param type\n * @param payload\n */\n static async postMessage(type, payload) {\n const data = { source: 'loaders.gl', type, payload };\n // console.log('posting message', data);\n // Cast to Node compatible transfer list\n const transferList = getTransferList(payload);\n const parentPort = await getParentPort();\n if (parentPort) {\n parentPort.postMessage(data, transferList);\n // console.log('posted message', data);\n }\n else {\n // @ts-expect-error Outside of worker scopes this call has a third parameter\n globalThis.postMessage(data, transferList);\n }\n }\n}\n// Filter out noise messages sent to workers\nfunction isKnownMessage(message) {\n const { type, data } = message;\n return (type === 'message' &&\n data &&\n typeof data.source === 'string' &&\n data.source.startsWith('loaders.gl'));\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { assert } from \"../env-utils/assert.js\";\nimport { isBrowser } from \"../env-utils/globals.js\";\nimport { VERSION, NPM_TAG } from \"../env-utils/version.js\";\n/**\n * Gets worker object's name (for debugging in Chrome thread inspector window)\n */\nexport function getWorkerName(worker) {\n const warning = worker.version !== VERSION ? ` (worker-utils@${VERSION})` : '';\n return `${worker.name}@${worker.version}${warning}`;\n}\n/**\n * Generate a worker URL based on worker object and options\n * @returns A URL to one of the following:\n * - a published worker on unpkg CDN\n * - a local test worker\n * - a URL provided by the user in options\n */\nexport function getWorkerURL(worker, options = {}) {\n const workerOptions = options[worker.id] || {};\n const workerFile = isBrowser ? `${worker.id}-worker.js` : `${worker.id}-worker-node.js`;\n let url = workerOptions.workerUrl;\n // HACK: Allow for non-nested workerUrl for the CompressionWorker.\n // For the compression worker, workerOptions is currently not nested correctly. For most loaders,\n // you'd have options within an object, i.e. `{mvt: {coordinates: ...}}` but the CompressionWorker\n // puts options at the top level, not within a `compression` key (its `id`). For this reason, the\n // above `workerOptions` will always be a string (i.e. `'gzip'`) for the CompressionWorker. To not\n // break backwards compatibility, we allow the CompressionWorker to have options at the top level.\n if (!url && worker.id === 'compression') {\n url = options.workerUrl;\n }\n // If URL is test, generate local loaders.gl url\n // @ts-ignore _workerType\n if (options._workerType === 'test') {\n if (isBrowser) {\n url = `modules/${worker.module}/dist/${workerFile}`;\n }\n else {\n // In the test environment the ts-node loader requires TypeScript code\n url = `modules/${worker.module}/src/workers/${worker.id}-worker-node.ts`;\n }\n }\n // If url override is not provided, generate a URL to published version on npm CDN unpkg.com\n if (!url) {\n // GENERATE\n let version = worker.version;\n // On master we need to load npm alpha releases published with the `beta` tag\n if (version === 'latest') {\n // throw new Error('latest worker version specified');\n version = NPM_TAG;\n }\n const versionTag = version ? `@${version}` : '';\n url = `https://unpkg.com/@loaders.gl/${worker.module}${versionTag}/dist/${workerFile}`;\n }\n assert(url);\n // Allow user to override location\n return url;\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport WorkerFarm from \"../worker-farm/worker-farm.js\";\nimport { getWorkerURL, getWorkerName } from \"./get-worker-url.js\";\nimport { getTransferListForWriter } from \"../worker-utils/get-transfer-list.js\";\n/**\n * Determines if we can parse with worker\n * @param loader\n * @param data\n * @param options\n */\nexport function canProcessOnWorker(worker, options) {\n if (!WorkerFarm.isSupported()) {\n return false;\n }\n return worker.worker && options?.worker;\n}\n/**\n * This function expects that the worker thread sends certain messages,\n * Creating such a worker can be automated if the worker is wrapper by a call to\n * createWorker in @loaders.gl/worker-utils.\n */\nexport async function processOnWorker(worker, data, options = {}, context = {}) {\n const name = getWorkerName(worker);\n const workerFarm = WorkerFarm.getWorkerFarm(options);\n const { source } = options;\n const workerPoolProps = { name, source };\n if (!source) {\n workerPoolProps.url = getWorkerURL(worker, options);\n }\n const workerPool = workerFarm.getWorkerPool(workerPoolProps);\n const jobName = options.jobName || worker.name;\n const job = await workerPool.startJob(jobName, \n // eslint-disable-next-line\n onMessage.bind(null, context));\n // Kick off the processing in the worker\n const transferableOptions = getTransferListForWriter(options);\n job.postMessage('process', { input: data, options: transferableOptions });\n const result = await job.result;\n return result.result;\n}\n/**\n * Job completes when we receive the result\n * @param job\n * @param message\n */\nasync function onMessage(context, job, type, payload) {\n switch (type) {\n case 'done':\n // Worker is done\n job.done(payload);\n break;\n case 'error':\n // Worker encountered an error\n job.error(new Error(payload.error));\n break;\n case 'process':\n // Worker is asking for us (main thread) to process something\n const { id, input, options } = payload;\n try {\n if (!context.process) {\n job.postMessage('error', { id, error: 'Worker not set up to process on main thread' });\n return;\n }\n const result = await context.process(input, options);\n job.postMessage('done', { id, result });\n }\n catch (error) {\n const message = error instanceof Error ? error.message : 'unknown error';\n job.postMessage('error', { id, error: message });\n }\n break;\n default:\n // eslint-disable-next-line\n console.warn(`process-on-worker: unknown message ${type}`);\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// From https://github.com/rauschma/async-iter-demo/tree/master/src under MIT license\n// http://2ality.com/2016/10/asynchronous-iteration.html\n/**\n * Async Queue\n * - AsyncIterable: An async iterator can be\n * - Values can be pushed onto the queue\n * @example\n * const asyncQueue = new AsyncQueue();\n * setTimeout(() => asyncQueue.enqueue('tick'), 1000);\n * setTimeout(() => asyncQueue.enqueue(new Error('done')), 10000);\n * for await (const value of asyncQueue) {\n * console.log(value); // tick\n * }\n */\nexport default class AsyncQueue {\n _values;\n _settlers;\n _closed;\n constructor() {\n this._values = []; // enqueues > dequeues\n this._settlers = []; // dequeues > enqueues\n this._closed = false;\n }\n /** Return an async iterator for this queue */\n [Symbol.asyncIterator]() {\n return this;\n }\n /** Push a new value - the async iterator will yield a promise resolved to this value */\n push(value) {\n return this.enqueue(value);\n }\n /**\n * Push a new value - the async iterator will yield a promise resolved to this value\n * Add an error - the async iterator will yield a promise rejected with this value\n */\n enqueue(value) {\n if (this._closed) {\n throw new Error('Closed');\n }\n if (this._settlers.length > 0) {\n if (this._values.length > 0) {\n throw new Error('Illegal internal state');\n }\n const settler = this._settlers.shift();\n if (value instanceof Error) {\n settler.reject(value);\n }\n else {\n settler.resolve({ value });\n }\n }\n else {\n this._values.push(value);\n }\n }\n /** Indicate that we not waiting for more values - The async iterator will be done */\n close() {\n while (this._settlers.length > 0) {\n const settler = this._settlers.shift();\n settler.resolve({ done: true });\n }\n this._closed = true;\n }\n // ITERATOR IMPLEMENTATION\n /** @returns a Promise for an IteratorResult */\n next() {\n // If values in queue, yield the first value\n if (this._values.length > 0) {\n const value = this._values.shift();\n if (value instanceof Error) {\n return Promise.reject(value);\n }\n return Promise.resolve({ done: false, value });\n }\n // If queue is closed, the iterator is done\n if (this._closed) {\n if (this._settlers.length > 0) {\n throw new Error('Illegal internal state');\n }\n return Promise.resolve({ done: true, value: undefined });\n }\n // Yield a promise that waits for new values to be enqueued\n return new Promise((resolve, reject) => {\n this._settlers.push({ resolve, reject });\n });\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport AsyncQueue from \"../async-queue/async-queue.js\";\nimport WorkerBody from \"../worker-farm/worker-body.js\";\n// import {validateWorkerVersion} from './validate-worker-version';\n/** Counter for jobs */\nlet requestId = 0;\nlet inputBatches;\nlet options;\n/**\n * Set up a WebWorkerGlobalScope to talk with the main thread\n */\nexport async function createWorker(process, processInBatches) {\n if (!(await WorkerBody.inWorkerThread())) {\n return;\n }\n const context = {\n process: processOnMainThread\n };\n // eslint-disable-next-line complexity\n WorkerBody.onmessage = async (type, payload) => {\n try {\n switch (type) {\n case 'process':\n if (!process) {\n throw new Error('Worker does not support atomic processing');\n }\n const result = await process(payload.input, payload.options || {}, context);\n WorkerBody.postMessage('done', { result });\n break;\n case 'process-in-batches':\n if (!processInBatches) {\n throw new Error('Worker does not support batched processing');\n }\n inputBatches = new AsyncQueue();\n options = payload.options || {};\n const resultIterator = processInBatches(inputBatches, options, context);\n for await (const batch of resultIterator) {\n WorkerBody.postMessage('output-batch', { result: batch });\n }\n WorkerBody.postMessage('done', {});\n break;\n case 'input-batch':\n inputBatches.push(payload.input);\n break;\n case 'input-done':\n inputBatches.close();\n break;\n default:\n }\n }\n catch (error) {\n const message = error instanceof Error ? error.message : '';\n WorkerBody.postMessage('error', { error: message });\n }\n };\n}\nfunction processOnMainThread(arrayBuffer, options = {}) {\n return new Promise((resolve, reject) => {\n const id = requestId++;\n /**\n */\n const onMessage = (type, payload) => {\n if (payload.id !== id) {\n // not ours\n return;\n }\n switch (type) {\n case 'done':\n WorkerBody.removeEventListener(onMessage);\n resolve(payload.result);\n break;\n case 'error':\n WorkerBody.removeEventListener(onMessage);\n reject(payload.error);\n break;\n default:\n // ignore\n }\n };\n WorkerBody.addEventListener(onMessage);\n // Ask the main thread to decode data\n const payload = { id, input: arrayBuffer, options };\n WorkerBody.postMessage('process', payload);\n });\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { assert } from \"../env-utils/assert.js\";\nimport { VERSION } from \"../env-utils/version.js\";\n/**\n * Check if worker is compatible with this library version\n * @param worker\n * @param libVersion\n * @returns `true` if the two versions are compatible\n */\nexport function validateWorkerVersion(worker, coreVersion = VERSION) {\n assert(worker, 'no worker provided');\n const workerVersion = worker.version;\n if (!coreVersion || !workerVersion) {\n return false;\n }\n // TODO enable when fix the __version__ injection\n // const coreVersions = parseVersion(coreVersion);\n // const workerVersions = parseVersion(workerVersion);\n // assert(\n // coreVersion.major === workerVersion.major && coreVersion.minor <= workerVersion.minor,\n // `worker: ${worker.name} is not compatible. ${coreVersion.major}.${\n // coreVersion.minor\n // }+ is required.`\n // );\n return true;\n}\n// @ts-ignore\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction parseVersion(version) {\n const parts = version.split('.').map(Number);\n return { major: parts[0], minor: parts[1] };\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n/* global importScripts */\nimport { isBrowser, isWorker } from \"../env-utils/globals.js\";\nimport { assert } from \"../env-utils/assert.js\";\nimport { VERSION } from \"../env-utils/version.js\";\nconst loadLibraryPromises = {}; // promises\n/**\n * Dynamically loads a library (\"module\")\n *\n * - wasm library: Array buffer is returned\n * - js library: Parse JS is returned\n *\n * Method depends on environment\n * - browser - script element is created and installed on document\n * - worker - eval is called on global context\n * - node - file is required\n *\n * @param libraryUrl\n * @param moduleName\n * @param options\n */\nexport async function loadLibrary(libraryUrl, moduleName = null, options = {}, libraryName = null) {\n if (moduleName) {\n libraryUrl = getLibraryUrl(libraryUrl, moduleName, options, libraryName);\n }\n // Ensure libraries are only loaded once\n loadLibraryPromises[libraryUrl] =\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n loadLibraryPromises[libraryUrl] || loadLibraryFromFile(libraryUrl);\n return await loadLibraryPromises[libraryUrl];\n}\n// TODO - sort out how to resolve paths for main/worker and dev/prod\nexport function getLibraryUrl(library, moduleName, options = {}, libraryName = null) {\n // Check if already a URL\n if (!options.useLocalLibraries && library.startsWith('http')) {\n return library;\n }\n libraryName = libraryName || library;\n // Allow application to import and supply libraries through `options.modules`\n // TODO - See js-module-utils in loader-utils\n const modules = options.modules || {};\n if (modules[libraryName]) {\n return modules[libraryName];\n }\n // Load from local files, not from CDN scripts in Node.js\n // TODO - needs to locate the modules directory when installed!\n if (!isBrowser) {\n return `modules/${moduleName}/dist/libs/${libraryName}`;\n }\n // In browser, load from external scripts\n if (options.CDN) {\n assert(options.CDN.startsWith('http'));\n return `${options.CDN}/${moduleName}@${VERSION}/dist/libs/${libraryName}`;\n }\n // TODO - loading inside workers requires paths relative to worker script location...\n if (isWorker) {\n return `../src/libs/${libraryName}`;\n }\n return `modules/${moduleName}/src/libs/${libraryName}`;\n}\nasync function loadLibraryFromFile(libraryUrl) {\n if (libraryUrl.endsWith('wasm')) {\n return await loadAsArrayBuffer(libraryUrl);\n }\n if (!isBrowser) {\n // TODO - Node doesn't yet support dynamic import from https URLs\n // try {\n // return await import(libraryUrl);\n // } catch (error) {\n // console.error(error);\n // }\n try {\n const { requireFromFile } = globalThis.loaders || {};\n return await requireFromFile?.(libraryUrl);\n }\n catch (error) {\n console.error(error); // eslint-disable-line no-console\n return null;\n }\n }\n if (isWorker) {\n return importScripts(libraryUrl);\n }\n // TODO - fix - should be more secure than string parsing since observes CORS\n // if (isBrowser) {\n // return await loadScriptFromFile(libraryUrl);\n // }\n const scriptSource = await loadAsText(libraryUrl);\n return loadLibraryFromString(scriptSource, libraryUrl);\n}\n/*\nasync function loadScriptFromFile(libraryUrl) {\n const script = document.createElement('script');\n script.src = libraryUrl;\n return await new Promise((resolve, reject) => {\n script.onload = data => {\n resolve(data);\n };\n script.onerror = reject;\n });\n}\n*/\n// TODO - Needs security audit...\n// - Raw eval call\n// - Potentially bypasses CORS\n// Upside is that this separates fetching and parsing\n// we could create a`LibraryLoader` or`ModuleLoader`\nfunction loadLibraryFromString(scriptSource, id) {\n if (!isBrowser) {\n const { requireFromString } = globalThis.loaders || {};\n return requireFromString?.(scriptSource, id);\n }\n if (isWorker) {\n