A microtask batch scheduler collects every item handed to it during one synchronous run of your code and delivers them together in a single callback on the next microtask, instead of reacting to each item the moment it arrives. Many independent parts of an app can each say "this changed, please react" within the same tick — a dozen store writes, a burst of DOM mutations, several dirty components. Firing your expensive reaction (a re-render, a network sync, a recompute) once per call is wasteful; you want to wait until the burst is over and react once with the full list. The earliest safe moment to do that is the next microtask — the gap right after your synchronous code returns control to the event loop.
Implement microtaskBatchScheduler(onFlush), which returns an object with a single schedule method.
function microtaskBatchScheduler<T>(
onFlush: (items: T[]) => void,
): {
schedule: (item: T) => void;
};
const scheduler = microtaskBatchScheduler((items) => {
console.log('flush', items);
});
scheduler.schedule('a');
scheduler.schedule('b');
scheduler.schedule('c');
// nothing logged yet — still inside the same synchronous frame
await Promise.resolve(); // let the microtask run
// → flush ['a', 'b', 'c'] (one call, all three items, in order)
// A schedule in a later frame is a brand-new batch.
scheduler.schedule('x');
await Promise.resolve(); // → flush ['x']
scheduler.schedule('y');
await Promise.resolve(); // → flush ['y'] (a second, separate flush)
schedule in a single synchronous run, onFlush fires exactly once, with every item from that run.queueMicrotask (or Promise.resolve().then) so it lands before the next timer, render, or setTimeout(…, 0).onFlush in the order they were scheduled; you never sort or reorder.onFlush receives it twice. Do not filter.schedule begins a fresh batch and queues a fresh flush.setTimeout.