2026-03-03 11:27:07 -06:00
|
|
|
import { db, getLastSync, setLastSync } from './db.js'
|
|
|
|
|
import { api } from './api.js'
|
|
|
|
|
|
|
|
|
|
let syncing = false
|
|
|
|
|
let sseSource = null
|
|
|
|
|
|
2026-03-10 14:24:51 -05:00
|
|
|
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()
|
2026-03-10 15:14:36 -05:00
|
|
|
await db.meta.put({ key: 'build', value: build })
|
2026-03-10 14:24:51 -05:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:27:07 -06:00
|
|
|
export async function syncPull() {
|
|
|
|
|
if (syncing) return
|
|
|
|
|
syncing = true
|
|
|
|
|
try {
|
2026-03-10 14:24:51 -05:00
|
|
|
await checkBuildChanged()
|
2026-03-03 11:27:07 -06:00
|
|
|
const since = await getLastSync()
|
|
|
|
|
const data = await api.sync.pull(since)
|
|
|
|
|
|
|
|
|
|
await db.transaction('rw',
|
2026-03-04 15:27:03 -06:00
|
|
|
[db.event, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts],
|
2026-03-03 11:27:07 -06:00
|
|
|
async () => {
|
|
|
|
|
if (data.event) {
|
|
|
|
|
await db.event.put(data.event)
|
|
|
|
|
}
|
2026-03-04 10:53:42 -06:00
|
|
|
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)
|
|
|
|
|
}
|
2026-03-03 11:27:07 -06:00
|
|
|
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)
|
2026-03-03 12:50:24 -06:00
|
|
|
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)
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-10 14:24:51 -05:00
|
|
|
if (data.server_time) await setLastSync(data.server_time)
|
2026-03-03 11:27:07 -06:00
|
|
|
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)}`)
|
|
|
|
|
|
2026-03-03 12:50:24 -06:00
|
|
|
sseSource.onmessage = async (e) => {
|
2026-03-03 11:27:07 -06:00
|
|
|
try {
|
|
|
|
|
const payload = JSON.parse(e.data)
|
|
|
|
|
if (payload.event === 'checkin') {
|
2026-03-04 10:53:42 -06:00
|
|
|
if (payload.data?.type === 'ticket' && payload.data?.ticket) {
|
|
|
|
|
await db.tickets.put(payload.data.ticket)
|
|
|
|
|
}
|
2026-03-03 11:27:07 -06:00
|
|
|
if (payload.data?.type === 'volunteer' && payload.data?.volunteer) {
|
2026-03-03 12:50:24 -06:00
|
|
|
await db.volunteers.put(payload.data.volunteer)
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
|
|
|
|
onEvent?.(payload)
|
|
|
|
|
}
|
2026-03-03 12:50:24 -06:00
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('SSE message error:', err.message)
|
|
|
|
|
}
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sseSource.onerror = () => {
|
|
|
|
|
sseSource?.close()
|
|
|
|
|
sseSource = null
|
2026-03-03 12:50:24 -06:00
|
|
|
setTimeout(() => {
|
|
|
|
|
connect()
|
|
|
|
|
syncPull()
|
|
|
|
|
}, 5000)
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
2026-03-10 14:24:51 -05:00
|
|
|
}).catch(() => {})
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stopSSE() {
|
|
|
|
|
sseSource?.close()
|
|
|
|
|
sseSource = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let syncInterval = null
|
2026-03-10 14:24:51 -05:00
|
|
|
let onlineHandler = null
|
2026-03-03 11:27:07 -06:00
|
|
|
|
|
|
|
|
export function startSyncLoop(intervalMs = 30000) {
|
|
|
|
|
if (syncInterval) return
|
|
|
|
|
syncInterval = setInterval(() => {
|
|
|
|
|
if (navigator.onLine) syncPull()
|
|
|
|
|
}, intervalMs)
|
2026-03-10 14:24:51 -05:00
|
|
|
onlineHandler = () => syncPull()
|
|
|
|
|
window.addEventListener('online', onlineHandler)
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stopSyncLoop() {
|
|
|
|
|
clearInterval(syncInterval)
|
|
|
|
|
syncInterval = null
|
2026-03-10 14:24:51 -05:00
|
|
|
if (onlineHandler) {
|
|
|
|
|
window.removeEventListener('online', onlineHandler)
|
|
|
|
|
onlineHandler = null
|
|
|
|
|
}
|
2026-03-03 11:27:07 -06:00
|
|
|
}
|