{"version":3,"file":"use-offline-sync.cjs","names":[],"sources":["../../src/offline/use-offline-sync.ts"],"sourcesContent":["import { useEffect, useSyncExternalStore } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport type {\n    OfflineSync,\n    OutboxOp,\n    SyncRunSummary,\n    SyncState,\n    SyncTrigger,\n} from \"./create-offline-sync\";\n\n/**\n * Options for {@link useOfflineSync}. All auto-flush triggers are opt-in except\n * `flushOnOnline`, which defaults to `true` so the app catches up the moment\n * connectivity returns.\n */\nexport interface UseOfflineSyncOptions {\n    /** Flush once when the component mounts (trigger `\"boot\"`). Default `false`. */\n    flushOnMount?: boolean;\n    /**\n     * Flush when the browser fires the `online` event (trigger\n     * `\"online-event\"`). Default `true`.\n     */\n    flushOnOnline?: boolean;\n    /**\n     * Flush on this interval in milliseconds (trigger `\"interval\"`). `0`\n     * disables the timer. Default `0`.\n     */\n    intervalMs?: number;\n}\n\n/**\n * Reactive view of an {@link OfflineSync} engine, plus its imperative actions.\n * Extends {@link SyncState} so `phase`, `pending`, `lastSummary`, `lastError`\n * and `lastSyncedAt` are read directly off the result.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n */\nexport interface UseOfflineSyncResult<TPayload> extends SyncState {\n    /** `true` while a flush run is in progress (`phase === \"syncing\"`). */\n    syncing: boolean;\n    /** Queue a mutation. Mirrors {@link OfflineSync.enqueue}. */\n    enqueue: (op: OutboxOp, recordId: string, payload?: TPayload) => Promise<string>;\n    /** Trigger a flush run. Mirrors {@link OfflineSync.flush}. */\n    flush: (trigger?: SyncTrigger) => Promise<SyncRunSummary>;\n}\n\n/**\n * Subscribe a React component to an {@link OfflineSync} engine.\n *\n * Re-renders whenever the engine's {@link SyncState} changes (enqueue, flush\n * transition, outbox clear) via `useSyncExternalStore`, and optionally wires\n * auto-flush on mount, on the `online` event and on an interval. The engine\n * itself is created once by the app and passed in — the hook never owns it, so\n * multiple components can observe the same queue.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n * @param sync - The engine returned by `createOfflineSync`.\n * @param options - Auto-flush triggers.\n * @returns The reactive state merged with `enqueue`/`flush` actions.\n *\n * @example\n * const { pending, syncing, phase, flush } = useOfflineSync(notesSync, {\n *     flushOnMount: true,\n *     intervalMs: 30_000,\n * });\n * return <SyncStatusBadge pending={pending} syncing={syncing} phase={phase} />;\n */\nexport function useOfflineSync<TPayload>(\n    sync: OfflineSync<TPayload>,\n    options: UseOfflineSyncOptions = {},\n): UseOfflineSyncResult<TPayload> {\n    const { flushOnMount = false, flushOnOnline = true, intervalMs = 0 } = options;\n\n    const state = useSyncExternalStore(sync.subscribe, sync.getState, sync.getState);\n\n    const flushRef = useLatestRef(sync.flush);\n\n    useEffect(() => {\n        if (flushOnMount) void flushRef.current(\"boot\");\n    }, [flushOnMount, flushRef]);\n\n    useEffect(() => {\n        if (!flushOnOnline || typeof window === \"undefined\") return;\n        const handleOnline = (): void => {\n            void flushRef.current(\"online-event\");\n        };\n        window.addEventListener(\"online\", handleOnline);\n        return () => window.removeEventListener(\"online\", handleOnline);\n    }, [flushOnOnline, flushRef]);\n\n    useEffect(() => {\n        if (intervalMs <= 0 || typeof window === \"undefined\") return;\n        const id = window.setInterval(() => {\n            void flushRef.current(\"interval\");\n        }, intervalMs);\n        return () => window.clearInterval(id);\n    }, [intervalMs, flushRef]);\n\n    return {\n        ...state,\n        syncing: state.phase === \"syncing\",\n        enqueue: sync.enqueue,\n        flush: sync.flush,\n    };\n}\n\n/**\n * Presentation-ready tone for a sync status indicator. Collapses the engine's\n * {@link SyncState} into the single most relevant signal for a badge/pill.\n */\nexport type SyncTone = \"idle\" | \"syncing\" | \"pending\" | \"offline\" | \"error\";\n\n/**\n * Compact status snapshot for a status badge/pill. Prefer this over\n * {@link useOfflineSync} when a component only needs to *display* status and\n * does not trigger flushes itself.\n */\nexport interface SyncStatus {\n    /** The dominant tone to render. */\n    tone: SyncTone;\n    /** Number of mutations still queued. */\n    pending: number;\n    /** `true` while a flush run is in progress. */\n    syncing: boolean;\n    /** `true` when the last run was skipped for being offline. */\n    offline: boolean;\n    /** Message of the most recent delivery failure, or `null`. */\n    lastError: string | null;\n}\n\n/**\n * Derive the dominant {@link SyncTone} from an engine snapshot.\n *\n * Precedence: syncing → offline → error → pending → idle. `syncing` wins so an\n * in-flight run always reads as active even while entries remain queued.\n *\n * @param state - The engine {@link SyncState}.\n * @returns The tone to render.\n */\nfunction toneFromState(state: SyncState): SyncTone {\n    if (state.phase === \"syncing\") return \"syncing\";\n    if (state.phase === \"offline\") return \"offline\";\n    if (state.phase === \"error\") return \"error\";\n    if (state.pending > 0) return \"pending\";\n    return \"idle\";\n}\n\n/**\n * Subscribe to an {@link OfflineSync} engine and return a display-only\n * {@link SyncStatus}. A thin read-only companion to {@link useOfflineSync},\n * intended to feed a `<SyncStatusBadge>` without exposing flush actions.\n *\n * @typeParam TPayload - Record snapshot carried by outbox entries.\n * @param sync - The engine returned by `createOfflineSync`.\n * @returns The compact status snapshot.\n */\nexport function useSyncStatus<TPayload>(sync: OfflineSync<TPayload>): SyncStatus {\n    const state = useSyncExternalStore(sync.subscribe, sync.getState, sync.getState);\n    return {\n        tone: toneFromState(state),\n        pending: state.pending,\n        syncing: state.phase === \"syncing\",\n        offline: state.phase === \"offline\",\n        lastError: state.lastError,\n    };\n}\n"],"mappings":"sEAmEA,SAAgB,EACZ,EACA,EAAiC,CAAC,EACJ,CAC9B,GAAM,CAAE,eAAe,GAAO,gBAAgB,GAAM,aAAa,GAAM,EAEjE,GAAA,EAAQ,EAAA,qBAAA,CAAqB,EAAK,UAAW,EAAK,SAAU,EAAK,QAAQ,EAEzE,EAAW,EAAA,aAAa,EAAK,KAAK,EAuBxC,OArBA,EAAA,EAAA,UAAA,KAAgB,CACR,GAAc,EAAc,QAAQ,MAAM,CAClD,EAAG,CAAC,EAAc,CAAQ,CAAC,GAE3B,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAiB,OAAO,OAAW,IAAa,OACrD,IAAM,MAA2B,CAC7B,EAAc,QAAQ,cAAc,CACxC,EAEA,OADA,OAAO,iBAAiB,SAAU,CAAY,MACjC,OAAO,oBAAoB,SAAU,CAAY,CAClE,EAAG,CAAC,EAAe,CAAQ,CAAC,GAE5B,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,GAAc,GAAK,OAAO,OAAW,IAAa,OACtD,IAAM,EAAK,OAAO,gBAAkB,CAChC,EAAc,QAAQ,UAAU,CACpC,EAAG,CAAU,EACb,UAAa,OAAO,cAAc,CAAE,CACxC,EAAG,CAAC,EAAY,CAAQ,CAAC,EAElB,CACH,GAAG,EACH,QAAS,EAAM,QAAU,UACzB,QAAS,EAAK,QACd,MAAO,EAAK,KAChB,CACJ,CAmCA,SAAS,EAAc,EAA4B,CAK/C,OAJI,EAAM,QAAU,UAAkB,UAClC,EAAM,QAAU,UAAkB,UAClC,EAAM,QAAU,QAAgB,QAChC,EAAM,QAAU,EAAU,UACvB,MACX,CAWA,SAAgB,EAAwB,EAAyC,CAC7E,IAAM,GAAA,EAAQ,EAAA,qBAAA,CAAqB,EAAK,UAAW,EAAK,SAAU,EAAK,QAAQ,EAC/E,MAAO,CACH,KAAM,EAAc,CAAK,EACzB,QAAS,EAAM,QACf,QAAS,EAAM,QAAU,UACzB,QAAS,EAAM,QAAU,UACzB,UAAW,EAAM,SACrB,CACJ"}