@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.cjs CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var asyncMutex = require('async-mutex');
4
-
5
3
  // https://www.sqlite.org/lang_expr.html#castexpr
6
4
  exports.ColumnType = void 0;
7
5
  (function (ColumnType) {
@@ -659,7 +657,7 @@ class SyncingService {
659
657
  updatedAttachments.push(downloaded);
660
658
  break;
661
659
  case exports.AttachmentState.QUEUED_DELETE:
662
- const deleted = await this.deleteAttachment(attachment);
660
+ const deleted = await this.deleteAttachment(attachment, context);
663
661
  updatedAttachments.push(deleted);
664
662
  break;
665
663
  }
@@ -737,17 +735,16 @@ class SyncingService {
737
735
  * On failure, defers to error handler or archives.
738
736
  *
739
737
  * @param attachment - The attachment record to delete
738
+ * @param context - Attachment context for database operations
740
739
  * @returns Updated attachment record
741
740
  */
742
- async deleteAttachment(attachment) {
741
+ async deleteAttachment(attachment, context) {
743
742
  try {
744
743
  await this.remoteStorage.deleteFile(attachment);
745
744
  if (attachment.localUri) {
746
745
  await this.localStorage.deleteFile(attachment.localUri);
747
746
  }
748
- await this.attachmentService.withContext(async (ctx) => {
749
- await ctx.deleteAttachment(attachment.id);
750
- });
747
+ await context.deleteAttachment(attachment.id);
751
748
  return {
752
749
  ...attachment,
753
750
  state: exports.AttachmentState.ARCHIVED
@@ -785,32 +782,198 @@ class SyncingService {
785
782
  }
786
783
 
787
784
  /**
788
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
785
+ * A simple fixed-capacity queue implementation.
786
+ *
787
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
788
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
789
789
  */
790
- async function mutexRunExclusive(mutex, callback, options) {
791
- return new Promise((resolve, reject) => {
792
- const timeout = options?.timeoutMs;
793
- let timedOut = false;
794
- const timeoutId = timeout
795
- ? setTimeout(() => {
796
- timedOut = true;
797
- reject(new Error('Timeout waiting for lock'));
798
- }, timeout)
799
- : undefined;
800
- mutex.runExclusive(async () => {
801
- if (timeoutId) {
802
- clearTimeout(timeoutId);
803
- }
804
- if (timedOut)
805
- return;
806
- try {
807
- resolve(await callback());
790
+ class Queue {
791
+ table;
792
+ // Index of the first element in the table.
793
+ head;
794
+ // Amount of items currently in the queue.
795
+ _length;
796
+ constructor(initialItems) {
797
+ this.table = [...initialItems];
798
+ this.head = 0;
799
+ this._length = this.table.length;
800
+ }
801
+ get isEmpty() {
802
+ return this.length == 0;
803
+ }
804
+ get length() {
805
+ return this._length;
806
+ }
807
+ removeFirst() {
808
+ if (this.isEmpty) {
809
+ throw new Error('Queue is empty');
810
+ }
811
+ const result = this.table[this.head];
812
+ this._length--;
813
+ this.table[this.head] = undefined;
814
+ this.head = (this.head + 1) % this.table.length;
815
+ return result;
816
+ }
817
+ addLast(element) {
818
+ if (this.length == this.table.length) {
819
+ throw new Error('Queue is full');
820
+ }
821
+ this.table[(this.head + this._length) % this.table.length] = element;
822
+ this._length++;
823
+ }
824
+ }
825
+
826
+ /**
827
+ * An asynchronous semaphore implementation with associated items per lease.
828
+ *
829
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
830
+ */
831
+ class Semaphore {
832
+ // Available items that are not currently assigned to a waiter.
833
+ available;
834
+ size;
835
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
836
+ // aborted waiters from the middle of the list efficiently.
837
+ firstWaiter;
838
+ lastWaiter;
839
+ constructor(elements) {
840
+ this.available = new Queue(elements);
841
+ this.size = this.available.length;
842
+ }
843
+ addWaiter(requestedItems, onAcquire) {
844
+ const node = {
845
+ isActive: true,
846
+ acquiredItems: [],
847
+ remainingItems: requestedItems,
848
+ onAcquire,
849
+ prev: this.lastWaiter
850
+ };
851
+ if (this.lastWaiter) {
852
+ this.lastWaiter.next = node;
853
+ this.lastWaiter = node;
854
+ }
855
+ else {
856
+ // First waiter
857
+ this.lastWaiter = this.firstWaiter = node;
858
+ }
859
+ return node;
860
+ }
861
+ deactivateWaiter(waiter) {
862
+ const { prev, next } = waiter;
863
+ waiter.isActive = false;
864
+ if (prev)
865
+ prev.next = next;
866
+ if (next)
867
+ next.prev = prev;
868
+ if (waiter == this.firstWaiter)
869
+ this.firstWaiter = next;
870
+ if (waiter == this.lastWaiter)
871
+ this.lastWaiter = prev;
872
+ }
873
+ requestPermits(amount, abort) {
874
+ if (amount <= 0 || amount > this.size) {
875
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
876
+ }
877
+ return new Promise((resolve, reject) => {
878
+ function rejectAborted() {
879
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
880
+ }
881
+ if (abort?.aborted) {
882
+ return rejectAborted();
883
+ }
884
+ let waiter;
885
+ const markCompleted = () => {
886
+ const items = waiter.acquiredItems;
887
+ waiter.acquiredItems = []; // Avoid releasing items twice.
888
+ for (const element of items) {
889
+ // Give to next waiter, if possible.
890
+ const nextWaiter = this.firstWaiter;
891
+ if (nextWaiter) {
892
+ nextWaiter.acquiredItems.push(element);
893
+ nextWaiter.remainingItems--;
894
+ if (nextWaiter.remainingItems == 0) {
895
+ nextWaiter.onAcquire();
896
+ }
897
+ }
898
+ else {
899
+ // No pending waiter, return lease into pool.
900
+ this.available.addLast(element);
901
+ }
902
+ }
903
+ };
904
+ const onAbort = () => {
905
+ abort?.removeEventListener('abort', onAbort);
906
+ if (waiter.isActive) {
907
+ this.deactivateWaiter(waiter);
908
+ rejectAborted();
909
+ }
910
+ };
911
+ const resolvePromise = () => {
912
+ this.deactivateWaiter(waiter);
913
+ abort?.removeEventListener('abort', onAbort);
914
+ const items = waiter.acquiredItems;
915
+ resolve({ items, release: markCompleted });
916
+ };
917
+ waiter = this.addWaiter(amount, resolvePromise);
918
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
919
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
920
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
921
+ waiter.acquiredItems.push(this.available.removeFirst());
922
+ waiter.remainingItems--;
808
923
  }
809
- catch (ex) {
810
- reject(ex);
924
+ if (waiter.remainingItems == 0) {
925
+ return resolvePromise();
811
926
  }
927
+ abort?.addEventListener('abort', onAbort);
812
928
  });
813
- });
929
+ }
930
+ /**
931
+ * Requests a single item from the pool.
932
+ *
933
+ * The returned `release` callback must be invoked to return the item into the pool.
934
+ */
935
+ async requestOne(abort) {
936
+ const { items, release } = await this.requestPermits(1, abort);
937
+ return { release, item: items[0] };
938
+ }
939
+ /**
940
+ * Requests access to all items from the pool.
941
+ *
942
+ * The returned `release` callback must be invoked to return items into the pool.
943
+ */
944
+ requestAll(abort) {
945
+ return this.requestPermits(this.size, abort);
946
+ }
947
+ }
948
+ /**
949
+ * An asynchronous mutex implementation.
950
+ *
951
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
952
+ */
953
+ class Mutex {
954
+ inner = new Semaphore([null]);
955
+ async acquire(abort) {
956
+ const { release } = await this.inner.requestOne(abort);
957
+ return release;
958
+ }
959
+ async runExclusive(fn, abort) {
960
+ const returnMutex = await this.acquire(abort);
961
+ try {
962
+ return await fn();
963
+ }
964
+ finally {
965
+ returnMutex();
966
+ }
967
+ }
968
+ }
969
+ function timeoutSignal(timeout) {
970
+ if (timeout == null)
971
+ return;
972
+ if ('timeout' in AbortSignal)
973
+ return AbortSignal.timeout(timeout);
974
+ const controller = new AbortController();
975
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
976
+ return controller.signal;
814
977
  }
815
978
 
816
979
  /**
@@ -822,7 +985,7 @@ class AttachmentService {
822
985
  db;
823
986
  logger;
824
987
  tableName;
825
- mutex = new asyncMutex.Mutex();
988
+ mutex = new Mutex();
826
989
  context;
827
990
  constructor(db, logger, tableName = 'attachments', archivedCacheLimit = 100) {
828
991
  this.db = db;
@@ -859,7 +1022,7 @@ class AttachmentService {
859
1022
  * Executes a callback with exclusive access to the attachment context.
860
1023
  */
861
1024
  async withContext(callback) {
862
- return mutexRunExclusive(this.mutex, async () => {
1025
+ return this.mutex.runExclusive(async () => {
863
1026
  return callback(this.context);
864
1027
  });
865
1028
  }
@@ -895,9 +1058,15 @@ class AttachmentQueue {
895
1058
  tableName;
896
1059
  /** Logger instance for diagnostic information */
897
1060
  logger;
898
- /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
1061
+ /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
1062
+ * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
899
1063
  syncIntervalMs = 30 * 1000;
900
- /** Duration in milliseconds to throttle sync operations */
1064
+ /** Throttle duration in milliseconds for the reactive watch query on the attachments table.
1065
+ * When attachment records change, a watch query detects the change and triggers a sync.
1066
+ * This throttle prevents the sync from firing too rapidly when many changes happen in
1067
+ * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
1068
+ * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
1069
+ * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
901
1070
  syncThrottleDuration;
902
1071
  /** Whether to automatically download remote attachments. Default: true */
903
1072
  downloadAttachments = true;
@@ -921,8 +1090,8 @@ class AttachmentQueue {
921
1090
  * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
922
1091
  * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
923
1092
  * @param options.logger - Logger instance. Defaults to db.logger
924
- * @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000
925
- * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
1093
+ * @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
1094
+ * @param options.syncThrottleDuration - Throttle duration in milliseconds for the reactive watch query that detects attachment changes. Prevents rapid-fire syncs during bulk changes. Default: 30
926
1095
  * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
927
1096
  * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
928
1097
  */
@@ -10583,7 +10752,7 @@ function requireDist () {
10583
10752
 
10584
10753
  var distExports = requireDist();
10585
10754
 
10586
- var version = "1.49.0";
10755
+ var version = "1.51.0";
10587
10756
  var PACKAGE = {
10588
10757
  version: version};
10589
10758
 
@@ -12975,7 +13144,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
12975
13144
  this._schema = schema;
12976
13145
  this.ready = false;
12977
13146
  this.sdkVersion = '';
12978
- this.runExclusiveMutex = new asyncMutex.Mutex();
13147
+ this.runExclusiveMutex = new Mutex();
12979
13148
  // Start async init
12980
13149
  this.subscriptions = {
12981
13150
  firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
@@ -13440,6 +13609,10 @@ SELECT * FROM crud_entries;
13440
13609
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
13441
13610
  * and optionally return results.
13442
13611
  *
13612
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
13613
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
13614
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
13615
+ *
13443
13616
  * @param sql The SQL query to execute
13444
13617
  * @param parameters Optional array of parameters to bind to the query
13445
13618
  * @returns The query result as an object with structured key-value pairs
@@ -13536,7 +13709,7 @@ SELECT * FROM crud_entries;
13536
13709
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
13537
13710
  await this.waitForReady();
13538
13711
  return this.database.readTransaction(async (tx) => {
13539
- const res = await callback({ ...tx });
13712
+ const res = await callback(tx);
13540
13713
  await tx.rollback();
13541
13714
  return res;
13542
13715
  }, { timeoutMs: lockTimeout });
@@ -14566,10 +14739,12 @@ exports.LogLevel = LogLevel;
14566
14739
  exports.MAX_AMOUNT_OF_COLUMNS = MAX_AMOUNT_OF_COLUMNS;
14567
14740
  exports.MAX_OP_ID = MAX_OP_ID;
14568
14741
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
14742
+ exports.Mutex = Mutex;
14569
14743
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
14570
14744
  exports.OpType = OpType;
14571
14745
  exports.OplogEntry = OplogEntry;
14572
14746
  exports.Schema = Schema;
14747
+ exports.Semaphore = Semaphore;
14573
14748
  exports.SqliteBucketStorage = SqliteBucketStorage;
14574
14749
  exports.SyncDataBatch = SyncDataBatch;
14575
14750
  exports.SyncDataBucket = SyncDataBucket;
@@ -14599,9 +14774,9 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
14599
14774
  exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
14600
14775
  exports.isStreamingSyncData = isStreamingSyncData;
14601
14776
  exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
14602
- exports.mutexRunExclusive = mutexRunExclusive;
14603
14777
  exports.parseQuery = parseQuery;
14604
14778
  exports.runOnSchemaChange = runOnSchemaChange;
14605
14779
  exports.sanitizeSQL = sanitizeSQL;
14606
14780
  exports.sanitizeUUID = sanitizeUUID;
14781
+ exports.timeoutSignal = timeoutSignal;
14607
14782
  //# sourceMappingURL=bundle.cjs.map