@pol-studios/powersync 1.0.6 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +933 -0
  2. package/dist/CacheSettingsManager-uz-kbnRH.d.ts +461 -0
  3. package/dist/attachments/index.d.ts +745 -332
  4. package/dist/attachments/index.js +152 -6
  5. package/dist/{types-Cd7RhNqf.d.ts → background-sync-ChCXW-EV.d.ts} +53 -2
  6. package/dist/chunk-24RDMMCL.js +44 -0
  7. package/dist/chunk-24RDMMCL.js.map +1 -0
  8. package/dist/chunk-4TXTAEF2.js +2060 -0
  9. package/dist/chunk-4TXTAEF2.js.map +1 -0
  10. package/dist/chunk-63PXSPIN.js +358 -0
  11. package/dist/chunk-63PXSPIN.js.map +1 -0
  12. package/dist/chunk-654ERHA7.js +1 -0
  13. package/dist/chunk-A4IBBWGO.js +377 -0
  14. package/dist/chunk-A4IBBWGO.js.map +1 -0
  15. package/dist/chunk-BRXQNASY.js +1720 -0
  16. package/dist/chunk-BRXQNASY.js.map +1 -0
  17. package/dist/chunk-CAB26E6F.js +142 -0
  18. package/dist/chunk-CAB26E6F.js.map +1 -0
  19. package/dist/{chunk-EJ23MXPQ.js → chunk-CGL33PL4.js} +3 -1
  20. package/dist/chunk-CGL33PL4.js.map +1 -0
  21. package/dist/{chunk-R4YFWQ3Q.js → chunk-CUCAYK7Z.js} +309 -92
  22. package/dist/chunk-CUCAYK7Z.js.map +1 -0
  23. package/dist/chunk-FV2HXEIY.js +124 -0
  24. package/dist/chunk-FV2HXEIY.js.map +1 -0
  25. package/dist/chunk-HWSNV45P.js +279 -0
  26. package/dist/chunk-HWSNV45P.js.map +1 -0
  27. package/dist/{chunk-62J2DPKX.js → chunk-KN2IZERF.js} +530 -413
  28. package/dist/chunk-KN2IZERF.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-P4HZA6ZT.js +83 -0
  32. package/dist/chunk-P4HZA6ZT.js.map +1 -0
  33. package/dist/chunk-P6WOZO7H.js +49 -0
  34. package/dist/chunk-P6WOZO7H.js.map +1 -0
  35. package/dist/chunk-T4AO7JIG.js +1 -0
  36. package/dist/chunk-TGBT5XBE.js +1 -0
  37. package/dist/{chunk-FPTDATY5.js → chunk-VACPAAQZ.js} +54 -12
  38. package/dist/chunk-VACPAAQZ.js.map +1 -0
  39. package/dist/chunk-WGHNIAF7.js +329 -0
  40. package/dist/chunk-WGHNIAF7.js.map +1 -0
  41. package/dist/{chunk-3AYXHQ4W.js → chunk-WN5ZJ3E2.js} +108 -47
  42. package/dist/chunk-WN5ZJ3E2.js.map +1 -0
  43. package/dist/chunk-XAEII4ZX.js +456 -0
  44. package/dist/chunk-XAEII4ZX.js.map +1 -0
  45. package/dist/chunk-XOY2CJ67.js +289 -0
  46. package/dist/chunk-XOY2CJ67.js.map +1 -0
  47. package/dist/chunk-YHTZ7VMV.js +1 -0
  48. package/dist/chunk-YSTEESEG.js +676 -0
  49. package/dist/chunk-YSTEESEG.js.map +1 -0
  50. package/dist/chunk-Z6VOBGTU.js +32 -0
  51. package/dist/chunk-Z6VOBGTU.js.map +1 -0
  52. package/dist/chunk-ZM4ENYMF.js +230 -0
  53. package/dist/chunk-ZM4ENYMF.js.map +1 -0
  54. package/dist/connector/index.d.ts +236 -4
  55. package/dist/connector/index.js +15 -4
  56. package/dist/core/index.d.ts +16 -3
  57. package/dist/core/index.js +6 -2
  58. package/dist/error/index.d.ts +54 -0
  59. package/dist/error/index.js +7 -0
  60. package/dist/error/index.js.map +1 -0
  61. package/dist/index.d.ts +102 -12
  62. package/dist/index.js +309 -37
  63. package/dist/index.native.d.ts +22 -10
  64. package/dist/index.native.js +309 -38
  65. package/dist/index.web.d.ts +22 -10
  66. package/dist/index.web.js +310 -38
  67. package/dist/maintenance/index.d.ts +118 -0
  68. package/dist/maintenance/index.js +16 -0
  69. package/dist/maintenance/index.js.map +1 -0
  70. package/dist/platform/index.d.ts +16 -1
  71. package/dist/platform/index.js.map +1 -1
  72. package/dist/platform/index.native.d.ts +2 -2
  73. package/dist/platform/index.native.js +1 -1
  74. package/dist/platform/index.web.d.ts +1 -1
  75. package/dist/platform/index.web.js +1 -1
  76. package/dist/pol-attachment-queue-BVAIueoP.d.ts +817 -0
  77. package/dist/provider/index.d.ts +451 -21
  78. package/dist/provider/index.js +32 -13
  79. package/dist/react/index.d.ts +372 -0
  80. package/dist/react/index.js +25 -0
  81. package/dist/react/index.js.map +1 -0
  82. package/dist/storage/index.d.ts +6 -0
  83. package/dist/storage/index.js +42 -0
  84. package/dist/storage/index.js.map +1 -0
  85. package/dist/storage/index.native.d.ts +6 -0
  86. package/dist/storage/index.native.js +40 -0
  87. package/dist/storage/index.native.js.map +1 -0
  88. package/dist/storage/index.web.d.ts +6 -0
  89. package/dist/storage/index.web.js +40 -0
  90. package/dist/storage/index.web.js.map +1 -0
  91. package/dist/storage/upload/index.d.ts +54 -0
  92. package/dist/storage/upload/index.js +15 -0
  93. package/dist/storage/upload/index.js.map +1 -0
  94. package/dist/storage/upload/index.native.d.ts +56 -0
  95. package/dist/storage/upload/index.native.js +15 -0
  96. package/dist/storage/upload/index.native.js.map +1 -0
  97. package/dist/storage/upload/index.web.d.ts +2 -0
  98. package/dist/storage/upload/index.web.js +14 -0
  99. package/dist/storage/upload/index.web.js.map +1 -0
  100. package/dist/supabase-connector-T9vHq_3i.d.ts +202 -0
  101. package/dist/sync/index.d.ts +288 -23
  102. package/dist/sync/index.js +22 -10
  103. package/dist/{index-l3iL9Jte.d.ts → types-B212hgfA.d.ts} +101 -158
  104. package/dist/{types-afHtE1U_.d.ts → types-CDqWh56B.d.ts} +2 -0
  105. package/dist/types-CyvBaAl8.d.ts +60 -0
  106. package/dist/types-D0WcHrq6.d.ts +234 -0
  107. package/package.json +89 -5
  108. package/dist/chunk-32OLICZO.js +0 -1
  109. package/dist/chunk-3AYXHQ4W.js.map +0 -1
  110. package/dist/chunk-5FIMA26D.js +0 -1
  111. package/dist/chunk-62J2DPKX.js.map +0 -1
  112. package/dist/chunk-7EMDVIZX.js.map +0 -1
  113. package/dist/chunk-EJ23MXPQ.js.map +0 -1
  114. package/dist/chunk-FPTDATY5.js.map +0 -1
  115. package/dist/chunk-KCDG2MNP.js +0 -1431
  116. package/dist/chunk-KCDG2MNP.js.map +0 -1
  117. package/dist/chunk-OLHGI472.js +0 -1
  118. package/dist/chunk-PAFBKNL3.js +0 -99
  119. package/dist/chunk-PAFBKNL3.js.map +0 -1
  120. package/dist/chunk-R4YFWQ3Q.js.map +0 -1
  121. package/dist/chunk-V6LJ6MR2.js +0 -740
  122. package/dist/chunk-V6LJ6MR2.js.map +0 -1
  123. package/dist/chunk-VJCL2SWD.js +0 -1
  124. package/dist/failed-upload-store-C0cLxxPz.d.ts +0 -33
  125. /package/dist/{chunk-32OLICZO.js.map → chunk-654ERHA7.js.map} +0 -0
  126. /package/dist/{chunk-5FIMA26D.js.map → chunk-T4AO7JIG.js.map} +0 -0
  127. /package/dist/{chunk-OLHGI472.js.map → chunk-TGBT5XBE.js.map} +0 -0
  128. /package/dist/{chunk-VJCL2SWD.js.map → chunk-YHTZ7VMV.js.map} +0 -0
@@ -1,56 +1,27 @@
1
+ import {
2
+ DEFAULT_CONNECTION_HEALTH,
3
+ DEFAULT_SYNC_METRICS,
4
+ DEFAULT_SYNC_STATUS
5
+ } from "./chunk-24RDMMCL.js";
1
6
  import {
2
7
  HEALTH_CHECK_INTERVAL_MS,
3
8
  HEALTH_CHECK_TIMEOUT_MS,
4
9
  LATENCY_DEGRADED_THRESHOLD_MS,
5
10
  MAX_CONSECUTIVE_FAILURES,
6
11
  STATUS_NOTIFY_THROTTLE_MS,
12
+ STORAGE_KEY_AUTO_OFFLINE,
7
13
  STORAGE_KEY_METRICS,
8
14
  STORAGE_KEY_PAUSED,
9
15
  STORAGE_KEY_SYNC_MODE
10
- } from "./chunk-EJ23MXPQ.js";
16
+ } from "./chunk-CGL33PL4.js";
11
17
  import {
12
18
  classifyError,
13
19
  generateFailureId
14
- } from "./chunk-FPTDATY5.js";
15
-
16
- // src/provider/types.ts
17
- var DEFAULT_SYNC_STATUS = {
18
- connected: false,
19
- connecting: false,
20
- hasSynced: false,
21
- lastSyncedAt: null,
22
- uploading: false,
23
- downloading: false,
24
- downloadProgress: null,
25
- failedTransactions: [],
26
- hasUploadErrors: false,
27
- permanentErrorCount: 0
28
- };
29
- var DEFAULT_CONNECTION_HEALTH = {
30
- status: "disconnected",
31
- latency: null,
32
- lastHealthCheck: null,
33
- consecutiveFailures: 0,
34
- reconnectAttempts: 0
35
- };
36
- var DEFAULT_SYNC_METRICS = {
37
- totalSyncs: 0,
38
- successfulSyncs: 0,
39
- failedSyncs: 0,
40
- lastSyncDuration: null,
41
- averageSyncDuration: null,
42
- totalDataDownloaded: 0,
43
- totalDataUploaded: 0,
44
- lastError: null
45
- };
46
- var DEFAULT_SYNC_CONFIG = {
47
- autoConnect: true,
48
- syncInterval: 0,
49
- enableHealthMonitoring: true,
50
- enableMetrics: true
51
- };
20
+ } from "./chunk-VACPAAQZ.js";
52
21
 
53
22
  // 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";
54
25
  var SyncStatusTracker = class _SyncStatusTracker {
55
26
  storage;
56
27
  logger;
@@ -64,6 +35,13 @@ var SyncStatusTracker = class _SyncStatusTracker {
64
35
  _syncModeListeners = /* @__PURE__ */ new Set();
65
36
  // Force next upload flag for "Sync Now" functionality
66
37
  _forceNextUpload = false;
38
+ // Network reachability gate - blocks uploads instantly when network is unreachable
39
+ _networkReachable = true;
40
+ _networkRestoreTimer = null;
41
+ _networkRestoreDelayMs = 1500;
42
+ // 1.5 seconds delay before restoring
43
+ // Debounce timer for persist operations to avoid race conditions
44
+ _persistDebounceTimer = null;
67
45
  // Track download progress separately to preserve it when offline
68
46
  _lastProgress = null;
69
47
  // Failed transaction tracking
@@ -72,10 +50,15 @@ var SyncStatusTracker = class _SyncStatusTracker {
72
50
  _failureTTLMs = 24 * 60 * 60 * 1e3;
73
51
  // 24 hours
74
52
  _failureListeners = /* @__PURE__ */ new Set();
75
- // Completed transaction tracking
53
+ // Completed transaction tracking (no limit - full audit trail)
76
54
  _completedTransactions = [];
77
- _maxCompletedHistory = 20;
78
55
  _completedListeners = /* @__PURE__ */ new Set();
56
+ // Track when notifications were last displayed/dismissed for "auto-dismiss on display"
57
+ // This allows filtering completed transactions to only show new ones since last display
58
+ _lastNotificationTime = Date.now();
59
+ // Auto-offline flag: tracks whether offline mode was set automatically (network loss)
60
+ // vs manually (user chose offline). Persisted so auto-restore works after app restart.
61
+ _isAutoOffline = false;
79
62
  constructor(storage, logger, options = {}) {
80
63
  this.storage = storage;
81
64
  this.logger = logger;
@@ -114,6 +97,53 @@ var SyncStatusTracker = class _SyncStatusTracker {
114
97
  } catch (err) {
115
98
  this.logger.warn("[StatusTracker] Failed to load sync mode:", err);
116
99
  }
100
+ try {
101
+ const autoOfflineValue = await this.storage.getItem(STORAGE_KEY_AUTO_OFFLINE);
102
+ this._isAutoOffline = autoOfflineValue === "true";
103
+ this.logger.debug("[StatusTracker] Loaded isAutoOffline:", this._isAutoOffline);
104
+ } catch (err) {
105
+ this.logger.warn("[StatusTracker] Failed to load auto-offline flag:", err);
106
+ }
107
+ try {
108
+ const completedJson = await this.storage.getItem(STORAGE_KEY_COMPLETED_TRANSACTIONS);
109
+ if (completedJson) {
110
+ const parsed = JSON.parse(completedJson);
111
+ this._completedTransactions = parsed.map((item) => {
112
+ const remappedEntries = item.entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
113
+ return {
114
+ ...item,
115
+ completedAt: new Date(item.completedAt),
116
+ entries: remappedEntries
117
+ };
118
+ }).filter((item) => !isNaN(item.completedAt.getTime()) && item.entries.length > 0);
119
+ this.logger.debug("[StatusTracker] Loaded", this._completedTransactions.length, "completed transactions");
120
+ }
121
+ } catch (err) {
122
+ this.logger.warn("[StatusTracker] Failed to load completed transactions:", err);
123
+ }
124
+ try {
125
+ const failedJson = await this.storage.getItem(STORAGE_KEY_FAILED_TRANSACTIONS);
126
+ if (failedJson) {
127
+ const parsed = JSON.parse(failedJson);
128
+ this._failedTransactions = parsed.map((item) => {
129
+ const remappedEntries = item.entries.map((e) => this.remapEntry(e)).filter((e) => e !== null);
130
+ return {
131
+ ...item,
132
+ firstFailedAt: new Date(item.firstFailedAt),
133
+ lastFailedAt: new Date(item.lastFailedAt),
134
+ error: {
135
+ ...item.error,
136
+ timestamp: new Date(item.error.timestamp)
137
+ },
138
+ entries: remappedEntries
139
+ };
140
+ }).filter((item) => !isNaN(item.firstFailedAt.getTime()) && !isNaN(item.lastFailedAt.getTime()) && item.entries.length > 0);
141
+ this.logger.debug("[StatusTracker] Loaded", this._failedTransactions.length, "failed transactions");
142
+ }
143
+ } catch (err) {
144
+ this.logger.warn("[StatusTracker] Failed to load failed transactions:", err);
145
+ }
146
+ this.cleanupStaleFailures();
117
147
  }
118
148
  /**
119
149
  * Dispose the tracker and clear timers.
@@ -123,6 +153,14 @@ var SyncStatusTracker = class _SyncStatusTracker {
123
153
  clearTimeout(this._notifyTimer);
124
154
  this._notifyTimer = null;
125
155
  }
156
+ if (this._persistDebounceTimer) {
157
+ clearTimeout(this._persistDebounceTimer);
158
+ this._persistDebounceTimer = null;
159
+ }
160
+ if (this._networkRestoreTimer) {
161
+ clearTimeout(this._networkRestoreTimer);
162
+ this._networkRestoreTimer = null;
163
+ }
126
164
  this._listeners.clear();
127
165
  this._syncModeListeners.clear();
128
166
  this._failureListeners.clear();
@@ -156,17 +194,10 @@ var SyncStatusTracker = class _SyncStatusTracker {
156
194
  return this._state.syncMode;
157
195
  }
158
196
  /**
159
- * Get whether sync is paused (offline mode).
160
- * @deprecated Use getSyncMode() instead
161
- */
162
- isPaused() {
163
- return this._state.syncMode === "offline";
164
- }
165
- /**
166
- * Check if uploads are allowed based on current sync mode.
197
+ * Check if uploads are allowed based on current sync mode and network reachability.
167
198
  */
168
199
  canUpload() {
169
- return this._state.syncMode === "push-pull";
200
+ return this._networkReachable && this._state.syncMode === "push-pull";
170
201
  }
171
202
  /**
172
203
  * Check if downloads are allowed based on current sync mode.
@@ -192,7 +223,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
192
223
  }
193
224
  }
194
225
  /**
195
- * Check if upload should proceed, considering force flag.
226
+ * Check if upload should proceed, considering force flag and network reachability.
196
227
  * NOTE: Does NOT auto-reset the flag - caller must use clearForceNextUpload()
197
228
  * after all uploads are complete. This prevents race conditions when
198
229
  * PowerSync calls uploadData() multiple times for multiple transactions.
@@ -201,8 +232,43 @@ var SyncStatusTracker = class _SyncStatusTracker {
201
232
  if (this._forceNextUpload) {
202
233
  return true;
203
234
  }
235
+ if (!this._networkReachable) {
236
+ return false;
237
+ }
204
238
  return this._state.syncMode === "push-pull";
205
239
  }
240
+ /**
241
+ * Set network reachability state.
242
+ * - When unreachable: Instantly blocks uploads (0ms)
243
+ * - When reachable: Delayed restore (1-2 seconds) to avoid flickering on brief disconnects
244
+ */
245
+ setNetworkReachable(reachable) {
246
+ if (this._networkRestoreTimer) {
247
+ clearTimeout(this._networkRestoreTimer);
248
+ this._networkRestoreTimer = null;
249
+ }
250
+ if (!reachable) {
251
+ if (this._networkReachable) {
252
+ this._networkReachable = false;
253
+ this.logger.debug("[StatusTracker] Network unreachable - uploads blocked instantly");
254
+ }
255
+ } else {
256
+ if (!this._networkReachable) {
257
+ this.logger.debug("[StatusTracker] Network reachable - scheduling delayed restore");
258
+ this._networkRestoreTimer = setTimeout(() => {
259
+ this._networkRestoreTimer = null;
260
+ this._networkReachable = true;
261
+ this.logger.debug("[StatusTracker] Network restored - uploads enabled");
262
+ }, this._networkRestoreDelayMs);
263
+ }
264
+ }
265
+ }
266
+ /**
267
+ * Get current network reachability state.
268
+ */
269
+ isNetworkReachable() {
270
+ return this._networkReachable;
271
+ }
206
272
  /**
207
273
  * Get pending mutations.
208
274
  */
@@ -290,12 +356,27 @@ var SyncStatusTracker = class _SyncStatusTracker {
290
356
  }
291
357
  this._notifyListeners(true);
292
358
  }
359
+ // ─── Auto-Offline Management ──────────────────────────────────────────────
293
360
  /**
294
- * Set paused state.
295
- * @deprecated Use setSyncMode() instead
361
+ * Get whether offline mode was set automatically (network loss) vs manually.
362
+ * Used to determine if sync should auto-resume when network returns.
296
363
  */
297
- async setPaused(paused) {
298
- await this.setSyncMode(paused ? "offline" : "push-pull");
364
+ getIsAutoOffline() {
365
+ return this._isAutoOffline;
366
+ }
367
+ /**
368
+ * Set the auto-offline flag and persist it.
369
+ * @param isAuto - true if offline was set automatically, false if user chose offline
370
+ */
371
+ async setIsAutoOffline(isAuto) {
372
+ if (this._isAutoOffline === isAuto) return;
373
+ this._isAutoOffline = isAuto;
374
+ try {
375
+ await this.storage.setItem(STORAGE_KEY_AUTO_OFFLINE, isAuto ? "true" : "false");
376
+ this.logger.debug("[StatusTracker] Auto-offline flag changed:", isAuto);
377
+ } catch (err) {
378
+ this.logger.warn("[StatusTracker] Failed to persist auto-offline flag:", err);
379
+ }
299
380
  }
300
381
  // ─── Subscriptions ─────────────────────────────────────────────────────────
301
382
  /**
@@ -320,30 +401,18 @@ var SyncStatusTracker = class _SyncStatusTracker {
320
401
  this._syncModeListeners.delete(listener);
321
402
  };
322
403
  }
323
- /**
324
- * Subscribe to paused state changes.
325
- * @deprecated Use onSyncModeChange() instead
326
- * @returns Unsubscribe function
327
- */
328
- onPausedChange(listener) {
329
- const wrappedListener = (mode) => {
330
- listener(mode === "offline");
331
- };
332
- this._syncModeListeners.add(wrappedListener);
333
- listener(this._state.syncMode === "offline");
334
- return () => {
335
- this._syncModeListeners.delete(wrappedListener);
336
- };
337
- }
338
404
  // ─── Failed Transaction Tracking ────────────────────────────────────────────
339
405
  /**
340
406
  * Record a transaction failure.
341
407
  * If a failure for the same entries already exists, updates the retry count.
342
408
  * Otherwise, creates a new failure record.
409
+ *
410
+ * @param preserveMetadata - Optional. If provided, preserves retryCount and firstFailedAt from a previous failure.
343
411
  */
344
- recordTransactionFailure(entries, error, isPermanent, affectedEntityIds, affectedTables) {
412
+ recordTransactionFailure(entries, error, isPermanent, affectedEntityIds, affectedTables, preserveMetadata) {
345
413
  const now = /* @__PURE__ */ new Date();
346
- const entryIds = entries.map((e) => e.id).sort().join(",");
414
+ const normalizedEntries = this.normalizeEntries(entries);
415
+ const entryIds = normalizedEntries.map((e) => e.id).sort().join(",");
347
416
  const existingIndex = this._failedTransactions.findIndex((f) => {
348
417
  const existingIds = f.entries.map((e) => e.id).sort().join(",");
349
418
  return existingIds === entryIds;
@@ -359,11 +428,11 @@ var SyncStatusTracker = class _SyncStatusTracker {
359
428
  };
360
429
  } else {
361
430
  const newFailure = {
362
- id: generateFailureId(entries),
363
- entries,
431
+ id: generateFailureId(normalizedEntries),
432
+ entries: normalizedEntries,
364
433
  error,
365
- retryCount: 1,
366
- firstFailedAt: now,
434
+ retryCount: preserveMetadata?.retryCount ?? 1,
435
+ firstFailedAt: preserveMetadata?.firstFailedAt ?? now,
367
436
  lastFailedAt: now,
368
437
  isPermanent,
369
438
  affectedEntityIds,
@@ -375,6 +444,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
375
444
  this._failedTransactions = this._failedTransactions.slice(-this._maxStoredFailures);
376
445
  }
377
446
  }
447
+ this._schedulePersist();
378
448
  this._notifyFailureListeners();
379
449
  this._notifyListeners();
380
450
  }
@@ -385,6 +455,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
385
455
  const initialLength = this._failedTransactions.length;
386
456
  this._failedTransactions = this._failedTransactions.filter((f) => f.id !== failureId);
387
457
  if (this._failedTransactions.length !== initialLength) {
458
+ this._schedulePersist();
388
459
  this._notifyFailureListeners();
389
460
  this._notifyListeners();
390
461
  }
@@ -395,9 +466,32 @@ var SyncStatusTracker = class _SyncStatusTracker {
395
466
  clearAllFailures() {
396
467
  if (this._failedTransactions.length === 0) return;
397
468
  this._failedTransactions = [];
469
+ this._schedulePersist();
398
470
  this._notifyFailureListeners();
399
471
  this._notifyListeners();
400
472
  }
473
+ /**
474
+ * Remove a failed transaction from tracking and return its entries.
475
+ * This is a "pop" operation - the failure is removed from the list.
476
+ *
477
+ * Note: The actual CRUD entries remain in PowerSync's ps_crud table
478
+ * until successfully uploaded. This just removes from our tracking.
479
+ *
480
+ * @param failureId - The failure ID to remove
481
+ * @returns The CrudEntry[] that were in the failure, or null if not found
482
+ */
483
+ takeFailureForRetry(failureId) {
484
+ const failure = this._failedTransactions.find((f) => f.id === failureId);
485
+ if (!failure) {
486
+ this.logger.warn("[StatusTracker] Failure not found for retry:", failureId);
487
+ return null;
488
+ }
489
+ this._failedTransactions = this._failedTransactions.filter((f) => f.id !== failureId);
490
+ this._notifyFailureListeners();
491
+ this._schedulePersist();
492
+ this.logger.info("[StatusTracker] Retrieved failure for retry:", failureId, "entries:", failure.entries.length);
493
+ return failure.entries;
494
+ }
401
495
  /**
402
496
  * Get failures affecting a specific entity.
403
497
  */
@@ -442,6 +536,7 @@ var SyncStatusTracker = class _SyncStatusTracker {
442
536
  this._failedTransactions = this._failedTransactions.filter((f) => f.lastFailedAt.getTime() > cutoff);
443
537
  if (this._failedTransactions.length !== initialLength) {
444
538
  this.logger.debug(`[StatusTracker] Cleaned up ${initialLength - this._failedTransactions.length} stale failures`);
539
+ this._schedulePersist();
445
540
  this._notifyFailureListeners();
446
541
  this._notifyListeners();
447
542
  }
@@ -452,20 +547,19 @@ var SyncStatusTracker = class _SyncStatusTracker {
452
547
  * Creates a CompletedTransaction record and adds it to history.
453
548
  */
454
549
  recordTransactionComplete(entries) {
455
- const affectedTables = [...new Set(entries.map((e) => e.table))];
456
- const affectedEntityIds = [...new Set(entries.map((e) => e.id))];
550
+ const normalizedEntries = this.normalizeEntries(entries);
551
+ const affectedTables = [...new Set(normalizedEntries.map((e) => e.table))];
552
+ const affectedEntityIds = [...new Set(normalizedEntries.map((e) => e.id))];
457
553
  const id = `completed_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
458
554
  const completed = {
459
555
  id,
460
- entries,
556
+ entries: normalizedEntries,
461
557
  completedAt: /* @__PURE__ */ new Date(),
462
558
  affectedTables,
463
559
  affectedEntityIds
464
560
  };
465
561
  this._completedTransactions.unshift(completed);
466
- if (this._completedTransactions.length > this._maxCompletedHistory) {
467
- this._completedTransactions = this._completedTransactions.slice(0, this._maxCompletedHistory);
468
- }
562
+ this._schedulePersist();
469
563
  this._notifyCompletedListeners();
470
564
  this.logger.debug(`[StatusTracker] Recorded completed transaction: ${completed.id} (${entries.length} entries)`);
471
565
  }
@@ -481,9 +575,22 @@ var SyncStatusTracker = class _SyncStatusTracker {
481
575
  clearCompletedHistory() {
482
576
  if (this._completedTransactions.length === 0) return;
483
577
  this._completedTransactions = [];
578
+ this._schedulePersist();
484
579
  this._notifyCompletedListeners();
485
580
  this.logger.debug("[StatusTracker] Cleared completed transaction history");
486
581
  }
582
+ /**
583
+ * Clear a specific completed transaction by ID.
584
+ */
585
+ clearCompletedItem(completedId) {
586
+ const initialLength = this._completedTransactions.length;
587
+ this._completedTransactions = this._completedTransactions.filter((c) => c.id !== completedId);
588
+ if (this._completedTransactions.length !== initialLength) {
589
+ this._schedulePersist();
590
+ this._notifyCompletedListeners();
591
+ this.logger.debug("[StatusTracker] Cleared completed transaction:", completedId);
592
+ }
593
+ }
487
594
  /**
488
595
  * Subscribe to completed transaction changes.
489
596
  * @returns Unsubscribe function
@@ -495,19 +602,78 @@ var SyncStatusTracker = class _SyncStatusTracker {
495
602
  this._completedListeners.delete(listener);
496
603
  };
497
604
  }
605
+ // ─── Notification Tracking ─────────────────────────────────────────────────
606
+ /**
607
+ * Get completed transactions that occurred AFTER the last notification time.
608
+ * This is used for displaying "X changes synced" notifications to avoid
609
+ * showing stale counts from historical completed transactions.
610
+ */
611
+ getNewCompletedTransactions() {
612
+ return this._completedTransactions.filter((tx) => tx.completedAt.getTime() > this._lastNotificationTime);
613
+ }
614
+ /**
615
+ * Mark notifications as seen by updating the last notification time.
616
+ * Call this when the notification is displayed or dismissed.
617
+ */
618
+ markNotificationsAsSeen() {
619
+ this._lastNotificationTime = Date.now();
620
+ this.logger.debug("[StatusTracker] Notifications marked as seen");
621
+ this._notifyCompletedListeners();
622
+ }
623
+ /**
624
+ * Get the timestamp of when notifications were last displayed/dismissed.
625
+ */
626
+ getLastNotificationTime() {
627
+ return this._lastNotificationTime;
628
+ }
498
629
  // ─── Private Methods ───────────────────────────────────────────────────────
630
+ /**
631
+ * Schedule a debounced persist operation.
632
+ * This prevents race conditions from multiple rapid persist calls.
633
+ */
634
+ _schedulePersist() {
635
+ if (this._persistDebounceTimer) {
636
+ clearTimeout(this._persistDebounceTimer);
637
+ }
638
+ this._persistDebounceTimer = setTimeout(() => {
639
+ this._persistDebounceTimer = null;
640
+ this._persistTransactions();
641
+ }, 100);
642
+ }
643
+ /**
644
+ * Persist completed and failed transactions to storage.
645
+ */
646
+ async _persistTransactions() {
647
+ try {
648
+ 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))]);
649
+ } catch (err) {
650
+ this.logger.warn("[StatusTracker] Failed to persist transactions:", err);
651
+ }
652
+ }
499
653
  _hasStatusChanged(newStatus) {
500
654
  const old = this._state.status;
501
655
  return old.connected !== newStatus.connected || old.connecting !== newStatus.connecting || old.hasSynced !== newStatus.hasSynced || old.uploading !== newStatus.uploading || old.downloading !== newStatus.downloading || old.lastSyncedAt?.getTime() !== newStatus.lastSyncedAt?.getTime() || old.downloadProgress?.current !== newStatus.downloadProgress?.current || old.downloadProgress?.target !== newStatus.downloadProgress?.target;
502
656
  }
657
+ /**
658
+ * Notify all listeners of status changes with throttling.
659
+ *
660
+ * Uses a "dirty" flag pattern: when throttled, we schedule a timer
661
+ * but get the CURRENT state when the timer fires, not the stale state
662
+ * from when the timer was scheduled. This ensures rapid state changes
663
+ * during the throttle window aren't lost.
664
+ */
503
665
  _notifyListeners(forceImmediate = false) {
504
666
  const now = Date.now();
505
667
  const timeSinceLastNotify = now - this._lastNotifyTime;
668
+ if (this._notifyTimer && !forceImmediate) {
669
+ return;
670
+ }
506
671
  if (this._notifyTimer) {
507
672
  clearTimeout(this._notifyTimer);
508
673
  this._notifyTimer = null;
509
674
  }
510
675
  const notify = () => {
676
+ this._notifyTimer = null;
511
677
  this._lastNotifyTime = Date.now();
512
678
  const status = this.getStatus();
513
679
  this.onStatusChange?.(status);
@@ -522,8 +688,8 @@ var SyncStatusTracker = class _SyncStatusTracker {
522
688
  if (forceImmediate || timeSinceLastNotify >= this.notifyThrottleMs) {
523
689
  notify();
524
690
  } else {
525
- const delay = this.notifyThrottleMs - timeSinceLastNotify;
526
- this._notifyTimer = setTimeout(notify, delay);
691
+ const delayMs = this.notifyThrottleMs - timeSinceLastNotify;
692
+ this._notifyTimer = setTimeout(notify, delayMs);
527
693
  }
528
694
  }
529
695
  _notifyFailureListeners() {
@@ -546,6 +712,46 @@ var SyncStatusTracker = class _SyncStatusTracker {
546
712
  }
547
713
  }
548
714
  }
715
+ /**
716
+ * Remap a CrudEntry from persisted JSON (handles toJSON() property remapping).
717
+ * PowerSync's CrudEntry.toJSON() remaps: opData→data, table→type, clientId→op_id, transactionId→tx_id
718
+ *
719
+ * @returns The remapped CrudEntry, or null if critical fields (table, id) are missing
720
+ */
721
+ remapEntry(entry) {
722
+ const table = entry.table ?? entry.type;
723
+ const id = entry.id;
724
+ if (!table || typeof table !== "string") {
725
+ this.logger.warn("[StatusTracker] Invalid CrudEntry: missing or invalid table field", entry);
726
+ return null;
727
+ }
728
+ if (!id || typeof id !== "string") {
729
+ this.logger.warn("[StatusTracker] Invalid CrudEntry: missing or invalid id field", entry);
730
+ return null;
731
+ }
732
+ return {
733
+ id,
734
+ clientId: entry.clientId ?? entry.op_id ?? 0,
735
+ op: entry.op,
736
+ table,
737
+ opData: entry.opData ?? entry.data,
738
+ transactionId: entry.transactionId ?? entry.tx_id
739
+ };
740
+ }
741
+ /**
742
+ * Normalize CrudEntry array to plain objects to avoid CrudEntry.toJSON() remapping issues.
743
+ * PowerSync's CrudEntry.toJSON() remaps property names which breaks deserialization.
744
+ */
745
+ normalizeEntries(entries) {
746
+ return entries.map((e) => ({
747
+ id: e.id,
748
+ clientId: e.clientId,
749
+ op: e.op,
750
+ table: e.table,
751
+ opData: e.opData,
752
+ transactionId: e.transactionId
753
+ }));
754
+ }
549
755
  };
550
756
 
551
757
  // src/sync/metrics-collector.ts
@@ -779,6 +985,7 @@ var HealthMonitor = class {
779
985
  _listeners = /* @__PURE__ */ new Set();
780
986
  _running = false;
781
987
  _paused = false;
988
+ _pendingTimers = /* @__PURE__ */ new Set();
782
989
  constructor(logger, options = {}) {
783
990
  this.logger = logger;
784
991
  this.checkIntervalMs = options.checkIntervalMs ?? HEALTH_CHECK_INTERVAL_MS;
@@ -813,10 +1020,14 @@ var HealthMonitor = class {
813
1020
  if (this._running) return;
814
1021
  this.logger.info("[HealthMonitor] Starting");
815
1022
  this._running = true;
816
- this._checkHealth();
1023
+ this._checkHealth().catch((err) => {
1024
+ this.logger.warn("[HealthMonitor] Initial check error:", err);
1025
+ });
817
1026
  this._intervalId = setInterval(() => {
818
1027
  if (!this._paused) {
819
- this._checkHealth();
1028
+ this._checkHealth().catch((err) => {
1029
+ this.logger.warn("[HealthMonitor] Periodic check error:", err);
1030
+ });
820
1031
  }
821
1032
  }, this.checkIntervalMs);
822
1033
  }
@@ -837,6 +1048,8 @@ var HealthMonitor = class {
837
1048
  */
838
1049
  pause() {
839
1050
  this._paused = true;
1051
+ this._pendingTimers.forEach(clearTimeout);
1052
+ this._pendingTimers.clear();
840
1053
  this._updateHealth({
841
1054
  ...this._health,
842
1055
  status: "disconnected"
@@ -847,13 +1060,17 @@ var HealthMonitor = class {
847
1060
  */
848
1061
  resume() {
849
1062
  this._paused = false;
850
- this._checkHealth();
1063
+ this._checkHealth().catch((err) => {
1064
+ this.logger.warn("[HealthMonitor] Resume check error:", err);
1065
+ });
851
1066
  }
852
1067
  /**
853
1068
  * Dispose the monitor and clear all resources.
854
1069
  */
855
1070
  dispose() {
856
1071
  this.stop();
1072
+ this._pendingTimers.forEach(clearTimeout);
1073
+ this._pendingTimers.clear();
857
1074
  this._listeners.clear();
858
1075
  }
859
1076
  // ─── Getters ───────────────────────────────────────────────────────────────
@@ -982,12 +1199,16 @@ var HealthMonitor = class {
982
1199
  _withTimeout(promise, timeoutMs) {
983
1200
  return new Promise((resolve, reject) => {
984
1201
  const timer = setTimeout(() => {
1202
+ this._pendingTimers.delete(timer);
985
1203
  reject(new Error(`Health check timeout after ${timeoutMs}ms`));
986
1204
  }, timeoutMs);
1205
+ this._pendingTimers.add(timer);
987
1206
  promise.then((result) => {
1207
+ this._pendingTimers.delete(timer);
988
1208
  clearTimeout(timer);
989
1209
  resolve(result);
990
1210
  }, (error) => {
1211
+ this._pendingTimers.delete(timer);
991
1212
  clearTimeout(timer);
992
1213
  reject(error);
993
1214
  });
@@ -996,12 +1217,8 @@ var HealthMonitor = class {
996
1217
  };
997
1218
 
998
1219
  export {
999
- DEFAULT_SYNC_STATUS,
1000
- DEFAULT_CONNECTION_HEALTH,
1001
- DEFAULT_SYNC_METRICS,
1002
- DEFAULT_SYNC_CONFIG,
1003
1220
  SyncStatusTracker,
1004
1221
  MetricsCollector,
1005
1222
  HealthMonitor
1006
1223
  };
1007
- //# sourceMappingURL=chunk-R4YFWQ3Q.js.map
1224
+ //# sourceMappingURL=chunk-CUCAYK7Z.js.map