@naturalcycles/nodejs-lib
Version:
Standard library for Node.js
80 lines (79 loc) • 3.22 kB
JavaScript
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();
}
}
}