UNPKG

@naturalcycles/nodejs-lib

Version:
80 lines (79 loc) 3.22 kB
import { Transform } from 'node:stream'; import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'; import { pDefer } from '@naturalcycles/js-lib/promise/pDefer.js'; /** * Transform that gradually increases concurrency from 1 to the configured maximum * over a warmup period. Useful for scenarios where you want to avoid overwhelming * a system at startup (e.g., database connections, API rate limits). * * During warmup: limits concurrent items based on elapsed time. * After warmup: passes items through immediately with zero overhead. * * @experimental */ export function transformWarmup(opt) { const { concurrency, warmupSeconds, objectMode = true, highWaterMark = 1 } = opt; const warmupMs = warmupSeconds * 1000; const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel); let startTime = 0; let warmupComplete = warmupSeconds <= 0 || concurrency <= 1; let inFlight = 0; const waiters = []; return new Transform({ objectMode, highWaterMark, async transform(item, _, cb) { // Initialize start time on first item if (startTime === 0) { startTime = Date.now(); } // Fast-path: after warmup, just pass through with zero overhead if (warmupComplete) { cb(null, item); return; } const currentConcurrency = getCurrentConcurrency(); if (inFlight < currentConcurrency) { // Have room, proceed immediately inFlight++; logger.debug(`inFlight++ ${inFlight}/${currentConcurrency}, waiters ${waiters.length}`); } else { // Wait for a slot const waiter = pDefer(); waiters.push(waiter); logger.debug(`inFlight ${inFlight}/${currentConcurrency}, waiters++ ${waiters.length}`); await waiter; logger.debug(`waiter resolved, inFlight ${inFlight}/${getCurrentConcurrency()}`); } // Push the item cb(null, item); // Release slot on next microtask - essential for concurrency control. // Without this, the slot would be freed immediately and items would // flow through without any limiting effect. queueMicrotask(release); }, }); function getCurrentConcurrency() { if (warmupComplete) return concurrency; const elapsed = Date.now() - startTime; if (elapsed >= warmupMs) { warmupComplete = true; logger.debug('warmup complete'); return concurrency; } // Linear interpolation from 1 to concurrency const progress = elapsed / warmupMs; return Math.max(1, Math.floor(1 + (concurrency - 1) * progress)); } function release() { inFlight--; // Wake up waiters based on current concurrency (may have increased) const currentConcurrency = getCurrentConcurrency(); while (waiters.length && inFlight < currentConcurrency) { inFlight++; waiters.shift().resolve(); } } }