From 40ac7703049b6d2246071451f422c7344bd31989 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 20 Jul 2026 13:43:02 +0100 Subject: [PATCH] feat: pure session clock with pause/resume --- src/core/clock.js | 28 ++++++++++++++++++++++++++++ tests/core/clock.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/core/clock.js create mode 100644 tests/core/clock.test.js diff --git a/src/core/clock.js b/src/core/clock.js new file mode 100644 index 0000000..265e601 --- /dev/null +++ b/src/core/clock.js @@ -0,0 +1,28 @@ +export function initialClock() { + return { startedAt: null, pausedAt: null, pausedAccumMs: 0 }; +} + +export function start(state, now) { + if (state.startedAt != null) return state; + return { startedAt: now, pausedAt: null, pausedAccumMs: 0 }; +} + +export function pause(state, now) { + if (state.startedAt == null || state.pausedAt != null) return state; + return { ...state, pausedAt: now }; +} + +export function resume(state, now) { + if (state.pausedAt == null) return state; + return { ...state, pausedAt: null, pausedAccumMs: state.pausedAccumMs + (now - state.pausedAt) }; +} + +export function elapsedMs(state, now) { + if (state.startedAt == null) return 0; + const end = state.pausedAt != null ? state.pausedAt : now; + return Math.max(0, end - state.startedAt - state.pausedAccumMs); +} + +export function isRunning(state) { return state.startedAt != null && state.pausedAt == null; } + +export function isPaused(state) { return state.pausedAt != null; } diff --git a/tests/core/clock.test.js b/tests/core/clock.test.js new file mode 100644 index 0000000..af71532 --- /dev/null +++ b/tests/core/clock.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { initialClock, start, pause, resume, elapsedMs, isRunning, isPaused } from '../../src/core/clock.js'; + +describe('session clock', () => { + it('reads zero before start', () => { + const c = initialClock(); + expect(elapsedMs(c, 1000)).toBe(0); + expect(isRunning(c)).toBe(false); + }); + + it('measures elapsed from start', () => { + const c = start(initialClock(), 1000); + expect(isRunning(c)).toBe(true); + expect(elapsedMs(c, 61000)).toBe(60000); // 1 minute + }); + + it('freezes while paused and excludes paused time after resume', () => { + let c = start(initialClock(), 0); + c = pause(c, 60000); // paused at 1:00 + expect(isPaused(c)).toBe(true); + expect(elapsedMs(c, 120000)).toBe(60000); // frozen at 1:00 while paused + c = resume(c, 180000); // resumed after 2 min paused + expect(elapsedMs(c, 240000)).toBe(120000); // 4 min wall − 2 min paused = 2 min + }); + + it('ignores double start', () => { + const c = start(start(initialClock(), 100), 5000); + expect(elapsedMs(c, 1100)).toBe(1000); + }); +});