import { db, getLastSync, setLastSync } from './db.js' import { api } from './api.js' let syncing = false let sseSource = null async function checkBuildChanged() { try { const res = await fetch('/api/version') const { build } = await res.json() if (!build) return const stored = await db.meta.get('build') if (!stored || stored.value !== build) { await db.transaction('rw', [db.meta, db.event, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts], async () => { await db.meta.clear() await db.event.clear() await db.participants.clear() await db.tickets.clear() await db.departments.clear() await db.volunteers.clear() await db.shifts.clear() await db.volunteer_shifts.clear() await db.meta.put({ key: 'build', value: build }) } ) } } catch {} } export async function syncPull() { if (syncing) return syncing = true try { await checkBuildChanged() const since = await getLastSync() const data = await api.sync.pull(since) await db.transaction('rw', [db.event, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts], async () => { if (data.event) { await db.event.put(data.event) } if (data.participants?.length) { await db.participants.bulkPut(data.participants) const deleted = data.participants.filter(p => p.deleted_at).map(p => p.id) if (deleted.length) await db.participants.bulkDelete(deleted) } if (data.tickets?.length) { await db.tickets.bulkPut(data.tickets) const deleted = data.tickets.filter(t => t.deleted_at).map(t => t.id) if (deleted.length) await db.tickets.bulkDelete(deleted) } if (data.departments?.length) { await db.departments.bulkPut(data.departments) const deleted = data.departments.filter(d => d.deleted_at).map(d => d.id) if (deleted.length) await db.departments.bulkDelete(deleted) } if (data.volunteers?.length) { await db.volunteers.bulkPut(data.volunteers) const deleted = data.volunteers.filter(v => v.deleted_at).map(v => v.id) if (deleted.length) await db.volunteers.bulkDelete(deleted) } if (data.shifts?.length) { await db.shifts.bulkPut(data.shifts) const deleted = data.shifts.filter(s => s.deleted_at).map(s => s.id) if (deleted.length) await db.shifts.bulkDelete(deleted) } if (data.volunteer_shifts?.length) { await db.volunteer_shifts.bulkPut(data.volunteer_shifts) const deleted = data.volunteer_shifts.filter(vs => vs.deleted_at) .map(vs => [vs.volunteer_id, vs.shift_id]) if (deleted.length) await db.volunteer_shifts.bulkDelete(deleted) } } ) if (data.server_time) await setLastSync(data.server_time) return true } catch (err) { console.warn('Sync pull failed:', err.message) return false } finally { syncing = false } } export function startSSE(onEvent) { if (sseSource) return const connect = () => { // Get token synchronously from Dexie — SSE doesn't support headers natively, // so we pass the token as a query param (acceptable since it's same-origin HTTPS). db.session.get(1).then(session => { if (!session?.token) return sseSource = new EventSource(`/api/sync/stream?token=${encodeURIComponent(session.token)}`) sseSource.onmessage = async (e) => { try { const payload = JSON.parse(e.data) if (payload.event === 'checkin') { if (payload.data?.type === 'ticket' && payload.data?.ticket) { await db.tickets.put(payload.data.ticket) } if (payload.data?.type === 'volunteer' && payload.data?.volunteer) { await db.volunteers.put(payload.data.volunteer) } onEvent?.(payload) } } catch (err) { console.warn('SSE message error:', err.message) } } sseSource.onerror = () => { sseSource?.close() sseSource = null setTimeout(() => { connect() syncPull() }, 5000) } }).catch(() => {}) } connect() } export function stopSSE() { sseSource?.close() sseSource = null } let syncInterval = null let onlineHandler = null export function startSyncLoop(intervalMs = 30000) { if (syncInterval) return syncInterval = setInterval(() => { if (navigator.onLine) syncPull() }, intervalMs) onlineHandler = () => syncPull() window.addEventListener('online', onlineHandler) } export function stopSyncLoop() { clearInterval(syncInterval) syncInterval = null if (onlineHandler) { window.removeEventListener('online', onlineHandler) onlineHandler = null } }