Skip to content

Commit 3124c9e

Browse files
committed
Added attachments support.
1 parent 082ce15 commit 3124c9e

3 files changed

Lines changed: 197 additions & 11 deletions

File tree

packages/powersync-db-collection/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@
5959
"p-defer": "^4.0.1"
6060
},
6161
"peerDependencies": {
62-
"@powersync/common": "^1.41.0"
62+
"@powersync/common": "^1.54.0"
6363
},
6464
"devDependencies": {
65-
"@powersync/common": "1.49.0",
65+
"@powersync/common": "1.54.0",
6666
"@powersync/node": "0.18.1",
6767
"@types/debug": "^4.1.12",
6868
"@vitest/coverage-istanbul": "^3.2.4",
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
}

pnpm-lock.yaml

Lines changed: 14 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)