UNPKG

taglib-wasm

Version:

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

296 lines (295 loc) 9.32 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { WorkerError } from "./errors.js"; class TagLibWorkerPool { /** * Create a new worker pool instance. * Note: Call initialize() before using the pool. * @param options Worker pool configuration */ constructor(options = {}) { __publicField(this, "workers", []); __publicField(this, "queue", []); __publicField(this, "terminated", false); __publicField(this, "initPromise"); __publicField(this, "waitForReadyTimer"); __publicField(this, "initialized", false); __publicField(this, "size"); __publicField(this, "debug"); __publicField(this, "initTimeout"); __publicField(this, "operationTimeout"); 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; } /** * Initialize the worker pool. Must be called before using the pool. * @returns Promise that resolves when all workers are initialized */ async initialize() { if (this.initialized) return this.initPromise; if (this.initPromise) return this.initPromise; this.initPromise = this.initializeWorkers(); await this.initPromise; this.initialized = true; return this.initPromise; } /** * Wait for the worker pool to be ready */ async waitForReady() { if (!this.initPromise) { await this.initialize(); } 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) => { this.waitForReadyTimer = setTimeout(() => { this.waitForReadyTimer = void 0; resolve(); }, 100); }); if (this.terminated) { throw new WorkerError("Worker pool terminated during initialization"); } } 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"); } if (!this.initialized) { await this.initialize(); } 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; if (this.waitForReadyTimer) { clearTimeout(this.waitForReadyTimer); this.waitForReadyTimer = void 0; } 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; async function createWorkerPool(options) { const pool = new TagLibWorkerPool(options); await pool.initialize(); return pool; } function getGlobalWorkerPool(options) { globalPool ?? (globalPool = new TagLibWorkerPool(options)); return globalPool; } function terminateGlobalWorkerPool() { if (globalPool) { globalPool.terminate(); globalPool = null; } } export { TagLibWorkerPool, createWorkerPool, getGlobalWorkerPool, terminateGlobalWorkerPool };