Architecture
Clock, Store, and Stopwatch layering for Watchstop.
Watchstop separates time (Clock), observation (Store), and elapsed measurement (Stopwatch). Frameworks only need get + subscribe. Runtimes only need now + schedule + cancel. Neither concern belongs inside Stopwatch itself.
Layers
Clock providers Core Framework adapters
───────────────── ──── ──────────────────
createBrowserClock ─┐
createTimerClock ─┼─► Clock ─► Stopwatch ─► Store<number> ─► React / Svelte / …
createMockClock ─┘ │
detectClock ───────────────┘| Layer | Responsibility | Package |
|---|---|---|
| Clock | Read monotonic time; schedule/cancel ticks | @watchstop/core |
| Store | Snapshot + subscription | @watchstop/core (interface); Stopwatch implements it |
| Stopwatch | Accumulate elapsed ms while running | @watchstop/core |
| Adapters | Bridge Store into framework reactivity | @watchstop/react, svelte, … |
Design rules
- No framework imports in core. Angular, Qwik, and Alpine must consume the same
Storecontract as React. - One timer clock for Node, Bun, and Deno. All three expose
performance.nowandsetTimeout/clearTimeout. Do not ship@watchstop/node,@watchstop/bun, or@watchstop/deno. - Default clock via
detectClock(). Prefer browser (requestAnimationFrame) when present; otherwise timer clock. Each call returns a fresh clock — not a realm singleton. No hardnode:imports in the browser build. - Shared schedule per clock identity. Running stopwatches that share one
Clockobject share one underlyingscheduleloop. Pass one shared clock when you want coalescing; barenew Stopwatch()/detectClock()do not share. - Countdown / Ticker are out of v1. Only
Stopwatchships in the first core release. - Adapters are thin. No timing math, no clocks, no elapsed accumulation in adapters.
Adapter entry-point naming
Each adapter ships exactly one instance-owning entry point. The name follows the framework idiom rather than forcing one shape across ecosystems:
| Package | Entry point | Why that name |
|---|---|---|
@watchstop/react | useStopwatch | React hooks |
@watchstop/vue | useStopwatch | Vue composables |
@watchstop/solid | useStopwatch | Solid hooks |
@watchstop/qwik | useStopwatch | Qwik hooks |
@watchstop/angular | injectStopwatch | Angular inject / injection-context idiom |
@watchstop/svelte | createStopwatch | Svelte has no hook context; factory + onDestroy |
@watchstop/alpine | createStopwatch | Alpine has no hook/inject context; factory returns the binding |
Factories exist where the framework has no hook or inject context to hang ownership and teardown on. The binding shape is the same everywhere: elapsed, running, start, stop, reset, and the owned stopwatch (Svelte exposes running as a separate Readable<boolean>; $stopwatch stays elapsed ms).
Data flow while running
start()recordsstartTime = clock.now(), notifies subscribers (even when elapsed is still0) so adapters can syncrunning, and registers with the shared driver for thatclock.- Each shared tick recomputes
elapsed = accumulated + (clock.now() - startTime)and notifies subscribers for every registered stopwatch that still wants ticks. stop()unregisters from the driver, adds the current segment intoaccumulated, clearsstartTime, and setsrunning = false.get()returns the live elapsed value (running or stopped).runningis the public flag for whether a segment is active.destroy()unregisters, clears listeners, setsrunning = false, and makes further use a no-op or safe idle state (see Stopwatch).
Rules 3 and 4 as a picture — one shared schedule per Clock object, while each stopwatch still owns its own banked time:
Same Clock object → one schedule loop. Each stopwatch still owns its banked time and start reading (dashed nodes — no arrow in). get() stays live; precisionMs only cuts how often subscribers update. See Options.
Runtime matrix
| Environment | Factory | now | schedule / cancel |
|---|---|---|---|
| Browser (display) | createBrowserClock() | performance.now() | requestAnimationFrame / cancelAnimationFrame |
| Node / Bun / Deno | createTimerClock({ intervalMs? }) | performance.now() | setTimeout / clearTimeout |
| Tests | createMockClock() | controllable | controllable + advance(ms) |
| Auto | detectClock() | browser if requestAnimationFrame exists, else timer | same |
Package map (v1)
@watchstop/core- Adapters: React, Svelte, Vue, Solid, Angular, Qwik, Alpine
See Spec for public names and adopter constraints that pressure the Store shape.