@powersync/common 1.45.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 +4165 -4914
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +4155 -4915
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +1502 -422
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +1492 -423
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +622 -42
- 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/client/AbstractPowerSyncDatabase.d.ts +9 -2
- package/lib/client/AbstractPowerSyncDatabase.js +18 -5
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
- package/lib/client/sync/stream/AbstractRemote.js +41 -32
- package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +7 -12
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +10 -12
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
- package/lib/client/triggers/MemoryTriggerClaimManager.d.ts +6 -0
- package/lib/client/triggers/MemoryTriggerClaimManager.js +21 -0
- package/lib/client/triggers/MemoryTriggerClaimManager.js.map +1 -0
- package/lib/client/triggers/TriggerManager.d.ts +37 -0
- package/lib/client/triggers/TriggerManagerImpl.d.ts +24 -3
- package/lib/client/triggers/TriggerManagerImpl.js +133 -11
- package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
- package/lib/index.d.ts +13 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/DataStream.js +11 -2
- package/lib/utils/DataStream.js.map +1 -1
- package/lib/utils/mutex.d.ts +1 -1
- package/lib/utils/mutex.js.map +1 -1
- package/package.json +4 -3
- 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/client/AbstractPowerSyncDatabase.ts +21 -6
- package/src/client/sync/stream/AbstractRemote.ts +47 -35
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +11 -14
- package/src/client/triggers/MemoryTriggerClaimManager.ts +25 -0
- package/src/client/triggers/TriggerManager.ts +50 -6
- package/src/client/triggers/TriggerManagerImpl.ts +177 -13
- package/src/index.ts +14 -0
- package/src/utils/DataStream.ts +13 -2
- package/src/utils/mutex.ts +1 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js';
|
|
2
|
+
import { ILogger } from '../utils/Logger.js';
|
|
3
|
+
import { Transaction } from '../db/DBAdapter.js';
|
|
4
|
+
import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AttachmentContext provides database operations for managing attachment records.
|
|
8
|
+
*
|
|
9
|
+
* Provides methods to query, insert, update, and delete attachment records with
|
|
10
|
+
* proper transaction management through PowerSync.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export class AttachmentContext {
|
|
15
|
+
/** PowerSync database instance for executing queries */
|
|
16
|
+
db: AbstractPowerSyncDatabase;
|
|
17
|
+
|
|
18
|
+
/** Name of the database table storing attachment records */
|
|
19
|
+
tableName: string;
|
|
20
|
+
|
|
21
|
+
/** Logger instance for diagnostic information */
|
|
22
|
+
logger: ILogger;
|
|
23
|
+
|
|
24
|
+
/** Maximum number of archived attachments to keep before cleanup */
|
|
25
|
+
archivedCacheLimit: number = 100;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new AttachmentContext instance.
|
|
29
|
+
*
|
|
30
|
+
* @param db - PowerSync database instance
|
|
31
|
+
* @param tableName - Name of the table storing attachment records. Default: 'attachments'
|
|
32
|
+
* @param logger - Logger instance for diagnostic output
|
|
33
|
+
*/
|
|
34
|
+
constructor(
|
|
35
|
+
db: AbstractPowerSyncDatabase,
|
|
36
|
+
tableName: string = 'attachments',
|
|
37
|
+
logger: ILogger,
|
|
38
|
+
archivedCacheLimit: number
|
|
39
|
+
) {
|
|
40
|
+
this.db = db;
|
|
41
|
+
this.tableName = tableName;
|
|
42
|
+
this.logger = logger;
|
|
43
|
+
this.archivedCacheLimit = archivedCacheLimit;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Retrieves all active attachments that require synchronization.
|
|
48
|
+
* Active attachments include those queued for upload, download, or delete.
|
|
49
|
+
* Results are ordered by timestamp in ascending order.
|
|
50
|
+
*
|
|
51
|
+
* @returns Promise resolving to an array of active attachment records
|
|
52
|
+
*/
|
|
53
|
+
async getActiveAttachments(): Promise<AttachmentRecord[]> {
|
|
54
|
+
const attachments = await this.db.getAll(
|
|
55
|
+
/* sql */
|
|
56
|
+
`
|
|
57
|
+
SELECT
|
|
58
|
+
*
|
|
59
|
+
FROM
|
|
60
|
+
${this.tableName}
|
|
61
|
+
WHERE
|
|
62
|
+
state = ?
|
|
63
|
+
OR state = ?
|
|
64
|
+
OR state = ?
|
|
65
|
+
ORDER BY
|
|
66
|
+
timestamp ASC
|
|
67
|
+
`,
|
|
68
|
+
[AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return attachments.map(attachmentFromSql);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Retrieves all archived attachments.
|
|
76
|
+
*
|
|
77
|
+
* Archived attachments are no longer referenced but haven't been permanently deleted.
|
|
78
|
+
* These are candidates for cleanup operations to free up storage space.
|
|
79
|
+
*
|
|
80
|
+
* @returns Promise resolving to an array of archived attachment records
|
|
81
|
+
*/
|
|
82
|
+
async getArchivedAttachments(): Promise<AttachmentRecord[]> {
|
|
83
|
+
const attachments = await this.db.getAll(
|
|
84
|
+
/* sql */
|
|
85
|
+
`
|
|
86
|
+
SELECT
|
|
87
|
+
*
|
|
88
|
+
FROM
|
|
89
|
+
${this.tableName}
|
|
90
|
+
WHERE
|
|
91
|
+
state = ?
|
|
92
|
+
ORDER BY
|
|
93
|
+
timestamp ASC
|
|
94
|
+
`,
|
|
95
|
+
[AttachmentState.ARCHIVED]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return attachments.map(attachmentFromSql);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Retrieves all attachment records regardless of state.
|
|
103
|
+
* Results are ordered by timestamp in ascending order.
|
|
104
|
+
*
|
|
105
|
+
* @returns Promise resolving to an array of all attachment records
|
|
106
|
+
*/
|
|
107
|
+
async getAttachments(): Promise<AttachmentRecord[]> {
|
|
108
|
+
const attachments = await this.db.getAll(
|
|
109
|
+
/* sql */
|
|
110
|
+
`
|
|
111
|
+
SELECT
|
|
112
|
+
*
|
|
113
|
+
FROM
|
|
114
|
+
${this.tableName}
|
|
115
|
+
ORDER BY
|
|
116
|
+
timestamp ASC
|
|
117
|
+
`,
|
|
118
|
+
[]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return attachments.map(attachmentFromSql);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Inserts or updates an attachment record within an existing transaction.
|
|
126
|
+
*
|
|
127
|
+
* Performs an upsert operation (INSERT OR REPLACE). Must be called within
|
|
128
|
+
* an active database transaction context.
|
|
129
|
+
*
|
|
130
|
+
* @param attachment - The attachment record to upsert
|
|
131
|
+
* @param context - Active database transaction context
|
|
132
|
+
*/
|
|
133
|
+
async upsertAttachment(attachment: AttachmentRecord, context: Transaction): Promise<void> {
|
|
134
|
+
await context.execute(
|
|
135
|
+
/* sql */
|
|
136
|
+
`
|
|
137
|
+
INSERT
|
|
138
|
+
OR REPLACE INTO ${this.tableName} (
|
|
139
|
+
id,
|
|
140
|
+
filename,
|
|
141
|
+
local_uri,
|
|
142
|
+
size,
|
|
143
|
+
media_type,
|
|
144
|
+
timestamp,
|
|
145
|
+
state,
|
|
146
|
+
has_synced,
|
|
147
|
+
meta_data
|
|
148
|
+
)
|
|
149
|
+
VALUES
|
|
150
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
151
|
+
`,
|
|
152
|
+
[
|
|
153
|
+
attachment.id,
|
|
154
|
+
attachment.filename,
|
|
155
|
+
attachment.localUri || null,
|
|
156
|
+
attachment.size || null,
|
|
157
|
+
attachment.mediaType || null,
|
|
158
|
+
attachment.timestamp,
|
|
159
|
+
attachment.state,
|
|
160
|
+
attachment.hasSynced ? 1 : 0,
|
|
161
|
+
attachment.metaData || null
|
|
162
|
+
]
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getAttachment(id: string): Promise<AttachmentRecord | undefined> {
|
|
167
|
+
const attachment = await this.db.get(
|
|
168
|
+
/* sql */
|
|
169
|
+
`
|
|
170
|
+
SELECT
|
|
171
|
+
*
|
|
172
|
+
FROM
|
|
173
|
+
${this.tableName}
|
|
174
|
+
WHERE
|
|
175
|
+
id = ?
|
|
176
|
+
`,
|
|
177
|
+
[id]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return attachment ? attachmentFromSql(attachment) : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Permanently deletes an attachment record from the database.
|
|
185
|
+
*
|
|
186
|
+
* This operation removes the attachment record but does not delete
|
|
187
|
+
* the associated local or remote files. File deletion should be handled
|
|
188
|
+
* separately through the appropriate storage adapters.
|
|
189
|
+
*
|
|
190
|
+
* @param attachmentId - Unique identifier of the attachment to delete
|
|
191
|
+
*/
|
|
192
|
+
async deleteAttachment(attachmentId: string): Promise<void> {
|
|
193
|
+
await this.db.writeTransaction((tx) =>
|
|
194
|
+
tx.execute(
|
|
195
|
+
/* sql */
|
|
196
|
+
`
|
|
197
|
+
DELETE FROM ${this.tableName}
|
|
198
|
+
WHERE
|
|
199
|
+
id = ?
|
|
200
|
+
`,
|
|
201
|
+
[attachmentId]
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async clearQueue(): Promise<void> {
|
|
207
|
+
await this.db.writeTransaction((tx) => tx.execute(/* sql */ ` DELETE FROM ${this.tableName} `));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async deleteArchivedAttachments(callback?: (attachments: AttachmentRecord[]) => Promise<void>): Promise<boolean> {
|
|
211
|
+
const limit = 1000;
|
|
212
|
+
|
|
213
|
+
const results = await this.db.getAll(
|
|
214
|
+
/* sql */
|
|
215
|
+
`
|
|
216
|
+
SELECT
|
|
217
|
+
*
|
|
218
|
+
FROM
|
|
219
|
+
${this.tableName}
|
|
220
|
+
WHERE
|
|
221
|
+
state = ?
|
|
222
|
+
ORDER BY
|
|
223
|
+
timestamp DESC
|
|
224
|
+
LIMIT
|
|
225
|
+
?
|
|
226
|
+
OFFSET
|
|
227
|
+
?
|
|
228
|
+
`,
|
|
229
|
+
[AttachmentState.ARCHIVED, limit, this.archivedCacheLimit]
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const archivedAttachments = results.map(attachmentFromSql);
|
|
233
|
+
if (archivedAttachments.length === 0) return false;
|
|
234
|
+
|
|
235
|
+
await callback?.(archivedAttachments);
|
|
236
|
+
this.logger.info(
|
|
237
|
+
`Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.`
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const ids = archivedAttachments.map((attachment) => attachment.id);
|
|
241
|
+
|
|
242
|
+
await this.db.execute(
|
|
243
|
+
/* sql */
|
|
244
|
+
`
|
|
245
|
+
DELETE FROM ${this.tableName}
|
|
246
|
+
WHERE
|
|
247
|
+
id IN (
|
|
248
|
+
SELECT
|
|
249
|
+
json_each.value
|
|
250
|
+
FROM
|
|
251
|
+
json_each (?)
|
|
252
|
+
);
|
|
253
|
+
`,
|
|
254
|
+
[JSON.stringify(ids)]
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
this.logger.info(`Deleted ${archivedAttachments.length} archived attachments`);
|
|
258
|
+
return archivedAttachments.length < limit;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Saves multiple attachment records in a single transaction.
|
|
263
|
+
*
|
|
264
|
+
* All updates are saved in a single batch after processing.
|
|
265
|
+
* If the attachments array is empty, no database operations are performed.
|
|
266
|
+
*
|
|
267
|
+
* @param attachments - Array of attachment records to save
|
|
268
|
+
*/
|
|
269
|
+
async saveAttachments(attachments: AttachmentRecord[]): Promise<void> {
|
|
270
|
+
if (attachments.length === 0) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await this.db.writeTransaction(async (tx) => {
|
|
274
|
+
for (const attachment of attachments) {
|
|
275
|
+
await this.upsertAttachment(attachment, tx);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AttachmentRecord } from './Schema.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SyncErrorHandler provides custom error handling for attachment sync operations.
|
|
5
|
+
* Implementations determine whether failed operations should be retried or archived.
|
|
6
|
+
*
|
|
7
|
+
* @experimental
|
|
8
|
+
* @alpha This is currently experimental and may change without a major version bump.
|
|
9
|
+
*/
|
|
10
|
+
export interface AttachmentErrorHandler {
|
|
11
|
+
/**
|
|
12
|
+
* Handles a download error for a specific attachment.
|
|
13
|
+
* @param attachment The attachment that failed to download
|
|
14
|
+
* @param error The error encountered during the download
|
|
15
|
+
* @returns `true` to retry the operation, `false` to archive the attachment
|
|
16
|
+
*/
|
|
17
|
+
onDownloadError(attachment: AttachmentRecord, error: unknown): Promise<boolean>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles an upload error for a specific attachment.
|
|
21
|
+
* @param attachment The attachment that failed to upload
|
|
22
|
+
* @param error The error encountered during the upload
|
|
23
|
+
* @returns `true` to retry the operation, `false` to archive the attachment
|
|
24
|
+
*/
|
|
25
|
+
onUploadError(attachment: AttachmentRecord, error: unknown): Promise<boolean>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handles a delete error for a specific attachment.
|
|
29
|
+
* @param attachment The attachment that failed to delete
|
|
30
|
+
* @param error The error encountered during the delete
|
|
31
|
+
* @returns `true` to retry the operation, `false` to archive the attachment
|
|
32
|
+
*/
|
|
33
|
+
onDeleteError(attachment: AttachmentRecord, error: unknown): Promise<boolean>;
|
|
34
|
+
}
|