@pol-studios/powersync 1.0.7 → 1.0.10
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.
- package/README.md +933 -0
- package/dist/CacheSettingsManager-uz-kbnRH.d.ts +461 -0
- package/dist/attachments/index.d.ts +709 -6
- package/dist/attachments/index.js +133 -5
- package/dist/chunk-24RDMMCL.js +44 -0
- package/dist/chunk-24RDMMCL.js.map +1 -0
- package/dist/chunk-4TXTAEF2.js +2060 -0
- package/dist/chunk-4TXTAEF2.js.map +1 -0
- package/dist/chunk-63PXSPIN.js +358 -0
- package/dist/chunk-63PXSPIN.js.map +1 -0
- package/dist/chunk-654ERHA7.js +1 -0
- package/dist/{chunk-BREGB4WL.js → chunk-BRXQNASY.js} +287 -335
- package/dist/chunk-BRXQNASY.js.map +1 -0
- package/dist/{chunk-DHYUBVP7.js → chunk-CAB26E6F.js} +20 -9
- package/dist/chunk-CAB26E6F.js.map +1 -0
- package/dist/{chunk-H772V6XQ.js → chunk-CUCAYK7Z.js} +7 -43
- package/dist/chunk-CUCAYK7Z.js.map +1 -0
- package/dist/{chunk-4C3RY5SU.js → chunk-HWSNV45P.js} +76 -1
- package/dist/chunk-HWSNV45P.js.map +1 -0
- package/dist/{chunk-HFOFLW5F.js → chunk-KN2IZERF.js} +139 -6
- package/dist/chunk-KN2IZERF.js.map +1 -0
- package/dist/{chunk-UEYRTLKE.js → chunk-P4HZA6ZT.js} +20 -9
- package/dist/chunk-P4HZA6ZT.js.map +1 -0
- package/dist/chunk-T4AO7JIG.js +1 -0
- package/dist/{chunk-XQAJM2MW.js → chunk-VACPAAQZ.js} +33 -2
- package/dist/{chunk-XQAJM2MW.js.map → chunk-VACPAAQZ.js.map} +1 -1
- package/dist/{chunk-53WH2JJV.js → chunk-WN5ZJ3E2.js} +5 -8
- package/dist/chunk-WN5ZJ3E2.js.map +1 -0
- package/dist/chunk-XAEII4ZX.js +456 -0
- package/dist/chunk-XAEII4ZX.js.map +1 -0
- package/dist/chunk-XOY2CJ67.js +289 -0
- package/dist/chunk-XOY2CJ67.js.map +1 -0
- package/dist/chunk-YHTZ7VMV.js +1 -0
- package/dist/{chunk-MKD2VCX3.js → chunk-Z6VOBGTU.js} +8 -8
- package/dist/chunk-Z6VOBGTU.js.map +1 -0
- package/dist/chunk-ZM4ENYMF.js +230 -0
- package/dist/chunk-ZM4ENYMF.js.map +1 -0
- package/dist/connector/index.d.ts +56 -3
- package/dist/connector/index.js +8 -5
- package/dist/core/index.d.ts +12 -1
- package/dist/core/index.js +3 -2
- package/dist/error/index.js +0 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +191 -29
- package/dist/index.native.d.ts +11 -9
- package/dist/index.native.js +191 -29
- package/dist/index.web.d.ts +11 -9
- package/dist/index.web.js +191 -29
- package/dist/maintenance/index.js +0 -1
- package/dist/platform/index.js +0 -2
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.js +1 -2
- package/dist/platform/index.web.js +0 -1
- package/dist/pol-attachment-queue-BVAIueoP.d.ts +817 -0
- package/dist/provider/index.d.ts +38 -34
- package/dist/provider/index.js +11 -12
- package/dist/react/index.d.ts +372 -0
- package/dist/react/index.js +25 -0
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.js +22 -8
- package/dist/storage/index.native.d.ts +3 -3
- package/dist/storage/index.native.js +21 -7
- package/dist/storage/index.web.d.ts +3 -3
- package/dist/storage/index.web.js +21 -7
- package/dist/storage/upload/index.d.ts +7 -8
- package/dist/storage/upload/index.js +3 -3
- package/dist/storage/upload/index.native.d.ts +7 -8
- package/dist/storage/upload/index.native.js +4 -3
- package/dist/storage/upload/index.web.d.ts +1 -4
- package/dist/storage/upload/index.web.js +3 -3
- package/dist/supabase-connector-T9vHq_3i.d.ts +202 -0
- package/dist/sync/index.js +3 -3
- package/dist/{supabase-connector-qLm-WHkM.d.ts → types-B212hgfA.d.ts} +48 -170
- package/dist/{types-BVacP54t.d.ts → types-CyvBaAl8.d.ts} +12 -4
- package/dist/types-D0WcHrq6.d.ts +234 -0
- package/package.json +18 -4
- package/dist/CacheSettingsManager-1exbOC6S.d.ts +0 -261
- package/dist/chunk-4C3RY5SU.js.map +0 -1
- package/dist/chunk-53WH2JJV.js.map +0 -1
- package/dist/chunk-BREGB4WL.js.map +0 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-DHYUBVP7.js.map +0 -1
- package/dist/chunk-GKF7TOMT.js +0 -1
- package/dist/chunk-H772V6XQ.js.map +0 -1
- package/dist/chunk-HFOFLW5F.js.map +0 -1
- package/dist/chunk-KGSFAE5B.js +0 -1
- package/dist/chunk-LNL64IJZ.js +0 -1
- package/dist/chunk-MKD2VCX3.js.map +0 -1
- package/dist/chunk-UEYRTLKE.js.map +0 -1
- package/dist/chunk-WQ5MPAVC.js +0 -449
- package/dist/chunk-WQ5MPAVC.js.map +0 -1
- package/dist/chunk-ZEOKPWUC.js +0 -1165
- package/dist/chunk-ZEOKPWUC.js.map +0 -1
- package/dist/pol-attachment-queue-C7YNXXhK.d.ts +0 -676
- package/dist/types-Bgvx7-E8.d.ts +0 -187
- /package/dist/{chunk-DGUM43GV.js.map → chunk-654ERHA7.js.map} +0 -0
- /package/dist/{chunk-GKF7TOMT.js.map → chunk-T4AO7JIG.js.map} +0 -0
- /package/dist/{chunk-KGSFAE5B.js.map → chunk-YHTZ7VMV.js.map} +0 -0
- /package/dist/{chunk-LNL64IJZ.js.map → react/index.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sync/status-tracker.ts","../src/sync/metrics-collector.ts","../src/sync/health-monitor.ts"],"sourcesContent":["/**\n * Sync Status Tracker for @pol-studios/powersync\n *\n * Tracks and normalizes PowerSync status changes, providing a consistent\n * interface for status updates with throttling support.\n */\n\nimport type { SyncStatus, DownloadProgress, CrudEntry, FailedTransaction, SyncError, CompletedTransaction, SyncMode } from '../core/types';\nimport { generateFailureId } from '../core/errors';\nimport type { LoggerAdapter, AsyncStorageAdapter } from '../platform/types';\nimport type { SyncStatusState, SyncStatusTrackerOptions, PowerSyncRawStatus, Unsubscribe } from './types';\nimport { STORAGE_KEY_PAUSED, STORAGE_KEY_SYNC_MODE, STORAGE_KEY_AUTO_OFFLINE, STATUS_NOTIFY_THROTTLE_MS } from '../core/constants';\nconst STORAGE_KEY_COMPLETED_TRANSACTIONS = '@pol-powersync:completed_transactions';\nconst STORAGE_KEY_FAILED_TRANSACTIONS = '@pol-powersync:failed_transactions';\nimport { DEFAULT_SYNC_STATUS } from '../provider/types';\n\n/**\n * Tracks sync status from PowerSync and provides normalized updates.\n *\n * Features:\n * - Normalizes raw PowerSync status to a consistent format\n * - Throttles notifications to prevent UI thrashing\n * - Tracks pending mutations count\n * - Persists and restores paused state\n *\n * @example\n * ```typescript\n * const tracker = new SyncStatusTracker({\n * storage,\n * logger,\n * onStatusChange: (status) => console.log('Status:', status),\n * });\n *\n * // Register with PowerSync\n * db.registerListener({\n * statusChanged: (rawStatus) => tracker.handleStatusChange(rawStatus),\n * });\n *\n * // Get current status\n * const status = tracker.getStatus();\n * ```\n */\nexport class SyncStatusTracker {\n private readonly storage: AsyncStorageAdapter;\n private readonly logger: LoggerAdapter;\n private readonly notifyThrottleMs: number;\n private readonly onStatusChange?: (status: SyncStatus) => void;\n private _state: SyncStatusState;\n private _pendingMutations: CrudEntry[] = [];\n private _lastNotifyTime = 0;\n private _notifyTimer: ReturnType<typeof setTimeout> | null = null;\n private _listeners = new Set<(status: SyncStatus) => void>();\n private _syncModeListeners = new Set<(mode: SyncMode) => void>();\n\n // Force next upload flag for \"Sync Now\" functionality\n private _forceNextUpload = false;\n\n // Network reachability gate - blocks uploads instantly when network is unreachable\n private _networkReachable = true;\n private _networkRestoreTimer: ReturnType<typeof setTimeout> | null = null;\n private readonly _networkRestoreDelayMs = 1500; // 1.5 seconds delay before restoring\n\n // Debounce timer for persist operations to avoid race conditions\n private _persistDebounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n // Track download progress separately to preserve it when offline\n private _lastProgress: DownloadProgress | null = null;\n\n // Failed transaction tracking\n private _failedTransactions: FailedTransaction[] = [];\n private readonly _maxStoredFailures = 50;\n private readonly _failureTTLMs = 24 * 60 * 60 * 1000; // 24 hours\n private _failureListeners = new Set<(failures: FailedTransaction[]) => void>();\n\n // Completed transaction tracking (no limit - full audit trail)\n private _completedTransactions: CompletedTransaction[] = [];\n private _completedListeners = new Set<(completed: CompletedTransaction[]) => void>();\n\n // Track when notifications were last displayed/dismissed for \"auto-dismiss on display\"\n // This allows filtering completed transactions to only show new ones since last display\n private _lastNotificationTime: number = Date.now();\n\n // Auto-offline flag: tracks whether offline mode was set automatically (network loss)\n // vs manually (user chose offline). Persisted so auto-restore works after app restart.\n private _isAutoOffline = false;\n constructor(storage: AsyncStorageAdapter, logger: LoggerAdapter, options: SyncStatusTrackerOptions = {}) {\n this.storage = storage;\n this.logger = logger;\n this.notifyThrottleMs = options.notifyThrottleMs ?? STATUS_NOTIFY_THROTTLE_MS;\n this.onStatusChange = options.onStatusChange;\n this._state = {\n status: {\n ...DEFAULT_SYNC_STATUS\n },\n syncMode: 'push-pull',\n lastUpdated: new Date()\n };\n }\n\n // ─── Initialization ────────────────────────────────────────────────────────\n\n /**\n * Initialize the tracker by loading persisted state.\n * Includes migration from old isPaused boolean to new syncMode.\n */\n async init(): Promise<void> {\n try {\n // Try to load new sync mode first\n const modeValue = await this.storage.getItem(STORAGE_KEY_SYNC_MODE);\n if (modeValue && ['push-pull', 'pull-only', 'offline'].includes(modeValue)) {\n this._state.syncMode = modeValue as SyncMode;\n this.logger.debug('[StatusTracker] Loaded sync mode:', this._state.syncMode);\n } else {\n // Migrate from old isPaused boolean\n const pausedValue = await this.storage.getItem(STORAGE_KEY_PAUSED);\n if (pausedValue === 'true') {\n this._state.syncMode = 'offline';\n await this.storage.setItem(STORAGE_KEY_SYNC_MODE, 'offline');\n this.logger.debug('[StatusTracker] Migrated isPaused=true to syncMode=offline');\n } else {\n this._state.syncMode = 'push-pull';\n }\n // Clean up old key\n await this.storage.removeItem(STORAGE_KEY_PAUSED);\n }\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to load sync mode:', err);\n }\n\n // Load persisted auto-offline flag\n try {\n const autoOfflineValue = await this.storage.getItem(STORAGE_KEY_AUTO_OFFLINE);\n this._isAutoOffline = autoOfflineValue === 'true';\n this.logger.debug('[StatusTracker] Loaded isAutoOffline:', this._isAutoOffline);\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to load auto-offline flag:', err);\n }\n\n // Load persisted completed transactions\n try {\n const completedJson = await this.storage.getItem(STORAGE_KEY_COMPLETED_TRANSACTIONS);\n if (completedJson) {\n const parsed = JSON.parse(completedJson) as Array<Omit<CompletedTransaction, 'completedAt'> & {\n completedAt: string;\n }>;\n this._completedTransactions = parsed.map(item => {\n const remappedEntries = item.entries.map(e => this.remapEntry(e)).filter((e): e is CrudEntry => e !== null);\n return {\n ...item,\n completedAt: new Date(item.completedAt),\n entries: remappedEntries\n };\n }).filter(item => !isNaN(item.completedAt.getTime()) && item.entries.length > 0);\n this.logger.debug('[StatusTracker] Loaded', this._completedTransactions.length, 'completed transactions');\n }\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to load completed transactions:', err);\n }\n\n // Load persisted failed transactions\n try {\n const failedJson = await this.storage.getItem(STORAGE_KEY_FAILED_TRANSACTIONS);\n if (failedJson) {\n const parsed = JSON.parse(failedJson) as Array<Omit<FailedTransaction, 'firstFailedAt' | 'lastFailedAt' | 'error'> & {\n firstFailedAt: string;\n lastFailedAt: string;\n error: Omit<SyncError, 'timestamp'> & {\n timestamp: string;\n };\n }>;\n this._failedTransactions = parsed.map(item => {\n const remappedEntries = item.entries.map(e => this.remapEntry(e)).filter((e): e is CrudEntry => e !== null);\n return {\n ...item,\n firstFailedAt: new Date(item.firstFailedAt),\n lastFailedAt: new Date(item.lastFailedAt),\n error: {\n ...item.error,\n timestamp: new Date(item.error.timestamp)\n },\n entries: remappedEntries\n };\n }).filter(item => !isNaN(item.firstFailedAt.getTime()) && !isNaN(item.lastFailedAt.getTime()) && item.entries.length > 0);\n this.logger.debug('[StatusTracker] Loaded', this._failedTransactions.length, 'failed transactions');\n }\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to load failed transactions:', err);\n }\n\n // Clean up any stale failures that were persisted\n this.cleanupStaleFailures();\n }\n\n /**\n * Dispose the tracker and clear timers.\n */\n dispose(): void {\n if (this._notifyTimer) {\n clearTimeout(this._notifyTimer);\n this._notifyTimer = null;\n }\n if (this._persistDebounceTimer) {\n clearTimeout(this._persistDebounceTimer);\n this._persistDebounceTimer = null;\n }\n if (this._networkRestoreTimer) {\n clearTimeout(this._networkRestoreTimer);\n this._networkRestoreTimer = null;\n }\n this._listeners.clear();\n this._syncModeListeners.clear();\n this._failureListeners.clear();\n this._completedListeners.clear();\n }\n\n // ─── Status Getters ────────────────────────────────────────────────────────\n\n /**\n * Get the current sync status.\n */\n getStatus(): SyncStatus {\n const baseStatus = this._state.status;\n\n // Build the status with failed transaction info\n const status: SyncStatus = {\n ...baseStatus,\n failedTransactions: this._failedTransactions,\n hasUploadErrors: this._failedTransactions.length > 0,\n permanentErrorCount: this._failedTransactions.filter(f => f.isPermanent).length\n };\n\n // If offline, use saved progress instead of live (which would be null)\n if (this._state.syncMode === 'offline' && this._lastProgress) {\n return {\n ...status,\n downloadProgress: this._lastProgress\n };\n }\n return status;\n }\n\n // ─── Sync Mode Getters ────────────────────────────────────────────────────────\n\n /**\n * Get the current sync mode.\n */\n getSyncMode(): SyncMode {\n return this._state.syncMode;\n }\n\n /**\n * Check if uploads are allowed based on current sync mode and network reachability.\n */\n canUpload(): boolean {\n return this._networkReachable && this._state.syncMode === 'push-pull';\n }\n\n /**\n * Check if downloads are allowed based on current sync mode.\n */\n canDownload(): boolean {\n return this._state.syncMode !== 'offline';\n }\n\n /**\n * Set the force next upload flag for \"Sync Now\" functionality.\n */\n setForceNextUpload(force: boolean): void {\n this._forceNextUpload = force;\n this.logger.debug('[StatusTracker] Force next upload set to:', force);\n }\n\n /**\n * Clear the force next upload flag.\n * Should be called after all pending uploads have been processed.\n */\n clearForceNextUpload(): void {\n if (this._forceNextUpload) {\n this._forceNextUpload = false;\n this.logger.debug('[StatusTracker] Force next upload flag cleared');\n }\n }\n\n /**\n * Check if upload should proceed, considering force flag and network reachability.\n * NOTE: Does NOT auto-reset the flag - caller must use clearForceNextUpload()\n * after all uploads are complete. This prevents race conditions when\n * PowerSync calls uploadData() multiple times for multiple transactions.\n */\n shouldUpload(): boolean {\n // Force flag bypasses all gates (user explicitly requested sync)\n if (this._forceNextUpload) {\n return true;\n }\n // Network gate - instant block when unreachable (0ms, no timeouts)\n if (!this._networkReachable) {\n return false;\n }\n return this._state.syncMode === 'push-pull';\n }\n\n /**\n * Set network reachability state.\n * - When unreachable: Instantly blocks uploads (0ms)\n * - When reachable: Delayed restore (1-2 seconds) to avoid flickering on brief disconnects\n */\n setNetworkReachable(reachable: boolean): void {\n // Clear any pending restore timer\n if (this._networkRestoreTimer) {\n clearTimeout(this._networkRestoreTimer);\n this._networkRestoreTimer = null;\n }\n if (!reachable) {\n // Instant block when network becomes unreachable\n if (this._networkReachable) {\n this._networkReachable = false;\n this.logger.debug('[StatusTracker] Network unreachable - uploads blocked instantly');\n }\n } else {\n // Delayed restore when network becomes reachable\n if (!this._networkReachable) {\n this.logger.debug('[StatusTracker] Network reachable - scheduling delayed restore');\n this._networkRestoreTimer = setTimeout(() => {\n this._networkRestoreTimer = null;\n this._networkReachable = true;\n this.logger.debug('[StatusTracker] Network restored - uploads enabled');\n }, this._networkRestoreDelayMs);\n }\n }\n }\n\n /**\n * Get current network reachability state.\n */\n isNetworkReachable(): boolean {\n return this._networkReachable;\n }\n\n /**\n * Get pending mutations.\n */\n getPendingMutations(): CrudEntry[] {\n return this._pendingMutations;\n }\n\n /**\n * Get pending mutation count.\n */\n getPendingCount(): number {\n return this._pendingMutations.length;\n }\n\n // ─── Status Updates ────────────────────────────────────────────────────────\n\n /**\n * Handle a raw status update from PowerSync.\n */\n handleStatusChange(rawStatus: PowerSyncRawStatus): void {\n const progress = rawStatus.downloadProgress;\n const dataFlow = rawStatus.dataFlowStatus;\n\n // Build normalized download progress\n let downloadProgress: DownloadProgress | null = null;\n if (progress && progress.totalOperations && progress.totalOperations > 0) {\n downloadProgress = {\n current: progress.downloadedOperations ?? 0,\n target: progress.totalOperations,\n percentage: Math.round((progress.downloadedFraction ?? 0) * 100)\n };\n // Save progress for when paused\n this._lastProgress = downloadProgress;\n }\n\n // Build normalized status (failed transaction fields are added in getStatus())\n const newStatus: SyncStatus = {\n connected: rawStatus.connected ?? false,\n connecting: rawStatus.connecting ?? false,\n hasSynced: rawStatus.hasSynced ?? false,\n lastSyncedAt: rawStatus.lastSyncedAt ?? null,\n uploading: dataFlow?.uploading ?? false,\n downloading: dataFlow?.downloading ?? false,\n downloadProgress,\n // These are computed from _failedTransactions in getStatus()\n failedTransactions: [],\n hasUploadErrors: false,\n permanentErrorCount: 0\n };\n\n // Check if status actually changed\n const changed = this._hasStatusChanged(newStatus);\n this._state = {\n status: newStatus,\n syncMode: this._state.syncMode,\n lastUpdated: new Date()\n };\n if (changed) {\n this._notifyListeners();\n }\n }\n\n /**\n * Update pending mutations from a CRUD transaction.\n */\n updatePendingMutations(mutations: CrudEntry[]): void {\n this._pendingMutations = mutations;\n }\n\n /**\n * Valid sync modes for runtime validation.\n */\n private static readonly VALID_SYNC_MODES: SyncMode[] = ['push-pull', 'pull-only', 'offline'];\n\n /**\n * Set the sync mode.\n */\n async setSyncMode(mode: SyncMode): Promise<void> {\n // Runtime validation\n if (!SyncStatusTracker.VALID_SYNC_MODES.includes(mode)) {\n this.logger.warn('[StatusTracker] Invalid sync mode, ignoring:', mode);\n return;\n }\n if (this._state.syncMode === mode) return;\n const previousMode = this._state.syncMode;\n this._state.syncMode = mode;\n\n // Persist to storage\n try {\n await this.storage.setItem(STORAGE_KEY_SYNC_MODE, mode);\n this.logger.info('[StatusTracker] Sync mode changed:', previousMode, '->', mode);\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to persist sync mode:', err);\n }\n\n // Notify sync mode listeners\n for (const listener of this._syncModeListeners) {\n try {\n listener(mode);\n } catch (err) {\n this.logger.warn('[StatusTracker] Sync mode listener error:', err);\n }\n }\n this._notifyListeners(true);\n }\n\n // ─── Auto-Offline Management ──────────────────────────────────────────────\n\n /**\n * Get whether offline mode was set automatically (network loss) vs manually.\n * Used to determine if sync should auto-resume when network returns.\n */\n getIsAutoOffline(): boolean {\n return this._isAutoOffline;\n }\n\n /**\n * Set the auto-offline flag and persist it.\n * @param isAuto - true if offline was set automatically, false if user chose offline\n */\n async setIsAutoOffline(isAuto: boolean): Promise<void> {\n if (this._isAutoOffline === isAuto) return;\n this._isAutoOffline = isAuto;\n\n // Persist to storage\n try {\n await this.storage.setItem(STORAGE_KEY_AUTO_OFFLINE, isAuto ? 'true' : 'false');\n this.logger.debug('[StatusTracker] Auto-offline flag changed:', isAuto);\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to persist auto-offline flag:', err);\n }\n }\n\n // ─── Subscriptions ─────────────────────────────────────────────────────────\n\n /**\n * Subscribe to status changes.\n * @returns Unsubscribe function\n */\n onStatusUpdate(listener: (status: SyncStatus) => void): Unsubscribe {\n this._listeners.add(listener);\n // Immediately call with current status\n listener(this.getStatus());\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /**\n * Subscribe to sync mode changes.\n * @returns Unsubscribe function\n */\n onSyncModeChange(listener: (mode: SyncMode) => void): Unsubscribe {\n this._syncModeListeners.add(listener);\n listener(this._state.syncMode);\n return () => {\n this._syncModeListeners.delete(listener);\n };\n }\n\n // ─── Failed Transaction Tracking ────────────────────────────────────────────\n\n /**\n * Record a transaction failure.\n * If a failure for the same entries already exists, updates the retry count.\n * Otherwise, creates a new failure record.\n *\n * @param preserveMetadata - Optional. If provided, preserves retryCount and firstFailedAt from a previous failure.\n */\n recordTransactionFailure(entries: CrudEntry[], error: SyncError, isPermanent: boolean, affectedEntityIds: string[], affectedTables: string[], preserveMetadata?: {\n retryCount: number;\n firstFailedAt: Date;\n }): void {\n const now = new Date();\n\n // Normalize entries to plain objects to avoid CrudEntry.toJSON() remapping issues\n const normalizedEntries = this.normalizeEntries(entries);\n const entryIds = normalizedEntries.map(e => e.id).sort().join(',');\n\n // Check if a failure for these entries already exists\n const existingIndex = this._failedTransactions.findIndex(f => {\n const existingIds = f.entries.map(e => e.id).sort().join(',');\n return existingIds === entryIds;\n });\n if (existingIndex !== -1) {\n // Update existing failure\n const existing = this._failedTransactions[existingIndex];\n this._failedTransactions[existingIndex] = {\n ...existing,\n error,\n retryCount: existing.retryCount + 1,\n lastFailedAt: now,\n isPermanent\n };\n } else {\n // Create new failure record\n const newFailure: FailedTransaction = {\n id: generateFailureId(normalizedEntries),\n entries: normalizedEntries,\n error,\n retryCount: preserveMetadata?.retryCount ?? 1,\n firstFailedAt: preserveMetadata?.firstFailedAt ?? now,\n lastFailedAt: now,\n isPermanent,\n affectedEntityIds,\n affectedTables\n };\n this._failedTransactions.push(newFailure);\n\n // Enforce max stored failures (remove oldest)\n if (this._failedTransactions.length > this._maxStoredFailures) {\n this._failedTransactions.sort((a, b) => a.firstFailedAt.getTime() - b.firstFailedAt.getTime());\n this._failedTransactions = this._failedTransactions.slice(-this._maxStoredFailures);\n }\n }\n this._schedulePersist();\n this._notifyFailureListeners();\n this._notifyListeners();\n }\n\n /**\n * Clear a specific failure by ID.\n */\n clearFailure(failureId: string): void {\n const initialLength = this._failedTransactions.length;\n this._failedTransactions = this._failedTransactions.filter(f => f.id !== failureId);\n if (this._failedTransactions.length !== initialLength) {\n this._schedulePersist();\n this._notifyFailureListeners();\n this._notifyListeners();\n }\n }\n\n /**\n * Clear all failures.\n */\n clearAllFailures(): void {\n if (this._failedTransactions.length === 0) return;\n this._failedTransactions = [];\n this._schedulePersist();\n this._notifyFailureListeners();\n this._notifyListeners();\n }\n\n /**\n * Remove a failed transaction from tracking and return its entries.\n * This is a \"pop\" operation - the failure is removed from the list.\n *\n * Note: The actual CRUD entries remain in PowerSync's ps_crud table\n * until successfully uploaded. This just removes from our tracking.\n *\n * @param failureId - The failure ID to remove\n * @returns The CrudEntry[] that were in the failure, or null if not found\n */\n takeFailureForRetry(failureId: string): CrudEntry[] | null {\n const failure = this._failedTransactions.find(f => f.id === failureId);\n if (!failure) {\n this.logger.warn('[StatusTracker] Failure not found for retry:', failureId);\n return null;\n }\n\n // Remove from failed list\n this._failedTransactions = this._failedTransactions.filter(f => f.id !== failureId);\n this._notifyFailureListeners();\n this._schedulePersist();\n this.logger.info('[StatusTracker] Retrieved failure for retry:', failureId, 'entries:', failure.entries.length);\n return failure.entries;\n }\n\n /**\n * Get failures affecting a specific entity.\n */\n getFailuresForEntity(entityId: string): FailedTransaction[] {\n return this._failedTransactions.filter(f => f.affectedEntityIds.includes(entityId));\n }\n\n /**\n * Get all failed transactions.\n */\n getFailedTransactions(): FailedTransaction[] {\n return [...this._failedTransactions];\n }\n\n /**\n * Check if there are any upload errors.\n */\n hasUploadErrors(): boolean {\n return this._failedTransactions.length > 0;\n }\n\n /**\n * Get count of permanent errors.\n */\n getPermanentErrorCount(): number {\n return this._failedTransactions.filter(f => f.isPermanent).length;\n }\n\n /**\n * Subscribe to failure changes.\n * @returns Unsubscribe function\n */\n onFailureChange(listener: (failures: FailedTransaction[]) => void): Unsubscribe {\n this._failureListeners.add(listener);\n // Immediately call with current failures\n listener(this.getFailedTransactions());\n return () => {\n this._failureListeners.delete(listener);\n };\n }\n\n /**\n * Clean up stale failures (older than TTL).\n */\n cleanupStaleFailures(): void {\n const cutoff = Date.now() - this._failureTTLMs;\n const initialLength = this._failedTransactions.length;\n this._failedTransactions = this._failedTransactions.filter(f => f.lastFailedAt.getTime() > cutoff);\n if (this._failedTransactions.length !== initialLength) {\n this.logger.debug(`[StatusTracker] Cleaned up ${initialLength - this._failedTransactions.length} stale failures`);\n this._schedulePersist();\n this._notifyFailureListeners();\n this._notifyListeners();\n }\n }\n\n // ─── Completed Transaction Tracking ────────────────────────────────────────\n\n /**\n * Record a successfully completed transaction.\n * Creates a CompletedTransaction record and adds it to history.\n */\n recordTransactionComplete(entries: CrudEntry[]): void {\n // Normalize entries to plain objects to avoid CrudEntry.toJSON() remapping issues\n const normalizedEntries = this.normalizeEntries(entries);\n\n // Extract unique affected tables\n const affectedTables = [...new Set(normalizedEntries.map(e => e.table))];\n\n // Extract unique affected entity IDs\n const affectedEntityIds = [...new Set(normalizedEntries.map(e => e.id))];\n\n // Generate unique ID\n const id = `completed_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n const completed: CompletedTransaction = {\n id,\n entries: normalizedEntries,\n completedAt: new Date(),\n affectedTables,\n affectedEntityIds\n };\n\n // Add to front of array (most recent first)\n this._completedTransactions.unshift(completed);\n\n // No limit on completed history - user wants full audit trail\n\n this._schedulePersist();\n this._notifyCompletedListeners();\n this.logger.debug(`[StatusTracker] Recorded completed transaction: ${completed.id} (${entries.length} entries)`);\n }\n\n /**\n * Get all completed transactions.\n */\n getCompletedTransactions(): CompletedTransaction[] {\n return [...this._completedTransactions];\n }\n\n /**\n * Clear completed transaction history.\n */\n clearCompletedHistory(): void {\n if (this._completedTransactions.length === 0) return;\n this._completedTransactions = [];\n this._schedulePersist();\n this._notifyCompletedListeners();\n this.logger.debug('[StatusTracker] Cleared completed transaction history');\n }\n\n /**\n * Clear a specific completed transaction by ID.\n */\n clearCompletedItem(completedId: string): void {\n const initialLength = this._completedTransactions.length;\n this._completedTransactions = this._completedTransactions.filter(c => c.id !== completedId);\n if (this._completedTransactions.length !== initialLength) {\n this._schedulePersist();\n this._notifyCompletedListeners();\n this.logger.debug('[StatusTracker] Cleared completed transaction:', completedId);\n }\n }\n\n /**\n * Subscribe to completed transaction changes.\n * @returns Unsubscribe function\n */\n onCompletedChange(listener: (completed: CompletedTransaction[]) => void): Unsubscribe {\n this._completedListeners.add(listener);\n // Immediately call with current completed transactions\n listener(this.getCompletedTransactions());\n return () => {\n this._completedListeners.delete(listener);\n };\n }\n\n // ─── Notification Tracking ─────────────────────────────────────────────────\n\n /**\n * Get completed transactions that occurred AFTER the last notification time.\n * This is used for displaying \"X changes synced\" notifications to avoid\n * showing stale counts from historical completed transactions.\n */\n getNewCompletedTransactions(): CompletedTransaction[] {\n return this._completedTransactions.filter(tx => tx.completedAt.getTime() > this._lastNotificationTime);\n }\n\n /**\n * Mark notifications as seen by updating the last notification time.\n * Call this when the notification is displayed or dismissed.\n */\n markNotificationsAsSeen(): void {\n this._lastNotificationTime = Date.now();\n this.logger.debug('[StatusTracker] Notifications marked as seen');\n // Notify listeners so UI can update (newCompletedTransactions will now be empty)\n this._notifyCompletedListeners();\n }\n\n /**\n * Get the timestamp of when notifications were last displayed/dismissed.\n */\n getLastNotificationTime(): number {\n return this._lastNotificationTime;\n }\n\n // ─── Private Methods ───────────────────────────────────────────────────────\n\n /**\n * Schedule a debounced persist operation.\n * This prevents race conditions from multiple rapid persist calls.\n */\n private _schedulePersist(): void {\n if (this._persistDebounceTimer) {\n clearTimeout(this._persistDebounceTimer);\n }\n this._persistDebounceTimer = setTimeout(() => {\n this._persistDebounceTimer = null;\n this._persistTransactions();\n }, 100); // 100ms debounce\n }\n\n /**\n * Persist completed and failed transactions to storage.\n */\n private async _persistTransactions(): Promise<void> {\n try {\n await Promise.all([this.storage.setItem(STORAGE_KEY_COMPLETED_TRANSACTIONS, JSON.stringify(this._completedTransactions)), this.storage.setItem(STORAGE_KEY_FAILED_TRANSACTIONS, JSON.stringify(this._failedTransactions))]);\n } catch (err) {\n this.logger.warn('[StatusTracker] Failed to persist transactions:', err);\n }\n }\n private _hasStatusChanged(newStatus: SyncStatus): boolean {\n const old = this._state.status;\n return old.connected !== newStatus.connected || old.connecting !== newStatus.connecting || old.hasSynced !== newStatus.hasSynced || old.uploading !== newStatus.uploading || old.downloading !== newStatus.downloading || old.lastSyncedAt?.getTime() !== newStatus.lastSyncedAt?.getTime() || old.downloadProgress?.current !== newStatus.downloadProgress?.current || old.downloadProgress?.target !== newStatus.downloadProgress?.target;\n }\n\n /**\n * Notify all listeners of status changes with throttling.\n *\n * Uses a \"dirty\" flag pattern: when throttled, we schedule a timer\n * but get the CURRENT state when the timer fires, not the stale state\n * from when the timer was scheduled. This ensures rapid state changes\n * during the throttle window aren't lost.\n */\n private _notifyListeners(forceImmediate = false): void {\n const now = Date.now();\n const timeSinceLastNotify = now - this._lastNotifyTime;\n\n // If a timer is already scheduled, don't reschedule - just let it fire\n // and it will pick up the current (latest) state at that time\n if (this._notifyTimer && !forceImmediate) {\n return; // Already scheduled, will get current state when it fires\n }\n if (this._notifyTimer) {\n clearTimeout(this._notifyTimer);\n this._notifyTimer = null;\n }\n const notify = () => {\n this._notifyTimer = null;\n this._lastNotifyTime = Date.now();\n // Get CURRENT state at notification time, not stale state\n const status = this.getStatus();\n\n // Call the main callback\n this.onStatusChange?.(status);\n\n // Call all listeners\n for (const listener of this._listeners) {\n try {\n listener(status);\n } catch (err) {\n this.logger.warn('[StatusTracker] Listener error:', err);\n }\n }\n };\n if (forceImmediate || timeSinceLastNotify >= this.notifyThrottleMs) {\n notify();\n } else {\n const delayMs = this.notifyThrottleMs - timeSinceLastNotify;\n this._notifyTimer = setTimeout(notify, delayMs);\n }\n }\n private _notifyFailureListeners(): void {\n const failures = this.getFailedTransactions();\n for (const listener of this._failureListeners) {\n try {\n listener(failures);\n } catch (err) {\n this.logger.warn('[StatusTracker] Failure listener error:', err);\n }\n }\n }\n private _notifyCompletedListeners(): void {\n const completed = this.getCompletedTransactions();\n for (const listener of this._completedListeners) {\n try {\n listener(completed);\n } catch (err) {\n this.logger.warn('[StatusTracker] Completed listener error:', err);\n }\n }\n }\n\n /**\n * Remap a CrudEntry from persisted JSON (handles toJSON() property remapping).\n * PowerSync's CrudEntry.toJSON() remaps: opData→data, table→type, clientId→op_id, transactionId→tx_id\n *\n * @returns The remapped CrudEntry, or null if critical fields (table, id) are missing\n */\n private remapEntry(entry: any): CrudEntry | null {\n const table = entry.table ?? entry.type;\n const id = entry.id;\n\n // Validate critical fields\n if (!table || typeof table !== 'string') {\n this.logger.warn('[StatusTracker] Invalid CrudEntry: missing or invalid table field', entry);\n return null;\n }\n if (!id || typeof id !== 'string') {\n this.logger.warn('[StatusTracker] Invalid CrudEntry: missing or invalid id field', entry);\n return null;\n }\n return {\n id,\n clientId: entry.clientId ?? entry.op_id ?? 0,\n op: entry.op,\n table,\n opData: entry.opData ?? entry.data,\n transactionId: entry.transactionId ?? entry.tx_id\n };\n }\n\n /**\n * Normalize CrudEntry array to plain objects to avoid CrudEntry.toJSON() remapping issues.\n * PowerSync's CrudEntry.toJSON() remaps property names which breaks deserialization.\n */\n private normalizeEntries(entries: CrudEntry[]): CrudEntry[] {\n return entries.map(e => ({\n id: e.id,\n clientId: e.clientId,\n op: e.op,\n table: e.table,\n opData: e.opData,\n transactionId: e.transactionId\n }));\n }\n}","/**\n * Sync Metrics Collector for @pol-studios/powersync\n *\n * Collects and persists sync operation metrics for monitoring and debugging.\n */\n\nimport type { SyncMetrics, SyncError, SyncErrorType } from '../core/types';\nimport type { AsyncStorageAdapter, LoggerAdapter } from '../platform/types';\nimport type { MetricsCollectorOptions, SyncOperationData, Unsubscribe } from './types';\nimport { STORAGE_KEY_METRICS } from '../core/constants';\nimport { classifyError } from '../core/errors';\nimport { DEFAULT_SYNC_METRICS } from '../provider/types';\n\n/**\n * Collects sync metrics including operation counts, timing, and errors.\n *\n * Features:\n * - Tracks sync operation success/failure rates\n * - Calculates average sync duration\n * - Monitors data transfer amounts\n * - Persists metrics to storage for continuity across sessions\n * - Records last error for debugging\n *\n * @example\n * ```typescript\n * const collector = new MetricsCollector({\n * storage,\n * logger,\n * onMetricsChange: (metrics) => updateUI(metrics),\n * });\n *\n * // Start tracking a sync\n * const startTime = Date.now();\n *\n * // On sync complete\n * collector.recordSync({\n * durationMs: Date.now() - startTime,\n * success: true,\n * operationsDownloaded: 150,\n * });\n *\n * // Get current metrics\n * const metrics = collector.getMetrics();\n * ```\n */\nexport class MetricsCollector {\n private readonly storage: AsyncStorageAdapter;\n private readonly logger: LoggerAdapter;\n private readonly storageKey: string;\n private readonly persistMetrics: boolean;\n private readonly onMetricsChange?: (metrics: SyncMetrics) => void;\n private _metrics: SyncMetrics;\n private _listeners = new Set<(metrics: SyncMetrics) => void>();\n private _initialized = false;\n\n // Track active sync for timing\n private _syncStartTime: number | null = null;\n private _wasSyncing = false;\n constructor(storage: AsyncStorageAdapter, logger: LoggerAdapter, options: MetricsCollectorOptions = {}) {\n this.storage = storage;\n this.logger = logger;\n this.storageKey = options.storageKey ?? STORAGE_KEY_METRICS;\n this.persistMetrics = options.persistMetrics ?? true;\n this.onMetricsChange = options.onMetricsChange;\n this._metrics = {\n ...DEFAULT_SYNC_METRICS\n };\n }\n\n // ─── Initialization ────────────────────────────────────────────────────────\n\n /**\n * Initialize the collector by loading persisted metrics.\n */\n async init(): Promise<void> {\n if (this._initialized) return;\n try {\n const stored = await this.storage.getItem(this.storageKey);\n if (stored) {\n const parsed = JSON.parse(stored);\n // Restore Date objects\n if (parsed.lastError?.timestamp) {\n parsed.lastError.timestamp = new Date(parsed.lastError.timestamp);\n }\n this._metrics = {\n ...DEFAULT_SYNC_METRICS,\n ...parsed\n };\n this.logger.debug('[MetricsCollector] Loaded persisted metrics');\n }\n } catch (err) {\n this.logger.warn('[MetricsCollector] Failed to load metrics:', err);\n }\n this._initialized = true;\n }\n\n /**\n * Dispose the collector.\n */\n dispose(): void {\n this._listeners.clear();\n }\n\n // ─── Getters ───────────────────────────────────────────────────────────────\n\n /**\n * Get current sync metrics.\n */\n getMetrics(): SyncMetrics {\n return {\n ...this._metrics\n };\n }\n\n // ─── Recording ─────────────────────────────────────────────────────────────\n\n /**\n * Record a completed sync operation.\n */\n async recordSync(data: SyncOperationData): Promise<void> {\n const {\n durationMs,\n success,\n operationsDownloaded,\n operationsUploaded,\n error\n } = data;\n const totalSyncs = this._metrics.totalSyncs + 1;\n const successfulSyncs = this._metrics.successfulSyncs + (success ? 1 : 0);\n const failedSyncs = this._metrics.failedSyncs + (success ? 0 : 1);\n\n // Calculate running average duration (only for successful syncs)\n let averageSyncDuration = this._metrics.averageSyncDuration;\n if (success) {\n if (averageSyncDuration !== null) {\n averageSyncDuration = (averageSyncDuration * (successfulSyncs - 1) + durationMs) / successfulSyncs;\n } else {\n averageSyncDuration = durationMs;\n }\n }\n\n // Estimate data transfer (rough approximation: ~100 bytes per operation)\n const bytesPerOp = 100;\n const downloaded = (operationsDownloaded ?? 0) * bytesPerOp;\n const uploaded = (operationsUploaded ?? 0) * bytesPerOp;\n\n // Build error record if failed\n let lastError: SyncError | null = this._metrics.lastError;\n if (!success && error) {\n const errorType = classifyError(error);\n lastError = {\n type: errorType,\n message: error.message,\n userMessage: error.message,\n // Use original message as user message for metrics\n timestamp: new Date(),\n isPermanent: false\n };\n }\n this._metrics = {\n totalSyncs,\n successfulSyncs,\n failedSyncs,\n lastSyncDuration: durationMs,\n averageSyncDuration: averageSyncDuration !== null ? Math.round(averageSyncDuration) : null,\n totalDataDownloaded: this._metrics.totalDataDownloaded + downloaded,\n totalDataUploaded: this._metrics.totalDataUploaded + uploaded,\n lastError\n };\n await this._persist();\n this._notifyListeners();\n }\n\n /**\n * Record a sync error without a full sync operation.\n */\n async recordError(error: Error): Promise<void> {\n this._metrics = {\n ...this._metrics,\n failedSyncs: this._metrics.failedSyncs + 1,\n lastError: {\n type: classifyError(error),\n message: error.message,\n userMessage: error.message,\n // Use original message as user message for metrics\n timestamp: new Date(),\n isPermanent: false\n }\n };\n await this._persist();\n this._notifyListeners();\n }\n\n /**\n * Record an upload operation.\n */\n async recordUpload(operationCount: number): Promise<void> {\n const bytesPerOp = 100;\n this._metrics = {\n ...this._metrics,\n totalDataUploaded: this._metrics.totalDataUploaded + operationCount * bytesPerOp\n };\n await this._persist();\n this._notifyListeners();\n }\n\n /**\n * Clear all metrics and start fresh.\n */\n async reset(): Promise<void> {\n this._metrics = {\n ...DEFAULT_SYNC_METRICS\n };\n await this._persist();\n this._notifyListeners();\n }\n\n // ─── Sync Timing Helpers ───────────────────────────────────────────────────\n\n /**\n * Called when sync starts (downloading becomes true).\n */\n markSyncStart(): void {\n if (!this._wasSyncing) {\n this._syncStartTime = Date.now();\n this._wasSyncing = true;\n }\n }\n\n /**\n * Called when sync ends (downloading becomes false).\n * Returns the duration if a sync was in progress.\n */\n markSyncEnd(): number | null {\n if (this._wasSyncing && this._syncStartTime !== null) {\n const duration = Date.now() - this._syncStartTime;\n this._syncStartTime = null;\n this._wasSyncing = false;\n return duration;\n }\n return null;\n }\n\n /**\n * Check if sync is currently in progress.\n */\n isSyncInProgress(): boolean {\n return this._wasSyncing;\n }\n\n // ─── Subscriptions ─────────────────────────────────────────────────────────\n\n /**\n * Subscribe to metrics changes.\n * @returns Unsubscribe function\n */\n onMetricsUpdate(listener: (metrics: SyncMetrics) => void): Unsubscribe {\n this._listeners.add(listener);\n listener(this.getMetrics());\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n // ─── Private Methods ───────────────────────────────────────────────────────\n\n private async _persist(): Promise<void> {\n if (!this.persistMetrics) return;\n try {\n await this.storage.setItem(this.storageKey, JSON.stringify(this._metrics));\n } catch (err) {\n this.logger.warn('[MetricsCollector] Failed to persist metrics:', err);\n }\n }\n private _notifyListeners(): void {\n const metrics = this.getMetrics();\n\n // Call main callback\n this.onMetricsChange?.(metrics);\n\n // Call all listeners\n for (const listener of this._listeners) {\n try {\n listener(metrics);\n } catch (err) {\n this.logger.warn('[MetricsCollector] Listener error:', err);\n }\n }\n }\n}","/**\n * Connection Health Monitor for @pol-studios/powersync\n *\n * Monitors database connection health with periodic checks and latency tracking.\n */\n\nimport type { ConnectionHealth } from '../core/types';\nimport type { AbstractPowerSyncDatabase } from '../core/types';\nimport type { LoggerAdapter } from '../platform/types';\nimport type { HealthMonitorOptions, HealthCheckResult, Unsubscribe } from './types';\nimport { HEALTH_CHECK_INTERVAL_MS, HEALTH_CHECK_TIMEOUT_MS, LATENCY_DEGRADED_THRESHOLD_MS, MAX_CONSECUTIVE_FAILURES } from '../core/constants';\nimport { DEFAULT_CONNECTION_HEALTH } from '../provider/types';\n\n/**\n * Monitors connection health with periodic checks.\n *\n * Features:\n * - Periodic health checks with configurable interval\n * - Latency measurement and degraded state detection\n * - Consecutive failure tracking\n * - Auto-recovery detection\n *\n * @example\n * ```typescript\n * const monitor = new HealthMonitor(db, logger, {\n * checkIntervalMs: 30000,\n * onHealthChange: (health) => {\n * if (health.status === 'degraded') {\n * showWarning('Connection is slow');\n * }\n * },\n * });\n *\n * // Start monitoring\n * monitor.start();\n *\n * // Get current health\n * const health = monitor.getHealth();\n *\n * // Stop when done\n * monitor.stop();\n * ```\n */\nexport class HealthMonitor {\n private readonly logger: LoggerAdapter;\n private readonly checkIntervalMs: number;\n private readonly checkTimeoutMs: number;\n private readonly degradedThresholdMs: number;\n private readonly maxConsecutiveFailures: number;\n private readonly onHealthChange?: (health: ConnectionHealth) => void;\n private _db: AbstractPowerSyncDatabase | null = null;\n private _health: ConnectionHealth;\n private _intervalId: ReturnType<typeof setInterval> | null = null;\n private _listeners = new Set<(health: ConnectionHealth) => void>();\n private _running = false;\n private _paused = false;\n private _pendingTimers = new Set<ReturnType<typeof setTimeout>>();\n constructor(logger: LoggerAdapter, options: HealthMonitorOptions = {}) {\n this.logger = logger;\n this.checkIntervalMs = options.checkIntervalMs ?? HEALTH_CHECK_INTERVAL_MS;\n this.checkTimeoutMs = options.checkTimeoutMs ?? HEALTH_CHECK_TIMEOUT_MS;\n this.degradedThresholdMs = options.degradedThresholdMs ?? LATENCY_DEGRADED_THRESHOLD_MS;\n this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? MAX_CONSECUTIVE_FAILURES;\n this.onHealthChange = options.onHealthChange;\n this._health = {\n ...DEFAULT_CONNECTION_HEALTH\n };\n }\n\n // ─── Lifecycle ─────────────────────────────────────────────────────────────\n\n /**\n * Set the database instance to monitor.\n */\n setDatabase(db: AbstractPowerSyncDatabase | null): void {\n this._db = db;\n if (!db) {\n // Reset health when database is cleared\n this._updateHealth({\n status: 'disconnected',\n latency: null,\n lastHealthCheck: new Date(),\n consecutiveFailures: 0,\n reconnectAttempts: this._health.reconnectAttempts\n });\n }\n }\n\n /**\n * Start the health monitor.\n */\n start(): void {\n if (this._running) return;\n this.logger.info('[HealthMonitor] Starting');\n this._running = true;\n\n // Perform initial check\n this._checkHealth().catch(err => {\n this.logger.warn('[HealthMonitor] Initial check error:', err);\n });\n\n // Set up periodic checks\n this._intervalId = setInterval(() => {\n if (!this._paused) {\n this._checkHealth().catch(err => {\n this.logger.warn('[HealthMonitor] Periodic check error:', err);\n });\n }\n }, this.checkIntervalMs);\n }\n\n /**\n * Stop the health monitor.\n */\n stop(): void {\n if (!this._running) return;\n this.logger.info('[HealthMonitor] Stopping');\n this._running = false;\n if (this._intervalId) {\n clearInterval(this._intervalId);\n this._intervalId = null;\n }\n }\n\n /**\n * Pause health checks temporarily.\n */\n pause(): void {\n this._paused = true;\n // Clear any pending timeout timers to prevent leaks\n this._pendingTimers.forEach(clearTimeout);\n this._pendingTimers.clear();\n this._updateHealth({\n ...this._health,\n status: 'disconnected'\n });\n }\n\n /**\n * Resume health checks.\n */\n resume(): void {\n this._paused = false;\n // Perform immediate check on resume\n this._checkHealth().catch(err => {\n this.logger.warn('[HealthMonitor] Resume check error:', err);\n });\n }\n\n /**\n * Dispose the monitor and clear all resources.\n */\n dispose(): void {\n this.stop();\n // Clear any pending timeout timers to prevent leaks\n this._pendingTimers.forEach(clearTimeout);\n this._pendingTimers.clear();\n this._listeners.clear();\n }\n\n // ─── Getters ───────────────────────────────────────────────────────────────\n\n /**\n * Get current connection health.\n */\n getHealth(): ConnectionHealth {\n return {\n ...this._health\n };\n }\n\n /**\n * Check if the monitor is running.\n */\n isRunning(): boolean {\n return this._running;\n }\n\n // ─── Manual Checks ─────────────────────────────────────────────────────────\n\n /**\n * Perform an immediate health check.\n * @returns The result of the health check\n */\n async checkNow(): Promise<HealthCheckResult> {\n return this._checkHealth();\n }\n\n /**\n * Record a reconnection attempt.\n */\n recordReconnectAttempt(): void {\n this._updateHealth({\n ...this._health,\n reconnectAttempts: this._health.reconnectAttempts + 1\n });\n }\n\n /**\n * Reset reconnection attempts counter (call on successful connection).\n */\n resetReconnectAttempts(): void {\n if (this._health.reconnectAttempts > 0) {\n this._updateHealth({\n ...this._health,\n reconnectAttempts: 0\n });\n }\n }\n\n // ─── Subscriptions ─────────────────────────────────────────────────────────\n\n /**\n * Subscribe to health changes.\n * @returns Unsubscribe function\n */\n onHealthUpdate(listener: (health: ConnectionHealth) => void): Unsubscribe {\n this._listeners.add(listener);\n listener(this.getHealth());\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n // ─── Private Methods ───────────────────────────────────────────────────────\n\n private async _checkHealth(): Promise<HealthCheckResult> {\n if (!this._db || this._paused) {\n return {\n success: false,\n error: new Error('Database not available or paused'),\n timestamp: new Date()\n };\n }\n const startTime = Date.now();\n const timestamp = new Date();\n try {\n // Execute a simple query with timeout\n await this._withTimeout(this._db.get('SELECT 1'), this.checkTimeoutMs);\n const latencyMs = Date.now() - startTime;\n\n // Determine status based on latency\n const status: ConnectionHealth['status'] = latencyMs < this.degradedThresholdMs ? 'healthy' : 'degraded';\n this._updateHealth({\n status,\n latency: latencyMs,\n lastHealthCheck: timestamp,\n consecutiveFailures: 0,\n reconnectAttempts: this._health.reconnectAttempts\n });\n return {\n success: true,\n latencyMs,\n timestamp\n };\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n this.logger.warn('[HealthMonitor] Health check failed:', error.message);\n const consecutiveFailures = this._health.consecutiveFailures + 1;\n const status: ConnectionHealth['status'] = consecutiveFailures >= this.maxConsecutiveFailures ? 'disconnected' : 'degraded';\n this._updateHealth({\n status,\n latency: null,\n lastHealthCheck: timestamp,\n consecutiveFailures,\n reconnectAttempts: this._health.reconnectAttempts\n });\n return {\n success: false,\n error,\n timestamp\n };\n }\n }\n private _updateHealth(health: ConnectionHealth): void {\n const changed = this._hasHealthChanged(health);\n this._health = health;\n if (changed) {\n this._notifyListeners();\n }\n }\n private _hasHealthChanged(newHealth: ConnectionHealth): boolean {\n const old = this._health;\n return old.status !== newHealth.status || old.latency !== newHealth.latency || old.consecutiveFailures !== newHealth.consecutiveFailures || old.reconnectAttempts !== newHealth.reconnectAttempts;\n }\n private _notifyListeners(): void {\n const health = this.getHealth();\n\n // Call main callback\n this.onHealthChange?.(health);\n\n // Call all listeners\n for (const listener of this._listeners) {\n try {\n listener(health);\n } catch (err) {\n this.logger.warn('[HealthMonitor] Listener error:', err);\n }\n }\n }\n private _withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n this._pendingTimers.delete(timer);\n reject(new Error(`Health check timeout after ${timeoutMs}ms`));\n }, timeoutMs);\n this._pendingTimers.add(timer);\n promise.then(result => {\n this._pendingTimers.delete(timer);\n clearTimeout(timer);\n resolve(result);\n }, error => {\n this._pendingTimers.delete(timer);\n clearTimeout(timer);\n reject(error);\n });\n });\n }\n}"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,76 @@
|
|
|
1
|
+
// src/connector/supabase-auth.ts
|
|
2
|
+
function createSupabaseAuth(supabase, options) {
|
|
3
|
+
const {
|
|
4
|
+
onRefreshError
|
|
5
|
+
} = options ?? {};
|
|
6
|
+
return {
|
|
7
|
+
async getSession() {
|
|
8
|
+
const {
|
|
9
|
+
data,
|
|
10
|
+
error
|
|
11
|
+
} = await supabase.auth.getSession();
|
|
12
|
+
if (error) {
|
|
13
|
+
console.warn("[SupabaseAuth] getSession error:", error.message);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const session = data.session;
|
|
17
|
+
if (!session) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return mapSupabaseSession(session);
|
|
21
|
+
},
|
|
22
|
+
async refreshSession() {
|
|
23
|
+
const {
|
|
24
|
+
data,
|
|
25
|
+
error
|
|
26
|
+
} = await supabase.auth.refreshSession();
|
|
27
|
+
if (error) {
|
|
28
|
+
onRefreshError?.(error);
|
|
29
|
+
throw new AuthRefreshError(error.message, {
|
|
30
|
+
cause: error
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const session = data.session;
|
|
34
|
+
if (!session) {
|
|
35
|
+
const noSessionError = new AuthRefreshError("Session refresh succeeded but no session returned");
|
|
36
|
+
onRefreshError?.(noSessionError);
|
|
37
|
+
throw noSessionError;
|
|
38
|
+
}
|
|
39
|
+
return mapSupabaseSession(session);
|
|
40
|
+
},
|
|
41
|
+
onAuthStateChange(cb) {
|
|
42
|
+
const {
|
|
43
|
+
data
|
|
44
|
+
} = supabase.auth.onAuthStateChange((_event, session) => {
|
|
45
|
+
cb(session ? mapSupabaseSession(session) : null);
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
data.subscription.unsubscribe();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
var AuthRefreshError = class _AuthRefreshError extends Error {
|
|
54
|
+
name = "AuthRefreshError";
|
|
55
|
+
cause;
|
|
56
|
+
constructor(message, options) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.cause = options?.cause;
|
|
59
|
+
if (Error.captureStackTrace) {
|
|
60
|
+
Error.captureStackTrace(this, _AuthRefreshError);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function mapSupabaseSession(supabaseSession) {
|
|
65
|
+
return {
|
|
66
|
+
accessToken: supabaseSession.access_token,
|
|
67
|
+
expiresAt: supabaseSession.expires_at ? new Date(supabaseSession.expires_at * 1e3) : void 0,
|
|
68
|
+
user: supabaseSession.user ? {
|
|
69
|
+
id: supabaseSession.user.id
|
|
70
|
+
} : void 0
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
1
74
|
// src/connector/circuit-breaker.ts
|
|
2
75
|
var DEFAULT_CIRCUIT_BREAKER_CONFIG = {
|
|
3
76
|
failureThreshold: 5,
|
|
@@ -197,8 +270,10 @@ var CircuitBreaker = class {
|
|
|
197
270
|
};
|
|
198
271
|
|
|
199
272
|
export {
|
|
273
|
+
createSupabaseAuth,
|
|
274
|
+
AuthRefreshError,
|
|
200
275
|
DEFAULT_CIRCUIT_BREAKER_CONFIG,
|
|
201
276
|
CircuitOpenError,
|
|
202
277
|
CircuitBreaker
|
|
203
278
|
};
|
|
204
|
-
//# sourceMappingURL=chunk-
|
|
279
|
+
//# sourceMappingURL=chunk-HWSNV45P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/connector/supabase-auth.ts","../src/connector/circuit-breaker.ts"],"sourcesContent":["/**\n * Supabase Auth Provider for @pol-studios/powersync\n *\n * Factory function that wraps a Supabase client's auth methods\n * to implement the AuthProvider interface.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AuthProvider, Session } from './types';\n\n/**\n * Options for createSupabaseAuth\n */\nexport interface SupabaseAuthOptions {\n /**\n * Called when a session refresh fails.\n * Use this to trigger logout flows or show error UI.\n */\n onRefreshError?: (error: Error) => void;\n}\n\n/**\n * Creates an AuthProvider that wraps Supabase auth.\n *\n * @param supabase - Supabase client instance\n * @param options - Optional configuration\n * @returns AuthProvider implementation\n *\n * @example\n * ```typescript\n * import { createSupabaseAuth } from '@pol-studios/powersync';\n *\n * const auth = createSupabaseAuth(supabaseClient, {\n * onRefreshError: (error) => {\n * console.error('Session refresh failed:', error);\n * // Navigate to login screen\n * },\n * });\n *\n * // Use with PowerSyncProvider\n * <PowerSyncProvider auth={auth} ... />\n * ```\n */\nexport function createSupabaseAuth(supabase: SupabaseClient, options?: SupabaseAuthOptions): AuthProvider {\n const {\n onRefreshError\n } = options ?? {};\n return {\n async getSession(): Promise<Session | null> {\n const {\n data,\n error\n } = await supabase.auth.getSession();\n if (error) {\n // Log but don't throw - treat as \"not authenticated\"\n console.warn('[SupabaseAuth] getSession error:', error.message);\n return null;\n }\n const session = data.session;\n if (!session) {\n return null;\n }\n return mapSupabaseSession(session);\n },\n async refreshSession(): Promise<Session> {\n const {\n data,\n error\n } = await supabase.auth.refreshSession();\n if (error) {\n onRefreshError?.(error);\n throw new AuthRefreshError(error.message, {\n cause: error\n });\n }\n const session = data.session;\n if (!session) {\n const noSessionError = new AuthRefreshError('Session refresh succeeded but no session returned');\n onRefreshError?.(noSessionError);\n throw noSessionError;\n }\n return mapSupabaseSession(session);\n },\n onAuthStateChange(cb: (session: Session | null) => void): () => void {\n const {\n data\n } = supabase.auth.onAuthStateChange((_event, session) => {\n cb(session ? mapSupabaseSession(session) : null);\n });\n return () => {\n data.subscription.unsubscribe();\n };\n }\n };\n}\n\n/**\n * Error thrown when session refresh fails.\n */\nexport class AuthRefreshError extends Error {\n readonly name = 'AuthRefreshError';\n readonly cause?: Error;\n constructor(message: string, options?: {\n cause?: Error;\n }) {\n super(message);\n this.cause = options?.cause;\n // Maintains proper stack trace in V8 environments\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, AuthRefreshError);\n }\n }\n}\n\n/**\n * Maps a Supabase session to our Session interface.\n */\nfunction mapSupabaseSession(supabaseSession: {\n access_token: string;\n expires_at?: number;\n user?: {\n id: string;\n };\n}): Session {\n return {\n accessToken: supabaseSession.access_token,\n expiresAt: supabaseSession.expires_at ? new Date(supabaseSession.expires_at * 1000) : undefined,\n user: supabaseSession.user ? {\n id: supabaseSession.user.id\n } : undefined\n };\n}","/**\n * Circuit Breaker Pattern for PowerSync Connector\n *\n * Prevents cascading failures by temporarily stopping requests when\n * a service is experiencing high failure rates.\n *\n * States:\n * - CLOSED: Normal operation, requests flow through\n * - OPEN: Service is failing, requests are blocked\n * - HALF_OPEN: Testing if service has recovered\n *\n * Transitions:\n * - CLOSED -> OPEN: After failureThreshold failures within failureWindowMs\n * - OPEN -> HALF_OPEN: After timeout (exponential backoff)\n * - HALF_OPEN -> CLOSED: On successful request\n * - HALF_OPEN -> OPEN: On failed request (with reset backoff multiplier)\n */\n\n/**\n * Circuit breaker states\n */\nexport type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';\n\n/**\n * Configuration for the circuit breaker\n */\nexport interface CircuitBreakerConfig {\n /**\n * Number of failures required to trip the circuit\n * @default 5\n */\n failureThreshold: number;\n\n /**\n * Time window in ms for counting failures\n * @default 60000 (60 seconds)\n */\n failureWindowMs: number;\n\n /**\n * Initial delay before transitioning from OPEN to HALF_OPEN\n * @default 1000 (1 second)\n */\n initialRecoveryDelayMs: number;\n\n /**\n * Maximum delay for exponential backoff\n * @default 32000 (32 seconds)\n */\n maxRecoveryDelayMs: number;\n\n /**\n * Multiplier for exponential backoff\n * @default 2\n */\n backoffMultiplier: number;\n\n /**\n * Optional callback when state changes\n */\n onStateChange?: (state: CircuitState, previousState: CircuitState) => void;\n\n /**\n * Optional logger\n */\n logger?: {\n debug: (...args: unknown[]) => void;\n info: (...args: unknown[]) => void;\n warn: (...args: unknown[]) => void;\n error: (...args: unknown[]) => void;\n };\n}\n\n/**\n * Default circuit breaker configuration\n */\nexport const DEFAULT_CIRCUIT_BREAKER_CONFIG: CircuitBreakerConfig = {\n failureThreshold: 5,\n failureWindowMs: 60000,\n initialRecoveryDelayMs: 1000,\n maxRecoveryDelayMs: 32000,\n backoffMultiplier: 2\n};\n\n/**\n * Statistics about the circuit breaker state\n */\nexport interface CircuitBreakerStats {\n /** Current state */\n state: CircuitState;\n /** Number of failures in current window */\n failureCount: number;\n /** Number of successful requests since last state change */\n successCount: number;\n /** Number of times circuit has opened */\n openCount: number;\n /** Time when circuit last opened */\n lastOpenedAt: Date | null;\n /** Time when circuit last closed */\n lastClosedAt: Date | null;\n /** Current recovery delay (when OPEN) */\n currentRecoveryDelayMs: number;\n /** Time until next HALF_OPEN attempt (when OPEN) */\n timeUntilHalfOpenMs: number | null;\n}\n\n/**\n * Error thrown when circuit is open\n */\nexport class CircuitOpenError extends Error {\n constructor(public readonly timeUntilRetryMs: number, public readonly stats: CircuitBreakerStats) {\n super(`Circuit breaker is OPEN. Service is unavailable. ` + `Retry in ${Math.ceil(timeUntilRetryMs / 1000)}s after ${stats.openCount} failures.`);\n this.name = 'CircuitOpenError';\n }\n}\n\n/**\n * Circuit Breaker implementation\n *\n * @example\n * ```typescript\n * const breaker = new CircuitBreaker({\n * failureThreshold: 5,\n * failureWindowMs: 60000,\n * onStateChange: (state) => console.log('Circuit state:', state),\n * });\n *\n * // Wrap your async operations\n * try {\n * const result = await breaker.execute(() => fetchData());\n * } catch (error) {\n * if (error instanceof CircuitOpenError) {\n * console.log('Service unavailable, retry later');\n * }\n * }\n * ```\n */\nexport class CircuitBreaker {\n private state: CircuitState = 'CLOSED';\n private failures: number[] = []; // Timestamps of failures within window\n private successCount = 0;\n private openCount = 0;\n private lastOpenedAt: Date | null = null;\n private lastClosedAt: Date | null = null;\n private currentRecoveryDelay: number;\n private halfOpenTimer: ReturnType<typeof setTimeout> | null = null;\n private halfOpenScheduledAt: number | null = null;\n private readonly config: Required<Pick<CircuitBreakerConfig, 'failureThreshold' | 'failureWindowMs' | 'initialRecoveryDelayMs' | 'maxRecoveryDelayMs' | 'backoffMultiplier'>> & Pick<CircuitBreakerConfig, 'onStateChange' | 'logger'>;\n constructor(config: Partial<CircuitBreakerConfig> = {}) {\n this.config = {\n ...DEFAULT_CIRCUIT_BREAKER_CONFIG,\n ...config\n };\n this.currentRecoveryDelay = this.config.initialRecoveryDelayMs;\n }\n\n /**\n * Get current state\n */\n getState(): CircuitState {\n return this.state;\n }\n\n /**\n * Get circuit breaker statistics\n */\n getStats(): CircuitBreakerStats {\n return {\n state: this.state,\n failureCount: this.failures.length,\n successCount: this.successCount,\n openCount: this.openCount,\n lastOpenedAt: this.lastOpenedAt,\n lastClosedAt: this.lastClosedAt,\n currentRecoveryDelayMs: this.currentRecoveryDelay,\n timeUntilHalfOpenMs: this.getTimeUntilHalfOpen()\n };\n }\n\n /**\n * Check if requests are allowed through the circuit\n */\n isAllowed(): boolean {\n this.cleanupOldFailures();\n switch (this.state) {\n case 'CLOSED':\n return true;\n case 'HALF_OPEN':\n return true;\n // Allow test request\n case 'OPEN':\n return false;\n }\n }\n\n /**\n * Check if request can proceed, throws if not\n */\n checkAllowed(): void {\n if (!this.isAllowed()) {\n throw new CircuitOpenError(this.getTimeUntilHalfOpen() ?? 0, this.getStats());\n }\n }\n\n /**\n * Record a successful operation\n */\n recordSuccess(): void {\n this.successCount++;\n if (this.state === 'HALF_OPEN') {\n // Success in HALF_OPEN means service is recovered\n this.transition('CLOSED');\n this.currentRecoveryDelay = this.config.initialRecoveryDelayMs;\n this.failures = [];\n }\n this.log('debug', '[CircuitBreaker] Success recorded, state:', this.state);\n }\n\n /**\n * Record a failed operation\n */\n recordFailure(): void {\n const now = Date.now();\n this.failures.push(now);\n this.cleanupOldFailures();\n if (this.state === 'HALF_OPEN') {\n // Failure in HALF_OPEN means service is still down\n this.transition('OPEN');\n this.scheduleHalfOpen();\n return;\n }\n if (this.state === 'CLOSED') {\n // Check if threshold exceeded\n if (this.failures.length >= this.config.failureThreshold) {\n this.log('warn', `[CircuitBreaker] Failure threshold (${this.config.failureThreshold}) ` + `exceeded in ${this.config.failureWindowMs}ms window. Opening circuit.`);\n this.transition('OPEN');\n this.scheduleHalfOpen();\n }\n }\n }\n\n /**\n * Execute an async operation through the circuit breaker\n */\n async execute<T>(operation: () => Promise<T>): Promise<T> {\n this.checkAllowed();\n try {\n const result = await operation();\n this.recordSuccess();\n return result;\n } catch (error) {\n this.recordFailure();\n throw error;\n }\n }\n\n /**\n * Manually reset the circuit breaker to CLOSED state\n */\n reset(): void {\n this.clearHalfOpenTimer();\n this.failures = [];\n this.successCount = 0;\n this.currentRecoveryDelay = this.config.initialRecoveryDelayMs;\n this.transition('CLOSED');\n }\n\n /**\n * Force the circuit to OPEN state (e.g., for manual intervention)\n */\n forceOpen(): void {\n this.clearHalfOpenTimer();\n this.transition('OPEN');\n this.scheduleHalfOpen();\n }\n\n /**\n * Dispose the circuit breaker (clear timers)\n */\n dispose(): void {\n this.clearHalfOpenTimer();\n }\n\n // ─── Private Methods ─────────────────────────────────────────────────────────\n\n private transition(newState: CircuitState): void {\n if (this.state === newState) return;\n const previousState = this.state;\n this.state = newState;\n if (newState === 'OPEN') {\n this.openCount++;\n this.lastOpenedAt = new Date();\n // Exponential backoff for repeated failures\n this.currentRecoveryDelay = Math.min(this.currentRecoveryDelay * this.config.backoffMultiplier, this.config.maxRecoveryDelayMs);\n } else if (newState === 'CLOSED') {\n this.lastClosedAt = new Date();\n this.successCount = 0;\n }\n this.log('info', `[CircuitBreaker] State transition: ${previousState} -> ${newState}`);\n this.config.onStateChange?.(newState, previousState);\n }\n private scheduleHalfOpen(): void {\n this.clearHalfOpenTimer();\n this.halfOpenScheduledAt = Date.now() + this.currentRecoveryDelay;\n this.log('debug', `[CircuitBreaker] Scheduling HALF_OPEN in ${this.currentRecoveryDelay}ms`);\n this.halfOpenTimer = setTimeout(() => {\n this.halfOpenTimer = null;\n this.halfOpenScheduledAt = null;\n this.transition('HALF_OPEN');\n this.log('info', '[CircuitBreaker] Circuit is now HALF_OPEN, allowing test request');\n }, this.currentRecoveryDelay);\n }\n private clearHalfOpenTimer(): void {\n if (this.halfOpenTimer) {\n clearTimeout(this.halfOpenTimer);\n this.halfOpenTimer = null;\n this.halfOpenScheduledAt = null;\n }\n }\n private getTimeUntilHalfOpen(): number | null {\n if (this.state !== 'OPEN' || !this.halfOpenScheduledAt) {\n return null;\n }\n return Math.max(0, this.halfOpenScheduledAt - Date.now());\n }\n private cleanupOldFailures(): void {\n const cutoff = Date.now() - this.config.failureWindowMs;\n this.failures = this.failures.filter(timestamp => timestamp > cutoff);\n }\n private log(level: 'debug' | 'info' | 'warn' | 'error', ...args: unknown[]): void {\n this.config.logger?.[level]?.(...args);\n }\n}"],"mappings":"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","names":[]}
|
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
withExponentialBackoff
|
|
3
3
|
} from "./chunk-FV2HXEIY.js";
|
|
4
4
|
import {
|
|
5
|
-
classifySupabaseError
|
|
6
|
-
|
|
5
|
+
classifySupabaseError,
|
|
6
|
+
isRlsError
|
|
7
|
+
} from "./chunk-VACPAAQZ.js";
|
|
7
8
|
|
|
8
9
|
// src/connector/types.ts
|
|
9
10
|
var defaultSchemaRouter = () => "public";
|
|
@@ -26,6 +27,16 @@ var DEFAULT_RETRY_CONFIG = {
|
|
|
26
27
|
// Irrelevant with 0 retries
|
|
27
28
|
backoffMultiplier: 1
|
|
28
29
|
// Irrelevant with 0 retries
|
|
30
|
+
},
|
|
31
|
+
rls: {
|
|
32
|
+
maxRetries: 5,
|
|
33
|
+
// Extended retries for RLS errors
|
|
34
|
+
baseDelayMs: 3e4,
|
|
35
|
+
// 30 seconds initial delay
|
|
36
|
+
maxDelayMs: 12e4,
|
|
37
|
+
// 2 minutes max delay
|
|
38
|
+
backoffMultiplier: 2
|
|
39
|
+
// 30s → 60s → 120s → 120s → 120s
|
|
29
40
|
}
|
|
30
41
|
};
|
|
31
42
|
|
|
@@ -223,7 +234,7 @@ function groupEntriesByTable(entries) {
|
|
|
223
234
|
}
|
|
224
235
|
return grouped;
|
|
225
236
|
}
|
|
226
|
-
var SupabaseConnector = class {
|
|
237
|
+
var SupabaseConnector = class _SupabaseConnector {
|
|
227
238
|
supabase;
|
|
228
239
|
powerSyncUrl;
|
|
229
240
|
schemaRouter;
|
|
@@ -252,7 +263,18 @@ var SupabaseConnector = class {
|
|
|
252
263
|
isDestroyed = false;
|
|
253
264
|
// Retry configuration
|
|
254
265
|
retryConfig;
|
|
266
|
+
// Track completion failures for circuit breaker logic
|
|
267
|
+
// Maps transaction fingerprint (hash of entry IDs) to failure tracking info
|
|
268
|
+
completionFailures = /* @__PURE__ */ new Map();
|
|
269
|
+
static COMPLETION_MAX_FAILURES = 3;
|
|
270
|
+
static COMPLETION_EXTENDED_TIMEOUT_MS = 6e4;
|
|
271
|
+
// 60s timeout for retry
|
|
255
272
|
autoRetryPaused = false;
|
|
273
|
+
// Per-entry cooldown tracking for failed operations
|
|
274
|
+
// Maps entry key (table:id) to the timestamp when it can be retried
|
|
275
|
+
entryCooldowns = /* @__PURE__ */ new Map();
|
|
276
|
+
static COOLDOWN_DURATION_MS = 6e4;
|
|
277
|
+
// 1 minute cooldown after exhausting retries
|
|
256
278
|
constructor(options) {
|
|
257
279
|
this.supabase = options.supabaseClient;
|
|
258
280
|
this.powerSyncUrl = options.powerSyncUrl;
|
|
@@ -274,6 +296,10 @@ var SupabaseConnector = class {
|
|
|
274
296
|
permanent: {
|
|
275
297
|
...DEFAULT_RETRY_CONFIG.permanent,
|
|
276
298
|
...options.retryConfig?.permanent
|
|
299
|
+
},
|
|
300
|
+
rls: {
|
|
301
|
+
...DEFAULT_RETRY_CONFIG.rls,
|
|
302
|
+
...options.retryConfig?.rls
|
|
277
303
|
}
|
|
278
304
|
};
|
|
279
305
|
if (this.conflictBus) {
|
|
@@ -303,6 +329,15 @@ var SupabaseConnector = class {
|
|
|
303
329
|
}
|
|
304
330
|
this.resolvedConflicts.clear();
|
|
305
331
|
this.versionColumnPromises.clear();
|
|
332
|
+
this.completionFailures.clear();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Generate a fingerprint for a set of entries to track completion failures.
|
|
336
|
+
* Uses sorted entry IDs to create a consistent fingerprint regardless of order.
|
|
337
|
+
*/
|
|
338
|
+
generateTransactionFingerprint(entries) {
|
|
339
|
+
const sortedIds = entries.map((e) => `${e.table}:${e.id}`).sort();
|
|
340
|
+
return sortedIds.join("|");
|
|
306
341
|
}
|
|
307
342
|
// ─── Retry Control Methods ─────────────────────────────────────────────────
|
|
308
343
|
/**
|
|
@@ -342,6 +377,19 @@ var SupabaseConnector = class {
|
|
|
342
377
|
if (this.isDestroyed) {
|
|
343
378
|
throw new Error("Connector destroyed");
|
|
344
379
|
}
|
|
380
|
+
const entryKey = `${entry.table}:${entry.id}`;
|
|
381
|
+
const cooldownUntil = this.entryCooldowns.get(entryKey);
|
|
382
|
+
if (cooldownUntil && Date.now() < cooldownUntil) {
|
|
383
|
+
const remainingMs = cooldownUntil - Date.now();
|
|
384
|
+
if (__DEV__) {
|
|
385
|
+
console.log("[Connector] Entry in cooldown, skipping:", {
|
|
386
|
+
table: entry.table,
|
|
387
|
+
id: entry.id,
|
|
388
|
+
remainingSec: Math.round(remainingMs / 1e3)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
throw new Error(`Entry in cooldown for ${Math.round(remainingMs / 1e3)}s`);
|
|
392
|
+
}
|
|
345
393
|
const classified = {
|
|
346
394
|
isPermanent: false,
|
|
347
395
|
pgCode: void 0,
|
|
@@ -350,6 +398,7 @@ var SupabaseConnector = class {
|
|
|
350
398
|
let lastError;
|
|
351
399
|
try {
|
|
352
400
|
await this.processCrudEntry(entry);
|
|
401
|
+
this.entryCooldowns.delete(entryKey);
|
|
353
402
|
return;
|
|
354
403
|
} catch (error) {
|
|
355
404
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -368,6 +417,7 @@ var SupabaseConnector = class {
|
|
|
368
417
|
try {
|
|
369
418
|
await this.supabase.auth.refreshSession();
|
|
370
419
|
await this.processCrudEntry(entry);
|
|
420
|
+
this.entryCooldowns.delete(entryKey);
|
|
371
421
|
return;
|
|
372
422
|
} catch (retryError) {
|
|
373
423
|
lastError = retryError instanceof Error ? retryError : new Error(String(retryError));
|
|
@@ -395,7 +445,18 @@ var SupabaseConnector = class {
|
|
|
395
445
|
isPermanent: classified.isPermanent
|
|
396
446
|
});
|
|
397
447
|
}
|
|
398
|
-
const
|
|
448
|
+
const isRlsPermissionError = lastError && isRlsError(lastError);
|
|
449
|
+
const selectedConfig = isRlsPermissionError ? this.retryConfig.rls : classified.isPermanent ? this.retryConfig.permanent : this.retryConfig.transient;
|
|
450
|
+
if (__DEV__ && isRlsPermissionError) {
|
|
451
|
+
console.log("[Connector] RLS/permission error detected, using extended retry config:", {
|
|
452
|
+
table: entry.table,
|
|
453
|
+
op: entry.op,
|
|
454
|
+
id: entry.id,
|
|
455
|
+
maxRetries: selectedConfig.maxRetries,
|
|
456
|
+
baseDelayMs: selectedConfig.baseDelayMs,
|
|
457
|
+
maxDelayMs: selectedConfig.maxDelayMs
|
|
458
|
+
});
|
|
459
|
+
}
|
|
399
460
|
try {
|
|
400
461
|
await withExponentialBackoff(async () => {
|
|
401
462
|
await this.processCrudEntry(entry);
|
|
@@ -422,6 +483,7 @@ var SupabaseConnector = class {
|
|
|
422
483
|
}
|
|
423
484
|
}
|
|
424
485
|
});
|
|
486
|
+
this.entryCooldowns.delete(entryKey);
|
|
425
487
|
} catch (error) {
|
|
426
488
|
const finalError = error instanceof Error ? error : new Error(String(error));
|
|
427
489
|
const category = classified.isPermanent ? "permanent" : classified.pgCode ? "transient" : "unknown";
|
|
@@ -441,6 +503,15 @@ var SupabaseConnector = class {
|
|
|
441
503
|
error: finalError.message,
|
|
442
504
|
category
|
|
443
505
|
});
|
|
506
|
+
const cooldownMs = _SupabaseConnector.COOLDOWN_DURATION_MS;
|
|
507
|
+
this.entryCooldowns.set(entryKey, Date.now() + cooldownMs);
|
|
508
|
+
if (__DEV__) {
|
|
509
|
+
console.log("[Connector] Entry placed in cooldown:", {
|
|
510
|
+
table: entry.table,
|
|
511
|
+
id: entry.id,
|
|
512
|
+
cooldownSec: cooldownMs / 1e3
|
|
513
|
+
});
|
|
514
|
+
}
|
|
444
515
|
throw finalError;
|
|
445
516
|
}
|
|
446
517
|
}
|
|
@@ -712,6 +783,11 @@ var SupabaseConnector = class {
|
|
|
712
783
|
* Finalize a transaction by completing it after all entries processed successfully.
|
|
713
784
|
* Extracted to eliminate duplication between uploadData and processTransaction.
|
|
714
785
|
*
|
|
786
|
+
* Implements circuit breaker logic for completion failures:
|
|
787
|
+
* - On first failure: retry once with extended timeout (60s)
|
|
788
|
+
* - After 3 failures for same entries: log and return without throwing
|
|
789
|
+
* (data is safely in Supabase via idempotent upserts, preventing infinite retry loop)
|
|
790
|
+
*
|
|
715
791
|
* @param context - The finalization context containing results and transaction
|
|
716
792
|
*/
|
|
717
793
|
async finalizeTransaction(context) {
|
|
@@ -724,6 +800,25 @@ var SupabaseConnector = class {
|
|
|
724
800
|
if (__DEV__) {
|
|
725
801
|
console.log("[Connector] All CRUD entries processed, completing transaction...");
|
|
726
802
|
}
|
|
803
|
+
const allEntries = [...successfulEntries, ...discardedEntries];
|
|
804
|
+
const fingerprint = this.generateTransactionFingerprint(allEntries);
|
|
805
|
+
const failureInfo = this.completionFailures.get(fingerprint);
|
|
806
|
+
const currentFailureCount = failureInfo?.count ?? 0;
|
|
807
|
+
if (currentFailureCount >= _SupabaseConnector.COMPLETION_MAX_FAILURES) {
|
|
808
|
+
this.logger?.warn("[Connector] Circuit breaker triggered - completion failed too many times, bypassing throw:", {
|
|
809
|
+
fingerprint: fingerprint.substring(0, 50) + "...",
|
|
810
|
+
failureCount: currentFailureCount,
|
|
811
|
+
entriesCount: allEntries.length,
|
|
812
|
+
message: "Data is safely in Supabase. Returning without throw to prevent infinite retry loop."
|
|
813
|
+
});
|
|
814
|
+
if (__DEV__) {
|
|
815
|
+
console.warn("[Connector] CIRCUIT BREAKER: Completion failed", currentFailureCount, "times. Data is in Supabase - returning without throw to break retry loop.");
|
|
816
|
+
}
|
|
817
|
+
this.completionFailures.delete(fingerprint);
|
|
818
|
+
this.onTransactionSuccess?.(successfulEntries);
|
|
819
|
+
this.onTransactionComplete?.(successfulEntries);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
727
822
|
try {
|
|
728
823
|
await withTimeout(transaction.complete(), 3e4, "Transaction complete timeout");
|
|
729
824
|
if (__DEV__) {
|
|
@@ -732,6 +827,7 @@ var SupabaseConnector = class {
|
|
|
732
827
|
discardedCount: discardedEntries.length
|
|
733
828
|
});
|
|
734
829
|
}
|
|
830
|
+
this.completionFailures.delete(fingerprint);
|
|
735
831
|
for (const entry of successfulEntries) {
|
|
736
832
|
this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);
|
|
737
833
|
}
|
|
@@ -774,10 +870,13 @@ var SupabaseConnector = class {
|
|
|
774
870
|
this.onTransactionComplete?.(successfulEntries);
|
|
775
871
|
} catch (error) {
|
|
776
872
|
const classified = classifySupabaseError(error);
|
|
873
|
+
const newFailureCount = currentFailureCount + 1;
|
|
777
874
|
this.logger?.error("[Connector] Transaction completion FAILED:", {
|
|
778
875
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
779
876
|
classified,
|
|
780
877
|
isPermanent: classified.isPermanent,
|
|
878
|
+
failureCount: newFailureCount,
|
|
879
|
+
maxFailures: _SupabaseConnector.COMPLETION_MAX_FAILURES,
|
|
781
880
|
entries: successfulEntries.map((e) => ({
|
|
782
881
|
table: e.table,
|
|
783
882
|
op: e.op,
|
|
@@ -787,12 +886,46 @@ var SupabaseConnector = class {
|
|
|
787
886
|
if (__DEV__) {
|
|
788
887
|
console.error("[Connector] Transaction completion error details:", {
|
|
789
888
|
errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
|
|
790
|
-
errorObject: JSON.stringify(error, null, 2)
|
|
889
|
+
errorObject: JSON.stringify(error, null, 2),
|
|
890
|
+
failureCount: newFailureCount
|
|
791
891
|
});
|
|
792
892
|
}
|
|
893
|
+
if (currentFailureCount === 0) {
|
|
894
|
+
if (__DEV__) {
|
|
895
|
+
console.log("[Connector] First completion failure - retrying with extended timeout (60s)...");
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
await withTimeout(transaction.complete(), _SupabaseConnector.COMPLETION_EXTENDED_TIMEOUT_MS, "Transaction complete extended timeout");
|
|
899
|
+
if (__DEV__) {
|
|
900
|
+
console.log("[Connector] Transaction completed on extended retry");
|
|
901
|
+
}
|
|
902
|
+
this.completionFailures.delete(fingerprint);
|
|
903
|
+
this.onTransactionSuccess?.(successfulEntries);
|
|
904
|
+
this.onTransactionComplete?.(successfulEntries);
|
|
905
|
+
return;
|
|
906
|
+
} catch (retryError) {
|
|
907
|
+
if (__DEV__) {
|
|
908
|
+
console.warn("[Connector] Extended retry also failed:", {
|
|
909
|
+
error: retryError instanceof Error ? retryError.message : String(retryError)
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
this.completionFailures.set(fingerprint, {
|
|
915
|
+
count: newFailureCount,
|
|
916
|
+
lastAttempt: /* @__PURE__ */ new Date()
|
|
917
|
+
});
|
|
918
|
+
if (this.completionFailures.size > 50) {
|
|
919
|
+
const oldestKey = [...this.completionFailures.keys()][0];
|
|
920
|
+
if (oldestKey) {
|
|
921
|
+
this.completionFailures.delete(oldestKey);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
793
924
|
this.logger?.error("[Connector] Transaction completion error:", {
|
|
794
925
|
error,
|
|
795
926
|
classified,
|
|
927
|
+
failureCount: newFailureCount,
|
|
928
|
+
willRetry: newFailureCount < _SupabaseConnector.COMPLETION_MAX_FAILURES,
|
|
796
929
|
entries: successfulEntries.map((e) => ({
|
|
797
930
|
table: e.table,
|
|
798
931
|
op: e.op,
|
|
@@ -1161,4 +1294,4 @@ export {
|
|
|
1161
1294
|
getLocalVersion,
|
|
1162
1295
|
SupabaseConnector
|
|
1163
1296
|
};
|
|
1164
|
-
//# sourceMappingURL=chunk-
|
|
1297
|
+
//# sourceMappingURL=chunk-KN2IZERF.js.map
|