@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.
Files changed (79) hide show
  1. package/README.md +5 -1
  2. package/dist/bundle.cjs +4165 -4914
  3. package/dist/bundle.cjs.map +1 -1
  4. package/dist/bundle.mjs +4155 -4915
  5. package/dist/bundle.mjs.map +1 -1
  6. package/dist/bundle.node.cjs +1502 -422
  7. package/dist/bundle.node.cjs.map +1 -1
  8. package/dist/bundle.node.mjs +1492 -423
  9. package/dist/bundle.node.mjs.map +1 -1
  10. package/dist/index.d.cts +622 -42
  11. package/lib/attachments/AttachmentContext.d.ts +86 -0
  12. package/lib/attachments/AttachmentContext.js +229 -0
  13. package/lib/attachments/AttachmentContext.js.map +1 -0
  14. package/lib/attachments/AttachmentErrorHandler.d.ts +31 -0
  15. package/lib/attachments/AttachmentErrorHandler.js +2 -0
  16. package/lib/attachments/AttachmentErrorHandler.js.map +1 -0
  17. package/lib/attachments/AttachmentQueue.d.ts +149 -0
  18. package/lib/attachments/AttachmentQueue.js +362 -0
  19. package/lib/attachments/AttachmentQueue.js.map +1 -0
  20. package/lib/attachments/AttachmentService.d.ts +29 -0
  21. package/lib/attachments/AttachmentService.js +56 -0
  22. package/lib/attachments/AttachmentService.js.map +1 -0
  23. package/lib/attachments/LocalStorageAdapter.d.ts +62 -0
  24. package/lib/attachments/LocalStorageAdapter.js +6 -0
  25. package/lib/attachments/LocalStorageAdapter.js.map +1 -0
  26. package/lib/attachments/RemoteStorageAdapter.d.ts +27 -0
  27. package/lib/attachments/RemoteStorageAdapter.js +2 -0
  28. package/lib/attachments/RemoteStorageAdapter.js.map +1 -0
  29. package/lib/attachments/Schema.d.ts +50 -0
  30. package/lib/attachments/Schema.js +62 -0
  31. package/lib/attachments/Schema.js.map +1 -0
  32. package/lib/attachments/SyncingService.d.ts +62 -0
  33. package/lib/attachments/SyncingService.js +168 -0
  34. package/lib/attachments/SyncingService.js.map +1 -0
  35. package/lib/attachments/WatchedAttachmentItem.d.ts +17 -0
  36. package/lib/attachments/WatchedAttachmentItem.js +2 -0
  37. package/lib/attachments/WatchedAttachmentItem.js.map +1 -0
  38. package/lib/client/AbstractPowerSyncDatabase.d.ts +9 -2
  39. package/lib/client/AbstractPowerSyncDatabase.js +18 -5
  40. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  41. package/lib/client/sync/stream/AbstractRemote.js +41 -32
  42. package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
  43. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +7 -12
  44. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +10 -12
  45. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
  46. package/lib/client/triggers/MemoryTriggerClaimManager.d.ts +6 -0
  47. package/lib/client/triggers/MemoryTriggerClaimManager.js +21 -0
  48. package/lib/client/triggers/MemoryTriggerClaimManager.js.map +1 -0
  49. package/lib/client/triggers/TriggerManager.d.ts +37 -0
  50. package/lib/client/triggers/TriggerManagerImpl.d.ts +24 -3
  51. package/lib/client/triggers/TriggerManagerImpl.js +133 -11
  52. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
  53. package/lib/index.d.ts +13 -0
  54. package/lib/index.js +13 -0
  55. package/lib/index.js.map +1 -1
  56. package/lib/utils/DataStream.js +11 -2
  57. package/lib/utils/DataStream.js.map +1 -1
  58. package/lib/utils/mutex.d.ts +1 -1
  59. package/lib/utils/mutex.js.map +1 -1
  60. package/package.json +4 -3
  61. package/src/attachments/AttachmentContext.ts +279 -0
  62. package/src/attachments/AttachmentErrorHandler.ts +34 -0
  63. package/src/attachments/AttachmentQueue.ts +472 -0
  64. package/src/attachments/AttachmentService.ts +62 -0
  65. package/src/attachments/LocalStorageAdapter.ts +72 -0
  66. package/src/attachments/README.md +718 -0
  67. package/src/attachments/RemoteStorageAdapter.ts +30 -0
  68. package/src/attachments/Schema.ts +87 -0
  69. package/src/attachments/SyncingService.ts +193 -0
  70. package/src/attachments/WatchedAttachmentItem.ts +19 -0
  71. package/src/client/AbstractPowerSyncDatabase.ts +21 -6
  72. package/src/client/sync/stream/AbstractRemote.ts +47 -35
  73. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +11 -14
  74. package/src/client/triggers/MemoryTriggerClaimManager.ts +25 -0
  75. package/src/client/triggers/TriggerManager.ts +50 -6
  76. package/src/client/triggers/TriggerManagerImpl.ts +177 -13
  77. package/src/index.ts +14 -0
  78. package/src/utils/DataStream.ts +13 -2
  79. 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
+ }