Skip to content

Commit ecd22e5

Browse files
committed
feat: add abortable attachment watchers
1 parent e9ac51a commit ecd22e5

File tree

5 files changed

+74
-37
lines changed

5 files changed

+74
-37
lines changed

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import '@azure/core-asynciterator-polyfill';
22

3-
import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation,
4-
AttachmentQueue,
5-
type AttachmentRecord,
6-
ExpoFileSystemAdapter,
7-
type WatchedAttachmentItem } from '@powersync/react-native';
3+
import {
4+
createBaseLogger,
5+
LogLevel,
6+
PowerSyncDatabase,
7+
SyncClientImplementation,
8+
AttachmentQueue,
9+
type AttachmentRecord,
10+
type WatchedAttachmentItem,
11+
ReactNativeFileSystemStorageAdapter
12+
} from '@powersync/react-native';
813
import React from 'react';
914
import { configureFts } from '../fts/fts_setup';
1015
import { KVStorage } from '../storage/KVStorage';
@@ -30,7 +35,7 @@ export class System {
3035
supabaseUrl: AppConfig.supabaseUrl,
3136
supabaseAnonKey: AppConfig.supabaseAnonKey
3237
});
33-
38+
3439
this.powersync = new PowerSyncDatabase({
3540
schema: AppSchema,
3641
database: {
@@ -54,7 +59,7 @@ export class System {
5459
*/
5560

5661
if (AppConfig.supabaseBucket) {
57-
const localStorage = new ExpoFileSystemAdapter();
62+
const localStorage = new ReactNativeFileSystemStorageAdapter();
5863
const remoteStorage = new SupabaseRemoteStorageAdapter({
5964
client: this.supabaseConnector.client,
6065
bucket: AppConfig.supabaseBucket
@@ -64,20 +69,22 @@ export class System {
6469
db: this.powersync,
6570
localStorage,
6671
remoteStorage,
67-
watchAttachments: (onUpdate) => {
68-
this.powersync.watch(
72+
watchAttachments: async (onUpdate, signal) => {
73+
const watcher = this.powersync.watch(
6974
`SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`,
7075
[],
7176
{
72-
onResult: (result: any) => {
73-
const attachments: WatchedAttachmentItem[] = (result.rows?._array ?? []).map((row: any) => ({
74-
id: row.id,
75-
fileExtension: 'jpg'
76-
}));
77-
onUpdate(attachments);
78-
}
77+
signal
7978
}
8079
);
80+
81+
for await (const result of watcher) {
82+
const attachments: WatchedAttachmentItem[] = (result.rows?._array ?? []).map((row: any) => ({
83+
id: row.id,
84+
fileExtension: 'jpg'
85+
}));
86+
await onUpdate(attachments);
87+
}
8188
},
8289
errorHandler: {
8390
onDownloadError: async (attachment: AttachmentRecord, error: Error) => {
@@ -93,7 +100,7 @@ export class System {
93100
return true; // Retry deletes by default
94101
}
95102
},
96-
logger,
103+
logger
97104
});
98105
}
99106
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"dependencies": {
1111
"@azure/core-asynciterator-polyfill": "^1.0.2",
12+
"@dr.pogodin/react-native-fs": "^2.36.1",
1213
"@expo/vector-icons": "^14.0.3",
1314
"@journeyapps/react-native-quick-sqlite": "^2.4.9",
1415
"@powersync/attachments": "workspace:*",

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

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import '@azure/core-asynciterator-polyfill';
22

33
import React from 'react';
4-
import { PowerSyncDatabase as PowerSyncDatabaseNative, AbstractPowerSyncDatabase, ExpoFileSystemAdapter } from '@powersync/react-native';
5-
import { PowerSyncDatabase as PowerSyncDatabaseWeb, WASQLiteOpenFactory, IndexDBFileSystemStorageAdapter } from '@powersync/web';
6-
import { type AttachmentRecord, AttachmentQueue, LogLevel, createBaseLogger, WatchedAttachmentItem } from '@powersync/common';
4+
import {
5+
PowerSyncDatabase as PowerSyncDatabaseNative,
6+
AbstractPowerSyncDatabase,
7+
ReactNativeFileSystemStorageAdapter
8+
} from '@powersync/react-native';
9+
import {
10+
PowerSyncDatabase as PowerSyncDatabaseWeb,
11+
WASQLiteOpenFactory,
12+
IndexDBFileSystemStorageAdapter
13+
} from '@powersync/web';
14+
import {
15+
type AttachmentRecord,
16+
AttachmentQueue,
17+
LogLevel,
18+
createBaseLogger,
19+
WatchedAttachmentItem
20+
} from '@powersync/common';
721
import { SupabaseRemoteStorageAdapter } from '../storage/SupabaseRemoteStorageAdapter';
822
import { ExpoKVStorage, WebKVStorage } from '../storage/KVStorage';
923
import { AppConfig } from '../supabase/AppConfig';
@@ -34,7 +48,7 @@ export class System {
3448
database: {
3549
dbFilename: 'sqlite.db'
3650
},
37-
logger,
51+
logger
3852
});
3953
} else {
4054
const factory = new WASQLiteOpenFactory({
@@ -72,13 +86,13 @@ export class System {
7286
// });
7387
// }
7488
},
75-
logger,
89+
logger
7690
});
7791
}
7892

7993
if (AppConfig.supabaseBucket) {
8094
const isWeb = Platform.OS === 'web';
81-
const localStorage = isWeb ? new IndexDBFileSystemStorageAdapter() : new ExpoFileSystemAdapter();
95+
const localStorage = isWeb ? new IndexDBFileSystemStorageAdapter() : new ReactNativeFileSystemStorageAdapter();
8296
const remoteStorage = new SupabaseRemoteStorageAdapter({
8397
client: this.supabaseConnector.client,
8498
bucket: AppConfig.supabaseBucket
@@ -87,16 +101,22 @@ export class System {
87101
db: this.powersync,
88102
localStorage,
89103
remoteStorage,
90-
watchAttachments: (onUpdate) => {
91-
this.powersync.watch(`SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`, [], {
92-
onResult: (result) => {
93-
const attachments: WatchedAttachmentItem[] = (result.rows?._array ?? []).map((row: any) => ({
94-
id: row.id,
95-
fileExtension: 'jpg'
96-
}));
97-
onUpdate(attachments);
104+
watchAttachments: async (onUpdate, signal) => {
105+
const watcher = this.powersync.watch(
106+
`SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`,
107+
[],
108+
{
109+
signal
98110
}
99-
});
111+
);
112+
113+
for await (const result of watcher) {
114+
const attachments: WatchedAttachmentItem[] = (result.rows?._array ?? []).map((row: any) => ({
115+
id: row.id,
116+
fileExtension: 'jpg'
117+
}));
118+
await onUpdate(attachments);
119+
}
100120
},
101121
errorHandler: {
102122
onDownloadError: async (attachment: AttachmentRecord, error: Error) => {
@@ -112,7 +132,7 @@ export class System {
112132
return true; // Retry deletes by default
113133
}
114134
},
115-
logger,
135+
logger
116136
});
117137
}
118138
}
@@ -122,6 +142,7 @@ export class System {
122142
await this.powersync.connect(this.supabaseConnector);
123143

124144
if (this.photoAttachmentQueue) {
145+
// await this.photoAttachmentQueue.localStorage.initialize();
125146
await this.photoAttachmentQueue.startSync();
126147
}
127148
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111
},
1212
"dependencies": {
1313
"@azure/core-asynciterator-polyfill": "^1.0.2",
14+
"@dr.pogodin/react-native-fs": "^2.36.1",
1415
"@expo/metro-runtime": "^4.0.1",
1516
"@expo/vector-icons": "^14.0.2",
1617
"@journeyapps/react-native-quick-sqlite": "^2.4.9",
1718
"@journeyapps/wa-sqlite": "^1.3.2",
1819
"@powersync/attachments": "workspace:*",
20+
"@powersync/common": "workspace:*",
1921
"@powersync/react": "workspace:*",
2022
"@powersync/react-native": "workspace:*",
2123
"@powersync/web": "workspace:*",
22-
"@powersync/common": "workspace:*",
2324
"@react-native-async-storage/async-storage": "1.23.1",
2425
"@react-navigation/bottom-tabs": "^7.2.0",
2526
"@react-navigation/drawer": "^7.1.1",

packages/common/src/attachments/AttachmentQueue.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export class AttachmentQueue {
6161

6262
watchActiveAttachments: DifferentialWatchedQuery<AttachmentRecord>;
6363

64+
watchAttachmentsAbortController: AbortController;
65+
6466
/**
6567
* Creates a new AttachmentQueue instance.
6668
*
@@ -92,7 +94,7 @@ export class AttachmentQueue {
9294
db: AbstractPowerSyncDatabase;
9395
remoteStorage: RemoteStorageAdapter;
9496
localStorage: LocalStorageAdapter;
95-
watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>) => void;
97+
watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>, signal: AbortSignal) => void;
9698
tableName?: string;
9799
logger?: ILogger;
98100
syncIntervalMs?: number;
@@ -131,7 +133,7 @@ export class AttachmentQueue {
131133
* @param onUpdate - Callback to invoke when attachment references change
132134
* @throws Error indicating this method must be implemented by the user
133135
*/
134-
watchAttachments(onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>): void {
136+
watchAttachments(onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>, signal: AbortSignal): void {
135137
throw new Error('watchAttachments should be implemented by the user of AttachmentQueue');
136138
}
137139

@@ -178,6 +180,8 @@ export class AttachmentQueue {
178180
}
179181
});
180182

183+
this.watchAttachmentsAbortController = new AbortController();
184+
181185
// Process attachments when there is a change in watched attachments
182186
this.watchAttachments(async (watchedAttachments) => {
183187
// Need to get all the attachments which are tracked in the DB.
@@ -256,7 +260,7 @@ export class AttachmentQueue {
256260
if (attachmentUpdates.length > 0) {
257261
await this.context.saveAttachments(attachmentUpdates);
258262
}
259-
});
263+
}, this.watchAttachmentsAbortController.signal);
260264
}
261265

262266
/**
@@ -281,6 +285,9 @@ export class AttachmentQueue {
281285
clearInterval(this.periodicSyncTimer);
282286
this.periodicSyncTimer = undefined;
283287
if (this.watchActiveAttachments) await this.watchActiveAttachments.close();
288+
if (this.watchAttachmentsAbortController) {
289+
this.watchAttachmentsAbortController.abort();
290+
}
284291
}
285292

286293
/**

0 commit comments

Comments
 (0)