1- import { AbstractPowerSyncDatabase , DifferentialWatchedQuery , ILogger , Transaction } from '@powersync/common' ;
1+ import { AbstractPowerSyncDatabase , DEFAULT_WATCH_THROTTLE_MS , DifferentialWatchedQuery , ILogger , Transaction } from '@powersync/common' ;
22import { AttachmentContext } from './AttachmentContext.js' ;
33import { AttachmentData , LocalStorageAdapter } from './LocalStorageAdapter.js' ;
44import { 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
0 commit comments