export type PlayLiveEventSource = | 'worker' | 'convex' | 'play' | 'system'; export type PlayRunTimelineEventType = | 'play.run.status' | 'play.run.snapshot' | 'play.run.final_status' | 'play.step.status' | 'play.step.progress' | 'play.run.health' | 'play.run.log' | 'play.sheet.summary' | 'play.sheet.delta'; export type PlayRunTimelineLogEntry = { kind: 'log'; at: string; source: PlayLiveEventSource; message: string; }; export type PlayRunTimelineEventEntry = { kind: 'event'; at: string; eventType: PlayRunTimelineEventType; summary: string; }; export type PlayRunTimelineEntry = | PlayRunTimelineLogEntry | PlayRunTimelineEventEntry; export function makePlayRunTimelineLogEntry(input: { at?: string; source: PlayLiveEventSource; message: string; }): PlayRunTimelineLogEntry { return { kind: 'log', at: input.at ?? new Date().toISOString(), source: input.source, message: input.message, }; } export function makePlayRunTimelineEventEntry(input: { at?: string; eventType: PlayRunTimelineEventType; summary: string; }): PlayRunTimelineEventEntry { return { kind: 'event', at: input.at ?? new Date().toISOString(), eventType: input.eventType, summary: input.summary, }; } export type PlayRuntimeTimingWindow = { startedAt?: number | null; completedAt?: number | null; updatedAt?: number | null; }; export type PlayStepStatusEventPayload = { runId: string; stepId: string; status: 'running' | 'completed' | 'failed' | 'skipped'; label?: string; artifactTableNamespace?: string | null; } & PlayRuntimeTimingWindow; export type PlayStepProgressEventPayload = { runId: string; stepId: string; completed?: number; total?: number; failed?: number; startedRows?: number; activeRows?: number; waitingRows?: number; completedRows?: number; message?: string; artifactTableNamespace?: string | null; } & PlayRuntimeTimingWindow; export type PlayRunHealthEventPayload = { runId: string; state: 'healthy' | 'degraded'; reasons: string[]; message: string; taskQueue: string | null; workflowPollerCount?: number; activityPollerCount?: number; lastHeartbeatAgeMs?: number | null; }; export type PlayRunLogEventPayload = { runId: string; lines: string[]; source: PlayLiveEventSource; }; export type PlaySheetStats = { total: number; queued: number; running: number; completed: number; failed: number; }; export type PlaySheetColumnStats = { queued: number; running: number; completed: number; failed: number; cached: number; missed: number; skipped: number; }; export type PlaySheetSummaryPayload = { runId: string; tableNamespace: string; deltaCursor: number; summary: { stats: PlaySheetStats; columns: Record; } | null; previewRows: Array<{ key: string; status: string; data: Record; cellMeta: Record; }>; }; export type PlaySheetDeltaEventPayload = { runId: string; tableNamespace: string; nextCursor: number; updates: Array<{ key: string; status: string; data: Record; cellMeta: Record; inputIndex?: number; runId?: string; error?: string; stage?: string; provider?: string; seq?: number; createdAt: string; updatedAt: string; }>; }; export function resolveTimingWindow(input: { startedAt?: number | null; completedAt?: number | null; updatedAt?: number | null; }): PlayRuntimeTimingWindow { const startedAt = typeof input.startedAt === 'number' && Number.isFinite(input.startedAt) ? input.startedAt : null; const completedAt = typeof input.completedAt === 'number' && Number.isFinite(input.completedAt) ? input.completedAt : null; const updatedAt = typeof input.updatedAt === 'number' && Number.isFinite(input.updatedAt) ? input.updatedAt : null; return { startedAt, completedAt, updatedAt, }; } export function getTimingDurationMs(input: { startedAt?: number | null; completedAt?: number | null; }): number | null { return typeof input.startedAt === 'number' && Number.isFinite(input.startedAt) && typeof input.completedAt === 'number' && Number.isFinite(input.completedAt) ? Math.max(0, input.completedAt - input.startedAt) : null; } export function getTimingActiveDurationMs(input: { startedAt?: number | null; completedAt?: number | null; updatedAt?: number | null; now?: number; }): number | null { if ( typeof input.startedAt !== 'number' || !Number.isFinite(input.startedAt) ) { return null; } if ( typeof input.completedAt === 'number' && Number.isFinite(input.completedAt) ) { return Math.max(0, input.completedAt - input.startedAt); } const referenceAt = typeof input.updatedAt === 'number' && Number.isFinite(input.updatedAt) ? input.updatedAt : (input.now ?? Date.now()); return Math.max(0, referenceAt - input.startedAt); }