Skip to content

Commit 9ac685c

Browse files
committed
Improve comments
1 parent 9509aeb commit 9ac685c

File tree

9 files changed

+240
-45
lines changed

9 files changed

+240
-45
lines changed

packages/attachments/src/AttachmentContext.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
11
import { AbstractPowerSyncDatabase, ILogger, Transaction } from '@powersync/common';
22
import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js';
33

4+
/**
5+
* AttachmentContext provides database operations for managing attachment records.
6+
*
7+
* Provides methods to query, insert, update, and delete attachment records with
8+
* proper transaction management through PowerSync.
9+
*/
410
export class AttachmentContext {
11+
/** PowerSync database instance for executing queries */
512
db: AbstractPowerSyncDatabase;
13+
14+
/** Name of the database table storing attachment records */
615
tableName: string;
16+
17+
/** Logger instance for diagnostic information */
718
logger: ILogger;
819

20+
/**
21+
* Creates a new AttachmentContext instance.
22+
*
23+
* @param db - PowerSync database instance
24+
* @param tableName - Name of the table storing attachment records. Default: 'attachments'
25+
* @param logger - Logger instance for diagnostic output
26+
*/
927
constructor(db: AbstractPowerSyncDatabase, tableName: string = 'attachments', logger: ILogger) {
1028
this.db = db;
1129
this.tableName = tableName;
1230
this.logger = logger;
1331
}
1432

15-
async getActiveAttachments(): Promise<any[]> {
33+
/**
34+
* Retrieves all active attachments that require synchronization.
35+
* Active attachments include those queued for upload, download, or delete.
36+
* Results are ordered by timestamp in ascending order.
37+
*
38+
* @returns Promise resolving to an array of active attachment records
39+
*/
40+
async getActiveAttachments(): Promise<AttachmentRecord[]> {
1641
const attachments = await this.db.getAll(
1742
/* sql */
1843
`
@@ -33,6 +58,14 @@ export class AttachmentContext {
3358
return attachments.map(attachmentFromSql);
3459
}
3560

61+
/**
62+
* Retrieves all archived attachments.
63+
*
64+
* Archived attachments are no longer referenced but haven't been permanently deleted.
65+
* These are candidates for cleanup operations to free up storage space.
66+
*
67+
* @returns Promise resolving to an array of archived attachment records
68+
*/
3669
async getArchivedAttachments(): Promise<AttachmentRecord[]> {
3770
const attachments = await this.db.getAll(
3871
/* sql */
@@ -52,6 +85,12 @@ export class AttachmentContext {
5285
return attachments.map(attachmentFromSql);
5386
}
5487

88+
/**
89+
* Retrieves all attachment records regardless of state.
90+
* Results are ordered by timestamp in ascending order.
91+
*
92+
* @returns Promise resolving to an array of all attachment records
93+
*/
5594
async getAttachments(): Promise<AttachmentRecord[]> {
5695
const attachments = await this.db.getAll(
5796
/* sql */
@@ -69,6 +108,15 @@ export class AttachmentContext {
69108
return attachments.map(attachmentFromSql);
70109
}
71110

111+
/**
112+
* Inserts or updates an attachment record within an existing transaction.
113+
*
114+
* Performs an upsert operation (INSERT OR REPLACE). Must be called within
115+
* an active database transaction context.
116+
*
117+
* @param attachment - The attachment record to upsert
118+
* @param context - Active database transaction context
119+
*/
72120
upsertAttachment(attachment: AttachmentRecord, context: Transaction): void {
73121
context.execute(
74122
/* sql */
@@ -102,6 +150,15 @@ export class AttachmentContext {
102150
);
103151
}
104152

153+
/**
154+
* Permanently deletes an attachment record from the database.
155+
*
156+
* This operation removes the attachment record but does not delete
157+
* the associated local or remote files. File deletion should be handled
158+
* separately through the appropriate storage adapters.
159+
*
160+
* @param attachmentId - Unique identifier of the attachment to delete
161+
*/
105162
async deleteAttachment(attachmentId: string): Promise<void> {
106163
await this.db.writeTransaction((tx) =>
107164
tx.execute(
@@ -116,6 +173,14 @@ export class AttachmentContext {
116173
);
117174
}
118175

176+
/**
177+
* Saves multiple attachment records in a single transaction.
178+
*
179+
* All updates are saved in a single batch after processing.
180+
* If the attachments array is empty, no database operations are performed.
181+
*
182+
* @param attachments - Array of attachment records to save
183+
*/
119184
async saveAttachments(attachments: AttachmentRecord[]): Promise<void> {
120185
if (attachments.length === 0) {
121186
return;

packages/attachments/src/AttachmentQueue.ts

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AbstractPowerSyncDatabase, ILogger } from '@powersync/common';
1+
import { AbstractPowerSyncDatabase, ILogger, Transaction } from '@powersync/common';
22
import { AttachmentContext } from './AttachmentContext.js';
33
import { LocalStorageAdapter } from './LocalStorageAdapter.js';
44
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
@@ -7,20 +7,68 @@ import { SyncingService } from './SyncingService.js';
77
import { WatchedAttachmentItem } from './WatchedAttachmentItem.js';
88
import { AttachmentService } from './AttachmentService.js';
99

10+
/**
11+
* AttachmentQueue manages the lifecycle and synchronization of attachments
12+
* between local and remote storage.
13+
*
14+
* Provides automatic synchronization, upload/download queuing, attachment monitoring,
15+
* verification and repair of local files, and cleanup of archived attachments.
16+
*/
1017
export class AttachmentQueue {
18+
/** Timer for periodic synchronization operations */
1119
periodicSyncTimer?: ReturnType<typeof setInterval>;
20+
21+
/** Context for managing attachment records in the database */
1222
context: AttachmentContext;
23+
24+
/** Service for synchronizing attachments between local and remote storage */
1325
syncingService: SyncingService;
26+
27+
/** Adapter for local file storage operations */
1428
localStorage: LocalStorageAdapter;
29+
30+
/** Adapter for remote file storage operations */
1531
remoteStorage: RemoteStorageAdapter;
32+
33+
/** @deprecated Directory path for storing attachments */
1634
attachmentsDirectory?: string;
35+
36+
/** Name of the database table storing attachment records */
1737
tableName?: string;
38+
39+
/** Logger instance for diagnostic information */
1840
logger?: ILogger;
41+
42+
/** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
43+
syncIntervalMs: number = 30 * 1000;
44+
45+
/** Duration in milliseconds to throttle sync operations */
1946
syncThrottleDuration: number;
47+
48+
/** Whether to automatically download remote attachments. Default: true */
2049
downloadAttachments: boolean = true;
50+
51+
/** Maximum number of archived attachments to keep before cleanup. Default: 100 */
2152
archivedCacheLimit: number;
53+
54+
/** Service for managing attachment-related database operations */
2255
attachmentService: AttachmentService;
2356

57+
/**
58+
* Creates a new AttachmentQueue instance.
59+
*
60+
* @param options - Configuration options
61+
* @param options.db - PowerSync database instance
62+
* @param options.remoteStorage - Remote storage adapter for upload/download operations
63+
* @param options.localStorage - Local storage adapter for file persistence
64+
* @param options.watchAttachments - Callback for monitoring attachment changes in your data model
65+
* @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
66+
* @param options.logger - Logger instance. Defaults to db.logger
67+
* @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000
68+
* @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
69+
* @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
70+
* @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
71+
*/
2472
constructor({
2573
db,
2674
localStorage,
@@ -57,8 +105,30 @@ export class AttachmentQueue {
57105
this.archivedCacheLimit = archivedCacheLimit;
58106
}
59107

108+
/**
109+
* Callback function to watch for changes in attachment references in your data model.
110+
*
111+
* This method should be implemented to monitor changes in your application's
112+
* data that reference attachments. When attachments are added, removed, or modified,
113+
* this callback should trigger the onUpdate function with the current set of attachments.
114+
*
115+
* @param onUpdate - Callback to invoke when attachment references change
116+
* @throws Error indicating this method must be implemented by the user
117+
*/
118+
watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => Promise<void>): void {
119+
throw new Error('watchAttachments should be implemented by the user of AttachmentQueue');
60120
}
61121

122+
/**
123+
* Starts the attachment synchronization process.
124+
*
125+
* This method:
126+
* - Stops any existing sync operations
127+
* - Sets up periodic synchronization based on syncIntervalMs
128+
* - Registers listeners for active attachment changes
129+
* - Processes watched attachments to queue uploads/downloads
130+
* - Handles state transitions for archived and new attachments
131+
*/
62132
async startSync(): Promise<void> {
63133
await this.stopSync();
64134

@@ -156,20 +226,43 @@ export class AttachmentQueue {
156226
});
157227
}
158228

159-
// Sync storage with all active attachments
229+
/**
230+
* Synchronizes all active attachments between local and remote storage.
231+
*
232+
* This is called automatically at regular intervals when sync is started,
233+
* but can also be called manually to trigger an immediate sync.
234+
*/
160235
async syncStorage(): Promise<void> {
161236
const activeAttachments = await this.context.getActiveAttachments();
162237
await this.localStorage.initialize();
163238
await this.syncingService.processAttachments(activeAttachments);
164239
await this.syncingService.deleteArchivedAttachments();
165240
}
166241

242+
/**
243+
* Stops the attachment synchronization process.
244+
*
245+
* Clears the periodic sync timer and closes all active attachment watchers.
246+
*/
167247
async stopSync(): Promise<void> {
168248
clearInterval(this.periodicSyncTimer);
169249
this.periodicSyncTimer = undefined;
170250
await this.attachmentService.watchActiveAttachments().close();
171251
}
172252

253+
/**
254+
* Saves a file to local storage and queues it for upload to remote storage.
255+
*
256+
* @param options - File save options
257+
* @param options.data - The file data as ArrayBuffer, Blob, or base64 string
258+
* @param options.fileExtension - File extension (e.g., 'jpg', 'pdf')
259+
* @param options.mediaType - MIME type of the file (e.g., 'image/jpeg')
260+
* @param options.metaData - Optional metadata to associate with the attachment
261+
* @param options.id - Optional custom ID. If not provided, a UUID will be generated
262+
* @param options.updateHook - Optional callback to execute additional database operations
263+
* within the same transaction as the attachment creation
264+
* @returns Promise resolving to the created attachment record
265+
*/
173266
async saveFile({
174267
data,
175268
fileExtension,
@@ -178,6 +271,7 @@ export class AttachmentQueue {
178271
id,
179272
updateHook
180273
}: {
274+
// TODO: create a dedicated type for data
181275
data: ArrayBuffer | Blob | string;
182276
fileExtension: string;
183277
mediaType?: string;
@@ -210,6 +304,14 @@ export class AttachmentQueue {
210304
return attachment;
211305
}
212306

307+
/**
308+
* Verifies the integrity of all attachment records and repairs inconsistencies.
309+
*
310+
* This method checks each attachment record against the local filesystem and:
311+
* - Updates localUri if the file exists at a different path
312+
* - Archives attachments with missing local files that haven't been uploaded
313+
* - Requeues synced attachments for download if their local files are missing
314+
*/
213315
verifyAttachments = async (): Promise<void> => {
214316
const attachments = await this.context.getAttachments();
215317
const updates: AttachmentRecord[] = [];

packages/attachments/src/LocalStorageAdapter.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@ export enum EncodingType {
33
Base64 = 'base64'
44
}
55

6+
/**
7+
* LocalStorageAdapter defines the interface for local file storage operations.
8+
* Implementations handle file I/O, directory management, and storage initialization.
9+
*/
610
export interface LocalStorageAdapter {
711
/**
8-
* Saves buffer data to a local file.
12+
* Saves data to a local file.
913
* @param filePath Path where the file will be stored
10-
* @param data Data string to store
14+
* @param data Data to store (ArrayBuffer, Blob, or string)
1115
* @returns Number of bytes written
1216
*/
1317
saveFile(filePath: string, data: ArrayBuffer | Blob | string): Promise<number>;
1418

1519
/**
16-
* Retrieves an ArrayBuffer with the file data from the given path.
20+
* Retrieves file data as an ArrayBuffer.
1721
* @param filePath Path where the file is stored
18-
* @returns ArrayBuffer with the file data
22+
* @returns ArrayBuffer containing the file data
1923
*/
2024
readFile(filePath: string): Promise<ArrayBuffer>;
2125

@@ -35,14 +39,12 @@ export interface LocalStorageAdapter {
3539
/**
3640
* Creates a directory at the specified path.
3741
* @param path The full path to the directory
38-
* @throws PowerSyncAttachmentError if creation fails
3942
*/
4043
makeDir(path: string): Promise<void>;
4144

4245
/**
4346
* Removes a directory at the specified path.
4447
* @param path The full path to the directory
45-
* @throws PowerSyncAttachmentError if removal fails
4648
*/
4749
rmDir(path: string): Promise<void>;
4850

@@ -57,7 +59,7 @@ export interface LocalStorageAdapter {
5759
clear(): Promise<void>;
5860

5961
/**
60-
* Returns the file path of the provided filename in the user storage directory.
62+
* Returns the file path for the provided filename in the storage directory.
6163
* @param filename The filename to get the path for
6264
* @returns The full file path
6365
*/
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
import { AttachmentRecord } from "./Schema.js";
22

3+
/**
4+
* RemoteStorageAdapter defines the interface for remote storage operations.
5+
* Implementations handle uploading, downloading, and deleting files from remote storage.
6+
*/
37
export interface RemoteStorageAdapter {
48
/**
59
* Uploads a file to remote storage.
6-
*
7-
* @param fileData The binary content of the file to upload.
8-
* @param attachment The associated `Attachment` metadata describing the file.
9-
* @throws An error if the upload fails.
10+
* @param fileData The binary content of the file to upload
11+
* @param attachment The associated attachment metadata
1012
*/
1113
uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void>;
14+
1215
/**
1316
* Downloads a file from remote storage.
14-
*
15-
* @param attachment The `Attachment` describing the file to download.
16-
* @returns The binary data of the downloaded file.
17-
* @throws An error if the download fails or the file is not found.
17+
* @param attachment The attachment describing the file to download
18+
* @returns The binary data of the downloaded file
1819
*/
19-
downloadFile(attachment: AttachmentRecord): Promise<Blob>;
20+
downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer>;
21+
2022
/**
2123
* Deletes a file from remote storage.
22-
*
23-
* @param attachment The `Attachment` describing the file to delete.
24-
* @throws An error if the deletion fails or the file does not exist.
24+
* @param attachment The attachment describing the file to delete
2525
*/
2626
deleteFile(attachment: AttachmentRecord): Promise<void>;
2727
}

0 commit comments

Comments
 (0)