|
| 1 | +import { |
| 2 | + AttachmentQueue, |
| 3 | + AttachmentState, |
| 4 | + AttachmentTable, |
| 5 | + Schema, |
| 6 | +} from '@powersync/common' |
| 7 | +import { createTransaction } from '@tanstack/db' |
| 8 | +import { PowerSyncTransactor } from './PowerSyncTransactor' |
| 9 | + |
| 10 | +import type { |
| 11 | + AbstractPowerSyncDatabase, |
| 12 | + AttachmentData, |
| 13 | + AttachmentErrorHandler, |
| 14 | + ILogger, |
| 15 | + LocalStorageAdapter, |
| 16 | + RemoteStorageAdapter, |
| 17 | + WatchedAttachmentItem, |
| 18 | +} from '@powersync/common' |
| 19 | +import type { Collection } from '@tanstack/db' |
| 20 | + |
| 21 | +type AttachmentQueueRow = (typeof _tmpSchema)['types']['attachments'] |
| 22 | + |
| 23 | +/** |
| 24 | + * This extends the default AttachmentQueue constructor params |
| 25 | + * FIXME(powersync) we should export this type from the common SDK. |
| 26 | + */ |
| 27 | +type TanStackDBAttachmentQueueOptions = { |
| 28 | + db: AbstractPowerSyncDatabase |
| 29 | + /** |
| 30 | + * For TanStack, we want access to the synced TanStackDB collection. |
| 31 | + * In order to have the same relational data be set in a single transaction. |
| 32 | + * This also allows for joining both TanStackDB collections. |
| 33 | + */ |
| 34 | + attachmentsCollection: Collection<AttachmentQueueRow> |
| 35 | + remoteStorage: RemoteStorageAdapter |
| 36 | + localStorage: LocalStorageAdapter |
| 37 | + watchAttachments: ( |
| 38 | + onUpdate: (attachment: Array<WatchedAttachmentItem>) => Promise<void>, |
| 39 | + signal: AbortSignal, |
| 40 | + ) => void |
| 41 | + tableName?: string |
| 42 | + logger?: ILogger |
| 43 | + syncIntervalMs?: number |
| 44 | + syncThrottleDuration?: number |
| 45 | + downloadAttachments?: boolean |
| 46 | + archivedCacheLimit?: number |
| 47 | + errorHandler?: AttachmentErrorHandler |
| 48 | +} |
| 49 | + |
| 50 | +interface SaveFileTanStackOptions { |
| 51 | + data: AttachmentData |
| 52 | + fileExtension: string |
| 53 | + mediaType?: string |
| 54 | + metaData?: string |
| 55 | + id?: string |
| 56 | + /** |
| 57 | + * Note that this is called inside a synchronous TanStackDB transaction, |
| 58 | + * any mutations made to other collections will be in the same transaction. |
| 59 | + */ |
| 60 | + updateHook?: (attachment: AttachmentQueueRow) => Promise<void> |
| 61 | +} |
| 62 | + |
| 63 | +interface DeleteFileTanStackOptions { |
| 64 | + id: string |
| 65 | + updateHook?: (attachment: AttachmentQueueRow) => Promise<void> |
| 66 | +} |
| 67 | + |
| 68 | +const _tmpSchema = new Schema({ |
| 69 | + attachments: new AttachmentTable(), |
| 70 | +}) |
| 71 | + |
| 72 | +/** |
| 73 | + * A custom extension of the PowerSyncAttachmentQueue for TanStackDB. |
| 74 | + */ |
| 75 | +export class TanStackDBAttachmentQueue extends AttachmentQueue { |
| 76 | + readonly powersync: AbstractPowerSyncDatabase |
| 77 | + readonly collection: Collection<AttachmentQueueRow> |
| 78 | + |
| 79 | + constructor(params: TanStackDBAttachmentQueueOptions) { |
| 80 | + super(params) |
| 81 | + this.powersync = params.db |
| 82 | + this.collection = params.attachmentsCollection |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Saves a file to local storage and queues it for upload to remote storage. |
| 87 | + * |
| 88 | + * Exposes an `updateHook` option which is called inside a TanStackDB transaction, |
| 89 | + * relational associations with the provided attachment ID should be made in this hook. |
| 90 | + */ |
| 91 | + async saveFileTanStack({ |
| 92 | + data, |
| 93 | + fileExtension, |
| 94 | + mediaType, |
| 95 | + metaData, |
| 96 | + id, |
| 97 | + updateHook, |
| 98 | + }: SaveFileTanStackOptions): Promise<AttachmentQueueRow> { |
| 99 | + const resolvedId = id ?? (await this.generateAttachmentId()) |
| 100 | + const filename = `${resolvedId}.${fileExtension}` |
| 101 | + const localUri = this.localStorage.getLocalUri(filename) |
| 102 | + const size = await this.localStorage.saveFile(localUri, data) |
| 103 | + |
| 104 | + const attachment: AttachmentQueueRow = { |
| 105 | + id: resolvedId, |
| 106 | + filename, |
| 107 | + media_type: mediaType ?? null, |
| 108 | + local_uri: localUri, |
| 109 | + state: AttachmentState.QUEUED_UPLOAD, |
| 110 | + has_synced: 0, |
| 111 | + size, |
| 112 | + timestamp: new Date().getTime(), |
| 113 | + meta_data: metaData ?? null, |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * We use the attachmentService lock to prevent attachment queue race conditions — specifically, |
| 118 | + * it stops the watcher from treating a newly inserted attachment record as one that needs |
| 119 | + * to be downloaded. |
| 120 | + * */ |
| 121 | + await this.withAttachmentContext(async (ctx) => { |
| 122 | + const tanStackDBTransaction = createTransaction({ |
| 123 | + autoCommit: false, |
| 124 | + mutationFn: async ({ transaction }) => { |
| 125 | + await new PowerSyncTransactor({ |
| 126 | + database: ctx.db, |
| 127 | + }).applyTransaction(transaction) |
| 128 | + }, |
| 129 | + }) |
| 130 | + |
| 131 | + tanStackDBTransaction.mutate(() => { |
| 132 | + this.collection.insert(attachment) |
| 133 | + // allow the user to associate values in this transaction |
| 134 | + updateHook?.(attachment) |
| 135 | + }) |
| 136 | + |
| 137 | + await tanStackDBTransaction.commit() |
| 138 | + }) |
| 139 | + |
| 140 | + return attachment |
| 141 | + } |
| 142 | + |
| 143 | + /** |
| 144 | + * Queues a file for deletion from local and remote storage. |
| 145 | + * |
| 146 | + * Exposes an `updateHook` option which is called inside a TanStackDB transaction, |
| 147 | + * relational associations with the provided attachment ID should be cleaned up in this hook. |
| 148 | + */ |
| 149 | + async deleteFileTanStack({ |
| 150 | + id, |
| 151 | + updateHook, |
| 152 | + }: DeleteFileTanStackOptions): Promise<void> { |
| 153 | + await this.withAttachmentContext(async (ctx) => { |
| 154 | + const tanStackDBTransaction = createTransaction({ |
| 155 | + autoCommit: false, |
| 156 | + mutationFn: async ({ transaction }) => { |
| 157 | + await new PowerSyncTransactor({ |
| 158 | + database: ctx.db, |
| 159 | + }).applyTransaction(transaction) |
| 160 | + }, |
| 161 | + }) |
| 162 | + |
| 163 | + tanStackDBTransaction.mutate(() => { |
| 164 | + const attachment = this.collection.get(id) |
| 165 | + if (!attachment) { |
| 166 | + throw new Error(`Attachment with id ${id} not found`) |
| 167 | + } |
| 168 | + |
| 169 | + this.collection.update(id, (draft) => { |
| 170 | + draft.state = AttachmentState.QUEUED_DELETE |
| 171 | + draft.has_synced = 0 |
| 172 | + }) |
| 173 | + |
| 174 | + // allow the user to associate values in this transaction |
| 175 | + updateHook?.(attachment) |
| 176 | + }) |
| 177 | + |
| 178 | + await tanStackDBTransaction.commit() |
| 179 | + }) |
| 180 | + } |
| 181 | +} |
0 commit comments