@journeyapps/powersync-attachments v2.0.3
@journeyapps/powersync-attachments
A PowerSync library to manage attachments in React Native and JavaScript/TypeScript apps.
Installation
yarn
yarn add @journeyapps/powersync-attachmentspnpm
pnpm add @journeyapps/powersync-attachmentsnpm
npm install @journeyapps/powersync-attachmentsUsage
The AttachmentQueue class is used to manage and sync attachments in your app.
Example
In this example, the user captures photos when checklist items are completed as part of an inspection workflow.
The schema for the checklist table:
const AppSchema = new Schema([
new Table({
name: 'checklists',
columns: [
new Column({ name: 'photo_id', type: ColumnType.TEXT }),
new Column({ name: 'description', type: ColumnType.TEXT }),
new Column({ name: 'completed', type: ColumnType.INTEGER }),
new Column({ name: 'completed_at', type: ColumnType.TEXT }),
new Column({ name: 'completed_by', type: ColumnType.TEXT })
],
indexes: [
new Index({
name: 'inspections',
columns: [new IndexedColumn({ name: 'checklist_id' })]
})
]
})
]);Steps to implement
- Create a new class
AttachmentQueuethat extendsAbstractAttachmentQueuefrom@journeyapps/powersync-attachments.
import { AbstractAttachmentQueue } from '@journeyapps/powersync-attachments';
export class AttachmentQueue extends AbstractAttachmentQueue {}Implement
onAttachmentIdsChange, which takes in a callback to handle an array ofstringvalues of IDs that relate to attachments in your app. We recommend usingPowerSync'swatchquery to return the all IDs of attachments in your app.In this example, we query all photos that have been captured as part of an inspection and map these to an array of
stringvalues.
import { AbstractAttachmentQueue } from '@journeyapps/powersync-attachments';
export class AttachmentQueue extends AbstractAttachmentQueue {
onAttachmentIdsChange(onUpdate) {
this.powersync.watch('SELECT photo_id as id FROM checklists WHERE photo_id IS NOT NULL', [], {
onResult: (result) => onUpdate(result.rows?._array.map((r) => r.id) ?? [])
});
}
}Implement
newAttachmentRecordto return an object that represents the attachment record in your app.In this example we always work with
JPEGimages, but you can use any media type that is supported by your app and storage solution. Note: we are set the state toQUEUED_UPLOADwhen creating a new photo record which assumes that the photo data is already on the device.
import { AbstractAttachmentQueue } from '@journeyapps/powersync-attachments';
export class AttachmentQueue extends AbstractAttachmentQueue {
// ...
async newAttachmentRecord(record) {
const photoId = record?.id ?? uuid();
const filename = record?.filename ?? `${photoId}.jpg`;
return {
id: photoId,
filename,
media_type: 'image/jpeg',
state: AttachmentState.QUEUED_UPLOAD,
...record
};
}
}- Add an
AttachmentTableto your app's PowerSync Schema:
import { AttachmentTable } from '@journeyapps/powersync-attachments';
const AppSchema = new Schema([
// ... other tables
new AttachmentTable()
]);In addition to Table options, the AttachmentTable can optionally be configured with the following options:
| Option | Description | Default |
|---|---|---|
name | The name of the table | attachments |
additionalColumns | An array of addition Column objects added to the default columns in the table | See below for default columns |
The default columns in AttachmentTable:
| Column Name | Type | Description |
|---|---|---|
id | TEXT | The ID of the attachment record |
filename | TEXT | The filename of the attachment |
media_type | TEXT | The media type of the attachment |
state | INTEGER | The state of the attachment, one of AttachmentState enum values |
timestamp | INTEGER | The timestamp of last update to the attachment record |
size | INTEGER | The size of the attachment in bytes |
To instantiate an
AttachmentQueue, one needs to provide an instance ofAbstractPowerSyncDatabasefrom PowerSync and an instance ofStorageAdapter. See theStorageAdapterinterface definition here.Instantiate a new
AttachmentQueueand callinit()to start syncing attachments. Our example, uses aStorageAdapterthat integrates with Supabase Storage.
this.storage = this.supabaseConnector.storage;
this.powersync = factory.getInstance();
this.attachmentQueue = new AttachmentQueue({
powersync: this.powersync,
storage: this.storage
});
// Initialize and connect PowerSync ...
// Then initialize the attachment queue
await this.attachmentQueue.init();Finally, to create an attachment and add it to the queue, call
saveToQueue().In our example we added a
savePhoto()method to ourAttachmentQueueclass, that does this:
export class AttachmentQueue extends AbstractAttachmentQueue {
// ...
async savePhoto(base64Data) {
const photoAttachment = await this.newAttachmentRecord();
photoAttachment.local_uri = this.getLocalFilePathSuffix(photoAttachment.filename);
const localFilePathUri = this.getLocalUri(photoAttachment.local_uri);
await this.storage.writeFile(localFilePathUri, base64Data, { encoding: 'base64' });
return this.saveToQueue(photoAttachment);
}
}Implementation details
Attachment State
The AttachmentQueue class manages attachments in your app by tracking their state.
The state of an attachment can be one of the following:
| State | Description |
|---|---|
QUEUED_SYNC | Check if the attachment needs to be uploaded or downloaded |
QUEUED_UPLOAD | The attachment has been queued for upload to the cloud storage |
QUEUED_DOWNLOAD | The attachment has been queued for download from the cloud storage |
SYNCED | The attachment has been synced |
ARCHIVED | The attachment has been orphaned, i.e. the associated record has been deleted |
Initial sync
Upon initializing the AttachmentQueue, an initial sync of attachments will take place if the performInitialSync is set to true.
Any AttachmentRecord with id in first set of IDs retrieved from the watch query will be marked as QUEUED_SYNC, and these records will be rechecked to see if they need to be uploaded or downloaded.
Syncing attachments
The AttachmentQueue sets up two watch queries on the attachments table, one for records in QUEUED_UPLOAD state and one for QUEUED_DOWNLOAD state.
In addition to watching for changes, the AttachmentQueue also triggers a sync every few seconds. This will retry any failed uploads/downloads, in particular after the app was offline.
By default, this is every 30 seconds, but can be configured by setting syncInterval in the AttachmentQueue constructor options, or disabled by setting the interval to 0.
Uploading
- An
AttachmentRecordis created or updated with a state ofQUEUED_UPLOAD. - The
AttachmentQueuepicks this up and upon successful upload to Supabase, sets the state toSYNCED. - If the upload is not successful, the record remains in
QUEUED_UPLOADstate and uploading will be retried when syncing triggers again.
Downloading
- An
AttachmentRecordis created or updated withQUEUED_DOWNLOADstate. - The watch query adds the
idinto a queue of IDs to download and triggers the download process - This checks whether the photo is already on the device and if so, skips downloading.
- If the photo is not on the device, it is downloaded from cloud storage.
- Writes file to the user's local storage.
- If this is successful, update the
AttachmentRecordstate toSYNCED. - If any of these fail, the download is retried in the next sync trigger.
Deleting attachments
When an attachment is deleted by a user action or cache expiration:
- Related
AttachmentRecordis removed from attachments table. - Local file (if exists) is deleted.
- File on cloud storage is deleted.
Expire Cache
When PowerSync removes a record, as a result of coming back online or conflict resolution for instance:
- Any associated
AttachmentRecordis orphaned. - On the next sync trigger, the
AttachmentQueuesets all records that are orphaned toARCHIVEDstate. - By default, the
AttachmentQueueonly keeps the last100attachment records and then expires the rest. - This can be configured by setting
cacheLimitin theAttachmentQueueconstructor options.
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago