@pol-studios/powersync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/attachments/index.d.ts +399 -0
  2. package/dist/attachments/index.js +16 -0
  3. package/dist/attachments/index.js.map +1 -0
  4. package/dist/chunk-32OLICZO.js +1 -0
  5. package/dist/chunk-32OLICZO.js.map +1 -0
  6. package/dist/chunk-4FJVBR3X.js +227 -0
  7. package/dist/chunk-4FJVBR3X.js.map +1 -0
  8. package/dist/chunk-7BPTGEVG.js +1 -0
  9. package/dist/chunk-7BPTGEVG.js.map +1 -0
  10. package/dist/chunk-7JQZBZ5N.js +1 -0
  11. package/dist/chunk-7JQZBZ5N.js.map +1 -0
  12. package/dist/chunk-BJ36QDFN.js +290 -0
  13. package/dist/chunk-BJ36QDFN.js.map +1 -0
  14. package/dist/chunk-CFCK2LHI.js +1002 -0
  15. package/dist/chunk-CFCK2LHI.js.map +1 -0
  16. package/dist/chunk-CHRTN5PF.js +322 -0
  17. package/dist/chunk-CHRTN5PF.js.map +1 -0
  18. package/dist/chunk-FLHDT4TS.js +327 -0
  19. package/dist/chunk-FLHDT4TS.js.map +1 -0
  20. package/dist/chunk-GBGATW2S.js +749 -0
  21. package/dist/chunk-GBGATW2S.js.map +1 -0
  22. package/dist/chunk-NPNBGCRC.js +65 -0
  23. package/dist/chunk-NPNBGCRC.js.map +1 -0
  24. package/dist/chunk-Q3LFFMRR.js +925 -0
  25. package/dist/chunk-Q3LFFMRR.js.map +1 -0
  26. package/dist/chunk-T225XEML.js +298 -0
  27. package/dist/chunk-T225XEML.js.map +1 -0
  28. package/dist/chunk-W7HSR35B.js +1 -0
  29. package/dist/chunk-W7HSR35B.js.map +1 -0
  30. package/dist/connector/index.d.ts +5 -0
  31. package/dist/connector/index.js +14 -0
  32. package/dist/connector/index.js.map +1 -0
  33. package/dist/core/index.d.ts +197 -0
  34. package/dist/core/index.js +96 -0
  35. package/dist/core/index.js.map +1 -0
  36. package/dist/index-nae7nzib.d.ts +147 -0
  37. package/dist/index.d.ts +68 -0
  38. package/dist/index.js +191 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/index.native.d.ts +14 -0
  41. package/dist/index.native.js +195 -0
  42. package/dist/index.native.js.map +1 -0
  43. package/dist/index.web.d.ts +14 -0
  44. package/dist/index.web.js +195 -0
  45. package/dist/index.web.js.map +1 -0
  46. package/dist/platform/index.d.ts +280 -0
  47. package/dist/platform/index.js +14 -0
  48. package/dist/platform/index.js.map +1 -0
  49. package/dist/platform/index.native.d.ts +37 -0
  50. package/dist/platform/index.native.js +7 -0
  51. package/dist/platform/index.native.js.map +1 -0
  52. package/dist/platform/index.web.d.ts +37 -0
  53. package/dist/platform/index.web.js +7 -0
  54. package/dist/platform/index.web.js.map +1 -0
  55. package/dist/provider/index.d.ts +873 -0
  56. package/dist/provider/index.js +63 -0
  57. package/dist/provider/index.js.map +1 -0
  58. package/dist/supabase-connector-D14-kl5v.d.ts +232 -0
  59. package/dist/sync/index.d.ts +421 -0
  60. package/dist/sync/index.js +14 -0
  61. package/dist/sync/index.js.map +1 -0
  62. package/dist/types-Cd7RhNqf.d.ts +224 -0
  63. package/dist/types-afHtE1U_.d.ts +391 -0
  64. package/package.json +101 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider/context.ts","../src/provider/PowerSyncProvider.tsx","../src/provider/hooks.ts"],"sourcesContent":["/**\n * React Contexts for @pol-studios/powersync\n *\n * This module creates the React contexts used by the PowerSyncProvider.\n */\n\nimport { createContext } from 'react';\nimport type {\n PowerSyncContextValue,\n SyncStatusContextValue,\n ConnectionHealthContextValue,\n SyncMetricsContextValue,\n DEFAULT_SYNC_STATUS,\n DEFAULT_CONNECTION_HEALTH,\n DEFAULT_SYNC_METRICS,\n} from './types';\nimport type { AttachmentQueue } from '../attachments/attachment-queue';\n\n// ─── Main PowerSync Context ──────────────────────────────────────────────────\n\n/**\n * Main context for PowerSync database instance and related state.\n *\n * Provides access to:\n * - PowerSync database instance\n * - SupabaseConnector instance\n * - AttachmentQueue instance (if configured)\n * - Initialization state\n *\n * @example\n * ```typescript\n * const { db, isReady, error } = useContext(PowerSyncContext);\n * ```\n */\nexport const PowerSyncContext = createContext<PowerSyncContextValue | null>(null);\n\nPowerSyncContext.displayName = 'PowerSyncContext';\n\n// ─── Sync Status Context ─────────────────────────────────────────────────────\n\n/**\n * Context for sync status updates.\n *\n * Provides access to:\n * - Current sync status (connected, syncing, etc.)\n * - Pending mutations count\n * - Paused state\n * - Last synced timestamp\n *\n * @example\n * ```typescript\n * const { status, pendingCount, isPaused } = useContext(SyncStatusContext);\n * ```\n */\nexport const SyncStatusContext = createContext<SyncStatusContextValue | null>(null);\n\nSyncStatusContext.displayName = 'SyncStatusContext';\n\n// ─── Connection Health Context ───────────────────────────────────────────────\n\n/**\n * Context for connection health monitoring.\n *\n * Provides access to:\n * - Health status (healthy, degraded, disconnected)\n * - Latency measurements\n * - Health check timestamps\n * - Failure counts\n *\n * @example\n * ```typescript\n * const { health } = useContext(ConnectionHealthContext);\n * if (health.status === 'degraded') {\n * showWarning('Connection is slow');\n * }\n * ```\n */\nexport const ConnectionHealthContext = createContext<ConnectionHealthContextValue | null>(null);\n\nConnectionHealthContext.displayName = 'ConnectionHealthContext';\n\n// ─── Sync Metrics Context ────────────────────────────────────────────────────\n\n/**\n * Context for sync metrics and statistics.\n *\n * Provides access to:\n * - Sync operation counts\n * - Sync durations\n * - Data transfer amounts\n * - Error tracking\n *\n * @example\n * ```typescript\n * const { metrics } = useContext(SyncMetricsContext);\n * console.log(`Total syncs: ${metrics.totalSyncs}`);\n * ```\n */\nexport const SyncMetricsContext = createContext<SyncMetricsContextValue | null>(null);\n\nSyncMetricsContext.displayName = 'SyncMetricsContext';\n\n// ─── Attachment Queue Context ────────────────────────────────────────────────\n\n/**\n * Context for the attachment queue (if configured).\n *\n * Provides direct access to the AttachmentQueue instance for:\n * - Checking attachment sync stats\n * - Pausing/resuming downloads\n * - Getting local URIs for attachments\n *\n * @example\n * ```typescript\n * const attachmentQueue = useContext(AttachmentQueueContext);\n * if (attachmentQueue) {\n * const stats = await attachmentQueue.getStats();\n * console.log(`Downloaded: ${stats.syncedCount}/${stats.totalExpected}`);\n * }\n * ```\n */\nexport const AttachmentQueueContext = createContext<AttachmentQueue | null>(null);\n\nAttachmentQueueContext.displayName = 'AttachmentQueueContext';\n","/**\n * PowerSyncProvider Component for @pol-studios/powersync\n *\n * Main provider component that initializes and manages the PowerSync database,\n * connector, attachment queue, and all monitoring services.\n */\n\nimport React, {\n useEffect,\n useState,\n useRef,\n useMemo,\n useCallback,\n} from 'react';\nimport type { Session } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase, SyncStatus, ConnectionHealth, SyncMetrics, CrudEntry, FailedTransaction, CompletedTransaction, SyncMode } from '../core/types';\nimport { SupabaseConnector } from '../connector/supabase-connector';\nimport { extractEntityIds, extractTableNames, createSyncError } from '../core/errors';\nimport { AttachmentQueue } from '../attachments/attachment-queue';\nimport { SyncStatusTracker } from '../sync/status-tracker';\nimport { MetricsCollector } from '../sync/metrics-collector';\nimport { HealthMonitor } from '../sync/health-monitor';\nimport {\n PowerSyncContext,\n SyncStatusContext,\n ConnectionHealthContext,\n SyncMetricsContext,\n AttachmentQueueContext,\n} from './context';\nimport type {\n PowerSyncProviderProps,\n PowerSyncContextValue,\n SyncStatusContextValue,\n ConnectionHealthContextValue,\n SyncMetricsContextValue,\n} from './types';\nimport {\n DEFAULT_SYNC_STATUS,\n DEFAULT_CONNECTION_HEALTH,\n DEFAULT_SYNC_METRICS,\n} from './types';\n\n/**\n * PowerSyncProvider initializes and manages the PowerSync database and related services.\n *\n * Features:\n * - Initializes PowerSync database using platform adapter\n * - Creates and manages SupabaseConnector\n * - Connects/disconnects based on auth state\n * - Tracks sync status and connection health\n * - Optionally initializes AttachmentQueue\n * - Provides all contexts to children\n * - Handles cleanup on unmount\n *\n * @example\n * ```tsx\n * import { PowerSyncProvider, usePowerSync, useSyncStatus } from '@pol-studios/powersync';\n *\n * function App() {\n * return (\n * <PowerSyncProvider\n * config={{\n * platform: createNativePlatformAdapter(logger),\n * schema: AppSchema,\n * powerSyncUrl: 'https://your-powersync.com',\n * supabaseClient: supabase,\n * }}\n * onReady={() => console.log('PowerSync ready!')}\n * onError={(err) => console.error('PowerSync error:', err)}\n * >\n * <MainApp />\n * </PowerSyncProvider>\n * );\n * }\n * ```\n */\nexport function PowerSyncProvider<TSchema = unknown>({\n config,\n children,\n onReady,\n onError,\n onSyncStatusChange,\n}: PowerSyncProviderProps<TSchema>): React.ReactElement {\n const {\n platform,\n schema,\n powerSyncUrl,\n supabaseClient,\n dbFilename = 'powersync.db',\n connector: connectorConfig,\n attachments: attachmentConfig,\n sync: syncConfig,\n } = config;\n\n const logger = platform.logger;\n\n // Merge sync config with defaults\n const mergedSyncConfig = {\n autoConnect: syncConfig?.autoConnect ?? true,\n syncInterval: syncConfig?.syncInterval ?? 0,\n enableHealthMonitoring: syncConfig?.enableHealthMonitoring ?? true,\n enableMetrics: syncConfig?.enableMetrics ?? true,\n };\n\n // ─── State ─────────────────────────────────────────────────────────────────\n\n const [db, setDb] = useState<AbstractPowerSyncDatabase | null>(null);\n const [connector, setConnector] = useState<SupabaseConnector | null>(null);\n const [attachmentQueue, setAttachmentQueue] = useState<AttachmentQueue | null>(null);\n const [isReady, setIsReady] = useState(false);\n const [isInitializing, setIsInitializing] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n const [session, setSession] = useState<Session | null>(null);\n\n // Sync status state\n const [syncStatus, setSyncStatus] = useState<SyncStatus>(DEFAULT_SYNC_STATUS);\n const [pendingMutations, setPendingMutations] = useState<CrudEntry[]>([]);\n // Combined sync mode state to ensure atomic updates and prevent React batching issues\n const [syncModeState, setSyncModeState] = useState<{ loaded: boolean; mode: SyncMode }>({\n loaded: false,\n mode: 'push-pull',\n });\n const [lastSyncedAt, setLastSyncedAt] = useState<Date | null>(null);\n const [connectionError, setConnectionError] = useState<Error | null>(null);\n const [failedTransactions, setFailedTransactions] = useState<FailedTransaction[]>([]);\n const [completedTransactions, setCompletedTransactions] = useState<CompletedTransaction[]>([]);\n\n // Health and metrics state\n const [connectionHealth, setConnectionHealth] = useState<ConnectionHealth>(DEFAULT_CONNECTION_HEALTH);\n const [syncMetrics, setSyncMetrics] = useState<SyncMetrics>(DEFAULT_SYNC_METRICS);\n\n // ─── Refs ──────────────────────────────────────────────────────────────────\n\n const statusTrackerRef = useRef<SyncStatusTracker | null>(null);\n const metricsCollectorRef = useRef<MetricsCollector | null>(null);\n const healthMonitorRef = useRef<HealthMonitor | null>(null);\n const attachmentQueueRef = useRef<AttachmentQueue | null>(null);\n const listenerUnsubscribeRef = useRef<(() => void) | null>(null);\n const wasSyncingRef = useRef(false);\n const initializingRef = useRef(false);\n // Track when database is being/has been closed to prevent operations on closed db\n const dbClosedRef = useRef(false);\n\n // ─── Initialize Monitoring Services ────────────────────────────────────────\n\n useEffect(() => {\n // Create status tracker\n const statusTracker = new SyncStatusTracker(\n platform.storage,\n logger,\n {\n onStatusChange: (status) => {\n setSyncStatus(status);\n setLastSyncedAt(status.lastSyncedAt);\n onSyncStatusChange?.(status);\n },\n }\n );\n statusTrackerRef.current = statusTracker;\n\n // Create metrics collector\n const metricsCollector = new MetricsCollector(\n platform.storage,\n logger,\n {\n onMetricsChange: setSyncMetrics,\n }\n );\n metricsCollectorRef.current = metricsCollector;\n\n // Create health monitor\n const healthMonitor = new HealthMonitor(\n logger,\n {\n onHealthChange: setConnectionHealth,\n }\n );\n healthMonitorRef.current = healthMonitor;\n\n // Initialize async with timeout fallback\n const initPromise = Promise.all([\n statusTracker.init(),\n metricsCollector.init(),\n ]);\n\n const timeoutPromise = new Promise<[void, void]>((resolve) => {\n setTimeout(() => {\n logger.warn('[PowerSyncProvider] Sync mode state load timed out, using default (push-pull)');\n resolve([undefined, undefined]);\n }, 5000); // 5 second timeout\n });\n\n Promise.race([initPromise, timeoutPromise]).then(() => {\n logger.info('[PowerSyncProvider] Sync mode state loaded:', {\n mode: statusTracker.getSyncMode(),\n });\n // Use single state update to ensure atomic change and prevent React batching issues\n setSyncModeState({ loaded: true, mode: statusTracker.getSyncMode() });\n });\n\n // Cleanup\n return () => {\n statusTracker.dispose();\n metricsCollector.dispose();\n healthMonitor.dispose();\n };\n }, [platform, logger, onSyncStatusChange]);\n\n // ─── Auth State Listener ───────────────────────────────────────────────────\n\n useEffect(() => {\n logger.debug('[PowerSyncProvider] Setting up auth listener');\n\n // Get initial session\n supabaseClient.auth.getSession().then(({ data: { session: initialSession } }) => {\n logger.debug('[PowerSyncProvider] Initial session:', !!initialSession);\n setSession(initialSession);\n });\n\n // Listen for auth changes\n const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(\n (_event, newSession) => {\n logger.debug('[PowerSyncProvider] Auth state changed, hasSession:', !!newSession);\n setSession(newSession);\n }\n );\n\n return () => {\n subscription.unsubscribe();\n };\n }, [supabaseClient, logger]);\n\n // ─── Database Initialization ───────────────────────────────────────────────\n\n useEffect(() => {\n // Guard against StrictMode double-mounting\n // The ref persists across the quick unmount/remount cycle\n if (initializingRef.current) {\n logger.debug('[PowerSyncProvider] Already initializing, skipping...');\n return;\n }\n initializingRef.current = true;\n\n // Use a controller object to track cancellation\n const controller = { cancelled: false };\n\n const initDatabase = async () => {\n try {\n logger.info('[PowerSyncProvider] Initializing database...');\n\n // Create database using platform adapter (includes init())\n const database = await platform.createDatabase({\n dbFilename,\n schema,\n });\n\n // Check if cancelled during async operation\n if (controller.cancelled) {\n logger.debug('[PowerSyncProvider] Init cancelled, closing database...');\n await database.close();\n initializingRef.current = false;\n return;\n }\n\n logger.info('[PowerSyncProvider] Database initialized');\n setDb(database);\n setIsReady(true);\n setIsInitializing(false);\n\n // Set database on health monitor\n healthMonitorRef.current?.setDatabase(database);\n\n // Start health monitoring if enabled\n if (mergedSyncConfig.enableHealthMonitoring) {\n healthMonitorRef.current?.start();\n }\n\n onReady?.();\n } catch (err) {\n const initError = err instanceof Error ? err : new Error(String(err));\n logger.error('[PowerSyncProvider] Initialization failed:', initError);\n\n // Only update state if not cancelled\n if (!controller.cancelled) {\n setError(initError);\n setIsInitializing(false);\n onError?.(initError);\n }\n\n // Reset ref on error so retry is possible\n initializingRef.current = false;\n }\n };\n\n initDatabase();\n\n return () => {\n // Mark as cancelled - the async operation will check this\n controller.cancelled = true;\n // Reset initializingRef so StrictMode double-mount can reinitialize\n initializingRef.current = false;\n };\n }, [platform, dbFilename, schema, logger, mergedSyncConfig.enableHealthMonitoring, onReady, onError]);\n\n // ─── Connect to PowerSync ──────────────────────────────────────────────────\n\n useEffect(() => {\n // Console.log fallback for critical diagnostic point (always visible regardless of logger level)\n if (__DEV__) {\n console.log('[PowerSyncProvider] Connect effect triggered:', {\n hasDb: !!db,\n hasSession: !!session,\n autoConnect: mergedSyncConfig.autoConnect,\n syncModeState,\n });\n }\n\n // Use info level for critical connect path so it always appears\n logger.info('[PowerSyncProvider] Connect effect - db:', !!db, 'session:', !!session, 'autoConnect:', mergedSyncConfig.autoConnect, 'syncModeLoaded:', syncModeState.loaded, 'syncMode:', syncModeState.mode);\n\n // Individual checks with logging for each early return condition\n if (!db) {\n logger.debug('[PowerSyncProvider] Connect effect - waiting for db');\n return;\n }\n if (!session) {\n logger.debug('[PowerSyncProvider] Connect effect - no session');\n return;\n }\n if (!mergedSyncConfig.autoConnect) {\n logger.debug('[PowerSyncProvider] Connect effect - autoConnect disabled');\n return;\n }\n\n // Wait for sync mode state to be loaded before deciding\n if (!syncModeState.loaded) {\n logger.info('[PowerSyncProvider] Waiting for sync mode state to load...');\n return;\n }\n\n // Skip connect if offline mode\n if (syncModeState.mode === 'offline') {\n logger.debug('[PowerSyncProvider] Skipping connect - offline mode');\n return;\n }\n\n const connectPowerSync = async () => {\n try {\n setConnectionError(null);\n // Create connector with failure callbacks\n const statusTracker = statusTrackerRef.current;\n const newConnector = new SupabaseConnector({\n supabaseClient,\n powerSyncUrl,\n schemaRouter: connectorConfig?.schemaRouter,\n crudHandler: connectorConfig?.crudHandler,\n logger,\n // Check if uploads should be performed based on sync mode\n shouldUpload: () => statusTrackerRef.current?.shouldUpload() ?? true,\n // Clear failures when transaction succeeds\n onTransactionSuccess: (entries) => {\n if (!statusTracker) return;\n const entityIds = extractEntityIds(entries);\n entityIds.forEach(id => {\n // Find and clear failures affecting this entity\n const failures = statusTracker.getFailuresForEntity(id);\n failures.forEach(f => statusTracker.clearFailure(f.id));\n });\n // Update local state\n setFailedTransactions(statusTracker.getFailedTransactions());\n },\n // Record failures when transaction fails\n onTransactionFailure: (entries, error, classified) => {\n if (!statusTracker) return;\n statusTracker.recordTransactionFailure(\n entries,\n createSyncError(classified, error.message),\n classified.isPermanent,\n extractEntityIds(entries),\n extractTableNames(entries)\n );\n // Update local state\n setFailedTransactions(statusTracker.getFailedTransactions());\n },\n // Record completed transactions\n onTransactionComplete: (entries) => {\n if (!statusTracker) return;\n statusTracker.recordTransactionComplete(entries);\n // Update local state\n setCompletedTransactions(statusTracker.getCompletedTransactions());\n },\n });\n\n setConnector(newConnector);\n\n // Check if already connected\n if (db.connected) {\n logger.debug('[PowerSyncProvider] Already connected, reconnecting...');\n await db.disconnect();\n }\n\n logger.info('[PowerSyncProvider] Connecting to PowerSync...');\n await db.connect(newConnector);\n logger.info('[PowerSyncProvider] Connected successfully');\n\n // Reset reconnect attempts on successful connection\n healthMonitorRef.current?.resetReconnectAttempts();\n } catch (err) {\n const connectError = err instanceof Error ? err : new Error(String(err));\n logger.error('[PowerSyncProvider] Connection failed:', connectError);\n setConnectionError(connectError);\n healthMonitorRef.current?.recordReconnectAttempt();\n }\n };\n\n connectPowerSync();\n }, [db, session, powerSyncUrl, supabaseClient, connectorConfig, mergedSyncConfig.autoConnect, syncModeState, logger]);\n\n // ─── Status Listener ───────────────────────────────────────────────────────\n\n useEffect(() => {\n if (!db) return;\n\n // Set initial status\n const initialStatus = db.currentStatus;\n if (initialStatus) {\n statusTrackerRef.current?.handleStatusChange(initialStatus);\n }\n\n // Register listener\n const unsubscribe = db.registerListener({\n statusChanged: (status) => {\n statusTrackerRef.current?.handleStatusChange(status as Record<string, unknown>);\n\n // Track sync timing for metrics\n const dataFlow = (status as Record<string, unknown>).dataFlowStatus as Record<string, boolean> | undefined;\n const isDownloading = dataFlow?.downloading ?? false;\n const progress = (status as Record<string, unknown>).downloadProgress as Record<string, number> | undefined;\n\n if (isDownloading && !wasSyncingRef.current) {\n metricsCollectorRef.current?.markSyncStart();\n }\n\n if (!isDownloading && wasSyncingRef.current) {\n const duration = metricsCollectorRef.current?.markSyncEnd();\n if (duration !== null && duration !== undefined) {\n metricsCollectorRef.current?.recordSync({\n durationMs: duration,\n success: true,\n operationsDownloaded: progress?.totalOperations ?? 0,\n });\n }\n }\n\n wasSyncingRef.current = isDownloading;\n },\n });\n\n listenerUnsubscribeRef.current = unsubscribe;\n\n return () => {\n unsubscribe();\n listenerUnsubscribeRef.current = null;\n };\n }, [db]);\n\n // ─── Watch ps_crud for Real-time Pending Count ─────────────────────────────\n\n // Track previous pending count to detect when all uploads complete\n const prevPendingCountRef = useRef<number>(0);\n\n /**\n * Parse raw ps_crud row into a CrudEntry object.\n * The ps_crud table has columns: id, tx_id, data\n * The data column is a JSON string with: op, type (table name), and operation data\n */\n const parseCrudRow = useCallback((row: { id: string; tx_id: number | null; data: string }): CrudEntry | null => {\n try {\n const parsed = JSON.parse(row.data);\n // Map PowerSync operation types to our CrudEntry op types\n const opMap: Record<string, 'PUT' | 'PATCH' | 'DELETE'> = {\n 'PUT': 'PUT',\n 'PATCH': 'PATCH',\n 'DELETE': 'DELETE',\n };\n const op = opMap[parsed.op] ?? 'PUT';\n\n // Extract entity ID: prefer parsed.data.id (actual record UUID), fallback to parsed.id, then row.id\n const entityId = (parsed.data?.id as string) ?? parsed.id ?? row.id;\n\n return {\n id: entityId,\n clientId: parseInt(row.id, 10) || 0,\n op,\n table: parsed.type ?? 'unknown',\n opData: parsed.data ?? undefined, // Ensure null becomes undefined\n transactionId: row.tx_id ?? undefined,\n };\n } catch (e) {\n logger.warn('[PowerSyncProvider] Failed to parse CRUD entry:', e, row);\n return null;\n }\n }, [logger]);\n\n useEffect(() => {\n if (!db) return;\n\n const abortController = new AbortController();\n\n // Watch the ps_crud table to get real-time pending upload count\n // This fires on local mutations, not just sync status changes\n db.watch(\n 'SELECT COUNT(*) as count FROM ps_crud',\n [],\n {\n onResult: async (results: { rows?: { _array?: Array<{ count: number }> } }) => {\n // Guard against operations on closed database\n if (dbClosedRef.current) {\n logger.debug('[PowerSyncProvider] Skipping watch callback - database is closed');\n return;\n }\n\n const count = results.rows?._array?.[0]?.count ?? 0;\n const prevCount = prevPendingCountRef.current;\n prevPendingCountRef.current = count;\n\n // Fetch actual CRUD entries when count > 0\n let mutations: CrudEntry[] = [];\n if (count > 0) {\n try {\n // Double-check database is still open before query\n if (dbClosedRef.current) {\n logger.debug('[PowerSyncProvider] Skipping getAll - database is closed');\n return;\n }\n // Query the actual ps_crud rows to get real data\n const rows = await db.getAll<{ id: string; tx_id: number | null; data: string }>(\n 'SELECT id, tx_id, data FROM ps_crud ORDER BY id ASC'\n );\n mutations = rows\n .map(parseCrudRow)\n .filter((entry): entry is CrudEntry => entry !== null);\n } catch (e) {\n // Check if this is a \"database is not open\" error and handle gracefully\n const errorMessage = e instanceof Error ? e.message : String(e);\n if (errorMessage.includes('not open') || errorMessage.includes('closed')) {\n logger.debug('[PowerSyncProvider] Database closed during CRUD fetch, ignoring');\n return;\n }\n logger.warn('[PowerSyncProvider] Failed to fetch CRUD entries:', e);\n // Fall back to empty array - count will still be accurate from the watch\n }\n }\n\n // Final check before updating state\n if (dbClosedRef.current) {\n return;\n }\n\n statusTrackerRef.current?.updatePendingMutations(mutations);\n setPendingMutations(mutations);\n\n // Clear force upload flag when pending count drops to 0\n // This ensures all pending transactions get uploaded during \"Sync Now\"\n // and then the flag is cleared when done\n if (prevCount > 0 && count === 0) {\n logger.debug('[PowerSyncProvider] All pending uploads complete, clearing force flag');\n statusTrackerRef.current?.clearForceNextUpload();\n }\n },\n onError: (error: Error) => {\n // Check if this is a \"database is not open\" error and handle gracefully\n const errorMessage = error.message ?? '';\n if (errorMessage.includes('not open') || errorMessage.includes('closed') || dbClosedRef.current) {\n logger.debug('[PowerSyncProvider] Watch error during database close, ignoring');\n return;\n }\n logger.warn('[PowerSyncProvider] Error watching ps_crud:', error);\n },\n },\n {\n signal: abortController.signal,\n throttleMs: 100, // Debounce to avoid excessive updates during bulk operations\n }\n );\n\n return () => {\n abortController.abort();\n };\n }, [db, logger, parseCrudRow]);\n\n // ─── Attachment Queue Initialization ───────────────────────────────────────\n\n useEffect(() => {\n if (!db || !attachmentConfig || attachmentQueueRef.current) {\n return;\n }\n\n const initAttachmentQueue = async () => {\n try {\n logger.info('[PowerSyncProvider] Initializing attachment queue...');\n\n const queue = new AttachmentQueue({\n powersync: db,\n platform,\n config: attachmentConfig,\n });\n\n await queue.init();\n\n attachmentQueueRef.current = queue;\n setAttachmentQueue(queue);\n logger.info('[PowerSyncProvider] Attachment queue initialized');\n } catch (err) {\n logger.error('[PowerSyncProvider] Attachment queue initialization failed:', err);\n }\n };\n\n initAttachmentQueue();\n\n return () => {\n attachmentQueueRef.current?.dispose();\n attachmentQueueRef.current = null;\n };\n }, [db, attachmentConfig, platform, logger]);\n\n // ─── Cleanup ───────────────────────────────────────────────────────────────\n\n useEffect(() => {\n // Reset closed flag when db changes (new db instance)\n if (db) {\n dbClosedRef.current = false;\n }\n\n return () => {\n // Cleanup on unmount\n logger.info('[PowerSyncProvider] Cleaning up...');\n\n // Mark database as closed FIRST to prevent any pending operations\n dbClosedRef.current = true;\n\n listenerUnsubscribeRef.current?.();\n attachmentQueueRef.current?.dispose();\n healthMonitorRef.current?.stop();\n\n if (db) {\n // Use async IIFE to properly sequence disconnect and close\n (async () => {\n try {\n await db.disconnect();\n await db.close();\n } catch (err) {\n // Only log if it's not a \"database already closed\" error\n const errorMessage = err instanceof Error ? err.message : String(err);\n if (!errorMessage.includes('not open') && !errorMessage.includes('closed')) {\n logger.warn('[PowerSyncProvider] Error during cleanup:', err);\n }\n }\n })();\n }\n };\n }, [db, logger]);\n\n // ─── Clear Failure Functions ────────────────────────────────────────────────\n\n const clearFailure = useCallback((failureId: string) => {\n const tracker = statusTrackerRef.current;\n if (!tracker) {\n logger.warn('[PowerSyncProvider] Cannot clear failure - tracker not initialized');\n return;\n }\n tracker.clearFailure(failureId);\n setFailedTransactions(tracker.getFailedTransactions());\n }, [logger]);\n\n const clearAllFailures = useCallback(() => {\n const tracker = statusTrackerRef.current;\n if (!tracker) {\n logger.warn('[PowerSyncProvider] Cannot clear failures - tracker not initialized');\n return;\n }\n tracker.clearAllFailures();\n setFailedTransactions(tracker.getFailedTransactions());\n }, [logger]);\n\n const clearCompletedHistory = useCallback(() => {\n const tracker = statusTrackerRef.current;\n if (!tracker) {\n logger.warn('[PowerSyncProvider] Cannot clear completed history - tracker not initialized');\n return;\n }\n tracker.clearCompletedHistory();\n setCompletedTransactions(tracker.getCompletedTransactions());\n }, [logger]);\n\n const setSyncMode = useCallback(async (mode: SyncMode) => {\n const tracker = statusTrackerRef.current;\n if (!tracker) {\n logger.warn('[PowerSyncProvider] Cannot set sync mode - tracker not initialized');\n return;\n }\n await tracker.setSyncMode(mode);\n setSyncModeState({ loaded: true, mode });\n }, [logger]);\n\n const setForceNextUpload = useCallback((force: boolean) => {\n const tracker = statusTrackerRef.current;\n if (!tracker) {\n logger.warn('[PowerSyncProvider] Cannot set force upload - tracker not initialized');\n return;\n }\n tracker.setForceNextUpload(force);\n }, [logger]);\n\n // ─── Discard Mutation Functions ────────────────────────────────────────────\n\n const discardPendingMutation = useCallback(async (clientId: number) => {\n if (!db || !connector) {\n logger.warn('[PowerSync] Cannot discard - not initialized');\n return;\n }\n if (syncStatus.uploading) {\n throw new Error('Cannot discard while upload is in progress');\n }\n\n logger.info('[PowerSync] Discarding pending mutation:', clientId);\n\n // Disconnect to ensure no active transaction\n await db.disconnect();\n\n try {\n await db.execute('DELETE FROM ps_crud WHERE id = ?', [clientId]);\n logger.info('[PowerSync] Mutation discarded successfully');\n } finally {\n // Always reconnect\n await db.connect(connector);\n }\n }, [db, connector, syncStatus.uploading, logger]);\n\n const discardAllPendingMutations = useCallback(async () => {\n if (!db || !connector) {\n logger.warn('[PowerSync] Cannot discard all - not initialized');\n return;\n }\n if (syncStatus.uploading) {\n throw new Error('Cannot discard while upload is in progress');\n }\n\n logger.info('[PowerSync] Discarding all pending mutations');\n\n await db.disconnect();\n\n try {\n await db.execute('DELETE FROM ps_crud');\n logger.info('[PowerSync] All mutations discarded successfully');\n } finally {\n await db.connect(connector);\n }\n }, [db, connector, syncStatus.uploading, logger]);\n\n // ─── Context Values ────────────────────────────────────────────────────────\n\n const powerSyncContextValue = useMemo<PowerSyncContextValue<TSchema>>(\n () => ({\n db,\n connector,\n attachmentQueue,\n isReady,\n isInitializing,\n error,\n schema,\n platform,\n }),\n [db, connector, attachmentQueue, isReady, isInitializing, error, schema, platform]\n );\n\n const syncStatusContextValue = useMemo<SyncStatusContextValue>(\n () => ({\n status: syncStatus,\n pendingMutations,\n pendingCount: pendingMutations.length,\n // Expose uploading/downloading directly from syncStatus for reliable activity detection\n isUploading: syncStatus.uploading,\n isDownloading: syncStatus.downloading,\n isPaused: syncModeState.mode === 'offline',\n syncMode: syncModeState.mode,\n lastSyncedAt,\n // Connection error for consumers to display\n connectionError,\n // Failed transaction fields\n failedTransactions,\n hasUploadErrors: failedTransactions.length > 0,\n permanentErrorCount: failedTransactions.filter(f => f.isPermanent).length,\n // Clear failure functions\n clearFailure,\n clearAllFailures,\n // Completed transaction fields\n completedTransactions,\n clearCompletedHistory,\n // Sync mode control functions\n setSyncMode,\n setForceNextUpload,\n // Discard mutation functions\n discardPendingMutation,\n discardAllPendingMutations,\n }),\n [syncStatus, pendingMutations, syncModeState.mode, lastSyncedAt, connectionError, failedTransactions, clearFailure, clearAllFailures, completedTransactions, clearCompletedHistory, setSyncMode, setForceNextUpload, discardPendingMutation, discardAllPendingMutations]\n );\n\n const connectionHealthContextValue = useMemo<ConnectionHealthContextValue>(\n () => ({ health: connectionHealth }),\n [connectionHealth]\n );\n\n const syncMetricsContextValue = useMemo<SyncMetricsContextValue>(\n () => ({ metrics: syncMetrics }),\n [syncMetrics]\n );\n\n // ─── Render ────────────────────────────────────────────────────────────────\n\n return (\n <PowerSyncContext.Provider value={powerSyncContextValue as PowerSyncContextValue}>\n <SyncStatusContext.Provider value={syncStatusContextValue}>\n <ConnectionHealthContext.Provider value={connectionHealthContextValue}>\n <SyncMetricsContext.Provider value={syncMetricsContextValue}>\n <AttachmentQueueContext.Provider value={attachmentQueue}>\n {children}\n </AttachmentQueueContext.Provider>\n </SyncMetricsContext.Provider>\n </ConnectionHealthContext.Provider>\n </SyncStatusContext.Provider>\n </PowerSyncContext.Provider>\n );\n}\n","/**\n * React Hooks for @pol-studios/powersync\n *\n * This module provides React hooks for accessing PowerSync functionality\n * within the provider's context.\n */\n\nimport { useContext, useCallback, useMemo, useRef, useState, useEffect } from 'react';\nimport type {\n AbstractPowerSyncDatabase,\n SyncStatus,\n ConnectionHealth,\n SyncMetrics,\n CrudEntry,\n EntitySyncState,\n FailedTransaction,\n CompletedTransaction,\n SyncError,\n} from '../core/types';\nimport type { PlatformAdapter } from '../platform/types';\nimport type { PowerSyncContextValue, SyncStatusContextValue, ConnectionHealthContextValue, SyncMetricsContextValue } from './types';\nimport type { SyncScope, SyncControlActions } from '../sync/types';\nimport type { AttachmentQueue } from '../attachments/attachment-queue';\nimport type { SupabaseConnector } from '../connector/supabase-connector';\nimport {\n PowerSyncContext,\n SyncStatusContext,\n ConnectionHealthContext,\n SyncMetricsContext,\n AttachmentQueueContext,\n} from './context';\nimport type { SyncMode } from '../core/types';\n// Note: STORAGE_KEY_PAUSED and STORAGE_KEY_SYNC_MODE are handled by the status tracker\n\n// ─── Main Hook ───────────────────────────────────────────────────────────────\n\n/**\n * Hook to access the PowerSync database and related services.\n *\n * @returns PowerSync context value with database, connector, and state\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const { db, isReady, error } = usePowerSync();\n *\n * if (!isReady) return <LoadingSpinner />;\n * if (error) return <Error message={error.message} />;\n *\n * // Use db for queries\n * const users = await db.getAll('SELECT * FROM users');\n * }\n * ```\n */\nexport function usePowerSync<TSchema = unknown>(): PowerSyncContextValue<TSchema> {\n const context = useContext(PowerSyncContext);\n\n if (!context) {\n throw new Error('usePowerSync must be used within a PowerSyncProvider');\n }\n\n return context as PowerSyncContextValue<TSchema>;\n}\n\n// ─── Sync Status Hook ────────────────────────────────────────────────────────\n\n/**\n * Hook to access the current sync status.\n *\n * @returns Sync status with connection state, pending uploads, and progress\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncIndicator() {\n * const { status, pendingCount, isPaused } = useSyncStatus();\n *\n * if (status.downloading) {\n * const { current, target, percentage } = status.downloadProgress ?? {};\n * return <Progress value={percentage} label={`${current}/${target}`} />;\n * }\n *\n * if (pendingCount > 0) {\n * return <Badge>{pendingCount} pending uploads</Badge>;\n * }\n *\n * return <Text>Synced</Text>;\n * }\n * ```\n */\nexport function useSyncStatus(): SyncStatusContextValue {\n const context = useContext(SyncStatusContext);\n\n if (!context) {\n throw new Error('useSyncStatus must be used within a PowerSyncProvider');\n }\n\n return context;\n}\n\n// ─── Sync Control Hook ───────────────────────────────────────────────────────\n\n/**\n * Hook to control sync operations.\n *\n * @returns Actions for triggering, pausing, and resuming sync\n *\n * @example\n * ```typescript\n * function SyncControls() {\n * const { triggerSync, syncNow, pause, resume, disconnect, setSyncMode } = useSyncControl();\n * const { isPaused, syncMode } = useSyncStatus();\n *\n * return (\n * <View>\n * <Button onPress={syncNow}>Sync Now</Button>\n * {isPaused ? (\n * <Button onPress={resume}>Resume</Button>\n * ) : (\n * <Button onPress={pause}>Pause</Button>\n * )}\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncControl(): SyncControlActions {\n const { db, connector, platform } = usePowerSync();\n const { setSyncMode: setContextSyncMode, setForceNextUpload } = useSyncStatus();\n const scopeRef = useRef<SyncScope | null>(null);\n\n const setSyncMode = useCallback(async (mode: SyncMode) => {\n // Update sync mode via context (which updates the status tracker)\n await setContextSyncMode(mode);\n\n if (mode === 'offline') {\n // Disconnect when going offline\n if (db?.connected) {\n platform.logger.info('[useSyncControl] Mode changed to offline - disconnecting');\n await db.disconnect();\n }\n } else if (db && connector && !db.connected) {\n // Reconnect when leaving offline mode\n platform.logger.info('[useSyncControl] Mode changed to', mode, '- reconnecting');\n await db.connect(connector);\n }\n }, [db, connector, platform, setContextSyncMode]);\n\n const syncNow = useCallback(async () => {\n if (!db || !connector) {\n platform.logger.warn('[useSyncControl] Cannot sync - database not initialized');\n return;\n }\n\n // Set force flag to ensure uploads happen regardless of mode\n setForceNextUpload(true);\n\n platform.logger.info('[useSyncControl] Sync Now triggered - forcing full sync');\n\n // Reconnect to trigger sync cycle\n if (db.connected) {\n await db.disconnect();\n }\n\n await db.connect(connector);\n platform.logger.info('[useSyncControl] Connected, sync should start automatically');\n }, [db, connector, platform, setForceNextUpload]);\n\n const triggerSync = useCallback(async () => {\n if (!db || !connector) {\n platform.logger.warn('[useSyncControl] Cannot trigger sync - not initialized');\n return;\n }\n\n // Set sync mode to push-pull via context (keeps tracker state in sync)\n await setContextSyncMode('push-pull');\n\n // Disconnect and reconnect to force a fresh sync\n if (db.connected) {\n platform.logger.info('[useSyncControl] Disconnecting to force fresh sync...');\n await db.disconnect();\n }\n\n platform.logger.info('[useSyncControl] Connecting...');\n await db.connect(connector);\n platform.logger.info('[useSyncControl] Connected, sync should start automatically');\n }, [db, connector, platform, setContextSyncMode]);\n\n const pause = useCallback(async () => {\n await setSyncMode('offline');\n platform.logger.info('[useSyncControl] Sync paused');\n }, [setSyncMode, platform]);\n\n const resume = useCallback(async () => {\n await setSyncMode('push-pull');\n platform.logger.info('[useSyncControl] Sync resumed');\n }, [setSyncMode, platform]);\n\n const disconnect = useCallback(async () => {\n if (!db) {\n platform.logger.warn('[useSyncControl] Cannot disconnect - not initialized');\n return;\n }\n\n platform.logger.info('[useSyncControl] Disconnecting...');\n await db.disconnect();\n platform.logger.info('[useSyncControl] Disconnected');\n }, [db, platform]);\n\n const setScope = useCallback((scope: SyncScope | null) => {\n scopeRef.current = scope;\n\n if (connector && scope) {\n // Update connector with new project IDs if it supports scoped sync\n connector.setActiveProjectIds(scope.ids);\n platform.logger.info('[useSyncControl] Scope set:', scope);\n }\n }, [connector, platform]);\n\n return useMemo(\n () => ({\n triggerSync,\n syncNow,\n pause,\n resume,\n disconnect,\n setScope,\n setSyncMode,\n }),\n [triggerSync, syncNow, pause, resume, disconnect, setScope, setSyncMode]\n );\n}\n\n// ─── Sync Mode Hook ──────────────────────────────────────────────────────────\n\n/**\n * Hook to get and set the current sync mode.\n *\n * @returns Object with current mode, setter, and capability flags\n *\n * @example\n * ```typescript\n * function SyncModeSelector() {\n * const { mode, setMode, canUpload, canDownload } = useSyncMode();\n *\n * return (\n * <View>\n * <Text>Current mode: {mode}</Text>\n * <Text>Can upload: {canUpload ? 'Yes' : 'No'}</Text>\n * <Button onPress={() => setMode('pull-only')}>Download Only</Button>\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncMode(): {\n mode: SyncMode;\n setMode: (mode: SyncMode) => Promise<void>;\n canUpload: boolean;\n canDownload: boolean;\n} {\n const { syncMode } = useSyncStatus();\n const { setSyncMode } = useSyncControl();\n\n return useMemo(() => ({\n mode: syncMode,\n setMode: setSyncMode,\n canUpload: syncMode === 'push-pull',\n canDownload: syncMode !== 'offline',\n }), [syncMode, setSyncMode]);\n}\n\n// ─── Connection Health Hook ──────────────────────────────────────────────────\n\n/**\n * Hook to access connection health status.\n *\n * @returns Current connection health with latency and failure tracking\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function ConnectionIndicator() {\n * const health = useConnectionHealth();\n *\n * const statusColor = {\n * healthy: 'green',\n * degraded: 'yellow',\n * disconnected: 'red',\n * }[health.status];\n *\n * return (\n * <View>\n * <StatusDot color={statusColor} />\n * {health.latency && <Text>{health.latency}ms</Text>}\n * </View>\n * );\n * }\n * ```\n */\nexport function useConnectionHealth(): ConnectionHealth {\n const context = useContext(ConnectionHealthContext);\n\n if (!context) {\n throw new Error('useConnectionHealth must be used within a PowerSyncProvider');\n }\n\n return context.health;\n}\n\n// ─── Sync Metrics Hook ───────────────────────────────────────────────────────\n\n/**\n * Hook to access sync metrics.\n *\n * @returns Sync metrics including success rates, timing, and data transfer\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncStats() {\n * const metrics = useSyncMetrics();\n *\n * const successRate = metrics.totalSyncs > 0\n * ? (metrics.successfulSyncs / metrics.totalSyncs * 100).toFixed(1)\n * : 100;\n *\n * return (\n * <View>\n * <Text>Total syncs: {metrics.totalSyncs}</Text>\n * <Text>Success rate: {successRate}%</Text>\n * <Text>Avg duration: {metrics.averageSyncDuration ?? 'N/A'}ms</Text>\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncMetrics(): SyncMetrics {\n const context = useContext(SyncMetricsContext);\n\n if (!context) {\n throw new Error('useSyncMetrics must be used within a PowerSyncProvider');\n }\n\n return context.metrics;\n}\n\n// ─── Attachment Queue Hook ───────────────────────────────────────────────────\n\n/**\n * Hook to access the attachment queue (if configured).\n *\n * @returns AttachmentQueue instance or null if not configured\n *\n * @example\n * ```typescript\n * function PhotoStats() {\n * const attachmentQueue = useAttachmentQueue();\n * const [stats, setStats] = useState(null);\n *\n * useEffect(() => {\n * if (!attachmentQueue) return;\n *\n * return attachmentQueue.onProgress((newStats) => {\n * setStats(newStats);\n * });\n * }, [attachmentQueue]);\n *\n * if (!stats) return null;\n *\n * return (\n * <View>\n * <Text>Photos: {stats.syncedCount}/{stats.totalExpected}</Text>\n * <Text>Cache used: {formatBytes(stats.syncedSize)}</Text>\n * </View>\n * );\n * }\n * ```\n */\nexport function useAttachmentQueue(): AttachmentQueue | null {\n return useContext(AttachmentQueueContext);\n}\n\n// ─── Database Query Hook ─────────────────────────────────────────────────────\n\n/**\n * Hook to get the PowerSync database instance.\n * Throws if not ready.\n *\n * @returns The PowerSync database instance\n * @throws Error if not initialized or used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function UserList() {\n * const db = useDatabase();\n * const [users, setUsers] = useState([]);\n *\n * useEffect(() => {\n * db.getAll('SELECT * FROM users').then(setUsers);\n * }, [db]);\n *\n * return <FlatList data={users} />;\n * }\n * ```\n */\nexport function useDatabase(): AbstractPowerSyncDatabase {\n const { db, isReady, error } = usePowerSync();\n\n if (error) {\n throw error;\n }\n\n if (!isReady || !db) {\n throw new Error('PowerSync database is not ready');\n }\n\n return db;\n}\n\n// ─── Platform Adapter Hook ───────────────────────────────────────────────────\n\n/**\n * Hook to access the platform adapter.\n *\n * @returns The platform adapter instance\n *\n * @example\n * ```typescript\n * function FileViewer({ filePath }) {\n * const { platform } = usePowerSync();\n *\n * const handleOpen = async () => {\n * const content = await platform.fileSystem.readFile(filePath);\n * // Process content...\n * };\n *\n * return <Button onPress={handleOpen}>Open File</Button>;\n * }\n * ```\n */\nexport function usePlatform(): PlatformAdapter {\n const { platform } = usePowerSync();\n return platform;\n}\n\n// ─── Online Status Hook ──────────────────────────────────────────────────────\n\n/**\n * Hook to track online/offline status using the platform's network adapter.\n *\n * @returns Whether the device is currently connected to the internet\n *\n * @example\n * ```typescript\n * function OfflineBanner() {\n * const isOnline = useOnlineStatus();\n *\n * if (isOnline) return null;\n *\n * return <Banner type=\"warning\">You are offline</Banner>;\n * }\n * ```\n */\nexport function useOnlineStatus(): boolean {\n const { platform } = usePowerSync();\n const [isOnline, setIsOnline] = useState(true);\n\n useEffect(() => {\n // Get initial status\n platform.network.isConnected().then(setIsOnline);\n\n // Subscribe to changes\n const unsubscribe = platform.network.addConnectionListener(setIsOnline);\n\n return unsubscribe;\n }, [platform]);\n\n return isOnline;\n}\n\n// ─── Pending Mutations Hook ──────────────────────────────────────────────────\n\n/**\n * Hook to get pending mutations that need to be uploaded.\n *\n * @returns Array of pending CRUD entries and count\n *\n * @example\n * ```typescript\n * function PendingChanges() {\n * const { mutations, count } = usePendingMutations();\n *\n * if (count === 0) return null;\n *\n * return (\n * <View>\n * <Text>{count} changes pending upload</Text>\n * <FlatList\n * data={mutations}\n * renderItem={({ item }) => (\n * <Text>{item.op} on {item.table}</Text>\n * )}\n * />\n * </View>\n * );\n * }\n * ```\n */\nexport function usePendingMutations(): { mutations: CrudEntry[]; count: number } {\n const { pendingMutations, pendingCount } = useSyncStatus();\n\n return useMemo(\n () => ({\n mutations: pendingMutations,\n count: pendingCount,\n }),\n [pendingMutations, pendingCount]\n );\n}\n\n// ─── Is Syncing Hook ─────────────────────────────────────────────────────────\n\n/**\n * Hook to check if sync is currently active.\n *\n * @returns Whether sync is currently in progress (uploading or downloading)\n *\n * @example\n * ```typescript\n * function SyncButton() {\n * const isSyncing = useIsSyncing();\n * const { triggerSync } = useSyncControl();\n *\n * return (\n * <Button\n * onPress={triggerSync}\n * disabled={isSyncing}\n * >\n * {isSyncing ? 'Syncing...' : 'Sync Now'}\n * </Button>\n * );\n * }\n * ```\n */\nexport function useIsSyncing(): boolean {\n const { status } = useSyncStatus();\n return status.uploading || status.downloading;\n}\n\n// ─── Download Progress Hook ──────────────────────────────────────────────────\n\n/**\n * Hook to get download progress during sync.\n *\n * @returns Download progress or null if not downloading\n *\n * @example\n * ```typescript\n * function DownloadProgress() {\n * const progress = useDownloadProgress();\n *\n * if (!progress) return null;\n *\n * return (\n * <ProgressBar\n * value={progress.percentage}\n * label={`${progress.current}/${progress.target} operations`}\n * />\n * );\n * }\n * ```\n */\nexport function useDownloadProgress() {\n const { status } = useSyncStatus();\n return status.downloadProgress;\n}\n\n// ─── Entity Sync Status Hook ─────────────────────────────────────────────────\n\n/**\n * Return type for useEntitySyncStatus hook.\n */\nexport interface EntitySyncStatusResult {\n /** Current sync state for this entity */\n state: EntitySyncState;\n /** Error details if state is 'error' */\n error: SyncError | null;\n /** Number of pending operations for this entity */\n pendingOperations: number;\n /** The failed transaction if any */\n failedTransaction: FailedTransaction | null;\n /** Dismiss the failure (remove from tracking) */\n dismiss: () => void;\n}\n\n// Track recently synced entities with timestamps\nconst recentlySyncedEntities = new Map<string, number>();\nconst SYNCED_DISPLAY_DURATION_MS = 3000; // Show 'synced' state for 3 seconds\n\n/**\n * Hook to get sync status for a specific entity.\n *\n * Combines local mutation state (from pending mutations) with\n * failure state to provide a unified status for UI.\n *\n * @param entityId - The entity ID to check status for\n * @returns Unified sync state and actions\n *\n * @example\n * ```typescript\n * function EquipmentHeader({ unitId }) {\n * const { state, error, dismiss } = useEntitySyncStatus(unitId);\n *\n * const borderColor = {\n * idle: 'transparent',\n * saving: 'orange',\n * syncing: 'amber',\n * synced: 'green',\n * error: 'red',\n * }[state];\n * }\n * ```\n */\nexport function useEntitySyncStatus(entityId: string | undefined): EntitySyncStatusResult {\n // Use top-level failedTransactions from context (updated immediately on failure)\n const { status, pendingMutations, clearFailure, failedTransactions } = useSyncStatus();\n const [, forceUpdate] = useState(0);\n\n // Find if entity is in pending mutations\n // Check both entry.id (PowerSync's internal CRUD entry ID) and entry.opData?.id (the actual record ID)\n const entityPendingMutations = useMemo(() => {\n if (!entityId) return [];\n return pendingMutations.filter(entry =>\n entry.id === entityId || String(entry.opData?.id) === entityId\n );\n }, [entityId, pendingMutations]);\n\n // Find if entity has a failed transaction\n // Use top-level failedTransactions (not status.failedTransactions) for immediate updates\n const failedTransaction = useMemo(() => {\n if (!entityId) return null;\n return failedTransactions.find(\n ft => ft.affectedEntityIds.includes(entityId)\n ) ?? null;\n }, [entityId, failedTransactions]);\n\n // Track transition from syncing to synced\n const wasSyncingRef = useRef(false);\n const isCurrentlySyncing = entityPendingMutations.length > 0;\n\n // When entity transitions from syncing to not syncing (and no error),\n // mark it as recently synced\n useEffect(() => {\n if (!entityId) return;\n\n if (wasSyncingRef.current && !isCurrentlySyncing && !failedTransaction) {\n recentlySyncedEntities.set(entityId, Date.now());\n\n // Schedule cleanup after the display duration\n const timer = setTimeout(() => {\n const syncedAt = recentlySyncedEntities.get(entityId);\n if (syncedAt && Date.now() - syncedAt >= SYNCED_DISPLAY_DURATION_MS) {\n recentlySyncedEntities.delete(entityId);\n forceUpdate(n => n + 1);\n }\n }, SYNCED_DISPLAY_DURATION_MS);\n\n return () => clearTimeout(timer);\n }\n\n wasSyncingRef.current = isCurrentlySyncing;\n }, [entityId, isCurrentlySyncing, failedTransaction]);\n\n // Determine current state\n const state = useMemo((): EntitySyncState => {\n if (!entityId) return 'idle';\n\n // Check for ANY failure (highest priority)\n // Both permanent and transient failures should show error state\n // The UI can use failedTransaction.isPermanent to distinguish if needed\n if (failedTransaction) {\n return 'error';\n }\n\n // Check if currently syncing\n if (entityPendingMutations.length > 0) {\n return 'syncing';\n }\n\n // Check if recently synced\n const syncedAt = recentlySyncedEntities.get(entityId);\n if (syncedAt && Date.now() - syncedAt < SYNCED_DISPLAY_DURATION_MS) {\n return 'synced';\n }\n\n return 'idle';\n }, [entityId, failedTransaction, entityPendingMutations.length]);\n\n // Get error from failed transaction\n const error = failedTransaction?.error ?? null;\n\n // Dismiss function - clears the failure from tracking\n const dismiss = useCallback(() => {\n if (failedTransaction) {\n clearFailure(failedTransaction.id);\n }\n }, [failedTransaction, clearFailure]);\n\n return {\n state,\n error,\n pendingOperations: entityPendingMutations.length,\n failedTransaction,\n dismiss,\n };\n}\n\n// ─── Upload Status Hook ──────────────────────────────────────────────────────\n\n/**\n * Return type for useUploadStatus hook.\n */\nexport interface UploadStatusResult {\n /** Number of operations waiting to upload */\n pendingCount: number;\n /** Number of failed transactions */\n failedCount: number;\n /** Number of permanent failures needing user action */\n permanentFailureCount: number;\n /** Whether there are any errors */\n hasErrors: boolean;\n /** Whether there are permanent errors needing attention */\n hasPermanentErrors: boolean;\n /** All failed transactions */\n failedTransactions: FailedTransaction[];\n /** Trigger sync retry (reconnect) */\n retryAll: () => Promise<void>;\n /** Dismiss all failures */\n dismissAll: () => void;\n}\n\n/**\n * Hook to get overall upload status across all entities.\n *\n * @returns Upload status with counts and actions\n *\n * @example\n * ```typescript\n * function SyncStatusBar() {\n * const { pendingCount, failedCount, hasPermanentErrors, retryAll } = useUploadStatus();\n *\n * if (hasPermanentErrors) {\n * return <Banner onRetry={retryAll}>\n * {failedCount} changes failed to sync\n * </Banner>;\n * }\n * }\n * ```\n */\nexport function useUploadStatus(): UploadStatusResult {\n // Use top-level failedTransactions from context (updated immediately on failure)\n const { status, pendingCount, clearAllFailures, failedTransactions, hasUploadErrors, permanentErrorCount } = useSyncStatus();\n const { triggerSync } = useSyncControl();\n\n // Retry all by triggering a fresh sync\n const retryAll = useCallback(async () => {\n await triggerSync();\n }, [triggerSync]);\n\n // Dismiss all failures\n const dismissAll = useCallback(() => {\n clearAllFailures();\n }, [clearAllFailures]);\n\n return useMemo(\n () => ({\n pendingCount,\n failedCount: failedTransactions.length,\n permanentFailureCount: permanentErrorCount,\n hasErrors: hasUploadErrors,\n hasPermanentErrors: permanentErrorCount > 0,\n failedTransactions,\n retryAll,\n dismissAll,\n }),\n [\n pendingCount,\n failedTransactions,\n permanentErrorCount,\n hasUploadErrors,\n retryAll,\n dismissAll,\n ]\n );\n}\n\n// ─── Sync Activity Hook ──────────────────────────────────────────────────────\n\n/**\n * Return type for useSyncActivity hook.\n */\nexport interface SyncActivityResult {\n /** Pending CRUD entries waiting to be synced */\n pending: CrudEntry[];\n /** Failed transactions that need attention */\n failed: FailedTransaction[];\n /** Recently completed transactions */\n completed: CompletedTransaction[];\n /** Counts summary */\n counts: {\n pending: number;\n failed: number;\n completed: number;\n };\n /** Whether there is any sync activity to show (pending or failed) */\n hasActivity: boolean;\n /** Retry all failed transactions */\n retryAll: () => Promise<void>;\n /** Dismiss a specific failure */\n dismissFailure: (failureId: string) => void;\n /** Clear all completed transactions from the list */\n clearCompleted: () => void;\n}\n\n/**\n * Hook to get comprehensive sync activity including pending, failed, and completed transactions.\n *\n * Uses the provider's completed transaction tracking via the status tracker.\n *\n * @returns Sync activity with all transaction states and actions\n *\n * @example\n * ```typescript\n * function SyncActivityBanner() {\n * const { pending, failed, completed, counts, hasActivity, retryAll, clearCompleted } = useSyncActivity();\n *\n * if (!hasActivity && completed.length === 0) return null;\n *\n * return (\n * <View>\n * <Text>{counts.pending} syncing, {counts.failed} failed</Text>\n * {failed.length > 0 && <Button onPress={retryAll}>Retry All</Button>}\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncActivity(): SyncActivityResult {\n const {\n pendingMutations,\n clearFailure,\n failedTransactions,\n completedTransactions,\n clearCompletedHistory,\n isUploading,\n isDownloading,\n } = useSyncStatus();\n const { triggerSync } = useSyncControl();\n\n // Retry all by triggering a fresh sync\n const retryAll = useCallback(async () => {\n await triggerSync();\n }, [triggerSync]);\n\n // Dismiss a specific failure\n const dismissFailure = useCallback((failureId: string) => {\n clearFailure(failureId);\n }, [clearFailure]);\n\n // Clear all completed transactions\n const clearCompleted = useCallback(() => {\n clearCompletedHistory();\n }, [clearCompletedHistory]);\n\n const counts = useMemo(() => ({\n pending: pendingMutations.length,\n failed: failedTransactions.length,\n completed: completedTransactions.length,\n }), [pendingMutations.length, failedTransactions.length, completedTransactions.length]);\n\n // Use PowerSync's uploading/downloading status flags for reliable activity detection\n // This fixes the \"stuck on Syncing...\" bug where getNextCrudTransaction() returns null\n // during active upload (when transaction is locked), causing stale pendingMutations state\n const hasActivity = isUploading || isDownloading || failedTransactions.length > 0;\n\n return useMemo(\n () => ({\n pending: pendingMutations,\n failed: failedTransactions,\n completed: completedTransactions,\n counts,\n hasActivity,\n retryAll,\n dismissFailure,\n clearCompleted,\n }),\n [pendingMutations, failedTransactions, completedTransactions, counts, hasActivity, retryAll, dismissFailure, clearCompleted]\n );\n}\n"],"mappings":"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","names":["error","useCallback","useMemo","useRef","useState","useEffect","useRef","useCallback","useMemo","useState","useEffect"]}
@@ -0,0 +1,298 @@
1
+ import {
2
+ SupabaseConnector
3
+ } from "./chunk-FLHDT4TS.js";
4
+
5
+ // src/conflicts/detect.ts
6
+ var DEFAULT_IGNORED_FIELDS = ["updatedAt", "createdAt", "_version", "id"];
7
+ async function detectConflicts(table, recordId, localVersion, serverVersion, pendingChanges, supabase, config) {
8
+ const ignoredFields = /* @__PURE__ */ new Set([
9
+ ...DEFAULT_IGNORED_FIELDS,
10
+ ...config?.ignoredFields ?? []
11
+ ]);
12
+ const filteredPendingChanges = {};
13
+ for (const [field, value] of Object.entries(pendingChanges)) {
14
+ if (!ignoredFields.has(field)) {
15
+ filteredPendingChanges[field] = value;
16
+ }
17
+ }
18
+ if (localVersion === serverVersion) {
19
+ return {
20
+ hasConflict: false,
21
+ conflicts: [],
22
+ nonConflictingChanges: Object.keys(filteredPendingChanges),
23
+ table,
24
+ recordId
25
+ };
26
+ }
27
+ const { data: auditLogs, error } = await supabase.schema("core").from("AuditLog").select("oldRecord, newRecord, changeBy, changeAt").eq("tableName", table).eq("recordId_text", recordId).order("changeAt", { ascending: false }).limit(20);
28
+ if (error) {
29
+ console.warn("[detectConflicts] Failed to query AuditLog:", error);
30
+ return {
31
+ hasConflict: false,
32
+ conflicts: [],
33
+ nonConflictingChanges: Object.keys(filteredPendingChanges),
34
+ table,
35
+ recordId
36
+ };
37
+ }
38
+ const serverChanges = /* @__PURE__ */ new Map();
39
+ for (const log of auditLogs ?? []) {
40
+ const oldRec = log.oldRecord;
41
+ const newRec = log.newRecord;
42
+ if (!oldRec || !newRec) continue;
43
+ for (const [field, newValue] of Object.entries(newRec)) {
44
+ if (ignoredFields.has(field)) continue;
45
+ if (oldRec[field] !== newValue && !serverChanges.has(field)) {
46
+ serverChanges.set(field, {
47
+ newValue,
48
+ changedBy: log.changeBy,
49
+ changedAt: new Date(log.changeAt)
50
+ });
51
+ }
52
+ }
53
+ }
54
+ const conflicts = [];
55
+ const nonConflictingChanges = [];
56
+ for (const [field, localValue] of Object.entries(filteredPendingChanges)) {
57
+ if (serverChanges.has(field)) {
58
+ const serverChange = serverChanges.get(field);
59
+ conflicts.push({
60
+ field,
61
+ localValue,
62
+ serverValue: serverChange.newValue,
63
+ changedBy: serverChange.changedBy,
64
+ changedAt: serverChange.changedAt
65
+ });
66
+ } else {
67
+ nonConflictingChanges.push(field);
68
+ }
69
+ }
70
+ return {
71
+ hasConflict: conflicts.length > 0,
72
+ conflicts,
73
+ nonConflictingChanges,
74
+ table,
75
+ recordId
76
+ };
77
+ }
78
+ async function hasVersionColumn(table, db) {
79
+ try {
80
+ const result = await db.getAll(
81
+ `PRAGMA table_info("${table}")`
82
+ );
83
+ return result.some((col) => col.name === "_version");
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ async function fetchServerVersion(table, recordId, schema, supabase) {
89
+ const query = schema === "public" ? supabase.from(table) : supabase.schema(schema).from(table);
90
+ const { data, error } = await query.select("_version").eq("id", recordId).single();
91
+ if (error || !data) {
92
+ return null;
93
+ }
94
+ return data._version ?? null;
95
+ }
96
+ async function getLocalVersion(table, recordId, db) {
97
+ const result = await db.get(
98
+ `SELECT _version FROM "${table}" WHERE id = ?`,
99
+ [recordId]
100
+ );
101
+ return result?._version ?? null;
102
+ }
103
+
104
+ // src/connector/conflict-aware-connector.ts
105
+ var ConflictAwareConnector = class extends SupabaseConnector {
106
+ conflictHandler;
107
+ conflictConfig;
108
+ supabaseClient;
109
+ schemaRouterFn;
110
+ // Cache for version column existence checks
111
+ versionColumnCache = /* @__PURE__ */ new Map();
112
+ constructor(options) {
113
+ super(options);
114
+ this.conflictHandler = options.conflictHandler;
115
+ this.conflictConfig = options.conflictDetection;
116
+ this.supabaseClient = options.supabaseClient;
117
+ this.schemaRouterFn = options.schemaRouter ?? (() => "public");
118
+ }
119
+ /**
120
+ * Override uploadData to check for conflicts before uploading.
121
+ *
122
+ * For each CRUD entry in the transaction:
123
+ * 1. Check if table has _version column (cached)
124
+ * 2. If yes, compare local vs server version
125
+ * 3. On version mismatch, query AuditLog for field conflicts
126
+ * 4. If conflicts found, call handler to determine resolution
127
+ * 5. Apply resolution or skip entry based on handler response
128
+ */
129
+ async uploadData(database) {
130
+ if (this.conflictConfig?.enabled === false) {
131
+ return super.uploadData(database);
132
+ }
133
+ const transaction = await database.getNextCrudTransaction();
134
+ if (!transaction) {
135
+ return;
136
+ }
137
+ const { crud } = transaction;
138
+ const skipTables = new Set(this.conflictConfig?.skipTables ?? []);
139
+ const entriesToProcess = [];
140
+ const skippedEntries = [];
141
+ for (const entry of crud) {
142
+ if (entry.op === "DELETE") {
143
+ entriesToProcess.push(entry);
144
+ continue;
145
+ }
146
+ if (skipTables.has(entry.table)) {
147
+ entriesToProcess.push(entry);
148
+ continue;
149
+ }
150
+ const hasVersion = await this.checkVersionColumn(entry.table, database);
151
+ if (!hasVersion) {
152
+ entriesToProcess.push(entry);
153
+ continue;
154
+ }
155
+ const localVersion = await getLocalVersion(entry.table, entry.id, database);
156
+ const schema = this.schemaRouterFn(entry.table);
157
+ const serverVersion = await fetchServerVersion(
158
+ entry.table,
159
+ entry.id,
160
+ schema,
161
+ this.supabaseClient
162
+ );
163
+ if (localVersion === null || serverVersion === null) {
164
+ entriesToProcess.push(entry);
165
+ continue;
166
+ }
167
+ const conflictResult = await detectConflicts(
168
+ entry.table,
169
+ entry.id,
170
+ localVersion,
171
+ serverVersion,
172
+ entry.opData ?? {},
173
+ this.supabaseClient,
174
+ this.conflictConfig
175
+ );
176
+ if (!conflictResult.hasConflict) {
177
+ entriesToProcess.push(entry);
178
+ continue;
179
+ }
180
+ if (this.conflictHandler) {
181
+ const resolution = await this.conflictHandler.onConflict(conflictResult);
182
+ if (resolution === null) {
183
+ skippedEntries.push(entry);
184
+ if (__DEV__) {
185
+ console.log("[ConflictAwareConnector] Conflict queued for UI resolution:", {
186
+ table: entry.table,
187
+ id: entry.id,
188
+ conflicts: conflictResult.conflicts.map((c) => c.field)
189
+ });
190
+ }
191
+ continue;
192
+ }
193
+ switch (resolution.action) {
194
+ case "overwrite":
195
+ entriesToProcess.push(entry);
196
+ break;
197
+ case "keep-server":
198
+ skippedEntries.push(entry);
199
+ break;
200
+ case "partial":
201
+ const partialEntry = {
202
+ ...entry,
203
+ opData: this.filterFields(entry.opData ?? {}, resolution.fields)
204
+ };
205
+ entriesToProcess.push(partialEntry);
206
+ break;
207
+ }
208
+ } else {
209
+ console.warn("[ConflictAwareConnector] Conflict detected but no handler:", {
210
+ table: entry.table,
211
+ id: entry.id,
212
+ conflicts: conflictResult.conflicts
213
+ });
214
+ entriesToProcess.push(entry);
215
+ }
216
+ }
217
+ if (entriesToProcess.length === 0) {
218
+ if (__DEV__) {
219
+ console.log("[ConflictAwareConnector] All entries skipped due to conflicts");
220
+ }
221
+ return;
222
+ }
223
+ try {
224
+ for (const entry of entriesToProcess) {
225
+ await this.processEntry(entry);
226
+ }
227
+ await transaction.complete();
228
+ } catch (error) {
229
+ console.error("[ConflictAwareConnector] Upload failed:", error);
230
+ throw error;
231
+ }
232
+ }
233
+ /**
234
+ * Check if a table has a _version column (cached).
235
+ */
236
+ async checkVersionColumn(table, db) {
237
+ if (this.versionColumnCache.has(table)) {
238
+ return this.versionColumnCache.get(table);
239
+ }
240
+ const hasVersion = await hasVersionColumn(table, db);
241
+ this.versionColumnCache.set(table, hasVersion);
242
+ return hasVersion;
243
+ }
244
+ /**
245
+ * Filter opData to only include specified fields.
246
+ */
247
+ filterFields(opData, fields) {
248
+ const fieldSet = new Set(fields);
249
+ const filtered = {};
250
+ for (const [key, value] of Object.entries(opData)) {
251
+ if (fieldSet.has(key)) {
252
+ filtered[key] = value;
253
+ }
254
+ }
255
+ return filtered;
256
+ }
257
+ /**
258
+ * Process a single CRUD entry - delegates to parent's private method.
259
+ *
260
+ * Note: This is a workaround since processCrudEntry is private in parent.
261
+ * We replicate the logic here for now.
262
+ */
263
+ async processEntry(entry) {
264
+ const table = entry.table;
265
+ const id = entry.id;
266
+ const schema = this.schemaRouterFn(table);
267
+ const query = schema === "public" ? this.supabaseClient.from(table) : this.supabaseClient.schema(schema).from(table);
268
+ switch (entry.op) {
269
+ case "PUT": {
270
+ const { error } = await query.upsert(
271
+ { id, ...entry.opData },
272
+ { onConflict: "id" }
273
+ ).select();
274
+ if (error) throw new Error(`Upsert failed for ${schema}.${table}: ${error.message}`);
275
+ break;
276
+ }
277
+ case "PATCH": {
278
+ const { error } = await query.update(entry.opData).eq("id", id).select();
279
+ if (error) throw new Error(`Update failed for ${schema}.${table}: ${error.message}`);
280
+ break;
281
+ }
282
+ case "DELETE": {
283
+ const { error } = await query.delete().eq("id", id).select();
284
+ if (error) throw new Error(`Delete failed for ${schema}.${table}: ${error.message}`);
285
+ break;
286
+ }
287
+ }
288
+ }
289
+ };
290
+
291
+ export {
292
+ detectConflicts,
293
+ hasVersionColumn,
294
+ fetchServerVersion,
295
+ getLocalVersion,
296
+ ConflictAwareConnector
297
+ };
298
+ //# sourceMappingURL=chunk-T225XEML.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/conflicts/detect.ts","../src/connector/conflict-aware-connector.ts"],"sourcesContent":["/**\n * Conflict Detection for @pol-studios/powersync\n *\n * Provides version-based conflict detection using AuditLog attribution.\n * Only queries AuditLog when version mismatch is detected.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase } from '../core/types';\nimport type { FieldConflict, ConflictCheckResult, ConflictDetectionConfig } from './types';\n\nconst DEFAULT_IGNORED_FIELDS = ['updatedAt', 'createdAt', '_version', 'id'];\n\n/**\n * Detect conflicts between local pending changes and server state.\n *\n * Uses a two-step approach:\n * 1. Version match (local._version == server._version) → Sync immediately\n * 2. Version mismatch → Query AuditLog for field changes and attribution\n *\n * @param table - The table name\n * @param recordId - The record ID\n * @param localVersion - Version number from local SQLite\n * @param serverVersion - Version number from server (fetched separately)\n * @param pendingChanges - Fields with local changes to sync\n * @param supabase - Supabase client for AuditLog queries\n * @param config - Optional detection configuration\n * @returns Conflict check result with field-level details\n */\nexport async function detectConflicts(\n table: string,\n recordId: string,\n localVersion: number,\n serverVersion: number,\n pendingChanges: Record<string, unknown>,\n supabase: SupabaseClient,\n config?: ConflictDetectionConfig\n): Promise<ConflictCheckResult> {\n const ignoredFields = new Set([\n ...DEFAULT_IGNORED_FIELDS,\n ...(config?.ignoredFields ?? []),\n ]);\n\n // Filter out ignored fields from pending changes\n const filteredPendingChanges: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(pendingChanges)) {\n if (!ignoredFields.has(field)) {\n filteredPendingChanges[field] = value;\n }\n }\n\n // Step 1: Version match = no conflict possible\n if (localVersion === serverVersion) {\n return {\n hasConflict: false,\n conflicts: [],\n nonConflictingChanges: Object.keys(filteredPendingChanges),\n table,\n recordId,\n };\n }\n\n // Step 2: Version mismatch - query AuditLog for changes since our version\n const { data: auditLogs, error } = await supabase\n .schema('core')\n .from('AuditLog')\n .select('oldRecord, newRecord, changeBy, changeAt')\n .eq('tableName', table)\n .eq('recordId_text', recordId)\n .order('changeAt', { ascending: false })\n .limit(20); // Recent changes should be sufficient\n\n if (error) {\n console.warn('[detectConflicts] Failed to query AuditLog:', error);\n // On error, assume no conflict and let sync proceed\n // (Server will reject if there's a real issue)\n return {\n hasConflict: false,\n conflicts: [],\n nonConflictingChanges: Object.keys(filteredPendingChanges),\n table,\n recordId,\n };\n }\n\n // Build map of server-changed fields with attribution\n // Key: field name, Value: most recent change info\n const serverChanges = new Map<string, {\n newValue: unknown;\n changedBy: string | null;\n changedAt: Date;\n }>();\n\n for (const log of auditLogs ?? []) {\n const oldRec = log.oldRecord as Record<string, unknown> | null;\n const newRec = log.newRecord as Record<string, unknown> | null;\n if (!oldRec || !newRec) continue;\n\n for (const [field, newValue] of Object.entries(newRec)) {\n // Skip ignored fields\n if (ignoredFields.has(field)) continue;\n\n // Only track if field actually changed AND we don't already have a more recent change\n if (oldRec[field] !== newValue && !serverChanges.has(field)) {\n serverChanges.set(field, {\n newValue,\n changedBy: log.changeBy as string | null,\n changedAt: new Date(log.changeAt as string),\n });\n }\n }\n }\n\n // Compare pending changes against server changes\n const conflicts: FieldConflict[] = [];\n const nonConflictingChanges: string[] = [];\n\n for (const [field, localValue] of Object.entries(filteredPendingChanges)) {\n if (serverChanges.has(field)) {\n // This field was changed on server - conflict!\n const serverChange = serverChanges.get(field)!;\n conflicts.push({\n field,\n localValue,\n serverValue: serverChange.newValue,\n changedBy: serverChange.changedBy,\n changedAt: serverChange.changedAt,\n });\n } else {\n // Field wasn't changed on server - safe to sync\n nonConflictingChanges.push(field);\n }\n }\n\n return {\n hasConflict: conflicts.length > 0,\n conflicts,\n nonConflictingChanges,\n table,\n recordId,\n };\n}\n\n/**\n * Check if a table has a _version column for conflict detection.\n *\n * @param table - The table name\n * @param db - PowerSync database instance\n * @returns True if the table has version tracking\n */\nexport async function hasVersionColumn(\n table: string,\n db: AbstractPowerSyncDatabase\n): Promise<boolean> {\n try {\n // Query the PowerSync internal schema for column info\n const result = await db.getAll<{ name: string }>(\n `PRAGMA table_info(\"${table}\")`\n );\n return result.some(col => col.name === '_version');\n } catch {\n return false;\n }\n}\n\n/**\n * Fetch the current server version for a record.\n *\n * @param table - The table name\n * @param recordId - The record ID\n * @param schema - The Supabase schema (default: 'public')\n * @param supabase - Supabase client\n * @returns The server version number, or null if record not found\n */\nexport async function fetchServerVersion(\n table: string,\n recordId: string,\n schema: string,\n supabase: SupabaseClient\n): Promise<number | null> {\n const query = schema === 'public'\n ? supabase.from(table)\n : (supabase.schema(schema) as unknown as ReturnType<typeof supabase.schema>).from(table);\n\n const { data, error } = await query\n .select('_version')\n .eq('id', recordId)\n .single();\n\n if (error || !data) {\n return null;\n }\n\n return (data as { _version?: number })._version ?? null;\n}\n\n/**\n * Get the local version for a record from PowerSync SQLite.\n *\n * @param table - The table name\n * @param recordId - The record ID\n * @param db - PowerSync database instance\n * @returns The local version number, or null if not found\n */\nexport async function getLocalVersion(\n table: string,\n recordId: string,\n db: AbstractPowerSyncDatabase\n): Promise<number | null> {\n const result = await db.get<{ _version?: number }>(\n `SELECT _version FROM \"${table}\" WHERE id = ?`,\n [recordId]\n );\n return result?._version ?? null;\n}\n","/**\n * Conflict-Aware Connector for @pol-studios/powersync\n *\n * Extends SupabaseConnector with version-based conflict detection.\n * Tables with a _version column automatically get conflict checking.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport { SupabaseConnector } from './supabase-connector';\nimport type { SupabaseConnectorOptions, SchemaRouter } from './types';\nimport type { AbstractPowerSyncDatabase, CrudEntry } from '../core/types';\nimport type { ConflictHandler, ConflictDetectionConfig, ConflictCheckResult } from '../conflicts/types';\nimport { detectConflicts, fetchServerVersion, getLocalVersion, hasVersionColumn } from '../conflicts/detect';\n\n/**\n * Options for ConflictAwareConnector.\n */\nexport interface ConflictAwareConnectorOptions extends SupabaseConnectorOptions {\n /** Handler for conflict resolution. If not provided, conflicts are logged. */\n conflictHandler?: ConflictHandler;\n /** Configuration for conflict detection behavior */\n conflictDetection?: ConflictDetectionConfig;\n}\n\n/**\n * A PowerSync connector with built-in conflict detection.\n *\n * This connector extends SupabaseConnector to add version-based conflict\n * detection for tables with a `_version` column. When a conflict is detected,\n * it calls the provided conflict handler to determine how to proceed.\n *\n * @example\n * ```typescript\n * const connector = new ConflictAwareConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: POWERSYNC_URL,\n * conflictHandler: {\n * onConflict: async (result) => {\n * // Queue for UI resolution\n * conflictContext.addConflict(result);\n * return null; // Don't proceed with upload\n * },\n * },\n * });\n * ```\n */\nexport class ConflictAwareConnector extends SupabaseConnector {\n private readonly conflictHandler?: ConflictHandler;\n private readonly conflictConfig?: ConflictDetectionConfig;\n private readonly supabaseClient: SupabaseClient;\n private readonly schemaRouterFn: SchemaRouter;\n\n // Cache for version column existence checks\n private versionColumnCache = new Map<string, boolean>();\n\n constructor(options: ConflictAwareConnectorOptions) {\n super(options);\n this.conflictHandler = options.conflictHandler;\n this.conflictConfig = options.conflictDetection;\n this.supabaseClient = options.supabaseClient;\n this.schemaRouterFn = options.schemaRouter ?? (() => 'public');\n }\n\n /**\n * Override uploadData to check for conflicts before uploading.\n *\n * For each CRUD entry in the transaction:\n * 1. Check if table has _version column (cached)\n * 2. If yes, compare local vs server version\n * 3. On version mismatch, query AuditLog for field conflicts\n * 4. If conflicts found, call handler to determine resolution\n * 5. Apply resolution or skip entry based on handler response\n */\n async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {\n // If conflict detection is disabled, use base implementation\n if (this.conflictConfig?.enabled === false) {\n return super.uploadData(database);\n }\n\n const transaction = await database.getNextCrudTransaction();\n if (!transaction) {\n return;\n }\n\n const { crud } = transaction;\n const skipTables = new Set(this.conflictConfig?.skipTables ?? []);\n const entriesToProcess: CrudEntry[] = [];\n const skippedEntries: CrudEntry[] = [];\n\n for (const entry of crud) {\n // Skip DELETE operations - no conflict checking needed\n if (entry.op === 'DELETE') {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Skip tables in the skip list\n if (skipTables.has(entry.table)) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Check for version column (cached)\n const hasVersion = await this.checkVersionColumn(entry.table, database);\n if (!hasVersion) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Get local and server versions\n const localVersion = await getLocalVersion(entry.table, entry.id, database);\n const schema = this.schemaRouterFn(entry.table);\n const serverVersion = await fetchServerVersion(\n entry.table,\n entry.id,\n schema,\n this.supabaseClient\n );\n\n // If we can't get versions, skip conflict check and proceed\n if (localVersion === null || serverVersion === null) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Detect conflicts\n const conflictResult = await detectConflicts(\n entry.table,\n entry.id,\n localVersion,\n serverVersion,\n entry.opData ?? {},\n this.supabaseClient,\n this.conflictConfig\n );\n\n if (!conflictResult.hasConflict) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Handle conflict\n if (this.conflictHandler) {\n const resolution = await this.conflictHandler.onConflict(conflictResult);\n\n if (resolution === null) {\n // Queue for UI - skip this entry\n skippedEntries.push(entry);\n if (__DEV__) {\n console.log('[ConflictAwareConnector] Conflict queued for UI resolution:', {\n table: entry.table,\n id: entry.id,\n conflicts: conflictResult.conflicts.map(c => c.field),\n });\n }\n continue;\n }\n\n switch (resolution.action) {\n case 'overwrite':\n // Proceed with upload (overwrite server)\n entriesToProcess.push(entry);\n break;\n\n case 'keep-server':\n // Discard local changes - skip this entry\n skippedEntries.push(entry);\n break;\n\n case 'partial':\n // Only sync specified fields\n const partialEntry: CrudEntry = {\n ...entry,\n opData: this.filterFields(entry.opData ?? {}, resolution.fields),\n };\n entriesToProcess.push(partialEntry);\n break;\n }\n } else {\n // No handler - log conflict and proceed with upload\n console.warn('[ConflictAwareConnector] Conflict detected but no handler:', {\n table: entry.table,\n id: entry.id,\n conflicts: conflictResult.conflicts,\n });\n entriesToProcess.push(entry);\n }\n }\n\n // If all entries were skipped, complete the transaction without uploading\n if (entriesToProcess.length === 0) {\n if (__DEV__) {\n console.log('[ConflictAwareConnector] All entries skipped due to conflicts');\n }\n // Don't complete the transaction - leave entries in queue for later\n return;\n }\n\n // Process remaining entries using parent's logic\n // We need to manually process since we've modified the entries\n try {\n for (const entry of entriesToProcess) {\n await this.processEntry(entry);\n }\n await transaction.complete();\n } catch (error) {\n console.error('[ConflictAwareConnector] Upload failed:', error);\n throw error;\n }\n }\n\n /**\n * Check if a table has a _version column (cached).\n */\n private async checkVersionColumn(\n table: string,\n db: AbstractPowerSyncDatabase\n ): Promise<boolean> {\n if (this.versionColumnCache.has(table)) {\n return this.versionColumnCache.get(table)!;\n }\n\n const hasVersion = await hasVersionColumn(table, db);\n this.versionColumnCache.set(table, hasVersion);\n return hasVersion;\n }\n\n /**\n * Filter opData to only include specified fields.\n */\n private filterFields(\n opData: Record<string, unknown>,\n fields: string[]\n ): Record<string, unknown> {\n const fieldSet = new Set(fields);\n const filtered: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(opData)) {\n if (fieldSet.has(key)) {\n filtered[key] = value;\n }\n }\n return filtered;\n }\n\n /**\n * Process a single CRUD entry - delegates to parent's private method.\n *\n * Note: This is a workaround since processCrudEntry is private in parent.\n * We replicate the logic here for now.\n */\n private async processEntry(entry: CrudEntry): Promise<void> {\n const table = entry.table;\n const id = entry.id;\n const schema = this.schemaRouterFn(table);\n\n const query = schema === 'public'\n ? this.supabaseClient.from(table)\n : (this.supabaseClient.schema(schema) as unknown as ReturnType<typeof this.supabaseClient.schema>).from(table);\n\n switch (entry.op) {\n case 'PUT': {\n const { error } = await query.upsert(\n { id, ...entry.opData },\n { onConflict: 'id' }\n ).select();\n if (error) throw new Error(`Upsert failed for ${schema}.${table}: ${error.message}`);\n break;\n }\n\n case 'PATCH': {\n const { error } = await query\n .update(entry.opData)\n .eq('id', id)\n .select();\n if (error) throw new Error(`Update failed for ${schema}.${table}: ${error.message}`);\n break;\n }\n\n case 'DELETE': {\n const { error } = await query.delete().eq('id', id).select();\n if (error) throw new Error(`Delete failed for ${schema}.${table}: ${error.message}`);\n break;\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=chunk-W7HSR35B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,5 @@
1
+ export { C as ConnectorConfig, c as CrudHandler, P as PowerSyncCredentials, b as SchemaRouter, S as SupabaseConnector, a as SupabaseConnectorOptions, d as defaultSchemaRouter } from '../supabase-connector-D14-kl5v.js';
2
+ export { C as ConflictAwareConnector, a as ConflictAwareConnectorOptions } from '../index-nae7nzib.js';
3
+ export { P as PowerSyncBackendConnector } from '../types-afHtE1U_.js';
4
+ import '@supabase/supabase-js';
5
+ import '../platform/index.js';
@@ -0,0 +1,14 @@
1
+ import {
2
+ ConflictAwareConnector
3
+ } from "../chunk-T225XEML.js";
4
+ import {
5
+ SupabaseConnector,
6
+ defaultSchemaRouter
7
+ } from "../chunk-FLHDT4TS.js";
8
+ import "../chunk-CHRTN5PF.js";
9
+ export {
10
+ ConflictAwareConnector,
11
+ SupabaseConnector,
12
+ defaultSchemaRouter
13
+ };
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}