@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,30 @@
|
|
|
1
|
+
import { AttachmentRecord } from './Schema.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RemoteStorageAdapter defines the interface for remote storage operations.
|
|
5
|
+
* Implementations handle uploading, downloading, and deleting files from remote storage.
|
|
6
|
+
*
|
|
7
|
+
* @experimental
|
|
8
|
+
* @alpha This is currently experimental and may change without a major version bump.
|
|
9
|
+
*/
|
|
10
|
+
export interface RemoteStorageAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* Uploads a file to remote storage.
|
|
13
|
+
* @param fileData The binary content of the file to upload
|
|
14
|
+
* @param attachment The associated attachment metadata
|
|
15
|
+
*/
|
|
16
|
+
uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Downloads a file from remote storage.
|
|
20
|
+
* @param attachment The attachment describing the file to download
|
|
21
|
+
* @returns The binary data of the downloaded file
|
|
22
|
+
*/
|
|
23
|
+
downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Deletes a file from remote storage.
|
|
27
|
+
* @param attachment The attachment describing the file to delete
|
|
28
|
+
*/
|
|
29
|
+
deleteFile(attachment: AttachmentRecord): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { column } from '../db/schema/Column.js';
|
|
2
|
+
import { Table } from '../db/schema/Table.js';
|
|
3
|
+
import { TableV2Options } from '../db/schema/Table.js';
|
|
4
|
+
|
|
5
|
+
export const ATTACHMENT_TABLE = 'attachments';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AttachmentRecord represents an attachment in the local database.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
export interface AttachmentRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
localUri?: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
mediaType?: string;
|
|
18
|
+
timestamp?: number;
|
|
19
|
+
metaData?: string;
|
|
20
|
+
hasSynced?: boolean;
|
|
21
|
+
state: AttachmentState;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maps a database row to an AttachmentRecord.
|
|
26
|
+
*
|
|
27
|
+
* @param row - The database row object
|
|
28
|
+
* @returns The corresponding AttachmentRecord
|
|
29
|
+
*
|
|
30
|
+
* @experimental
|
|
31
|
+
*/
|
|
32
|
+
export function attachmentFromSql(row: any): AttachmentRecord {
|
|
33
|
+
return {
|
|
34
|
+
id: row.id,
|
|
35
|
+
filename: row.filename,
|
|
36
|
+
localUri: row.local_uri,
|
|
37
|
+
size: row.size,
|
|
38
|
+
mediaType: row.media_type,
|
|
39
|
+
timestamp: row.timestamp,
|
|
40
|
+
metaData: row.meta_data,
|
|
41
|
+
hasSynced: row.has_synced === 1,
|
|
42
|
+
state: row.state
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* AttachmentState represents the current synchronization state of an attachment.
|
|
48
|
+
*
|
|
49
|
+
* @experimental
|
|
50
|
+
*/
|
|
51
|
+
export enum AttachmentState {
|
|
52
|
+
QUEUED_UPLOAD = 0, // Attachment to be uploaded
|
|
53
|
+
QUEUED_DOWNLOAD = 1, // Attachment to be downloaded
|
|
54
|
+
QUEUED_DELETE = 2, // Attachment to be deleted
|
|
55
|
+
SYNCED = 3, // Attachment has been synced
|
|
56
|
+
ARCHIVED = 4 // Attachment has been orphaned, i.e. the associated record has been deleted
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AttachmentTableOptions extends Omit<TableV2Options, 'name' | 'columns'> {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* AttachmentTable defines the schema for the attachment queue table.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export class AttachmentTable extends Table {
|
|
67
|
+
constructor(options?: AttachmentTableOptions) {
|
|
68
|
+
super(
|
|
69
|
+
{
|
|
70
|
+
filename: column.text,
|
|
71
|
+
local_uri: column.text,
|
|
72
|
+
timestamp: column.integer,
|
|
73
|
+
size: column.integer,
|
|
74
|
+
media_type: column.text,
|
|
75
|
+
state: column.integer, // Corresponds to AttachmentState
|
|
76
|
+
has_synced: column.integer,
|
|
77
|
+
meta_data: column.text
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
...options,
|
|
81
|
+
viewName: options?.viewName ?? ATTACHMENT_TABLE,
|
|
82
|
+
localOnly: true,
|
|
83
|
+
insertOnly: false
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ILogger } from '../utils/Logger.js';
|
|
2
|
+
import { AttachmentService } from './AttachmentService.js';
|
|
3
|
+
import { LocalStorageAdapter } from './LocalStorageAdapter.js';
|
|
4
|
+
import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
|
|
5
|
+
import { AttachmentRecord, AttachmentState } from './Schema.js';
|
|
6
|
+
import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
|
|
7
|
+
import { AttachmentContext } from './AttachmentContext.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Orchestrates attachment synchronization between local and remote storage.
|
|
11
|
+
* Handles uploads, downloads, deletions, and state transitions.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export class SyncingService {
|
|
16
|
+
private attachmentService: AttachmentService;
|
|
17
|
+
private localStorage: LocalStorageAdapter;
|
|
18
|
+
private remoteStorage: RemoteStorageAdapter;
|
|
19
|
+
private logger: ILogger;
|
|
20
|
+
private errorHandler?: AttachmentErrorHandler;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
attachmentService: AttachmentService,
|
|
24
|
+
localStorage: LocalStorageAdapter,
|
|
25
|
+
remoteStorage: RemoteStorageAdapter,
|
|
26
|
+
logger: ILogger,
|
|
27
|
+
errorHandler?: AttachmentErrorHandler
|
|
28
|
+
) {
|
|
29
|
+
this.attachmentService = attachmentService;
|
|
30
|
+
this.localStorage = localStorage;
|
|
31
|
+
this.remoteStorage = remoteStorage;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
this.errorHandler = errorHandler;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Processes attachments based on their state (upload, download, or delete).
|
|
38
|
+
* All updates are saved in a single batch after processing.
|
|
39
|
+
*
|
|
40
|
+
* @param attachments - Array of attachment records to process
|
|
41
|
+
* @param context - Attachment context for database operations
|
|
42
|
+
* @returns Promise that resolves when all attachments have been processed and saved
|
|
43
|
+
*/
|
|
44
|
+
async processAttachments(attachments: AttachmentRecord[], context: AttachmentContext): Promise<void> {
|
|
45
|
+
const updatedAttachments: AttachmentRecord[] = [];
|
|
46
|
+
for (const attachment of attachments) {
|
|
47
|
+
switch (attachment.state) {
|
|
48
|
+
case AttachmentState.QUEUED_UPLOAD:
|
|
49
|
+
const uploaded = await this.uploadAttachment(attachment);
|
|
50
|
+
updatedAttachments.push(uploaded);
|
|
51
|
+
break;
|
|
52
|
+
case AttachmentState.QUEUED_DOWNLOAD:
|
|
53
|
+
const downloaded = await this.downloadAttachment(attachment);
|
|
54
|
+
updatedAttachments.push(downloaded);
|
|
55
|
+
break;
|
|
56
|
+
case AttachmentState.QUEUED_DELETE:
|
|
57
|
+
const deleted = await this.deleteAttachment(attachment);
|
|
58
|
+
updatedAttachments.push(deleted);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await context.saveAttachments(updatedAttachments);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Uploads an attachment from local storage to remote storage.
|
|
71
|
+
* On success, marks as SYNCED. On failure, defers to error handler or archives.
|
|
72
|
+
*
|
|
73
|
+
* @param attachment - The attachment record to upload
|
|
74
|
+
* @returns Updated attachment record with new state
|
|
75
|
+
* @throws Error if the attachment has no localUri
|
|
76
|
+
*/
|
|
77
|
+
async uploadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
78
|
+
this.logger.info(`Uploading attachment ${attachment.filename}`);
|
|
79
|
+
try {
|
|
80
|
+
if (attachment.localUri == null) {
|
|
81
|
+
throw new Error(`No localUri for attachment ${attachment.id}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fileBlob = await this.localStorage.readFile(attachment.localUri);
|
|
85
|
+
await this.remoteStorage.uploadFile(fileBlob, attachment);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...attachment,
|
|
89
|
+
state: AttachmentState.SYNCED,
|
|
90
|
+
hasSynced: true
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const shouldRetry = (await this.errorHandler?.onUploadError(attachment, error)) ?? true;
|
|
94
|
+
if (!shouldRetry) {
|
|
95
|
+
return {
|
|
96
|
+
...attachment,
|
|
97
|
+
state: AttachmentState.ARCHIVED
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return attachment;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Downloads an attachment from remote storage to local storage.
|
|
107
|
+
* Retrieves the file, converts to base64, and saves locally.
|
|
108
|
+
* On success, marks as SYNCED. On failure, defers to error handler or archives.
|
|
109
|
+
*
|
|
110
|
+
* @param attachment - The attachment record to download
|
|
111
|
+
* @returns Updated attachment record with local URI and new state
|
|
112
|
+
*/
|
|
113
|
+
async downloadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
114
|
+
this.logger.info(`Downloading attachment ${attachment.filename}`);
|
|
115
|
+
try {
|
|
116
|
+
const fileData = await this.remoteStorage.downloadFile(attachment);
|
|
117
|
+
|
|
118
|
+
const localUri = this.localStorage.getLocalUri(attachment.filename);
|
|
119
|
+
await this.localStorage.saveFile(localUri, fileData);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
...attachment,
|
|
123
|
+
state: AttachmentState.SYNCED,
|
|
124
|
+
localUri: localUri,
|
|
125
|
+
hasSynced: true
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const shouldRetry = (await this.errorHandler?.onDownloadError(attachment, error)) ?? true;
|
|
129
|
+
if (!shouldRetry) {
|
|
130
|
+
return {
|
|
131
|
+
...attachment,
|
|
132
|
+
state: AttachmentState.ARCHIVED
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return attachment;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Deletes an attachment from both remote and local storage.
|
|
142
|
+
* Removes the remote file, local file (if exists), and the attachment record.
|
|
143
|
+
* On failure, defers to error handler or archives.
|
|
144
|
+
*
|
|
145
|
+
* @param attachment - The attachment record to delete
|
|
146
|
+
* @returns Updated attachment record
|
|
147
|
+
*/
|
|
148
|
+
async deleteAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
149
|
+
try {
|
|
150
|
+
await this.remoteStorage.deleteFile(attachment);
|
|
151
|
+
if (attachment.localUri) {
|
|
152
|
+
await this.localStorage.deleteFile(attachment.localUri);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await this.attachmentService.withContext(async (ctx) => {
|
|
156
|
+
await ctx.deleteAttachment(attachment.id);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...attachment,
|
|
161
|
+
state: AttachmentState.ARCHIVED
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const shouldRetry = (await this.errorHandler?.onDeleteError(attachment, error)) ?? true;
|
|
165
|
+
if (!shouldRetry) {
|
|
166
|
+
return {
|
|
167
|
+
...attachment,
|
|
168
|
+
state: AttachmentState.ARCHIVED
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return attachment;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Performs cleanup of archived attachments by removing their local files and records.
|
|
178
|
+
* Errors during local file deletion are logged but do not prevent record deletion.
|
|
179
|
+
*/
|
|
180
|
+
async deleteArchivedAttachments(context: AttachmentContext): Promise<boolean> {
|
|
181
|
+
return await context.deleteArchivedAttachments(async (archivedAttachments) => {
|
|
182
|
+
for (const attachment of archivedAttachments) {
|
|
183
|
+
if (attachment.localUri) {
|
|
184
|
+
try {
|
|
185
|
+
await this.localStorage.deleteFile(attachment.localUri);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logger.error('Error deleting local file for archived attachment', error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WatchedAttachmentItem represents an attachment reference in your application's data model.
|
|
3
|
+
* Use either filename OR fileExtension (not both).
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
*/
|
|
7
|
+
export type WatchedAttachmentItem =
|
|
8
|
+
| {
|
|
9
|
+
id: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
fileExtension?: never;
|
|
12
|
+
metaData?: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
id: string;
|
|
16
|
+
fileExtension: string;
|
|
17
|
+
filename?: never;
|
|
18
|
+
metaData?: string;
|
|
19
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export * from './attachments/AttachmentContext.js';
|
|
2
|
+
export * from './attachments/AttachmentErrorHandler.js';
|
|
3
|
+
export * from './attachments/AttachmentQueue.js';
|
|
4
|
+
export * from './attachments/AttachmentService.js';
|
|
5
|
+
export * from './attachments/LocalStorageAdapter.js';
|
|
6
|
+
export * from './attachments/RemoteStorageAdapter.js';
|
|
7
|
+
export * from './attachments/Schema.js';
|
|
8
|
+
export * from './attachments/SyncingService.js';
|
|
9
|
+
export * from './attachments/WatchedAttachmentItem.js';
|
|
10
|
+
|
|
1
11
|
export * from './client/AbstractPowerSyncDatabase.js';
|
|
2
12
|
export * from './client/AbstractPowerSyncOpenFactory.js';
|
|
3
13
|
export { compilableQueryWatch, CompilableQueryWatchHandler } from './client/compilableQueryWatch.js';
|
|
@@ -51,6 +61,7 @@ export * from './utils/BaseObserver.js';
|
|
|
51
61
|
export * from './utils/ControlledExecutor.js';
|
|
52
62
|
export * from './utils/DataStream.js';
|
|
53
63
|
export * from './utils/Logger.js';
|
|
64
|
+
export * from './utils/mutex.js';
|
|
54
65
|
export * from './utils/parseQuery.js';
|
|
55
66
|
|
|
56
67
|
export * from './types/types.js';
|
package/src/utils/mutex.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Mutex } from 'async-mutex';
|
|
|
6
6
|
export async function mutexRunExclusive<T>(
|
|
7
7
|
mutex: Mutex,
|
|
8
8
|
callback: () => Promise<T>,
|
|
9
|
-
options?: { timeoutMs
|
|
9
|
+
options?: { timeoutMs?: number }
|
|
10
10
|
): Promise<T> {
|
|
11
11
|
return new Promise((resolve, reject) => {
|
|
12
12
|
const timeout = options?.timeoutMs;
|