autotel
Version:
Write Once, Observe Anywhere
146 lines (144 loc) • 4.57 kB
JavaScript
;
// src/drain-pipeline.ts
function wait(ms) {
return new Promise((resolve) => {
const timer = setTimeout(resolve, ms);
timer.unref?.();
});
}
function createDrainPipeline(options) {
const batchSize = options?.batch?.size ?? 50;
const intervalMs = options?.batch?.intervalMs ?? 5e3;
const maxBufferSize = options?.maxBufferSize ?? 1e3;
const maxAttempts = options?.retry?.maxAttempts ?? 3;
const backoff = options?.retry?.backoff ?? "exponential";
const initialDelayMs = options?.retry?.initialDelayMs ?? 1e3;
const maxDelayMs = options?.retry?.maxDelayMs ?? 3e4;
const jitter = options?.retry?.jitter ?? true;
const dropPolicy = options?.dropPolicy ?? "oldest";
const onDropped = options?.onDropped;
if (!Number.isFinite(batchSize) || batchSize <= 0) {
throw new Error(
`[autotel/drain-pipeline] batch.size must be a positive finite number, got: ${batchSize}`
);
}
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new Error(
`[autotel/drain-pipeline] batch.intervalMs must be a positive finite number, got: ${intervalMs}`
);
}
if (!Number.isFinite(maxBufferSize) || maxBufferSize <= 0) {
throw new Error(
`[autotel/drain-pipeline] maxBufferSize must be a positive finite number, got: ${maxBufferSize}`
);
}
if (!Number.isFinite(maxAttempts) || maxAttempts <= 0) {
throw new Error(
`[autotel/drain-pipeline] retry.maxAttempts must be a positive finite number, got: ${maxAttempts}`
);
}
return (drain) => {
const buffer = [];
let timer = null;
let activeFlush = null;
let isShutdown = false;
const clearTimer = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
};
const computeDelay = (attempt) => {
const base = backoff === "fixed" ? initialDelayMs : backoff === "linear" ? initialDelayMs * attempt : initialDelayMs * 2 ** (attempt - 1);
const bounded = Math.min(base, maxDelayMs);
if (!jitter || bounded <= 0) return bounded;
const factor = 0.5 + Math.random();
return Math.max(0, Math.round(bounded * factor));
};
const sendWithRetry = async (batch) => {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await drain(batch);
return;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxAttempts) {
await wait(computeDelay(attempt));
}
}
}
onDropped?.(batch, lastError);
};
const drainBuffer = async () => {
while (buffer.length > 0) {
const batch = buffer.splice(0, batchSize);
await sendWithRetry(batch);
}
};
const scheduleFlush = () => {
if (isShutdown || timer || activeFlush) return;
timer = setTimeout(() => {
timer = null;
startFlush();
}, intervalMs);
timer.unref?.();
};
const startFlush = () => {
if (activeFlush || isShutdown) return;
activeFlush = drainBuffer().finally(() => {
activeFlush = null;
if (isShutdown) return;
if (buffer.length >= batchSize) {
startFlush();
} else if (buffer.length > 0) {
scheduleFlush();
}
});
};
const push = (ctx) => {
if (isShutdown) return;
if (buffer.length >= maxBufferSize) {
if (dropPolicy === "newest") {
onDropped?.([ctx]);
return;
}
const dropped = buffer.splice(0, 1);
onDropped?.(dropped);
}
buffer.push(ctx);
if (buffer.length >= batchSize) {
clearTimer();
startFlush();
} else {
scheduleFlush();
}
};
const flush = async () => {
clearTimer();
if (activeFlush) await activeFlush;
const snapshot = buffer.length;
if (snapshot <= 0) return;
const toFlush = buffer.splice(0, snapshot);
while (toFlush.length > 0) {
const batch = toFlush.splice(0, batchSize);
await sendWithRetry(batch);
}
};
const shutdown = async () => {
isShutdown = true;
await flush();
};
const fn = push;
fn.flush = flush;
fn.shutdown = shutdown;
Object.defineProperty(fn, "pending", {
enumerable: true,
get: () => buffer.length
});
return fn;
};
}
exports.createDrainPipeline = createDrainPipeline;
//# sourceMappingURL=chunk-7EQ4G4SI.cjs.map
//# sourceMappingURL=chunk-7EQ4G4SI.cjs.map