UNPKG

uid-pool

Version:

High-performance UUID and unique ID pooling for Node.js. Pre-generate IDs in background worker threads for O(1) synchronous acquisition. Drop-in replacement for uuid.v4() and nanoid() with 10-100x better performance under load.

207 lines (203 loc) 7.38 kB
import { Worker } from "worker_threads"; import { BaseIdPool } from "../core/pool.js"; import { WorkerCrashError, WorkerNotInitializedError, WorkerTimeoutError, WorkerGenerationError, SerializationError, } from "../core/errors.js"; export const WORKER_TIMEOUT = 5000; /** * Worker thread code for ID generation. * * This code is stored as a string and evaluated in the worker thread context. * We use this approach instead of a separate file to avoid path resolution issues * when the package is installed in different environments (especially with bundlers). * * The worker receives generator function code as a string, evaluates it, and * generates IDs in batches to avoid blocking the main thread. * * @internal */ const WORKER_THREAD_CODE = ` import { parentPort } from "worker_threads"; if (!parentPort) { throw new Error("This file must be run as a worker thread"); } parentPort.on("message", async (message) => { if (message.type === "GENERATE") { try { const generator = new Function("return " + message.generatorCode)(); const ids = []; for (let i = 0; i < message.count; i++) { const id = await generator(); ids.push(id); } parentPort.postMessage({ type: "GENERATED", ids, }); } catch (error) { parentPort.postMessage({ type: "GENERATED", ids: [], error: error.message, }); } } }); `; /** * Node.js implementation of IdPool using worker threads for non-blocking generation. */ export class IdPool extends BaseIdPool { worker = null; pendingRefill = null; constructor(options) { super(options); } /** * Create a new IdPool instance that is ready for use. */ static async create(options) { const pool = new IdPool(options); await pool.initialize(); return pool; } startInternal() { if (this.options.useWorker && !this.worker) { // Create worker from string code using eval option this.worker = new Worker(WORKER_THREAD_CODE, { eval: true, }); this.worker.on("error", (error) => { this.emit("error", error); }); this.worker.on("exit", (code) => { if (code !== 0 && this.isRunning) { this.emit("error", new WorkerCrashError(`Worker exited with code ${code}`, code)); } this.worker = null; }); } } async stopInternal() { if (this.pendingRefill) { await this.pendingRefill; } if (this.worker) { await this.worker.terminate(); this.worker = null; } } async triggerRefill() { // Avoid multiple concurrent refills if (this.pendingRefill) { return this.pendingRefill; } const refillPromise = this.doRefill(); this.pendingRefill = refillPromise; try { await refillPromise; } finally { this.pendingRefill = null; } } async doRefill() { const needed = this.buffer.getAvailableSpace(); const toGenerate = Math.min(needed, this.options.refillBatchSize); if (toGenerate <= 0) { return; } const startTime = Date.now(); try { let ids; if (this.options.useWorker && this.worker) { // Use worker thread ids = await this.generateInWorker(toGenerate); } else { // Use main thread with chunking ids = await this.generateInMainThread(toGenerate); } const duration = Date.now() - startTime; this.addToBuffer(ids, duration); } catch (error) { this.emit("error", error); } } async generateInWorker(count) { if (!this.worker) { throw new WorkerNotInitializedError(); } // Store worker reference to avoid null checking issues const worker = this.worker; return new Promise((resolve, reject) => { const { timeout, handler } = this.setupWorkerMessageHandler(worker, resolve, reject); // Type assertion is safe here as we control the worker implementation // eslint-disable-next-line @typescript-eslint/no-explicit-any worker.on("message", handler); // Send generation request to worker this.sendGenerationRequest(worker, count, timeout, handler, reject); }); } /** * Sets up timeout and message handler for worker communication. */ setupWorkerMessageHandler(worker, resolve, reject) { const timeout = setTimeout(() => { worker.off("message", handler); reject(new WorkerTimeoutError()); }, WORKER_TIMEOUT); const handler = (message) => { if (message.type === "GENERATED") { clearTimeout(timeout); worker.off("message", handler); if (message.error) { reject(new WorkerGenerationError(message.error)); } else if (message.ids) { resolve(message.ids); } else { reject(new WorkerGenerationError("Worker returned no IDs")); } } }; return { timeout, handler }; } /** * Sends generation request to worker with proper error handling. */ sendGenerationRequest(worker, count, timeout, handler, reject) { try { const generatorCode = this.options.generator.toString(); worker.postMessage({ type: "GENERATE", count, generatorCode, }); } catch (error) { clearTimeout(timeout); worker.off("message", handler); reject(error instanceof Error ? new SerializationError(`Failed to serialize generator: ${error.message}`, error) : new SerializationError(`Failed to serialize generator: ${String(error)}`)); } } async generateInMainThread(count) { const ids = []; const chunkSize = 10; // Generate in small chunks to avoid blocking for (let i = 0; i < count; i += chunkSize) { const batchSize = Math.min(chunkSize, count - i); const batch = await this.generateIds(batchSize); ids.push(...batch); // Yield to event loop if (i + chunkSize < count) { await new Promise((resolve) => setImmediate(resolve)); } } return ids; } } // Re-export error classes for convenience export { IdPoolError, InvalidConfigurationError, InvalidGeneratorError, InvalidPoolSizeError, InvalidMinSizeError, InvalidRefillBatchSizeError, InvalidCapacityError, PoolEmptyError, PoolNotReadyError, PoolStoppedError, PoolTimeoutError, GeneratorFailureError, InvalidGeneratorOutputError, WorkerNotInitializedError, WorkerTimeoutError, WorkerCrashError, WorkerGenerationError, SerializationError, ResourceExhaustionError, InvalidRuntimeError, } from "../core/errors.js"; //# sourceMappingURL=index.js.map