@powersync/common 1.52.0 → 1.53.1

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.
Files changed (79) hide show
  1. package/dist/bundle.cjs +167 -767
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +168 -756
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +167 -767
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +168 -756
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +39 -370
  10. package/legacy/sync_protocol.d.ts +103 -0
  11. package/lib/client/ConnectionManager.js +1 -1
  12. package/lib/client/ConnectionManager.js.map +1 -1
  13. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -64
  14. package/lib/client/sync/bucket/BucketStorageAdapter.js +4 -0
  15. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -1
  16. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -28
  17. package/lib/client/sync/bucket/SqliteBucketStorage.js +0 -162
  18. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -1
  19. package/lib/client/sync/stream/AbstractRemote.d.ts +2 -12
  20. package/lib/client/sync/stream/AbstractRemote.js +3 -13
  21. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  22. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +12 -35
  23. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +145 -424
  24. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  25. package/lib/client/sync/stream/JsonValue.d.ts +7 -0
  26. package/lib/client/sync/stream/JsonValue.js +2 -0
  27. package/lib/client/sync/stream/JsonValue.js.map +1 -0
  28. package/lib/client/sync/stream/core-instruction.d.ts +14 -9
  29. package/lib/client/sync/stream/core-instruction.js +3 -0
  30. package/lib/client/sync/stream/core-instruction.js.map +1 -1
  31. package/lib/db/DBAdapter.d.ts +9 -0
  32. package/lib/db/DBAdapter.js +8 -1
  33. package/lib/db/DBAdapter.js.map +1 -1
  34. package/lib/db/crud/SyncStatus.d.ts +3 -4
  35. package/lib/db/crud/SyncStatus.js +0 -4
  36. package/lib/db/crud/SyncStatus.js.map +1 -1
  37. package/lib/db/schema/RawTable.d.ts +0 -5
  38. package/lib/db/schema/Schema.d.ts +0 -2
  39. package/lib/db/schema/Schema.js +0 -2
  40. package/lib/db/schema/Schema.js.map +1 -1
  41. package/lib/index.d.ts +1 -5
  42. package/lib/index.js +1 -5
  43. package/lib/index.js.map +1 -1
  44. package/lib/utils/stream_transform.js +6 -1
  45. package/lib/utils/stream_transform.js.map +1 -1
  46. package/package.json +9 -6
  47. package/src/client/ConnectionManager.ts +1 -1
  48. package/src/client/sync/bucket/BucketStorageAdapter.ts +6 -71
  49. package/src/client/sync/bucket/SqliteBucketStorage.ts +1 -197
  50. package/src/client/sync/stream/AbstractRemote.ts +5 -27
  51. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +168 -496
  52. package/src/client/sync/stream/JsonValue.ts +8 -0
  53. package/src/client/sync/stream/core-instruction.ts +15 -5
  54. package/src/db/DBAdapter.ts +20 -2
  55. package/src/db/crud/SyncStatus.ts +4 -5
  56. package/src/db/schema/RawTable.ts +0 -5
  57. package/src/db/schema/Schema.ts +0 -2
  58. package/src/index.ts +1 -5
  59. package/src/utils/stream_transform.ts +7 -1
  60. package/lib/client/sync/bucket/OpType.d.ts +0 -16
  61. package/lib/client/sync/bucket/OpType.js +0 -23
  62. package/lib/client/sync/bucket/OpType.js.map +0 -1
  63. package/lib/client/sync/bucket/OplogEntry.d.ts +0 -23
  64. package/lib/client/sync/bucket/OplogEntry.js +0 -36
  65. package/lib/client/sync/bucket/OplogEntry.js.map +0 -1
  66. package/lib/client/sync/bucket/SyncDataBatch.d.ts +0 -6
  67. package/lib/client/sync/bucket/SyncDataBatch.js +0 -12
  68. package/lib/client/sync/bucket/SyncDataBatch.js.map +0 -1
  69. package/lib/client/sync/bucket/SyncDataBucket.d.ts +0 -40
  70. package/lib/client/sync/bucket/SyncDataBucket.js +0 -40
  71. package/lib/client/sync/bucket/SyncDataBucket.js.map +0 -1
  72. package/lib/client/sync/stream/streaming-sync-types.d.ts +0 -143
  73. package/lib/client/sync/stream/streaming-sync-types.js +0 -26
  74. package/lib/client/sync/stream/streaming-sync-types.js.map +0 -1
  75. package/src/client/sync/bucket/OpType.ts +0 -23
  76. package/src/client/sync/bucket/OplogEntry.ts +0 -50
  77. package/src/client/sync/bucket/SyncDataBatch.ts +0 -11
  78. package/src/client/sync/bucket/SyncDataBucket.ts +0 -49
  79. package/src/client/sync/stream/streaming-sync-types.ts +0 -210
package/dist/bundle.cjs CHANGED
@@ -1971,8 +1971,15 @@ class BaseTransaction {
1971
1971
  class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
1972
1972
  static async runWith(ctx, fn) {
1973
1973
  let tx = new TransactionImplementation(ctx);
1974
+ // For write transactions, use BEGIN IMMEDIATE to immediately obtain a write lock on the database (instead of doing
1975
+ // that on the first statement). If we have a genuine read-only connection, we also use BEGIN IMMEDIATE there: In
1976
+ // WAL mode, that ensures we pin the current state of the database (instead of the state at the first statement in
1977
+ // the transaction). But if we have a "fake" read-only connection implemented through `pragma query_only = true`, we
1978
+ // can't use this trick because it would attempt to lock the connection. So there, we use a regular `BEGIN`
1979
+ // statement.
1980
+ const useBeginImmediate = ctx.connectionType != 'queryOnly';
1974
1981
  try {
1975
- await ctx.execute('BEGIN IMMEDIATE');
1982
+ await ctx.execute(useBeginImmediate ? 'BEGIN IMMEDIATE' : 'BEGIN');
1976
1983
  const result = await fn(tx);
1977
1984
  await tx.commit();
1978
1985
  return result;
@@ -2131,16 +2138,12 @@ class SyncStatus {
2131
2138
  *
2132
2139
  * This returns null when the database is currently being opened and we don't have reliable information about all
2133
2140
  * included streams yet.
2134
- *
2135
- * @experimental Sync streams are currently in alpha.
2136
2141
  */
2137
2142
  get syncStreams() {
2138
2143
  return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core));
2139
2144
  }
2140
2145
  /**
2141
2146
  * If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
2142
- *
2143
- * @experimental Sync streams are currently in alpha.
2144
2147
  */
2145
2148
  forStream(stream) {
2146
2149
  const asJson = JSON.stringify(stream.parameters);
@@ -2723,7 +2726,7 @@ class SyncStreamSubscriptionHandle {
2723
2726
  constructor(subscription) {
2724
2727
  this.subscription = subscription;
2725
2728
  subscription.refcount++;
2726
- _finalizer?.register(this, subscription);
2729
+ _finalizer?.register(this, subscription, this);
2727
2730
  }
2728
2731
  get name() {
2729
2732
  return this.subscription.name;
@@ -3312,6 +3315,10 @@ exports.PowerSyncControlCommand = void 0;
3312
3315
  PowerSyncControlCommand["NOTIFY_TOKEN_REFRESHED"] = "refreshed_token";
3313
3316
  PowerSyncControlCommand["NOTIFY_CRUD_UPLOAD_COMPLETED"] = "completed_upload";
3314
3317
  PowerSyncControlCommand["UPDATE_SUBSCRIPTIONS"] = "update_subscriptions";
3318
+ /**
3319
+ * An `established` or `end` event for response streams.
3320
+ */
3321
+ PowerSyncControlCommand["CONNECTION_STATE"] = "connection";
3315
3322
  })(exports.PowerSyncControlCommand || (exports.PowerSyncControlCommand = {}));
3316
3323
 
3317
3324
  /**
@@ -3493,103 +3500,6 @@ class AbortOperation extends Error {
3493
3500
  }
3494
3501
  }
3495
3502
 
3496
- exports.OpTypeEnum = void 0;
3497
- (function (OpTypeEnum) {
3498
- OpTypeEnum[OpTypeEnum["CLEAR"] = 1] = "CLEAR";
3499
- OpTypeEnum[OpTypeEnum["MOVE"] = 2] = "MOVE";
3500
- OpTypeEnum[OpTypeEnum["PUT"] = 3] = "PUT";
3501
- OpTypeEnum[OpTypeEnum["REMOVE"] = 4] = "REMOVE";
3502
- })(exports.OpTypeEnum || (exports.OpTypeEnum = {}));
3503
- /**
3504
- * Used internally for sync buckets.
3505
- */
3506
- class OpType {
3507
- value;
3508
- static fromJSON(jsonValue) {
3509
- return new OpType(exports.OpTypeEnum[jsonValue]);
3510
- }
3511
- constructor(value) {
3512
- this.value = value;
3513
- }
3514
- toJSON() {
3515
- return Object.entries(exports.OpTypeEnum).find(([, value]) => value === this.value)[0];
3516
- }
3517
- }
3518
-
3519
- class OplogEntry {
3520
- op_id;
3521
- op;
3522
- checksum;
3523
- subkey;
3524
- object_type;
3525
- object_id;
3526
- data;
3527
- static fromRow(row) {
3528
- return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum, row.subkey, row.object_type, row.object_id, row.data);
3529
- }
3530
- constructor(op_id, op, checksum, subkey, object_type, object_id, data) {
3531
- this.op_id = op_id;
3532
- this.op = op;
3533
- this.checksum = checksum;
3534
- this.subkey = subkey;
3535
- this.object_type = object_type;
3536
- this.object_id = object_id;
3537
- this.data = data;
3538
- }
3539
- toJSON(fixedKeyEncoding = false) {
3540
- return {
3541
- op_id: this.op_id,
3542
- op: this.op.toJSON(),
3543
- object_type: this.object_type,
3544
- object_id: this.object_id,
3545
- checksum: this.checksum,
3546
- data: this.data,
3547
- // Older versions of the JS SDK used to always JSON.stringify here. That has always been wrong,
3548
- // but we need to migrate gradually to not break existing databases.
3549
- subkey: fixedKeyEncoding ? this.subkey : JSON.stringify(this.subkey)
3550
- };
3551
- }
3552
- }
3553
-
3554
- class SyncDataBucket {
3555
- bucket;
3556
- data;
3557
- has_more;
3558
- after;
3559
- next_after;
3560
- static fromRow(row) {
3561
- return new SyncDataBucket(row.bucket, row.data.map((entry) => OplogEntry.fromRow(entry)), row.has_more ?? false, row.after, row.next_after);
3562
- }
3563
- constructor(bucket, data,
3564
- /**
3565
- * True if the response does not contain all the data for this bucket, and another request must be made.
3566
- */
3567
- has_more,
3568
- /**
3569
- * The `after` specified in the request.
3570
- */
3571
- after,
3572
- /**
3573
- * Use this for the next request.
3574
- */
3575
- next_after) {
3576
- this.bucket = bucket;
3577
- this.data = data;
3578
- this.has_more = has_more;
3579
- this.after = after;
3580
- this.next_after = next_after;
3581
- }
3582
- toJSON(fixedKeyEncoding = false) {
3583
- return {
3584
- bucket: this.bucket,
3585
- has_more: this.has_more,
3586
- after: this.after,
3587
- next_after: this.next_after,
3588
- data: this.data.map((entry) => entry.toJSON(fixedKeyEncoding))
3589
- };
3590
- }
3591
- }
3592
-
3593
3503
  var buffer = {};
3594
3504
 
3595
3505
  var base64Js = {};
@@ -10745,7 +10655,7 @@ function requireDist () {
10745
10655
 
10746
10656
  var distExports = requireDist();
10747
10657
 
10748
- var version = "1.52.0";
10658
+ var version = "1.53.1";
10749
10659
  var PACKAGE = {
10750
10660
  version: version};
10751
10661
 
@@ -10915,22 +10825,6 @@ const doneResult = { done: true, value: undefined };
10915
10825
  function valueResult(value) {
10916
10826
  return { done: false, value };
10917
10827
  }
10918
- /**
10919
- * A variant of {@link Array.map} for async iterators.
10920
- */
10921
- function map(source, map) {
10922
- return {
10923
- next: async () => {
10924
- const value = await source.next();
10925
- if (value.done) {
10926
- return value;
10927
- }
10928
- else {
10929
- return { value: map(value.value) };
10930
- }
10931
- }
10932
- };
10933
- }
10934
10828
  /**
10935
10829
  * Expands a source async iterator by allowing to inject events asynchronously.
10936
10830
  *
@@ -10945,6 +10839,7 @@ function injectable(source) {
10945
10839
  let waiter = undefined; // An active, waiting next() call.
10946
10840
  // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
10947
10841
  let pendingSourceEvent = null;
10842
+ let sourceFetchInFlight = false;
10948
10843
  let pendingInjectedEvents = [];
10949
10844
  const consumeWaiter = () => {
10950
10845
  const pending = waiter;
@@ -10953,6 +10848,7 @@ function injectable(source) {
10953
10848
  };
10954
10849
  const fetchFromSource = () => {
10955
10850
  const resolveWaiter = (propagate) => {
10851
+ sourceFetchInFlight = false;
10956
10852
  const active = consumeWaiter();
10957
10853
  if (active) {
10958
10854
  propagate(active);
@@ -10961,6 +10857,7 @@ function injectable(source) {
10961
10857
  pendingSourceEvent = propagate;
10962
10858
  }
10963
10859
  };
10860
+ sourceFetchInFlight = true;
10964
10861
  const nextFromSource = source.next();
10965
10862
  nextFromSource.then((value) => {
10966
10863
  sourceIsDone = value.done == true;
@@ -10987,7 +10884,9 @@ function injectable(source) {
10987
10884
  }
10988
10885
  // Nothing pending? Fetch from source
10989
10886
  waiter = { resolve, reject };
10990
- return fetchFromSource();
10887
+ if (!sourceFetchInFlight) {
10888
+ fetchFromSource();
10889
+ }
10991
10890
  });
10992
10891
  },
10993
10892
  inject: (event) => {
@@ -11302,22 +11201,12 @@ class AbstractRemote {
11302
11201
  * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
11303
11202
  *
11304
11203
  * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
11305
- *
11306
- * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
11307
- * (required for compatibility with older sync services).
11308
11204
  */
11309
- async socketStreamRaw(options, bson) {
11205
+ async socketStreamRaw(options) {
11310
11206
  const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
11311
- const mimeType = bson == null ? 'application/json' : 'application/bson';
11207
+ const mimeType = 'application/json';
11312
11208
  function toBuffer(js) {
11313
- let contents;
11314
- if (bson != null) {
11315
- contents = bson.serialize(js);
11316
- }
11317
- else {
11318
- contents = JSON.stringify(js);
11319
- }
11320
- return bufferExports.Buffer.from(contents);
11209
+ return bufferExports.Buffer.from(JSON.stringify(js));
11321
11210
  }
11322
11211
  const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
11323
11212
  const request = await this.buildRequest(path);
@@ -11618,31 +11507,8 @@ function coreStatusToJs(status) {
11618
11507
  priorityStatusEntries: status.priority_status.map(priorityToJs)
11619
11508
  };
11620
11509
  }
11621
-
11622
- function isStreamingSyncData(line) {
11623
- return line.data != null;
11624
- }
11625
- function isStreamingKeepalive(line) {
11626
- return line.token_expires_in != null;
11627
- }
11628
- function isStreamingSyncCheckpoint(line) {
11629
- return line.checkpoint != null;
11630
- }
11631
- function isStreamingSyncCheckpointComplete(line) {
11632
- return line.checkpoint_complete != null;
11633
- }
11634
- function isStreamingSyncCheckpointPartiallyComplete(line) {
11635
- return line.partial_checkpoint_complete != null;
11636
- }
11637
- function isStreamingSyncCheckpointDiff(line) {
11638
- return line.checkpoint_diff != null;
11639
- }
11640
- function isContinueCheckpointRequest(request) {
11641
- return (Array.isArray(request.buckets) &&
11642
- typeof request.checkpoint_token == 'string');
11643
- }
11644
- function isSyncNewCheckpointRequest(request) {
11645
- return typeof request.request_checkpoint == 'object';
11510
+ function isInterruptingInstruction(instruction) {
11511
+ return 'EstablishSyncStream' in instruction || 'CloseSyncStream' in instruction;
11646
11512
  }
11647
11513
 
11648
11514
  exports.LockType = void 0;
@@ -11657,35 +11523,21 @@ exports.SyncStreamConnectionMethod = void 0;
11657
11523
  })(exports.SyncStreamConnectionMethod || (exports.SyncStreamConnectionMethod = {}));
11658
11524
  exports.SyncClientImplementation = void 0;
11659
11525
  (function (SyncClientImplementation) {
11660
- /**
11661
- * Decodes and handles sync lines received from the sync service in JavaScript.
11662
- *
11663
- * This is the default option.
11664
- *
11665
- * @deprecated We recommend the {@link RUST} client implementation for all apps. If you have issues with
11666
- * the Rust client, please file an issue or reach out to us. The JavaScript client will be removed in a future
11667
- * version of the PowerSync SDK.
11668
- */
11669
- SyncClientImplementation["JAVASCRIPT"] = "js";
11670
11526
  /**
11671
11527
  * This implementation offloads the sync line decoding and handling into the PowerSync
11672
11528
  * core extension.
11673
11529
  *
11674
- * This option is more performant than the {@link JAVASCRIPT} client, enabled by default and the
11675
- * recommended client implementation for all apps.
11530
+ * This is the only option, as an older JavaScript client implementation has been removed from the SDK.
11676
11531
  *
11677
11532
  * ## Compatibility warning
11678
11533
  *
11679
11534
  * The Rust sync client stores sync data in a format that is slightly different than the one used
11680
- * by the old {@link JAVASCRIPT} implementation. When adopting the {@link RUST} client on existing
11681
- * databases, the PowerSync SDK will migrate the format automatically.
11682
- * Further, the {@link JAVASCRIPT} client in recent versions of the PowerSync JS SDK (starting from
11683
- * the version introducing {@link RUST} as an option) also supports the new format, so you can switch
11684
- * back to {@link JAVASCRIPT} later.
11535
+ * by the old JavaScript client. When adopting the {@link RUST} client on existing databases, the PowerSync SDK will
11536
+ * migrate the format automatically.
11685
11537
  *
11686
- * __However__: Upgrading the SDK version, then adopting {@link RUST} as a sync client and later
11687
- * downgrading the SDK to an older version (necessarily using the JavaScript-based implementation then)
11688
- * can lead to sync issues.
11538
+ * SDK versions supporting both the JavaScript and the Rust client support both formats with the JavaScript client
11539
+ * implementaiton. However, downgrading to an SDK version that only supports the JavaScript client would not be
11540
+ * possible anymore. Problematic SDK versions have been released before 2025-06-09.
11689
11541
  */
11690
11542
  SyncClientImplementation["RUST"] = "rust";
11691
11543
  })(exports.SyncClientImplementation || (exports.SyncClientImplementation = {}));
@@ -11708,13 +11560,7 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
11708
11560
  serializedSchema: undefined,
11709
11561
  includeDefaultStreams: true
11710
11562
  };
11711
- // The priority we assume when we receive checkpoint lines where no priority is set.
11712
- // This is the default priority used by the sync service, but can be set to an arbitrary
11713
- // value since sync services without priorities also won't send partial sync completion
11714
- // messages.
11715
- const FALLBACK_PRIORITY = 3;
11716
11563
  class AbstractStreamingSyncImplementation extends BaseObserver {
11717
- _lastSyncedAt;
11718
11564
  options;
11719
11565
  abortController;
11720
11566
  // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
@@ -11724,6 +11570,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11724
11570
  streamingSyncPromise;
11725
11571
  logger;
11726
11572
  activeStreams;
11573
+ connectionMayHaveChanged = false;
11727
11574
  isUploadingCrud = false;
11728
11575
  notifyCompletedUploads;
11729
11576
  handleActiveStreamsChange;
@@ -11803,9 +11650,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11803
11650
  this.crudUpdateListener = undefined;
11804
11651
  this.uploadAbortController?.abort();
11805
11652
  }
11806
- async hasCompletedSync() {
11807
- return this.options.adapter.hasCompletedSync();
11808
- }
11809
11653
  async getWriteCheckpoint() {
11810
11654
  const clientId = await this.options.adapter.getClientId();
11811
11655
  let path = `/write-checkpoint2.json?client_id=${clientId}`;
@@ -11887,7 +11731,7 @@ The next upload iteration will be delayed.`);
11887
11731
  });
11888
11732
  }
11889
11733
  }
11890
- this.uploadAbortController = null;
11734
+ this.uploadAbortController = undefined;
11891
11735
  }
11892
11736
  });
11893
11737
  }
@@ -12003,6 +11847,11 @@ The next upload iteration will be delayed.`);
12003
11847
  shouldDelayRetry = false;
12004
11848
  // A disconnect was requested, we should not delay since there is no explicit retry
12005
11849
  }
11850
+ else if (this.connectionMayHaveChanged && ex.message?.indexOf('No iteration is active') >= 0) {
11851
+ this.connectionMayHaveChanged = false;
11852
+ this.logger.info('Sync error after changed connection, retrying immediately');
11853
+ shouldDelayRetry = false;
11854
+ }
12006
11855
  else {
12007
11856
  this.logger.error(ex);
12008
11857
  }
@@ -12033,17 +11882,14 @@ The next upload iteration will be delayed.`);
12033
11882
  // Mark as disconnected if here
12034
11883
  this.updateSyncStatus({ connected: false, connecting: false });
12035
11884
  }
12036
- async collectLocalBucketState() {
12037
- const bucketEntries = await this.options.adapter.getBucketStates();
12038
- const req = bucketEntries.map((entry) => ({
12039
- name: entry.bucket,
12040
- after: entry.op_id
12041
- }));
12042
- const localDescriptions = new Map();
12043
- for (const entry of bucketEntries) {
12044
- localDescriptions.set(entry.bucket, null);
12045
- }
12046
- return [req, localDescriptions];
11885
+ markConnectionMayHaveChanged() {
11886
+ // By setting this field, we'll immediately retry if the next sync event causes an error triggered by us not having
11887
+ // an active sync iteration on the connection in use.
11888
+ this.connectionMayHaveChanged = true;
11889
+ // This triggers a `powersync_control` invocation if a sync iteration is currently active. This is a cheap call to
11890
+ // make when no subscriptions have actually changed, we're mainly interested in this immediately throwing if no
11891
+ // iteration is active. That allows us to reconnect ASAP, instead of having to wait for the next sync line.
11892
+ this.handleActiveStreamsChange?.();
12047
11893
  }
12048
11894
  /**
12049
11895
  * Older versions of the JS SDK used to encode subkeys as JSON in {@link OplogEntry.toJSON}.
@@ -12084,328 +11930,98 @@ The next upload iteration will be delayed.`);
12084
11930
  if (invalidMetadata.length > 0) {
12085
11931
  throw new Error(`Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}`);
12086
11932
  }
12087
- const clientImplementation = resolvedOptions.clientImplementation;
12088
- this.updateSyncStatus({ clientImplementation });
12089
- if (clientImplementation == exports.SyncClientImplementation.JAVASCRIPT) {
12090
- await this.legacyStreamingSyncIteration(signal, resolvedOptions);
12091
- return null;
12092
- }
12093
- else {
12094
- await this.requireKeyFormat(true);
12095
- return await this.rustSyncIteration(signal, resolvedOptions);
12096
- }
11933
+ await this.requireKeyFormat(true);
11934
+ return await this.rustSyncIteration(signal, resolvedOptions);
12097
11935
  }
12098
11936
  });
12099
11937
  }
12100
- async receiveSyncLines(data) {
12101
- const { options, connection, bson } = data;
11938
+ receiveSyncLines(data) {
11939
+ const { options, connection } = data;
12102
11940
  const remote = this.options.remote;
12103
- if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
12104
- return await remote.fetchStream(options);
12105
- }
12106
- else {
12107
- return await this.options.remote.socketStreamRaw({
12108
- ...options,
12109
- ...{ fetchStrategy: connection.fetchStrategy }
12110
- }, bson);
12111
- }
12112
- }
12113
- async legacyStreamingSyncIteration(signal, resolvedOptions) {
12114
- const rawTables = resolvedOptions.serializedSchema?.raw_tables;
12115
- if (rawTables != null && rawTables.length) {
12116
- this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
12117
- }
12118
- if (this.activeStreams.length) {
12119
- this.logger.error('Sync streams require `clientImplementation: SyncClientImplementation.RUST` when connecting.');
12120
- }
12121
- this.logger.debug('Streaming sync iteration started');
12122
- this.options.adapter.startSession();
12123
- let [req, bucketMap] = await this.collectLocalBucketState();
12124
- let targetCheckpoint = null;
12125
- // A checkpoint that has been validated but not applied (e.g. due to pending local writes)
12126
- let pendingValidatedCheckpoint = null;
12127
- const clientId = await this.options.adapter.getClientId();
12128
- const usingFixedKeyFormat = await this.requireKeyFormat(false);
12129
- this.logger.debug('Requesting stream from server');
12130
- const syncOptions = {
12131
- path: '/sync/stream',
12132
- abortSignal: signal,
12133
- data: {
12134
- buckets: req,
12135
- include_checksum: true,
12136
- raw_data: true,
12137
- parameters: resolvedOptions.params,
12138
- app_metadata: resolvedOptions.appMetadata,
12139
- client_id: clientId
12140
- }
12141
- };
12142
- const bson = await this.options.remote.getBSON();
12143
- const source = await this.receiveSyncLines({
12144
- options: syncOptions,
12145
- connection: resolvedOptions,
12146
- bson
12147
- });
12148
- const stream = injectable(map(source, (line) => {
12149
- if (typeof line == 'string') {
12150
- return JSON.parse(line);
11941
+ const openInner = async () => {
11942
+ if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
11943
+ return await remote.fetchStream(options);
12151
11944
  }
12152
11945
  else {
12153
- return bson.deserialize(line);
11946
+ return await this.options.remote.socketStreamRaw({
11947
+ ...options,
11948
+ ...{ fetchStrategy: connection.fetchStrategy }
11949
+ });
12154
11950
  }
12155
- }));
12156
- this.logger.debug('Stream established. Processing events');
12157
- this.notifyCompletedUploads = () => {
12158
- stream.inject({ crud_upload_completed: null });
12159
11951
  };
12160
- while (true) {
12161
- const { value: line, done } = await stream.next();
12162
- if (done) {
12163
- // The stream has closed while waiting
12164
- return;
12165
- }
12166
- if ('crud_upload_completed' in line) {
12167
- if (pendingValidatedCheckpoint != null) {
12168
- const { applied, endIteration } = await this.applyCheckpoint(pendingValidatedCheckpoint);
12169
- if (applied) {
12170
- pendingValidatedCheckpoint = null;
12171
- }
12172
- else if (endIteration) {
12173
- break;
12174
- }
11952
+ let inner;
11953
+ let done = false;
11954
+ return {
11955
+ async next() {
11956
+ if (done) {
11957
+ return doneResult;
12175
11958
  }
12176
- continue;
12177
- }
12178
- // A connection is active and messages are being received
12179
- if (!this.syncStatus.connected) {
12180
- // There is a connection now
12181
- Promise.resolve().then(() => this.triggerCrudUpload());
12182
- this.updateSyncStatus({
12183
- connected: true
12184
- });
12185
- }
12186
- if (isStreamingSyncCheckpoint(line)) {
12187
- targetCheckpoint = line.checkpoint;
12188
- // New checkpoint - existing validated checkpoint is no longer valid
12189
- pendingValidatedCheckpoint = null;
12190
- const bucketsToDelete = new Set(bucketMap.keys());
12191
- const newBuckets = new Map();
12192
- for (const checksum of line.checkpoint.buckets) {
12193
- newBuckets.set(checksum.bucket, {
12194
- name: checksum.bucket,
12195
- priority: checksum.priority ?? FALLBACK_PRIORITY
11959
+ else if (inner == null) {
11960
+ inner = await openInner();
11961
+ // We're connected here, so we can tell the core extension about it.
11962
+ return valueResult({
11963
+ command: exports.PowerSyncControlCommand.CONNECTION_STATE,
11964
+ payload: 'established'
12196
11965
  });
12197
- bucketsToDelete.delete(checksum.bucket);
12198
- }
12199
- if (bucketsToDelete.size > 0) {
12200
- this.logger.debug('Removing buckets', [...bucketsToDelete]);
12201
- }
12202
- bucketMap = newBuckets;
12203
- await this.options.adapter.removeBuckets([...bucketsToDelete]);
12204
- await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
12205
- await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
12206
- }
12207
- else if (isStreamingSyncCheckpointComplete(line)) {
12208
- const result = await this.applyCheckpoint(targetCheckpoint);
12209
- if (result.endIteration) {
12210
- return;
12211
- }
12212
- else if (!result.applied) {
12213
- // "Could not apply checkpoint due to local data". We need to retry after
12214
- // finishing uploads.
12215
- pendingValidatedCheckpoint = targetCheckpoint;
12216
11966
  }
12217
11967
  else {
12218
- // Nothing to retry later. This would likely already be null from the last
12219
- // checksum or checksum_diff operation, but we make sure.
12220
- pendingValidatedCheckpoint = null;
12221
- }
12222
- }
12223
- else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
12224
- const priority = line.partial_checkpoint_complete.priority;
12225
- this.logger.debug('Partial checkpoint complete', priority);
12226
- const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
12227
- if (!result.checkpointValid) {
12228
- // This means checksums failed. Start again with a new checkpoint.
12229
- // TODO: better back-off
12230
- await new Promise((resolve) => setTimeout(resolve, 50));
12231
- return;
12232
- }
12233
- else if (!result.ready) ;
12234
- else {
12235
- // We'll keep on downloading, but can report that this priority is synced now.
12236
- this.logger.debug('partial checkpoint validation succeeded');
12237
- // All states with a higher priority can be deleted since this partial sync includes them.
12238
- const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
12239
- priorityStates.push({
12240
- priority,
12241
- lastSyncedAt: new Date(),
12242
- hasSynced: true
12243
- });
12244
- this.updateSyncStatus({
12245
- connected: true,
12246
- priorityStatusEntries: priorityStates
12247
- });
12248
- }
12249
- }
12250
- else if (isStreamingSyncCheckpointDiff(line)) {
12251
- // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
12252
- if (targetCheckpoint == null) {
12253
- throw new Error('Checkpoint diff without previous checkpoint');
12254
- }
12255
- // New checkpoint - existing validated checkpoint is no longer valid
12256
- pendingValidatedCheckpoint = null;
12257
- const diff = line.checkpoint_diff;
12258
- const newBuckets = new Map();
12259
- for (const checksum of targetCheckpoint.buckets) {
12260
- newBuckets.set(checksum.bucket, checksum);
12261
- }
12262
- for (const checksum of diff.updated_buckets) {
12263
- newBuckets.set(checksum.bucket, checksum);
12264
- }
12265
- for (const bucket of diff.removed_buckets) {
12266
- newBuckets.delete(bucket);
12267
- }
12268
- const newCheckpoint = {
12269
- last_op_id: diff.last_op_id,
12270
- buckets: [...newBuckets.values()],
12271
- write_checkpoint: diff.write_checkpoint
12272
- };
12273
- targetCheckpoint = newCheckpoint;
12274
- await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
12275
- bucketMap = new Map();
12276
- newBuckets.forEach((checksum, name) => bucketMap.set(name, {
12277
- name: checksum.bucket,
12278
- priority: checksum.priority ?? FALLBACK_PRIORITY
12279
- }));
12280
- const bucketsToDelete = diff.removed_buckets;
12281
- if (bucketsToDelete.length > 0) {
12282
- this.logger.debug('Remove buckets', bucketsToDelete);
12283
- }
12284
- await this.options.adapter.removeBuckets(bucketsToDelete);
12285
- await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
12286
- }
12287
- else if (isStreamingSyncData(line)) {
12288
- const { data } = line;
12289
- const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
12290
- let updatedProgress = null;
12291
- if (previousProgress) {
12292
- updatedProgress = { ...previousProgress };
12293
- const progressForBucket = updatedProgress[data.bucket];
12294
- if (progressForBucket) {
12295
- updatedProgress[data.bucket] = {
12296
- ...progressForBucket,
12297
- since_last: progressForBucket.since_last + data.data.length
12298
- };
11968
+ const event = await inner.next();
11969
+ if (event.done) {
11970
+ done = true;
11971
+ return valueResult({ command: exports.PowerSyncControlCommand.CONNECTION_STATE, payload: 'end' });
12299
11972
  }
12300
- }
12301
- this.updateSyncStatus({
12302
- dataFlow: {
12303
- downloading: true,
12304
- downloadProgress: updatedProgress
11973
+ else {
11974
+ return valueResult({
11975
+ command: typeof event.value == 'string'
11976
+ ? exports.PowerSyncControlCommand.PROCESS_TEXT_LINE
11977
+ : exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
11978
+ payload: event.value
11979
+ });
12305
11980
  }
12306
- });
12307
- await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] }, usingFixedKeyFormat);
12308
- }
12309
- else if (isStreamingKeepalive(line)) {
12310
- const remaining_seconds = line.token_expires_in;
12311
- if (remaining_seconds == 0) {
12312
- // Connection would be closed automatically right after this
12313
- this.logger.debug('Token expiring; reconnect');
12314
- /**
12315
- * For a rare case where the backend connector does not update the token
12316
- * (uses the same one), this should have some delay.
12317
- */
12318
- await this.delayRetry();
12319
- return;
12320
- }
12321
- else if (remaining_seconds < 30) {
12322
- this.logger.debug('Token will expire soon; reconnect');
12323
- // Pre-emptively refresh the token
12324
- this.options.remote.invalidateCredentials();
12325
- return;
12326
11981
  }
12327
- this.triggerCrudUpload();
12328
- }
12329
- else {
12330
- this.logger.debug('Received unknown sync line', line);
12331
11982
  }
12332
- }
12333
- this.logger.debug('Stream input empty');
12334
- // Connection closed. Likely due to auth issue.
12335
- return;
11983
+ };
12336
11984
  }
12337
11985
  async rustSyncIteration(signal, resolvedOptions) {
12338
11986
  const syncImplementation = this;
12339
11987
  const adapter = this.options.adapter;
12340
11988
  const remote = this.options.remote;
12341
- const controller = new AbortController();
12342
- const abort = () => {
12343
- return controller.abort(signal.reason);
12344
- };
12345
- signal.addEventListener('abort', abort);
12346
- let receivingLines = null;
12347
- let hadSyncLine = false;
12348
11989
  let hideDisconnectOnRestart = false;
11990
+ let notifyTokenRefreshed;
12349
11991
  if (signal.aborted) {
12350
11992
  throw new AbortOperation('Connection request has been aborted');
12351
11993
  }
12352
- // Pending sync lines received from the service, as well as local events that trigger a powersync_control
12353
- // invocation (local events include refreshed tokens and completed uploads).
12354
- // This is a single data stream so that we can handle all control calls from a single place.
12355
- let controlInvocations = null;
12356
- async function connect(instr) {
12357
- const syncOptions = {
12358
- path: '/sync/stream',
12359
- abortSignal: controller.signal,
12360
- data: instr.request
11994
+ function startCommand() {
11995
+ const options = {
11996
+ parameters: resolvedOptions.params,
11997
+ app_metadata: resolvedOptions.appMetadata,
11998
+ active_streams: syncImplementation.activeStreams,
11999
+ include_defaults: resolvedOptions.includeDefaultStreams
12361
12000
  };
12362
- controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
12363
- options: syncOptions,
12364
- connection: resolvedOptions
12365
- }), (line) => {
12366
- if (typeof line == 'string') {
12367
- return {
12368
- command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
12369
- payload: line
12370
- };
12371
- }
12372
- else {
12373
- return {
12374
- command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
12375
- payload: line
12376
- };
12377
- }
12378
- }));
12379
- // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
12380
- // we're already connected here and can report that.
12381
- syncImplementation.updateSyncStatus({ connected: true });
12382
- try {
12383
- while (true) {
12384
- let event = await controlInvocations.next();
12385
- if (event.done) {
12386
- break;
12387
- }
12388
- const line = event.value;
12389
- await control(line.command, line.payload);
12390
- if (!hadSyncLine) {
12391
- syncImplementation.triggerCrudUpload();
12392
- hadSyncLine = true;
12393
- }
12394
- }
12395
- }
12396
- finally {
12397
- abort();
12398
- signal.removeEventListener('abort', abort);
12001
+ if (resolvedOptions.serializedSchema) {
12002
+ options.schema = resolvedOptions.serializedSchema;
12399
12003
  }
12004
+ return invokePowerSyncControl(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12400
12005
  }
12401
12006
  async function stop() {
12402
- await control(exports.PowerSyncControlCommand.STOP);
12007
+ const instructions = await invokePowerSyncControl(exports.PowerSyncControlCommand.STOP);
12008
+ for (const instruction of instructions) {
12009
+ // We don't need to handle interrupting instructions since we're unconditionally ending the sync iteration at
12010
+ // this point.
12011
+ if (isInterruptingInstruction(instruction))
12012
+ continue;
12013
+ await handleInstruction(instruction);
12014
+ }
12403
12015
  }
12404
- async function control(op, payload) {
12016
+ async function invokePowerSyncControl(op, payload) {
12405
12017
  const rawResponse = await adapter.control(op, payload ?? null);
12406
12018
  const logger = syncImplementation.logger;
12407
12019
  logger.trace('powersync_control', op, payload == null || typeof payload == 'string' ? payload : '<bytes>', rawResponse);
12408
- await handleInstructions(JSON.parse(rawResponse));
12020
+ if (op != exports.PowerSyncControlCommand.STOP) {
12021
+ // Evidently we have a working connection here, otherwise powersync_control would have failed.
12022
+ syncImplementation.connectionMayHaveChanged = false;
12023
+ }
12024
+ return JSON.parse(rawResponse);
12409
12025
  }
12410
12026
  async function handleInstruction(instruction) {
12411
12027
  if ('LogLine' in instruction) {
@@ -12424,13 +12040,6 @@ The next upload iteration will be delayed.`);
12424
12040
  else if ('UpdateSyncStatus' in instruction) {
12425
12041
  syncImplementation.updateSyncStatus(coreStatusToJs(instruction.UpdateSyncStatus.status));
12426
12042
  }
12427
- else if ('EstablishSyncStream' in instruction) {
12428
- if (receivingLines != null) {
12429
- // Already connected, this shouldn't happen during a single iteration.
12430
- throw 'Unexpected request to establish sync stream, already connected';
12431
- }
12432
- receivingLines = connect(instruction.EstablishSyncStream);
12433
- }
12434
12043
  else if ('FetchCredentials' in instruction) {
12435
12044
  if (instruction.FetchCredentials.did_expire) {
12436
12045
  remote.invalidateCredentials();
@@ -12439,16 +12048,12 @@ The next upload iteration will be delayed.`);
12439
12048
  remote.invalidateCredentials();
12440
12049
  // Restart iteration after the credentials have been refreshed.
12441
12050
  remote.fetchCredentials().then((_) => {
12442
- controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12051
+ notifyTokenRefreshed?.();
12443
12052
  }, (err) => {
12444
12053
  syncImplementation.logger.warn('Could not prefetch credentials', err);
12445
12054
  });
12446
12055
  }
12447
12056
  }
12448
- else if ('CloseSyncStream' in instruction) {
12449
- controller.abort();
12450
- hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12451
- }
12452
12057
  else if ('FlushFileSystem' in instruction) ;
12453
12058
  else if ('DidCompleteSync' in instruction) {
12454
12059
  syncImplementation.updateSyncStatus({
@@ -12458,101 +12063,83 @@ The next upload iteration will be delayed.`);
12458
12063
  });
12459
12064
  }
12460
12065
  }
12461
- async function handleInstructions(instructions) {
12462
- for (const instr of instructions) {
12463
- await handleInstruction(instr);
12464
- }
12465
- }
12466
12066
  try {
12467
- const options = {
12468
- parameters: resolvedOptions.params,
12469
- app_metadata: resolvedOptions.appMetadata,
12470
- active_streams: this.activeStreams,
12471
- include_defaults: resolvedOptions.includeDefaultStreams
12472
- };
12473
- if (resolvedOptions.serializedSchema) {
12474
- options.schema = resolvedOptions.serializedSchema;
12067
+ const defaultResult = { immediateRestart: false };
12068
+ // Pending sync lines received from the service, as well as local events that trigger a powersync_control
12069
+ // invocation (local events include refreshed tokens and completed uploads).
12070
+ // This is a single data stream so that we can handle all control calls from a single place.
12071
+ let controlInvocations = null;
12072
+ for (const startInstruction of await startCommand()) {
12073
+ if ('EstablishSyncStream' in startInstruction) {
12074
+ const syncOptions = {
12075
+ path: '/sync/stream',
12076
+ abortSignal: signal,
12077
+ data: startInstruction.EstablishSyncStream.request
12078
+ };
12079
+ controlInvocations = injectable(syncImplementation.receiveSyncLines({
12080
+ options: syncOptions,
12081
+ connection: resolvedOptions
12082
+ }));
12083
+ }
12084
+ else if ('CloseSyncStream' in startInstruction) {
12085
+ return defaultResult;
12086
+ }
12087
+ else {
12088
+ await handleInstruction(startInstruction);
12089
+ }
12475
12090
  }
12476
- await control(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12091
+ if (controlInvocations == null)
12092
+ return defaultResult;
12477
12093
  this.notifyCompletedUploads = () => {
12478
- controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12094
+ controlInvocations.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12479
12095
  };
12480
12096
  this.handleActiveStreamsChange = () => {
12481
- controlInvocations?.inject({
12097
+ controlInvocations.inject({
12482
12098
  command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12483
12099
  payload: JSON.stringify(this.activeStreams)
12484
12100
  });
12485
12101
  };
12486
- await receivingLines;
12102
+ notifyTokenRefreshed = () => {
12103
+ controlInvocations.inject({
12104
+ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED
12105
+ });
12106
+ };
12107
+ let hadSyncLine = false;
12108
+ loop: while (true) {
12109
+ const { done, value } = await controlInvocations.next();
12110
+ if (done)
12111
+ break;
12112
+ if (!hadSyncLine) {
12113
+ // Trigger a local CRUD upload when the first sync line has been received, this allows uploading local changes
12114
+ // that have been made while offline or disconnected.
12115
+ if (value.command == exports.PowerSyncControlCommand.PROCESS_TEXT_LINE ||
12116
+ value.command == exports.PowerSyncControlCommand.PROCESS_BSON_LINE) {
12117
+ hadSyncLine = true;
12118
+ this.triggerCrudUpload?.();
12119
+ }
12120
+ }
12121
+ const instructions = await invokePowerSyncControl(value.command, value.payload);
12122
+ for (const instruction of instructions) {
12123
+ if ('EstablishSyncStream' in instruction) {
12124
+ throw new Error('Received EstablishSyncStream while already connected.');
12125
+ }
12126
+ else if ('CloseSyncStream' in instruction) {
12127
+ hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12128
+ break loop;
12129
+ }
12130
+ else {
12131
+ await handleInstruction(instruction);
12132
+ }
12133
+ }
12134
+ }
12487
12135
  }
12488
12136
  finally {
12489
12137
  this.notifyCompletedUploads = this.handleActiveStreamsChange = undefined;
12138
+ notifyTokenRefreshed = undefined;
12490
12139
  await stop();
12491
12140
  }
12492
12141
  return { immediateRestart: hideDisconnectOnRestart };
12493
12142
  }
12494
- async updateSyncStatusForStartingCheckpoint(checkpoint) {
12495
- const localProgress = await this.options.adapter.getBucketOperationProgress();
12496
- const progress = {};
12497
- let invalidated = false;
12498
- for (const bucket of checkpoint.buckets) {
12499
- const savedProgress = localProgress[bucket.bucket];
12500
- const atLast = savedProgress?.atLast ?? 0;
12501
- const sinceLast = savedProgress?.sinceLast ?? 0;
12502
- progress[bucket.bucket] = {
12503
- // The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
12504
- // will use by default.
12505
- priority: bucket.priority ?? 3,
12506
- at_last: atLast,
12507
- since_last: sinceLast,
12508
- target_count: bucket.count ?? 0
12509
- };
12510
- if (bucket.count != null && bucket.count < atLast + sinceLast) {
12511
- // Either due to a defrag / sync rule deploy or a compaction operation, the size
12512
- // of the bucket shrank so much that the local ops exceed the ops in the updated
12513
- // bucket. We can't prossibly report progress in this case (it would overshoot 100%).
12514
- invalidated = true;
12515
- }
12516
- }
12517
- if (invalidated) {
12518
- for (const bucket in progress) {
12519
- const bucketProgress = progress[bucket];
12520
- bucketProgress.at_last = 0;
12521
- bucketProgress.since_last = 0;
12522
- }
12523
- }
12524
- this.updateSyncStatus({
12525
- dataFlow: {
12526
- downloading: true,
12527
- downloadProgress: progress
12528
- }
12529
- });
12530
- }
12531
- async applyCheckpoint(checkpoint) {
12532
- let result = await this.options.adapter.syncLocalDatabase(checkpoint);
12533
- if (!result.checkpointValid) {
12534
- this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
12535
- // This means checksums failed. Start again with a new checkpoint.
12536
- // TODO: better back-off
12537
- await new Promise((resolve) => setTimeout(resolve, 50));
12538
- return { applied: false, endIteration: true };
12539
- }
12540
- else if (!result.ready) {
12541
- this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. We will retry applying the checkpoint after that upload is completed.`);
12542
- return { applied: false, endIteration: false };
12543
- }
12544
- this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
12545
- this.updateSyncStatus({
12546
- connected: true,
12547
- lastSyncedAt: new Date(),
12548
- dataFlow: {
12549
- downloading: false,
12550
- downloadProgress: null,
12551
- downloadError: undefined
12552
- }
12553
- });
12554
- return { applied: true, endIteration: false };
12555
- }
12556
12143
  updateSyncStatus(options) {
12557
12144
  const updatedStatus = new SyncStatus({
12558
12145
  connected: options.connected ?? this.syncStatus.connected,
@@ -14091,14 +13678,12 @@ class SqliteBucketStorage extends BaseObserver {
14091
13678
  db;
14092
13679
  logger;
14093
13680
  tableNames;
14094
- _hasCompletedSync;
14095
13681
  updateListener;
14096
13682
  _clientId;
14097
13683
  constructor(db, logger = Logger.get('SqliteBucketStorage')) {
14098
13684
  super();
14099
13685
  this.db = db;
14100
13686
  this.logger = logger;
14101
- this._hasCompletedSync = false;
14102
13687
  this.tableNames = new Set();
14103
13688
  this.updateListener = db.registerListener({
14104
13689
  tablesUpdated: (update) => {
@@ -14110,7 +13695,6 @@ class SqliteBucketStorage extends BaseObserver {
14110
13695
  });
14111
13696
  }
14112
13697
  async init() {
14113
- this._hasCompletedSync = false;
14114
13698
  const existingTableRows = await this.db.getAll(`SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'`);
14115
13699
  for (const row of existingTableRows ?? []) {
14116
13700
  this.tableNames.add(row.name);
@@ -14132,156 +13716,6 @@ class SqliteBucketStorage extends BaseObserver {
14132
13716
  getMaxOpId() {
14133
13717
  return MAX_OP_ID;
14134
13718
  }
14135
- /**
14136
- * Reset any caches.
14137
- */
14138
- startSession() { }
14139
- async getBucketStates() {
14140
- const result = await this.db.getAll("SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0 AND name != '$local'");
14141
- return result;
14142
- }
14143
- async getBucketOperationProgress() {
14144
- const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
14145
- return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
14146
- }
14147
- async saveSyncData(batch, fixedKeyFormat = false) {
14148
- await this.writeTransaction(async (tx) => {
14149
- for (const b of batch.buckets) {
14150
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
14151
- 'save',
14152
- JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
14153
- ]);
14154
- this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
14155
- }
14156
- });
14157
- }
14158
- async removeBuckets(buckets) {
14159
- for (const bucket of buckets) {
14160
- await this.deleteBucket(bucket);
14161
- }
14162
- }
14163
- /**
14164
- * Mark a bucket for deletion.
14165
- */
14166
- async deleteBucket(bucket) {
14167
- await this.writeTransaction(async (tx) => {
14168
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
14169
- });
14170
- this.logger.debug(`Done deleting bucket ${bucket}`);
14171
- }
14172
- async hasCompletedSync() {
14173
- if (this._hasCompletedSync) {
14174
- return true;
14175
- }
14176
- const r = await this.db.get(`SELECT powersync_last_synced_at() as synced_at`);
14177
- const completed = r.synced_at != null;
14178
- if (completed) {
14179
- this._hasCompletedSync = true;
14180
- }
14181
- return completed;
14182
- }
14183
- async syncLocalDatabase(checkpoint, priority) {
14184
- const r = await this.validateChecksums(checkpoint, priority);
14185
- if (!r.checkpointValid) {
14186
- this.logger.error('Checksums failed for', r.checkpointFailures);
14187
- for (const b of r.checkpointFailures ?? []) {
14188
- await this.deleteBucket(b);
14189
- }
14190
- return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
14191
- }
14192
- if (priority == null) {
14193
- this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
14194
- }
14195
- else {
14196
- this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
14197
- }
14198
- let buckets = checkpoint.buckets;
14199
- if (priority !== undefined) {
14200
- buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
14201
- }
14202
- const bucketNames = buckets.map((b) => b.bucket);
14203
- await this.writeTransaction(async (tx) => {
14204
- await tx.execute(`UPDATE ps_buckets SET last_op = ? WHERE name IN (SELECT json_each.value FROM json_each(?))`, [
14205
- checkpoint.last_op_id,
14206
- JSON.stringify(bucketNames)
14207
- ]);
14208
- if (priority == null && checkpoint.write_checkpoint) {
14209
- await tx.execute("UPDATE ps_buckets SET last_op = ? WHERE name = '$local'", [checkpoint.write_checkpoint]);
14210
- }
14211
- });
14212
- const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
14213
- if (!valid) {
14214
- return { ready: false, checkpointValid: true };
14215
- }
14216
- return {
14217
- ready: true,
14218
- checkpointValid: true
14219
- };
14220
- }
14221
- /**
14222
- * Atomically update the local state to the current checkpoint.
14223
- *
14224
- * This includes creating new tables, dropping old tables, and copying data over from the oplog.
14225
- */
14226
- async updateObjectsFromBuckets(checkpoint, priority) {
14227
- let arg = '';
14228
- if (priority !== undefined) {
14229
- const affectedBuckets = [];
14230
- for (const desc of checkpoint.buckets) {
14231
- if (hasMatchingPriority(priority, desc)) {
14232
- affectedBuckets.push(desc.bucket);
14233
- }
14234
- }
14235
- arg = JSON.stringify({ priority, buckets: affectedBuckets });
14236
- }
14237
- return this.writeTransaction(async (tx) => {
14238
- const { insertId: result } = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
14239
- 'sync_local',
14240
- arg
14241
- ]);
14242
- if (result == 1) {
14243
- if (priority == null) {
14244
- const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
14245
- // The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
14246
- const jsonBucketCount = JSON.stringify(bucketToCount);
14247
- await tx.execute("UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?->name WHERE name != '$local' AND ?->name IS NOT NULL", [jsonBucketCount, jsonBucketCount]);
14248
- }
14249
- return true;
14250
- }
14251
- else {
14252
- return false;
14253
- }
14254
- });
14255
- }
14256
- async validateChecksums(checkpoint, priority) {
14257
- if (priority !== undefined) {
14258
- // Only validate the buckets within the priority we care about
14259
- const newBuckets = checkpoint.buckets.filter((cs) => hasMatchingPriority(priority, cs));
14260
- checkpoint = { ...checkpoint, buckets: newBuckets };
14261
- }
14262
- const rs = await this.db.execute('SELECT powersync_validate_checkpoint(?) as result', [
14263
- JSON.stringify({ ...checkpoint })
14264
- ]);
14265
- const resultItem = rs.rows?.item(0);
14266
- if (!resultItem) {
14267
- return {
14268
- checkpointValid: false,
14269
- ready: false,
14270
- checkpointFailures: []
14271
- };
14272
- }
14273
- const result = JSON.parse(resultItem['result']);
14274
- if (result['valid']) {
14275
- return { ready: true, checkpointValid: true };
14276
- }
14277
- else {
14278
- return {
14279
- checkpointValid: false,
14280
- ready: false,
14281
- checkpointFailures: result['failed_buckets']
14282
- };
14283
- }
14284
- }
14285
13719
  async updateLocalTarget(cb) {
14286
13720
  const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)", [MAX_OP_ID]);
14287
13721
  if (!rs1.length) {
@@ -14372,12 +13806,6 @@ class SqliteBucketStorage extends BaseObserver {
14372
13806
  async writeTransaction(callback, options) {
14373
13807
  return this.db.writeTransaction(callback, options);
14374
13808
  }
14375
- /**
14376
- * Set a target checkpoint.
14377
- */
14378
- async setTargetCheckpoint(checkpoint) {
14379
- // No-op for now
14380
- }
14381
13809
  async control(op, payload) {
14382
13810
  return await this.writeTransaction(async (tx) => {
14383
13811
  const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
@@ -14401,20 +13829,6 @@ class SqliteBucketStorage extends BaseObserver {
14401
13829
  }
14402
13830
  static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
14403
13831
  }
14404
- function hasMatchingPriority(priority, bucket) {
14405
- return bucket.priority != null && bucket.priority <= priority;
14406
- }
14407
-
14408
- // TODO JSON
14409
- class SyncDataBatch {
14410
- buckets;
14411
- static fromJSON(json) {
14412
- return new SyncDataBatch(json.buckets.map((bucket) => SyncDataBucket.fromRow(bucket)));
14413
- }
14414
- constructor(buckets) {
14415
- this.buckets = buckets;
14416
- }
14417
- }
14418
13832
 
14419
13833
  /**
14420
13834
  * Thrown when an underlying database connection is closed.
@@ -14474,10 +13888,8 @@ class Schema {
14474
13888
  * developer instead of automatically by PowerSync.
14475
13889
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
14476
13890
  * using client-side table and column constraints.
14477
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
14478
13891
  *
14479
13892
  * @param tables An object of (table name, raw table definition) entries.
14480
- * @experimental Note that the raw tables API is still experimental and may change in the future.
14481
13893
  */
14482
13894
  withRawTables(tables) {
14483
13895
  for (const [name, rawTableDefinition] of Object.entries(tables)) {
@@ -14727,13 +14139,9 @@ exports.MAX_OP_ID = MAX_OP_ID;
14727
14139
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
14728
14140
  exports.Mutex = Mutex;
14729
14141
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
14730
- exports.OpType = OpType;
14731
- exports.OplogEntry = OplogEntry;
14732
14142
  exports.Schema = Schema;
14733
14143
  exports.Semaphore = Semaphore;
14734
14144
  exports.SqliteBucketStorage = SqliteBucketStorage;
14735
- exports.SyncDataBatch = SyncDataBatch;
14736
- exports.SyncDataBucket = SyncDataBucket;
14737
14145
  exports.SyncProgress = SyncProgress;
14738
14146
  exports.SyncStatus = SyncStatus;
14739
14147
  exports.SyncingService = SyncingService;
@@ -14748,18 +14156,10 @@ exports.createBaseLogger = createBaseLogger;
14748
14156
  exports.createLogger = createLogger;
14749
14157
  exports.extractTableUpdates = extractTableUpdates;
14750
14158
  exports.isBatchedUpdateNotification = isBatchedUpdateNotification;
14751
- exports.isContinueCheckpointRequest = isContinueCheckpointRequest;
14752
14159
  exports.isDBAdapter = isDBAdapter;
14753
14160
  exports.isPowerSyncDatabaseOptionsWithSettings = isPowerSyncDatabaseOptionsWithSettings;
14754
14161
  exports.isSQLOpenFactory = isSQLOpenFactory;
14755
14162
  exports.isSQLOpenOptions = isSQLOpenOptions;
14756
- exports.isStreamingKeepalive = isStreamingKeepalive;
14757
- exports.isStreamingSyncCheckpoint = isStreamingSyncCheckpoint;
14758
- exports.isStreamingSyncCheckpointComplete = isStreamingSyncCheckpointComplete;
14759
- exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
14760
- exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
14761
- exports.isStreamingSyncData = isStreamingSyncData;
14762
- exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
14763
14163
  exports.parseQuery = parseQuery;
14764
14164
  exports.runOnSchemaChange = runOnSchemaChange;
14765
14165
  exports.sanitizeSQL = sanitizeSQL;