@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.
- package/README.md +933 -0
- package/dist/CacheSettingsManager-uz-kbnRH.d.ts +461 -0
- package/dist/attachments/index.d.ts +745 -332
- package/dist/attachments/index.js +152 -6
- package/dist/{types-Cd7RhNqf.d.ts → background-sync-ChCXW-EV.d.ts} +53 -2
- package/dist/chunk-24RDMMCL.js +44 -0
- package/dist/chunk-24RDMMCL.js.map +1 -0
- package/dist/chunk-4TXTAEF2.js +2060 -0
- package/dist/chunk-4TXTAEF2.js.map +1 -0
- package/dist/chunk-63PXSPIN.js +358 -0
- package/dist/chunk-63PXSPIN.js.map +1 -0
- package/dist/chunk-654ERHA7.js +1 -0
- package/dist/chunk-A4IBBWGO.js +377 -0
- package/dist/chunk-A4IBBWGO.js.map +1 -0
- package/dist/chunk-BRXQNASY.js +1720 -0
- package/dist/chunk-BRXQNASY.js.map +1 -0
- package/dist/chunk-CAB26E6F.js +142 -0
- package/dist/chunk-CAB26E6F.js.map +1 -0
- package/dist/{chunk-EJ23MXPQ.js → chunk-CGL33PL4.js} +3 -1
- package/dist/chunk-CGL33PL4.js.map +1 -0
- package/dist/{chunk-R4YFWQ3Q.js → chunk-CUCAYK7Z.js} +309 -92
- package/dist/chunk-CUCAYK7Z.js.map +1 -0
- package/dist/chunk-FV2HXEIY.js +124 -0
- package/dist/chunk-FV2HXEIY.js.map +1 -0
- package/dist/chunk-HWSNV45P.js +279 -0
- package/dist/chunk-HWSNV45P.js.map +1 -0
- package/dist/{chunk-62J2DPKX.js → chunk-KN2IZERF.js} +530 -413
- package/dist/chunk-KN2IZERF.js.map +1 -0
- package/dist/{chunk-7EMDVIZX.js → chunk-N75DEF5J.js} +19 -1
- package/dist/chunk-N75DEF5J.js.map +1 -0
- package/dist/chunk-P4HZA6ZT.js +83 -0
- package/dist/chunk-P4HZA6ZT.js.map +1 -0
- package/dist/chunk-P6WOZO7H.js +49 -0
- package/dist/chunk-P6WOZO7H.js.map +1 -0
- package/dist/chunk-T4AO7JIG.js +1 -0
- package/dist/chunk-TGBT5XBE.js +1 -0
- package/dist/{chunk-FPTDATY5.js → chunk-VACPAAQZ.js} +54 -12
- package/dist/chunk-VACPAAQZ.js.map +1 -0
- package/dist/chunk-WGHNIAF7.js +329 -0
- package/dist/chunk-WGHNIAF7.js.map +1 -0
- package/dist/{chunk-3AYXHQ4W.js → chunk-WN5ZJ3E2.js} +108 -47
- package/dist/chunk-WN5ZJ3E2.js.map +1 -0
- package/dist/chunk-XAEII4ZX.js +456 -0
- package/dist/chunk-XAEII4ZX.js.map +1 -0
- package/dist/chunk-XOY2CJ67.js +289 -0
- package/dist/chunk-XOY2CJ67.js.map +1 -0
- package/dist/chunk-YHTZ7VMV.js +1 -0
- package/dist/chunk-YSTEESEG.js +676 -0
- package/dist/chunk-YSTEESEG.js.map +1 -0
- package/dist/chunk-Z6VOBGTU.js +32 -0
- package/dist/chunk-Z6VOBGTU.js.map +1 -0
- package/dist/chunk-ZM4ENYMF.js +230 -0
- package/dist/chunk-ZM4ENYMF.js.map +1 -0
- package/dist/connector/index.d.ts +236 -4
- package/dist/connector/index.js +15 -4
- package/dist/core/index.d.ts +16 -3
- package/dist/core/index.js +6 -2
- package/dist/error/index.d.ts +54 -0
- package/dist/error/index.js +7 -0
- package/dist/error/index.js.map +1 -0
- package/dist/index.d.ts +102 -12
- package/dist/index.js +309 -37
- package/dist/index.native.d.ts +22 -10
- package/dist/index.native.js +309 -38
- package/dist/index.web.d.ts +22 -10
- package/dist/index.web.js +310 -38
- package/dist/maintenance/index.d.ts +118 -0
- package/dist/maintenance/index.js +16 -0
- package/dist/maintenance/index.js.map +1 -0
- package/dist/platform/index.d.ts +16 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.d.ts +2 -2
- package/dist/platform/index.native.js +1 -1
- package/dist/platform/index.web.d.ts +1 -1
- package/dist/platform/index.web.js +1 -1
- package/dist/pol-attachment-queue-BVAIueoP.d.ts +817 -0
- package/dist/provider/index.d.ts +451 -21
- package/dist/provider/index.js +32 -13
- package/dist/react/index.d.ts +372 -0
- package/dist/react/index.js +25 -0
- package/dist/react/index.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.js +42 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.native.d.ts +6 -0
- package/dist/storage/index.native.js +40 -0
- package/dist/storage/index.native.js.map +1 -0
- package/dist/storage/index.web.d.ts +6 -0
- package/dist/storage/index.web.js +40 -0
- package/dist/storage/index.web.js.map +1 -0
- package/dist/storage/upload/index.d.ts +54 -0
- package/dist/storage/upload/index.js +15 -0
- package/dist/storage/upload/index.js.map +1 -0
- package/dist/storage/upload/index.native.d.ts +56 -0
- package/dist/storage/upload/index.native.js +15 -0
- package/dist/storage/upload/index.native.js.map +1 -0
- package/dist/storage/upload/index.web.d.ts +2 -0
- package/dist/storage/upload/index.web.js +14 -0
- package/dist/storage/upload/index.web.js.map +1 -0
- package/dist/supabase-connector-T9vHq_3i.d.ts +202 -0
- package/dist/sync/index.d.ts +288 -23
- package/dist/sync/index.js +22 -10
- package/dist/{index-l3iL9Jte.d.ts → types-B212hgfA.d.ts} +101 -158
- package/dist/{types-afHtE1U_.d.ts → types-CDqWh56B.d.ts} +2 -0
- package/dist/types-CyvBaAl8.d.ts +60 -0
- package/dist/types-D0WcHrq6.d.ts +234 -0
- package/package.json +89 -5
- package/dist/chunk-32OLICZO.js +0 -1
- package/dist/chunk-3AYXHQ4W.js.map +0 -1
- package/dist/chunk-5FIMA26D.js +0 -1
- package/dist/chunk-62J2DPKX.js.map +0 -1
- package/dist/chunk-7EMDVIZX.js.map +0 -1
- package/dist/chunk-EJ23MXPQ.js.map +0 -1
- package/dist/chunk-FPTDATY5.js.map +0 -1
- package/dist/chunk-KCDG2MNP.js +0 -1431
- package/dist/chunk-KCDG2MNP.js.map +0 -1
- package/dist/chunk-OLHGI472.js +0 -1
- package/dist/chunk-PAFBKNL3.js +0 -99
- package/dist/chunk-PAFBKNL3.js.map +0 -1
- package/dist/chunk-R4YFWQ3Q.js.map +0 -1
- package/dist/chunk-V6LJ6MR2.js +0 -740
- package/dist/chunk-V6LJ6MR2.js.map +0 -1
- package/dist/chunk-VJCL2SWD.js +0 -1
- package/dist/failed-upload-store-C0cLxxPz.d.ts +0 -33
- /package/dist/{chunk-32OLICZO.js.map → chunk-654ERHA7.js.map} +0 -0
- /package/dist/{chunk-5FIMA26D.js.map → chunk-T4AO7JIG.js.map} +0 -0
- /package/dist/{chunk-OLHGI472.js.map → chunk-TGBT5XBE.js.map} +0 -0
- /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-
|
|
16
|
+
} from "./chunk-CGL33PL4.js";
|
|
11
17
|
import {
|
|
12
18
|
classifyError,
|
|
13
19
|
generateFailureId
|
|
14
|
-
} from "./chunk-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
295
|
-
*
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
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(
|
|
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
|
|
456
|
-
const
|
|
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
|
-
|
|
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
|
|
526
|
-
this._notifyTimer = setTimeout(notify,
|
|
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-
|
|
1224
|
+
//# sourceMappingURL=chunk-CUCAYK7Z.js.map
|