@powersync/web 1.38.5 → 1.38.6

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.
@@ -1641,6 +1641,7 @@ class WorkerClient {
1641
1641
  __webpack_require__.r(__webpack_exports__);
1642
1642
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1643
1643
  /* harmony export */ ATTACHMENT_TABLE: () => (/* binding */ ATTACHMENT_TABLE),
1644
+ /* harmony export */ ATTACHMENT_TABLE_COLUMNS: () => (/* binding */ ATTACHMENT_TABLE_COLUMNS),
1644
1645
  /* harmony export */ AbortOperation: () => (/* binding */ AbortOperation),
1645
1646
  /* harmony export */ AbstractPowerSyncDatabase: () => (/* binding */ AbstractPowerSyncDatabase),
1646
1647
  /* harmony export */ AbstractPowerSyncDatabaseOpenFactory: () => (/* binding */ AbstractPowerSyncDatabaseOpenFactory),
@@ -2122,6 +2123,19 @@ var AttachmentState;
2122
2123
  AttachmentState[AttachmentState["SYNCED"] = 3] = "SYNCED";
2123
2124
  AttachmentState[AttachmentState["ARCHIVED"] = 4] = "ARCHIVED"; // Attachment has been orphaned, i.e. the associated record has been deleted
2124
2125
  })(AttachmentState || (AttachmentState = {}));
2126
+ /**
2127
+ * @alpha
2128
+ */
2129
+ const ATTACHMENT_TABLE_COLUMNS = {
2130
+ filename: column.text,
2131
+ local_uri: column.text,
2132
+ timestamp: column.integer,
2133
+ size: column.integer,
2134
+ media_type: column.text,
2135
+ state: column.integer, // Corresponds to AttachmentState
2136
+ has_synced: column.integer,
2137
+ meta_data: column.text
2138
+ };
2125
2139
  /**
2126
2140
  * AttachmentTable defines the schema for the attachment queue table.
2127
2141
  *
@@ -2129,16 +2143,7 @@ var AttachmentState;
2129
2143
  */
2130
2144
  class AttachmentTable extends Table {
2131
2145
  constructor(options) {
2132
- super({
2133
- filename: column.text,
2134
- local_uri: column.text,
2135
- timestamp: column.integer,
2136
- size: column.integer,
2137
- media_type: column.text,
2138
- state: column.integer, // Corresponds to AttachmentState
2139
- has_synced: column.integer,
2140
- meta_data: column.text
2141
- }, {
2146
+ super(ATTACHMENT_TABLE_COLUMNS, {
2142
2147
  ...options,
2143
2148
  viewName: options?.viewName ?? ATTACHMENT_TABLE,
2144
2149
  localOnly: true,
@@ -2670,31 +2675,51 @@ class SyncingService {
2670
2675
  }
2671
2676
  /**
2672
2677
  * Processes attachments based on their state (upload, download, or delete).
2673
- * All updates are saved in a single batch after processing.
2678
+ *
2679
+ * Each attachment's I/O runs outside the attachment-service mutex, and the row's
2680
+ * state transition is persisted immediately after it completes. This keeps the
2681
+ * mutex available to concurrent `saveFile` / `deleteFile` / watched-attachment
2682
+ * processing while a batch is in flight, and means consumer queries against the
2683
+ * attachments queue see incremental progress instead of one atomic commit at the
2684
+ * end of the batch.
2674
2685
  *
2675
2686
  * @param attachments - Array of attachment records to process
2676
- * @param context - Attachment context for database operations
2677
- * @returns Promise that resolves when all attachments have been processed and saved
2678
- */
2679
- async processAttachments(attachments, context) {
2680
- const updatedAttachments = [];
2687
+ * @param options - Optional controls. Pass `signal` (an `AbortSignal`) to interrupt
2688
+ * the batch: it is checked between attachments and, once aborted, the
2689
+ * loop exits early — letting `stopSync` stop a running batch within
2690
+ * one attachment's processing time.
2691
+ */
2692
+ async processAttachments(attachments, options) {
2693
+ const signal = options?.signal;
2694
+ this.logger.info(`Starting processAttachments with ${attachments.length} attachments`);
2681
2695
  for (const attachment of attachments) {
2682
- switch (attachment.state) {
2683
- case AttachmentState.QUEUED_UPLOAD:
2684
- const uploaded = await this.uploadAttachment(attachment);
2685
- updatedAttachments.push(uploaded);
2686
- break;
2687
- case AttachmentState.QUEUED_DOWNLOAD:
2688
- const downloaded = await this.downloadAttachment(attachment);
2689
- updatedAttachments.push(downloaded);
2690
- break;
2691
- case AttachmentState.QUEUED_DELETE:
2692
- const deleted = await this.deleteAttachment(attachment, context);
2693
- updatedAttachments.push(deleted);
2694
- break;
2696
+ if (signal?.aborted) {
2697
+ this.logger.info('Sync cancelled; stopping iteration early');
2698
+ return;
2699
+ }
2700
+ try {
2701
+ let updated;
2702
+ switch (attachment.state) {
2703
+ case AttachmentState.QUEUED_UPLOAD:
2704
+ updated = await this.uploadAttachment(attachment);
2705
+ break;
2706
+ case AttachmentState.QUEUED_DOWNLOAD:
2707
+ updated = await this.downloadAttachment(attachment);
2708
+ break;
2709
+ case AttachmentState.QUEUED_DELETE:
2710
+ // `deleteAttachment` needs a context (it removes the row in a
2711
+ // transaction); briefly re-acquire the mutex for just this row.
2712
+ updated = await this.attachmentService.withContext((ctx) => this.deleteAttachment(attachment, ctx));
2713
+ break;
2714
+ default:
2715
+ continue;
2716
+ }
2717
+ await this.attachmentService.withContext((ctx) => ctx.saveAttachments([updated]));
2718
+ }
2719
+ catch (error) {
2720
+ this.logger.warn(`Error during sync for ${attachment.id}`, error);
2695
2721
  }
2696
2722
  }
2697
- await context.saveAttachments(updatedAttachments);
2698
2723
  }
2699
2724
  /**
2700
2725
  * Uploads an attachment from local storage to remote storage.
@@ -2865,6 +2890,19 @@ class AttachmentQueue {
2865
2890
  statusListenerDispose;
2866
2891
  watchActiveAttachments;
2867
2892
  watchAttachmentsAbortController;
2893
+ /**
2894
+ * Serializes concurrent `syncStorage()` triggers (periodic timer, watch-onDiff,
2895
+ * status-changed). Held across the whole batch, but only contended by other
2896
+ * sync triggers — foreground `saveFile` / `deleteFile` / watched-attachment
2897
+ * processing don't take this lock and proceed in parallel via the
2898
+ * `AttachmentService` mutex, which is acquired only briefly per row.
2899
+ */
2900
+ syncLoopMutex = new Mutex();
2901
+ /**
2902
+ * Aborted by `stopSync()` to interrupt an in-flight batch within one
2903
+ * attachment's processing time. Polled between rows by `SyncingService`.
2904
+ */
2905
+ syncAbortController;
2868
2906
  /**
2869
2907
  * Creates a new AttachmentQueue instance.
2870
2908
  *
@@ -2904,6 +2942,7 @@ class AttachmentQueue {
2904
2942
  */
2905
2943
  async startSync() {
2906
2944
  await this.stopSync();
2945
+ this.syncAbortController = new AbortController();
2907
2946
  this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
2908
2947
  throttleMs: this.syncThrottleDuration
2909
2948
  });
@@ -3018,23 +3057,44 @@ class AttachmentQueue {
3018
3057
  *
3019
3058
  * This is called automatically at regular intervals when sync is started,
3020
3059
  * but can also be called manually to trigger an immediate sync.
3060
+ *
3061
+ * Concurrent invocations are serialized via `syncLoopMutex`.
3021
3062
  */
3022
3063
  async syncStorage() {
3023
- await this.attachmentService.withContext(async (ctx) => {
3024
- const activeAttachments = await ctx.getActiveAttachments();
3025
- await this.localStorage.initialize();
3026
- await this.syncingService.processAttachments(activeAttachments, ctx);
3027
- await this.syncingService.deleteArchivedAttachments(ctx);
3028
- });
3064
+ const signal = this.syncAbortController?.signal;
3065
+ if (signal?.aborted)
3066
+ return;
3067
+ try {
3068
+ await this.syncLoopMutex.runExclusive(async () => {
3069
+ const activeAttachments = await this.attachmentService.withContext((ctx) => ctx.getActiveAttachments());
3070
+ await this.localStorage.initialize();
3071
+ await this.syncingService.processAttachments(activeAttachments, { signal });
3072
+ if (signal?.aborted)
3073
+ return;
3074
+ await this.attachmentService.withContext((ctx) => this.syncingService.deleteArchivedAttachments(ctx));
3075
+ }, signal);
3076
+ }
3077
+ catch (error) {
3078
+ // A queued batch's acquire rejects when `stopSync` aborts — expected, not an error.
3079
+ if (signal?.aborted)
3080
+ return;
3081
+ throw error;
3082
+ }
3029
3083
  }
3030
3084
  /**
3031
3085
  * Stops the attachment synchronization process.
3032
3086
  *
3033
- * Clears the periodic sync timer and closes all active attachment watchers.
3087
+ * Clears the periodic sync timer, closes all active attachment watchers, and
3088
+ * aborts any in-flight `syncStorage()` call so it exits within one
3089
+ * attachment's processing time instead of running the batch to completion.
3034
3090
  */
3035
3091
  async stopSync() {
3036
3092
  clearInterval(this.periodicSyncTimer);
3037
3093
  this.periodicSyncTimer = undefined;
3094
+ if (this.syncAbortController) {
3095
+ this.syncAbortController.abort();
3096
+ this.syncAbortController = undefined;
3097
+ }
3038
3098
  if (this.watchActiveAttachments)
3039
3099
  await this.watchActiveAttachments.close();
3040
3100
  if (this.watchAttachmentsAbortController) {
@@ -12649,7 +12709,7 @@ function requireDist () {
12649
12709
 
12650
12710
  var distExports = requireDist();
12651
12711
 
12652
- var version = "1.56.0";
12712
+ var version = "1.57.0";
12653
12713
  var PACKAGE = {
12654
12714
  version: version};
12655
12715