@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.
@@ -2834,7 +2834,6 @@ __webpack_require__.r(__webpack_exports__);
2834
2834
  /* harmony export */ DEFAULT_LOCK_TIMEOUT_MS: () => (/* binding */ DEFAULT_LOCK_TIMEOUT_MS),
2835
2835
  /* harmony export */ DEFAULT_POWERSYNC_CLOSE_OPTIONS: () => (/* binding */ DEFAULT_POWERSYNC_CLOSE_OPTIONS),
2836
2836
  /* harmony export */ DEFAULT_POWERSYNC_DB_OPTIONS: () => (/* binding */ DEFAULT_POWERSYNC_DB_OPTIONS),
2837
- /* harmony export */ DEFAULT_PRESSURE_LIMITS: () => (/* binding */ DEFAULT_PRESSURE_LIMITS),
2838
2837
  /* harmony export */ DEFAULT_REMOTE_LOGGER: () => (/* binding */ DEFAULT_REMOTE_LOGGER),
2839
2838
  /* harmony export */ DEFAULT_REMOTE_OPTIONS: () => (/* binding */ DEFAULT_REMOTE_OPTIONS),
2840
2839
  /* harmony export */ DEFAULT_RETRY_DELAY_MS: () => (/* binding */ DEFAULT_RETRY_DELAY_MS),
@@ -2845,7 +2844,6 @@ __webpack_require__.r(__webpack_exports__);
2845
2844
  /* harmony export */ DEFAULT_TABLE_OPTIONS: () => (/* binding */ DEFAULT_TABLE_OPTIONS),
2846
2845
  /* harmony export */ DEFAULT_WATCH_QUERY_OPTIONS: () => (/* binding */ DEFAULT_WATCH_QUERY_OPTIONS),
2847
2846
  /* harmony export */ DEFAULT_WATCH_THROTTLE_MS: () => (/* binding */ DEFAULT_WATCH_THROTTLE_MS),
2848
- /* harmony export */ DataStream: () => (/* binding */ DataStream),
2849
2847
  /* harmony export */ DiffTriggerOperation: () => (/* binding */ DiffTriggerOperation),
2850
2848
  /* harmony export */ DifferentialQueryProcessor: () => (/* binding */ DifferentialQueryProcessor),
2851
2849
  /* harmony export */ EMPTY_DIFFERENTIAL: () => (/* binding */ EMPTY_DIFFERENTIAL),
@@ -4309,6 +4307,8 @@ var EncodingType;
4309
4307
  EncodingType["Base64"] = "base64";
4310
4308
  })(EncodingType || (EncodingType = {}));
4311
4309
 
4310
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
4311
+
4312
4312
  function getDefaultExportFromCjs (x) {
4313
4313
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
4314
4314
  }
@@ -4389,7 +4389,7 @@ function requireEventIterator () {
4389
4389
  this.removeCallback();
4390
4390
  });
4391
4391
  }
4392
- [Symbol.asyncIterator]() {
4392
+ [symbolAsyncIterator]() {
4393
4393
  return {
4394
4394
  next: (value) => {
4395
4395
  const result = this.pushQueue.shift();
@@ -4436,7 +4436,7 @@ function requireEventIterator () {
4436
4436
  queue.eventHandlers[event] = fn;
4437
4437
  },
4438
4438
  }) || (() => { });
4439
- this[Symbol.asyncIterator] = () => queue[Symbol.asyncIterator]();
4439
+ this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
4440
4440
  Object.freeze(this);
4441
4441
  }
4442
4442
  }
@@ -5318,15 +5318,6 @@ class ControlledExecutor {
5318
5318
  }
5319
5319
  }
5320
5320
 
5321
- /**
5322
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
5323
- * [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)
5324
- * we recommend for React Native.
5325
- *
5326
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
5327
- * iterators without requiring them.
5328
- */
5329
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
5330
5321
  /**
5331
5322
  * Throttle a function to be called at most once every "wait" milliseconds,
5332
5323
  * on the trailing edge.
@@ -13663,177 +13654,10 @@ function requireDist () {
13663
13654
 
13664
13655
  var distExports = requireDist();
13665
13656
 
13666
- var version = "1.51.0";
13657
+ var version = "1.52.0";
13667
13658
  var PACKAGE = {
13668
13659
  version: version};
13669
13660
 
13670
- const DEFAULT_PRESSURE_LIMITS = {
13671
- highWater: 10,
13672
- lowWater: 0
13673
- };
13674
- /**
13675
- * A very basic implementation of a data stream with backpressure support which does not use
13676
- * native JS streams or async iterators.
13677
- * This is handy for environments such as React Native which need polyfills for the above.
13678
- */
13679
- class DataStream extends BaseObserver {
13680
- options;
13681
- dataQueue;
13682
- isClosed;
13683
- processingPromise;
13684
- notifyDataAdded;
13685
- logger;
13686
- mapLine;
13687
- constructor(options) {
13688
- super();
13689
- this.options = options;
13690
- this.processingPromise = null;
13691
- this.isClosed = false;
13692
- this.dataQueue = [];
13693
- this.mapLine = options?.mapLine ?? ((line) => line);
13694
- this.logger = options?.logger ?? Logger.get('DataStream');
13695
- if (options?.closeOnError) {
13696
- const l = this.registerListener({
13697
- error: (ex) => {
13698
- l?.();
13699
- this.close();
13700
- }
13701
- });
13702
- }
13703
- }
13704
- get highWatermark() {
13705
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
13706
- }
13707
- get lowWatermark() {
13708
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
13709
- }
13710
- get closed() {
13711
- return this.isClosed;
13712
- }
13713
- async close() {
13714
- this.isClosed = true;
13715
- await this.processingPromise;
13716
- this.iterateListeners((l) => l.closed?.());
13717
- // Discard any data in the queue
13718
- this.dataQueue = [];
13719
- this.listeners.clear();
13720
- }
13721
- /**
13722
- * Enqueues data for the consumers to read
13723
- */
13724
- enqueueData(data) {
13725
- if (this.isClosed) {
13726
- throw new Error('Cannot enqueue data into closed stream.');
13727
- }
13728
- this.dataQueue.push(data);
13729
- this.notifyDataAdded?.();
13730
- this.processQueue();
13731
- }
13732
- /**
13733
- * Reads data once from the data stream
13734
- * @returns a Data payload or Null if the stream closed.
13735
- */
13736
- async read() {
13737
- if (this.closed) {
13738
- return null;
13739
- }
13740
- // Wait for any pending processing to complete first.
13741
- // This ensures we register our listener before calling processQueue(),
13742
- // avoiding a race where processQueue() sees no reader and returns early.
13743
- if (this.processingPromise) {
13744
- await this.processingPromise;
13745
- }
13746
- // Re-check after await - stream may have closed while we were waiting
13747
- if (this.closed) {
13748
- return null;
13749
- }
13750
- return new Promise((resolve, reject) => {
13751
- const l = this.registerListener({
13752
- data: async (data) => {
13753
- resolve(data);
13754
- // Remove the listener
13755
- l?.();
13756
- },
13757
- closed: () => {
13758
- resolve(null);
13759
- l?.();
13760
- },
13761
- error: (ex) => {
13762
- reject(ex);
13763
- l?.();
13764
- }
13765
- });
13766
- this.processQueue();
13767
- });
13768
- }
13769
- /**
13770
- * Executes a callback for each data item in the stream
13771
- */
13772
- forEach(callback) {
13773
- if (this.dataQueue.length <= this.lowWatermark) {
13774
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
13775
- }
13776
- return this.registerListener({
13777
- data: callback
13778
- });
13779
- }
13780
- processQueue() {
13781
- if (this.processingPromise) {
13782
- return;
13783
- }
13784
- const promise = (this.processingPromise = this._processQueue());
13785
- promise.finally(() => {
13786
- this.processingPromise = null;
13787
- });
13788
- return promise;
13789
- }
13790
- hasDataReader() {
13791
- return Array.from(this.listeners.values()).some((l) => !!l.data);
13792
- }
13793
- async _processQueue() {
13794
- /**
13795
- * Allow listeners to mutate the queue before processing.
13796
- * This allows for operations such as dropping or compressing data
13797
- * on high water or requesting more data on low water.
13798
- */
13799
- if (this.dataQueue.length >= this.highWatermark) {
13800
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
13801
- }
13802
- if (this.isClosed || !this.hasDataReader()) {
13803
- return;
13804
- }
13805
- if (this.dataQueue.length) {
13806
- const data = this.dataQueue.shift();
13807
- const mapped = this.mapLine(data);
13808
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
13809
- }
13810
- if (this.dataQueue.length <= this.lowWatermark) {
13811
- const dataAdded = new Promise((resolve) => {
13812
- this.notifyDataAdded = resolve;
13813
- });
13814
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
13815
- this.notifyDataAdded = null;
13816
- }
13817
- if (this.dataQueue.length > 0) {
13818
- setTimeout(() => this.processQueue());
13819
- }
13820
- }
13821
- async iterateAsyncErrored(cb) {
13822
- // Important: We need to copy the listeners, as calling a listener could result in adding another
13823
- // listener, resulting in infinite loops.
13824
- const listeners = Array.from(this.listeners.values());
13825
- for (let i of listeners) {
13826
- try {
13827
- await cb(i);
13828
- }
13829
- catch (ex) {
13830
- this.logger.error(ex);
13831
- this.iterateListeners((l) => l.error?.(ex));
13832
- }
13833
- }
13834
- }
13835
- }
13836
-
13837
13661
  var WebsocketDuplexConnection = {};
13838
13662
 
13839
13663
  var hasRequiredWebsocketDuplexConnection;
@@ -13996,8 +13820,215 @@ class WebsocketClientTransport {
13996
13820
  }
13997
13821
  }
13998
13822
 
13823
+ const doneResult = { done: true, value: undefined };
13824
+ function valueResult(value) {
13825
+ return { done: false, value };
13826
+ }
13827
+ /**
13828
+ * A variant of {@link Array.map} for async iterators.
13829
+ */
13830
+ function map(source, map) {
13831
+ return {
13832
+ next: async () => {
13833
+ const value = await source.next();
13834
+ if (value.done) {
13835
+ return value;
13836
+ }
13837
+ else {
13838
+ return { value: map(value.value) };
13839
+ }
13840
+ }
13841
+ };
13842
+ }
13843
+ /**
13844
+ * Expands a source async iterator by allowing to inject events asynchronously.
13845
+ *
13846
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
13847
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
13848
+ *
13849
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
13850
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
13851
+ */
13852
+ function injectable(source) {
13853
+ let sourceIsDone = false;
13854
+ let waiter = undefined; // An active, waiting next() call.
13855
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
13856
+ let pendingSourceEvent = null;
13857
+ let pendingInjectedEvents = [];
13858
+ const consumeWaiter = () => {
13859
+ const pending = waiter;
13860
+ waiter = undefined;
13861
+ return pending;
13862
+ };
13863
+ const fetchFromSource = () => {
13864
+ const resolveWaiter = (propagate) => {
13865
+ const active = consumeWaiter();
13866
+ if (active) {
13867
+ propagate(active);
13868
+ }
13869
+ else {
13870
+ pendingSourceEvent = propagate;
13871
+ }
13872
+ };
13873
+ const nextFromSource = source.next();
13874
+ nextFromSource.then((value) => {
13875
+ sourceIsDone = value.done == true;
13876
+ resolveWaiter((w) => w.resolve(value));
13877
+ }, (error) => {
13878
+ resolveWaiter((w) => w.reject(error));
13879
+ });
13880
+ };
13881
+ return {
13882
+ next: () => {
13883
+ return new Promise((resolve, reject) => {
13884
+ // First priority: Dispatch ready upstream events.
13885
+ if (sourceIsDone) {
13886
+ return resolve(doneResult);
13887
+ }
13888
+ if (pendingSourceEvent) {
13889
+ pendingSourceEvent({ resolve, reject });
13890
+ pendingSourceEvent = null;
13891
+ return;
13892
+ }
13893
+ // Second priority: Dispatch injected events
13894
+ if (pendingInjectedEvents.length) {
13895
+ return resolve(valueResult(pendingInjectedEvents.shift()));
13896
+ }
13897
+ // Nothing pending? Fetch from source
13898
+ waiter = { resolve, reject };
13899
+ return fetchFromSource();
13900
+ });
13901
+ },
13902
+ inject: (event) => {
13903
+ const pending = consumeWaiter();
13904
+ if (pending != null) {
13905
+ pending.resolve(valueResult(event));
13906
+ }
13907
+ else {
13908
+ pendingInjectedEvents.push(event);
13909
+ }
13910
+ }
13911
+ };
13912
+ }
13913
+ /**
13914
+ * Splits a byte stream at line endings, emitting each line as a string.
13915
+ */
13916
+ function extractJsonLines(source, decoder) {
13917
+ let buffer = '';
13918
+ const pendingLines = [];
13919
+ let isFinalEvent = false;
13920
+ return {
13921
+ next: async () => {
13922
+ while (true) {
13923
+ if (isFinalEvent) {
13924
+ return doneResult;
13925
+ }
13926
+ {
13927
+ const first = pendingLines.shift();
13928
+ if (first) {
13929
+ return { done: false, value: first };
13930
+ }
13931
+ }
13932
+ const { done, value } = await source.next();
13933
+ if (done) {
13934
+ const remaining = buffer.trim();
13935
+ if (remaining.length != 0) {
13936
+ isFinalEvent = true;
13937
+ return { done: false, value: remaining };
13938
+ }
13939
+ return doneResult;
13940
+ }
13941
+ const data = decoder.decode(value, { stream: true });
13942
+ buffer += data;
13943
+ const lines = buffer.split('\n');
13944
+ for (let i = 0; i < lines.length - 1; i++) {
13945
+ const l = lines[i].trim();
13946
+ if (l.length > 0) {
13947
+ pendingLines.push(l);
13948
+ }
13949
+ }
13950
+ buffer = lines[lines.length - 1];
13951
+ }
13952
+ }
13953
+ };
13954
+ }
13955
+ /**
13956
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
13957
+ */
13958
+ function extractBsonObjects(source) {
13959
+ // Fully read but not emitted yet.
13960
+ const completedObjects = [];
13961
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
13962
+ let isDone = false;
13963
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
13964
+ let objectBody = null;
13965
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
13966
+ // If we're consuming a document, the bytes remaining.
13967
+ let remainingLength = 4;
13968
+ return {
13969
+ async next() {
13970
+ while (true) {
13971
+ // Before fetching new data from upstream, return completed objects.
13972
+ if (completedObjects.length) {
13973
+ return valueResult(completedObjects.shift());
13974
+ }
13975
+ if (isDone) {
13976
+ return doneResult;
13977
+ }
13978
+ const upstreamEvent = await source.next();
13979
+ if (upstreamEvent.done) {
13980
+ isDone = true;
13981
+ if (objectBody || remainingLength != 4) {
13982
+ throw new Error('illegal end of stream in BSON object');
13983
+ }
13984
+ return doneResult;
13985
+ }
13986
+ const chunk = upstreamEvent.value;
13987
+ for (let i = 0; i < chunk.length;) {
13988
+ const availableInData = chunk.length - i;
13989
+ if (objectBody) {
13990
+ // We're in the middle of reading a BSON document.
13991
+ const bytesToRead = Math.min(availableInData, remainingLength);
13992
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
13993
+ objectBody.set(copySource, objectBody.length - remainingLength);
13994
+ i += bytesToRead;
13995
+ remainingLength -= bytesToRead;
13996
+ if (remainingLength == 0) {
13997
+ completedObjects.push(objectBody);
13998
+ // Prepare to read another document, starting with its length
13999
+ objectBody = null;
14000
+ remainingLength = 4;
14001
+ }
14002
+ }
14003
+ else {
14004
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
14005
+ const bytesToRead = Math.min(availableInData, remainingLength);
14006
+ for (let j = 0; j < bytesToRead; j++) {
14007
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
14008
+ }
14009
+ i += bytesToRead;
14010
+ remainingLength -= bytesToRead;
14011
+ if (remainingLength == 0) {
14012
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
14013
+ // header is included in length.
14014
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
14015
+ remainingLength = length - 4;
14016
+ if (remainingLength < 1) {
14017
+ throw new Error(`invalid length for bson: ${length}`);
14018
+ }
14019
+ objectBody = new Uint8Array(length);
14020
+ new DataView(objectBody.buffer).setInt32(0, length, true);
14021
+ }
14022
+ }
14023
+ }
14024
+ }
14025
+ }
14026
+ };
14027
+ }
14028
+
13999
14029
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
14000
14030
  const POWERSYNC_JS_VERSION = PACKAGE.version;
14031
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
14001
14032
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
14002
14033
  // Keep alive message is sent every period
14003
14034
  const KEEP_ALIVE_MS = 20_000;
@@ -14177,13 +14208,14 @@ class AbstractRemote {
14177
14208
  return new WebSocket(url);
14178
14209
  }
14179
14210
  /**
14180
- * Returns a data stream of sync line data.
14211
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
14212
+ *
14213
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
14181
14214
  *
14182
- * @param map Maps received payload frames to the typed event value.
14183
14215
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
14184
14216
  * (required for compatibility with older sync services).
14185
14217
  */
14186
- async socketStreamRaw(options, map, bson) {
14218
+ async socketStreamRaw(options, bson) {
14187
14219
  const { path, fetchStrategy = FetchStrategy.Buffered } = options;
14188
14220
  const mimeType = bson == null ? 'application/json' : 'application/bson';
14189
14221
  function toBuffer(js) {
@@ -14198,52 +14230,55 @@ class AbstractRemote {
14198
14230
  }
14199
14231
  const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
14200
14232
  const request = await this.buildRequest(path);
14233
+ const url = this.options.socketUrlTransformer(request.url);
14201
14234
  // Add the user agent in the setup payload - we can't set custom
14202
14235
  // headers with websockets on web. The browser userAgent is however added
14203
14236
  // automatically as a header.
14204
14237
  const userAgent = this.getUserAgent();
14205
- const stream = new DataStream({
14206
- logger: this.logger,
14207
- pressure: {
14208
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
14209
- },
14210
- mapLine: map
14211
- });
14238
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
14239
+ // to abort the connection.
14240
+ let pendingSocket = null;
14241
+ let keepAliveTimeout;
14242
+ let rsocket = null;
14243
+ let queue = null;
14244
+ let didClose = false;
14245
+ const abortRequest = () => {
14246
+ if (didClose) {
14247
+ return;
14248
+ }
14249
+ didClose = true;
14250
+ clearTimeout(keepAliveTimeout);
14251
+ if (pendingSocket) {
14252
+ pendingSocket.close();
14253
+ }
14254
+ if (rsocket) {
14255
+ rsocket.close();
14256
+ }
14257
+ if (queue) {
14258
+ queue.stop();
14259
+ }
14260
+ };
14212
14261
  // Handle upstream abort
14213
- if (options.abortSignal?.aborted) {
14262
+ if (options.abortSignal.aborted) {
14214
14263
  throw new AbortOperation('Connection request aborted');
14215
14264
  }
14216
14265
  else {
14217
- options.abortSignal?.addEventListener('abort', () => {
14218
- stream.close();
14219
- }, { once: true });
14266
+ options.abortSignal.addEventListener('abort', abortRequest);
14220
14267
  }
14221
- let keepAliveTimeout;
14222
14268
  const resetTimeout = () => {
14223
14269
  clearTimeout(keepAliveTimeout);
14224
14270
  keepAliveTimeout = setTimeout(() => {
14225
14271
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
14226
- stream.close();
14272
+ abortRequest();
14227
14273
  }, SOCKET_TIMEOUT_MS);
14228
14274
  };
14229
14275
  resetTimeout();
14230
- // Typescript complains about this being `never` if it's not assigned here.
14231
- // This is assigned in `wsCreator`.
14232
- let disposeSocketConnectionTimeout = () => { };
14233
- const url = this.options.socketUrlTransformer(request.url);
14234
14276
  const connector = new distExports.RSocketConnector({
14235
14277
  transport: new WebsocketClientTransport({
14236
14278
  url,
14237
14279
  wsCreator: (url) => {
14238
- const socket = this.createSocket(url);
14239
- disposeSocketConnectionTimeout = stream.registerListener({
14240
- closed: () => {
14241
- // Allow closing the underlying WebSocket if the stream was closed before the
14242
- // RSocket connect completed. This should effectively abort the request.
14243
- socket.close();
14244
- }
14245
- });
14246
- socket.addEventListener('message', (event) => {
14280
+ const socket = (pendingSocket = this.createSocket(url));
14281
+ socket.addEventListener('message', () => {
14247
14282
  resetTimeout();
14248
14283
  });
14249
14284
  return socket;
@@ -14263,43 +14298,40 @@ class AbstractRemote {
14263
14298
  }
14264
14299
  }
14265
14300
  });
14266
- let rsocket;
14267
14301
  try {
14268
14302
  rsocket = await connector.connect();
14269
14303
  // The connection is established, we no longer need to monitor the initial timeout
14270
- disposeSocketConnectionTimeout();
14304
+ pendingSocket = null;
14271
14305
  }
14272
14306
  catch (ex) {
14273
14307
  this.logger.error(`Failed to connect WebSocket`, ex);
14274
- clearTimeout(keepAliveTimeout);
14275
- if (!stream.closed) {
14276
- await stream.close();
14277
- }
14308
+ abortRequest();
14278
14309
  throw ex;
14279
14310
  }
14280
14311
  resetTimeout();
14281
- let socketIsClosed = false;
14282
- const closeSocket = () => {
14283
- clearTimeout(keepAliveTimeout);
14284
- if (socketIsClosed) {
14285
- return;
14286
- }
14287
- socketIsClosed = true;
14288
- rsocket.close();
14289
- };
14290
14312
  // Helps to prevent double close scenarios
14291
- rsocket.onClose(() => (socketIsClosed = true));
14292
- // We initially request this amount and expect these to arrive eventually
14293
- let pendingEventsCount = syncQueueRequestSize;
14294
- const disposeClosedListener = stream.registerListener({
14295
- closed: () => {
14296
- closeSocket();
14297
- disposeClosedListener();
14298
- }
14299
- });
14300
- const socket = await new Promise((resolve, reject) => {
14313
+ rsocket.onClose(() => (rsocket = null));
14314
+ return await new Promise((resolve, reject) => {
14301
14315
  let connectionEstablished = false;
14302
- const res = rsocket.requestStream({
14316
+ let pendingEventsCount = syncQueueRequestSize;
14317
+ let paused = false;
14318
+ let res = null;
14319
+ function requestMore() {
14320
+ const delta = syncQueueRequestSize - pendingEventsCount;
14321
+ if (!paused && delta > 0) {
14322
+ res?.request(delta);
14323
+ pendingEventsCount = syncQueueRequestSize;
14324
+ }
14325
+ }
14326
+ const events = new domExports.EventIterator((q) => {
14327
+ queue = q;
14328
+ q.on('highWater', () => (paused = true));
14329
+ q.on('lowWater', () => {
14330
+ paused = false;
14331
+ requestMore();
14332
+ });
14333
+ }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[symbolAsyncIterator]();
14334
+ res = rsocket.requestStream({
14303
14335
  data: toBuffer(options.data),
14304
14336
  metadata: toBuffer({
14305
14337
  path
@@ -14324,7 +14356,7 @@ class AbstractRemote {
14324
14356
  }
14325
14357
  // RSocket will close the RSocket stream automatically
14326
14358
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
14327
- stream.close();
14359
+ abortRequest();
14328
14360
  // Handles cases where the connection failed e.g. auth error or connection error
14329
14361
  if (!connectionEstablished) {
14330
14362
  reject(e);
@@ -14334,41 +14366,40 @@ class AbstractRemote {
14334
14366
  // The connection is active
14335
14367
  if (!connectionEstablished) {
14336
14368
  connectionEstablished = true;
14337
- resolve(res);
14369
+ resolve(events);
14338
14370
  }
14339
14371
  const { data } = payload;
14372
+ if (data) {
14373
+ queue.push(data);
14374
+ }
14340
14375
  // Less events are now pending
14341
14376
  pendingEventsCount--;
14342
- if (!data) {
14343
- return;
14344
- }
14345
- stream.enqueueData(data);
14377
+ // Request another event (unless the downstream consumer is paused).
14378
+ requestMore();
14346
14379
  },
14347
14380
  onComplete: () => {
14348
- stream.close();
14381
+ abortRequest(); // this will also emit a done event
14349
14382
  },
14350
14383
  onExtension: () => { }
14351
14384
  });
14352
14385
  });
14353
- const l = stream.registerListener({
14354
- lowWater: async () => {
14355
- // Request to fill up the queue
14356
- const required = syncQueueRequestSize - pendingEventsCount;
14357
- if (required > 0) {
14358
- socket.request(syncQueueRequestSize - pendingEventsCount);
14359
- pendingEventsCount = syncQueueRequestSize;
14360
- }
14361
- },
14362
- closed: () => {
14363
- l();
14364
- }
14365
- });
14366
- return stream;
14367
14386
  }
14368
14387
  /**
14369
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
14388
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
14389
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
14390
+ *
14391
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
14392
+ */
14393
+ get supportsStreamingBinaryResponses() {
14394
+ return true;
14395
+ }
14396
+ /**
14397
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
14398
+ * async iterator of byte blobs.
14399
+ *
14400
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
14370
14401
  */
14371
- async postStreamRaw(options, mapLine) {
14402
+ async fetchStreamRaw(options) {
14372
14403
  const { data, path, headers, abortSignal } = options;
14373
14404
  const request = await this.buildRequest(path);
14374
14405
  /**
@@ -14380,119 +14411,94 @@ class AbstractRemote {
14380
14411
  * Aborting the active fetch request while it is being consumed seems to throw
14381
14412
  * an unhandled exception on the window level.
14382
14413
  */
14383
- if (abortSignal?.aborted) {
14384
- throw new AbortOperation('Abort request received before making postStreamRaw request');
14414
+ if (abortSignal.aborted) {
14415
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
14385
14416
  }
14386
14417
  const controller = new AbortController();
14387
- let requestResolved = false;
14388
- abortSignal?.addEventListener('abort', () => {
14389
- if (!requestResolved) {
14418
+ let reader = null;
14419
+ abortSignal.addEventListener('abort', () => {
14420
+ const reason = abortSignal.reason ??
14421
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
14422
+ if (reader == null) {
14390
14423
  // Only abort via the abort controller if the request has not resolved yet
14391
- controller.abort(abortSignal.reason ??
14392
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.'));
14424
+ controller.abort(reason);
14425
+ }
14426
+ else {
14427
+ reader.cancel(reason).catch(() => {
14428
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
14429
+ // ignore it here.
14430
+ });
14393
14431
  }
14394
14432
  });
14395
- const res = await this.fetch(request.url, {
14396
- method: 'POST',
14397
- headers: { ...headers, ...request.headers },
14398
- body: JSON.stringify(data),
14399
- signal: controller.signal,
14400
- cache: 'no-store',
14401
- ...(this.options.fetchOptions ?? {}),
14402
- ...options.fetchOptions
14403
- }).catch((ex) => {
14433
+ let res;
14434
+ let responseIsBson = false;
14435
+ try {
14436
+ const ndJson = 'application/x-ndjson';
14437
+ const bson = 'application/vnd.powersync.bson-stream';
14438
+ res = await this.fetch(request.url, {
14439
+ method: 'POST',
14440
+ headers: {
14441
+ ...headers,
14442
+ ...request.headers,
14443
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
14444
+ },
14445
+ body: JSON.stringify(data),
14446
+ signal: controller.signal,
14447
+ cache: 'no-store',
14448
+ ...(this.options.fetchOptions ?? {}),
14449
+ ...options.fetchOptions
14450
+ });
14451
+ if (!res.ok || !res.body) {
14452
+ const text = await res.text();
14453
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
14454
+ const error = new Error(`HTTP ${res.statusText}: ${text}`);
14455
+ error.status = res.status;
14456
+ throw error;
14457
+ }
14458
+ const contentType = res.headers.get('content-type');
14459
+ responseIsBson = contentType == bson;
14460
+ }
14461
+ catch (ex) {
14404
14462
  if (ex.name == 'AbortError') {
14405
14463
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
14406
14464
  }
14407
14465
  throw ex;
14408
- });
14409
- if (!res) {
14410
- throw new Error('Fetch request was aborted');
14411
14466
  }
14412
- requestResolved = true;
14413
- if (!res.ok || !res.body) {
14414
- const text = await res.text();
14415
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
14416
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
14417
- error.status = res.status;
14418
- throw error;
14419
- }
14420
- // Create a new stream splitting the response at line endings while also handling cancellations
14421
- // by closing the reader.
14422
- const reader = res.body.getReader();
14423
- let readerReleased = false;
14424
- // This will close the network request and read stream
14425
- const closeReader = async () => {
14426
- try {
14427
- readerReleased = true;
14428
- await reader.cancel();
14429
- }
14430
- catch (ex) {
14431
- // an error will throw if the reader hasn't been used yet
14432
- }
14433
- reader.releaseLock();
14434
- };
14435
- const stream = new DataStream({
14436
- logger: this.logger,
14437
- mapLine: mapLine,
14438
- pressure: {
14439
- highWaterMark: 20,
14440
- lowWaterMark: 10
14441
- }
14442
- });
14443
- abortSignal?.addEventListener('abort', () => {
14444
- closeReader();
14445
- stream.close();
14446
- });
14447
- const decoder = this.createTextDecoder();
14448
- let buffer = '';
14449
- const consumeStream = async () => {
14450
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
14451
- const { done, value } = await reader.read();
14452
- if (done) {
14453
- const remaining = buffer.trim();
14454
- if (remaining.length != 0) {
14455
- stream.enqueueData(remaining);
14456
- }
14457
- stream.close();
14458
- await closeReader();
14459
- return;
14467
+ reader = res.body.getReader();
14468
+ const stream = {
14469
+ next: async () => {
14470
+ if (controller.signal.aborted) {
14471
+ return doneResult;
14460
14472
  }
14461
- const data = decoder.decode(value, { stream: true });
14462
- buffer += data;
14463
- const lines = buffer.split('\n');
14464
- for (var i = 0; i < lines.length - 1; i++) {
14465
- var l = lines[i].trim();
14466
- if (l.length > 0) {
14467
- stream.enqueueData(l);
14468
- }
14473
+ try {
14474
+ return await reader.read();
14469
14475
  }
14470
- buffer = lines[lines.length - 1];
14471
- // Implement backpressure by waiting for the low water mark to be reached
14472
- if (stream.dataQueue.length > stream.highWatermark) {
14473
- await new Promise((resolve) => {
14474
- const dispose = stream.registerListener({
14475
- lowWater: async () => {
14476
- resolve();
14477
- dispose();
14478
- },
14479
- closed: () => {
14480
- resolve();
14481
- dispose();
14482
- }
14483
- });
14484
- });
14476
+ catch (ex) {
14477
+ if (controller.signal.aborted) {
14478
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
14479
+ // things working as intended, we can return a done event and consider the exception handled.
14480
+ return doneResult;
14481
+ }
14482
+ throw ex;
14485
14483
  }
14486
14484
  }
14487
14485
  };
14488
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
14489
- const l = stream.registerListener({
14490
- closed: () => {
14491
- closeReader();
14492
- l?.();
14493
- }
14494
- });
14495
- return stream;
14486
+ return { isBson: responseIsBson, stream };
14487
+ }
14488
+ /**
14489
+ * Posts a `/sync/stream` request.
14490
+ *
14491
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
14492
+ * {@link Uint8Array}s.
14493
+ */
14494
+ async fetchStream(options) {
14495
+ const { isBson, stream } = await this.fetchStreamRaw(options);
14496
+ if (isBson) {
14497
+ return extractBsonObjects(stream);
14498
+ }
14499
+ else {
14500
+ return extractJsonLines(stream, this.createTextDecoder());
14501
+ }
14496
14502
  }
14497
14503
  }
14498
14504
 
@@ -15000,6 +15006,19 @@ The next upload iteration will be delayed.`);
15000
15006
  }
15001
15007
  });
15002
15008
  }
15009
+ async receiveSyncLines(data) {
15010
+ const { options, connection, bson } = data;
15011
+ const remote = this.options.remote;
15012
+ if (connection.connectionMethod == SyncStreamConnectionMethod.HTTP) {
15013
+ return await remote.fetchStream(options);
15014
+ }
15015
+ else {
15016
+ return await this.options.remote.socketStreamRaw({
15017
+ ...options,
15018
+ ...{ fetchStrategy: connection.fetchStrategy }
15019
+ }, bson);
15020
+ }
15021
+ }
15003
15022
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
15004
15023
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
15005
15024
  if (rawTables != null && rawTables.length) {
@@ -15029,42 +15048,27 @@ The next upload iteration will be delayed.`);
15029
15048
  client_id: clientId
15030
15049
  }
15031
15050
  };
15032
- let stream;
15033
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
15034
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
15035
- if (typeof line == 'string') {
15036
- return JSON.parse(line);
15037
- }
15038
- else {
15039
- // Directly enqueued by us
15040
- return line;
15041
- }
15042
- });
15043
- }
15044
- else {
15045
- const bson = await this.options.remote.getBSON();
15046
- stream = await this.options.remote.socketStreamRaw({
15047
- ...syncOptions,
15048
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
15049
- }, (payload) => {
15050
- if (payload instanceof Uint8Array) {
15051
- return bson.deserialize(payload);
15052
- }
15053
- else {
15054
- // Directly enqueued by us
15055
- return payload;
15056
- }
15057
- }, bson);
15058
- }
15051
+ const bson = await this.options.remote.getBSON();
15052
+ const source = await this.receiveSyncLines({
15053
+ options: syncOptions,
15054
+ connection: resolvedOptions,
15055
+ bson
15056
+ });
15057
+ const stream = injectable(map(source, (line) => {
15058
+ if (typeof line == 'string') {
15059
+ return JSON.parse(line);
15060
+ }
15061
+ else {
15062
+ return bson.deserialize(line);
15063
+ }
15064
+ }));
15059
15065
  this.logger.debug('Stream established. Processing events');
15060
15066
  this.notifyCompletedUploads = () => {
15061
- if (!stream.closed) {
15062
- stream.enqueueData({ crud_upload_completed: null });
15063
- }
15067
+ stream.inject({ crud_upload_completed: null });
15064
15068
  };
15065
- while (!stream.closed) {
15066
- const line = await stream.read();
15067
- if (!line) {
15069
+ while (true) {
15070
+ const { value: line, done } = await stream.next();
15071
+ if (done) {
15068
15072
  // The stream has closed while waiting
15069
15073
  return;
15070
15074
  }
@@ -15243,14 +15247,17 @@ The next upload iteration will be delayed.`);
15243
15247
  const syncImplementation = this;
15244
15248
  const adapter = this.options.adapter;
15245
15249
  const remote = this.options.remote;
15250
+ const controller = new AbortController();
15251
+ const abort = () => {
15252
+ return controller.abort(signal.reason);
15253
+ };
15254
+ signal.addEventListener('abort', abort);
15246
15255
  let receivingLines = null;
15247
15256
  let hadSyncLine = false;
15248
15257
  let hideDisconnectOnRestart = false;
15249
15258
  if (signal.aborted) {
15250
15259
  throw new AbortOperation('Connection request has been aborted');
15251
15260
  }
15252
- const abortController = new AbortController();
15253
- signal.addEventListener('abort', () => abortController.abort());
15254
15261
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
15255
15262
  // invocation (local events include refreshed tokens and completed uploads).
15256
15263
  // This is a single data stream so that we can handle all control calls from a single place.
@@ -15258,49 +15265,36 @@ The next upload iteration will be delayed.`);
15258
15265
  async function connect(instr) {
15259
15266
  const syncOptions = {
15260
15267
  path: '/sync/stream',
15261
- abortSignal: abortController.signal,
15268
+ abortSignal: controller.signal,
15262
15269
  data: instr.request
15263
15270
  };
15264
- if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
15265
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
15266
- if (typeof line == 'string') {
15267
- return {
15268
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
15269
- payload: line
15270
- };
15271
- }
15272
- else {
15273
- // Directly enqueued by us
15274
- return line;
15275
- }
15276
- });
15277
- }
15278
- else {
15279
- controlInvocations = await remote.socketStreamRaw({
15280
- ...syncOptions,
15281
- fetchStrategy: resolvedOptions.fetchStrategy
15282
- }, (payload) => {
15283
- if (payload instanceof Uint8Array) {
15284
- return {
15285
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
15286
- payload: payload
15287
- };
15288
- }
15289
- else {
15290
- // Directly enqueued by us
15291
- return payload;
15292
- }
15293
- });
15294
- }
15271
+ controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
15272
+ options: syncOptions,
15273
+ connection: resolvedOptions
15274
+ }), (line) => {
15275
+ if (typeof line == 'string') {
15276
+ return {
15277
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
15278
+ payload: line
15279
+ };
15280
+ }
15281
+ else {
15282
+ return {
15283
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
15284
+ payload: line
15285
+ };
15286
+ }
15287
+ }));
15295
15288
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
15296
15289
  // we're already connected here and can report that.
15297
15290
  syncImplementation.updateSyncStatus({ connected: true });
15298
15291
  try {
15299
- while (!controlInvocations.closed) {
15300
- const line = await controlInvocations.read();
15301
- if (line == null) {
15302
- return;
15292
+ while (true) {
15293
+ let event = await controlInvocations.next();
15294
+ if (event.done) {
15295
+ break;
15303
15296
  }
15297
+ const line = event.value;
15304
15298
  await control(line.command, line.payload);
15305
15299
  if (!hadSyncLine) {
15306
15300
  syncImplementation.triggerCrudUpload();
@@ -15309,12 +15303,8 @@ The next upload iteration will be delayed.`);
15309
15303
  }
15310
15304
  }
15311
15305
  finally {
15312
- const activeInstructions = controlInvocations;
15313
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
15314
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
15315
- // to null.
15316
- controlInvocations = null;
15317
- await activeInstructions.close();
15306
+ abort();
15307
+ signal.removeEventListener('abort', abort);
15318
15308
  }
15319
15309
  }
15320
15310
  async function stop() {
@@ -15358,14 +15348,14 @@ The next upload iteration will be delayed.`);
15358
15348
  remote.invalidateCredentials();
15359
15349
  // Restart iteration after the credentials have been refreshed.
15360
15350
  remote.fetchCredentials().then((_) => {
15361
- controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
15351
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
15362
15352
  }, (err) => {
15363
15353
  syncImplementation.logger.warn('Could not prefetch credentials', err);
15364
15354
  });
15365
15355
  }
15366
15356
  }
15367
15357
  else if ('CloseSyncStream' in instruction) {
15368
- abortController.abort();
15358
+ controller.abort();
15369
15359
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
15370
15360
  }
15371
15361
  else if ('FlushFileSystem' in instruction) ;
@@ -15394,17 +15384,13 @@ The next upload iteration will be delayed.`);
15394
15384
  }
15395
15385
  await control(PowerSyncControlCommand.START, JSON.stringify(options));
15396
15386
  this.notifyCompletedUploads = () => {
15397
- if (controlInvocations && !controlInvocations?.closed) {
15398
- controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
15399
- }
15387
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
15400
15388
  };
15401
15389
  this.handleActiveStreamsChange = () => {
15402
- if (controlInvocations && !controlInvocations?.closed) {
15403
- controlInvocations.enqueueData({
15404
- command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
15405
- payload: JSON.stringify(this.activeStreams)
15406
- });
15407
- }
15390
+ controlInvocations?.inject({
15391
+ command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
15392
+ payload: JSON.stringify(this.activeStreams)
15393
+ });
15408
15394
  };
15409
15395
  await receivingLines;
15410
15396
  }