A setTimeout that can be paused, resumed, skipped and aborted, with typed events and optional ticks.
- Measures remaining time against the clock (
performance.now()), so the countdown doesn't drift. - Ticks land on interval boundaries of the remaining time (e.g. 3000, 2000, 1000, 0).
- No dependencies and no
node:events, so it also works in the browser.
npm install @davidihl/pausable-timerRequires Node.js 18 or later.
import { createTimer } from "@davidihl/pausable-timer";
const timer = createTimer({ duration: 10_000, interval: 1000 });
timer.on("tick", ({ remaining }) => {
console.log(`${Math.ceil(remaining / 1000)}s left`);
});
timer.on("complete", () => {
console.log("done");
});
timer.start();The remaining time is frozen while the timer is paused.
const timer = createTimer({ duration: 5000 });
timer.start();
// 2 seconds later
timer.pause();
timer.getRemaining(); // ~3000
// any time later
timer.resume(); // completes ~3000ms from nowBoth stop the timer for good, and complete never fires. skip sets the remaining time to 0. abort keeps the remaining time from the moment it was called.
timer.skip();
// or
timer.abort();getState() returns the current state: idle, running, paused, completed, skipped or aborted. See States for the allowed transitions.
const timer = createTimer({ duration: 5000 });
timer.getState(); // "idle"
timer.start();
timer.getState(); // "running"
timer.pause();
timer.getState(); // "paused"Every action emits an event of the same name after the state has changed:
timer.on("start", () => console.log("started"));
timer.on("pause", () => console.log("paused at", timer.getRemaining()));
timer.on("resume", () => console.log("resumed with", timer.getRemaining()));
timer.on("skip", () => console.log("skipped"));
timer.on("abort", () => console.log("aborted at", timer.getRemaining()));
timer.on("complete", () => console.log("done"));on() returns a function that removes the listener:
const off = timer.on("tick", ({ remaining }) => render(remaining));
// later
off();This works well as a React effect cleanup:
useEffect(
() => timer.on("tick", ({ remaining }) => setRemaining(remaining)),
[timer],
);| Option | Type | Default | Description |
|---|---|---|---|
duration |
number |
- | Total time in milliseconds. |
interval |
number |
1000 |
Time between tick events, in ms. |
Returns a Timer:
| Member | Description |
|---|---|
start() |
Starts the countdown. |
pause() |
Pauses and freezes the remaining time. |
resume() |
Continues from the remaining time. |
skip() |
Ends the timer immediately; remaining becomes 0. |
abort() |
Ends the timer immediately; remaining is kept. |
on(type, listener) |
Adds a listener and returns an unsubscribe function. |
getRemaining() |
Remaining time in milliseconds. |
getState() |
Current state (see States). |
config |
The config the timer was created with. |
| Event | Payload | When |
|---|---|---|
tick |
{ remaining: number } |
On every interval boundary while running, plus a final 0. |
start |
- | After start(). |
pause |
- | After pause(). |
resume |
- | After resume(). |
skip |
- | After skip(). |
abort |
- | After abort(). |
complete |
- | When the duration ends. |
Allowed transitions, from the current state (rows) by action (columns):
| Current state | start() |
pause() |
resume() |
skip() |
abort() |
duration ends |
|---|---|---|---|---|---|---|
idle |
running |
- | - | skipped |
aborted |
- |
running |
- | paused |
- | skipped |
aborted |
completed |
paused |
- | - | running |
skipped |
aborted |
- |
completed |
- | - | - | - | - | - |
skipped |
- | - | - | - | - | - |
aborted |
- | - | - | - | - | - |
- means not allowed: the call throws (e.g. pause() while idle) and no event is emitted. completed, skipped and aborted are final.