import { useEffect, useRef } from 'react' import { useSession, type SessionState } from '@/store/session' import { pushTeam, pushSubmission, type SubmissionPush } from './api' import type { TeamSnapshot } from '@/types' const OUTBOX_KEY = 'apess_outbox' /** Project the live store into a collective team snapshot. */ export function projectSnapshot(s: SessionState): TeamSnapshot { return { id: s.teamId, name: s.team.name, kit: s.team.kit, members: s.team.members, domain: s.domain, phases: s.phases, stats: s.stats, deviceConnected: s.device.connected, updatedAt: new Date().toISOString(), } } type OutboxItem = { kind: 'team'; payload: TeamSnapshot } | { kind: 'submission'; payload: SubmissionPush } function readOutbox(): OutboxItem[] { try { return JSON.parse(localStorage.getItem(OUTBOX_KEY) ?? '[]') as OutboxItem[] } catch { return [] } } function writeOutbox(items: OutboxItem[]): void { localStorage.setItem(OUTBOX_KEY, JSON.stringify(items)) } function enqueue(item: OutboxItem): void { // collapse duplicate team pushes — only the latest snapshot matters const items = readOutbox().filter((i) => i.kind !== item.kind || item.kind === 'submission') items.push(item) writeOutbox(items) } async function send(item: OutboxItem): Promise { if (item.kind === 'team') await pushTeam(item.payload) else await pushSubmission(item.payload) } /** Retry everything queued while offline; survivors stay queued. */ export async function flushOutbox(): Promise { const items = readOutbox() if (!items.length) return const remaining: OutboxItem[] = [] for (const item of items) { try { await send(item) } catch { remaining.push(item) } } writeOutbox(remaining) } export async function safePushTeam(snapshot: TeamSnapshot): Promise { try { await pushTeam(snapshot) } catch { enqueue({ kind: 'team', payload: snapshot }) } } export async function safePushSubmission(payload: SubmissionPush): Promise { try { await pushSubmission(payload) } catch { enqueue({ kind: 'submission', payload }) } } export interface UseCollectiveSyncOptions { throttleMs?: number } /** * Best-effort, offline-first sync of this team's state to the collective. * Mounted once near the router root. Pushes on phase changes and submission, * throttles stat-driven churn, and never blocks the participant flow — failed * pushes go to a localStorage outbox and retry on interval / `online`. */ export function useCollectiveSync(opts: UseCollectiveSyncOptions = {}): void { const throttleMs = opts.throttleMs ?? 8000 const lastPush = useRef(0) const lastSubmission = useRef(null) useEffect(() => { void flushOutbox() const onOnline = () => void flushOutbox() window.addEventListener('online', onOnline) const retry = setInterval(() => void flushOutbox(), 20000) const unsub = useSession.subscribe((state, prev) => { // submission is the one push that matters most — fire immediately, once const code = state.submission.code if (code && code !== lastSubmission.current) { lastSubmission.current = code void safePushSubmission({ teamId: state.teamId, teamName: state.team.name, code, add: state.add, submittedAt: state.submission.submittedAt ?? new Date().toISOString(), }) } const phaseChanged = state.phases !== prev.phases const now = Date.now() if (phaseChanged || now - lastPush.current > throttleMs) { lastPush.current = now void safePushTeam(projectSnapshot(state)) } }) return () => { window.removeEventListener('online', onOnline) clearInterval(retry) unsub() } }, [throttleMs]) }