// BigInt keeps entered integers exact, even for very large countdowns. export function integer(value) { return /^\d+$/.test(String(value)) ? BigInt(value) : 0n; } export function splitSeconds(seconds) { return { minutes: String(seconds / 60n), seconds: String(seconds % 60n).padStart(2, '0'), }; } export class Countdown { constructor(now = Date.now) { this.now = now; this.state = 'idle'; this.finished = false; this.duration = 0n; this.remaining = 0n; this.deadline = 0n; } edit(minutes, seconds) { if (this.state === 'running') return; this.duration = (integer(minutes) * 60n + integer(seconds)) * 1000n; this.remaining = this.duration; this.state = 'idle'; this.finished = false; } update() { if (this.state !== 'running') return; const delta = this.deadline - BigInt(this.now()); this.remaining = delta > 0n ? delta : 0n; if (this.remaining === 0n) { this.state = 'idle'; this.finished = true; } } toggle() { if (this.state === 'running') { this.update(); if (!this.finished) this.state = 'paused'; return; } if (this.finished) this.remaining = this.duration; this.finished = false; this.deadline = BigInt(this.now()) + this.remaining; this.state = 'running'; this.update(); } reset() { this.remaining = this.duration; this.state = 'idle'; this.finished = false; } display() { // Round up so the timer only displays zero once its deadline is reached. return splitSeconds((this.remaining + 999n) / 1000n); } }