1- import { AbstractPowerSyncDatabase , DEFAULT_WATCH_THROTTLE_MS , DifferentialWatchedQuery , ILogger , Transaction } from '@powersync/common' ;
1+ import {
2+ AbstractPowerSyncDatabase ,
3+ DEFAULT_WATCH_THROTTLE_MS ,
4+ DifferentialWatchedQuery ,
5+ ILogger ,
6+ Transaction
7+ } from '@powersync/common' ;
28import { AttachmentContext } from './AttachmentContext.js' ;
39import { AttachmentData , LocalStorageAdapter } from './LocalStorageAdapter.js' ;
410import { RemoteStorageAdapter } from './RemoteStorageAdapter.js' ;
@@ -11,55 +17,55 @@ import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
1117/**
1218 * AttachmentQueue manages the lifecycle and synchronization of attachments
1319 * between local and remote storage.
14- *
20+ *
1521 * Provides automatic synchronization, upload/download queuing, attachment monitoring,
1622 * verification and repair of local files, and cleanup of archived attachments.
1723 */
1824export class AttachmentQueue {
1925 /** Timer for periodic synchronization operations */
2026 periodicSyncTimer ?: ReturnType < typeof setInterval > ;
21-
27+
2228 /** Context for managing attachment records in the database */
2329 context : AttachmentContext ;
24-
30+
2531 /** Service for synchronizing attachments between local and remote storage */
2632 syncingService : SyncingService ;
27-
33+
2834 /** Adapter for local file storage operations */
2935 localStorage : LocalStorageAdapter ;
30-
36+
3137 /** Adapter for remote file storage operations */
3238 remoteStorage : RemoteStorageAdapter ;
33-
39+
3440 /** @deprecated Directory path for storing attachments */
3541 attachmentsDirectory ?: string ;
36-
42+
3743 /** Name of the database table storing attachment records */
3844 tableName ?: string ;
39-
45+
4046 /** Logger instance for diagnostic information */
4147 logger ?: ILogger ;
42-
48+
4349 /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
4450 syncIntervalMs : number = 30 * 1000 ;
45-
51+
4652 /** Duration in milliseconds to throttle sync operations */
4753 syncThrottleDuration : number ;
48-
54+
4955 /** Whether to automatically download remote attachments. Default: true */
5056 downloadAttachments : boolean = true ;
51-
57+
5258 /** Maximum number of archived attachments to keep before cleanup. Default: 100 */
5359 archivedCacheLimit : number ;
54-
60+
5561 /** Service for managing attachment-related database operations */
5662 attachmentService : AttachmentService ;
5763
5864 watchActiveAttachments : DifferentialWatchedQuery < AttachmentRecord > ;
5965
6066 /**
6167 * Creates a new AttachmentQueue instance.
62- *
68+ *
6369 * @param options - Configuration options
6470 * @param options.db - PowerSync database instance
6571 * @param options.remoteStorage - Remote storage adapter for upload/download operations
@@ -107,17 +113,23 @@ export class AttachmentQueue {
107113 this . downloadAttachments = downloadAttachments ;
108114 this . context = new AttachmentContext ( db , tableName , logger ?? db . logger , archivedCacheLimit ) ;
109115 this . attachmentService = new AttachmentService ( db , logger ?? db . logger , tableName ) ;
110- this . syncingService = new SyncingService ( this . context , localStorage , remoteStorage , logger ?? db . logger , errorHandler ) ;
111- this . watchActiveAttachments = this . attachmentService . watchActiveAttachments ( { throttleMs : this . syncThrottleDuration } ) ;
116+ this . syncingService = new SyncingService (
117+ this . context ,
118+ localStorage ,
119+ remoteStorage ,
120+ logger ?? db . logger ,
121+ errorHandler
122+ ) ;
123+ this . logger = logger ?? db . logger ;
112124 }
113125
114126 /**
115127 * Callback function to watch for changes in attachment references in your data model.
116- *
128+ *
117129 * This method should be implemented to monitor changes in your application's
118130 * data that reference attachments. When attachments are added, removed, or modified,
119131 * this callback should trigger the onUpdate function with the current set of attachments.
120- *
132+ *
121133 * @param onUpdate - Callback to invoke when attachment references change
122134 * @throws Error indicating this method must be implemented by the user
123135 */
@@ -127,7 +139,7 @@ export class AttachmentQueue {
127139
128140 /**
129141 * Generates a new attachment ID using a SQLite UUID function.
130- *
142+ *
131143 * @returns Promise resolving to the new attachment ID
132144 */
133145 async generateAttachmentId ( ) : Promise < string > {
@@ -136,7 +148,7 @@ export class AttachmentQueue {
136148
137149 /**
138150 * Starts the attachment synchronization process.
139- *
151+ *
140152 * This method:
141153 * - Stops any existing sync operations
142154 * - Sets up periodic synchronization based on syncIntervalMs
@@ -145,11 +157,11 @@ export class AttachmentQueue {
145157 * - Handles state transitions for archived and new attachments
146158 */
147159 async startSync ( ) : Promise < void > {
148- if ( this . attachmentService . watchActiveAttachments ) {
149- await this . stopSync ( ) ;
150- // re-create the watch after it was stopped
151- this . watchActiveAttachments = this . attachmentService . watchActiveAttachments ( { throttleMs : this . syncThrottleDuration } ) ;
152- }
160+ await this . stopSync ( ) ;
161+
162+ this . watchActiveAttachments = this . attachmentService . watchActiveAttachments ( {
163+ throttleMs : this . syncThrottleDuration
164+ } ) ;
153165
154166 // immediately invoke the sync storage to initialize local storage
155167 await this . localStorage . initialize ( ) ;
@@ -251,7 +263,7 @@ export class AttachmentQueue {
251263
252264 /**
253265 * Synchronizes all active attachments between local and remote storage.
254- *
266+ *
255267 * This is called automatically at regular intervals when sync is started,
256268 * but can also be called manually to trigger an immediate sync.
257269 */
@@ -264,18 +276,18 @@ export class AttachmentQueue {
264276
265277 /**
266278 * Stops the attachment synchronization process.
267- *
279+ *
268280 * Clears the periodic sync timer and closes all active attachment watchers.
269281 */
270282 async stopSync ( ) : Promise < void > {
271283 clearInterval ( this . periodicSyncTimer ) ;
272284 this . periodicSyncTimer = undefined ;
273- await this . watchActiveAttachments . close ( ) ;
285+ if ( this . watchActiveAttachments ) await this . watchActiveAttachments . close ( ) ;
274286 }
275287
276288 /**
277289 * Saves a file to local storage and queues it for upload to remote storage.
278- *
290+ *
279291 * @param options - File save options
280292 * @param options.data - The file data as ArrayBuffer, Blob, or base64 string
281293 * @param options.fileExtension - File extension (e.g., 'jpg', 'pdf')
@@ -294,15 +306,14 @@ export class AttachmentQueue {
294306 id,
295307 updateHook
296308 } : {
297- // TODO: create a dedicated type for data
298309 data : AttachmentData ;
299310 fileExtension : string ;
300311 mediaType ?: string ;
301312 metaData ?: string ;
302313 id ?: string ;
303314 updateHook ?: ( transaction : Transaction , attachment : AttachmentRecord ) => Promise < void > ;
304315 } ) : Promise < AttachmentRecord > {
305- const resolvedId = id ?? await this . generateAttachmentId ( ) ;
316+ const resolvedId = id ?? ( await this . generateAttachmentId ( ) ) ;
306317 const filename = `${ resolvedId } .${ fileExtension } ` ;
307318 const localUri = this . localStorage . getLocalUri ( filename ) ;
308319 const size = await this . localStorage . saveFile ( localUri , data ) ;
@@ -327,9 +338,12 @@ export class AttachmentQueue {
327338 return attachment ;
328339 }
329340
330- async deleteFile ( { id, updateHook } : {
331- id : string ,
332- updateHook ?: ( transaction : Transaction , attachment : AttachmentRecord ) => Promise < void >
341+ async deleteFile ( {
342+ id,
343+ updateHook
344+ } : {
345+ id : string ;
346+ updateHook ?: ( transaction : Transaction , attachment : AttachmentRecord ) => Promise < void > ;
333347 } ) : Promise < void > {
334348 const attachment = await this . context . getAttachment ( id ) ;
335349 if ( ! attachment ) {
@@ -338,11 +352,14 @@ export class AttachmentQueue {
338352
339353 await this . context . db . writeTransaction ( async ( tx ) => {
340354 await updateHook ?.( tx , attachment ) ;
341- await this . context . upsertAttachment ( {
342- ...attachment ,
343- state : AttachmentState . QUEUED_DELETE ,
344- hasSynced : false ,
345- } , tx ) ;
355+ await this . context . upsertAttachment (
356+ {
357+ ...attachment ,
358+ state : AttachmentState . QUEUED_DELETE ,
359+ hasSynced : false
360+ } ,
361+ tx
362+ ) ;
346363 } ) ;
347364 }
348365
@@ -360,7 +377,7 @@ export class AttachmentQueue {
360377
361378 /**
362379 * Verifies the integrity of all attachment records and repairs inconsistencies.
363- *
380+ *
364381 * This method checks each attachment record against the local filesystem and:
365382 * - Updates localUri if the file exists at a different path
366383 * - Archives attachments with missing local files that haven't been uploaded
@@ -369,12 +386,12 @@ export class AttachmentQueue {
369386 verifyAttachments = async ( ) : Promise < void > => {
370387 const attachments = await this . context . getAttachments ( ) ;
371388 const updates : AttachmentRecord [ ] = [ ] ;
372-
389+
373390 for ( const attachment of attachments ) {
374391 if ( attachment . localUri == null ) {
375392 continue ;
376393 }
377-
394+
378395 const exists = await this . localStorage . fileExists ( attachment . localUri ) ;
379396 if ( exists ) {
380397 // The file exists, this is correct
0 commit comments