Skip to content

Commit 5120755

Browse files
committed
refactor(attachments): update local storage adapter interfaces to support streaming
This commit refines the local storage adapter interfaces by replacing the `LocalStorageAdapter` with `StreamingLocalStorageAdapter` in multiple components, including `ExpoFileSystemStorageAdapter`, `ReactNativeFileSystemStorageAdapter`, and `NodeFileSystemAdapter`. The `AttachmentQueue` and related types have been updated to leverage the new streaming capabilities, ensuring that the `saveFileFromUri` method is only available when a streaming-capable adapter is used. Documentation has been adjusted to reflect these changes.
1 parent 160b2ad commit 5120755

7 files changed

Lines changed: 67 additions & 45 deletions

File tree

packages/attachments-storage-react-native/src/ExpoFileSystemStorageAdapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { decode as decodeBase64 } from 'base64-arraybuffer';
2-
import type { AttachmentData, LocalStorageAdapter } from '@powersync/common';
2+
import type { AttachmentData, StreamingLocalStorageAdapter } from '@powersync/common';
33
import type { File, Directory } from 'expo-file-system';
44

55
/**
@@ -9,7 +9,7 @@ import type { File, Directory } from 'expo-file-system';
99
* @experimental
1010
* @alpha This is currently experimental and may change without a major version bump.
1111
*/
12-
export class ExpoFileSystemStorageAdapter implements LocalStorageAdapter {
12+
export class ExpoFileSystemStorageAdapter implements StreamingLocalStorageAdapter {
1313
private File: typeof File;
1414
private Directory: typeof Directory;
1515
private storageDir: Directory;

packages/attachments-storage-react-native/src/ReactNativeFileSystemStorageAdapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { decode as decodeBase64, encode as encodeBase64 } from 'base64-arraybuffer';
2-
import type { AttachmentData, LocalStorageAdapter } from '@powersync/common';
2+
import type { AttachmentData, StreamingLocalStorageAdapter } from '@powersync/common';
33

44
/**
55
* ReactNativeFileSystemStorageAdapter implements LocalStorageAdapter using @dr.pogodin/react-native-fs.
@@ -8,7 +8,7 @@ import type { AttachmentData, LocalStorageAdapter } from '@powersync/common';
88
* @experimental
99
* @alpha This is currently experimental and may change without a major version bump.
1010
*/
11-
export class ReactNativeFileSystemStorageAdapter implements LocalStorageAdapter {
11+
export class ReactNativeFileSystemStorageAdapter implements StreamingLocalStorageAdapter {
1212
private rnfs: typeof import('@dr.pogodin/react-native-fs');
1313
private storageDirectory: string;
1414

packages/common/etc/common.api.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ export interface AttachmentErrorHandler {
8080
export function attachmentFromSql(row: any): AttachmentRecord;
8181

8282
// @alpha
83-
export class AttachmentQueue {
84-
constructor(input: AttachmentQueueOptions);
83+
export class AttachmentQueue<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
84+
constructor(input: AttachmentQueueOptions<TLocal>);
8585
readonly archivedCacheLimit: number;
8686
// (undocumented)
8787
clearQueue(): Promise<void>;
@@ -94,12 +94,12 @@ export class AttachmentQueue {
9494
// (undocumented)
9595
expireCache(): Promise<void>;
9696
generateAttachmentId(): Promise<string>;
97-
readonly localStorage: LocalStorageAdapter;
97+
readonly localStorage: TLocal;
9898
readonly logger: PowerSyncLogger;
9999
saveFile(options: SaveAttachmentOptions & {
100100
data: AttachmentData;
101101
}): Promise<AttachmentRecord>;
102-
saveFileFromUri(options: SaveAttachmentOptions & {
102+
saveFileFromUri(this: AttachmentQueue<StreamingLocalStorageAdapter>, options: SaveAttachmentOptions & {
103103
localUri: string;
104104
}): Promise<AttachmentRecord>;
105105
startSync(): Promise<void>;
@@ -113,7 +113,7 @@ export class AttachmentQueue {
113113
}
114114

115115
// @alpha
116-
export type AttachmentQueueOptions = BaseAttachmentQueueOptions & ({
116+
export type AttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> = BaseAttachmentQueueOptions<TLocal> & ({
117117
remoteStorage: RemoteStorageAdapter;
118118
transportAdapter?: never;
119119
} | {
@@ -177,12 +177,12 @@ export interface AttachmentTransportAdapter {
177177
}
178178

179179
// @alpha
180-
export interface BaseAttachmentQueueOptions {
180+
export interface BaseAttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
181181
archivedCacheLimit?: number;
182182
db: CommonPowerSyncDatabase;
183183
downloadAttachments?: boolean;
184184
errorHandler?: AttachmentErrorHandler;
185-
localStorage: LocalStorageAdapter;
185+
localStorage: TLocal;
186186
logger?: PowerSyncLogger;
187187
syncIntervalMs?: number;
188188
syncThrottleDuration?: number;
@@ -663,7 +663,6 @@ export interface LocalStorageAdapter {
663663
getLocalUri(filename: string): string;
664664
initialize(): Promise<void>;
665665
makeDir(path: string): Promise<void>;
666-
moveFile?(sourceUri: string, targetUri: string): Promise<number>;
667666
readFile(filePath: string): Promise<ArrayBuffer>;
668667
rmDir(path: string): Promise<void>;
669668
saveFile(filePath: string, data: AttachmentData): Promise<number>;
@@ -1015,6 +1014,11 @@ export interface StandardWatchedQueryOptions<RowType> extends WatchedQueryOption
10151014
placeholderData?: RowType[];
10161015
}
10171016

1017+
// @alpha
1018+
export interface StreamingLocalStorageAdapter extends LocalStorageAdapter {
1019+
moveFile(sourceUri: string, targetUri: string): Promise<number>;
1020+
}
1021+
10181022
// Warning: (ae-forgotten-export) The symbol "JSONValue" needs to be exported by the entry point index.d.ts
10191023
//
10201024
// @public (undocumented)

packages/common/src/attachments/AttachmentQueue.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
77
import { AttachmentService } from './AttachmentService.js';
88
import { AttachmentTransportAdapter } from './AttachmentTransportAdapter.js';
99
import { BufferedAttachmentTransport } from './BufferedAttachmentTransport.js';
10-
import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js';
10+
import { AttachmentData, LocalStorageAdapter, StreamingLocalStorageAdapter } from './LocalStorageAdapter.js';
1111
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
1212
import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js';
1313
import { SyncingService } from './SyncingService.js';
@@ -20,15 +20,17 @@ import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js';
2020
* @experimental
2121
* @alpha This is currently experimental and may change without a major version bump.
2222
*/
23-
export interface BaseAttachmentQueueOptions {
23+
export interface BaseAttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
2424
/**
2525
* PowerSync database instance
2626
*/
2727
db: CommonPowerSyncDatabase;
2828
/**
29-
* Local storage adapter for file persistence
29+
* Local storage adapter for file persistence. Its type determines whether
30+
* {@link AttachmentQueue.saveFileFromUri} is available (a
31+
* {@link StreamingLocalStorageAdapter} enables it).
3032
*/
31-
localStorage: LocalStorageAdapter;
33+
localStorage: TLocal;
3234
/**
3335
* Callback for monitoring attachment changes in your data model
3436
*/
@@ -77,11 +79,12 @@ export interface BaseAttachmentQueueOptions {
7779
* @experimental
7880
* @alpha This is currently experimental and may change without a major version bump.
7981
*/
80-
export type AttachmentQueueOptions = BaseAttachmentQueueOptions &
81-
(
82-
| { remoteStorage: RemoteStorageAdapter; transportAdapter?: never }
83-
| { transportAdapter: AttachmentTransportAdapter; remoteStorage?: never }
84-
);
82+
export type AttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> =
83+
BaseAttachmentQueueOptions<TLocal> &
84+
(
85+
| { remoteStorage: RemoteStorageAdapter; transportAdapter?: never }
86+
| { transportAdapter: AttachmentTransportAdapter; remoteStorage?: never }
87+
);
8588

8689
/**
8790
* Fields shared by {@link AttachmentQueue.saveFile} and {@link AttachmentQueue.saveFileFromUri}.
@@ -116,15 +119,15 @@ type AttachmentSource = { kind: 'data'; data: AttachmentData } | { kind: 'uri';
116119
* @experimental
117120
* @alpha This is currently experimental and may change without a major version bump.
118121
*/
119-
export class AttachmentQueue {
122+
export class AttachmentQueue<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
120123
/** Timer for periodic synchronization operations */
121124
private periodicSyncTimer?: ReturnType<typeof setInterval>;
122125

123126
/** Service for synchronizing attachments between local and remote storage */
124127
private readonly syncingService: SyncingService;
125128

126129
/** Adapter for local file storage operations */
127-
readonly localStorage: LocalStorageAdapter;
130+
readonly localStorage: TLocal;
128131

129132
/**
130133
* Callback function to watch for changes in attachment references in your data model.
@@ -208,7 +211,7 @@ export class AttachmentQueue {
208211
downloadAttachments = true,
209212
archivedCacheLimit = 100,
210213
errorHandler
211-
}: AttachmentQueueOptions) {
214+
}: AttachmentQueueOptions<TLocal>) {
212215
this.db = db;
213216
this.syncLoopMutex = db.createMutex();
214217
this.localStorage = localStorage;
@@ -470,10 +473,13 @@ export class AttachmentQueue {
470473
if (source.kind === 'data') {
471474
size = await this.localStorage.saveFile(localUri, source.data);
472475
} else {
473-
if (!this.localStorage.moveFile) {
476+
// saveFileFromUri is only exposed for streaming-capable local adapters; guard at
477+
// runtime too for plain-JS callers.
478+
const localStorage = this.localStorage as Partial<StreamingLocalStorageAdapter>;
479+
if (!localStorage.moveFile) {
474480
throw new Error('The configured local storage adapter does not support moveFile, required by saveFileFromUri.');
475481
}
476-
size = await this.localStorage.moveFile(source.localUri, localUri);
482+
size = await localStorage.moveFile(source.localUri, localUri);
477483
}
478484

479485
const attachment: AttachmentRecord = {
@@ -516,11 +522,16 @@ export class AttachmentQueue {
516522
* (recordings, videos): it avoids reading the file into an `ArrayBuffer` just to write
517523
* it back to disk. Requires the local storage adapter to implement `moveFile`.
518524
*
525+
* Only available when the queue is configured with a {@link StreamingLocalStorageAdapter}
526+
* (one that implements `moveFile`).
527+
*
519528
* @param options - The existing file's `localUri` plus {@link SaveAttachmentOptions}
520529
* @returns Promise resolving to the created attachment record
521-
* @throws Error if the local storage adapter does not support moving files
522530
*/
523-
async saveFileFromUri(options: SaveAttachmentOptions & { localUri: string }): Promise<AttachmentRecord> {
531+
async saveFileFromUri(
532+
this: AttachmentQueue<StreamingLocalStorageAdapter>,
533+
options: SaveAttachmentOptions & { localUri: string }
534+
): Promise<AttachmentRecord> {
524535
return this.createUploadAttachment(options, { kind: 'uri', localUri: options.localUri });
525536
}
526537

packages/common/src/attachments/LocalStorageAdapter.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,6 @@ export interface LocalStorageAdapter {
3434
*/
3535
readFile(filePath: string): Promise<ArrayBuffer>;
3636

37-
/**
38-
* Moves a file into managed storage without loading it into memory.
39-
* Used by {@link AttachmentQueue.saveFileFromUri} to register a file that already
40-
* exists on disk. Overwrites any existing file at the target. When source and
41-
* target are the same path, this is a no-op that just reports the size.
42-
*
43-
* Optional: adapters that cannot move files natively may omit this, in which case
44-
* {@link AttachmentQueue.saveFileFromUri} is unavailable.
45-
* @param sourceUri - Path of the existing file
46-
* @param targetUri - Destination path within managed storage
47-
* @returns Number of bytes in the moved file
48-
*/
49-
moveFile?(sourceUri: string, targetUri: string): Promise<number>;
50-
5137
/**
5238
* Deletes the file at the given path.
5339
* @param filePath - Path where the file is stored
@@ -90,3 +76,23 @@ export interface LocalStorageAdapter {
9076
*/
9177
getLocalUri(filename: string): string;
9278
}
79+
80+
/**
81+
* A {@link LocalStorageAdapter} that can relocate a file into managed storage without
82+
* loading it into memory. Required for {@link AttachmentQueue.saveFileFromUri}; only
83+
* queues configured with a streaming-capable local adapter expose that method.
84+
*
85+
* @experimental
86+
* @alpha This is currently experimental and may change without a major version bump.
87+
*/
88+
export interface StreamingLocalStorageAdapter extends LocalStorageAdapter {
89+
/**
90+
* Moves a file into managed storage without loading it into memory.
91+
* Overwrites any existing file at the target. When source and target are the same
92+
* path, this is a no-op that just reports the size.
93+
* @param sourceUri - Path of the existing file
94+
* @param targetUri - Destination path within managed storage
95+
* @returns Number of bytes in the moved file
96+
*/
97+
moveFile(sourceUri: string, targetUri: string): Promise<number>;
98+
}

packages/node/src/attachments/NodeFileSystemAdapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { promises as fs } from 'fs';
22
import * as path from 'path';
3-
import { AttachmentData, EncodingType, LocalStorageAdapter } from '@powersync/common';
3+
import { AttachmentData, EncodingType, StreamingLocalStorageAdapter } from '@powersync/common';
44

55
/**
66
* NodeFileSystemAdapter implements LocalStorageAdapter using Node.js filesystem.
77
* Suitable for Node.js environments and Electron applications.
88
*/
9-
export class NodeFileSystemAdapter implements LocalStorageAdapter {
9+
export class NodeFileSystemAdapter implements StreamingLocalStorageAdapter {
1010
constructor(private storageDirectory: string = './user_data') {}
1111

1212
async initialize(): Promise<void> {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
build/
1+
build/
2+
.gradle/

0 commit comments

Comments
 (0)