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