@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.
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var asyncMutex = require('async-mutex');
4
3
  var eventIterator = require('event-iterator');
5
4
  var node_buffer = require('node:buffer');
6
5
 
@@ -661,7 +660,7 @@ class SyncingService {
661
660
  updatedAttachments.push(downloaded);
662
661
  break;
663
662
  case exports.AttachmentState.QUEUED_DELETE:
664
- const deleted = await this.deleteAttachment(attachment);
663
+ const deleted = await this.deleteAttachment(attachment, context);
665
664
  updatedAttachments.push(deleted);
666
665
  break;
667
666
  }
@@ -739,17 +738,16 @@ class SyncingService {
739
738
  * On failure, defers to error handler or archives.
740
739
  *
741
740
  * @param attachment - The attachment record to delete
741
+ * @param context - Attachment context for database operations
742
742
  * @returns Updated attachment record
743
743
  */
744
- async deleteAttachment(attachment) {
744
+ async deleteAttachment(attachment, context) {
745
745
  try {
746
746
  await this.remoteStorage.deleteFile(attachment);
747
747
  if (attachment.localUri) {
748
748
  await this.localStorage.deleteFile(attachment.localUri);
749
749
  }
750
- await this.attachmentService.withContext(async (ctx) => {
751
- await ctx.deleteAttachment(attachment.id);
752
- });
750
+ await context.deleteAttachment(attachment.id);
753
751
  return {
754
752
  ...attachment,
755
753
  state: exports.AttachmentState.ARCHIVED
@@ -787,32 +785,108 @@ class SyncingService {
787
785
  }
788
786
 
789
787
  /**
790
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
788
+ * An asynchronous mutex implementation.
789
+ *
790
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
791
791
  */
792
- async function mutexRunExclusive(mutex, callback, options) {
793
- return new Promise((resolve, reject) => {
794
- const timeout = options?.timeoutMs;
795
- let timedOut = false;
796
- const timeoutId = timeout
797
- ? setTimeout(() => {
798
- timedOut = true;
799
- reject(new Error('Timeout waiting for lock'));
800
- }, timeout)
801
- : undefined;
802
- mutex.runExclusive(async () => {
803
- if (timeoutId) {
804
- clearTimeout(timeoutId);
805
- }
806
- if (timedOut)
807
- return;
808
- try {
809
- resolve(await callback());
792
+ class Mutex {
793
+ inCriticalSection = false;
794
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
795
+ // aborted waiters from the middle of the list efficiently.
796
+ firstWaiter;
797
+ lastWaiter;
798
+ addWaiter(onAcquire) {
799
+ const node = {
800
+ isActive: true,
801
+ onAcquire,
802
+ prev: this.lastWaiter
803
+ };
804
+ if (this.lastWaiter) {
805
+ this.lastWaiter.next = node;
806
+ this.lastWaiter = node;
807
+ }
808
+ else {
809
+ // First waiter
810
+ this.lastWaiter = this.firstWaiter = node;
811
+ }
812
+ return node;
813
+ }
814
+ deactivateWaiter(waiter) {
815
+ const { prev, next } = waiter;
816
+ waiter.isActive = false;
817
+ if (prev)
818
+ prev.next = next;
819
+ if (next)
820
+ next.prev = prev;
821
+ if (waiter == this.firstWaiter)
822
+ this.firstWaiter = next;
823
+ if (waiter == this.lastWaiter)
824
+ this.lastWaiter = prev;
825
+ }
826
+ acquire(abort) {
827
+ return new Promise((resolve, reject) => {
828
+ function rejectAborted() {
829
+ reject(abort?.reason ?? new Error('Mutex acquire aborted'));
810
830
  }
811
- catch (ex) {
812
- reject(ex);
831
+ if (abort?.aborted) {
832
+ return rejectAborted();
833
+ }
834
+ let holdsMutex = false;
835
+ const markCompleted = () => {
836
+ if (!holdsMutex)
837
+ return;
838
+ holdsMutex = false;
839
+ const waiter = this.firstWaiter;
840
+ if (waiter) {
841
+ this.deactivateWaiter(waiter);
842
+ // Still in critical section, but owned by next waiter now.
843
+ waiter.onAcquire();
844
+ }
845
+ else {
846
+ this.inCriticalSection = false;
847
+ }
848
+ };
849
+ if (!this.inCriticalSection) {
850
+ this.inCriticalSection = true;
851
+ holdsMutex = true;
852
+ return resolve(markCompleted);
853
+ }
854
+ else {
855
+ let node;
856
+ const onAbort = () => {
857
+ abort?.removeEventListener('abort', onAbort);
858
+ if (node.isActive) {
859
+ this.deactivateWaiter(node);
860
+ rejectAborted();
861
+ }
862
+ };
863
+ node = this.addWaiter(() => {
864
+ abort?.removeEventListener('abort', onAbort);
865
+ holdsMutex = true;
866
+ resolve(markCompleted);
867
+ });
868
+ abort?.addEventListener('abort', onAbort);
813
869
  }
814
870
  });
815
- });
871
+ }
872
+ async runExclusive(fn, abort) {
873
+ const returnMutex = await this.acquire(abort);
874
+ try {
875
+ return await fn();
876
+ }
877
+ finally {
878
+ returnMutex();
879
+ }
880
+ }
881
+ }
882
+ function timeoutSignal(timeout) {
883
+ if (timeout == null)
884
+ return;
885
+ if ('timeout' in AbortSignal)
886
+ return AbortSignal.timeout(timeout);
887
+ const controller = new AbortController();
888
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
889
+ return controller.signal;
816
890
  }
817
891
 
818
892
  /**
@@ -824,7 +898,7 @@ class AttachmentService {
824
898
  db;
825
899
  logger;
826
900
  tableName;
827
- mutex = new asyncMutex.Mutex();
901
+ mutex = new Mutex();
828
902
  context;
829
903
  constructor(db, logger, tableName = 'attachments', archivedCacheLimit = 100) {
830
904
  this.db = db;
@@ -861,7 +935,7 @@ class AttachmentService {
861
935
  * Executes a callback with exclusive access to the attachment context.
862
936
  */
863
937
  async withContext(callback) {
864
- return mutexRunExclusive(this.mutex, async () => {
938
+ return this.mutex.runExclusive(async () => {
865
939
  return callback(this.context);
866
940
  });
867
941
  }
@@ -897,9 +971,15 @@ class AttachmentQueue {
897
971
  tableName;
898
972
  /** Logger instance for diagnostic information */
899
973
  logger;
900
- /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
974
+ /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
975
+ * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
901
976
  syncIntervalMs = 30 * 1000;
902
- /** Duration in milliseconds to throttle sync operations */
977
+ /** Throttle duration in milliseconds for the reactive watch query on the attachments table.
978
+ * When attachment records change, a watch query detects the change and triggers a sync.
979
+ * This throttle prevents the sync from firing too rapidly when many changes happen in
980
+ * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
981
+ * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
982
+ * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
903
983
  syncThrottleDuration;
904
984
  /** Whether to automatically download remote attachments. Default: true */
905
985
  downloadAttachments = true;
@@ -923,8 +1003,8 @@ class AttachmentQueue {
923
1003
  * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
924
1004
  * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
925
1005
  * @param options.logger - Logger instance. Defaults to db.logger
926
- * @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000
927
- * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
1006
+ * @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
1007
+ * @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
928
1008
  * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
929
1009
  * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
930
1010
  */
@@ -8061,7 +8141,7 @@ function requireDist () {
8061
8141
 
8062
8142
  var distExports = requireDist();
8063
8143
 
8064
- var version = "1.49.0";
8144
+ var version = "1.50.0";
8065
8145
  var PACKAGE = {
8066
8146
  version: version};
8067
8147
 
@@ -10453,7 +10533,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10453
10533
  this._schema = schema;
10454
10534
  this.ready = false;
10455
10535
  this.sdkVersion = '';
10456
- this.runExclusiveMutex = new asyncMutex.Mutex();
10536
+ this.runExclusiveMutex = new Mutex();
10457
10537
  // Start async init
10458
10538
  this.subscriptions = {
10459
10539
  firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
@@ -10918,6 +10998,10 @@ SELECT * FROM crud_entries;
10918
10998
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
10919
10999
  * and optionally return results.
10920
11000
  *
11001
+ * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
11002
+ * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
11003
+ * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
11004
+ *
10921
11005
  * @param sql The SQL query to execute
10922
11006
  * @param parameters Optional array of parameters to bind to the query
10923
11007
  * @returns The query result as an object with structured key-value pairs
@@ -11014,7 +11098,7 @@ SELECT * FROM crud_entries;
11014
11098
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
11015
11099
  await this.waitForReady();
11016
11100
  return this.database.readTransaction(async (tx) => {
11017
- const res = await callback({ ...tx });
11101
+ const res = await callback(tx);
11018
11102
  await tx.rollback();
11019
11103
  return res;
11020
11104
  }, { timeoutMs: lockTimeout });
@@ -12044,6 +12128,7 @@ exports.LogLevel = LogLevel;
12044
12128
  exports.MAX_AMOUNT_OF_COLUMNS = MAX_AMOUNT_OF_COLUMNS;
12045
12129
  exports.MAX_OP_ID = MAX_OP_ID;
12046
12130
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
12131
+ exports.Mutex = Mutex;
12047
12132
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
12048
12133
  exports.OpType = OpType;
12049
12134
  exports.OplogEntry = OplogEntry;
@@ -12077,9 +12162,9 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
12077
12162
  exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
12078
12163
  exports.isStreamingSyncData = isStreamingSyncData;
12079
12164
  exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
12080
- exports.mutexRunExclusive = mutexRunExclusive;
12081
12165
  exports.parseQuery = parseQuery;
12082
12166
  exports.runOnSchemaChange = runOnSchemaChange;
12083
12167
  exports.sanitizeSQL = sanitizeSQL;
12084
12168
  exports.sanitizeUUID = sanitizeUUID;
12169
+ exports.timeoutSignal = timeoutSignal;
12085
12170
  //# sourceMappingURL=bundle.node.cjs.map