Skip to content

Commit be450ff

Browse files
DominicGBauerDominicGBauerstevensJourney
authored
feat(attachments): add error handlers (#73)
Co-authored-by: DominicGBauer <dominic@nomanini.com> Co-authored-by: Steven Ontong <steven@journeyapps.com>
1 parent 6802316 commit be450ff

File tree

8 files changed

+1924
-543
lines changed

8 files changed

+1924
-543
lines changed

.changeset/twenty-cups-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@journeyapps/powersync-attachments": minor
3+
---
4+
5+
Add ability to allow users to handle errors through onDownloadError and onUploadError when setting up the queue

demos/react-native-supabase-todolist/ios/Podfile.lock

Lines changed: 127 additions & 127 deletions
Large diffs are not rendered by default.

demos/react-native-supabase-todolist/ios/powersyncexample.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@
477477
"-Wl",
478478
"-ld_classic",
479479
);
480-
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
480+
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
481481
SDKROOT = iphoneos;
482482
USE_HERMES = true;
483483
};
@@ -535,7 +535,7 @@
535535
"-Wl",
536536
"-ld_classic",
537537
);
538-
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
538+
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
539539
SDKROOT = iphoneos;
540540
USE_HERMES = true;
541541
VALIDATE_PRODUCT = YES;

demos/react-native-supabase-todolist/library/powersync/system.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AppSchema } from './AppSchema';
88
import { SupabaseConnector } from '../supabase/SupabaseConnector';
99
import { KVStorage } from '../storage/KVStorage';
1010
import { PhotoAttachmentQueue } from './PhotoAttachmentQueue';
11+
import { type AttachmentRecord } from '@journeyapps/powersync-attachments';
1112

1213
export class System {
1314
kvStorage: KVStorage;
@@ -30,7 +31,16 @@ export class System {
3031

3132
this.attachmentQueue = new PhotoAttachmentQueue({
3233
powersync: this.powersync,
33-
storage: this.storage
34+
storage: this.storage,
35+
// Use this to handle download errors where you can use the attachment
36+
// and/or the exception to decide if you want to retry the download
37+
onDownloadError: async (attachment: AttachmentRecord, exception: any) => {
38+
if (exception.toString() === 'StorageApiError: Object not found') {
39+
return { retry: false };
40+
}
41+
42+
return { retry: true };
43+
}
3444
});
3545
}
3646

demos/react-native-supabase-todolist/library/widgets/TodoItemWidget.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CameraWidget } from './CameraWidget';
66
import { TodoRecord } from '../powersync/AppSchema';
77
import { AttachmentRecord } from '@journeyapps/powersync-attachments';
88
import { AppConfig } from '../supabase/AppConfig';
9+
import { useSystem } from '../powersync/system';
910

1011
export interface TodoItemWidgetProps {
1112
record: TodoRecord;
@@ -19,6 +20,7 @@ export const TodoItemWidget: React.FC<TodoItemWidgetProps> = (props) => {
1920
const { record, photoAttachment, onDelete, onToggleCompletion, onSavePhoto } = props;
2021
const [loading, setLoading] = React.useState(false);
2122
const [isCameraVisible, setCameraVisible] = React.useState(false);
23+
const system = useSystem();
2224

2325
const handleCancel = React.useCallback(() => {
2426
setCameraVisible(false);
@@ -49,8 +51,7 @@ export const TodoItemWidget: React.FC<TodoItemWidgetProps> = (props) => {
4951
);
5052
}}
5153
/>
52-
}
53-
>
54+
}>
5455
{loading ? (
5556
<ActivityIndicator />
5657
) : (
@@ -74,7 +75,7 @@ export const TodoItemWidget: React.FC<TodoItemWidgetProps> = (props) => {
7475
<Icon name={'camera'} type="font-awesome" onPress={() => setCameraVisible(true)} />
7576
) : photoAttachment?.local_uri != null ? (
7677
<Image
77-
source={{ uri: photoAttachment.local_uri }}
78+
source={{ uri: system.attachmentQueue.getLocalUri(photoAttachment.local_uri) }}
7879
containerStyle={styles.item}
7980
PlaceholderContent={<ActivityIndicator />}
8081
/>

demos/react-native-supabase-todolist/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,16 @@
5757
"web-streams-polyfill": "^3.3.2"
5858
},
5959
"devDependencies": {
60-
"@babel/core": "^7.20.0",
61-
"@babel/plugin-transform-async-generator-functions": "^7.22.15",
62-
"@babel/preset-env": "^7.1.6",
63-
"@types/base-64": "^1.0.0",
64-
"@types/lodash": "^4.14.199",
65-
"@types/react": "~18.2.14",
60+
"@babel/core": "^7.23.9",
61+
"@babel/plugin-transform-async-generator-functions": "^7.23.9",
62+
"@babel/preset-env": "^7.23.9",
63+
"@types/base-64": "^1.0.2",
64+
"@types/lodash": "^4.14.202",
65+
"@types/react": "~18.2.57",
6666
"@types/uuid": "3.4.11",
6767
"babel-preset-expo": "^10.0.1",
68-
"prettier": "^3.0.3",
69-
"typescript": "^5.1.3"
68+
"prettier": "^3.2.5",
69+
"typescript": "^5.3.3"
7070
},
7171
"private": true
7272
}

packages/powersync-attachments/src/AbstractAttachmentQueue.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export interface AttachmentQueueOptions {
2121
* Whether to mark the initial watched attachment IDs to be synced
2222
*/
2323
performInitialSync?: boolean;
24+
/**
25+
* How to handle download errors, return { retry: false } to ignore the download
26+
*/
27+
onDownloadError?: (attachment: AttachmentRecord, exception: any) => Promise<{ retry?: boolean }>;
28+
/**
29+
* How to handle upload errors, return { retry: false } to ignore the upload
30+
*/
31+
onUploadError?: (attachment: AttachmentRecord, exception: any) => Promise<{ retry?: boolean }>;
2432
}
2533

2634
export const DEFAULT_ATTACHMENT_QUEUE_OPTIONS: Partial<AttachmentQueueOptions> = {
@@ -106,11 +114,11 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
106114
this.initialSync = false;
107115
// Mark AttachmentIds for sync
108116
await this.powersync.execute(
109-
`UPDATE
110-
${this.table}
111-
SET state = ${AttachmentState.QUEUED_SYNC}
112-
WHERE
113-
state < ${AttachmentState.SYNCED}
117+
`UPDATE
118+
${this.table}
119+
SET state = ${AttachmentState.QUEUED_SYNC}
120+
WHERE
121+
state < ${AttachmentState.SYNCED}
114122
AND
115123
id IN (${_ids})`
116124
);
@@ -142,9 +150,12 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
142150

143151
// 3. Attachment in database and not in AttachmentIds, mark as archived
144152
await this.powersync.execute(
145-
`UPDATE ${this.table} SET state = ${AttachmentState.ARCHIVED} WHERE state < ${
146-
AttachmentState.ARCHIVED
147-
} AND id NOT IN (${ids.map((id) => `'${id}'`).join(',')})`
153+
`UPDATE ${this.table}
154+
SET state = ${AttachmentState.ARCHIVED}
155+
WHERE
156+
state < ${AttachmentState.ARCHIVED}
157+
AND
158+
id NOT IN (${ids.map((id) => `'${id}'`).join(',')})`
148159
);
149160
}
150161
}
@@ -179,7 +190,7 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
179190
const timestamp = new Date().getTime();
180191
await this.powersync.execute(
181192
`UPDATE ${this.table}
182-
SET
193+
SET
183194
timestamp = ?,
184195
filename = ?,
185196
local_uri = ?,
@@ -267,6 +278,13 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
267278
await this.update({ ...record, state: AttachmentState.SYNCED });
268279
return false;
269280
}
281+
if (this.options.onUploadError) {
282+
const { retry } = await this.options.onUploadError(record, e);
283+
if (!retry) {
284+
await this.update({ ...record, state: AttachmentState.ARCHIVED });
285+
return true;
286+
}
287+
}
270288
console.error(`UploadAttachment error for record ${JSON.stringify(record, null, 2)}`);
271289
return false;
272290
}
@@ -312,6 +330,13 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
312330
console.debug(`Downloaded attachment "${record.id}"`);
313331
return true;
314332
} catch (e) {
333+
if (this.options.onDownloadError) {
334+
const { retry } = await this.options.onDownloadError(record, e);
335+
if (!retry) {
336+
await this.update({ ...record, state: AttachmentState.ARCHIVED });
337+
return true;
338+
}
339+
}
315340
console.error(`Download attachment error for record ${JSON.stringify(record, null, 2)}`, e);
316341
}
317342
return false;
@@ -320,11 +345,11 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
320345
async *idsToUpload(): AsyncIterable<string[]> {
321346
for await (const result of this.powersync.watch(
322347
`SELECT id
323-
FROM ${this.table}
348+
FROM ${this.table}
324349
WHERE
325350
local_uri IS NOT NULL
326351
AND
327-
(state = ${AttachmentState.QUEUED_UPLOAD}
352+
(state = ${AttachmentState.QUEUED_UPLOAD}
328353
OR
329354
state = ${AttachmentState.QUEUED_SYNC})`,
330355
[]
@@ -374,9 +399,9 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
374399
async getIdsToDownload(): Promise<string[]> {
375400
const res = await this.powersync.getAll<{ id: string }>(
376401
`SELECT id
377-
FROM ${this.table}
402+
FROM ${this.table}
378403
WHERE
379-
state = ${AttachmentState.QUEUED_DOWNLOAD}
404+
state = ${AttachmentState.QUEUED_DOWNLOAD}
380405
OR
381406
state = ${AttachmentState.QUEUED_SYNC}
382407
ORDER BY timestamp ASC`
@@ -387,10 +412,10 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
387412
async *idsToDownload(): AsyncIterable<string[]> {
388413
for await (const result of this.powersync.watch(
389414
`SELECT id
390-
FROM ${this.table}
391-
WHERE
392-
state = ${AttachmentState.QUEUED_DOWNLOAD}
393-
OR
415+
FROM ${this.table}
416+
WHERE
417+
state = ${AttachmentState.QUEUED_DOWNLOAD}
418+
OR
394419
state = ${AttachmentState.QUEUED_SYNC}`,
395420
[]
396421
)) {
@@ -460,10 +485,10 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
460485
}
461486

462487
async expireCache() {
463-
const res = await this.powersync.getAll<AttachmentRecord>(`SELECT * FROM ${this.table}
464-
WHERE
488+
const res = await this.powersync.getAll<AttachmentRecord>(`SELECT * FROM ${this.table}
489+
WHERE
465490
state = ${AttachmentState.SYNCED} OR state = ${AttachmentState.ARCHIVED}
466-
ORDER BY
491+
ORDER BY
467492
timestamp DESC
468493
LIMIT 100 OFFSET ${this.options.cacheLimit}`);
469494

0 commit comments

Comments
 (0)