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.
260 lines • 9.18 kB
JavaScript
import { EventEmitter } from "events";
import { CircularBuffer } from "./circular-buffer.js";
import { GeneratorFailureError, InvalidGeneratorError, InvalidGeneratorOutputError, InvalidMinSizeError, InvalidPoolSizeError, InvalidRefillBatchSizeError, PoolEmptyError, PoolNotReadyError, PoolStoppedError, } from "./errors.js";
/**
* Enhanced BaseIdPool with improved error handling and state management
*/
export class BaseIdPoolFixed extends EventEmitter {
options;
buffer;
isRunning = false;
isReady = false;
stats;
// Track async acquires waiting for IDs
pendingAcquires = [];
constructor(options) {
super();
// Validate generator first
if (typeof options.generator !== "function") {
throw new InvalidGeneratorError();
}
// 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 (!Number.isInteger(this.options.poolSize) ||
this.options.poolSize <= 0) {
throw new InvalidPoolSizeError("poolSize must be a positive integer");
}
if (!Number.isInteger(this.options.minSize) ||
this.options.minSize < 0 ||
this.options.minSize > this.options.poolSize) {
throw new InvalidMinSizeError("minSize must be an integer between 0 and poolSize");
}
if (!Number.isInteger(this.options.refillBatchSize) ||
this.options.refillBatchSize <= 0) {
throw new InvalidRefillBatchSizeError("refillBatchSize must be a positive integer");
}
this.buffer = new CircularBuffer(this.options.poolSize);
this.stats = {
totalAcquired: 0,
refillCount: 0,
emptyHits: 0,
refillTimes: [],
};
// Set max listeners to avoid warnings
this.setMaxListeners(50);
}
/**
* Synchronously acquire an ID from the pool.
* Returns undefined if pool is empty (unless throwOnEmpty is true).
*/
acquire() {
if (!this.isRunning) {
if (this.options.throwOnEmpty) {
throw new PoolNotReadyError();
}
return undefined;
}
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().catch((err) => {
this.emit("error", err);
});
}
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() {
if (!this.isRunning) {
throw new PoolNotReadyError();
}
// Try synchronous acquisition first
try {
const id = this.acquire();
if (id !== undefined) {
return id;
}
}
catch (err) {
// If throwOnEmpty is true, acquire() will throw
// For async, we want to wait instead
if (this.options.throwOnEmpty &&
err instanceof Error &&
err.message === "ID pool is empty") {
// Continue to wait for refill
}
else {
throw err;
}
}
// Wait for refill
return new Promise((resolve, reject) => {
// Add to pending queue
this.pendingAcquires.push({ resolve, reject });
// Ensure refill is triggered
if (this.isRunning) {
this.triggerRefill().catch((err) => {
reject(err);
// Remove from pending queue
const index = this.pendingAcquires.findIndex((p) => p.resolve === resolve);
if (index > -1) {
this.pendingAcquires.splice(index, 1);
}
});
}
else {
reject(new PoolStoppedError());
}
});
}
/**
* Start the pool and begin filling it.
*/
start() {
if (this.isRunning) {
return;
}
this.isRunning = true;
this.isReady = false; // Reset ready state
this.startInternal();
// Initial fill
this.triggerRefill()
.then(() => {
if (!this.isReady && this.buffer.getSize() >= this.options.minSize) {
this.isReady = true;
this.emit("ready");
}
})
.catch((err) => {
this.emit("error", err);
});
}
/**
* Stop the pool and clean up resources.
*/
async stop() {
if (!this.isRunning) {
return;
}
this.isRunning = false;
this.isReady = false; // Reset ready state
// Reject all pending async acquires
const pending = [...this.pendingAcquires];
this.pendingAcquires = [];
for (const { reject } of pending) {
reject(new PoolStoppedError("ID pool stopped"));
}
await this.stopInternal();
this.buffer.clear();
// Clear stats
this.stats = {
totalAcquired: 0,
refillCount: 0,
emptyHits: 0,
refillTimes: [],
};
}
/**
* 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.
* Ensures all IDs are strings.
*/
async generateIds(count) {
const ids = [];
for (let i = 0; i < count; i++) {
try {
const id = await this.options.generator();
// Ensure ID is a string
if (typeof id !== "string") {
this.emit("error", new InvalidGeneratorOutputError(`Generator returned non-string value: ${typeof id}`));
// Convert to string for compatibility
ids.push(String(id));
}
else {
ids.push(id);
}
}
catch (error) {
this.emit("error", error instanceof Error
? new GeneratorFailureError(`Generator failed: ${error.message}`, error)
: new GeneratorFailureError(`Generator failed: ${String(error)}`));
// Don't break the loop - continue generating remaining IDs
}
}
return ids;
}
/**
* Add generated IDs to the buffer and emit refill event.
* Also resolves any pending async acquires.
*/
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();
}
// Resolve pending async acquires
const pending = [...this.pendingAcquires];
this.pendingAcquires = [];
for (const { resolve, reject } of pending) {
const id = this.buffer.dequeue();
if (id) {
this.stats.totalAcquired++;
resolve(id);
}
else {
// Put back in queue if we ran out of IDs
this.pendingAcquires.push({ resolve, reject });
}
}
this.emit("refill", { added, duration });
// Check if we need to trigger another refill for remaining pending acquires
if (this.pendingAcquires.length > 0 && this.isRunning) {
this.triggerRefill().catch((err) => {
this.emit("error", err);
});
}
}
}
}
//# sourceMappingURL=pool-fixes.js.map