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

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 (72) hide show
  1. package/dist/bundle.cjs +791 -489
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +785 -485
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +789 -488
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +783 -484
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +165 -103
  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 +4 -0
  25. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +88 -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/db/crud/SyncStatus.d.ts +0 -4
  31. package/lib/db/crud/SyncStatus.js +0 -4
  32. package/lib/db/crud/SyncStatus.js.map +1 -1
  33. package/lib/db/schema/RawTable.d.ts +0 -5
  34. package/lib/db/schema/Schema.d.ts +0 -2
  35. package/lib/db/schema/Schema.js +0 -2
  36. package/lib/db/schema/Schema.js.map +1 -1
  37. package/lib/index.d.ts +1 -1
  38. package/lib/index.js +0 -1
  39. package/lib/index.js.map +1 -1
  40. package/lib/utils/async.d.ts +0 -9
  41. package/lib/utils/async.js +0 -9
  42. package/lib/utils/async.js.map +1 -1
  43. package/lib/utils/mutex.d.ts +47 -5
  44. package/lib/utils/mutex.js +146 -21
  45. package/lib/utils/mutex.js.map +1 -1
  46. package/lib/utils/queue.d.ts +16 -0
  47. package/lib/utils/queue.js +42 -0
  48. package/lib/utils/queue.js.map +1 -0
  49. package/lib/utils/stream_transform.d.ts +39 -0
  50. package/lib/utils/stream_transform.js +206 -0
  51. package/lib/utils/stream_transform.js.map +1 -0
  52. package/package.json +9 -8
  53. package/src/attachments/AttachmentQueue.ts +10 -4
  54. package/src/attachments/AttachmentService.ts +2 -3
  55. package/src/attachments/README.md +6 -4
  56. package/src/attachments/SyncingService.ts +4 -5
  57. package/src/client/AbstractPowerSyncDatabase.ts +9 -5
  58. package/src/client/sync/stream/AbstractRemote.ts +182 -206
  59. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +96 -83
  60. package/src/db/DBAdapter.ts +167 -9
  61. package/src/db/crud/SyncStatus.ts +0 -4
  62. package/src/db/schema/RawTable.ts +0 -5
  63. package/src/db/schema/Schema.ts +0 -2
  64. package/src/index.ts +1 -1
  65. package/src/utils/async.ts +0 -11
  66. package/src/utils/mutex.ts +184 -26
  67. package/src/utils/queue.ts +48 -0
  68. package/src/utils/stream_transform.ts +252 -0
  69. package/lib/utils/DataStream.d.ts +0 -62
  70. package/lib/utils/DataStream.js +0 -169
  71. package/lib/utils/DataStream.js.map +0 -1
  72. package/src/utils/DataStream.ts +0 -222
package/dist/bundle.mjs CHANGED
@@ -1,5 +1,3 @@
1
- import { Mutex } from 'async-mutex';
2
-
3
1
  // https://www.sqlite.org/lang_expr.html#castexpr
4
2
  var ColumnType;
5
3
  (function (ColumnType) {
@@ -657,7 +655,7 @@ class SyncingService {
657
655
  updatedAttachments.push(downloaded);
658
656
  break;
659
657
  case AttachmentState.QUEUED_DELETE:
660
- const deleted = await this.deleteAttachment(attachment);
658
+ const deleted = await this.deleteAttachment(attachment, context);
661
659
  updatedAttachments.push(deleted);
662
660
  break;
663
661
  }
@@ -735,17 +733,16 @@ class SyncingService {
735
733
  * On failure, defers to error handler or archives.
736
734
  *
737
735
  * @param attachment - The attachment record to delete
736
+ * @param context - Attachment context for database operations
738
737
  * @returns Updated attachment record
739
738
  */
740
- async deleteAttachment(attachment) {
739
+ async deleteAttachment(attachment, context) {
741
740
  try {
742
741
  await this.remoteStorage.deleteFile(attachment);
743
742
  if (attachment.localUri) {
744
743
  await this.localStorage.deleteFile(attachment.localUri);
745
744
  }
746
- await this.attachmentService.withContext(async (ctx) => {
747
- await ctx.deleteAttachment(attachment.id);
748
- });
745
+ await context.deleteAttachment(attachment.id);
749
746
  return {
750
747
  ...attachment,
751
748
  state: AttachmentState.ARCHIVED
@@ -783,32 +780,198 @@ class SyncingService {
783
780
  }
784
781
 
785
782
  /**
786
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
783
+ * A simple fixed-capacity queue implementation.
784
+ *
785
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
786
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
787
787
  */
788
- async function mutexRunExclusive(mutex, callback, options) {
789
- return new Promise((resolve, reject) => {
790
- const timeout = options?.timeoutMs;
791
- let timedOut = false;
792
- const timeoutId = timeout
793
- ? setTimeout(() => {
794
- timedOut = true;
795
- reject(new Error('Timeout waiting for lock'));
796
- }, timeout)
797
- : undefined;
798
- mutex.runExclusive(async () => {
799
- if (timeoutId) {
800
- clearTimeout(timeoutId);
801
- }
802
- if (timedOut)
803
- return;
804
- try {
805
- resolve(await callback());
788
+ class Queue {
789
+ table;
790
+ // Index of the first element in the table.
791
+ head;
792
+ // Amount of items currently in the queue.
793
+ _length;
794
+ constructor(initialItems) {
795
+ this.table = [...initialItems];
796
+ this.head = 0;
797
+ this._length = this.table.length;
798
+ }
799
+ get isEmpty() {
800
+ return this.length == 0;
801
+ }
802
+ get length() {
803
+ return this._length;
804
+ }
805
+ removeFirst() {
806
+ if (this.isEmpty) {
807
+ throw new Error('Queue is empty');
808
+ }
809
+ const result = this.table[this.head];
810
+ this._length--;
811
+ this.table[this.head] = undefined;
812
+ this.head = (this.head + 1) % this.table.length;
813
+ return result;
814
+ }
815
+ addLast(element) {
816
+ if (this.length == this.table.length) {
817
+ throw new Error('Queue is full');
818
+ }
819
+ this.table[(this.head + this._length) % this.table.length] = element;
820
+ this._length++;
821
+ }
822
+ }
823
+
824
+ /**
825
+ * An asynchronous semaphore implementation with associated items per lease.
826
+ *
827
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
828
+ */
829
+ class Semaphore {
830
+ // Available items that are not currently assigned to a waiter.
831
+ available;
832
+ size;
833
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
834
+ // aborted waiters from the middle of the list efficiently.
835
+ firstWaiter;
836
+ lastWaiter;
837
+ constructor(elements) {
838
+ this.available = new Queue(elements);
839
+ this.size = this.available.length;
840
+ }
841
+ addWaiter(requestedItems, onAcquire) {
842
+ const node = {
843
+ isActive: true,
844
+ acquiredItems: [],
845
+ remainingItems: requestedItems,
846
+ onAcquire,
847
+ prev: this.lastWaiter
848
+ };
849
+ if (this.lastWaiter) {
850
+ this.lastWaiter.next = node;
851
+ this.lastWaiter = node;
852
+ }
853
+ else {
854
+ // First waiter
855
+ this.lastWaiter = this.firstWaiter = node;
856
+ }
857
+ return node;
858
+ }
859
+ deactivateWaiter(waiter) {
860
+ const { prev, next } = waiter;
861
+ waiter.isActive = false;
862
+ if (prev)
863
+ prev.next = next;
864
+ if (next)
865
+ next.prev = prev;
866
+ if (waiter == this.firstWaiter)
867
+ this.firstWaiter = next;
868
+ if (waiter == this.lastWaiter)
869
+ this.lastWaiter = prev;
870
+ }
871
+ requestPermits(amount, abort) {
872
+ if (amount <= 0 || amount > this.size) {
873
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
874
+ }
875
+ return new Promise((resolve, reject) => {
876
+ function rejectAborted() {
877
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
878
+ }
879
+ if (abort?.aborted) {
880
+ return rejectAborted();
881
+ }
882
+ let waiter;
883
+ const markCompleted = () => {
884
+ const items = waiter.acquiredItems;
885
+ waiter.acquiredItems = []; // Avoid releasing items twice.
886
+ for (const element of items) {
887
+ // Give to next waiter, if possible.
888
+ const nextWaiter = this.firstWaiter;
889
+ if (nextWaiter) {
890
+ nextWaiter.acquiredItems.push(element);
891
+ nextWaiter.remainingItems--;
892
+ if (nextWaiter.remainingItems == 0) {
893
+ nextWaiter.onAcquire();
894
+ }
895
+ }
896
+ else {
897
+ // No pending waiter, return lease into pool.
898
+ this.available.addLast(element);
899
+ }
900
+ }
901
+ };
902
+ const onAbort = () => {
903
+ abort?.removeEventListener('abort', onAbort);
904
+ if (waiter.isActive) {
905
+ this.deactivateWaiter(waiter);
906
+ rejectAborted();
907
+ }
908
+ };
909
+ const resolvePromise = () => {
910
+ this.deactivateWaiter(waiter);
911
+ abort?.removeEventListener('abort', onAbort);
912
+ const items = waiter.acquiredItems;
913
+ resolve({ items, release: markCompleted });
914
+ };
915
+ waiter = this.addWaiter(amount, resolvePromise);
916
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
917
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
918
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
919
+ waiter.acquiredItems.push(this.available.removeFirst());
920
+ waiter.remainingItems--;
806
921
  }
807
- catch (ex) {
808
- reject(ex);
922
+ if (waiter.remainingItems == 0) {
923
+ return resolvePromise();
809
924
  }
925
+ abort?.addEventListener('abort', onAbort);
810
926
  });
811
- });
927
+ }
928
+ /**
929
+ * Requests a single item from the pool.
930
+ *
931
+ * The returned `release` callback must be invoked to return the item into the pool.
932
+ */
933
+ async requestOne(abort) {
934
+ const { items, release } = await this.requestPermits(1, abort);
935
+ return { release, item: items[0] };
936
+ }
937
+ /**
938
+ * Requests access to all items from the pool.
939
+ *
940
+ * The returned `release` callback must be invoked to return items into the pool.
941
+ */
942
+ requestAll(abort) {
943
+ return this.requestPermits(this.size, abort);
944
+ }
945
+ }
946
+ /**
947
+ * An asynchronous mutex implementation.
948
+ *
949
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
950
+ */
951
+ class Mutex {
952
+ inner = new Semaphore([null]);
953
+ async acquire(abort) {
954
+ const { release } = await this.inner.requestOne(abort);
955
+ return release;
956
+ }
957
+ async runExclusive(fn, abort) {
958
+ const returnMutex = await this.acquire(abort);
959
+ try {
960
+ return await fn();
961
+ }
962
+ finally {
963
+ returnMutex();
964
+ }
965
+ }
966
+ }
967
+ function timeoutSignal(timeout) {
968
+ if (timeout == null)
969
+ return;
970
+ if ('timeout' in AbortSignal)
971
+ return AbortSignal.timeout(timeout);
972
+ const controller = new AbortController();
973
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
974
+ return controller.signal;
812
975
  }
813
976
 
814
977
  /**
@@ -857,7 +1020,7 @@ class AttachmentService {
857
1020
  * Executes a callback with exclusive access to the attachment context.
858
1021
  */
859
1022
  async withContext(callback) {
860
- return mutexRunExclusive(this.mutex, async () => {
1023
+ return this.mutex.runExclusive(async () => {
861
1024
  return callback(this.context);
862
1025
  });
863
1026
  }
@@ -893,9 +1056,15 @@ class AttachmentQueue {
893
1056
  tableName;
894
1057
  /** Logger instance for diagnostic information */
895
1058
  logger;
896
- /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
1059
+ /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
1060
+ * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
897
1061
  syncIntervalMs = 30 * 1000;
898
- /** Duration in milliseconds to throttle sync operations */
1062
+ /** Throttle duration in milliseconds for the reactive watch query on the attachments table.
1063
+ * When attachment records change, a watch query detects the change and triggers a sync.
1064
+ * This throttle prevents the sync from firing too rapidly when many changes happen in
1065
+ * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
1066
+ * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
1067
+ * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
899
1068
  syncThrottleDuration;
900
1069
  /** Whether to automatically download remote attachments. Default: true */
901
1070
  downloadAttachments = true;
@@ -919,8 +1088,8 @@ class AttachmentQueue {
919
1088
  * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
920
1089
  * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
921
1090
  * @param options.logger - Logger instance. Defaults to db.logger
922
- * @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000
923
- * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
1091
+ * @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
1092
+ * @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
924
1093
  * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
925
1094
  * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
926
1095
  */
@@ -1227,6 +1396,8 @@ var EncodingType;
1227
1396
  EncodingType["Base64"] = "base64";
1228
1397
  })(EncodingType || (EncodingType = {}));
1229
1398
 
1399
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
1400
+
1230
1401
  function getDefaultExportFromCjs (x) {
1231
1402
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
1232
1403
  }
@@ -1307,7 +1478,7 @@ function requireEventIterator () {
1307
1478
  this.removeCallback();
1308
1479
  });
1309
1480
  }
1310
- [Symbol.asyncIterator]() {
1481
+ [symbolAsyncIterator]() {
1311
1482
  return {
1312
1483
  next: (value) => {
1313
1484
  const result = this.pushQueue.shift();
@@ -1354,7 +1525,7 @@ function requireEventIterator () {
1354
1525
  queue.eventHandlers[event] = fn;
1355
1526
  },
1356
1527
  }) || (() => { });
1357
- this[Symbol.asyncIterator] = () => queue[Symbol.asyncIterator]();
1528
+ this[symbolAsyncIterator] = () => queue[symbolAsyncIterator]();
1358
1529
  Object.freeze(this);
1359
1530
  }
1360
1531
  }
@@ -1681,6 +1852,49 @@ var Logger = /*@__PURE__*/getDefaultExportFromCjs(loggerExports);
1681
1852
  * Set of generic interfaces to allow PowerSync compatibility with
1682
1853
  * different SQLite DB implementations.
1683
1854
  */
1855
+ /**
1856
+ * Implements {@link DBGetUtils} on a {@link SqlRunner}.
1857
+ */
1858
+ function DBGetUtilsDefaultMixin(Base) {
1859
+ return class extends Base {
1860
+ async getAll(sql, parameters) {
1861
+ const res = await this.execute(sql, parameters);
1862
+ return res.rows?._array ?? [];
1863
+ }
1864
+ async getOptional(sql, parameters) {
1865
+ const res = await this.execute(sql, parameters);
1866
+ return res.rows?.item(0) ?? null;
1867
+ }
1868
+ async get(sql, parameters) {
1869
+ const res = await this.execute(sql, parameters);
1870
+ const first = res.rows?.item(0);
1871
+ if (!first) {
1872
+ throw new Error('Result set is empty');
1873
+ }
1874
+ return first;
1875
+ }
1876
+ async executeBatch(query, params = []) {
1877
+ // If this context can run batch statements natively, use that.
1878
+ // @ts-ignore
1879
+ if (super.executeBatch) {
1880
+ // @ts-ignore
1881
+ return super.executeBatch(query, params);
1882
+ }
1883
+ // Emulate executeBatch by running statements individually.
1884
+ let lastInsertId;
1885
+ let rowsAffected = 0;
1886
+ for (const set of params) {
1887
+ const result = await this.execute(query, set);
1888
+ lastInsertId = result.insertId;
1889
+ rowsAffected += result.rowsAffected;
1890
+ }
1891
+ return {
1892
+ rowsAffected,
1893
+ insertId: lastInsertId
1894
+ };
1895
+ }
1896
+ };
1897
+ }
1684
1898
  /**
1685
1899
  * Update table operation numbers from SQLite
1686
1900
  */
@@ -1690,6 +1904,89 @@ var RowUpdateType;
1690
1904
  RowUpdateType[RowUpdateType["SQLITE_DELETE"] = 9] = "SQLITE_DELETE";
1691
1905
  RowUpdateType[RowUpdateType["SQLITE_UPDATE"] = 23] = "SQLITE_UPDATE";
1692
1906
  })(RowUpdateType || (RowUpdateType = {}));
1907
+ /**
1908
+ * A mixin to implement {@link DBAdapter} by delegating to {@link ConnectionPool.readLock} and
1909
+ * {@link ConnectionPool.writeLock}.
1910
+ */
1911
+ function DBAdapterDefaultMixin(Base) {
1912
+ return class extends Base {
1913
+ readTransaction(fn, options) {
1914
+ return this.readLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
1915
+ }
1916
+ writeTransaction(fn, options) {
1917
+ return this.writeLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
1918
+ }
1919
+ getAll(sql, parameters) {
1920
+ return this.readLock((ctx) => ctx.getAll(sql, parameters));
1921
+ }
1922
+ getOptional(sql, parameters) {
1923
+ return this.readLock((ctx) => ctx.getOptional(sql, parameters));
1924
+ }
1925
+ get(sql, parameters) {
1926
+ return this.readLock((ctx) => ctx.get(sql, parameters));
1927
+ }
1928
+ execute(query, params) {
1929
+ return this.writeLock((ctx) => ctx.execute(query, params));
1930
+ }
1931
+ executeRaw(query, params) {
1932
+ return this.writeLock((ctx) => ctx.executeRaw(query, params));
1933
+ }
1934
+ executeBatch(query, params) {
1935
+ return this.writeTransaction((tx) => tx.executeBatch(query, params));
1936
+ }
1937
+ };
1938
+ }
1939
+ class BaseTransaction {
1940
+ inner;
1941
+ finalized = false;
1942
+ constructor(inner) {
1943
+ this.inner = inner;
1944
+ }
1945
+ async commit() {
1946
+ if (this.finalized) {
1947
+ return { rowsAffected: 0 };
1948
+ }
1949
+ this.finalized = true;
1950
+ return this.inner.execute('COMMIT');
1951
+ }
1952
+ async rollback() {
1953
+ if (this.finalized) {
1954
+ return { rowsAffected: 0 };
1955
+ }
1956
+ this.finalized = true;
1957
+ return this.inner.execute('ROLLBACK');
1958
+ }
1959
+ execute(query, params) {
1960
+ return this.inner.execute(query, params);
1961
+ }
1962
+ executeRaw(query, params) {
1963
+ return this.inner.executeRaw(query, params);
1964
+ }
1965
+ executeBatch(query, params) {
1966
+ return this.inner.executeBatch(query, params);
1967
+ }
1968
+ }
1969
+ class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
1970
+ static async runWith(ctx, fn) {
1971
+ let tx = new TransactionImplementation(ctx);
1972
+ try {
1973
+ await ctx.execute('BEGIN IMMEDIATE');
1974
+ const result = await fn(tx);
1975
+ await tx.commit();
1976
+ return result;
1977
+ }
1978
+ catch (ex) {
1979
+ try {
1980
+ await tx.rollback();
1981
+ }
1982
+ catch (ex2) {
1983
+ // In rare cases, a rollback may fail.
1984
+ // Safe to ignore.
1985
+ }
1986
+ throw ex;
1987
+ }
1988
+ }
1989
+ }
1693
1990
  function isBatchedUpdateNotification(update) {
1694
1991
  return 'tables' in update;
1695
1992
  }
@@ -1832,16 +2129,12 @@ class SyncStatus {
1832
2129
  *
1833
2130
  * This returns null when the database is currently being opened and we don't have reliable information about all
1834
2131
  * included streams yet.
1835
- *
1836
- * @experimental Sync streams are currently in alpha.
1837
2132
  */
1838
2133
  get syncStreams() {
1839
2134
  return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core));
1840
2135
  }
1841
2136
  /**
1842
2137
  * If the `stream` appears in {@link syncStreams}, returns the current status for that stream.
1843
- *
1844
- * @experimental Sync streams are currently in alpha.
1845
2138
  */
1846
2139
  forStream(stream) {
1847
2140
  const asJson = JSON.stringify(stream.parameters);
@@ -2110,15 +2403,6 @@ class ControlledExecutor {
2110
2403
  }
2111
2404
  }
2112
2405
 
2113
- /**
2114
- * A ponyfill for `Symbol.asyncIterator` that is compatible with the
2115
- * [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)
2116
- * we recommend for React Native.
2117
- *
2118
- * As long as we use this symbol (instead of `for await` and `async *`) in this package, we can be compatible with async
2119
- * iterators without requiring them.
2120
- */
2121
- const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
2122
2406
  /**
2123
2407
  * Throttle a function to be called at most once every "wait" milliseconds,
2124
2408
  * on the trailing edge.
@@ -10455,177 +10739,10 @@ function requireDist () {
10455
10739
 
10456
10740
  var distExports = requireDist();
10457
10741
 
10458
- var version = "1.48.0";
10742
+ var version = "1.52.0";
10459
10743
  var PACKAGE = {
10460
10744
  version: version};
10461
10745
 
10462
- const DEFAULT_PRESSURE_LIMITS = {
10463
- highWater: 10,
10464
- lowWater: 0
10465
- };
10466
- /**
10467
- * A very basic implementation of a data stream with backpressure support which does not use
10468
- * native JS streams or async iterators.
10469
- * This is handy for environments such as React Native which need polyfills for the above.
10470
- */
10471
- class DataStream extends BaseObserver {
10472
- options;
10473
- dataQueue;
10474
- isClosed;
10475
- processingPromise;
10476
- notifyDataAdded;
10477
- logger;
10478
- mapLine;
10479
- constructor(options) {
10480
- super();
10481
- this.options = options;
10482
- this.processingPromise = null;
10483
- this.isClosed = false;
10484
- this.dataQueue = [];
10485
- this.mapLine = options?.mapLine ?? ((line) => line);
10486
- this.logger = options?.logger ?? Logger.get('DataStream');
10487
- if (options?.closeOnError) {
10488
- const l = this.registerListener({
10489
- error: (ex) => {
10490
- l?.();
10491
- this.close();
10492
- }
10493
- });
10494
- }
10495
- }
10496
- get highWatermark() {
10497
- return this.options?.pressure?.highWaterMark ?? DEFAULT_PRESSURE_LIMITS.highWater;
10498
- }
10499
- get lowWatermark() {
10500
- return this.options?.pressure?.lowWaterMark ?? DEFAULT_PRESSURE_LIMITS.lowWater;
10501
- }
10502
- get closed() {
10503
- return this.isClosed;
10504
- }
10505
- async close() {
10506
- this.isClosed = true;
10507
- await this.processingPromise;
10508
- this.iterateListeners((l) => l.closed?.());
10509
- // Discard any data in the queue
10510
- this.dataQueue = [];
10511
- this.listeners.clear();
10512
- }
10513
- /**
10514
- * Enqueues data for the consumers to read
10515
- */
10516
- enqueueData(data) {
10517
- if (this.isClosed) {
10518
- throw new Error('Cannot enqueue data into closed stream.');
10519
- }
10520
- this.dataQueue.push(data);
10521
- this.notifyDataAdded?.();
10522
- this.processQueue();
10523
- }
10524
- /**
10525
- * Reads data once from the data stream
10526
- * @returns a Data payload or Null if the stream closed.
10527
- */
10528
- async read() {
10529
- if (this.closed) {
10530
- return null;
10531
- }
10532
- // Wait for any pending processing to complete first.
10533
- // This ensures we register our listener before calling processQueue(),
10534
- // avoiding a race where processQueue() sees no reader and returns early.
10535
- if (this.processingPromise) {
10536
- await this.processingPromise;
10537
- }
10538
- // Re-check after await - stream may have closed while we were waiting
10539
- if (this.closed) {
10540
- return null;
10541
- }
10542
- return new Promise((resolve, reject) => {
10543
- const l = this.registerListener({
10544
- data: async (data) => {
10545
- resolve(data);
10546
- // Remove the listener
10547
- l?.();
10548
- },
10549
- closed: () => {
10550
- resolve(null);
10551
- l?.();
10552
- },
10553
- error: (ex) => {
10554
- reject(ex);
10555
- l?.();
10556
- }
10557
- });
10558
- this.processQueue();
10559
- });
10560
- }
10561
- /**
10562
- * Executes a callback for each data item in the stream
10563
- */
10564
- forEach(callback) {
10565
- if (this.dataQueue.length <= this.lowWatermark) {
10566
- this.iterateAsyncErrored(async (l) => l.lowWater?.());
10567
- }
10568
- return this.registerListener({
10569
- data: callback
10570
- });
10571
- }
10572
- processQueue() {
10573
- if (this.processingPromise) {
10574
- return;
10575
- }
10576
- const promise = (this.processingPromise = this._processQueue());
10577
- promise.finally(() => {
10578
- this.processingPromise = null;
10579
- });
10580
- return promise;
10581
- }
10582
- hasDataReader() {
10583
- return Array.from(this.listeners.values()).some((l) => !!l.data);
10584
- }
10585
- async _processQueue() {
10586
- /**
10587
- * Allow listeners to mutate the queue before processing.
10588
- * This allows for operations such as dropping or compressing data
10589
- * on high water or requesting more data on low water.
10590
- */
10591
- if (this.dataQueue.length >= this.highWatermark) {
10592
- await this.iterateAsyncErrored(async (l) => l.highWater?.());
10593
- }
10594
- if (this.isClosed || !this.hasDataReader()) {
10595
- return;
10596
- }
10597
- if (this.dataQueue.length) {
10598
- const data = this.dataQueue.shift();
10599
- const mapped = this.mapLine(data);
10600
- await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
10601
- }
10602
- if (this.dataQueue.length <= this.lowWatermark) {
10603
- const dataAdded = new Promise((resolve) => {
10604
- this.notifyDataAdded = resolve;
10605
- });
10606
- await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
10607
- this.notifyDataAdded = null;
10608
- }
10609
- if (this.dataQueue.length > 0) {
10610
- setTimeout(() => this.processQueue());
10611
- }
10612
- }
10613
- async iterateAsyncErrored(cb) {
10614
- // Important: We need to copy the listeners, as calling a listener could result in adding another
10615
- // listener, resulting in infinite loops.
10616
- const listeners = Array.from(this.listeners.values());
10617
- for (let i of listeners) {
10618
- try {
10619
- await cb(i);
10620
- }
10621
- catch (ex) {
10622
- this.logger.error(ex);
10623
- this.iterateListeners((l) => l.error?.(ex));
10624
- }
10625
- }
10626
- }
10627
- }
10628
-
10629
10746
  var WebsocketDuplexConnection = {};
10630
10747
 
10631
10748
  var hasRequiredWebsocketDuplexConnection;
@@ -10788,8 +10905,215 @@ class WebsocketClientTransport {
10788
10905
  }
10789
10906
  }
10790
10907
 
10908
+ const doneResult = { done: true, value: undefined };
10909
+ function valueResult(value) {
10910
+ return { done: false, value };
10911
+ }
10912
+ /**
10913
+ * A variant of {@link Array.map} for async iterators.
10914
+ */
10915
+ function map(source, map) {
10916
+ return {
10917
+ next: async () => {
10918
+ const value = await source.next();
10919
+ if (value.done) {
10920
+ return value;
10921
+ }
10922
+ else {
10923
+ return { value: map(value.value) };
10924
+ }
10925
+ }
10926
+ };
10927
+ }
10928
+ /**
10929
+ * Expands a source async iterator by allowing to inject events asynchronously.
10930
+ *
10931
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
10932
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
10933
+ *
10934
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
10935
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
10936
+ */
10937
+ function injectable(source) {
10938
+ let sourceIsDone = false;
10939
+ let waiter = undefined; // An active, waiting next() call.
10940
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
10941
+ let pendingSourceEvent = null;
10942
+ let pendingInjectedEvents = [];
10943
+ const consumeWaiter = () => {
10944
+ const pending = waiter;
10945
+ waiter = undefined;
10946
+ return pending;
10947
+ };
10948
+ const fetchFromSource = () => {
10949
+ const resolveWaiter = (propagate) => {
10950
+ const active = consumeWaiter();
10951
+ if (active) {
10952
+ propagate(active);
10953
+ }
10954
+ else {
10955
+ pendingSourceEvent = propagate;
10956
+ }
10957
+ };
10958
+ const nextFromSource = source.next();
10959
+ nextFromSource.then((value) => {
10960
+ sourceIsDone = value.done == true;
10961
+ resolveWaiter((w) => w.resolve(value));
10962
+ }, (error) => {
10963
+ resolveWaiter((w) => w.reject(error));
10964
+ });
10965
+ };
10966
+ return {
10967
+ next: () => {
10968
+ return new Promise((resolve, reject) => {
10969
+ // First priority: Dispatch ready upstream events.
10970
+ if (sourceIsDone) {
10971
+ return resolve(doneResult);
10972
+ }
10973
+ if (pendingSourceEvent) {
10974
+ pendingSourceEvent({ resolve, reject });
10975
+ pendingSourceEvent = null;
10976
+ return;
10977
+ }
10978
+ // Second priority: Dispatch injected events
10979
+ if (pendingInjectedEvents.length) {
10980
+ return resolve(valueResult(pendingInjectedEvents.shift()));
10981
+ }
10982
+ // Nothing pending? Fetch from source
10983
+ waiter = { resolve, reject };
10984
+ return fetchFromSource();
10985
+ });
10986
+ },
10987
+ inject: (event) => {
10988
+ const pending = consumeWaiter();
10989
+ if (pending != null) {
10990
+ pending.resolve(valueResult(event));
10991
+ }
10992
+ else {
10993
+ pendingInjectedEvents.push(event);
10994
+ }
10995
+ }
10996
+ };
10997
+ }
10998
+ /**
10999
+ * Splits a byte stream at line endings, emitting each line as a string.
11000
+ */
11001
+ function extractJsonLines(source, decoder) {
11002
+ let buffer = '';
11003
+ const pendingLines = [];
11004
+ let isFinalEvent = false;
11005
+ return {
11006
+ next: async () => {
11007
+ while (true) {
11008
+ if (isFinalEvent) {
11009
+ return doneResult;
11010
+ }
11011
+ {
11012
+ const first = pendingLines.shift();
11013
+ if (first) {
11014
+ return { done: false, value: first };
11015
+ }
11016
+ }
11017
+ const { done, value } = await source.next();
11018
+ if (done) {
11019
+ const remaining = buffer.trim();
11020
+ if (remaining.length != 0) {
11021
+ isFinalEvent = true;
11022
+ return { done: false, value: remaining };
11023
+ }
11024
+ return doneResult;
11025
+ }
11026
+ const data = decoder.decode(value, { stream: true });
11027
+ buffer += data;
11028
+ const lines = buffer.split('\n');
11029
+ for (let i = 0; i < lines.length - 1; i++) {
11030
+ const l = lines[i].trim();
11031
+ if (l.length > 0) {
11032
+ pendingLines.push(l);
11033
+ }
11034
+ }
11035
+ buffer = lines[lines.length - 1];
11036
+ }
11037
+ }
11038
+ };
11039
+ }
11040
+ /**
11041
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
11042
+ */
11043
+ function extractBsonObjects(source) {
11044
+ // Fully read but not emitted yet.
11045
+ const completedObjects = [];
11046
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
11047
+ let isDone = false;
11048
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
11049
+ let objectBody = null;
11050
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
11051
+ // If we're consuming a document, the bytes remaining.
11052
+ let remainingLength = 4;
11053
+ return {
11054
+ async next() {
11055
+ while (true) {
11056
+ // Before fetching new data from upstream, return completed objects.
11057
+ if (completedObjects.length) {
11058
+ return valueResult(completedObjects.shift());
11059
+ }
11060
+ if (isDone) {
11061
+ return doneResult;
11062
+ }
11063
+ const upstreamEvent = await source.next();
11064
+ if (upstreamEvent.done) {
11065
+ isDone = true;
11066
+ if (objectBody || remainingLength != 4) {
11067
+ throw new Error('illegal end of stream in BSON object');
11068
+ }
11069
+ return doneResult;
11070
+ }
11071
+ const chunk = upstreamEvent.value;
11072
+ for (let i = 0; i < chunk.length;) {
11073
+ const availableInData = chunk.length - i;
11074
+ if (objectBody) {
11075
+ // We're in the middle of reading a BSON document.
11076
+ const bytesToRead = Math.min(availableInData, remainingLength);
11077
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
11078
+ objectBody.set(copySource, objectBody.length - remainingLength);
11079
+ i += bytesToRead;
11080
+ remainingLength -= bytesToRead;
11081
+ if (remainingLength == 0) {
11082
+ completedObjects.push(objectBody);
11083
+ // Prepare to read another document, starting with its length
11084
+ objectBody = null;
11085
+ remainingLength = 4;
11086
+ }
11087
+ }
11088
+ else {
11089
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
11090
+ const bytesToRead = Math.min(availableInData, remainingLength);
11091
+ for (let j = 0; j < bytesToRead; j++) {
11092
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
11093
+ }
11094
+ i += bytesToRead;
11095
+ remainingLength -= bytesToRead;
11096
+ if (remainingLength == 0) {
11097
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
11098
+ // header is included in length.
11099
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
11100
+ remainingLength = length - 4;
11101
+ if (remainingLength < 1) {
11102
+ throw new Error(`invalid length for bson: ${length}`);
11103
+ }
11104
+ objectBody = new Uint8Array(length);
11105
+ new DataView(objectBody.buffer).setInt32(0, length, true);
11106
+ }
11107
+ }
11108
+ }
11109
+ }
11110
+ }
11111
+ };
11112
+ }
11113
+
10791
11114
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
10792
11115
  const POWERSYNC_JS_VERSION = PACKAGE.version;
11116
+ const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
10793
11117
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
10794
11118
  // Keep alive message is sent every period
10795
11119
  const KEEP_ALIVE_MS = 20_000;
@@ -10969,13 +11293,14 @@ class AbstractRemote {
10969
11293
  return new WebSocket(url);
10970
11294
  }
10971
11295
  /**
10972
- * Returns a data stream of sync line data.
11296
+ * Returns a data stream of sync line data, fetched via RSocket-over-WebSocket.
11297
+ *
11298
+ * The only mechanism to abort the returned stream is to use the abort signal in {@link SocketSyncStreamOptions}.
10973
11299
  *
10974
- * @param map Maps received payload frames to the typed event value.
10975
11300
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
10976
11301
  * (required for compatibility with older sync services).
10977
11302
  */
10978
- async socketStreamRaw(options, map, bson) {
11303
+ async socketStreamRaw(options, bson) {
10979
11304
  const { path, fetchStrategy = FetchStrategy.Buffered } = options;
10980
11305
  const mimeType = bson == null ? 'application/json' : 'application/bson';
10981
11306
  function toBuffer(js) {
@@ -10990,52 +11315,55 @@ class AbstractRemote {
10990
11315
  }
10991
11316
  const syncQueueRequestSize = fetchStrategy == FetchStrategy.Buffered ? 10 : 1;
10992
11317
  const request = await this.buildRequest(path);
11318
+ const url = this.options.socketUrlTransformer(request.url);
10993
11319
  // Add the user agent in the setup payload - we can't set custom
10994
11320
  // headers with websockets on web. The browser userAgent is however added
10995
11321
  // automatically as a header.
10996
11322
  const userAgent = this.getUserAgent();
10997
- const stream = new DataStream({
10998
- logger: this.logger,
10999
- pressure: {
11000
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
11001
- },
11002
- mapLine: map
11003
- });
11323
+ // While we're connecting (a process that can't be aborted in RSocket), the WebSocket instance to close if we wanted
11324
+ // to abort the connection.
11325
+ let pendingSocket = null;
11326
+ let keepAliveTimeout;
11327
+ let rsocket = null;
11328
+ let queue = null;
11329
+ let didClose = false;
11330
+ const abortRequest = () => {
11331
+ if (didClose) {
11332
+ return;
11333
+ }
11334
+ didClose = true;
11335
+ clearTimeout(keepAliveTimeout);
11336
+ if (pendingSocket) {
11337
+ pendingSocket.close();
11338
+ }
11339
+ if (rsocket) {
11340
+ rsocket.close();
11341
+ }
11342
+ if (queue) {
11343
+ queue.stop();
11344
+ }
11345
+ };
11004
11346
  // Handle upstream abort
11005
- if (options.abortSignal?.aborted) {
11347
+ if (options.abortSignal.aborted) {
11006
11348
  throw new AbortOperation('Connection request aborted');
11007
11349
  }
11008
11350
  else {
11009
- options.abortSignal?.addEventListener('abort', () => {
11010
- stream.close();
11011
- }, { once: true });
11351
+ options.abortSignal.addEventListener('abort', abortRequest);
11012
11352
  }
11013
- let keepAliveTimeout;
11014
11353
  const resetTimeout = () => {
11015
11354
  clearTimeout(keepAliveTimeout);
11016
11355
  keepAliveTimeout = setTimeout(() => {
11017
11356
  this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
11018
- stream.close();
11357
+ abortRequest();
11019
11358
  }, SOCKET_TIMEOUT_MS);
11020
11359
  };
11021
11360
  resetTimeout();
11022
- // Typescript complains about this being `never` if it's not assigned here.
11023
- // This is assigned in `wsCreator`.
11024
- let disposeSocketConnectionTimeout = () => { };
11025
- const url = this.options.socketUrlTransformer(request.url);
11026
11361
  const connector = new distExports.RSocketConnector({
11027
11362
  transport: new WebsocketClientTransport({
11028
11363
  url,
11029
11364
  wsCreator: (url) => {
11030
- const socket = this.createSocket(url);
11031
- disposeSocketConnectionTimeout = stream.registerListener({
11032
- closed: () => {
11033
- // Allow closing the underlying WebSocket if the stream was closed before the
11034
- // RSocket connect completed. This should effectively abort the request.
11035
- socket.close();
11036
- }
11037
- });
11038
- socket.addEventListener('message', (event) => {
11365
+ const socket = (pendingSocket = this.createSocket(url));
11366
+ socket.addEventListener('message', () => {
11039
11367
  resetTimeout();
11040
11368
  });
11041
11369
  return socket;
@@ -11055,43 +11383,40 @@ class AbstractRemote {
11055
11383
  }
11056
11384
  }
11057
11385
  });
11058
- let rsocket;
11059
11386
  try {
11060
11387
  rsocket = await connector.connect();
11061
11388
  // The connection is established, we no longer need to monitor the initial timeout
11062
- disposeSocketConnectionTimeout();
11389
+ pendingSocket = null;
11063
11390
  }
11064
11391
  catch (ex) {
11065
11392
  this.logger.error(`Failed to connect WebSocket`, ex);
11066
- clearTimeout(keepAliveTimeout);
11067
- if (!stream.closed) {
11068
- await stream.close();
11069
- }
11393
+ abortRequest();
11070
11394
  throw ex;
11071
11395
  }
11072
11396
  resetTimeout();
11073
- let socketIsClosed = false;
11074
- const closeSocket = () => {
11075
- clearTimeout(keepAliveTimeout);
11076
- if (socketIsClosed) {
11077
- return;
11078
- }
11079
- socketIsClosed = true;
11080
- rsocket.close();
11081
- };
11082
11397
  // Helps to prevent double close scenarios
11083
- rsocket.onClose(() => (socketIsClosed = true));
11084
- // We initially request this amount and expect these to arrive eventually
11085
- let pendingEventsCount = syncQueueRequestSize;
11086
- const disposeClosedListener = stream.registerListener({
11087
- closed: () => {
11088
- closeSocket();
11089
- disposeClosedListener();
11090
- }
11091
- });
11092
- const socket = await new Promise((resolve, reject) => {
11398
+ rsocket.onClose(() => (rsocket = null));
11399
+ return await new Promise((resolve, reject) => {
11093
11400
  let connectionEstablished = false;
11094
- const res = rsocket.requestStream({
11401
+ let pendingEventsCount = syncQueueRequestSize;
11402
+ let paused = false;
11403
+ let res = null;
11404
+ function requestMore() {
11405
+ const delta = syncQueueRequestSize - pendingEventsCount;
11406
+ if (!paused && delta > 0) {
11407
+ res?.request(delta);
11408
+ pendingEventsCount = syncQueueRequestSize;
11409
+ }
11410
+ }
11411
+ const events = new domExports.EventIterator((q) => {
11412
+ queue = q;
11413
+ q.on('highWater', () => (paused = true));
11414
+ q.on('lowWater', () => {
11415
+ paused = false;
11416
+ requestMore();
11417
+ });
11418
+ }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[symbolAsyncIterator]();
11419
+ res = rsocket.requestStream({
11095
11420
  data: toBuffer(options.data),
11096
11421
  metadata: toBuffer({
11097
11422
  path
@@ -11116,7 +11441,7 @@ class AbstractRemote {
11116
11441
  }
11117
11442
  // RSocket will close the RSocket stream automatically
11118
11443
  // Close the downstream stream as well - this will close the RSocket connection and WebSocket
11119
- stream.close();
11444
+ abortRequest();
11120
11445
  // Handles cases where the connection failed e.g. auth error or connection error
11121
11446
  if (!connectionEstablished) {
11122
11447
  reject(e);
@@ -11126,41 +11451,40 @@ class AbstractRemote {
11126
11451
  // The connection is active
11127
11452
  if (!connectionEstablished) {
11128
11453
  connectionEstablished = true;
11129
- resolve(res);
11454
+ resolve(events);
11130
11455
  }
11131
11456
  const { data } = payload;
11457
+ if (data) {
11458
+ queue.push(data);
11459
+ }
11132
11460
  // Less events are now pending
11133
11461
  pendingEventsCount--;
11134
- if (!data) {
11135
- return;
11136
- }
11137
- stream.enqueueData(data);
11462
+ // Request another event (unless the downstream consumer is paused).
11463
+ requestMore();
11138
11464
  },
11139
11465
  onComplete: () => {
11140
- stream.close();
11466
+ abortRequest(); // this will also emit a done event
11141
11467
  },
11142
11468
  onExtension: () => { }
11143
11469
  });
11144
11470
  });
11145
- const l = stream.registerListener({
11146
- lowWater: async () => {
11147
- // Request to fill up the queue
11148
- const required = syncQueueRequestSize - pendingEventsCount;
11149
- if (required > 0) {
11150
- socket.request(syncQueueRequestSize - pendingEventsCount);
11151
- pendingEventsCount = syncQueueRequestSize;
11152
- }
11153
- },
11154
- closed: () => {
11155
- l();
11156
- }
11157
- });
11158
- return stream;
11159
11471
  }
11160
11472
  /**
11161
- * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
11473
+ * @returns Whether the HTTP implementation on this platform can receive streamed binary responses. This is true on
11474
+ * all platforms except React Native (who would have guessed...), where we must not request BSON responses.
11475
+ *
11476
+ * @see https://github.com/react-native-community/fetch?tab=readme-ov-file#motivation
11162
11477
  */
11163
- async postStreamRaw(options, mapLine) {
11478
+ get supportsStreamingBinaryResponses() {
11479
+ return true;
11480
+ }
11481
+ /**
11482
+ * Posts a `/sync/stream` request, asserts that it completes successfully and returns the streaming response as an
11483
+ * async iterator of byte blobs.
11484
+ *
11485
+ * To cancel the async iterator, use the abort signal from {@link SyncStreamOptions} passed to this method.
11486
+ */
11487
+ async fetchStreamRaw(options) {
11164
11488
  const { data, path, headers, abortSignal } = options;
11165
11489
  const request = await this.buildRequest(path);
11166
11490
  /**
@@ -11172,119 +11496,94 @@ class AbstractRemote {
11172
11496
  * Aborting the active fetch request while it is being consumed seems to throw
11173
11497
  * an unhandled exception on the window level.
11174
11498
  */
11175
- if (abortSignal?.aborted) {
11176
- throw new AbortOperation('Abort request received before making postStreamRaw request');
11499
+ if (abortSignal.aborted) {
11500
+ throw new AbortOperation('Abort request received before making fetchStreamRaw request');
11177
11501
  }
11178
11502
  const controller = new AbortController();
11179
- let requestResolved = false;
11180
- abortSignal?.addEventListener('abort', () => {
11181
- if (!requestResolved) {
11503
+ let reader = null;
11504
+ abortSignal.addEventListener('abort', () => {
11505
+ const reason = abortSignal.reason ??
11506
+ new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.');
11507
+ if (reader == null) {
11182
11508
  // Only abort via the abort controller if the request has not resolved yet
11183
- controller.abort(abortSignal.reason ??
11184
- new AbortOperation('Cancelling network request before it resolves. Abort signal has been received.'));
11509
+ controller.abort(reason);
11510
+ }
11511
+ else {
11512
+ reader.cancel(reason).catch(() => {
11513
+ // Cancelling the reader might rethrow an exception we would have handled by throwing in next(). So we can
11514
+ // ignore it here.
11515
+ });
11185
11516
  }
11186
11517
  });
11187
- const res = await this.fetch(request.url, {
11188
- method: 'POST',
11189
- headers: { ...headers, ...request.headers },
11190
- body: JSON.stringify(data),
11191
- signal: controller.signal,
11192
- cache: 'no-store',
11193
- ...(this.options.fetchOptions ?? {}),
11194
- ...options.fetchOptions
11195
- }).catch((ex) => {
11518
+ let res;
11519
+ let responseIsBson = false;
11520
+ try {
11521
+ const ndJson = 'application/x-ndjson';
11522
+ const bson = 'application/vnd.powersync.bson-stream';
11523
+ res = await this.fetch(request.url, {
11524
+ method: 'POST',
11525
+ headers: {
11526
+ ...headers,
11527
+ ...request.headers,
11528
+ accept: this.supportsStreamingBinaryResponses ? `${bson};q=0.9,${ndJson};q=0.8` : ndJson
11529
+ },
11530
+ body: JSON.stringify(data),
11531
+ signal: controller.signal,
11532
+ cache: 'no-store',
11533
+ ...(this.options.fetchOptions ?? {}),
11534
+ ...options.fetchOptions
11535
+ });
11536
+ if (!res.ok || !res.body) {
11537
+ const text = await res.text();
11538
+ this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
11539
+ const error = new Error(`HTTP ${res.statusText}: ${text}`);
11540
+ error.status = res.status;
11541
+ throw error;
11542
+ }
11543
+ const contentType = res.headers.get('content-type');
11544
+ responseIsBson = contentType == bson;
11545
+ }
11546
+ catch (ex) {
11196
11547
  if (ex.name == 'AbortError') {
11197
11548
  throw new AbortOperation(`Pending fetch request to ${request.url} has been aborted.`);
11198
11549
  }
11199
11550
  throw ex;
11200
- });
11201
- if (!res) {
11202
- throw new Error('Fetch request was aborted');
11203
- }
11204
- requestResolved = true;
11205
- if (!res.ok || !res.body) {
11206
- const text = await res.text();
11207
- this.logger.error(`Could not POST streaming to ${path} - ${res.status} - ${res.statusText}: ${text}`);
11208
- const error = new Error(`HTTP ${res.statusText}: ${text}`);
11209
- error.status = res.status;
11210
- throw error;
11211
11551
  }
11212
- // Create a new stream splitting the response at line endings while also handling cancellations
11213
- // by closing the reader.
11214
- const reader = res.body.getReader();
11215
- let readerReleased = false;
11216
- // This will close the network request and read stream
11217
- const closeReader = async () => {
11218
- try {
11219
- readerReleased = true;
11220
- await reader.cancel();
11221
- }
11222
- catch (ex) {
11223
- // an error will throw if the reader hasn't been used yet
11224
- }
11225
- reader.releaseLock();
11226
- };
11227
- const stream = new DataStream({
11228
- logger: this.logger,
11229
- mapLine: mapLine,
11230
- pressure: {
11231
- highWaterMark: 20,
11232
- lowWaterMark: 10
11233
- }
11234
- });
11235
- abortSignal?.addEventListener('abort', () => {
11236
- closeReader();
11237
- stream.close();
11238
- });
11239
- const decoder = this.createTextDecoder();
11240
- let buffer = '';
11241
- const consumeStream = async () => {
11242
- while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
11243
- const { done, value } = await reader.read();
11244
- if (done) {
11245
- const remaining = buffer.trim();
11246
- if (remaining.length != 0) {
11247
- stream.enqueueData(remaining);
11248
- }
11249
- stream.close();
11250
- await closeReader();
11251
- return;
11552
+ reader = res.body.getReader();
11553
+ const stream = {
11554
+ next: async () => {
11555
+ if (controller.signal.aborted) {
11556
+ return doneResult;
11252
11557
  }
11253
- const data = decoder.decode(value, { stream: true });
11254
- buffer += data;
11255
- const lines = buffer.split('\n');
11256
- for (var i = 0; i < lines.length - 1; i++) {
11257
- var l = lines[i].trim();
11258
- if (l.length > 0) {
11259
- stream.enqueueData(l);
11260
- }
11558
+ try {
11559
+ return await reader.read();
11261
11560
  }
11262
- buffer = lines[lines.length - 1];
11263
- // Implement backpressure by waiting for the low water mark to be reached
11264
- if (stream.dataQueue.length > stream.highWatermark) {
11265
- await new Promise((resolve) => {
11266
- const dispose = stream.registerListener({
11267
- lowWater: async () => {
11268
- resolve();
11269
- dispose();
11270
- },
11271
- closed: () => {
11272
- resolve();
11273
- dispose();
11274
- }
11275
- });
11276
- });
11561
+ catch (ex) {
11562
+ if (controller.signal.aborted) {
11563
+ // .read() completes with an error if we cancel the reader, which we do to disconnect. So this is just
11564
+ // things working as intended, we can return a done event and consider the exception handled.
11565
+ return doneResult;
11566
+ }
11567
+ throw ex;
11277
11568
  }
11278
11569
  }
11279
11570
  };
11280
- consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
11281
- const l = stream.registerListener({
11282
- closed: () => {
11283
- closeReader();
11284
- l?.();
11285
- }
11286
- });
11287
- return stream;
11571
+ return { isBson: responseIsBson, stream };
11572
+ }
11573
+ /**
11574
+ * Posts a `/sync/stream` request.
11575
+ *
11576
+ * Depending on the `Content-Type` of the response, this returns strings for sync lines or encoded BSON documents as
11577
+ * {@link Uint8Array}s.
11578
+ */
11579
+ async fetchStream(options) {
11580
+ const { isBson, stream } = await this.fetchStreamRaw(options);
11581
+ if (isBson) {
11582
+ return extractBsonObjects(stream);
11583
+ }
11584
+ else {
11585
+ return extractJsonLines(stream, this.createTextDecoder());
11586
+ }
11288
11587
  }
11289
11588
  }
11290
11589
 
@@ -11419,6 +11718,7 @@ class AbstractStreamingSyncImplementation extends BaseObserver {
11419
11718
  streamingSyncPromise;
11420
11719
  logger;
11421
11720
  activeStreams;
11721
+ connectionMayHaveChanged = false;
11422
11722
  isUploadingCrud = false;
11423
11723
  notifyCompletedUploads;
11424
11724
  handleActiveStreamsChange;
@@ -11698,6 +11998,11 @@ The next upload iteration will be delayed.`);
11698
11998
  shouldDelayRetry = false;
11699
11999
  // A disconnect was requested, we should not delay since there is no explicit retry
11700
12000
  }
12001
+ else if (this.connectionMayHaveChanged && ex.message?.indexOf('No iteration is active') >= 0) {
12002
+ this.connectionMayHaveChanged = false;
12003
+ this.logger.info('Sync error after changed connection, retrying immediately');
12004
+ shouldDelayRetry = false;
12005
+ }
11701
12006
  else {
11702
12007
  this.logger.error(ex);
11703
12008
  }
@@ -11728,6 +12033,15 @@ The next upload iteration will be delayed.`);
11728
12033
  // Mark as disconnected if here
11729
12034
  this.updateSyncStatus({ connected: false, connecting: false });
11730
12035
  }
12036
+ markConnectionMayHaveChanged() {
12037
+ // By setting this field, we'll immediately retry if the next sync event causes an error triggered by us not having
12038
+ // an active sync iteration on the connection in use.
12039
+ this.connectionMayHaveChanged = true;
12040
+ // This triggers a `powersync_control` invocation if a sync iteration is currently active. This is a cheap call to
12041
+ // make when no subscriptions have actually changed, we're mainly interested in this immediately throwing if no
12042
+ // iteration is active. That allows us to reconnect ASAP, instead of having to wait for the next sync line.
12043
+ this.handleActiveStreamsChange?.();
12044
+ }
11731
12045
  async collectLocalBucketState() {
11732
12046
  const bucketEntries = await this.options.adapter.getBucketStates();
11733
12047
  const req = bucketEntries.map((entry) => ({
@@ -11792,6 +12106,19 @@ The next upload iteration will be delayed.`);
11792
12106
  }
11793
12107
  });
11794
12108
  }
12109
+ async receiveSyncLines(data) {
12110
+ const { options, connection, bson } = data;
12111
+ const remote = this.options.remote;
12112
+ if (connection.connectionMethod == SyncStreamConnectionMethod.HTTP) {
12113
+ return await remote.fetchStream(options);
12114
+ }
12115
+ else {
12116
+ return await this.options.remote.socketStreamRaw({
12117
+ ...options,
12118
+ ...{ fetchStrategy: connection.fetchStrategy }
12119
+ }, bson);
12120
+ }
12121
+ }
11795
12122
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
11796
12123
  const rawTables = resolvedOptions.serializedSchema?.raw_tables;
11797
12124
  if (rawTables != null && rawTables.length) {
@@ -11821,42 +12148,27 @@ The next upload iteration will be delayed.`);
11821
12148
  client_id: clientId
11822
12149
  }
11823
12150
  };
11824
- let stream;
11825
- if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
11826
- stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
11827
- if (typeof line == 'string') {
11828
- return JSON.parse(line);
11829
- }
11830
- else {
11831
- // Directly enqueued by us
11832
- return line;
11833
- }
11834
- });
11835
- }
11836
- else {
11837
- const bson = await this.options.remote.getBSON();
11838
- stream = await this.options.remote.socketStreamRaw({
11839
- ...syncOptions,
11840
- ...{ fetchStrategy: resolvedOptions.fetchStrategy }
11841
- }, (payload) => {
11842
- if (payload instanceof Uint8Array) {
11843
- return bson.deserialize(payload);
11844
- }
11845
- else {
11846
- // Directly enqueued by us
11847
- return payload;
11848
- }
11849
- }, bson);
11850
- }
12151
+ const bson = await this.options.remote.getBSON();
12152
+ const source = await this.receiveSyncLines({
12153
+ options: syncOptions,
12154
+ connection: resolvedOptions,
12155
+ bson
12156
+ });
12157
+ const stream = injectable(map(source, (line) => {
12158
+ if (typeof line == 'string') {
12159
+ return JSON.parse(line);
12160
+ }
12161
+ else {
12162
+ return bson.deserialize(line);
12163
+ }
12164
+ }));
11851
12165
  this.logger.debug('Stream established. Processing events');
11852
12166
  this.notifyCompletedUploads = () => {
11853
- if (!stream.closed) {
11854
- stream.enqueueData({ crud_upload_completed: null });
11855
- }
12167
+ stream.inject({ crud_upload_completed: null });
11856
12168
  };
11857
- while (!stream.closed) {
11858
- const line = await stream.read();
11859
- if (!line) {
12169
+ while (true) {
12170
+ const { value: line, done } = await stream.next();
12171
+ if (done) {
11860
12172
  // The stream has closed while waiting
11861
12173
  return;
11862
12174
  }
@@ -12035,14 +12347,17 @@ The next upload iteration will be delayed.`);
12035
12347
  const syncImplementation = this;
12036
12348
  const adapter = this.options.adapter;
12037
12349
  const remote = this.options.remote;
12350
+ const controller = new AbortController();
12351
+ const abort = () => {
12352
+ return controller.abort(signal.reason);
12353
+ };
12354
+ signal.addEventListener('abort', abort);
12038
12355
  let receivingLines = null;
12039
12356
  let hadSyncLine = false;
12040
12357
  let hideDisconnectOnRestart = false;
12041
12358
  if (signal.aborted) {
12042
12359
  throw new AbortOperation('Connection request has been aborted');
12043
12360
  }
12044
- const abortController = new AbortController();
12045
- signal.addEventListener('abort', () => abortController.abort());
12046
12361
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
12047
12362
  // invocation (local events include refreshed tokens and completed uploads).
12048
12363
  // This is a single data stream so that we can handle all control calls from a single place.
@@ -12050,49 +12365,36 @@ The next upload iteration will be delayed.`);
12050
12365
  async function connect(instr) {
12051
12366
  const syncOptions = {
12052
12367
  path: '/sync/stream',
12053
- abortSignal: abortController.signal,
12368
+ abortSignal: controller.signal,
12054
12369
  data: instr.request
12055
12370
  };
12056
- if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
12057
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
12058
- if (typeof line == 'string') {
12059
- return {
12060
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
12061
- payload: line
12062
- };
12063
- }
12064
- else {
12065
- // Directly enqueued by us
12066
- return line;
12067
- }
12068
- });
12069
- }
12070
- else {
12071
- controlInvocations = await remote.socketStreamRaw({
12072
- ...syncOptions,
12073
- fetchStrategy: resolvedOptions.fetchStrategy
12074
- }, (payload) => {
12075
- if (payload instanceof Uint8Array) {
12076
- return {
12077
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
12078
- payload: payload
12079
- };
12080
- }
12081
- else {
12082
- // Directly enqueued by us
12083
- return payload;
12084
- }
12085
- });
12086
- }
12371
+ controlInvocations = injectable(map(await syncImplementation.receiveSyncLines({
12372
+ options: syncOptions,
12373
+ connection: resolvedOptions
12374
+ }), (line) => {
12375
+ if (typeof line == 'string') {
12376
+ return {
12377
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
12378
+ payload: line
12379
+ };
12380
+ }
12381
+ else {
12382
+ return {
12383
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
12384
+ payload: line
12385
+ };
12386
+ }
12387
+ }));
12087
12388
  // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
12088
12389
  // we're already connected here and can report that.
12089
12390
  syncImplementation.updateSyncStatus({ connected: true });
12090
12391
  try {
12091
- while (!controlInvocations.closed) {
12092
- const line = await controlInvocations.read();
12093
- if (line == null) {
12094
- return;
12392
+ while (true) {
12393
+ let event = await controlInvocations.next();
12394
+ if (event.done) {
12395
+ break;
12095
12396
  }
12397
+ const line = event.value;
12096
12398
  await control(line.command, line.payload);
12097
12399
  if (!hadSyncLine) {
12098
12400
  syncImplementation.triggerCrudUpload();
@@ -12101,12 +12403,8 @@ The next upload iteration will be delayed.`);
12101
12403
  }
12102
12404
  }
12103
12405
  finally {
12104
- const activeInstructions = controlInvocations;
12105
- // We concurrently add events to the active data stream when e.g. a CRUD upload is completed or a token is
12106
- // refreshed. That would throw after closing (and we can't handle those events either way), so set this back
12107
- // to null.
12108
- controlInvocations = null;
12109
- await activeInstructions.close();
12406
+ abort();
12407
+ signal.removeEventListener('abort', abort);
12110
12408
  }
12111
12409
  }
12112
12410
  async function stop() {
@@ -12116,6 +12414,10 @@ The next upload iteration will be delayed.`);
12116
12414
  const rawResponse = await adapter.control(op, payload ?? null);
12117
12415
  const logger = syncImplementation.logger;
12118
12416
  logger.trace('powersync_control', op, payload == null || typeof payload == 'string' ? payload : '<bytes>', rawResponse);
12417
+ if (op != PowerSyncControlCommand.STOP) {
12418
+ // Evidently we have a working connection here, otherwise powersync_control would have failed.
12419
+ syncImplementation.connectionMayHaveChanged = false;
12420
+ }
12119
12421
  await handleInstructions(JSON.parse(rawResponse));
12120
12422
  }
12121
12423
  async function handleInstruction(instruction) {
@@ -12150,14 +12452,14 @@ The next upload iteration will be delayed.`);
12150
12452
  remote.invalidateCredentials();
12151
12453
  // Restart iteration after the credentials have been refreshed.
12152
12454
  remote.fetchCredentials().then((_) => {
12153
- controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12455
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_TOKEN_REFRESHED });
12154
12456
  }, (err) => {
12155
12457
  syncImplementation.logger.warn('Could not prefetch credentials', err);
12156
12458
  });
12157
12459
  }
12158
12460
  }
12159
12461
  else if ('CloseSyncStream' in instruction) {
12160
- abortController.abort();
12462
+ controller.abort();
12161
12463
  hideDisconnectOnRestart = instruction.CloseSyncStream.hide_disconnect;
12162
12464
  }
12163
12465
  else if ('FlushFileSystem' in instruction) ;
@@ -12186,17 +12488,13 @@ The next upload iteration will be delayed.`);
12186
12488
  }
12187
12489
  await control(PowerSyncControlCommand.START, JSON.stringify(options));
12188
12490
  this.notifyCompletedUploads = () => {
12189
- if (controlInvocations && !controlInvocations?.closed) {
12190
- controlInvocations.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12191
- }
12491
+ controlInvocations?.inject({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
12192
12492
  };
12193
12493
  this.handleActiveStreamsChange = () => {
12194
- if (controlInvocations && !controlInvocations?.closed) {
12195
- controlInvocations.enqueueData({
12196
- command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12197
- payload: JSON.stringify(this.activeStreams)
12198
- });
12199
- }
12494
+ controlInvocations?.inject({
12495
+ command: PowerSyncControlCommand.UPDATE_SUBSCRIPTIONS,
12496
+ payload: JSON.stringify(this.activeStreams)
12497
+ });
12200
12498
  };
12201
12499
  await receivingLines;
12202
12500
  }
@@ -13312,6 +13610,10 @@ SELECT * FROM crud_entries;
13312
13610
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
13313
13611
  * and optionally return results.
13314
13612
  *
13613
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
13614
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
13615
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
13616
+ *
13315
13617
  * @param sql The SQL query to execute
13316
13618
  * @param parameters Optional array of parameters to bind to the query
13317
13619
  * @returns The query result as an object with structured key-value pairs
@@ -13408,7 +13710,7 @@ SELECT * FROM crud_entries;
13408
13710
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
13409
13711
  await this.waitForReady();
13410
13712
  return this.database.readTransaction(async (tx) => {
13411
- const res = await callback({ ...tx });
13713
+ const res = await callback(tx);
13412
13714
  await tx.rollback();
13413
13715
  return res;
13414
13716
  }, { timeoutMs: lockTimeout });
@@ -14185,10 +14487,8 @@ class Schema {
14185
14487
  * developer instead of automatically by PowerSync.
14186
14488
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
14187
14489
  * using client-side table and column constraints.
14188
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
14189
14490
  *
14190
14491
  * @param tables An object of (table name, raw table definition) entries.
14191
- * @experimental Note that the raw tables API is still experimental and may change in the future.
14192
14492
  */
14193
14493
  withRawTables(tables) {
14194
14494
  for (const [name, rawTableDefinition] of Object.entries(tables)) {
@@ -14385,5 +14685,5 @@ const parseQuery = (query, parameters) => {
14385
14685
  return { sqlStatement, parameters: parameters };
14386
14686
  };
14387
14687
 
14388
- export { ATTACHMENT_TABLE, AbortOperation, AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, AbstractQueryProcessor, AbstractRemote, AbstractStreamingSyncImplementation, ArrayComparator, AttachmentContext, AttachmentQueue, AttachmentService, AttachmentState, AttachmentTable, BaseObserver, Column, ColumnType, ConnectionClosedError, ConnectionManager, ControlledExecutor, CrudBatch, CrudEntry, CrudTransaction, DEFAULT_CRUD_BATCH_LIMIT, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_INDEX_COLUMN_OPTIONS, DEFAULT_INDEX_OPTIONS, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_POWERSYNC_CLOSE_OPTIONS, DEFAULT_POWERSYNC_DB_OPTIONS, DEFAULT_PRESSURE_LIMITS, DEFAULT_REMOTE_LOGGER, DEFAULT_REMOTE_OPTIONS, DEFAULT_RETRY_DELAY_MS, DEFAULT_ROW_COMPARATOR, DEFAULT_STREAMING_SYNC_OPTIONS, DEFAULT_STREAM_CONNECTION_OPTIONS, DEFAULT_SYNC_CLIENT_IMPLEMENTATION, DEFAULT_TABLE_OPTIONS, DEFAULT_WATCH_QUERY_OPTIONS, DEFAULT_WATCH_THROTTLE_MS, DataStream, DiffTriggerOperation, DifferentialQueryProcessor, EMPTY_DIFFERENTIAL, EncodingType, FalsyComparator, FetchImplementationProvider, FetchStrategy, GetAllQuery, Index, IndexedColumn, InvalidSQLCharacters, LockType, LogLevel, MAX_AMOUNT_OF_COLUMNS, MAX_OP_ID, MEMORY_TRIGGER_CLAIM_MANAGER, OnChangeQueryProcessor, OpType, OpTypeEnum, OplogEntry, PSInternalTable, PowerSyncControlCommand, RowUpdateType, Schema, SqliteBucketStorage, SyncClientImplementation, SyncDataBatch, SyncDataBucket, SyncProgress, SyncStatus, SyncStreamConnectionMethod, SyncingService, Table, TableV2, TriggerManagerImpl, UpdateType, UploadQueueStats, WatchedQueryListenerEvent, attachmentFromSql, column, compilableQueryWatch, createBaseLogger, createLogger, extractTableUpdates, isBatchedUpdateNotification, isContinueCheckpointRequest, isDBAdapter, isPowerSyncDatabaseOptionsWithSettings, isSQLOpenFactory, isSQLOpenOptions, isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData, isSyncNewCheckpointRequest, mutexRunExclusive, parseQuery, runOnSchemaChange, sanitizeSQL, sanitizeUUID };
14688
+ export { ATTACHMENT_TABLE, AbortOperation, AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, AbstractQueryProcessor, AbstractRemote, AbstractStreamingSyncImplementation, ArrayComparator, AttachmentContext, AttachmentQueue, AttachmentService, AttachmentState, AttachmentTable, BaseObserver, Column, ColumnType, ConnectionClosedError, ConnectionManager, ControlledExecutor, CrudBatch, CrudEntry, CrudTransaction, DBAdapterDefaultMixin, DBGetUtilsDefaultMixin, DEFAULT_CRUD_BATCH_LIMIT, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_INDEX_COLUMN_OPTIONS, DEFAULT_INDEX_OPTIONS, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_POWERSYNC_CLOSE_OPTIONS, DEFAULT_POWERSYNC_DB_OPTIONS, DEFAULT_REMOTE_LOGGER, DEFAULT_REMOTE_OPTIONS, DEFAULT_RETRY_DELAY_MS, DEFAULT_ROW_COMPARATOR, DEFAULT_STREAMING_SYNC_OPTIONS, DEFAULT_STREAM_CONNECTION_OPTIONS, DEFAULT_SYNC_CLIENT_IMPLEMENTATION, DEFAULT_TABLE_OPTIONS, DEFAULT_WATCH_QUERY_OPTIONS, DEFAULT_WATCH_THROTTLE_MS, DiffTriggerOperation, DifferentialQueryProcessor, EMPTY_DIFFERENTIAL, EncodingType, FalsyComparator, FetchImplementationProvider, FetchStrategy, GetAllQuery, Index, IndexedColumn, InvalidSQLCharacters, LockType, LogLevel, MAX_AMOUNT_OF_COLUMNS, MAX_OP_ID, MEMORY_TRIGGER_CLAIM_MANAGER, Mutex, OnChangeQueryProcessor, OpType, OpTypeEnum, OplogEntry, PSInternalTable, PowerSyncControlCommand, RowUpdateType, Schema, Semaphore, SqliteBucketStorage, SyncClientImplementation, SyncDataBatch, SyncDataBucket, SyncProgress, SyncStatus, SyncStreamConnectionMethod, SyncingService, Table, TableV2, TriggerManagerImpl, UpdateType, UploadQueueStats, WatchedQueryListenerEvent, attachmentFromSql, column, compilableQueryWatch, createBaseLogger, createLogger, extractTableUpdates, isBatchedUpdateNotification, isContinueCheckpointRequest, isDBAdapter, isPowerSyncDatabaseOptionsWithSettings, isSQLOpenFactory, isSQLOpenOptions, isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData, isSyncNewCheckpointRequest, parseQuery, runOnSchemaChange, sanitizeSQL, sanitizeUUID, timeoutSignal };
14389
14689
  //# sourceMappingURL=bundle.mjs.map