"use client"; import { useCallback, useEffect, useRef } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useSyncQueueStore } from "@/lib/stores/sync-queue.store"; import { enqueueOutboxOp, getAllQueueItems, getOutboxCount, getQueueCount, removeQueueItem, } from "@/lib/offline/offline-db"; import { drainOutbox } from "@/lib/offline/outbox"; function newId(prefix: string): string { if (prefix === "idem" && typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; } /** * One-time migration of any items left in the legacy POS `order_queue` into the * generic outbox, so orders queued before this release still sync. Best-effort. */ async function migrateLegacyQueue(): Promise { let legacy: Awaited> = []; try { legacy = await getAllQueueItems(); } catch { return; } for (const item of legacy) { try { if (item.type === "create_order") { const { cafeId, body } = item.payload as { cafeId: string; body: unknown }; await enqueueOutboxOp({ id: newId("op"), idempotencyKey: newId("idem"), method: "POST", url: `/api/cafes/${cafeId}/orders`, body, entityType: "order", idField: "id", createdAt: Date.parse(item.createdAt) || Date.now(), }); } else if (item.type === "add_items") { const { cafeId, orderId, body } = item.payload as { cafeId: string; orderId: string; body: unknown; }; await enqueueOutboxOp({ id: newId("op"), idempotencyKey: newId("idem"), method: "POST", url: `/api/cafes/${cafeId}/orders/${orderId}/items`, body, entityType: "order_items", createdAt: Date.parse(item.createdAt) || Date.now(), }); } await removeQueueItem(item.id); } catch { // leave the legacy item in place; we'll try again next mount } } } /** * Mount once in the app shell to: * - migrate any legacy queued orders into the outbox, * - keep the pending-count badge and online flag in sync, * - drain the outbox when back online or the tab regains focus, * - refresh server data once writes have synced. */ export function useOfflineSync() { const { setQueueCount, setSyncing, setOnline } = useSyncQueueStore(); const queryClient = useQueryClient(); const syncLock = useRef(false); const refreshCount = useCallback(async () => { const n = (await getOutboxCount()) + (await getQueueCount()); setQueueCount(n); return n; }, [setQueueCount]); const syncQueue = useCallback(async () => { if (syncLock.current) return; if (typeof navigator !== "undefined" && !navigator.onLine) return; syncLock.current = true; setSyncing(true); try { const result = await drainOutbox(); if (result.sent > 0) { // Replace optimistic local data with the authoritative server state. await queryClient.invalidateQueries(); } } finally { syncLock.current = false; setSyncing(false); await refreshCount(); } }, [refreshCount, setSyncing, queryClient]); useEffect(() => { // Ask the browser to keep our IndexedDB (outbox + cache) from being evicted // under storage pressure, so unsynced writes survive. if (typeof navigator !== "undefined" && navigator.storage?.persist) { void navigator.storage.persisted().then((granted) => { if (!granted) void navigator.storage.persist(); }); } void (async () => { await migrateLegacyQueue(); await refreshCount(); // Drain anything pending if we mounted already online. if (typeof navigator === "undefined" || navigator.onLine) void syncQueue(); })(); const handleOnline = () => { setOnline(true); void syncQueue(); }; const handleOffline = () => setOnline(false); setOnline(typeof navigator !== "undefined" ? navigator.onLine : true); window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); const handleVisibility = () => { if (document.visibilityState === "visible" && navigator.onLine) { void syncQueue(); } }; document.addEventListener("visibilitychange", handleVisibility); return () => { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); document.removeEventListener("visibilitychange", handleVisibility); }; }, [syncQueue, setOnline, refreshCount]); return { syncQueue, refreshCount }; }