@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.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.0";
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
  *
@@ -11302,22 +11196,12 @@ class AbstractRemote {
11302
11196
  * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
11303
11197
  *
11304
11198
  * 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
11199
  */
11309
- async socketStreamRaw(options, bson) {
11200
+ async socketStreamRaw(options) {
11310
11201
  const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
11311
- const mimeType = bson == null ? 'application/json' : 'application/bson';
11202
+ const mimeType = 'application/json';
11312
11203
  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);
11204
+ return bufferExports.Buffer.from(JSON.stringify(js));
11321
11205
  }
11322
11206
  const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
11323
11207
  const request = await this.buildRequest(path);
@@ -11618,31 +11502,8 @@ function coreStatusToJs(status) {
11618
11502
  priorityStatusEntries: status.priority_status.map(priorityToJs)
11619
11503
  };
11620
11504
  }
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';
11505
+ function isInterruptingInstruction(instruction) {
11506
+ return 'EstablishSyncStream' in instruction || 'CloseSyncStream' in instruction;
11646
11507
  }
11647
11508
 
11648
11509
  exports.LockType = void 0;
@@ -11657,35 +11518,21 @@ exports.SyncStreamConnectionMethod = void 0;
11657
11518
  })(exports.SyncStreamConnectionMethod || (exports.SyncStreamConnectionMethod = {}));
11658
11519
  exports.SyncClientImplementation = void 0;
11659
11520
  (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
11521
  /**
11671
11522
  * This implementation offloads the sync line decoding and handling into the PowerSync
11672
11523
  * core extension.
11673
11524
  *
11674
- * This option is more performant than the {@link JAVASCRIPT} client, enabled by default and the
11675
- * recommended client implementation for all apps.
11525
+ * This is the only option, as an older JavaScript client implementation has been removed from the SDK.
11676
11526
  *
11677
11527
  * ## Compatibility warning
11678
11528
  *
11679
11529
  * 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.
11530
+ * by the old JavaScript client. When adopting the {@link RUST} client on existing databases, the PowerSync SDK will
11531
+ * migrate the format automatically.
11685
11532
  *
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.
11533
+ * SDK versions supporting both the JavaScript and the Rust client support both formats with the JavaScript client
11534
+ * implementaiton. However, downgrading to an SDK version that only supports the JavaScript client would not be
11535
+ * possible anymore. Problematic SDK versions have been released before 2025-06-09.
11689
11536
  */
11690
11537
  SyncClientImplementation["RUST"] = "rust";
11691
11538
  })(exports.SyncClientImplementation || (exports.SyncClientImplementation = {}));
@@ -11708,13 +11555,7 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
11708
11555
  serializedSchema: undefined,
11709
11556
  includeDefaultStreams: true
11710
11557
  };
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
11558
  class AbstractStreamingSyncImplementation extends BaseObserver {
11717
- _lastSyncedAt;
11718
11559
  options;
11719
11560
  abortController;
11720
11561
  // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
@@ -11724,6 +11565,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11724
11565
  streamingSyncPromise;
11725
11566
  logger;
11726
11567
  activeStreams;
11568
+ connectionMayHaveChanged = false;
11727
11569
  isUploadingCrud = false;
11728
11570
  notifyCompletedUploads;
11729
11571
  handleActiveStreamsChange;
@@ -11803,9 +11645,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11803
11645
  this.crudUpdateListener = undefined;
11804
11646
  this.uploadAbortController?.abort();
11805
11647
  }
11806
- async hasCompletedSync() {
11807
- return this.options.adapter.hasCompletedSync();
11808
- }
11809
11648
  async getWriteCheckpoint() {
11810
11649
  const clientId = await this.options.adapter.getClientId();
11811
11650
  let path = `/write-checkpoint2.json?client_id=${clientId}`;
@@ -11887,7 +11726,7 @@ The next upload iteration will be delayed.`);
11887
11726
  });
11888
11727
  }
11889
11728
  }
11890
- this.uploadAbortController = null;
11729
+ this.uploadAbortController = undefined;
11891
11730
  }
11892
11731
  });
11893
11732
  }
@@ -12003,6 +11842,11 @@ The next upload iteration will be delayed.`);
12003
11842
  shouldDelayRetry = false;
12004
11843
  // A disconnect was requested, we should not delay since there is no explicit retry
12005
11844
  }
11845
+ else if (this.connectionMayHaveChanged && ex.message?.indexOf('No iteration is active') >= 0) {
11846
+ this.connectionMayHaveChanged = false;
11847
+ this.logger.info('Sync error after changed connection, retrying immediately');
11848
+ shouldDelayRetry = false;
11849
+ }
12006
11850
  else {
12007
11851
  this.logger.error(ex);
12008
11852
  }
@@ -12033,17 +11877,14 @@ The next upload iteration will be delayed.`);
12033
11877
  // Mark as disconnected if here
12034
11878
  this.updateSyncStatus({ connected: false, connecting: false });
12035
11879
  }
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];
11880
+ markConnectionMayHaveChanged() {
11881
+ // By setting this field, we'll immediately retry if the next sync event causes an error triggered by us not having
11882
+ // an active sync iteration on the connection in use.
11883
+ this.connectionMayHaveChanged = true;
11884
+ // This triggers a `powersync_control` invocation if a sync iteration is currently active. This is a cheap call to
11885
+ // make when no subscriptions have actually changed, we're mainly interested in this immediately throwing if no
11886
+ // iteration is active. That allows us to reconnect ASAP, instead of having to wait for the next sync line.
11887
+ this.handleActiveStreamsChange?.();
12047
11888
  }
12048
11889
  /**
12049
11890
  * Older versions of the JS SDK used to encode subkeys as JSON in {@link OplogEntry.toJSON}.
@@ -12084,328 +11925,98 @@ The next upload iteration will be delayed.`);
12084
11925
  if (invalidMetadata.length > 0) {
12085
11926
  throw new Error(`Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}`);
12086
11927
  }
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
- }
11928
+ await this.requireKeyFormat(true);
11929
+ return await this.rustSyncIteration(signal, resolvedOptions);
12097
11930
  }
12098
11931
  });
12099
11932
  }
12100
- async receiveSyncLines(data) {
12101
- const { options, connection, bson } = data;
11933
+ receiveSyncLines(data) {
11934
+ const { options, connection } = data;
12102
11935
  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);
11936
+ const openInner = async () => {
11937
+ if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
11938
+ return await remote.fetchStream(options);
12151
11939
  }
12152
11940
  else {
12153
- return bson.deserialize(line);
11941
+ return await this.options.remote.socketStreamRaw({
11942
+ ...options,
11943
+ ...{ fetchStrategy: connection.fetchStrategy }
11944
+ });
12154
11945
  }
12155
- }));
12156
- this.logger.debug('Stream established. Processing events');
12157
- this.notifyCompletedUploads = () => {
12158
- stream.inject({ crud_upload_completed: null });
12159
11946
  };
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
- }
11947
+ let inner;
11948
+ let done = false;
11949
+ return {
11950
+ async next() {
11951
+ if (done) {
11952
+ return doneResult;
12175
11953
  }
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
11954
+ else if (inner == null) {
11955
+ inner = await openInner();
11956
+ // We're connected here, so we can tell the core extension about it.
11957
+ return valueResult({
11958
+ command: exports.PowerSyncControlCommand.CONNECTION_STATE,
11959
+ payload: 'established'
12196
11960
  });
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
11961
  }
12217
11962
  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
- };
11963
+ const event = await inner.next();
11964
+ if (event.done) {
11965
+ done = true;
11966
+ return valueResult({ command: exports.PowerSyncControlCommand.CONNECTION_STATE, payload: 'end' });
12299
11967
  }
12300
- }
12301
- this.updateSyncStatus({
12302
- dataFlow: {
12303
- downloading: true,
12304
- downloadProgress: updatedProgress
11968
+ else {
11969
+ return valueResult({
11970
+ command: typeof event.value == 'string'
11971
+ ? exports.PowerSyncControlCommand.PROCESS_TEXT_LINE
11972
+ : exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
11973
+ payload: event.value
11974
+ });
12305
11975
  }
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
11976
  }
12327
- this.triggerCrudUpload();
12328
- }
12329
- else {
12330
- this.logger.debug('Received unknown sync line', line);
12331
11977
  }
12332
- }
12333
- this.logger.debug('Stream input empty');
12334
- // Connection closed. Likely due to auth issue.
12335
- return;
11978
+ };
12336
11979
  }
12337
11980
  async rustSyncIteration(signal, resolvedOptions) {
12338
11981
  const syncImplementation = this;
12339
11982
  const adapter = this.options.adapter;
12340
11983
  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
11984
  let hideDisconnectOnRestart = false;
11985
+ let notifyTokenRefreshed;
12349
11986
  if (signal.aborted) {
12350
11987
  throw new AbortOperation('Connection request has been aborted');
12351
11988
  }
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
11989
+ function startCommand() {
11990
+ const options = {
11991
+ parameters: resolvedOptions.params,
11992
+ app_metadata: resolvedOptions.appMetadata,
11993
+ active_streams: syncImplementation.activeStreams,
11994
+ include_defaults: resolvedOptions.includeDefaultStreams
12361
11995
  };
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);
11996
+ if (resolvedOptions.serializedSchema) {
11997
+ options.schema = resolvedOptions.serializedSchema;
12399
11998
  }
11999
+ return invokePowerSyncControl(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12400
12000
  }
12401
12001
  async function stop() {
12402
- await control(exports.PowerSyncControlCommand.STOP);
12002
+ const instructions = await invokePowerSyncControl(exports.PowerSyncControlCommand.STOP);
12003
+ for (const instruction of instructions) {
12004
+ // We don't need to handle interrupting instructions since we're unconditionally ending the sync iteration at
12005
+ // this point.
12006
+ if (isInterruptingInstruction(instruction))
12007
+ continue;
12008
+ await handleInstruction(instruction);
12009
+ }
12403
12010
  }
12404
- async function control(op, payload) {
12011
+ async function invokePowerSyncControl(op, payload) {
12405
12012
  const rawResponse = await adapter.control(op, payload ?? null);
12406
12013
  const logger = syncImplementation.logger;
12407
12014
  logger.trace('powersync_control', op, payload == null || typeof payload == 'string' ? payload : '<bytes>', rawResponse);
12408
- await handleInstructions(JSON.parse(rawResponse));
12015
+ if (op != exports.PowerSyncControlCommand.STOP) {
12016
+ // Evidently we have a working connection here, otherwise powersync_control would have failed.
12017
+ syncImplementation.connectionMayHaveChanged = false;
12018
+ }
12019
+ return JSON.parse(rawResponse);
12409
12020
  }
12410
12021
  async function handleInstruction(instruction) {
12411
12022
  if ('LogLine' in instruction) {
@@ -12424,13 +12035,6 @@ The next upload iteration will be delayed.`);
12424
12035
  else if ('UpdateSyncStatus' in instruction) {
12425
12036
  syncImplementation.updateSyncStatus(coreStatusToJs(instruction.UpdateSyncStatus.status));
12426
12037
  }
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
12038
  else if ('FetchCredentials' in instruction) {
12435
12039
  if (instruction.FetchCredentials.did_expire) {
12436
12040
  remote.invalidateCredentials();
@@ -12439,16 +12043,12 @@ The next upload iteration will be delayed.`);
12439
12043
  remote.invalidateCredentials();
12440
12044
  // Restart iteration after the credentials have been refreshed.
12441
12045
  remote.fetchCredentials().then((_) => {
12442
- controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12046
+ notifyTokenRefreshed?.();
12443
12047
  }, (err) => {
12444
12048
  syncImplementation.logger.warn('Could not prefetch credentials', err);
12445
12049
  });
12446
12050
  }
12447
12051
  }
12448
- else if ('CloseSyncStream' in instruction) {
12449
- controller.abort();
12450
- hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12451
- }
12452
12052
  else if ('FlushFileSystem' in instruction) ;
12453
12053
  else if ('DidCompleteSync' in instruction) {
12454
12054
  syncImplementation.updateSyncStatus({
@@ -12458,101 +12058,83 @@ The next upload iteration will be delayed.`);
12458
12058
  });
12459
12059
  }
12460
12060
  }
12461
- async function handleInstructions(instructions) {
12462
- for (const instr of instructions) {
12463
- await handleInstruction(instr);
12464
- }
12465
- }
12466
12061
  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;
12062
+ const defaultResult = { immediateRestart: false };
12063
+ // Pending sync lines received from the service, as well as local events that trigger a powersync_control
12064
+ // invocation (local events include refreshed tokens and completed uploads).
12065
+ // This is a single data stream so that we can handle all control calls from a single place.
12066
+ let controlInvocations = null;
12067
+ for (const startInstruction of await startCommand()) {
12068
+ if ('EstablishSyncStream' in startInstruction) {
12069
+ const syncOptions = {
12070
+ path: '/sync/stream',
12071
+ abortSignal: signal,
12072
+ data: startInstruction.EstablishSyncStream.request
12073
+ };
12074
+ controlInvocations = injectable(syncImplementation.receiveSyncLines({
12075
+ options: syncOptions,
12076
+ connection: resolvedOptions
12077
+ }));
12078
+ }
12079
+ else if ('CloseSyncStream' in startInstruction) {
12080
+ return defaultResult;
12081
+ }
12082
+ else {
12083
+ await handleInstruction(startInstruction);
12084
+ }
12475
12085
  }
12476
- await control(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12086
+ if (controlInvocations == null)
12087
+ return defaultResult;
12477
12088
  this.notifyCompletedUploads = () => {
12478
- controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12089
+ controlInvocations.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12479
12090
  };
12480
12091
  this.handleActiveStreamsChange = () => {
12481
- controlInvocations?.inject({
12092
+ controlInvocations.inject({
12482
12093
  command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12483
12094
  payload: JSON.stringify(this.activeStreams)
12484
12095
  });
12485
12096
  };
12486
- await receivingLines;
12097
+ notifyTokenRefreshed = () => {
12098
+ controlInvocations.inject({
12099
+ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED
12100
+ });
12101
+ };
12102
+ let hadSyncLine = false;
12103
+ loop: while (true) {
12104
+ const { done, value } = await controlInvocations.next();
12105
+ if (done)
12106
+ break;
12107
+ if (!hadSyncLine) {
12108
+ // Trigger a local CRUD upload when the first sync line has been received, this allows uploading local changes
12109
+ // that have been made while offline or disconnected.
12110
+ if (value.command == exports.PowerSyncControlCommand.PROCESS_TEXT_LINE ||
12111
+ value.command == exports.PowerSyncControlCommand.PROCESS_BSON_LINE) {
12112
+ hadSyncLine = true;
12113
+ this.triggerCrudUpload?.();
12114
+ }
12115
+ }
12116
+ const instructions = await invokePowerSyncControl(value.command, value.payload);
12117
+ for (const instruction of instructions) {
12118
+ if ('EstablishSyncStream' in instruction) {
12119
+ throw new Error('Received EstablishSyncStream while already connected.');
12120
+ }
12121
+ else if ('CloseSyncStream' in instruction) {
12122
+ hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12123
+ break loop;
12124
+ }
12125
+ else {
12126
+ await handleInstruction(instruction);
12127
+ }
12128
+ }
12129
+ }
12487
12130
  }
12488
12131
  finally {
12489
12132
  this.notifyCompletedUploads = this.handleActiveStreamsChange = undefined;
12133
+ notifyTokenRefreshed = undefined;
12490
12134
  await stop();
12491
12135
  }
12492
12136
  return { immediateRestart: hideDisconnectOnRestart };
12493
12137
  }
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
12138
  updateSyncStatus(options) {
12557
12139
  const updatedStatus = new SyncStatus({
12558
12140
  connected: options.connected ?? this.syncStatus.connected,
@@ -14091,14 +13673,12 @@ class SqliteBucketStorage extends BaseObserver {
14091
13673
  db;
14092
13674
  logger;
14093
13675
  tableNames;
14094
- _hasCompletedSync;
14095
13676
  updateListener;
14096
13677
  _clientId;
14097
13678
  constructor(db, logger = Logger.get('SqliteBucketStorage')) {
14098
13679
  super();
14099
13680
  this.db = db;
14100
13681
  this.logger = logger;
14101
- this._hasCompletedSync = false;
14102
13682
  this.tableNames = new Set();
14103
13683
  this.updateListener = db.registerListener({
14104
13684
  tablesUpdated: (update) => {
@@ -14110,7 +13690,6 @@ class SqliteBucketStorage extends BaseObserver {
14110
13690
  });
14111
13691
  }
14112
13692
  async init() {
14113
- this._hasCompletedSync = false;
14114
13693
  const existingTableRows = await this.db.getAll(`SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'`);
14115
13694
  for (const row of existingTableRows ?? []) {
14116
13695
  this.tableNames.add(row.name);
@@ -14132,156 +13711,6 @@ class SqliteBucketStorage extends BaseObserver {
14132
13711
  getMaxOpId() {
14133
13712
  return MAX_OP_ID;
14134
13713
  }
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
13714
  async updateLocalTarget(cb) {
14286
13715
  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
13716
  if (!rs1.length) {
@@ -14372,12 +13801,6 @@ class SqliteBucketStorage extends BaseObserver {
14372
13801
  async writeTransaction(callback, options) {
14373
13802
  return this.db.writeTransaction(callback, options);
14374
13803
  }
14375
- /**
14376
- * Set a target checkpoint.
14377
- */
14378
- async setTargetCheckpoint(checkpoint) {
14379
- // No-op for now
14380
- }
14381
13804
  async control(op, payload) {
14382
13805
  return await this.writeTransaction(async (tx) => {
14383
13806
  const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
@@ -14401,20 +13824,6 @@ class SqliteBucketStorage extends BaseObserver {
14401
13824
  }
14402
13825
  static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
14403
13826
  }
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
13827
 
14419
13828
  /**
14420
13829
  * Thrown when an underlying database connection is closed.
@@ -14474,10 +13883,8 @@ class Schema {
14474
13883
  * developer instead of automatically by PowerSync.
14475
13884
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
14476
13885
  * using client-side table and column constraints.
14477
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
14478
13886
  *
14479
13887
  * @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
13888
  */
14482
13889
  withRawTables(tables) {
14483
13890
  for (const [name, rawTableDefinition] of Object.entries(tables)) {
@@ -14727,13 +14134,9 @@ exports.MAX_OP_ID = MAX_OP_ID;
14727
14134
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
14728
14135
  exports.Mutex = Mutex;
14729
14136
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
14730
- exports.OpType = OpType;
14731
- exports.OplogEntry = OplogEntry;
14732
14137
  exports.Schema = Schema;
14733
14138
  exports.Semaphore = Semaphore;
14734
14139
  exports.SqliteBucketStorage = SqliteBucketStorage;
14735
- exports.SyncDataBatch = SyncDataBatch;
14736
- exports.SyncDataBucket = SyncDataBucket;
14737
14140
  exports.SyncProgress = SyncProgress;
14738
14141
  exports.SyncStatus = SyncStatus;
14739
14142
  exports.SyncingService = SyncingService;
@@ -14748,18 +14151,10 @@ exports.createBaseLogger = createBaseLogger;
14748
14151
  exports.createLogger = createLogger;
14749
14152
  exports.extractTableUpdates = extractTableUpdates;
14750
14153
  exports.isBatchedUpdateNotification = isBatchedUpdateNotification;
14751
- exports.isContinueCheckpointRequest = isContinueCheckpointRequest;
14752
14154
  exports.isDBAdapter = isDBAdapter;
14753
14155
  exports.isPowerSyncDatabaseOptionsWithSettings = isPowerSyncDatabaseOptionsWithSettings;
14754
14156
  exports.isSQLOpenFactory = isSQLOpenFactory;
14755
14157
  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
14158
  exports.parseQuery = parseQuery;
14764
14159
  exports.runOnSchemaChange = runOnSchemaChange;
14765
14160
  exports.sanitizeSQL = sanitizeSQL;