@powersync/common 0.0.0-dev-20250710153817 → 0.0.0-dev-20250711131120
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 +1 -1
- package/dist/bundle.mjs +1 -1
- package/lib/client/sync/stream/AbstractRemote.d.ts +1 -10
- package/lib/client/sync/stream/AbstractRemote.js +0 -16
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -1
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +65 -69
- package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
- package/lib/utils/async.d.ts +0 -1
- package/lib/utils/async.js +0 -10
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { type fetch } from 'cross-fetch';
|
|
|
3
3
|
import Logger, { ILogger } from 'js-logger';
|
|
4
4
|
import { DataStream } from '../../../utils/DataStream.js';
|
|
5
5
|
import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js';
|
|
6
|
-
import {
|
|
6
|
+
import { StreamingSyncRequest } from './streaming-sync-types.js';
|
|
7
7
|
export type BSONImplementation = typeof BSON;
|
|
8
8
|
export type RemoteConnector = {
|
|
9
9
|
fetchCredentials: () => Promise<PowerSyncCredentials | null>;
|
|
@@ -120,11 +120,6 @@ export declare abstract class AbstractRemote {
|
|
|
120
120
|
*/
|
|
121
121
|
abstract getBSON(): Promise<BSONImplementation>;
|
|
122
122
|
protected createSocket(url: string): WebSocket;
|
|
123
|
-
/**
|
|
124
|
-
* Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
|
|
125
|
-
* sent by the server.
|
|
126
|
-
*/
|
|
127
|
-
socketStream(options: SocketSyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
|
|
128
123
|
/**
|
|
129
124
|
* Returns a data stream of sync line data.
|
|
130
125
|
*
|
|
@@ -133,10 +128,6 @@ export declare abstract class AbstractRemote {
|
|
|
133
128
|
* (required for compatibility with older sync services).
|
|
134
129
|
*/
|
|
135
130
|
socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
|
|
136
|
-
/**
|
|
137
|
-
* Connects to the sync/stream http endpoint, parsing lines as JSON.
|
|
138
|
-
*/
|
|
139
|
-
postStream(options: SyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
|
|
140
131
|
/**
|
|
141
132
|
* Connects to the sync/stream http endpoint, mapping and emitting each received string line.
|
|
142
133
|
*/
|
|
@@ -178,14 +178,6 @@ export class AbstractRemote {
|
|
|
178
178
|
createSocket(url) {
|
|
179
179
|
return new WebSocket(url);
|
|
180
180
|
}
|
|
181
|
-
/**
|
|
182
|
-
* Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
|
|
183
|
-
* sent by the server.
|
|
184
|
-
*/
|
|
185
|
-
async socketStream(options) {
|
|
186
|
-
const bson = await this.getBSON();
|
|
187
|
-
return await this.socketStreamRaw(options, (data) => bson.deserialize(data), bson);
|
|
188
|
-
}
|
|
189
181
|
/**
|
|
190
182
|
* Returns a data stream of sync line data.
|
|
191
183
|
*
|
|
@@ -363,14 +355,6 @@ export class AbstractRemote {
|
|
|
363
355
|
}
|
|
364
356
|
return stream;
|
|
365
357
|
}
|
|
366
|
-
/**
|
|
367
|
-
* Connects to the sync/stream http endpoint, parsing lines as JSON.
|
|
368
|
-
*/
|
|
369
|
-
async postStream(options) {
|
|
370
|
-
return await this.postStreamRaw(options, (line) => {
|
|
371
|
-
return JSON.parse(line);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
358
|
/**
|
|
375
359
|
* Connects to the sync/stream http endpoint, mapping and emitting each received string line.
|
|
376
360
|
*/
|
|
@@ -166,7 +166,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
166
166
|
protected abortController: AbortController | null;
|
|
167
167
|
protected crudUpdateListener?: () => void;
|
|
168
168
|
protected streamingSyncPromise?: Promise<void>;
|
|
169
|
-
private
|
|
169
|
+
private isUploadingCrud;
|
|
170
170
|
private notifyCompletedUploads?;
|
|
171
171
|
syncStatus: SyncStatus;
|
|
172
172
|
triggerCrudUpload: () => void;
|
|
@@ -3,7 +3,7 @@ import { SyncStatus } from '../../../db/crud/SyncStatus.js';
|
|
|
3
3
|
import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
|
|
4
4
|
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
5
5
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
6
|
-
import {
|
|
6
|
+
import { throttleLeadingTrailing } from '../../../utils/async.js';
|
|
7
7
|
import { PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
|
|
8
8
|
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
|
|
9
9
|
import { FetchStrategy } from './AbstractRemote.js';
|
|
@@ -85,7 +85,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
85
85
|
abortController;
|
|
86
86
|
crudUpdateListener;
|
|
87
87
|
streamingSyncPromise;
|
|
88
|
-
|
|
88
|
+
isUploadingCrud = false;
|
|
89
89
|
notifyCompletedUploads;
|
|
90
90
|
syncStatus;
|
|
91
91
|
triggerCrudUpload;
|
|
@@ -103,15 +103,13 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
103
103
|
});
|
|
104
104
|
this.abortController = null;
|
|
105
105
|
this.triggerCrudUpload = throttleLeadingTrailing(() => {
|
|
106
|
-
if (!this.syncStatus.connected || this.
|
|
106
|
+
if (!this.syncStatus.connected || this.isUploadingCrud) {
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
resolve();
|
|
114
|
-
});
|
|
109
|
+
this.isUploadingCrud = true;
|
|
110
|
+
this._uploadAllCrud().finally(() => {
|
|
111
|
+
this.notifyCompletedUploads?.();
|
|
112
|
+
this.isUploadingCrud = false;
|
|
115
113
|
});
|
|
116
114
|
}, this.options.crudUploadThrottleMs);
|
|
117
115
|
}
|
|
@@ -368,6 +366,7 @@ The next upload iteration will be delayed.`);
|
|
|
368
366
|
});
|
|
369
367
|
}
|
|
370
368
|
finally {
|
|
369
|
+
this.notifyCompletedUploads = undefined;
|
|
371
370
|
if (!signal.aborted) {
|
|
372
371
|
nestedAbortController.abort(new AbortOperation('Closing sync stream network requests before retry.'));
|
|
373
372
|
nestedAbortController = new AbortController();
|
|
@@ -445,10 +444,9 @@ The next upload iteration will be delayed.`);
|
|
|
445
444
|
this.logger.debug('Streaming sync iteration started');
|
|
446
445
|
this.options.adapter.startSession();
|
|
447
446
|
let [req, bucketMap] = await this.collectLocalBucketState();
|
|
448
|
-
// These are compared by reference
|
|
449
447
|
let targetCheckpoint = null;
|
|
450
|
-
|
|
451
|
-
let
|
|
448
|
+
// A checkpoint that has been validated but not applied (e.g. due to pending local writes)
|
|
449
|
+
let pendingValidatedCheckpoint = null;
|
|
452
450
|
const clientId = await this.options.adapter.getClientId();
|
|
453
451
|
const usingFixedKeyFormat = await this.requireKeyFormat(false);
|
|
454
452
|
this.logger.debug('Requesting stream from server');
|
|
@@ -465,21 +463,55 @@ The next upload iteration will be delayed.`);
|
|
|
465
463
|
};
|
|
466
464
|
let stream;
|
|
467
465
|
if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
468
|
-
stream = await this.options.remote.
|
|
466
|
+
stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
|
|
467
|
+
if (typeof line == 'string') {
|
|
468
|
+
return JSON.parse(line);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
// Directly enqueued by us
|
|
472
|
+
return line;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
469
475
|
}
|
|
470
476
|
else {
|
|
471
|
-
|
|
477
|
+
const bson = await this.options.remote.getBSON();
|
|
478
|
+
stream = await this.options.remote.socketStreamRaw({
|
|
472
479
|
...syncOptions,
|
|
473
480
|
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
474
|
-
})
|
|
481
|
+
}, (payload) => {
|
|
482
|
+
if (payload instanceof Uint8Array) {
|
|
483
|
+
return bson.deserialize(payload);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// Directly enqueued by us
|
|
487
|
+
return payload;
|
|
488
|
+
}
|
|
489
|
+
}, bson);
|
|
475
490
|
}
|
|
476
491
|
this.logger.debug('Stream established. Processing events');
|
|
492
|
+
this.notifyCompletedUploads = () => {
|
|
493
|
+
if (!stream.closed) {
|
|
494
|
+
stream.enqueueData({ crud_upload_completed: null });
|
|
495
|
+
}
|
|
496
|
+
};
|
|
477
497
|
while (!stream.closed) {
|
|
478
498
|
const line = await stream.read();
|
|
479
499
|
if (!line) {
|
|
480
500
|
// The stream has closed while waiting
|
|
481
501
|
return;
|
|
482
502
|
}
|
|
503
|
+
if ('crud_upload_completed' in line) {
|
|
504
|
+
if (pendingValidatedCheckpoint != null) {
|
|
505
|
+
const { applied, endIteration } = await this.applyCheckpoint(pendingValidatedCheckpoint);
|
|
506
|
+
if (applied) {
|
|
507
|
+
pendingValidatedCheckpoint = null;
|
|
508
|
+
}
|
|
509
|
+
else if (endIteration) {
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
483
515
|
// A connection is active and messages are being received
|
|
484
516
|
if (!this.syncStatus.connected) {
|
|
485
517
|
// There is a connection now
|
|
@@ -508,14 +540,13 @@ The next upload iteration will be delayed.`);
|
|
|
508
540
|
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
509
541
|
}
|
|
510
542
|
else if (isStreamingSyncCheckpointComplete(line)) {
|
|
511
|
-
const result = await this.applyCheckpoint(targetCheckpoint
|
|
543
|
+
const result = await this.applyCheckpoint(targetCheckpoint);
|
|
512
544
|
if (result.endIteration) {
|
|
513
545
|
return;
|
|
514
546
|
}
|
|
515
|
-
else if (result.applied) {
|
|
516
|
-
|
|
547
|
+
else if (!result.applied) {
|
|
548
|
+
pendingValidatedCheckpoint = targetCheckpoint;
|
|
517
549
|
}
|
|
518
|
-
validatedCheckpoint = targetCheckpoint;
|
|
519
550
|
}
|
|
520
551
|
else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
|
|
521
552
|
const priority = line.partial_checkpoint_complete.priority;
|
|
@@ -625,26 +656,7 @@ The next upload iteration will be delayed.`);
|
|
|
625
656
|
this.triggerCrudUpload();
|
|
626
657
|
}
|
|
627
658
|
else {
|
|
628
|
-
this.logger.debug('
|
|
629
|
-
if (targetCheckpoint === appliedCheckpoint) {
|
|
630
|
-
this.updateSyncStatus({
|
|
631
|
-
connected: true,
|
|
632
|
-
lastSyncedAt: new Date(),
|
|
633
|
-
priorityStatusEntries: [],
|
|
634
|
-
dataFlow: {
|
|
635
|
-
downloadError: undefined
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
else if (validatedCheckpoint === targetCheckpoint) {
|
|
640
|
-
const result = await this.applyCheckpoint(targetCheckpoint, signal);
|
|
641
|
-
if (result.endIteration) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
else if (result.applied) {
|
|
645
|
-
appliedCheckpoint = targetCheckpoint;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
659
|
+
this.logger.debug('Received unknown sync line', line);
|
|
648
660
|
}
|
|
649
661
|
}
|
|
650
662
|
this.logger.debug('Stream input empty');
|
|
@@ -860,9 +872,8 @@ The next upload iteration will be delayed.`);
|
|
|
860
872
|
}
|
|
861
873
|
});
|
|
862
874
|
}
|
|
863
|
-
async applyCheckpoint(checkpoint
|
|
875
|
+
async applyCheckpoint(checkpoint) {
|
|
864
876
|
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
865
|
-
const pending = this.pendingCrudUpload;
|
|
866
877
|
if (!result.checkpointValid) {
|
|
867
878
|
this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
|
|
868
879
|
// This means checksums failed. Start again with a new checkpoint.
|
|
@@ -870,36 +881,21 @@ The next upload iteration will be delayed.`);
|
|
|
870
881
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
871
882
|
return { applied: false, endIteration: true };
|
|
872
883
|
}
|
|
873
|
-
else if (!result.ready
|
|
874
|
-
|
|
875
|
-
// checkpoint, which prevented this checkpoint from applying. Wait for that to complete and
|
|
876
|
-
// try again.
|
|
877
|
-
this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. Waiting for in-progress upload before retrying.`);
|
|
878
|
-
await Promise.race([pending, onAbortPromise(abort)]);
|
|
879
|
-
this.logger.debug(`Pending uploads complete, retrying local checkpoint at ${checkpoint.last_op_id}`);
|
|
880
|
-
if (abort.aborted) {
|
|
881
|
-
return { applied: false, endIteration: true };
|
|
882
|
-
}
|
|
883
|
-
// Try again now that uploads have completed.
|
|
884
|
-
result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
885
|
-
}
|
|
886
|
-
if (result.checkpointValid && result.ready) {
|
|
887
|
-
this.logger.debug('validated checkpoint', checkpoint);
|
|
888
|
-
this.updateSyncStatus({
|
|
889
|
-
connected: true,
|
|
890
|
-
lastSyncedAt: new Date(),
|
|
891
|
-
dataFlow: {
|
|
892
|
-
downloading: false,
|
|
893
|
-
downloadProgress: null,
|
|
894
|
-
downloadError: undefined
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
return { applied: true, endIteration: false };
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
900
|
-
this.logger.debug('Could not apply checkpoint. Waiting for next sync complete line.');
|
|
884
|
+
else if (!result.ready) {
|
|
885
|
+
this.logger.debug('Could not apply checkpoint due to local data. We will retry applying the checkpoint after that upload is completed.');
|
|
901
886
|
return { applied: false, endIteration: false };
|
|
902
887
|
}
|
|
888
|
+
this.logger.debug('validated checkpoint', checkpoint);
|
|
889
|
+
this.updateSyncStatus({
|
|
890
|
+
connected: true,
|
|
891
|
+
lastSyncedAt: new Date(),
|
|
892
|
+
dataFlow: {
|
|
893
|
+
downloading: false,
|
|
894
|
+
downloadProgress: null,
|
|
895
|
+
downloadError: undefined
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
return { applied: true, endIteration: false };
|
|
903
899
|
}
|
|
904
900
|
updateSyncStatus(options) {
|
|
905
901
|
const updatedStatus = new SyncStatus({
|
|
@@ -101,6 +101,10 @@ export interface StreamingSyncKeepalive {
|
|
|
101
101
|
token_expires_in: number;
|
|
102
102
|
}
|
|
103
103
|
export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncCheckpointPartiallyComplete | StreamingSyncKeepalive;
|
|
104
|
+
export type CrudUploadNotification = {
|
|
105
|
+
crud_upload_completed: null;
|
|
106
|
+
};
|
|
107
|
+
export type StreamingSyncLineOrCrudUploadComplete = StreamingSyncLine | CrudUploadNotification;
|
|
104
108
|
export interface BucketRequest {
|
|
105
109
|
name: string;
|
|
106
110
|
/**
|
package/lib/utils/async.d.ts
CHANGED
|
@@ -12,4 +12,3 @@ export declare function throttleTrailing(func: () => void, wait: number): () =>
|
|
|
12
12
|
* Roughly equivalent to lodash/throttle with {leading: true, trailing: true}
|
|
13
13
|
*/
|
|
14
14
|
export declare function throttleLeadingTrailing(func: () => void, wait: number): () => void;
|
|
15
|
-
export declare function onAbortPromise(signal: AbortSignal): Promise<void>;
|
package/lib/utils/async.js
CHANGED
|
@@ -43,13 +43,3 @@ export function throttleLeadingTrailing(func, wait) {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
export function onAbortPromise(signal) {
|
|
47
|
-
return new Promise((resolve) => {
|
|
48
|
-
if (signal.aborted) {
|
|
49
|
-
resolve();
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
signal.onabort = () => resolve();
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|