@powersync/common 0.0.0-dev-20260311103504 → 0.0.0-dev-20260414110516

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/bundle.cjs +772 -483
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +766 -479
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +770 -482
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +764 -478
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +162 -92
  10. package/lib/attachments/AttachmentQueue.d.ts +10 -4
  11. package/lib/attachments/AttachmentQueue.js +10 -4
  12. package/lib/attachments/AttachmentQueue.js.map +1 -1
  13. package/lib/attachments/AttachmentService.js +2 -3
  14. package/lib/attachments/AttachmentService.js.map +1 -1
  15. package/lib/attachments/SyncingService.d.ts +2 -1
  16. package/lib/attachments/SyncingService.js +4 -5
  17. package/lib/attachments/SyncingService.js.map +1 -1
  18. package/lib/client/AbstractPowerSyncDatabase.d.ts +5 -1
  19. package/lib/client/AbstractPowerSyncDatabase.js +9 -5
  20. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  21. package/lib/client/sync/stream/AbstractRemote.d.ts +29 -8
  22. package/lib/client/sync/stream/AbstractRemote.js +154 -177
  23. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  24. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +1 -0
  25. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +69 -88
  26. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  27. package/lib/db/DBAdapter.d.ts +55 -9
  28. package/lib/db/DBAdapter.js +126 -0
  29. package/lib/db/DBAdapter.js.map +1 -1
  30. package/lib/index.d.ts +1 -1
  31. package/lib/index.js +0 -1
  32. package/lib/index.js.map +1 -1
  33. package/lib/utils/async.d.ts +0 -9
  34. package/lib/utils/async.js +0 -9
  35. package/lib/utils/async.js.map +1 -1
  36. package/lib/utils/mutex.d.ts +47 -5
  37. package/lib/utils/mutex.js +146 -21
  38. package/lib/utils/mutex.js.map +1 -1
  39. package/lib/utils/queue.d.ts +16 -0
  40. package/lib/utils/queue.js +42 -0
  41. package/lib/utils/queue.js.map +1 -0
  42. package/lib/utils/stream_transform.d.ts +39 -0
  43. package/lib/utils/stream_transform.js +206 -0
  44. package/lib/utils/stream_transform.js.map +1 -0
  45. package/package.json +9 -8
  46. package/src/attachments/AttachmentQueue.ts +10 -4
  47. package/src/attachments/AttachmentService.ts +2 -3
  48. package/src/attachments/README.md +6 -4
  49. package/src/attachments/SyncingService.ts +4 -5
  50. package/src/client/AbstractPowerSyncDatabase.ts +9 -5
  51. package/src/client/sync/stream/AbstractRemote.ts +182 -206
  52. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +82 -83
  53. package/src/db/DBAdapter.ts +167 -9
  54. package/src/index.ts +1 -1
  55. package/src/utils/async.ts +0 -11
  56. package/src/utils/mutex.ts +184 -26
  57. package/src/utils/queue.ts +48 -0
  58. package/src/utils/stream_transform.ts +252 -0
  59. package/lib/utils/DataStream.d.ts +0 -62
  60. package/lib/utils/DataStream.js +0 -169
  61. package/lib/utils/DataStream.js.map +0 -1
  62. package/src/utils/DataStream.ts +0 -222
package/dist/bundle.cjs CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var asyncMutex = require('async-mutex');
4
-
5
3
  // https://www.sqlite.org/lang_expr.html#castexpr
6
4
  exports.ColumnType = void 0;
7
5
  (function (ColumnType) {
@@ -659,7 +657,7 @@ class SyncingService {
659
657
  updatedAttachments.push(downloaded);
660
658
  break;
661
659
  case exports.AttachmentState.QUEUED_DELETE:
662
- const deleted = await this.deleteAttachment(attachment);
660
+ const deleted = await this.deleteAttachment(attachment, context);
663
661
  updatedAttachments.push(deleted);
664
662
  break;
665
663
  }
@@ -737,17 +735,16 @@ class SyncingService {
737
735
  * On failure, defers to error handler or archives.
738
736
  *
739
737
  * @param attachment - The attachment record to delete
738
+ * @param context - Attachment context for database operations
740
739
  * @returns Updated attachment record
741
740
  */
742
- async deleteAttachment(attachment) {
741
+ async deleteAttachment(attachment, context) {
743
742
  try {
744
743
  await this.remoteStorage.deleteFile(attachment);
745
744
  if (attachment.localUri) {
746
745
  await this.localStorage.deleteFile(attachment.localUri);
747
746
  }
748
- await this.attachmentService.withContext(async (ctx) => {
749
- await ctx.deleteAttachment(attachment.id);
750
- });
747
+ await context.deleteAttachment(attachment.id);
751
748
  return {
752
749
  ...attachment,
753
750
  state: exports.AttachmentState.ARCHIVED
@@ -785,32 +782,198 @@ class SyncingService {
785
782
  }
786
783
 
787
784
  /**
788
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
785
+ * A simple fixed-capacity queue implementation.
786
+ *
787
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
788
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
789
789
  */
790
- async function mutexRunExclusive(mutex, callback, options) {
791
- return new Promise((resolve, reject) => {
792
- const timeout = options?.timeoutMs;
793
- let timedOut = false;
794
- const timeoutId = timeout
795
- ? setTimeout(() => {
796
- timedOut = true;
797
- reject(new Error('Timeout waiting for lock'));
798
- }, timeout)
799
- : undefined;
800
- mutex.runExclusive(async () => {
801
- if (timeoutId) {
802
- clearTimeout(timeoutId);
803
- }
804
- if (timedOut)
805
- return;
806
- try {
807
- resolve(await callback());
790
+ class Queue {
791
+ table;
792
+ // Index of the first element in the table.
793
+ head;
794
+ // Amount of items currently in the queue.
795
+ _length;
796
+ constructor(initialItems) {
797
+ this.table = [...initialItems];
798
+ this.head = 0;
799
+ this._length = this.table.length;
800
+ }
801
+ get isEmpty() {
802
+ return this.length == 0;
803
+ }
804
+ get length() {
805
+ return this._length;
806
+ }
807
+ removeFirst() {
808
+ if (this.isEmpty) {
809
+ throw new Error('Queue is empty');
810
+ }
811
+ const result = this.table[this.head];
812
+ this._length--;
813
+ this.table[this.head] = undefined;
814
+ this.head = (this.head + 1) % this.table.length;
815
+ return result;
816
+ }
817
+ addLast(element) {
818
+ if (this.length == this.table.length) {
819
+ throw new Error('Queue is full');
820
+ }
821
+ this.table[(this.head + this._length) % this.table.length] = element;
822
+ this._length++;
823
+ }
824
+ }
825
+
826
+ /**
827
+ * An asynchronous semaphore implementation with associated items per lease.
828
+ *
829
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
830
+ */
831
+ class Semaphore {
832
+ // Available items that are not currently assigned to a waiter.
833
+ available;
834
+ size;
835
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
836
+ // aborted waiters from the middle of the list efficiently.
837
+ firstWaiter;
838
+ lastWaiter;
839
+ constructor(elements) {
840
+ this.available = new Queue(elements);
841
+ this.size = this.available.length;
842
+ }
843
+ addWaiter(requestedItems, onAcquire) {
844
+ const node = {
845
+ isActive: true,
846
+ acquiredItems: [],
847
+ remainingItems: requestedItems,
848
+ onAcquire,
849
+ prev: this.lastWaiter
850
+ };
851
+ if (this.lastWaiter) {
852
+ this.lastWaiter.next = node;
853
+ this.lastWaiter = node;
854
+ }
855
+ else {
856
+ // First waiter
857
+ this.lastWaiter = this.firstWaiter = node;
858
+ }
859
+ return node;
860
+ }
861
+ deactivateWaiter(waiter) {
862
+ const { prev, next } = waiter;
863
+ waiter.isActive = false;
864
+ if (prev)
865
+ prev.next = next;
866
+ if (next)
867
+ next.prev = prev;
868
+ if (waiter == this.firstWaiter)
869
+ this.firstWaiter = next;
870
+ if (waiter == this.lastWaiter)
871
+ this.lastWaiter = prev;
872
+ }
873
+ requestPermits(amount, abort) {
874
+ if (amount <= 0 || amount > this.size) {
875
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
876
+ }
877
+ return new Promise((resolve, reject) => {
878
+ function rejectAborted() {
879
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
880
+ }
881
+ if (abort?.aborted) {
882
+ return rejectAborted();
883
+ }
884
+ let waiter;
885
+ const markCompleted = () => {
886
+ const items = waiter.acquiredItems;
887
+ waiter.acquiredItems = []; // Avoid releasing items twice.
888
+ for (const element of items) {
889
+ // Give to next waiter, if possible.
890
+ const nextWaiter = this.firstWaiter;
891
+ if (nextWaiter) {
892
+ nextWaiter.acquiredItems.push(element);
893
+ nextWaiter.remainingItems--;
894
+ if (nextWaiter.remainingItems == 0) {
895
+ nextWaiter.onAcquire();
896
+ }
897
+ }
898
+ else {
899
+ // No pending waiter, return lease into pool.
900
+ this.available.addLast(element);
901
+ }
902
+ }
903
+ };
904
+ const onAbort = () => {
905
+ abort?.removeEventListener('abort', onAbort);
906
+ if (waiter.isActive) {
907
+ this.deactivateWaiter(waiter);
908
+ rejectAborted();
909
+ }
910
+ };
911
+ const resolvePromise = () => {
912
+ this.deactivateWaiter(waiter);
913
+ abort?.removeEventListener('abort', onAbort);
914
+ const items = waiter.acquiredItems;
915
+ resolve({ items, release: markCompleted });
916
+ };
917
+ waiter = this.addWaiter(amount, resolvePromise);
918
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
919
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
920
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
921
+ waiter.acquiredItems.push(this.available.removeFirst());
922
+ waiter.remainingItems--;
808
923
  }
809
- catch (ex) {
810
- reject(ex);
924
+ if (waiter.remainingItems == 0) {
925
+ return resolvePromise();
811
926
  }
927
+ abort?.addEventListener('abort', onAbort);
812
928
  });
813
- });
929
+ }
930
+ /**
931
+ * Requests a single item from the pool.
932
+ *
933
+ * The returned `release` callback must be invoked to return the item into the pool.
934
+ */
935
+ async requestOne(abort) {
936
+ const { items, release } = await this.requestPermits(1, abort);
937
+ return { release, item: items[0] };
938
+ }
939
+ /**
940
+ * Requests access to all items from the pool.
941
+ *
942
+ * The returned `release` callback must be invoked to return items into the pool.
943
+ */
944
+ requestAll(abort) {
945
+ return this.requestPermits(this.size, abort);
946
+ }
947
+ }
948
+ /**
949
+ * An asynchronous mutex implementation.
950
+ *
951
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
952
+ */
953
+ class Mutex {
954
+ inner = new Semaphore([null]);
955
+ async acquire(abort) {
956
+ const { release } = await this.inner.requestOne(abort);
957
+ return release;
958
+ }
959
+ async runExclusive(fn, abort) {
960
+ const returnMutex = await this.acquire(abort);
961
+ try {
962
+ return await fn();
963
+ }
964
+ finally {
965
+ returnMutex();
966
+ }
967
+ }
968
+ }
969
+ function timeoutSignal(timeout) {
970
+ if (timeout == null)
971
+ return;
972
+ if ('timeout' in AbortSignal)
973
+ return AbortSignal.timeout(timeout);
974
+ const controller = new AbortController();
975
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
976
+ return controller.signal;
814
977
  }
815
978
 
816
979
  /**
@@ -822,7 +985,7 @@ class AttachmentService {
822
985
  db;
823
986
  logger;
824
987
  tableName;
825
- mutex = new asyncMutex.Mutex();
988
+ mutex = new Mutex();
826
989
  context;
827
990
  constructor(db, logger, tableName = 'attachments', archivedCacheLimit = 100) {
828
991
  this.db = db;
@@ -859,7 +1022,7 @@ class AttachmentService {
859
1022
  * Executes a callback with exclusive access to the attachment context.
860
1023
  */
861
1024
  async withContext(callback) {
862
- return mutexRunExclusive(this.mutex, async () => {
1025
+ return this.mutex.runExclusive(async () => {
863
1026
  return callback(this.context);
864
1027
  });
865
1028
  }
@@ -895,9 +1058,15 @@ class AttachmentQueue {
895
1058
  tableName;
896
1059
  /** Logger instance for diagnostic information */
897
1060
  logger;
898
- /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
1061
+ /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
1062
+ * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
899
1063
  syncIntervalMs = 30 * 1000;
900
- /** Duration in milliseconds to throttle sync operations */
1064
+ /** Throttle duration in milliseconds for the reactive watch query on the attachments table.
1065
+ * When attachment records change, a watch query detects the change and triggers a sync.
1066
+ * This throttle prevents the sync from firing too rapidly when many changes happen in
1067
+ * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
1068
+ * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
1069
+ * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
901
1070
  syncThrottleDuration;
902
1071
  /** Whether to automatically download remote attachments. Default: true */
903
1072
  downloadAttachments = true;
@@ -921,8 +1090,8 @@ class AttachmentQueue {
921
1090
  * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
922
1091
  * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
923
1092
  * @param options.logger - Logger instance. Defaults to db.logger
924
- * @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000
925
- * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
1093
+ * @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
1094
+ * @param options.syncThrottleDuration - Throttle duration in milliseconds for the reactive watch query that detects attachment changes. Prevents rapid-fire syncs during bulk changes. Default: 30
926
1095
  * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
927
1096
  * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
928
1097
  */
@@ -1229,6 +1398,8 @@ exports.EncodingType = void 0;
1229
1398
  EncodingType["Base64"] = "base64";
1230
1399
  })(exports.EncodingType || (exports.EncodingType = {}));
1231
1400
 
1401
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
1402
+
1232
1403
  function getDefaultExportFromCjs (x) {
1233
1404
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
1234
1405
  }
@@ -1309,7 +1480,7 @@ function requireEventIterator () {
1309
1480
  this.removeCallback();
1310
1481
  });
1311
1482
  }
1312
- [Symbol.asyncIterator]() {
1483
+ [symbolAsyncIterator]() {
1313
1484
  return {
1314
1485
  next: (value) => {
1315
1486
  const result = this.pushQueue.shift();
@@ -1356,7 +1527,7 @@ function requireEventIterator () {
1356
1527
  queue.eventHandlers[event] = fn;
1357
1528
  },
1358
1529
  }) || (() => { });
1359
- this[Symbol.asyncIterator] = () => queue[Symbol.asyncIterator]();
1530
+ this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
1360
1531
  Object.freeze(this);
1361
1532
  }
1362
1533
  }
@@ -1683,6 +1854,49 @@ var Logger = /*@__PURE__*/getDefaultExportFromCjs(loggerExports);
1683
1854
  * Set of generic interfaces to allow PowerSync compatibility with
1684
1855
  * different SQLite DB implementations.
1685
1856
  */
1857
+ /**
1858
+ * Implements {@link DBGetUtils} on a {@link SqlRunner}.
1859
+ */
1860
+ function DBGetUtilsDefaultMixin(Base) {
1861
+ return class extends Base {
1862
+ async getAll(sql, parameters) {
1863
+ const res = await this.execute(sql, parameters);
1864
+ return res.rows?._array ?? [];
1865
+ }
1866
+ async getOptional(sql, parameters) {
1867
+ const res = await this.execute(sql, parameters);
1868
+ return res.rows?.item(0) ?? null;
1869
+ }
1870
+ async get(sql, parameters) {
1871
+ const res = await this.execute(sql, parameters);
1872
+ const first = res.rows?.item(0);
1873
+ if (!first) {
1874
+ throw new Error('Result set is empty');
1875
+ }
1876
+ return first;
1877
+ }
1878
+ async executeBatch(query, params = []) {
1879
+ // If this context can run batch statements natively, use that.
1880
+ // @ts-ignore
1881
+ if (super.executeBatch) {
1882
+ // @ts-ignore
1883
+ return super.executeBatch(query, params);
1884
+ }
1885
+ // Emulate executeBatch by running statements individually.
1886
+ let lastInsertId;
1887
+ let rowsAffected = 0;
1888
+ for (const set of params) {
1889
+ const result = await this.execute(query, set);
1890
+ lastInsertId = result.insertId;
1891
+ rowsAffected += result.rowsAffected;
1892
+ }
1893
+ return {
1894
+ rowsAffected,
1895
+ insertId: lastInsertId
1896
+ };
1897
+ }
1898
+ };
1899
+ }
1686
1900
  /**
1687
1901
  * Update table operation numbers from SQLite
1688
1902
  */
@@ -1692,6 +1906,89 @@ exports.RowUpdateType = void 0;
1692
1906
  RowUpdateType[RowUpdateType["SQLITE_DELETE"] = 9] = "SQLITE_DELETE";
1693
1907
  RowUpdateType[RowUpdateType["SQLITE_UPDATE"] = 23] = "SQLITE_UPDATE";
1694
1908
  })(exports.RowUpdateType || (exports.RowUpdateType = {}));
1909
+ /**
1910
+ * A mixin to implement {@link DBAdapter} by delegating to {@link ConnectionPool.readLock} and
1911
+ * {@link ConnectionPool.writeLock}.
1912
+ */
1913
+ function DBAdapterDefaultMixin(Base) {
1914
+ return class extends Base {
1915
+ readTransaction(fn, options) {
1916
+ return this.readLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
1917
+ }
1918
+ writeTransaction(fn, options) {
1919
+ return this.writeLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
1920
+ }
1921
+ getAll(sql, parameters) {
1922
+ return this.readLock((ctx) => ctx.getAll(sql, parameters));
1923
+ }
1924
+ getOptional(sql, parameters) {
1925
+ return this.readLock((ctx) => ctx.getOptional(sql, parameters));
1926
+ }
1927
+ get(sql, parameters) {
1928
+ return this.readLock((ctx) => ctx.get(sql, parameters));
1929
+ }
1930
+ execute(query, params) {
1931
+ return this.writeLock((ctx) => ctx.execute(query, params));
1932
+ }
1933
+ executeRaw(query, params) {
1934
+ return this.writeLock((ctx) => ctx.executeRaw(query, params));
1935
+ }
1936
+ executeBatch(query, params) {
1937
+ return this.writeTransaction((tx) => tx.executeBatch(query, params));
1938
+ }
1939
+ };
1940
+ }
1941
+ class BaseTransaction {
1942
+ inner;
1943
+ finalized = false;
1944
+ constructor(inner) {
1945
+ this.inner = inner;
1946
+ }
1947
+ async commit() {
1948
+ if (this.finalized) {
1949
+ return { rowsAffected: 0 };
1950
+ }
1951
+ this.finalized = true;
1952
+ return this.inner.execute('COMMIT');
1953
+ }
1954
+ async rollback() {
1955
+ if (this.finalized) {
1956
+ return { rowsAffected: 0 };
1957
+ }
1958
+ this.finalized = true;
1959
+ return this.inner.execute('ROLLBACK');
1960
+ }
1961
+ execute(query, params) {
1962
+ return this.inner.execute(query, params);
1963
+ }
1964
+ executeRaw(query, params) {
1965
+ return this.inner.executeRaw(query, params);
1966
+ }
1967
+ executeBatch(query, params) {
1968
+ return this.inner.executeBatch(query, params);
1969
+ }
1970
+ }
1971
+ class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
1972
+ static async runWith(ctx, fn) {
1973
+ let tx = new TransactionImplementation(ctx);
1974
+ try {
1975
+ await ctx.execute('BEGIN IMMEDIATE');
1976
+ const result = await fn(tx);
1977
+ await tx.commit();
1978
+ return result;
1979
+ }
1980
+ catch (ex) {
1981
+ try {
1982
+ await tx.rollback();
1983
+ }
1984
+ catch (ex2) {
1985
+ // In rare cases, a rollback may fail.
1986
+ // Safe to ignore.
1987
+ }
1988
+ throw ex;
1989
+ }
1990
+ }
1991
+ }
1695
1992
  function isBatchedUpdateNotification(update) {
1696
1993
  return 'tables' in update;
1697
1994
  }
@@ -2112,15 +2409,6 @@ class ControlledExecutor {
2112
2409
  }
2113
2410
  }
2114
2411
 
2115
- /**
2116
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
2117
- * [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)
2118
- * we recommend for React Native.
2119
- *
2120
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
2121
- * iterators without requiring them.
2122
- */
2123
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
2124
2412
  /**
2125
2413
  * Throttle a function to be called at most once every "wait" milliseconds,
2126
2414
  * on the trailing edge.
@@ -10457,177 +10745,10 @@ function requireDist () {
10457
10745
 
10458
10746
  var distExports = requireDist();
10459
10747
 
10460
- var version = "1.48.0";
10748
+ var version = "1.51.0";
10461
10749
  var PACKAGE = {
10462
10750
  version: version};
10463
10751
 
10464
- const DEFAULT_PRESSURE_LIMITS = {
10465
- highWater: 10,
10466
- lowWater: 0
10467
- };
10468
- /**
10469
- * A very basic implementation of a data stream with backpressure support which does not use
10470
- * native JS streams or async iterators.
10471
- * This is handy for environments such as React Native which need polyfills for the above.
10472
- */
10473
- class DataStream extends BaseObserver {
10474
- options;
10475
- dataQueue;
10476
- isClosed;
10477
- processingPromise;
10478
- notifyDataAdded;
10479
- logger;
10480
- mapLine;
10481
- constructor(options) {
10482
- super();
10483
- this.options = options;
10484
- this.processingPromise = null;
10485
- this.isClosed = false;
10486
- this.dataQueue = [];
10487
- this.mapLine = options?.mapLine ?? ((line) => line);
10488
- this.logger = options?.logger ?? Logger.get('DataStream');
10489
- if (options?.closeOnError) {
10490
- const l = this.registerListener({
10491
- error: (ex) => {
10492
- l?.();
10493
- this.close();
10494
- }
10495
- });
10496
- }
10497
- }
10498
- get highWatermark() {
10499
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
10500
- }
10501
- get lowWatermark() {
10502
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
10503
- }
10504
- get closed() {
10505
- return this.isClosed;
10506
- }
10507
- async close() {
10508
- this.isClosed = true;
10509
- await this.processingPromise;
10510
- this.iterateListeners((l) => l.closed?.());
10511
- // Discard any data in the queue
10512
- this.dataQueue = [];
10513
- this.listeners.clear();
10514
- }
10515
- /**
10516
- * Enqueues data for the consumers to read
10517
- */
10518
- enqueueData(data) {
10519
- if (this.isClosed) {
10520
- throw new Error('Cannot enqueue data into closed stream.');
10521
- }
10522
- this.dataQueue.push(data);
10523
- this.notifyDataAdded?.();
10524
- this.processQueue();
10525
- }
10526
- /**
10527
- * Reads data once from the data stream
10528
- * @returns a Data payload or Null if the stream closed.
10529
- */
10530
- async read() {
10531
- if (this.closed) {
10532
- return null;
10533
- }
10534
- // Wait for any pending processing to complete first.
10535
- // This ensures we register our listener before calling processQueue(),
10536
- // avoiding a race where processQueue() sees no reader and returns early.
10537
- if (this.processingPromise) {
10538
- await this.processingPromise;
10539
- }
10540
- // Re-check after await - stream may have closed while we were waiting
10541
- if (this.closed) {
10542
- return null;
10543
- }
10544
- return new Promise((resolve, reject) => {
10545
- const l = this.registerListener({
10546
- data: async (data) => {
10547
- resolve(data);
10548
- // Remove the listener
10549
- l?.();
10550
- },
10551
- closed: () => {
10552
- resolve(null);
10553
- l?.();
10554
- },
10555
- error: (ex) => {
10556
- reject(ex);
10557
- l?.();
10558
- }
10559
- });
10560
- this.processQueue();
10561
- });
10562
- }
10563
- /**
10564
- * Executes a callback for each data item in the stream
10565
- */
10566
- forEach(callback) {
10567
- if (this.dataQueue.length <= this.lowWatermark) {
10568
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
10569
- }
10570
- return this.registerListener({
10571
- data: callback
10572
- });
10573
- }
10574
- processQueue() {
10575
- if (this.processingPromise) {
10576
- return;
10577
- }
10578
- const promise = (this.processingPromise = this._processQueue());
10579
- promise.finally(() => {
10580
- this.processingPromise = null;
10581
- });
10582
- return promise;
10583
- }
10584
- hasDataReader() {
10585
- return Array.from(this.listeners.values()).some((l) => !!l.data);
10586
- }
10587
- async _processQueue() {
10588
- /**
10589
- * Allow listeners to mutate the queue before processing.
10590
- * This allows for operations such as dropping or compressing data
10591
- * on high water or requesting more data on low water.
10592
- */
10593
- if (this.dataQueue.length >= this.highWatermark) {
10594
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
10595
- }
10596
- if (this.isClosed || !this.hasDataReader()) {
10597
- return;
10598
- }
10599
- if (this.dataQueue.length) {
10600
- const data = this.dataQueue.shift();
10601
- const mapped = this.mapLine(data);
10602
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
10603
- }
10604
- if (this.dataQueue.length <= this.lowWatermark) {
10605
- const dataAdded = new Promise((resolve) => {
10606
- this.notifyDataAdded = resolve;
10607
- });
10608
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
10609
- this.notifyDataAdded = null;
10610
- }
10611
- if (this.dataQueue.length > 0) {
10612
- setTimeout(() => this.processQueue());
10613
- }
10614
- }
10615
- async iterateAsyncErrored(cb) {
10616
- // Important: We need to copy the listeners, as calling a listener could result in adding another
10617
- // listener, resulting in infinite loops.
10618
- const listeners = Array.from(this.listeners.values());
10619
- for (let i of listeners) {
10620
- try {
10621
- await cb(i);
10622
- }
10623
- catch (ex) {
10624
- this.logger.error(ex);
10625
- this.iterateListeners((l) => l.error?.(ex));
10626
- }
10627
- }
10628
- }
10629
- }
10630
-
10631
10752
  var WebsocketDuplexConnection = {};
10632
10753
 
10633
10754
  var hasRequiredWebsocketDuplexConnection;
@@ -10790,8 +10911,215 @@ class WebsocketClientTransport {
10790
10911
  }
10791
10912
  }
10792
10913
 
10914
+ const doneResult = { done: true, value: undefined };
10915
+ function valueResult(value) {
10916
+ return { done: false, value };
10917
+ }
10918
+ /**
10919
+ * A variant of {@link Array.map} for async iterators.
10920
+ */
10921
+ function map(source, map) {
10922
+ return {
10923
+ next: async () => {
10924
+ const value = await source.next();
10925
+ if (value.done) {
10926
+ return value;
10927
+ }
10928
+ else {
10929
+ return { value: map(value.value) };
10930
+ }
10931
+ }
10932
+ };
10933
+ }
10934
+ /**
10935
+ * Expands a source async iterator by allowing to inject events asynchronously.
10936
+ *
10937
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
10938
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
10939
+ *
10940
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
10941
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
10942
+ */
10943
+ function injectable(source) {
10944
+ let sourceIsDone = false;
10945
+ let waiter = undefined; // An active, waiting next() call.
10946
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
10947
+ let pendingSourceEvent = null;
10948
+ let pendingInjectedEvents = [];
10949
+ const consumeWaiter = () => {
10950
+ const pending = waiter;
10951
+ waiter = undefined;
10952
+ return pending;
10953
+ };
10954
+ const fetchFromSource = () => {
10955
+ const resolveWaiter = (propagate) => {
10956
+ const active = consumeWaiter();
10957
+ if (active) {
10958
+ propagate(active);
10959
+ }
10960
+ else {
10961
+ pendingSourceEvent = propagate;
10962
+ }
10963
+ };
10964
+ const nextFromSource = source.next();
10965
+ nextFromSource.then((value) => {
10966
+ sourceIsDone = value.done == true;
10967
+ resolveWaiter((w) => w.resolve(value));
10968
+ }, (error) => {
10969
+ resolveWaiter((w) => w.reject(error));
10970
+ });
10971
+ };
10972
+ return {
10973
+ next: () => {
10974
+ return new Promise((resolve, reject) => {
10975
+ // First priority: Dispatch ready upstream events.
10976
+ if (sourceIsDone) {
10977
+ return resolve(doneResult);
10978
+ }
10979
+ if (pendingSourceEvent) {
10980
+ pendingSourceEvent({ resolve, reject });
10981
+ pendingSourceEvent = null;
10982
+ return;
10983
+ }
10984
+ // Second priority: Dispatch injected events
10985
+ if (pendingInjectedEvents.length) {
10986
+ return resolve(valueResult(pendingInjectedEvents.shift()));
10987
+ }
10988
+ // Nothing pending? Fetch from source
10989
+ waiter = { resolve, reject };
10990
+ return fetchFromSource();
10991
+ });
10992
+ },
10993
+ inject: (event) => {
10994
+ const pending = consumeWaiter();
10995
+ if (pending != null) {
10996
+ pending.resolve(valueResult(event));
10997
+ }
10998
+ else {
10999
+ pendingInjectedEvents.push(event);
11000
+ }
11001
+ }
11002
+ };
11003
+ }
11004
+ /**
11005
+ * Splits a byte stream at line endings, emitting each line as a string.
11006
+ */
11007
+ function extractJsonLines(source, decoder) {
11008
+ let buffer = '';
11009
+ const pendingLines = [];
11010
+ let isFinalEvent = false;
11011
+ return {
11012
+ next: async () => {
11013
+ while (true) {
11014
+ if (isFinalEvent) {
11015
+ return doneResult;
11016
+ }
11017
+ {
11018
+ const first = pendingLines.shift();
11019
+ if (first) {
11020
+ return { done: false, value: first };
11021
+ }
11022
+ }
11023
+ const { done, value } = await source.next();
11024
+ if (done) {
11025
+ const remaining = buffer.trim();
11026
+ if (remaining.length != 0) {
11027
+ isFinalEvent = true;
11028
+ return { done: false, value: remaining };
11029
+ }
11030
+ return doneResult;
11031
+ }
11032
+ const data = decoder.decode(value, { stream: true });
11033
+ buffer += data;
11034
+ const lines = buffer.split('\n');
11035
+ for (let i = 0; i < lines.length - 1; i++) {
11036
+ const l = lines[i].trim();
11037
+ if (l.length > 0) {
11038
+ pendingLines.push(l);
11039
+ }
11040
+ }
11041
+ buffer = lines[lines.length - 1];
11042
+ }
11043
+ }
11044
+ };
11045
+ }
11046
+ /**
11047
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
11048
+ */
11049
+ function extractBsonObjects(source) {
11050
+ // Fully read but not emitted yet.
11051
+ const completedObjects = [];
11052
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
11053
+ let isDone = false;
11054
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
11055
+ let objectBody = null;
11056
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
11057
+ // If we're consuming a document, the bytes remaining.
11058
+ let remainingLength = 4;
11059
+ return {
11060
+ async next() {
11061
+ while (true) {
11062
+ // Before fetching new data from upstream, return completed objects.
11063
+ if (completedObjects.length) {
11064
+ return valueResult(completedObjects.shift());
11065
+ }
11066
+ if (isDone) {
11067
+ return doneResult;
11068
+ }
11069
+ const upstreamEvent = await source.next();
11070
+ if (upstreamEvent.done) {
11071
+ isDone = true;
11072
+ if (objectBody || remainingLength != 4) {
11073
+ throw new Error('illegal end of stream in BSON object');
11074
+ }
11075
+ return doneResult;
11076
+ }
11077
+ const chunk = upstreamEvent.value;
11078
+ for (let i = 0; i < chunk.length;) {
11079
+ const availableInData = chunk.length - i;
11080
+ if (objectBody) {
11081
+ // We're in the middle of reading a BSON document.
11082
+ const bytesToRead = Math.min(availableInData, remainingLength);
11083
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
11084
+ objectBody.set(copySource, objectBody.length - remainingLength);
11085
+ i += bytesToRead;
11086
+ remainingLength -= bytesToRead;
11087
+ if (remainingLength == 0) {
11088
+ completedObjects.push(objectBody);
11089
+ // Prepare to read another document, starting with its length
11090
+ objectBody = null;
11091
+ remainingLength = 4;
11092
+ }
11093
+ }
11094
+ else {
11095
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
11096
+ const bytesToRead = Math.min(availableInData, remainingLength);
11097
+ for (let j = 0; j < bytesToRead; j++) {
11098
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
11099
+ }
11100
+ i += bytesToRead;
11101
+ remainingLength -= bytesToRead;
11102
+ if (remainingLength == 0) {
11103
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
11104
+ // header is included in length.
11105
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
11106
+ remainingLength = length - 4;
11107
+ if (remainingLength < 1) {
11108
+ throw new Error(`invalid length for bson: ${length}`);
11109
+ }
11110
+ objectBody = new Uint8Array(length);
11111
+ new DataView(objectBody.buffer).setInt32(0, length, true);
11112
+ }
11113
+ }
11114
+ }
11115
+ }
11116
+ }
11117
+ };
11118
+ }
11119
+
10793
11120
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
10794
11121
  const POWERSYNC_JS_VERSION = PACKAGE.version;
11122
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
10795
11123
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
10796
11124
  // Keep alive message is sent every period
10797
11125
  const KEEP_ALIVE_MS = 20_000;
@@ -10971,13 +11299,14 @@ class AbstractRemote {
10971
11299
  return new WebSocket(url);
10972
11300
  }
10973
11301
  /**
10974
- * Returns a data stream of sync line data.
11302
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
11303
+ *
11304
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
10975
11305
  *
10976
- * @param map Maps received payload frames to the typed event value.
10977
11306
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
10978
11307
  * (required for compatibility with older sync services).
10979
11308
  */
10980
- async socketStreamRaw(options, map, bson) {
11309
+ async socketStreamRaw(options, bson) {
10981
11310
  const { path, fetchStrategy = exports.FetchStrategy.Buffered } = options;
10982
11311
  const mimeType = bson == null ? 'application/json' : 'application/bson';
10983
11312
  function toBuffer(js) {
@@ -10992,52 +11321,55 @@ class AbstractRemote {
10992
11321
  }
10993
11322
  const syncQueueRequestSize = fetchStrategy == exports.FetchStrategy.Buffered ? 10 : 1;
10994
11323
  const request = await this.buildRequest(path);
11324
+ const url = this.options.socketUrlTransformer(request.url);
10995
11325
  // Add the user agent in the setup payload - we can't set custom
10996
11326
  // headers with websockets on web. The browser userAgent is however added
10997
11327
  // automatically as a header.
10998
11328
  const userAgent = this.getUserAgent();
10999
- const stream = new DataStream({
11000
- logger: this.logger,
11001
- pressure: {
11002
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
11003
- },
11004
- mapLine: map
11005
- });
11329
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
11330
+ // to abort the connection.
11331
+ let pendingSocket = null;
11332
+ let keepAliveTimeout;
11333
+ let rsocket = null;
11334
+ let queue = null;
11335
+ let didClose = false;
11336
+ const abortRequest = () => {
11337
+ if (didClose) {
11338
+ return;
11339
+ }
11340
+ didClose = true;
11341
+ clearTimeout(keepAliveTimeout);
11342
+ if (pendingSocket) {
11343
+ pendingSocket.close();
11344
+ }
11345
+ if (rsocket) {
11346
+ rsocket.close();
11347
+ }
11348
+ if (queue) {
11349
+ queue.stop();
11350
+ }
11351
+ };
11006
11352
  // Handle upstream abort
11007
- if (options.abortSignal?.aborted) {
11353
+ if (options.abortSignal.aborted) {
11008
11354
  throw new AbortOperation('Connection request aborted');
11009
11355
  }
11010
11356
  else {
11011
- options.abortSignal?.addEventListener('abort', () => {
11012
- stream.close();
11013
- }, { once: true });
11357
+ options.abortSignal.addEventListener('abort', abortRequest);
11014
11358
  }
11015
- let keepAliveTimeout;
11016
11359
  const resetTimeout = () => {
11017
11360
  clearTimeout(keepAliveTimeout);
11018
11361
  keepAliveTimeout = setTimeout(() => {
11019
11362
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
11020
- stream.close();
11363
+ abortRequest();
11021
11364
  }, SOCKET_TIMEOUT_MS);
11022
11365
  };
11023
11366
  resetTimeout();
11024
- // Typescript complains about this being `never` if it's not assigned here.
11025
- // This is assigned in `wsCreator`.
11026
- let disposeSocketConnectionTimeout = () => { };
11027
- const url = this.options.socketUrlTransformer(request.url);
11028
11367
  const connector = new distExports.RSocketConnector({
11029
11368
  transport: new WebsocketClientTransport({
11030
11369
  url,
11031
11370
  wsCreator: (url) => {
11032
- const socket = this.createSocket(url);
11033
- disposeSocketConnectionTimeout = stream.registerListener({
11034
- closed: () => {
11035
- // Allow closing the underlying WebSocket if the stream was closed before the
11036
- // RSocket connect completed. This should effectively abort the request.
11037
- socket.close();
11038
- }
11039
- });
11040
- socket.addEventListener('message', (event) => {
11371
+ const socket = (pendingSocket = this.createSocket(url));
11372
+ socket.addEventListener('message', () => {
11041
11373
  resetTimeout();
11042
11374
  });
11043
11375
  return socket;
@@ -11057,43 +11389,40 @@ class AbstractRemote {
11057
11389
  }
11058
11390
  }
11059
11391
  });
11060
- let rsocket;
11061
11392
  try {
11062
11393
  rsocket = await connector.connect();
11063
11394
  // The connection is established, we no longer need to monitor the initial timeout
11064
- disposeSocketConnectionTimeout();
11395
+ pendingSocket = null;
11065
11396
  }
11066
11397
  catch (ex) {
11067
11398
  this.logger.error(`Failed to connect WebSocket`, ex);
11068
- clearTimeout(keepAliveTimeout);
11069
- if (!stream.closed) {
11070
- await stream.close();
11071
- }
11399
+ abortRequest();
11072
11400
  throw ex;
11073
11401
  }
11074
11402
  resetTimeout();
11075
- let socketIsClosed = false;
11076
- const closeSocket = () => {
11077
- clearTimeout(keepAliveTimeout);
11078
- if (socketIsClosed) {
11079
- return;
11080
- }
11081
- socketIsClosed = true;
11082
- rsocket.close();
11083
- };
11084
11403
  // Helps to prevent double close scenarios
11085
- rsocket.onClose(() => (socketIsClosed = true));
11086
- // We initially request this amount and expect these to arrive eventually
11087
- let pendingEventsCount = syncQueueRequestSize;
11088
- const disposeClosedListener = stream.registerListener({
11089
- closed: () => {
11090
- closeSocket();
11091
- disposeClosedListener();
11092
- }
11093
- });
11094
- const socket = await new Promise((resolve, reject) => {
11404
+ rsocket.onClose(() => (rsocket = null));
11405
+ return await new Promise((resolve, reject) => {
11095
11406
  let connectionEstablished = false;
11096
- const res = rsocket.requestStream({
11407
+ let pendingEventsCount = syncQueueRequestSize;
11408
+ let paused = false;
11409
+ let res = null;
11410
+ function requestMore() {
11411
+ const delta = syncQueueRequestSize - pendingEventsCount;
11412
+ if (!paused && delta > 0) {
11413
+ res?.request(delta);
11414
+ pendingEventsCount = syncQueueRequestSize;
11415
+ }
11416
+ }
11417
+ const events = new domExports.EventIterator((q) => {
11418
+ queue = q;
11419
+ q.on('highWater', () => (paused = true));
11420
+ q.on('lowWater', () => {
11421
+ paused = false;
11422
+ requestMore();
11423
+ });
11424
+ }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[symbolAsyncIterator]();
11425
+ res = rsocket.requestStream({
11097
11426
  data: toBuffer(options.data),
11098
11427
  metadata: toBuffer({
11099
11428
  path
@@ -11118,7 +11447,7 @@ class AbstractRemote {
11118
11447
  }
11119
11448
  // RSocket will close the RSocket stream automatically
11120
11449
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
11121
- stream.close();
11450
+ abortRequest();
11122
11451
  // Handles cases where the connection failed e.g. auth error or connection error
11123
11452
  if (!connectionEstablished) {
11124
11453
  reject(e);
@@ -11128,41 +11457,40 @@ class AbstractRemote {
11128
11457
  // The connection is active
11129
11458
  if (!connectionEstablished) {
11130
11459
  connectionEstablished = true;
11131
- resolve(res);
11460
+ resolve(events);
11132
11461
  }
11133
11462
  const { data } = payload;
11463
+ if (data) {
11464
+ queue.push(data);
11465
+ }
11134
11466
  // Less events are now pending
11135
11467
  pendingEventsCount--;
11136
- if (!data) {
11137
- return;
11138
- }
11139
- stream.enqueueData(data);
11468
+ // Request another event (unless the downstream consumer is paused).
11469
+ requestMore();
11140
11470
  },
11141
11471
  onComplete: () => {
11142
- stream.close();
11472
+ abortRequest(); // this will also emit a done event
11143
11473
  },
11144
11474
  onExtension: () => { }
11145
11475
  });
11146
11476
  });
11147
- const l = stream.registerListener({
11148
- lowWater: async () => {
11149
- // Request to fill up the queue
11150
- const required = syncQueueRequestSize - pendingEventsCount;
11151
- if (required > 0) {
11152
- socket.request(syncQueueRequestSize - pendingEventsCount);
11153
- pendingEventsCount = syncQueueRequestSize;
11154
- }
11155
- },
11156
- closed: () => {
11157
- l();
11158
- }
11159
- });
11160
- return stream;
11161
11477
  }
11162
11478
  /**
11163
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
11479
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
11480
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
11481
+ *
11482
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
11483
+ */
11484
+ get supportsStreamingBinaryResponses() {
11485
+ return true;
11486
+ }
11487
+ /**
11488
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
11489
+ * async iterator of byte blobs.
11490
+ *
11491
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
11164
11492
  */
11165
- async postStreamRaw(options, mapLine) {
11493
+ async fetchStreamRaw(options) {
11166
11494
  const { data, path, headers, abortSignal } = options;
11167
11495
  const request = await this.buildRequest(path);
11168
11496
  /**
@@ -11174,119 +11502,94 @@ class AbstractRemote {
11174
11502
  * Aborting the active fetch request while it is being consumed seems to throw
11175
11503
  * an unhandled exception on the window level.
11176
11504
  */
11177
- if (abortSignal?.aborted) {
11178
- throw new AbortOperation('Abort request received before making postStreamRaw request');
11505
+ if (abortSignal.aborted) {
11506
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
11179
11507
  }
11180
11508
  const controller = new AbortController();
11181
- let requestResolved = false;
11182
- abortSignal?.addEventListener('abort', () => {
11183
- if (!requestResolved) {
11509
+ let reader = null;
11510
+ abortSignal.addEventListener('abort', () => {
11511
+ const reason = abortSignal.reason ??
11512
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
11513
+ if (reader == null) {
11184
11514
  // Only abort via the abort controller if the request has not resolved yet
11185
- controller.abort(abortSignal.reason ??
11186
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.'));
11515
+ controller.abort(reason);
11516
+ }
11517
+ else {
11518
+ reader.cancel(reason).catch(() => {
11519
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
11520
+ // ignore it here.
11521
+ });
11187
11522
  }
11188
11523
  });
11189
- const res = await this.fetch(request.url, {
11190
- method: 'POST',
11191
- headers: { ...headers, ...request.headers },
11192
- body: JSON.stringify(data),
11193
- signal: controller.signal,
11194
- cache: 'no-store',
11195
- ...(this.options.fetchOptions ?? {}),
11196
- ...options.fetchOptions
11197
- }).catch((ex) => {
11524
+ let res;
11525
+ let responseIsBson = false;
11526
+ try {
11527
+ const ndJson = 'application/x-ndjson';
11528
+ const bson = 'application/vnd.powersync.bson-stream';
11529
+ res = await this.fetch(request.url, {
11530
+ method: 'POST',
11531
+ headers: {
11532
+ ...headers,
11533
+ ...request.headers,
11534
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
11535
+ },
11536
+ body: JSON.stringify(data),
11537
+ signal: controller.signal,
11538
+ cache: 'no-store',
11539
+ ...(this.options.fetchOptions ?? {}),
11540
+ ...options.fetchOptions
11541
+ });
11542
+ if (!res.ok || !res.body) {
11543
+ const text = await res.text();
11544
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
11545
+ const error = new Error(`HTTP ${res.statusText}: ${text}`);
11546
+ error.status = res.status;
11547
+ throw error;
11548
+ }
11549
+ const contentType = res.headers.get('content-type');
11550
+ responseIsBson = contentType == bson;
11551
+ }
11552
+ catch (ex) {
11198
11553
  if (ex.name == 'AbortError') {
11199
11554
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
11200
11555
  }
11201
11556
  throw ex;
11202
- });
11203
- if (!res) {
11204
- throw new Error('Fetch request was aborted');
11205
- }
11206
- requestResolved = true;
11207
- if (!res.ok || !res.body) {
11208
- const text = await res.text();
11209
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
11210
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
11211
- error.status = res.status;
11212
- throw error;
11213
11557
  }
11214
- // Create a new stream splitting the response at line endings while also handling cancellations
11215
- // by closing the reader.
11216
- const reader = res.body.getReader();
11217
- let readerReleased = false;
11218
- // This will close the network request and read stream
11219
- const closeReader = async () => {
11220
- try {
11221
- readerReleased = true;
11222
- await reader.cancel();
11223
- }
11224
- catch (ex) {
11225
- // an error will throw if the reader hasn't been used yet
11226
- }
11227
- reader.releaseLock();
11228
- };
11229
- const stream = new DataStream({
11230
- logger: this.logger,
11231
- mapLine: mapLine,
11232
- pressure: {
11233
- highWaterMark: 20,
11234
- lowWaterMark: 10
11235
- }
11236
- });
11237
- abortSignal?.addEventListener('abort', () => {
11238
- closeReader();
11239
- stream.close();
11240
- });
11241
- const decoder = this.createTextDecoder();
11242
- let buffer = '';
11243
- const consumeStream = async () => {
11244
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
11245
- const { done, value } = await reader.read();
11246
- if (done) {
11247
- const remaining = buffer.trim();
11248
- if (remaining.length != 0) {
11249
- stream.enqueueData(remaining);
11250
- }
11251
- stream.close();
11252
- await closeReader();
11253
- return;
11558
+ reader = res.body.getReader();
11559
+ const stream = {
11560
+ next: async () => {
11561
+ if (controller.signal.aborted) {
11562
+ return doneResult;
11254
11563
  }
11255
- const data = decoder.decode(value, { stream: true });
11256
- buffer += data;
11257
- const lines = buffer.split('\n');
11258
- for (var i = 0; i < lines.length - 1; i++) {
11259
- var l = lines[i].trim();
11260
- if (l.length > 0) {
11261
- stream.enqueueData(l);
11262
- }
11564
+ try {
11565
+ return await reader.read();
11263
11566
  }
11264
- buffer = lines[lines.length - 1];
11265
- // Implement backpressure by waiting for the low water mark to be reached
11266
- if (stream.dataQueue.length > stream.highWatermark) {
11267
- await new Promise((resolve) => {
11268
- const dispose = stream.registerListener({
11269
- lowWater: async () => {
11270
- resolve();
11271
- dispose();
11272
- },
11273
- closed: () => {
11274
- resolve();
11275
- dispose();
11276
- }
11277
- });
11278
- });
11567
+ catch (ex) {
11568
+ if (controller.signal.aborted) {
11569
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
11570
+ // things working as intended, we can return a done event and consider the exception handled.
11571
+ return doneResult;
11572
+ }
11573
+ throw ex;
11279
11574
  }
11280
11575
  }
11281
11576
  };
11282
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
11283
- const l = stream.registerListener({
11284
- closed: () => {
11285
- closeReader();
11286
- l?.();
11287
- }
11288
- });
11289
- return stream;
11577
+ return { isBson: responseIsBson, stream };
11578
+ }
11579
+ /**
11580
+ * Posts a `/sync/stream` request.
11581
+ *
11582
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
11583
+ * {@link Uint8Array}s.
11584
+ */
11585
+ async fetchStream(options) {
11586
+ const { isBson, stream } = await this.fetchStreamRaw(options);
11587
+ if (isBson) {
11588
+ return extractBsonObjects(stream);
11589
+ }
11590
+ else {
11591
+ return extractJsonLines(stream, this.createTextDecoder());
11592
+ }
11290
11593
  }
11291
11594
  }
11292
11595
 
@@ -11794,6 +12097,19 @@ The next upload iteration will be delayed.`);
11794
12097
  }
11795
12098
  });
11796
12099
  }
12100
+ async receiveSyncLines(data) {
12101
+ const { options, connection, bson } = data;
12102
+ const remote = this.options.remote;
12103
+ if (connection.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
12104
+ return await remote.fetchStream(options);
12105
+ }
12106
+ else {
12107
+ return await this.options.remote.socketStreamRaw({
12108
+ ...options,
12109
+ ...{ fetchStrategy: connection.fetchStrategy }
12110
+ }, bson);
12111
+ }
12112
+ }
11797
12113
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
11798
12114
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
11799
12115
  if (rawTables != null && rawTables.length) {
@@ -11823,42 +12139,27 @@ The next upload iteration will be delayed.`);
11823
12139
  client_id: clientId
11824
12140
  }
11825
12141
  };
11826
- let stream;
11827
- if (resolvedOptions?.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
11828
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
11829
- if (typeof line == 'string') {
11830
- return JSON.parse(line);
11831
- }
11832
- else {
11833
- // Directly enqueued by us
11834
- return line;
11835
- }
11836
- });
11837
- }
11838
- else {
11839
- const bson = await this.options.remote.getBSON();
11840
- stream = await this.options.remote.socketStreamRaw({
11841
- ...syncOptions,
11842
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
11843
- }, (payload) => {
11844
- if (payload instanceof Uint8Array) {
11845
- return bson.deserialize(payload);
11846
- }
11847
- else {
11848
- // Directly enqueued by us
11849
- return payload;
11850
- }
11851
- }, bson);
11852
- }
12142
+ const bson = await this.options.remote.getBSON();
12143
+ const source = await this.receiveSyncLines({
12144
+ options: syncOptions,
12145
+ connection: resolvedOptions,
12146
+ bson
12147
+ });
12148
+ const stream = injectable(map(source, (line) => {
12149
+ if (typeof line == 'string') {
12150
+ return JSON.parse(line);
12151
+ }
12152
+ else {
12153
+ return bson.deserialize(line);
12154
+ }
12155
+ }));
11853
12156
  this.logger.debug('Stream established. Processing events');
11854
12157
  this.notifyCompletedUploads = () => {
11855
- if (!stream.closed) {
11856
- stream.enqueueData({ crud_upload_completed: null });
11857
- }
12158
+ stream.inject({ crud_upload_completed: null });
11858
12159
  };
11859
- while (!stream.closed) {
11860
- const line = await stream.read();
11861
- if (!line) {
12160
+ while (true) {
12161
+ const { value: line, done } = await stream.next();
12162
+ if (done) {
11862
12163
  // The stream has closed while waiting
11863
12164
  return;
11864
12165
  }
@@ -12037,14 +12338,17 @@ The next upload iteration will be delayed.`);
12037
12338
  const syncImplementation = this;
12038
12339
  const adapter = this.options.adapter;
12039
12340
  const remote = this.options.remote;
12341
+ const controller = new AbortController();
12342
+ const abort = () => {
12343
+ return controller.abort(signal.reason);
12344
+ };
12345
+ signal.addEventListener('abort', abort);
12040
12346
  let receivingLines = null;
12041
12347
  let hadSyncLine = false;
12042
12348
  let hideDisconnectOnRestart = false;
12043
12349
  if (signal.aborted) {
12044
12350
  throw new AbortOperation('Connection request has been aborted');
12045
12351
  }
12046
- const abortController = new AbortController();
12047
- signal.addEventListener('abort', () => abortController.abort());
12048
12352
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
12049
12353
  // invocation (local events include refreshed tokens and completed uploads).
12050
12354
  // This is a single data stream so that we can handle all control calls from a single place.
@@ -12052,49 +12356,36 @@ The next upload iteration will be delayed.`);
12052
12356
  async function connect(instr) {
12053
12357
  const syncOptions = {
12054
12358
  path: '/sync/stream',
12055
- abortSignal: abortController.signal,
12359
+ abortSignal: controller.signal,
12056
12360
  data: instr.request
12057
12361
  };
12058
- if (resolvedOptions.connectionMethod == exports.SyncStreamConnectionMethod.HTTP) {
12059
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
12060
- if (typeof line == 'string') {
12061
- return {
12062
- command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
12063
- payload: line
12064
- };
12065
- }
12066
- else {
12067
- // Directly enqueued by us
12068
- return line;
12069
- }
12070
- });
12071
- }
12072
- else {
12073
- controlInvocations = await remote.socketStreamRaw({
12074
- ...syncOptions,
12075
- fetchStrategy: resolvedOptions.fetchStrategy
12076
- }, (payload) => {
12077
- if (payload instanceof Uint8Array) {
12078
- return {
12079
- command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
12080
- payload: payload
12081
- };
12082
- }
12083
- else {
12084
- // Directly enqueued by us
12085
- return payload;
12086
- }
12087
- });
12088
- }
12362
+ controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
12363
+ options: syncOptions,
12364
+ connection: resolvedOptions
12365
+ }), (line) => {
12366
+ if (typeof line == 'string') {
12367
+ return {
12368
+ command: exports.PowerSyncControlCommand.PROCESS_TEXT_LINE,
12369
+ payload: line
12370
+ };
12371
+ }
12372
+ else {
12373
+ return {
12374
+ command: exports.PowerSyncControlCommand.PROCESS_BSON_LINE,
12375
+ payload: line
12376
+ };
12377
+ }
12378
+ }));
12089
12379
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
12090
12380
  // we're already connected here and can report that.
12091
12381
  syncImplementation.updateSyncStatus({ connected: true });
12092
12382
  try {
12093
- while (!controlInvocations.closed) {
12094
- const line = await controlInvocations.read();
12095
- if (line == null) {
12096
- return;
12383
+ while (true) {
12384
+ let event = await controlInvocations.next();
12385
+ if (event.done) {
12386
+ break;
12097
12387
  }
12388
+ const line = event.value;
12098
12389
  await control(line.command, line.payload);
12099
12390
  if (!hadSyncLine) {
12100
12391
  syncImplementation.triggerCrudUpload();
@@ -12103,12 +12394,8 @@ The next upload iteration will be delayed.`);
12103
12394
  }
12104
12395
  }
12105
12396
  finally {
12106
- const activeInstructions = controlInvocations;
12107
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
12108
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
12109
- // to null.
12110
- controlInvocations = null;
12111
- await activeInstructions.close();
12397
+ abort();
12398
+ signal.removeEventListener('abort', abort);
12112
12399
  }
12113
12400
  }
12114
12401
  async function stop() {
@@ -12152,14 +12439,14 @@ The next upload iteration will be delayed.`);
12152
12439
  remote.invalidateCredentials();
12153
12440
  // Restart iteration after the credentials have been refreshed.
12154
12441
  remote.fetchCredentials().then((_) => {
12155
- controlInvocations?.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12442
+ controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12156
12443
  }, (err) => {
12157
12444
  syncImplementation.logger.warn('Could not prefetch credentials', err);
12158
12445
  });
12159
12446
  }
12160
12447
  }
12161
12448
  else if ('CloseSyncStream' in instruction) {
12162
- abortController.abort();
12449
+ controller.abort();
12163
12450
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12164
12451
  }
12165
12452
  else if ('FlushFileSystem' in instruction) ;
@@ -12188,17 +12475,13 @@ The next upload iteration will be delayed.`);
12188
12475
  }
12189
12476
  await control(exports.PowerSyncControlCommand.START, JSON.stringify(options));
12190
12477
  this.notifyCompletedUploads = () => {
12191
- if (controlInvocations && !controlInvocations?.closed) {
12192
- controlInvocations.enqueueData({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12193
- }
12478
+ controlInvocations?.inject({ command: exports.PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12194
12479
  };
12195
12480
  this.handleActiveStreamsChange = () => {
12196
- if (controlInvocations && !controlInvocations?.closed) {
12197
- controlInvocations.enqueueData({
12198
- command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12199
- payload: JSON.stringify(this.activeStreams)
12200
- });
12201
- }
12481
+ controlInvocations?.inject({
12482
+ command: exports.PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12483
+ payload: JSON.stringify(this.activeStreams)
12484
+ });
12202
12485
  };
12203
12486
  await receivingLines;
12204
12487
  }
@@ -12849,7 +13132,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
12849
13132
  this._schema = schema;
12850
13133
  this.ready = false;
12851
13134
  this.sdkVersion = '';
12852
- this.runExclusiveMutex = new asyncMutex.Mutex();
13135
+ this.runExclusiveMutex = new Mutex();
12853
13136
  // Start async init
12854
13137
  this.subscriptions = {
12855
13138
  firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
@@ -13314,6 +13597,10 @@ SELECT * FROM crud_entries;
13314
13597
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
13315
13598
  * and optionally return results.
13316
13599
  *
13600
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
13601
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
13602
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
13603
+ *
13317
13604
  * @param sql The SQL query to execute
13318
13605
  * @param parameters Optional array of parameters to bind to the query
13319
13606
  * @returns The query result as an object with structured key-value pairs
@@ -13410,7 +13697,7 @@ SELECT * FROM crud_entries;
13410
13697
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
13411
13698
  await this.waitForReady();
13412
13699
  return this.database.readTransaction(async (tx) => {
13413
- const res = await callback({ ...tx });
13700
+ const res = await callback(tx);
13414
13701
  await tx.rollback();
13415
13702
  return res;
13416
13703
  }, { timeoutMs: lockTimeout });
@@ -14407,6 +14694,8 @@ exports.ControlledExecutor = ControlledExecutor;
14407
14694
  exports.CrudBatch = CrudBatch;
14408
14695
  exports.CrudEntry = CrudEntry;
14409
14696
  exports.CrudTransaction = CrudTransaction;
14697
+ exports.DBAdapterDefaultMixin = DBAdapterDefaultMixin;
14698
+ exports.DBGetUtilsDefaultMixin = DBGetUtilsDefaultMixin;
14410
14699
  exports.DEFAULT_CRUD_BATCH_LIMIT = DEFAULT_CRUD_BATCH_LIMIT;
14411
14700
  exports.DEFAULT_CRUD_UPLOAD_THROTTLE_MS = DEFAULT_CRUD_UPLOAD_THROTTLE_MS;
14412
14701
  exports.DEFAULT_INDEX_COLUMN_OPTIONS = DEFAULT_INDEX_COLUMN_OPTIONS;
@@ -14414,7 +14703,6 @@ exports.DEFAULT_INDEX_OPTIONS = DEFAULT_INDEX_OPTIONS;
14414
14703
  exports.DEFAULT_LOCK_TIMEOUT_MS = DEFAULT_LOCK_TIMEOUT_MS;
14415
14704
  exports.DEFAULT_POWERSYNC_CLOSE_OPTIONS = DEFAULT_POWERSYNC_CLOSE_OPTIONS;
14416
14705
  exports.DEFAULT_POWERSYNC_DB_OPTIONS = DEFAULT_POWERSYNC_DB_OPTIONS;
14417
- exports.DEFAULT_PRESSURE_LIMITS = DEFAULT_PRESSURE_LIMITS;
14418
14706
  exports.DEFAULT_REMOTE_LOGGER = DEFAULT_REMOTE_LOGGER;
14419
14707
  exports.DEFAULT_REMOTE_OPTIONS = DEFAULT_REMOTE_OPTIONS;
14420
14708
  exports.DEFAULT_RETRY_DELAY_MS = DEFAULT_RETRY_DELAY_MS;
@@ -14425,7 +14713,6 @@ exports.DEFAULT_SYNC_CLIENT_IMPLEMENTATION = DEFAULT_SYNC_CLIENT_IMPLEMENTATION;
14425
14713
  exports.DEFAULT_TABLE_OPTIONS = DEFAULT_TABLE_OPTIONS;
14426
14714
  exports.DEFAULT_WATCH_QUERY_OPTIONS = DEFAULT_WATCH_QUERY_OPTIONS;
14427
14715
  exports.DEFAULT_WATCH_THROTTLE_MS = DEFAULT_WATCH_THROTTLE_MS;
14428
- exports.DataStream = DataStream;
14429
14716
  exports.DifferentialQueryProcessor = DifferentialQueryProcessor;
14430
14717
  exports.EMPTY_DIFFERENTIAL = EMPTY_DIFFERENTIAL;
14431
14718
  exports.FalsyComparator = FalsyComparator;
@@ -14438,10 +14725,12 @@ exports.LogLevel = LogLevel;
14438
14725
  exports.MAX_AMOUNT_OF_COLUMNS = MAX_AMOUNT_OF_COLUMNS;
14439
14726
  exports.MAX_OP_ID = MAX_OP_ID;
14440
14727
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
14728
+ exports.Mutex = Mutex;
14441
14729
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
14442
14730
  exports.OpType = OpType;
14443
14731
  exports.OplogEntry = OplogEntry;
14444
14732
  exports.Schema = Schema;
14733
+ exports.Semaphore = Semaphore;
14445
14734
  exports.SqliteBucketStorage = SqliteBucketStorage;
14446
14735
  exports.SyncDataBatch = SyncDataBatch;
14447
14736
  exports.SyncDataBucket = SyncDataBucket;
@@ -14471,9 +14760,9 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
14471
14760
  exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
14472
14761
  exports.isStreamingSyncData = isStreamingSyncData;
14473
14762
  exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
14474
- exports.mutexRunExclusive = mutexRunExclusive;
14475
14763
  exports.parseQuery = parseQuery;
14476
14764
  exports.runOnSchemaChange = runOnSchemaChange;
14477
14765
  exports.sanitizeSQL = sanitizeSQL;
14478
14766
  exports.sanitizeUUID = sanitizeUUID;
14767
+ exports.timeoutSignal = timeoutSignal;
14479
14768
  //# sourceMappingURL=bundle.cjs.map