Skip to content

Commit fae7f27

Browse files
author
Amine
committed
test: workflow tests working and passing
- Added `getAttachment` method to retrieve an attachment by ID in AttachmentContext. - Updated `upsertAttachment` to handle null values for optional fields. - Introduced `generateAttachmentId` method in AttachmentQueue for generating unique IDs. - Modified `watchActiveAttachments` to accept a throttle parameter. - Added `deleteFile` method to have attachment deletion. - Updated tests to cover new functionality and ensure reliability.
1 parent ff25b2b commit fae7f27

File tree

4 files changed

+370
-113
lines changed

4 files changed

+370
-113
lines changed

packages/attachments/src/AttachmentContext.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class AttachmentContext {
119119
*/
120120
async upsertAttachment(attachment: AttachmentRecord, context: Transaction): Promise<void> {
121121
try {
122-
const result = await context.execute(
122+
await context.execute(
123123
/* sql */
124124
`
125125
INSERT
@@ -140,20 +140,34 @@ export class AttachmentContext {
140140
[
141141
attachment.id,
142142
attachment.filename,
143-
attachment.localUri || 'dummy',
144-
attachment.size || 1,
145-
attachment.mediaType || 'dummy',
146-
attachment.timestamp || Date.now(),
143+
attachment.localUri || null,
144+
attachment.size || null,
145+
attachment.mediaType || null,
146+
attachment.timestamp,
147147
attachment.state,
148148
attachment.hasSynced ? 1 : 0,
149-
attachment.metaData || 'dummy'
149+
attachment.metaData || null
150150
]
151151
);
152152
} catch (error) {
153153
throw error;
154154
}
155155
}
156156

157+
async getAttachment(id: string): Promise<AttachmentRecord | undefined> {
158+
const attachment = await this.db.get(
159+
/* sql */
160+
`
161+
SELECT * FROM ${this.tableName}
162+
WHERE
163+
id = ?
164+
`,
165+
[id]
166+
);
167+
168+
return attachment ? attachmentFromSql(attachment) : undefined;
169+
}
170+
157171
/**
158172
* Permanently deletes an attachment record from the database.
159173
*
@@ -191,7 +205,7 @@ export class AttachmentContext {
191205
}
192206
await this.db.writeTransaction(async (tx) => {
193207
for (const attachment of attachments) {
194-
this.upsertAttachment(attachment, tx);
208+
await this.upsertAttachment(attachment, tx);
195209
}
196210
});
197211
}

packages/attachments/src/AttachmentQueue.ts

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AbstractPowerSyncDatabase, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common';
1+
import { AbstractPowerSyncDatabase, DEFAULT_WATCH_THROTTLE_MS, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common';
22
import { AttachmentContext } from './AttachmentContext.js';
33
import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js';
44
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
@@ -79,14 +79,14 @@ export class AttachmentQueue {
7979
logger,
8080
tableName = ATTACHMENT_TABLE,
8181
syncIntervalMs = 30 * 1000,
82-
syncThrottleDuration = 1000,
82+
syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS,
8383
downloadAttachments = true,
8484
archivedCacheLimit = 100
8585
}: {
8686
db: AbstractPowerSyncDatabase;
8787
remoteStorage: RemoteStorageAdapter;
8888
localStorage: LocalStorageAdapter;
89-
watchAttachments: (onUpdate: (attachement: WatchedAttachmentItem[]) => Promise<void>) => void;
89+
watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>) => void;
9090
tableName?: string;
9191
logger?: ILogger;
9292
syncIntervalMs?: number;
@@ -101,9 +101,9 @@ export class AttachmentQueue {
101101
this.tableName = tableName;
102102
this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger);
103103
this.attachmentService = new AttachmentService(tableName, db);
104-
this.watchActiveAttachments = this.attachmentService.watchActiveAttachments();
105104
this.syncIntervalMs = syncIntervalMs;
106105
this.syncThrottleDuration = syncThrottleDuration;
106+
this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({ throttleMs: this.syncThrottleDuration });
107107
this.downloadAttachments = downloadAttachments;
108108
this.archivedCacheLimit = archivedCacheLimit;
109109
}
@@ -118,10 +118,19 @@ export class AttachmentQueue {
118118
* @param onUpdate - Callback to invoke when attachment references change
119119
* @throws Error indicating this method must be implemented by the user
120120
*/
121-
watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => Promise<void>): void {
121+
watchAttachments(onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>): void {
122122
throw new Error('watchAttachments should be implemented by the user of AttachmentQueue');
123123
}
124124

125+
/**
126+
* Generates a new attachment ID using a SQLite UUID function.
127+
*
128+
* @returns Promise resolving to the new attachment ID
129+
*/
130+
async generateAttachmentId(): Promise<string> {
131+
return (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id;
132+
}
133+
125134
/**
126135
* Starts the attachment synchronization process.
127136
*
@@ -136,9 +145,14 @@ export class AttachmentQueue {
136145
if (this.attachmentService.watchActiveAttachments) {
137146
await this.stopSync();
138147
// re-create the watch after it was stopped
139-
this.watchActiveAttachments = this.attachmentService.watchActiveAttachments();
148+
this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({ throttleMs: this.syncThrottleDuration });
140149
}
141150

151+
// immediately invoke the sync storage to initialize local storage
152+
await this.localStorage.initialize();
153+
154+
await this.verifyAttachments();
155+
142156
// Sync storage periodically
143157
this.periodicSyncTimer = setInterval(async () => {
144158
await this.syncStorage();
@@ -162,7 +176,6 @@ export class AttachmentQueue {
162176
const existingQueueItem = currentAttachments.find((a) => a.id === watchedAttachment.id);
163177
if (!existingQueueItem) {
164178
// Item is watched but not in the queue yet. Need to add it.
165-
166179
if (!this.downloadAttachments) {
167180
continue;
168181
}
@@ -284,9 +297,9 @@ export class AttachmentQueue {
284297
mediaType?: string;
285298
metaData?: string;
286299
id?: string;
287-
updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => void;
300+
updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise<void>;
288301
}): Promise<AttachmentRecord> {
289-
const resolvedId = id ?? (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id;
302+
const resolvedId = id ?? await this.generateAttachmentId();
290303
const filename = `${resolvedId}.${fileExtension}`;
291304
const localUri = this.localStorage.getLocalUri(filename);
292305
const size = await this.localStorage.saveFile(localUri, data);
@@ -304,13 +317,32 @@ export class AttachmentQueue {
304317
};
305318

306319
await this.context.db.writeTransaction(async (tx) => {
307-
updateHook?.(tx, attachment);
308-
this.context.upsertAttachment(attachment, tx);
320+
await updateHook?.(tx, attachment);
321+
await this.context.upsertAttachment(attachment, tx);
309322
});
310323

311324
return attachment;
312325
}
313326

327+
async deleteFile({ id, updateHook }: {
328+
id: string,
329+
updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise<void>
330+
}): Promise<void> {
331+
const attachment = await this.context.getAttachment(id);
332+
if (!attachment) {
333+
throw new Error(`Attachment with id ${id} not found`);
334+
}
335+
336+
await this.context.db.writeTransaction(async (tx) => {
337+
await updateHook?.(tx, attachment);
338+
await this.context.upsertAttachment({
339+
...attachment,
340+
state: AttachmentState.QUEUED_DELETE,
341+
hasSynced: false,
342+
}, tx);
343+
});
344+
}
345+
314346
/**
315347
* Verifies the integrity of all attachment records and repairs inconsistencies.
316348
*
@@ -322,12 +354,12 @@ export class AttachmentQueue {
322354
verifyAttachments = async (): Promise<void> => {
323355
const attachments = await this.context.getAttachments();
324356
const updates: AttachmentRecord[] = [];
325-
357+
326358
for (const attachment of attachments) {
327359
if (attachment.localUri == null) {
328360
continue;
329361
}
330-
362+
331363
const exists = await this.localStorage.fileExists(attachment.localUri);
332364
if (exists) {
333365
// The file exists, this is correct
@@ -342,19 +374,16 @@ export class AttachmentQueue {
342374
...attachment,
343375
localUri: newLocalUri
344376
});
345-
} else if (attachment.state === AttachmentState.QUEUED_UPLOAD || attachment.state === AttachmentState.ARCHIVED) {
346-
// The file must have been removed from the local storage before upload was completed
347-
updates.push({
348-
...attachment,
349-
state: AttachmentState.ARCHIVED,
350-
localUri: undefined // Clears the value
351-
});
352-
} else if (attachment.state === AttachmentState.SYNCED) {
353-
// The file was downloaded, but removed - trigger redownload
354-
updates.push({
355-
...attachment,
356-
state: AttachmentState.QUEUED_DOWNLOAD
357-
});
377+
} else {
378+
// no new exists
379+
if (attachment.state === AttachmentState.QUEUED_UPLOAD || attachment.state === AttachmentState.SYNCED) {
380+
// The file must have been removed from the local storage before upload was completed
381+
updates.push({
382+
...attachment,
383+
state: AttachmentState.ARCHIVED,
384+
localUri: undefined // Clears the value
385+
});
386+
}
358387
}
359388
}
360389

packages/attachments/src/AttachmentService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AbstractPowerSyncDatabase, DifferentialWatchedQuery } from '@powersync/common';
1+
import { AbstractPowerSyncDatabase, DEFAULT_WATCH_THROTTLE_MS, DifferentialWatchedQuery } from '@powersync/common';
22
import { AttachmentRecord, AttachmentState } from './Schema.js';
33

44
/**
@@ -14,7 +14,7 @@ export class AttachmentService {
1414
* Creates a differential watch query for active attachments requiring synchronization.
1515
* @returns Watch query that emits changes for queued uploads, downloads, and deletes
1616
*/
17-
watchActiveAttachments(): DifferentialWatchedQuery<AttachmentRecord> {
17+
watchActiveAttachments({ throttleMs }: { throttleMs?: number } = {}): DifferentialWatchedQuery<AttachmentRecord> {
1818
const watch = this.db
1919
.query<AttachmentRecord>({
2020
sql: /* sql */ `
@@ -31,7 +31,7 @@ export class AttachmentService {
3131
`,
3232
parameters: [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE]
3333
})
34-
.differentialWatch();
34+
.differentialWatch({ throttleMs });
3535

3636
return watch;
3737
}

0 commit comments

Comments
 (0)