@powersync/common 1.50.0 → 1.52.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 (45) hide show
  1. package/dist/bundle.cjs +558 -481
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +558 -480
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +556 -481
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +556 -480
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +73 -73
  10. package/lib/client/AbstractPowerSyncDatabase.js +3 -3
  11. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  12. package/lib/client/sync/stream/AbstractRemote.d.ts +29 -8
  13. package/lib/client/sync/stream/AbstractRemote.js +154 -177
  14. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  15. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -0
  16. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +69 -88
  17. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  18. package/lib/index.d.ts +1 -1
  19. package/lib/index.js +0 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/utils/async.d.ts +0 -9
  22. package/lib/utils/async.js +0 -9
  23. package/lib/utils/async.js.map +1 -1
  24. package/lib/utils/mutex.d.ts +32 -3
  25. package/lib/utils/mutex.js +85 -36
  26. package/lib/utils/mutex.js.map +1 -1
  27. package/lib/utils/queue.d.ts +16 -0
  28. package/lib/utils/queue.js +42 -0
  29. package/lib/utils/queue.js.map +1 -0
  30. package/lib/utils/stream_transform.d.ts +39 -0
  31. package/lib/utils/stream_transform.js +206 -0
  32. package/lib/utils/stream_transform.js.map +1 -0
  33. package/package.json +9 -7
  34. package/src/client/AbstractPowerSyncDatabase.ts +3 -3
  35. package/src/client/sync/stream/AbstractRemote.ts +182 -206
  36. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +82 -83
  37. package/src/index.ts +1 -1
  38. package/src/utils/async.ts +0 -11
  39. package/src/utils/mutex.ts +111 -48
  40. package/src/utils/queue.ts +48 -0
  41. package/src/utils/stream_transform.ts +252 -0
  42. package/lib/utils/DataStream.d.ts +0 -62
  43. package/lib/utils/DataStream.js +0 -169
  44. package/lib/utils/DataStream.js.map +0 -1
  45. package/src/utils/DataStream.ts +0 -222
@@ -785,19 +785,69 @@ class SyncingService {
785
785
  }
786
786
 
787
787
  /**
788
- * An asynchronous mutex implementation.
788
+ * A simple fixed-capacity queue implementation.
789
+ *
790
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
791
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
792
+ */
793
+ class Queue {
794
+ table;
795
+ // Index of the first element in the table.
796
+ head;
797
+ // Amount of items currently in the queue.
798
+ _length;
799
+ constructor(initialItems) {
800
+ this.table = [...initialItems];
801
+ this.head = 0;
802
+ this._length = this.table.length;
803
+ }
804
+ get isEmpty() {
805
+ return this.length == 0;
806
+ }
807
+ get length() {
808
+ return this._length;
809
+ }
810
+ removeFirst() {
811
+ if (this.isEmpty) {
812
+ throw new Error('Queue is empty');
813
+ }
814
+ const result = this.table[this.head];
815
+ this._length--;
816
+ this.table[this.head] = undefined;
817
+ this.head = (this.head + 1) % this.table.length;
818
+ return result;
819
+ }
820
+ addLast(element) {
821
+ if (this.length == this.table.length) {
822
+ throw new Error('Queue is full');
823
+ }
824
+ this.table[(this.head + this._length) % this.table.length] = element;
825
+ this._length++;
826
+ }
827
+ }
828
+
829
+ /**
830
+ * An asynchronous semaphore implementation with associated items per lease.
789
831
  *
790
832
  * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
791
833
  */
792
- class Mutex {
793
- inCriticalSection = false;
834
+ class Semaphore {
835
+ // Available items that are not currently assigned to a waiter.
836
+ available;
837
+ size;
794
838
  // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
795
839
  // aborted waiters from the middle of the list efficiently.
796
840
  firstWaiter;
797
841
  lastWaiter;
798
- addWaiter(onAcquire) {
842
+ constructor(elements) {
843
+ this.available = new Queue(elements);
844
+ this.size = this.available.length;
845
+ }
846
+ addWaiter(requestedItems, onAcquire) {
799
847
  const node = {
800
848
  isActive: true,
849
+ acquiredItems: [],
850
+ remainingItems: requestedItems,
801
851
  onAcquire,
802
852
  prev: this.lastWaiter
803
853
  };
@@ -823,52 +873,92 @@ class Mutex {
823
873
  if (waiter == this.lastWaiter)
824
874
  this.lastWaiter = prev;
825
875
  }
826
- acquire(abort) {
876
+ requestPermits(amount, abort) {
877
+ if (amount <= 0 || amount > this.size) {
878
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
879
+ }
827
880
  return new Promise((resolve, reject) => {
828
881
  function rejectAborted() {
829
- reject(abort?.reason ?? new Error('Mutex acquire aborted'));
882
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
830
883
  }
831
884
  if (abort?.aborted) {
832
885
  return rejectAborted();
833
886
  }
834
- let holdsMutex = false;
887
+ let waiter;
835
888
  const markCompleted = () => {
836
- if (!holdsMutex)
837
- return;
838
- holdsMutex = false;
839
- const waiter = this.firstWaiter;
840
- if (waiter) {
841
- this.deactivateWaiter(waiter);
842
- // Still in critical section, but owned by next waiter now.
843
- waiter.onAcquire();
889
+ const items = waiter.acquiredItems;
890
+ waiter.acquiredItems = []; // Avoid releasing items twice.
891
+ for (const element of items) {
892
+ // Give to next waiter, if possible.
893
+ const nextWaiter = this.firstWaiter;
894
+ if (nextWaiter) {
895
+ nextWaiter.acquiredItems.push(element);
896
+ nextWaiter.remainingItems--;
897
+ if (nextWaiter.remainingItems == 0) {
898
+ nextWaiter.onAcquire();
899
+ }
900
+ }
901
+ else {
902
+ // No pending waiter, return lease into pool.
903
+ this.available.addLast(element);
904
+ }
844
905
  }
845
- else {
846
- this.inCriticalSection = false;
906
+ };
907
+ const onAbort = () => {
908
+ abort?.removeEventListener('abort', onAbort);
909
+ if (waiter.isActive) {
910
+ this.deactivateWaiter(waiter);
911
+ rejectAborted();
847
912
  }
848
913
  };
849
- if (!this.inCriticalSection) {
850
- this.inCriticalSection = true;
851
- holdsMutex = true;
852
- return resolve(markCompleted);
914
+ const resolvePromise = () => {
915
+ this.deactivateWaiter(waiter);
916
+ abort?.removeEventListener('abort', onAbort);
917
+ const items = waiter.acquiredItems;
918
+ resolve({ items, release: markCompleted });
919
+ };
920
+ waiter = this.addWaiter(amount, resolvePromise);
921
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
922
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
923
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
924
+ waiter.acquiredItems.push(this.available.removeFirst());
925
+ waiter.remainingItems--;
853
926
  }
854
- else {
855
- let node;
856
- const onAbort = () => {
857
- abort?.removeEventListener('abort', onAbort);
858
- if (node.isActive) {
859
- this.deactivateWaiter(node);
860
- rejectAborted();
861
- }
862
- };
863
- node = this.addWaiter(() => {
864
- abort?.removeEventListener('abort', onAbort);
865
- holdsMutex = true;
866
- resolve(markCompleted);
867
- });
868
- abort?.addEventListener('abort', onAbort);
927
+ if (waiter.remainingItems == 0) {
928
+ return resolvePromise();
869
929
  }
930
+ abort?.addEventListener('abort', onAbort);
870
931
  });
871
932
  }
933
+ /**
934
+ * Requests a single item from the pool.
935
+ *
936
+ * The returned `release` callback must be invoked to return the item into the pool.
937
+ */
938
+ async requestOne(abort) {
939
+ const { items, release } = await this.requestPermits(1, abort);
940
+ return { release, item: items[0] };
941
+ }
942
+ /**
943
+ * Requests access to all items from the pool.
944
+ *
945
+ * The returned `release` callback must be invoked to return items into the pool.
946
+ */
947
+ requestAll(abort) {
948
+ return this.requestPermits(this.size, abort);
949
+ }
950
+ }
951
+ /**
952
+ * An asynchronous mutex implementation.
953
+ *
954
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
955
+ */
956
+ class Mutex {
957
+ inner = new Semaphore([null]);
958
+ async acquire(abort) {
959
+ const { release } = await this.inner.requestOne(abort);
960
+ return release;
961
+ }
872
962
  async runExclusive(fn, abort) {
873
963
  const returnMutex = await this.acquire(abort);
874
964
  try {
@@ -2167,15 +2257,6 @@ class ControlledExecutor {
2167
2257
  }
2168
2258
  }
2169
2259
 
2170
- /**
2171
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
2172
- * [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)
2173
- * we recommend for React Native.
2174
- *
2175
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
2176
- * iterators without requiring them.
2177
- */
2178
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
2179
2260
  /**
2180
2261
  * Throttle a function to be called at most once every "wait" milliseconds,
2181
2262
  * on the trailing edge.
@@ -8141,177 +8222,10 @@ function requireDist () {
8141
8222
 
8142
8223
  var distExports = requireDist();
8143
8224
 
8144
- var version = "1.50.0";
8225
+ var version = "1.52.0";
8145
8226
  var PACKAGE = {
8146
8227
  version: version};
8147
8228
 
8148
- const DEFAULT_PRESSURE_LIMITS = {
8149
- highWater: 10,
8150
- lowWater: 0
8151
- };
8152
- /**
8153
- * A very basic implementation of a data stream with backpressure support which does not use
8154
- * native JS streams or async iterators.
8155
- * This is handy for environments such as React Native which need polyfills for the above.
8156
- */
8157
- class DataStream extends BaseObserver {
8158
- options;
8159
- dataQueue;
8160
- isClosed;
8161
- processingPromise;
8162
- notifyDataAdded;
8163
- logger;
8164
- mapLine;
8165
- constructor(options) {
8166
- super();
8167
- this.options = options;
8168
- this.processingPromise = null;
8169
- this.isClosed = false;
8170
- this.dataQueue = [];
8171
- this.mapLine = options?.mapLine ?? ((line) => line);
8172
- this.logger = options?.logger ?? Logger.get('DataStream');
8173
- if (options?.closeOnError) {
8174
- const l = this.registerListener({
8175
- error: (ex) => {
8176
- l?.();
8177
- this.close();
8178
- }
8179
- });
8180
- }
8181
- }
8182
- get highWatermark() {
8183
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
8184
- }
8185
- get lowWatermark() {
8186
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
8187
- }
8188
- get closed() {
8189
- return this.isClosed;
8190
- }
8191
- async close() {
8192
- this.isClosed = true;
8193
- await this.processingPromise;
8194
- this.iterateListeners((l) => l.closed?.());
8195
- // Discard any data in the queue
8196
- this.dataQueue = [];
8197
- this.listeners.clear();
8198
- }
8199
- /**
8200
- * Enqueues data for the consumers to read
8201
- */
8202
- enqueueData(data) {
8203
- if (this.isClosed) {
8204
- throw new Error('Cannot enqueue data into closed stream.');
8205
- }
8206
- this.dataQueue.push(data);
8207
- this.notifyDataAdded?.();
8208
- this.processQueue();
8209
- }
8210
- /**
8211
- * Reads data once from the data stream
8212
- * @returns a Data payload or Null if the stream closed.
8213
- */
8214
- async read() {
8215
- if (this.closed) {
8216
- return null;
8217
- }
8218
- // Wait for any pending processing to complete first.
8219
- // This ensures we register our listener before calling processQueue(),
8220
- // avoiding a race where processQueue() sees no reader and returns early.
8221
- if (this.processingPromise) {
8222
- await this.processingPromise;
8223
- }
8224
- // Re-check after await - stream may have closed while we were waiting
8225
- if (this.closed) {
8226
- return null;
8227
- }
8228
- return new Promise((resolve, reject) => {
8229
- const l = this.registerListener({
8230
- data: async (data) => {
8231
- resolve(data);
8232
- // Remove the listener
8233
- l?.();
8234
- },
8235
- closed: () => {
8236
- resolve(null);
8237
- l?.();
8238
- },
8239
- error: (ex) => {
8240
- reject(ex);
8241
- l?.();
8242
- }
8243
- });
8244
- this.processQueue();
8245
- });
8246
- }
8247
- /**
8248
- * Executes a callback for each data item in the stream
8249
- */
8250
- forEach(callback) {
8251
- if (this.dataQueue.length <= this.lowWatermark) {
8252
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
8253
- }
8254
- return this.registerListener({
8255
- data: callback
8256
- });
8257
- }
8258
- processQueue() {
8259
- if (this.processingPromise) {
8260
- return;
8261
- }
8262
- const promise = (this.processingPromise = this._processQueue());
8263
- promise.finally(() => {
8264
- this.processingPromise = null;
8265
- });
8266
- return promise;
8267
- }
8268
- hasDataReader() {
8269
- return Array.from(this.listeners.values()).some((l) => !!l.data);
8270
- }
8271
- async _processQueue() {
8272
- /**
8273
- * Allow listeners to mutate the queue before processing.
8274
- * This allows for operations such as dropping or compressing data
8275
- * on high water or requesting more data on low water.
8276
- */
8277
- if (this.dataQueue.length >= this.highWatermark) {
8278
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
8279
- }
8280
- if (this.isClosed || !this.hasDataReader()) {
8281
- return;
8282
- }
8283
- if (this.dataQueue.length) {
8284
- const data = this.dataQueue.shift();
8285
- const mapped = this.mapLine(data);
8286
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
8287
- }
8288
- if (this.dataQueue.length <= this.lowWatermark) {
8289
- const dataAdded = new Promise((resolve) => {
8290
- this.notifyDataAdded = resolve;
8291
- });
8292
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
8293
- this.notifyDataAdded = null;
8294
- }
8295
- if (this.dataQueue.length > 0) {
8296
- setTimeout(() => this.processQueue());
8297
- }
8298
- }
8299
- async iterateAsyncErrored(cb) {
8300
- // Important: We need to copy the listeners, as calling a listener could result in adding another
8301
- // listener, resulting in infinite loops.
8302
- const listeners = Array.from(this.listeners.values());
8303
- for (let i of listeners) {
8304
- try {
8305
- await cb(i);
8306
- }
8307
- catch (ex) {
8308
- this.logger.error(ex);
8309
- this.iterateListeners((l) => l.error?.(ex));
8310
- }
8311
- }
8312
- }
8313
- }
8314
-
8315
8229
  var WebsocketDuplexConnection = {};
8316
8230
 
8317
8231
  var hasRequiredWebsocketDuplexConnection;
@@ -8474,8 +8388,215 @@ class WebsocketClientTransport {
8474
8388
  }
8475
8389
  }
8476
8390
 
8391
+ const doneResult = { done: true, value: undefined };
8392
+ function valueResult(value) {
8393
+ return { done: false, value };
8394
+ }
8395
+ /**
8396
+ * A variant of {@link Array.map} for async iterators.
8397
+ */
8398
+ function map(source, map) {
8399
+ return {
8400
+ next: async () => {
8401
+ const value = await source.next();
8402
+ if (value.done) {
8403
+ return value;
8404
+ }
8405
+ else {
8406
+ return { value: map(value.value) };
8407
+ }
8408
+ }
8409
+ };
8410
+ }
8411
+ /**
8412
+ * Expands a source async iterator by allowing to inject events asynchronously.
8413
+ *
8414
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
8415
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
8416
+ *
8417
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
8418
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
8419
+ */
8420
+ function injectable(source) {
8421
+ let sourceIsDone = false;
8422
+ let waiter = undefined; // An active, waiting next() call.
8423
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
8424
+ let pendingSourceEvent = null;
8425
+ let pendingInjectedEvents = [];
8426
+ const consumeWaiter = () => {
8427
+ const pending = waiter;
8428
+ waiter = undefined;
8429
+ return pending;
8430
+ };
8431
+ const fetchFromSource = () => {
8432
+ const resolveWaiter = (propagate) => {
8433
+ const active = consumeWaiter();
8434
+ if (active) {
8435
+ propagate(active);
8436
+ }
8437
+ else {
8438
+ pendingSourceEvent = propagate;
8439
+ }
8440
+ };
8441
+ const nextFromSource = source.next();
8442
+ nextFromSource.then((value) => {
8443
+ sourceIsDone = value.done == true;
8444
+ resolveWaiter((w) => w.resolve(value));
8445
+ }, (error) => {
8446
+ resolveWaiter((w) => w.reject(error));
8447
+ });
8448
+ };
8449
+ return {
8450
+ next: () => {
8451
+ return new Promise((resolve, reject) => {
8452
+ // First priority: Dispatch ready upstream events.
8453
+ if (sourceIsDone) {
8454
+ return resolve(doneResult);
8455
+ }
8456
+ if (pendingSourceEvent) {
8457
+ pendingSourceEvent({ resolve, reject });
8458
+ pendingSourceEvent = null;
8459
+ return;
8460
+ }
8461
+ // Second priority: Dispatch injected events
8462
+ if (pendingInjectedEvents.length) {
8463
+ return resolve(valueResult(pendingInjectedEvents.shift()));
8464
+ }
8465
+ // Nothing pending? Fetch from source
8466
+ waiter = { resolve, reject };
8467
+ return fetchFromSource();
8468
+ });
8469
+ },
8470
+ inject: (event) => {
8471
+ const pending = consumeWaiter();
8472
+ if (pending != null) {
8473
+ pending.resolve(valueResult(event));
8474
+ }
8475
+ else {
8476
+ pendingInjectedEvents.push(event);
8477
+ }
8478
+ }
8479
+ };
8480
+ }
8481
+ /**
8482
+ * Splits a byte stream at line endings, emitting each line as a string.
8483
+ */
8484
+ function extractJsonLines(source, decoder) {
8485
+ let buffer = '';
8486
+ const pendingLines = [];
8487
+ let isFinalEvent = false;
8488
+ return {
8489
+ next: async () => {
8490
+ while (true) {
8491
+ if (isFinalEvent) {
8492
+ return doneResult;
8493
+ }
8494
+ {
8495
+ const first = pendingLines.shift();
8496
+ if (first) {
8497
+ return { done: false, value: first };
8498
+ }
8499
+ }
8500
+ const { done, value } = await source.next();
8501
+ if (done) {
8502
+ const remaining = buffer.trim();
8503
+ if (remaining.length != 0) {
8504
+ isFinalEvent = true;
8505
+ return { done: false, value: remaining };
8506
+ }
8507
+ return doneResult;
8508
+ }
8509
+ const data = decoder.decode(value, { stream: true });
8510
+ buffer += data;
8511
+ const lines = buffer.split('\n');
8512
+ for (let i = 0; i < lines.length - 1; i++) {
8513
+ const l = lines[i].trim();
8514
+ if (l.length > 0) {
8515
+ pendingLines.push(l);
8516
+ }
8517
+ }
8518
+ buffer = lines[lines.length - 1];
8519
+ }
8520
+ }
8521
+ };
8522
+ }
8523
+ /**
8524
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
8525
+ */
8526
+ function extractBsonObjects(source) {
8527
+ // Fully read but not emitted yet.
8528
+ const completedObjects = [];
8529
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
8530
+ let isDone = false;
8531
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
8532
+ let objectBody = null;
8533
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
8534
+ // If we're consuming a document, the bytes remaining.
8535
+ let remainingLength = 4;
8536
+ return {
8537
+ async next() {
8538
+ while (true) {
8539
+ // Before fetching new data from upstream, return completed objects.
8540
+ if (completedObjects.length) {
8541
+ return valueResult(completedObjects.shift());
8542
+ }
8543
+ if (isDone) {
8544
+ return doneResult;
8545
+ }
8546
+ const upstreamEvent = await source.next();
8547
+ if (upstreamEvent.done) {
8548
+ isDone = true;
8549
+ if (objectBody || remainingLength != 4) {
8550
+ throw new Error('illegal end of stream in BSON object');
8551
+ }
8552
+ return doneResult;
8553
+ }
8554
+ const chunk = upstreamEvent.value;
8555
+ for (let i = 0; i < chunk.length;) {
8556
+ const availableInData = chunk.length - i;
8557
+ if (objectBody) {
8558
+ // We're in the middle of reading a BSON document.
8559
+ const bytesToRead = Math.min(availableInData, remainingLength);
8560
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
8561
+ objectBody.set(copySource, objectBody.length - remainingLength);
8562
+ i += bytesToRead;
8563
+ remainingLength -= bytesToRead;
8564
+ if (remainingLength == 0) {
8565
+ completedObjects.push(objectBody);
8566
+ // Prepare to read another document, starting with its length
8567
+ objectBody = null;
8568
+ remainingLength = 4;
8569
+ }
8570
+ }
8571
+ else {
8572
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
8573
+ const bytesToRead = Math.min(availableInData, remainingLength);
8574
+ for (let j = 0; j < bytesToRead; j++) {
8575
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
8576
+ }
8577
+ i += bytesToRead;
8578
+ remainingLength -= bytesToRead;
8579
+ if (remainingLength == 0) {
8580
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
8581
+ // header is included in length.
8582
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
8583
+ remainingLength = length - 4;
8584
+ if (remainingLength < 1) {
8585
+ throw new Error(`invalid length for bson: ${length}`);
8586
+ }
8587
+ objectBody = new Uint8Array(length);
8588
+ new DataView(objectBody.buffer).setInt32(0, length, true);
8589
+ }
8590
+ }
8591
+ }
8592
+ }
8593
+ }
8594
+ };
8595
+ }
8596
+
8477
8597
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
8478
8598
  const POWERSYNC_JS_VERSION = PACKAGE.version;
8599
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
8479
8600
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
8480
8601
  // Keep alive message is sent every period
8481
8602
  const KEEP_ALIVE_MS = 20_000;
@@ -8655,13 +8776,14 @@ class AbstractRemote {
8655
8776
  return new WebSocket(url);
8656
8777
  }
8657
8778
  /**
8658
- * Returns a data stream of sync line data.
8779
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
8780
+ *
8781
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
8659
8782
  *
8660
- * @param map Maps received payload frames to the typed event value.
8661
8783
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
8662
8784
  * (required for compatibility with older sync services).
8663
8785
  */
8664
- async socketStreamRaw(options, map, bson) {
8786
+ async socketStreamRaw(options, bson) {
8665
8787
  const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
8666
8788
  const mimeType = bson == null ? 'application/json' : 'application/bson';
8667
8789
  function toBuffer(js) {
@@ -8676,52 +8798,55 @@ class AbstractRemote {
8676
8798
  }
8677
8799
  const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
8678
8800
  const request = await this.buildRequest(path);
8801
+ const url = this.options.socketUrlTransformer(request.url);
8679
8802
  // Add the user agent in the setup payload - we can't set custom
8680
8803
  // headers with websockets on web. The browser userAgent is however added
8681
8804
  // automatically as a header.
8682
8805
  const userAgent = this.getUserAgent();
8683
- const stream = new DataStream({
8684
- logger: this.logger,
8685
- pressure: {
8686
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
8687
- },
8688
- mapLine: map
8689
- });
8806
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
8807
+ // to abort the connection.
8808
+ let pendingSocket = null;
8809
+ let keepAliveTimeout;
8810
+ let rsocket = null;
8811
+ let queue = null;
8812
+ let didClose = false;
8813
+ const abortRequest = () => {
8814
+ if (didClose) {
8815
+ return;
8816
+ }
8817
+ didClose = true;
8818
+ clearTimeout(keepAliveTimeout);
8819
+ if (pendingSocket) {
8820
+ pendingSocket.close();
8821
+ }
8822
+ if (rsocket) {
8823
+ rsocket.close();
8824
+ }
8825
+ if (queue) {
8826
+ queue.stop();
8827
+ }
8828
+ };
8690
8829
  // Handle upstream abort
8691
- if (options.abortSignal?.aborted) {
8830
+ if (options.abortSignal.aborted) {
8692
8831
  throw new AbortOperation('Connection request aborted');
8693
8832
  }
8694
8833
  else {
8695
- options.abortSignal?.addEventListener('abort', () => {
8696
- stream.close();
8697
- }, { once: true });
8834
+ options.abortSignal.addEventListener('abort', abortRequest);
8698
8835
  }
8699
- let keepAliveTimeout;
8700
8836
  const resetTimeout = () => {
8701
8837
  clearTimeout(keepAliveTimeout);
8702
8838
  keepAliveTimeout = setTimeout(() => {
8703
8839
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
8704
- stream.close();
8840
+ abortRequest();
8705
8841
  }, SOCKET_TIMEOUT_MS);
8706
8842
  };
8707
8843
  resetTimeout();
8708
- // Typescript complains about this being `never` if it's not assigned here.
8709
- // This is assigned in `wsCreator`.
8710
- let disposeSocketConnectionTimeout = () => { };
8711
- const url = this.options.socketUrlTransformer(request.url);
8712
8844
  const connector = new distExports.RSocketConnector({
8713
8845
  transport: new WebsocketClientTransport({
8714
8846
  url,
8715
8847
  wsCreator: (url) => {
8716
- const socket = this.createSocket(url);
8717
- disposeSocketConnectionTimeout = stream.registerListener({
8718
- closed: () => {
8719
- // Allow closing the underlying WebSocket if the stream was closed before the
8720
- // RSocket connect completed. This should effectively abort the request.
8721
- socket.close();
8722
- }
8723
- });
8724
- socket.addEventListener('message', (event) => {
8848
+ const socket = (pendingSocket = this.createSocket(url));
8849
+ socket.addEventListener('message', () => {
8725
8850
  resetTimeout();
8726
8851
  });
8727
8852
  return socket;
@@ -8741,43 +8866,40 @@ class AbstractRemote {
8741
8866
  }
8742
8867
  }
8743
8868
  });
8744
- let rsocket;
8745
8869
  try {
8746
8870
  rsocket = await connector.connect();
8747
8871
  // The connection is established, we no longer need to monitor the initial timeout
8748
- disposeSocketConnectionTimeout();
8872
+ pendingSocket = null;
8749
8873
  }
8750
8874
  catch (ex) {
8751
8875
  this.logger.error(`Failed to connect WebSocket`, ex);
8752
- clearTimeout(keepAliveTimeout);
8753
- if (!stream.closed) {
8754
- await stream.close();
8755
- }
8876
+ abortRequest();
8756
8877
  throw ex;
8757
8878
  }
8758
8879
  resetTimeout();
8759
- let socketIsClosed = false;
8760
- const closeSocket = () => {
8761
- clearTimeout(keepAliveTimeout);
8762
- if (socketIsClosed) {
8763
- return;
8764
- }
8765
- socketIsClosed = true;
8766
- rsocket.close();
8767
- };
8768
8880
  // Helps to prevent double close scenarios
8769
- rsocket.onClose(() => (socketIsClosed = true));
8770
- // We initially request this amount and expect these to arrive eventually
8771
- let pendingEventsCount = syncQueueRequestSize;
8772
- const disposeClosedListener = stream.registerListener({
8773
- closed: () => {
8774
- closeSocket();
8775
- disposeClosedListener();
8776
- }
8777
- });
8778
- const socket = await new Promise((resolve, reject) => {
8881
+ rsocket.onClose(() => (rsocket = null));
8882
+ return await new Promise((resolve, reject) => {
8779
8883
  let connectionEstablished = false;
8780
- const res = rsocket.requestStream({
8884
+ let pendingEventsCount = syncQueueRequestSize;
8885
+ let paused = false;
8886
+ let res = null;
8887
+ function requestMore() {
8888
+ const delta = syncQueueRequestSize - pendingEventsCount;
8889
+ if (!paused && delta > 0) {
8890
+ res?.request(delta);
8891
+ pendingEventsCount = syncQueueRequestSize;
8892
+ }
8893
+ }
8894
+ const events = new eventIterator.EventIterator((q) => {
8895
+ queue = q;
8896
+ q.on('highWater', () => (paused = true));
8897
+ q.on('lowWater', () => {
8898
+ paused = false;
8899
+ requestMore();
8900
+ });
8901
+ }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[Symbol.asyncIterator]();
8902
+ res = rsocket.requestStream({
8781
8903
  data: toBuffer(options.data),
8782
8904
  metadata: toBuffer({
8783
8905
  path
@@ -8802,7 +8924,7 @@ class AbstractRemote {
8802
8924
  }
8803
8925
  // RSocket will close the RSocket stream automatically
8804
8926
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
8805
- stream.close();
8927
+ abortRequest();
8806
8928
  // Handles cases where the connection failed e.g. auth error or connection error
8807
8929
  if (!connectionEstablished) {
8808
8930
  reject(e);
@@ -8812,41 +8934,40 @@ class AbstractRemote {
8812
8934
  // The connection is active
8813
8935
  if (!connectionEstablished) {
8814
8936
  connectionEstablished = true;
8815
- resolve(res);
8937
+ resolve(events);
8816
8938
  }
8817
8939
  const { data } = payload;
8940
+ if (data) {
8941
+ queue.push(data);
8942
+ }
8818
8943
  // Less events are now pending
8819
8944
  pendingEventsCount--;
8820
- if (!data) {
8821
- return;
8822
- }
8823
- stream.enqueueData(data);
8945
+ // Request another event (unless the downstream consumer is paused).
8946
+ requestMore();
8824
8947
  },
8825
8948
  onComplete: () => {
8826
- stream.close();
8949
+ abortRequest(); // this will also emit a done event
8827
8950
  },
8828
8951
  onExtension: () => { }
8829
8952
  });
8830
8953
  });
8831
- const l = stream.registerListener({
8832
- lowWater: async () => {
8833
- // Request to fill up the queue
8834
- const required = syncQueueRequestSize - pendingEventsCount;
8835
- if (required > 0) {
8836
- socket.request(syncQueueRequestSize - pendingEventsCount);
8837
- pendingEventsCount = syncQueueRequestSize;
8838
- }
8839
- },
8840
- closed: () => {
8841
- l();
8842
- }
8843
- });
8844
- return stream;
8845
8954
  }
8846
8955
  /**
8847
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
8956
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
8957
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
8958
+ *
8959
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
8960
+ */
8961
+ get supportsStreamingBinaryResponses() {
8962
+ return true;
8963
+ }
8964
+ /**
8965
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
8966
+ * async iterator of byte blobs.
8967
+ *
8968
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
8848
8969
  */
8849
- async postStreamRaw(options, mapLine) {
8970
+ async fetchStreamRaw(options) {
8850
8971
  const { data, path, headers, abortSignal } = options;
8851
8972
  const request = await this.buildRequest(path);
8852
8973
  /**
@@ -8858,119 +8979,94 @@ class AbstractRemote {
8858
8979
  * Aborting the active fetch request while it is being consumed seems to throw
8859
8980
  * an unhandled exception on the window level.
8860
8981
  */
8861
- if (abortSignal?.aborted) {
8862
- throw new AbortOperation('Abort request received before making postStreamRaw request');
8982
+ if (abortSignal.aborted) {
8983
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
8863
8984
  }
8864
8985
  const controller = new AbortController();
8865
- let requestResolved = false;
8866
- abortSignal?.addEventListener('abort', () => {
8867
- if (!requestResolved) {
8986
+ let reader = null;
8987
+ abortSignal.addEventListener('abort', () => {
8988
+ const reason = abortSignal.reason ??
8989
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
8990
+ if (reader == null) {
8868
8991
  // Only abort via the abort controller if the request has not resolved yet
8869
- controller.abort(abortSignal.reason ??
8870
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.'));
8992
+ controller.abort(reason);
8993
+ }
8994
+ else {
8995
+ reader.cancel(reason).catch(() => {
8996
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
8997
+ // ignore it here.
8998
+ });
8871
8999
  }
8872
9000
  });
8873
- const res = await this.fetch(request.url, {
8874
- method: 'POST',
8875
- headers: { ...headers, ...request.headers },
8876
- body: JSON.stringify(data),
8877
- signal: controller.signal,
8878
- cache: 'no-store',
8879
- ...(this.options.fetchOptions ?? {}),
8880
- ...options.fetchOptions
8881
- }).catch((ex) => {
9001
+ let res;
9002
+ let responseIsBson = false;
9003
+ try {
9004
+ const ndJson = 'application/x-ndjson';
9005
+ const bson = 'application/vnd.powersync.bson-stream';
9006
+ res = await this.fetch(request.url, {
9007
+ method: 'POST',
9008
+ headers: {
9009
+ ...headers,
9010
+ ...request.headers,
9011
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
9012
+ },
9013
+ body: JSON.stringify(data),
9014
+ signal: controller.signal,
9015
+ cache: 'no-store',
9016
+ ...(this.options.fetchOptions ?? {}),
9017
+ ...options.fetchOptions
9018
+ });
9019
+ if (!res.ok || !res.body) {
9020
+ const text = await res.text();
9021
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
9022
+ const error = new Error(`HTTP ${res.statusText}: ${text}`);
9023
+ error.status = res.status;
9024
+ throw error;
9025
+ }
9026
+ const contentType = res.headers.get('content-type');
9027
+ responseIsBson = contentType == bson;
9028
+ }
9029
+ catch (ex) {
8882
9030
  if (ex.name == 'AbortError') {
8883
9031
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
8884
9032
  }
8885
9033
  throw ex;
8886
- });
8887
- if (!res) {
8888
- throw new Error('Fetch request was aborted');
8889
- }
8890
- requestResolved = true;
8891
- if (!res.ok || !res.body) {
8892
- const text = await res.text();
8893
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
8894
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
8895
- error.status = res.status;
8896
- throw error;
8897
9034
  }
8898
- // Create a new stream splitting the response at line endings while also handling cancellations
8899
- // by closing the reader.
8900
- const reader = res.body.getReader();
8901
- let readerReleased = false;
8902
- // This will close the network request and read stream
8903
- const closeReader = async () => {
8904
- try {
8905
- readerReleased = true;
8906
- await reader.cancel();
8907
- }
8908
- catch (ex) {
8909
- // an error will throw if the reader hasn't been used yet
8910
- }
8911
- reader.releaseLock();
8912
- };
8913
- const stream = new DataStream({
8914
- logger: this.logger,
8915
- mapLine: mapLine,
8916
- pressure: {
8917
- highWaterMark: 20,
8918
- lowWaterMark: 10
8919
- }
8920
- });
8921
- abortSignal?.addEventListener('abort', () => {
8922
- closeReader();
8923
- stream.close();
8924
- });
8925
- const decoder = this.createTextDecoder();
8926
- let buffer = '';
8927
- const consumeStream = async () => {
8928
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
8929
- const { done, value } = await reader.read();
8930
- if (done) {
8931
- const remaining = buffer.trim();
8932
- if (remaining.length != 0) {
8933
- stream.enqueueData(remaining);
8934
- }
8935
- stream.close();
8936
- await closeReader();
8937
- return;
9035
+ reader = res.body.getReader();
9036
+ const stream = {
9037
+ next: async () => {
9038
+ if (controller.signal.aborted) {
9039
+ return doneResult;
8938
9040
  }
8939
- const data = decoder.decode(value, { stream: true });
8940
- buffer += data;
8941
- const lines = buffer.split('\n');
8942
- for (var i = 0; i < lines.length - 1; i++) {
8943
- var l = lines[i].trim();
8944
- if (l.length > 0) {
8945
- stream.enqueueData(l);
8946
- }
9041
+ try {
9042
+ return await reader.read();
8947
9043
  }
8948
- buffer = lines[lines.length - 1];
8949
- // Implement backpressure by waiting for the low water mark to be reached
8950
- if (stream.dataQueue.length > stream.highWatermark) {
8951
- await new Promise((resolve) => {
8952
- const dispose = stream.registerListener({
8953
- lowWater: async () => {
8954
- resolve();
8955
- dispose();
8956
- },
8957
- closed: () => {
8958
- resolve();
8959
- dispose();
8960
- }
8961
- });
8962
- });
9044
+ catch (ex) {
9045
+ if (controller.signal.aborted) {
9046
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
9047
+ // things working as intended, we can return a done event and consider the exception handled.
9048
+ return doneResult;
9049
+ }
9050
+ throw ex;
8963
9051
  }
8964
9052
  }
8965
9053
  };
8966
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
8967
- const l = stream.registerListener({
8968
- closed: () => {
8969
- closeReader();
8970
- l?.();
8971
- }
8972
- });
8973
- return stream;
9054
+ return { isBson: responseIsBson, stream };
9055
+ }
9056
+ /**
9057
+ * Posts a `/sync/stream` request.
9058
+ *
9059
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
9060
+ * {@link Uint8Array}s.
9061
+ */
9062
+ async fetchStream(options) {
9063
+ const { isBson, stream } = await this.fetchStreamRaw(options);
9064
+ if (isBson) {
9065
+ return extractBsonObjects(stream);
9066
+ }
9067
+ else {
9068
+ return extractJsonLines(stream, this.createTextDecoder());
9069
+ }
8974
9070
  }
8975
9071
  }
8976
9072
 
@@ -9478,6 +9574,19 @@ The next upload iteration will be delayed.`);
9478
9574
  }
9479
9575
  });
9480
9576
  }
9577
+ async receiveSyncLines(data) {
9578
+ const { options, connection, bson } = data;
9579
+ const remote = this.options.remote;
9580
+ if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
9581
+ return await remote.fetchStream(options);
9582
+ }
9583
+ else {
9584
+ return await this.options.remote.socketStreamRaw({
9585
+ ...options,
9586
+ ...{ fetchStrategy: connection.fetchStrategy }
9587
+ }, bson);
9588
+ }
9589
+ }
9481
9590
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
9482
9591
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
9483
9592
  if (rawTables != null && rawTables.length) {
@@ -9507,42 +9616,27 @@ The next upload iteration will be delayed.`);
9507
9616
  client_id: clientId
9508
9617
  }
9509
9618
  };
9510
- let stream;
9511
- if (resolvedOptions?.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
9512
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
9513
- if (typeof line == 'string') {
9514
- return JSON.parse(line);
9515
- }
9516
- else {
9517
- // Directly enqueued by us
9518
- return line;
9519
- }
9520
- });
9521
- }
9522
- else {
9523
- const bson = await this.options.remote.getBSON();
9524
- stream = await this.options.remote.socketStreamRaw({
9525
- ...syncOptions,
9526
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
9527
- }, (payload) => {
9528
- if (payload instanceof Uint8Array) {
9529
- return bson.deserialize(payload);
9530
- }
9531
- else {
9532
- // Directly enqueued by us
9533
- return payload;
9534
- }
9535
- }, bson);
9536
- }
9619
+ const bson = await this.options.remote.getBSON();
9620
+ const source = await this.receiveSyncLines({
9621
+ options: syncOptions,
9622
+ connection: resolvedOptions,
9623
+ bson
9624
+ });
9625
+ const stream = injectable(map(source, (line) => {
9626
+ if (typeof line == 'string') {
9627
+ return JSON.parse(line);
9628
+ }
9629
+ else {
9630
+ return bson.deserialize(line);
9631
+ }
9632
+ }));
9537
9633
  this.logger.debug('Stream established. Processing events');
9538
9634
  this.notifyCompletedUploads = () => {
9539
- if (!stream.closed) {
9540
- stream.enqueueData({ crud_upload_completed: null });
9541
- }
9635
+ stream.inject({ crud_upload_completed: null });
9542
9636
  };
9543
- while (!stream.closed) {
9544
- const line = await stream.read();
9545
- if (!line) {
9637
+ while (true) {
9638
+ const { value: line, done } = await stream.next();
9639
+ if (done) {
9546
9640
  // The stream has closed while waiting
9547
9641
  return;
9548
9642
  }
@@ -9721,14 +9815,17 @@ The next upload iteration will be delayed.`);
9721
9815
  const syncImplementation = this;
9722
9816
  const adapter = this.options.adapter;
9723
9817
  const remote = this.options.remote;
9818
+ const controller = new AbortController();
9819
+ const abort = () => {
9820
+ return controller.abort(signal.reason);
9821
+ };
9822
+ signal.addEventListener('abort', abort);
9724
9823
  let receivingLines = null;
9725
9824
  let hadSyncLine = false;
9726
9825
  let hideDisconnectOnRestart = false;
9727
9826
  if (signal.aborted) {
9728
9827
  throw new AbortOperation('Connection request has been aborted');
9729
9828
  }
9730
- const abortController = new AbortController();
9731
- signal.addEventListener('abort', () => abortController.abort());
9732
9829
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
9733
9830
  // invocation (local events include refreshed tokens and completed uploads).
9734
9831
  // This is a single data stream so that we can handle all control calls from a single place.
@@ -9736,49 +9833,36 @@ The next upload iteration will be delayed.`);
9736
9833
  async function connect(instr) {
9737
9834
  const syncOptions = {
9738
9835
  path: '/sync/stream',
9739
- abortSignal: abortController.signal,
9836
+ abortSignal: controller.signal,
9740
9837
  data: instr.request
9741
9838
  };
9742
- if (resolvedOptions.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
9743
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
9744
- if (typeof line == 'string') {
9745
- return {
9746
- command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
9747
- payload: line
9748
- };
9749
- }
9750
- else {
9751
- // Directly enqueued by us
9752
- return line;
9753
- }
9754
- });
9755
- }
9756
- else {
9757
- controlInvocations = await remote.socketStreamRaw({
9758
- ...syncOptions,
9759
- fetchStrategy: resolvedOptions.fetchStrategy
9760
- }, (payload) => {
9761
- if (payload instanceof Uint8Array) {
9762
- return {
9763
- command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
9764
- payload: payload
9765
- };
9766
- }
9767
- else {
9768
- // Directly enqueued by us
9769
- return payload;
9770
- }
9771
- });
9772
- }
9839
+ controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
9840
+ options: syncOptions,
9841
+ connection: resolvedOptions
9842
+ }), (line) => {
9843
+ if (typeof line == 'string') {
9844
+ return {
9845
+ command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
9846
+ payload: line
9847
+ };
9848
+ }
9849
+ else {
9850
+ return {
9851
+ command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
9852
+ payload: line
9853
+ };
9854
+ }
9855
+ }));
9773
9856
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
9774
9857
  // we're already connected here and can report that.
9775
9858
  syncImplementation.updateSyncStatus({ connected: true });
9776
9859
  try {
9777
- while (!controlInvocations.closed) {
9778
- const line = await controlInvocations.read();
9779
- if (line == null) {
9780
- return;
9860
+ while (true) {
9861
+ let event = await controlInvocations.next();
9862
+ if (event.done) {
9863
+ break;
9781
9864
  }
9865
+ const line = event.value;
9782
9866
  await control(line.command, line.payload);
9783
9867
  if (!hadSyncLine) {
9784
9868
  syncImplementation.triggerCrudUpload();
@@ -9787,12 +9871,8 @@ The next upload iteration will be delayed.`);
9787
9871
  }
9788
9872
  }
9789
9873
  finally {
9790
- const activeInstructions = controlInvocations;
9791
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
9792
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
9793
- // to null.
9794
- controlInvocations = null;
9795
- await activeInstructions.close();
9874
+ abort();
9875
+ signal.removeEventListener('abort', abort);
9796
9876
  }
9797
9877
  }
9798
9878
  async function stop() {
@@ -9836,14 +9916,14 @@ The next upload iteration will be delayed.`);
9836
9916
  remote.invalidateCredentials();
9837
9917
  // Restart iteration after the credentials have been refreshed.
9838
9918
  remote.fetchCredentials().then((_) => {
9839
- controlInvocations?.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
9919
+ controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
9840
9920
  }, (err) => {
9841
9921
  syncImplementation.logger.warn('Could not prefetch credentials', err);
9842
9922
  });
9843
9923
  }
9844
9924
  }
9845
9925
  else if ('CloseSyncStream' in instruction) {
9846
- abortController.abort();
9926
+ controller.abort();
9847
9927
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
9848
9928
  }
9849
9929
  else if ('FlushFileSystem' in instruction) ;
@@ -9872,17 +9952,13 @@ The next upload iteration will be delayed.`);
9872
9952
  }
9873
9953
  await control(exports.PowerSyncControlCommand.START, JSON.stringify(options));
9874
9954
  this.notifyCompletedUploads = () => {
9875
- if (controlInvocations && !controlInvocations?.closed) {
9876
- controlInvocations.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
9877
- }
9955
+ controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
9878
9956
  };
9879
9957
  this.handleActiveStreamsChange = () => {
9880
- if (controlInvocations && !controlInvocations?.closed) {
9881
- controlInvocations.enqueueData({
9882
- command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
9883
- payload: JSON.stringify(this.activeStreams)
9884
- });
9885
- }
9958
+ controlInvocations?.inject({
9959
+ command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
9960
+ payload: JSON.stringify(this.activeStreams)
9961
+ });
9886
9962
  };
9887
9963
  await receivingLines;
9888
9964
  }
@@ -10899,7 +10975,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10899
10975
  * @returns A transaction of CRUD operations to upload, or null if there are none
10900
10976
  */
10901
10977
  async getNextCrudTransaction() {
10902
- const iterator = this.getCrudTransactions()[symbolAsyncIterator]();
10978
+ const iterator = this.getCrudTransactions()[Symbol.asyncIterator]();
10903
10979
  return (await iterator.next()).value;
10904
10980
  }
10905
10981
  /**
@@ -10935,7 +11011,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10935
11011
  */
10936
11012
  getCrudTransactions() {
10937
11013
  return {
10938
- [symbolAsyncIterator]: () => {
11014
+ [Symbol.asyncIterator]: () => {
10939
11015
  let lastCrudItemId = -1;
10940
11016
  const sql = `
10941
11017
  WITH RECURSIVE crud_entries AS (
@@ -12104,7 +12180,6 @@ exports.DEFAULT_INDEX_OPTIONS = DEFAULT_INDEX_OPTIONS;
12104
12180
  exports.DEFAULT_LOCK_TIMEOUT_MS = DEFAULT_LOCK_TIMEOUT_MS;
12105
12181
  exports.DEFAULT_POWERSYNC_CLOSE_OPTIONS = DEFAULT_POWERSYNC_CLOSE_OPTIONS;
12106
12182
  exports.DEFAULT_POWERSYNC_DB_OPTIONS = DEFAULT_POWERSYNC_DB_OPTIONS;
12107
- exports.DEFAULT_PRESSURE_LIMITS = DEFAULT_PRESSURE_LIMITS;
12108
12183
  exports.DEFAULT_REMOTE_LOGGER = DEFAULT_REMOTE_LOGGER;
12109
12184
  exports.DEFAULT_REMOTE_OPTIONS = DEFAULT_REMOTE_OPTIONS;
12110
12185
  exports.DEFAULT_RETRY_DELAY_MS = DEFAULT_RETRY_DELAY_MS;
@@ -12115,7 +12190,6 @@ exports.DEFAULT_SYNC_CLIENT_IMPLEMENTATION = DEFAULT_SYNC_CLIENT_IMPLEMENTATION;
12115
12190
  exports.DEFAULT_TABLE_OPTIONS = DEFAULT_TABLE_OPTIONS;
12116
12191
  exports.DEFAULT_WATCH_QUERY_OPTIONS = DEFAULT_WATCH_QUERY_OPTIONS;
12117
12192
  exports.DEFAULT_WATCH_THROTTLE_MS = DEFAULT_WATCH_THROTTLE_MS;
12118
- exports.DataStream = DataStream;
12119
12193
  exports.DifferentialQueryProcessor = DifferentialQueryProcessor;
12120
12194
  exports.EMPTY_DIFFERENTIAL = EMPTY_DIFFERENTIAL;
12121
12195
  exports.FalsyComparator = FalsyComparator;
@@ -12133,6 +12207,7 @@ exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
12133
12207
  exports.OpType = OpType;
12134
12208
  exports.OplogEntry = OplogEntry;
12135
12209
  exports.Schema = Schema;
12210
+ exports.Semaphore = Semaphore;
12136
12211
  exports.SqliteBucketStorage = SqliteBucketStorage;
12137
12212
  exports.SyncDataBatch = SyncDataBatch;
12138
12213
  exports.SyncDataBucket = SyncDataBucket;