mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
|
|
// Live timing state model.
|
||
|
|
//
|
||
|
|
// The Live page has to hold four *orthogonal* concepts that used to be
|
||
|
|
// conflated into a single "connected / offline" flag:
|
||
|
|
//
|
||
|
|
// 1. transport health — is the SSE stream up? (connecting/connected/…)
|
||
|
|
// 2. active session — is a session actually running right now?
|
||
|
|
// 3. archive availability — do we retain a final snapshot to inspect?
|
||
|
|
// 4. analysis readiness — is the completed session ingested for Race Hub?
|
||
|
|
//
|
||
|
|
// `deriveLivePhase` collapses the first three inputs into a single UI phase so
|
||
|
|
// the page never, for example, calls a finished session "offline" or lets a
|
||
|
|
// dropped socket masquerade as "archive mode".
|
||
|
|
|
||
|
|
export type TransportHealth = 'connecting' | 'connected' | 'disconnected' | 'error'
|
||
|
|
|
||
|
|
export type LivePhase =
|
||
|
|
| 'connecting' // cold start: no snapshot yet, still opening the feed
|
||
|
|
| 'live' // a session is running and streaming
|
||
|
|
| 'disconnected' // was live, transport dropped — keep the last snapshot, warn
|
||
|
|
| 'settling' // session ended; a final snapshot is retained, analysis pending
|
||
|
|
| 'archive' // user opened the retained snapshot as an explicit read-only view
|
||
|
|
| 'inactive' // no session and nothing retained — show weekend context instead
|
||
|
|
|
||
|
|
export interface LiveStateInputs {
|
||
|
|
transport: TransportHealth
|
||
|
|
/** The feed reports an active session AND carries a snapshot for it. */
|
||
|
|
isLive: boolean
|
||
|
|
hasActiveSnapshot: boolean
|
||
|
|
/** A final snapshot from the last session is retained. */
|
||
|
|
hasArchive: boolean
|
||
|
|
/** The user explicitly opened the archive as a read-only timing view. */
|
||
|
|
archiveMode: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
export function transportDown(transport: TransportHealth): boolean {
|
||
|
|
return transport === 'disconnected' || transport === 'error'
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Map transport + session + archive inputs to one UI phase. Session lifecycle
|
||
|
|
* and transport health are deliberately orthogonal: a live session with a
|
||
|
|
* dropped socket is `disconnected` (snapshot retained), never `archive`.
|
||
|
|
*/
|
||
|
|
export function deriveLivePhase(input: LiveStateInputs): LivePhase {
|
||
|
|
const { transport, isLive, hasActiveSnapshot, hasArchive, archiveMode } = input
|
||
|
|
|
||
|
|
// Explicit read-only archive wins — it is a user-chosen mode.
|
||
|
|
if (archiveMode && hasArchive) return 'archive'
|
||
|
|
|
||
|
|
// An active session: transport health only downgrades the *presentation*,
|
||
|
|
// it never removes the session.
|
||
|
|
if (isLive && hasActiveSnapshot) {
|
||
|
|
return transportDown(transport) ? 'disconnected' : 'live'
|
||
|
|
}
|
||
|
|
|
||
|
|
// No active session but we still hold the final snapshot -> settling handoff.
|
||
|
|
if (hasArchive) return 'settling'
|
||
|
|
|
||
|
|
// Nothing yet and the feed is still opening.
|
||
|
|
if (transport === 'connecting') return 'connecting'
|
||
|
|
|
||
|
|
return 'inactive'
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Phases that render the live timing tower / snapshot surface. */
|
||
|
|
export function rendersSnapshot(phase: LivePhase): boolean {
|
||
|
|
return phase === 'live' || phase === 'disconnected' || phase === 'archive'
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Whether live-only interpretations (pit-window rejoin, tyre-deg slope, "what
|
||
|
|
* just happened" deltas) are meaningful. They require a moving session, so an
|
||
|
|
* archived single frame must never present them as current insight.
|
||
|
|
*/
|
||
|
|
export function allowsLiveInterpretations(phase: LivePhase): boolean {
|
||
|
|
return phase === 'live' || phase === 'disconnected'
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Read-only, timestamped phases that must not show live/connected chrome. */
|
||
|
|
export function isReadOnlyPhase(phase: LivePhase): boolean {
|
||
|
|
return phase === 'archive'
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Short, human transport-health label — always secondary to session state. */
|
||
|
|
export function feedHealthLabel(transport: TransportHealth): string {
|
||
|
|
switch (transport) {
|
||
|
|
case 'connected':
|
||
|
|
return 'Feed healthy'
|
||
|
|
case 'connecting':
|
||
|
|
return 'Connecting'
|
||
|
|
case 'disconnected':
|
||
|
|
return 'Reconnecting'
|
||
|
|
case 'error':
|
||
|
|
return 'Feed unavailable'
|
||
|
|
}
|
||
|
|
}
|