Skip to main content

TimeoutManager

TimeoutManager provides window-safe timeout registration plus a small set of static async helpers for pausing execution and for keeping long-running loops cooperative with the runtime's I/O phase.

Package: io-oidis-commons

import { TimeoutManager } from "@io-oidis-commons/Io/Oidis/Commons/Events/TimeoutManager.js";
Static helpers

The methods below are static — call them as TimeoutManager.Sleep(), TimeoutManager.YieldToIO(), etc. No instance is required.

Sleep​

public static async Sleep($time : number = 1000) : Promise<void>

Pauses the current async flow for $time milliseconds. Prefer this over a hand-rolled new Promise(($resolve) => setTimeout($resolve, ms)).

await TimeoutManager.Sleep(500);   // wait half a second

Event-loop starvation and YieldToIO​

Node.js runs your code on a single thread. A long synchronous block, or a "microtask-starvation" loop (a for await whose steps resolve without real I/O — e.g. serializing thousands of already-loaded records), never lets the runtime reach its I/O phase. While the thread is pinned, the process cannot answer WebSocket keepalive pings; the peer times out and drops the connection, and the next keepalive write fails with EPIPE.

YieldToIO releases the event loop for one full turn so pending sockets and timers get serviced:

public static async YieldToIO() : Promise<void>
for (let i = 0; i < items.length; i++) {
doSomeSynchronousWork(items[i]);
if ((i + 1) % 500 === 0) {
await TimeoutManager.YieldToIO(); // let keepalive / I/O run
}
}
Why not just await Promise.resolve()?

An awaited micro-task (await Promise.resolve()) does not hand control back to the I/O phase — it only drains the micro-task queue and the loop resumes immediately. YieldToIO uses setImmediate on Node (a full loop turn including the I/O poll phase) and falls back to setTimeout(0) in the browser, so queued sockets, timers, and events actually get a turn.

ForEach​

public static async ForEach<T>(
$items : T[],
$callback : ($item : T, $index : number) => Promise<void> | void,
$yieldEvery : number = 100
) : Promise<void>

Iterates a collection and calls YieldToIO every $yieldEvery items. Use it instead of a bare for/for await when the loop body does synchronous or micro-task-only work over a large in-memory batch. A $yieldEvery below 1 disables the periodic yield.

await TimeoutManager.ForEach(records, ($record) => {
output.push(buildRowSync($record)); // sync work, no real I/O
});

Map​

public static async Map<T, R>(
$items : T[],
$mapper : ($item : T, $index : number) => Promise<R> | R,
$yieldEvery : number = 200
) : Promise<R[]>

Maps a collection into a new array while yielding every $yieldEvery items. This is the drop-in fix for the common "load all rows, serialize each in memory" list-endpoint pattern, which otherwise runs as one uninterrupted micro-task burst.

// Before — starves the event loop over a large result set:
const data : any[] = [];
for await (const record of records) {
data.push(await record.ToJSON());
}

// After — yields to I/O every 200 records:
const data : any[] = await TimeoutManager.Map(records, ($record) => $record.ToJSON());

For a loop that conditionally emits, or breaks early, keep the loop and place a counter-based YieldToIO inside it instead of Map:

let scanned : number = 0;
for await (const record of records) {
if ((++scanned) % 500 === 0) {
await TimeoutManager.YieldToIO();
}
if (matches(record, query)) {
output.push(await record.ToJSON());
}
}

Choosing a helper​

Loop shapeUse
Serialize / transform every item into a new arrayMap
Iterate every item for side effects (push, accumulate)ForEach
Conditional emit, break/continue, or custom control flowmanual counter + YieldToIO
A single unavoidable sync call (JSON.stringify of a huge blob, a third-party parseSync)cannot be chunked in place — yield around it, not inside
Rule of thumb

Any loop that runs over an unbounded collection with no real I/O (.Save(), .Find(), file, network) between iterations should yield. Loops that already await real I/O each iteration yield naturally and need no change.

Complete Method Reference​

MethodDescription
Sleep($time?)Pause the async flow for $time ms (default 1000)
YieldToIO()Release the event loop for one full turn (setImmediate, browser fallback setTimeout(0))
ForEach($items, $callback, $yieldEvery?)Iterate with a YieldToIO every $yieldEvery items (default 100)
Map($items, $mapper, $yieldEvery?)Map into a new array with a YieldToIO every $yieldEvery items (default 200)

See also: Utilities Overview | Stopwatch | Events | Metrics