@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.
@@ -1,5 +1,5 @@
1
1
  import { column } from '../db/schema/Column.js';
2
- import { Table } from '../db/schema/Table.js';
2
+ import { RowType, Table } from '../db/schema/Table.js';
3
3
  import { TableV2Options } from '../db/schema/Table.js';
4
4
 
5
5
  /**
@@ -66,30 +66,39 @@ export enum AttachmentState {
66
66
  */
67
67
  export interface AttachmentTableOptions extends Omit<TableV2Options, 'name' | 'columns'> {}
68
68
 
69
+ /**
70
+ * @alpha
71
+ */
72
+ export const ATTACHMENT_TABLE_COLUMNS = {
73
+ filename: column.text,
74
+ local_uri: column.text,
75
+ timestamp: column.integer,
76
+ size: column.integer,
77
+ media_type: column.text,
78
+ state: column.integer, // Corresponds to AttachmentState
79
+ has_synced: column.integer,
80
+ meta_data: column.text
81
+ };
82
+
69
83
  /**
70
84
  * AttachmentTable defines the schema for the attachment queue table.
71
85
  *
72
86
  * @alpha
73
87
  */
74
- export class AttachmentTable extends Table {
88
+ export class AttachmentTable extends Table<typeof ATTACHMENT_TABLE_COLUMNS> {
75
89
  constructor(options?: AttachmentTableOptions) {
76
- super(
77
- {
78
- filename: column.text,
79
- local_uri: column.text,
80
- timestamp: column.integer,
81
- size: column.integer,
82
- media_type: column.text,
83
- state: column.integer, // Corresponds to AttachmentState
84
- has_synced: column.integer,
85
- meta_data: column.text
86
- },
87
- {
88
- ...options,
89
- viewName: options?.viewName ?? ATTACHMENT_TABLE,
90
- localOnly: true,
91
- insertOnly: false
92
- }
93
- );
90
+ super(ATTACHMENT_TABLE_COLUMNS, {
91
+ ...options,
92
+ viewName: options?.viewName ?? ATTACHMENT_TABLE,
93
+ localOnly: true,
94
+ insertOnly: false
95
+ });
94
96
  }
95
97
  }
98
+
99
+ /**
100
+ * AttachmentTableRecord represents the row type of the attachment table.
101
+ *
102
+ * @alpha
103
+ */
104
+ export type AttachmentTableRecord = RowType<AttachmentTable>;
@@ -35,35 +35,58 @@ export class SyncingService {
35
35
 
36
36
  /**
37
37
  * Processes attachments based on their state (upload, download, or delete).
38
- * All updates are saved in a single batch after processing.
38
+ *
39
+ * Each attachment's I/O runs outside the attachment-service mutex, and the row's
40
+ * state transition is persisted immediately after it completes. This keeps the
41
+ * mutex available to concurrent `saveFile` / `deleteFile` / watched-attachment
42
+ * processing while a batch is in flight, and means consumer queries against the
43
+ * attachments queue see incremental progress instead of one atomic commit at the
44
+ * end of the batch.
39
45
  *
40
46
  * @param attachments - Array of attachment records to process
41
- * @param context - Attachment context for database operations
42
- * @returns Promise that resolves when all attachments have been processed and saved
47
+ * @param options - Optional controls. Pass `signal` (an `AbortSignal`) to interrupt
48
+ * the batch: it is checked between attachments and, once aborted, the
49
+ * loop exits early — letting `stopSync` stop a running batch within
50
+ * one attachment's processing time.
43
51
  */
44
- async processAttachments(attachments: AttachmentRecord[], context: AttachmentContext): Promise<void> {
45
- const updatedAttachments: AttachmentRecord[] = [];
52
+ async processAttachments(
53
+ attachments: AttachmentRecord[],
54
+ options?: {
55
+ signal?: AbortSignal;
56
+ }
57
+ ): Promise<void> {
58
+ const signal = options?.signal;
59
+ this.logger.info(`Starting processAttachments with ${attachments.length} attachments`);
60
+
46
61
  for (const attachment of attachments) {
47
- switch (attachment.state) {
48
- case AttachmentState.QUEUED_UPLOAD:
49
- const uploaded = await this.uploadAttachment(attachment);
50
- updatedAttachments.push(uploaded);
51
- break;
52
- case AttachmentState.QUEUED_DOWNLOAD:
53
- const downloaded = await this.downloadAttachment(attachment);
54
- updatedAttachments.push(downloaded);
55
- break;
56
- case AttachmentState.QUEUED_DELETE:
57
- const deleted = await this.deleteAttachment(attachment, context);
58
- updatedAttachments.push(deleted);
59
- break;
60
-
61
- default:
62
- break;
62
+ if (signal?.aborted) {
63
+ this.logger.info('Sync cancelled; stopping iteration early');
64
+ return;
63
65
  }
64
- }
65
66
 
66
- await context.saveAttachments(updatedAttachments);
67
+ try {
68
+ let updated: AttachmentRecord;
69
+ switch (attachment.state) {
70
+ case AttachmentState.QUEUED_UPLOAD:
71
+ updated = await this.uploadAttachment(attachment);
72
+ break;
73
+ case AttachmentState.QUEUED_DOWNLOAD:
74
+ updated = await this.downloadAttachment(attachment);
75
+ break;
76
+ case AttachmentState.QUEUED_DELETE:
77
+ // `deleteAttachment` needs a context (it removes the row in a
78
+ // transaction); briefly re-acquire the mutex for just this row.
79
+ updated = await this.attachmentService.withContext((ctx) => this.deleteAttachment(attachment, ctx));
80
+ break;
81
+ default:
82
+ continue;
83
+ }
84
+
85
+ await this.attachmentService.withContext((ctx) => ctx.saveAttachments([updated]));
86
+ } catch (error) {
87
+ this.logger.warn(`Error during sync for ${attachment.id}`, error);
88
+ }
89
+ }
67
90
  }
68
91
 
69
92
  /**