@powersync/common 1.51.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 (91) hide show
  1. package/dist/bundle.cjs +510 -1129
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +511 -1116
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +508 -1129
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +509 -1116
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +73 -433
  10. package/legacy/sync_protocol.d.ts +103 -0
  11. package/lib/client/AbstractPowerSyncDatabase.js +3 -3
  12. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  13. package/lib/client/ConnectionManager.js +1 -1
  14. package/lib/client/ConnectionManager.js.map +1 -1
  15. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +6 -64
  16. package/lib/client/sync/bucket/BucketStorageAdapter.js +4 -0
  17. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -1
  18. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -28
  19. package/lib/client/sync/bucket/SqliteBucketStorage.js +0 -162
  20. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -1
  21. package/lib/client/sync/stream/AbstractRemote.d.ts +29 -18
  22. package/lib/client/sync/stream/AbstractRemote.js +155 -188
  23. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  24. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -35
  25. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +150 -448
  26. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  27. package/lib/client/sync/stream/JsonValue.d.ts +7 -0
  28. package/lib/client/sync/stream/JsonValue.js +2 -0
  29. package/lib/client/sync/stream/JsonValue.js.map +1 -0
  30. package/lib/client/sync/stream/core-instruction.d.ts +14 -9
  31. package/lib/client/sync/stream/core-instruction.js +3 -0
  32. package/lib/client/sync/stream/core-instruction.js.map +1 -1
  33. package/lib/db/DBAdapter.d.ts +9 -0
  34. package/lib/db/DBAdapter.js +8 -1
  35. package/lib/db/DBAdapter.js.map +1 -1
  36. package/lib/db/crud/SyncStatus.d.ts +3 -4
  37. package/lib/db/crud/SyncStatus.js +0 -4
  38. package/lib/db/crud/SyncStatus.js.map +1 -1
  39. package/lib/db/schema/RawTable.d.ts +0 -5
  40. package/lib/db/schema/Schema.d.ts +0 -2
  41. package/lib/db/schema/Schema.js +0 -2
  42. package/lib/db/schema/Schema.js.map +1 -1
  43. package/lib/index.d.ts +2 -6
  44. package/lib/index.js +1 -6
  45. package/lib/index.js.map +1 -1
  46. package/lib/utils/async.d.ts +0 -9
  47. package/lib/utils/async.js +0 -9
  48. package/lib/utils/async.js.map +1 -1
  49. package/lib/utils/stream_transform.d.ts +39 -0
  50. package/lib/utils/stream_transform.js +206 -0
  51. package/lib/utils/stream_transform.js.map +1 -0
  52. package/package.json +15 -10
  53. package/src/client/AbstractPowerSyncDatabase.ts +3 -3
  54. package/src/client/ConnectionManager.ts +1 -1
  55. package/src/client/sync/bucket/BucketStorageAdapter.ts +6 -71
  56. package/src/client/sync/bucket/SqliteBucketStorage.ts +1 -197
  57. package/src/client/sync/stream/AbstractRemote.ts +183 -229
  58. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +181 -510
  59. package/src/client/sync/stream/JsonValue.ts +8 -0
  60. package/src/client/sync/stream/core-instruction.ts +15 -5
  61. package/src/db/DBAdapter.ts +20 -2
  62. package/src/db/crud/SyncStatus.ts +4 -5
  63. package/src/db/schema/RawTable.ts +0 -5
  64. package/src/db/schema/Schema.ts +0 -2
  65. package/src/index.ts +2 -6
  66. package/src/utils/async.ts +0 -11
  67. package/src/utils/stream_transform.ts +252 -0
  68. package/lib/client/sync/bucket/OpType.d.ts +0 -16
  69. package/lib/client/sync/bucket/OpType.js +0 -23
  70. package/lib/client/sync/bucket/OpType.js.map +0 -1
  71. package/lib/client/sync/bucket/OplogEntry.d.ts +0 -23
  72. package/lib/client/sync/bucket/OplogEntry.js +0 -36
  73. package/lib/client/sync/bucket/OplogEntry.js.map +0 -1
  74. package/lib/client/sync/bucket/SyncDataBatch.d.ts +0 -6
  75. package/lib/client/sync/bucket/SyncDataBatch.js +0 -12
  76. package/lib/client/sync/bucket/SyncDataBatch.js.map +0 -1
  77. package/lib/client/sync/bucket/SyncDataBucket.d.ts +0 -40
  78. package/lib/client/sync/bucket/SyncDataBucket.js +0 -40
  79. package/lib/client/sync/bucket/SyncDataBucket.js.map +0 -1
  80. package/lib/client/sync/stream/streaming-sync-types.d.ts +0 -143
  81. package/lib/client/sync/stream/streaming-sync-types.js +0 -26
  82. package/lib/client/sync/stream/streaming-sync-types.js.map +0 -1
  83. package/lib/utils/DataStream.d.ts +0 -62
  84. package/lib/utils/DataStream.js +0 -169
  85. package/lib/utils/DataStream.js.map +0 -1
  86. package/src/client/sync/bucket/OpType.ts +0 -23
  87. package/src/client/sync/bucket/OplogEntry.ts +0 -50
  88. package/src/client/sync/bucket/SyncDataBatch.ts +0 -11
  89. package/src/client/sync/bucket/SyncDataBucket.ts +0 -49
  90. package/src/client/sync/stream/streaming-sync-types.ts +0 -210
  91. package/src/utils/DataStream.ts +0 -222
package/dist/bundle.cjs CHANGED
@@ -1398,6 +1398,8 @@ exports.EncodingType = void 0;
1398
1398
  EncodingType["Base64"] = "base64";
1399
1399
  })(exports.EncodingType || (exports.EncodingType = {}));
1400
1400
 
1401
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
1402
+
1401
1403
  function getDefaultExportFromCjs (x) {
1402
1404
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
1403
1405
  }
@@ -1478,7 +1480,7 @@ function requireEventIterator () {
1478
1480
  this.removeCallback();
1479
1481
  });
1480
1482
  }
1481
- [Symbol.asyncIterator]() {
1483
+ [symbolAsyncIterator]() {
1482
1484
  return {
1483
1485
  next: (value) => {
1484
1486
  const result = this.pushQueue.shift();
@@ -1525,7 +1527,7 @@ function requireEventIterator () {
1525
1527
  queue.eventHandlers[event] = fn;
1526
1528
  },
1527
1529
  }) || (() => { });
1528
- this[Symbol.asyncIterator] = () => queue[Symbol.asyncIterator]();
1530
+ this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
1529
1531
  Object.freeze(this);
1530
1532
  }
1531
1533
  }
@@ -1969,8 +1971,15 @@ class BaseTransaction {
1969
1971
  class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
1970
1972
  static async runWith(ctx, fn) {
1971
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';
1972
1981
  try {
1973
- await ctx.execute('BEGIN IMMEDIATE');
1982
+ await ctx.execute(useBeginImmediate ? 'BEGIN IMMEDIATE' : 'BEGIN');
1974
1983
  const result = await fn(tx);
1975
1984
  await tx.commit();
1976
1985
  return result;
@@ -2129,16 +2138,12 @@ class SyncStatus {
2129
2138
  *
2130
2139
  * This returns null when the database is currently being opened and we don't have reliable information about all
2131
2140
  * included streams yet.
2132
- *
2133
- * @experimental Sync streams are currently in alpha.
2134
2141
  */
2135
2142
  get syncStreams() {
2136
2143
  return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core));
2137
2144
  }
2138
2145
  /**
2139
2146
  * If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
2140
- *
2141
- * @experimental Sync streams are currently in alpha.
2142
2147
  */
2143
2148
  forStream(stream) {
2144
2149
  const asJson = JSON.stringify(stream.parameters);
@@ -2407,15 +2412,6 @@ class ControlledExecutor {
2407
2412
  }
2408
2413
  }
2409
2414
 
2410
- /**
2411
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
2412
- * [recommended polyfill](https://github.com/Azure/azure-sdk-for-js/blob/%40azure/core-asynciterator-polyfill_1.0.2/sdk/core/core-asynciterator-polyfill/src/index.ts#L4-L6)
2413
- * we recommend for React Native.
2414
- *
2415
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
2416
- * iterators without requiring them.
2417
- */
2418
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
2419
2415
  /**
2420
2416
  * Throttle a function to be called at most once every "wait" milliseconds,
2421
2417
  * on the trailing edge.
@@ -2730,7 +2726,7 @@ class SyncStreamSubscriptionHandle {
2730
2726
  constructor(subscription) {
2731
2727
  this.subscription = subscription;
2732
2728
  subscription.refcount++;
2733
- _finalizer?.register(this, subscription);
2729
+ _finalizer?.register(this, subscription, this);
2734
2730
  }
2735
2731
  get name() {
2736
2732
  return this.subscription.name;
@@ -3319,6 +3315,10 @@ exports.PowerSyncControlCommand = void 0;
3319
3315
  PowerSyncControlCommand["NOTIFY_TOKEN_REFRESHED"] = "refreshed_token";
3320
3316
  PowerSyncControlCommand["NOTIFY_CRUD_UPLOAD_COMPLETED"] = "completed_upload";
3321
3317
  PowerSyncControlCommand["UPDATE_SUBSCRIPTIONS"] = "update_subscriptions";
3318
+ /**
3319
+ * An `established` or `end` event for response streams.
3320
+ */
3321
+ PowerSyncControlCommand["CONNECTION_STATE"] = "connection";
3322
3322
  })(exports.PowerSyncControlCommand || (exports.PowerSyncControlCommand = {}));
3323
3323
 
3324
3324
  /**
@@ -3500,103 +3500,6 @@ class AbortOperation extends Error {
3500
3500
  }
3501
3501
  }
3502
3502
 
3503
- exports.OpTypeEnum = void 0;
3504
- (function (OpTypeEnum) {
3505
- OpTypeEnum[OpTypeEnum["CLEAR"] = 1] = "CLEAR";
3506
- OpTypeEnum[OpTypeEnum["MOVE"] = 2] = "MOVE";
3507
- OpTypeEnum[OpTypeEnum["PUT"] = 3] = "PUT";
3508
- OpTypeEnum[OpTypeEnum["REMOVE"] = 4] = "REMOVE";
3509
- })(exports.OpTypeEnum || (exports.OpTypeEnum = {}));
3510
- /**
3511
- * Used internally for sync buckets.
3512
- */
3513
- class OpType {
3514
- value;
3515
- static fromJSON(jsonValue) {
3516
- return new OpType(exports.OpTypeEnum[jsonValue]);
3517
- }
3518
- constructor(value) {
3519
- this.value = value;
3520
- }
3521
- toJSON() {
3522
- return Object.entries(exports.OpTypeEnum).find(([, value]) => value === this.value)[0];
3523
- }
3524
- }
3525
-
3526
- class OplogEntry {
3527
- op_id;
3528
- op;
3529
- checksum;
3530
- subkey;
3531
- object_type;
3532
- object_id;
3533
- data;
3534
- static fromRow(row) {
3535
- return new OplogEntry(row.op_id, OpType.fromJSON(row.op), row.checksum, row.subkey, row.object_type, row.object_id, row.data);
3536
- }
3537
- constructor(op_id, op, checksum, subkey, object_type, object_id, data) {
3538
- this.op_id = op_id;
3539
- this.op = op;
3540
- this.checksum = checksum;
3541
- this.subkey = subkey;
3542
- this.object_type = object_type;
3543
- this.object_id = object_id;
3544
- this.data = data;
3545
- }
3546
- toJSON(fixedKeyEncoding = false) {
3547
- return {
3548
- op_id: this.op_id,
3549
- op: this.op.toJSON(),
3550
- object_type: this.object_type,
3551
- object_id: this.object_id,
3552
- checksum: this.checksum,
3553
- data: this.data,
3554
- // Older versions of the JS SDK used to always JSON.stringify here. That has always been wrong,
3555
- // but we need to migrate gradually to not break existing databases.
3556
- subkey: fixedKeyEncoding ? this.subkey : JSON.stringify(this.subkey)
3557
- };
3558
- }
3559
- }
3560
-
3561
- class SyncDataBucket {
3562
- bucket;
3563
- data;
3564
- has_more;
3565
- after;
3566
- next_after;
3567
- static fromRow(row) {
3568
- return new SyncDataBucket(row.bucket, row.data.map((entry) => OplogEntry.fromRow(entry)), row.has_more ?? false, row.after, row.next_after);
3569
- }
3570
- constructor(bucket, data,
3571
- /**
3572
- * True if the response does not contain all the data for this bucket, and another request must be made.
3573
- */
3574
- has_more,
3575
- /**
3576
- * The `after` specified in the request.
3577
- */
3578
- after,
3579
- /**
3580
- * Use this for the next request.
3581
- */
3582
- next_after) {
3583
- this.bucket = bucket;
3584
- this.data = data;
3585
- this.has_more = has_more;
3586
- this.after = after;
3587
- this.next_after = next_after;
3588
- }
3589
- toJSON(fixedKeyEncoding = false) {
3590
- return {
3591
- bucket: this.bucket,
3592
- has_more: this.has_more,
3593
- after: this.after,
3594
- next_after: this.next_after,
3595
- data: this.data.map((entry) => entry.toJSON(fixedKeyEncoding))
3596
- };
3597
- }
3598
- }
3599
-
3600
3503
  var buffer = {};
3601
3504
 
3602
3505
  var base64Js = {};
@@ -10752,177 +10655,10 @@ function requireDist () {
10752
10655
 
10753
10656
  var distExports = requireDist();
10754
10657
 
10755
- var version = "1.51.0";
10658
+ var version = "1.53.0";
10756
10659
  var PACKAGE = {
10757
10660
  version: version};
10758
10661
 
10759
- const DEFAULT_PRESSURE_LIMITS = {
10760
- highWater: 10,
10761
- lowWater: 0
10762
- };
10763
- /**
10764
- * A very basic implementation of a data stream with backpressure support which does not use
10765
- * native JS streams or async iterators.
10766
- * This is handy for environments such as React Native which need polyfills for the above.
10767
- */
10768
- class DataStream extends BaseObserver {
10769
- options;
10770
- dataQueue;
10771
- isClosed;
10772
- processingPromise;
10773
- notifyDataAdded;
10774
- logger;
10775
- mapLine;
10776
- constructor(options) {
10777
- super();
10778
- this.options = options;
10779
- this.processingPromise = null;
10780
- this.isClosed = false;
10781
- this.dataQueue = [];
10782
- this.mapLine = options?.mapLine ?? ((line) => line);
10783
- this.logger = options?.logger ?? Logger.get('DataStream');
10784
- if (options?.closeOnError) {
10785
- const l = this.registerListener({
10786
- error: (ex) => {
10787
- l?.();
10788
- this.close();
10789
- }
10790
- });
10791
- }
10792
- }
10793
- get highWatermark() {
10794
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
10795
- }
10796
- get lowWatermark() {
10797
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
10798
- }
10799
- get closed() {
10800
- return this.isClosed;
10801
- }
10802
- async close() {
10803
- this.isClosed = true;
10804
- await this.processingPromise;
10805
- this.iterateListeners((l) => l.closed?.());
10806
- // Discard any data in the queue
10807
- this.dataQueue = [];
10808
- this.listeners.clear();
10809
- }
10810
- /**
10811
- * Enqueues data for the consumers to read
10812
- */
10813
- enqueueData(data) {
10814
- if (this.isClosed) {
10815
- throw new Error('Cannot enqueue data into closed stream.');
10816
- }
10817
- this.dataQueue.push(data);
10818
- this.notifyDataAdded?.();
10819
- this.processQueue();
10820
- }
10821
- /**
10822
- * Reads data once from the data stream
10823
- * @returns a Data payload or Null if the stream closed.
10824
- */
10825
- async read() {
10826
- if (this.closed) {
10827
- return null;
10828
- }
10829
- // Wait for any pending processing to complete first.
10830
- // This ensures we register our listener before calling processQueue(),
10831
- // avoiding a race where processQueue() sees no reader and returns early.
10832
- if (this.processingPromise) {
10833
- await this.processingPromise;
10834
- }
10835
- // Re-check after await - stream may have closed while we were waiting
10836
- if (this.closed) {
10837
- return null;
10838
- }
10839
- return new Promise((resolve, reject) => {
10840
- const l = this.registerListener({
10841
- data: async (data) => {
10842
- resolve(data);
10843
- // Remove the listener
10844
- l?.();
10845
- },
10846
- closed: () => {
10847
- resolve(null);
10848
- l?.();
10849
- },
10850
- error: (ex) => {
10851
- reject(ex);
10852
- l?.();
10853
- }
10854
- });
10855
- this.processQueue();
10856
- });
10857
- }
10858
- /**
10859
- * Executes a callback for each data item in the stream
10860
- */
10861
- forEach(callback) {
10862
- if (this.dataQueue.length <= this.lowWatermark) {
10863
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
10864
- }
10865
- return this.registerListener({
10866
- data: callback
10867
- });
10868
- }
10869
- processQueue() {
10870
- if (this.processingPromise) {
10871
- return;
10872
- }
10873
- const promise = (this.processingPromise = this._processQueue());
10874
- promise.finally(() => {
10875
- this.processingPromise = null;
10876
- });
10877
- return promise;
10878
- }
10879
- hasDataReader() {
10880
- return Array.from(this.listeners.values()).some((l) => !!l.data);
10881
- }
10882
- async _processQueue() {
10883
- /**
10884
- * Allow listeners to mutate the queue before processing.
10885
- * This allows for operations such as dropping or compressing data
10886
- * on high water or requesting more data on low water.
10887
- */
10888
- if (this.dataQueue.length >= this.highWatermark) {
10889
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
10890
- }
10891
- if (this.isClosed || !this.hasDataReader()) {
10892
- return;
10893
- }
10894
- if (this.dataQueue.length) {
10895
- const data = this.dataQueue.shift();
10896
- const mapped = this.mapLine(data);
10897
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
10898
- }
10899
- if (this.dataQueue.length <= this.lowWatermark) {
10900
- const dataAdded = new Promise((resolve) => {
10901
- this.notifyDataAdded = resolve;
10902
- });
10903
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
10904
- this.notifyDataAdded = null;
10905
- }
10906
- if (this.dataQueue.length > 0) {
10907
- setTimeout(() => this.processQueue());
10908
- }
10909
- }
10910
- async iterateAsyncErrored(cb) {
10911
- // Important: We need to copy the listeners, as calling a listener could result in adding another
10912
- // listener, resulting in infinite loops.
10913
- const listeners = Array.from(this.listeners.values());
10914
- for (let i of listeners) {
10915
- try {
10916
- await cb(i);
10917
- }
10918
- catch (ex) {
10919
- this.logger.error(ex);
10920
- this.iterateListeners((l) => l.error?.(ex));
10921
- }
10922
- }
10923
- }
10924
- }
10925
-
10926
10662
  var WebsocketDuplexConnection = {};
10927
10663
 
10928
10664
  var hasRequiredWebsocketDuplexConnection;
@@ -11085,8 +10821,199 @@ class WebsocketClientTransport {
11085
10821
  }
11086
10822
  }
11087
10823
 
10824
+ const doneResult = { done: true, value: undefined };
10825
+ function valueResult(value) {
10826
+ return { done: false, value };
10827
+ }
10828
+ /**
10829
+ * Expands a source async iterator by allowing to inject events asynchronously.
10830
+ *
10831
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
10832
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
10833
+ *
10834
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
10835
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
10836
+ */
10837
+ function injectable(source) {
10838
+ let sourceIsDone = false;
10839
+ let waiter = undefined; // An active, waiting next() call.
10840
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
10841
+ let pendingSourceEvent = null;
10842
+ let pendingInjectedEvents = [];
10843
+ const consumeWaiter = () => {
10844
+ const pending = waiter;
10845
+ waiter = undefined;
10846
+ return pending;
10847
+ };
10848
+ const fetchFromSource = () => {
10849
+ const resolveWaiter = (propagate) => {
10850
+ const active = consumeWaiter();
10851
+ if (active) {
10852
+ propagate(active);
10853
+ }
10854
+ else {
10855
+ pendingSourceEvent = propagate;
10856
+ }
10857
+ };
10858
+ const nextFromSource = source.next();
10859
+ nextFromSource.then((value) => {
10860
+ sourceIsDone = value.done == true;
10861
+ resolveWaiter((w) => w.resolve(value));
10862
+ }, (error) => {
10863
+ resolveWaiter((w) => w.reject(error));
10864
+ });
10865
+ };
10866
+ return {
10867
+ next: () => {
10868
+ return new Promise((resolve, reject) => {
10869
+ // First priority: Dispatch ready upstream events.
10870
+ if (sourceIsDone) {
10871
+ return resolve(doneResult);
10872
+ }
10873
+ if (pendingSourceEvent) {
10874
+ pendingSourceEvent({ resolve, reject });
10875
+ pendingSourceEvent = null;
10876
+ return;
10877
+ }
10878
+ // Second priority: Dispatch injected events
10879
+ if (pendingInjectedEvents.length) {
10880
+ return resolve(valueResult(pendingInjectedEvents.shift()));
10881
+ }
10882
+ // Nothing pending? Fetch from source
10883
+ waiter = { resolve, reject };
10884
+ return fetchFromSource();
10885
+ });
10886
+ },
10887
+ inject: (event) => {
10888
+ const pending = consumeWaiter();
10889
+ if (pending != null) {
10890
+ pending.resolve(valueResult(event));
10891
+ }
10892
+ else {
10893
+ pendingInjectedEvents.push(event);
10894
+ }
10895
+ }
10896
+ };
10897
+ }
10898
+ /**
10899
+ * Splits a byte stream at line endings, emitting each line as a string.
10900
+ */
10901
+ function extractJsonLines(source, decoder) {
10902
+ let buffer = '';
10903
+ const pendingLines = [];
10904
+ let isFinalEvent = false;
10905
+ return {
10906
+ next: async () => {
10907
+ while (true) {
10908
+ if (isFinalEvent) {
10909
+ return doneResult;
10910
+ }
10911
+ {
10912
+ const first = pendingLines.shift();
10913
+ if (first) {
10914
+ return { done: false, value: first };
10915
+ }
10916
+ }
10917
+ const { done, value } = await source.next();
10918
+ if (done) {
10919
+ const remaining = buffer.trim();
10920
+ if (remaining.length != 0) {
10921
+ isFinalEvent = true;
10922
+ return { done: false, value: remaining };
10923
+ }
10924
+ return doneResult;
10925
+ }
10926
+ const data = decoder.decode(value, { stream: true });
10927
+ buffer += data;
10928
+ const lines = buffer.split('\n');
10929
+ for (let i = 0; i < lines.length - 1; i++) {
10930
+ const l = lines[i].trim();
10931
+ if (l.length > 0) {
10932
+ pendingLines.push(l);
10933
+ }
10934
+ }
10935
+ buffer = lines[lines.length - 1];
10936
+ }
10937
+ }
10938
+ };
10939
+ }
10940
+ /**
10941
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
10942
+ */
10943
+ function extractBsonObjects(source) {
10944
+ // Fully read but not emitted yet.
10945
+ const completedObjects = [];
10946
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
10947
+ let isDone = false;
10948
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
10949
+ let objectBody = null;
10950
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
10951
+ // If we're consuming a document, the bytes remaining.
10952
+ let remainingLength = 4;
10953
+ return {
10954
+ async next() {
10955
+ while (true) {
10956
+ // Before fetching new data from upstream, return completed objects.
10957
+ if (completedObjects.length) {
10958
+ return valueResult(completedObjects.shift());
10959
+ }
10960
+ if (isDone) {
10961
+ return doneResult;
10962
+ }
10963
+ const upstreamEvent = await source.next();
10964
+ if (upstreamEvent.done) {
10965
+ isDone = true;
10966
+ if (objectBody || remainingLength != 4) {
10967
+ throw new Error('illegal end of stream in BSON object');
10968
+ }
10969
+ return doneResult;
10970
+ }
10971
+ const chunk = upstreamEvent.value;
10972
+ for (let i = 0; i < chunk.length;) {
10973
+ const availableInData = chunk.length - i;
10974
+ if (objectBody) {
10975
+ // We're in the middle of reading a BSON document.
10976
+ const bytesToRead = Math.min(availableInData, remainingLength);
10977
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
10978
+ objectBody.set(copySource, objectBody.length - remainingLength);
10979
+ i += bytesToRead;
10980
+ remainingLength -= bytesToRead;
10981
+ if (remainingLength == 0) {
10982
+ completedObjects.push(objectBody);
10983
+ // Prepare to read another document, starting with its length
10984
+ objectBody = null;
10985
+ remainingLength = 4;
10986
+ }
10987
+ }
10988
+ else {
10989
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
10990
+ const bytesToRead = Math.min(availableInData, remainingLength);
10991
+ for (let j = 0; j < bytesToRead; j++) {
10992
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
10993
+ }
10994
+ i += bytesToRead;
10995
+ remainingLength -= bytesToRead;
10996
+ if (remainingLength == 0) {
10997
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
10998
+ // header is included in length.
10999
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
11000
+ remainingLength = length - 4;
11001
+ if (remainingLength < 1) {
11002
+ throw new Error(`invalid length for bson: ${length}`);
11003
+ }
11004
+ objectBody = new Uint8Array(length);
11005
+ new DataView(objectBody.buffer).setInt32(0, length, true);
11006
+ }
11007
+ }
11008
+ }
11009
+ }
11010
+ }
11011
+ };
11012
+ }
11013
+
11088
11014
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
11089
11015
  const POWERSYNC_JS_VERSION = PACKAGE.version;
11016
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
11090
11017
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
11091
11018
  // Keep alive message is sent every period
11092
11019
  const KEEP_ALIVE_MS = 20_000;
@@ -11266,73 +11193,67 @@ class AbstractRemote {
11266
11193
  return new WebSocket(url);
11267
11194
  }
11268
11195
  /**
11269
- * Returns a data stream of sync line data.
11196
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
11270
11197
  *
11271
- * @param map Maps received payload frames to the typed event value.
11272
- * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
11273
- * (required for compatibility with older sync services).
11198
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
11274
11199
  */
11275
- async socketStreamRaw(options, map, bson) {
11200
+ async socketStreamRaw(options) {
11276
11201
  const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
11277
- const mimeType = bson == null ? 'application/json' : 'application/bson';
11202
+ const mimeType = 'application/json';
11278
11203
  function toBuffer(js) {
11279
- let contents;
11280
- if (bson != null) {
11281
- contents = bson.serialize(js);
11282
- }
11283
- else {
11284
- contents = JSON.stringify(js);
11285
- }
11286
- return bufferExports.Buffer.from(contents);
11204
+ return bufferExports.Buffer.from(JSON.stringify(js));
11287
11205
  }
11288
11206
  const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
11289
11207
  const request = await this.buildRequest(path);
11208
+ const url = this.options.socketUrlTransformer(request.url);
11290
11209
  // Add the user agent in the setup payload - we can't set custom
11291
11210
  // headers with websockets on web. The browser userAgent is however added
11292
11211
  // automatically as a header.
11293
11212
  const userAgent = this.getUserAgent();
11294
- const stream = new DataStream({
11295
- logger: this.logger,
11296
- pressure: {
11297
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
11298
- },
11299
- mapLine: map
11300
- });
11213
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
11214
+ // to abort the connection.
11215
+ let pendingSocket = null;
11216
+ let keepAliveTimeout;
11217
+ let rsocket = null;
11218
+ let queue = null;
11219
+ let didClose = false;
11220
+ const abortRequest = () => {
11221
+ if (didClose) {
11222
+ return;
11223
+ }
11224
+ didClose = true;
11225
+ clearTimeout(keepAliveTimeout);
11226
+ if (pendingSocket) {
11227
+ pendingSocket.close();
11228
+ }
11229
+ if (rsocket) {
11230
+ rsocket.close();
11231
+ }
11232
+ if (queue) {
11233
+ queue.stop();
11234
+ }
11235
+ };
11301
11236
  // Handle upstream abort
11302
- if (options.abortSignal?.aborted) {
11237
+ if (options.abortSignal.aborted) {
11303
11238
  throw new AbortOperation('Connection request aborted');
11304
11239
  }
11305
11240
  else {
11306
- options.abortSignal?.addEventListener('abort', () => {
11307
- stream.close();
11308
- }, { once: true });
11241
+ options.abortSignal.addEventListener('abort', abortRequest);
11309
11242
  }
11310
- let keepAliveTimeout;
11311
11243
  const resetTimeout = () => {
11312
11244
  clearTimeout(keepAliveTimeout);
11313
11245
  keepAliveTimeout = setTimeout(() => {
11314
11246
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
11315
- stream.close();
11247
+ abortRequest();
11316
11248
  }, SOCKET_TIMEOUT_MS);
11317
11249
  };
11318
11250
  resetTimeout();
11319
- // Typescript complains about this being `never` if it's not assigned here.
11320
- // This is assigned in `wsCreator`.
11321
- let disposeSocketConnectionTimeout = () => { };
11322
- const url = this.options.socketUrlTransformer(request.url);
11323
11251
  const connector = new distExports.RSocketConnector({
11324
11252
  transport: new WebsocketClientTransport({
11325
11253
  url,
11326
11254
  wsCreator: (url) => {
11327
- const socket = this.createSocket(url);
11328
- disposeSocketConnectionTimeout = stream.registerListener({
11329
- closed: () => {
11330
- // Allow closing the underlying WebSocket if the stream was closed before the
11331
- // RSocket connect completed. This should effectively abort the request.
11332
- socket.close();
11333
- }
11334
- });
11335
- socket.addEventListener('message', (event) => {
11255
+ const socket = (pendingSocket = this.createSocket(url));
11256
+ socket.addEventListener('message', () => {
11336
11257
  resetTimeout();
11337
11258
  });
11338
11259
  return socket;
@@ -11352,43 +11273,40 @@ class AbstractRemote {
11352
11273
  }
11353
11274
  }
11354
11275
  });
11355
- let rsocket;
11356
11276
  try {
11357
11277
  rsocket = await connector.connect();
11358
11278
  // The connection is established, we no longer need to monitor the initial timeout
11359
- disposeSocketConnectionTimeout();
11279
+ pendingSocket = null;
11360
11280
  }
11361
11281
  catch (ex) {
11362
11282
  this.logger.error(`Failed to connect WebSocket`, ex);
11363
- clearTimeout(keepAliveTimeout);
11364
- if (!stream.closed) {
11365
- await stream.close();
11366
- }
11283
+ abortRequest();
11367
11284
  throw ex;
11368
11285
  }
11369
11286
  resetTimeout();
11370
- let socketIsClosed = false;
11371
- const closeSocket = () => {
11372
- clearTimeout(keepAliveTimeout);
11373
- if (socketIsClosed) {
11374
- return;
11375
- }
11376
- socketIsClosed = true;
11377
- rsocket.close();
11378
- };
11379
11287
  // Helps to prevent double close scenarios
11380
- rsocket.onClose(() => (socketIsClosed = true));
11381
- // We initially request this amount and expect these to arrive eventually
11382
- let pendingEventsCount = syncQueueRequestSize;
11383
- const disposeClosedListener = stream.registerListener({
11384
- closed: () => {
11385
- closeSocket();
11386
- disposeClosedListener();
11387
- }
11388
- });
11389
- const socket = await new Promise((resolve, reject) => {
11288
+ rsocket.onClose(() => (rsocket = null));
11289
+ return await new Promise((resolve, reject) => {
11390
11290
  let connectionEstablished = false;
11391
- const res = rsocket.requestStream({
11291
+ let pendingEventsCount = syncQueueRequestSize;
11292
+ let paused = false;
11293
+ let res = null;
11294
+ function requestMore() {
11295
+ const delta = syncQueueRequestSize - pendingEventsCount;
11296
+ if (!paused && delta > 0) {
11297
+ res?.request(delta);
11298
+ pendingEventsCount = syncQueueRequestSize;
11299
+ }
11300
+ }
11301
+ const events = new domExports.EventIterator((q) => {
11302
+ queue = q;
11303
+ q.on('highWater', () => (paused = true));
11304
+ q.on('lowWater', () => {
11305
+ paused = false;
11306
+ requestMore();
11307
+ });
11308
+ }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[symbolAsyncIterator]();
11309
+ res = rsocket.requestStream({
11392
11310
  data: toBuffer(options.data),
11393
11311
  metadata: toBuffer({
11394
11312
  path
@@ -11413,7 +11331,7 @@ class AbstractRemote {
11413
11331
  }
11414
11332
  // RSocket will close the RSocket stream automatically
11415
11333
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
11416
- stream.close();
11334
+ abortRequest();
11417
11335
  // Handles cases where the connection failed e.g. auth error or connection error
11418
11336
  if (!connectionEstablished) {
11419
11337
  reject(e);
@@ -11423,41 +11341,40 @@ class AbstractRemote {
11423
11341
  // The connection is active
11424
11342
  if (!connectionEstablished) {
11425
11343
  connectionEstablished = true;
11426
- resolve(res);
11344
+ resolve(events);
11427
11345
  }
11428
11346
  const { data } = payload;
11347
+ if (data) {
11348
+ queue.push(data);
11349
+ }
11429
11350
  // Less events are now pending
11430
11351
  pendingEventsCount--;
11431
- if (!data) {
11432
- return;
11433
- }
11434
- stream.enqueueData(data);
11352
+ // Request another event (unless the downstream consumer is paused).
11353
+ requestMore();
11435
11354
  },
11436
11355
  onComplete: () => {
11437
- stream.close();
11356
+ abortRequest(); // this will also emit a done event
11438
11357
  },
11439
11358
  onExtension: () => { }
11440
11359
  });
11441
11360
  });
11442
- const l = stream.registerListener({
11443
- lowWater: async () => {
11444
- // Request to fill up the queue
11445
- const required = syncQueueRequestSize - pendingEventsCount;
11446
- if (required > 0) {
11447
- socket.request(syncQueueRequestSize - pendingEventsCount);
11448
- pendingEventsCount = syncQueueRequestSize;
11449
- }
11450
- },
11451
- closed: () => {
11452
- l();
11453
- }
11454
- });
11455
- return stream;
11456
11361
  }
11457
11362
  /**
11458
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
11363
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
11364
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
11365
+ *
11366
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
11367
+ */
11368
+ get supportsStreamingBinaryResponses() {
11369
+ return true;
11370
+ }
11371
+ /**
11372
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
11373
+ * async iterator of byte blobs.
11374
+ *
11375
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
11459
11376
  */
11460
- async postStreamRaw(options, mapLine) {
11377
+ async fetchStreamRaw(options) {
11461
11378
  const { data, path, headers, abortSignal } = options;
11462
11379
  const request = await this.buildRequest(path);
11463
11380
  /**
@@ -11469,119 +11386,94 @@ class AbstractRemote {
11469
11386
  * Aborting the active fetch request while it is being consumed seems to throw
11470
11387
  * an unhandled exception on the window level.
11471
11388
  */
11472
- if (abortSignal?.aborted) {
11473
- throw new AbortOperation('Abort request received before making postStreamRaw request');
11389
+ if (abortSignal.aborted) {
11390
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
11474
11391
  }
11475
11392
  const controller = new AbortController();
11476
- let requestResolved = false;
11477
- abortSignal?.addEventListener('abort', () => {
11478
- if (!requestResolved) {
11393
+ let reader = null;
11394
+ abortSignal.addEventListener('abort', () => {
11395
+ const reason = abortSignal.reason ??
11396
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
11397
+ if (reader == null) {
11479
11398
  // Only abort via the abort controller if the request has not resolved yet
11480
- controller.abort(abortSignal.reason ??
11481
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.'));
11399
+ controller.abort(reason);
11400
+ }
11401
+ else {
11402
+ reader.cancel(reason).catch(() => {
11403
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
11404
+ // ignore it here.
11405
+ });
11482
11406
  }
11483
11407
  });
11484
- const res = await this.fetch(request.url, {
11485
- method: 'POST',
11486
- headers: { ...headers, ...request.headers },
11487
- body: JSON.stringify(data),
11488
- signal: controller.signal,
11489
- cache: 'no-store',
11490
- ...(this.options.fetchOptions ?? {}),
11491
- ...options.fetchOptions
11492
- }).catch((ex) => {
11408
+ let res;
11409
+ let responseIsBson = false;
11410
+ try {
11411
+ const ndJson = 'application/x-ndjson';
11412
+ const bson = 'application/vnd.powersync.bson-stream';
11413
+ res = await this.fetch(request.url, {
11414
+ method: 'POST',
11415
+ headers: {
11416
+ ...headers,
11417
+ ...request.headers,
11418
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
11419
+ },
11420
+ body: JSON.stringify(data),
11421
+ signal: controller.signal,
11422
+ cache: 'no-store',
11423
+ ...(this.options.fetchOptions ?? {}),
11424
+ ...options.fetchOptions
11425
+ });
11426
+ if (!res.ok || !res.body) {
11427
+ const text = await res.text();
11428
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
11429
+ const error = new Error(`HTTP ${res.statusText}: ${text}`);
11430
+ error.status = res.status;
11431
+ throw error;
11432
+ }
11433
+ const contentType = res.headers.get('content-type');
11434
+ responseIsBson = contentType == bson;
11435
+ }
11436
+ catch (ex) {
11493
11437
  if (ex.name == 'AbortError') {
11494
11438
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
11495
11439
  }
11496
11440
  throw ex;
11497
- });
11498
- if (!res) {
11499
- throw new Error('Fetch request was aborted');
11500
- }
11501
- requestResolved = true;
11502
- if (!res.ok || !res.body) {
11503
- const text = await res.text();
11504
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
11505
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
11506
- error.status = res.status;
11507
- throw error;
11508
11441
  }
11509
- // Create a new stream splitting the response at line endings while also handling cancellations
11510
- // by closing the reader.
11511
- const reader = res.body.getReader();
11512
- let readerReleased = false;
11513
- // This will close the network request and read stream
11514
- const closeReader = async () => {
11515
- try {
11516
- readerReleased = true;
11517
- await reader.cancel();
11518
- }
11519
- catch (ex) {
11520
- // an error will throw if the reader hasn't been used yet
11521
- }
11522
- reader.releaseLock();
11523
- };
11524
- const stream = new DataStream({
11525
- logger: this.logger,
11526
- mapLine: mapLine,
11527
- pressure: {
11528
- highWaterMark: 20,
11529
- lowWaterMark: 10
11530
- }
11531
- });
11532
- abortSignal?.addEventListener('abort', () => {
11533
- closeReader();
11534
- stream.close();
11535
- });
11536
- const decoder = this.createTextDecoder();
11537
- let buffer = '';
11538
- const consumeStream = async () => {
11539
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
11540
- const { done, value } = await reader.read();
11541
- if (done) {
11542
- const remaining = buffer.trim();
11543
- if (remaining.length != 0) {
11544
- stream.enqueueData(remaining);
11545
- }
11546
- stream.close();
11547
- await closeReader();
11548
- return;
11442
+ reader = res.body.getReader();
11443
+ const stream = {
11444
+ next: async () => {
11445
+ if (controller.signal.aborted) {
11446
+ return doneResult;
11549
11447
  }
11550
- const data = decoder.decode(value, { stream: true });
11551
- buffer += data;
11552
- const lines = buffer.split('\n');
11553
- for (var i = 0; i < lines.length - 1; i++) {
11554
- var l = lines[i].trim();
11555
- if (l.length > 0) {
11556
- stream.enqueueData(l);
11557
- }
11448
+ try {
11449
+ return await reader.read();
11558
11450
  }
11559
- buffer = lines[lines.length - 1];
11560
- // Implement backpressure by waiting for the low water mark to be reached
11561
- if (stream.dataQueue.length > stream.highWatermark) {
11562
- await new Promise((resolve) => {
11563
- const dispose = stream.registerListener({
11564
- lowWater: async () => {
11565
- resolve();
11566
- dispose();
11567
- },
11568
- closed: () => {
11569
- resolve();
11570
- dispose();
11571
- }
11572
- });
11573
- });
11451
+ catch (ex) {
11452
+ if (controller.signal.aborted) {
11453
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
11454
+ // things working as intended, we can return a done event and consider the exception handled.
11455
+ return doneResult;
11456
+ }
11457
+ throw ex;
11574
11458
  }
11575
11459
  }
11576
11460
  };
11577
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
11578
- const l = stream.registerListener({
11579
- closed: () => {
11580
- closeReader();
11581
- l?.();
11582
- }
11583
- });
11584
- return stream;
11461
+ return { isBson: responseIsBson, stream };
11462
+ }
11463
+ /**
11464
+ * Posts a `/sync/stream` request.
11465
+ *
11466
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
11467
+ * {@link Uint8Array}s.
11468
+ */
11469
+ async fetchStream(options) {
11470
+ const { isBson, stream } = await this.fetchStreamRaw(options);
11471
+ if (isBson) {
11472
+ return extractBsonObjects(stream);
11473
+ }
11474
+ else {
11475
+ return extractJsonLines(stream, this.createTextDecoder());
11476
+ }
11585
11477
  }
11586
11478
  }
11587
11479
 
@@ -11610,31 +11502,8 @@ function coreStatusToJs(status) {
11610
11502
  priorityStatusEntries: status.priority_status.map(priorityToJs)
11611
11503
  };
11612
11504
  }
11613
-
11614
- function isStreamingSyncData(line) {
11615
- return line.data != null;
11616
- }
11617
- function isStreamingKeepalive(line) {
11618
- return line.token_expires_in != null;
11619
- }
11620
- function isStreamingSyncCheckpoint(line) {
11621
- return line.checkpoint != null;
11622
- }
11623
- function isStreamingSyncCheckpointComplete(line) {
11624
- return line.checkpoint_complete != null;
11625
- }
11626
- function isStreamingSyncCheckpointPartiallyComplete(line) {
11627
- return line.partial_checkpoint_complete != null;
11628
- }
11629
- function isStreamingSyncCheckpointDiff(line) {
11630
- return line.checkpoint_diff != null;
11631
- }
11632
- function isContinueCheckpointRequest(request) {
11633
- return (Array.isArray(request.buckets) &&
11634
- typeof request.checkpoint_token == 'string');
11635
- }
11636
- function isSyncNewCheckpointRequest(request) {
11637
- return typeof request.request_checkpoint == 'object';
11505
+ function isInterruptingInstruction(instruction) {
11506
+ return 'EstablishSyncStream' in instruction || 'CloseSyncStream' in instruction;
11638
11507
  }
11639
11508
 
11640
11509
  exports.LockType = void 0;
@@ -11649,35 +11518,21 @@ exports.SyncStreamConnectionMethod = void 0;
11649
11518
  })(exports.SyncStreamConnectionMethod || (exports.SyncStreamConnectionMethod = {}));
11650
11519
  exports.SyncClientImplementation = void 0;
11651
11520
  (function (SyncClientImplementation) {
11652
- /**
11653
- * Decodes and handles sync lines received from the sync service in JavaScript.
11654
- *
11655
- * This is the default option.
11656
- *
11657
- * @deprecated We recommend the {@link RUST} client implementation for all apps. If you have issues with
11658
- * the Rust client, please file an issue or reach out to us. The JavaScript client will be removed in a future
11659
- * version of the PowerSync SDK.
11660
- */
11661
- SyncClientImplementation["JAVASCRIPT"] = "js";
11662
11521
  /**
11663
11522
  * This implementation offloads the sync line decoding and handling into the PowerSync
11664
11523
  * core extension.
11665
11524
  *
11666
- * This option is more performant than the {@link JAVASCRIPT} client, enabled by default and the
11667
- * recommended client implementation for all apps.
11525
+ * This is the only option, as an older JavaScript client implementation has been removed from the SDK.
11668
11526
  *
11669
11527
  * ## Compatibility warning
11670
11528
  *
11671
11529
  * The Rust sync client stores sync data in a format that is slightly different than the one used
11672
- * by the old {@link JAVASCRIPT} implementation. When adopting the {@link RUST} client on existing
11673
- * databases, the PowerSync SDK will migrate the format automatically.
11674
- * Further, the {@link JAVASCRIPT} client in recent versions of the PowerSync JS SDK (starting from
11675
- * the version introducing {@link RUST} as an option) also supports the new format, so you can switch
11676
- * 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.
11677
11532
  *
11678
- * __However__: Upgrading the SDK version, then adopting {@link RUST} as a sync client and later
11679
- * downgrading the SDK to an older version (necessarily using the JavaScript-based implementation then)
11680
- * 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.
11681
11536
  */
11682
11537
  SyncClientImplementation["RUST"] = "rust";
11683
11538
  })(exports.SyncClientImplementation || (exports.SyncClientImplementation = {}));
@@ -11700,13 +11555,7 @@ const DEFAULT_STREAM_CONNECTION_OPTIONS = {
11700
11555
  serializedSchema: undefined,
11701
11556
  includeDefaultStreams: true
11702
11557
  };
11703
- // The priority we assume when we receive checkpoint lines where no priority is set.
11704
- // This is the default priority used by the sync service, but can be set to an arbitrary
11705
- // value since sync services without priorities also won't send partial sync completion
11706
- // messages.
11707
- const FALLBACK_PRIORITY = 3;
11708
11558
  class AbstractStreamingSyncImplementation extends BaseObserver {
11709
- _lastSyncedAt;
11710
11559
  options;
11711
11560
  abortController;
11712
11561
  // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
@@ -11716,6 +11565,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11716
11565
  streamingSyncPromise;
11717
11566
  logger;
11718
11567
  activeStreams;
11568
+ connectionMayHaveChanged = false;
11719
11569
  isUploadingCrud = false;
11720
11570
  notifyCompletedUploads;
11721
11571
  handleActiveStreamsChange;
@@ -11795,9 +11645,6 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11795
11645
  this.crudUpdateListener = undefined;
11796
11646
  this.uploadAbortController?.abort();
11797
11647
  }
11798
- async hasCompletedSync() {
11799
- return this.options.adapter.hasCompletedSync();
11800
- }
11801
11648
  async getWriteCheckpoint() {
11802
11649
  const clientId = await this.options.adapter.getClientId();
11803
11650
  let path = `/write-checkpoint2.json?client_id=${clientId}`;
@@ -11879,7 +11726,7 @@ The next upload iteration will be delayed.`);
11879
11726
  });
11880
11727
  }
11881
11728
  }
11882
- this.uploadAbortController = null;
11729
+ this.uploadAbortController = undefined;
11883
11730
  }
11884
11731
  });
11885
11732
  }
@@ -11995,6 +11842,11 @@ The next upload iteration will be delayed.`);
11995
11842
  shouldDelayRetry = false;
11996
11843
  // A disconnect was requested, we should not delay since there is no explicit retry
11997
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
+ }
11998
11850
  else {
11999
11851
  this.logger.error(ex);
12000
11852
  }
@@ -12025,17 +11877,14 @@ The next upload iteration will be delayed.`);
12025
11877
  // Mark as disconnected if here
12026
11878
  this.updateSyncStatus({ connected: false, connecting: false });
12027
11879
  }
12028
- async collectLocalBucketState() {
12029
- const bucketEntries = await this.options.adapter.getBucketStates();
12030
- const req = bucketEntries.map((entry) => ({
12031
- name: entry.bucket,
12032
- after: entry.op_id
12033
- }));
12034
- const localDescriptions = new Map();
12035
- for (const entry of bucketEntries) {
12036
- localDescriptions.set(entry.bucket, null);
12037
- }
12038
- 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?.();
12039
11888
  }
12040
11889
  /**
12041
11890
  * Older versions of the JS SDK used to encode subkeys as JSON in {@link OplogEntry.toJSON}.
@@ -12076,344 +11925,98 @@ The next upload iteration will be delayed.`);
12076
11925
  if (invalidMetadata.length > 0) {
12077
11926
  throw new Error(`Invalid appMetadata provided. Only string values are allowed. Invalid values: ${invalidMetadata.map(([key, value]) => `${key}: ${value}`).join(', ')}`);
12078
11927
  }
12079
- const clientImplementation = resolvedOptions.clientImplementation;
12080
- this.updateSyncStatus({ clientImplementation });
12081
- if (clientImplementation == exports.SyncClientImplementation.JAVASCRIPT) {
12082
- await this.legacyStreamingSyncIteration(signal, resolvedOptions);
12083
- return null;
12084
- }
12085
- else {
12086
- await this.requireKeyFormat(true);
12087
- return await this.rustSyncIteration(signal, resolvedOptions);
12088
- }
11928
+ await this.requireKeyFormat(true);
11929
+ return await this.rustSyncIteration(signal, resolvedOptions);
12089
11930
  }
12090
11931
  });
12091
11932
  }
12092
- async legacyStreamingSyncIteration(signal, resolvedOptions) {
12093
- const rawTables = resolvedOptions.serializedSchema?.raw_tables;
12094
- if (rawTables != null && rawTables.length) {
12095
- this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
12096
- }
12097
- if (this.activeStreams.length) {
12098
- this.logger.error('Sync streams require `clientImplementation: SyncClientImplementation.RUST` when connecting.');
12099
- }
12100
- this.logger.debug('Streaming sync iteration started');
12101
- this.options.adapter.startSession();
12102
- let [req, bucketMap] = await this.collectLocalBucketState();
12103
- let targetCheckpoint = null;
12104
- // A checkpoint that has been validated but not applied (e.g. due to pending local writes)
12105
- let pendingValidatedCheckpoint = null;
12106
- const clientId = await this.options.adapter.getClientId();
12107
- const usingFixedKeyFormat = await this.requireKeyFormat(false);
12108
- this.logger.debug('Requesting stream from server');
12109
- const syncOptions = {
12110
- path: '/sync/stream',
12111
- abortSignal: signal,
12112
- data: {
12113
- buckets: req,
12114
- include_checksum: true,
12115
- raw_data: true,
12116
- parameters: resolvedOptions.params,
12117
- app_metadata: resolvedOptions.appMetadata,
12118
- client_id: clientId
11933
+ receiveSyncLines(data) {
11934
+ const { options, connection } = data;
11935
+ const remote = this.options.remote;
11936
+ const openInner = async () => {
11937
+ if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
11938
+ return await remote.fetchStream(options);
12119
11939
  }
12120
- };
12121
- let stream;
12122
- if (resolvedOptions?.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
12123
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
12124
- if (typeof line == 'string') {
12125
- return JSON.parse(line);
12126
- }
12127
- else {
12128
- // Directly enqueued by us
12129
- return line;
12130
- }
12131
- });
12132
- }
12133
- else {
12134
- const bson = await this.options.remote.getBSON();
12135
- stream = await this.options.remote.socketStreamRaw({
12136
- ...syncOptions,
12137
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
12138
- }, (payload) => {
12139
- if (payload instanceof Uint8Array) {
12140
- return bson.deserialize(payload);
12141
- }
12142
- else {
12143
- // Directly enqueued by us
12144
- return payload;
12145
- }
12146
- }, bson);
12147
- }
12148
- this.logger.debug('Stream established. Processing events');
12149
- this.notifyCompletedUploads = () => {
12150
- if (!stream.closed) {
12151
- stream.enqueueData({ crud_upload_completed: null });
11940
+ else {
11941
+ return await this.options.remote.socketStreamRaw({
11942
+ ...options,
11943
+ ...{ fetchStrategy: connection.fetchStrategy }
11944
+ });
12152
11945
  }
12153
11946
  };
12154
- while (!stream.closed) {
12155
- const line = await stream.read();
12156
- if (!line) {
12157
- // The stream has closed while waiting
12158
- return;
12159
- }
12160
- if ('crud_upload_completed' in line) {
12161
- if (pendingValidatedCheckpoint != null) {
12162
- const { applied, endIteration } = await this.applyCheckpoint(pendingValidatedCheckpoint);
12163
- if (applied) {
12164
- pendingValidatedCheckpoint = null;
12165
- }
12166
- else if (endIteration) {
12167
- break;
12168
- }
11947
+ let inner;
11948
+ let done = false;
11949
+ return {
11950
+ async next() {
11951
+ if (done) {
11952
+ return doneResult;
12169
11953
  }
12170
- continue;
12171
- }
12172
- // A connection is active and messages are being received
12173
- if (!this.syncStatus.connected) {
12174
- // There is a connection now
12175
- Promise.resolve().then(() => this.triggerCrudUpload());
12176
- this.updateSyncStatus({
12177
- connected: true
12178
- });
12179
- }
12180
- if (isStreamingSyncCheckpoint(line)) {
12181
- targetCheckpoint = line.checkpoint;
12182
- // New checkpoint - existing validated checkpoint is no longer valid
12183
- pendingValidatedCheckpoint = null;
12184
- const bucketsToDelete = new Set(bucketMap.keys());
12185
- const newBuckets = new Map();
12186
- for (const checksum of line.checkpoint.buckets) {
12187
- newBuckets.set(checksum.bucket, {
12188
- name: checksum.bucket,
12189
- 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'
12190
11960
  });
12191
- bucketsToDelete.delete(checksum.bucket);
12192
- }
12193
- if (bucketsToDelete.size > 0) {
12194
- this.logger.debug('Removing buckets', [...bucketsToDelete]);
12195
- }
12196
- bucketMap = newBuckets;
12197
- await this.options.adapter.removeBuckets([...bucketsToDelete]);
12198
- await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
12199
- await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
12200
- }
12201
- else if (isStreamingSyncCheckpointComplete(line)) {
12202
- const result = await this.applyCheckpoint(targetCheckpoint);
12203
- if (result.endIteration) {
12204
- return;
12205
- }
12206
- else if (!result.applied) {
12207
- // "Could not apply checkpoint due to local data". We need to retry after
12208
- // finishing uploads.
12209
- pendingValidatedCheckpoint = targetCheckpoint;
12210
- }
12211
- else {
12212
- // Nothing to retry later. This would likely already be null from the last
12213
- // checksum or checksum_diff operation, but we make sure.
12214
- pendingValidatedCheckpoint = null;
12215
11961
  }
12216
- }
12217
- else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
12218
- const priority = line.partial_checkpoint_complete.priority;
12219
- this.logger.debug('Partial checkpoint complete', priority);
12220
- const result = await this.options.adapter.syncLocalDatabase(targetCheckpoint, priority);
12221
- if (!result.checkpointValid) {
12222
- // This means checksums failed. Start again with a new checkpoint.
12223
- // TODO: better back-off
12224
- await new Promise((resolve) => setTimeout(resolve, 50));
12225
- return;
12226
- }
12227
- else if (!result.ready) ;
12228
11962
  else {
12229
- // We'll keep on downloading, but can report that this priority is synced now.
12230
- this.logger.debug('partial checkpoint validation succeeded');
12231
- // All states with a higher priority can be deleted since this partial sync includes them.
12232
- const priorityStates = this.syncStatus.priorityStatusEntries.filter((s) => s.priority <= priority);
12233
- priorityStates.push({
12234
- priority,
12235
- lastSyncedAt: new Date(),
12236
- hasSynced: true
12237
- });
12238
- this.updateSyncStatus({
12239
- connected: true,
12240
- priorityStatusEntries: priorityStates
12241
- });
12242
- }
12243
- }
12244
- else if (isStreamingSyncCheckpointDiff(line)) {
12245
- // TODO: It may be faster to just keep track of the diff, instead of the entire checkpoint
12246
- if (targetCheckpoint == null) {
12247
- throw new Error('Checkpoint diff without previous checkpoint');
12248
- }
12249
- // New checkpoint - existing validated checkpoint is no longer valid
12250
- pendingValidatedCheckpoint = null;
12251
- const diff = line.checkpoint_diff;
12252
- const newBuckets = new Map();
12253
- for (const checksum of targetCheckpoint.buckets) {
12254
- newBuckets.set(checksum.bucket, checksum);
12255
- }
12256
- for (const checksum of diff.updated_buckets) {
12257
- newBuckets.set(checksum.bucket, checksum);
12258
- }
12259
- for (const bucket of diff.removed_buckets) {
12260
- newBuckets.delete(bucket);
12261
- }
12262
- const newCheckpoint = {
12263
- last_op_id: diff.last_op_id,
12264
- buckets: [...newBuckets.values()],
12265
- write_checkpoint: diff.write_checkpoint
12266
- };
12267
- targetCheckpoint = newCheckpoint;
12268
- await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
12269
- bucketMap = new Map();
12270
- newBuckets.forEach((checksum, name) => bucketMap.set(name, {
12271
- name: checksum.bucket,
12272
- priority: checksum.priority ?? FALLBACK_PRIORITY
12273
- }));
12274
- const bucketsToDelete = diff.removed_buckets;
12275
- if (bucketsToDelete.length > 0) {
12276
- this.logger.debug('Remove buckets', bucketsToDelete);
12277
- }
12278
- await this.options.adapter.removeBuckets(bucketsToDelete);
12279
- await this.options.adapter.setTargetCheckpoint(targetCheckpoint);
12280
- }
12281
- else if (isStreamingSyncData(line)) {
12282
- const { data } = line;
12283
- const previousProgress = this.syncStatus.dataFlowStatus.downloadProgress;
12284
- let updatedProgress = null;
12285
- if (previousProgress) {
12286
- updatedProgress = { ...previousProgress };
12287
- const progressForBucket = updatedProgress[data.bucket];
12288
- if (progressForBucket) {
12289
- updatedProgress[data.bucket] = {
12290
- ...progressForBucket,
12291
- since_last: progressForBucket.since_last + data.data.length
12292
- };
11963
+ const event = await inner.next();
11964
+ if (event.done) {
11965
+ done = true;
11966
+ return valueResult({ command: exports.PowerSyncControlCommand.CONNECTION_STATE, payload: 'end' });
12293
11967
  }
12294
- }
12295
- this.updateSyncStatus({
12296
- dataFlow: {
12297
- downloading: true,
12298
- 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
+ });
12299
11975
  }
12300
- });
12301
- await this.options.adapter.saveSyncData({ buckets: [SyncDataBucket.fromRow(data)] }, usingFixedKeyFormat);
12302
- }
12303
- else if (isStreamingKeepalive(line)) {
12304
- const remaining_seconds = line.token_expires_in;
12305
- if (remaining_seconds == 0) {
12306
- // Connection would be closed automatically right after this
12307
- this.logger.debug('Token expiring; reconnect');
12308
- /**
12309
- * For a rare case where the backend connector does not update the token
12310
- * (uses the same one), this should have some delay.
12311
- */
12312
- await this.delayRetry();
12313
- return;
12314
- }
12315
- else if (remaining_seconds < 30) {
12316
- this.logger.debug('Token will expire soon; reconnect');
12317
- // Pre-emptively refresh the token
12318
- this.options.remote.invalidateCredentials();
12319
- return;
12320
11976
  }
12321
- this.triggerCrudUpload();
12322
11977
  }
12323
- else {
12324
- this.logger.debug('Received unknown sync line', line);
12325
- }
12326
- }
12327
- this.logger.debug('Stream input empty');
12328
- // Connection closed. Likely due to auth issue.
12329
- return;
11978
+ };
12330
11979
  }
12331
11980
  async rustSyncIteration(signal, resolvedOptions) {
12332
11981
  const syncImplementation = this;
12333
11982
  const adapter = this.options.adapter;
12334
11983
  const remote = this.options.remote;
12335
- let receivingLines = null;
12336
- let hadSyncLine = false;
12337
11984
  let hideDisconnectOnRestart = false;
11985
+ let notifyTokenRefreshed;
12338
11986
  if (signal.aborted) {
12339
11987
  throw new AbortOperation('Connection request has been aborted');
12340
11988
  }
12341
- const abortController = new AbortController();
12342
- signal.addEventListener('abort', () => abortController.abort());
12343
- // Pending sync lines received from the service, as well as local events that trigger a powersync_control
12344
- // invocation (local events include refreshed tokens and completed uploads).
12345
- // This is a single data stream so that we can handle all control calls from a single place.
12346
- let controlInvocations = null;
12347
- async function connect(instr) {
12348
- const syncOptions = {
12349
- path: '/sync/stream',
12350
- abortSignal: abortController.signal,
12351
- 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
12352
11995
  };
12353
- if (resolvedOptions.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
12354
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
12355
- if (typeof line == 'string') {
12356
- return {
12357
- command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
12358
- payload: line
12359
- };
12360
- }
12361
- else {
12362
- // Directly enqueued by us
12363
- return line;
12364
- }
12365
- });
12366
- }
12367
- else {
12368
- controlInvocations = await remote.socketStreamRaw({
12369
- ...syncOptions,
12370
- fetchStrategy: resolvedOptions.fetchStrategy
12371
- }, (payload) => {
12372
- if (payload instanceof Uint8Array) {
12373
- return {
12374
- command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
12375
- payload: payload
12376
- };
12377
- }
12378
- else {
12379
- // Directly enqueued by us
12380
- return payload;
12381
- }
12382
- });
12383
- }
12384
- // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
12385
- // we're already connected here and can report that.
12386
- syncImplementation.updateSyncStatus({ connected: true });
12387
- try {
12388
- while (!controlInvocations.closed) {
12389
- const line = await controlInvocations.read();
12390
- if (line == null) {
12391
- return;
12392
- }
12393
- await control(line.command, line.payload);
12394
- if (!hadSyncLine) {
12395
- syncImplementation.triggerCrudUpload();
12396
- hadSyncLine = true;
12397
- }
12398
- }
12399
- }
12400
- finally {
12401
- const activeInstructions = controlInvocations;
12402
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
12403
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
12404
- // to null.
12405
- controlInvocations = null;
12406
- await activeInstructions.close();
11996
+ if (resolvedOptions.serializedSchema) {
11997
+ options.schema = resolvedOptions.serializedSchema;
12407
11998
  }
11999
+ return invokePowerSyncControl(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12408
12000
  }
12409
12001
  async function stop() {
12410
- 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
+ }
12411
12010
  }
12412
- async function control(op, payload) {
12011
+ async function invokePowerSyncControl(op, payload) {
12413
12012
  const rawResponse = await adapter.control(op, payload ?? null);
12414
12013
  const logger = syncImplementation.logger;
12415
12014
  logger.trace('powersync_control', op, payload == null || typeof payload == 'string' ? payload : '<bytes>', rawResponse);
12416
- 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);
12417
12020
  }
12418
12021
  async function handleInstruction(instruction) {
12419
12022
  if ('LogLine' in instruction) {
@@ -12432,13 +12035,6 @@ The next upload iteration will be delayed.`);
12432
12035
  else if ('UpdateSyncStatus' in instruction) {
12433
12036
  syncImplementation.updateSyncStatus(coreStatusToJs(instruction.UpdateSyncStatus.status));
12434
12037
  }
12435
- else if ('EstablishSyncStream' in instruction) {
12436
- if (receivingLines != null) {
12437
- // Already connected, this shouldn't happen during a single iteration.
12438
- throw 'Unexpected request to establish sync stream, already connected';
12439
- }
12440
- receivingLines = connect(instruction.EstablishSyncStream);
12441
- }
12442
12038
  else if ('FetchCredentials' in instruction) {
12443
12039
  if (instruction.FetchCredentials.did_expire) {
12444
12040
  remote.invalidateCredentials();
@@ -12447,16 +12043,12 @@ The next upload iteration will be delayed.`);
12447
12043
  remote.invalidateCredentials();
12448
12044
  // Restart iteration after the credentials have been refreshed.
12449
12045
  remote.fetchCredentials().then((_) => {
12450
- controlInvocations?.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12046
+ notifyTokenRefreshed?.();
12451
12047
  }, (err) => {
12452
12048
  syncImplementation.logger.warn('Could not prefetch credentials', err);
12453
12049
  });
12454
12050
  }
12455
12051
  }
12456
- else if ('CloseSyncStream' in instruction) {
12457
- abortController.abort();
12458
- hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12459
- }
12460
12052
  else if ('FlushFileSystem' in instruction) ;
12461
12053
  else if ('DidCompleteSync' in instruction) {
12462
12054
  syncImplementation.updateSyncStatus({
@@ -12466,105 +12058,83 @@ The next upload iteration will be delayed.`);
12466
12058
  });
12467
12059
  }
12468
12060
  }
12469
- async function handleInstructions(instructions) {
12470
- for (const instr of instructions) {
12471
- await handleInstruction(instr);
12472
- }
12473
- }
12474
12061
  try {
12475
- const options = {
12476
- parameters: resolvedOptions.params,
12477
- app_metadata: resolvedOptions.appMetadata,
12478
- active_streams: this.activeStreams,
12479
- include_defaults: resolvedOptions.includeDefaultStreams
12480
- };
12481
- if (resolvedOptions.serializedSchema) {
12482
- 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
+ }
12483
12085
  }
12484
- await control(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12086
+ if (controlInvocations == null)
12087
+ return defaultResult;
12485
12088
  this.notifyCompletedUploads = () => {
12486
- if (controlInvocations && !controlInvocations?.closed) {
12487
- controlInvocations.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12488
- }
12089
+ controlInvocations.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12489
12090
  };
12490
12091
  this.handleActiveStreamsChange = () => {
12491
- if (controlInvocations && !controlInvocations?.closed) {
12492
- controlInvocations.enqueueData({
12493
- command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12494
- payload: JSON.stringify(this.activeStreams)
12495
- });
12496
- }
12092
+ controlInvocations.inject({
12093
+ command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12094
+ payload: JSON.stringify(this.activeStreams)
12095
+ });
12497
12096
  };
12498
- 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
+ }
12499
12130
  }
12500
12131
  finally {
12501
12132
  this.notifyCompletedUploads = this.handleActiveStreamsChange = undefined;
12133
+ notifyTokenRefreshed = undefined;
12502
12134
  await stop();
12503
12135
  }
12504
12136
  return { immediateRestart: hideDisconnectOnRestart };
12505
12137
  }
12506
- async updateSyncStatusForStartingCheckpoint(checkpoint) {
12507
- const localProgress = await this.options.adapter.getBucketOperationProgress();
12508
- const progress = {};
12509
- let invalidated = false;
12510
- for (const bucket of checkpoint.buckets) {
12511
- const savedProgress = localProgress[bucket.bucket];
12512
- const atLast = savedProgress?.atLast ?? 0;
12513
- const sinceLast = savedProgress?.sinceLast ?? 0;
12514
- progress[bucket.bucket] = {
12515
- // The fallback priority doesn't matter here, but 3 is the one newer versions of the sync service
12516
- // will use by default.
12517
- priority: bucket.priority ?? 3,
12518
- at_last: atLast,
12519
- since_last: sinceLast,
12520
- target_count: bucket.count ?? 0
12521
- };
12522
- if (bucket.count != null && bucket.count < atLast + sinceLast) {
12523
- // Either due to a defrag / sync rule deploy or a compaction operation, the size
12524
- // of the bucket shrank so much that the local ops exceed the ops in the updated
12525
- // bucket. We can't prossibly report progress in this case (it would overshoot 100%).
12526
- invalidated = true;
12527
- }
12528
- }
12529
- if (invalidated) {
12530
- for (const bucket in progress) {
12531
- const bucketProgress = progress[bucket];
12532
- bucketProgress.at_last = 0;
12533
- bucketProgress.since_last = 0;
12534
- }
12535
- }
12536
- this.updateSyncStatus({
12537
- dataFlow: {
12538
- downloading: true,
12539
- downloadProgress: progress
12540
- }
12541
- });
12542
- }
12543
- async applyCheckpoint(checkpoint) {
12544
- let result = await this.options.adapter.syncLocalDatabase(checkpoint);
12545
- if (!result.checkpointValid) {
12546
- this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
12547
- // This means checksums failed. Start again with a new checkpoint.
12548
- // TODO: better back-off
12549
- await new Promise((resolve) => setTimeout(resolve, 50));
12550
- return { applied: false, endIteration: true };
12551
- }
12552
- else if (!result.ready) {
12553
- 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.`);
12554
- return { applied: false, endIteration: false };
12555
- }
12556
- this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
12557
- this.updateSyncStatus({
12558
- connected: true,
12559
- lastSyncedAt: new Date(),
12560
- dataFlow: {
12561
- downloading: false,
12562
- downloadProgress: null,
12563
- downloadError: undefined
12564
- }
12565
- });
12566
- return { applied: true, endIteration: false };
12567
- }
12568
12138
  updateSyncStatus(options) {
12569
12139
  const updatedStatus = new SyncStatus({
12570
12140
  connected: options.connected ?? this.syncStatus.connected,
@@ -14103,14 +13673,12 @@ class SqliteBucketStorage extends BaseObserver {
14103
13673
  db;
14104
13674
  logger;
14105
13675
  tableNames;
14106
- _hasCompletedSync;
14107
13676
  updateListener;
14108
13677
  _clientId;
14109
13678
  constructor(db, logger = Logger.get('SqliteBucketStorage')) {
14110
13679
  super();
14111
13680
  this.db = db;
14112
13681
  this.logger = logger;
14113
- this._hasCompletedSync = false;
14114
13682
  this.tableNames = new Set();
14115
13683
  this.updateListener = db.registerListener({
14116
13684
  tablesUpdated: (update) => {
@@ -14122,7 +13690,6 @@ class SqliteBucketStorage extends BaseObserver {
14122
13690
  });
14123
13691
  }
14124
13692
  async init() {
14125
- this._hasCompletedSync = false;
14126
13693
  const existingTableRows = await this.db.getAll(`SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'`);
14127
13694
  for (const row of existingTableRows ?? []) {
14128
13695
  this.tableNames.add(row.name);
@@ -14144,156 +13711,6 @@ class SqliteBucketStorage extends BaseObserver {
14144
13711
  getMaxOpId() {
14145
13712
  return MAX_OP_ID;
14146
13713
  }
14147
- /**
14148
- * Reset any caches.
14149
- */
14150
- startSession() { }
14151
- async getBucketStates() {
14152
- 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'");
14153
- return result;
14154
- }
14155
- async getBucketOperationProgress() {
14156
- const rows = await this.db.getAll('SELECT name, count_at_last, count_since_last FROM ps_buckets');
14157
- return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
14158
- }
14159
- async saveSyncData(batch, fixedKeyFormat = false) {
14160
- await this.writeTransaction(async (tx) => {
14161
- for (const b of batch.buckets) {
14162
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
14163
- 'save',
14164
- JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
14165
- ]);
14166
- this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
14167
- }
14168
- });
14169
- }
14170
- async removeBuckets(buckets) {
14171
- for (const bucket of buckets) {
14172
- await this.deleteBucket(bucket);
14173
- }
14174
- }
14175
- /**
14176
- * Mark a bucket for deletion.
14177
- */
14178
- async deleteBucket(bucket) {
14179
- await this.writeTransaction(async (tx) => {
14180
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
14181
- });
14182
- this.logger.debug(`Done deleting bucket ${bucket}`);
14183
- }
14184
- async hasCompletedSync() {
14185
- if (this._hasCompletedSync) {
14186
- return true;
14187
- }
14188
- const r = await this.db.get(`SELECT powersync_last_synced_at() as synced_at`);
14189
- const completed = r.synced_at != null;
14190
- if (completed) {
14191
- this._hasCompletedSync = true;
14192
- }
14193
- return completed;
14194
- }
14195
- async syncLocalDatabase(checkpoint, priority) {
14196
- const r = await this.validateChecksums(checkpoint, priority);
14197
- if (!r.checkpointValid) {
14198
- this.logger.error('Checksums failed for', r.checkpointFailures);
14199
- for (const b of r.checkpointFailures ?? []) {
14200
- await this.deleteBucket(b);
14201
- }
14202
- return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
14203
- }
14204
- if (priority == null) {
14205
- this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
14206
- }
14207
- else {
14208
- this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
14209
- }
14210
- let buckets = checkpoint.buckets;
14211
- if (priority !== undefined) {
14212
- buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
14213
- }
14214
- const bucketNames = buckets.map((b) => b.bucket);
14215
- await this.writeTransaction(async (tx) => {
14216
- await tx.execute(`UPDATE ps_buckets SET last_op = ? WHERE name IN (SELECT json_each.value FROM json_each(?))`, [
14217
- checkpoint.last_op_id,
14218
- JSON.stringify(bucketNames)
14219
- ]);
14220
- if (priority == null && checkpoint.write_checkpoint) {
14221
- await tx.execute("UPDATE ps_buckets SET last_op = ? WHERE name = '$local'", [checkpoint.write_checkpoint]);
14222
- }
14223
- });
14224
- const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
14225
- if (!valid) {
14226
- return { ready: false, checkpointValid: true };
14227
- }
14228
- return {
14229
- ready: true,
14230
- checkpointValid: true
14231
- };
14232
- }
14233
- /**
14234
- * Atomically update the local state to the current checkpoint.
14235
- *
14236
- * This includes creating new tables, dropping old tables, and copying data over from the oplog.
14237
- */
14238
- async updateObjectsFromBuckets(checkpoint, priority) {
14239
- let arg = '';
14240
- if (priority !== undefined) {
14241
- const affectedBuckets = [];
14242
- for (const desc of checkpoint.buckets) {
14243
- if (hasMatchingPriority(priority, desc)) {
14244
- affectedBuckets.push(desc.bucket);
14245
- }
14246
- }
14247
- arg = JSON.stringify({ priority, buckets: affectedBuckets });
14248
- }
14249
- return this.writeTransaction(async (tx) => {
14250
- const { insertId: result } = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
14251
- 'sync_local',
14252
- arg
14253
- ]);
14254
- if (result == 1) {
14255
- if (priority == null) {
14256
- const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
14257
- // The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
14258
- const jsonBucketCount = JSON.stringify(bucketToCount);
14259
- 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]);
14260
- }
14261
- return true;
14262
- }
14263
- else {
14264
- return false;
14265
- }
14266
- });
14267
- }
14268
- async validateChecksums(checkpoint, priority) {
14269
- if (priority !== undefined) {
14270
- // Only validate the buckets within the priority we care about
14271
- const newBuckets = checkpoint.buckets.filter((cs) => hasMatchingPriority(priority, cs));
14272
- checkpoint = { ...checkpoint, buckets: newBuckets };
14273
- }
14274
- const rs = await this.db.execute('SELECT powersync_validate_checkpoint(?) as result', [
14275
- JSON.stringify({ ...checkpoint })
14276
- ]);
14277
- const resultItem = rs.rows?.item(0);
14278
- if (!resultItem) {
14279
- return {
14280
- checkpointValid: false,
14281
- ready: false,
14282
- checkpointFailures: []
14283
- };
14284
- }
14285
- const result = JSON.parse(resultItem['result']);
14286
- if (result['valid']) {
14287
- return { ready: true, checkpointValid: true };
14288
- }
14289
- else {
14290
- return {
14291
- checkpointValid: false,
14292
- ready: false,
14293
- checkpointFailures: result['failed_buckets']
14294
- };
14295
- }
14296
- }
14297
13714
  async updateLocalTarget(cb) {
14298
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]);
14299
13716
  if (!rs1.length) {
@@ -14384,12 +13801,6 @@ class SqliteBucketStorage extends BaseObserver {
14384
13801
  async writeTransaction(callback, options) {
14385
13802
  return this.db.writeTransaction(callback, options);
14386
13803
  }
14387
- /**
14388
- * Set a target checkpoint.
14389
- */
14390
- async setTargetCheckpoint(checkpoint) {
14391
- // No-op for now
14392
- }
14393
13804
  async control(op, payload) {
14394
13805
  return await this.writeTransaction(async (tx) => {
14395
13806
  const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
@@ -14413,20 +13824,6 @@ class SqliteBucketStorage extends BaseObserver {
14413
13824
  }
14414
13825
  static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
14415
13826
  }
14416
- function hasMatchingPriority(priority, bucket) {
14417
- return bucket.priority != null && bucket.priority <= priority;
14418
- }
14419
-
14420
- // TODO JSON
14421
- class SyncDataBatch {
14422
- buckets;
14423
- static fromJSON(json) {
14424
- return new SyncDataBatch(json.buckets.map((bucket) => SyncDataBucket.fromRow(bucket)));
14425
- }
14426
- constructor(buckets) {
14427
- this.buckets = buckets;
14428
- }
14429
- }
14430
13827
 
14431
13828
  /**
14432
13829
  * Thrown when an underlying database connection is closed.
@@ -14486,10 +13883,8 @@ class Schema {
14486
13883
  * developer instead of automatically by PowerSync.
14487
13884
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
14488
13885
  * using client-side table and column constraints.
14489
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
14490
13886
  *
14491
13887
  * @param tables An object of (table name, raw table definition) entries.
14492
- * @experimental Note that the raw tables API is still experimental and may change in the future.
14493
13888
  */
14494
13889
  withRawTables(tables) {
14495
13890
  for (const [name, rawTableDefinition] of Object.entries(tables)) {
@@ -14715,7 +14110,6 @@ exports.DEFAULT_INDEX_OPTIONS = DEFAULT_INDEX_OPTIONS;
14715
14110
  exports.DEFAULT_LOCK_TIMEOUT_MS = DEFAULT_LOCK_TIMEOUT_MS;
14716
14111
  exports.DEFAULT_POWERSYNC_CLOSE_OPTIONS = DEFAULT_POWERSYNC_CLOSE_OPTIONS;
14717
14112
  exports.DEFAULT_POWERSYNC_DB_OPTIONS = DEFAULT_POWERSYNC_DB_OPTIONS;
14718
- exports.DEFAULT_PRESSURE_LIMITS = DEFAULT_PRESSURE_LIMITS;
14719
14113
  exports.DEFAULT_REMOTE_LOGGER = DEFAULT_REMOTE_LOGGER;
14720
14114
  exports.DEFAULT_REMOTE_OPTIONS = DEFAULT_REMOTE_OPTIONS;
14721
14115
  exports.DEFAULT_RETRY_DELAY_MS = DEFAULT_RETRY_DELAY_MS;
@@ -14726,7 +14120,6 @@ exports.DEFAULT_SYNC_CLIENT_IMPLEMENTATION = DEFAULT_SYNC_CLIENT_IMPLEMENTATION;
14726
14120
  exports.DEFAULT_TABLE_OPTIONS = DEFAULT_TABLE_OPTIONS;
14727
14121
  exports.DEFAULT_WATCH_QUERY_OPTIONS = DEFAULT_WATCH_QUERY_OPTIONS;
14728
14122
  exports.DEFAULT_WATCH_THROTTLE_MS = DEFAULT_WATCH_THROTTLE_MS;
14729
- exports.DataStream = DataStream;
14730
14123
  exports.DifferentialQueryProcessor = DifferentialQueryProcessor;
14731
14124
  exports.EMPTY_DIFFERENTIAL = EMPTY_DIFFERENTIAL;
14732
14125
  exports.FalsyComparator = FalsyComparator;
@@ -14741,13 +14134,9 @@ exports.MAX_OP_ID = MAX_OP_ID;
14741
14134
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
14742
14135
  exports.Mutex = Mutex;
14743
14136
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
14744
- exports.OpType = OpType;
14745
- exports.OplogEntry = OplogEntry;
14746
14137
  exports.Schema = Schema;
14747
14138
  exports.Semaphore = Semaphore;
14748
14139
  exports.SqliteBucketStorage = SqliteBucketStorage;
14749
- exports.SyncDataBatch = SyncDataBatch;
14750
- exports.SyncDataBucket = SyncDataBucket;
14751
14140
  exports.SyncProgress = SyncProgress;
14752
14141
  exports.SyncStatus = SyncStatus;
14753
14142
  exports.SyncingService = SyncingService;
@@ -14762,18 +14151,10 @@ exports.createBaseLogger = createBaseLogger;
14762
14151
  exports.createLogger = createLogger;
14763
14152
  exports.extractTableUpdates = extractTableUpdates;
14764
14153
  exports.isBatchedUpdateNotification = isBatchedUpdateNotification;
14765
- exports.isContinueCheckpointRequest = isContinueCheckpointRequest;
14766
14154
  exports.isDBAdapter = isDBAdapter;
14767
14155
  exports.isPowerSyncDatabaseOptionsWithSettings = isPowerSyncDatabaseOptionsWithSettings;
14768
14156
  exports.isSQLOpenFactory = isSQLOpenFactory;
14769
14157
  exports.isSQLOpenOptions = isSQLOpenOptions;
14770
- exports.isStreamingKeepalive = isStreamingKeepalive;
14771
- exports.isStreamingSyncCheckpoint = isStreamingSyncCheckpoint;
14772
- exports.isStreamingSyncCheckpointComplete = isStreamingSyncCheckpointComplete;
14773
- exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
14774
- exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
14775
- exports.isStreamingSyncData = isStreamingSyncData;
14776
- exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
14777
14158
  exports.parseQuery = parseQuery;
14778
14159
  exports.runOnSchemaChange = runOnSchemaChange;
14779
14160
  exports.sanitizeSQL = sanitizeSQL;