@powersync/common 0.0.0-dev-20250529141956 → 0.0.0-dev-20250609122429
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.mjs +2 -2
- package/lib/client/ConnectionManager.js +8 -2
- package/lib/client/sync/stream/AbstractRemote.js +5 -13
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +45 -11
- package/lib/client/sync/stream/WebsocketClientTransport.js +13 -1
- package/lib/db/crud/SyncStatus.js +15 -1
- package/package.json +1 -1
|
@@ -73,7 +73,9 @@ export class ConnectionManager extends BaseObserver {
|
|
|
73
73
|
if (this.pendingConnectionOptions) {
|
|
74
74
|
// Pending options have been placed while connecting.
|
|
75
75
|
// Need to reconnect.
|
|
76
|
-
this.connectingPromise = this.connectInternal()
|
|
76
|
+
this.connectingPromise = this.connectInternal()
|
|
77
|
+
.catch(() => { })
|
|
78
|
+
.finally(checkConnection);
|
|
77
79
|
return this.connectingPromise;
|
|
78
80
|
}
|
|
79
81
|
else {
|
|
@@ -82,7 +84,9 @@ export class ConnectionManager extends BaseObserver {
|
|
|
82
84
|
return;
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
|
-
this.connectingPromise ??= this.connectInternal()
|
|
87
|
+
this.connectingPromise ??= this.connectInternal()
|
|
88
|
+
.catch(() => { })
|
|
89
|
+
.finally(checkConnection);
|
|
86
90
|
return this.connectingPromise;
|
|
87
91
|
}
|
|
88
92
|
async connectInternal() {
|
|
@@ -100,9 +104,11 @@ export class ConnectionManager extends BaseObserver {
|
|
|
100
104
|
if (!this.pendingConnectionOptions) {
|
|
101
105
|
this.logger.debug('No pending connection options found, not creating sync stream implementation');
|
|
102
106
|
// A disconnect could have cleared this.
|
|
107
|
+
resolve();
|
|
103
108
|
return;
|
|
104
109
|
}
|
|
105
110
|
if (this.disconnectingPromise) {
|
|
111
|
+
resolve();
|
|
106
112
|
return;
|
|
107
113
|
}
|
|
108
114
|
const { connector, options } = this.pendingConnectionOptions;
|
|
@@ -211,17 +211,12 @@ export class AbstractRemote {
|
|
|
211
211
|
// headers with websockets on web. The browser userAgent is however added
|
|
212
212
|
// automatically as a header.
|
|
213
213
|
const userAgent = this.getUserAgent();
|
|
214
|
-
|
|
214
|
+
const url = this.options.socketUrlTransformer(request.url);
|
|
215
215
|
const connector = new RSocketConnector({
|
|
216
216
|
transport: new WebsocketClientTransport({
|
|
217
|
-
url
|
|
217
|
+
url,
|
|
218
218
|
wsCreator: (url) => {
|
|
219
|
-
|
|
220
|
-
s.addEventListener('error', (e) => {
|
|
221
|
-
socketCreationError = new Error('Failed to create connection to websocket: ', e.target.url ?? '');
|
|
222
|
-
this.logger.warn('Socket error', e);
|
|
223
|
-
});
|
|
224
|
-
return s;
|
|
219
|
+
return this.createSocket(url);
|
|
225
220
|
}
|
|
226
221
|
}),
|
|
227
222
|
setup: {
|
|
@@ -243,11 +238,8 @@ export class AbstractRemote {
|
|
|
243
238
|
rsocket = await connector.connect();
|
|
244
239
|
}
|
|
245
240
|
catch (ex) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
* with detecting the exception inside async-mutex
|
|
249
|
-
*/
|
|
250
|
-
throw new Error(`Could not connect to PowerSync instance: ${JSON.stringify(ex ?? socketCreationError)}`);
|
|
241
|
+
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
242
|
+
throw ex;
|
|
251
243
|
}
|
|
252
244
|
const stream = new DataStream({
|
|
253
245
|
logger: this.logger,
|
|
@@ -286,6 +286,7 @@ The next upload iteration will be delayed.`);
|
|
|
286
286
|
*/
|
|
287
287
|
while (true) {
|
|
288
288
|
this.updateSyncStatus({ connecting: true });
|
|
289
|
+
let shouldDelayRetry = true;
|
|
289
290
|
try {
|
|
290
291
|
if (signal?.aborted) {
|
|
291
292
|
break;
|
|
@@ -303,10 +304,9 @@ The next upload iteration will be delayed.`);
|
|
|
303
304
|
* The nested abort controller will cleanup any open network requests and streams.
|
|
304
305
|
* The WebRemote should only abort pending fetch requests or close active Readable streams.
|
|
305
306
|
*/
|
|
306
|
-
let delay = true;
|
|
307
307
|
if (ex instanceof AbortOperation) {
|
|
308
308
|
this.logger.warn(ex);
|
|
309
|
-
|
|
309
|
+
shouldDelayRetry = false;
|
|
310
310
|
// A disconnect was requested, we should not delay since there is no explicit retry
|
|
311
311
|
}
|
|
312
312
|
else {
|
|
@@ -317,10 +317,6 @@ The next upload iteration will be delayed.`);
|
|
|
317
317
|
downloadError: ex
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
|
-
// On error, wait a little before retrying
|
|
321
|
-
if (delay) {
|
|
322
|
-
await this.delayRetry();
|
|
323
|
-
}
|
|
324
320
|
}
|
|
325
321
|
finally {
|
|
326
322
|
if (!signal.aborted) {
|
|
@@ -331,6 +327,10 @@ The next upload iteration will be delayed.`);
|
|
|
331
327
|
connected: false,
|
|
332
328
|
connecting: true // May be unnecessary
|
|
333
329
|
});
|
|
330
|
+
// On error, wait a little before retrying
|
|
331
|
+
if (shouldDelayRetry) {
|
|
332
|
+
await this.delayRetry(nestedAbortController.signal);
|
|
333
|
+
}
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
// Mark as disconnected if here
|
|
@@ -509,7 +509,7 @@ The next upload iteration will be delayed.`);
|
|
|
509
509
|
if (progressForBucket) {
|
|
510
510
|
updatedProgress[data.bucket] = {
|
|
511
511
|
...progressForBucket,
|
|
512
|
-
sinceLast: progressForBucket.sinceLast + data.data.length
|
|
512
|
+
sinceLast: Math.min(progressForBucket.sinceLast + data.data.length, progressForBucket.targetCount - progressForBucket.atLast)
|
|
513
513
|
};
|
|
514
514
|
}
|
|
515
515
|
}
|
|
@@ -574,16 +574,32 @@ The next upload iteration will be delayed.`);
|
|
|
574
574
|
async updateSyncStatusForStartingCheckpoint(checkpoint) {
|
|
575
575
|
const localProgress = await this.options.adapter.getBucketOperationProgress();
|
|
576
576
|
const progress = {};
|
|
577
|
+
let invalidated = false;
|
|
577
578
|
for (const bucket of checkpoint.buckets) {
|
|
578
579
|
const savedProgress = localProgress[bucket.bucket];
|
|
580
|
+
const atLast = savedProgress?.atLast ?? 0;
|
|
581
|
+
const sinceLast = savedProgress?.sinceLast ?? 0;
|
|
579
582
|
progress[bucket.bucket] = {
|
|
580
583
|
// The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
|
|
581
584
|
// will use by default.
|
|
582
585
|
priority: bucket.priority ?? 3,
|
|
583
|
-
atLast:
|
|
584
|
-
sinceLast:
|
|
586
|
+
atLast: atLast,
|
|
587
|
+
sinceLast: sinceLast,
|
|
585
588
|
targetCount: bucket.count ?? 0
|
|
586
589
|
};
|
|
590
|
+
if (bucket.count != null && bucket.count < atLast + sinceLast) {
|
|
591
|
+
// Either due to a defrag / sync rule deploy or a compaction operation, the size
|
|
592
|
+
// of the bucket shrank so much that the local ops exceed the ops in the updated
|
|
593
|
+
// bucket. We can't prossibly report progress in this case (it would overshoot 100%).
|
|
594
|
+
invalidated = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (invalidated) {
|
|
598
|
+
for (const bucket in progress) {
|
|
599
|
+
const bucketProgress = progress[bucket];
|
|
600
|
+
bucketProgress.atLast = 0;
|
|
601
|
+
bucketProgress.sinceLast = 0;
|
|
602
|
+
}
|
|
587
603
|
}
|
|
588
604
|
this.updateSyncStatus({
|
|
589
605
|
dataFlow: {
|
|
@@ -651,7 +667,25 @@ The next upload iteration will be delayed.`);
|
|
|
651
667
|
// trigger this for all updates
|
|
652
668
|
this.iterateListeners((cb) => cb.statusUpdated?.(options));
|
|
653
669
|
}
|
|
654
|
-
async delayRetry() {
|
|
655
|
-
return new Promise((resolve) =>
|
|
670
|
+
async delayRetry(signal) {
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
if (signal?.aborted) {
|
|
673
|
+
// If the signal is already aborted, resolve immediately
|
|
674
|
+
resolve();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const { retryDelayMs } = this.options;
|
|
678
|
+
let timeoutId;
|
|
679
|
+
const endDelay = () => {
|
|
680
|
+
resolve();
|
|
681
|
+
if (timeoutId) {
|
|
682
|
+
clearTimeout(timeoutId);
|
|
683
|
+
timeoutId = undefined;
|
|
684
|
+
}
|
|
685
|
+
signal?.removeEventListener('abort', endDelay);
|
|
686
|
+
};
|
|
687
|
+
signal?.addEventListener('abort', endDelay, { once: true });
|
|
688
|
+
timeoutId = setTimeout(endDelay, retryDelayMs);
|
|
689
|
+
});
|
|
656
690
|
}
|
|
657
691
|
}
|
|
@@ -25,7 +25,19 @@ export class WebsocketClientTransport {
|
|
|
25
25
|
};
|
|
26
26
|
const errorListener = (ev) => {
|
|
27
27
|
removeListeners();
|
|
28
|
-
|
|
28
|
+
// We add a default error in that case.
|
|
29
|
+
if (ev.error != null) {
|
|
30
|
+
// undici typically provides an error object
|
|
31
|
+
reject(ev.error);
|
|
32
|
+
}
|
|
33
|
+
else if (ev.message != null) {
|
|
34
|
+
// React Native typically does not provide an error object, but does provide a message
|
|
35
|
+
reject(new Error(`Failed to create websocket connection: ${ev.message}`));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Browsers often provide no details at all
|
|
39
|
+
reject(new Error(`Failed to create websocket connection to ${this.url}`));
|
|
40
|
+
}
|
|
29
41
|
};
|
|
30
42
|
/**
|
|
31
43
|
* In some cases, such as React Native iOS, the WebSocket connection may close immediately after opening
|
|
@@ -123,7 +123,21 @@ export class SyncStatus {
|
|
|
123
123
|
* @returns {boolean} True if the instances are considered equal, false otherwise
|
|
124
124
|
*/
|
|
125
125
|
isEqual(status) {
|
|
126
|
-
|
|
126
|
+
/**
|
|
127
|
+
* By default Error object are serialized to an empty object.
|
|
128
|
+
* This replaces Errors with more useful information before serialization.
|
|
129
|
+
*/
|
|
130
|
+
const replacer = (_, value) => {
|
|
131
|
+
if (value instanceof Error) {
|
|
132
|
+
return {
|
|
133
|
+
name: value.name,
|
|
134
|
+
message: value.message,
|
|
135
|
+
stack: value.stack
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
};
|
|
140
|
+
return JSON.stringify(this.options, replacer) == JSON.stringify(status.options, replacer);
|
|
127
141
|
}
|
|
128
142
|
/**
|
|
129
143
|
* Creates a human-readable string representation of the current sync status.
|