/** * App-wide sync queue singleton. * * One SyncQueue per browser tab, initialised lazily on first access. Pass * mutations through `enqueue*` helpers below so they're persisted to IDB * BEFORE the network call — that way a lost connection doesn't lose work. * * On window 'online' we flush the queue. Before returning from a mutation * helper we also update the pendingOpsCount store so the banner can react. */ import { browser } from '$app/environment'; import { getSupabase } from '$lib/supabase'; import { pendingOpsCount } from '$lib/stores/syncStatus'; import { SyncQueue, attachOnlineFlush, type NewOp, type QueueContext } from './queue'; let singleton: SyncQueue | null = null; let detachOnline: (() => void) | null = null; export function getQueue(): SyncQueue { if (!singleton) { // The SyncQueue's constructor parameter is structurally compatible with // supabase-js's query builder surface. Cast through unknown rather than // introducing a shared interface for a single integration point. singleton = new SyncQueue(getSupabase() as unknown as ConstructorParameters[0]); if (browser) { detachOnline = attachOnlineFlush(singleton); // Refresh the pendingOpsCount whenever we flush. const originalFlush = singleton.flush.bind(singleton); singleton.flush = async (...args: Parameters) => { const r = await originalFlush(...args); await refreshPending(); return r; }; } } return singleton; } export async function refreshPending(): Promise { if (!singleton) return; const ops = await singleton.pending(); pendingOpsCount.set(ops.length); } /** * Update the queue's "who am I / which collective" context. Called from * the top-level layout when auth + active collective resolve, so the * conflict logger (Fase 9.3) attributes rows correctly. */ export function setSyncContext(ctx: QueueContext): void { getQueue().setContext(ctx); } /** * Enqueue-then-send convenience wrapper. Returns immediately on offline (op * stays queued) and propagates the online-success/failure otherwise. */ export async function enqueueOp(op: NewOp): Promise { const q = getQueue(); await q.enqueue(op); await refreshPending(); } /** * Called at app startup (from a top-level layout) to hydrate the * pendingOpsCount store from IDB so the banner reflects leftover ops. */ export async function hydrateSyncState(): Promise { if (!browser) return; const q = getQueue(); const ops = await q.pending(); pendingOpsCount.set(ops.length); // If we booted up with pending ops AND we're online, flush immediately. if (ops.length > 0 && navigator.onLine) { await q.flush(); } } // For tests that want to tear down the singleton. export function __resetSyncForTests(): void { detachOnline?.(); singleton = null; detachOnline = null; }