I build Pianory, a browser-based piano practice app. AI helped draft this article; I checked the behavior against the timer and timed-run source and its focused tests.
A practice timer should count time spent in the active session, not the whole interval since a tab was opened. Pianory's foreground timer reads performance.now() to compute elapsed milliseconds. A 250 ms interval asks for updates, but those ticks are not added as 250 ms chunks; the elapsed duration comes from the clock. The callback reports only when the whole-second count changes.
The page lifecycle matters. When the document becomes hidden or the page is leaving, the timed-practice component pauses the controller. A saved run with status running is restored as paused after reload. That avoids counting the time the browser was closed or in the background as active practice. One focused test runs for three seconds, hides the page for 60 seconds and still records only three seconds before resuming.
Each measured delta is applied to the current unfinished activity. The domain function rejects invalid deltas, caps elapsed time at the session budget and marks a run expired at that limit. A five-minute test reaches 300 seconds while the current activity remains unfinished; duration does not pretend to grade the playing or complete a learning goal.
Local storage can also change in another tab. Before writing, the controller compares the current stored string with the snapshot it loaded. If the value changed, it stops rather than silently overwriting that other state. This is conflict detection, not an atomic cross-tab lock. Tests cover storage failures and recovery as well.
The design lesson is to define what a timer measures and what it does not. The focused suite pnpm exec vitest run src/lib/components/practice/timed-run.spec.ts src/lib/components/stats/session-groups.spec.ts passed 10 tests on 30 October 2026. It verifies these units, not timing precision in every browser or a measured improvement in practice outcomes. Relevant source: src/lib/components/practice/timing.ts, timed-run.ts, TimedPracticeSession.svelte, and src/lib/domain/timed-session-run.ts.
Top comments (0)