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.

181 lines 6.12 kB
import { EventEmitter } from "events"; import { CircularBuffer } from "./circular-buffer.js"; import { InvalidGeneratorError, InvalidPoolSizeError, InvalidMinSizeError, InvalidRefillBatchSizeError, PoolEmptyError, PoolTimeoutError, GeneratorFailureError, } from "./errors.js"; export const ACQUIRE_TIMEOUT = 10000; /** * Base implementation of IdPool with core functionality. * Extended by platform-specific implementations (Node, Edge). */ export class BaseIdPool extends EventEmitter { options; buffer; isRunning = false; stats; constructor(options) { super(); // Apply defaults this.options = { generator: options.generator, poolSize: options.poolSize ?? 1000, minSize: options.minSize ?? Math.floor((options.poolSize ?? 1000) * 0.25), refillBatchSize: options.refillBatchSize ?? Math.max(1, Math.floor((options.poolSize ?? 1000) * 0.5)), useWorker: options.useWorker ?? true, throwOnEmpty: options.throwOnEmpty ?? false, }; // Validate options if (typeof this.options.generator !== "function") { throw new InvalidGeneratorError(); } if (this.options.poolSize <= 0) { throw new InvalidPoolSizeError(); } if (this.options.minSize < 0 || this.options.minSize > this.options.poolSize) { throw new InvalidMinSizeError(); } if (this.options.refillBatchSize <= 0) { throw new InvalidRefillBatchSizeError(); } this.buffer = new CircularBuffer(this.options.poolSize); this.stats = { totalAcquired: 0, refillCount: 0, emptyHits: 0, refillTimes: [], }; } /** * Initialize the pool and wait for it to be ready. * Called by the static create() method. */ async initialize() { this.isRunning = true; this.startInternal(); // Initial fill await this.triggerRefill(); // Wait until we have at least minSize items while (this.buffer.getSize() < this.options.minSize) { await new Promise((resolve) => { this.once("refill", () => resolve()); }); } } /** * Synchronously acquire an ID from the pool. * Returns undefined if pool is empty (unless throwOnEmpty is true). */ acquire() { const id = this.buffer.dequeue(); if (id) { this.stats.totalAcquired++; // Check if we need to trigger a refill if (this.buffer.getSize() < this.options.minSize && this.isRunning) { this.triggerRefill(); } return id; } // Pool is empty this.stats.emptyHits++; this.emit("empty"); if (this.options.throwOnEmpty) { throw new PoolEmptyError(); } return undefined; } /** * Asynchronously acquire an ID, waiting for refill if necessary. */ async acquireAsync() { // Try synchronous acquisition first const id = this.acquire(); if (id !== undefined) { return id; } // Wait for refill return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.off("refill", onRefill); reject(new PoolTimeoutError()); }, ACQUIRE_TIMEOUT); const onRefill = () => { const id = this.acquire(); if (id !== undefined) { clearTimeout(timeout); this.off("refill", onRefill); resolve(id); } // If still empty, the event will fire again when more IDs are added }; this.on("refill", onRefill); // Ensure refill is triggered if (this.isRunning) { this.triggerRefill(); } }); } /** * Stop the pool and clean up resources. */ async stop() { if (!this.isRunning) { return; } this.isRunning = false; await this.stopInternal(); this.buffer.clear(); } /** * Get current pool statistics. */ getStats() { const avgRefillTime = this.stats.refillTimes.length > 0 ? this.stats.refillTimes.reduce((a, b) => a + b, 0) / this.stats.refillTimes.length : 0; return { available: this.buffer.getSize(), capacity: this.options.poolSize, totalAcquired: this.stats.totalAcquired, refillCount: this.stats.refillCount, emptyHits: this.stats.emptyHits, avgRefillTime, }; } /** * Helper method to generate IDs using the configured generator. */ async generateIds(count) { const ids = []; for (let i = 0; i < count; i++) { try { const id = await this.options.generator(); // Convert to string if not already ids.push(String(id)); } catch (error) { this.emit("error", error instanceof Error ? new GeneratorFailureError(`Generator failed: ${error.message}`, error) : new GeneratorFailureError(`Generator failed: ${String(error)}`)); break; } } return ids; } /** * Add generated IDs to the buffer and emit refill event. */ addToBuffer(ids, duration) { const added = this.buffer.enqueueBatch(ids); if (added > 0) { this.stats.refillCount++; this.stats.refillTimes.push(duration); // Keep only last 100 refill times for avg calculation if (this.stats.refillTimes.length > 100) { this.stats.refillTimes.shift(); } this.emit("refill", { added, duration }); } } } //# sourceMappingURL=pool.js.map