const PRACTICE_TYPES = new Set(["open_practice", "free_practice", "practice", "qualification"]); export function normalizeSessionReplayPassings(passings) { return (Array.isArray(passings) ? passings : []) .filter((passing) => Number.isFinite(Number(passing?.timestamp))) .map((passing, index) => ({ ...passing, replayIndex: index, timestamp: Number(passing.timestamp), lapMs: Math.max(0, Number(passing.lapMs || 0) || 0), transponder: String(passing.transponder || ""), competitorKey: String(passing.competitorKey || passing.driverId || passing.teamId || passing.transponder || `passing_${index}`), driverName: String(passing.displayName || passing.teamName || passing.driverName || passing.transponder || "Unknown"), carName: String(passing.subLabel || passing.carName || ""), })) .sort((left, right) => left.timestamp - right.timestamp || left.replayIndex - right.replayIndex); } export function createSessionReplayRuntime() { return { sessionId: "", sessionType: "", sessionName: "", status: "idle", speed: 1, elapsedMs: 0, durationMs: 0, startedWallAt: 0, startedElapsedMs: 0, passings: [], timer: null, }; } export function loadSessionReplay(runtime, session, result) { const passings = normalizeSessionReplayPassings(result?.passings); runtime.sessionId = String(session?.id || ""); runtime.sessionType = String(session?.type || ""); runtime.sessionName = String(session?.name || ""); runtime.status = passings.length ? "paused" : "empty"; runtime.elapsedMs = 0; runtime.durationMs = passings.length > 1 ? Math.max(0, passings[passings.length - 1].timestamp - passings[0].timestamp) : 0; runtime.startedWallAt = 0; runtime.startedElapsedMs = 0; runtime.passings = passings; return runtime; } export function playSessionReplay(runtime, nowTs = Date.now()) { if (!runtime.passings.length) return false; if (runtime.elapsedMs >= runtime.durationMs && runtime.durationMs > 0) runtime.elapsedMs = 0; runtime.status = "running"; runtime.startedWallAt = nowTs; runtime.startedElapsedMs = runtime.elapsedMs; return true; } export function pauseSessionReplay(runtime, nowTs = Date.now()) { if (runtime.status !== "running") return false; advanceSessionReplay(runtime, nowTs); runtime.status = "paused"; return true; } export function resetSessionReplay(runtime) { runtime.status = runtime.passings.length ? "paused" : "idle"; runtime.elapsedMs = 0; runtime.startedWallAt = 0; runtime.startedElapsedMs = 0; return runtime; } export function setSessionReplaySpeed(runtime, speed, nowTs = Date.now()) { if (runtime.status === "running") { advanceSessionReplay(runtime, nowTs); runtime.startedWallAt = nowTs; runtime.startedElapsedMs = runtime.elapsedMs; } runtime.speed = [1, 2, 5, 10].includes(Number(speed)) ? Number(speed) : 1; return runtime.speed; } export function advanceSessionReplay(runtime, nowTs = Date.now()) { if (runtime.status !== "running") return runtime.elapsedMs; const advanced = runtime.startedElapsedMs + Math.max(0, nowTs - runtime.startedWallAt) * runtime.speed; runtime.elapsedMs = Math.min(runtime.durationMs, advanced); if (runtime.elapsedMs >= runtime.durationMs) runtime.status = "finished"; return runtime.elapsedMs; } export function buildSessionReplaySnapshot(runtime) { const firstTimestamp = runtime.passings[0]?.timestamp || 0; const cutoff = firstTimestamp + Math.max(0, runtime.elapsedMs); const visiblePassings = runtime.passings.filter((passing, index) => index === 0 || passing.timestamp <= cutoff); const competitors = new Map(); visiblePassings.forEach((passing) => { const key = passing.competitorKey; const row = competitors.get(key) || { key, driverName: passing.driverName, carName: passing.carName, transponder: passing.transponder, laps: 0, bestLapMs: 0, lastLapMs: 0, firstTimestamp: passing.timestamp, lastTimestamp: passing.timestamp, }; row.driverName = passing.driverName || row.driverName; row.carName = passing.carName || row.carName; row.transponder = passing.transponder || row.transponder; row.lastTimestamp = passing.timestamp; if (passing.validLap !== false && passing.lapMs > 0) { row.laps += 1; row.lastLapMs = passing.lapMs; row.bestLapMs = row.bestLapMs ? Math.min(row.bestLapMs, passing.lapMs) : passing.lapMs; } competitors.set(key, row); }); const practiceRanking = PRACTICE_TYPES.has(runtime.sessionType); const leaderboard = [...competitors.values()] .map((row) => ({ ...row, totalMs: Math.max(0, row.lastTimestamp - row.firstTimestamp) })) .sort((left, right) => { if (practiceRanking) { return (left.bestLapMs || Number.MAX_SAFE_INTEGER) - (right.bestLapMs || Number.MAX_SAFE_INTEGER) || right.laps - left.laps || left.driverName.localeCompare(right.driverName); } return right.laps - left.laps || left.totalMs - right.totalMs || left.driverName.localeCompare(right.driverName); }); return { status: runtime.status, elapsedMs: runtime.elapsedMs, durationMs: runtime.durationMs, progress: runtime.durationMs ? Math.min(1, runtime.elapsedMs / runtime.durationMs) : visiblePassings.length ? 1 : 0, visiblePassings, latestPassing: visiblePassings[visiblePassings.length - 1] || null, leaderboard, }; }