Fluent, human-readable time control for JavaScript. A modern, composable upgrade to setTimeout and setInterval with chaining, conditional logic, pause/resume control, retries, and RAF utilities.
- Readable durations:
"1s","500ms", plain numbers - Pause / resume / cancel controllers for
after(),every(), RAF variants flow()builds declarative timelines with branching and labelsretry()with backoff, jitter, andAbortSignalcancellationwaitFor()polls a predicate with timeout- RAF variants (
afterRaf,everyRaf,debounceRaf,throttleRaf,waitForRaf) for frame-aligned timing - ESM-first, tree-shakeable. Per-file subpath exports for size-sensitive consumers (~1.2 KB gzipped for a single primitive)
- Zero dependencies
- ~6–7 KB minified for the full kit (~2–3 KB gzipped)
pnpm add timeout-flowimport { after } from 'timeout-flow';
function greet() {
console.log('Hello');
}
const ctrl = after('2s', greet);
ctrl.pause();
ctrl.resume();
ctrl.cancel();Run once after a delay.
import { after } from 'timeout-flow';
after('2s', function greet() {
console.log('Hello');
});Repeat execution with optional limit.
import { every } from 'timeout-flow';
const ticker = every('1s', function tick(i) {
console.log('Tick', i);
}, { max: 5 });Delay execution until inactivity.
import { debounce } from 'timeout-flow';
const debouncedSearch = debounce('300ms', function search(event) {
console.log('Searching for:', event.target.value);
});
debouncedSearch.cancel();
debouncedSearch.flush();Limit execution frequency.
import { throttle } from 'timeout-flow';
const onScroll = throttle('250ms', function handleScroll() {
console.log('scroll');
});
onScroll.cancel();
onScroll.flush();Retry async operations with backoff and jitter.
import { retry } from 'timeout-flow';
await retry(async function fetchData() {
return fetch('/api/data');
}, {
attempts: 5,
delay: '500ms',
backoff: true,
factor: 2,
maxDelay: '5s',
jitter: 'decorrelated'
});Wait until a condition becomes true.
import { waitFor } from 'timeout-flow';
await waitFor(function checkLoaded() {
return document.querySelector('#loaded');
}, {
interval: '250ms',
timeout: '5s',
immediate: true
});Build declarative time flows.
import { flow } from 'timeout-flow';
flow()
.after('1s', function stepOne() {
console.log('Step 1');
})
.every('500ms', function tick(i) {
console.log('Tick', i);
}, { max: 3 })
.after('1s', function finalStep() {
console.log('Final Step');
})
.start();Flow reads like a timeline and keeps duration-first ordering for readability.
Frame-based timing powered by requestAnimationFrame. Ideal for visual updates, scroll handlers, layout checks, and animation loops.
Available: afterRaf(), everyRaf(), debounceRaf(), throttleRaf(), waitForRaf().
import { throttleRaf } from 'timeout-flow';
const onScroll = throttleRaf(function drawFrame() {
console.log('draw');
});
onScroll.cancel();
onScroll.flush();Most time-based primitives return a controller object:
const ctrl = after('1s', () => console.log('done'));
ctrl.pause();
ctrl.resume();
ctrl.cancel();
ctrl.reset?.();
console.log(ctrl.isRunning);
console.log(ctrl.isPaused);
console.log(ctrl.isFinished);Shared by after(), every(), afterRaf(), everyRaf(). All controllers are pause-safe (paused time does not count), cancel-safe, monotonic-time based, and AbortSignal-aware where supported.
Many utilities accept { signal } for automatic cancellation.
import { after } from 'timeout-flow';
const ac = new AbortController();
after('2s', function doWork() {
console.log('Will not run if aborted');
}, { signal: ac.signal });
ac.abort();If already aborted at creation time, no work is scheduled.
import {
after,
every,
debounce,
throttle,
retry,
waitFor,
flow,
afterRaf,
everyRaf,
debounceRaf,
throttleRaf,
waitForRaf
} from 'timeout-flow';Two import styles, same package, choose by bundle-size sensitivity:
// Full kit. Every primitive available on one import.
// Modern bundlers (esbuild, vite, rollup, webpack 5) will tree-shake
// unused exports because the package ships `sideEffects: false` and the
// `.` entrypoint points at ESM source instead of a pre-minified blob.
import { after, debounce, retry } from 'timeout-flow';// Per-primitive subpath. Explicit minimum surface, useful when you know
// you only need one primitive and want the bundle to reflect that
// without relying on the bundler's tree-shaker. Same source either way.
import { after } from 'timeout-flow/after';
import { every } from 'timeout-flow/every';
import { debounce } from 'timeout-flow/debounce';
import { throttle } from 'timeout-flow/throttle';
import { retry } from 'timeout-flow/retry';
import { waitFor } from 'timeout-flow/wait-for';
import { flow } from 'timeout-flow/flow';
import { afterRaf, everyRaf, debounceRaf, throttleRaf, waitForRaf } from 'timeout-flow/raf';
import { parseDuration } from 'timeout-flow/parse-duration';Measured for a single after() call in a typical browser bundler (esbuild, minified + gzip):
| Import style | Gzip |
|---|---|
| pre-0.0.19 default (full minified bundle) | 5098 bytes |
0.0.19 default (import { after } from 'timeout-flow') |
1199 bytes |
0.0.19 subpath (import { after } from 'timeout-flow/after') |
1199 bytes |
For unbundled <script type="module"> consumption of the full kit, use the timeout-flow/min subpath, which points at the pre-built minified IIFE.
timeout-flow supports both natural-language and function-first argument ordering:
after('1s', done);
// or
after(done, '1s');This applies to after(), every(), debounce(), throttle(). Use whichever reads best in your codebase.
timeout-flow is not just a wrapper around timers, it's a composable toolkit for expressing time as readable logic. Temporal behavior should be:
- Readable. Durations like
"1s"and"500ms"beat magic numbers. - Composable. Sequencing should be declarative.
- Controllable. Timers should pause, resume, cancel.
- Branchable. Real flows need
if,while,label,jumpTo(). - Tiny. No runtime bloat.
Think of timeout-flow as
setTimeout()with superpowers.
Licensed under AGPL-3.0 with WATT3D Additional Terms. See LICENSE and ADDITIONAL_TERMS.md. Commercial AI/model-training use requires compliance with those terms or a separate WATT3D license. © WATT3D.