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