@powersync/common 1.49.0 → 1.50.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.
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,108 @@ class SyncingService {
785
782
  }
786
783
 
787
784
  /**
788
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
785
+ * An asynchronous mutex implementation.
786
+ *
787
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
789
788
  */
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());
789
+ class Mutex {
790
+ inCriticalSection = false;
791
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
792
+ // aborted waiters from the middle of the list efficiently.
793
+ firstWaiter;
794
+ lastWaiter;
795
+ addWaiter(onAcquire) {
796
+ const node = {
797
+ isActive: true,
798
+ onAcquire,
799
+ prev: this.lastWaiter
800
+ };
801
+ if (this.lastWaiter) {
802
+ this.lastWaiter.next = node;
803
+ this.lastWaiter = node;
804
+ }
805
+ else {
806
+ // First waiter
807
+ this.lastWaiter = this.firstWaiter = node;
808
+ }
809
+ return node;
810
+ }
811
+ deactivateWaiter(waiter) {
812
+ const { prev, next } = waiter;
813
+ waiter.isActive = false;
814
+ if (prev)
815
+ prev.next = next;
816
+ if (next)
817
+ next.prev = prev;
818
+ if (waiter == this.firstWaiter)
819
+ this.firstWaiter = next;
820
+ if (waiter == this.lastWaiter)
821
+ this.lastWaiter = prev;
822
+ }
823
+ acquire(abort) {
824
+ return new Promise((resolve, reject) => {
825
+ function rejectAborted() {
826
+ reject(abort?.reason ?? new Error('Mutex acquire aborted'));
808
827
  }
809
- catch (ex) {
810
- reject(ex);
828
+ if (abort?.aborted) {
829
+ return rejectAborted();
830
+ }
831
+ let holdsMutex = false;
832
+ const markCompleted = () => {
833
+ if (!holdsMutex)
834
+ return;
835
+ holdsMutex = false;
836
+ const waiter = this.firstWaiter;
837
+ if (waiter) {
838
+ this.deactivateWaiter(waiter);
839
+ // Still in critical section, but owned by next waiter now.
840
+ waiter.onAcquire();
841
+ }
842
+ else {
843
+ this.inCriticalSection = false;
844
+ }
845
+ };
846
+ if (!this.inCriticalSection) {
847
+ this.inCriticalSection = true;
848
+ holdsMutex = true;
849
+ return resolve(markCompleted);
850
+ }
851
+ else {
852
+ let node;
853
+ const onAbort = () => {
854
+ abort?.removeEventListener('abort', onAbort);
855
+ if (node.isActive) {
856
+ this.deactivateWaiter(node);
857
+ rejectAborted();
858
+ }
859
+ };
860
+ node = this.addWaiter(() => {
861
+ abort?.removeEventListener('abort', onAbort);
862
+ holdsMutex = true;
863
+ resolve(markCompleted);
864
+ });
865
+ abort?.addEventListener('abort', onAbort);
811
866
  }
812
867
  });
813
- });
868
+ }
869
+ async runExclusive(fn, abort) {
870
+ const returnMutex = await this.acquire(abort);
871
+ try {
872
+ return await fn();
873
+ }
874
+ finally {
875
+ returnMutex();
876
+ }
877
+ }
878
+ }
879
+ function timeoutSignal(timeout) {
880
+ if (timeout == null)
881
+ return;
882
+ if ('timeout' in AbortSignal)
883
+ return AbortSignal.timeout(timeout);
884
+ const controller = new AbortController();
885
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
886
+ return controller.signal;
814
887
  }
815
888
 
816
889
  /**
@@ -822,7 +895,7 @@ class AttachmentService {
822
895
  db;
823
896
  logger;
824
897
  tableName;
825
- mutex = new asyncMutex.Mutex();
898
+ mutex = new Mutex();
826
899
  context;
827
900
  constructor(db, logger, tableName = 'attachments', archivedCacheLimit = 100) {
828
901
  this.db = db;
@@ -859,7 +932,7 @@ class AttachmentService {
859
932
  * Executes a callback with exclusive access to the attachment context.
860
933
  */
861
934
  async withContext(callback) {
862
- return mutexRunExclusive(this.mutex, async () => {
935
+ return this.mutex.runExclusive(async () => {
863
936
  return callback(this.context);
864
937
  });
865
938
  }
@@ -895,9 +968,15 @@ class AttachmentQueue {
895
968
  tableName;
896
969
  /** Logger instance for diagnostic information */
897
970
  logger;
898
- /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
971
+ /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
972
+ * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
899
973
  syncIntervalMs = 30 * 1000;
900
- /** Duration in milliseconds to throttle sync operations */
974
+ /** Throttle duration in milliseconds for the reactive watch query on the attachments table.
975
+ * When attachment records change, a watch query detects the change and triggers a sync.
976
+ * This throttle prevents the sync from firing too rapidly when many changes happen in
977
+ * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
978
+ * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
979
+ * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
901
980
  syncThrottleDuration;
902
981
  /** Whether to automatically download remote attachments. Default: true */
903
982
  downloadAttachments = true;
@@ -921,8 +1000,8 @@ class AttachmentQueue {
921
1000
  * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
922
1001
  * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
923
1002
  * @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
1003
+ * @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
1004
+ * @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
1005
  * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
927
1006
  * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
928
1007
  */
@@ -10583,7 +10662,7 @@ function requireDist () {
10583
10662
 
10584
10663
  var distExports = requireDist();
10585
10664
 
10586
- var version = "1.49.0";
10665
+ var version = "1.50.0";
10587
10666
  var PACKAGE = {
10588
10667
  version: version};
10589
10668
 
@@ -12975,7 +13054,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
12975
13054
  this._schema = schema;
12976
13055
  this.ready = false;
12977
13056
  this.sdkVersion = '';
12978
- this.runExclusiveMutex = new asyncMutex.Mutex();
13057
+ this.runExclusiveMutex = new Mutex();
12979
13058
  // Start async init
12980
13059
  this.subscriptions = {
12981
13060
  firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
@@ -13440,6 +13519,10 @@ SELECT * FROM crud_entries;
13440
13519
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
13441
13520
  * and optionally return results.
13442
13521
  *
13522
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
13523
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
13524
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
13525
+ *
13443
13526
  * @param sql The SQL query to execute
13444
13527
  * @param parameters Optional array of parameters to bind to the query
13445
13528
  * @returns The query result as an object with structured key-value pairs
@@ -13536,7 +13619,7 @@ SELECT * FROM crud_entries;
13536
13619
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
13537
13620
  await this.waitForReady();
13538
13621
  return this.database.readTransaction(async (tx) => {
13539
- const res = await callback({ ...tx });
13622
+ const res = await callback(tx);
13540
13623
  await tx.rollback();
13541
13624
  return res;
13542
13625
  }, { timeoutMs: lockTimeout });
@@ -14566,6 +14649,7 @@ exports.LogLevel = LogLevel;
14566
14649
  exports.MAX_AMOUNT_OF_COLUMNS = MAX_AMOUNT_OF_COLUMNS;
14567
14650
  exports.MAX_OP_ID = MAX_OP_ID;
14568
14651
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
14652
+ exports.Mutex = Mutex;
14569
14653
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
14570
14654
  exports.OpType = OpType;
14571
14655
  exports.OplogEntry = OplogEntry;
@@ -14599,9 +14683,9 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
14599
14683
  exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
14600
14684
  exports.isStreamingSyncData = isStreamingSyncData;
14601
14685
  exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
14602
- exports.mutexRunExclusive = mutexRunExclusive;
14603
14686
  exports.parseQuery = parseQuery;
14604
14687
  exports.runOnSchemaChange = runOnSchemaChange;
14605
14688
  exports.sanitizeSQL = sanitizeSQL;
14606
14689
  exports.sanitizeUUID = sanitizeUUID;
14690
+ exports.timeoutSignal = timeoutSignal;
14607
14691
  //# sourceMappingURL=bundle.cjs.map