@powersync/web 1.37.0 → 1.37.2

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.
@@ -1698,7 +1698,6 @@ __webpack_require__.r(__webpack_exports__);
1698
1698
  /* harmony export */ DEFAULT_LOCK_TIMEOUT_MS: () => (/* binding */ DEFAULT_LOCK_TIMEOUT_MS),
1699
1699
  /* harmony export */ DEFAULT_POWERSYNC_CLOSE_OPTIONS: () => (/* binding */ DEFAULT_POWERSYNC_CLOSE_OPTIONS),
1700
1700
  /* harmony export */ DEFAULT_POWERSYNC_DB_OPTIONS: () => (/* binding */ DEFAULT_POWERSYNC_DB_OPTIONS),
1701
- /* harmony export */ DEFAULT_PRESSURE_LIMITS: () => (/* binding */ DEFAULT_PRESSURE_LIMITS),
1702
1701
  /* harmony export */ DEFAULT_REMOTE_LOGGER: () => (/* binding */ DEFAULT_REMOTE_LOGGER),
1703
1702
  /* harmony export */ DEFAULT_REMOTE_OPTIONS: () => (/* binding */ DEFAULT_REMOTE_OPTIONS),
1704
1703
  /* harmony export */ DEFAULT_RETRY_DELAY_MS: () => (/* binding */ DEFAULT_RETRY_DELAY_MS),
@@ -1709,7 +1708,6 @@ __webpack_require__.r(__webpack_exports__);
1709
1708
  /* harmony export */ DEFAULT_TABLE_OPTIONS: () => (/* binding */ DEFAULT_TABLE_OPTIONS),
1710
1709
  /* harmony export */ DEFAULT_WATCH_QUERY_OPTIONS: () => (/* binding */ DEFAULT_WATCH_QUERY_OPTIONS),
1711
1710
  /* harmony export */ DEFAULT_WATCH_THROTTLE_MS: () => (/* binding */ DEFAULT_WATCH_THROTTLE_MS),
1712
- /* harmony export */ DataStream: () => (/* binding */ DataStream),
1713
1711
  /* harmony export */ DiffTriggerOperation: () => (/* binding */ DiffTriggerOperation),
1714
1712
  /* harmony export */ DifferentialQueryProcessor: () => (/* binding */ DifferentialQueryProcessor),
1715
1713
  /* harmony export */ EMPTY_DIFFERENTIAL: () => (/* binding */ EMPTY_DIFFERENTIAL),
@@ -1735,6 +1733,7 @@ __webpack_require__.r(__webpack_exports__);
1735
1733
  /* harmony export */ PowerSyncControlCommand: () => (/* binding */ PowerSyncControlCommand),
1736
1734
  /* harmony export */ RowUpdateType: () => (/* binding */ RowUpdateType),
1737
1735
  /* harmony export */ Schema: () => (/* binding */ Schema),
1736
+ /* harmony export */ Semaphore: () => (/* binding */ Semaphore),
1738
1737
  /* harmony export */ SqliteBucketStorage: () => (/* binding */ SqliteBucketStorage),
1739
1738
  /* harmony export */ SyncClientImplementation: () => (/* binding */ SyncClientImplementation),
1740
1739
  /* harmony export */ SyncDataBatch: () => (/* binding */ SyncDataBatch),
@@ -2556,19 +2555,69 @@ class SyncingService {
2556
2555
  }
2557
2556
 
2558
2557
  /**
2559
- * An asynchronous mutex implementation.
2558
+ * A simple fixed-capacity queue implementation.
2559
+ *
2560
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
2561
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
2562
+ */
2563
+ class Queue {
2564
+ table;
2565
+ // Index of the first element in the table.
2566
+ head;
2567
+ // Amount of items currently in the queue.
2568
+ _length;
2569
+ constructor(initialItems) {
2570
+ this.table = [...initialItems];
2571
+ this.head = 0;
2572
+ this._length = this.table.length;
2573
+ }
2574
+ get isEmpty() {
2575
+ return this.length == 0;
2576
+ }
2577
+ get length() {
2578
+ return this._length;
2579
+ }
2580
+ removeFirst() {
2581
+ if (this.isEmpty) {
2582
+ throw new Error('Queue is empty');
2583
+ }
2584
+ const result = this.table[this.head];
2585
+ this._length--;
2586
+ this.table[this.head] = undefined;
2587
+ this.head = (this.head + 1) % this.table.length;
2588
+ return result;
2589
+ }
2590
+ addLast(element) {
2591
+ if (this.length == this.table.length) {
2592
+ throw new Error('Queue is full');
2593
+ }
2594
+ this.table[(this.head + this._length) % this.table.length] = element;
2595
+ this._length++;
2596
+ }
2597
+ }
2598
+
2599
+ /**
2600
+ * An asynchronous semaphore implementation with associated items per lease.
2560
2601
  *
2561
2602
  * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
2562
2603
  */
2563
- class Mutex {
2564
- inCriticalSection = false;
2604
+ class Semaphore {
2605
+ // Available items that are not currently assigned to a waiter.
2606
+ available;
2607
+ size;
2565
2608
  // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
2566
2609
  // aborted waiters from the middle of the list efficiently.
2567
2610
  firstWaiter;
2568
2611
  lastWaiter;
2569
- addWaiter(onAcquire) {
2612
+ constructor(elements) {
2613
+ this.available = new Queue(elements);
2614
+ this.size = this.available.length;
2615
+ }
2616
+ addWaiter(requestedItems, onAcquire) {
2570
2617
  const node = {
2571
2618
  isActive: true,
2619
+ acquiredItems: [],
2620
+ remainingItems: requestedItems,
2572
2621
  onAcquire,
2573
2622
  prev: this.lastWaiter
2574
2623
  };
@@ -2594,52 +2643,92 @@ class Mutex {
2594
2643
  if (waiter == this.lastWaiter)
2595
2644
  this.lastWaiter = prev;
2596
2645
  }
2597
- acquire(abort) {
2646
+ requestPermits(amount, abort) {
2647
+ if (amount <= 0 || amount > this.size) {
2648
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
2649
+ }
2598
2650
  return new Promise((resolve, reject) => {
2599
2651
  function rejectAborted() {
2600
- reject(abort?.reason ?? new Error('Mutex acquire aborted'));
2652
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
2601
2653
  }
2602
2654
  if (abort?.aborted) {
2603
2655
  return rejectAborted();
2604
2656
  }
2605
- let holdsMutex = false;
2657
+ let waiter;
2606
2658
  const markCompleted = () => {
2607
- if (!holdsMutex)
2608
- return;
2609
- holdsMutex = false;
2610
- const waiter = this.firstWaiter;
2611
- if (waiter) {
2612
- this.deactivateWaiter(waiter);
2613
- // Still in critical section, but owned by next waiter now.
2614
- waiter.onAcquire();
2659
+ const items = waiter.acquiredItems;
2660
+ waiter.acquiredItems = []; // Avoid releasing items twice.
2661
+ for (const element of items) {
2662
+ // Give to next waiter, if possible.
2663
+ const nextWaiter = this.firstWaiter;
2664
+ if (nextWaiter) {
2665
+ nextWaiter.acquiredItems.push(element);
2666
+ nextWaiter.remainingItems--;
2667
+ if (nextWaiter.remainingItems == 0) {
2668
+ nextWaiter.onAcquire();
2669
+ }
2670
+ }
2671
+ else {
2672
+ // No pending waiter, return lease into pool.
2673
+ this.available.addLast(element);
2674
+ }
2615
2675
  }
2616
- else {
2617
- this.inCriticalSection = false;
2676
+ };
2677
+ const onAbort = () => {
2678
+ abort?.removeEventListener('abort', onAbort);
2679
+ if (waiter.isActive) {
2680
+ this.deactivateWaiter(waiter);
2681
+ rejectAborted();
2618
2682
  }
2619
2683
  };
2620
- if (!this.inCriticalSection) {
2621
- this.inCriticalSection = true;
2622
- holdsMutex = true;
2623
- return resolve(markCompleted);
2684
+ const resolvePromise = () => {
2685
+ this.deactivateWaiter(waiter);
2686
+ abort?.removeEventListener('abort', onAbort);
2687
+ const items = waiter.acquiredItems;
2688
+ resolve({ items, release: markCompleted });
2689
+ };
2690
+ waiter = this.addWaiter(amount, resolvePromise);
2691
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
2692
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
2693
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
2694
+ waiter.acquiredItems.push(this.available.removeFirst());
2695
+ waiter.remainingItems--;
2624
2696
  }
2625
- else {
2626
- let node;
2627
- const onAbort = () => {
2628
- abort?.removeEventListener('abort', onAbort);
2629
- if (node.isActive) {
2630
- this.deactivateWaiter(node);
2631
- rejectAborted();
2632
- }
2633
- };
2634
- node = this.addWaiter(() => {
2635
- abort?.removeEventListener('abort', onAbort);
2636
- holdsMutex = true;
2637
- resolve(markCompleted);
2638
- });
2639
- abort?.addEventListener('abort', onAbort);
2697
+ if (waiter.remainingItems == 0) {
2698
+ return resolvePromise();
2640
2699
  }
2700
+ abort?.addEventListener('abort', onAbort);
2641
2701
  });
2642
2702
  }
2703
+ /**
2704
+ * Requests a single item from the pool.
2705
+ *
2706
+ * The returned `release` callback must be invoked to return the item into the pool.
2707
+ */
2708
+ async requestOne(abort) {
2709
+ const { items, release } = await this.requestPermits(1, abort);
2710
+ return { release, item: items[0] };
2711
+ }
2712
+ /**
2713
+ * Requests access to all items from the pool.
2714
+ *
2715
+ * The returned `release` callback must be invoked to return items into the pool.
2716
+ */
2717
+ requestAll(abort) {
2718
+ return this.requestPermits(this.size, abort);
2719
+ }
2720
+ }
2721
+ /**
2722
+ * An asynchronous mutex implementation.
2723
+ *
2724
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
2725
+ */
2726
+ class Mutex {
2727
+ inner = new Semaphore([null]);
2728
+ async acquire(abort) {
2729
+ const { release } = await this.inner.requestOne(abort);
2730
+ return release;
2731
+ }
2643
2732
  async runExclusive(fn, abort) {
2644
2733
  const returnMutex = await this.acquire(abort);
2645
2734
  try {
@@ -3082,6 +3171,8 @@ var EncodingType;
3082
3171
  EncodingType["Base64"] = "base64";
3083
3172
  })(EncodingType || (EncodingType = {}));
3084
3173
 
3174
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
3175
+
3085
3176
  function getDefaultExportFromCjs (x) {
3086
3177
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
3087
3178
  }
@@ -3162,7 +3253,7 @@ function requireEventIterator () {
3162
3253
  this.removeCallback();
3163
3254
  });
3164
3255
  }
3165
- [Symbol.asyncIterator]() {
3256
+ [symbolAsyncIterator]() {
3166
3257
  return {
3167
3258
  next: (value) => {
3168
3259
  const result = this.pushQueue.shift();
@@ -3209,7 +3300,7 @@ function requireEventIterator () {
3209
3300
  queue.eventHandlers[event] = fn;
3210
3301
  },
3211
3302
  }) || (() => { });
3212
- this[Symbol.asyncIterator] = () => queue[Symbol.asyncIterator]();
3303
+ this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
3213
3304
  Object.freeze(this);
3214
3305
  }
3215
3306
  }
@@ -4091,15 +4182,6 @@ class ControlledExecutor {
4091
4182
  }
4092
4183
  }
4093
4184
 
4094
- /**
4095
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
4096
- * [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)
4097
- * we recommend for React Native.
4098
- *
4099
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
4100
- * iterators without requiring them.
4101
- */
4102
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
4103
4185
  /**
4104
4186
  * Throttle a function to be called at most once every "wait" milliseconds,
4105
4187
  * on the trailing edge.
@@ -12436,177 +12518,10 @@ function requireDist () {
12436
12518
 
12437
12519
  var distExports = requireDist();
12438
12520
 
12439
- var version = "1.50.0";
12521
+ var version = "1.52.0";
12440
12522
  var PACKAGE = {
12441
12523
  version: version};
12442
12524
 
12443
- const DEFAULT_PRESSURE_LIMITS = {
12444
- highWater: 10,
12445
- lowWater: 0
12446
- };
12447
- /**
12448
- * A very basic implementation of a data stream with backpressure support which does not use
12449
- * native JS streams or async iterators.
12450
- * This is handy for environments such as React Native which need polyfills for the above.
12451
- */
12452
- class DataStream extends BaseObserver {
12453
- options;
12454
- dataQueue;
12455
- isClosed;
12456
- processingPromise;
12457
- notifyDataAdded;
12458
- logger;
12459
- mapLine;
12460
- constructor(options) {
12461
- super();
12462
- this.options = options;
12463
- this.processingPromise = null;
12464
- this.isClosed = false;
12465
- this.dataQueue = [];
12466
- this.mapLine = options?.mapLine ?? ((line) => line);
12467
- this.logger = options?.logger ?? Logger.get('DataStream');
12468
- if (options?.closeOnError) {
12469
- const l = this.registerListener({
12470
- error: (ex) => {
12471
- l?.();
12472
- this.close();
12473
- }
12474
- });
12475
- }
12476
- }
12477
- get highWatermark() {
12478
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
12479
- }
12480
- get lowWatermark() {
12481
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
12482
- }
12483
- get closed() {
12484
- return this.isClosed;
12485
- }
12486
- async close() {
12487
- this.isClosed = true;
12488
- await this.processingPromise;
12489
- this.iterateListeners((l) => l.closed?.());
12490
- // Discard any data in the queue
12491
- this.dataQueue = [];
12492
- this.listeners.clear();
12493
- }
12494
- /**
12495
- * Enqueues data for the consumers to read
12496
- */
12497
- enqueueData(data) {
12498
- if (this.isClosed) {
12499
- throw new Error('Cannot enqueue data into closed stream.');
12500
- }
12501
- this.dataQueue.push(data);
12502
- this.notifyDataAdded?.();
12503
- this.processQueue();
12504
- }
12505
- /**
12506
- * Reads data once from the data stream
12507
- * @returns a Data payload or Null if the stream closed.
12508
- */
12509
- async read() {
12510
- if (this.closed) {
12511
- return null;
12512
- }
12513
- // Wait for any pending processing to complete first.
12514
- // This ensures we register our listener before calling processQueue(),
12515
- // avoiding a race where processQueue() sees no reader and returns early.
12516
- if (this.processingPromise) {
12517
- await this.processingPromise;
12518
- }
12519
- // Re-check after await - stream may have closed while we were waiting
12520
- if (this.closed) {
12521
- return null;
12522
- }
12523
- return new Promise((resolve, reject) => {
12524
- const l = this.registerListener({
12525
- data: async (data) => {
12526
- resolve(data);
12527
- // Remove the listener
12528
- l?.();
12529
- },
12530
- closed: () => {
12531
- resolve(null);
12532
- l?.();
12533
- },
12534
- error: (ex) => {
12535
- reject(ex);
12536
- l?.();
12537
- }
12538
- });
12539
- this.processQueue();
12540
- });
12541
- }
12542
- /**
12543
- * Executes a callback for each data item in the stream
12544
- */
12545
- forEach(callback) {
12546
- if (this.dataQueue.length <= this.lowWatermark) {
12547
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
12548
- }
12549
- return this.registerListener({
12550
- data: callback
12551
- });
12552
- }
12553
- processQueue() {
12554
- if (this.processingPromise) {
12555
- return;
12556
- }
12557
- const promise = (this.processingPromise = this._processQueue());
12558
- promise.finally(() => {
12559
- this.processingPromise = null;
12560
- });
12561
- return promise;
12562
- }
12563
- hasDataReader() {
12564
- return Array.from(this.listeners.values()).some((l) => !!l.data);
12565
- }
12566
- async _processQueue() {
12567
- /**
12568
- * Allow listeners to mutate the queue before processing.
12569
- * This allows for operations such as dropping or compressing data
12570
- * on high water or requesting more data on low water.
12571
- */
12572
- if (this.dataQueue.length >= this.highWatermark) {
12573
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
12574
- }
12575
- if (this.isClosed || !this.hasDataReader()) {
12576
- return;
12577
- }
12578
- if (this.dataQueue.length) {
12579
- const data = this.dataQueue.shift();
12580
- const mapped = this.mapLine(data);
12581
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
12582
- }
12583
- if (this.dataQueue.length <= this.lowWatermark) {
12584
- const dataAdded = new Promise((resolve) => {
12585
- this.notifyDataAdded = resolve;
12586
- });
12587
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
12588
- this.notifyDataAdded = null;
12589
- }
12590
- if (this.dataQueue.length > 0) {
12591
- setTimeout(() => this.processQueue());
12592
- }
12593
- }
12594
- async iterateAsyncErrored(cb) {
12595
- // Important: We need to copy the listeners, as calling a listener could result in adding another
12596
- // listener, resulting in infinite loops.
12597
- const listeners = Array.from(this.listeners.values());
12598
- for (let i of listeners) {
12599
- try {
12600
- await cb(i);
12601
- }
12602
- catch (ex) {
12603
- this.logger.error(ex);
12604
- this.iterateListeners((l) => l.error?.(ex));
12605
- }
12606
- }
12607
- }
12608
- }
12609
-
12610
12525
  var WebsocketDuplexConnection = {};
12611
12526
 
12612
12527
  var hasRequiredWebsocketDuplexConnection;
@@ -12769,8 +12684,215 @@ class WebsocketClientTransport {
12769
12684
  }
12770
12685
  }
12771
12686
 
12687
+ const doneResult = { done: true, value: undefined };
12688
+ function valueResult(value) {
12689
+ return { done: false, value };
12690
+ }
12691
+ /**
12692
+ * A variant of {@link Array.map} for async iterators.
12693
+ */
12694
+ function map(source, map) {
12695
+ return {
12696
+ next: async () => {
12697
+ const value = await source.next();
12698
+ if (value.done) {
12699
+ return value;
12700
+ }
12701
+ else {
12702
+ return { value: map(value.value) };
12703
+ }
12704
+ }
12705
+ };
12706
+ }
12707
+ /**
12708
+ * Expands a source async iterator by allowing to inject events asynchronously.
12709
+ *
12710
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
12711
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
12712
+ *
12713
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
12714
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
12715
+ */
12716
+ function injectable(source) {
12717
+ let sourceIsDone = false;
12718
+ let waiter = undefined; // An active, waiting next() call.
12719
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
12720
+ let pendingSourceEvent = null;
12721
+ let pendingInjectedEvents = [];
12722
+ const consumeWaiter = () => {
12723
+ const pending = waiter;
12724
+ waiter = undefined;
12725
+ return pending;
12726
+ };
12727
+ const fetchFromSource = () => {
12728
+ const resolveWaiter = (propagate) => {
12729
+ const active = consumeWaiter();
12730
+ if (active) {
12731
+ propagate(active);
12732
+ }
12733
+ else {
12734
+ pendingSourceEvent = propagate;
12735
+ }
12736
+ };
12737
+ const nextFromSource = source.next();
12738
+ nextFromSource.then((value) => {
12739
+ sourceIsDone = value.done == true;
12740
+ resolveWaiter((w) => w.resolve(value));
12741
+ }, (error) => {
12742
+ resolveWaiter((w) => w.reject(error));
12743
+ });
12744
+ };
12745
+ return {
12746
+ next: () => {
12747
+ return new Promise((resolve, reject) => {
12748
+ // First priority: Dispatch ready upstream events.
12749
+ if (sourceIsDone) {
12750
+ return resolve(doneResult);
12751
+ }
12752
+ if (pendingSourceEvent) {
12753
+ pendingSourceEvent({ resolve, reject });
12754
+ pendingSourceEvent = null;
12755
+ return;
12756
+ }
12757
+ // Second priority: Dispatch injected events
12758
+ if (pendingInjectedEvents.length) {
12759
+ return resolve(valueResult(pendingInjectedEvents.shift()));
12760
+ }
12761
+ // Nothing pending? Fetch from source
12762
+ waiter = { resolve, reject };
12763
+ return fetchFromSource();
12764
+ });
12765
+ },
12766
+ inject: (event) => {
12767
+ const pending = consumeWaiter();
12768
+ if (pending != null) {
12769
+ pending.resolve(valueResult(event));
12770
+ }
12771
+ else {
12772
+ pendingInjectedEvents.push(event);
12773
+ }
12774
+ }
12775
+ };
12776
+ }
12777
+ /**
12778
+ * Splits a byte stream at line endings, emitting each line as a string.
12779
+ */
12780
+ function extractJsonLines(source, decoder) {
12781
+ let buffer = '';
12782
+ const pendingLines = [];
12783
+ let isFinalEvent = false;
12784
+ return {
12785
+ next: async () => {
12786
+ while (true) {
12787
+ if (isFinalEvent) {
12788
+ return doneResult;
12789
+ }
12790
+ {
12791
+ const first = pendingLines.shift();
12792
+ if (first) {
12793
+ return { done: false, value: first };
12794
+ }
12795
+ }
12796
+ const { done, value } = await source.next();
12797
+ if (done) {
12798
+ const remaining = buffer.trim();
12799
+ if (remaining.length != 0) {
12800
+ isFinalEvent = true;
12801
+ return { done: false, value: remaining };
12802
+ }
12803
+ return doneResult;
12804
+ }
12805
+ const data = decoder.decode(value, { stream: true });
12806
+ buffer += data;
12807
+ const lines = buffer.split('\n');
12808
+ for (let i = 0; i < lines.length - 1; i++) {
12809
+ const l = lines[i].trim();
12810
+ if (l.length > 0) {
12811
+ pendingLines.push(l);
12812
+ }
12813
+ }
12814
+ buffer = lines[lines.length - 1];
12815
+ }
12816
+ }
12817
+ };
12818
+ }
12819
+ /**
12820
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
12821
+ */
12822
+ function extractBsonObjects(source) {
12823
+ // Fully read but not emitted yet.
12824
+ const completedObjects = [];
12825
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
12826
+ let isDone = false;
12827
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
12828
+ let objectBody = null;
12829
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
12830
+ // If we're consuming a document, the bytes remaining.
12831
+ let remainingLength = 4;
12832
+ return {
12833
+ async next() {
12834
+ while (true) {
12835
+ // Before fetching new data from upstream, return completed objects.
12836
+ if (completedObjects.length) {
12837
+ return valueResult(completedObjects.shift());
12838
+ }
12839
+ if (isDone) {
12840
+ return doneResult;
12841
+ }
12842
+ const upstreamEvent = await source.next();
12843
+ if (upstreamEvent.done) {
12844
+ isDone = true;
12845
+ if (objectBody || remainingLength != 4) {
12846
+ throw new Error('illegal end of stream in BSON object');
12847
+ }
12848
+ return doneResult;
12849
+ }
12850
+ const chunk = upstreamEvent.value;
12851
+ for (let i = 0; i < chunk.length;) {
12852
+ const availableInData = chunk.length - i;
12853
+ if (objectBody) {
12854
+ // We're in the middle of reading a BSON document.
12855
+ const bytesToRead = Math.min(availableInData, remainingLength);
12856
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
12857
+ objectBody.set(copySource, objectBody.length - remainingLength);
12858
+ i += bytesToRead;
12859
+ remainingLength -= bytesToRead;
12860
+ if (remainingLength == 0) {
12861
+ completedObjects.push(objectBody);
12862
+ // Prepare to read another document, starting with its length
12863
+ objectBody = null;
12864
+ remainingLength = 4;
12865
+ }
12866
+ }
12867
+ else {
12868
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
12869
+ const bytesToRead = Math.min(availableInData, remainingLength);
12870
+ for (let j = 0; j < bytesToRead; j++) {
12871
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
12872
+ }
12873
+ i += bytesToRead;
12874
+ remainingLength -= bytesToRead;
12875
+ if (remainingLength == 0) {
12876
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
12877
+ // header is included in length.
12878
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
12879
+ remainingLength = length - 4;
12880
+ if (remainingLength < 1) {
12881
+ throw new Error(`invalid length for bson: ${length}`);
12882
+ }
12883
+ objectBody = new Uint8Array(length);
12884
+ new DataView(objectBody.buffer).setInt32(0, length, true);
12885
+ }
12886
+ }
12887
+ }
12888
+ }
12889
+ }
12890
+ };
12891
+ }
12892
+
12772
12893
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
12773
12894
  const POWERSYNC_JS_VERSION = PACKAGE.version;
12895
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
12774
12896
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
12775
12897
  // Keep alive message is sent every period
12776
12898
  const KEEP_ALIVE_MS = 20_000;
@@ -12950,13 +13072,14 @@ class AbstractRemote {
12950
13072
  return new WebSocket(url);
12951
13073
  }
12952
13074
  /**
12953
- * Returns a data stream of sync line data.
13075
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
13076
+ *
13077
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
12954
13078
  *
12955
- * @param map Maps received payload frames to the typed event value.
12956
13079
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
12957
13080
  * (required for compatibility with older sync services).
12958
13081
  */
12959
- async socketStreamRaw(options, map, bson) {
13082
+ async socketStreamRaw(options, bson) {
12960
13083
  const { path, fetchStrategy = FetchStrategy.Buffered } = options;
12961
13084
  const mimeType = bson == null ? 'application/json' : 'application/bson';
12962
13085
  function toBuffer(js) {
@@ -12971,52 +13094,55 @@ class AbstractRemote {
12971
13094
  }
12972
13095
  const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
12973
13096
  const request = await this.buildRequest(path);
13097
+ const url = this.options.socketUrlTransformer(request.url);
12974
13098
  // Add the user agent in the setup payload - we can't set custom
12975
13099
  // headers with websockets on web. The browser userAgent is however added
12976
13100
  // automatically as a header.
12977
13101
  const userAgent = this.getUserAgent();
12978
- const stream = new DataStream({
12979
- logger: this.logger,
12980
- pressure: {
12981
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
12982
- },
12983
- mapLine: map
12984
- });
13102
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
13103
+ // to abort the connection.
13104
+ let pendingSocket = null;
13105
+ let keepAliveTimeout;
13106
+ let rsocket = null;
13107
+ let queue = null;
13108
+ let didClose = false;
13109
+ const abortRequest = () => {
13110
+ if (didClose) {
13111
+ return;
13112
+ }
13113
+ didClose = true;
13114
+ clearTimeout(keepAliveTimeout);
13115
+ if (pendingSocket) {
13116
+ pendingSocket.close();
13117
+ }
13118
+ if (rsocket) {
13119
+ rsocket.close();
13120
+ }
13121
+ if (queue) {
13122
+ queue.stop();
13123
+ }
13124
+ };
12985
13125
  // Handle upstream abort
12986
- if (options.abortSignal?.aborted) {
13126
+ if (options.abortSignal.aborted) {
12987
13127
  throw new AbortOperation('Connection request aborted');
12988
13128
  }
12989
13129
  else {
12990
- options.abortSignal?.addEventListener('abort', () => {
12991
- stream.close();
12992
- }, { once: true });
13130
+ options.abortSignal.addEventListener('abort', abortRequest);
12993
13131
  }
12994
- let keepAliveTimeout;
12995
13132
  const resetTimeout = () => {
12996
13133
  clearTimeout(keepAliveTimeout);
12997
13134
  keepAliveTimeout = setTimeout(() => {
12998
13135
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
12999
- stream.close();
13136
+ abortRequest();
13000
13137
  }, SOCKET_TIMEOUT_MS);
13001
13138
  };
13002
13139
  resetTimeout();
13003
- // Typescript complains about this being `never` if it's not assigned here.
13004
- // This is assigned in `wsCreator`.
13005
- let disposeSocketConnectionTimeout = () => { };
13006
- const url = this.options.socketUrlTransformer(request.url);
13007
13140
  const connector = new distExports.RSocketConnector({
13008
13141
  transport: new WebsocketClientTransport({
13009
13142
  url,
13010
13143
  wsCreator: (url) => {
13011
- const socket = this.createSocket(url);
13012
- disposeSocketConnectionTimeout = stream.registerListener({
13013
- closed: () => {
13014
- // Allow closing the underlying WebSocket if the stream was closed before the
13015
- // RSocket connect completed. This should effectively abort the request.
13016
- socket.close();
13017
- }
13018
- });
13019
- socket.addEventListener('message', (event) => {
13144
+ const socket = (pendingSocket = this.createSocket(url));
13145
+ socket.addEventListener('message', () => {
13020
13146
  resetTimeout();
13021
13147
  });
13022
13148
  return socket;
@@ -13036,43 +13162,40 @@ class AbstractRemote {
13036
13162
  }
13037
13163
  }
13038
13164
  });
13039
- let rsocket;
13040
13165
  try {
13041
13166
  rsocket = await connector.connect();
13042
13167
  // The connection is established, we no longer need to monitor the initial timeout
13043
- disposeSocketConnectionTimeout();
13168
+ pendingSocket = null;
13044
13169
  }
13045
13170
  catch (ex) {
13046
13171
  this.logger.error(`Failed to connect WebSocket`, ex);
13047
- clearTimeout(keepAliveTimeout);
13048
- if (!stream.closed) {
13049
- await stream.close();
13050
- }
13172
+ abortRequest();
13051
13173
  throw ex;
13052
13174
  }
13053
13175
  resetTimeout();
13054
- let socketIsClosed = false;
13055
- const closeSocket = () => {
13056
- clearTimeout(keepAliveTimeout);
13057
- if (socketIsClosed) {
13058
- return;
13059
- }
13060
- socketIsClosed = true;
13061
- rsocket.close();
13062
- };
13063
13176
  // Helps to prevent double close scenarios
13064
- rsocket.onClose(() => (socketIsClosed = true));
13065
- // We initially request this amount and expect these to arrive eventually
13066
- let pendingEventsCount = syncQueueRequestSize;
13067
- const disposeClosedListener = stream.registerListener({
13068
- closed: () => {
13069
- closeSocket();
13070
- disposeClosedListener();
13071
- }
13072
- });
13073
- const socket = await new Promise((resolve, reject) => {
13177
+ rsocket.onClose(() => (rsocket = null));
13178
+ return await new Promise((resolve, reject) => {
13074
13179
  let connectionEstablished = false;
13075
- const res = rsocket.requestStream({
13180
+ let pendingEventsCount = syncQueueRequestSize;
13181
+ let paused = false;
13182
+ let res = null;
13183
+ function requestMore() {
13184
+ const delta = syncQueueRequestSize - pendingEventsCount;
13185
+ if (!paused && delta > 0) {
13186
+ res?.request(delta);
13187
+ pendingEventsCount = syncQueueRequestSize;
13188
+ }
13189
+ }
13190
+ const events = new domExports.EventIterator((q) => {
13191
+ queue = q;
13192
+ q.on('highWater', () => (paused = true));
13193
+ q.on('lowWater', () => {
13194
+ paused = false;
13195
+ requestMore();
13196
+ });
13197
+ }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[symbolAsyncIterator]();
13198
+ res = rsocket.requestStream({
13076
13199
  data: toBuffer(options.data),
13077
13200
  metadata: toBuffer({
13078
13201
  path
@@ -13097,7 +13220,7 @@ class AbstractRemote {
13097
13220
  }
13098
13221
  // RSocket will close the RSocket stream automatically
13099
13222
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
13100
- stream.close();
13223
+ abortRequest();
13101
13224
  // Handles cases where the connection failed e.g. auth error or connection error
13102
13225
  if (!connectionEstablished) {
13103
13226
  reject(e);
@@ -13107,41 +13230,40 @@ class AbstractRemote {
13107
13230
  // The connection is active
13108
13231
  if (!connectionEstablished) {
13109
13232
  connectionEstablished = true;
13110
- resolve(res);
13233
+ resolve(events);
13111
13234
  }
13112
13235
  const { data } = payload;
13236
+ if (data) {
13237
+ queue.push(data);
13238
+ }
13113
13239
  // Less events are now pending
13114
13240
  pendingEventsCount--;
13115
- if (!data) {
13116
- return;
13117
- }
13118
- stream.enqueueData(data);
13241
+ // Request another event (unless the downstream consumer is paused).
13242
+ requestMore();
13119
13243
  },
13120
13244
  onComplete: () => {
13121
- stream.close();
13245
+ abortRequest(); // this will also emit a done event
13122
13246
  },
13123
13247
  onExtension: () => { }
13124
13248
  });
13125
13249
  });
13126
- const l = stream.registerListener({
13127
- lowWater: async () => {
13128
- // Request to fill up the queue
13129
- const required = syncQueueRequestSize - pendingEventsCount;
13130
- if (required > 0) {
13131
- socket.request(syncQueueRequestSize - pendingEventsCount);
13132
- pendingEventsCount = syncQueueRequestSize;
13133
- }
13134
- },
13135
- closed: () => {
13136
- l();
13137
- }
13138
- });
13139
- return stream;
13140
13250
  }
13141
13251
  /**
13142
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
13252
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
13253
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
13254
+ *
13255
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
13143
13256
  */
13144
- async postStreamRaw(options, mapLine) {
13257
+ get supportsStreamingBinaryResponses() {
13258
+ return true;
13259
+ }
13260
+ /**
13261
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
13262
+ * async iterator of byte blobs.
13263
+ *
13264
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
13265
+ */
13266
+ async fetchStreamRaw(options) {
13145
13267
  const { data, path, headers, abortSignal } = options;
13146
13268
  const request = await this.buildRequest(path);
13147
13269
  /**
@@ -13153,119 +13275,94 @@ class AbstractRemote {
13153
13275
  * Aborting the active fetch request while it is being consumed seems to throw
13154
13276
  * an unhandled exception on the window level.
13155
13277
  */
13156
- if (abortSignal?.aborted) {
13157
- throw new AbortOperation('Abort request received before making postStreamRaw request');
13278
+ if (abortSignal.aborted) {
13279
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
13158
13280
  }
13159
13281
  const controller = new AbortController();
13160
- let requestResolved = false;
13161
- abortSignal?.addEventListener('abort', () => {
13162
- if (!requestResolved) {
13282
+ let reader = null;
13283
+ abortSignal.addEventListener('abort', () => {
13284
+ const reason = abortSignal.reason ??
13285
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
13286
+ if (reader == null) {
13163
13287
  // Only abort via the abort controller if the request has not resolved yet
13164
- controller.abort(abortSignal.reason ??
13165
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.'));
13288
+ controller.abort(reason);
13289
+ }
13290
+ else {
13291
+ reader.cancel(reason).catch(() => {
13292
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
13293
+ // ignore it here.
13294
+ });
13166
13295
  }
13167
13296
  });
13168
- const res = await this.fetch(request.url, {
13169
- method: 'POST',
13170
- headers: { ...headers, ...request.headers },
13171
- body: JSON.stringify(data),
13172
- signal: controller.signal,
13173
- cache: 'no-store',
13174
- ...(this.options.fetchOptions ?? {}),
13175
- ...options.fetchOptions
13176
- }).catch((ex) => {
13297
+ let res;
13298
+ let responseIsBson = false;
13299
+ try {
13300
+ const ndJson = 'application/x-ndjson';
13301
+ const bson = 'application/vnd.powersync.bson-stream';
13302
+ res = await this.fetch(request.url, {
13303
+ method: 'POST',
13304
+ headers: {
13305
+ ...headers,
13306
+ ...request.headers,
13307
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
13308
+ },
13309
+ body: JSON.stringify(data),
13310
+ signal: controller.signal,
13311
+ cache: 'no-store',
13312
+ ...(this.options.fetchOptions ?? {}),
13313
+ ...options.fetchOptions
13314
+ });
13315
+ if (!res.ok || !res.body) {
13316
+ const text = await res.text();
13317
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
13318
+ const error = new Error(`HTTP ${res.statusText}: ${text}`);
13319
+ error.status = res.status;
13320
+ throw error;
13321
+ }
13322
+ const contentType = res.headers.get('content-type');
13323
+ responseIsBson = contentType == bson;
13324
+ }
13325
+ catch (ex) {
13177
13326
  if (ex.name == 'AbortError') {
13178
13327
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
13179
13328
  }
13180
13329
  throw ex;
13181
- });
13182
- if (!res) {
13183
- throw new Error('Fetch request was aborted');
13184
- }
13185
- requestResolved = true;
13186
- if (!res.ok || !res.body) {
13187
- const text = await res.text();
13188
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
13189
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
13190
- error.status = res.status;
13191
- throw error;
13192
13330
  }
13193
- // Create a new stream splitting the response at line endings while also handling cancellations
13194
- // by closing the reader.
13195
- const reader = res.body.getReader();
13196
- let readerReleased = false;
13197
- // This will close the network request and read stream
13198
- const closeReader = async () => {
13199
- try {
13200
- readerReleased = true;
13201
- await reader.cancel();
13202
- }
13203
- catch (ex) {
13204
- // an error will throw if the reader hasn't been used yet
13205
- }
13206
- reader.releaseLock();
13207
- };
13208
- const stream = new DataStream({
13209
- logger: this.logger,
13210
- mapLine: mapLine,
13211
- pressure: {
13212
- highWaterMark: 20,
13213
- lowWaterMark: 10
13214
- }
13215
- });
13216
- abortSignal?.addEventListener('abort', () => {
13217
- closeReader();
13218
- stream.close();
13219
- });
13220
- const decoder = this.createTextDecoder();
13221
- let buffer = '';
13222
- const consumeStream = async () => {
13223
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
13224
- const { done, value } = await reader.read();
13225
- if (done) {
13226
- const remaining = buffer.trim();
13227
- if (remaining.length != 0) {
13228
- stream.enqueueData(remaining);
13229
- }
13230
- stream.close();
13231
- await closeReader();
13232
- return;
13331
+ reader = res.body.getReader();
13332
+ const stream = {
13333
+ next: async () => {
13334
+ if (controller.signal.aborted) {
13335
+ return doneResult;
13233
13336
  }
13234
- const data = decoder.decode(value, { stream: true });
13235
- buffer += data;
13236
- const lines = buffer.split('\n');
13237
- for (var i = 0; i < lines.length - 1; i++) {
13238
- var l = lines[i].trim();
13239
- if (l.length > 0) {
13240
- stream.enqueueData(l);
13241
- }
13337
+ try {
13338
+ return await reader.read();
13242
13339
  }
13243
- buffer = lines[lines.length - 1];
13244
- // Implement backpressure by waiting for the low water mark to be reached
13245
- if (stream.dataQueue.length > stream.highWatermark) {
13246
- await new Promise((resolve) => {
13247
- const dispose = stream.registerListener({
13248
- lowWater: async () => {
13249
- resolve();
13250
- dispose();
13251
- },
13252
- closed: () => {
13253
- resolve();
13254
- dispose();
13255
- }
13256
- });
13257
- });
13340
+ catch (ex) {
13341
+ if (controller.signal.aborted) {
13342
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
13343
+ // things working as intended, we can return a done event and consider the exception handled.
13344
+ return doneResult;
13345
+ }
13346
+ throw ex;
13258
13347
  }
13259
13348
  }
13260
13349
  };
13261
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
13262
- const l = stream.registerListener({
13263
- closed: () => {
13264
- closeReader();
13265
- l?.();
13266
- }
13267
- });
13268
- return stream;
13350
+ return { isBson: responseIsBson, stream };
13351
+ }
13352
+ /**
13353
+ * Posts a `/sync/stream` request.
13354
+ *
13355
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
13356
+ * {@link Uint8Array}s.
13357
+ */
13358
+ async fetchStream(options) {
13359
+ const { isBson, stream } = await this.fetchStreamRaw(options);
13360
+ if (isBson) {
13361
+ return extractBsonObjects(stream);
13362
+ }
13363
+ else {
13364
+ return extractJsonLines(stream, this.createTextDecoder());
13365
+ }
13269
13366
  }
13270
13367
  }
13271
13368
 
@@ -13773,6 +13870,19 @@ The next upload iteration will be delayed.`);
13773
13870
  }
13774
13871
  });
13775
13872
  }
13873
+ async receiveSyncLines(data) {
13874
+ const { options, connection, bson } = data;
13875
+ const remote = this.options.remote;
13876
+ if (connection.connectionMethod == SyncStreamConnectionMethod.HTTP) {
13877
+ return await remote.fetchStream(options);
13878
+ }
13879
+ else {
13880
+ return await this.options.remote.socketStreamRaw({
13881
+ ...options,
13882
+ ...{ fetchStrategy: connection.fetchStrategy }
13883
+ }, bson);
13884
+ }
13885
+ }
13776
13886
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
13777
13887
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
13778
13888
  if (rawTables != null && rawTables.length) {
@@ -13802,42 +13912,27 @@ The next upload iteration will be delayed.`);
13802
13912
  client_id: clientId
13803
13913
  }
13804
13914
  };
13805
- let stream;
13806
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
13807
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
13808
- if (typeof line == 'string') {
13809
- return JSON.parse(line);
13810
- }
13811
- else {
13812
- // Directly enqueued by us
13813
- return line;
13814
- }
13815
- });
13816
- }
13817
- else {
13818
- const bson = await this.options.remote.getBSON();
13819
- stream = await this.options.remote.socketStreamRaw({
13820
- ...syncOptions,
13821
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
13822
- }, (payload) => {
13823
- if (payload instanceof Uint8Array) {
13824
- return bson.deserialize(payload);
13825
- }
13826
- else {
13827
- // Directly enqueued by us
13828
- return payload;
13829
- }
13830
- }, bson);
13831
- }
13915
+ const bson = await this.options.remote.getBSON();
13916
+ const source = await this.receiveSyncLines({
13917
+ options: syncOptions,
13918
+ connection: resolvedOptions,
13919
+ bson
13920
+ });
13921
+ const stream = injectable(map(source, (line) => {
13922
+ if (typeof line == 'string') {
13923
+ return JSON.parse(line);
13924
+ }
13925
+ else {
13926
+ return bson.deserialize(line);
13927
+ }
13928
+ }));
13832
13929
  this.logger.debug('Stream established. Processing events');
13833
13930
  this.notifyCompletedUploads = () => {
13834
- if (!stream.closed) {
13835
- stream.enqueueData({ crud_upload_completed: null });
13836
- }
13931
+ stream.inject({ crud_upload_completed: null });
13837
13932
  };
13838
- while (!stream.closed) {
13839
- const line = await stream.read();
13840
- if (!line) {
13933
+ while (true) {
13934
+ const { value: line, done } = await stream.next();
13935
+ if (done) {
13841
13936
  // The stream has closed while waiting
13842
13937
  return;
13843
13938
  }
@@ -14016,14 +14111,17 @@ The next upload iteration will be delayed.`);
14016
14111
  const syncImplementation = this;
14017
14112
  const adapter = this.options.adapter;
14018
14113
  const remote = this.options.remote;
14114
+ const controller = new AbortController();
14115
+ const abort = () => {
14116
+ return controller.abort(signal.reason);
14117
+ };
14118
+ signal.addEventListener('abort', abort);
14019
14119
  let receivingLines = null;
14020
14120
  let hadSyncLine = false;
14021
14121
  let hideDisconnectOnRestart = false;
14022
14122
  if (signal.aborted) {
14023
14123
  throw new AbortOperation('Connection request has been aborted');
14024
14124
  }
14025
- const abortController = new AbortController();
14026
- signal.addEventListener('abort', () => abortController.abort());
14027
14125
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
14028
14126
  // invocation (local events include refreshed tokens and completed uploads).
14029
14127
  // This is a single data stream so that we can handle all control calls from a single place.
@@ -14031,49 +14129,36 @@ The next upload iteration will be delayed.`);
14031
14129
  async function connect(instr) {
14032
14130
  const syncOptions = {
14033
14131
  path: '/sync/stream',
14034
- abortSignal: abortController.signal,
14132
+ abortSignal: controller.signal,
14035
14133
  data: instr.request
14036
14134
  };
14037
- if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
14038
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
14039
- if (typeof line == 'string') {
14040
- return {
14041
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
14042
- payload: line
14043
- };
14044
- }
14045
- else {
14046
- // Directly enqueued by us
14047
- return line;
14048
- }
14049
- });
14050
- }
14051
- else {
14052
- controlInvocations = await remote.socketStreamRaw({
14053
- ...syncOptions,
14054
- fetchStrategy: resolvedOptions.fetchStrategy
14055
- }, (payload) => {
14056
- if (payload instanceof Uint8Array) {
14057
- return {
14058
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
14059
- payload: payload
14060
- };
14061
- }
14062
- else {
14063
- // Directly enqueued by us
14064
- return payload;
14065
- }
14066
- });
14067
- }
14135
+ controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
14136
+ options: syncOptions,
14137
+ connection: resolvedOptions
14138
+ }), (line) => {
14139
+ if (typeof line == 'string') {
14140
+ return {
14141
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
14142
+ payload: line
14143
+ };
14144
+ }
14145
+ else {
14146
+ return {
14147
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
14148
+ payload: line
14149
+ };
14150
+ }
14151
+ }));
14068
14152
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
14069
14153
  // we're already connected here and can report that.
14070
14154
  syncImplementation.updateSyncStatus({ connected: true });
14071
14155
  try {
14072
- while (!controlInvocations.closed) {
14073
- const line = await controlInvocations.read();
14074
- if (line == null) {
14075
- return;
14156
+ while (true) {
14157
+ let event = await controlInvocations.next();
14158
+ if (event.done) {
14159
+ break;
14076
14160
  }
14161
+ const line = event.value;
14077
14162
  await control(line.command, line.payload);
14078
14163
  if (!hadSyncLine) {
14079
14164
  syncImplementation.triggerCrudUpload();
@@ -14082,12 +14167,8 @@ The next upload iteration will be delayed.`);
14082
14167
  }
14083
14168
  }
14084
14169
  finally {
14085
- const activeInstructions = controlInvocations;
14086
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
14087
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
14088
- // to null.
14089
- controlInvocations = null;
14090
- await activeInstructions.close();
14170
+ abort();
14171
+ signal.removeEventListener('abort', abort);
14091
14172
  }
14092
14173
  }
14093
14174
  async function stop() {
@@ -14131,14 +14212,14 @@ The next upload iteration will be delayed.`);
14131
14212
  remote.invalidateCredentials();
14132
14213
  // Restart iteration after the credentials have been refreshed.
14133
14214
  remote.fetchCredentials().then((_) => {
14134
- controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
14215
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
14135
14216
  }, (err) => {
14136
14217
  syncImplementation.logger.warn('Could not prefetch credentials', err);
14137
14218
  });
14138
14219
  }
14139
14220
  }
14140
14221
  else if ('CloseSyncStream' in instruction) {
14141
- abortController.abort();
14222
+ controller.abort();
14142
14223
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
14143
14224
  }
14144
14225
  else if ('FlushFileSystem' in instruction) ;
@@ -14167,17 +14248,13 @@ The next upload iteration will be delayed.`);
14167
14248
  }
14168
14249
  await control(PowerSyncControlCommand.START, JSON.stringify(options));
14169
14250
  this.notifyCompletedUploads = () => {
14170
- if (controlInvocations && !controlInvocations?.closed) {
14171
- controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
14172
- }
14251
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
14173
14252
  };
14174
14253
  this.handleActiveStreamsChange = () => {
14175
- if (controlInvocations && !controlInvocations?.closed) {
14176
- controlInvocations.enqueueData({
14177
- command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
14178
- payload: JSON.stringify(this.activeStreams)
14179
- });
14180
- }
14254
+ controlInvocations?.inject({
14255
+ command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
14256
+ payload: JSON.stringify(this.activeStreams)
14257
+ });
14181
14258
  };
14182
14259
  await receivingLines;
14183
14260
  }