taglib-wasm
Version:
TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers
249 lines (248 loc) • 7.55 kB
JavaScript
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
};