@powersync/common 1.56.0 → 1.57.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.mjs CHANGED
@@ -387,6 +387,19 @@ var AttachmentState;
387
387
  AttachmentState[AttachmentState["SYNCED"] = 3] = "SYNCED";
388
388
  AttachmentState[AttachmentState["ARCHIVED"] = 4] = "ARCHIVED"; // Attachment has been orphaned, i.e. the associated record has been deleted
389
389
  })(AttachmentState || (AttachmentState = {}));
390
+ /**
391
+ * @alpha
392
+ */
393
+ const ATTACHMENT_TABLE_COLUMNS = {
394
+ filename: column.text,
395
+ local_uri: column.text,
396
+ timestamp: column.integer,
397
+ size: column.integer,
398
+ media_type: column.text,
399
+ state: column.integer, // Corresponds to AttachmentState
400
+ has_synced: column.integer,
401
+ meta_data: column.text
402
+ };
390
403
  /**
391
404
  * AttachmentTable defines the schema for the attachment queue table.
392
405
  *
@@ -394,16 +407,7 @@ var AttachmentState;
394
407
  */
395
408
  class AttachmentTable extends Table {
396
409
  constructor(options) {
397
- super({
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
- }, {
410
+ super(ATTACHMENT_TABLE_COLUMNS, {
407
411
  ...options,
408
412
  viewName: options?.viewName ?? ATTACHMENT_TABLE,
409
413
  localOnly: true,
@@ -935,31 +939,51 @@ class SyncingService {
935
939
  }
936
940
  /**
937
941
  * Processes attachments based on their state (upload, download, or delete).
938
- * All updates are saved in a single batch after processing.
942
+ *
943
+ * Each attachment's I/O runs outside the attachment-service mutex, and the row's
944
+ * state transition is persisted immediately after it completes. This keeps the
945
+ * mutex available to concurrent `saveFile` / `deleteFile` / watched-attachment
946
+ * processing while a batch is in flight, and means consumer queries against the
947
+ * attachments queue see incremental progress instead of one atomic commit at the
948
+ * end of the batch.
939
949
  *
940
950
  * @param attachments - Array of attachment records to process
941
- * @param context - Attachment context for database operations
942
- * @returns Promise that resolves when all attachments have been processed and saved
943
- */
944
- async processAttachments(attachments, context) {
945
- const updatedAttachments = [];
951
+ * @param options - Optional controls. Pass `signal` (an `AbortSignal`) to interrupt
952
+ * the batch: it is checked between attachments and, once aborted, the
953
+ * loop exits early — letting `stopSync` stop a running batch within
954
+ * one attachment's processing time.
955
+ */
956
+ async processAttachments(attachments, options) {
957
+ const signal = options?.signal;
958
+ this.logger.info(`Starting processAttachments with ${attachments.length} attachments`);
946
959
  for (const attachment of attachments) {
947
- switch (attachment.state) {
948
- case AttachmentState.QUEUED_UPLOAD:
949
- const uploaded = await this.uploadAttachment(attachment);
950
- updatedAttachments.push(uploaded);
951
- break;
952
- case AttachmentState.QUEUED_DOWNLOAD:
953
- const downloaded = await this.downloadAttachment(attachment);
954
- updatedAttachments.push(downloaded);
955
- break;
956
- case AttachmentState.QUEUED_DELETE:
957
- const deleted = await this.deleteAttachment(attachment, context);
958
- updatedAttachments.push(deleted);
959
- break;
960
+ if (signal?.aborted) {
961
+ this.logger.info('Sync cancelled; stopping iteration early');
962
+ return;
963
+ }
964
+ try {
965
+ let updated;
966
+ switch (attachment.state) {
967
+ case AttachmentState.QUEUED_UPLOAD:
968
+ updated = await this.uploadAttachment(attachment);
969
+ break;
970
+ case AttachmentState.QUEUED_DOWNLOAD:
971
+ updated = await this.downloadAttachment(attachment);
972
+ break;
973
+ case AttachmentState.QUEUED_DELETE:
974
+ // `deleteAttachment` needs a context (it removes the row in a
975
+ // transaction); briefly re-acquire the mutex for just this row.
976
+ updated = await this.attachmentService.withContext((ctx) => this.deleteAttachment(attachment, ctx));
977
+ break;
978
+ default:
979
+ continue;
980
+ }
981
+ await this.attachmentService.withContext((ctx) => ctx.saveAttachments([updated]));
982
+ }
983
+ catch (error) {
984
+ this.logger.warn(`Error during sync for ${attachment.id}`, error);
960
985
  }
961
986
  }
962
- await context.saveAttachments(updatedAttachments);
963
987
  }
964
988
  /**
965
989
  * Uploads an attachment from local storage to remote storage.
@@ -1130,6 +1154,19 @@ class AttachmentQueue {
1130
1154
  statusListenerDispose;
1131
1155
  watchActiveAttachments;
1132
1156
  watchAttachmentsAbortController;
1157
+ /**
1158
+ * Serializes concurrent `syncStorage()` triggers (periodic timer, watch-onDiff,
1159
+ * status-changed). Held across the whole batch, but only contended by other
1160
+ * sync triggers — foreground `saveFile` / `deleteFile` / watched-attachment
1161
+ * processing don't take this lock and proceed in parallel via the
1162
+ * `AttachmentService` mutex, which is acquired only briefly per row.
1163
+ */
1164
+ syncLoopMutex = new Mutex();
1165
+ /**
1166
+ * Aborted by `stopSync()` to interrupt an in-flight batch within one
1167
+ * attachment's processing time. Polled between rows by `SyncingService`.
1168
+ */
1169
+ syncAbortController;
1133
1170
  /**
1134
1171
  * Creates a new AttachmentQueue instance.
1135
1172
  *
@@ -1169,6 +1206,7 @@ class AttachmentQueue {
1169
1206
  */
1170
1207
  async startSync() {
1171
1208
  await this.stopSync();
1209
+ this.syncAbortController = new AbortController();
1172
1210
  this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
1173
1211
  throttleMs: this.syncThrottleDuration
1174
1212
  });
@@ -1283,23 +1321,44 @@ class AttachmentQueue {
1283
1321
  *
1284
1322
  * This is called automatically at regular intervals when sync is started,
1285
1323
  * but can also be called manually to trigger an immediate sync.
1324
+ *
1325
+ * Concurrent invocations are serialized via `syncLoopMutex`.
1286
1326
  */
1287
1327
  async syncStorage() {
1288
- await this.attachmentService.withContext(async (ctx) => {
1289
- const activeAttachments = await ctx.getActiveAttachments();
1290
- await this.localStorage.initialize();
1291
- await this.syncingService.processAttachments(activeAttachments, ctx);
1292
- await this.syncingService.deleteArchivedAttachments(ctx);
1293
- });
1328
+ const signal = this.syncAbortController?.signal;
1329
+ if (signal?.aborted)
1330
+ return;
1331
+ try {
1332
+ await this.syncLoopMutex.runExclusive(async () => {
1333
+ const activeAttachments = await this.attachmentService.withContext((ctx) => ctx.getActiveAttachments());
1334
+ await this.localStorage.initialize();
1335
+ await this.syncingService.processAttachments(activeAttachments, { signal });
1336
+ if (signal?.aborted)
1337
+ return;
1338
+ await this.attachmentService.withContext((ctx) => this.syncingService.deleteArchivedAttachments(ctx));
1339
+ }, signal);
1340
+ }
1341
+ catch (error) {
1342
+ // A queued batch's acquire rejects when `stopSync` aborts — expected, not an error.
1343
+ if (signal?.aborted)
1344
+ return;
1345
+ throw error;
1346
+ }
1294
1347
  }
1295
1348
  /**
1296
1349
  * Stops the attachment synchronization process.
1297
1350
  *
1298
- * Clears the periodic sync timer and closes all active attachment watchers.
1351
+ * Clears the periodic sync timer, closes all active attachment watchers, and
1352
+ * aborts any in-flight `syncStorage()` call so it exits within one
1353
+ * attachment's processing time instead of running the batch to completion.
1299
1354
  */
1300
1355
  async stopSync() {
1301
1356
  clearInterval(this.periodicSyncTimer);
1302
1357
  this.periodicSyncTimer = undefined;
1358
+ if (this.syncAbortController) {
1359
+ this.syncAbortController.abort();
1360
+ this.syncAbortController = undefined;
1361
+ }
1303
1362
  if (this.watchActiveAttachments)
1304
1363
  await this.watchActiveAttachments.close();
1305
1364
  if (this.watchAttachmentsAbortController) {
@@ -10914,7 +10973,7 @@ function requireDist () {
10914
10973
 
10915
10974
  var distExports = requireDist();
10916
10975
 
10917
- var version = "1.56.0";
10976
+ var version = "1.57.0";
10918
10977
  var PACKAGE = {
10919
10978
  version: version};
10920
10979
 
@@ -14241,5 +14300,5 @@ const parseQuery = (query, parameters) => {
14241
14300
  return { sqlStatement, parameters: parameters };
14242
14301
  };
14243
14302
 
14244
- export { ATTACHMENT_TABLE, AbortOperation, AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, AbstractQueryProcessor, AbstractRemote, AbstractStreamingSyncImplementation, ArrayComparator, AttachmentContext, AttachmentQueue, AttachmentService, AttachmentState, AttachmentTable, BaseObserver, Column, ColumnType, ConnectionClosedError, ConnectionManager, ControlledExecutor, CrudBatch, CrudEntry, CrudTransaction, DBAdapterDefaultMixin, DBGetUtilsDefaultMixin, DEFAULT_CRUD_BATCH_LIMIT, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_INDEX_COLUMN_OPTIONS, DEFAULT_INDEX_OPTIONS, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_POWERSYNC_CLOSE_OPTIONS, DEFAULT_POWERSYNC_DB_OPTIONS, DEFAULT_REMOTE_LOGGER, DEFAULT_REMOTE_OPTIONS, DEFAULT_RETRY_DELAY_MS, DEFAULT_ROW_COMPARATOR, DEFAULT_STREAMING_SYNC_OPTIONS, DEFAULT_STREAM_CONNECTION_OPTIONS, DEFAULT_SYNC_CLIENT_IMPLEMENTATION, DEFAULT_TABLE_OPTIONS, DEFAULT_WATCH_QUERY_OPTIONS, DEFAULT_WATCH_THROTTLE_MS, DiffTriggerOperation, DifferentialQueryProcessor, EMPTY_DIFFERENTIAL, EncodingType, FalsyComparator, FetchImplementationProvider, FetchStrategy, GetAllQuery, Index, IndexedColumn, InvalidSQLCharacters, LockType, LogLevel, MAX_AMOUNT_OF_COLUMNS, MAX_OP_ID, MEMORY_TRIGGER_CLAIM_MANAGER, Mutex, OnChangeQueryProcessor, PSInternalTable, PowerSyncControlCommand, RowUpdateType, Schema, Semaphore, SqliteBucketStorage, SyncClientImplementation, SyncProgress, SyncStatus, SyncStreamConnectionMethod, SyncingService, Table, TableV2, TriggerManagerImpl, UpdateType, UploadQueueStats, WatchedQueryListenerEvent, attachmentFromSql, column, compilableQueryWatch, createBaseLogger, createLogger, extractTableUpdates, isBatchedUpdateNotification, isDBAdapter, isPowerSyncDatabaseOptionsWithSettings, isSQLOpenFactory, isSQLOpenOptions, parseQuery, runOnSchemaChange, sanitizeSQL, sanitizeUUID, timeoutSignal };
14303
+ export { ATTACHMENT_TABLE, ATTACHMENT_TABLE_COLUMNS, AbortOperation, AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, AbstractQueryProcessor, AbstractRemote, AbstractStreamingSyncImplementation, ArrayComparator, AttachmentContext, AttachmentQueue, AttachmentService, AttachmentState, AttachmentTable, BaseObserver, Column, ColumnType, ConnectionClosedError, ConnectionManager, ControlledExecutor, CrudBatch, CrudEntry, CrudTransaction, DBAdapterDefaultMixin, DBGetUtilsDefaultMixin, DEFAULT_CRUD_BATCH_LIMIT, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_INDEX_COLUMN_OPTIONS, DEFAULT_INDEX_OPTIONS, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_POWERSYNC_CLOSE_OPTIONS, DEFAULT_POWERSYNC_DB_OPTIONS, DEFAULT_REMOTE_LOGGER, DEFAULT_REMOTE_OPTIONS, DEFAULT_RETRY_DELAY_MS, DEFAULT_ROW_COMPARATOR, DEFAULT_STREAMING_SYNC_OPTIONS, DEFAULT_STREAM_CONNECTION_OPTIONS, DEFAULT_SYNC_CLIENT_IMPLEMENTATION, DEFAULT_TABLE_OPTIONS, DEFAULT_WATCH_QUERY_OPTIONS, DEFAULT_WATCH_THROTTLE_MS, DiffTriggerOperation, DifferentialQueryProcessor, EMPTY_DIFFERENTIAL, EncodingType, FalsyComparator, FetchImplementationProvider, FetchStrategy, GetAllQuery, Index, IndexedColumn, InvalidSQLCharacters, LockType, LogLevel, MAX_AMOUNT_OF_COLUMNS, MAX_OP_ID, MEMORY_TRIGGER_CLAIM_MANAGER, Mutex, OnChangeQueryProcessor, PSInternalTable, PowerSyncControlCommand, RowUpdateType, Schema, Semaphore, SqliteBucketStorage, SyncClientImplementation, SyncProgress, SyncStatus, SyncStreamConnectionMethod, SyncingService, Table, TableV2, TriggerManagerImpl, UpdateType, UploadQueueStats, WatchedQueryListenerEvent, attachmentFromSql, column, compilableQueryWatch, createBaseLogger, createLogger, extractTableUpdates, isBatchedUpdateNotification, isDBAdapter, isPowerSyncDatabaseOptionsWithSettings, isSQLOpenFactory, isSQLOpenOptions, parseQuery, runOnSchemaChange, sanitizeSQL, sanitizeUUID, timeoutSignal };
14245
14304
  //# sourceMappingURL=bundle.mjs.map