Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bb050f9
Refactor TabataTimer: monotonic timing and optimizations
google-labs-jules[bot] Feb 20, 2026
000c053
Refactor TabataTimer: monotonic timing and optimizations
google-labs-jules[bot] Feb 20, 2026
2bf4c6f
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
0976899
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
512a994
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
7b5e062
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
88ed4f0
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
1cf369a
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
94d2a06
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
faaac7d
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
315f821
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
773c710
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
5cbd484
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
5022f8e
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
eac550e
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
89f7420
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
d5bbc81
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
1049ee6
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
7ed4cbc
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
85f95de
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
901c6ea
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
82e1834
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
b1cb493
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
25f0cbb
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
768e581
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
6b4dd41
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
c965b21
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
01ef93a
Refactor TabataTimer: monotonic timing and optimizations (Addressed r…
google-labs-jules[bot] Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 32 additions & 28 deletions services/tabataTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Consolidated Dual-Mode Timer Service.
* This class manages the state and logic for both TABATA and STOPWATCH modes,
* handling transitions, sound cues, and broadcasting updates to clients.
* It uses absolute timing (Date.now()) to maintain accuracy against drift.
* It uses absolute timing (performance.now()) to maintain accuracy against drift.
*/
import { ServerMessage } from '../types/websocket'
import { TimerData, TimerMode, TimerPhase } from '../types/core'
Expand All @@ -30,7 +30,7 @@ class TabataTimer {
private timerInterval: NodeJS.Timeout | null = null
private pausedTimeRemaining: number = DEFAULT_WORK_DURATION
private pausedTimeElapsed: number = 0
private countdownMarker: string | null = null
private lastCountdownSecond: number = -1

private readonly broadcastUpdate: (message: ServerMessage) => void

Expand Down Expand Up @@ -66,15 +66,15 @@ class TabataTimer {
if (this.isRunning) return

this.isRunning = true
const now = Date.now()
const now = performance.now()

if (this.currentPhase === 'IDLE') {
this.currentPhase = 'PREPARE'
this.timeRemaining = START_COUNTDOWN_DURATION
this.pausedTimeRemaining = START_COUNTDOWN_DURATION
this.timeElapsed = 0
this.pausedTimeElapsed = 0
this.countdownMarker = null
this.lastCountdownSecond = -1
}

this.startTime = now
Expand All @@ -88,7 +88,18 @@ class TabataTimer {
public pause(): void {
if (!this.isRunning || !this.startTime) return

this.updateTimer() // Final sync before pausing
const now = performance.now()
const elapsedSinceLastStart = Math.floor((now - this.startTime) / 1000)

if (this.mode === 'STOPWATCH' && this.currentPhase === 'RUNNING') {
this.timeElapsed = this.pausedTimeElapsed + elapsedSinceLastStart
} else if (this.mode === 'TABATA' || this.currentPhase === 'PREPARE') {
const nextRemaining = Math.max(
0,
this.pausedTimeRemaining - elapsedSinceLastStart
)
this.timeRemaining = nextRemaining
}

this.isRunning = false
if (this.timerInterval) clearInterval(this.timerInterval)
Expand All @@ -112,7 +123,7 @@ class TabataTimer {
this.timeElapsed = 0
this.timeRemaining = this.mode === 'TABATA' ? this.workDuration : 0

this.countdownMarker = null
this.lastCountdownSecond = -1
this.pausedTimeRemaining = this.timeRemaining
this.pausedTimeElapsed = 0
this.startTime = null
Expand All @@ -139,7 +150,7 @@ class TabataTimer {
this.timeElapsed = 0
this.pausedTimeElapsed = 0
this.soundToPlay = undefined
this.countdownMarker = null
this.lastCountdownSecond = -1
this.broadcastUpdate({
type: 'TIMER_UPDATE',
payload: this.getState(),
Expand Down Expand Up @@ -180,13 +191,10 @@ class TabataTimer {

// --- Internal Timer Logic ---

/**
* Core timer tick logic.
*/
private updateTimer = (): void => {
if (!this.isRunning || !this.startTime) return

const now = Date.now()
const now = performance.now()
const elapsedSinceLastStart = Math.floor((now - this.startTime) / 1000)

if (this.mode === 'STOPWATCH' && this.currentPhase === 'RUNNING') {
Expand Down Expand Up @@ -216,7 +224,7 @@ class TabataTimer {
* @param {number} now The current timestamp to use as the new start time.
*/
private transitionPhase(now: number): void {
this.countdownMarker = null
this.lastCountdownSecond = -1
this.startTime = now
this.pausedTimeElapsed = 0

Expand Down Expand Up @@ -272,27 +280,23 @@ class TabataTimer {
* Handles playing countdown sound cues.
*/
private handleCountdownCue(): void {
const phase = this.currentPhase
const remaining = this.timeRemaining
if (
phase === 'IDLE' ||
phase === 'RUNNING' ||
phase === 'COOLDOWN' ||
remaining <= 0
this.currentPhase === 'PREPARE' ||
this.currentPhase === 'WORK' ||
this.currentPhase === 'REST'
) {
return
}

const marker = `${phase}-${remaining}`
if (remaining <= 3 && this.countdownMarker !== marker) {
this.queueSound('COUNTDOWN')
this.countdownMarker = marker
const remaining = this.timeRemaining
if (
remaining <= 3 &&
remaining > 0 &&
remaining !== this.lastCountdownSecond
) {
this.queueSound('COUNTDOWN')
this.lastCountdownSecond = remaining
}
}
}

/**
* Cleanup resources.
*/
public dispose(): void {
if (this.timerInterval) {
clearInterval(this.timerInterval)
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/services/tabataTimer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ describe('TabataTimer (Refactored)', () => {
// Clear any previous mocks and timers
broadcastUpdate.mockClear()
jest.clearAllTimers()

// Mock performance.now to use Date.now() so it syncs with jest.advanceTimersByTime
jest.spyOn(performance, 'now').mockImplementation(() => Date.now())

timer = new TabataTimer(broadcastUpdate)
})

afterEach(() => {
jest.restoreAllMocks()
})

// Test initial state
it('should initialize with the correct default state', () => {
const state = timer.getState()
Expand Down
Loading