@pol-studios/powersync 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/CacheSettingsManager-1exbOC6S.d.ts +261 -0
  2. package/dist/attachments/index.d.ts +65 -355
  3. package/dist/attachments/index.js +24 -6
  4. package/dist/{types-Cd7RhNqf.d.ts → background-sync-ChCXW-EV.d.ts} +53 -2
  5. package/dist/chunk-4C3RY5SU.js +204 -0
  6. package/dist/chunk-4C3RY5SU.js.map +1 -0
  7. package/dist/{chunk-3AYXHQ4W.js → chunk-53WH2JJV.js} +111 -47
  8. package/dist/chunk-53WH2JJV.js.map +1 -0
  9. package/dist/chunk-A4IBBWGO.js +377 -0
  10. package/dist/chunk-A4IBBWGO.js.map +1 -0
  11. package/dist/chunk-BREGB4WL.js +1768 -0
  12. package/dist/chunk-BREGB4WL.js.map +1 -0
  13. package/dist/{chunk-EJ23MXPQ.js → chunk-CGL33PL4.js} +3 -1
  14. package/dist/chunk-CGL33PL4.js.map +1 -0
  15. package/dist/chunk-DGUM43GV.js +11 -0
  16. package/dist/chunk-DHYUBVP7.js +131 -0
  17. package/dist/chunk-DHYUBVP7.js.map +1 -0
  18. package/dist/chunk-FV2HXEIY.js +124 -0
  19. package/dist/chunk-FV2HXEIY.js.map +1 -0
  20. package/dist/chunk-GKF7TOMT.js +1 -0
  21. package/dist/{chunk-OTJXIRWX.js → chunk-H772V6XQ.js} +304 -51
  22. package/dist/chunk-H772V6XQ.js.map +1 -0
  23. package/dist/{chunk-C2RSTGDC.js → chunk-HFOFLW5F.js} +525 -87
  24. package/dist/chunk-HFOFLW5F.js.map +1 -0
  25. package/dist/chunk-KGSFAE5B.js +1 -0
  26. package/dist/chunk-LNL64IJZ.js +1 -0
  27. package/dist/chunk-MKD2VCX3.js +32 -0
  28. package/dist/chunk-MKD2VCX3.js.map +1 -0
  29. package/dist/{chunk-7EMDVIZX.js → chunk-N75DEF5J.js} +19 -1
  30. package/dist/chunk-N75DEF5J.js.map +1 -0
  31. package/dist/chunk-P6WOZO7H.js +49 -0
  32. package/dist/chunk-P6WOZO7H.js.map +1 -0
  33. package/dist/chunk-TGBT5XBE.js +1 -0
  34. package/dist/chunk-TGBT5XBE.js.map +1 -0
  35. package/dist/chunk-UEYRTLKE.js +72 -0
  36. package/dist/chunk-UEYRTLKE.js.map +1 -0
  37. package/dist/chunk-WGHNIAF7.js +329 -0
  38. package/dist/chunk-WGHNIAF7.js.map +1 -0
  39. package/dist/chunk-WQ5MPAVC.js +449 -0
  40. package/dist/chunk-WQ5MPAVC.js.map +1 -0
  41. package/dist/{chunk-FPTDATY5.js → chunk-XQAJM2MW.js} +22 -11
  42. package/dist/chunk-XQAJM2MW.js.map +1 -0
  43. package/dist/chunk-YSTEESEG.js +676 -0
  44. package/dist/chunk-YSTEESEG.js.map +1 -0
  45. package/dist/chunk-ZEOKPWUC.js +1165 -0
  46. package/dist/chunk-ZEOKPWUC.js.map +1 -0
  47. package/dist/connector/index.d.ts +182 -2
  48. package/dist/connector/index.js +14 -3
  49. package/dist/core/index.d.ts +5 -3
  50. package/dist/core/index.js +5 -2
  51. package/dist/error/index.d.ts +54 -0
  52. package/dist/error/index.js +8 -0
  53. package/dist/error/index.js.map +1 -0
  54. package/dist/index.d.ts +237 -11
  55. package/dist/index.js +183 -27
  56. package/dist/index.native.d.ts +20 -9
  57. package/dist/index.native.js +183 -28
  58. package/dist/index.web.d.ts +20 -9
  59. package/dist/index.web.js +184 -28
  60. package/dist/maintenance/index.d.ts +118 -0
  61. package/dist/maintenance/index.js +17 -0
  62. package/dist/maintenance/index.js.map +1 -0
  63. package/dist/platform/index.d.ts +16 -1
  64. package/dist/platform/index.js +2 -0
  65. package/dist/platform/index.js.map +1 -1
  66. package/dist/platform/index.native.d.ts +2 -2
  67. package/dist/platform/index.native.js +2 -1
  68. package/dist/platform/index.web.d.ts +1 -1
  69. package/dist/platform/index.web.js +2 -1
  70. package/dist/pol-attachment-queue-C7YNXXhK.d.ts +676 -0
  71. package/dist/provider/index.d.ts +693 -12
  72. package/dist/provider/index.js +57 -12
  73. package/dist/storage/index.d.ts +6 -0
  74. package/dist/storage/index.js +28 -0
  75. package/dist/storage/index.js.map +1 -0
  76. package/dist/storage/index.native.d.ts +6 -0
  77. package/dist/storage/index.native.js +26 -0
  78. package/dist/storage/index.native.js.map +1 -0
  79. package/dist/storage/index.web.d.ts +6 -0
  80. package/dist/storage/index.web.js +26 -0
  81. package/dist/storage/index.web.js.map +1 -0
  82. package/dist/storage/upload/index.d.ts +55 -0
  83. package/dist/storage/upload/index.js +15 -0
  84. package/dist/storage/upload/index.js.map +1 -0
  85. package/dist/storage/upload/index.native.d.ts +57 -0
  86. package/dist/storage/upload/index.native.js +14 -0
  87. package/dist/storage/upload/index.native.js.map +1 -0
  88. package/dist/storage/upload/index.web.d.ts +5 -0
  89. package/dist/storage/upload/index.web.js +14 -0
  90. package/dist/storage/upload/index.web.js.map +1 -0
  91. package/dist/{index-Cb-NI0Ct.d.ts → supabase-connector-qLm-WHkM.d.ts} +146 -10
  92. package/dist/sync/index.d.ts +288 -22
  93. package/dist/sync/index.js +23 -5
  94. package/dist/types-BVacP54t.d.ts +52 -0
  95. package/dist/types-Bgvx7-E8.d.ts +187 -0
  96. package/dist/{types-afHtE1U_.d.ts → types-CDqWh56B.d.ts} +2 -0
  97. package/package.json +72 -2
  98. package/dist/chunk-32OLICZO.js +0 -1
  99. package/dist/chunk-3AYXHQ4W.js.map +0 -1
  100. package/dist/chunk-7EMDVIZX.js.map +0 -1
  101. package/dist/chunk-7JQZBZ5N.js +0 -1
  102. package/dist/chunk-C2RSTGDC.js.map +0 -1
  103. package/dist/chunk-EJ23MXPQ.js.map +0 -1
  104. package/dist/chunk-FPTDATY5.js.map +0 -1
  105. package/dist/chunk-GMFDCVMZ.js +0 -1285
  106. package/dist/chunk-GMFDCVMZ.js.map +0 -1
  107. package/dist/chunk-OLHGI472.js +0 -1
  108. package/dist/chunk-OTJXIRWX.js.map +0 -1
  109. package/dist/chunk-V6LJ6MR2.js +0 -740
  110. package/dist/chunk-V6LJ6MR2.js.map +0 -1
  111. package/dist/chunk-VJCL2SWD.js +0 -1
  112. /package/dist/{chunk-32OLICZO.js.map → chunk-DGUM43GV.js.map} +0 -0
  113. /package/dist/{chunk-7JQZBZ5N.js.map → chunk-GKF7TOMT.js.map} +0 -0
  114. /package/dist/{chunk-OLHGI472.js.map → chunk-KGSFAE5B.js.map} +0 -0
  115. /package/dist/{chunk-VJCL2SWD.js.map → chunk-LNL64IJZ.js.map} +0 -0
@@ -1,11 +1,70 @@
1
+ import {
2
+ withExponentialBackoff
3
+ } from "./chunk-FV2HXEIY.js";
1
4
  import {
2
5
  classifySupabaseError
3
- } from "./chunk-FPTDATY5.js";
6
+ } from "./chunk-XQAJM2MW.js";
4
7
 
5
8
  // src/connector/types.ts
6
9
  var defaultSchemaRouter = () => "public";
10
+ var DEFAULT_RETRY_CONFIG = {
11
+ transient: {
12
+ maxRetries: 3,
13
+ baseDelayMs: 1e3,
14
+ // 1 second initial delay
15
+ maxDelayMs: 4e3,
16
+ // 4 second cap
17
+ backoffMultiplier: 2
18
+ // 1s → 2s → 4s
19
+ },
20
+ permanent: {
21
+ maxRetries: 0,
22
+ // No retries - permanent errors won't succeed
23
+ baseDelayMs: 1e3,
24
+ // Irrelevant with 0 retries, but needed for type
25
+ maxDelayMs: 1e3,
26
+ // Irrelevant with 0 retries
27
+ backoffMultiplier: 1
28
+ // Irrelevant with 0 retries
29
+ }
30
+ };
7
31
 
8
32
  // src/conflicts/detect.ts
33
+ var ConflictDetectionError = class extends Error {
34
+ constructor(message, options) {
35
+ super(message);
36
+ this.name = "ConflictDetectionError";
37
+ this.cause = options?.cause;
38
+ }
39
+ };
40
+ function deepEqual(a, b, maxDepth = 10) {
41
+ if (a === b) return true;
42
+ if (typeof a === "number" && typeof b === "number") {
43
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
44
+ }
45
+ if (a === null || a === void 0 || b === null || b === void 0) return false;
46
+ if (typeof a !== typeof b) return false;
47
+ if (typeof a !== "object") return false;
48
+ if (maxDepth <= 0) {
49
+ console.warn("[deepEqual] Max depth reached, falling back to reference equality");
50
+ return a === b;
51
+ }
52
+ if (a instanceof Date && b instanceof Date) {
53
+ return a.getTime() === b.getTime();
54
+ }
55
+ if (a instanceof Date || b instanceof Date) return false;
56
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
57
+ const keysA = Object.keys(a);
58
+ const keysB = Object.keys(b);
59
+ if (keysA.length !== keysB.length) return false;
60
+ for (const key of keysA) {
61
+ if (!keysB.includes(key)) return false;
62
+ if (!deepEqual(a[key], b[key], maxDepth - 1)) {
63
+ return false;
64
+ }
65
+ }
66
+ return true;
67
+ }
9
68
  var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10
69
  function validateTableName(table) {
11
70
  if (!TABLE_NAME_REGEX.test(table)) {
@@ -37,14 +96,9 @@ async function detectConflicts(table, recordId, localVersion, serverVersion, pen
37
96
  ascending: false
38
97
  }).limit(20);
39
98
  if (error) {
40
- console.warn("[detectConflicts] Failed to query AuditLog:", error);
41
- return {
42
- hasConflict: false,
43
- conflicts: [],
44
- nonConflictingChanges: Object.keys(filteredPendingChanges),
45
- table,
46
- recordId
47
- };
99
+ throw new ConflictDetectionError(`AuditLog query failed for ${table}/${recordId}`, {
100
+ cause: error
101
+ });
48
102
  }
49
103
  const serverChanges = /* @__PURE__ */ new Map();
50
104
  for (const log of auditLogs ?? []) {
@@ -53,7 +107,7 @@ async function detectConflicts(table, recordId, localVersion, serverVersion, pen
53
107
  if (!oldRec || !newRec) continue;
54
108
  for (const [field, newValue] of Object.entries(newRec)) {
55
109
  if (ignoredFields.has(field)) continue;
56
- if (oldRec[field] !== newValue && !serverChanges.has(field)) {
110
+ if (!deepEqual(oldRec[field], newValue) && !serverChanges.has(field)) {
57
111
  serverChanges.set(field, {
58
112
  newValue,
59
113
  changedBy: log.changeBy,
@@ -113,6 +167,62 @@ async function getLocalVersion(table, recordId, db) {
113
167
  }
114
168
 
115
169
  // src/connector/supabase-connector.ts
170
+ var ValidationError = class extends Error {
171
+ constructor(message) {
172
+ super(message);
173
+ this.name = "ValidationError";
174
+ }
175
+ };
176
+ function isAuthError(error) {
177
+ if (!error) return false;
178
+ if (typeof error === "object" && error !== null) {
179
+ const statusCode = error.status ?? error.statusCode;
180
+ if (statusCode === 401 || statusCode === 403) {
181
+ return true;
182
+ }
183
+ const code = error.code;
184
+ if (code === "401" || code === "403" || code === "42501") {
185
+ return true;
186
+ }
187
+ }
188
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
189
+ const authPatterns = [
190
+ /\bjwt\s+(expired|invalid|malformed)\b/,
191
+ /\btoken\s+(expired|invalid|revoked)\b/,
192
+ /\bsession\s+(expired|invalid)\b/,
193
+ /\bunauthorized\s*(access|request|error)?\b/,
194
+ /\baccess\s+(denied|forbidden)\b/,
195
+ /\bnot\s+authorized\b/,
196
+ /\bauth(entication)?\s+(failed|error|required)\b/,
197
+ /\bpermission\s+denied\b/,
198
+ /\binvalid\s+(credentials|api[_\s]?key)\b/,
199
+ /\brls\b.*\bpolicy\b/,
200
+ /\brow[_\s]?level[_\s]?security\b/,
201
+ /\b42501\b/
202
+ // PostgreSQL insufficient privilege error code
203
+ ];
204
+ return authPatterns.some((pattern) => pattern.test(message));
205
+ }
206
+ function withTimeout(promise, ms, message) {
207
+ let timeoutId;
208
+ const timeoutPromise = new Promise((_, reject) => {
209
+ timeoutId = setTimeout(() => reject(new Error(message)), ms);
210
+ });
211
+ return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
212
+ }
213
+ var MAX_PAYLOAD_SIZE = 900 * 1024;
214
+ function groupEntriesByTable(entries) {
215
+ const grouped = /* @__PURE__ */ new Map();
216
+ for (const entry of entries) {
217
+ const existing = grouped.get(entry.table);
218
+ if (existing) {
219
+ existing.push(entry);
220
+ } else {
221
+ grouped.set(entry.table, [entry]);
222
+ }
223
+ }
224
+ return grouped;
225
+ }
116
226
  var SupabaseConnector = class {
117
227
  supabase;
118
228
  powerSyncUrl;
@@ -136,6 +246,13 @@ var SupabaseConnector = class {
136
246
  resolvedConflicts = /* @__PURE__ */ new Map();
137
247
  // Cleanup function for resolution listener subscription
138
248
  unsubscribeResolution;
249
+ // Promise-based locking for version column checks to prevent duplicate queries
250
+ versionColumnPromises = /* @__PURE__ */ new Map();
251
+ // Flag to track if connector has been destroyed
252
+ isDestroyed = false;
253
+ // Retry configuration
254
+ retryConfig;
255
+ autoRetryPaused = false;
139
256
  constructor(options) {
140
257
  this.supabase = options.supabaseClient;
141
258
  this.powerSyncUrl = options.powerSyncUrl;
@@ -149,6 +266,16 @@ var SupabaseConnector = class {
149
266
  this.conflictDetection = options.conflictDetection;
150
267
  this.conflictHandler = options.conflictHandler;
151
268
  this.conflictBus = options.conflictBus;
269
+ this.retryConfig = {
270
+ transient: {
271
+ ...DEFAULT_RETRY_CONFIG.transient,
272
+ ...options.retryConfig?.transient
273
+ },
274
+ permanent: {
275
+ ...DEFAULT_RETRY_CONFIG.permanent,
276
+ ...options.retryConfig?.permanent
277
+ }
278
+ };
152
279
  if (this.conflictBus) {
153
280
  this.unsubscribeResolution = this.conflictBus.onResolution((table, recordId, resolution) => {
154
281
  const key = `${table}:${recordId}`;
@@ -169,11 +296,153 @@ var SupabaseConnector = class {
169
296
  * Call this when the connector is no longer needed.
170
297
  */
171
298
  destroy() {
299
+ this.isDestroyed = true;
172
300
  if (this.unsubscribeResolution) {
173
301
  this.unsubscribeResolution();
174
302
  this.unsubscribeResolution = void 0;
175
303
  }
176
304
  this.resolvedConflicts.clear();
305
+ this.versionColumnPromises.clear();
306
+ }
307
+ // ─── Retry Control Methods ─────────────────────────────────────────────────
308
+ /**
309
+ * Pause automatic retry of failed uploads.
310
+ * Use this when the user goes offline intentionally or wants manual control.
311
+ */
312
+ pauseAutoRetry() {
313
+ this.autoRetryPaused = true;
314
+ if (__DEV__) {
315
+ console.log("[Connector] Auto-retry paused");
316
+ }
317
+ }
318
+ /**
319
+ * Resume automatic retry of failed uploads.
320
+ */
321
+ resumeAutoRetry() {
322
+ this.autoRetryPaused = false;
323
+ if (__DEV__) {
324
+ console.log("[Connector] Auto-retry resumed");
325
+ }
326
+ }
327
+ // ─── Private Retry Logic ───────────────────────────────────────────────────
328
+ /**
329
+ * Process a single CRUD entry with exponential backoff retry.
330
+ *
331
+ * This method uses a two-phase approach:
332
+ * 1. First attempt - try the operation to classify the error type
333
+ * 2. Retry phase - use the appropriate config (transient vs permanent) based on classification
334
+ *
335
+ * This fixes the issue where reassigning selectedConfig inside withExponentialBackoff's
336
+ * callback had no effect because the config was already destructured at call time.
337
+ *
338
+ * @param entry - The CRUD entry to process
339
+ * @throws Error if all retries exhausted (for critical failures)
340
+ */
341
+ async processWithRetry(entry) {
342
+ if (this.isDestroyed) {
343
+ throw new Error("Connector destroyed");
344
+ }
345
+ const classified = {
346
+ isPermanent: false,
347
+ pgCode: void 0,
348
+ userMessage: ""
349
+ };
350
+ let lastError;
351
+ try {
352
+ await this.processCrudEntry(entry);
353
+ return;
354
+ } catch (error) {
355
+ lastError = error instanceof Error ? error : new Error(String(error));
356
+ const classifiedError = classifySupabaseError(error);
357
+ classified.isPermanent = classifiedError.isPermanent;
358
+ classified.pgCode = classifiedError.pgCode;
359
+ classified.userMessage = classifiedError.userMessage;
360
+ if (isAuthError(error)) {
361
+ if (__DEV__) {
362
+ console.log("[Connector] Auth error detected, refreshing session before retry:", {
363
+ table: entry.table,
364
+ op: entry.op,
365
+ id: entry.id
366
+ });
367
+ }
368
+ try {
369
+ await this.supabase.auth.refreshSession();
370
+ await this.processCrudEntry(entry);
371
+ return;
372
+ } catch (retryError) {
373
+ lastError = retryError instanceof Error ? retryError : new Error(String(retryError));
374
+ const retriedClassification = classifySupabaseError(retryError);
375
+ classified.isPermanent = retriedClassification.isPermanent;
376
+ classified.pgCode = retriedClassification.pgCode;
377
+ classified.userMessage = retriedClassification.userMessage;
378
+ }
379
+ }
380
+ if (__DEV__) {
381
+ console.log("[Connector] Initial attempt failed, will retry with appropriate config:", {
382
+ table: entry.table,
383
+ op: entry.op,
384
+ id: entry.id,
385
+ isPermanent: classified.isPermanent,
386
+ pgCode: classified.pgCode,
387
+ userMessage: classified.userMessage
388
+ });
389
+ }
390
+ this.logger?.warn("[Connector] Initial attempt failed:", {
391
+ table: entry.table,
392
+ op: entry.op,
393
+ id: entry.id,
394
+ error: lastError.message,
395
+ isPermanent: classified.isPermanent
396
+ });
397
+ }
398
+ const selectedConfig = classified.isPermanent ? this.retryConfig.permanent : this.retryConfig.transient;
399
+ try {
400
+ await withExponentialBackoff(async () => {
401
+ await this.processCrudEntry(entry);
402
+ }, selectedConfig, {
403
+ onRetry: (attempt, delay, error) => {
404
+ this.logger?.debug("[Connector] Retry attempt:", {
405
+ table: entry.table,
406
+ op: entry.op,
407
+ id: entry.id,
408
+ attempt,
409
+ delay,
410
+ error: error.message
411
+ });
412
+ if (__DEV__) {
413
+ console.log("[Connector] Retry attempt:", {
414
+ table: entry.table,
415
+ op: entry.op,
416
+ id: entry.id,
417
+ attempt,
418
+ maxRetries: selectedConfig.maxRetries,
419
+ delayMs: delay,
420
+ errorMessage: error.message
421
+ });
422
+ }
423
+ }
424
+ });
425
+ } catch (error) {
426
+ const finalError = error instanceof Error ? error : new Error(String(error));
427
+ const category = classified.isPermanent ? "permanent" : classified.pgCode ? "transient" : "unknown";
428
+ if (__DEV__) {
429
+ console.log("[Connector] CRUD entry failed after retries, leaving in ps_crud:", {
430
+ table: entry.table,
431
+ op: entry.op,
432
+ id: entry.id,
433
+ category,
434
+ error: finalError.message
435
+ });
436
+ }
437
+ this.logger?.error("[Connector] CRUD entry failed after retries:", {
438
+ table: entry.table,
439
+ op: entry.op,
440
+ id: entry.id,
441
+ error: finalError.message,
442
+ category
443
+ });
444
+ throw finalError;
445
+ }
177
446
  }
178
447
  /**
179
448
  * Set the active project IDs for scoped sync.
@@ -230,20 +499,33 @@ var SupabaseConnector = class {
230
499
  * 5. Applies resolution or skips entry based on handler response
231
500
  */
232
501
  async uploadData(database) {
502
+ if (this.isDestroyed) {
503
+ throw new Error("Connector destroyed - aborting upload");
504
+ }
233
505
  if (this.shouldUploadFn && !this.shouldUploadFn()) {
506
+ this.logger?.debug("[Connector] Upload blocked - not currently permitted, will retry");
507
+ throw new Error("Upload not permitted - will retry on next sync cycle");
508
+ }
509
+ const {
510
+ data: {
511
+ session
512
+ },
513
+ error: sessionError
514
+ } = await this.supabase.auth.getSession();
515
+ if (sessionError || !session) {
516
+ const noSessionError = new Error("No active session - cannot upload data");
234
517
  if (__DEV__) {
235
- console.log("[Connector] Upload skipped - sync mode does not allow uploads");
518
+ console.error("[Connector] uploadData failed: no session");
236
519
  }
237
- this.logger?.debug("[Connector] Upload skipped - sync mode does not allow uploads");
238
- return;
520
+ throw noSessionError;
239
521
  }
240
522
  if (__DEV__) {
241
523
  console.log("[Connector] uploadData called, fetching next CRUD transaction...");
242
524
  }
243
525
  const transaction = await database.getNextCrudTransaction();
244
- if (!transaction) {
526
+ if (!transaction || transaction.crud.length === 0) {
245
527
  if (__DEV__) {
246
- console.log("[Connector] No pending CRUD transaction found");
528
+ console.log("[Connector] No pending CRUD transaction found or transaction is empty");
247
529
  }
248
530
  return;
249
531
  }
@@ -300,6 +582,9 @@ var SupabaseConnector = class {
300
582
  entriesDiscarded.push(entry);
301
583
  break;
302
584
  case "partial":
585
+ if (!existingResolution.fields || existingResolution.fields.length === 0) {
586
+ throw new Error("Partial resolution requires at least one field");
587
+ }
303
588
  const partialEntry = {
304
589
  ...entry,
305
590
  opData: this.filterFields(entry.opData ?? {}, existingResolution.fields)
@@ -354,6 +639,9 @@ var SupabaseConnector = class {
354
639
  }
355
640
  break;
356
641
  case "partial":
642
+ if (!resolution.fields || resolution.fields.length === 0) {
643
+ throw new Error("Partial resolution requires at least one field");
644
+ }
357
645
  const partialEntry = {
358
646
  ...entry,
359
647
  opData: this.filterFields(entry.opData ?? {}, resolution.fields)
@@ -366,7 +654,7 @@ var SupabaseConnector = class {
366
654
  break;
367
655
  }
368
656
  } else {
369
- console.warn("[Connector] Conflict detected but no handler:", {
657
+ this.logger?.warn("[Connector] Conflict detected but no handler:", {
370
658
  table: entry.table,
371
659
  id: entry.id,
372
660
  conflicts: conflictResult.conflicts
@@ -383,7 +671,7 @@ var SupabaseConnector = class {
383
671
  });
384
672
  }
385
673
  this.onTransactionComplete?.(entriesQueuedForUI);
386
- return;
674
+ throw new Error("Entries queued for UI resolution - retry will occur on next sync cycle");
387
675
  }
388
676
  if (entriesToProcess.length === 0 && entriesDiscarded.length > 0) {
389
677
  if (__DEV__) {
@@ -400,28 +688,62 @@ var SupabaseConnector = class {
400
688
  }
401
689
  return;
402
690
  }
403
- try {
404
- for (const entry of entriesToProcess) {
405
- if (__DEV__) {
406
- console.log("[Connector] Processing CRUD entry:", {
407
- table: entry.table,
408
- op: entry.op,
409
- id: entry.id,
410
- opData: entry.opData
411
- });
412
- }
413
- await this.processCrudEntry(entry);
414
- }
691
+ const successfulEntries = [];
692
+ for (const entry of entriesToProcess) {
415
693
  if (__DEV__) {
416
- console.log("[Connector] All CRUD entries processed, completing transaction...");
694
+ console.log("[Connector] Processing CRUD entry with retry:", {
695
+ table: entry.table,
696
+ op: entry.op,
697
+ id: entry.id,
698
+ opData: entry.opData
699
+ });
417
700
  }
418
- await transaction.complete();
701
+ await this.processWithRetry(entry);
702
+ successfulEntries.push(entry);
703
+ }
704
+ await this.finalizeTransaction({
705
+ transaction,
706
+ successfulEntries,
707
+ discardedEntries: entriesDiscarded,
708
+ partialResolutions
709
+ });
710
+ }
711
+ /**
712
+ * Finalize a transaction by completing it after all entries processed successfully.
713
+ * Extracted to eliminate duplication between uploadData and processTransaction.
714
+ *
715
+ * @param context - The finalization context containing results and transaction
716
+ */
717
+ async finalizeTransaction(context) {
718
+ const {
719
+ transaction,
720
+ successfulEntries,
721
+ discardedEntries = [],
722
+ partialResolutions = []
723
+ } = context;
724
+ if (__DEV__) {
725
+ console.log("[Connector] All CRUD entries processed, completing transaction...");
726
+ }
727
+ try {
728
+ await withTimeout(transaction.complete(), 3e4, "Transaction complete timeout");
419
729
  if (__DEV__) {
420
730
  console.log("[Connector] Transaction completed successfully:", {
421
- entriesCount: entriesToProcess.length,
422
- discardedCount: entriesDiscarded.length
731
+ entriesCount: successfulEntries.length,
732
+ discardedCount: discardedEntries.length
423
733
  });
424
734
  }
735
+ for (const entry of successfulEntries) {
736
+ this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);
737
+ }
738
+ for (const entry of discardedEntries) {
739
+ this.resolvedConflicts.delete(`${entry.table}:${entry.id}`);
740
+ }
741
+ if (this.resolvedConflicts.size > 100) {
742
+ const oldest = [...this.resolvedConflicts.keys()][0];
743
+ if (oldest) {
744
+ this.resolvedConflicts.delete(oldest);
745
+ }
746
+ }
425
747
  if (this.conflictBus && partialResolutions.length > 0) {
426
748
  for (const {
427
749
  originalConflict,
@@ -448,100 +770,210 @@ var SupabaseConnector = class {
448
770
  }
449
771
  }
450
772
  }
451
- this.onTransactionSuccess?.(entriesToProcess);
452
- this.onTransactionComplete?.(entriesToProcess);
773
+ this.onTransactionSuccess?.(successfulEntries);
774
+ this.onTransactionComplete?.(successfulEntries);
453
775
  } catch (error) {
454
776
  const classified = classifySupabaseError(error);
455
- console.error("[PowerSync Connector] Upload FAILED:", {
777
+ this.logger?.error("[Connector] Transaction completion FAILED:", {
456
778
  errorMessage: error instanceof Error ? error.message : String(error),
457
- errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
458
- errorObject: JSON.stringify(error, null, 2),
459
779
  classified,
460
780
  isPermanent: classified.isPermanent,
461
- entries: entriesToProcess.map((e) => ({
781
+ entries: successfulEntries.map((e) => ({
462
782
  table: e.table,
463
783
  op: e.op,
464
784
  id: e.id
465
785
  }))
466
786
  });
467
- this.logger?.error("[Connector] Upload error:", {
787
+ if (__DEV__) {
788
+ console.error("[Connector] Transaction completion error details:", {
789
+ errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
790
+ errorObject: JSON.stringify(error, null, 2)
791
+ });
792
+ }
793
+ this.logger?.error("[Connector] Transaction completion error:", {
468
794
  error,
469
795
  classified,
470
- entries: entriesToProcess.map((e) => ({
796
+ entries: successfulEntries.map((e) => ({
471
797
  table: e.table,
472
798
  op: e.op,
473
799
  id: e.id
474
800
  }))
475
801
  });
476
- this.onTransactionFailure?.(entriesToProcess, error instanceof Error ? error : new Error(String(error)), classified);
802
+ this.onTransactionFailure?.(successfulEntries, error instanceof Error ? error : new Error(String(error)), classified);
477
803
  throw error;
478
804
  }
479
805
  }
480
806
  /**
481
807
  * Process a transaction without conflict detection.
482
- * Used when conflict detection is disabled.
808
+ * Uses batched operations for PUT and DELETE, individual processing for PATCH.
483
809
  */
484
810
  async processTransaction(transaction, _database) {
485
- try {
486
- for (const entry of transaction.crud) {
811
+ const successfulEntries = [];
812
+ const entriesByTable = groupEntriesByTable(transaction.crud);
813
+ if (__DEV__) {
814
+ console.log("[Connector] Processing transaction with batching:", {
815
+ totalEntries: transaction.crud.length,
816
+ tables: Array.from(entriesByTable.keys()),
817
+ entriesPerTable: Object.fromEntries(Array.from(entriesByTable.entries()).map(([table, entries]) => [table, {
818
+ total: entries.length,
819
+ put: entries.filter((e) => e.op === "PUT" /* PUT */).length,
820
+ patch: entries.filter((e) => e.op === "PATCH" /* PATCH */).length,
821
+ delete: entries.filter((e) => e.op === "DELETE" /* DELETE */).length
822
+ }]))
823
+ });
824
+ }
825
+ for (const [table, entries] of entriesByTable) {
826
+ const schema = this.schemaRouter(table);
827
+ const putEntries = entries.filter((e) => e.op === "PUT" /* PUT */);
828
+ const patchEntries = entries.filter((e) => e.op === "PATCH" /* PATCH */);
829
+ const deleteEntries = entries.filter((e) => e.op === "DELETE" /* DELETE */);
830
+ if (this.crudHandler) {
831
+ for (const entry of entries) {
832
+ await this.processWithRetry(entry);
833
+ successfulEntries.push(entry);
834
+ }
835
+ continue;
836
+ }
837
+ if (putEntries.length > 0) {
838
+ const successful = await this.processBatchedPuts(table, schema, putEntries);
839
+ successfulEntries.push(...successful);
840
+ }
841
+ if (deleteEntries.length > 0) {
842
+ const successful = await this.processBatchedDeletes(table, schema, deleteEntries);
843
+ successfulEntries.push(...successful);
844
+ }
845
+ for (const entry of patchEntries) {
487
846
  if (__DEV__) {
488
- console.log("[Connector] Processing CRUD entry:", {
847
+ console.log("[Connector] Processing PATCH entry individually:", {
489
848
  table: entry.table,
490
849
  op: entry.op,
491
850
  id: entry.id,
492
851
  opData: entry.opData
493
852
  });
494
853
  }
495
- await this.processCrudEntry(entry);
854
+ await this.processWithRetry(entry);
855
+ successfulEntries.push(entry);
496
856
  }
857
+ }
858
+ await this.finalizeTransaction({
859
+ transaction,
860
+ successfulEntries
861
+ });
862
+ }
863
+ /**
864
+ * Process batched PUT (upsert) operations for a single table.
865
+ * Falls back to individual processing if batch fails.
866
+ * CRITICAL: Throws on first failure to maintain transaction atomicity.
867
+ * @returns Array of successfully processed entries
868
+ * @throws Error on first failure - keeps entire transaction in ps_crud
869
+ */
870
+ async processBatchedPuts(table, schema, entries) {
871
+ const successful = [];
872
+ if (__DEV__) {
873
+ console.log("[Connector] Processing batched PUTs:", {
874
+ schema,
875
+ table,
876
+ count: entries.length
877
+ });
878
+ }
879
+ const rows = entries.map((entry) => ({
880
+ id: entry.id,
881
+ ...entry.opData
882
+ }));
883
+ const query = schema === "public" ? this.supabase.from(table) : this.supabase.schema(schema).from(table);
884
+ const {
885
+ error
886
+ } = await query.upsert(rows, {
887
+ onConflict: "id"
888
+ });
889
+ if (error) {
497
890
  if (__DEV__) {
498
- console.log("[Connector] All CRUD entries processed, completing transaction...");
891
+ console.warn("[Connector] Batched PUT failed, falling back to individual processing:", {
892
+ schema,
893
+ table,
894
+ error: error.message
895
+ });
499
896
  }
500
- await transaction.complete();
897
+ for (const entry of entries) {
898
+ await this.processWithRetry(entry);
899
+ successful.push(entry);
900
+ }
901
+ } else {
501
902
  if (__DEV__) {
502
- console.log("[Connector] Transaction completed successfully:", {
503
- entriesCount: transaction.crud.length
903
+ console.log("[Connector] Batched PUT successful:", {
904
+ schema,
905
+ table,
906
+ count: entries.length
504
907
  });
505
908
  }
506
- this.onTransactionSuccess?.(transaction.crud);
507
- this.onTransactionComplete?.(transaction.crud);
508
- } catch (error) {
509
- const classified = classifySupabaseError(error);
510
- console.error("[PowerSync Connector] Upload FAILED:", {
511
- errorMessage: error instanceof Error ? error.message : String(error),
512
- errorKeys: error && typeof error === "object" ? Object.keys(error) : [],
513
- errorObject: JSON.stringify(error, null, 2),
514
- classified,
515
- isPermanent: classified.isPermanent,
516
- entries: transaction.crud.map((e) => ({
517
- table: e.table,
518
- op: e.op,
519
- id: e.id
520
- }))
521
- });
522
- this.logger?.error("[Connector] Upload error:", {
523
- error,
524
- classified,
525
- entries: transaction.crud.map((e) => ({
526
- table: e.table,
527
- op: e.op,
528
- id: e.id
529
- }))
909
+ successful.push(...entries);
910
+ }
911
+ return successful;
912
+ }
913
+ /**
914
+ * Process batched DELETE operations for a single table.
915
+ * Falls back to individual processing if batch fails.
916
+ * CRITICAL: Throws on first failure to maintain transaction atomicity.
917
+ * @returns Array of successfully processed entries
918
+ * @throws Error on first failure - keeps entire transaction in ps_crud
919
+ */
920
+ async processBatchedDeletes(table, schema, entries) {
921
+ const successful = [];
922
+ if (__DEV__) {
923
+ console.log("[Connector] Processing batched DELETEs:", {
924
+ schema,
925
+ table,
926
+ count: entries.length
530
927
  });
531
- this.onTransactionFailure?.(transaction.crud, error instanceof Error ? error : new Error(String(error)), classified);
532
- throw error;
533
928
  }
929
+ const ids = entries.map((entry) => entry.id);
930
+ const query = schema === "public" ? this.supabase.from(table) : this.supabase.schema(schema).from(table);
931
+ const {
932
+ error
933
+ } = await query.delete().in("id", ids);
934
+ if (error) {
935
+ if (__DEV__) {
936
+ console.warn("[Connector] Batched DELETE failed, falling back to individual processing:", {
937
+ schema,
938
+ table,
939
+ error: error.message
940
+ });
941
+ }
942
+ for (const entry of entries) {
943
+ await this.processWithRetry(entry);
944
+ successful.push(entry);
945
+ }
946
+ } else {
947
+ if (__DEV__) {
948
+ console.log("[Connector] Batched DELETE successful:", {
949
+ schema,
950
+ table,
951
+ count: entries.length
952
+ });
953
+ }
954
+ successful.push(...entries);
955
+ }
956
+ return successful;
534
957
  }
535
958
  /**
536
959
  * Check if a table has a _version column (cached).
960
+ * P4.1: Uses Promise-based locking to prevent duplicate concurrent queries.
537
961
  */
538
962
  async checkVersionColumn(table, db) {
539
963
  if (this.versionColumnCache.has(table)) {
540
964
  return this.versionColumnCache.get(table);
541
965
  }
542
- const hasVersion = await hasVersionColumn(table, db);
543
- this.versionColumnCache.set(table, hasVersion);
544
- return hasVersion;
966
+ if (!this.versionColumnPromises.has(table)) {
967
+ this.versionColumnPromises.set(table, hasVersionColumn(table, db).then((result) => {
968
+ this.versionColumnCache.set(table, result);
969
+ this.versionColumnPromises.delete(table);
970
+ return result;
971
+ }).catch((error) => {
972
+ this.versionColumnPromises.delete(table);
973
+ throw error;
974
+ }));
975
+ }
976
+ return this.versionColumnPromises.get(table);
545
977
  }
546
978
  /**
547
979
  * Filter opData to only include specified fields.
@@ -557,12 +989,6 @@ var SupabaseConnector = class {
557
989
  }
558
990
  return filtered;
559
991
  }
560
- /**
561
- * Process a single CRUD operation.
562
- *
563
- * UUID-native tables (public schema, post-migration) use `id` as the UUID column.
564
- * Core schema tables (Profile, Comment, CommentSection) still use a separate `uuid` column.
565
- */
566
992
  /**
567
993
  * Process a single CRUD operation.
568
994
  *
@@ -572,6 +998,16 @@ var SupabaseConnector = class {
572
998
  const table = entry.table;
573
999
  const id = entry.id;
574
1000
  const schema = this.schemaRouter(table);
1001
+ if (entry.op === "PATCH" /* PATCH */ && Object.keys(entry.opData ?? {}).length === 0) {
1002
+ this.logger?.debug(`[Connector] Skipping empty PATCH for ${entry.table}:${entry.id}`);
1003
+ return;
1004
+ }
1005
+ if (entry.opData) {
1006
+ const payloadSize = JSON.stringify(entry.opData).length;
1007
+ if (payloadSize > MAX_PAYLOAD_SIZE) {
1008
+ throw new ValidationError(`Payload too large (${(payloadSize / 1024).toFixed(0)}KB > 900KB) for ${schema}.${table}`);
1009
+ }
1010
+ }
575
1011
  if (this.crudHandler) {
576
1012
  let handled = false;
577
1013
  switch (entry.op) {
@@ -717,10 +1153,12 @@ var SupabaseConnector = class {
717
1153
 
718
1154
  export {
719
1155
  defaultSchemaRouter,
1156
+ DEFAULT_RETRY_CONFIG,
1157
+ ConflictDetectionError,
720
1158
  detectConflicts,
721
1159
  hasVersionColumn,
722
1160
  fetchServerVersion,
723
1161
  getLocalVersion,
724
1162
  SupabaseConnector
725
1163
  };
726
- //# sourceMappingURL=chunk-C2RSTGDC.js.map
1164
+ //# sourceMappingURL=chunk-HFOFLW5F.js.map