UNPKG

taglib-wasm

Version:

TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers

249 lines (248 loc) 7.55 kB
import { WorkerError } from "./errors.js"; class TagLibWorkerPool { constructor(options = {}) { this.workers = []; this.queue = []; this.terminated = false; this.size = options.size || (typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 4) || 4; this.debug = options.debug || false; this.initTimeout = options.initTimeout || 3e4; this.operationTimeout = options.operationTimeout || 6e4; this.initPromise = this.initializeWorkers(); } /** * Wait for the worker pool to be ready */ async waitForReady() { await this.initPromise; const maxRetries = 20; for (let i = 0; i < maxRetries; i++) { if (this.workers.every((w) => w.initialized)) { return; } await new Promise((resolve) => setTimeout(resolve, 100)); } const initializedCount = this.workers.filter((w) => w.initialized).length; throw new WorkerError( `Worker pool initialization timeout: ${initializedCount}/${this.workers.length} workers initialized` ); } /** * Initialize worker threads */ async initializeWorkers() { const initPromises = []; for (let i = 0; i < this.size; i++) { const workerState = { worker: this.createWorker(), busy: false, initialized: false }; this.workers.push(workerState); const initPromise = new Promise((resolve, reject) => { workerState.initTimeout = setTimeout(() => { workerState.initTimeout = void 0; reject(new WorkerError("Worker initialization timed out")); }, this.initTimeout); const messageHandler = (e) => { if (e.data.type === "initialized") { if (workerState.initTimeout) { clearTimeout(workerState.initTimeout); workerState.initTimeout = void 0; } workerState.initialized = true; workerState.worker.removeEventListener("message", messageHandler); if (this.debug) console.log(`Worker ${i} initialized`); resolve(); } else if (e.data.type === "error") { if (workerState.initTimeout) { clearTimeout(workerState.initTimeout); workerState.initTimeout = void 0; } workerState.worker.removeEventListener("message", messageHandler); reject(new WorkerError(e.data.error)); } }; workerState.worker.addEventListener("message", messageHandler); workerState.worker.addEventListener("error", (event) => { if (workerState.initTimeout) { clearTimeout(workerState.initTimeout); workerState.initTimeout = void 0; } const message = event instanceof ErrorEvent ? event.message : String(event); reject(new WorkerError(`Worker error: ${message}`)); }); workerState.worker.postMessage({ op: "init" }); }); initPromises.push(initPromise); } await Promise.all(initPromises); } /** * Create a new worker instance */ createWorker() { try { return new Worker( new URL("./workers/taglib-worker.ts", import.meta.url), { type: "module" } ); } catch (error) { throw new WorkerError( `Failed to create worker: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Execute a task in the worker pool */ async execute(task) { if (this.terminated) { throw new WorkerError("Worker pool has been terminated"); } await this.initPromise; return new Promise((resolve, reject) => { const queuedTask = { task, resolve, reject }; if (this.operationTimeout > 0) { queuedTask.timeout = setTimeout(() => { const index = this.queue.indexOf(queuedTask); if (index !== -1) { this.queue.splice(index, 1); } reject(new WorkerError("Operation timed out")); }, this.operationTimeout); } this.queue.push(queuedTask); this.processQueue(); }); } /** * Process queued tasks */ processQueue() { if (this.queue.length === 0) return; const availableWorker = this.workers.find((w) => !w.busy && w.initialized); if (!availableWorker) return; const task = this.queue.shift(); if (!task) return; availableWorker.busy = true; const messageHandler = (e) => { if (e.data.type === "result") { availableWorker.worker.removeEventListener("message", messageHandler); availableWorker.busy = false; if (task.timeout) clearTimeout(task.timeout); task.resolve(e.data.result); this.processQueue(); } else if (e.data.type === "error") { availableWorker.worker.removeEventListener("message", messageHandler); availableWorker.busy = false; if (task.timeout) clearTimeout(task.timeout); task.reject(new WorkerError(e.data.error)); this.processQueue(); } }; availableWorker.worker.addEventListener("message", messageHandler); availableWorker.worker.postMessage(task.task); } /** * Simple API: Read tags from a file */ async readTags(file) { return this.execute({ op: "readTags", file }); } /** * Simple API: Read tags from multiple files */ async readTagsBatch(files) { return Promise.all(files.map((file) => this.readTags(file))); } /** * Simple API: Read audio properties */ async readProperties(file) { return this.execute({ op: "readProperties", file }); } /** * Simple API: Apply tags and return modified buffer */ async applyTags(file, tags) { return this.execute({ op: "applyTags", file, tags }); } /** * Simple API: Update tags on disk */ async updateTags(file, tags) { return this.execute({ op: "updateTags", file, tags }); } /** * Simple API: Read pictures */ async readPictures(file) { return this.execute({ op: "readPictures", file }); } /** * Simple API: Set cover art */ async setCoverArt(file, coverArt, mimeType) { return this.execute({ op: "setCoverArt", file, coverArt, mimeType }); } /** * Full API: Execute batch operations */ async batchOperations(file, operations) { return this.execute({ op: "batch", file, operations }); } /** * Get current pool statistics */ getStats() { return { poolSize: this.workers.length, busyWorkers: this.workers.filter((w) => w.busy).length, queueLength: this.queue.length, initialized: this.workers.every((w) => w.initialized) }; } /** * Terminate all workers and clean up resources */ terminate() { this.terminated = true; this.queue.forEach((task) => { if (task.timeout) clearTimeout(task.timeout); task.reject(new WorkerError("Worker pool terminated")); }); this.queue = []; this.workers.forEach((workerState) => { if (workerState.initTimeout) { clearTimeout(workerState.initTimeout); } workerState.worker.terminate(); }); this.workers = []; if (this.debug) console.log("Worker pool terminated"); } } let globalPool = null; function getGlobalWorkerPool(options) { if (!globalPool) { globalPool = new TagLibWorkerPool(options); } return globalPool; } function terminateGlobalWorkerPool() { if (globalPool) { globalPool.terminate(); globalPool = null; } } export { TagLibWorkerPool, getGlobalWorkerPool, terminateGlobalWorkerPool };