@pol-studios/powersync 1.0.22 → 1.0.24

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 (35) hide show
  1. package/dist/attachments/index.d.ts +51 -17
  2. package/dist/attachments/index.js +6 -2
  3. package/dist/{chunk-PG2NPQG3.js → chunk-55DKCJV4.js} +25 -5
  4. package/dist/chunk-55DKCJV4.js.map +1 -0
  5. package/dist/{chunk-IMRSLJRV.js → chunk-BGBQYQV3.js} +129 -38
  6. package/dist/chunk-BGBQYQV3.js.map +1 -0
  7. package/dist/{chunk-ZM4ENYMF.js → chunk-C5ODS3XH.js} +51 -8
  8. package/dist/chunk-C5ODS3XH.js.map +1 -0
  9. package/dist/{chunk-4TXTAEF2.js → chunk-CACKC6XG.js} +3 -2
  10. package/dist/chunk-CACKC6XG.js.map +1 -0
  11. package/dist/{chunk-XOCIONAA.js → chunk-TIFL2KWE.js} +3 -3
  12. package/dist/{chunk-N4K7E53V.js → chunk-YVX3A36I.js} +4 -4
  13. package/dist/connector/index.d.ts +1 -1
  14. package/dist/connector/index.js +1 -1
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +10 -6
  17. package/dist/index.native.d.ts +3 -3
  18. package/dist/index.native.js +10 -6
  19. package/dist/index.web.d.ts +3 -3
  20. package/dist/index.web.js +10 -6
  21. package/dist/{pol-attachment-queue-BVAIueoP.d.ts → pol-attachment-queue-BE2HU3Us.d.ts} +71 -7
  22. package/dist/provider/index.d.ts +2 -2
  23. package/dist/provider/index.js +4 -4
  24. package/dist/react/index.d.ts +1 -1
  25. package/dist/react/index.js +3 -3
  26. package/dist/{supabase-connector-WuiFiBnV.d.ts → supabase-connector-D2oIl2t8.d.ts} +13 -2
  27. package/dist/sync/index.d.ts +3 -0
  28. package/dist/sync/index.js +1 -1
  29. package/package.json +14 -4
  30. package/dist/chunk-4TXTAEF2.js.map +0 -1
  31. package/dist/chunk-IMRSLJRV.js.map +0 -1
  32. package/dist/chunk-PG2NPQG3.js.map +0 -1
  33. package/dist/chunk-ZM4ENYMF.js.map +0 -1
  34. /package/dist/{chunk-XOCIONAA.js.map → chunk-TIFL2KWE.js.map} +0 -0
  35. /package/dist/{chunk-N4K7E53V.js.map → chunk-YVX3A36I.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/connector/types.ts","../src/connector/middleware/upload-error.ts","../src/connector/errors.ts","../src/conflicts/detect.ts","../src/connector/supabase-connector.ts"],"sourcesContent":["/**\n * Connector Types for @pol-studios/powersync\n *\n * Defines interfaces for PowerSync backend connectors.\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase, ClassifiedError, CrudEntry } from '../core/types';\nimport type { LoggerAdapter } from '../platform/types';\nimport type { ConflictHandler, ConflictDetectionConfig } from '../conflicts/types';\nimport type { ConflictBus } from '../conflicts/conflict-bus';\n// Re-export ConflictBus type for convenience\nexport type { ConflictBus } from '../conflicts/conflict-bus';\n\n// ─── Connector Configuration ─────────────────────────────────────────────────\n\n/**\n * Circuit breaker configuration for the connector.\n * Prevents cascading failures by stopping requests when service is down.\n */\nexport interface ConnectorCircuitBreakerConfig {\n /**\n * Enable circuit breaker pattern.\n * @default false\n */\n enabled: boolean;\n /**\n * Number of failures required to trip the circuit.\n * @default 5\n */\n failureThreshold?: number;\n /**\n * Time window in ms for counting failures.\n * @default 60000 (60 seconds)\n */\n failureWindowMs?: number;\n /**\n * Initial delay before transitioning from OPEN to HALF_OPEN.\n * @default 1000 (1 second)\n */\n initialRecoveryDelayMs?: number;\n /**\n * Maximum delay for exponential backoff.\n * @default 32000 (32 seconds)\n */\n maxRecoveryDelayMs?: number;\n}\n\n/**\n * Options for creating a SupabaseConnector\n */\nexport interface SupabaseConnectorOptions {\n /** Supabase client instance */\n supabaseClient: SupabaseClient;\n /** PowerSync service URL */\n powerSyncUrl: string;\n /**\n * Optional: Custom schema routing function.\n * Determines which Supabase schema a table belongs to.\n * @default Returns 'public' for all tables\n */\n schemaRouter?: SchemaRouter;\n /**\n * Optional: Custom CRUD handler for complex mutations.\n * Allows overriding default upsert/update/delete behavior.\n */\n crudHandler?: CrudHandler;\n /** Logger for debugging */\n logger?: LoggerAdapter;\n /** Called when a transaction is successfully uploaded */\n onTransactionSuccess?: (entries: CrudEntry[]) => void;\n /** Called when a transaction fails to upload */\n onTransactionFailure?: (entries: CrudEntry[], error: Error, classified: ClassifiedError) => void;\n /** Called when a transaction is fully completed (after transaction.complete()) */\n onTransactionComplete?: (entries: CrudEntry[]) => void;\n /** Function to check if upload should proceed. Used for sync mode gating. */\n shouldUpload?: () => boolean;\n /**\n * Optional: Configuration for version-based conflict detection.\n * When enabled, checks for conflicts before uploading changes.\n */\n conflictDetection?: ConflictDetectionConfig;\n /**\n * Optional: Handler for conflict resolution.\n * If not provided, conflicts are logged and upload proceeds.\n */\n conflictHandler?: ConflictHandler;\n /**\n * Optional: Event bus for publishing conflict events.\n * Use this to notify the UI layer about detected conflicts.\n */\n conflictBus?: ConflictBus;\n /**\n * Optional: Configuration for retry behavior on upload failures.\n * Allows customizing retry attempts, delays, and backoff for different error types.\n * @default DEFAULT_RETRY_CONFIG\n */\n retryConfig?: Partial<RetryConfig>;\n /**\n * Optional: Configuration for circuit breaker pattern.\n * When enabled, prevents cascading failures by stopping requests when\n * the service is experiencing high failure rates.\n * @default { enabled: false }\n */\n circuitBreaker?: ConnectorCircuitBreakerConfig;\n /**\n * Optional: Middleware chain for classifying upload errors.\n * Allows customizing error handling for specific tables or error codes.\n *\n * Middleware functions are called in order until one returns a non-'continue' result:\n * - 'success': Treat as successful (e.g., idempotent duplicate)\n * - 'retry': Use the retry configuration to retry the operation\n * - 'discard': Permanent failure, remove from queue without retry\n * - 'continue': Pass to the next middleware in the chain\n *\n * If all middleware return 'continue', the default classification is used.\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * // Treat duplicate key as success for idempotent tables\n * idempotentTables(['ReadReceipt', 'Like', 'Bookmark']),\n * // Discard FK violations (orphaned records)\n * discardOrphaned(),\n * // Custom handling\n * ({ entry, pgCode }) => {\n * if (entry.table === 'MyTable' && pgCode === '23505') {\n * return 'success';\n * }\n * return 'continue';\n * },\n * ]\n * ```\n */\n uploadErrorMiddleware?: UploadErrorMiddleware[];\n}\n\n/**\n * Configuration passed to PowerSyncProvider for connector setup\n */\nexport interface ConnectorConfig {\n /**\n * Custom schema routing function.\n * Determines which Supabase schema a table belongs to.\n * @default Returns 'public' for all tables\n *\n * @example\n * ```typescript\n * schemaRouter: (table) => {\n * if (['Profile', 'Comment'].includes(table)) return 'core';\n * return 'public';\n * }\n * ```\n */\n schemaRouter?: SchemaRouter;\n\n /**\n * Custom CRUD handler for complex mutations.\n * @default Uses standard upsert/update/delete operations\n */\n crudHandler?: CrudHandler;\n\n /**\n * Token refresh configuration.\n */\n tokenRefresh?: {\n /** Refresh token when it expires within this many seconds (default: 60) */\n refreshThresholdSeconds?: number;\n };\n\n /**\n * Optional retry configuration for upload failures.\n * Allows customizing retry attempts, delays, and backoff for different error types.\n * @default DEFAULT_RETRY_CONFIG\n */\n retryConfig?: Partial<RetryConfig>;\n}\n\n// ─── Schema Routing ──────────────────────────────────────────────────────────\n\n/**\n * Function that determines which Supabase schema a table belongs to.\n *\n * @param tableName - The name of the table\n * @returns The schema name (e.g., 'public', 'core')\n */\nexport type SchemaRouter = (tableName: string) => string;\n\n/**\n * Default schema router that returns 'public' for all tables\n */\nexport const defaultSchemaRouter: SchemaRouter = () => 'public';\n\n// ─── CRUD Handling ───────────────────────────────────────────────────────────\n\n/**\n * Custom handler for CRUD operations.\n *\n * Return `true` from a handler to indicate the operation was handled.\n * Return `false` to fall back to default behavior.\n */\nexport interface CrudHandler {\n /**\n * Handle a PUT operation (insert or replace).\n * @param entry - The CRUD entry\n * @param supabase - Supabase client\n * @param schema - The resolved schema for this table\n * @returns true if handled, false to use default behavior\n */\n handlePut?(entry: CrudEntry, supabase: SupabaseClient, schema: string): Promise<boolean>;\n\n /**\n * Handle a PATCH operation (update).\n * @param entry - The CRUD entry\n * @param supabase - Supabase client\n * @param schema - The resolved schema for this table\n * @returns true if handled, false to use default behavior\n */\n handlePatch?(entry: CrudEntry, supabase: SupabaseClient, schema: string): Promise<boolean>;\n\n /**\n * Handle a DELETE operation.\n * @param entry - The CRUD entry\n * @param supabase - Supabase client\n * @param schema - The resolved schema for this table\n * @returns true if handled, false to use default behavior\n */\n handleDelete?(entry: CrudEntry, supabase: SupabaseClient, schema: string): Promise<boolean>;\n}\n\n// ─── Credentials ─────────────────────────────────────────────────────────────\n\n/**\n * Credentials returned by fetchCredentials\n */\nexport interface PowerSyncCredentials {\n /** PowerSync service endpoint URL */\n endpoint: string;\n /** JWT token for authentication */\n token: string;\n /** When the token expires */\n expiresAt?: Date;\n}\n\n// ─── Retry Configuration ─────────────────────────────────────────────────────\n\n/**\n * Configuration for a single retry category\n */\nexport interface RetryStrategyConfig {\n /** Maximum number of retry attempts */\n maxRetries: number;\n /** Initial delay in milliseconds */\n baseDelayMs: number;\n /** Maximum delay cap in milliseconds */\n maxDelayMs: number;\n /** Multiplier for exponential backoff */\n backoffMultiplier: number;\n}\n\n/**\n * Full retry configuration for the connector\n */\nexport interface RetryConfig {\n /** Retry config for transient errors (network, server 5xx) */\n transient: RetryStrategyConfig;\n /** Retry config for permanent errors (validation, constraints) */\n permanent: RetryStrategyConfig;\n /** Retry config for RLS/permission errors (42501, row-level security violations) */\n rls: RetryStrategyConfig;\n}\n\n/**\n * Default retry configuration\n *\n * Uses fast exponential backoff for transient errors (network issues):\n * - Transient: 1s → 2s → 4s\n *\n * RLS/permission errors (42501, row-level security violations) use extended delays\n * because parent data may need time to sync before child records can be inserted.\n * - RLS: 30s → 60s → 120s → 120s → 120s (5 retries over ~7.5 minutes)\n *\n * Other permanent errors (validation, constraints) get NO retries\n * because they will never succeed - fail fast and surface to user.\n * PowerSync's native retry mechanism will re-attempt on next sync cycle.\n */\nexport const DEFAULT_RETRY_CONFIG: RetryConfig = {\n transient: {\n maxRetries: 3,\n baseDelayMs: 1000,\n // 1 second initial delay\n maxDelayMs: 4000,\n // 4 second cap\n backoffMultiplier: 2 // 1s → 2s → 4s\n },\n permanent: {\n maxRetries: 0,\n // No retries - permanent errors won't succeed\n baseDelayMs: 1000,\n // Irrelevant with 0 retries, but needed for type\n maxDelayMs: 1000,\n // Irrelevant with 0 retries\n backoffMultiplier: 1 // Irrelevant with 0 retries\n },\n rls: {\n maxRetries: 5,\n // Extended retries for RLS errors\n baseDelayMs: 30000,\n // 30 seconds initial delay\n maxDelayMs: 120000,\n // 2 minutes max delay\n backoffMultiplier: 2 // 30s → 60s → 120s → 120s → 120s\n }\n};\n\n// ─── Auth Abstraction ────────────────────────────────────────────────────────\n\n/**\n * Session representation for authentication.\n * Used by AuthProvider to communicate auth state.\n */\nexport interface Session {\n /** Access token for API requests */\n accessToken: string;\n /** When the token expires (optional) */\n expiresAt?: Date;\n /** User information (optional) */\n user?: {\n id: string;\n };\n}\n\n/**\n * Authentication provider interface.\n * Abstracts auth for the connector - works with any auth backend.\n *\n * @example\n * ```typescript\n * // Using built-in Supabase adapter\n * const auth = createSupabaseAuth(supabaseClient);\n *\n * // Custom implementation\n * const auth: AuthProvider = {\n * getSession: async () => ({ accessToken: myToken }),\n * refreshSession: async () => {\n * const newToken = await myAuthService.refresh();\n * return { accessToken: newToken };\n * },\n * onAuthStateChange: (cb) => {\n * return myAuthService.subscribe(cb);\n * },\n * };\n * ```\n */\nexport interface AuthProvider {\n /**\n * Get the current session.\n * @returns Current session or null if not authenticated\n */\n getSession(): Promise<Session | null>;\n\n /**\n * Refresh the current session.\n * @throws Error if refresh fails (e.g., refresh token expired)\n * @returns New session with fresh access token\n */\n refreshSession(): Promise<Session>;\n\n /**\n * Subscribe to auth state changes.\n * @param cb - Callback fired when auth state changes\n * @returns Unsubscribe function\n */\n onAuthStateChange(cb: (session: Session | null) => void): () => void;\n}\n\n// ─── Upload Error Middleware ─────────────────────────────────────────────────\n\n/**\n * Classification result for upload errors.\n * Determines how the connector should handle a failed upload.\n *\n * - 'success': Treat as successful (e.g., idempotent duplicate)\n * - 'retry': Use the retry configuration to retry the operation\n * - 'discard': Permanent failure, remove from queue without retry\n * - 'continue': Pass to the next middleware in the chain\n * - 'fail_transaction': Abort the entire transaction immediately without retry\n */\nexport type UploadErrorClassification = 'success' | 'retry' | 'discard' | 'continue' | 'fail_transaction';\n\n/**\n * Context provided to upload error middleware functions.\n * Contains all information needed to classify an upload error.\n */\nexport interface UploadErrorContext {\n /** The CRUD entry that failed */\n entry: CrudEntry;\n /** The error that occurred */\n error: Error;\n /** PostgreSQL error code if available (e.g., '23505' for unique violation) */\n pgCode: string | undefined;\n /** HTTP status code if available */\n httpStatusCode: number | undefined;\n /** The classified error from the default classifier */\n classified: ClassifiedError;\n /** The original error object (may contain additional properties) */\n originalError: unknown;\n /** The schema the table belongs to */\n schema: string;\n /** Supabase client for queries (use sparingly - prefer local checks) */\n supabase: SupabaseClient;\n}\n\n/**\n * Middleware function for classifying upload errors.\n *\n * Middleware functions are called in order until one returns a non-'continue' result.\n * Return 'continue' to pass to the next middleware in the chain.\n *\n * @example\n * ```typescript\n * const myMiddleware: UploadErrorMiddleware = ({ entry, pgCode }) => {\n * if (entry.table === 'MyTable' && pgCode === '23505') {\n * return 'success'; // Treat as successful\n * }\n * return 'continue'; // Let the next middleware handle it\n * };\n * ```\n */\nexport type UploadErrorMiddleware = (context: UploadErrorContext) => Promise<UploadErrorClassification> | UploadErrorClassification;\n\n// ─── Re-export from Core ─────────────────────────────────────────────────────\n\n// Re-export PowerSyncBackendConnector from core/types to maintain API compatibility\nexport type { PowerSyncBackendConnector } from '../core/types';","/**\n * Upload Error Middleware for @pol-studios/powersync\n *\n * Provides a middleware pattern for classifying and handling upload errors.\n * Includes built-in factory functions for common patterns and an executor\n * for running the middleware chain.\n */\n\nimport type { UploadErrorClassification, UploadErrorContext, UploadErrorMiddleware } from '../types';\n\n// ─── Built-in Middleware Factories ───────────────────────────────────────────\n\n/**\n * Create middleware that treats duplicate key errors (23505) as success\n * for specified tables.\n *\n * This is useful for idempotent operations where re-inserting the same\n * record should be treated as a success, not an error.\n *\n * @param tables - Array of table names to treat as idempotent\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * idempotentTables(['ReadReceipt', 'Like', 'Bookmark']),\n * ]\n * ```\n */\nexport function idempotentTables(tables: string[]): UploadErrorMiddleware {\n const tableSet = new Set(tables);\n return ({\n entry,\n pgCode\n }) => {\n if (tableSet.has(entry.table) && pgCode === '23505') {\n return 'success';\n }\n return 'continue';\n };\n}\n\n/**\n * Create middleware that discards entries with foreign key violations (23503).\n *\n * This is useful for handling orphaned records where the parent record\n * was deleted before the child record could be uploaded.\n *\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * discardOrphaned(),\n * ]\n * ```\n */\nexport function discardOrphaned(): UploadErrorMiddleware {\n return ({\n pgCode\n }) => {\n if (pgCode === '23503') {\n return 'discard';\n }\n return 'continue';\n };\n}\n\n/**\n * Create middleware that discards entries with specific PostgreSQL error codes.\n *\n * @param codes - Array of PostgreSQL error codes to discard\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * // Discard check constraint violations and not-null violations\n * discardOnPgCodes(['23514', '23502']),\n * ]\n * ```\n */\nexport function discardOnPgCodes(codes: string[]): UploadErrorMiddleware {\n const codeSet = new Set(codes);\n return ({\n pgCode\n }) => {\n if (pgCode && codeSet.has(pgCode)) {\n return 'discard';\n }\n return 'continue';\n };\n}\n\n/**\n * Create middleware that treats specific PostgreSQL error codes as success\n * for specific tables.\n *\n * @param config - Map of table names to arrays of PostgreSQL error codes to treat as success\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * successOnPgCodes({\n * 'ReadReceipt': ['23505'], // Duplicate key is OK\n * 'Notification': ['23505', '23503'], // Duplicate and FK violations are OK\n * }),\n * ]\n * ```\n */\nexport function successOnPgCodes(config: Record<string, string[]>): UploadErrorMiddleware {\n return ({\n entry,\n pgCode\n }) => {\n const codes = config[entry.table];\n if (codes && pgCode && codes.includes(pgCode)) {\n return 'success';\n }\n return 'continue';\n };\n}\n\n/**\n * Create middleware that retries entries on specific HTTP status codes.\n *\n * @param statusCodes - Array of HTTP status codes to retry\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * // Retry on rate limiting and server errors\n * retryOnHttpStatus([429, 500, 502, 503, 504]),\n * ]\n * ```\n */\nexport function retryOnHttpStatus(statusCodes: number[]): UploadErrorMiddleware {\n const statusSet = new Set(statusCodes);\n return ({\n httpStatusCode\n }) => {\n if (httpStatusCode && statusSet.has(httpStatusCode)) {\n return 'retry';\n }\n return 'continue';\n };\n}\n\n/**\n * Create middleware that discards entries on specific HTTP status codes.\n *\n * @param statusCodes - Array of HTTP status codes to discard\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * // Discard on 400 Bad Request (malformed data)\n * discardOnHttpStatus([400]),\n * ]\n * ```\n */\nexport function discardOnHttpStatus(statusCodes: number[]): UploadErrorMiddleware {\n const statusSet = new Set(statusCodes);\n return ({\n httpStatusCode\n }) => {\n if (httpStatusCode && statusSet.has(httpStatusCode)) {\n return 'discard';\n }\n return 'continue';\n };\n}\n\n/**\n * Create middleware that applies table-specific handling.\n *\n * @param handlers - Map of table names to handler functions\n * @returns Middleware function\n *\n * @example\n * ```typescript\n * uploadErrorMiddleware: [\n * tableHandlers({\n * 'AuditLog': () => 'discard', // Never retry audit logs\n * 'UserSession': ({ pgCode }) =>\n * pgCode === '23505' ? 'success' : 'continue',\n * }),\n * ]\n * ```\n */\nexport function tableHandlers(handlers: Record<string, UploadErrorMiddleware>): UploadErrorMiddleware {\n return context => {\n const handler = handlers[context.entry.table];\n if (handler) {\n return handler(context);\n }\n return 'continue';\n };\n}\n\n// ─── Middleware Executor ─────────────────────────────────────────────────────\n\n/**\n * Run upload error middleware chain and return the classification.\n *\n * Executes middleware in order until one returns a non-'continue' result.\n * If all middleware return 'continue', returns the default classification.\n *\n * @param context - The error context to pass to middleware\n * @param middleware - Array of middleware functions to execute\n * @param defaultClassification - Classification to use if all middleware return 'continue'\n * @returns The final classification\n *\n * @example\n * ```typescript\n * const classification = await runUploadErrorMiddleware(\n * context,\n * [idempotentTables(['ReadReceipt']), discardOrphaned()],\n * 'retry' // Default if no middleware matches\n * );\n * ```\n */\nexport async function runUploadErrorMiddleware(context: UploadErrorContext, middleware: UploadErrorMiddleware[], defaultClassification: UploadErrorClassification = 'retry'): Promise<UploadErrorClassification> {\n for (const mw of middleware) {\n const result = await mw(context);\n if (result !== 'continue') {\n return result;\n }\n }\n return defaultClassification;\n}\n\n/**\n * Synchronous version of runUploadErrorMiddleware for middleware that don't use async.\n *\n * @param context - The error context to pass to middleware\n * @param middleware - Array of middleware functions to execute\n * @param defaultClassification - Classification to use if all middleware return 'continue'\n * @returns The final classification\n */\nexport function runUploadErrorMiddlewareSync(context: UploadErrorContext, middleware: UploadErrorMiddleware[], defaultClassification: UploadErrorClassification = 'retry'): UploadErrorClassification {\n for (const mw of middleware) {\n const result = mw(context);\n // If the middleware returns a promise, throw an error\n if (result && typeof result === 'object' && 'then' in result) {\n throw new Error('Async middleware detected in runUploadErrorMiddlewareSync. ' + 'Use runUploadErrorMiddleware instead.');\n }\n // At this point we know result is UploadErrorClassification (not a Promise)\n const syncResult = result as UploadErrorClassification;\n if (syncResult !== 'continue') {\n return syncResult;\n }\n }\n return defaultClassification;\n}","/**\n * Custom errors for the @pol-studios/powersync connector.\n *\n * These errors are used by the upload error middleware to control\n * how failed CRUD entries are handled.\n */\n\n/**\n * Error thrown when a CRUD entry is discarded by middleware.\n * When this error is thrown, the entry is removed from the queue without retry.\n */\nexport class DiscardEntryError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'DiscardEntryError';\n }\n}\n\n/**\n * Error thrown when a transaction is aborted by middleware.\n * When thrown, stops the entire transaction immediately without retry.\n */\nexport class TransactionAbortError extends Error {\n constructor(message: string, public readonly cause?: Error) {\n super(message);\n this.name = 'TransactionAbortError';\n }\n}","/**\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\n/**\n * Error thrown when conflict detection fails due to external issues\n * (e.g., AuditLog query failure). Callers should handle this and decide\n * whether to retry, fail the sync, or escalate to the user.\n */\nexport class ConflictDetectionError extends Error {\n constructor(message: string, options?: {\n cause?: unknown;\n }) {\n super(message);\n this.name = 'ConflictDetectionError';\n this.cause = options?.cause;\n }\n}\n\n/**\n * Deep equality comparison for detecting field changes.\n *\n * Handles:\n * - Primitive types (string, number, boolean, null, undefined)\n * - NaN values (NaN === NaN returns true)\n * - Date objects (compared by getTime() value)\n * - Arrays (recursive element comparison)\n * - Plain objects (recursive property comparison)\n * - Circular references (protected by maxDepth limit)\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @param maxDepth - Maximum recursion depth to prevent infinite loops (default: 10)\n * @returns true if values are deeply equal, false otherwise\n */\nfunction deepEqual(a: unknown, b: unknown, maxDepth = 10): boolean {\n // Strict equality handles primitives, same reference, null === null, undefined === undefined\n if (a === b) return true;\n\n // Handle NaN (NaN !== NaN in JavaScript, but we want NaN === NaN for conflict detection)\n if (typeof a === 'number' && typeof b === 'number') {\n if (Number.isNaN(a) && Number.isNaN(b)) return true;\n }\n\n // Handle null and undefined (both must be checked since undefined !== null)\n if (a === null || a === undefined || b === null || b === undefined) return false;\n\n // Type mismatch\n if (typeof a !== typeof b) return false;\n\n // Non-object types that aren't equal already returned above\n if (typeof a !== 'object') return false;\n\n // Depth limit to prevent infinite loops from circular references\n if (maxDepth <= 0) {\n console.warn('[deepEqual] Max depth reached, falling back to reference equality');\n return a === b;\n }\n\n // Handle Date objects - compare by timestamp\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n // Handle Date vs non-Date mismatch\n if (a instanceof Date || b instanceof Date) return false;\n\n // Array type mismatch\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const keysA = Object.keys(a as object);\n const keysB = Object.keys(b as object);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n if (!keysB.includes(key)) return false;\n if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key], maxDepth - 1)) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Regex to validate table names and prevent SQL injection.\n * Only allows alphanumeric characters and underscores, starting with a letter or underscore.\n */\nconst TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validates a table name to prevent SQL injection.\n * @throws Error if the table name is invalid\n */\nfunction validateTableName(table: string): void {\n if (!TABLE_NAME_REGEX.test(table)) {\n throw new Error(`Invalid table name: ${table}`);\n }\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(table: string, recordId: string, localVersion: number, serverVersion: number, pendingChanges: Record<string, unknown>, supabase: SupabaseClient, config?: ConflictDetectionConfig): Promise<ConflictCheckResult> {\n const ignoredFields = new Set([...DEFAULT_IGNORED_FIELDS, ...(config?.ignoredFields ?? [])]);\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 {\n data: auditLogs,\n error\n } = await supabase.schema('core').from('AuditLog').select('oldRecord, newRecord, changeBy, changeAt').eq('tableName', table).eq('recordId_text', recordId).order('changeAt', {\n ascending: false\n }).limit(20); // Recent changes should be sufficient\n\n if (error) {\n // Don't assume no conflict - throw and let caller decide how to handle\n throw new ConflictDetectionError(`AuditLog query failed for ${table}/${recordId}`, {\n cause: error\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 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 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 // Use deep equality to properly compare arrays and objects\n if (!deepEqual(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 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 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(table: string, db: AbstractPowerSyncDatabase): Promise<boolean> {\n try {\n // Validate table name to prevent SQL injection\n validateTableName(table);\n\n // Query the PowerSync internal schema for column info\n const result = await db.getAll<{\n name: string;\n }>(`PRAGMA table_info(\"${table}\")`);\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(table: string, recordId: string, schema: string, supabase: SupabaseClient): Promise<number | null> {\n const query = schema === 'public' ? supabase.from(table) : (supabase.schema(schema) as unknown as ReturnType<typeof supabase.schema>).from(table);\n const {\n data,\n error\n } = await query.select('_version').eq('id', recordId).single();\n if (error || !data) {\n return null;\n }\n return (data as {\n _version?: number;\n })._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(table: string, recordId: string, db: AbstractPowerSyncDatabase): Promise<number | null> {\n // Validate table name to prevent SQL injection\n validateTableName(table);\n const result = await db.get<{\n _version?: number;\n }>(`SELECT _version FROM \"${table}\" WHERE id = ?`, [recordId]);\n return result?._version ?? null;\n}","/**\n * Supabase Connector for PowerSync\n *\n * A generic, configurable connector that handles:\n * - Authentication with Supabase JWT tokens\n * - Uploading local changes back to Supabase\n * - Schema routing for multi-schema databases\n * - Custom CRUD handling for complex operations\n * - Version-based conflict detection (when enabled)\n */\n\nimport type { SupabaseClient } from '@supabase/supabase-js';\nimport type { AbstractPowerSyncDatabase, ClassifiedError, CrudEntry } from '../core/types';\nimport type { LoggerAdapter } from '../platform/types';\nimport type { SupabaseConnectorOptions, PowerSyncBackendConnector, PowerSyncCredentials, SchemaRouter, CrudHandler, RetryConfig, UploadErrorMiddleware, UploadErrorContext } from './types';\nimport { runUploadErrorMiddleware } from './middleware';\nimport { DiscardEntryError, TransactionAbortError } from './errors';\nimport type { ConflictBus } from '../conflicts/conflict-bus';\nimport type { ConflictHandler, ConflictDetectionConfig, ConflictCheckResult, ConflictResolution } from '../conflicts/types';\nimport { defaultSchemaRouter, DEFAULT_RETRY_CONFIG } from './types';\nimport { classifySupabaseError, isRlsError, extractHttpStatusCode } from '../core/errors';\nimport { detectConflicts, fetchServerVersion, getLocalVersion, hasVersionColumn } from '../conflicts/detect';\nimport { withExponentialBackoff } from '../utils/retry';\n\n/**\n * Custom error for validation failures (payload size, required fields, etc.)\n */\nclass ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Check if an error is an authentication/authorization error (401 or JWT-related).\n * Used to trigger token refresh on auth failures during CRUD operations.\n *\n * Uses specific patterns with word boundaries to avoid false positives\n * (e.g., \"Cache entry expired\" should NOT match).\n */\nfunction isAuthError(error: unknown): boolean {\n if (!error) return false;\n\n // Check for HTTP 401/403 status codes\n if (typeof error === 'object' && error !== null) {\n const statusCode = (error as {\n status?: number;\n statusCode?: number;\n }).status ?? (error as {\n status?: number;\n statusCode?: number;\n }).statusCode;\n if (statusCode === 401 || statusCode === 403) {\n return true;\n }\n\n // Also check string code for compatibility\n const code = (error as {\n code?: string | number;\n }).code;\n if (code === '401' || code === '403' || code === '42501') {\n return true;\n }\n }\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();\n\n // Use specific patterns to avoid false positives like \"Cache entry expired\"\n const authPatterns = [/\\bjwt\\s+(expired|invalid|malformed)\\b/, /\\btoken\\s+(expired|invalid|revoked)\\b/, /\\bsession\\s+(expired|invalid)\\b/, /\\bunauthorized\\s*(access|request|error)?\\b/, /\\baccess\\s+(denied|forbidden)\\b/, /\\bnot\\s+authorized\\b/, /\\bauth(entication)?\\s+(failed|error|required)\\b/, /\\bpermission\\s+denied\\b/, /\\binvalid\\s+(credentials|api[_\\s]?key)\\b/, /\\brls\\b.*\\bpolicy\\b/, /\\brow[_\\s]?level[_\\s]?security\\b/, /\\b42501\\b/ // PostgreSQL insufficient privilege error code\n ];\n return authPatterns.some(pattern => pattern.test(message));\n}\n\n/**\n * Wrap a promise with a timeout that cleans up when the race completes.\n * Prevents timer leaks that would cause unhandled promise rejections.\n *\n * @param promise - The promise to wrap\n * @param ms - Timeout in milliseconds\n * @param message - Error message if timeout occurs\n */\nfunction withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {\n let timeoutId: ReturnType<typeof setTimeout>;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(message)), ms);\n });\n return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));\n}\n\n// Maximum size limit for CRUD payloads (with margin for safety)\nconst MAX_PAYLOAD_SIZE = 900 * 1024; // 900KB\n\n/**\n * Update type enum matching @powersync/common\n */\nenum UpdateType {\n PUT = 'PUT',\n PATCH = 'PATCH',\n DELETE = 'DELETE',\n}\n\n/**\n * Group CRUD entries by table name for batched processing.\n */\nfunction groupEntriesByTable(entries: CrudEntry[]): Map<string, CrudEntry[]> {\n const grouped = new Map<string, CrudEntry[]>();\n for (const entry of entries) {\n const existing = grouped.get(entry.table);\n if (existing) {\n existing.push(entry);\n } else {\n grouped.set(entry.table, [entry]);\n }\n }\n return grouped;\n}\n\n/**\n * Context for transaction finalization (DRY helper).\n */\ninterface TransactionFinalizationContext {\n transaction: {\n complete: () => Promise<void>;\n };\n successfulEntries: CrudEntry[];\n discardedEntries?: CrudEntry[];\n partialResolutions?: Array<{\n originalConflict: ConflictCheckResult;\n syncedFields: string[];\n }>;\n}\n\n/**\n * Generic Supabase connector for PowerSync.\n *\n * This connector handles authentication and CRUD uploads to Supabase.\n * It supports configurable schema routing and custom CRUD handlers\n * for complex use cases.\n *\n * @example Basic usage\n * ```typescript\n * const connector = new SupabaseConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: 'https://your-powersync-instance.com',\n * });\n * ```\n *\n * @example With schema routing\n * ```typescript\n * const connector = new SupabaseConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: 'https://your-powersync-instance.com',\n * schemaRouter: (table) => {\n * if (['Profile', 'Comment', 'CommentSection'].includes(table)) {\n * return 'core';\n * }\n * return 'public';\n * },\n * });\n * ```\n *\n * @example With custom CRUD handler\n * ```typescript\n * const connector = new SupabaseConnector({\n * supabaseClient: supabase,\n * powerSyncUrl: 'https://your-powersync-instance.com',\n * crudHandler: {\n * async handlePut(entry, supabase, schema) {\n * // Custom handling for specific tables\n * if (entry.table === 'SpecialTable') {\n * await myCustomUpsert(entry);\n * return true; // Handled\n * }\n * return false; // Use default\n * },\n * },\n * });\n * ```\n */\nexport class SupabaseConnector implements PowerSyncBackendConnector {\n private readonly supabase: SupabaseClient;\n private readonly powerSyncUrl: string;\n private readonly schemaRouter: SchemaRouter;\n private readonly crudHandler?: CrudHandler;\n private readonly logger?: LoggerAdapter;\n private readonly onTransactionSuccess?: (entries: CrudEntry[]) => void;\n private readonly onTransactionFailure?: (entries: CrudEntry[], error: Error, classified: ClassifiedError) => void;\n private readonly onTransactionComplete?: (entries: CrudEntry[]) => void;\n private readonly shouldUploadFn?: () => boolean;\n\n // Conflict detection configuration\n private readonly conflictDetection?: ConflictDetectionConfig;\n private readonly conflictHandler?: ConflictHandler;\n private readonly conflictBus?: ConflictBus;\n\n // Cache for version column existence checks (table -> hasVersionColumn)\n private versionColumnCache = new Map<string, boolean>();\n\n // Active project IDs for scoped sync (optional feature)\n private activeProjectIds: string[] = [];\n\n // Store resolutions for retry - when PowerSync retries, we apply stored resolutions\n // instead of re-detecting conflicts that the user already resolved\n private resolvedConflicts = new Map<string, ConflictResolution>();\n\n // Cleanup function for resolution listener subscription\n private unsubscribeResolution?: () => void;\n\n // Promise-based locking for version column checks to prevent duplicate queries\n private versionColumnPromises = new Map<string, Promise<boolean>>();\n\n // Flag to track if connector has been destroyed\n private isDestroyed = false;\n\n // Retry configuration\n private retryConfig: RetryConfig;\n\n // Track completion failures for circuit breaker logic\n // Maps transaction fingerprint (hash of entry IDs) to failure tracking info\n private completionFailures = new Map<string, {\n count: number;\n lastAttempt: Date;\n }>();\n private static readonly COMPLETION_MAX_FAILURES = 3;\n private static readonly COMPLETION_EXTENDED_TIMEOUT_MS = 60_000; // 60s timeout for retry\n private autoRetryPaused: boolean = false;\n\n // Per-entry cooldown tracking for failed operations\n // Maps entry key (table:id) to the timestamp when it can be retried\n private entryCooldowns = new Map<string, number>();\n private static readonly COOLDOWN_DURATION_MS = 60_000; // 1 minute cooldown after exhausting retries\n\n // Upload error middleware chain\n private readonly uploadErrorMiddleware: UploadErrorMiddleware[];\n constructor(options: SupabaseConnectorOptions) {\n this.supabase = options.supabaseClient;\n this.powerSyncUrl = options.powerSyncUrl;\n this.schemaRouter = options.schemaRouter ?? defaultSchemaRouter;\n this.crudHandler = options.crudHandler;\n this.logger = options.logger;\n this.onTransactionSuccess = options.onTransactionSuccess;\n this.onTransactionFailure = options.onTransactionFailure;\n this.onTransactionComplete = options.onTransactionComplete;\n this.shouldUploadFn = options.shouldUpload;\n\n // Conflict detection options\n this.conflictDetection = options.conflictDetection;\n this.conflictHandler = options.conflictHandler;\n this.conflictBus = options.conflictBus;\n\n // Initialize retry configuration by merging user options with defaults\n this.retryConfig = {\n transient: {\n ...DEFAULT_RETRY_CONFIG.transient,\n ...options.retryConfig?.transient\n },\n permanent: {\n ...DEFAULT_RETRY_CONFIG.permanent,\n ...options.retryConfig?.permanent\n },\n rls: {\n ...DEFAULT_RETRY_CONFIG.rls,\n ...options.retryConfig?.rls\n }\n };\n\n // Initialize upload error middleware chain\n this.uploadErrorMiddleware = options.uploadErrorMiddleware ?? [];\n\n // Subscribe to resolution events from the conflict bus\n // This stores resolutions so that on retry, we apply them instead of re-detecting\n if (this.conflictBus) {\n this.unsubscribeResolution = this.conflictBus.onResolution((table, recordId, resolution) => {\n // Key by table:recordId to avoid collisions across tables with same UUID\n const key = `${table}:${recordId}`;\n if (__DEV__) {\n console.log('[Connector] Storing resolution for retry:', {\n table,\n recordId,\n key,\n resolution\n });\n }\n this.resolvedConflicts.set(key, resolution);\n });\n }\n }\n\n /**\n * Clean up resources (unsubscribe from event listeners).\n * Call this when the connector is no longer needed.\n */\n destroy(): void {\n this.isDestroyed = true;\n if (this.unsubscribeResolution) {\n this.unsubscribeResolution();\n this.unsubscribeResolution = undefined;\n }\n this.resolvedConflicts.clear();\n this.versionColumnPromises.clear();\n this.completionFailures.clear();\n }\n\n /**\n * Generate a fingerprint for a set of entries to track completion failures.\n * Uses sorted entry IDs to create a consistent fingerprint regardless of order.\n */\n private generateTransactionFingerprint(entries: CrudEntry[]): string {\n // Sort IDs for consistency and join them\n const sortedIds = entries.map(e => `${e.table}:${e.id}`).sort();\n return sortedIds.join('|');\n }\n\n // ─── Retry Control Methods ─────────────────────────────────────────────────\n\n /**\n * Pause automatic retry of failed uploads.\n * Use this when the user goes offline intentionally or wants manual control.\n */\n pauseAutoRetry(): void {\n this.autoRetryPaused = true;\n if (__DEV__) {\n console.log('[Connector] Auto-retry paused');\n }\n }\n\n /**\n * Resume automatic retry of failed uploads.\n */\n resumeAutoRetry(): void {\n this.autoRetryPaused = false;\n if (__DEV__) {\n console.log('[Connector] Auto-retry resumed');\n }\n }\n\n // ─── Private Retry Logic ───────────────────────────────────────────────────\n\n /**\n * Process a single CRUD entry with exponential backoff retry.\n *\n * This method uses a two-phase approach:\n * 1. First attempt - try the operation to classify the error type\n * 2. Retry phase - use the appropriate config (transient vs permanent) based on classification\n *\n * This fixes the issue where reassigning selectedConfig inside withExponentialBackoff's\n * callback had no effect because the config was already destructured at call time.\n *\n * @param entry - The CRUD entry to process\n * @throws Error if all retries exhausted (for critical failures)\n */\n private async processWithRetry(entry: CrudEntry): Promise<void> {\n if (this.isDestroyed) {\n throw new Error('Connector destroyed');\n }\n\n // Check if this entry is in cooldown from a previous failure\n const entryKey = `${entry.table}:${entry.id}`;\n const cooldownUntil = this.entryCooldowns.get(entryKey);\n if (cooldownUntil && Date.now() < cooldownUntil) {\n const remainingMs = cooldownUntil - Date.now();\n if (__DEV__) {\n console.log('[Connector] Entry in cooldown, skipping:', {\n table: entry.table,\n id: entry.id,\n remainingSec: Math.round(remainingMs / 1000)\n });\n }\n // Throw a specific error so PowerSync keeps the entry but we don't spam retries\n throw new Error(`Entry in cooldown for ${Math.round(remainingMs / 1000)}s`);\n }\n const classified = {\n isPermanent: false,\n pgCode: undefined as string | undefined,\n userMessage: ''\n };\n let lastError: Error | undefined;\n\n // Phase 1: First attempt - no retry, just to classify the error type\n try {\n await this.processCrudEntry(entry);\n this.entryCooldowns.delete(entryKey); // Clear any previous cooldown on success\n return; // Success on first try\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n const classifiedError = classifySupabaseError(error);\n classified.isPermanent = classifiedError.isPermanent;\n classified.pgCode = classifiedError.pgCode;\n classified.userMessage = classifiedError.userMessage;\n\n // If first attempt failed with auth error, try refresh and one more attempt\n if (isAuthError(error)) {\n if (__DEV__) {\n console.log('[Connector] Auth error detected, refreshing session before retry:', {\n table: entry.table,\n op: entry.op,\n id: entry.id\n });\n }\n try {\n await this.supabase.auth.refreshSession();\n await this.processCrudEntry(entry);\n this.entryCooldowns.delete(entryKey); // Clear any previous cooldown on success\n return; // Success after token refresh\n } catch (retryError) {\n lastError = retryError instanceof Error ? retryError : new Error(String(retryError));\n const retriedClassification = classifySupabaseError(retryError);\n classified.isPermanent = retriedClassification.isPermanent;\n classified.pgCode = retriedClassification.pgCode;\n classified.userMessage = retriedClassification.userMessage;\n }\n }\n\n // Log the initial failure\n if (__DEV__) {\n console.log('[Connector] Initial attempt failed, will retry with appropriate config:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n isPermanent: classified.isPermanent,\n pgCode: classified.pgCode,\n userMessage: classified.userMessage\n });\n }\n this.logger?.warn('[Connector] Initial attempt failed:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n error: lastError.message,\n isPermanent: classified.isPermanent\n });\n\n // Run upload error middleware if configured\n if (this.uploadErrorMiddleware.length > 0) {\n const classifiedError = classifySupabaseError(error);\n const schema = this.schemaRouter(entry.table);\n const middlewareContext: UploadErrorContext = {\n entry,\n error: lastError,\n pgCode: classified.pgCode,\n httpStatusCode: extractHttpStatusCode(error),\n classified: classifiedError,\n originalError: error,\n schema,\n supabase: this.supabase\n };\n const middlewareResult = await runUploadErrorMiddleware(middlewareContext, this.uploadErrorMiddleware, 'continue' // Default to continue if no middleware matches\n );\n if (__DEV__) {\n console.log('[Connector] Upload error middleware result:', {\n table: entry.table,\n id: entry.id,\n result: middlewareResult\n });\n }\n switch (middlewareResult) {\n case 'success':\n // Treat as successful - clear cooldown and return\n this.entryCooldowns.delete(entryKey);\n this.logger?.info('[Connector] Entry treated as success by middleware:', {\n table: entry.table,\n id: entry.id,\n pgCode: classified.pgCode\n });\n return;\n case 'discard':\n // Discard entry - throw DiscardEntryError to signal removal from queue\n this.entryCooldowns.delete(entryKey);\n this.logger?.warn('[Connector] Entry discarded by middleware:', {\n table: entry.table,\n id: entry.id,\n pgCode: classified.pgCode,\n error: lastError.message\n });\n throw new DiscardEntryError(`Entry discarded by middleware: ${entry.table}:${entry.id} (${classified.pgCode || 'unknown'})`);\n case 'retry':\n // Continue with retry flow (fall through to Phase 2)\n break;\n case 'continue':\n // Use default classification (fall through to Phase 2)\n break;\n case 'fail_transaction':\n // Abort entire transaction immediately\n this.logger?.error('[Connector] Transaction aborted by middleware:', {\n table: entry.table,\n id: entry.id,\n pgCode: classified.pgCode,\n error: lastError.message\n });\n throw new TransactionAbortError(`Transaction aborted by middleware: ${entry.table}:${entry.id}`, lastError);\n }\n }\n }\n\n // Phase 2: Select config based on classified error type, then retry\n // RLS errors (42501) get extended retries since parent data may need to sync first\n const isRlsPermissionError = lastError && isRlsError(lastError);\n const selectedConfig = isRlsPermissionError ? this.retryConfig.rls : classified.isPermanent ? this.retryConfig.permanent : this.retryConfig.transient;\n if (__DEV__ && isRlsPermissionError) {\n console.log('[Connector] RLS/permission error detected, using extended retry config:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n maxRetries: selectedConfig.maxRetries,\n baseDelayMs: selectedConfig.baseDelayMs,\n maxDelayMs: selectedConfig.maxDelayMs\n });\n }\n try {\n await withExponentialBackoff(async () => {\n await this.processCrudEntry(entry);\n }, selectedConfig, {\n onRetry: (attempt, delay, error) => {\n this.logger?.debug('[Connector] Retry attempt:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n attempt,\n delay,\n error: error.message\n });\n if (__DEV__) {\n console.log('[Connector] Retry attempt:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n attempt,\n maxRetries: selectedConfig.maxRetries,\n delayMs: delay,\n errorMessage: error.message\n });\n }\n }\n });\n // Retries succeeded - clear any cooldown\n this.entryCooldowns.delete(entryKey);\n } catch (error) {\n // All retries exhausted - let PowerSync's native retry mechanism handle it\n const finalError = error instanceof Error ? error : new Error(String(error));\n\n // Determine error category for logging\n const category: 'transient' | 'permanent' | 'unknown' = classified.isPermanent ? 'permanent' : classified.pgCode ? 'transient' : 'unknown';\n if (__DEV__) {\n console.log('[Connector] CRUD entry failed after retries, leaving in ps_crud:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n category,\n error: finalError.message\n });\n }\n this.logger?.error('[Connector] CRUD entry failed after retries:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n error: finalError.message,\n category\n });\n\n // Set cooldown to prevent rapid retries from PowerSync's sync cycle\n // This gives time for parent data to sync or conditions to change\n const cooldownMs = SupabaseConnector.COOLDOWN_DURATION_MS;\n this.entryCooldowns.set(entryKey, Date.now() + cooldownMs);\n if (__DEV__) {\n console.log('[Connector] Entry placed in cooldown:', {\n table: entry.table,\n id: entry.id,\n cooldownSec: cooldownMs / 1000\n });\n }\n\n // ALWAYS throw after exhausting retries to prevent transaction.complete()\n // from removing the entry from PowerSync's queue. This ensures:\n // 1. Data stays in PowerSync's durable SQLite queue\n // 2. PowerSync's native retry mechanism will retry the transaction\n // 3. No reliance on AsyncStorage persistence (which is fire-and-forget)\n //\n // For transient errors: PowerSync will retry the whole transaction later\n // For permanent errors: User intervention needed, but data isn't lost\n throw finalError;\n }\n }\n\n /**\n * Set the active project IDs for scoped sync.\n * Call this when user selects/opens projects.\n */\n setActiveProjectIds(projectIds: string[]): void {\n this.activeProjectIds = projectIds;\n }\n\n /**\n * Get the current active project IDs.\n */\n getActiveProjectIds(): string[] {\n return this.activeProjectIds;\n }\n\n /**\n * Get credentials for PowerSync connection.\n * Uses Supabase session token.\n *\n * Note: Token refresh is handled by Supabase's startAutoRefresh() which must be\n * called on app initialization. getSession() returns the auto-refreshed token.\n */\n async fetchCredentials(): Promise<PowerSyncCredentials> {\n this.logger?.debug('[Connector] Fetching credentials...');\n const {\n data: {\n session\n },\n error\n } = await this.supabase.auth.getSession();\n if (error) {\n this.logger?.error('[Connector] Auth error:', error);\n throw new Error(`Failed to get Supabase session: ${error.message}`);\n }\n if (!session) {\n this.logger?.error('[Connector] No active session');\n throw new Error('No active Supabase session');\n }\n this.logger?.debug('[Connector] Credentials fetched, token expires at:', session.expires_at);\n return {\n endpoint: this.powerSyncUrl,\n token: session.access_token,\n expiresAt: session.expires_at ? new Date(session.expires_at * 1000) : undefined\n };\n }\n\n /**\n * Upload local changes to Supabase.\n * Called automatically by PowerSync when there are pending uploads.\n *\n * When conflict detection is enabled:\n * 1. Checks if table has _version column (cached)\n * 2. If yes, compares local vs server version\n * 3. On version mismatch, queries AuditLog for field conflicts\n * 4. If conflicts found, calls handler or publishes to conflict bus\n * 5. Applies resolution or skips entry based on handler response\n */\n async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {\n // P4.3: Check if connector has been destroyed\n if (this.isDestroyed) {\n throw new Error('Connector destroyed - aborting upload');\n }\n\n // Network reachability gate - check FIRST, before any network calls\n // This enables instant blocking when offline without waiting for timeouts\n // CRITICAL: Must THROW (not return) to keep entries in ps_crud for retry\n // A silent return would cause PowerSync to call complete() and discard the transaction\n if (this.shouldUploadFn && !this.shouldUploadFn()) {\n this.logger?.debug('[Connector] Upload blocked - not currently permitted, will retry');\n throw new Error('Upload not permitted - will retry on next sync cycle');\n }\n\n // CRITICAL: Ensure session is fresh before uploading\n // This prevents RLS failures (42501) from stale/expired tokens\n // Without this, auth.uid() in RLS policies may return NULL or fail\n const {\n data: {\n session\n },\n error: sessionError\n } = await this.supabase.auth.getSession();\n if (sessionError || !session) {\n const noSessionError = new Error('No active session - cannot upload data');\n if (__DEV__) {\n console.error('[Connector] uploadData failed: no session');\n }\n throw noSessionError;\n }\n if (__DEV__) {\n console.log('[Connector] uploadData called, fetching next CRUD transaction...');\n }\n const transaction = await database.getNextCrudTransaction();\n\n // P1.2: Validate transaction is not empty\n if (!transaction || transaction.crud.length === 0) {\n if (__DEV__) {\n console.log('[Connector] No pending CRUD transaction found or transaction is empty');\n }\n return;\n }\n if (__DEV__) {\n console.log('[Connector] Transaction fetched:', {\n crudCount: transaction.crud.length,\n entries: transaction.crud.map(e => ({\n table: e.table,\n op: e.op,\n id: e.id,\n opDataKeys: e.opData ? Object.keys(e.opData) : []\n }))\n });\n }\n\n // If conflict detection is disabled, use standard upload\n const conflictDetectionEnabled = this.conflictDetection?.enabled !== false;\n if (!conflictDetectionEnabled) {\n await this.processTransaction(transaction, database);\n return;\n }\n\n // Process with conflict detection\n const {\n crud\n } = transaction;\n const skipTables = new Set(this.conflictDetection?.skipTables ?? []);\n const entriesToProcess: CrudEntry[] = [];\n // Entries queued for UI resolution - these should NOT complete the transaction (leave in queue)\n const entriesQueuedForUI: CrudEntry[] = [];\n // Entries resolved with keep-server - these WILL complete the transaction (discard local changes)\n const entriesDiscarded: CrudEntry[] = [];\n\n // Track partial resolutions to re-emit remaining conflicts after sync completes\n const partialResolutions: Array<{\n originalConflict: ConflictCheckResult;\n syncedFields: string[];\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 existing resolution from a previous retry\n // This prevents re-detecting conflicts that the user already resolved\n // Key by table:recordId to handle same UUID in different tables\n const resolutionKey = `${entry.table}:${entry.id}`;\n const existingResolution = this.resolvedConflicts.get(resolutionKey);\n if (existingResolution) {\n if (__DEV__) {\n console.log('[Connector] Applying stored resolution for retry:', {\n table: entry.table,\n id: entry.id,\n key: resolutionKey,\n resolution: existingResolution\n });\n }\n // Remove from stored resolutions (single use)\n this.resolvedConflicts.delete(resolutionKey);\n switch (existingResolution.action) {\n case 'overwrite':\n // Proceed with upload (overwrite server)\n entriesToProcess.push(entry);\n break;\n case 'keep-server':\n // Discard local changes - mark for completion (removes from queue)\n entriesDiscarded.push(entry);\n break;\n case 'partial':\n // P5.3: Validate that partial resolution has at least one field\n if (!existingResolution.fields || existingResolution.fields.length === 0) {\n throw new Error('Partial resolution requires at least one field');\n }\n // Only sync specified fields\n const partialEntry: CrudEntry = {\n ...entry,\n opData: this.filterFields(entry.opData ?? {}, existingResolution.fields)\n };\n entriesToProcess.push(partialEntry);\n break;\n }\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.schemaRouter(entry.table);\n const serverVersion = await fetchServerVersion(entry.table, entry.id, schema, this.supabase);\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(entry.table, entry.id, localVersion, serverVersion, entry.opData ?? {}, this.supabase, this.conflictDetection);\n if (!conflictResult.hasConflict) {\n entriesToProcess.push(entry);\n continue;\n }\n\n // Emit conflict event to bus if available\n this.conflictBus?.emitConflict(conflictResult);\n\n // Handle conflict with handler if available\n if (this.conflictHandler) {\n const resolution = await this.conflictHandler.onConflict(conflictResult);\n if (resolution === null) {\n // Queue for UI - skip this entry, leave in queue for later resolution\n entriesQueuedForUI.push(entry);\n if (__DEV__) {\n console.log('[Connector] 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 switch (resolution.action) {\n case 'overwrite':\n // Proceed with upload (overwrite server)\n entriesToProcess.push(entry);\n break;\n case 'keep-server':\n // Discard local changes - mark for completion (removes from queue)\n entriesDiscarded.push(entry);\n if (__DEV__) {\n console.log('[Connector] Conflict resolved with keep-server, discarding local changes:', {\n table: entry.table,\n id: entry.id\n });\n }\n break;\n case 'partial':\n // P5.3: Validate that partial resolution has at least one field\n if (!resolution.fields || resolution.fields.length === 0) {\n throw new Error('Partial resolution requires at least one field');\n }\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\n // Track this partial resolution to re-emit remaining conflicts after sync\n partialResolutions.push({\n originalConflict: conflictResult,\n syncedFields: resolution.fields\n });\n break;\n }\n } else {\n // No handler - log conflict and proceed with upload\n this.logger?.warn('[Connector] 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 any entries are queued for UI resolution, we must NOT complete the transaction.\n // Per PowerSync docs, uploadData() must THROW on failure to keep entries in ps_crud.\n // A silent return would cause PowerSync to think upload succeeded and remove entries.\n if (entriesQueuedForUI.length > 0) {\n if (__DEV__) {\n console.log('[Connector] Entries queued for UI resolution, leaving in queue:', {\n queuedForUI: entriesQueuedForUI.length,\n discarded: entriesDiscarded.length,\n toProcess: entriesToProcess.length\n });\n }\n // Fire onTransactionComplete to notify listeners, but NOT onTransactionSuccess\n // because we're not completing the transaction. Entries remain in the queue.\n this.onTransactionComplete?.(entriesQueuedForUI);\n // MUST throw to prevent PowerSync from removing entries from ps_crud\n throw new Error('Entries queued for UI resolution - retry will occur on next sync cycle');\n }\n\n // If all entries were discarded (keep-server) with none to process,\n // still complete the transaction to remove them from the queue\n if (entriesToProcess.length === 0 && entriesDiscarded.length > 0) {\n if (__DEV__) {\n console.log('[Connector] All entries resolved with keep-server, completing transaction to discard local changes');\n }\n try {\n await transaction.complete();\n // Fire success (entries were \"successfully\" discarded)\n this.onTransactionSuccess?.(entriesDiscarded);\n this.onTransactionComplete?.(entriesDiscarded);\n } catch (error) {\n const classified = classifySupabaseError(error);\n this.onTransactionFailure?.(entriesDiscarded, error instanceof Error ? error : new Error(String(error)), classified);\n throw error;\n }\n return;\n }\n\n // Process remaining entries with retry logic\n const successfulEntries: CrudEntry[] = [];\n\n // CRITICAL: Process entries atomically - STOP on first failure\n // PowerSync transactions must be atomic: either ALL entries succeed or NONE are committed\n // If we continue after a failure, we violate atomicity and risk partial uploads\n for (const entry of entriesToProcess) {\n if (__DEV__) {\n console.log('[Connector] Processing CRUD entry with retry:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n opData: entry.opData\n });\n }\n\n // No try-catch here - let errors propagate immediately to stop processing\n // This keeps the ENTIRE transaction in ps_crud for retry\n await this.processWithRetry(entry);\n successfulEntries.push(entry);\n }\n\n // Finalize the transaction using the shared helper\n await this.finalizeTransaction({\n transaction,\n successfulEntries,\n discardedEntries: entriesDiscarded,\n partialResolutions\n });\n }\n\n /**\n * Finalize a transaction by completing it after all entries processed successfully.\n * Extracted to eliminate duplication between uploadData and processTransaction.\n *\n * Implements circuit breaker logic for completion failures:\n * - On first failure: retry once with extended timeout (60s)\n * - After 3 failures for same entries: log and return without throwing\n * (data is safely in Supabase via idempotent upserts, preventing infinite retry loop)\n *\n * @param context - The finalization context containing results and transaction\n */\n private async finalizeTransaction(context: TransactionFinalizationContext): Promise<void> {\n const {\n transaction,\n successfulEntries,\n discardedEntries = [],\n partialResolutions = []\n } = context;\n if (__DEV__) {\n console.log('[Connector] All CRUD entries processed, completing transaction...');\n }\n\n // Generate fingerprint for tracking completion failures\n const allEntries = [...successfulEntries, ...discardedEntries];\n const fingerprint = this.generateTransactionFingerprint(allEntries);\n\n // Check existing failure count for circuit breaker\n const failureInfo = this.completionFailures.get(fingerprint);\n const currentFailureCount = failureInfo?.count ?? 0;\n\n // Circuit breaker: if we've failed COMPLETION_MAX_FAILURES times, don't throw\n // Data is already in Supabase (upserts are idempotent), so we log and return\n if (currentFailureCount >= SupabaseConnector.COMPLETION_MAX_FAILURES) {\n this.logger?.warn('[Connector] Circuit breaker triggered - completion failed too many times, bypassing throw:', {\n fingerprint: fingerprint.substring(0, 50) + '...',\n failureCount: currentFailureCount,\n entriesCount: allEntries.length,\n message: 'Data is safely in Supabase. Returning without throw to prevent infinite retry loop.'\n });\n if (__DEV__) {\n console.warn('[Connector] CIRCUIT BREAKER: Completion failed', currentFailureCount, 'times. Data is in Supabase - returning without throw to break retry loop.');\n }\n\n // Clear the failure tracking since we're breaking out\n this.completionFailures.delete(fingerprint);\n\n // Still notify callbacks that we \"succeeded\" (data is in Supabase)\n this.onTransactionSuccess?.(successfulEntries);\n this.onTransactionComplete?.(successfulEntries);\n return;\n }\n try {\n // P1.5: Add timeout on transaction.complete() to prevent indefinite hangs\n await withTimeout(transaction.complete(), 30000, 'Transaction complete timeout');\n if (__DEV__) {\n console.log('[Connector] Transaction completed successfully:', {\n entriesCount: successfulEntries.length,\n discardedCount: discardedEntries.length\n });\n }\n\n // Clear completion failure tracking on success\n this.completionFailures.delete(fingerprint);\n\n // P3.1: Clear resolved conflicts for successful entries to prevent unbounded growth\n for (const entry of successfulEntries) {\n this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);\n }\n for (const entry of discardedEntries) {\n this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);\n }\n\n // P3.1: Apply LRU eviction if map grows too large\n if (this.resolvedConflicts.size > 100) {\n const oldest = [...this.resolvedConflicts.keys()][0];\n if (oldest) {\n this.resolvedConflicts.delete(oldest);\n }\n }\n\n // After partial sync completes, re-emit conflicts for remaining fields\n if (this.conflictBus && partialResolutions.length > 0) {\n for (const {\n originalConflict,\n syncedFields\n } of partialResolutions) {\n const syncedFieldSet = new Set(syncedFields);\n\n // Find conflicts that weren't synced\n const remainingConflicts = originalConflict.conflicts.filter(c => !syncedFieldSet.has(c.field));\n if (remainingConflicts.length > 0) {\n if (__DEV__) {\n console.log('[Connector] Re-emitting conflict for remaining fields:', {\n table: originalConflict.table,\n recordId: originalConflict.recordId,\n syncedFields,\n remainingConflictFields: remainingConflicts.map(c => c.field)\n });\n }\n\n // Emit new conflict for remaining fields\n this.conflictBus.emitConflict({\n ...originalConflict,\n conflicts: remainingConflicts,\n // All remaining are conflicts now - clear nonConflictingChanges since\n // the non-conflicting ones were already synced in the partial resolution\n nonConflictingChanges: []\n });\n }\n }\n }\n\n // P1.4: Notify success AFTER complete() to ensure data is durably persisted\n this.onTransactionSuccess?.(successfulEntries);\n\n // Notify completion (after transaction.complete() succeeds)\n this.onTransactionComplete?.(successfulEntries);\n } catch (error) {\n const classified = classifySupabaseError(error);\n const newFailureCount = currentFailureCount + 1;\n this.logger?.error('[Connector] Transaction completion FAILED:', {\n errorMessage: error instanceof Error ? error.message : String(error),\n classified,\n isPermanent: classified.isPermanent,\n failureCount: newFailureCount,\n maxFailures: SupabaseConnector.COMPLETION_MAX_FAILURES,\n entries: successfulEntries.map(e => ({\n table: e.table,\n op: e.op,\n id: e.id\n }))\n });\n\n // Additional debug info in development\n if (__DEV__) {\n console.error('[Connector] Transaction completion error details:', {\n errorKeys: error && typeof error === 'object' ? Object.keys(error) : [],\n errorObject: JSON.stringify(error, null, 2),\n failureCount: newFailureCount\n });\n }\n\n // On FIRST failure, retry once with extended timeout before incrementing counter\n if (currentFailureCount === 0) {\n if (__DEV__) {\n console.log('[Connector] First completion failure - retrying with extended timeout (60s)...');\n }\n try {\n await withTimeout(transaction.complete(), SupabaseConnector.COMPLETION_EXTENDED_TIMEOUT_MS, 'Transaction complete extended timeout');\n\n // Extended retry succeeded!\n if (__DEV__) {\n console.log('[Connector] Transaction completed on extended retry');\n }\n\n // Clear any tracking and notify success\n this.completionFailures.delete(fingerprint);\n this.onTransactionSuccess?.(successfulEntries);\n this.onTransactionComplete?.(successfulEntries);\n return;\n } catch (retryError) {\n // Extended retry also failed - update failure count and continue to throw\n if (__DEV__) {\n console.warn('[Connector] Extended retry also failed:', {\n error: retryError instanceof Error ? retryError.message : String(retryError)\n });\n }\n }\n }\n\n // Update failure tracking\n this.completionFailures.set(fingerprint, {\n count: newFailureCount,\n lastAttempt: new Date()\n });\n\n // Clean up old failure entries (LRU-style) to prevent unbounded growth\n if (this.completionFailures.size > 50) {\n const oldestKey = [...this.completionFailures.keys()][0];\n if (oldestKey) {\n this.completionFailures.delete(oldestKey);\n }\n }\n this.logger?.error('[Connector] Transaction completion error:', {\n error,\n classified,\n failureCount: newFailureCount,\n willRetry: newFailureCount < SupabaseConnector.COMPLETION_MAX_FAILURES,\n entries: successfulEntries.map(e => ({\n table: e.table,\n op: e.op,\n id: e.id\n }))\n });\n\n // Notify failure with classification\n this.onTransactionFailure?.(successfulEntries, error instanceof Error ? error : new Error(String(error)), classified);\n\n // Re-throw for PowerSync's native retry mechanism\n // (Circuit breaker at top of function will catch repeated failures)\n throw error;\n }\n }\n\n /**\n * Process a transaction without conflict detection.\n * Uses batched operations for PUT and DELETE, individual processing for PATCH.\n */\n private async processTransaction(transaction: {\n crud: CrudEntry[];\n complete: () => Promise<void>;\n }, _database: AbstractPowerSyncDatabase): Promise<void> {\n const successfulEntries: CrudEntry[] = [];\n const discardedEntries: CrudEntry[] = [];\n\n // Group entries by table for batched processing\n const entriesByTable = groupEntriesByTable(transaction.crud);\n if (__DEV__) {\n console.log('[Connector] Processing transaction with batching:', {\n totalEntries: transaction.crud.length,\n tables: Array.from(entriesByTable.keys()),\n entriesPerTable: Object.fromEntries(Array.from(entriesByTable.entries()).map(([table, entries]) => [table, {\n total: entries.length,\n put: entries.filter(e => e.op === UpdateType.PUT).length,\n patch: entries.filter(e => e.op === UpdateType.PATCH).length,\n delete: entries.filter(e => e.op === UpdateType.DELETE).length\n }]))\n });\n }\n\n // Process each table's entries\n for (const [table, entries] of entriesByTable) {\n const schema = this.schemaRouter(table);\n\n // Separate entries by operation type\n const putEntries = entries.filter(e => e.op === UpdateType.PUT);\n const patchEntries = entries.filter(e => e.op === UpdateType.PATCH);\n const deleteEntries = entries.filter(e => e.op === UpdateType.DELETE);\n\n // Check if custom handler exists - if so, process individually\n // CRITICAL: Only catch DiscardEntryError - other errors propagate to maintain atomicity\n if (this.crudHandler) {\n // Process all entries individually when custom handler is configured\n for (const entry of entries) {\n try {\n await this.processWithRetry(entry);\n successfulEntries.push(entry);\n } catch (error) {\n if (error instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - treat as \"completed\"\n discardedEntries.push(entry);\n continue;\n }\n // Other errors propagate to maintain transaction atomicity\n throw error;\n }\n }\n continue;\n }\n\n // Batch PUT operations (upserts)\n // CRITICAL: Errors propagate immediately to maintain transaction atomicity\n if (putEntries.length > 0) {\n const successful = await this.processBatchedPuts(table, schema, putEntries);\n successfulEntries.push(...successful);\n }\n\n // Batch DELETE operations\n // CRITICAL: Errors propagate immediately to maintain transaction atomicity\n if (deleteEntries.length > 0) {\n const successful = await this.processBatchedDeletes(table, schema, deleteEntries);\n successfulEntries.push(...successful);\n }\n\n // PATCH operations still need individual processing (different fields per entry)\n // CRITICAL: Only catch DiscardEntryError - other errors propagate to maintain atomicity\n for (const entry of patchEntries) {\n if (__DEV__) {\n console.log('[Connector] Processing PATCH entry individually:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n opData: entry.opData\n });\n }\n try {\n await this.processWithRetry(entry);\n successfulEntries.push(entry);\n } catch (error) {\n if (error instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - treat as \"completed\"\n discardedEntries.push(entry);\n continue;\n }\n // Other errors propagate to maintain transaction atomicity\n throw error;\n }\n }\n }\n\n // Finalize the transaction using the shared helper\n // Both successful and discarded entries are included for transaction completion\n // (discarded entries should be removed from queue, just like successful ones)\n await this.finalizeTransaction({\n transaction,\n successfulEntries,\n discardedEntries: discardedEntries.length > 0 ? discardedEntries : undefined\n });\n }\n\n /**\n * Process batched PUT (upsert) operations for a single table.\n * Falls back to individual processing if batch fails.\n * CRITICAL: Throws on first failure to maintain transaction atomicity.\n * @returns Array of successfully processed entries\n * @throws Error on first failure - keeps entire transaction in ps_crud\n */\n private async processBatchedPuts(table: string, schema: string, entries: CrudEntry[]): Promise<CrudEntry[]> {\n const successful: CrudEntry[] = [];\n if (__DEV__) {\n console.log('[Connector] Processing batched PUTs:', {\n schema,\n table,\n count: entries.length\n });\n }\n\n // Build the rows for upsert\n const rows = entries.map(entry => ({\n id: entry.id,\n ...entry.opData\n }));\n\n // Get the correct Supabase query builder for this table's schema\n const query = schema === 'public' ? this.supabase.from(table) : (this.supabase.schema(schema) as unknown as ReturnType<typeof this.supabase.schema>).from(table);\n const {\n error\n } = await query.upsert(rows, {\n onConflict: 'id'\n });\n if (error) {\n if (__DEV__) {\n console.warn('[Connector] Batched PUT failed, falling back to individual processing:', {\n schema,\n table,\n error: error.message\n });\n }\n\n // Batch failed - fall back to individual processing\n // CRITICAL: Only catch DiscardEntryError - other errors propagate to maintain atomicity\n for (const entry of entries) {\n try {\n await this.processWithRetry(entry);\n successful.push(entry);\n } catch (e) {\n if (e instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - treat as \"completed\"\n successful.push(entry);\n continue;\n }\n // Other errors propagate to maintain transaction atomicity\n throw e;\n }\n }\n } else {\n if (__DEV__) {\n console.log('[Connector] Batched PUT successful:', {\n schema,\n table,\n count: entries.length\n });\n }\n successful.push(...entries);\n }\n return successful;\n }\n\n /**\n * Process batched DELETE operations for a single table.\n * Falls back to individual processing if batch fails.\n * CRITICAL: Throws on first failure to maintain transaction atomicity.\n * @returns Array of successfully processed entries\n * @throws Error on first failure - keeps entire transaction in ps_crud\n */\n private async processBatchedDeletes(table: string, schema: string, entries: CrudEntry[]): Promise<CrudEntry[]> {\n const successful: CrudEntry[] = [];\n if (__DEV__) {\n console.log('[Connector] Processing batched DELETEs:', {\n schema,\n table,\n count: entries.length\n });\n }\n\n // Collect all IDs for batch delete\n const ids = entries.map(entry => entry.id);\n\n // Get the correct Supabase query builder for this table's schema\n const query = schema === 'public' ? this.supabase.from(table) : (this.supabase.schema(schema) as unknown as ReturnType<typeof this.supabase.schema>).from(table);\n const {\n error\n } = await query.delete().in('id', ids);\n if (error) {\n if (__DEV__) {\n console.warn('[Connector] Batched DELETE failed, falling back to individual processing:', {\n schema,\n table,\n error: error.message\n });\n }\n\n // Batch failed - fall back to individual processing\n // CRITICAL: Only catch DiscardEntryError - other errors propagate to maintain atomicity\n for (const entry of entries) {\n try {\n await this.processWithRetry(entry);\n successful.push(entry);\n } catch (e) {\n if (e instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - treat as \"completed\"\n successful.push(entry);\n continue;\n }\n // Other errors propagate to maintain transaction atomicity\n throw e;\n }\n }\n } else {\n if (__DEV__) {\n console.log('[Connector] Batched DELETE successful:', {\n schema,\n table,\n count: entries.length\n });\n }\n successful.push(...entries);\n }\n return successful;\n }\n\n /**\n * Check if a table has a _version column (cached).\n * P4.1: Uses Promise-based locking to prevent duplicate concurrent queries.\n */\n private async checkVersionColumn(table: string, db: AbstractPowerSyncDatabase): Promise<boolean> {\n // Return cached result if available\n if (this.versionColumnCache.has(table)) {\n return this.versionColumnCache.get(table)!;\n }\n\n // Check if there's already a pending query for this table\n if (!this.versionColumnPromises.has(table)) {\n // Create a new promise for this table's query\n this.versionColumnPromises.set(table, hasVersionColumn(table, db).then(result => {\n this.versionColumnCache.set(table, result);\n this.versionColumnPromises.delete(table);\n return result;\n }).catch(error => {\n // Clean up promise on error so retries are possible\n this.versionColumnPromises.delete(table);\n throw error;\n }));\n }\n\n // Wait for the (possibly shared) promise\n return this.versionColumnPromises.get(table)!;\n }\n\n /**\n * Filter opData to only include specified fields.\n * Used for partial sync resolution.\n */\n private filterFields(opData: Record<string, unknown>, fields: string[]): 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 operation.\n *\n * All synced tables use `id` as their UUID primary key column.\n */\n private async processCrudEntry(entry: CrudEntry): Promise<void> {\n const table = entry.table;\n const id = entry.id; // PowerSync sends UUID as the entry.id\n const schema = this.schemaRouter(table);\n\n // P5.1: Skip empty PATCH operations that would result in no-op updates\n if (entry.op === UpdateType.PATCH && Object.keys(entry.opData ?? {}).length === 0) {\n this.logger?.debug(`[Connector] Skipping empty PATCH for ${entry.table}:${entry.id}`);\n return; // Empty PATCH is a no-op - treating as success to clear from queue\n }\n\n // P5.2: Validate payload size to prevent Supabase failures with unhelpful errors\n if (entry.opData) {\n const payloadSize = JSON.stringify(entry.opData).length;\n if (payloadSize > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(`Payload too large (${(payloadSize / 1024).toFixed(0)}KB > 900KB) for ${schema}.${table}`);\n }\n }\n\n // Try custom handler first\n if (this.crudHandler) {\n let handled = false;\n switch (entry.op) {\n case UpdateType.PUT:\n handled = (await this.crudHandler.handlePut?.(entry, this.supabase, schema)) ?? false;\n break;\n case UpdateType.PATCH:\n handled = (await this.crudHandler.handlePatch?.(entry, this.supabase, schema)) ?? false;\n break;\n case UpdateType.DELETE:\n handled = (await this.crudHandler.handleDelete?.(entry, this.supabase, schema)) ?? false;\n break;\n }\n if (handled) {\n this.logger?.debug(`[Connector] Custom handler processed ${entry.op} for ${schema}.${table}`);\n return;\n }\n }\n\n // Default behavior\n // Get the correct Supabase query builder for this table's schema\n const query = schema === 'public' ? this.supabase.from(table) : (this.supabase.schema(schema) as unknown as ReturnType<typeof this.supabase.schema>).from(table);\n switch (entry.op) {\n case UpdateType.PUT:\n if (__DEV__) {\n console.log('[Connector] Executing PUT/UPSERT:', {\n schema,\n table,\n id,\n data: {\n id,\n ...entry.opData\n }\n });\n }\n // Insert/upsert using id column\n const {\n data: upsertData,\n error: upsertError\n } = await query.upsert({\n id,\n ...entry.opData\n }, {\n onConflict: 'id'\n }).select();\n if (upsertError) {\n if (__DEV__) {\n console.error('[Connector] PUT/UPSERT FAILED:', {\n schema,\n table,\n id,\n error: upsertError,\n errorMessage: upsertError.message,\n errorCode: upsertError.code,\n errorDetails: upsertError.details,\n errorHint: upsertError.hint\n });\n }\n throw new Error(`Upsert failed for ${schema}.${table}: ${upsertError.message}`);\n }\n if (__DEV__) {\n console.log('[Connector] PUT/UPSERT SUCCESS:', {\n schema,\n table,\n id,\n responseData: upsertData\n });\n }\n break;\n case UpdateType.PATCH:\n if (__DEV__) {\n console.log('[Connector] Executing PATCH/UPDATE:', {\n schema,\n table,\n id,\n opData: entry.opData\n });\n }\n // Update by id column\n const {\n data: updateData,\n error: updateError\n } = await query.update(entry.opData).eq('id', id).select();\n if (updateError) {\n if (__DEV__) {\n console.error('[Connector] PATCH/UPDATE FAILED:', {\n schema,\n table,\n id,\n error: updateError,\n errorMessage: updateError.message,\n errorCode: updateError.code,\n errorDetails: updateError.details,\n errorHint: updateError.hint\n });\n }\n throw new Error(`Update failed for ${schema}.${table}: ${updateError.message}`);\n }\n if (__DEV__) {\n console.log('[Connector] PATCH/UPDATE SUCCESS:', {\n schema,\n table,\n id,\n responseData: updateData\n });\n }\n break;\n case UpdateType.DELETE:\n if (__DEV__) {\n console.log('[Connector] Executing DELETE:', {\n schema,\n table,\n id\n });\n }\n // Delete by id column\n const {\n data: deleteData,\n error: deleteError\n } = await query.delete().eq('id', id).select();\n if (deleteError) {\n if (__DEV__) {\n console.error('[Connector] DELETE FAILED:', {\n schema,\n table,\n id,\n error: deleteError,\n errorMessage: deleteError.message,\n errorCode: deleteError.code,\n errorDetails: deleteError.details,\n errorHint: deleteError.hint\n });\n }\n throw new Error(`Delete failed for ${schema}.${table}: ${deleteError.message}`);\n }\n if (__DEV__) {\n console.log('[Connector] DELETE SUCCESS:', {\n schema,\n table,\n id,\n responseData: deleteData\n });\n }\n break;\n }\n this.logger?.debug(`[Connector] Processed ${entry.op} for ${schema}.${table} (id: ${id})`);\n }\n}"],"mappings":"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","names":["classifiedError"]}
@@ -1 +0,0 @@
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 +0,0 @@
1
- {"version":3,"sources":["../src/attachments/query-builder.ts","../src/attachments/migration.ts"],"sourcesContent":["/**\n * Query Builder for Attachment Watch Queries\n *\n * Generates SQL queries from WatchConfig objects.\n * Provides type-safe query generation without raw SQL strings.\n */\n\nimport type { WatchConfig } from './types';\n\n// ─── SQL Identifier Validation ────────────────────────────────────────────────\n\n/**\n * Valid SQL identifier pattern.\n * Allows alphanumeric characters and underscores, must start with letter or underscore.\n */\nconst VALID_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * SQL reserved words that cannot be used as identifiers (subset of common ones).\n */\nconst SQL_RESERVED_WORDS = new Set(['SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'AND', 'OR', 'NOT', 'NULL', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'TABLE', 'INDEX', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'DISTINCT', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'UNION', 'EXCEPT', 'INTERSECT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'TRUE', 'FALSE', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'ASC', 'DESC', 'NULLS', 'FIRST', 'LAST']);\n\n/**\n * Validates that a string is a safe SQL identifier.\n * Throws an error if the identifier is invalid or potentially dangerous.\n *\n * @param identifier - The identifier to validate\n * @param context - Description of where this identifier is used (for error messages)\n * @throws Error if identifier is invalid\n */\nexport function validateSqlIdentifier(identifier: string, context: string): void {\n if (!identifier || typeof identifier !== 'string') {\n throw new Error(`Invalid ${context}: must be a non-empty string`);\n }\n if (!VALID_IDENTIFIER_PATTERN.test(identifier)) {\n throw new Error(`Invalid ${context}: \"${identifier}\" contains invalid characters. ` + `Identifiers must start with a letter or underscore and contain only alphanumeric characters and underscores.`);\n }\n if (SQL_RESERVED_WORDS.has(identifier.toUpperCase())) {\n throw new Error(`Invalid ${context}: \"${identifier}\" is a SQL reserved word. ` + `Use a different name or quote the identifier.`);\n }\n\n // Additional safety: check for SQL injection patterns\n const dangerousPatterns = [/--/,\n // SQL comment\n /;/,\n // Statement terminator\n /'/,\n // String delimiter\n /\"/,\n // Quote\n /\\\\/ // Escape character\n ];\n for (const pattern of dangerousPatterns) {\n if (pattern.test(identifier)) {\n throw new Error(`Invalid ${context}: \"${identifier}\" contains potentially dangerous characters.`);\n }\n }\n}\n\n/**\n * Validates a WHERE clause fragment for basic safety.\n * Note: This is a best-effort validation. Complex WHERE clauses should be reviewed.\n *\n * @param whereClause - The WHERE clause fragment to validate\n * @throws Error if the clause contains dangerous patterns\n */\nexport function validateWhereClause(whereClause: string): void {\n if (!whereClause || typeof whereClause !== 'string') {\n throw new Error('Invalid WHERE clause: must be a non-empty string');\n }\n\n // Check for dangerous patterns\n const dangerousPatterns = [{\n pattern: /;\\s*(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)/i,\n name: 'SQL injection (statement)'\n }, {\n pattern: /UNION\\s+(ALL\\s+)?SELECT/i,\n name: 'UNION injection'\n }, {\n pattern: /--/,\n name: 'SQL comment'\n }, {\n pattern: /\\/\\*/,\n name: 'block comment'\n }, {\n pattern: /xp_|sp_|exec\\s*\\(/i,\n name: 'stored procedure'\n }];\n for (const {\n pattern,\n name\n } of dangerousPatterns) {\n if (pattern.test(whereClause)) {\n throw new Error(`Invalid WHERE clause: contains ${name}`);\n }\n }\n}\n\n// ─── Query Builder ────────────────────────────────────────────────────────────\n\n/**\n * Build a SQL watch query from a WatchConfig.\n *\n * Generates a SELECT statement that:\n * - Selects the ID column (aliased as `id`)\n * - Optionally selects additional columns\n * - Applies an optional WHERE clause\n * - Optionally orders by a column\n *\n * @param config - The WatchConfig to build a query from\n * @returns SQL query string\n *\n * @example\n * ```typescript\n * const query = buildWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * selectColumns: ['equipmentUnitId', 'takenOn'],\n * where: 'storagePath IS NOT NULL',\n * orderBy: { column: 'takenOn', direction: 'DESC' },\n * });\n *\n * // Result:\n * // SELECT storagePath AS id, equipmentUnitId, takenOn\n * // FROM EquipmentUnitMediaContent\n * // WHERE storagePath IS NOT NULL\n * // ORDER BY takenOn DESC\n * ```\n */\nexport function buildWatchQuery(config: WatchConfig): string {\n // Validate all identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(config.idColumn, 'idColumn');\n if (config.selectColumns) {\n for (const col of config.selectColumns) {\n validateSqlIdentifier(col, 'selectColumns');\n }\n }\n if (config.orderBy) {\n validateSqlIdentifier(config.orderBy.column, 'orderBy.column');\n if (config.orderBy.direction !== 'ASC' && config.orderBy.direction !== 'DESC') {\n throw new Error(`Invalid orderBy.direction: must be \"ASC\" or \"DESC\"`);\n }\n }\n if (config.where) {\n validateWhereClause(config.where);\n }\n\n // Build SELECT clause\n const selectParts: string[] = [`${config.idColumn} AS id`];\n if (config.selectColumns && config.selectColumns.length > 0) {\n selectParts.push(...config.selectColumns);\n }\n const selectClause = selectParts.join(', ');\n\n // Build FROM clause\n const fromClause = config.table;\n\n // Build WHERE clause\n let whereClause = `${config.idColumn} IS NOT NULL AND ${config.idColumn} != ''`;\n if (config.where) {\n whereClause = `${whereClause} AND (${config.where})`;\n }\n\n // Build ORDER BY clause\n let orderByClause = '';\n if (config.orderBy) {\n orderByClause = `ORDER BY ${config.orderBy.column} ${config.orderBy.direction}`;\n }\n\n // Assemble query\n const parts = [`SELECT ${selectClause}`, `FROM ${fromClause}`, `WHERE ${whereClause}`];\n if (orderByClause) {\n parts.push(orderByClause);\n }\n return parts.join('\\n');\n}\n\n/**\n * Build a simpler ID-only watch query.\n * Use this when you only need IDs without additional columns.\n *\n * @param config - The WatchConfig to build a query from\n * @returns SQL query string that selects only IDs\n *\n * @example\n * ```typescript\n * const query = buildIdOnlyWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * where: 'storagePath IS NOT NULL',\n * });\n *\n * // Result:\n * // SELECT storagePath AS id\n * // FROM EquipmentUnitMediaContent\n * // WHERE storagePath IS NOT NULL AND storagePath != ''\n * ```\n */\nexport function buildIdOnlyWatchQuery(config: WatchConfig): string {\n // Use the full builder but ignore selectColumns\n return buildWatchQuery({\n ...config,\n selectColumns: undefined\n });\n}\n\n/**\n * Build a query to fetch records with their IDs and additional columns.\n * Used for populating the BatchFilterContext.records map.\n *\n * @param config - The WatchConfig to build a query from\n * @param ids - Optional list of IDs to filter to (for efficiency)\n * @returns SQL query string and parameters\n *\n * @example\n * ```typescript\n * const { query, params } = buildRecordFetchQuery(\n * {\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * selectColumns: ['equipmentUnitId'],\n * },\n * ['path/to/file1.jpg', 'path/to/file2.jpg']\n * );\n * ```\n */\nexport function buildRecordFetchQuery(config: WatchConfig, ids?: string[]): {\n query: string;\n params: unknown[];\n} {\n // Validate identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(config.idColumn, 'idColumn');\n if (config.selectColumns) {\n for (const col of config.selectColumns) {\n validateSqlIdentifier(col, 'selectColumns');\n }\n }\n\n // Build SELECT clause - always include the ID column\n const selectParts: string[] = [`${config.idColumn} AS id`];\n if (config.selectColumns && config.selectColumns.length > 0) {\n selectParts.push(...config.selectColumns);\n }\n const selectClause = selectParts.join(', ');\n\n // Build query\n let query = `SELECT ${selectClause} FROM ${config.table}`;\n const params: unknown[] = [];\n\n // Add WHERE clause for IDs if provided\n if (ids && ids.length > 0) {\n const placeholders = ids.map(() => '?').join(', ');\n query += ` WHERE ${config.idColumn} IN (${placeholders})`;\n params.push(...ids);\n }\n return {\n query,\n params\n };\n}\n\n/**\n * Convert WatchConfig to the legacy AttachmentSourceConfig format.\n * Useful during migration to maintain backwards compatibility.\n *\n * @param watchConfig - The new WatchConfig\n * @returns Legacy AttachmentSourceConfig\n */\nexport function watchConfigToSourceConfig(watchConfig: WatchConfig): {\n table: string;\n idColumn: string;\n orderByColumn?: string | null;\n} {\n return {\n table: watchConfig.table,\n idColumn: watchConfig.idColumn,\n orderByColumn: watchConfig.orderBy?.column ?? null\n };\n}","/**\n * Migration Utilities for @pol-studios/powersync Attachments\n *\n * This module provides utilities for migrating from the old attachment API\n * to the new callback-based API. It includes:\n *\n * - State mapping constants for old → new state transitions\n * - `migrateAttachmentState()` for converting state values\n * - Validation helpers for migration safety\n *\n * @example\n * ```typescript\n * import { migrateAttachmentState, isValidAttachmentState } from '@pol-studios/powersync/attachments';\n *\n * // Migrate a single state value\n * const newState = migrateAttachmentState(oldState);\n *\n * // Validate before migration\n * if (isValidAttachmentState(value)) {\n * const migrated = migrateAttachmentState(value);\n * }\n * ```\n */\n\nimport { AttachmentState } from '@powersync/attachments';\nimport { PolAttachmentState } from './types';\n\n// ─── State Mapping Constants ──────────────────────────────────────────────────\n\n/**\n * Maps old state values to new state values.\n *\n * The official @powersync/attachments AttachmentState enum has values 0-4:\n * QUEUED_SYNC=0, QUEUED_UPLOAD=1, QUEUED_DOWNLOAD=2, SYNCED=3, ARCHIVED=4\n *\n * POL extensions add:\n * FAILED_PERMANENT=5, DOWNLOAD_SKIPPED=6\n *\n * For migration purposes, most states map 1:1. The mapping exists to:\n * 1. Document the relationship between old and new states\n * 2. Provide a clear upgrade path for custom state handling code\n * 3. Allow future state reorganization if needed\n */\nexport const STATE_MAPPING: ReadonlyMap<number, number> = new Map<number, number>([\n// Official states (1:1 mapping)\n[AttachmentState.QUEUED_SYNC as number, PolAttachmentState.QUEUED_SYNC], [AttachmentState.QUEUED_UPLOAD as number, PolAttachmentState.QUEUED_UPLOAD], [AttachmentState.QUEUED_DOWNLOAD as number, PolAttachmentState.QUEUED_DOWNLOAD], [AttachmentState.SYNCED as number, PolAttachmentState.SYNCED], [AttachmentState.ARCHIVED as number, PolAttachmentState.ARCHIVED],\n// POL extension states (identity mapping)\n[PolAttachmentState.FAILED_PERMANENT, PolAttachmentState.FAILED_PERMANENT], [PolAttachmentState.DOWNLOAD_SKIPPED, PolAttachmentState.DOWNLOAD_SKIPPED]]);\n\n/**\n * Human-readable names for attachment states.\n * Useful for logging and debugging during migration.\n */\nexport const STATE_NAMES: ReadonlyMap<number, string> = new Map([[PolAttachmentState.QUEUED_SYNC, 'QUEUED_SYNC'], [PolAttachmentState.QUEUED_UPLOAD, 'QUEUED_UPLOAD'], [PolAttachmentState.QUEUED_DOWNLOAD, 'QUEUED_DOWNLOAD'], [PolAttachmentState.SYNCED, 'SYNCED'], [PolAttachmentState.ARCHIVED, 'ARCHIVED'], [PolAttachmentState.FAILED_PERMANENT, 'FAILED_PERMANENT'], [PolAttachmentState.DOWNLOAD_SKIPPED, 'DOWNLOAD_SKIPPED']]);\n\n/**\n * All valid state values (official + POL extensions).\n */\nexport const VALID_STATES: ReadonlySet<number> = new Set([PolAttachmentState.QUEUED_SYNC, PolAttachmentState.QUEUED_UPLOAD, PolAttachmentState.QUEUED_DOWNLOAD, PolAttachmentState.SYNCED, PolAttachmentState.ARCHIVED, PolAttachmentState.FAILED_PERMANENT, PolAttachmentState.DOWNLOAD_SKIPPED]);\n\n/**\n * States that indicate an active upload workflow.\n * Records in these states should not be migrated to download states.\n */\nexport const UPLOAD_WORKFLOW_STATES: ReadonlySet<number> = new Set([PolAttachmentState.QUEUED_UPLOAD, PolAttachmentState.FAILED_PERMANENT]);\n\n/**\n * States that indicate an active download workflow.\n */\nexport const DOWNLOAD_WORKFLOW_STATES: ReadonlySet<number> = new Set([PolAttachmentState.QUEUED_DOWNLOAD, PolAttachmentState.QUEUED_SYNC]);\n\n/**\n * Terminal states (no further processing needed).\n */\nexport const TERMINAL_STATES: ReadonlySet<number> = new Set([PolAttachmentState.SYNCED, PolAttachmentState.ARCHIVED, PolAttachmentState.DOWNLOAD_SKIPPED]);\n\n// ─── Migration Functions ──────────────────────────────────────────────────────\n\n/**\n * Migrates an attachment state from the old API to the new API.\n *\n * Currently, this is a 1:1 mapping since the state values haven't changed.\n * This function exists to:\n * 1. Provide a clear migration path for apps using custom state handling\n * 2. Document the state relationship\n * 3. Allow future state reorganization without breaking existing code\n *\n * @param oldState - The state value from the old API\n * @returns The corresponding state value in the new API\n * @throws Error if the state value is invalid\n *\n * @example\n * ```typescript\n * import { migrateAttachmentState } from '@pol-studios/powersync/attachments';\n *\n * // Migrate a record's state\n * const newState = migrateAttachmentState(record.state);\n *\n * // Migrate with fallback for unknown states\n * const safeState = isValidAttachmentState(record.state)\n * ? migrateAttachmentState(record.state)\n * : PolAttachmentState.QUEUED_SYNC;\n * ```\n */\nexport function migrateAttachmentState(oldState: number): number {\n const newState = STATE_MAPPING.get(oldState);\n if (newState === undefined) {\n throw new Error(`Invalid attachment state: ${oldState}. ` + `Valid states are: ${Array.from(STATE_NAMES.entries()).map(([v, n]) => `${n}(${v})`).join(', ')}`);\n }\n return newState;\n}\n\n/**\n * Safely migrates an attachment state with a fallback.\n *\n * Unlike `migrateAttachmentState`, this function never throws.\n * Invalid states are mapped to the provided fallback.\n *\n * @param oldState - The state value from the old API\n * @param fallback - State to use if oldState is invalid (default: QUEUED_SYNC)\n * @returns The corresponding state value in the new API, or the fallback\n *\n * @example\n * ```typescript\n * // Safely migrate with QUEUED_SYNC as fallback\n * const state = migrateAttachmentStateSafe(unknownValue);\n *\n * // Use custom fallback\n * const state = migrateAttachmentStateSafe(unknownValue, PolAttachmentState.ARCHIVED);\n * ```\n */\nexport function migrateAttachmentStateSafe(oldState: number, fallback: number = PolAttachmentState.QUEUED_SYNC): number {\n const newState = STATE_MAPPING.get(oldState);\n return newState !== undefined ? newState : fallback;\n}\n\n// ─── Validation Helpers ───────────────────────────────────────────────────────\n\n/**\n * Checks if a value is a valid attachment state.\n *\n * @param value - The value to check\n * @returns true if the value is a valid attachment state\n *\n * @example\n * ```typescript\n * if (isValidAttachmentState(record.state)) {\n * // Safe to use record.state\n * } else {\n * console.warn(`Invalid state: ${record.state}`);\n * }\n * ```\n */\nexport function isValidAttachmentState(value: unknown): value is number {\n return typeof value === 'number' && VALID_STATES.has(value);\n}\n\n/**\n * Checks if a state represents an upload workflow.\n *\n * Records in upload workflow states should not be demoted to download states.\n *\n * @param state - The state to check\n * @returns true if the state is part of an upload workflow\n */\nexport function isUploadWorkflowState(state: number): boolean {\n return UPLOAD_WORKFLOW_STATES.has(state);\n}\n\n/**\n * Checks if a state represents a download workflow.\n *\n * @param state - The state to check\n * @returns true if the state is part of a download workflow\n */\nexport function isDownloadWorkflowState(state: number): boolean {\n return DOWNLOAD_WORKFLOW_STATES.has(state);\n}\n\n/**\n * Checks if a state is terminal (no further processing needed).\n *\n * @param state - The state to check\n * @returns true if the state is terminal\n */\nexport function isTerminalState(state: number): boolean {\n return TERMINAL_STATES.has(state);\n}\n\n/**\n * Gets the human-readable name of a state.\n *\n * @param state - The state value\n * @returns The state name, or \"UNKNOWN\" for invalid states\n *\n * @example\n * ```typescript\n * console.log(`State: ${getStateName(record.state)}`); // \"State: SYNCED\"\n * ```\n */\nexport function getStateName(state: number): string {\n return STATE_NAMES.get(state) ?? 'UNKNOWN';\n}\n\n// ─── Migration Report ─────────────────────────────────────────────────────────\n\n/**\n * Statistics about a batch migration.\n */\nexport interface MigrationStats {\n /** Total records processed */\n total: number;\n /** Records successfully migrated */\n migrated: number;\n /** Records with invalid states (used fallback) */\n invalid: number;\n /** Breakdown by state */\n byState: Map<number, number>;\n}\n\n/**\n * Creates empty migration stats.\n */\nexport function createMigrationStats(): MigrationStats {\n return {\n total: 0,\n migrated: 0,\n invalid: 0,\n byState: new Map()\n };\n}\n\n/**\n * Records a migration result in the stats.\n *\n * @param stats - The stats object to update\n * @param oldState - The original state\n * @param newState - The migrated state\n * @param wasValid - Whether the original state was valid\n */\nexport function recordMigration(stats: MigrationStats, oldState: number, newState: number, wasValid: boolean): void {\n stats.total++;\n if (wasValid) {\n stats.migrated++;\n } else {\n stats.invalid++;\n }\n stats.byState.set(newState, (stats.byState.get(newState) ?? 0) + 1);\n}\n\n/**\n * Formats migration stats as a human-readable summary.\n *\n * @param stats - The stats to format\n * @returns A formatted string summary\n *\n * @example\n * ```typescript\n * const stats = createMigrationStats();\n * // ... process records ...\n * console.log(formatMigrationStats(stats));\n * // Output:\n * // Migration Summary:\n * // Total: 100\n * // Migrated: 98\n * // Invalid (used fallback): 2\n * // By State:\n * // SYNCED: 50\n * // QUEUED_DOWNLOAD: 30\n * // QUEUED_UPLOAD: 18\n * // QUEUED_SYNC: 2\n * ```\n */\nexport function formatMigrationStats(stats: MigrationStats): string {\n const lines = ['Migration Summary:', ` Total: ${stats.total}`, ` Migrated: ${stats.migrated}`, ` Invalid (used fallback): ${stats.invalid}`, ' By State:'];\n for (const [state, count] of stats.byState.entries()) {\n lines.push(` ${getStateName(state)}: ${count}`);\n }\n return lines.join('\\n');\n}"],"mappings":"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","names":[]}