diff --git a/frontend/src/ts/legacy-states/time.ts b/frontend/src/ts/legacy-states/time.ts deleted file mode 100644 index c18761a1a208..000000000000 --- a/frontend/src/ts/legacy-states/time.ts +++ /dev/null @@ -1,13 +0,0 @@ -let time = 0; - -export function get(): number { - return time; -} - -export function set(number: number): void { - time = number; -} - -export function increment(): void { - time++; -} diff --git a/frontend/src/ts/test/events/live-cache.ts b/frontend/src/ts/test/events/live-cache.ts index 7f66d82da7ed..52372ab297b6 100644 --- a/frontend/src/ts/test/events/live-cache.ts +++ b/frontend/src/ts/test/events/live-cache.ts @@ -48,9 +48,18 @@ export function getLiveCachedMsSinceLastInputEvent(): number | null { return cache.msSinceLastInputEvent.value; } +export function getLiveCachedTimerStartMs(): number | null { + return cache.timerStartMs; +} + export function getLiveCachedTestDurationMs(now: number): number { if (cache.timerStartMs === null) { throw new Error("Timer start ms not found in cache"); } return now - cache.timerStartMs; } + +export function getLiveCachedTestSeconds(now: number): number { + const startMs = cache.timerStartMs ?? now; + return Math.floor((now - startMs) / 1000); +} diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index cabf7eb36dc4..fd61dba222f0 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -87,7 +87,6 @@ import { canQuickRestart } from "../utils/quick-restart"; import { animate } from "animejs"; import { setInputElementValue } from "../input/input-element"; import { debounce } from "throttle-debounce"; -import * as Time from "../legacy-states/time"; import { qs } from "../utils/dom"; import { setAccountButtonSpinner } from "../states/header"; import { Config } from "../config/store"; @@ -138,7 +137,6 @@ export function startTest(now: number): boolean { } setTestActive(true); - Time.set(0); TestTimer.clear(); for (const fb of getActiveFunboxesWithFunction("start")) { diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index f410da020534..5ae5ce276321 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -15,7 +15,6 @@ import { import * as Caret from "./caret"; import * as SlowTimer from "../legacy-states/slow-timer"; import * as TestState from "./test-state"; -import * as Time from "../legacy-states/time"; import { timerEvent } from "../events/timer"; import { highlight } from "../events/keymap"; import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer"; @@ -29,12 +28,14 @@ import { roundTo2 } from "@monkeytype/util/numbers"; import { getLiveCachedAccuracy, getLiveCachedTestDurationMs, + getLiveCachedTestSeconds, + getLiveCachedTimerStartMs, } from "./events/live-cache"; import { getChars } from "./events/stats"; import { calculateWpm } from "../utils/numbers"; import { isTestActive, setCurrentLiveStats } from "../states/test"; -let timerStartMs = 0; +let emittedTicks = 0; let stopped = true; const newTimer = createTimer({ duration: 1000, @@ -42,8 +43,14 @@ const newTimer = createTimer({ onComplete: () => { // sync guard — finish() is async and isTestActive() flips behind an await if (stopped) return; + + const timerStartMs = getLiveCachedTimerStartMs(); + if (timerStartMs === null) { + throw new Error("Timer start ms not found in cache"); + } + const now = performance.now(); - const expectedThisFireMs = timerStartMs + (Time.get() + 1) * 1000; + const expectedThisFireMs = timerStartMs + (emittedTicks + 1) * 1000; const drift = roundTo2(now - expectedThisFireMs); // animejs is rAF-quantized and can fire fractionally early — reschedule @@ -61,16 +68,16 @@ const newTimer = createTimer({ // doesn't pay N times for buildEventLog/WPM/UI. Each missed tick still // gets a step event + per-tick side effects (playTimeWarning, layoutfluid). const ticksDue = Math.floor((now - timerStartMs) / 1000); - while (!stopped && Time.get() + 1 < ticksDue) { + while (!stopped && emittedTicks + 1 < ticksDue) { console.debug( "Catching up timer, missed tick at", - Time.get() + 1, + emittedTicks + 1, "seconds", ); timerStep(now, true); logTestEvent("timer", now, { event: "step", - timer: Time.get(), + timer: emittedTicks, slowTimer: SlowTimer.get() ? true : undefined, catchup: true, }); @@ -82,7 +89,7 @@ const newTimer = createTimer({ timerStep(now, false); logTestEvent("timer", now, { event: "step", - timer: Time.get(), + timer: emittedTicks, slowTimer: SlowTimer.get() ? true : undefined, drift, }); @@ -92,7 +99,7 @@ const newTimer = createTimer({ // Anchor to the ideal grid relative to test start (not `now`) so a late // tick doesn't permanently offset every tick after it. - const expectedNextFireMs = timerStartMs + (Time.get() + 1) * 1000; + const expectedNextFireMs = timerStartMs + (emittedTicks + 1) * 1000; newTimer.duration = Math.max(0, expectedNextFireMs - now); newTimer.restart(); @@ -130,28 +137,28 @@ export function clear(logEnd = false, now = performance.now()): void { if (logEnd) { logTestEvent("timer", now, { event: "end", - timer: Time.get(), + timer: getLiveCachedTestSeconds(now), date: new Date().getTime(), }); } } -function premid(): void { +function premid(testTime: number): void { if (timerDebug) console.time("premid"); const premidSecondsLeft = document.querySelector("#premidSecondsLeft"); if (premidSecondsLeft !== null) { - premidSecondsLeft.innerHTML = (Config.time - Time.get()).toString(); + premidSecondsLeft.innerHTML = (Config.time - testTime).toString(); } if (timerDebug) console.timeEnd("premid"); } -function layoutfluid(): void { +function layoutfluid(time: number): void { if (timerDebug) console.time("layoutfluid"); + if (Config.funbox.includes("layoutfluid") && Config.mode === "time") { const layouts = Config.customLayoutfluid; const switchTime = Config.time / layouts.length; - const time = Time.get(); const index = Math.floor(time / switchTime); const layout = layouts[index]; const flooredSwitchTimes = []; @@ -219,8 +226,9 @@ function checkIfFailed( return false; } -function checkIfTimeIsUp(): void { +function checkIfTimeIsUp(testTime: number): void { if (timerDebug) console.time("times up check"); + let maxTime = undefined; if (Config.mode === "time") { @@ -228,7 +236,7 @@ function checkIfTimeIsUp(): void { } else if (Config.mode === "custom" && CustomText.getLimitMode() === "time") { maxTime = CustomText.getLimitValue(); } - if (maxTime !== undefined && maxTime !== 0 && Time.get() >= maxTime) { + if (maxTime !== undefined && maxTime !== 0 && testTime >= maxTime) { //times up if (timer !== null) clearTimeout(timer); Caret.hide(); @@ -241,7 +249,7 @@ function checkIfTimeIsUp(): void { if (timerDebug) console.timeEnd("times up check"); } -function playTimeWarning(): void { +function playTimeWarning(testTime: number): void { if (timerDebug) console.time("play timer warning"); let maxTime = undefined; @@ -254,7 +262,7 @@ function playTimeWarning(): void { if ( maxTime !== undefined && - Time.get() === maxTime - parseInt(Config.playTimeWarning, 10) + testTime === maxTime - parseInt(Config.playTimeWarning, 10) ) { void SoundController.playTimeWarning(); } @@ -272,14 +280,15 @@ export function getTimerStats(): TimerStats[] { function timerStep(now: number, catchingUp: boolean): void { if (timerDebug) console.time("timer step -----------------------------"); - Time.increment(); + emittedTicks++; + const testTime = emittedTicks; if (catchingUp) { // cheap per-tick side effects — must run for every missed tick during catch-up // so warnings/layout switches still fire on the correct seconds - if (Config.playTimeWarning !== "off") playTimeWarning(); - layoutfluid(); - checkIfTimeIsUp(); + if (Config.playTimeWarning !== "off") playTimeWarning(testTime); + layoutfluid(testTime); + checkIfTimeIsUp(testTime); } else { //calc — only the final, real-time tick pays for these const eventLog = buildEventLog(); @@ -302,7 +311,7 @@ function timerStep(now: number, catchingUp: boolean): void { //ui updates requestDebouncedAnimationFrame("test-timer.timerStep", () => { - premid(); + premid(testTime); }); // already using raf @@ -311,10 +320,10 @@ function timerStep(now: number, catchingUp: boolean): void { setCurrentLiveStats({ wpm: wpmAndRaw.wpm, acc, raw: wpmAndRaw.raw }); //logic - if (Config.playTimeWarning !== "off") playTimeWarning(); - layoutfluid(); + if (Config.playTimeWarning !== "off") playTimeWarning(testTime); + layoutfluid(testTime); const failed = checkIfFailed(wpmAndRaw, acc); - if (!failed) checkIfTimeIsUp(); + if (!failed) checkIfTimeIsUp(testTime); } if (timerDebug) console.timeEnd("timer step -----------------------------"); @@ -356,6 +365,7 @@ function checkIfTimerIsSlow(drift: number): void { export async function start(now: number): Promise { SlowTimer.clear(); slowTimerCount = 0; + emittedTicks = 0; for (const id of slowTimerNotifIds) { removeNotification(id, "clear"); } @@ -366,12 +376,11 @@ export async function start(now: number): Promise { async function _startNew(now: number): Promise { stopped = false; - timerStartMs = now; newTimer.duration = 1000; newTimer.play(); logTestEvent("timer", now, { event: "start", - timer: Time.get(), + timer: 0, date: new Date().getTime(), }); } @@ -381,7 +390,7 @@ async function _startOld(now: number): Promise { expected = now + interval; logTestEvent("timer", now, { event: "start", - timer: Time.get(), + timer: 0, date: new Date().getTime(), }); (function loop(): void { @@ -406,7 +415,7 @@ async function _startOld(now: number): Promise { logTestEvent("timer", now, { event: "step", - timer: Time.get(), + timer: getLiveCachedTestSeconds(now), drift: drift, slowTimer: SlowTimer.get() ? true : undefined, }); diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index ca15f28d2940..686c9d08a3cc 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -2,13 +2,13 @@ import { Config } from "../config/store"; import * as CustomText from "./custom-text"; import * as DateTime from "../utils/date-and-time"; import * as TestWords from "./test-words"; -import * as Time from "../legacy-states/time"; import * as TestState from "./test-state"; import { configEvent } from "../events/config"; import { applyReducedMotion } from "../utils/misc"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; import { animate } from "animejs"; import { getCurrentQuote, isTestActive } from "../states/test"; +import { getLiveCachedTestSeconds } from "./events/live-cache"; const barEl = document.querySelector("#barTimerProgress .bar") as HTMLElement; const barOpacityEl = document.querySelector( @@ -139,7 +139,7 @@ function updateTimer(el: HTMLElement, outof: number, wrapInDiv: boolean): void { export function update(): void { requestDebouncedAnimationFrame("timer-progress.update", () => { - const time = Time.get(); + const time = getLiveCachedTestSeconds(performance.now()); if ( Config.mode === "time" || (Config.mode === "custom" && CustomText.getLimitMode() === "time")