@powersync/web 1.37.1 → 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),
@@ -3173,6 +3171,8 @@ var EncodingType;
3173
3171
  EncodingType["Base64"] = "base64";
3174
3172
  })(EncodingType || (EncodingType = {}));
3175
3173
 
3174
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
3175
+
3176
3176
  function getDefaultExportFromCjs (x) {
3177
3177
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
3178
3178
  }
@@ -3253,7 +3253,7 @@ function requireEventIterator () {
3253
3253
  this.removeCallback();
3254
3254
  });
3255
3255
  }
3256
- [Symbol.asyncIterator]() {
3256
+ [symbolAsyncIterator]() {
3257
3257
  return {
3258
3258
  next: (value) => {
3259
3259
  const result = this.pushQueue.shift();
@@ -3300,7 +3300,7 @@ function requireEventIterator () {
3300
3300
  queue.eventHandlers[event] = fn;
3301
3301
  },
3302
3302
  }) || (() => { });
3303
- this[Symbol.asyncIterator] = () => queue[Symbol.asyncIterator]();
3303
+ this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
3304
3304
  Object.freeze(this);
3305
3305
  }
3306
3306
  }
@@ -4182,15 +4182,6 @@ class ControlledExecutor {
4182
4182
  }
4183
4183
  }
4184
4184
 
4185
- /**
4186
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
4187
- * [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)
4188
- * we recommend for React Native.
4189
- *
4190
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
4191
- * iterators without requiring them.
4192
- */
4193
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
4194
4185
  /**
4195
4186
  * Throttle a function to be called at most once every "wait" milliseconds,
4196
4187
  * on the trailing edge.
@@ -12527,177 +12518,10 @@ function requireDist () {
12527
12518
 
12528
12519
  var distExports = requireDist();
12529
12520
 
12530
- var version = "1.51.0";
12521
+ var version = "1.52.0";
12531
12522
  var PACKAGE = {
12532
12523
  version: version};
12533
12524
 
12534
- const DEFAULT_PRESSURE_LIMITS = {
12535
- highWater: 10,
12536
- lowWater: 0
12537
- };
12538
- /**
12539
- * A very basic implementation of a data stream with backpressure support which does not use
12540
- * native JS streams or async iterators.
12541
- * This is handy for environments such as React Native which need polyfills for the above.
12542
- */
12543
- class DataStream extends BaseObserver {
12544
- options;
12545
- dataQueue;
12546
- isClosed;
12547
- processingPromise;
12548
- notifyDataAdded;
12549
- logger;
12550
- mapLine;
12551
- constructor(options) {
12552
- super();
12553
- this.options = options;
12554
- this.processingPromise = null;
12555
- this.isClosed = false;
12556
- this.dataQueue = [];
12557
- this.mapLine = options?.mapLine ?? ((line) => line);
12558
- this.logger = options?.logger ?? Logger.get('DataStream');
12559
- if (options?.closeOnError) {
12560
- const l = this.registerListener({
12561
- error: (ex) => {
12562
- l?.();
12563
- this.close();
12564
- }
12565
- });
12566
- }
12567
- }
12568
- get highWatermark() {
12569
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
12570
- }
12571
- get lowWatermark() {
12572
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
12573
- }
12574
- get closed() {
12575
- return this.isClosed;
12576
- }
12577
- async close() {
12578
- this.isClosed = true;
12579
- await this.processingPromise;
12580
- this.iterateListeners((l) => l.closed?.());
12581
- // Discard any data in the queue
12582
- this.dataQueue = [];
12583
- this.listeners.clear();
12584
- }
12585
- /**
12586
- * Enqueues data for the consumers to read
12587
- */
12588
- enqueueData(data) {
12589
- if (this.isClosed) {
12590
- throw new Error('Cannot enqueue data into closed stream.');
12591
- }
12592
- this.dataQueue.push(data);
12593
- this.notifyDataAdded?.();
12594
- this.processQueue();
12595
- }
12596
- /**
12597
- * Reads data once from the data stream
12598
- * @returns a Data payload or Null if the stream closed.
12599
- */
12600
- async read() {
12601
- if (this.closed) {
12602
- return null;
12603
- }
12604
- // Wait for any pending processing to complete first.
12605
- // This ensures we register our listener before calling processQueue(),
12606
- // avoiding a race where processQueue() sees no reader and returns early.
12607
- if (this.processingPromise) {
12608
- await this.processingPromise;
12609
- }
12610
- // Re-check after await - stream may have closed while we were waiting
12611
- if (this.closed) {
12612
- return null;
12613
- }
12614
- return new Promise((resolve, reject) => {
12615
- const l = this.registerListener({
12616
- data: async (data) => {
12617
- resolve(data);
12618
- // Remove the listener
12619
- l?.();
12620
- },
12621
- closed: () => {
12622
- resolve(null);
12623
- l?.();
12624
- },
12625
- error: (ex) => {
12626
- reject(ex);
12627
- l?.();
12628
- }
12629
- });
12630
- this.processQueue();
12631
- });
12632
- }
12633
- /**
12634
- * Executes a callback for each data item in the stream
12635
- */
12636
- forEach(callback) {
12637
- if (this.dataQueue.length <= this.lowWatermark) {
12638
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
12639
- }
12640
- return this.registerListener({
12641
- data: callback
12642
- });
12643
- }
12644
- processQueue() {
12645
- if (this.processingPromise) {
12646
- return;
12647
- }
12648
- const promise = (this.processingPromise = this._processQueue());
12649
- promise.finally(() => {
12650
- this.processingPromise = null;
12651
- });
12652
- return promise;
12653
- }
12654
- hasDataReader() {
12655
- return Array.from(this.listeners.values()).some((l) => !!l.data);
12656
- }
12657
- async _processQueue() {
12658
- /**
12659
- * Allow listeners to mutate the queue before processing.
12660
- * This allows for operations such as dropping or compressing data
12661
- * on high water or requesting more data on low water.
12662
- */
12663
- if (this.dataQueue.length >= this.highWatermark) {
12664
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
12665
- }
12666
- if (this.isClosed || !this.hasDataReader()) {
12667
- return;
12668
- }
12669
- if (this.dataQueue.length) {
12670
- const data = this.dataQueue.shift();
12671
- const mapped = this.mapLine(data);
12672
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
12673
- }
12674
- if (this.dataQueue.length <= this.lowWatermark) {
12675
- const dataAdded = new Promise((resolve) => {
12676
- this.notifyDataAdded = resolve;
12677
- });
12678
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
12679
- this.notifyDataAdded = null;
12680
- }
12681
- if (this.dataQueue.length > 0) {
12682
- setTimeout(() => this.processQueue());
12683
- }
12684
- }
12685
- async iterateAsyncErrored(cb) {
12686
- // Important: We need to copy the listeners, as calling a listener could result in adding another
12687
- // listener, resulting in infinite loops.
12688
- const listeners = Array.from(this.listeners.values());
12689
- for (let i of listeners) {
12690
- try {
12691
- await cb(i);
12692
- }
12693
- catch (ex) {
12694
- this.logger.error(ex);
12695
- this.iterateListeners((l) => l.error?.(ex));
12696
- }
12697
- }
12698
- }
12699
- }
12700
-
12701
12525
  var WebsocketDuplexConnection = {};
12702
12526
 
12703
12527
  var hasRequiredWebsocketDuplexConnection;
@@ -12860,8 +12684,215 @@ class WebsocketClientTransport {
12860
12684
  }
12861
12685
  }
12862
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
+
12863
12893
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
12864
12894
  const POWERSYNC_JS_VERSION = PACKAGE.version;
12895
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
12865
12896
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
12866
12897
  // Keep alive message is sent every period
12867
12898
  const KEEP_ALIVE_MS = 20_000;
@@ -13041,13 +13072,14 @@ class AbstractRemote {
13041
13072
  return new WebSocket(url);
13042
13073
  }
13043
13074
  /**
13044
- * 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}.
13045
13078
  *
13046
- * @param map Maps received payload frames to the typed event value.
13047
13079
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
13048
13080
  * (required for compatibility with older sync services).
13049
13081
  */
13050
- async socketStreamRaw(options, map, bson) {
13082
+ async socketStreamRaw(options, bson) {
13051
13083
  const { path, fetchStrategy = FetchStrategy.Buffered } = options;
13052
13084
  const mimeType = bson == null ? 'application/json' : 'application/bson';
13053
13085
  function toBuffer(js) {
@@ -13062,52 +13094,55 @@ class AbstractRemote {
13062
13094
  }
13063
13095
  const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
13064
13096
  const request = await this.buildRequest(path);
13097
+ const url = this.options.socketUrlTransformer(request.url);
13065
13098
  // Add the user agent in the setup payload - we can't set custom
13066
13099
  // headers with websockets on web. The browser userAgent is however added
13067
13100
  // automatically as a header.
13068
13101
  const userAgent = this.getUserAgent();
13069
- const stream = new DataStream({
13070
- logger: this.logger,
13071
- pressure: {
13072
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
13073
- },
13074
- mapLine: map
13075
- });
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
+ };
13076
13125
  // Handle upstream abort
13077
- if (options.abortSignal?.aborted) {
13126
+ if (options.abortSignal.aborted) {
13078
13127
  throw new AbortOperation('Connection request aborted');
13079
13128
  }
13080
13129
  else {
13081
- options.abortSignal?.addEventListener('abort', () => {
13082
- stream.close();
13083
- }, { once: true });
13130
+ options.abortSignal.addEventListener('abort', abortRequest);
13084
13131
  }
13085
- let keepAliveTimeout;
13086
13132
  const resetTimeout = () => {
13087
13133
  clearTimeout(keepAliveTimeout);
13088
13134
  keepAliveTimeout = setTimeout(() => {
13089
13135
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
13090
- stream.close();
13136
+ abortRequest();
13091
13137
  }, SOCKET_TIMEOUT_MS);
13092
13138
  };
13093
13139
  resetTimeout();
13094
- // Typescript complains about this being `never` if it's not assigned here.
13095
- // This is assigned in `wsCreator`.
13096
- let disposeSocketConnectionTimeout = () => { };
13097
- const url = this.options.socketUrlTransformer(request.url);
13098
13140
  const connector = new distExports.RSocketConnector({
13099
13141
  transport: new WebsocketClientTransport({
13100
13142
  url,
13101
13143
  wsCreator: (url) => {
13102
- const socket = this.createSocket(url);
13103
- disposeSocketConnectionTimeout = stream.registerListener({
13104
- closed: () => {
13105
- // Allow closing the underlying WebSocket if the stream was closed before the
13106
- // RSocket connect completed. This should effectively abort the request.
13107
- socket.close();
13108
- }
13109
- });
13110
- socket.addEventListener('message', (event) => {
13144
+ const socket = (pendingSocket = this.createSocket(url));
13145
+ socket.addEventListener('message', () => {
13111
13146
  resetTimeout();
13112
13147
  });
13113
13148
  return socket;
@@ -13127,43 +13162,40 @@ class AbstractRemote {
13127
13162
  }
13128
13163
  }
13129
13164
  });
13130
- let rsocket;
13131
13165
  try {
13132
13166
  rsocket = await connector.connect();
13133
13167
  // The connection is established, we no longer need to monitor the initial timeout
13134
- disposeSocketConnectionTimeout();
13168
+ pendingSocket = null;
13135
13169
  }
13136
13170
  catch (ex) {
13137
13171
  this.logger.error(`Failed to connect WebSocket`, ex);
13138
- clearTimeout(keepAliveTimeout);
13139
- if (!stream.closed) {
13140
- await stream.close();
13141
- }
13172
+ abortRequest();
13142
13173
  throw ex;
13143
13174
  }
13144
13175
  resetTimeout();
13145
- let socketIsClosed = false;
13146
- const closeSocket = () => {
13147
- clearTimeout(keepAliveTimeout);
13148
- if (socketIsClosed) {
13149
- return;
13150
- }
13151
- socketIsClosed = true;
13152
- rsocket.close();
13153
- };
13154
13176
  // Helps to prevent double close scenarios
13155
- rsocket.onClose(() => (socketIsClosed = true));
13156
- // We initially request this amount and expect these to arrive eventually
13157
- let pendingEventsCount = syncQueueRequestSize;
13158
- const disposeClosedListener = stream.registerListener({
13159
- closed: () => {
13160
- closeSocket();
13161
- disposeClosedListener();
13162
- }
13163
- });
13164
- const socket = await new Promise((resolve, reject) => {
13177
+ rsocket.onClose(() => (rsocket = null));
13178
+ return await new Promise((resolve, reject) => {
13165
13179
  let connectionEstablished = false;
13166
- 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({
13167
13199
  data: toBuffer(options.data),
13168
13200
  metadata: toBuffer({
13169
13201
  path
@@ -13188,7 +13220,7 @@ class AbstractRemote {
13188
13220
  }
13189
13221
  // RSocket will close the RSocket stream automatically
13190
13222
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
13191
- stream.close();
13223
+ abortRequest();
13192
13224
  // Handles cases where the connection failed e.g. auth error or connection error
13193
13225
  if (!connectionEstablished) {
13194
13226
  reject(e);
@@ -13198,41 +13230,40 @@ class AbstractRemote {
13198
13230
  // The connection is active
13199
13231
  if (!connectionEstablished) {
13200
13232
  connectionEstablished = true;
13201
- resolve(res);
13233
+ resolve(events);
13202
13234
  }
13203
13235
  const { data } = payload;
13236
+ if (data) {
13237
+ queue.push(data);
13238
+ }
13204
13239
  // Less events are now pending
13205
13240
  pendingEventsCount--;
13206
- if (!data) {
13207
- return;
13208
- }
13209
- stream.enqueueData(data);
13241
+ // Request another event (unless the downstream consumer is paused).
13242
+ requestMore();
13210
13243
  },
13211
13244
  onComplete: () => {
13212
- stream.close();
13245
+ abortRequest(); // this will also emit a done event
13213
13246
  },
13214
13247
  onExtension: () => { }
13215
13248
  });
13216
13249
  });
13217
- const l = stream.registerListener({
13218
- lowWater: async () => {
13219
- // Request to fill up the queue
13220
- const required = syncQueueRequestSize - pendingEventsCount;
13221
- if (required > 0) {
13222
- socket.request(syncQueueRequestSize - pendingEventsCount);
13223
- pendingEventsCount = syncQueueRequestSize;
13224
- }
13225
- },
13226
- closed: () => {
13227
- l();
13228
- }
13229
- });
13230
- return stream;
13231
13250
  }
13232
13251
  /**
13233
- * 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
13256
+ */
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.
13234
13265
  */
13235
- async postStreamRaw(options, mapLine) {
13266
+ async fetchStreamRaw(options) {
13236
13267
  const { data, path, headers, abortSignal } = options;
13237
13268
  const request = await this.buildRequest(path);
13238
13269
  /**
@@ -13244,119 +13275,94 @@ class AbstractRemote {
13244
13275
  * Aborting the active fetch request while it is being consumed seems to throw
13245
13276
  * an unhandled exception on the window level.
13246
13277
  */
13247
- if (abortSignal?.aborted) {
13248
- 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');
13249
13280
  }
13250
13281
  const controller = new AbortController();
13251
- let requestResolved = false;
13252
- abortSignal?.addEventListener('abort', () => {
13253
- 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) {
13254
13287
  // Only abort via the abort controller if the request has not resolved yet
13255
- controller.abort(abortSignal.reason ??
13256
- 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
+ });
13257
13295
  }
13258
13296
  });
13259
- const res = await this.fetch(request.url, {
13260
- method: 'POST',
13261
- headers: { ...headers, ...request.headers },
13262
- body: JSON.stringify(data),
13263
- signal: controller.signal,
13264
- cache: 'no-store',
13265
- ...(this.options.fetchOptions ?? {}),
13266
- ...options.fetchOptions
13267
- }).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) {
13268
13326
  if (ex.name == 'AbortError') {
13269
13327
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
13270
13328
  }
13271
13329
  throw ex;
13272
- });
13273
- if (!res) {
13274
- throw new Error('Fetch request was aborted');
13275
13330
  }
13276
- requestResolved = true;
13277
- if (!res.ok || !res.body) {
13278
- const text = await res.text();
13279
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
13280
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
13281
- error.status = res.status;
13282
- throw error;
13283
- }
13284
- // Create a new stream splitting the response at line endings while also handling cancellations
13285
- // by closing the reader.
13286
- const reader = res.body.getReader();
13287
- let readerReleased = false;
13288
- // This will close the network request and read stream
13289
- const closeReader = async () => {
13290
- try {
13291
- readerReleased = true;
13292
- await reader.cancel();
13293
- }
13294
- catch (ex) {
13295
- // an error will throw if the reader hasn't been used yet
13296
- }
13297
- reader.releaseLock();
13298
- };
13299
- const stream = new DataStream({
13300
- logger: this.logger,
13301
- mapLine: mapLine,
13302
- pressure: {
13303
- highWaterMark: 20,
13304
- lowWaterMark: 10
13305
- }
13306
- });
13307
- abortSignal?.addEventListener('abort', () => {
13308
- closeReader();
13309
- stream.close();
13310
- });
13311
- const decoder = this.createTextDecoder();
13312
- let buffer = '';
13313
- const consumeStream = async () => {
13314
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
13315
- const { done, value } = await reader.read();
13316
- if (done) {
13317
- const remaining = buffer.trim();
13318
- if (remaining.length != 0) {
13319
- stream.enqueueData(remaining);
13320
- }
13321
- stream.close();
13322
- await closeReader();
13323
- return;
13331
+ reader = res.body.getReader();
13332
+ const stream = {
13333
+ next: async () => {
13334
+ if (controller.signal.aborted) {
13335
+ return doneResult;
13324
13336
  }
13325
- const data = decoder.decode(value, { stream: true });
13326
- buffer += data;
13327
- const lines = buffer.split('\n');
13328
- for (var i = 0; i < lines.length - 1; i++) {
13329
- var l = lines[i].trim();
13330
- if (l.length > 0) {
13331
- stream.enqueueData(l);
13332
- }
13337
+ try {
13338
+ return await reader.read();
13333
13339
  }
13334
- buffer = lines[lines.length - 1];
13335
- // Implement backpressure by waiting for the low water mark to be reached
13336
- if (stream.dataQueue.length > stream.highWatermark) {
13337
- await new Promise((resolve) => {
13338
- const dispose = stream.registerListener({
13339
- lowWater: async () => {
13340
- resolve();
13341
- dispose();
13342
- },
13343
- closed: () => {
13344
- resolve();
13345
- dispose();
13346
- }
13347
- });
13348
- });
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;
13349
13347
  }
13350
13348
  }
13351
13349
  };
13352
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
13353
- const l = stream.registerListener({
13354
- closed: () => {
13355
- closeReader();
13356
- l?.();
13357
- }
13358
- });
13359
- 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
+ }
13360
13366
  }
13361
13367
  }
13362
13368
 
@@ -13864,6 +13870,19 @@ The next upload iteration will be delayed.`);
13864
13870
  }
13865
13871
  });
13866
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
+ }
13867
13886
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
13868
13887
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
13869
13888
  if (rawTables != null && rawTables.length) {
@@ -13893,42 +13912,27 @@ The next upload iteration will be delayed.`);
13893
13912
  client_id: clientId
13894
13913
  }
13895
13914
  };
13896
- let stream;
13897
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
13898
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
13899
- if (typeof line == 'string') {
13900
- return JSON.parse(line);
13901
- }
13902
- else {
13903
- // Directly enqueued by us
13904
- return line;
13905
- }
13906
- });
13907
- }
13908
- else {
13909
- const bson = await this.options.remote.getBSON();
13910
- stream = await this.options.remote.socketStreamRaw({
13911
- ...syncOptions,
13912
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
13913
- }, (payload) => {
13914
- if (payload instanceof Uint8Array) {
13915
- return bson.deserialize(payload);
13916
- }
13917
- else {
13918
- // Directly enqueued by us
13919
- return payload;
13920
- }
13921
- }, bson);
13922
- }
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
+ }));
13923
13929
  this.logger.debug('Stream established. Processing events');
13924
13930
  this.notifyCompletedUploads = () => {
13925
- if (!stream.closed) {
13926
- stream.enqueueData({ crud_upload_completed: null });
13927
- }
13931
+ stream.inject({ crud_upload_completed: null });
13928
13932
  };
13929
- while (!stream.closed) {
13930
- const line = await stream.read();
13931
- if (!line) {
13933
+ while (true) {
13934
+ const { value: line, done } = await stream.next();
13935
+ if (done) {
13932
13936
  // The stream has closed while waiting
13933
13937
  return;
13934
13938
  }
@@ -14107,14 +14111,17 @@ The next upload iteration will be delayed.`);
14107
14111
  const syncImplementation = this;
14108
14112
  const adapter = this.options.adapter;
14109
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);
14110
14119
  let receivingLines = null;
14111
14120
  let hadSyncLine = false;
14112
14121
  let hideDisconnectOnRestart = false;
14113
14122
  if (signal.aborted) {
14114
14123
  throw new AbortOperation('Connection request has been aborted');
14115
14124
  }
14116
- const abortController = new AbortController();
14117
- signal.addEventListener('abort', () => abortController.abort());
14118
14125
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
14119
14126
  // invocation (local events include refreshed tokens and completed uploads).
14120
14127
  // This is a single data stream so that we can handle all control calls from a single place.
@@ -14122,49 +14129,36 @@ The next upload iteration will be delayed.`);
14122
14129
  async function connect(instr) {
14123
14130
  const syncOptions = {
14124
14131
  path: '/sync/stream',
14125
- abortSignal: abortController.signal,
14132
+ abortSignal: controller.signal,
14126
14133
  data: instr.request
14127
14134
  };
14128
- if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
14129
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
14130
- if (typeof line == 'string') {
14131
- return {
14132
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
14133
- payload: line
14134
- };
14135
- }
14136
- else {
14137
- // Directly enqueued by us
14138
- return line;
14139
- }
14140
- });
14141
- }
14142
- else {
14143
- controlInvocations = await remote.socketStreamRaw({
14144
- ...syncOptions,
14145
- fetchStrategy: resolvedOptions.fetchStrategy
14146
- }, (payload) => {
14147
- if (payload instanceof Uint8Array) {
14148
- return {
14149
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
14150
- payload: payload
14151
- };
14152
- }
14153
- else {
14154
- // Directly enqueued by us
14155
- return payload;
14156
- }
14157
- });
14158
- }
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
+ }));
14159
14152
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
14160
14153
  // we're already connected here and can report that.
14161
14154
  syncImplementation.updateSyncStatus({ connected: true });
14162
14155
  try {
14163
- while (!controlInvocations.closed) {
14164
- const line = await controlInvocations.read();
14165
- if (line == null) {
14166
- return;
14156
+ while (true) {
14157
+ let event = await controlInvocations.next();
14158
+ if (event.done) {
14159
+ break;
14167
14160
  }
14161
+ const line = event.value;
14168
14162
  await control(line.command, line.payload);
14169
14163
  if (!hadSyncLine) {
14170
14164
  syncImplementation.triggerCrudUpload();
@@ -14173,12 +14167,8 @@ The next upload iteration will be delayed.`);
14173
14167
  }
14174
14168
  }
14175
14169
  finally {
14176
- const activeInstructions = controlInvocations;
14177
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
14178
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
14179
- // to null.
14180
- controlInvocations = null;
14181
- await activeInstructions.close();
14170
+ abort();
14171
+ signal.removeEventListener('abort', abort);
14182
14172
  }
14183
14173
  }
14184
14174
  async function stop() {
@@ -14222,14 +14212,14 @@ The next upload iteration will be delayed.`);
14222
14212
  remote.invalidateCredentials();
14223
14213
  // Restart iteration after the credentials have been refreshed.
14224
14214
  remote.fetchCredentials().then((_) => {
14225
- controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
14215
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
14226
14216
  }, (err) => {
14227
14217
  syncImplementation.logger.warn('Could not prefetch credentials', err);
14228
14218
  });
14229
14219
  }
14230
14220
  }
14231
14221
  else if ('CloseSyncStream' in instruction) {
14232
- abortController.abort();
14222
+ controller.abort();
14233
14223
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
14234
14224
  }
14235
14225
  else if ('FlushFileSystem' in instruction) ;
@@ -14258,17 +14248,13 @@ The next upload iteration will be delayed.`);
14258
14248
  }
14259
14249
  await control(PowerSyncControlCommand.START, JSON.stringify(options));
14260
14250
  this.notifyCompletedUploads = () => {
14261
- if (controlInvocations && !controlInvocations?.closed) {
14262
- controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
14263
- }
14251
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
14264
14252
  };
14265
14253
  this.handleActiveStreamsChange = () => {
14266
- if (controlInvocations && !controlInvocations?.closed) {
14267
- controlInvocations.enqueueData({
14268
- command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
14269
- payload: JSON.stringify(this.activeStreams)
14270
- });
14271
- }
14254
+ controlInvocations?.inject({
14255
+ command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
14256
+ payload: JSON.stringify(this.activeStreams)
14257
+ });
14272
14258
  };
14273
14259
  await receivingLines;
14274
14260
  }