Typewriter PacerLoading saved progress…

Typewriter Pacer

A typewriter pacer takes text that arrives in uneven bursts and releases it one character at a time at a steady, even rhythm — the smooth "typing" effect you see in AI chat apps. When a model streams a reply, the network delivers text in lumps: ten characters, a pause, then thirty more. Painting each lump the instant it lands looks jittery, so a pacer buffers whatever arrives and drains that buffer at a fixed pace, giving the reader a calm, constant stream no matter how bursty the source is.

Signature

function typewriterPacer(
  onChar: (char: string) => void, // called once per released character
  delay?: number,                 // milliseconds between characters (default 20)
): {
  push(text: string): void; // feed a burst of text into the buffer
  flush(): void;            // emit everything buffered right now
  stop(): void;             // cancel pending output and drop the buffer
};

Examples

const pacer = typewriterPacer((ch) => process.stdout.write(ch), 20);
pacer.push('Hello');  // 'H','e','l','l','o' released at 20ms, 40ms, 60ms...
pacer.push(' world'); // queued behind the rest — the cadence stays steady
// flush() skips the wait and emits whatever is left immediately
const pacer = typewriterPacer(onChar, 50);
pacer.push('done');
pacer.flush(); // 'd','o','n','e' all emitted now; the pending timer is cleared

Notes

  • Steady cadence — exactly one character is released every delay ms, no matter how much text is waiting.
  • Bursts merge — a push while the buffer is still draining appends to the same queue; it must not start a second, competing timer.
  • Idle then resume — once the buffer empties the timer stops, and a later push restarts it cleanly.
  • Unicode — treat a multi-byte character such as an emoji as one unit, not two.
  • Out of scope — variable speed, pause/resume, and the actual rendering are not required; onChar is your only output.

FAQ

What are the time and space complexity?
Each character is enqueued once and released once, so the work is O(1) per character. Space is O(n) for the buffer holding text that has not been shown yet.
How is this different from debounce or throttle?
Debounce and throttle drop or delay calls to cap how often a function runs. A typewriter pacer keeps every character but spaces the output evenly, draining a buffer one unit per interval.
Why not schedule a setTimeout for every character up front?
Overlapping bursts each start their own timeline from now, so their timers collide and the cadence breaks. One self-rescheduling timer draining a single shared queue keeps the rhythm steady.
Loading editor…