@pol-studios/powersync 1.0.25 → 1.0.32

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 (118) hide show
  1. package/README.md +0 -1
  2. package/dist/{CacheSettingsManager-uz-kbnRH.d.ts → CacheSettingsManager-0H_7thHW.d.ts} +21 -3
  3. package/dist/attachments/index.d.ts +30 -30
  4. package/dist/attachments/index.js +13 -4
  5. package/dist/{background-sync-ChCXW-EV.d.ts → background-sync-BujnI3IR.d.ts} +1 -1
  6. package/dist/{chunk-55DKCJV4.js → chunk-2RDWLXJW.js} +408 -78
  7. package/dist/chunk-2RDWLXJW.js.map +1 -0
  8. package/dist/{chunk-P4HZA6ZT.js → chunk-4665ZSE5.js} +2 -2
  9. package/dist/chunk-4665ZSE5.js.map +1 -0
  10. package/dist/{chunk-XOY2CJ67.js → chunk-4F5B5CZ7.js} +3 -3
  11. package/dist/chunk-5WRI5ZAA.js +31 -0
  12. package/dist/{chunk-BGBQYQV3.js → chunk-65A3SYJZ.js} +193 -299
  13. package/dist/chunk-65A3SYJZ.js.map +1 -0
  14. package/dist/chunk-6SZ64KCZ.js +755 -0
  15. package/dist/chunk-6SZ64KCZ.js.map +1 -0
  16. package/dist/{chunk-YSTEESEG.js → chunk-74TBHWJ4.js} +122 -11
  17. package/dist/chunk-74TBHWJ4.js.map +1 -0
  18. package/dist/chunk-ANXWYQEJ.js +1 -0
  19. package/dist/chunk-ANXWYQEJ.js.map +1 -0
  20. package/dist/{chunk-CAB26E6F.js → chunk-C4J4MLER.js} +29 -24
  21. package/dist/chunk-C4J4MLER.js.map +1 -0
  22. package/dist/{chunk-C5ODS3XH.js → chunk-EOW7JK7Q.js} +9 -16
  23. package/dist/chunk-EOW7JK7Q.js.map +1 -0
  24. package/dist/chunk-HRAVPIAZ.js +220 -0
  25. package/dist/chunk-HRAVPIAZ.js.map +1 -0
  26. package/dist/{chunk-XAEII4ZX.js → chunk-NUGQOTEM.js} +32 -4
  27. package/dist/chunk-NUGQOTEM.js.map +1 -0
  28. package/dist/chunk-OGUFUZSY.js +5415 -0
  29. package/dist/chunk-OGUFUZSY.js.map +1 -0
  30. package/dist/{chunk-VB737IVN.js → chunk-P4D6BQ4X.js} +328 -706
  31. package/dist/chunk-P4D6BQ4X.js.map +1 -0
  32. package/dist/{chunk-CACKC6XG.js → chunk-PGEDE6IM.js} +136 -89
  33. package/dist/chunk-PGEDE6IM.js.map +1 -0
  34. package/dist/{chunk-A4IBBWGO.js → chunk-RALHHPTU.js} +1 -1
  35. package/dist/chunk-RIDSPLE5.js +42 -0
  36. package/dist/chunk-RIDSPLE5.js.map +1 -0
  37. package/dist/{chunk-Z6VOBGTU.js → chunk-UOMHWUHV.js} +2 -12
  38. package/dist/chunk-UOMHWUHV.js.map +1 -0
  39. package/dist/{chunk-WGHNIAF7.js → chunk-YONQYTVH.js} +2 -2
  40. package/dist/chunk-ZAN22NGL.js +13 -0
  41. package/dist/chunk-ZAN22NGL.js.map +1 -0
  42. package/dist/config/index.d.ts +200 -0
  43. package/dist/config/index.js +23 -0
  44. package/dist/config/index.js.map +1 -0
  45. package/dist/connector/index.d.ts +23 -5
  46. package/dist/connector/index.js +4 -2
  47. package/dist/core/index.d.ts +2 -2
  48. package/dist/core/index.js +1 -0
  49. package/dist/error/index.js +1 -0
  50. package/dist/generator/index.js +2 -0
  51. package/dist/generator/index.js.map +1 -1
  52. package/dist/index.d.ts +19 -16
  53. package/dist/index.js +88 -46
  54. package/dist/index.native.d.ts +18 -14
  55. package/dist/index.native.js +93 -44
  56. package/dist/index.web.d.ts +17 -14
  57. package/dist/index.web.js +88 -46
  58. package/dist/maintenance/index.d.ts +2 -2
  59. package/dist/maintenance/index.js +3 -2
  60. package/dist/platform/index.d.ts +1 -1
  61. package/dist/platform/index.js +2 -0
  62. package/dist/platform/index.js.map +1 -1
  63. package/dist/platform/index.native.d.ts +1 -1
  64. package/dist/platform/index.native.js +1 -0
  65. package/dist/platform/index.web.d.ts +1 -1
  66. package/dist/platform/index.web.js +1 -0
  67. package/dist/pol-attachment-queue-DqBvLAEY.d.ts +255 -0
  68. package/dist/provider/index.d.ts +319 -124
  69. package/dist/provider/index.js +21 -16
  70. package/dist/provider/index.native.d.ts +108 -0
  71. package/dist/provider/index.native.js +121 -0
  72. package/dist/provider/index.native.js.map +1 -0
  73. package/dist/provider/index.web.d.ts +16 -0
  74. package/dist/provider/index.web.js +112 -0
  75. package/dist/provider/index.web.js.map +1 -0
  76. package/dist/react/index.d.ts +16 -65
  77. package/dist/react/index.js +2 -9
  78. package/dist/storage/index.d.ts +5 -4
  79. package/dist/storage/index.js +12 -9
  80. package/dist/storage/index.native.d.ts +5 -4
  81. package/dist/storage/index.native.js +8 -5
  82. package/dist/storage/index.web.d.ts +5 -4
  83. package/dist/storage/index.web.js +11 -8
  84. package/dist/storage/upload/index.d.ts +4 -3
  85. package/dist/storage/upload/index.js +4 -2
  86. package/dist/storage/upload/index.native.d.ts +4 -3
  87. package/dist/storage/upload/index.native.js +4 -2
  88. package/dist/storage/upload/index.web.d.ts +2 -1
  89. package/dist/storage/upload/index.web.js +4 -2
  90. package/dist/{supabase-connector-D2oIl2t8.d.ts → supabase-connector-HMxBA9Kg.d.ts} +23 -25
  91. package/dist/sync/index.d.ts +183 -11
  92. package/dist/sync/index.js +13 -3
  93. package/dist/{types-CyvBaAl8.d.ts → types-6QHGELuY.d.ts} +4 -1
  94. package/dist/{types-CDqWh56B.d.ts → types-B9MptP7E.d.ts} +13 -1
  95. package/dist/types-BhAEsJj-.d.ts +330 -0
  96. package/dist/{types-D0WcHrq6.d.ts → types-CGMibJKD.d.ts} +8 -0
  97. package/dist/{types-DiBvmGEi.d.ts → types-DqJnP50o.d.ts} +22 -24
  98. package/dist/{pol-attachment-queue-BE2HU3Us.d.ts → types-JCEhw2Lf.d.ts} +139 -346
  99. package/package.json +18 -4
  100. package/dist/chunk-24RDMMCL.js +0 -44
  101. package/dist/chunk-24RDMMCL.js.map +0 -1
  102. package/dist/chunk-55DKCJV4.js.map +0 -1
  103. package/dist/chunk-654ERHA7.js +0 -1
  104. package/dist/chunk-BGBQYQV3.js.map +0 -1
  105. package/dist/chunk-C5ODS3XH.js.map +0 -1
  106. package/dist/chunk-CAB26E6F.js.map +0 -1
  107. package/dist/chunk-CACKC6XG.js.map +0 -1
  108. package/dist/chunk-P4HZA6ZT.js.map +0 -1
  109. package/dist/chunk-TIFL2KWE.js +0 -358
  110. package/dist/chunk-TIFL2KWE.js.map +0 -1
  111. package/dist/chunk-VB737IVN.js.map +0 -1
  112. package/dist/chunk-XAEII4ZX.js.map +0 -1
  113. package/dist/chunk-YSTEESEG.js.map +0 -1
  114. package/dist/chunk-Z6VOBGTU.js.map +0 -1
  115. /package/dist/{chunk-XOY2CJ67.js.map → chunk-4F5B5CZ7.js.map} +0 -0
  116. /package/dist/{chunk-654ERHA7.js.map → chunk-5WRI5ZAA.js.map} +0 -0
  117. /package/dist/{chunk-A4IBBWGO.js.map → chunk-RALHHPTU.js.map} +0 -0
  118. /package/dist/{chunk-WGHNIAF7.js.map → chunk-YONQYTVH.js.map} +0 -0
@@ -1,8 +1,6 @@
1
1
  import {
2
- DEFAULT_CONNECTION_HEALTH,
3
- DEFAULT_SYNC_METRICS,
4
- DEFAULT_SYNC_STATUS
5
- } from "./chunk-24RDMMCL.js";
2
+ LOCAL_TABLE_NAMES
3
+ } from "./chunk-OGUFUZSY.js";
6
4
  import {
7
5
  HEALTH_CHECK_INTERVAL_MS,
8
6
  HEALTH_CHECK_TIMEOUT_MS,
@@ -19,15 +17,52 @@ import {
19
17
  generateFailureId
20
18
  } from "./chunk-I2AYMY5O.js";
21
19
 
20
+ // src/provider/types.ts
21
+ var DEFAULT_SYNC_STATUS = {
22
+ connected: false,
23
+ connecting: false,
24
+ hasSynced: false,
25
+ lastSyncedAt: null,
26
+ uploading: false,
27
+ downloading: false,
28
+ downloadProgress: null,
29
+ failedTransactions: [],
30
+ hasUploadErrors: false,
31
+ permanentErrorCount: 0
32
+ };
33
+ var DEFAULT_CONNECTION_HEALTH = {
34
+ status: "disconnected",
35
+ latency: null,
36
+ lastHealthCheck: null,
37
+ consecutiveFailures: 0,
38
+ reconnectAttempts: 0
39
+ };
40
+ var DEFAULT_SYNC_METRICS = {
41
+ totalSyncs: 0,
42
+ successfulSyncs: 0,
43
+ failedSyncs: 0,
44
+ lastSyncDuration: null,
45
+ averageSyncDuration: null,
46
+ totalDataDownloaded: 0,
47
+ totalDataUploaded: 0,
48
+ lastError: null
49
+ };
50
+ var DEFAULT_SYNC_CONFIG = {
51
+ autoConnect: true,
52
+ syncInterval: 0,
53
+ enableHealthMonitoring: true,
54
+ enableMetrics: true
55
+ };
56
+
22
57
  // src/sync/status-tracker.ts
23
- var STORAGE_KEY_COMPLETED_TRANSACTIONS = "@pol-powersync:completed_transactions";
24
- var STORAGE_KEY_FAILED_TRANSACTIONS = "@pol-powersync:failed_transactions";
25
58
  var MAX_COMPLETED_TRANSACTIONS = 1e3;
26
59
  var SyncStatusTracker = class _SyncStatusTracker {
27
60
  storage;
28
61
  logger;
29
62
  notifyThrottleMs;
30
63
  onStatusChange;
64
+ // PowerSync database for local-only table persistence
65
+ _db = null;
31
66
  _state;
32
67
  _pendingMutations = [];
33
68
  _lastNotifyTime = 0;
@@ -73,10 +108,89 @@ var SyncStatusTracker = class _SyncStatusTracker {
73
108
  lastUpdated: /* @__PURE__ */ new Date()
74
109
  };
75
110
  }
111
+ // ─── Database Setup ─────────────────────────────────────────────────────────
112
+ // Flag to prevent operations during initialization
113
+ _dbInitializing = false;
114
+ _dbReady = false;
115
+ // Track persistence errors for degraded mode detection
116
+ _persistenceErrorCount = 0;
117
+ _maxPersistenceErrors = 5;
118
+ /**
119
+ * Set the PowerSync database instance AND initialize transactions atomically.
120
+ * This prevents race conditions between setDatabase() and initTransactions().
121
+ *
122
+ * @param db - The PowerSync database instance
123
+ * @returns Promise that resolves when initialization is complete
124
+ */
125
+ async setDatabaseAndInit(db) {
126
+ if (this._dbInitializing) {
127
+ this.logger.warn("[StatusTracker] Database initialization already in progress");
128
+ return;
129
+ }
130
+ this._dbInitializing = true;
131
+ this._dbReady = false;
132
+ this._db = db;
133
+ this.logger.debug("[StatusTracker] Database set, initializing transactions...");
134
+ try {
135
+ await this.initTransactions();
136
+ this._dbReady = true;
137
+ this.logger.debug("[StatusTracker] Database ready for local-only table persistence");
138
+ } finally {
139
+ this._dbInitializing = false;
140
+ }
141
+ }
142
+ /**
143
+ * Set the PowerSync database instance for local-only table persistence.
144
+ * @deprecated Use setDatabaseAndInit() instead to avoid race conditions.
145
+ */
146
+ setDatabase(db) {
147
+ this._db = db;
148
+ this.logger.debug("[StatusTracker] Database set for local-only table persistence");
149
+ }
150
+ /**
151
+ * Get the PowerSync database instance.
152
+ */
153
+ getDatabase() {
154
+ return this._db;
155
+ }
156
+ /**
157
+ * Check if the database is ready for persistence operations.
158
+ */
159
+ isDatabaseReady() {
160
+ return this._dbReady && this._db !== null;
161
+ }
162
+ /**
163
+ * Check if persistence is in a degraded state (too many errors).
164
+ */
165
+ isPersistenceDegraded() {
166
+ return this._persistenceErrorCount >= this._maxPersistenceErrors;
167
+ }
168
+ /**
169
+ * Track a persistence error and log if we enter degraded mode.
170
+ */
171
+ _trackPersistenceError(operation, error) {
172
+ this._persistenceErrorCount++;
173
+ this.logger.warn(`[StatusTracker] Persistence error (${this._persistenceErrorCount}/${this._maxPersistenceErrors}):`, operation, error);
174
+ if (this._persistenceErrorCount === this._maxPersistenceErrors) {
175
+ this.logger.error("[StatusTracker] Persistence degraded - too many errors. Transaction history may not persist.");
176
+ }
177
+ }
178
+ /**
179
+ * Reset persistence error count (e.g., after successful operation).
180
+ */
181
+ _resetPersistenceErrors() {
182
+ if (this._persistenceErrorCount > 0) {
183
+ this._persistenceErrorCount = 0;
184
+ this.logger.debug("[StatusTracker] Persistence recovered");
185
+ }
186
+ }
76
187
  // ─── Initialization ────────────────────────────────────────────────────────
77
188
  /**
78
189
  * Initialize the tracker by loading persisted state.
79
190
  * Includes migration from old isPaused boolean to new syncMode.
191
+ *
192
+ * Note: Transaction data is loaded from SQLite after the database is set.
193
+ * Call initTransactions() after setDatabase() to load transaction history.
80
194
  */
81
195
  async init() {
82
196
  try {
@@ -105,46 +219,81 @@ var SyncStatusTracker = class _SyncStatusTracker {
105
219
  } catch (err) {
106
220
  this.logger.warn("[StatusTracker] Failed to load auto-offline flag:", err);
107
221
  }
222
+ }
223
+ /**
224
+ * Initialize transactions from SQLite local-only tables.
225
+ * Must be called after setDatabase() has been called.
226
+ */
227
+ async initTransactions() {
228
+ if (!this._db) {
229
+ this.logger.warn("[StatusTracker] Cannot init transactions: database not set");
230
+ return;
231
+ }
232
+ await this._loadCompletedTransactionsFromDb();
233
+ await this._loadFailedTransactionsFromDb();
234
+ this.cleanupStaleFailures();
235
+ }
236
+ /**
237
+ * Load completed transactions from SQLite local-only table.
238
+ */
239
+ async _loadCompletedTransactionsFromDb() {
240
+ if (!this._db) return;
108
241
  try {
109
- const completedJson = await this.storage.getItem(STORAGE_KEY_COMPLETED_TRANSACTIONS);
110
- if (completedJson) {
111
- const parsed = JSON.parse(completedJson);
112
- this._completedTransactions = parsed.map((item) => {
113
- const remappedEntries = item.entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
114
- return {
115
- ...item,
116
- completedAt: new Date(item.completedAt),
117
- entries: remappedEntries
118
- };
119
- }).filter((item) => !isNaN(item.completedAt.getTime()) && item.entries.length > 0);
120
- this.logger.debug("[StatusTracker] Loaded", this._completedTransactions.length, "completed transactions");
121
- }
242
+ const rows = await this._db.getAll(`SELECT * FROM ${LOCAL_TABLE_NAMES.COMPLETED_TRANSACTIONS} ORDER BY completedAt DESC LIMIT ?`, [MAX_COMPLETED_TRANSACTIONS]);
243
+ this._completedTransactions = rows.map((row) => {
244
+ const entries = JSON.parse(row.entries);
245
+ const remappedEntries = entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
246
+ return {
247
+ id: row.id,
248
+ entries: remappedEntries,
249
+ affectedEntityIds: JSON.parse(row.affectedEntityIds),
250
+ affectedTables: JSON.parse(row.affectedTables),
251
+ completedAt: new Date(row.completedAt)
252
+ };
253
+ }).filter((item) => !isNaN(item.completedAt.getTime()) && item.entries.length > 0);
254
+ this._resetPersistenceErrors();
255
+ this.logger.debug("[StatusTracker] Loaded", this._completedTransactions.length, "completed transactions from SQLite");
122
256
  } catch (err) {
123
- this.logger.warn("[StatusTracker] Failed to load completed transactions:", err);
257
+ this._trackPersistenceError("loadCompletedTransactions", err);
258
+ this.logger.warn("[StatusTracker] Failed to load completed transactions from SQLite:", err);
124
259
  }
260
+ }
261
+ /**
262
+ * Load failed transactions from SQLite local-only table.
263
+ */
264
+ async _loadFailedTransactionsFromDb() {
265
+ if (!this._db) return;
125
266
  try {
126
- const failedJson = await this.storage.getItem(STORAGE_KEY_FAILED_TRANSACTIONS);
127
- if (failedJson) {
128
- const parsed = JSON.parse(failedJson);
129
- this._failedTransactions = parsed.map((item) => {
130
- const remappedEntries = item.entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
131
- return {
132
- ...item,
133
- firstFailedAt: new Date(item.firstFailedAt),
134
- lastFailedAt: new Date(item.lastFailedAt),
135
- error: {
136
- ...item.error,
137
- timestamp: new Date(item.error.timestamp)
138
- },
139
- entries: remappedEntries
140
- };
141
- }).filter((item) => !isNaN(item.firstFailedAt.getTime()) && !isNaN(item.lastFailedAt.getTime()) && item.entries.length > 0);
142
- this.logger.debug("[StatusTracker] Loaded", this._failedTransactions.length, "failed transactions");
143
- }
267
+ const rows = await this._db.getAll(`SELECT * FROM ${LOCAL_TABLE_NAMES.FAILED_TRANSACTIONS} ORDER BY lastFailedAt DESC`);
268
+ this._failedTransactions = rows.map((row) => {
269
+ const entries = JSON.parse(row.entries);
270
+ const remappedEntries = entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
271
+ const error = {
272
+ type: row.errorType,
273
+ message: row.errorMessage,
274
+ userMessage: row.errorUserMessage,
275
+ timestamp: new Date(row.errorTimestamp),
276
+ isPermanent: row.errorIsPermanent === 1,
277
+ pgCode: row.errorPgCode ?? void 0
278
+ };
279
+ return {
280
+ id: row.id,
281
+ entries: remappedEntries,
282
+ error,
283
+ affectedEntityIds: JSON.parse(row.affectedEntityIds),
284
+ affectedTables: JSON.parse(row.affectedTables),
285
+ retryCount: row.retryCount,
286
+ isPermanent: row.isPermanent === 1,
287
+ firstFailedAt: new Date(row.firstFailedAt),
288
+ lastFailedAt: new Date(row.lastFailedAt)
289
+ };
290
+ }).filter((item) => !isNaN(item.firstFailedAt.getTime()) && !isNaN(item.lastFailedAt.getTime()) && item.entries.length > 0);
291
+ this._resetPersistenceErrors();
292
+ this.logger.debug("[StatusTracker] Loaded", this._failedTransactions.length, "failed transactions from SQLite");
144
293
  } catch (err) {
145
- this.logger.warn("[StatusTracker] Failed to load failed transactions:", err);
294
+ this._trackPersistenceError("loadFailedTransactions", err);
295
+ this.logger.warn("[StatusTracker] Failed to load failed transactions from SQLite:", err);
146
296
  }
147
- this.cleanupStaleFailures();
148
297
  }
149
298
  /**
150
299
  * Dispose the tracker and clear timers.
@@ -194,6 +343,73 @@ var SyncStatusTracker = class _SyncStatusTracker {
194
343
  getSyncMode() {
195
344
  return this._state.syncMode;
196
345
  }
346
+ /**
347
+ * Get the reason why uploads are currently blocked.
348
+ * Returns 'none' if uploads are ready to proceed.
349
+ *
350
+ * Priority order (first match wins):
351
+ * 1. offline_mode - User explicitly chose offline mode
352
+ * 2. auto_offline - System went offline automatically (network loss with auto-offline flag)
353
+ * 3. pull_only_mode - Pull-only mode active
354
+ * 4. network_unreachable - No internet connection
355
+ * 5. disconnected - PowerSync not connected
356
+ * 6. connecting - PowerSync is connecting
357
+ * 7. uploading - Actively uploading
358
+ * 8. none - Ready to sync
359
+ */
360
+ getUploadBlockReason() {
361
+ const status = this._state.status;
362
+ const syncMode = this._state.syncMode;
363
+ if (syncMode === "offline") {
364
+ if (this._isAutoOffline) {
365
+ return "auto_offline";
366
+ }
367
+ return "offline_mode";
368
+ }
369
+ if (syncMode === "pull-only") {
370
+ return "pull_only_mode";
371
+ }
372
+ if (!this._networkReachable) {
373
+ return "network_unreachable";
374
+ }
375
+ if (!status.connected && !status.connecting) {
376
+ return "disconnected";
377
+ }
378
+ if (status.connecting) {
379
+ return "connecting";
380
+ }
381
+ if (status.uploading) {
382
+ return "uploading";
383
+ }
384
+ return "none";
385
+ }
386
+ /**
387
+ * Get a human-readable description of why uploads are blocked.
388
+ * Returns null if uploads are not blocked.
389
+ */
390
+ getUploadBlockDescription() {
391
+ const reason = this.getUploadBlockReason();
392
+ switch (reason) {
393
+ case "offline_mode":
394
+ return "Offline mode is enabled";
395
+ case "auto_offline":
396
+ return "Went offline due to network loss";
397
+ case "pull_only_mode":
398
+ return "Pull-only mode is active";
399
+ case "network_unreachable":
400
+ return "No internet connection";
401
+ case "disconnected":
402
+ return "Not connected to sync service";
403
+ case "connecting":
404
+ return "Connecting to sync service";
405
+ case "uploading":
406
+ return "Upload in progress";
407
+ case "none":
408
+ return null;
409
+ default:
410
+ return null;
411
+ }
412
+ }
197
413
  /**
198
414
  * Check if uploads are allowed based on current sync mode and network reachability.
199
415
  */
@@ -418,34 +634,42 @@ var SyncStatusTracker = class _SyncStatusTracker {
418
634
  const existingIds = f.entries.map((e) => e.id).sort().join(",");
419
635
  return existingIds === entryIds;
420
636
  });
637
+ let failedTransaction;
421
638
  if (existingIndex !== -1) {
422
639
  const existing = this._failedTransactions[existingIndex];
423
- this._failedTransactions[existingIndex] = {
640
+ const newRetryCount = existing.retryCount + 1;
641
+ failedTransaction = {
424
642
  ...existing,
425
643
  error,
426
- retryCount: existing.retryCount + 1,
644
+ retryCount: newRetryCount,
427
645
  lastFailedAt: now,
428
646
  isPermanent
429
647
  };
648
+ this._failedTransactions[existingIndex] = failedTransaction;
430
649
  } else {
431
- const newFailure = {
650
+ const retryCount = preserveMetadata?.retryCount ?? 1;
651
+ failedTransaction = {
432
652
  id: generateFailureId(normalizedEntries),
433
653
  entries: normalizedEntries,
434
654
  error,
435
- retryCount: preserveMetadata?.retryCount ?? 1,
655
+ retryCount,
436
656
  firstFailedAt: preserveMetadata?.firstFailedAt ?? now,
437
657
  lastFailedAt: now,
438
658
  isPermanent,
439
659
  affectedEntityIds,
440
660
  affectedTables
441
661
  };
442
- this._failedTransactions.push(newFailure);
662
+ this._failedTransactions.push(failedTransaction);
443
663
  if (this._failedTransactions.length > this._maxStoredFailures) {
444
664
  this._failedTransactions.sort((a, b) => a.firstFailedAt.getTime() - b.firstFailedAt.getTime());
665
+ const removed = this._failedTransactions.slice(0, this._failedTransactions.length - this._maxStoredFailures);
445
666
  this._failedTransactions = this._failedTransactions.slice(-this._maxStoredFailures);
667
+ for (const r of removed) {
668
+ void this._deleteFailedTransaction(r.id);
669
+ }
446
670
  }
447
671
  }
448
- this._schedulePersist();
672
+ void this._insertFailedTransaction(failedTransaction);
449
673
  this._notifyFailureListeners();
450
674
  this._notifyListeners();
451
675
  }
@@ -456,20 +680,40 @@ var SyncStatusTracker = class _SyncStatusTracker {
456
680
  const initialLength = this._failedTransactions.length;
457
681
  this._failedTransactions = this._failedTransactions.filter((f) => f.id !== failureId);
458
682
  if (this._failedTransactions.length !== initialLength) {
459
- this._schedulePersist();
683
+ void this._deleteFailedTransaction(failureId);
460
684
  this._notifyFailureListeners();
461
685
  this._notifyListeners();
462
686
  }
463
687
  }
464
688
  /**
465
- * Clear all failures.
466
- */
467
- clearAllFailures() {
468
- if (this._failedTransactions.length === 0) return;
469
- this._failedTransactions = [];
470
- this._schedulePersist();
471
- this._notifyFailureListeners();
472
- this._notifyListeners();
689
+ * Clear failures for successfully synced entries.
690
+ * Removes any failed transaction that contains entries with the given IDs.
691
+ *
692
+ * This is called when `onTransactionSuccess` fires - if any entry in a failed
693
+ * transaction has now succeeded, we remove that entire failure record.
694
+ *
695
+ * @param entryIds - Array of CrudEntry.id values that succeeded
696
+ */
697
+ clearSuccessfulEntries(entryIds) {
698
+ if (entryIds.length === 0 || this._failedTransactions.length === 0) return;
699
+ const entryIdSet = new Set(entryIds);
700
+ const removedIds = [];
701
+ this._failedTransactions = this._failedTransactions.filter((failure) => {
702
+ const hasSuccessfulEntry = failure.entries.some((entry) => entryIdSet.has(entry.id));
703
+ if (hasSuccessfulEntry) {
704
+ removedIds.push(failure.id);
705
+ return false;
706
+ }
707
+ return true;
708
+ });
709
+ if (removedIds.length > 0) {
710
+ this.logger.debug(`[StatusTracker] Cleared ${removedIds.length} failures for successful entries`);
711
+ for (const id of removedIds) {
712
+ void this._deleteFailedTransaction(id);
713
+ }
714
+ this._notifyFailureListeners();
715
+ this._notifyListeners();
716
+ }
473
717
  }
474
718
  /**
475
719
  * Remove a failed transaction from tracking and return its entries.
@@ -488,8 +732,8 @@ var SyncStatusTracker = class _SyncStatusTracker {
488
732
  return null;
489
733
  }
490
734
  this._failedTransactions = this._failedTransactions.filter((f) => f.id !== failureId);
735
+ void this._deleteFailedTransaction(failureId);
491
736
  this._notifyFailureListeners();
492
- this._schedulePersist();
493
737
  this.logger.info("[StatusTracker] Retrieved failure for retry:", failureId, "entries:", failure.entries.length);
494
738
  return failure.entries;
495
739
  }
@@ -533,11 +777,19 @@ var SyncStatusTracker = class _SyncStatusTracker {
533
777
  */
534
778
  cleanupStaleFailures() {
535
779
  const cutoff = Date.now() - this._failureTTLMs;
536
- const initialLength = this._failedTransactions.length;
537
- this._failedTransactions = this._failedTransactions.filter((f) => f.lastFailedAt.getTime() > cutoff);
538
- if (this._failedTransactions.length !== initialLength) {
539
- this.logger.debug(`[StatusTracker] Cleaned up ${initialLength - this._failedTransactions.length} stale failures`);
540
- this._schedulePersist();
780
+ const removedIds = [];
781
+ this._failedTransactions = this._failedTransactions.filter((f) => {
782
+ if (f.lastFailedAt.getTime() <= cutoff) {
783
+ removedIds.push(f.id);
784
+ return false;
785
+ }
786
+ return true;
787
+ });
788
+ if (removedIds.length > 0) {
789
+ this.logger.debug(`[StatusTracker] Cleaned up ${removedIds.length} stale failures`);
790
+ for (const id of removedIds) {
791
+ void this._deleteFailedTransaction(id);
792
+ }
541
793
  this._notifyFailureListeners();
542
794
  this._notifyListeners();
543
795
  }
@@ -563,7 +815,8 @@ var SyncStatusTracker = class _SyncStatusTracker {
563
815
  if (this._completedTransactions.length > MAX_COMPLETED_TRANSACTIONS) {
564
816
  this._completedTransactions = this._completedTransactions.slice(0, MAX_COMPLETED_TRANSACTIONS);
565
817
  }
566
- this._schedulePersist();
818
+ void this._insertCompletedTransaction(completed);
819
+ void this._enforceCompletedTransactionsLimit();
567
820
  this._notifyCompletedListeners();
568
821
  this.logger.debug(`[StatusTracker] Recorded completed transaction: ${completed.id} (${entries.length} entries)`);
569
822
  }
@@ -579,7 +832,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
579
832
  clearCompletedHistory() {
580
833
  if (this._completedTransactions.length === 0) return;
581
834
  this._completedTransactions = [];
582
- this._schedulePersist();
835
+ void this._deleteAllCompletedTransactions();
583
836
  this._notifyCompletedListeners();
584
837
  this.logger.debug("[StatusTracker] Cleared completed transaction history");
585
838
  }
@@ -590,7 +843,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
590
843
  const initialLength = this._completedTransactions.length;
591
844
  this._completedTransactions = this._completedTransactions.filter((c) => c.id !== completedId);
592
845
  if (this._completedTransactions.length !== initialLength) {
593
- this._schedulePersist();
846
+ void this._deleteCompletedTransaction(completedId);
594
847
  this._notifyCompletedListeners();
595
848
  this.logger.debug("[StatusTracker] Cleared completed transaction:", completedId);
596
849
  }
@@ -631,27 +884,100 @@ var SyncStatusTracker = class _SyncStatusTracker {
631
884
  return this._lastNotificationTime;
632
885
  }
633
886
  // ─── Private Methods ───────────────────────────────────────────────────────
887
+ // ─── SQLite Persistence Methods ─────────────────────────────────────────────
634
888
  /**
635
- * Schedule a debounced persist operation.
636
- * This prevents race conditions from multiple rapid persist calls.
889
+ * Insert a completed transaction into SQLite.
637
890
  */
638
- _schedulePersist() {
639
- if (this._persistDebounceTimer) {
640
- clearTimeout(this._persistDebounceTimer);
891
+ async _insertCompletedTransaction(transaction) {
892
+ if (!this._db) {
893
+ this.logger.warn("[StatusTracker] Cannot persist completed transaction: database not set");
894
+ return;
895
+ }
896
+ try {
897
+ await this._db.execute(`INSERT OR REPLACE INTO ${LOCAL_TABLE_NAMES.COMPLETED_TRANSACTIONS}
898
+ (id, entries, affectedEntityIds, affectedTables, completedAt)
899
+ VALUES (?, ?, ?, ?, ?)`, [transaction.id, JSON.stringify(transaction.entries), JSON.stringify(transaction.affectedEntityIds), JSON.stringify(transaction.affectedTables), transaction.completedAt.toISOString()]);
900
+ this._resetPersistenceErrors();
901
+ } catch (err) {
902
+ this._trackPersistenceError("insertCompletedTransaction", err);
903
+ this.logger.warn("[StatusTracker] Failed to insert completed transaction:", err);
904
+ }
905
+ }
906
+ /**
907
+ * Delete a completed transaction from SQLite.
908
+ */
909
+ async _deleteCompletedTransaction(id) {
910
+ if (!this._db) return;
911
+ try {
912
+ await this._db.execute(`DELETE FROM ${LOCAL_TABLE_NAMES.COMPLETED_TRANSACTIONS} WHERE id = ?`, [id]);
913
+ this._resetPersistenceErrors();
914
+ } catch (err) {
915
+ this._trackPersistenceError("deleteCompletedTransaction", err);
916
+ this.logger.warn("[StatusTracker] Failed to delete completed transaction:", err);
917
+ }
918
+ }
919
+ /**
920
+ * Delete all completed transactions from SQLite.
921
+ */
922
+ async _deleteAllCompletedTransactions() {
923
+ if (!this._db) return;
924
+ try {
925
+ await this._db.execute(`DELETE FROM ${LOCAL_TABLE_NAMES.COMPLETED_TRANSACTIONS}`);
926
+ this._resetPersistenceErrors();
927
+ } catch (err) {
928
+ this._trackPersistenceError("deleteAllCompletedTransactions", err);
929
+ this.logger.warn("[StatusTracker] Failed to delete all completed transactions:", err);
641
930
  }
642
- this._persistDebounceTimer = setTimeout(() => {
643
- this._persistDebounceTimer = null;
644
- this._persistTransactions();
645
- }, 100);
646
931
  }
647
932
  /**
648
- * Persist completed and failed transactions to storage.
933
+ * Insert a failed transaction into SQLite.
649
934
  */
650
- async _persistTransactions() {
935
+ async _insertFailedTransaction(transaction) {
936
+ if (!this._db) {
937
+ this.logger.warn("[StatusTracker] Cannot persist failed transaction: database not set");
938
+ return;
939
+ }
940
+ try {
941
+ await this._db.execute(`INSERT OR REPLACE INTO ${LOCAL_TABLE_NAMES.FAILED_TRANSACTIONS}
942
+ (id, entries, errorType, errorMessage, errorUserMessage, errorPgCode, errorTimestamp, errorIsPermanent,
943
+ affectedEntityIds, affectedTables, retryCount, isPermanent, firstFailedAt, lastFailedAt)
944
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [transaction.id, JSON.stringify(transaction.entries), transaction.error.type, transaction.error.message, transaction.error.userMessage, transaction.error.pgCode ?? null, transaction.error.timestamp.toISOString(), transaction.error.isPermanent ? 1 : 0, JSON.stringify(transaction.affectedEntityIds), JSON.stringify(transaction.affectedTables), transaction.retryCount, transaction.isPermanent ? 1 : 0, transaction.firstFailedAt.toISOString(), transaction.lastFailedAt.toISOString()]);
945
+ this._resetPersistenceErrors();
946
+ } catch (err) {
947
+ this._trackPersistenceError("insertFailedTransaction", err);
948
+ this.logger.warn("[StatusTracker] Failed to insert failed transaction:", err);
949
+ }
950
+ }
951
+ /**
952
+ * Delete a failed transaction from SQLite.
953
+ */
954
+ async _deleteFailedTransaction(id) {
955
+ if (!this._db) return;
651
956
  try {
652
- await Promise.all([this.storage.setItem(STORAGE_KEY_COMPLETED_TRANSACTIONS, JSON.stringify(this._completedTransactions)), this.storage.setItem(STORAGE_KEY_FAILED_TRANSACTIONS, JSON.stringify(this._failedTransactions))]);
957
+ await this._db.execute(`DELETE FROM ${LOCAL_TABLE_NAMES.FAILED_TRANSACTIONS} WHERE id = ?`, [id]);
958
+ this._resetPersistenceErrors();
653
959
  } catch (err) {
654
- this.logger.warn("[StatusTracker] Failed to persist transactions:", err);
960
+ this._trackPersistenceError("deleteFailedTransaction", err);
961
+ this.logger.warn("[StatusTracker] Failed to delete failed transaction:", err);
962
+ }
963
+ }
964
+ /**
965
+ * Enforce the max completed transactions limit in SQLite.
966
+ * Removes oldest entries beyond the limit.
967
+ */
968
+ async _enforceCompletedTransactionsLimit() {
969
+ if (!this._db) return;
970
+ try {
971
+ await this._db.execute(`DELETE FROM ${LOCAL_TABLE_NAMES.COMPLETED_TRANSACTIONS}
972
+ WHERE id NOT IN (
973
+ SELECT id FROM ${LOCAL_TABLE_NAMES.COMPLETED_TRANSACTIONS}
974
+ ORDER BY completedAt DESC
975
+ LIMIT ?
976
+ )`, [MAX_COMPLETED_TRANSACTIONS]);
977
+ this._resetPersistenceErrors();
978
+ } catch (err) {
979
+ this._trackPersistenceError("enforceCompletedTransactionsLimit", err);
980
+ this.logger.warn("[StatusTracker] Failed to enforce completed transactions limit:", err);
655
981
  }
656
982
  }
657
983
  _hasStatusChanged(newStatus) {
@@ -1237,8 +1563,12 @@ var HealthMonitor = class {
1237
1563
  };
1238
1564
 
1239
1565
  export {
1566
+ DEFAULT_SYNC_STATUS,
1567
+ DEFAULT_CONNECTION_HEALTH,
1568
+ DEFAULT_SYNC_METRICS,
1569
+ DEFAULT_SYNC_CONFIG,
1240
1570
  SyncStatusTracker,
1241
1571
  MetricsCollector,
1242
1572
  HealthMonitor
1243
1573
  };
1244
- //# sourceMappingURL=chunk-55DKCJV4.js.map
1574
+ //# sourceMappingURL=chunk-2RDWLXJW.js.map