@powersync/common 1.49.0 → 1.51.0

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 (37) hide show
  1. package/dist/bundle.cjs +214 -39
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +210 -37
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +214 -38
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +210 -36
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +75 -16
  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 +6 -2
  20. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  21. package/lib/db/DBAdapter.d.ts +7 -1
  22. package/lib/db/DBAdapter.js.map +1 -1
  23. package/lib/utils/mutex.d.ts +47 -5
  24. package/lib/utils/mutex.js +146 -21
  25. package/lib/utils/mutex.js.map +1 -1
  26. package/lib/utils/queue.d.ts +16 -0
  27. package/lib/utils/queue.js +42 -0
  28. package/lib/utils/queue.js.map +1 -0
  29. package/package.json +1 -2
  30. package/src/attachments/AttachmentQueue.ts +10 -4
  31. package/src/attachments/AttachmentService.ts +2 -3
  32. package/src/attachments/README.md +6 -4
  33. package/src/attachments/SyncingService.ts +4 -5
  34. package/src/client/AbstractPowerSyncDatabase.ts +6 -2
  35. package/src/db/DBAdapter.ts +7 -1
  36. package/src/utils/mutex.ts +184 -26
  37. package/src/utils/queue.ts +48 -0
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
  */
@@ -10581,7 +10750,7 @@ function requireDist () {
10581
10750
 
10582
10751
  var distExports = requireDist();
10583
10752
 
10584
- var version = "1.49.0";
10753
+ var version = "1.51.0";
10585
10754
  var PACKAGE = {
10586
10755
  version: version};
10587
10756
 
@@ -13438,6 +13607,10 @@ SELECT * FROM crud_entries;
13438
13607
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
13439
13608
  * and optionally return results.
13440
13609
  *
13610
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
13611
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
13612
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
13613
+ *
13441
13614
  * @param sql The SQL query to execute
13442
13615
  * @param parameters Optional array of parameters to bind to the query
13443
13616
  * @returns The query result as an object with structured key-value pairs
@@ -13534,7 +13707,7 @@ SELECT * FROM crud_entries;
13534
13707
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
13535
13708
  await this.waitForReady();
13536
13709
  return this.database.readTransaction(async (tx) => {
13537
- const res = await callback({ ...tx });
13710
+ const res = await callback(tx);
13538
13711
  await tx.rollback();
13539
13712
  return res;
13540
13713
  }, { timeoutMs: lockTimeout });
@@ -14511,5 +14684,5 @@ const parseQuery = (query, parameters) => {
14511
14684
  return { sqlStatement, parameters: parameters };
14512
14685
  };
14513
14686
 
14514
- 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_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 };
14687
+ 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_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, 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 };
14515
14688
  //# sourceMappingURL=bundle.mjs.map