@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
@@ -1,4 +1,3 @@
1
- import { Mutex } from 'async-mutex';
2
1
  import { EventIterator } from 'event-iterator';
3
2
  import { Buffer } from 'node:buffer';
4
3
 
@@ -659,7 +658,7 @@ class SyncingService {
659
658
  updatedAttachments.push(downloaded);
660
659
  break;
661
660
  case AttachmentState.QUEUED_DELETE:
662
- const deleted = await this.deleteAttachment(attachment);
661
+ const deleted = await this.deleteAttachment(attachment, context);
663
662
  updatedAttachments.push(deleted);
664
663
  break;
665
664
  }
@@ -737,17 +736,16 @@ class SyncingService {
737
736
  * On failure, defers to error handler or archives.
738
737
  *
739
738
  * @param attachment - The attachment record to delete
739
+ * @param context - Attachment context for database operations
740
740
  * @returns Updated attachment record
741
741
  */
742
- async deleteAttachment(attachment) {
742
+ async deleteAttachment(attachment, context) {
743
743
  try {
744
744
  await this.remoteStorage.deleteFile(attachment);
745
745
  if (attachment.localUri) {
746
746
  await this.localStorage.deleteFile(attachment.localUri);
747
747
  }
748
- await this.attachmentService.withContext(async (ctx) => {
749
- await ctx.deleteAttachment(attachment.id);
750
- });
748
+ await context.deleteAttachment(attachment.id);
751
749
  return {
752
750
  ...attachment,
753
751
  state: AttachmentState.ARCHIVED
@@ -785,32 +783,198 @@ class SyncingService {
785
783
  }
786
784
 
787
785
  /**
788
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
786
+ * A simple fixed-capacity queue implementation.
787
+ *
788
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
789
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
789
790
  */
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());
791
+ class Queue {
792
+ table;
793
+ // Index of the first element in the table.
794
+ head;
795
+ // Amount of items currently in the queue.
796
+ _length;
797
+ constructor(initialItems) {
798
+ this.table = [...initialItems];
799
+ this.head = 0;
800
+ this._length = this.table.length;
801
+ }
802
+ get isEmpty() {
803
+ return this.length == 0;
804
+ }
805
+ get length() {
806
+ return this._length;
807
+ }
808
+ removeFirst() {
809
+ if (this.isEmpty) {
810
+ throw new Error('Queue is empty');
811
+ }
812
+ const result = this.table[this.head];
813
+ this._length--;
814
+ this.table[this.head] = undefined;
815
+ this.head = (this.head + 1) % this.table.length;
816
+ return result;
817
+ }
818
+ addLast(element) {
819
+ if (this.length == this.table.length) {
820
+ throw new Error('Queue is full');
821
+ }
822
+ this.table[(this.head + this._length) % this.table.length] = element;
823
+ this._length++;
824
+ }
825
+ }
826
+
827
+ /**
828
+ * An asynchronous semaphore implementation with associated items per lease.
829
+ *
830
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
831
+ */
832
+ class Semaphore {
833
+ // Available items that are not currently assigned to a waiter.
834
+ available;
835
+ size;
836
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
837
+ // aborted waiters from the middle of the list efficiently.
838
+ firstWaiter;
839
+ lastWaiter;
840
+ constructor(elements) {
841
+ this.available = new Queue(elements);
842
+ this.size = this.available.length;
843
+ }
844
+ addWaiter(requestedItems, onAcquire) {
845
+ const node = {
846
+ isActive: true,
847
+ acquiredItems: [],
848
+ remainingItems: requestedItems,
849
+ onAcquire,
850
+ prev: this.lastWaiter
851
+ };
852
+ if (this.lastWaiter) {
853
+ this.lastWaiter.next = node;
854
+ this.lastWaiter = node;
855
+ }
856
+ else {
857
+ // First waiter
858
+ this.lastWaiter = this.firstWaiter = node;
859
+ }
860
+ return node;
861
+ }
862
+ deactivateWaiter(waiter) {
863
+ const { prev, next } = waiter;
864
+ waiter.isActive = false;
865
+ if (prev)
866
+ prev.next = next;
867
+ if (next)
868
+ next.prev = prev;
869
+ if (waiter == this.firstWaiter)
870
+ this.firstWaiter = next;
871
+ if (waiter == this.lastWaiter)
872
+ this.lastWaiter = prev;
873
+ }
874
+ requestPermits(amount, abort) {
875
+ if (amount <= 0 || amount > this.size) {
876
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
877
+ }
878
+ return new Promise((resolve, reject) => {
879
+ function rejectAborted() {
880
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
881
+ }
882
+ if (abort?.aborted) {
883
+ return rejectAborted();
884
+ }
885
+ let waiter;
886
+ const markCompleted = () => {
887
+ const items = waiter.acquiredItems;
888
+ waiter.acquiredItems = []; // Avoid releasing items twice.
889
+ for (const element of items) {
890
+ // Give to next waiter, if possible.
891
+ const nextWaiter = this.firstWaiter;
892
+ if (nextWaiter) {
893
+ nextWaiter.acquiredItems.push(element);
894
+ nextWaiter.remainingItems--;
895
+ if (nextWaiter.remainingItems == 0) {
896
+ nextWaiter.onAcquire();
897
+ }
898
+ }
899
+ else {
900
+ // No pending waiter, return lease into pool.
901
+ this.available.addLast(element);
902
+ }
903
+ }
904
+ };
905
+ const onAbort = () => {
906
+ abort?.removeEventListener('abort', onAbort);
907
+ if (waiter.isActive) {
908
+ this.deactivateWaiter(waiter);
909
+ rejectAborted();
910
+ }
911
+ };
912
+ const resolvePromise = () => {
913
+ this.deactivateWaiter(waiter);
914
+ abort?.removeEventListener('abort', onAbort);
915
+ const items = waiter.acquiredItems;
916
+ resolve({ items, release: markCompleted });
917
+ };
918
+ waiter = this.addWaiter(amount, resolvePromise);
919
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
920
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
921
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
922
+ waiter.acquiredItems.push(this.available.removeFirst());
923
+ waiter.remainingItems--;
808
924
  }
809
- catch (ex) {
810
- reject(ex);
925
+ if (waiter.remainingItems == 0) {
926
+ return resolvePromise();
811
927
  }
928
+ abort?.addEventListener('abort', onAbort);
812
929
  });
813
- });
930
+ }
931
+ /**
932
+ * Requests a single item from the pool.
933
+ *
934
+ * The returned `release` callback must be invoked to return the item into the pool.
935
+ */
936
+ async requestOne(abort) {
937
+ const { items, release } = await this.requestPermits(1, abort);
938
+ return { release, item: items[0] };
939
+ }
940
+ /**
941
+ * Requests access to all items from the pool.
942
+ *
943
+ * The returned `release` callback must be invoked to return items into the pool.
944
+ */
945
+ requestAll(abort) {
946
+ return this.requestPermits(this.size, abort);
947
+ }
948
+ }
949
+ /**
950
+ * An asynchronous mutex implementation.
951
+ *
952
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
953
+ */
954
+ class Mutex {
955
+ inner = new Semaphore([null]);
956
+ async acquire(abort) {
957
+ const { release } = await this.inner.requestOne(abort);
958
+ return release;
959
+ }
960
+ async runExclusive(fn, abort) {
961
+ const returnMutex = await this.acquire(abort);
962
+ try {
963
+ return await fn();
964
+ }
965
+ finally {
966
+ returnMutex();
967
+ }
968
+ }
969
+ }
970
+ function timeoutSignal(timeout) {
971
+ if (timeout == null)
972
+ return;
973
+ if ('timeout' in AbortSignal)
974
+ return AbortSignal.timeout(timeout);
975
+ const controller = new AbortController();
976
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
977
+ return controller.signal;
814
978
  }
815
979
 
816
980
  /**
@@ -859,7 +1023,7 @@ class AttachmentService {
859
1023
  * Executes a callback with exclusive access to the attachment context.
860
1024
  */
861
1025
  async withContext(callback) {
862
- return mutexRunExclusive(this.mutex, async () => {
1026
+ return this.mutex.runExclusive(async () => {
863
1027
  return callback(this.context);
864
1028
  });
865
1029
  }
@@ -895,9 +1059,15 @@ class AttachmentQueue {
895
1059
  tableName;
896
1060
  /** Logger instance for diagnostic information */
897
1061
  logger;
898
- /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
1062
+ /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
1063
+ * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
899
1064
  syncIntervalMs = 30 * 1000;
900
- /** Duration in milliseconds to throttle sync operations */
1065
+ /** Throttle duration in milliseconds for the reactive watch query on the attachments table.
1066
+ * When attachment records change, a watch query detects the change and triggers a sync.
1067
+ * This throttle prevents the sync from firing too rapidly when many changes happen in
1068
+ * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
1069
+ * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
1070
+ * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
901
1071
  syncThrottleDuration;
902
1072
  /** Whether to automatically download remote attachments. Default: true */
903
1073
  downloadAttachments = true;
@@ -921,8 +1091,8 @@ class AttachmentQueue {
921
1091
  * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
922
1092
  * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
923
1093
  * @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
1094
+ * @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
1095
+ * @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
1096
  * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
927
1097
  * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
928
1098
  */
@@ -8059,7 +8229,7 @@ function requireDist () {
8059
8229
 
8060
8230
  var distExports = requireDist();
8061
8231
 
8062
- var version = "1.49.0";
8232
+ var version = "1.51.0";
8063
8233
  var PACKAGE = {
8064
8234
  version: version};
8065
8235
 
@@ -10916,6 +11086,10 @@ SELECT * FROM crud_entries;
10916
11086
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
10917
11087
  * and optionally return results.
10918
11088
  *
11089
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
11090
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
11091
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
11092
+ *
10919
11093
  * @param sql The SQL query to execute
10920
11094
  * @param parameters Optional array of parameters to bind to the query
10921
11095
  * @returns The query result as an object with structured key-value pairs
@@ -11012,7 +11186,7 @@ SELECT * FROM crud_entries;
11012
11186
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
11013
11187
  await this.waitForReady();
11014
11188
  return this.database.readTransaction(async (tx) => {
11015
- const res = await callback({ ...tx });
11189
+ const res = await callback(tx);
11016
11190
  await tx.rollback();
11017
11191
  return res;
11018
11192
  }, { timeoutMs: lockTimeout });
@@ -11989,5 +12163,5 @@ const parseQuery = (query, parameters) => {
11989
12163
  return { sqlStatement, parameters: parameters };
11990
12164
  };
11991
12165
 
11992
- 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 };
12166
+ 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 };
11993
12167
  //# sourceMappingURL=bundle.node.mjs.map