@powersync/common 1.48.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.
Files changed (40) hide show
  1. package/dist/bundle.cjs +270 -46
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +265 -44
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +270 -45
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +265 -43
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +108 -26
  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/client/triggers/TriggerManager.d.ts +13 -1
  22. package/lib/client/triggers/TriggerManagerImpl.d.ts +2 -2
  23. package/lib/client/triggers/TriggerManagerImpl.js +19 -7
  24. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
  25. package/lib/db/DBAdapter.d.ts +55 -9
  26. package/lib/db/DBAdapter.js +126 -0
  27. package/lib/db/DBAdapter.js.map +1 -1
  28. package/lib/utils/mutex.d.ts +18 -5
  29. package/lib/utils/mutex.js +97 -21
  30. package/lib/utils/mutex.js.map +1 -1
  31. package/package.json +1 -2
  32. package/src/attachments/AttachmentQueue.ts +10 -4
  33. package/src/attachments/AttachmentService.ts +2 -3
  34. package/src/attachments/README.md +6 -4
  35. package/src/attachments/SyncingService.ts +4 -5
  36. package/src/client/AbstractPowerSyncDatabase.ts +6 -2
  37. package/src/client/triggers/TriggerManager.ts +15 -2
  38. package/src/client/triggers/TriggerManagerImpl.ts +18 -6
  39. package/src/db/DBAdapter.ts +167 -9
  40. package/src/utils/mutex.ts +121 -26
@@ -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
  */
@@ -1532,6 +1612,49 @@ var Logger = /*@__PURE__*/getDefaultExportFromCjs(loggerExports);
1532
1612
  * Set of generic interfaces to allow PowerSync compatibility with
1533
1613
  * different SQLite DB implementations.
1534
1614
  */
1615
+ /**
1616
+ * Implements {@link DBGetUtils} on a {@link SqlRunner}.
1617
+ */
1618
+ function DBGetUtilsDefaultMixin(Base) {
1619
+ return class extends Base {
1620
+ async getAll(sql, parameters) {
1621
+ const res = await this.execute(sql, parameters);
1622
+ return res.rows?._array ?? [];
1623
+ }
1624
+ async getOptional(sql, parameters) {
1625
+ const res = await this.execute(sql, parameters);
1626
+ return res.rows?.item(0) ?? null;
1627
+ }
1628
+ async get(sql, parameters) {
1629
+ const res = await this.execute(sql, parameters);
1630
+ const first = res.rows?.item(0);
1631
+ if (!first) {
1632
+ throw new Error('Result set is empty');
1633
+ }
1634
+ return first;
1635
+ }
1636
+ async executeBatch(query, params = []) {
1637
+ // If this context can run batch statements natively, use that.
1638
+ // @ts-ignore
1639
+ if (super.executeBatch) {
1640
+ // @ts-ignore
1641
+ return super.executeBatch(query, params);
1642
+ }
1643
+ // Emulate executeBatch by running statements individually.
1644
+ let lastInsertId;
1645
+ let rowsAffected = 0;
1646
+ for (const set of params) {
1647
+ const result = await this.execute(query, set);
1648
+ lastInsertId = result.insertId;
1649
+ rowsAffected += result.rowsAffected;
1650
+ }
1651
+ return {
1652
+ rowsAffected,
1653
+ insertId: lastInsertId
1654
+ };
1655
+ }
1656
+ };
1657
+ }
1535
1658
  /**
1536
1659
  * Update table operation numbers from SQLite
1537
1660
  */
@@ -1541,6 +1664,89 @@ exports.RowUpdateType = void 0;
1541
1664
  RowUpdateType[RowUpdateType["SQLITE_DELETE"] = 9] = "SQLITE_DELETE";
1542
1665
  RowUpdateType[RowUpdateType["SQLITE_UPDATE"] = 23] = "SQLITE_UPDATE";
1543
1666
  })(exports.RowUpdateType || (exports.RowUpdateType = {}));
1667
+ /**
1668
+ * A mixin to implement {@link DBAdapter} by delegating to {@link ConnectionPool.readLock} and
1669
+ * {@link ConnectionPool.writeLock}.
1670
+ */
1671
+ function DBAdapterDefaultMixin(Base) {
1672
+ return class extends Base {
1673
+ readTransaction(fn, options) {
1674
+ return this.readLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
1675
+ }
1676
+ writeTransaction(fn, options) {
1677
+ return this.writeLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
1678
+ }
1679
+ getAll(sql, parameters) {
1680
+ return this.readLock((ctx) => ctx.getAll(sql, parameters));
1681
+ }
1682
+ getOptional(sql, parameters) {
1683
+ return this.readLock((ctx) => ctx.getOptional(sql, parameters));
1684
+ }
1685
+ get(sql, parameters) {
1686
+ return this.readLock((ctx) => ctx.get(sql, parameters));
1687
+ }
1688
+ execute(query, params) {
1689
+ return this.writeLock((ctx) => ctx.execute(query, params));
1690
+ }
1691
+ executeRaw(query, params) {
1692
+ return this.writeLock((ctx) => ctx.executeRaw(query, params));
1693
+ }
1694
+ executeBatch(query, params) {
1695
+ return this.writeTransaction((tx) => tx.executeBatch(query, params));
1696
+ }
1697
+ };
1698
+ }
1699
+ class BaseTransaction {
1700
+ inner;
1701
+ finalized = false;
1702
+ constructor(inner) {
1703
+ this.inner = inner;
1704
+ }
1705
+ async commit() {
1706
+ if (this.finalized) {
1707
+ return { rowsAffected: 0 };
1708
+ }
1709
+ this.finalized = true;
1710
+ return this.inner.execute('COMMIT');
1711
+ }
1712
+ async rollback() {
1713
+ if (this.finalized) {
1714
+ return { rowsAffected: 0 };
1715
+ }
1716
+ this.finalized = true;
1717
+ return this.inner.execute('ROLLBACK');
1718
+ }
1719
+ execute(query, params) {
1720
+ return this.inner.execute(query, params);
1721
+ }
1722
+ executeRaw(query, params) {
1723
+ return this.inner.executeRaw(query, params);
1724
+ }
1725
+ executeBatch(query, params) {
1726
+ return this.inner.executeBatch(query, params);
1727
+ }
1728
+ }
1729
+ class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
1730
+ static async runWith(ctx, fn) {
1731
+ let tx = new TransactionImplementation(ctx);
1732
+ try {
1733
+ await ctx.execute('BEGIN IMMEDIATE');
1734
+ const result = await fn(tx);
1735
+ await tx.commit();
1736
+ return result;
1737
+ }
1738
+ catch (ex) {
1739
+ try {
1740
+ await tx.rollback();
1741
+ }
1742
+ catch (ex2) {
1743
+ // In rare cases, a rollback may fail.
1744
+ // Safe to ignore.
1745
+ }
1746
+ throw ex;
1747
+ }
1748
+ }
1749
+ }
1544
1750
  function isBatchedUpdateNotification(update) {
1545
1751
  return 'tables' in update;
1546
1752
  }
@@ -7935,7 +8141,7 @@ function requireDist () {
7935
8141
 
7936
8142
  var distExports = requireDist();
7937
8143
 
7938
- var version = "1.48.0";
8144
+ var version = "1.50.0";
7939
8145
  var PACKAGE = {
7940
8146
  version: version};
7941
8147
 
@@ -9968,7 +10174,7 @@ class TriggerManagerImpl {
9968
10174
  }
9969
10175
  async createDiffTrigger(options) {
9970
10176
  await this.db.waitForReady();
9971
- const { source, destination, columns, when, hooks,
10177
+ const { source, destination, columns, when, hooks, setupContext,
9972
10178
  // Fall back to the provided default if not given on this level
9973
10179
  useStorage = this.defaultConfig.useStorageByDefault } = options;
9974
10180
  const operations = Object.keys(when);
@@ -10023,13 +10229,20 @@ class TriggerManagerImpl {
10023
10229
  * we need to ensure we can cleanup the created resources.
10024
10230
  * We unfortunately cannot rely on transaction rollback.
10025
10231
  */
10026
- const cleanup = async () => {
10232
+ const cleanup = async (options) => {
10233
+ const { context } = options ?? {};
10027
10234
  disposeWarningListener();
10028
- return this.db.writeLock(async (tx) => {
10235
+ const doCleanup = async (tx) => {
10029
10236
  await this.removeTriggers(tx, triggerIds);
10030
- await tx.execute(/* sql */ `DROP TABLE IF EXISTS ${destination};`);
10237
+ await tx.execute(`DROP TABLE IF EXISTS ${destination};`);
10031
10238
  await releaseStorageClaim?.();
10032
- });
10239
+ };
10240
+ if (context) {
10241
+ await doCleanup(context);
10242
+ }
10243
+ else {
10244
+ await this.db.writeLock(doCleanup);
10245
+ }
10033
10246
  };
10034
10247
  const setup = async (tx) => {
10035
10248
  // Allow user code to execute in this lock context before the trigger is created.
@@ -10103,12 +10316,17 @@ class TriggerManagerImpl {
10103
10316
  }
10104
10317
  };
10105
10318
  try {
10106
- await this.db.writeLock(setup);
10319
+ if (setupContext) {
10320
+ await setup(setupContext);
10321
+ }
10322
+ else {
10323
+ await this.db.writeLock(setup);
10324
+ }
10107
10325
  return cleanup;
10108
10326
  }
10109
10327
  catch (error) {
10110
10328
  try {
10111
- await cleanup();
10329
+ await cleanup(setupContext ? { context: setupContext } : undefined);
10112
10330
  }
10113
10331
  catch (cleanupError) {
10114
10332
  throw new AggregateError([error, cleanupError], 'Error during operation and cleanup');
@@ -10315,7 +10533,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10315
10533
  this._schema = schema;
10316
10534
  this.ready = false;
10317
10535
  this.sdkVersion = '';
10318
- this.runExclusiveMutex = new asyncMutex.Mutex();
10536
+ this.runExclusiveMutex = new Mutex();
10319
10537
  // Start async init
10320
10538
  this.subscriptions = {
10321
10539
  firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
@@ -10780,6 +10998,10 @@ SELECT * FROM crud_entries;
10780
10998
  * Execute a SQL write (INSERT/UPDATE/DELETE) query
10781
10999
  * and optionally return results.
10782
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
+ *
10783
11005
  * @param sql The SQL query to execute
10784
11006
  * @param parameters Optional array of parameters to bind to the query
10785
11007
  * @returns The query result as an object with structured key-value pairs
@@ -10876,7 +11098,7 @@ SELECT * FROM crud_entries;
10876
11098
  async readTransaction(callback, lockTimeout = DEFAULT_LOCK_TIMEOUT_MS) {
10877
11099
  await this.waitForReady();
10878
11100
  return this.database.readTransaction(async (tx) => {
10879
- const res = await callback({ ...tx });
11101
+ const res = await callback(tx);
10880
11102
  await tx.rollback();
10881
11103
  return res;
10882
11104
  }, { timeoutMs: lockTimeout });
@@ -11873,6 +12095,8 @@ exports.ControlledExecutor = ControlledExecutor;
11873
12095
  exports.CrudBatch = CrudBatch;
11874
12096
  exports.CrudEntry = CrudEntry;
11875
12097
  exports.CrudTransaction = CrudTransaction;
12098
+ exports.DBAdapterDefaultMixin = DBAdapterDefaultMixin;
12099
+ exports.DBGetUtilsDefaultMixin = DBGetUtilsDefaultMixin;
11876
12100
  exports.DEFAULT_CRUD_BATCH_LIMIT = DEFAULT_CRUD_BATCH_LIMIT;
11877
12101
  exports.DEFAULT_CRUD_UPLOAD_THROTTLE_MS = DEFAULT_CRUD_UPLOAD_THROTTLE_MS;
11878
12102
  exports.DEFAULT_INDEX_COLUMN_OPTIONS = DEFAULT_INDEX_COLUMN_OPTIONS;
@@ -11904,6 +12128,7 @@ exports.LogLevel = LogLevel;
11904
12128
  exports.MAX_AMOUNT_OF_COLUMNS = MAX_AMOUNT_OF_COLUMNS;
11905
12129
  exports.MAX_OP_ID = MAX_OP_ID;
11906
12130
  exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
12131
+ exports.Mutex = Mutex;
11907
12132
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
11908
12133
  exports.OpType = OpType;
11909
12134
  exports.OplogEntry = OplogEntry;
@@ -11937,9 +12162,9 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
11937
12162
  exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
11938
12163
  exports.isStreamingSyncData = isStreamingSyncData;
11939
12164
  exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
11940
- exports.mutexRunExclusive = mutexRunExclusive;
11941
12165
  exports.parseQuery = parseQuery;
11942
12166
  exports.runOnSchemaChange = runOnSchemaChange;
11943
12167
  exports.sanitizeSQL = sanitizeSQL;
11944
12168
  exports.sanitizeUUID = sanitizeUUID;
12169
+ exports.timeoutSignal = timeoutSignal;
11945
12170
  //# sourceMappingURL=bundle.node.cjs.map