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
JavaScript
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