@powersync/common 1.56.0 → 1.57.1

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.
@@ -391,6 +391,19 @@ exports.AttachmentState = void 0;
391
391
  AttachmentState[AttachmentState["SYNCED"] = 3] = "SYNCED";
392
392
  AttachmentState[AttachmentState["ARCHIVED"] = 4] = "ARCHIVED"; // Attachment has been orphaned, i.e. the associated record has been deleted
393
393
  })(exports.AttachmentState || (exports.AttachmentState = {}));
394
+ /**
395
+ * @alpha
396
+ */
397
+ const ATTACHMENT_TABLE_COLUMNS = {
398
+ filename: column.text,
399
+ local_uri: column.text,
400
+ timestamp: column.integer,
401
+ size: column.integer,
402
+ media_type: column.text,
403
+ state: column.integer, // Corresponds to AttachmentState
404
+ has_synced: column.integer,
405
+ meta_data: column.text
406
+ };
394
407
  /**
395
408
  * AttachmentTable defines the schema for the attachment queue table.
396
409
  *
@@ -398,16 +411,7 @@ exports.AttachmentState = void 0;
398
411
  */
399
412
  class AttachmentTable extends Table {
400
413
  constructor(options) {
401
- super({
402
- filename: column.text,
403
- local_uri: column.text,
404
- timestamp: column.integer,
405
- size: column.integer,
406
- media_type: column.text,
407
- state: column.integer, // Corresponds to AttachmentState
408
- has_synced: column.integer,
409
- meta_data: column.text
410
- }, {
414
+ super(ATTACHMENT_TABLE_COLUMNS, {
411
415
  ...options,
412
416
  viewName: options?.viewName ?? ATTACHMENT_TABLE,
413
417
  localOnly: true,
@@ -939,31 +943,51 @@ class SyncingService {
939
943
  }
940
944
  /**
941
945
  * Processes attachments based on their state (upload, download, or delete).
942
- * All updates are saved in a single batch after processing.
946
+ *
947
+ * Each attachment's I/O runs outside the attachment-service mutex, and the row's
948
+ * state transition is persisted immediately after it completes. This keeps the
949
+ * mutex available to concurrent `saveFile` / `deleteFile` / watched-attachment
950
+ * processing while a batch is in flight, and means consumer queries against the
951
+ * attachments queue see incremental progress instead of one atomic commit at the
952
+ * end of the batch.
943
953
  *
944
954
  * @param attachments - Array of attachment records to process
945
- * @param context - Attachment context for database operations
946
- * @returns Promise that resolves when all attachments have been processed and saved
947
- */
948
- async processAttachments(attachments, context) {
949
- const updatedAttachments = [];
955
+ * @param options - Optional controls. Pass `signal` (an `AbortSignal`) to interrupt
956
+ * the batch: it is checked between attachments and, once aborted, the
957
+ * loop exits early — letting `stopSync` stop a running batch within
958
+ * one attachment's processing time.
959
+ */
960
+ async processAttachments(attachments, options) {
961
+ const signal = options?.signal;
962
+ this.logger.info(`Starting processAttachments with ${attachments.length} attachments`);
950
963
  for (const attachment of attachments) {
951
- switch (attachment.state) {
952
- case exports.AttachmentState.QUEUED_UPLOAD:
953
- const uploaded = await this.uploadAttachment(attachment);
954
- updatedAttachments.push(uploaded);
955
- break;
956
- case exports.AttachmentState.QUEUED_DOWNLOAD:
957
- const downloaded = await this.downloadAttachment(attachment);
958
- updatedAttachments.push(downloaded);
959
- break;
960
- case exports.AttachmentState.QUEUED_DELETE:
961
- const deleted = await this.deleteAttachment(attachment, context);
962
- updatedAttachments.push(deleted);
963
- break;
964
+ if (signal?.aborted) {
965
+ this.logger.info('Sync cancelled; stopping iteration early');
966
+ return;
967
+ }
968
+ try {
969
+ let updated;
970
+ switch (attachment.state) {
971
+ case exports.AttachmentState.QUEUED_UPLOAD:
972
+ updated = await this.uploadAttachment(attachment);
973
+ break;
974
+ case exports.AttachmentState.QUEUED_DOWNLOAD:
975
+ updated = await this.downloadAttachment(attachment);
976
+ break;
977
+ case exports.AttachmentState.QUEUED_DELETE:
978
+ // `deleteAttachment` needs a context (it removes the row in a
979
+ // transaction); briefly re-acquire the mutex for just this row.
980
+ updated = await this.attachmentService.withContext((ctx) => this.deleteAttachment(attachment, ctx));
981
+ break;
982
+ default:
983
+ continue;
984
+ }
985
+ await this.attachmentService.withContext((ctx) => ctx.saveAttachments([updated]));
986
+ }
987
+ catch (error) {
988
+ this.logger.warn(`Error during sync for ${attachment.id}`, error);
964
989
  }
965
990
  }
966
- await context.saveAttachments(updatedAttachments);
967
991
  }
968
992
  /**
969
993
  * Uploads an attachment from local storage to remote storage.
@@ -1134,6 +1158,19 @@ class AttachmentQueue {
1134
1158
  statusListenerDispose;
1135
1159
  watchActiveAttachments;
1136
1160
  watchAttachmentsAbortController;
1161
+ /**
1162
+ * Serializes concurrent `syncStorage()` triggers (periodic timer, watch-onDiff,
1163
+ * status-changed). Held across the whole batch, but only contended by other
1164
+ * sync triggers — foreground `saveFile` / `deleteFile` / watched-attachment
1165
+ * processing don't take this lock and proceed in parallel via the
1166
+ * `AttachmentService` mutex, which is acquired only briefly per row.
1167
+ */
1168
+ syncLoopMutex = new Mutex();
1169
+ /**
1170
+ * Aborted by `stopSync()` to interrupt an in-flight batch within one
1171
+ * attachment's processing time. Polled between rows by `SyncingService`.
1172
+ */
1173
+ syncAbortController;
1137
1174
  /**
1138
1175
  * Creates a new AttachmentQueue instance.
1139
1176
  *
@@ -1173,6 +1210,7 @@ class AttachmentQueue {
1173
1210
  */
1174
1211
  async startSync() {
1175
1212
  await this.stopSync();
1213
+ this.syncAbortController = new AbortController();
1176
1214
  this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
1177
1215
  throttleMs: this.syncThrottleDuration
1178
1216
  });
@@ -1287,23 +1325,44 @@ class AttachmentQueue {
1287
1325
  *
1288
1326
  * This is called automatically at regular intervals when sync is started,
1289
1327
  * but can also be called manually to trigger an immediate sync.
1328
+ *
1329
+ * Concurrent invocations are serialized via `syncLoopMutex`.
1290
1330
  */
1291
1331
  async syncStorage() {
1292
- await this.attachmentService.withContext(async (ctx) => {
1293
- const activeAttachments = await ctx.getActiveAttachments();
1294
- await this.localStorage.initialize();
1295
- await this.syncingService.processAttachments(activeAttachments, ctx);
1296
- await this.syncingService.deleteArchivedAttachments(ctx);
1297
- });
1332
+ const signal = this.syncAbortController?.signal;
1333
+ if (signal?.aborted)
1334
+ return;
1335
+ try {
1336
+ await this.syncLoopMutex.runExclusive(async () => {
1337
+ const activeAttachments = await this.attachmentService.withContext((ctx) => ctx.getActiveAttachments());
1338
+ await this.localStorage.initialize();
1339
+ await this.syncingService.processAttachments(activeAttachments, { signal });
1340
+ if (signal?.aborted)
1341
+ return;
1342
+ await this.attachmentService.withContext((ctx) => this.syncingService.deleteArchivedAttachments(ctx));
1343
+ }, signal);
1344
+ }
1345
+ catch (error) {
1346
+ // A queued batch's acquire rejects when `stopSync` aborts — expected, not an error.
1347
+ if (signal?.aborted)
1348
+ return;
1349
+ throw error;
1350
+ }
1298
1351
  }
1299
1352
  /**
1300
1353
  * Stops the attachment synchronization process.
1301
1354
  *
1302
- * Clears the periodic sync timer and closes all active attachment watchers.
1355
+ * Clears the periodic sync timer, closes all active attachment watchers, and
1356
+ * aborts any in-flight `syncStorage()` call so it exits within one
1357
+ * attachment's processing time instead of running the batch to completion.
1303
1358
  */
1304
1359
  async stopSync() {
1305
1360
  clearInterval(this.periodicSyncTimer);
1306
1361
  this.periodicSyncTimer = undefined;
1362
+ if (this.syncAbortController) {
1363
+ this.syncAbortController.abort();
1364
+ this.syncAbortController = undefined;
1365
+ }
1307
1366
  if (this.watchActiveAttachments)
1308
1367
  await this.watchActiveAttachments.close();
1309
1368
  if (this.watchAttachmentsAbortController) {
@@ -2053,7 +2112,7 @@ class SyncStatus {
2053
2112
  });
2054
2113
  }
2055
2114
  /**
2056
- * All sync streams currently being tracked in teh database.
2115
+ * All sync streams currently being tracked in the database.
2057
2116
  *
2058
2117
  * This returns null when the database is currently being opened and we don't have reliable information about all
2059
2118
  * included streams yet.
@@ -8547,7 +8606,7 @@ function requireDist () {
8547
8606
 
8548
8607
  var distExports = requireDist();
8549
8608
 
8550
- var version = "1.56.0";
8609
+ var version = "1.57.1";
8551
8610
  var PACKAGE = {
8552
8611
  version: version};
8553
8612
 
@@ -10298,7 +10357,12 @@ class TriggerManagerImpl {
10298
10357
  // destination table consistent.
10299
10358
  await this.db.writeTransaction(async (tx) => {
10300
10359
  const callbackResult = await options.onChange({
10301
- ...tx,
10360
+ execute: (query, params) => tx.execute(query, params),
10361
+ executeBatch: (query, params) => tx.executeBatch(query, params),
10362
+ executeRaw: (query, params) => tx.executeRaw(query, params),
10363
+ get: (query, params) => tx.get(query, params),
10364
+ getAll: (query, params) => tx.getAll(query, params),
10365
+ getOptional: (query, params) => tx.getOptional(query, params),
10302
10366
  destinationTable: destination,
10303
10367
  withDiff: async (query, params, options) => {
10304
10368
  // Wrap the query to expose the destination table
@@ -11875,6 +11939,7 @@ const parseQuery = (query, parameters) => {
11875
11939
  };
11876
11940
 
11877
11941
  exports.ATTACHMENT_TABLE = ATTACHMENT_TABLE;
11942
+ exports.ATTACHMENT_TABLE_COLUMNS = ATTACHMENT_TABLE_COLUMNS;
11878
11943
  exports.AbortOperation = AbortOperation;
11879
11944
  exports.AbstractPowerSyncDatabase = AbstractPowerSyncDatabase;
11880
11945
  exports.AbstractPowerSyncDatabaseOpenFactory = AbstractPowerSyncDatabaseOpenFactory;