@powersync/common 1.32.0 → 1.33.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 +5 -5
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +15 -1
- package/lib/client/sync/bucket/BucketStorageAdapter.js +9 -0
- package/lib/client/sync/bucket/CrudEntry.d.ts +2 -0
- package/lib/client/sync/bucket/CrudEntry.js +13 -2
- package/lib/client/sync/bucket/OplogEntry.d.ts +4 -4
- package/lib/client/sync/bucket/OplogEntry.js +5 -3
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +6 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.js +24 -2
- package/lib/client/sync/bucket/SyncDataBucket.d.ts +1 -1
- package/lib/client/sync/bucket/SyncDataBucket.js +2 -2
- package/lib/client/sync/stream/AbstractRemote.d.ts +16 -3
- package/lib/client/sync/stream/AbstractRemote.js +68 -78
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +69 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +431 -204
- package/lib/client/sync/stream/core-instruction.d.ts +53 -0
- package/lib/client/sync/stream/core-instruction.js +1 -0
- package/lib/db/crud/SyncProgress.d.ts +2 -6
- package/lib/db/crud/SyncProgress.js +2 -2
- package/package.json +1 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import Logger from 'js-logger';
|
|
2
2
|
import { SyncStatus } from '../../../db/crud/SyncStatus.js';
|
|
3
|
+
import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
|
|
3
4
|
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
4
5
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
5
6
|
import { onAbortPromise, throttleLeadingTrailing } from '../../../utils/async.js';
|
|
7
|
+
import { PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
|
|
6
8
|
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
|
|
7
9
|
import { FetchStrategy } from './AbstractRemote.js';
|
|
8
10
|
import { isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData } from './streaming-sync-types.js';
|
|
@@ -16,6 +18,49 @@ export var SyncStreamConnectionMethod;
|
|
|
16
18
|
SyncStreamConnectionMethod["HTTP"] = "http";
|
|
17
19
|
SyncStreamConnectionMethod["WEB_SOCKET"] = "web-socket";
|
|
18
20
|
})(SyncStreamConnectionMethod || (SyncStreamConnectionMethod = {}));
|
|
21
|
+
export var SyncClientImplementation;
|
|
22
|
+
(function (SyncClientImplementation) {
|
|
23
|
+
/**
|
|
24
|
+
* Decodes and handles sync lines received from the sync service in JavaScript.
|
|
25
|
+
*
|
|
26
|
+
* This is the default option.
|
|
27
|
+
*
|
|
28
|
+
* @deprecated Don't use {@link SyncClientImplementation.JAVASCRIPT} directly. Instead, use
|
|
29
|
+
* {@link DEFAULT_SYNC_CLIENT_IMPLEMENTATION} or omit the option. The explicit choice to use
|
|
30
|
+
* the JavaScript-based sync implementation will be removed from a future version of the SDK.
|
|
31
|
+
*/
|
|
32
|
+
SyncClientImplementation["JAVASCRIPT"] = "js";
|
|
33
|
+
/**
|
|
34
|
+
* This implementation offloads the sync line decoding and handling into the PowerSync
|
|
35
|
+
* core extension.
|
|
36
|
+
*
|
|
37
|
+
* @experimental
|
|
38
|
+
* While this implementation is more performant than {@link SyncClientImplementation.JAVASCRIPT},
|
|
39
|
+
* it has seen less real-world testing and is marked as __experimental__ at the moment.
|
|
40
|
+
*
|
|
41
|
+
* ## Compatibility warning
|
|
42
|
+
*
|
|
43
|
+
* The Rust sync client stores sync data in a format that is slightly different than the one used
|
|
44
|
+
* by the old {@link JAVASCRIPT} implementation. When adopting the {@link RUST} client on existing
|
|
45
|
+
* databases, the PowerSync SDK will migrate the format automatically.
|
|
46
|
+
* Further, the {@link JAVASCRIPT} client in recent versions of the PowerSync JS SDK (starting from
|
|
47
|
+
* the version introducing {@link RUST} as an option) also supports the new format, so you can switch
|
|
48
|
+
* back to {@link JAVASCRIPT} later.
|
|
49
|
+
*
|
|
50
|
+
* __However__: Upgrading the SDK version, then adopting {@link RUST} as a sync client and later
|
|
51
|
+
* downgrading the SDK to an older version (necessarily using the JavaScript-based implementation then)
|
|
52
|
+
* can lead to sync issues.
|
|
53
|
+
*/
|
|
54
|
+
SyncClientImplementation["RUST"] = "rust";
|
|
55
|
+
})(SyncClientImplementation || (SyncClientImplementation = {}));
|
|
56
|
+
/**
|
|
57
|
+
* The default {@link SyncClientImplementation} to use.
|
|
58
|
+
*
|
|
59
|
+
* Please use this field instead of {@link SyncClientImplementation.JAVASCRIPT} directly. A future version
|
|
60
|
+
* of the PowerSync SDK will enable {@link SyncClientImplementation.RUST} by default and remove the JavaScript
|
|
61
|
+
* option.
|
|
62
|
+
*/
|
|
63
|
+
export const DEFAULT_SYNC_CLIENT_IMPLEMENTATION = SyncClientImplementation.JAVASCRIPT;
|
|
19
64
|
export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
|
|
20
65
|
export const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
21
66
|
export const DEFAULT_STREAMING_SYNC_OPTIONS = {
|
|
@@ -25,6 +70,7 @@ export const DEFAULT_STREAMING_SYNC_OPTIONS = {
|
|
|
25
70
|
};
|
|
26
71
|
export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
27
72
|
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
|
|
73
|
+
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
|
|
28
74
|
fetchStrategy: FetchStrategy.Buffered,
|
|
29
75
|
params: {}
|
|
30
76
|
};
|
|
@@ -40,6 +86,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
40
86
|
crudUpdateListener;
|
|
41
87
|
streamingSyncPromise;
|
|
42
88
|
pendingCrudUpload;
|
|
89
|
+
notifyCompletedUploads;
|
|
43
90
|
syncStatus;
|
|
44
91
|
triggerCrudUpload;
|
|
45
92
|
constructor(options) {
|
|
@@ -61,6 +108,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
61
108
|
}
|
|
62
109
|
this.pendingCrudUpload = new Promise((resolve) => {
|
|
63
110
|
this._uploadAllCrud().finally(() => {
|
|
111
|
+
this.notifyCompletedUploads?.();
|
|
64
112
|
this.pendingCrudUpload = undefined;
|
|
65
113
|
resolve();
|
|
66
114
|
});
|
|
@@ -348,6 +396,31 @@ The next upload iteration will be delayed.`);
|
|
|
348
396
|
}
|
|
349
397
|
return [req, localDescriptions];
|
|
350
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Older versions of the JS SDK used to encode subkeys as JSON in {@link OplogEntry.toJSON}.
|
|
401
|
+
* Because subkeys are always strings, this leads to quotes being added around them in `ps_oplog`.
|
|
402
|
+
* While this is not a problem as long as it's done consistently, it causes issues when a database
|
|
403
|
+
* created by the JS SDK is used with other SDKs, or (more likely) when the new Rust sync client
|
|
404
|
+
* is enabled.
|
|
405
|
+
*
|
|
406
|
+
* So, we add a migration from the old key format (with quotes) to the new one (no quotes). The
|
|
407
|
+
* migration is only triggered when necessary (for now). The function returns whether the new format
|
|
408
|
+
* should be used, so that the JS SDK is able to write to updated databases.
|
|
409
|
+
*
|
|
410
|
+
* @param requireFixedKeyFormat Whether we require the new format or also support the old one.
|
|
411
|
+
* The Rust client requires the new subkey format.
|
|
412
|
+
* @returns Whether the database is now using the new, fixed subkey format.
|
|
413
|
+
*/
|
|
414
|
+
async requireKeyFormat(requireFixedKeyFormat) {
|
|
415
|
+
const hasMigrated = await this.options.adapter.hasMigratedSubkeys();
|
|
416
|
+
if (requireFixedKeyFormat && !hasMigrated) {
|
|
417
|
+
await this.options.adapter.migrateToFixedSubkeys();
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
return hasMigrated;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
351
424
|
async streamingSyncIteration(signal, options) {
|
|
352
425
|
await this.obtainLock({
|
|
353
426
|
type: LockType.SYNC,
|
|
@@ -357,219 +430,373 @@ The next upload iteration will be delayed.`);
|
|
|
357
430
|
...DEFAULT_STREAM_CONNECTION_OPTIONS,
|
|
358
431
|
...(options ?? {})
|
|
359
432
|
};
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
433
|
+
if (resolvedOptions.clientImplementation == SyncClientImplementation.JAVASCRIPT) {
|
|
434
|
+
await this.legacyStreamingSyncIteration(signal, resolvedOptions);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
await this.requireKeyFormat(true);
|
|
438
|
+
await this.rustSyncIteration(signal, resolvedOptions);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async legacyStreamingSyncIteration(signal, resolvedOptions) {
|
|
444
|
+
this.logger.debug('Streaming sync iteration started');
|
|
445
|
+
this.options.adapter.startSession();
|
|
446
|
+
let [req, bucketMap] = await this.collectLocalBucketState();
|
|
447
|
+
// These are compared by reference
|
|
448
|
+
let targetCheckpoint = null;
|
|
449
|
+
let validatedCheckpoint = null;
|
|
450
|
+
let appliedCheckpoint = null;
|
|
451
|
+
const clientId = await this.options.adapter.getClientId();
|
|
452
|
+
const usingFixedKeyFormat = await this.requireKeyFormat(false);
|
|
453
|
+
this.logger.debug('Requesting stream from server');
|
|
454
|
+
const syncOptions = {
|
|
455
|
+
path: '/sync/stream',
|
|
456
|
+
abortSignal: signal,
|
|
457
|
+
data: {
|
|
458
|
+
buckets: req,
|
|
459
|
+
include_checksum: true,
|
|
460
|
+
raw_data: true,
|
|
461
|
+
parameters: resolvedOptions.params,
|
|
462
|
+
client_id: clientId
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
let stream;
|
|
466
|
+
if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
467
|
+
stream = await this.options.remote.postStream(syncOptions);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
stream = await this.options.remote.socketStream({
|
|
471
|
+
...syncOptions,
|
|
472
|
+
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
this.logger.debug('Stream established. Processing events');
|
|
476
|
+
while (!stream.closed) {
|
|
477
|
+
const line = await stream.read();
|
|
478
|
+
if (!line) {
|
|
479
|
+
// The stream has closed while waiting
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// A connection is active and messages are being received
|
|
483
|
+
if (!this.syncStatus.connected) {
|
|
484
|
+
// There is a connection now
|
|
485
|
+
Promise.resolve().then(() => this.triggerCrudUpload());
|
|
486
|
+
this.updateSyncStatus({
|
|
487
|
+
connected: true
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (isStreamingSyncCheckpoint(line)) {
|
|
491
|
+
targetCheckpoint = line.checkpoint;
|
|
492
|
+
const bucketsToDelete = new Set(bucketMap.keys());
|
|
493
|
+
const newBuckets = new Map();
|
|
494
|
+
for (const checksum of line.checkpoint.buckets) {
|
|
495
|
+
newBuckets.set(checksum.bucket, {
|
|
496
|
+
name: checksum.bucket,
|
|
497
|
+
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
498
|
+
});
|
|
499
|
+
bucketsToDelete.delete(checksum.bucket);
|
|
500
|
+
}
|
|
501
|
+
if (bucketsToDelete.size > 0) {
|
|
502
|
+
this.logger.debug('Removing buckets', [...bucketsToDelete]);
|
|
503
|
+
}
|
|
504
|
+
bucketMap = newBuckets;
|
|
505
|
+
await this.options.adapter.removeBuckets([...bucketsToDelete]);
|
|
506
|
+
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
507
|
+
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
508
|
+
}
|
|
509
|
+
else if (isStreamingSyncCheckpointComplete(line)) {
|
|
510
|
+
const result = await this.applyCheckpoint(targetCheckpoint, signal);
|
|
511
|
+
if (result.endIteration) {
|
|
369
512
|
return;
|
|
370
513
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
514
|
+
else if (result.applied) {
|
|
515
|
+
appliedCheckpoint = targetCheckpoint;
|
|
516
|
+
}
|
|
517
|
+
validatedCheckpoint = targetCheckpoint;
|
|
518
|
+
}
|
|
519
|
+
else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
|
|
520
|
+
const priority = line.partial_checkpoint_complete.priority;
|
|
521
|
+
this.logger.debug('Partial checkpoint complete', priority);
|
|
522
|
+
const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
|
|
523
|
+
if (!result.checkpointValid) {
|
|
524
|
+
// This means checksums failed. Start again with a new checkpoint.
|
|
525
|
+
// TODO: better back-off
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
else if (!result.ready) {
|
|
530
|
+
// If we have pending uploads, we can't complete new checkpoints outside of priority 0.
|
|
531
|
+
// We'll resolve this for a complete checkpoint.
|
|
386
532
|
}
|
|
387
533
|
else {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
534
|
+
// We'll keep on downloading, but can report that this priority is synced now.
|
|
535
|
+
this.logger.debug('partial checkpoint validation succeeded');
|
|
536
|
+
// All states with a higher priority can be deleted since this partial sync includes them.
|
|
537
|
+
const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
|
|
538
|
+
priorityStates.push({
|
|
539
|
+
priority,
|
|
540
|
+
lastSyncedAt: new Date(),
|
|
541
|
+
hasSynced: true
|
|
542
|
+
});
|
|
543
|
+
this.updateSyncStatus({
|
|
544
|
+
connected: true,
|
|
545
|
+
priorityStatusEntries: priorityStates
|
|
391
546
|
});
|
|
392
547
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
|
|
441
|
-
if (!result.checkpointValid) {
|
|
442
|
-
// This means checksums failed. Start again with a new checkpoint.
|
|
443
|
-
// TODO: better back-off
|
|
444
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
else if (!result.ready) {
|
|
448
|
-
// If we have pending uploads, we can't complete new checkpoints outside of priority 0.
|
|
449
|
-
// We'll resolve this for a complete checkpoint.
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
// We'll keep on downloading, but can report that this priority is synced now.
|
|
453
|
-
this.logger.debug('partial checkpoint validation succeeded');
|
|
454
|
-
// All states with a higher priority can be deleted since this partial sync includes them.
|
|
455
|
-
const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
|
|
456
|
-
priorityStates.push({
|
|
457
|
-
priority,
|
|
458
|
-
lastSyncedAt: new Date(),
|
|
459
|
-
hasSynced: true
|
|
460
|
-
});
|
|
461
|
-
this.updateSyncStatus({
|
|
462
|
-
connected: true,
|
|
463
|
-
priorityStatusEntries: priorityStates
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
else if (isStreamingSyncCheckpointDiff(line)) {
|
|
468
|
-
// TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
|
|
469
|
-
if (targetCheckpoint == null) {
|
|
470
|
-
throw new Error('Checkpoint diff without previous checkpoint');
|
|
471
|
-
}
|
|
472
|
-
const diff = line.checkpoint_diff;
|
|
473
|
-
const newBuckets = new Map();
|
|
474
|
-
for (const checksum of targetCheckpoint.buckets) {
|
|
475
|
-
newBuckets.set(checksum.bucket, checksum);
|
|
476
|
-
}
|
|
477
|
-
for (const checksum of diff.updated_buckets) {
|
|
478
|
-
newBuckets.set(checksum.bucket, checksum);
|
|
479
|
-
}
|
|
480
|
-
for (const bucket of diff.removed_buckets) {
|
|
481
|
-
newBuckets.delete(bucket);
|
|
482
|
-
}
|
|
483
|
-
const newCheckpoint = {
|
|
484
|
-
last_op_id: diff.last_op_id,
|
|
485
|
-
buckets: [...newBuckets.values()],
|
|
486
|
-
write_checkpoint: diff.write_checkpoint
|
|
548
|
+
}
|
|
549
|
+
else if (isStreamingSyncCheckpointDiff(line)) {
|
|
550
|
+
// TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
|
|
551
|
+
if (targetCheckpoint == null) {
|
|
552
|
+
throw new Error('Checkpoint diff without previous checkpoint');
|
|
553
|
+
}
|
|
554
|
+
const diff = line.checkpoint_diff;
|
|
555
|
+
const newBuckets = new Map();
|
|
556
|
+
for (const checksum of targetCheckpoint.buckets) {
|
|
557
|
+
newBuckets.set(checksum.bucket, checksum);
|
|
558
|
+
}
|
|
559
|
+
for (const checksum of diff.updated_buckets) {
|
|
560
|
+
newBuckets.set(checksum.bucket, checksum);
|
|
561
|
+
}
|
|
562
|
+
for (const bucket of diff.removed_buckets) {
|
|
563
|
+
newBuckets.delete(bucket);
|
|
564
|
+
}
|
|
565
|
+
const newCheckpoint = {
|
|
566
|
+
last_op_id: diff.last_op_id,
|
|
567
|
+
buckets: [...newBuckets.values()],
|
|
568
|
+
write_checkpoint: diff.write_checkpoint
|
|
569
|
+
};
|
|
570
|
+
targetCheckpoint = newCheckpoint;
|
|
571
|
+
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
572
|
+
bucketMap = new Map();
|
|
573
|
+
newBuckets.forEach((checksum, name) => bucketMap.set(name, {
|
|
574
|
+
name: checksum.bucket,
|
|
575
|
+
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
576
|
+
}));
|
|
577
|
+
const bucketsToDelete = diff.removed_buckets;
|
|
578
|
+
if (bucketsToDelete.length > 0) {
|
|
579
|
+
this.logger.debug('Remove buckets', bucketsToDelete);
|
|
580
|
+
}
|
|
581
|
+
await this.options.adapter.removeBuckets(bucketsToDelete);
|
|
582
|
+
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
583
|
+
}
|
|
584
|
+
else if (isStreamingSyncData(line)) {
|
|
585
|
+
const { data } = line;
|
|
586
|
+
const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
|
|
587
|
+
let updatedProgress = null;
|
|
588
|
+
if (previousProgress) {
|
|
589
|
+
updatedProgress = { ...previousProgress };
|
|
590
|
+
const progressForBucket = updatedProgress[data.bucket];
|
|
591
|
+
if (progressForBucket) {
|
|
592
|
+
updatedProgress[data.bucket] = {
|
|
593
|
+
...progressForBucket,
|
|
594
|
+
since_last: progressForBucket.since_last + data.data.length
|
|
487
595
|
};
|
|
488
|
-
targetCheckpoint = newCheckpoint;
|
|
489
|
-
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
490
|
-
bucketMap = new Map();
|
|
491
|
-
newBuckets.forEach((checksum, name) => bucketMap.set(name, {
|
|
492
|
-
name: checksum.bucket,
|
|
493
|
-
priority: checksum.priority ?? FALLBACK_PRIORITY
|
|
494
|
-
}));
|
|
495
|
-
const bucketsToDelete = diff.removed_buckets;
|
|
496
|
-
if (bucketsToDelete.length > 0) {
|
|
497
|
-
this.logger.debug('Remove buckets', bucketsToDelete);
|
|
498
|
-
}
|
|
499
|
-
await this.options.adapter.removeBuckets(bucketsToDelete);
|
|
500
|
-
await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
|
|
501
596
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
updatedProgress = { ...previousProgress };
|
|
508
|
-
const progressForBucket = updatedProgress[data.bucket];
|
|
509
|
-
if (progressForBucket) {
|
|
510
|
-
updatedProgress[data.bucket] = {
|
|
511
|
-
...progressForBucket,
|
|
512
|
-
sinceLast: Math.min(progressForBucket.sinceLast + data.data.length, progressForBucket.targetCount - progressForBucket.atLast)
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
this.updateSyncStatus({
|
|
517
|
-
dataFlow: {
|
|
518
|
-
downloading: true,
|
|
519
|
-
downloadProgress: updatedProgress
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] });
|
|
597
|
+
}
|
|
598
|
+
this.updateSyncStatus({
|
|
599
|
+
dataFlow: {
|
|
600
|
+
downloading: true,
|
|
601
|
+
downloadProgress: updatedProgress
|
|
523
602
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
603
|
+
});
|
|
604
|
+
await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] }, usingFixedKeyFormat);
|
|
605
|
+
}
|
|
606
|
+
else if (isStreamingKeepalive(line)) {
|
|
607
|
+
const remaining_seconds = line.token_expires_in;
|
|
608
|
+
if (remaining_seconds == 0) {
|
|
609
|
+
// Connection would be closed automatically right after this
|
|
610
|
+
this.logger.debug('Token expiring; reconnect');
|
|
611
|
+
/**
|
|
612
|
+
* For a rare case where the backend connector does not update the token
|
|
613
|
+
* (uses the same one), this should have some delay.
|
|
614
|
+
*/
|
|
615
|
+
await this.delayRetry();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
else if (remaining_seconds < 30) {
|
|
619
|
+
this.logger.debug('Token will expire soon; reconnect');
|
|
620
|
+
// Pre-emptively refresh the token
|
|
621
|
+
this.options.remote.invalidateCredentials();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
this.triggerCrudUpload();
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
this.logger.debug('Sync complete');
|
|
628
|
+
if (targetCheckpoint === appliedCheckpoint) {
|
|
629
|
+
this.updateSyncStatus({
|
|
630
|
+
connected: true,
|
|
631
|
+
lastSyncedAt: new Date(),
|
|
632
|
+
priorityStatusEntries: [],
|
|
633
|
+
dataFlow: {
|
|
634
|
+
downloadError: undefined
|
|
542
635
|
}
|
|
543
|
-
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
else if (validatedCheckpoint === targetCheckpoint) {
|
|
639
|
+
const result = await this.applyCheckpoint(targetCheckpoint, signal);
|
|
640
|
+
if (result.endIteration) {
|
|
641
|
+
return;
|
|
544
642
|
}
|
|
545
|
-
else {
|
|
546
|
-
|
|
547
|
-
if (targetCheckpoint === appliedCheckpoint) {
|
|
548
|
-
this.updateSyncStatus({
|
|
549
|
-
connected: true,
|
|
550
|
-
lastSyncedAt: new Date(),
|
|
551
|
-
priorityStatusEntries: [],
|
|
552
|
-
dataFlow: {
|
|
553
|
-
downloadError: undefined
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
else if (validatedCheckpoint === targetCheckpoint) {
|
|
558
|
-
const result = await this.applyCheckpoint(targetCheckpoint, signal);
|
|
559
|
-
if (result.endIteration) {
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
else if (result.applied) {
|
|
563
|
-
appliedCheckpoint = targetCheckpoint;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
643
|
+
else if (result.applied) {
|
|
644
|
+
appliedCheckpoint = targetCheckpoint;
|
|
566
645
|
}
|
|
567
646
|
}
|
|
568
|
-
this.logger.debug('Stream input empty');
|
|
569
|
-
// Connection closed. Likely due to auth issue.
|
|
570
|
-
return;
|
|
571
647
|
}
|
|
572
|
-
}
|
|
648
|
+
}
|
|
649
|
+
this.logger.debug('Stream input empty');
|
|
650
|
+
// Connection closed. Likely due to auth issue.
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
async rustSyncIteration(signal, resolvedOptions) {
|
|
654
|
+
const syncImplementation = this;
|
|
655
|
+
const adapter = this.options.adapter;
|
|
656
|
+
const remote = this.options.remote;
|
|
657
|
+
let receivingLines = null;
|
|
658
|
+
const abortController = new AbortController();
|
|
659
|
+
signal.addEventListener('abort', () => abortController.abort());
|
|
660
|
+
// Pending sync lines received from the service, as well as local events that trigger a powersync_control
|
|
661
|
+
// invocation (local events include refreshed tokens and completed uploads).
|
|
662
|
+
// This is a single data stream so that we can handle all control calls from a single place.
|
|
663
|
+
let controlInvocations = null;
|
|
664
|
+
async function connect(instr) {
|
|
665
|
+
const syncOptions = {
|
|
666
|
+
path: '/sync/stream',
|
|
667
|
+
abortSignal: abortController.signal,
|
|
668
|
+
data: instr.request
|
|
669
|
+
};
|
|
670
|
+
if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
671
|
+
controlInvocations = await remote.postStreamRaw(syncOptions, (line) => ({
|
|
672
|
+
command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
|
|
673
|
+
payload: line
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
controlInvocations = await remote.socketStreamRaw({
|
|
678
|
+
...syncOptions,
|
|
679
|
+
fetchStrategy: resolvedOptions.fetchStrategy
|
|
680
|
+
}, (buffer) => ({
|
|
681
|
+
command: PowerSyncControlCommand.PROCESS_BSON_LINE,
|
|
682
|
+
payload: buffer
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
while (!controlInvocations.closed) {
|
|
687
|
+
const line = await controlInvocations.read();
|
|
688
|
+
if (line == null) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
await control(line.command, line.payload);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
const activeInstructions = controlInvocations;
|
|
696
|
+
// We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
|
|
697
|
+
// refreshed. That would throw after closing (and we can't handle those events either way), so set this back
|
|
698
|
+
// to null.
|
|
699
|
+
controlInvocations = null;
|
|
700
|
+
await activeInstructions.close();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async function stop() {
|
|
704
|
+
await control(PowerSyncControlCommand.STOP);
|
|
705
|
+
}
|
|
706
|
+
async function control(op, payload) {
|
|
707
|
+
const rawResponse = await adapter.control(op, payload ?? null);
|
|
708
|
+
await handleInstructions(JSON.parse(rawResponse));
|
|
709
|
+
}
|
|
710
|
+
async function handleInstruction(instruction) {
|
|
711
|
+
if ('LogLine' in instruction) {
|
|
712
|
+
switch (instruction.LogLine.severity) {
|
|
713
|
+
case 'DEBUG':
|
|
714
|
+
syncImplementation.logger.debug(instruction.LogLine.line);
|
|
715
|
+
break;
|
|
716
|
+
case 'INFO':
|
|
717
|
+
syncImplementation.logger.info(instruction.LogLine.line);
|
|
718
|
+
break;
|
|
719
|
+
case 'WARNING':
|
|
720
|
+
syncImplementation.logger.warn(instruction.LogLine.line);
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else if ('UpdateSyncStatus' in instruction) {
|
|
725
|
+
function coreStatusToJs(status) {
|
|
726
|
+
return {
|
|
727
|
+
priority: status.priority,
|
|
728
|
+
hasSynced: status.has_synced ?? undefined,
|
|
729
|
+
lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const info = instruction.UpdateSyncStatus.status;
|
|
733
|
+
const coreCompleteSync = info.priority_status.find((s) => s.priority == FULL_SYNC_PRIORITY);
|
|
734
|
+
const completeSync = coreCompleteSync != null ? coreStatusToJs(coreCompleteSync) : null;
|
|
735
|
+
syncImplementation.updateSyncStatus({
|
|
736
|
+
connected: info.connected,
|
|
737
|
+
connecting: info.connecting,
|
|
738
|
+
dataFlow: {
|
|
739
|
+
downloading: info.downloading != null,
|
|
740
|
+
downloadProgress: info.downloading?.buckets
|
|
741
|
+
},
|
|
742
|
+
lastSyncedAt: completeSync?.lastSyncedAt,
|
|
743
|
+
hasSynced: completeSync?.hasSynced,
|
|
744
|
+
priorityStatusEntries: info.priority_status.map(coreStatusToJs)
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
else if ('EstablishSyncStream' in instruction) {
|
|
748
|
+
if (receivingLines != null) {
|
|
749
|
+
// Already connected, this shouldn't happen during a single iteration.
|
|
750
|
+
throw 'Unexpected request to establish sync stream, already connected';
|
|
751
|
+
}
|
|
752
|
+
receivingLines = connect(instruction.EstablishSyncStream);
|
|
753
|
+
}
|
|
754
|
+
else if ('FetchCredentials' in instruction) {
|
|
755
|
+
if (instruction.FetchCredentials.did_expire) {
|
|
756
|
+
remote.invalidateCredentials();
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
remote.invalidateCredentials();
|
|
760
|
+
// Restart iteration after the credentials have been refreshed.
|
|
761
|
+
remote.fetchCredentials().then((_) => {
|
|
762
|
+
controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
|
|
763
|
+
}, (err) => {
|
|
764
|
+
syncImplementation.logger.warn('Could not prefetch credentials', err);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
else if ('CloseSyncStream' in instruction) {
|
|
769
|
+
abortController.abort();
|
|
770
|
+
}
|
|
771
|
+
else if ('FlushFileSystem' in instruction) {
|
|
772
|
+
// Not necessary on JS platforms.
|
|
773
|
+
}
|
|
774
|
+
else if ('DidCompleteSync' in instruction) {
|
|
775
|
+
syncImplementation.updateSyncStatus({
|
|
776
|
+
dataFlow: {
|
|
777
|
+
downloadError: undefined
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function handleInstructions(instructions) {
|
|
783
|
+
for (const instr of instructions) {
|
|
784
|
+
await handleInstruction(instr);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
await control(PowerSyncControlCommand.START, JSON.stringify({
|
|
789
|
+
parameters: resolvedOptions.params
|
|
790
|
+
}));
|
|
791
|
+
this.notifyCompletedUploads = () => {
|
|
792
|
+
controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
793
|
+
};
|
|
794
|
+
await receivingLines;
|
|
795
|
+
}
|
|
796
|
+
finally {
|
|
797
|
+
this.notifyCompletedUploads = undefined;
|
|
798
|
+
await stop();
|
|
799
|
+
}
|
|
573
800
|
}
|
|
574
801
|
async updateSyncStatusForStartingCheckpoint(checkpoint) {
|
|
575
802
|
const localProgress = await this.options.adapter.getBucketOperationProgress();
|
|
@@ -583,9 +810,9 @@ The next upload iteration will be delayed.`);
|
|
|
583
810
|
// The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
|
|
584
811
|
// will use by default.
|
|
585
812
|
priority: bucket.priority ?? 3,
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
813
|
+
at_last: atLast,
|
|
814
|
+
since_last: sinceLast,
|
|
815
|
+
target_count: bucket.count ?? 0
|
|
589
816
|
};
|
|
590
817
|
if (bucket.count != null && bucket.count < atLast + sinceLast) {
|
|
591
818
|
// Either due to a defrag / sync rule deploy or a compaction operation, the size
|
|
@@ -597,8 +824,8 @@ The next upload iteration will be delayed.`);
|
|
|
597
824
|
if (invalidated) {
|
|
598
825
|
for (const bucket in progress) {
|
|
599
826
|
const bucketProgress = progress[bucket];
|
|
600
|
-
bucketProgress.
|
|
601
|
-
bucketProgress.
|
|
827
|
+
bucketProgress.at_last = 0;
|
|
828
|
+
bucketProgress.since_last = 0;
|
|
602
829
|
}
|
|
603
830
|
}
|
|
604
831
|
this.updateSyncStatus({
|