@powersync/common 1.46.0 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/bundle.cjs +1266 -360
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +1259 -361
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +1266 -360
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +1259 -361
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +530 -29
- package/lib/attachments/AttachmentContext.d.ts +86 -0
- package/lib/attachments/AttachmentContext.js +229 -0
- package/lib/attachments/AttachmentContext.js.map +1 -0
- package/lib/attachments/AttachmentErrorHandler.d.ts +31 -0
- package/lib/attachments/AttachmentErrorHandler.js +2 -0
- package/lib/attachments/AttachmentErrorHandler.js.map +1 -0
- package/lib/attachments/AttachmentQueue.d.ts +149 -0
- package/lib/attachments/AttachmentQueue.js +362 -0
- package/lib/attachments/AttachmentQueue.js.map +1 -0
- package/lib/attachments/AttachmentService.d.ts +29 -0
- package/lib/attachments/AttachmentService.js +56 -0
- package/lib/attachments/AttachmentService.js.map +1 -0
- package/lib/attachments/LocalStorageAdapter.d.ts +62 -0
- package/lib/attachments/LocalStorageAdapter.js +6 -0
- package/lib/attachments/LocalStorageAdapter.js.map +1 -0
- package/lib/attachments/RemoteStorageAdapter.d.ts +27 -0
- package/lib/attachments/RemoteStorageAdapter.js +2 -0
- package/lib/attachments/RemoteStorageAdapter.js.map +1 -0
- package/lib/attachments/Schema.d.ts +50 -0
- package/lib/attachments/Schema.js +62 -0
- package/lib/attachments/Schema.js.map +1 -0
- package/lib/attachments/SyncingService.d.ts +62 -0
- package/lib/attachments/SyncingService.js +168 -0
- package/lib/attachments/SyncingService.js.map +1 -0
- package/lib/attachments/WatchedAttachmentItem.d.ts +17 -0
- package/lib/attachments/WatchedAttachmentItem.js +2 -0
- package/lib/attachments/WatchedAttachmentItem.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/mutex.d.ts +1 -1
- package/lib/utils/mutex.js.map +1 -1
- package/package.json +1 -1
- package/src/attachments/AttachmentContext.ts +279 -0
- package/src/attachments/AttachmentErrorHandler.ts +34 -0
- package/src/attachments/AttachmentQueue.ts +472 -0
- package/src/attachments/AttachmentService.ts +62 -0
- package/src/attachments/LocalStorageAdapter.ts +72 -0
- package/src/attachments/README.md +718 -0
- package/src/attachments/RemoteStorageAdapter.ts +30 -0
- package/src/attachments/Schema.ts +87 -0
- package/src/attachments/SyncingService.ts +193 -0
- package/src/attachments/WatchedAttachmentItem.ts +19 -0
- package/src/index.ts +11 -0
- package/src/utils/mutex.ts +1 -1
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
# Attachments utilities and functions
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> Attachment helpers are currently in an **alpha** state, intended strictly for testing. Expect breaking changes and instability as development continues.
|
|
5
|
+
>
|
|
6
|
+
> Do not rely on this package for production use.
|
|
7
|
+
|
|
8
|
+
PowerSync utilities and classes managing file attachments in JavaScript/TypeScript applications. Automatically handles synchronization of files between local storage and remote storage (S3, Supabase Storage, etc.), with support for upload/download queuing, offline functionality, and cache management.
|
|
9
|
+
|
|
10
|
+
For detailed concepts and guides, see the [PowerSync documentation](https://docs.powersync.com/usage/use-case-examples/attachments-files).
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
This example shows a web application where users have profile photos stored as attachments.
|
|
15
|
+
|
|
16
|
+
### 1. Add AttachmentTable to your schema
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { Schema, Table, column, AttachmentTable } from '@powersync/web';
|
|
20
|
+
|
|
21
|
+
const appSchema = new Schema({
|
|
22
|
+
users: new Table({
|
|
23
|
+
name: column.text,
|
|
24
|
+
email: column.text,
|
|
25
|
+
photo_id: column.text
|
|
26
|
+
}),
|
|
27
|
+
attachments: new AttachmentTable()
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Set up storage adapters
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { IndexDBFileSystemStorageAdapter } from '@powersync/web';
|
|
35
|
+
|
|
36
|
+
// Local storage for the browser (IndexedDB)
|
|
37
|
+
const localStorage = new IndexDBFileSystemStorageAdapter('my-app-files');
|
|
38
|
+
|
|
39
|
+
// Remote storage adapter for your cloud storage (e.g., S3, Supabase)
|
|
40
|
+
const remoteStorage = {
|
|
41
|
+
async uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void> {
|
|
42
|
+
// Get signed upload URL from your backend
|
|
43
|
+
const { uploadUrl } = await fetch('/api/attachments/upload-url', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
body: JSON.stringify({ filename: attachment.filename })
|
|
46
|
+
}).then(r => r.json());
|
|
47
|
+
|
|
48
|
+
// Upload file to cloud storage
|
|
49
|
+
await fetch(uploadUrl, {
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
body: fileData,
|
|
52
|
+
headers: { 'Content-Type': attachment.mediaType || 'application/octet-stream' }
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer> {
|
|
57
|
+
// Get signed download URL from your backend
|
|
58
|
+
const { downloadUrl } = await fetch(`/api/attachments/download-url/${attachment.id}`)
|
|
59
|
+
.then(r => r.json());
|
|
60
|
+
|
|
61
|
+
// Download file from cloud storage
|
|
62
|
+
const response = await fetch(downloadUrl);
|
|
63
|
+
return response.arrayBuffer();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async deleteFile(attachment: AttachmentRecord): Promise<void> {
|
|
67
|
+
// Delete from cloud storage via your backend
|
|
68
|
+
await fetch(`/api/attachments/${attachment.id}`, { method: 'DELETE' });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Note:** For Node.js or Electron apps, use `NodeFileSystemAdapter` instead:
|
|
74
|
+
> ```typescript
|
|
75
|
+
> import { NodeFileSystemAdapter } from '@powersync/node';
|
|
76
|
+
> const localStorage = new NodeFileSystemAdapter('./user-attachments');
|
|
77
|
+
> ```
|
|
78
|
+
|
|
79
|
+
### 3. Create and start AttachmentQueue
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { AttachmentQueue } from '@powersync/web';
|
|
83
|
+
|
|
84
|
+
const profilePicturesQueue = new AttachmentQueue({
|
|
85
|
+
db: powersync,
|
|
86
|
+
localStorage,
|
|
87
|
+
remoteStorage,
|
|
88
|
+
// Determine what attachments the queue should handle
|
|
89
|
+
// in this case it handles only the user profile pictures
|
|
90
|
+
watchAttachments: (onUpdate) => {
|
|
91
|
+
powersync.watch(
|
|
92
|
+
'SELECT photo_id FROM users WHERE photo_id IS NOT NULL',
|
|
93
|
+
[],
|
|
94
|
+
{
|
|
95
|
+
onResult: (result) => {
|
|
96
|
+
const attachments = result.rows?._array.map(row => ({
|
|
97
|
+
id: row.photo_id,
|
|
98
|
+
fileExtension: 'jpg'
|
|
99
|
+
})) ?? [];
|
|
100
|
+
onUpdate(attachments);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Start automatic syncing
|
|
108
|
+
await profilePicturesQueue.startSync();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 4. Save files with atomic updates
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// When user uploads a profile photo
|
|
115
|
+
async function uploadProfilePhoto(imageBlob: Blob) {
|
|
116
|
+
const arrayBuffer = await imageBlob.arrayBuffer();
|
|
117
|
+
|
|
118
|
+
const attachment = await queue.saveFile({
|
|
119
|
+
data: arrayBuffer,
|
|
120
|
+
fileExtension: 'jpg',
|
|
121
|
+
mediaType: 'image/jpeg',
|
|
122
|
+
// Atomically update the user record in the same transaction
|
|
123
|
+
updateHook: async (tx, attachment) => {
|
|
124
|
+
await tx.execute(
|
|
125
|
+
'UPDATE users SET photo_id = ? WHERE id = ?',
|
|
126
|
+
[attachment.id, currentUserId]
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log('Photo queued for upload:', attachment.id);
|
|
132
|
+
// File will automatically upload in the background
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Storage Adapters
|
|
137
|
+
|
|
138
|
+
### Local Storage Adapters
|
|
139
|
+
|
|
140
|
+
Local storage adapters handle file persistence on the device.
|
|
141
|
+
|
|
142
|
+
#### IndexDBFileSystemStorageAdapter
|
|
143
|
+
|
|
144
|
+
For web browsers using IndexedDB:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { IndexDBFileSystemStorageAdapter } from '@powersync/web';
|
|
148
|
+
|
|
149
|
+
const localStorage = new IndexDBFileSystemStorageAdapter('database-name');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Constructor Parameters:**
|
|
153
|
+
- `databaseName` (string, optional): IndexedDB database name. Default: `'PowerSyncFiles'`
|
|
154
|
+
|
|
155
|
+
#### NodeFileSystemAdapter
|
|
156
|
+
|
|
157
|
+
For Node.js and Electron using Node filesystem:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { NodeFileSystemAdapter } from '@powersync/node';
|
|
161
|
+
|
|
162
|
+
const localStorage = new NodeFileSystemAdapter('./attachments');
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### ExpoFileSystemAdapter
|
|
166
|
+
|
|
167
|
+
For React Native using Expo:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { ExpoFileSystemAdapter } from '@powersync/react-native'
|
|
171
|
+
|
|
172
|
+
const localeStorage = new ExpoFileSystemAdapter();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Constructor Parameters:**
|
|
176
|
+
- `storageDirectory` (string, optional): Directory path for storing files. Default: `'./user_data'`
|
|
177
|
+
|
|
178
|
+
#### Custom Local Storage Adapter
|
|
179
|
+
|
|
180
|
+
Implement the `LocalStorageAdapter` interface for other environments:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
interface LocalStorageAdapter {
|
|
184
|
+
initialize(): Promise<void>;
|
|
185
|
+
clear(): Promise<void>;
|
|
186
|
+
getLocalUri(filename: string): string;
|
|
187
|
+
saveFile(filePath: string, data: ArrayBuffer | string): Promise<number>;
|
|
188
|
+
readFile(filePath: string): Promise<ArrayBuffer>;
|
|
189
|
+
deleteFile(filePath: string): Promise<void>;
|
|
190
|
+
fileExists(filePath: string): Promise<boolean>;
|
|
191
|
+
makeDir(path: string): Promise<void>;
|
|
192
|
+
rmDir(path: string): Promise<void>;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Remote Storage Adapter
|
|
197
|
+
|
|
198
|
+
Remote storage adapters handle communication with your cloud storage (S3, Supabase Storage, Cloudflare R2, etc.).
|
|
199
|
+
|
|
200
|
+
#### Interface
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
interface RemoteStorageAdapter {
|
|
204
|
+
uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void>;
|
|
205
|
+
downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer>;
|
|
206
|
+
deleteFile(attachment: AttachmentRecord): Promise<void>;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Example: S3-Compatible Storage with Signed URLs
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { RemoteStorageAdapter, AttachmentRecord } from '@powersync/web';
|
|
214
|
+
|
|
215
|
+
const remoteStorage: RemoteStorageAdapter = {
|
|
216
|
+
async uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void> {
|
|
217
|
+
// Request signed upload URL from your backend
|
|
218
|
+
const response = await fetch('https://api.example.com/attachments/upload-url', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'Authorization': `Bearer ${getAuthToken()}`
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
filename: attachment.filename,
|
|
226
|
+
contentType: attachment.mediaType
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const { uploadUrl } = await response.json();
|
|
231
|
+
|
|
232
|
+
// Upload directly to S3 using signed URL
|
|
233
|
+
await fetch(uploadUrl, {
|
|
234
|
+
method: 'PUT',
|
|
235
|
+
body: fileData,
|
|
236
|
+
headers: { 'Content-Type': attachment.mediaType || 'application/octet-stream' }
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer> {
|
|
241
|
+
// Request signed download URL from your backend
|
|
242
|
+
const response = await fetch(
|
|
243
|
+
`https://api.example.com/attachments/${attachment.id}/download-url`,
|
|
244
|
+
{ headers: { 'Authorization': `Bearer ${getAuthToken()}` } }
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const { downloadUrl } = await response.json();
|
|
248
|
+
|
|
249
|
+
// Download from S3 using signed URL
|
|
250
|
+
const fileResponse = await fetch(downloadUrl);
|
|
251
|
+
if (!fileResponse.ok) {
|
|
252
|
+
throw new Error(`Download failed: ${fileResponse.statusText}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return fileResponse.arrayBuffer();
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async deleteFile(attachment: AttachmentRecord): Promise<void> {
|
|
259
|
+
// Delete via your backend (backend handles S3 deletion)
|
|
260
|
+
await fetch(`https://api.example.com/attachments/${attachment.id}`, {
|
|
261
|
+
method: 'DELETE',
|
|
262
|
+
headers: { 'Authorization': `Bearer ${getAuthToken()}` }
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
> **Security Note:** Always use your backend to generate signed URLs and validate permissions. Never expose storage credentials to the client.
|
|
269
|
+
|
|
270
|
+
## API Reference
|
|
271
|
+
|
|
272
|
+
### AttachmentQueue
|
|
273
|
+
|
|
274
|
+
Main class for managing attachment synchronization.
|
|
275
|
+
|
|
276
|
+
#### Constructor
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
new AttachmentQueue(options: AttachmentQueueOptions)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Options:**
|
|
283
|
+
|
|
284
|
+
| Parameter | Type | Required | Default | Description |
|
|
285
|
+
|-----------|------|----------|---------|-------------|
|
|
286
|
+
| `db` | `AbstractPowerSyncDatabase` | Yes | - | PowerSync database instance |
|
|
287
|
+
| `remoteStorage` | `RemoteStorageAdapter` | Yes | - | Remote storage adapter implementation |
|
|
288
|
+
| `localStorage` | `LocalStorageAdapter` | Yes | - | Local storage adapter implementation |
|
|
289
|
+
| `watchAttachments` | `(onUpdate: (attachments: WatchedAttachmentItem[]) => Promise<void>) => void` | Yes | - | Callback to determine which attachments to handle by the queue from your user defined query |
|
|
290
|
+
| `tableName` | `string` | No | `'attachments'` | Name of the attachments table |
|
|
291
|
+
| `logger` | `ILogger` | No | `db.logger` | Logger instance for diagnostic output |
|
|
292
|
+
| `syncIntervalMs` | `number` | No | `30000` | Interval between automatic syncs in milliseconds |
|
|
293
|
+
| `syncThrottleDuration` | `number` | No | `30` | Throttle duration for sync operations in milliseconds |
|
|
294
|
+
| `downloadAttachments` | `boolean` | No | `true` | Whether to automatically download remote attachments |
|
|
295
|
+
| `archivedCacheLimit` | `number` | No | `100` | Maximum number of archived attachments before cleanup |
|
|
296
|
+
| `errorHandler` | `AttachmentErrorHandler` | No | `undefined` | Custom error handler for upload/download/delete operations |
|
|
297
|
+
|
|
298
|
+
#### Methods
|
|
299
|
+
|
|
300
|
+
##### `startSync()`
|
|
301
|
+
|
|
302
|
+
Starts automatic attachment synchronization.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
await queue.startSync();
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
This will:
|
|
309
|
+
- Initialize local storage
|
|
310
|
+
- Set up periodic sync based on `syncIntervalMs`
|
|
311
|
+
- Watch for changes in active attachments
|
|
312
|
+
- Process queued uploads, downloads, and deletes
|
|
313
|
+
|
|
314
|
+
##### `stopSync()`
|
|
315
|
+
|
|
316
|
+
Stops automatic attachment synchronization.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
await queue.stopSync();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
##### `saveFile(options)`
|
|
323
|
+
|
|
324
|
+
Saves a file locally and queues it for upload to remote storage.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
const attachment = await queue.saveFile({
|
|
328
|
+
data: arrayBuffer,
|
|
329
|
+
fileExtension: 'pdf',
|
|
330
|
+
mediaType: 'application/pdf',
|
|
331
|
+
id: 'custom-id', // optional
|
|
332
|
+
metaData: '{"description": "Invoice"}', // optional
|
|
333
|
+
updateHook: async (tx, attachment) => {
|
|
334
|
+
// Update your data model in the same transaction
|
|
335
|
+
await tx.execute(
|
|
336
|
+
'INSERT INTO documents (id, attachment_id) VALUES (?, ?)',
|
|
337
|
+
[documentId, attachment.id]
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Parameters:**
|
|
344
|
+
|
|
345
|
+
| Parameter | Type | Required | Description |
|
|
346
|
+
|-----------|------|----------|-------------|
|
|
347
|
+
| `data` | `ArrayBuffer \| string` | Yes | File data as ArrayBuffer or base64 string |
|
|
348
|
+
| `fileExtension` | `string` | Yes | File extension (e.g., 'jpg', 'pdf') |
|
|
349
|
+
| `mediaType` | `string` | No | MIME type (e.g., 'image/jpeg') |
|
|
350
|
+
| `id` | `string` | No | Custom attachment ID (UUID generated if not provided) |
|
|
351
|
+
| `metaData` | `string` | No | Optional metadata JSON string |
|
|
352
|
+
| `updateHook` | `(tx: Transaction, attachment: AttachmentRecord) => Promise<void>` | No | Callback to update your data model atomically |
|
|
353
|
+
|
|
354
|
+
**Returns:** `Promise<AttachmentRecord>` - The created attachment record
|
|
355
|
+
|
|
356
|
+
The `updateHook` is executed in the same database transaction as the attachment creation, ensuring atomic operations. This is the recommended way to link attachments to your data model.
|
|
357
|
+
|
|
358
|
+
##### `deleteFile(options)`
|
|
359
|
+
|
|
360
|
+
Deletes an attachment from both local and remote storage.
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
await queue.deleteFile({
|
|
364
|
+
id: attachmentId,
|
|
365
|
+
updateHook: async (tx, attachment) => {
|
|
366
|
+
// Update your data model in the same transaction
|
|
367
|
+
await tx.execute(
|
|
368
|
+
'UPDATE users SET photo_id = NULL WHERE photo_id = ?',
|
|
369
|
+
[attachment.id]
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Parameters:**
|
|
376
|
+
|
|
377
|
+
| Parameter | Type | Required | Description |
|
|
378
|
+
|-----------|------|----------|-------------|
|
|
379
|
+
| `id` | `string` | Yes | Attachment ID to delete |
|
|
380
|
+
| `updateHook` | `(tx: Transaction, attachment: AttachmentRecord) => Promise<void>` | No | Callback to update your data model atomically |
|
|
381
|
+
|
|
382
|
+
##### `generateAttachmentId()`
|
|
383
|
+
|
|
384
|
+
Generates a new UUID for an attachment using SQLite's `uuid()` function.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
const id = await queue.generateAttachmentId();
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Returns:** `Promise<string>` - A new UUID
|
|
391
|
+
|
|
392
|
+
##### `syncStorage()`
|
|
393
|
+
|
|
394
|
+
Manually triggers a sync operation. This is called automatically at regular intervals, but can be invoked manually if needed.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
await queue.syncStorage();
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
##### `verifyAttachments()`
|
|
401
|
+
|
|
402
|
+
Verifies the integrity of all attachment records and repairs inconsistencies. Checks each attachment against local storage and:
|
|
403
|
+
- Updates `localUri` if file exists at a different path
|
|
404
|
+
- Archives attachments with missing local files that haven't been uploaded
|
|
405
|
+
- Requeues synced attachments for download if local files are missing
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
await queue.verifyAttachments();
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
This is automatically called when `startSync()` is invoked.
|
|
412
|
+
|
|
413
|
+
##### `watchAttachments` callback
|
|
414
|
+
|
|
415
|
+
The `watchAttachments` callback is a required parameter that tells the AttachmentQueue which attachments to handle. This tells the queue which attachments to download, upload, or archive.
|
|
416
|
+
|
|
417
|
+
**Signature:**
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
(onUpdate: (attachments: WatchedAttachmentItem[]) => Promise<void>) => void
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**WatchedAttachmentItem:**
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
type WatchedAttachmentItem = {
|
|
427
|
+
id: string;
|
|
428
|
+
fileExtension: string; // e.g., 'jpg', 'pdf'
|
|
429
|
+
metaData?: string;
|
|
430
|
+
} | {
|
|
431
|
+
id: string;
|
|
432
|
+
filename: string; // e.g., 'document.pdf'
|
|
433
|
+
metaData?: string;
|
|
434
|
+
};
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Use either `fileExtension` OR `filename`, not both.
|
|
438
|
+
|
|
439
|
+
**Example:**
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
watchAttachments: (onUpdate) => {
|
|
443
|
+
// Watch for photo references in users table
|
|
444
|
+
db.watch(
|
|
445
|
+
'SELECT photo_id, metadata FROM users WHERE photo_id IS NOT NULL',
|
|
446
|
+
[],
|
|
447
|
+
{
|
|
448
|
+
onResult: async (result) => {
|
|
449
|
+
const attachments = result.rows?._array.map(row => ({
|
|
450
|
+
id: row.photo_id,
|
|
451
|
+
fileExtension: 'jpg',
|
|
452
|
+
metaData: row.metadata
|
|
453
|
+
})) ?? [];
|
|
454
|
+
await onUpdate(attachments);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
### AttachmentTable
|
|
464
|
+
|
|
465
|
+
PowerSync schema table for storing attachment metadata.
|
|
466
|
+
|
|
467
|
+
#### Constructor
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
new AttachmentTable(options?: AttachmentTableOptions)
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Options:**
|
|
474
|
+
|
|
475
|
+
Extends PowerSync `TableV2Options` (excluding `name` and `columns`).
|
|
476
|
+
|
|
477
|
+
| Parameter | Type | Description |
|
|
478
|
+
|-----------|------|-------------|
|
|
479
|
+
| `viewName` | `string` | View name for the table. Default: `'attachments'` |
|
|
480
|
+
| `localOnly` | `boolean` | Whether table is local-only. Default: `true` |
|
|
481
|
+
| `insertOnly` | `boolean` | Whether table is insert-only. Default: `false` |
|
|
482
|
+
|
|
483
|
+
#### Default Columns
|
|
484
|
+
|
|
485
|
+
| Column | Type | Description |
|
|
486
|
+
|--------|------|-------------|
|
|
487
|
+
| `id` | `TEXT` | Attachment ID (primary key) |
|
|
488
|
+
| `filename` | `TEXT` | Filename with extension |
|
|
489
|
+
| `local_uri` | `TEXT` | Local file path or URI |
|
|
490
|
+
| `timestamp` | `INTEGER` | Last update timestamp |
|
|
491
|
+
| `size` | `INTEGER` | File size in bytes |
|
|
492
|
+
| `media_type` | `TEXT` | MIME type |
|
|
493
|
+
| `state` | `INTEGER` | Sync state (see `AttachmentState`) |
|
|
494
|
+
| `has_synced` | `INTEGER` | Whether file has synced (0 or 1) |
|
|
495
|
+
| `meta_data` | `TEXT` | Optional metadata JSON string |
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### AttachmentRecord
|
|
500
|
+
|
|
501
|
+
Interface representing an attachment record.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
interface AttachmentRecord {
|
|
505
|
+
id: string;
|
|
506
|
+
filename: string;
|
|
507
|
+
localUri?: string;
|
|
508
|
+
size?: number;
|
|
509
|
+
mediaType?: string;
|
|
510
|
+
timestamp?: number;
|
|
511
|
+
metaData?: string;
|
|
512
|
+
hasSynced?: boolean;
|
|
513
|
+
state: AttachmentState;
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
### AttachmentState
|
|
520
|
+
|
|
521
|
+
Enum representing attachment synchronization states.
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
enum AttachmentState {
|
|
525
|
+
QUEUED_UPLOAD = 0, // Queued for upload
|
|
526
|
+
QUEUED_DOWNLOAD = 1, // Queued for download
|
|
527
|
+
QUEUED_DELETE = 2, // Queued for deletion
|
|
528
|
+
SYNCED = 3, // Successfully synced
|
|
529
|
+
ARCHIVED = 4 // No longer referenced (orphaned)
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
### LocalStorageAdapter
|
|
536
|
+
|
|
537
|
+
Interface for local file storage operations.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
interface LocalStorageAdapter {
|
|
541
|
+
initialize(): Promise<void>;
|
|
542
|
+
clear(): Promise<void>;
|
|
543
|
+
getLocalUri(filename: string): string;
|
|
544
|
+
saveFile(filePath: string, data: ArrayBuffer | string): Promise<number>;
|
|
545
|
+
readFile(filePath: string): Promise<ArrayBuffer>;
|
|
546
|
+
deleteFile(filePath: string): Promise<void>;
|
|
547
|
+
fileExists(filePath: string): Promise<boolean>;
|
|
548
|
+
makeDir(path: string): Promise<void>;
|
|
549
|
+
rmDir(path: string): Promise<void>;
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
### RemoteStorageAdapter
|
|
556
|
+
|
|
557
|
+
Interface for remote storage operations.
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
interface RemoteStorageAdapter {
|
|
561
|
+
uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void>;
|
|
562
|
+
downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer>;
|
|
563
|
+
deleteFile(attachment: AttachmentRecord): Promise<void>;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### NodeFileSystemAdapter
|
|
570
|
+
|
|
571
|
+
Local storage adapter for Node.js and Electron.
|
|
572
|
+
|
|
573
|
+
**Constructor:**
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
new NodeFileSystemAdapter(storageDirectory?: string)
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
- `storageDirectory` (optional): Directory path for storing files. Default: `'./user_data'`
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
### IndexDBFileSystemStorageAdapter
|
|
584
|
+
|
|
585
|
+
Local storage adapter for web browsers using IndexedDB.
|
|
586
|
+
|
|
587
|
+
**Constructor:**
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
new IndexDBFileSystemStorageAdapter(databaseName?: string)
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
- `databaseName` (optional): IndexedDB database name. Default: `'PowerSyncFiles'`
|
|
594
|
+
|
|
595
|
+
## Error Handling
|
|
596
|
+
|
|
597
|
+
The `AttachmentErrorHandler` interface allows you to customize error handling for sync operations.
|
|
598
|
+
|
|
599
|
+
### Interface
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
interface AttachmentErrorHandler {
|
|
603
|
+
onDownloadError(attachment: AttachmentRecord, error: Error): Promise<boolean>;
|
|
604
|
+
onUploadError(attachment: AttachmentRecord, error: Error): Promise<boolean>;
|
|
605
|
+
onDeleteError(attachment: AttachmentRecord, error: Error): Promise<boolean>;
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Each method returns:
|
|
610
|
+
- `true` to retry the operation
|
|
611
|
+
- `false` to archive the attachment and skip retrying
|
|
612
|
+
|
|
613
|
+
### Example
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
const errorHandler: AttachmentErrorHandler = {
|
|
617
|
+
async onDownloadError(attachment, error) {
|
|
618
|
+
console.error(`Download failed for ${attachment.filename}:`, error);
|
|
619
|
+
|
|
620
|
+
// Retry on network errors, archive on 404s
|
|
621
|
+
if (error.message.includes('404') || error.message.includes('Not Found')) {
|
|
622
|
+
console.log('File not found, archiving attachment');
|
|
623
|
+
return false; // Archive
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log('Will retry download on next sync');
|
|
627
|
+
return true; // Retry
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
async onUploadError(attachment, error) {
|
|
631
|
+
console.error(`Upload failed for ${attachment.filename}:`, error);
|
|
632
|
+
|
|
633
|
+
// Always retry uploads
|
|
634
|
+
return true;
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
async onDeleteError(attachment, error) {
|
|
638
|
+
console.error(`Delete failed for ${attachment.filename}:`, error);
|
|
639
|
+
|
|
640
|
+
// Retry deletes, but archive after too many attempts
|
|
641
|
+
const attempts = attachment.metaData ?
|
|
642
|
+
JSON.parse(attachment.metaData).deleteAttempts || 0 : 0;
|
|
643
|
+
|
|
644
|
+
return attempts < 3; // Retry up to 3 times
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const queue = new AttachmentQueue({
|
|
649
|
+
// ... other options
|
|
650
|
+
errorHandler
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Advanced Usage
|
|
655
|
+
|
|
656
|
+
### Verification and Recovery
|
|
657
|
+
|
|
658
|
+
The `verifyAttachments()` method checks attachment integrity and repairs issues:
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
// Manually verify all attachments
|
|
662
|
+
await queue.verifyAttachments();
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
This is useful if:
|
|
666
|
+
- Local files may have been manually deleted
|
|
667
|
+
- Storage paths changed
|
|
668
|
+
- You suspect data inconsistencies
|
|
669
|
+
|
|
670
|
+
Verification is automatically run when `startSync()` is called.
|
|
671
|
+
|
|
672
|
+
### Custom Sync Intervals
|
|
673
|
+
|
|
674
|
+
Adjust sync frequency based on your needs:
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
const queue = new AttachmentQueue({
|
|
678
|
+
// ... other options
|
|
679
|
+
syncIntervalMs: 60000, // Sync every 60 seconds instead of 30
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Set to `0` to disable periodic syncing (manual `syncStorage()` calls only).
|
|
684
|
+
|
|
685
|
+
### Archive and Cache Management
|
|
686
|
+
|
|
687
|
+
Control how many archived attachments are kept before cleanup:
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
const queue = new AttachmentQueue({
|
|
691
|
+
// ... other options
|
|
692
|
+
archivedCacheLimit: 200, // Keep up to 200 archived attachments
|
|
693
|
+
});
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Archived attachments are those no longer referenced in your data model but not yet deleted. This allows for:
|
|
697
|
+
- Quick restoration if references are added back
|
|
698
|
+
- Caching of recently used files
|
|
699
|
+
- Gradual cleanup to avoid storage bloat
|
|
700
|
+
|
|
701
|
+
When the limit is reached, the oldest archived attachments are permanently deleted.
|
|
702
|
+
|
|
703
|
+
## Examples
|
|
704
|
+
|
|
705
|
+
See the following demo applications in this repository:
|
|
706
|
+
|
|
707
|
+
- **[react-native-supabase-todolist](../../../../demos/react-native-supabase-todolist)** - React Native mobile app with attachment support
|
|
708
|
+
- **[react-native-web-supabase-todolist](../../../../demos/react-native-web-supabase-todolist)** - React Native web app with Supabase Storage integration
|
|
709
|
+
|
|
710
|
+
Each demo shows a complete implementation including:
|
|
711
|
+
- Schema setup
|
|
712
|
+
- Storage adapter configuration
|
|
713
|
+
- File upload/download UI
|
|
714
|
+
- Error handling
|
|
715
|
+
|
|
716
|
+
## License
|
|
717
|
+
|
|
718
|
+
Apache 2.0
|