Skip to content

Timing ​

Use timing operators to control read rates, output spacing, and time limits. All durations are in milliseconds. No waiting or timing begins until the stage is read.

Choose an operator ​

OperatorGoal
rateLimitLimit reads within a time window
spaceoutSpace values apart
delayStartDelay the start
timeoutEachLimit each read's wait
deadlineSet a stage's overall time limit
bufferTimeBatch by time or count

Limit upstream reads ​

rateLimit({ count: 2, perMs: 1_000 }) allows at most two upstream read starts in any 1,000 ms window. Available slots can be used immediately, so this does not evenly space outputs.

ts
import { from, rateLimit } from "mogami-stream";

const values = await from([1, 2, 3, 4, 5])
  .pipe(rateLimit({ count: 2, perMs: 1_000 }))
  .toArray();

console.log(values); // [1, 2, 3, 4, 5]

Put rateLimit before map to pace its upstream reads. For a request budget shared across retries and executions, use the shared limiter recipe. Set simultaneous work separately with map's concurrency option.

Each execution has its own limit. Failed reads and the final read that observes completion also count. Retry attempts inside map do not count; see Retries for limits that must include retries.

Space outputs apart ​

spaceout(500) adds no delay to the first value, then ensures at least 500 ms between emissions. Time spent reading upstream or processing downstream counts toward the gap. If 500 ms has already elapsed, no additional wait is added.

ts
import { from, spaceout } from "mogami-stream";

const values = await from([1, 2, 3, 4, 5]).pipe(spaceout(500)).toArray();

console.log(values); // [1, 2, 3, 4, 5]

With immediately available input and continuous downstream demand, approximate times are:

ValuerateLimit read startspaceout emission
10 ms0 ms
20 ms500 ms
31,000 ms1,000 ms
41,000 ms1,500 ms
52,000 ms2,000 ms

spaceout reads the value from upstream before waiting to emit it. Use rateLimit when you need to limit how often upstream work starts.

Delay only the start ​

delayStart(500) waits 500 ms after the first read request before opening upstream.

ts
import { delayStart, of } from "mogami-stream";

const stream = of("ready").pipe(delayStart(500));
console.log(await stream.toArray()); // ["ready"]

Per-read timeouts and overall deadlines ​

timeoutEach measures only the wait for one upstream read and fails with TimeoutError at the deadline. Time between downstream requests does not count. When a read returns a value or completes the stream, monotonic elapsed time is checked even if the timer callback has not run. Results at or after the deadline are rejected. timeoutEach(0) fails on the first read without opening upstream.

deadline measures elapsed time from that stage's first read and fails with DeadlineExceededError at the deadline. Time continues to count while downstream processes a value.

ts
import {
  DeadlineExceededError,
  deadline,
  fromTimeline,
  timeoutEach,
} from "mogami-stream";

const source = fromTimeline([
  { at: 0, kind: "value", value: "A" },
  { at: 800, kind: "value", value: "B" },
  { at: 1_600, kind: "value", value: "C" },
]);

console.log(await source.pipe(timeoutEach(1_000)).toArray()); // ["A", "B", "C"]

try {
  await source.pipe(deadline(1_500)).toArray();
} catch (error) {
  if (!(error instanceof DeadlineExceededError)) throw error;
  console.log(error.name); // "DeadlineExceededError"
}

Values arrive 800 ms apart in this example, so each read fits within its 1,000 ms limit. The overall 1,500 ms deadline expires before the third value arrives. The source is replayable, so both executions start from the beginning.

The operator times its own upstream. Placing timeoutEach after map limits the wait for the next result from map. To limit every individual request running concurrently, also set a timeout on the I/O inside the callback.

A timeout signals upstream to stop. Pass the callback's signal to I/O, as in fetch(url, { signal }). Terminal consumers wait for cleanup, so I/O that ignores cancellation can delay settlement beyond the deadline. See Cancellation and cleanup.

Batch by time or count ​

bufferTime emits an array when windowMs elapses after its first item arrives or when it reaches maxItems.

ts
import { bufferTime, from } from "mogami-stream";

const batches = await from([1, 2, 3, 4, 5])
  .pipe(bufferTime(1_000, { maxItems: 2 }))
  .toArray();

console.log(batches); // [[1, 2], [3, 4], [5]]

This example reaches the two-item limit for each full batch and emits the remaining item on normal completion. Empty arrays are never emitted. Errors and cancellation discard the unfinished batch.

If an upstream read is still pending when the window expires, only that read is retained. No further reads or batches accumulate until downstream requests the next batch. Use chunks to batch only by count.

Actual times follow the host runtime's timer precision. rateLimit's perMs and bufferTime's windowMs must be positive; other durations must be finite and non-negative. count and maxItems must be positive safe integers.