@powersync/common 1.53.1 → 1.53.2
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.cjs +66 -62
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +66 -62
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +66 -62
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +66 -62
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +6 -9
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +6 -9
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +28 -43
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
- package/lib/utils/async.d.ts +13 -7
- package/lib/utils/async.js +38 -24
- package/lib/utils/async.js.map +1 -1
- package/package.json +1 -1
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +31 -54
- package/src/utils/async.ts +51 -24
|
@@ -3,7 +3,6 @@ import Logger, { ILogger } from 'js-logger';
|
|
|
3
3
|
import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
|
|
4
4
|
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
5
5
|
import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
|
|
6
|
-
import { throttleLeadingTrailing } from '../../../utils/async.js';
|
|
7
6
|
import { BucketStorageAdapter, PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
|
|
8
7
|
import { CrudEntry } from '../bucket/CrudEntry.js';
|
|
9
8
|
import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js';
|
|
@@ -17,10 +16,10 @@ import {
|
|
|
17
16
|
doneResult,
|
|
18
17
|
injectable,
|
|
19
18
|
InjectableIterator,
|
|
20
|
-
map,
|
|
21
19
|
SimpleAsyncIterator,
|
|
22
20
|
valueResult
|
|
23
21
|
} from '../../../utils/stream_transform.js';
|
|
22
|
+
import { asyncNotifier } from '../../../utils/async.js';
|
|
24
23
|
import { StreamingSyncRequestParameterType } from './JsonValue.js';
|
|
25
24
|
|
|
26
25
|
export enum LockType {
|
|
@@ -209,33 +208,23 @@ export type SubscribedStream = {
|
|
|
209
208
|
params: Record<string, any> | null;
|
|
210
209
|
};
|
|
211
210
|
|
|
212
|
-
// The priority we assume when we receive checkpoint lines where no priority is set.
|
|
213
|
-
// This is the default priority used by the sync service, but can be set to an arbitrary
|
|
214
|
-
// value since sync services without priorities also won't send partial sync completion
|
|
215
|
-
// messages.
|
|
216
|
-
const FALLBACK_PRIORITY = 3;
|
|
217
|
-
|
|
218
211
|
export abstract class AbstractStreamingSyncImplementation
|
|
219
212
|
extends BaseObserver<StreamingSyncImplementationListener>
|
|
220
213
|
implements StreamingSyncImplementation
|
|
221
214
|
{
|
|
222
215
|
protected options: AbstractStreamingSyncImplementationOptions;
|
|
223
216
|
protected abortController: AbortController | null;
|
|
224
|
-
// In rare cases, mostly for tests, uploads can be triggered without being properly connected.
|
|
225
|
-
// This allows ensuring that all upload processes can be aborted.
|
|
226
|
-
protected uploadAbortController: AbortController | undefined;
|
|
227
217
|
protected crudUpdateListener?: () => void;
|
|
228
|
-
protected streamingSyncPromise?: Promise<void>;
|
|
218
|
+
protected streamingSyncPromise?: Promise<[void, void]>;
|
|
229
219
|
protected logger: ILogger;
|
|
230
220
|
private activeStreams: SubscribedStream[];
|
|
231
221
|
private connectionMayHaveChanged = false;
|
|
222
|
+
private crudUploadNotifier = asyncNotifier();
|
|
232
223
|
|
|
233
|
-
private isUploadingCrud: boolean = false;
|
|
234
224
|
private notifyCompletedUploads?: () => void;
|
|
235
225
|
private handleActiveStreamsChange?: () => void;
|
|
236
226
|
|
|
237
227
|
syncStatus: SyncStatus;
|
|
238
|
-
triggerCrudUpload: () => void;
|
|
239
228
|
|
|
240
229
|
constructor(options: AbstractStreamingSyncImplementationOptions) {
|
|
241
230
|
super();
|
|
@@ -253,18 +242,10 @@ export abstract class AbstractStreamingSyncImplementation
|
|
|
253
242
|
}
|
|
254
243
|
});
|
|
255
244
|
this.abortController = null;
|
|
245
|
+
}
|
|
256
246
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
this.isUploadingCrud = true;
|
|
263
|
-
this._uploadAllCrud().finally(() => {
|
|
264
|
-
this.notifyCompletedUploads?.();
|
|
265
|
-
this.isUploadingCrud = false;
|
|
266
|
-
});
|
|
267
|
-
}, this.options.crudUploadThrottleMs!);
|
|
247
|
+
triggerCrudUpload() {
|
|
248
|
+
this.crudUploadNotifier.notify();
|
|
268
249
|
}
|
|
269
250
|
|
|
270
251
|
async waitForReady() {}
|
|
@@ -320,7 +301,6 @@ export abstract class AbstractStreamingSyncImplementation
|
|
|
320
301
|
super.dispose();
|
|
321
302
|
this.crudUpdateListener?.();
|
|
322
303
|
this.crudUpdateListener = undefined;
|
|
323
|
-
this.uploadAbortController?.abort();
|
|
324
304
|
}
|
|
325
305
|
|
|
326
306
|
abstract obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
|
|
@@ -334,7 +314,19 @@ export abstract class AbstractStreamingSyncImplementation
|
|
|
334
314
|
return checkpoint;
|
|
335
315
|
}
|
|
336
316
|
|
|
337
|
-
|
|
317
|
+
private async crudUploadLoop(signal: AbortSignal): Promise<void> {
|
|
318
|
+
while (!signal.aborted) {
|
|
319
|
+
await Promise.all([
|
|
320
|
+
// Start the initial CRUD upload on connect. Then, keep polling until we're done.
|
|
321
|
+
this._uploadAllCrud(signal),
|
|
322
|
+
this.delayRetry(signal, this.options.crudUploadThrottleMs!)
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
await this.crudUploadNotifier.waitForNotification(signal);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async _uploadAllCrud(signal: AbortSignal): Promise<void> {
|
|
338
330
|
return this.obtainLock({
|
|
339
331
|
type: LockType.CRUD,
|
|
340
332
|
callback: async () => {
|
|
@@ -343,17 +335,7 @@ export abstract class AbstractStreamingSyncImplementation
|
|
|
343
335
|
*/
|
|
344
336
|
let checkedCrudItem: CrudEntry | undefined;
|
|
345
337
|
|
|
346
|
-
|
|
347
|
-
this.uploadAbortController = controller;
|
|
348
|
-
this.abortController?.signal.addEventListener(
|
|
349
|
-
'abort',
|
|
350
|
-
() => {
|
|
351
|
-
controller.abort();
|
|
352
|
-
},
|
|
353
|
-
{ once: true }
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
while (!controller.signal.aborted) {
|
|
338
|
+
while (!signal.aborted) {
|
|
357
339
|
try {
|
|
358
340
|
/**
|
|
359
341
|
* This is the first item in the FIFO CRUD queue.
|
|
@@ -384,7 +366,9 @@ The next upload iteration will be delayed.`);
|
|
|
384
366
|
} else {
|
|
385
367
|
// Uploading is completed
|
|
386
368
|
const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
|
|
387
|
-
if (neededUpdate
|
|
369
|
+
if (neededUpdate) {
|
|
370
|
+
this.notifyCompletedUploads?.();
|
|
371
|
+
} else if (checkedCrudItem != null) {
|
|
388
372
|
// Only log this if there was something to upload
|
|
389
373
|
this.logger.debug('Upload complete, no write checkpoint needed.');
|
|
390
374
|
}
|
|
@@ -398,7 +382,7 @@ The next upload iteration will be delayed.`);
|
|
|
398
382
|
uploadError: ex as Error
|
|
399
383
|
}
|
|
400
384
|
});
|
|
401
|
-
await this.delayRetry(
|
|
385
|
+
await this.delayRetry(signal);
|
|
402
386
|
if (!this.isConnected) {
|
|
403
387
|
// Exit the upload loop if the sync stream is no longer connected
|
|
404
388
|
break;
|
|
@@ -414,7 +398,6 @@ The next upload iteration will be delayed.`);
|
|
|
414
398
|
});
|
|
415
399
|
}
|
|
416
400
|
}
|
|
417
|
-
this.uploadAbortController = undefined;
|
|
418
401
|
}
|
|
419
402
|
});
|
|
420
403
|
}
|
|
@@ -426,7 +409,10 @@ The next upload iteration will be delayed.`);
|
|
|
426
409
|
|
|
427
410
|
const controller = new AbortController();
|
|
428
411
|
this.abortController = controller;
|
|
429
|
-
this.streamingSyncPromise =
|
|
412
|
+
this.streamingSyncPromise = Promise.all([
|
|
413
|
+
this.crudUploadLoop(controller.signal).catch((ex) => this.logger.error('Error in crud upload loop', ex)),
|
|
414
|
+
this.streamingSync(controller.signal, options)
|
|
415
|
+
]);
|
|
430
416
|
|
|
431
417
|
// Return a promise that resolves when the connection status is updated to indicate that we're connected.
|
|
432
418
|
return new Promise<void>((resolve) => {
|
|
@@ -469,15 +455,7 @@ The next upload iteration will be delayed.`);
|
|
|
469
455
|
this.updateSyncStatus({ connected: false, connecting: false });
|
|
470
456
|
}
|
|
471
457
|
|
|
472
|
-
|
|
473
|
-
* @deprecated use [connect instead]
|
|
474
|
-
*/
|
|
475
|
-
async streamingSync(signal?: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void> {
|
|
476
|
-
if (!signal) {
|
|
477
|
-
this.abortController = new AbortController();
|
|
478
|
-
signal = this.abortController.signal;
|
|
479
|
-
}
|
|
480
|
-
|
|
458
|
+
private async streamingSync(signal: AbortSignal, options?: PowerSyncConnectionOptions): Promise<void> {
|
|
481
459
|
/**
|
|
482
460
|
* Listen for CRUD updates and trigger upstream uploads
|
|
483
461
|
*/
|
|
@@ -902,14 +880,13 @@ The next upload iteration will be delayed.`);
|
|
|
902
880
|
this.iterateListeners((cb) => cb.statusUpdated?.(options));
|
|
903
881
|
}
|
|
904
882
|
|
|
905
|
-
private async delayRetry(signal?: AbortSignal): Promise<void> {
|
|
883
|
+
private async delayRetry(signal?: AbortSignal, delay = this.options.retryDelayMs): Promise<void> {
|
|
906
884
|
return new Promise((resolve) => {
|
|
907
885
|
if (signal?.aborted) {
|
|
908
886
|
// If the signal is already aborted, resolve immediately
|
|
909
887
|
resolve();
|
|
910
888
|
return;
|
|
911
889
|
}
|
|
912
|
-
const { retryDelayMs } = this.options;
|
|
913
890
|
|
|
914
891
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
915
892
|
|
|
@@ -923,7 +900,7 @@ The next upload iteration will be delayed.`);
|
|
|
923
900
|
};
|
|
924
901
|
|
|
925
902
|
signal?.addEventListener('abort', endDelay, { once: true });
|
|
926
|
-
timeoutId = setTimeout(endDelay,
|
|
903
|
+
timeoutId = setTimeout(endDelay, delay);
|
|
927
904
|
});
|
|
928
905
|
}
|
|
929
906
|
|
package/src/utils/async.ts
CHANGED
|
@@ -19,32 +19,59 @@ export function throttleTrailing(func: () => void, wait: number) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export function throttleLeadingTrailing(func: () => void, wait: number) {
|
|
29
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
30
|
-
let lastCallTime: number = 0;
|
|
22
|
+
export interface AsyncNotifier {
|
|
23
|
+
/**
|
|
24
|
+
* @param signal Also resolve the promise once this signal completes.
|
|
25
|
+
* @returns A promise that resolves once {@link notify} is called after this promise was last resolved.
|
|
26
|
+
*/
|
|
27
|
+
waitForNotification(signal: AbortSignal): Promise<void>;
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Notifies a pending listener, or makes the next {@link waitForNotification} complete immediately if no listener
|
|
31
|
+
* is currently active.
|
|
32
|
+
*/
|
|
33
|
+
notify(): void;
|
|
34
|
+
}
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
export function asyncNotifier(): AsyncNotifier {
|
|
37
|
+
let waitingConsumer: (() => void) | null = null;
|
|
38
|
+
let hasPendingNotification = false;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
notify() {
|
|
42
|
+
if (waitingConsumer != null) {
|
|
43
|
+
waitingConsumer();
|
|
44
|
+
waitingConsumer = null;
|
|
45
|
+
} else {
|
|
46
|
+
hasPendingNotification = true;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
waitForNotification(signal: AbortSignal) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
if (waitingConsumer != null) {
|
|
52
|
+
throw new Error('Illegal call to waitForNotification, already has a waiter.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (signal.aborted) {
|
|
56
|
+
resolve();
|
|
57
|
+
} else if (hasPendingNotification) {
|
|
58
|
+
resolve();
|
|
59
|
+
hasPendingNotification = false;
|
|
60
|
+
} else {
|
|
61
|
+
function complete() {
|
|
62
|
+
signal.removeEventListener('abort', onAbort);
|
|
63
|
+
resolve();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function onAbort() {
|
|
67
|
+
waitingConsumer = null;
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
waitingConsumer = complete;
|
|
72
|
+
signal.addEventListener('abort', onAbort);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
48
75
|
}
|
|
49
76
|
};
|
|
50
77
|
}
|