@oxog/delay
Version:
A comprehensive, zero-dependency delay/timeout utility library with advanced timing features
134 lines • 4.55 kB
JavaScript
import { getHighResolutionTime } from '../utils/time.js';
import { createBasicDelay } from './delay.js';
export function createBatchScheduler(options = {}) {
const { maxBatchSize = 100, batchWindow = 16 } = options; // 16ms ≈ 1 frame
const pendingDelays = [];
let batchTimeoutId;
let isProcessing = false;
const processBatch = async () => {
if (isProcessing || pendingDelays.length === 0) {
return;
}
isProcessing = true;
batchTimeoutId = undefined;
// Group delays by duration for efficiency
const delayGroups = new Map();
const itemsToProcess = pendingDelays.splice(0, maxBatchSize);
for (const item of itemsToProcess) {
const group = delayGroups.get(item.ms) || [];
group.push({ resolve: item.resolve, reject: item.reject });
delayGroups.set(item.ms, group);
}
// Process each group
const promises = Array.from(delayGroups.entries()).map(async ([ms, items]) => {
try {
await createBasicDelay(ms);
items.forEach(item => item.resolve());
}
catch (error) {
items.forEach(item => item.reject(error));
}
});
await Promise.all(promises);
isProcessing = false;
// Schedule next batch if there are more items
if (pendingDelays.length > 0) {
scheduleBatch();
}
};
const scheduleBatch = () => {
if (batchTimeoutId === undefined && !isProcessing) {
batchTimeoutId = setTimeout(processBatch, batchWindow);
}
};
return {
add(ms) {
return new Promise((resolve, reject) => {
pendingDelays.push({ ms, resolve, reject });
scheduleBatch();
});
},
flush() {
if (batchTimeoutId !== undefined) {
if (typeof batchTimeoutId === 'number') {
clearTimeout(batchTimeoutId);
}
else {
clearTimeout(batchTimeoutId);
}
batchTimeoutId = undefined;
}
void processBatch();
},
clear() {
if (batchTimeoutId !== undefined) {
if (typeof batchTimeoutId === 'number') {
clearTimeout(batchTimeoutId);
}
else {
clearTimeout(batchTimeoutId);
}
batchTimeoutId = undefined;
}
// Reject all pending delays
const error = new Error('Batch scheduler cleared');
pendingDelays.forEach(item => item.reject(error));
pendingDelays.length = 0;
isProcessing = false;
},
};
}
export async function preciseDelay(ms) {
const startTime = getHighResolutionTime();
let remaining = ms;
// Use a combination of setTimeout and busy waiting for high precision
const threshold = 4; // Switch to busy waiting when less than 4ms remain
while (remaining > threshold) {
const sleepTime = Math.min(remaining - threshold, 15); // Sleep in chunks
await createBasicDelay(sleepTime);
const elapsed = getHighResolutionTime() - startTime;
remaining = ms - elapsed;
}
// Busy wait for the remaining time
while (getHighResolutionTime() - startTime < ms) {
// Busy wait - this is intentionally blocking for precision
}
}
export class DelayScheduler {
constructor() {
this.scheduler = createBatchScheduler();
}
schedule(ms) {
return this.scheduler.add(ms);
}
flush() {
this.scheduler.flush();
}
clear() {
this.scheduler.clear();
}
}
export function createDriftCompensatedTimer(callback, interval) {
const start = getHighResolutionTime();
let count = 0;
let timeoutId;
const tick = () => {
count++;
const target = start + count * interval;
const current = getHighResolutionTime();
const drift = current - target;
callback();
const nextInterval = Math.max(0, interval - drift);
timeoutId = setTimeout(tick, nextInterval);
};
timeoutId = setTimeout(tick, interval);
return () => {
if (typeof timeoutId === 'number') {
clearTimeout(timeoutId);
}
else {
clearTimeout(timeoutId);
}
};
}
//# sourceMappingURL=scheduler.js.map