Skip to content

Commit eedd224

Browse files
author
Amine
committed
feat: introduce AttachmentErrorHandler for custom error handling in attachment sync operations
- Added AttachmentErrorHandler interface to manage download, upload, and delete errors for attachments. - Updated AttachmentQueue and SyncingService to utilize the new error handler. - Enhanced tests to verify error handling behavior during attachment sync processes.
1 parent fae7f27 commit eedd224

File tree

5 files changed

+48
-8
lines changed

5 files changed

+48
-8
lines changed

packages/attachments/src/SyncErrorHandler.ts renamed to packages/attachments/src/AttachmentErrorHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AttachmentRecord } from './Schema.js';
44
* SyncErrorHandler provides custom error handling for attachment sync operations.
55
* Implementations determine whether failed operations should be retried or archived.
66
*/
7-
export interface SyncErrorHandler {
7+
export interface AttachmentErrorHandler {
88
/**
99
* Handles a download error for a specific attachment.
1010
* @param attachment The attachment that failed to download

packages/attachments/src/AttachmentQueue.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js
66
import { SyncingService } from './SyncingService.js';
77
import { WatchedAttachmentItem } from './WatchedAttachmentItem.js';
88
import { AttachmentService } from './AttachmentService.js';
9+
import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
910

1011
/**
1112
* AttachmentQueue manages the lifecycle and synchronization of attachments
@@ -81,7 +82,8 @@ export class AttachmentQueue {
8182
syncIntervalMs = 30 * 1000,
8283
syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS,
8384
downloadAttachments = true,
84-
archivedCacheLimit = 100
85+
archivedCacheLimit = 100,
86+
errorHandler
8587
}: {
8688
db: AbstractPowerSyncDatabase;
8789
remoteStorage: RemoteStorageAdapter;
@@ -93,13 +95,14 @@ export class AttachmentQueue {
9395
syncThrottleDuration?: number;
9496
downloadAttachments?: boolean;
9597
archivedCacheLimit?: number;
98+
errorHandler?: AttachmentErrorHandler;
9699
}) {
97100
this.context = new AttachmentContext(db, tableName, logger ?? db.logger);
98101
this.remoteStorage = remoteStorage;
99102
this.localStorage = localStorage;
100103
this.watchAttachments = watchAttachments;
101104
this.tableName = tableName;
102-
this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger);
105+
this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger, errorHandler);
103106
this.attachmentService = new AttachmentService(tableName, db);
104107
this.syncIntervalMs = syncIntervalMs;
105108
this.syncThrottleDuration = syncThrottleDuration;

packages/attachments/src/AttachmentService.ts

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

44
/**

packages/attachments/src/SyncingService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AttachmentContext } from './AttachmentContext.js';
33
import { LocalStorageAdapter } from './LocalStorageAdapter.js';
44
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
55
import { AttachmentRecord, AttachmentState } from './Schema.js';
6-
import { SyncErrorHandler } from './SyncErrorHandler.js';
6+
import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
77

88
/**
99
* Orchestrates attachment synchronization between local and remote storage.
@@ -14,14 +14,14 @@ export class SyncingService {
1414
localStorage: LocalStorageAdapter;
1515
remoteStorage: RemoteStorageAdapter;
1616
logger: ILogger;
17-
errorHandler?: SyncErrorHandler;
17+
errorHandler?: AttachmentErrorHandler;
1818

1919
constructor(
2020
context: AttachmentContext,
2121
localStorage: LocalStorageAdapter,
2222
remoteStorage: RemoteStorageAdapter,
2323
logger: ILogger,
24-
errorHandler?: SyncErrorHandler
24+
errorHandler?: AttachmentErrorHandler
2525
) {
2626
this.context = context;
2727
this.localStorage = localStorage;

packages/attachments/tests/attachments.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { attachmentFromSql, AttachmentRecord, AttachmentState, AttachmentTable }
77
import { RemoteStorageAdapter } from '../src/RemoteStorageAdapter.js';
88
import { WatchedAttachmentItem } from '../src/WatchedAttachmentItem.js';
99
import { NodeFileSystemAdapter } from '../src/storageAdapters/NodeFileSystemAdapter.js';
10+
import { AttachmentErrorHandler } from '../src/AttachmentErrorHandler.js';
1011

1112
const MOCK_JPEG_U8A = [
1213
0xFF, 0xD8, 0xFF, 0xE0,
@@ -438,14 +439,50 @@ describe('attachment queue', () => {
438439
await queue.stopSync();
439440
})
440441

441-
it.todo('should skip failed download and retry it in the next sync', async () => {
442+
it('should skip failed download and retry it in the next sync', async () => {
443+
const mockErrorHandler = vi.fn().mockRejectedValue(false);
444+
const errorHandler: AttachmentErrorHandler = {
445+
onDeleteError: mockErrorHandler,
446+
onDownloadError: mockErrorHandler,
447+
onUploadError: mockErrorHandler,
448+
}
449+
const mockDownloadFile = vi.fn()
450+
.mockRejectedValueOnce(new Error('Download failed'))
451+
.mockResolvedValueOnce(createMockJpegBuffer());
452+
453+
const mockRemoteStorage: RemoteStorageAdapter = {
454+
downloadFile: mockDownloadFile,
455+
uploadFile: mockUploadFile,
456+
deleteFile: mockDeleteFile
457+
};
458+
442459
// no error handling yet expose error handling
443460
const localeQueue = new AttachmentQueue({
444461
db: db,
445462
watchAttachments,
446463
remoteStorage: mockRemoteStorage,
447464
localStorage: mockLocalStorage,
448465
syncIntervalMs: INTERVAL_MILLISECONDS,
466+
errorHandler,
449467
});
468+
469+
const id = await localeQueue.generateAttachmentId();
470+
471+
await localeQueue.startSync()
472+
473+
await db.execute(
474+
'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)',
475+
['testuser', 'testuser@journeyapps.com', id],
476+
);
477+
478+
await waitForMatchCondition(
479+
() => watchAttachmentsTable(),
480+
(results) => results.some((r) => r.id === id && r.state === AttachmentState.SYNCED),
481+
5
482+
);
483+
484+
expect(mockErrorHandler).toHaveBeenCalledOnce();
485+
expect(mockDownloadFile).toHaveBeenCalledTimes(2);
486+
450487
})
451488
});

0 commit comments

Comments
 (0)