A cooperative scheduler runs a queue of tasks in short time slices, handing control back to the event loop between slices so a long batch of work never blocks the main thread. It is the idea behind the browser's native scheduler.postTask and React's concurrent rendering: do a little work, let the page breathe, then pick up where you left off. You will build a small version of it that accepts tasks one at a time and hands each caller a promise for that task's result.
function cooperativeScheduler(options?: { budgetMs?: number }): {
// Enqueue fn. It runs asynchronously (never inline) in FIFO order.
// Returns a Promise that fulfils with fn()'s return value, or
// rejects with whatever fn threw.
postTask<T>(fn: () => T): Promise<T>;
// How many tasks are still waiting to run.
size(): number;
};
const { postTask, size } = cooperativeScheduler();
let ran = false;
const p = postTask(() => { ran = true; return 'done'; });
console.log(ran); // false — postTask never runs fn inline
console.log(size()); // 1 — the task is queued, waiting for a later tick
console.log(await p); // 'done' — settles once the slice runs the task
console.log(ran); // true
const s = cooperativeScheduler({ budgetMs: 5 });
const order = [];
const a = s.postTask(() => { order.push('a'); return 1; });
const b = s.postTask(() => { order.push('b'); throw new Error('boom'); });
const c = s.postTask(() => { order.push('c'); return 3; });
await a; // 1
await b.catch((e) => e.message); // 'boom' — only b's promise rejects
await c; // 3 — b throwing did not skip c
console.log(order); // ['a', 'b', 'c'] — FIFO, all three ran
postTask must not call fn during the postTask call itself. The task runs on a later tick, scheduled with setTimeout(fn, 0).budgetMs (default 5); once it reaches the budget, hand control back to the event loop and resume in the next slice.setTimeout(fn, 0) and measure elapsed time with Date.now(), so the scheduler works in Node, jsdom, and the sandbox. Faster primitives exist in a real browser (see the solution).clear(). Just FIFO draining in time slices.