@pol-studios/powersync 1.0.30 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CacheSettingsManager-uz-kbnRH.d.ts → CacheSettingsManager-0H_7thHW.d.ts} +21 -3
- package/dist/attachments/index.d.ts +30 -30
- package/dist/attachments/index.js +13 -4
- package/dist/{background-sync-CVR3PkFi.d.ts → background-sync-BujnI3IR.d.ts} +1 -1
- package/dist/{chunk-RE5HWLCB.js → chunk-2RDWLXJW.js} +322 -103
- package/dist/chunk-2RDWLXJW.js.map +1 -0
- package/dist/{chunk-P4HZA6ZT.js → chunk-4665ZSE5.js} +2 -2
- package/dist/chunk-4665ZSE5.js.map +1 -0
- package/dist/{chunk-XOY2CJ67.js → chunk-4F5B5CZ7.js} +3 -3
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/{chunk-BC2SRII2.js → chunk-65A3SYJZ.js} +14 -1
- package/dist/chunk-65A3SYJZ.js.map +1 -0
- package/dist/chunk-6SZ64KCZ.js +755 -0
- package/dist/chunk-6SZ64KCZ.js.map +1 -0
- package/dist/{chunk-C2ACBYBZ.js → chunk-74TBHWJ4.js} +10 -96
- package/dist/{chunk-C2ACBYBZ.js.map → chunk-74TBHWJ4.js.map} +1 -1
- package/dist/chunk-ANXWYQEJ.js +1 -0
- package/dist/chunk-ANXWYQEJ.js.map +1 -0
- package/dist/{chunk-CAB26E6F.js → chunk-C4J4MLER.js} +29 -24
- package/dist/chunk-C4J4MLER.js.map +1 -0
- package/dist/{chunk-C5ODS3XH.js → chunk-EOW7JK7Q.js} +9 -16
- package/dist/chunk-EOW7JK7Q.js.map +1 -0
- package/dist/chunk-HRAVPIAZ.js +220 -0
- package/dist/chunk-HRAVPIAZ.js.map +1 -0
- package/dist/{chunk-XAEII4ZX.js → chunk-NUGQOTEM.js} +32 -4
- package/dist/chunk-NUGQOTEM.js.map +1 -0
- package/dist/chunk-OGUFUZSY.js +5415 -0
- package/dist/chunk-OGUFUZSY.js.map +1 -0
- package/dist/{chunk-JCGOZVWL.js → chunk-P4D6BQ4X.js} +115 -576
- package/dist/chunk-P4D6BQ4X.js.map +1 -0
- package/dist/{chunk-CACKC6XG.js → chunk-PGEDE6IM.js} +136 -89
- package/dist/chunk-PGEDE6IM.js.map +1 -0
- package/dist/{chunk-A4IBBWGO.js → chunk-RALHHPTU.js} +1 -1
- package/dist/chunk-RIDSPLE5.js +42 -0
- package/dist/chunk-RIDSPLE5.js.map +1 -0
- package/dist/{chunk-Z6VOBGTU.js → chunk-UOMHWUHV.js} +2 -12
- package/dist/chunk-UOMHWUHV.js.map +1 -0
- package/dist/{chunk-QREWE3NR.js → chunk-YONQYTVH.js} +2 -2
- package/dist/chunk-ZAN22NGL.js +13 -0
- package/dist/chunk-ZAN22NGL.js.map +1 -0
- package/dist/config/index.d.ts +200 -0
- package/dist/config/index.js +23 -0
- package/dist/config/index.js.map +1 -0
- package/dist/connector/index.d.ts +23 -5
- package/dist/connector/index.js +4 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -0
- package/dist/error/index.js +1 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.js +68 -36
- package/dist/index.native.d.ts +18 -14
- package/dist/index.native.js +73 -34
- package/dist/index.web.d.ts +17 -14
- package/dist/index.web.js +68 -36
- package/dist/maintenance/index.d.ts +2 -2
- package/dist/maintenance/index.js +3 -2
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js +2 -0
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.d.ts +1 -1
- package/dist/platform/index.native.js +1 -0
- package/dist/platform/index.web.d.ts +1 -1
- package/dist/platform/index.web.js +1 -0
- package/dist/pol-attachment-queue-DqBvLAEY.d.ts +255 -0
- package/dist/provider/index.d.ts +149 -114
- package/dist/provider/index.js +9 -14
- package/dist/provider/index.native.d.ts +108 -0
- package/dist/provider/index.native.js +121 -0
- package/dist/provider/index.native.js.map +1 -0
- package/dist/provider/index.web.d.ts +16 -0
- package/dist/provider/index.web.js +112 -0
- package/dist/provider/index.web.js.map +1 -0
- package/dist/react/index.d.ts +16 -65
- package/dist/react/index.js +2 -9
- package/dist/storage/index.d.ts +5 -4
- package/dist/storage/index.js +12 -9
- package/dist/storage/index.native.d.ts +5 -4
- package/dist/storage/index.native.js +8 -5
- package/dist/storage/index.web.d.ts +5 -4
- package/dist/storage/index.web.js +11 -8
- package/dist/storage/upload/index.d.ts +4 -3
- package/dist/storage/upload/index.js +4 -2
- package/dist/storage/upload/index.native.d.ts +4 -3
- package/dist/storage/upload/index.native.js +4 -2
- package/dist/storage/upload/index.web.d.ts +2 -1
- package/dist/storage/upload/index.web.js +4 -2
- package/dist/{supabase-connector-C4YpH_l3.d.ts → supabase-connector-HMxBA9Kg.d.ts} +2 -2
- package/dist/sync/index.d.ts +155 -20
- package/dist/sync/index.js +13 -3
- package/dist/{types-CyvBaAl8.d.ts → types-6QHGELuY.d.ts} +4 -1
- package/dist/{types-Dv1uf0LZ.d.ts → types-B9MptP7E.d.ts} +7 -10
- package/dist/types-BhAEsJj-.d.ts +330 -0
- package/dist/{types-D0WcHrq6.d.ts → types-CGMibJKD.d.ts} +8 -0
- package/dist/{types-CpM2_LhU.d.ts → types-DqJnP50o.d.ts} +6 -1
- package/dist/{pol-attachment-queue-BE2HU3Us.d.ts → types-JCEhw2Lf.d.ts} +139 -346
- package/package.json +18 -4
- package/dist/chunk-654ERHA7.js +0 -1
- package/dist/chunk-BC2SRII2.js.map +0 -1
- package/dist/chunk-C5ODS3XH.js.map +0 -1
- package/dist/chunk-CAB26E6F.js.map +0 -1
- package/dist/chunk-CACKC6XG.js.map +0 -1
- package/dist/chunk-FNYQFILT.js +0 -44
- package/dist/chunk-FNYQFILT.js.map +0 -1
- package/dist/chunk-JCGOZVWL.js.map +0 -1
- package/dist/chunk-P4HZA6ZT.js.map +0 -1
- package/dist/chunk-RBPWEOIV.js +0 -358
- package/dist/chunk-RBPWEOIV.js.map +0 -1
- package/dist/chunk-RE5HWLCB.js.map +0 -1
- package/dist/chunk-XAEII4ZX.js.map +0 -1
- package/dist/chunk-Z6VOBGTU.js.map +0 -1
- /package/dist/{chunk-XOY2CJ67.js.map → chunk-4F5B5CZ7.js.map} +0 -0
- /package/dist/{chunk-654ERHA7.js.map → chunk-5WRI5ZAA.js.map} +0 -0
- /package/dist/{chunk-A4IBBWGO.js.map → chunk-RALHHPTU.js.map} +0 -0
- /package/dist/{chunk-QREWE3NR.js.map → chunk-YONQYTVH.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 * @deprecated No longer used. The connector no longer has internal retry loops.\n * PowerSync SDK handles all retries naturally via its uploadData() recall mechanism.\n * Use `uploadErrorMiddleware` to classify errors as transient (retry) or permanent (fail).\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 * This is the single source of truth for error handling decisions.\n *\n * Middleware functions are called in order until one returns a non-'continue' result:\n * - 'success': Treat as completed (e.g., idempotent duplicate already exists)\n * - 'discard': Remove from queue silently (e.g., orphaned record)\n * - 'retry': Transient error - throw to let PowerSync retry on next sync cycle\n * - 'fail_transaction': Permanent error - record failure and surface to user\n * - 'continue': Pass to next middleware (default: uses isPermanent from classifySupabaseError)\n *\n * Architecture: No internal retry loops. PowerSync SDK handles all retries.\n * This keeps error handling simple and predictable.\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 * @deprecated No longer used. PowerSync SDK handles all retries.\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 * @deprecated No longer used. The connector no longer has internal retry loops.\n * PowerSync SDK handles all retries via its uploadData() recall mechanism.\n * Use `uploadErrorMiddleware` to classify errors as transient (retry) or permanent (fail).\n *\n * Kept for backwards compatibility only.\n */\nexport const DEFAULT_RETRY_CONFIG: RetryConfig = {\n transient: {\n maxRetries: 3,\n baseDelayMs: 1000,\n maxDelayMs: 4000,\n backoffMultiplier: 2\n },\n permanent: {\n maxRetries: 0,\n baseDelayMs: 1000,\n maxDelayMs: 1000,\n backoffMultiplier: 1\n },\n rls: {\n maxRetries: 0,\n baseDelayMs: 1000,\n maxDelayMs: 1000,\n backoffMultiplier: 1\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 * Error Handling Architecture (Simplified):\n * - NO internal retry loops - PowerSync SDK handles all retries naturally\n * - Middleware decides once: success/discard/retry/fail_transaction\n * - ALL errors throw to let PowerSync retry naturally\n * - ALL errors call onTransactionFailure (at outer level) for UI visibility\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, 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 } from './types';\nimport { classifySupabaseError, extractHttpStatusCode } from '../core/errors';\nimport { detectConflicts, fetchServerVersion, getLocalVersion, hasVersionColumn } from '../conflicts/detect';\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 // 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 static readonly MAX_STORED_RESOLUTIONS = 100;\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 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 // Apply LRU eviction before adding new entry to prevent unbounded growth\n if (this.resolvedConflicts.size >= SupabaseConnector.MAX_STORED_RESOLUTIONS) {\n const firstKey = this.resolvedConflicts.keys().next().value;\n if (firstKey) this.resolvedConflicts.delete(firstKey);\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 // ─── Simplified Entry Processing ───────────────────────────────────────────\n //\n // Architecture:\n // - NO internal retry loops - PowerSync SDK handles all retries naturally\n // - Middleware decides once: success/discard/retry/fail_transaction\n // - 'retry' = transient error, throw to let PowerSync retry on next sync cycle\n // - 'fail_transaction' = permanent error, throw immediately with failure callback\n // - 'discard' = silently skip via DiscardEntryError\n // - 'success' = treat as completed, return normally\n //\n // This keeps error handling simple and lets the user configure middleware\n // to decide what to do with each error type.\n\n /**\n * Process a single CRUD entry once (no internal retries).\n *\n * On failure, middleware classifies the error:\n * - 'success': Treat as completed (e.g., idempotent duplicate)\n * - 'discard': Remove from queue silently\n * - 'retry': Throw to let PowerSync retry on next sync cycle\n * - 'fail_transaction': Throw TransactionAbortError immediately\n * - 'continue': Throw original error (PowerSync will retry)\n *\n * @param entry - The CRUD entry to process\n * @throws Error for all errors (PowerSync will retry)\n * @throws TransactionAbortError when middleware returns 'fail_transaction'\n * @throws DiscardEntryError for discarded entries\n */\n private async processEntryOnce(entry: CrudEntry): Promise<void> {\n if (this.isDestroyed) {\n throw new Error('Connector destroyed');\n }\n try {\n await this.processCrudEntry(entry);\n return; // Success\n } catch (error) {\n const classifiedError = classifySupabaseError(error);\n const errorObj = error instanceof Error ? error : new Error(String(error));\n\n // If auth error, try refreshing session and retry once\n if (isAuthError(error)) {\n if (__DEV__) {\n console.log('[Connector] Auth error detected, refreshing session:', {\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 return; // Success after token refresh\n } catch (retryError) {\n // Continue to middleware with the retry error\n const retryClassified = classifySupabaseError(retryError);\n return this.handleEntryError(entry, retryError instanceof Error ? retryError : new Error(String(retryError)), retryClassified, retryError);\n }\n }\n return this.handleEntryError(entry, errorObj, classifiedError, error);\n }\n }\n\n /**\n * Handle an entry error by running middleware and deciding what to do.\n */\n private async handleEntryError(entry: CrudEntry, error: Error, classified: ClassifiedError, originalError: unknown): Promise<void> {\n // Log the failure\n if (__DEV__) {\n console.log('[Connector] Entry failed:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n isPermanent: classified.isPermanent,\n pgCode: classified.pgCode,\n message: error.message\n });\n }\n this.logger?.warn('[Connector] Entry failed:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n error: error.message,\n isPermanent: classified.isPermanent\n });\n\n // Run upload error middleware to classify the error\n let middlewareResult: 'success' | 'retry' | 'discard' | 'continue' | 'fail_transaction' = 'continue';\n if (this.uploadErrorMiddleware.length > 0) {\n const schema = this.schemaRouter(entry.table);\n const middlewareContext: UploadErrorContext = {\n entry,\n error,\n pgCode: classified.pgCode,\n httpStatusCode: extractHttpStatusCode(originalError),\n classified,\n originalError,\n schema,\n supabase: this.supabase\n };\n middlewareResult = await runUploadErrorMiddleware(middlewareContext, this.uploadErrorMiddleware, 'continue');\n if (__DEV__) {\n console.log('[Connector] Middleware result:', {\n table: entry.table,\n id: entry.id,\n result: middlewareResult\n });\n }\n }\n\n // Handle middleware result\n switch (middlewareResult) {\n case 'success':\n // Treat as successful - return normally\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 remove from queue\n this.logger?.warn('[Connector] Entry discarded by middleware:', {\n table: entry.table,\n id: entry.id,\n pgCode: classified.pgCode\n });\n throw new DiscardEntryError(`Entry discarded by middleware: ${entry.table}:${entry.id} (${classified.pgCode || 'unknown'})`);\n case 'fail_transaction':\n // Abort entire transaction immediately - permanent failure\n this.logger?.error('[Connector] Transaction aborted by middleware:', {\n table: entry.table,\n id: entry.id,\n pgCode: classified.pgCode\n });\n throw new TransactionAbortError(`Transaction aborted by middleware: ${entry.table}:${entry.id}`, error);\n case 'retry':\n // Transient error - throw to let PowerSync retry on next sync cycle\n if (__DEV__) {\n console.log('[Connector] Middleware returned retry - throwing for PowerSync retry:', {\n table: entry.table,\n id: entry.id\n });\n }\n throw error;\n case 'continue':\n default:\n // All errors throw to let PowerSync retry naturally\n // The outer catch block calls onTransactionFailure for UI visibility\n // Classification info (isPermanent) is still available in the callback for display\n if (__DEV__) {\n console.log('[Connector] Error - throwing for PowerSync retry:', {\n table: entry.table,\n id: entry.id,\n isPermanent: classified.isPermanent,\n pgCode: classified.pgCode\n });\n }\n throw error;\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 try {\n for (const entry of entriesToProcess) {\n if (__DEV__) {\n console.log('[Connector] Processing CRUD entry:', {\n table: entry.table,\n op: entry.op,\n id: entry.id,\n opData: entry.opData\n });\n }\n\n // Errors propagate immediately to stop processing\n // ALL errors: onTransactionFailure called in outer catch, entry stays in queue for retry\n await this.processEntryOnce(entry);\n successfulEntries.push(entry);\n }\n } catch (error) {\n // CRITICAL: Notify failure callback BEFORE re-throwing\n // This ensures RLS errors, validation errors, etc. are recorded as failed\n // transactions with the actual error message visible to users\n const classified = classifySupabaseError(error);\n this.onTransactionFailure?.(entriesToProcess, error instanceof Error ? error : new Error(String(error)), classified);\n // Re-throw to keep entries in ps_crud for retry\n throw error;\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 > SupabaseConnector.MAX_STORED_RESOLUTIONS) {\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 try {\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.processEntryOnce(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 {\n successful,\n discarded\n } = await this.processBatchedPuts(table, schema, putEntries);\n successfulEntries.push(...successful);\n discardedEntries.push(...discarded);\n }\n\n // Batch DELETE operations\n // CRITICAL: Errors propagate immediately to maintain transaction atomicity\n if (deleteEntries.length > 0) {\n const {\n successful,\n discarded\n } = await this.processBatchedDeletes(table, schema, deleteEntries);\n successfulEntries.push(...successful);\n discardedEntries.push(...discarded);\n }\n\n // Batch PATCH operations with parallel processing (different fields per entry)\n // CRITICAL: Errors propagate immediately to maintain transaction atomicity\n if (patchEntries.length > 0) {\n const {\n successful,\n discarded\n } = await this.processBatchedPatches(table, schema, patchEntries);\n successfulEntries.push(...successful);\n discardedEntries.push(...discarded);\n }\n }\n } catch (error) {\n // CRITICAL: Notify failure callback BEFORE re-throwing\n // This ensures RLS errors, validation errors, etc. are recorded as failed\n // transactions with the actual error message visible to users\n const classified = classifySupabaseError(error);\n this.onTransactionFailure?.(transaction.crud, error instanceof Error ? error : new Error(String(error)), classified);\n // Re-throw to keep entries in ps_crud for retry\n throw error;\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 Object containing successful and discarded 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<{\n successful: CrudEntry[];\n discarded: CrudEntry[];\n }> {\n const successful: CrudEntry[] = [];\n const discarded: 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.processEntryOnce(entry);\n successful.push(entry);\n } catch (e) {\n if (e instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - track separately\n discarded.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 {\n successful,\n discarded\n };\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 Object containing successful and discarded 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<{\n successful: CrudEntry[];\n discarded: CrudEntry[];\n }> {\n const successful: CrudEntry[] = [];\n const discarded: 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.processEntryOnce(entry);\n successful.push(entry);\n } catch (e) {\n if (e instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - track separately\n discarded.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 {\n successful,\n discarded\n };\n }\n\n /**\n * Process batched PATCH (update) operations for a single table.\n * Uses parallel processing with configurable concurrency since each PATCH may have different fields.\n * CRITICAL: Throws on first non-DiscardEntryError failure to maintain transaction atomicity.\n * @param batchSize Number of concurrent PATCH operations (default: 5)\n * @returns Object containing successful and discarded entries\n * @throws Error on first non-DiscardEntryError failure - keeps entire transaction in ps_crud\n */\n private async processBatchedPatches(table: string, schema: string, entries: CrudEntry[], batchSize: number = 5): Promise<{\n successful: CrudEntry[];\n discarded: CrudEntry[];\n }> {\n const successful: CrudEntry[] = [];\n const discarded: CrudEntry[] = [];\n if (__DEV__) {\n console.log('[Connector] Processing batched PATCHes:', {\n schema,\n table,\n count: entries.length,\n batchSize\n });\n }\n\n // Process in batches of batchSize concurrently\n // Uses Promise.all for fail-fast behavior on non-DiscardEntryError\n for (let i = 0; i < entries.length; i += batchSize) {\n const batch = entries.slice(i, i + batchSize);\n if (__DEV__) {\n console.log('[Connector] Processing PATCH batch:', {\n batchIndex: Math.floor(i / batchSize) + 1,\n totalBatches: Math.ceil(entries.length / batchSize),\n batchSize: batch.length\n });\n }\n\n // Marker type for tracking discarded vs successful entries\n type BatchResult = {\n status: 'success';\n entry: CrudEntry;\n } | {\n status: 'discarded';\n entry: CrudEntry;\n };\n\n // Use Promise.all for fail-fast behavior\n // Wrap each operation to catch DiscardEntryError (which is expected/acceptable)\n // while letting other errors propagate immediately\n const results = await Promise.all(batch.map(async (entry): Promise<BatchResult> => {\n try {\n await this.processEntryOnce(entry);\n return {\n status: 'success',\n entry\n };\n } catch (error) {\n if (error instanceof DiscardEntryError) {\n // Entry was intentionally discarded by middleware - treat as \"completed\"\n return {\n status: 'discarded',\n entry\n };\n }\n // Non-DiscardEntryError - rethrow to fail-fast\n if (__DEV__) {\n console.error('[Connector] PATCH batch failed with non-discard error:', {\n table,\n entryId: entry.id,\n error: error instanceof Error ? error.message : String(error)\n });\n }\n throw error;\n }\n }));\n\n // Process results - all completed successfully (or were discarded)\n for (const result of results) {\n if (result.status === 'success') {\n successful.push(result.entry);\n } else {\n discarded.push(result.entry);\n }\n }\n }\n if (__DEV__) {\n console.log('[Connector] Batched PATCH complete:', {\n schema,\n table,\n successfulCount: successful.length,\n discardedCount: discarded.length\n });\n }\n return {\n successful,\n discarded\n };\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 const upsertErr = new Error(`Upsert failed for ${schema}.${table}: ${upsertError.message}`);\n // Preserve PostgreSQL error code for uploadErrorMiddleware (e.g., '23505' for unique violation)\n (upsertErr as any).code = upsertError.code;\n throw upsertErr;\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 const updateErr = new Error(`Update failed for ${schema}.${table}: ${updateError.message}`);\n (updateErr as any).code = updateError.code;\n throw updateErr;\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 const deleteErr = new Error(`Delete failed for ${schema}.${table}: ${deleteError.message}`);\n (deleteErr as any).code = deleteError.code;\n throw deleteErr;\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":[]}
|
|
@@ -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 * Resolves the column name to use for the attachment path/ID.\n * Prefers `pathColumn` over `idColumn` when both are provided.\n *\n * @param config - The WatchConfig to resolve the column from\n * @returns The resolved column name\n * @throws Error if neither pathColumn nor idColumn is provided\n */\nfunction resolvePathColumn(config: WatchConfig): string {\n const column = config.pathColumn ?? config.idColumn;\n if (!column) {\n throw new Error('WatchConfig requires either pathColumn or idColumn. ' + 'pathColumn is preferred; idColumn is deprecated.');\n }\n return column;\n}\n\n/**\n * Build a SQL watch query from a WatchConfig.\n *\n * Generates a SELECT statement that:\n * - Selects the path 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 * // Using pathColumn (preferred)\n * const query = buildWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * pathColumn: '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 * // Using idColumn (deprecated, still works)\n * const query = buildWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * });\n * ```\n */\nexport function buildWatchQuery(config: WatchConfig): string {\n // Resolve path column (pathColumn ?? idColumn)\n const pathColumn = resolvePathColumn(config);\n\n // Validate all identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(pathColumn, 'pathColumn');\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[] = [`${pathColumn} 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 = `${pathColumn} IS NOT NULL AND ${pathColumn} != ''`;\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 * pathColumn: '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 // Resolve path column (pathColumn ?? idColumn)\n const pathColumn = resolvePathColumn(config);\n\n // Validate identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(pathColumn, 'pathColumn');\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 path column\n const selectParts: string[] = [`${pathColumn} 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 ${pathColumn} IN (${placeholders})`;\n params.push(...ids);\n }\n return {\n query,\n params\n };\n}\n\n/**\n * Convert WatchConfig to a legacy format for backwards compatibility.\n *\n * @param watchConfig - The WatchConfig to convert\n * @returns Object with table, pathColumn, and optional orderByColumn\n */\nexport function watchConfigToSourceConfig(watchConfig: WatchConfig): {\n table: string;\n pathColumn: string;\n /** @deprecated Use pathColumn instead */\n idColumn: string;\n orderByColumn?: string | null;\n} {\n const pathColumn = resolvePathColumn(watchConfig);\n return {\n table: watchConfig.table,\n pathColumn,\n idColumn: pathColumn,\n // For backwards compatibility\n orderByColumn: watchConfig.orderBy?.column ?? null\n };\n}\n\n// ─── Row Extraction Utility ────────────────────────────────────────────────────\n\n/**\n * Extracts an array of string IDs from PowerSync watch results.\n * Handles platform differences between React Native (`rows._array`) and web (`rows` as array).\n * Uses single-pass extraction for efficiency.\n *\n * @param results - The results object from db.watch() onResult callback\n * @returns Array of non-null string IDs\n */\nexport function extractIdsFromRows(results: {\n rows?: unknown;\n}): string[] {\n const rows = results.rows;\n if (!rows) return [];\n // React Native: { _array: Row[] }, Web: Row[]\n const rowArray = (rows as {\n _array?: unknown[];\n })?._array ?? rows;\n if (!Array.isArray(rowArray)) {\n return [];\n }\n\n // Single-pass extraction (more efficient than .map().filter())\n const ids: string[] = [];\n for (let i = 0; i < rowArray.length; i++) {\n const id = (rowArray[i] as {\n id?: string;\n })?.id;\n if (id) ids.push(id);\n }\n return ids;\n}\n\n// ─── Helper for Simple Attachment Configuration ────────────────────────────────\n\n/**\n * Creates a watchIds callback for simple single-table attachment configurations.\n *\n * This is a convenience function that generates a properly typed watchIds callback\n * from a table name and path column. Use this for simple cases where you don't need\n * JOINs or complex watch logic.\n *\n * @param table - The source table name\n * @param pathColumn - The column containing the attachment path/ID\n * @returns A properly typed watchIds callback for use in AttachmentSourceConfig\n *\n * @example\n * ```typescript\n * import { createWatchIds } from '@pol-studios/powersync/attachments';\n *\n * const config: AttachmentSourceConfig = {\n * bucket: 'photos',\n * watchIds: createWatchIds('EquipmentUnitMediaContent', 'storagePath'),\n * };\n * ```\n */\nexport function createWatchIds(table: string, pathColumn: string): (db: import('./types').PowerSyncDBInterface, onUpdate: (ids: string[]) => void) => () => void {\n // Validate identifiers and generate SQL at creation time (not on every callback)\n const sql = buildIdOnlyWatchQuery({\n table,\n pathColumn\n });\n return (db, onUpdate) => {\n const abortController = new AbortController();\n db.watch(sql, [], {\n onResult: results => onUpdate(extractIdsFromRows(results))\n }, {\n signal: abortController.signal\n });\n return () => abortController.abort();\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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/storage/upload/SupabaseUploadHandler.native.ts"],"sourcesContent":["/**\n * Supabase Upload Handler for React Native\n *\n * Implements UploadHandler interface using react-native-background-upload\n * for iOS background upload capability.\n */\n\nimport { Platform } from 'react-native';\nimport type { SupabaseStorageOptions } from '../types';\nimport { resolveBucket } from '../types';\nimport type { SupabaseUploadHandlerOptions, UploadNotificationConfig, BucketConfig } from './types';\nimport { DEFAULT_UPLOAD_NOTIFICATION } from './types';\nimport { AbortError } from '../../utils/retry';\n\n// Lazy-loaded module reference\nlet Upload: typeof import('react-native-background-upload').default | null = null;\n\n/**\n * Load react-native-background-upload lazily.\n * This prevents the module from being bundled on web.\n */\nasync function loadUploadModule(): Promise<typeof import('react-native-background-upload').default> {\n if (!Upload) {\n const module = await import('react-native-background-upload');\n Upload = module.default;\n }\n return Upload;\n}\n\n/**\n * React Native upload handler using react-native-background-upload.\n *\n * Features:\n * - iOS background upload support\n * - AbortSignal cancellation support\n * - Android notification support\n * - Signed URL-based uploads\n *\n * @example\n * ```typescript\n * const handler = createSupabaseUploadHandler(supabaseClient, {\n * defaultBucket: 'attachments',\n * bucketMap: new Map([['avatars/', 'user-avatars']]),\n * });\n *\n * await handler.uploadFile(\n * 'photos/image.jpg',\n * 'file:///path/to/image.jpg',\n * 'image/jpeg'\n * );\n * ```\n */\nexport class SupabaseUploadHandler {\n private supabase: any;\n private defaultBucket: string;\n private bucketMap?: Map<string, string>;\n private bucketResolver?: (storagePath: string) => string | undefined;\n private notificationConfig: UploadNotificationConfig;\n constructor(options: SupabaseUploadHandlerOptions, notificationConfig?: Partial<UploadNotificationConfig>) {\n this.supabase = options.supabaseClient;\n this.defaultBucket = options.bucketConfig.defaultBucket;\n this.bucketMap = options.bucketConfig.bucketMap;\n this.bucketResolver = options.bucketConfig.resolver;\n this.notificationConfig = {\n ...DEFAULT_UPLOAD_NOTIFICATION,\n ...notificationConfig\n };\n }\n\n /**\n * Upload a file to Supabase Storage using react-native-background-upload.\n * Supports iOS background uploads and AbortSignal for cancellation.\n */\n async uploadFile(storagePath: string, localFileUri: string, mediaType: string, signal?: AbortSignal): Promise<void> {\n // Check if already aborted\n if (signal?.aborted) {\n throw new AbortError();\n }\n const bucket = this.resolveBucket(storagePath);\n\n // 1. Get signed upload URL from Supabase\n const {\n data,\n error\n } = await this.supabase.storage.from(bucket).createSignedUploadUrl(storagePath);\n if (error || !data?.signedUrl) {\n throw new Error(error?.message || 'Failed to create signed upload URL');\n }\n\n // Check abort after async operation\n if (signal?.aborted) {\n throw new AbortError();\n }\n\n // Load the upload module\n const UploadModule = await loadUploadModule();\n\n // 2. Upload using react-native-background-upload\n return new Promise((resolve, reject) => {\n const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n let isSettled = false;\n\n // Setup listeners before starting\n const completedSub = UploadModule.addListener('completed', uploadId, (eventData: {\n responseCode: number;\n }) => {\n if (isSettled) return;\n isSettled = true;\n cleanup();\n if (eventData.responseCode >= 200 && eventData.responseCode < 300) {\n resolve();\n } else {\n reject(new Error(`Upload failed with status ${eventData.responseCode}`));\n }\n });\n const errorSub = UploadModule.addListener('error', uploadId, (eventData: {\n error: string;\n }) => {\n if (isSettled) return;\n isSettled = true;\n cleanup();\n reject(new Error(eventData.error));\n });\n const cancelledSub = UploadModule.addListener('cancelled', uploadId, () => {\n if (isSettled) return;\n isSettled = true;\n cleanup();\n reject(new AbortError());\n });\n const cleanup = () => {\n completedSub.remove();\n errorSub.remove();\n cancelledSub.remove();\n if (signal) {\n signal.removeEventListener('abort', abortHandler);\n }\n };\n\n // Register abort handler to cancel the upload\n const abortHandler = () => {\n if (isSettled) return;\n UploadModule.cancelUpload(uploadId).catch(() => {\n // Ignore errors when cancelling - the upload may have already completed\n });\n };\n if (signal) {\n signal.addEventListener('abort', abortHandler, {\n once: true\n });\n }\n\n // Determine platform for notification settings\n const isAndroid = Platform.OS === 'android';\n\n // Start the upload\n UploadModule.startUpload({\n url: data.signedUrl,\n path: localFileUri,\n method: 'PUT',\n type: 'raw',\n customUploadId: uploadId,\n headers: {\n 'Content-Type': mediaType\n },\n // Android notification settings\n ...(isAndroid && {\n notification: this.notificationConfig\n })\n }).catch((err: Error) => {\n if (isSettled) return;\n isSettled = true;\n cleanup();\n reject(err);\n });\n });\n }\n\n /**\n * Resolve the storage bucket for a given path.\n */\n resolveBucket(storagePath: string): string {\n return resolveBucket({\n defaultBucket: this.defaultBucket,\n bucketMap: this.bucketMap,\n bucketResolver: this.bucketResolver\n }, storagePath);\n }\n}\n\n/**\n * Factory function for creating a SupabaseUploadHandler.\n */\nexport function createSupabaseUploadHandler(supabaseClient: any, bucketConfig: BucketConfig, notificationConfig?: Partial<UploadNotificationConfig>): SupabaseUploadHandler {\n return new SupabaseUploadHandler({\n supabaseClient,\n bucketConfig\n }, notificationConfig);\n}"],"mappings":"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","names":[]}
|