@powersync/common 1.23.0 → 1.25.0
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 +3 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +12 -2
- package/lib/client/AbstractPowerSyncDatabase.js +53 -15
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -1
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +2 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.js +34 -10
- package/lib/client/sync/stream/AbstractRemote.d.ts +16 -1
- package/lib/client/sync/stream/AbstractRemote.js +22 -8
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +8 -1
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +109 -39
- package/lib/client/sync/stream/streaming-sync-types.d.ts +8 -1
- package/lib/client/sync/stream/streaming-sync-types.js +3 -0
- package/lib/db/DBAdapter.d.ts +1 -1
- package/lib/db/crud/SyncStatus.d.ts +29 -0
- package/lib/db/crud/SyncStatus.js +46 -2
- package/lib/utils/DataStream.js +5 -6
- package/package.json +1 -3
|
@@ -4,7 +4,8 @@ import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
|
4
4
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
5
5
|
import { throttleLeadingTrailing } from '../../../utils/throttle.js';
|
|
6
6
|
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
|
|
7
|
-
import {
|
|
7
|
+
import { FetchStrategy } from './AbstractRemote.js';
|
|
8
|
+
import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData } from './streaming-sync-types.js';
|
|
8
9
|
export var LockType;
|
|
9
10
|
(function (LockType) {
|
|
10
11
|
LockType["CRUD"] = "crud";
|
|
@@ -24,8 +25,14 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
|
|
|
24
25
|
};
|
|
25
26
|
export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
26
27
|
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
|
|
28
|
+
fetchStrategy: FetchStrategy.Buffered,
|
|
27
29
|
params: {}
|
|
28
30
|
};
|
|
31
|
+
// The priority we assume when we receive checkpoint lines where no priority is set.
|
|
32
|
+
// This is the default priority used by the sync service, but can be set to an arbitrary
|
|
33
|
+
// value since sync services without priorities also won't send partial sync completion
|
|
34
|
+
// messages.
|
|
35
|
+
const FALLBACK_PRIORITY = 3;
|
|
29
36
|
export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
30
37
|
_lastSyncedAt;
|
|
31
38
|
options;
|
|
@@ -39,6 +46,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
39
46
|
this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
|
|
40
47
|
this.syncStatus = new SyncStatus({
|
|
41
48
|
connected: false,
|
|
49
|
+
connecting: false,
|
|
42
50
|
lastSyncedAt: undefined,
|
|
43
51
|
dataFlow: {
|
|
44
52
|
uploading: false,
|
|
@@ -55,23 +63,32 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
55
63
|
}
|
|
56
64
|
async waitForReady() { }
|
|
57
65
|
waitForStatus(status) {
|
|
66
|
+
return this.waitUntilStatusMatches((currentStatus) => {
|
|
67
|
+
/**
|
|
68
|
+
* Match only the partial status options provided in the
|
|
69
|
+
* matching status
|
|
70
|
+
*/
|
|
71
|
+
const matchPartialObject = (compA, compB) => {
|
|
72
|
+
return Object.entries(compA).every(([key, value]) => {
|
|
73
|
+
const comparisonBValue = compB[key];
|
|
74
|
+
if (typeof value == 'object' && typeof comparisonBValue == 'object') {
|
|
75
|
+
return matchPartialObject(value, comparisonBValue);
|
|
76
|
+
}
|
|
77
|
+
return value == comparisonBValue;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
return matchPartialObject(status, currentStatus);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
waitUntilStatusMatches(predicate) {
|
|
58
84
|
return new Promise((resolve) => {
|
|
85
|
+
if (predicate(this.syncStatus)) {
|
|
86
|
+
resolve();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
59
89
|
const l = this.registerListener({
|
|
60
90
|
statusChanged: (updatedStatus) => {
|
|
61
|
-
|
|
62
|
-
* Match only the partial status options provided in the
|
|
63
|
-
* matching status
|
|
64
|
-
*/
|
|
65
|
-
const matchPartialObject = (compA, compB) => {
|
|
66
|
-
return Object.entries(compA).every(([key, value]) => {
|
|
67
|
-
const comparisonBValue = compB[key];
|
|
68
|
-
if (typeof value == 'object' && typeof comparisonBValue == 'object') {
|
|
69
|
-
return matchPartialObject(value, comparisonBValue);
|
|
70
|
-
}
|
|
71
|
-
return value == comparisonBValue;
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
if (matchPartialObject(status, updatedStatus.toJSON())) {
|
|
91
|
+
if (predicate(updatedStatus)) {
|
|
75
92
|
resolve();
|
|
76
93
|
l?.();
|
|
77
94
|
}
|
|
@@ -208,7 +225,7 @@ The next upload iteration will be delayed.`);
|
|
|
208
225
|
}
|
|
209
226
|
this.streamingSyncPromise = undefined;
|
|
210
227
|
this.abortController = null;
|
|
211
|
-
this.updateSyncStatus({ connected: false });
|
|
228
|
+
this.updateSyncStatus({ connected: false, connecting: false });
|
|
212
229
|
}
|
|
213
230
|
/**
|
|
214
231
|
* @deprecated use [connect instead]
|
|
@@ -239,6 +256,7 @@ The next upload iteration will be delayed.`);
|
|
|
239
256
|
this.crudUpdateListener = undefined;
|
|
240
257
|
this.updateSyncStatus({
|
|
241
258
|
connected: false,
|
|
259
|
+
connecting: false,
|
|
242
260
|
dataFlow: {
|
|
243
261
|
downloading: false
|
|
244
262
|
}
|
|
@@ -251,6 +269,7 @@ The next upload iteration will be delayed.`);
|
|
|
251
269
|
* - Close any sync stream ReadableStreams (which will also close any established network requests)
|
|
252
270
|
*/
|
|
253
271
|
while (true) {
|
|
272
|
+
this.updateSyncStatus({ connecting: true });
|
|
254
273
|
try {
|
|
255
274
|
if (signal?.aborted) {
|
|
256
275
|
break;
|
|
@@ -281,6 +300,7 @@ The next upload iteration will be delayed.`);
|
|
|
281
300
|
else {
|
|
282
301
|
this.logger.error(ex);
|
|
283
302
|
}
|
|
303
|
+
// On error, wait a little before retrying
|
|
284
304
|
await this.delayRetry();
|
|
285
305
|
}
|
|
286
306
|
finally {
|
|
@@ -289,13 +309,25 @@ The next upload iteration will be delayed.`);
|
|
|
289
309
|
nestedAbortController = new AbortController();
|
|
290
310
|
}
|
|
291
311
|
this.updateSyncStatus({
|
|
292
|
-
connected: false
|
|
312
|
+
connected: false,
|
|
313
|
+
connecting: true // May be unnecessary
|
|
293
314
|
});
|
|
294
|
-
// On error, wait a little before retrying
|
|
295
315
|
}
|
|
296
316
|
}
|
|
297
317
|
// Mark as disconnected if here
|
|
298
|
-
this.updateSyncStatus({ connected: false });
|
|
318
|
+
this.updateSyncStatus({ connected: false, connecting: false });
|
|
319
|
+
}
|
|
320
|
+
async collectLocalBucketState() {
|
|
321
|
+
const bucketEntries = await this.options.adapter.getBucketStates();
|
|
322
|
+
const req = bucketEntries.map((entry) => ({
|
|
323
|
+
name: entry.bucket,
|
|
324
|
+
after: entry.op_id
|
|
325
|
+
}));
|
|
326
|
+
const localDescriptions = new Map();
|
|
327
|
+
for (const entry of bucketEntries) {
|
|
328
|
+
localDescriptions.set(entry.bucket, null);
|
|
329
|
+
}
|
|
330
|
+
return [req, localDescriptions];
|
|
299
331
|
}
|
|
300
332
|
async streamingSyncIteration(signal, options) {
|
|
301
333
|
return await this.obtainLock({
|
|
@@ -308,20 +340,11 @@ The next upload iteration will be delayed.`);
|
|
|
308
340
|
};
|
|
309
341
|
this.logger.debug('Streaming sync iteration started');
|
|
310
342
|
this.options.adapter.startSession();
|
|
311
|
-
|
|
312
|
-
const initialBuckets = new Map();
|
|
313
|
-
bucketEntries.forEach((entry) => {
|
|
314
|
-
initialBuckets.set(entry.bucket, entry.op_id);
|
|
315
|
-
});
|
|
316
|
-
const req = Array.from(initialBuckets.entries()).map(([bucket, after]) => ({
|
|
317
|
-
name: bucket,
|
|
318
|
-
after: after
|
|
319
|
-
}));
|
|
343
|
+
let [req, bucketMap] = await this.collectLocalBucketState();
|
|
320
344
|
// These are compared by reference
|
|
321
345
|
let targetCheckpoint = null;
|
|
322
346
|
let validatedCheckpoint = null;
|
|
323
347
|
let appliedCheckpoint = null;
|
|
324
|
-
let bucketSet = new Set(initialBuckets.keys());
|
|
325
348
|
const clientId = await this.options.adapter.getClientId();
|
|
326
349
|
this.logger.debug('Requesting stream from server');
|
|
327
350
|
const syncOptions = {
|
|
@@ -335,9 +358,16 @@ The next upload iteration will be delayed.`);
|
|
|
335
358
|
client_id: clientId
|
|
336
359
|
}
|
|
337
360
|
};
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
let stream;
|
|
362
|
+
if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
363
|
+
stream = await this.options.remote.postStream(syncOptions);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
stream = await this.options.remote.socketStream({
|
|
367
|
+
...syncOptions,
|
|
368
|
+
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
369
|
+
});
|
|
370
|
+
}
|
|
341
371
|
this.logger.debug('Stream established. Processing events');
|
|
342
372
|
while (!stream.closed) {
|
|
343
373
|
const line = await stream.read();
|
|
@@ -355,16 +385,19 @@ The next upload iteration will be delayed.`);
|
|
|
355
385
|
}
|
|
356
386
|
if (isStreamingSyncCheckpoint(line)) {
|
|
357
387
|
targetCheckpoint = line.checkpoint;
|
|
358
|
-
const bucketsToDelete = new Set(
|
|
359
|
-
const newBuckets = new
|
|
388
|
+
const bucketsToDelete = new Set(bucketMap.keys());
|
|
389
|
+
const newBuckets = new Map();
|
|
360
390
|
for (const checksum of line.checkpoint.buckets) {
|
|
361
|
-
newBuckets.
|
|
391
|
+
newBuckets.set(checksum.bucket, {
|
|
392
|
+
name: checksum.bucket,
|
|
393
|
+
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
394
|
+
});
|
|
362
395
|
bucketsToDelete.delete(checksum.bucket);
|
|
363
396
|
}
|
|
364
397
|
if (bucketsToDelete.size > 0) {
|
|
365
398
|
this.logger.debug('Removing buckets', [...bucketsToDelete]);
|
|
366
399
|
}
|
|
367
|
-
|
|
400
|
+
bucketMap = newBuckets;
|
|
368
401
|
await this.options.adapter.removeBuckets([...bucketsToDelete]);
|
|
369
402
|
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
370
403
|
}
|
|
@@ -395,6 +428,35 @@ The next upload iteration will be delayed.`);
|
|
|
395
428
|
}
|
|
396
429
|
validatedCheckpoint = targetCheckpoint;
|
|
397
430
|
}
|
|
431
|
+
else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
|
|
432
|
+
const priority = line.partial_checkpoint_complete.priority;
|
|
433
|
+
this.logger.debug('Partial checkpoint complete', priority);
|
|
434
|
+
const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
|
|
435
|
+
if (!result.checkpointValid) {
|
|
436
|
+
// This means checksums failed. Start again with a new checkpoint.
|
|
437
|
+
// TODO: better back-off
|
|
438
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
439
|
+
return { retry: true };
|
|
440
|
+
}
|
|
441
|
+
else if (!result.ready) {
|
|
442
|
+
// Need more data for a consistent partial sync within a priority - continue waiting.
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
// We'll keep on downloading, but can report that this priority is synced now.
|
|
446
|
+
this.logger.debug('partial checkpoint validation succeeded');
|
|
447
|
+
// All states with a higher priority can be deleted since this partial sync includes them.
|
|
448
|
+
const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
|
|
449
|
+
priorityStates.push({
|
|
450
|
+
priority,
|
|
451
|
+
lastSyncedAt: new Date(),
|
|
452
|
+
hasSynced: true
|
|
453
|
+
});
|
|
454
|
+
this.updateSyncStatus({
|
|
455
|
+
connected: true,
|
|
456
|
+
priorityStatusEntries: priorityStates
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
398
460
|
else if (isStreamingSyncCheckpointDiff(line)) {
|
|
399
461
|
// TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
|
|
400
462
|
if (targetCheckpoint == null) {
|
|
@@ -417,7 +479,11 @@ The next upload iteration will be delayed.`);
|
|
|
417
479
|
write_checkpoint: diff.write_checkpoint
|
|
418
480
|
};
|
|
419
481
|
targetCheckpoint = newCheckpoint;
|
|
420
|
-
|
|
482
|
+
bucketMap = new Map();
|
|
483
|
+
newBuckets.forEach((checksum, name) => bucketMap.set(name, {
|
|
484
|
+
name: checksum.bucket,
|
|
485
|
+
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
486
|
+
}));
|
|
421
487
|
const bucketsToDelete = diff.removed_buckets;
|
|
422
488
|
if (bucketsToDelete.length > 0) {
|
|
423
489
|
this.logger.debug('Remove buckets', bucketsToDelete);
|
|
@@ -453,7 +519,8 @@ The next upload iteration will be delayed.`);
|
|
|
453
519
|
if (targetCheckpoint === appliedCheckpoint) {
|
|
454
520
|
this.updateSyncStatus({
|
|
455
521
|
connected: true,
|
|
456
|
-
lastSyncedAt: new Date()
|
|
522
|
+
lastSyncedAt: new Date(),
|
|
523
|
+
priorityStatusEntries: []
|
|
457
524
|
});
|
|
458
525
|
}
|
|
459
526
|
else if (validatedCheckpoint === targetCheckpoint) {
|
|
@@ -473,6 +540,7 @@ The next upload iteration will be delayed.`);
|
|
|
473
540
|
this.updateSyncStatus({
|
|
474
541
|
connected: true,
|
|
475
542
|
lastSyncedAt: new Date(),
|
|
543
|
+
priorityStatusEntries: [],
|
|
476
544
|
dataFlow: {
|
|
477
545
|
downloading: false
|
|
478
546
|
}
|
|
@@ -490,11 +558,13 @@ The next upload iteration will be delayed.`);
|
|
|
490
558
|
updateSyncStatus(options) {
|
|
491
559
|
const updatedStatus = new SyncStatus({
|
|
492
560
|
connected: options.connected ?? this.syncStatus.connected,
|
|
561
|
+
connecting: !options.connected && (options.connecting ?? this.syncStatus.connecting),
|
|
493
562
|
lastSyncedAt: options.lastSyncedAt ?? this.syncStatus.lastSyncedAt,
|
|
494
563
|
dataFlow: {
|
|
495
564
|
...this.syncStatus.dataFlowStatus,
|
|
496
565
|
...options.dataFlow
|
|
497
|
-
}
|
|
566
|
+
},
|
|
567
|
+
priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries
|
|
498
568
|
});
|
|
499
569
|
if (!this.syncStatus.isEqual(updatedStatus)) {
|
|
500
570
|
this.syncStatus = updatedStatus;
|
|
@@ -90,11 +90,17 @@ export interface StreamingSyncCheckpointComplete {
|
|
|
90
90
|
last_op_id: OpId;
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
export interface StreamingSyncCheckpointPartiallyComplete {
|
|
94
|
+
partial_checkpoint_complete: {
|
|
95
|
+
priority: number;
|
|
96
|
+
last_op_id: OpId;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
93
99
|
export interface StreamingSyncKeepalive {
|
|
94
100
|
/** If specified, token expires in this many seconds. */
|
|
95
101
|
token_expires_in: number;
|
|
96
102
|
}
|
|
97
|
-
export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncKeepalive;
|
|
103
|
+
export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncCheckpointPartiallyComplete | StreamingSyncKeepalive;
|
|
98
104
|
export interface BucketRequest {
|
|
99
105
|
name: string;
|
|
100
106
|
/**
|
|
@@ -106,6 +112,7 @@ export declare function isStreamingSyncData(line: StreamingSyncLine): line is St
|
|
|
106
112
|
export declare function isStreamingKeepalive(line: StreamingSyncLine): line is StreamingSyncKeepalive;
|
|
107
113
|
export declare function isStreamingSyncCheckpoint(line: StreamingSyncLine): line is StreamingSyncCheckpoint;
|
|
108
114
|
export declare function isStreamingSyncCheckpointComplete(line: StreamingSyncLine): line is StreamingSyncCheckpointComplete;
|
|
115
|
+
export declare function isStreamingSyncCheckpointPartiallyComplete(line: StreamingSyncLine): line is StreamingSyncCheckpointPartiallyComplete;
|
|
109
116
|
export declare function isStreamingSyncCheckpointDiff(line: StreamingSyncLine): line is StreamingSyncCheckpointDiff;
|
|
110
117
|
export declare function isContinueCheckpointRequest(request: SyncRequest): request is ContinueCheckpointRequest;
|
|
111
118
|
export declare function isSyncNewCheckpointRequest(request: SyncRequest): request is SyncNewCheckpointRequest;
|
|
@@ -10,6 +10,9 @@ export function isStreamingSyncCheckpoint(line) {
|
|
|
10
10
|
export function isStreamingSyncCheckpointComplete(line) {
|
|
11
11
|
return line.checkpoint_complete != null;
|
|
12
12
|
}
|
|
13
|
+
export function isStreamingSyncCheckpointPartiallyComplete(line) {
|
|
14
|
+
return line.partial_checkpoint_complete != null;
|
|
15
|
+
}
|
|
13
16
|
export function isStreamingSyncCheckpointDiff(line) {
|
|
14
17
|
return line.checkpoint_diff != null;
|
|
15
18
|
}
|
package/lib/db/DBAdapter.d.ts
CHANGED
|
@@ -82,7 +82,7 @@ export interface DBLockOptions {
|
|
|
82
82
|
timeoutMs?: number;
|
|
83
83
|
}
|
|
84
84
|
export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBGetUtils {
|
|
85
|
-
close: () => void
|
|
85
|
+
close: () => void | Promise<void>;
|
|
86
86
|
execute: (query: string, params?: any[]) => Promise<QueryResult>;
|
|
87
87
|
executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
|
|
88
88
|
name: string;
|
|
@@ -2,11 +2,18 @@ export type SyncDataFlowStatus = Partial<{
|
|
|
2
2
|
downloading: boolean;
|
|
3
3
|
uploading: boolean;
|
|
4
4
|
}>;
|
|
5
|
+
export interface SyncPriorityStatus {
|
|
6
|
+
priority: number;
|
|
7
|
+
lastSyncedAt?: Date;
|
|
8
|
+
hasSynced?: boolean;
|
|
9
|
+
}
|
|
5
10
|
export type SyncStatusOptions = {
|
|
6
11
|
connected?: boolean;
|
|
12
|
+
connecting?: boolean;
|
|
7
13
|
dataFlow?: SyncDataFlowStatus;
|
|
8
14
|
lastSyncedAt?: Date;
|
|
9
15
|
hasSynced?: boolean;
|
|
16
|
+
priorityStatusEntries?: SyncPriorityStatus[];
|
|
10
17
|
};
|
|
11
18
|
export declare class SyncStatus {
|
|
12
19
|
protected options: SyncStatusOptions;
|
|
@@ -15,6 +22,7 @@ export declare class SyncStatus {
|
|
|
15
22
|
* true if currently connected.
|
|
16
23
|
*/
|
|
17
24
|
get connected(): boolean;
|
|
25
|
+
get connecting(): boolean;
|
|
18
26
|
/**
|
|
19
27
|
* Time that a last sync has fully completed, if any.
|
|
20
28
|
* Currently this is reset to null after a restart.
|
|
@@ -32,7 +40,28 @@ export declare class SyncStatus {
|
|
|
32
40
|
downloading: boolean;
|
|
33
41
|
uploading: boolean;
|
|
34
42
|
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Partial sync status for involved bucket priorities.
|
|
45
|
+
*/
|
|
46
|
+
get priorityStatusEntries(): SyncPriorityStatus[];
|
|
47
|
+
/**
|
|
48
|
+
* Reports a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields that apply
|
|
49
|
+
* to a specific bucket priority instead of the entire sync operation.
|
|
50
|
+
*
|
|
51
|
+
* When buckets with different priorities are declared, PowerSync may choose to synchronize higher-priority
|
|
52
|
+
* buckets first. When a consistent view over all buckets for all priorities up until the given priority is
|
|
53
|
+
* reached, PowerSync makes data from those buckets available before lower-priority buckets have finished
|
|
54
|
+
* synchronizing.
|
|
55
|
+
* When PowerSync makes data for a given priority available, all buckets in higher-priorities are guaranteed to
|
|
56
|
+
* be consistent with that checkpoint. For this reason, this method may also return the status for lower priorities.
|
|
57
|
+
* In a state where the PowerSync just finished synchronizing buckets in priority level 3, calling this method
|
|
58
|
+
* with a priority of 1 may return information for priority level 3.
|
|
59
|
+
*
|
|
60
|
+
* @param priority The bucket priority for which the status should be reported.
|
|
61
|
+
*/
|
|
62
|
+
statusForPriority(priority: number): SyncPriorityStatus;
|
|
35
63
|
isEqual(status: SyncStatus): boolean;
|
|
36
64
|
getMessage(): string;
|
|
37
65
|
toJSON(): SyncStatusOptions;
|
|
66
|
+
private static comparePriorities;
|
|
38
67
|
}
|
|
@@ -9,6 +9,9 @@ export class SyncStatus {
|
|
|
9
9
|
get connected() {
|
|
10
10
|
return this.options.connected ?? false;
|
|
11
11
|
}
|
|
12
|
+
get connecting() {
|
|
13
|
+
return this.options.connecting ?? false;
|
|
14
|
+
}
|
|
12
15
|
/**
|
|
13
16
|
* Time that a last sync has fully completed, if any.
|
|
14
17
|
* Currently this is reset to null after a restart.
|
|
@@ -39,19 +42,60 @@ export class SyncStatus {
|
|
|
39
42
|
uploading: false
|
|
40
43
|
});
|
|
41
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Partial sync status for involved bucket priorities.
|
|
47
|
+
*/
|
|
48
|
+
get priorityStatusEntries() {
|
|
49
|
+
return (this.options.priorityStatusEntries ?? []).slice().sort(SyncStatus.comparePriorities);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reports a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields that apply
|
|
53
|
+
* to a specific bucket priority instead of the entire sync operation.
|
|
54
|
+
*
|
|
55
|
+
* When buckets with different priorities are declared, PowerSync may choose to synchronize higher-priority
|
|
56
|
+
* buckets first. When a consistent view over all buckets for all priorities up until the given priority is
|
|
57
|
+
* reached, PowerSync makes data from those buckets available before lower-priority buckets have finished
|
|
58
|
+
* synchronizing.
|
|
59
|
+
* When PowerSync makes data for a given priority available, all buckets in higher-priorities are guaranteed to
|
|
60
|
+
* be consistent with that checkpoint. For this reason, this method may also return the status for lower priorities.
|
|
61
|
+
* In a state where the PowerSync just finished synchronizing buckets in priority level 3, calling this method
|
|
62
|
+
* with a priority of 1 may return information for priority level 3.
|
|
63
|
+
*
|
|
64
|
+
* @param priority The bucket priority for which the status should be reported.
|
|
65
|
+
*/
|
|
66
|
+
statusForPriority(priority) {
|
|
67
|
+
// priorityStatusEntries are sorted by ascending priorities (so higher numbers to lower numbers).
|
|
68
|
+
for (const known of this.priorityStatusEntries) {
|
|
69
|
+
// We look for the first entry that doesn't have a higher priority.
|
|
70
|
+
if (known.priority >= priority) {
|
|
71
|
+
return known;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// If we have a complete sync, that necessarily includes all priorities.
|
|
75
|
+
return {
|
|
76
|
+
priority,
|
|
77
|
+
lastSyncedAt: this.lastSyncedAt,
|
|
78
|
+
hasSynced: this.hasSynced
|
|
79
|
+
};
|
|
80
|
+
}
|
|
42
81
|
isEqual(status) {
|
|
43
82
|
return JSON.stringify(this.options) == JSON.stringify(status.options);
|
|
44
83
|
}
|
|
45
84
|
getMessage() {
|
|
46
85
|
const dataFlow = this.dataFlowStatus;
|
|
47
|
-
return `SyncStatus<connected: ${this.connected} lastSyncedAt: ${this.lastSyncedAt} hasSynced: ${this.hasSynced}. Downloading: ${dataFlow.downloading}. Uploading: ${dataFlow.uploading}`;
|
|
86
|
+
return `SyncStatus<connected: ${this.connected} connecting: ${this.connecting} lastSyncedAt: ${this.lastSyncedAt} hasSynced: ${this.hasSynced}. Downloading: ${dataFlow.downloading}. Uploading: ${dataFlow.uploading}`;
|
|
48
87
|
}
|
|
49
88
|
toJSON() {
|
|
50
89
|
return {
|
|
51
90
|
connected: this.connected,
|
|
91
|
+
connecting: this.connecting,
|
|
52
92
|
dataFlow: this.dataFlowStatus,
|
|
53
93
|
lastSyncedAt: this.lastSyncedAt,
|
|
54
|
-
hasSynced: this.hasSynced
|
|
94
|
+
hasSynced: this.hasSynced,
|
|
95
|
+
priorityStatusEntries: this.priorityStatusEntries
|
|
55
96
|
};
|
|
56
97
|
}
|
|
98
|
+
static comparePriorities(a, b) {
|
|
99
|
+
return b.priority - a.priority; // Reverse because higher priorities have lower numbers
|
|
100
|
+
}
|
|
57
101
|
}
|
package/lib/utils/DataStream.js
CHANGED
|
@@ -63,9 +63,6 @@ export class DataStream extends BaseObserver {
|
|
|
63
63
|
* @returns a Data payload or Null if the stream closed.
|
|
64
64
|
*/
|
|
65
65
|
async read() {
|
|
66
|
-
if (this.dataQueue.length <= this.lowWatermark) {
|
|
67
|
-
await this.iterateAsyncErrored(async (l) => l.lowWater?.());
|
|
68
|
-
}
|
|
69
66
|
if (this.closed) {
|
|
70
67
|
return null;
|
|
71
68
|
}
|
|
@@ -133,12 +130,14 @@ export class DataStream extends BaseObserver {
|
|
|
133
130
|
return Array.from(this.listeners.values()).some((l) => !!l.data);
|
|
134
131
|
}
|
|
135
132
|
async _processQueue() {
|
|
136
|
-
if (
|
|
133
|
+
if (this.isClosed || !this.hasDataReader()) {
|
|
137
134
|
Promise.resolve().then(() => (this.processingPromise = null));
|
|
138
135
|
return;
|
|
139
136
|
}
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
if (this.dataQueue.length) {
|
|
138
|
+
const data = this.dataQueue.shift();
|
|
139
|
+
await this.iterateAsyncErrored(async (l) => l.data?.(data));
|
|
140
|
+
}
|
|
142
141
|
if (this.dataQueue.length <= this.lowWatermark) {
|
|
143
142
|
await this.iterateAsyncErrored(async (l) => l.lowWater?.());
|
|
144
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -52,8 +52,6 @@
|
|
|
52
52
|
"rsocket-core": "1.0.0-alpha.3",
|
|
53
53
|
"rsocket-websocket-client": "1.0.0-alpha.3",
|
|
54
54
|
"text-encoding": "^0.7.0",
|
|
55
|
-
"typescript": "^5.5.3",
|
|
56
|
-
"vitest": "^2.0.5",
|
|
57
55
|
"web-streams-polyfill": "3.2.1"
|
|
58
56
|
},
|
|
59
57
|
"scripts": {
|