@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.
- package/dist/bundle.cjs +105 -40
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +105 -41
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +105 -40
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +105 -41
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +55 -9
- package/lib/attachments/AttachmentQueue.d.ts +18 -1
- package/lib/attachments/AttachmentQueue.js +43 -7
- package/lib/attachments/AttachmentQueue.js.map +1 -1
- package/lib/attachments/Schema.d.ts +21 -2
- package/lib/attachments/Schema.js +14 -10
- package/lib/attachments/Schema.js.map +1 -1
- package/lib/attachments/SyncingService.d.ts +14 -4
- package/lib/attachments/SyncingService.js +39 -21
- package/lib/attachments/SyncingService.js.map +1 -1
- package/lib/client/triggers/TriggerManagerImpl.js +6 -1
- package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
- package/lib/db/crud/SyncStatus.d.ts +1 -1
- package/lib/db/crud/SyncStatus.js +1 -1
- package/package.json +1 -1
- package/src/attachments/AttachmentQueue.ts +46 -7
- package/src/attachments/Schema.ts +29 -20
- package/src/attachments/SyncingService.ts +46 -23
- package/src/client/triggers/TriggerManagerImpl.ts +6 -1
- package/src/db/crud/SyncStatus.ts +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js';
|
|
2
2
|
import { DEFAULT_WATCH_THROTTLE_MS } from '../client/watched/WatchedQuery.js';
|
|
3
3
|
import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js';
|
|
4
|
+
import { Mutex } from '../utils/mutex.js';
|
|
4
5
|
import { Transaction } from '../db/DBAdapter.js';
|
|
5
6
|
import { ILogger } from '../utils/Logger.js';
|
|
6
7
|
import { AttachmentContext } from './AttachmentContext.js';
|
|
@@ -135,6 +136,21 @@ export class AttachmentQueue {
|
|
|
135
136
|
|
|
136
137
|
private watchAttachmentsAbortController!: AbortController;
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Serializes concurrent `syncStorage()` triggers (periodic timer, watch-onDiff,
|
|
141
|
+
* status-changed). Held across the whole batch, but only contended by other
|
|
142
|
+
* sync triggers — foreground `saveFile` / `deleteFile` / watched-attachment
|
|
143
|
+
* processing don't take this lock and proceed in parallel via the
|
|
144
|
+
* `AttachmentService` mutex, which is acquired only briefly per row.
|
|
145
|
+
*/
|
|
146
|
+
private syncLoopMutex = new Mutex();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Aborted by `stopSync()` to interrupt an in-flight batch within one
|
|
150
|
+
* attachment's processing time. Polled between rows by `SyncingService`.
|
|
151
|
+
*/
|
|
152
|
+
private syncAbortController?: AbortController;
|
|
153
|
+
|
|
138
154
|
/**
|
|
139
155
|
* Creates a new AttachmentQueue instance.
|
|
140
156
|
*
|
|
@@ -195,6 +211,8 @@ export class AttachmentQueue {
|
|
|
195
211
|
async startSync(): Promise<void> {
|
|
196
212
|
await this.stopSync();
|
|
197
213
|
|
|
214
|
+
this.syncAbortController = new AbortController();
|
|
215
|
+
|
|
198
216
|
this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
|
|
199
217
|
throttleMs: this.syncThrottleDuration
|
|
200
218
|
});
|
|
@@ -325,24 +343,45 @@ export class AttachmentQueue {
|
|
|
325
343
|
*
|
|
326
344
|
* This is called automatically at regular intervals when sync is started,
|
|
327
345
|
* but can also be called manually to trigger an immediate sync.
|
|
346
|
+
*
|
|
347
|
+
* Concurrent invocations are serialized via `syncLoopMutex`.
|
|
328
348
|
*/
|
|
329
349
|
async syncStorage(): Promise<void> {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
await this.
|
|
335
|
-
|
|
350
|
+
const signal = this.syncAbortController?.signal;
|
|
351
|
+
if (signal?.aborted) return;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await this.syncLoopMutex.runExclusive(async () => {
|
|
355
|
+
const activeAttachments = await this.attachmentService.withContext((ctx) => ctx.getActiveAttachments());
|
|
356
|
+
await this.localStorage.initialize();
|
|
357
|
+
|
|
358
|
+
await this.syncingService.processAttachments(activeAttachments, { signal });
|
|
359
|
+
|
|
360
|
+
if (signal?.aborted) return;
|
|
361
|
+
|
|
362
|
+
await this.attachmentService.withContext((ctx) => this.syncingService.deleteArchivedAttachments(ctx));
|
|
363
|
+
}, signal);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
// A queued batch's acquire rejects when `stopSync` aborts — expected, not an error.
|
|
366
|
+
if (signal?.aborted) return;
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
336
369
|
}
|
|
337
370
|
|
|
338
371
|
/**
|
|
339
372
|
* Stops the attachment synchronization process.
|
|
340
373
|
*
|
|
341
|
-
* Clears the periodic sync timer
|
|
374
|
+
* Clears the periodic sync timer, closes all active attachment watchers, and
|
|
375
|
+
* aborts any in-flight `syncStorage()` call so it exits within one
|
|
376
|
+
* attachment's processing time instead of running the batch to completion.
|
|
342
377
|
*/
|
|
343
378
|
async stopSync(): Promise<void> {
|
|
344
379
|
clearInterval(this.periodicSyncTimer);
|
|
345
380
|
this.periodicSyncTimer = undefined;
|
|
381
|
+
if (this.syncAbortController) {
|
|
382
|
+
this.syncAbortController.abort();
|
|
383
|
+
this.syncAbortController = undefined;
|
|
384
|
+
}
|
|
346
385
|
if (this.watchActiveAttachments) await this.watchActiveAttachments.close();
|
|
347
386
|
if (this.watchAttachmentsAbortController) {
|
|
348
387
|
this.watchAttachmentsAbortController.abort();
|
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
*
|
|
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
|
|
42
|
-
*
|
|
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(
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -419,7 +419,12 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
419
419
|
// destination table consistent.
|
|
420
420
|
await this.db.writeTransaction(async (tx) => {
|
|
421
421
|
const callbackResult = await options.onChange({
|
|
422
|
-
|
|
422
|
+
execute: (query, params) => tx.execute(query, params),
|
|
423
|
+
executeBatch: (query, params) => tx.executeBatch(query, params),
|
|
424
|
+
executeRaw: (query, params) => tx.executeRaw(query, params),
|
|
425
|
+
get: (query, params) => tx.get(query, params),
|
|
426
|
+
getAll: (query, params) => tx.getAll(query, params),
|
|
427
|
+
getOptional: (query, params) => tx.getOptional(query, params),
|
|
423
428
|
destinationTable: destination,
|
|
424
429
|
withDiff: async <T>(query: string, params: any, options?: WithDiffOptions) => {
|
|
425
430
|
// Wrap the query to expose the destination table
|
|
@@ -138,7 +138,7 @@ export class SyncStatus {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* All sync streams currently being tracked in
|
|
141
|
+
* All sync streams currently being tracked in the database.
|
|
142
142
|
*
|
|
143
143
|
* This returns null when the database is currently being opened and we don't have reliable information about all
|
|
144
144
|
* included streams yet.
|