Pomodoro timer
The point of this package is that a running timer carries no seconds. While
running, the state holds endsAt — a wall-clock epoch ms — and the client draws
the countdown from it. So the clock below ticks every second, but state only
changes when you press something or when a phase actually expires.
What to try
Section titled “What to try”- Press start, then start again. The second one returns
null—reducetreats an already-running timer as a no-op rather than throwing or restarting. - Press pause and watch
endsAtbecomenullwhileremainingfreezes at whatever was left. - Press skip a few times. The phase alternates, but
cyclesstays put — only a phase that expires on its own counts as a completed cycle. - Set work to
0and send it. It clamps to 1 minute rather than falling back to the current config, because0is a value that converts to a number. Try999too: it clamps to 120. - Try configure while the timer is running. It returns
null— changing a phase length mid-phase has ambiguous semantics, so only a stopped timer accepts it.
The code behind it
Section titled “The code behind it”The demo imports the published package and nothing else:
import { defaultState, onTimer, reduce, timerDelay } from '@insession/extension-pomodoro';
let state = defaultState();
// A button pressconst next = reduce(state, 'start');if (next) state = next;
// Phase transitions are driven by your own timerconst delay = timerDelay(state); // ms until the phase ends, or nullif (delay !== null) setTimeout(() => { state = onTimer(state); }, delay);reduce is a pure function — no I/O, no timers started inside it. Scheduling
is yours, which is what lets the same state machine run on a server, in a test,
or in this page.
See the package reference for the full
action list and the persistence helpers (restore / persistState).