@powersync/common 1.46.0 → 1.48.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 (71) hide show
  1. package/README.md +5 -1
  2. package/dist/bundle.cjs +1298 -395
  3. package/dist/bundle.cjs.map +1 -1
  4. package/dist/bundle.mjs +1291 -395
  5. package/dist/bundle.mjs.map +1 -1
  6. package/dist/bundle.node.cjs +1298 -395
  7. package/dist/bundle.node.cjs.map +1 -1
  8. package/dist/bundle.node.mjs +1291 -395
  9. package/dist/bundle.node.mjs.map +1 -1
  10. package/dist/index.d.cts +652 -106
  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/db/schema/RawTable.d.ts +61 -26
  39. package/lib/db/schema/RawTable.js +1 -32
  40. package/lib/db/schema/RawTable.js.map +1 -1
  41. package/lib/db/schema/Schema.d.ts +14 -7
  42. package/lib/db/schema/Schema.js +25 -3
  43. package/lib/db/schema/Schema.js.map +1 -1
  44. package/lib/db/schema/Table.d.ts +13 -8
  45. package/lib/db/schema/Table.js +3 -8
  46. package/lib/db/schema/Table.js.map +1 -1
  47. package/lib/db/schema/internal.d.ts +12 -0
  48. package/lib/db/schema/internal.js +15 -0
  49. package/lib/db/schema/internal.js.map +1 -0
  50. package/lib/index.d.ts +11 -1
  51. package/lib/index.js +10 -1
  52. package/lib/index.js.map +1 -1
  53. package/lib/utils/mutex.d.ts +1 -1
  54. package/lib/utils/mutex.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/attachments/AttachmentContext.ts +279 -0
  57. package/src/attachments/AttachmentErrorHandler.ts +34 -0
  58. package/src/attachments/AttachmentQueue.ts +472 -0
  59. package/src/attachments/AttachmentService.ts +62 -0
  60. package/src/attachments/LocalStorageAdapter.ts +72 -0
  61. package/src/attachments/README.md +718 -0
  62. package/src/attachments/RemoteStorageAdapter.ts +30 -0
  63. package/src/attachments/Schema.ts +87 -0
  64. package/src/attachments/SyncingService.ts +193 -0
  65. package/src/attachments/WatchedAttachmentItem.ts +19 -0
  66. package/src/db/schema/RawTable.ts +66 -31
  67. package/src/db/schema/Schema.ts +27 -2
  68. package/src/db/schema/Table.ts +11 -11
  69. package/src/db/schema/internal.ts +17 -0
  70. package/src/index.ts +12 -1
  71. package/src/utils/mutex.ts +1 -1
package/dist/bundle.cjs CHANGED
@@ -2,6 +2,1233 @@
2
2
 
3
3
  var asyncMutex = require('async-mutex');
4
4
 
5
+ // https://www.sqlite.org/lang_expr.html#castexpr
6
+ exports.ColumnType = void 0;
7
+ (function (ColumnType) {
8
+ ColumnType["TEXT"] = "TEXT";
9
+ ColumnType["INTEGER"] = "INTEGER";
10
+ ColumnType["REAL"] = "REAL";
11
+ })(exports.ColumnType || (exports.ColumnType = {}));
12
+ const text = {
13
+ type: exports.ColumnType.TEXT
14
+ };
15
+ const integer = {
16
+ type: exports.ColumnType.INTEGER
17
+ };
18
+ const real = {
19
+ type: exports.ColumnType.REAL
20
+ };
21
+ // powersync-sqlite-core limits the number of column per table to 1999, due to internal SQLite limits.
22
+ // In earlier versions this was limited to 63.
23
+ const MAX_AMOUNT_OF_COLUMNS = 1999;
24
+ const column = {
25
+ text,
26
+ integer,
27
+ real
28
+ };
29
+ class Column {
30
+ options;
31
+ constructor(options) {
32
+ this.options = options;
33
+ }
34
+ get name() {
35
+ return this.options.name;
36
+ }
37
+ get type() {
38
+ return this.options.type;
39
+ }
40
+ toJSON() {
41
+ return {
42
+ name: this.name,
43
+ type: this.type
44
+ };
45
+ }
46
+ }
47
+
48
+ const DEFAULT_INDEX_COLUMN_OPTIONS = {
49
+ ascending: true
50
+ };
51
+ class IndexedColumn {
52
+ options;
53
+ static createAscending(column) {
54
+ return new IndexedColumn({
55
+ name: column,
56
+ ascending: true
57
+ });
58
+ }
59
+ constructor(options) {
60
+ this.options = { ...DEFAULT_INDEX_COLUMN_OPTIONS, ...options };
61
+ }
62
+ get name() {
63
+ return this.options.name;
64
+ }
65
+ get ascending() {
66
+ return this.options.ascending;
67
+ }
68
+ toJSON(table) {
69
+ return {
70
+ name: this.name,
71
+ ascending: this.ascending,
72
+ type: table.columns.find((column) => column.name === this.name)?.type ?? exports.ColumnType.TEXT
73
+ };
74
+ }
75
+ }
76
+
77
+ const DEFAULT_INDEX_OPTIONS = {
78
+ columns: []
79
+ };
80
+ class Index {
81
+ options;
82
+ static createAscending(options, columnNames) {
83
+ return new Index({
84
+ ...options,
85
+ columns: columnNames.map((name) => IndexedColumn.createAscending(name))
86
+ });
87
+ }
88
+ constructor(options) {
89
+ this.options = options;
90
+ this.options = { ...DEFAULT_INDEX_OPTIONS, ...options };
91
+ }
92
+ get name() {
93
+ return this.options.name;
94
+ }
95
+ get columns() {
96
+ return this.options.columns ?? [];
97
+ }
98
+ toJSON(table) {
99
+ return {
100
+ name: this.name,
101
+ columns: this.columns.map((c) => c.toJSON(table))
102
+ };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @internal Not exported from `index.ts`.
108
+ */
109
+ function encodeTableOptions(options) {
110
+ const trackPrevious = options.trackPrevious;
111
+ return {
112
+ local_only: options.localOnly,
113
+ insert_only: options.insertOnly,
114
+ include_old: trackPrevious && (trackPrevious.columns ?? true),
115
+ include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
116
+ include_metadata: options.trackMetadata,
117
+ ignore_empty_update: options.ignoreEmptyUpdates
118
+ };
119
+ }
120
+
121
+ const DEFAULT_TABLE_OPTIONS = {
122
+ indexes: [],
123
+ insertOnly: false,
124
+ localOnly: false,
125
+ trackPrevious: false,
126
+ trackMetadata: false,
127
+ ignoreEmptyUpdates: false
128
+ };
129
+ const InvalidSQLCharacters = /["'%,.#\s[\]]/;
130
+ class Table {
131
+ options;
132
+ _mappedColumns;
133
+ static createLocalOnly(options) {
134
+ return new Table({ ...options, localOnly: true, insertOnly: false });
135
+ }
136
+ static createInsertOnly(options) {
137
+ return new Table({ ...options, localOnly: false, insertOnly: true });
138
+ }
139
+ /**
140
+ * Create a table.
141
+ * @deprecated This was only only included for TableV2 and is no longer necessary.
142
+ * Prefer to use new Table() directly.
143
+ *
144
+ * TODO remove in the next major release.
145
+ */
146
+ static createTable(name, table) {
147
+ return new Table({
148
+ name,
149
+ columns: table.columns,
150
+ indexes: table.indexes,
151
+ localOnly: table.options.localOnly,
152
+ insertOnly: table.options.insertOnly,
153
+ viewName: table.options.viewName
154
+ });
155
+ }
156
+ constructor(optionsOrColumns, v2Options) {
157
+ if (this.isTableV1(optionsOrColumns)) {
158
+ this.initTableV1(optionsOrColumns);
159
+ }
160
+ else {
161
+ this.initTableV2(optionsOrColumns, v2Options);
162
+ }
163
+ }
164
+ copyWithName(name) {
165
+ return new Table({
166
+ ...this.options,
167
+ name
168
+ });
169
+ }
170
+ isTableV1(arg) {
171
+ return 'columns' in arg && Array.isArray(arg.columns);
172
+ }
173
+ initTableV1(options) {
174
+ this.options = {
175
+ ...options,
176
+ indexes: options.indexes || []
177
+ };
178
+ this.applyDefaultOptions();
179
+ }
180
+ initTableV2(columns, options) {
181
+ const convertedColumns = Object.entries(columns).map(([name, columnInfo]) => new Column({ name, type: columnInfo.type }));
182
+ const convertedIndexes = Object.entries(options?.indexes ?? {}).map(([name, columnNames]) => new Index({
183
+ name,
184
+ columns: columnNames.map((name) => new IndexedColumn({
185
+ name: name.replace(/^-/, ''),
186
+ ascending: !name.startsWith('-')
187
+ }))
188
+ }));
189
+ this.options = {
190
+ name: '',
191
+ columns: convertedColumns,
192
+ indexes: convertedIndexes,
193
+ viewName: options?.viewName,
194
+ insertOnly: options?.insertOnly,
195
+ localOnly: options?.localOnly,
196
+ trackPrevious: options?.trackPrevious,
197
+ trackMetadata: options?.trackMetadata,
198
+ ignoreEmptyUpdates: options?.ignoreEmptyUpdates
199
+ };
200
+ this.applyDefaultOptions();
201
+ this._mappedColumns = columns;
202
+ }
203
+ applyDefaultOptions() {
204
+ this.options.insertOnly ??= DEFAULT_TABLE_OPTIONS.insertOnly;
205
+ this.options.localOnly ??= DEFAULT_TABLE_OPTIONS.localOnly;
206
+ this.options.trackPrevious ??= DEFAULT_TABLE_OPTIONS.trackPrevious;
207
+ this.options.trackMetadata ??= DEFAULT_TABLE_OPTIONS.trackMetadata;
208
+ this.options.ignoreEmptyUpdates ??= DEFAULT_TABLE_OPTIONS.ignoreEmptyUpdates;
209
+ }
210
+ get name() {
211
+ return this.options.name;
212
+ }
213
+ get viewNameOverride() {
214
+ return this.options.viewName;
215
+ }
216
+ get viewName() {
217
+ return this.viewNameOverride ?? this.name;
218
+ }
219
+ get columns() {
220
+ return this.options.columns;
221
+ }
222
+ get columnMap() {
223
+ return (this._mappedColumns ??
224
+ this.columns.reduce((hash, column) => {
225
+ hash[column.name] = { type: column.type ?? exports.ColumnType.TEXT };
226
+ return hash;
227
+ }, {}));
228
+ }
229
+ get indexes() {
230
+ return this.options.indexes ?? [];
231
+ }
232
+ get localOnly() {
233
+ return this.options.localOnly;
234
+ }
235
+ get insertOnly() {
236
+ return this.options.insertOnly;
237
+ }
238
+ get trackPrevious() {
239
+ return this.options.trackPrevious;
240
+ }
241
+ get trackMetadata() {
242
+ return this.options.trackMetadata;
243
+ }
244
+ get ignoreEmptyUpdates() {
245
+ return this.options.ignoreEmptyUpdates;
246
+ }
247
+ get internalName() {
248
+ if (this.options.localOnly) {
249
+ return `ps_data_local__${this.name}`;
250
+ }
251
+ return `ps_data__${this.name}`;
252
+ }
253
+ get validName() {
254
+ if (InvalidSQLCharacters.test(this.name)) {
255
+ return false;
256
+ }
257
+ if (this.viewNameOverride != null && InvalidSQLCharacters.test(this.viewNameOverride)) {
258
+ return false;
259
+ }
260
+ return true;
261
+ }
262
+ validate() {
263
+ if (InvalidSQLCharacters.test(this.name)) {
264
+ throw new Error(`Invalid characters in table name: ${this.name}`);
265
+ }
266
+ if (this.viewNameOverride && InvalidSQLCharacters.test(this.viewNameOverride)) {
267
+ throw new Error(`Invalid characters in view name: ${this.viewNameOverride}`);
268
+ }
269
+ if (this.columns.length > MAX_AMOUNT_OF_COLUMNS) {
270
+ throw new Error(`Table has too many columns. The maximum number of columns is ${MAX_AMOUNT_OF_COLUMNS}.`);
271
+ }
272
+ if (this.trackMetadata && this.localOnly) {
273
+ throw new Error(`Can't include metadata for local-only tables.`);
274
+ }
275
+ if (this.trackPrevious != false && this.localOnly) {
276
+ throw new Error(`Can't include old values for local-only tables.`);
277
+ }
278
+ const columnNames = new Set();
279
+ columnNames.add('id');
280
+ for (const column of this.columns) {
281
+ const { name: columnName } = column;
282
+ if (column.name === 'id') {
283
+ throw new Error(`An id column is automatically added, custom id columns are not supported`);
284
+ }
285
+ if (columnNames.has(columnName)) {
286
+ throw new Error(`Duplicate column ${columnName}`);
287
+ }
288
+ if (InvalidSQLCharacters.test(columnName)) {
289
+ throw new Error(`Invalid characters in column name: ${column.name}`);
290
+ }
291
+ columnNames.add(columnName);
292
+ }
293
+ const indexNames = new Set();
294
+ for (const index of this.indexes) {
295
+ if (indexNames.has(index.name)) {
296
+ throw new Error(`Duplicate index ${index.name}`);
297
+ }
298
+ if (InvalidSQLCharacters.test(index.name)) {
299
+ throw new Error(`Invalid characters in index name: ${index.name}`);
300
+ }
301
+ for (const column of index.columns) {
302
+ if (!columnNames.has(column.name)) {
303
+ throw new Error(`Column ${column.name} not found for index ${index.name}`);
304
+ }
305
+ }
306
+ indexNames.add(index.name);
307
+ }
308
+ }
309
+ toJSON() {
310
+ return {
311
+ name: this.name,
312
+ view_name: this.viewName,
313
+ columns: this.columns.map((c) => c.toJSON()),
314
+ indexes: this.indexes.map((e) => e.toJSON(this)),
315
+ ...encodeTableOptions(this)
316
+ };
317
+ }
318
+ }
319
+
320
+ const ATTACHMENT_TABLE = 'attachments';
321
+ /**
322
+ * Maps a database row to an AttachmentRecord.
323
+ *
324
+ * @param row - The database row object
325
+ * @returns The corresponding AttachmentRecord
326
+ *
327
+ * @experimental
328
+ */
329
+ function attachmentFromSql(row) {
330
+ return {
331
+ id: row.id,
332
+ filename: row.filename,
333
+ localUri: row.local_uri,
334
+ size: row.size,
335
+ mediaType: row.media_type,
336
+ timestamp: row.timestamp,
337
+ metaData: row.meta_data,
338
+ hasSynced: row.has_synced === 1,
339
+ state: row.state
340
+ };
341
+ }
342
+ /**
343
+ * AttachmentState represents the current synchronization state of an attachment.
344
+ *
345
+ * @experimental
346
+ */
347
+ exports.AttachmentState = void 0;
348
+ (function (AttachmentState) {
349
+ AttachmentState[AttachmentState["QUEUED_UPLOAD"] = 0] = "QUEUED_UPLOAD";
350
+ AttachmentState[AttachmentState["QUEUED_DOWNLOAD"] = 1] = "QUEUED_DOWNLOAD";
351
+ AttachmentState[AttachmentState["QUEUED_DELETE"] = 2] = "QUEUED_DELETE";
352
+ AttachmentState[AttachmentState["SYNCED"] = 3] = "SYNCED";
353
+ AttachmentState[AttachmentState["ARCHIVED"] = 4] = "ARCHIVED"; // Attachment has been orphaned, i.e. the associated record has been deleted
354
+ })(exports.AttachmentState || (exports.AttachmentState = {}));
355
+ /**
356
+ * AttachmentTable defines the schema for the attachment queue table.
357
+ *
358
+ * @internal
359
+ */
360
+ class AttachmentTable extends Table {
361
+ constructor(options) {
362
+ super({
363
+ filename: column.text,
364
+ local_uri: column.text,
365
+ timestamp: column.integer,
366
+ size: column.integer,
367
+ media_type: column.text,
368
+ state: column.integer, // Corresponds to AttachmentState
369
+ has_synced: column.integer,
370
+ meta_data: column.text
371
+ }, {
372
+ ...options,
373
+ viewName: options?.viewName ?? ATTACHMENT_TABLE,
374
+ localOnly: true,
375
+ insertOnly: false
376
+ });
377
+ }
378
+ }
379
+
380
+ /**
381
+ * AttachmentContext provides database operations for managing attachment records.
382
+ *
383
+ * Provides methods to query, insert, update, and delete attachment records with
384
+ * proper transaction management through PowerSync.
385
+ *
386
+ * @internal
387
+ */
388
+ class AttachmentContext {
389
+ /** PowerSync database instance for executing queries */
390
+ db;
391
+ /** Name of the database table storing attachment records */
392
+ tableName;
393
+ /** Logger instance for diagnostic information */
394
+ logger;
395
+ /** Maximum number of archived attachments to keep before cleanup */
396
+ archivedCacheLimit = 100;
397
+ /**
398
+ * Creates a new AttachmentContext instance.
399
+ *
400
+ * @param db - PowerSync database instance
401
+ * @param tableName - Name of the table storing attachment records. Default: 'attachments'
402
+ * @param logger - Logger instance for diagnostic output
403
+ */
404
+ constructor(db, tableName = 'attachments', logger, archivedCacheLimit) {
405
+ this.db = db;
406
+ this.tableName = tableName;
407
+ this.logger = logger;
408
+ this.archivedCacheLimit = archivedCacheLimit;
409
+ }
410
+ /**
411
+ * Retrieves all active attachments that require synchronization.
412
+ * Active attachments include those queued for upload, download, or delete.
413
+ * Results are ordered by timestamp in ascending order.
414
+ *
415
+ * @returns Promise resolving to an array of active attachment records
416
+ */
417
+ async getActiveAttachments() {
418
+ const attachments = await this.db.getAll(
419
+ /* sql */
420
+ `
421
+ SELECT
422
+ *
423
+ FROM
424
+ ${this.tableName}
425
+ WHERE
426
+ state = ?
427
+ OR state = ?
428
+ OR state = ?
429
+ ORDER BY
430
+ timestamp ASC
431
+ `, [exports.AttachmentState.QUEUED_UPLOAD, exports.AttachmentState.QUEUED_DOWNLOAD, exports.AttachmentState.QUEUED_DELETE]);
432
+ return attachments.map(attachmentFromSql);
433
+ }
434
+ /**
435
+ * Retrieves all archived attachments.
436
+ *
437
+ * Archived attachments are no longer referenced but haven't been permanently deleted.
438
+ * These are candidates for cleanup operations to free up storage space.
439
+ *
440
+ * @returns Promise resolving to an array of archived attachment records
441
+ */
442
+ async getArchivedAttachments() {
443
+ const attachments = await this.db.getAll(
444
+ /* sql */
445
+ `
446
+ SELECT
447
+ *
448
+ FROM
449
+ ${this.tableName}
450
+ WHERE
451
+ state = ?
452
+ ORDER BY
453
+ timestamp ASC
454
+ `, [exports.AttachmentState.ARCHIVED]);
455
+ return attachments.map(attachmentFromSql);
456
+ }
457
+ /**
458
+ * Retrieves all attachment records regardless of state.
459
+ * Results are ordered by timestamp in ascending order.
460
+ *
461
+ * @returns Promise resolving to an array of all attachment records
462
+ */
463
+ async getAttachments() {
464
+ const attachments = await this.db.getAll(
465
+ /* sql */
466
+ `
467
+ SELECT
468
+ *
469
+ FROM
470
+ ${this.tableName}
471
+ ORDER BY
472
+ timestamp ASC
473
+ `, []);
474
+ return attachments.map(attachmentFromSql);
475
+ }
476
+ /**
477
+ * Inserts or updates an attachment record within an existing transaction.
478
+ *
479
+ * Performs an upsert operation (INSERT OR REPLACE). Must be called within
480
+ * an active database transaction context.
481
+ *
482
+ * @param attachment - The attachment record to upsert
483
+ * @param context - Active database transaction context
484
+ */
485
+ async upsertAttachment(attachment, context) {
486
+ await context.execute(
487
+ /* sql */
488
+ `
489
+ INSERT
490
+ OR REPLACE INTO ${this.tableName} (
491
+ id,
492
+ filename,
493
+ local_uri,
494
+ size,
495
+ media_type,
496
+ timestamp,
497
+ state,
498
+ has_synced,
499
+ meta_data
500
+ )
501
+ VALUES
502
+ (?, ?, ?, ?, ?, ?, ?, ?, ?)
503
+ `, [
504
+ attachment.id,
505
+ attachment.filename,
506
+ attachment.localUri || null,
507
+ attachment.size || null,
508
+ attachment.mediaType || null,
509
+ attachment.timestamp,
510
+ attachment.state,
511
+ attachment.hasSynced ? 1 : 0,
512
+ attachment.metaData || null
513
+ ]);
514
+ }
515
+ async getAttachment(id) {
516
+ const attachment = await this.db.get(
517
+ /* sql */
518
+ `
519
+ SELECT
520
+ *
521
+ FROM
522
+ ${this.tableName}
523
+ WHERE
524
+ id = ?
525
+ `, [id]);
526
+ return attachment ? attachmentFromSql(attachment) : undefined;
527
+ }
528
+ /**
529
+ * Permanently deletes an attachment record from the database.
530
+ *
531
+ * This operation removes the attachment record but does not delete
532
+ * the associated local or remote files. File deletion should be handled
533
+ * separately through the appropriate storage adapters.
534
+ *
535
+ * @param attachmentId - Unique identifier of the attachment to delete
536
+ */
537
+ async deleteAttachment(attachmentId) {
538
+ await this.db.writeTransaction((tx) => tx.execute(
539
+ /* sql */
540
+ `
541
+ DELETE FROM ${this.tableName}
542
+ WHERE
543
+ id = ?
544
+ `, [attachmentId]));
545
+ }
546
+ async clearQueue() {
547
+ await this.db.writeTransaction((tx) => tx.execute(/* sql */ ` DELETE FROM ${this.tableName} `));
548
+ }
549
+ async deleteArchivedAttachments(callback) {
550
+ const limit = 1000;
551
+ const results = await this.db.getAll(
552
+ /* sql */
553
+ `
554
+ SELECT
555
+ *
556
+ FROM
557
+ ${this.tableName}
558
+ WHERE
559
+ state = ?
560
+ ORDER BY
561
+ timestamp DESC
562
+ LIMIT
563
+ ?
564
+ OFFSET
565
+ ?
566
+ `, [exports.AttachmentState.ARCHIVED, limit, this.archivedCacheLimit]);
567
+ const archivedAttachments = results.map(attachmentFromSql);
568
+ if (archivedAttachments.length === 0)
569
+ return false;
570
+ await callback?.(archivedAttachments);
571
+ this.logger.info(`Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.`);
572
+ const ids = archivedAttachments.map((attachment) => attachment.id);
573
+ await this.db.execute(
574
+ /* sql */
575
+ `
576
+ DELETE FROM ${this.tableName}
577
+ WHERE
578
+ id IN (
579
+ SELECT
580
+ json_each.value
581
+ FROM
582
+ json_each (?)
583
+ );
584
+ `, [JSON.stringify(ids)]);
585
+ this.logger.info(`Deleted ${archivedAttachments.length} archived attachments`);
586
+ return archivedAttachments.length < limit;
587
+ }
588
+ /**
589
+ * Saves multiple attachment records in a single transaction.
590
+ *
591
+ * All updates are saved in a single batch after processing.
592
+ * If the attachments array is empty, no database operations are performed.
593
+ *
594
+ * @param attachments - Array of attachment records to save
595
+ */
596
+ async saveAttachments(attachments) {
597
+ if (attachments.length === 0) {
598
+ return;
599
+ }
600
+ await this.db.writeTransaction(async (tx) => {
601
+ for (const attachment of attachments) {
602
+ await this.upsertAttachment(attachment, tx);
603
+ }
604
+ });
605
+ }
606
+ }
607
+
608
+ exports.WatchedQueryListenerEvent = void 0;
609
+ (function (WatchedQueryListenerEvent) {
610
+ WatchedQueryListenerEvent["ON_DATA"] = "onData";
611
+ WatchedQueryListenerEvent["ON_ERROR"] = "onError";
612
+ WatchedQueryListenerEvent["ON_STATE_CHANGE"] = "onStateChange";
613
+ WatchedQueryListenerEvent["SETTINGS_WILL_UPDATE"] = "settingsWillUpdate";
614
+ WatchedQueryListenerEvent["CLOSED"] = "closed";
615
+ })(exports.WatchedQueryListenerEvent || (exports.WatchedQueryListenerEvent = {}));
616
+ const DEFAULT_WATCH_THROTTLE_MS = 30;
617
+ const DEFAULT_WATCH_QUERY_OPTIONS = {
618
+ throttleMs: DEFAULT_WATCH_THROTTLE_MS,
619
+ reportFetching: true
620
+ };
621
+
622
+ /**
623
+ * Orchestrates attachment synchronization between local and remote storage.
624
+ * Handles uploads, downloads, deletions, and state transitions.
625
+ *
626
+ * @internal
627
+ */
628
+ class SyncingService {
629
+ attachmentService;
630
+ localStorage;
631
+ remoteStorage;
632
+ logger;
633
+ errorHandler;
634
+ constructor(attachmentService, localStorage, remoteStorage, logger, errorHandler) {
635
+ this.attachmentService = attachmentService;
636
+ this.localStorage = localStorage;
637
+ this.remoteStorage = remoteStorage;
638
+ this.logger = logger;
639
+ this.errorHandler = errorHandler;
640
+ }
641
+ /**
642
+ * Processes attachments based on their state (upload, download, or delete).
643
+ * All updates are saved in a single batch after processing.
644
+ *
645
+ * @param attachments - Array of attachment records to process
646
+ * @param context - Attachment context for database operations
647
+ * @returns Promise that resolves when all attachments have been processed and saved
648
+ */
649
+ async processAttachments(attachments, context) {
650
+ const updatedAttachments = [];
651
+ for (const attachment of attachments) {
652
+ switch (attachment.state) {
653
+ case exports.AttachmentState.QUEUED_UPLOAD:
654
+ const uploaded = await this.uploadAttachment(attachment);
655
+ updatedAttachments.push(uploaded);
656
+ break;
657
+ case exports.AttachmentState.QUEUED_DOWNLOAD:
658
+ const downloaded = await this.downloadAttachment(attachment);
659
+ updatedAttachments.push(downloaded);
660
+ break;
661
+ case exports.AttachmentState.QUEUED_DELETE:
662
+ const deleted = await this.deleteAttachment(attachment);
663
+ updatedAttachments.push(deleted);
664
+ break;
665
+ }
666
+ }
667
+ await context.saveAttachments(updatedAttachments);
668
+ }
669
+ /**
670
+ * Uploads an attachment from local storage to remote storage.
671
+ * On success, marks as SYNCED. On failure, defers to error handler or archives.
672
+ *
673
+ * @param attachment - The attachment record to upload
674
+ * @returns Updated attachment record with new state
675
+ * @throws Error if the attachment has no localUri
676
+ */
677
+ async uploadAttachment(attachment) {
678
+ this.logger.info(`Uploading attachment ${attachment.filename}`);
679
+ try {
680
+ if (attachment.localUri == null) {
681
+ throw new Error(`No localUri for attachment ${attachment.id}`);
682
+ }
683
+ const fileBlob = await this.localStorage.readFile(attachment.localUri);
684
+ await this.remoteStorage.uploadFile(fileBlob, attachment);
685
+ return {
686
+ ...attachment,
687
+ state: exports.AttachmentState.SYNCED,
688
+ hasSynced: true
689
+ };
690
+ }
691
+ catch (error) {
692
+ const shouldRetry = (await this.errorHandler?.onUploadError(attachment, error)) ?? true;
693
+ if (!shouldRetry) {
694
+ return {
695
+ ...attachment,
696
+ state: exports.AttachmentState.ARCHIVED
697
+ };
698
+ }
699
+ return attachment;
700
+ }
701
+ }
702
+ /**
703
+ * Downloads an attachment from remote storage to local storage.
704
+ * Retrieves the file, converts to base64, and saves locally.
705
+ * On success, marks as SYNCED. On failure, defers to error handler or archives.
706
+ *
707
+ * @param attachment - The attachment record to download
708
+ * @returns Updated attachment record with local URI and new state
709
+ */
710
+ async downloadAttachment(attachment) {
711
+ this.logger.info(`Downloading attachment ${attachment.filename}`);
712
+ try {
713
+ const fileData = await this.remoteStorage.downloadFile(attachment);
714
+ const localUri = this.localStorage.getLocalUri(attachment.filename);
715
+ await this.localStorage.saveFile(localUri, fileData);
716
+ return {
717
+ ...attachment,
718
+ state: exports.AttachmentState.SYNCED,
719
+ localUri: localUri,
720
+ hasSynced: true
721
+ };
722
+ }
723
+ catch (error) {
724
+ const shouldRetry = (await this.errorHandler?.onDownloadError(attachment, error)) ?? true;
725
+ if (!shouldRetry) {
726
+ return {
727
+ ...attachment,
728
+ state: exports.AttachmentState.ARCHIVED
729
+ };
730
+ }
731
+ return attachment;
732
+ }
733
+ }
734
+ /**
735
+ * Deletes an attachment from both remote and local storage.
736
+ * Removes the remote file, local file (if exists), and the attachment record.
737
+ * On failure, defers to error handler or archives.
738
+ *
739
+ * @param attachment - The attachment record to delete
740
+ * @returns Updated attachment record
741
+ */
742
+ async deleteAttachment(attachment) {
743
+ try {
744
+ await this.remoteStorage.deleteFile(attachment);
745
+ if (attachment.localUri) {
746
+ await this.localStorage.deleteFile(attachment.localUri);
747
+ }
748
+ await this.attachmentService.withContext(async (ctx) => {
749
+ await ctx.deleteAttachment(attachment.id);
750
+ });
751
+ return {
752
+ ...attachment,
753
+ state: exports.AttachmentState.ARCHIVED
754
+ };
755
+ }
756
+ catch (error) {
757
+ const shouldRetry = (await this.errorHandler?.onDeleteError(attachment, error)) ?? true;
758
+ if (!shouldRetry) {
759
+ return {
760
+ ...attachment,
761
+ state: exports.AttachmentState.ARCHIVED
762
+ };
763
+ }
764
+ return attachment;
765
+ }
766
+ }
767
+ /**
768
+ * Performs cleanup of archived attachments by removing their local files and records.
769
+ * Errors during local file deletion are logged but do not prevent record deletion.
770
+ */
771
+ async deleteArchivedAttachments(context) {
772
+ return await context.deleteArchivedAttachments(async (archivedAttachments) => {
773
+ for (const attachment of archivedAttachments) {
774
+ if (attachment.localUri) {
775
+ try {
776
+ await this.localStorage.deleteFile(attachment.localUri);
777
+ }
778
+ catch (error) {
779
+ this.logger.error('Error deleting local file for archived attachment', error);
780
+ }
781
+ }
782
+ }
783
+ });
784
+ }
785
+ }
786
+
787
+ /**
788
+ * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
789
+ */
790
+ async function mutexRunExclusive(mutex, callback, options) {
791
+ return new Promise((resolve, reject) => {
792
+ const timeout = options?.timeoutMs;
793
+ let timedOut = false;
794
+ const timeoutId = timeout
795
+ ? setTimeout(() => {
796
+ timedOut = true;
797
+ reject(new Error('Timeout waiting for lock'));
798
+ }, timeout)
799
+ : undefined;
800
+ mutex.runExclusive(async () => {
801
+ if (timeoutId) {
802
+ clearTimeout(timeoutId);
803
+ }
804
+ if (timedOut)
805
+ return;
806
+ try {
807
+ resolve(await callback());
808
+ }
809
+ catch (ex) {
810
+ reject(ex);
811
+ }
812
+ });
813
+ });
814
+ }
815
+
816
+ /**
817
+ * Service for querying and watching attachment records in the database.
818
+ *
819
+ * @internal
820
+ */
821
+ class AttachmentService {
822
+ db;
823
+ logger;
824
+ tableName;
825
+ mutex = new asyncMutex.Mutex();
826
+ context;
827
+ constructor(db, logger, tableName = 'attachments', archivedCacheLimit = 100) {
828
+ this.db = db;
829
+ this.logger = logger;
830
+ this.tableName = tableName;
831
+ this.context = new AttachmentContext(db, tableName, logger, archivedCacheLimit);
832
+ }
833
+ /**
834
+ * Creates a differential watch query for active attachments requiring synchronization.
835
+ * @returns Watch query that emits changes for queued uploads, downloads, and deletes
836
+ */
837
+ watchActiveAttachments({ throttleMs } = {}) {
838
+ this.logger.info('Watching active attachments...');
839
+ const watch = this.db
840
+ .query({
841
+ sql: /* sql */ `
842
+ SELECT
843
+ *
844
+ FROM
845
+ ${this.tableName}
846
+ WHERE
847
+ state = ?
848
+ OR state = ?
849
+ OR state = ?
850
+ ORDER BY
851
+ timestamp ASC
852
+ `,
853
+ parameters: [exports.AttachmentState.QUEUED_UPLOAD, exports.AttachmentState.QUEUED_DOWNLOAD, exports.AttachmentState.QUEUED_DELETE]
854
+ })
855
+ .differentialWatch({ throttleMs });
856
+ return watch;
857
+ }
858
+ /**
859
+ * Executes a callback with exclusive access to the attachment context.
860
+ */
861
+ async withContext(callback) {
862
+ return mutexRunExclusive(this.mutex, async () => {
863
+ return callback(this.context);
864
+ });
865
+ }
866
+ }
867
+
868
+ /**
869
+ * AttachmentQueue manages the lifecycle and synchronization of attachments
870
+ * between local and remote storage.
871
+ * Provides automatic synchronization, upload/download queuing, attachment monitoring,
872
+ * verification and repair of local files, and cleanup of archived attachments.
873
+ *
874
+ * @experimental
875
+ * @alpha This is currently experimental and may change without a major version bump.
876
+ */
877
+ class AttachmentQueue {
878
+ /** Timer for periodic synchronization operations */
879
+ periodicSyncTimer;
880
+ /** Service for synchronizing attachments between local and remote storage */
881
+ syncingService;
882
+ /** Adapter for local file storage operations */
883
+ localStorage;
884
+ /** Adapter for remote file storage operations */
885
+ remoteStorage;
886
+ /**
887
+ * Callback function to watch for changes in attachment references in your data model.
888
+ *
889
+ * This should be implemented by the user of AttachmentQueue to monitor changes in your application's
890
+ * data that reference attachments. When attachments are added, removed, or modified,
891
+ * this callback should trigger the onUpdate function with the current set of attachments.
892
+ */
893
+ watchAttachments;
894
+ /** Name of the database table storing attachment records */
895
+ tableName;
896
+ /** Logger instance for diagnostic information */
897
+ logger;
898
+ /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */
899
+ syncIntervalMs = 30 * 1000;
900
+ /** Duration in milliseconds to throttle sync operations */
901
+ syncThrottleDuration;
902
+ /** Whether to automatically download remote attachments. Default: true */
903
+ downloadAttachments = true;
904
+ /** Maximum number of archived attachments to keep before cleanup. Default: 100 */
905
+ archivedCacheLimit;
906
+ /** Service for managing attachment-related database operations */
907
+ attachmentService;
908
+ /** PowerSync database instance */
909
+ db;
910
+ /** Cleanup function for status change listener */
911
+ statusListenerDispose;
912
+ watchActiveAttachments;
913
+ watchAttachmentsAbortController;
914
+ /**
915
+ * Creates a new AttachmentQueue instance.
916
+ *
917
+ * @param options - Configuration options
918
+ * @param options.db - PowerSync database instance
919
+ * @param options.remoteStorage - Remote storage adapter for upload/download operations
920
+ * @param options.localStorage - Local storage adapter for file persistence
921
+ * @param options.watchAttachments - Callback for monitoring attachment changes in your data model
922
+ * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
923
+ * @param options.logger - Logger instance. Defaults to db.logger
924
+ * @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000
925
+ * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
926
+ * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
927
+ * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
928
+ */
929
+ constructor({ db, localStorage, remoteStorage, watchAttachments, logger, tableName = ATTACHMENT_TABLE, syncIntervalMs = 30 * 1000, syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS, downloadAttachments = true, archivedCacheLimit = 100, errorHandler }) {
930
+ this.db = db;
931
+ this.remoteStorage = remoteStorage;
932
+ this.localStorage = localStorage;
933
+ this.watchAttachments = watchAttachments;
934
+ this.tableName = tableName;
935
+ this.syncIntervalMs = syncIntervalMs;
936
+ this.syncThrottleDuration = syncThrottleDuration;
937
+ this.archivedCacheLimit = archivedCacheLimit;
938
+ this.downloadAttachments = downloadAttachments;
939
+ this.logger = logger ?? db.logger;
940
+ this.attachmentService = new AttachmentService(db, this.logger, tableName, archivedCacheLimit);
941
+ this.syncingService = new SyncingService(this.attachmentService, localStorage, remoteStorage, this.logger, errorHandler);
942
+ }
943
+ /**
944
+ * Generates a new attachment ID using a SQLite UUID function.
945
+ *
946
+ * @returns Promise resolving to the new attachment ID
947
+ */
948
+ async generateAttachmentId() {
949
+ return this.db.get('SELECT uuid() as id').then((row) => row.id);
950
+ }
951
+ /**
952
+ * Starts the attachment synchronization process.
953
+ *
954
+ * This method:
955
+ * - Stops any existing sync operations
956
+ * - Sets up periodic synchronization based on syncIntervalMs
957
+ * - Registers listeners for active attachment changes
958
+ * - Processes watched attachments to queue uploads/downloads
959
+ * - Handles state transitions for archived and new attachments
960
+ */
961
+ async startSync() {
962
+ await this.stopSync();
963
+ this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
964
+ throttleMs: this.syncThrottleDuration
965
+ });
966
+ // immediately invoke the sync storage to initialize local storage
967
+ await this.localStorage.initialize();
968
+ await this.verifyAttachments();
969
+ // Sync storage periodically
970
+ this.periodicSyncTimer = setInterval(async () => {
971
+ await this.syncStorage();
972
+ }, this.syncIntervalMs);
973
+ // Sync storage when there is a change in active attachments
974
+ this.watchActiveAttachments.registerListener({
975
+ onDiff: async () => {
976
+ await this.syncStorage();
977
+ }
978
+ });
979
+ this.statusListenerDispose = this.db.registerListener({
980
+ statusChanged: (status) => {
981
+ if (status.connected) {
982
+ // Device came online, process attachments immediately
983
+ this.syncStorage().catch((error) => {
984
+ this.logger.error('Error syncing storage on connection:', error);
985
+ });
986
+ }
987
+ }
988
+ });
989
+ this.watchAttachmentsAbortController = new AbortController();
990
+ const signal = this.watchAttachmentsAbortController.signal;
991
+ // Process attachments when there is a change in watched attachments
992
+ this.watchAttachments(async (watchedAttachments) => {
993
+ // Skip processing if sync has been stopped
994
+ if (signal.aborted) {
995
+ return;
996
+ }
997
+ await this.attachmentService.withContext(async (ctx) => {
998
+ // Need to get all the attachments which are tracked in the DB.
999
+ // We might need to restore an archived attachment.
1000
+ const currentAttachments = await ctx.getAttachments();
1001
+ const attachmentUpdates = [];
1002
+ for (const watchedAttachment of watchedAttachments) {
1003
+ const existingQueueItem = currentAttachments.find((a) => a.id === watchedAttachment.id);
1004
+ if (!existingQueueItem) {
1005
+ // Item is watched but not in the queue yet. Need to add it.
1006
+ if (!this.downloadAttachments) {
1007
+ continue;
1008
+ }
1009
+ const filename = watchedAttachment.filename ?? `${watchedAttachment.id}.${watchedAttachment.fileExtension}`;
1010
+ attachmentUpdates.push({
1011
+ id: watchedAttachment.id,
1012
+ filename,
1013
+ state: exports.AttachmentState.QUEUED_DOWNLOAD,
1014
+ hasSynced: false,
1015
+ metaData: watchedAttachment.metaData,
1016
+ timestamp: new Date().getTime()
1017
+ });
1018
+ continue;
1019
+ }
1020
+ if (existingQueueItem.state === exports.AttachmentState.ARCHIVED) {
1021
+ // The attachment is present again. Need to queue it for sync.
1022
+ // We might be able to optimize this in future
1023
+ if (existingQueueItem.hasSynced === true) {
1024
+ // No remote action required, we can restore the record (avoids deletion)
1025
+ attachmentUpdates.push({
1026
+ ...existingQueueItem,
1027
+ state: exports.AttachmentState.SYNCED
1028
+ });
1029
+ }
1030
+ else {
1031
+ // The localURI should be set if the record was meant to be uploaded
1032
+ // and hasSynced is false then
1033
+ // it must be an upload operation
1034
+ const newState = existingQueueItem.localUri == null ? exports.AttachmentState.QUEUED_DOWNLOAD : exports.AttachmentState.QUEUED_UPLOAD;
1035
+ attachmentUpdates.push({
1036
+ ...existingQueueItem,
1037
+ state: newState
1038
+ });
1039
+ }
1040
+ }
1041
+ }
1042
+ for (const attachment of currentAttachments) {
1043
+ const notInWatchedItems = watchedAttachments.find((i) => i.id === attachment.id) == null;
1044
+ if (notInWatchedItems) {
1045
+ switch (attachment.state) {
1046
+ case exports.AttachmentState.QUEUED_DELETE:
1047
+ case exports.AttachmentState.QUEUED_UPLOAD:
1048
+ // Only archive if it has synced
1049
+ if (attachment.hasSynced === true) {
1050
+ attachmentUpdates.push({
1051
+ ...attachment,
1052
+ state: exports.AttachmentState.ARCHIVED
1053
+ });
1054
+ }
1055
+ break;
1056
+ default:
1057
+ // Archive other states such as QUEUED_DOWNLOAD
1058
+ attachmentUpdates.push({
1059
+ ...attachment,
1060
+ state: exports.AttachmentState.ARCHIVED
1061
+ });
1062
+ }
1063
+ }
1064
+ }
1065
+ if (attachmentUpdates.length > 0) {
1066
+ await ctx.saveAttachments(attachmentUpdates);
1067
+ }
1068
+ });
1069
+ }, signal);
1070
+ }
1071
+ /**
1072
+ * Synchronizes all active attachments between local and remote storage.
1073
+ *
1074
+ * This is called automatically at regular intervals when sync is started,
1075
+ * but can also be called manually to trigger an immediate sync.
1076
+ */
1077
+ async syncStorage() {
1078
+ await this.attachmentService.withContext(async (ctx) => {
1079
+ const activeAttachments = await ctx.getActiveAttachments();
1080
+ await this.localStorage.initialize();
1081
+ await this.syncingService.processAttachments(activeAttachments, ctx);
1082
+ await this.syncingService.deleteArchivedAttachments(ctx);
1083
+ });
1084
+ }
1085
+ /**
1086
+ * Stops the attachment synchronization process.
1087
+ *
1088
+ * Clears the periodic sync timer and closes all active attachment watchers.
1089
+ */
1090
+ async stopSync() {
1091
+ clearInterval(this.periodicSyncTimer);
1092
+ this.periodicSyncTimer = undefined;
1093
+ if (this.watchActiveAttachments)
1094
+ await this.watchActiveAttachments.close();
1095
+ if (this.watchAttachmentsAbortController) {
1096
+ this.watchAttachmentsAbortController.abort();
1097
+ }
1098
+ if (this.statusListenerDispose) {
1099
+ this.statusListenerDispose();
1100
+ this.statusListenerDispose = undefined;
1101
+ }
1102
+ }
1103
+ /**
1104
+ * Saves a file to local storage and queues it for upload to remote storage.
1105
+ *
1106
+ * @param options - File save options
1107
+ * @param options.data - The file data as ArrayBuffer, Blob, or base64 string
1108
+ * @param options.fileExtension - File extension (e.g., 'jpg', 'pdf')
1109
+ * @param options.mediaType - MIME type of the file (e.g., 'image/jpeg')
1110
+ * @param options.metaData - Optional metadata to associate with the attachment
1111
+ * @param options.id - Optional custom ID. If not provided, a UUID will be generated
1112
+ * @param options.updateHook - Optional callback to execute additional database operations
1113
+ * within the same transaction as the attachment creation
1114
+ * @returns Promise resolving to the created attachment record
1115
+ */
1116
+ async saveFile({ data, fileExtension, mediaType, metaData, id, updateHook }) {
1117
+ const resolvedId = id ?? (await this.generateAttachmentId());
1118
+ const filename = `${resolvedId}.${fileExtension}`;
1119
+ const localUri = this.localStorage.getLocalUri(filename);
1120
+ const size = await this.localStorage.saveFile(localUri, data);
1121
+ const attachment = {
1122
+ id: resolvedId,
1123
+ filename,
1124
+ mediaType,
1125
+ localUri,
1126
+ state: exports.AttachmentState.QUEUED_UPLOAD,
1127
+ hasSynced: false,
1128
+ size,
1129
+ timestamp: new Date().getTime(),
1130
+ metaData
1131
+ };
1132
+ await this.attachmentService.withContext(async (ctx) => {
1133
+ await ctx.db.writeTransaction(async (tx) => {
1134
+ await updateHook?.(tx, attachment);
1135
+ await ctx.upsertAttachment(attachment, tx);
1136
+ });
1137
+ });
1138
+ return attachment;
1139
+ }
1140
+ async deleteFile({ id, updateHook }) {
1141
+ await this.attachmentService.withContext(async (ctx) => {
1142
+ const attachment = await ctx.getAttachment(id);
1143
+ if (!attachment) {
1144
+ throw new Error(`Attachment with id ${id} not found`);
1145
+ }
1146
+ await ctx.db.writeTransaction(async (tx) => {
1147
+ await updateHook?.(tx, attachment);
1148
+ await ctx.upsertAttachment({
1149
+ ...attachment,
1150
+ state: exports.AttachmentState.QUEUED_DELETE,
1151
+ hasSynced: false
1152
+ }, tx);
1153
+ });
1154
+ });
1155
+ }
1156
+ async expireCache() {
1157
+ let isDone = false;
1158
+ while (!isDone) {
1159
+ await this.attachmentService.withContext(async (ctx) => {
1160
+ isDone = await this.syncingService.deleteArchivedAttachments(ctx);
1161
+ });
1162
+ }
1163
+ }
1164
+ async clearQueue() {
1165
+ await this.attachmentService.withContext(async (ctx) => {
1166
+ await ctx.clearQueue();
1167
+ });
1168
+ await this.localStorage.clear();
1169
+ }
1170
+ /**
1171
+ * Verifies the integrity of all attachment records and repairs inconsistencies.
1172
+ *
1173
+ * This method checks each attachment record against the local filesystem and:
1174
+ * - Updates localUri if the file exists at a different path
1175
+ * - Archives attachments with missing local files that haven't been uploaded
1176
+ * - Requeues synced attachments for download if their local files are missing
1177
+ */
1178
+ async verifyAttachments() {
1179
+ await this.attachmentService.withContext(async (ctx) => {
1180
+ const attachments = await ctx.getAttachments();
1181
+ const updates = [];
1182
+ for (const attachment of attachments) {
1183
+ if (attachment.localUri == null) {
1184
+ continue;
1185
+ }
1186
+ const exists = await this.localStorage.fileExists(attachment.localUri);
1187
+ if (exists) {
1188
+ // The file exists, this is correct
1189
+ continue;
1190
+ }
1191
+ const newLocalUri = this.localStorage.getLocalUri(attachment.filename);
1192
+ const newExists = await this.localStorage.fileExists(newLocalUri);
1193
+ if (newExists) {
1194
+ // The file exists locally but the localUri is broken, we update it.
1195
+ updates.push({
1196
+ ...attachment,
1197
+ localUri: newLocalUri
1198
+ });
1199
+ }
1200
+ else {
1201
+ // the file doesn't exist locally.
1202
+ if (attachment.state === exports.AttachmentState.SYNCED) {
1203
+ // the file has been successfully synced to remote storage but is missing
1204
+ // we download it again
1205
+ updates.push({
1206
+ ...attachment,
1207
+ state: exports.AttachmentState.QUEUED_DOWNLOAD,
1208
+ localUri: undefined
1209
+ });
1210
+ }
1211
+ else {
1212
+ // the file wasn't successfully synced to remote storage, we archive it
1213
+ updates.push({
1214
+ ...attachment,
1215
+ state: exports.AttachmentState.ARCHIVED,
1216
+ localUri: undefined // Clears the value
1217
+ });
1218
+ }
1219
+ }
1220
+ }
1221
+ await ctx.saveAttachments(updates);
1222
+ });
1223
+ }
1224
+ }
1225
+
1226
+ exports.EncodingType = void 0;
1227
+ (function (EncodingType) {
1228
+ EncodingType["UTF8"] = "utf8";
1229
+ EncodingType["Base64"] = "base64";
1230
+ })(exports.EncodingType || (exports.EncodingType = {}));
1231
+
5
1232
  function getDefaultExportFromCjs (x) {
6
1233
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
7
1234
  }
@@ -1318,20 +2545,6 @@ class MetaBaseObserver extends BaseObserver {
1318
2545
  }
1319
2546
  }
1320
2547
 
1321
- exports.WatchedQueryListenerEvent = void 0;
1322
- (function (WatchedQueryListenerEvent) {
1323
- WatchedQueryListenerEvent["ON_DATA"] = "onData";
1324
- WatchedQueryListenerEvent["ON_ERROR"] = "onError";
1325
- WatchedQueryListenerEvent["ON_STATE_CHANGE"] = "onStateChange";
1326
- WatchedQueryListenerEvent["SETTINGS_WILL_UPDATE"] = "settingsWillUpdate";
1327
- WatchedQueryListenerEvent["CLOSED"] = "closed";
1328
- })(exports.WatchedQueryListenerEvent || (exports.WatchedQueryListenerEvent = {}));
1329
- const DEFAULT_WATCH_THROTTLE_MS = 30;
1330
- const DEFAULT_WATCH_QUERY_OPTIONS = {
1331
- throttleMs: DEFAULT_WATCH_THROTTLE_MS,
1332
- reportFetching: true
1333
- };
1334
-
1335
2548
  /**
1336
2549
  * Performs underlying watching and yields a stream of results.
1337
2550
  * @internal
@@ -9244,7 +10457,7 @@ function requireDist () {
9244
10457
 
9245
10458
  var distExports = requireDist();
9246
10459
 
9247
- var version = "1.46.0";
10460
+ var version = "1.48.0";
9248
10461
  var PACKAGE = {
9249
10462
  version: version};
9250
10463
 
@@ -12869,193 +14082,59 @@ class SqliteBucketStorage extends BaseObserver {
12869
14082
  async control(op, payload) {
12870
14083
  return await this.writeTransaction(async (tx) => {
12871
14084
  const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
12872
- return raw;
12873
- });
12874
- }
12875
- async hasMigratedSubkeys() {
12876
- const { r } = await this.db.get('SELECT EXISTS(SELECT * FROM ps_kv WHERE key = ?) as r', [
12877
- SqliteBucketStorage._subkeyMigrationKey
12878
- ]);
12879
- return r != 0;
12880
- }
12881
- async migrateToFixedSubkeys() {
12882
- await this.writeTransaction(async (tx) => {
12883
- await tx.execute('UPDATE ps_oplog SET key = powersync_remove_duplicate_key_encoding(key);');
12884
- await tx.execute('INSERT OR REPLACE INTO ps_kv (key, value) VALUES (?, ?);', [
12885
- SqliteBucketStorage._subkeyMigrationKey,
12886
- '1'
12887
- ]);
12888
- });
12889
- }
12890
- static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
12891
- }
12892
- function hasMatchingPriority(priority, bucket) {
12893
- return bucket.priority != null && bucket.priority <= priority;
12894
- }
12895
-
12896
- // TODO JSON
12897
- class SyncDataBatch {
12898
- buckets;
12899
- static fromJSON(json) {
12900
- return new SyncDataBatch(json.buckets.map((bucket) => SyncDataBucket.fromRow(bucket)));
12901
- }
12902
- constructor(buckets) {
12903
- this.buckets = buckets;
12904
- }
12905
- }
12906
-
12907
- /**
12908
- * Thrown when an underlying database connection is closed.
12909
- * This is particularly relevant when worker connections are marked as closed while
12910
- * operations are still in progress.
12911
- */
12912
- class ConnectionClosedError extends Error {
12913
- static NAME = 'ConnectionClosedError';
12914
- static MATCHES(input) {
12915
- /**
12916
- * If there are weird package issues which cause multiple versions of classes to be present, the instanceof
12917
- * check might fail. This also performs a failsafe check.
12918
- * This might also happen if the Error is serialized and parsed over a bridging channel like a MessagePort.
12919
- */
12920
- return (input instanceof ConnectionClosedError || (input instanceof Error && input.name == ConnectionClosedError.NAME));
12921
- }
12922
- constructor(message) {
12923
- super(message);
12924
- this.name = ConnectionClosedError.NAME;
12925
- }
12926
- }
12927
-
12928
- // https://www.sqlite.org/lang_expr.html#castexpr
12929
- exports.ColumnType = void 0;
12930
- (function (ColumnType) {
12931
- ColumnType["TEXT"] = "TEXT";
12932
- ColumnType["INTEGER"] = "INTEGER";
12933
- ColumnType["REAL"] = "REAL";
12934
- })(exports.ColumnType || (exports.ColumnType = {}));
12935
- const text = {
12936
- type: exports.ColumnType.TEXT
12937
- };
12938
- const integer = {
12939
- type: exports.ColumnType.INTEGER
12940
- };
12941
- const real = {
12942
- type: exports.ColumnType.REAL
12943
- };
12944
- // powersync-sqlite-core limits the number of column per table to 1999, due to internal SQLite limits.
12945
- // In earlier versions this was limited to 63.
12946
- const MAX_AMOUNT_OF_COLUMNS = 1999;
12947
- const column = {
12948
- text,
12949
- integer,
12950
- real
12951
- };
12952
- class Column {
12953
- options;
12954
- constructor(options) {
12955
- this.options = options;
12956
- }
12957
- get name() {
12958
- return this.options.name;
12959
- }
12960
- get type() {
12961
- return this.options.type;
12962
- }
12963
- toJSON() {
12964
- return {
12965
- name: this.name,
12966
- type: this.type
12967
- };
12968
- }
12969
- }
12970
-
12971
- const DEFAULT_INDEX_COLUMN_OPTIONS = {
12972
- ascending: true
12973
- };
12974
- class IndexedColumn {
12975
- options;
12976
- static createAscending(column) {
12977
- return new IndexedColumn({
12978
- name: column,
12979
- ascending: true
12980
- });
12981
- }
12982
- constructor(options) {
12983
- this.options = { ...DEFAULT_INDEX_COLUMN_OPTIONS, ...options };
12984
- }
12985
- get name() {
12986
- return this.options.name;
12987
- }
12988
- get ascending() {
12989
- return this.options.ascending;
12990
- }
12991
- toJSON(table) {
12992
- return {
12993
- name: this.name,
12994
- ascending: this.ascending,
12995
- type: table.columns.find((column) => column.name === this.name)?.type ?? exports.ColumnType.TEXT
12996
- };
12997
- }
12998
- }
12999
-
13000
- const DEFAULT_INDEX_OPTIONS = {
13001
- columns: []
13002
- };
13003
- class Index {
13004
- options;
13005
- static createAscending(options, columnNames) {
13006
- return new Index({
13007
- ...options,
13008
- columns: columnNames.map((name) => IndexedColumn.createAscending(name))
14085
+ return raw;
13009
14086
  });
13010
14087
  }
13011
- constructor(options) {
13012
- this.options = options;
13013
- this.options = { ...DEFAULT_INDEX_OPTIONS, ...options };
14088
+ async hasMigratedSubkeys() {
14089
+ const { r } = await this.db.get('SELECT EXISTS(SELECT * FROM ps_kv WHERE key = ?) as r', [
14090
+ SqliteBucketStorage._subkeyMigrationKey
14091
+ ]);
14092
+ return r != 0;
13014
14093
  }
13015
- get name() {
13016
- return this.options.name;
14094
+ async migrateToFixedSubkeys() {
14095
+ await this.writeTransaction(async (tx) => {
14096
+ await tx.execute('UPDATE ps_oplog SET key = powersync_remove_duplicate_key_encoding(key);');
14097
+ await tx.execute('INSERT OR REPLACE INTO ps_kv (key, value) VALUES (?, ?);', [
14098
+ SqliteBucketStorage._subkeyMigrationKey,
14099
+ '1'
14100
+ ]);
14101
+ });
13017
14102
  }
13018
- get columns() {
13019
- return this.options.columns ?? [];
14103
+ static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
14104
+ }
14105
+ function hasMatchingPriority(priority, bucket) {
14106
+ return bucket.priority != null && bucket.priority <= priority;
14107
+ }
14108
+
14109
+ // TODO JSON
14110
+ class SyncDataBatch {
14111
+ buckets;
14112
+ static fromJSON(json) {
14113
+ return new SyncDataBatch(json.buckets.map((bucket) => SyncDataBucket.fromRow(bucket)));
13020
14114
  }
13021
- toJSON(table) {
13022
- return {
13023
- name: this.name,
13024
- columns: this.columns.map((c) => c.toJSON(table))
13025
- };
14115
+ constructor(buckets) {
14116
+ this.buckets = buckets;
13026
14117
  }
13027
14118
  }
13028
14119
 
13029
14120
  /**
13030
- * Instructs PowerSync to sync data into a "raw" table.
13031
- *
13032
- * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
13033
- * using client-side table and column constraints.
13034
- *
13035
- * To collect local writes to raw tables with PowerSync, custom triggers are required. See
13036
- * {@link https://docs.powersync.com/usage/use-case-examples/raw-tables the documentation} for details and an example on
13037
- * using raw tables.
13038
- *
13039
- * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
13040
- *
13041
- * @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
13042
- * stability guarantees.
14121
+ * Thrown when an underlying database connection is closed.
14122
+ * This is particularly relevant when worker connections are marked as closed while
14123
+ * operations are still in progress.
13043
14124
  */
13044
- class RawTable {
13045
- /**
13046
- * The name of the table.
13047
- *
13048
- * This does not have to match the actual table name in the schema - {@link put} and {@link delete} are free to use
13049
- * another table. Instead, this name is used by the sync client to recognize that operations on this table (as it
13050
- * appears in the source / backend database) are to be handled specially.
13051
- */
13052
- name;
13053
- put;
13054
- delete;
13055
- constructor(name, type) {
13056
- this.name = name;
13057
- this.put = type.put;
13058
- this.delete = type.delete;
14125
+ class ConnectionClosedError extends Error {
14126
+ static NAME = 'ConnectionClosedError';
14127
+ static MATCHES(input) {
14128
+ /**
14129
+ * If there are weird package issues which cause multiple versions of classes to be present, the instanceof
14130
+ * check might fail. This also performs a failsafe check.
14131
+ * This might also happen if the Error is serialized and parsed over a bridging channel like a MessagePort.
14132
+ */
14133
+ return (input instanceof ConnectionClosedError || (input instanceof Error && input.name == ConnectionClosedError.NAME));
14134
+ }
14135
+ constructor(message) {
14136
+ super(message);
14137
+ this.name = ConnectionClosedError.NAME;
13059
14138
  }
13060
14139
  }
13061
14140
 
@@ -13103,7 +14182,7 @@ class Schema {
13103
14182
  */
13104
14183
  withRawTables(tables) {
13105
14184
  for (const [name, rawTableDefinition] of Object.entries(tables)) {
13106
- this.rawTables.push(new RawTable(name, rawTableDefinition));
14185
+ this.rawTables.push({ name, ...rawTableDefinition });
13107
14186
  }
13108
14187
  }
13109
14188
  validate() {
@@ -13114,213 +14193,30 @@ class Schema {
13114
14193
  toJSON() {
13115
14194
  return {
13116
14195
  tables: this.tables.map((t) => t.toJSON()),
13117
- raw_tables: this.rawTables
14196
+ raw_tables: this.rawTables.map(Schema.rawTableToJson)
13118
14197
  };
13119
14198
  }
13120
- }
13121
-
13122
- const DEFAULT_TABLE_OPTIONS = {
13123
- indexes: [],
13124
- insertOnly: false,
13125
- localOnly: false,
13126
- trackPrevious: false,
13127
- trackMetadata: false,
13128
- ignoreEmptyUpdates: false
13129
- };
13130
- const InvalidSQLCharacters = /["'%,.#\s[\]]/;
13131
- class Table {
13132
- options;
13133
- _mappedColumns;
13134
- static createLocalOnly(options) {
13135
- return new Table({ ...options, localOnly: true, insertOnly: false });
13136
- }
13137
- static createInsertOnly(options) {
13138
- return new Table({ ...options, localOnly: false, insertOnly: true });
13139
- }
13140
14199
  /**
13141
- * Create a table.
13142
- * @deprecated This was only only included for TableV2 and is no longer necessary.
13143
- * Prefer to use new Table() directly.
14200
+ * Returns a representation of the raw table that is understood by the PowerSync SQLite core extension.
13144
14201
  *
13145
- * TODO remove in the next major release.
13146
- */
13147
- static createTable(name, table) {
13148
- return new Table({
13149
- name,
13150
- columns: table.columns,
13151
- indexes: table.indexes,
13152
- localOnly: table.options.localOnly,
13153
- insertOnly: table.options.insertOnly,
13154
- viewName: table.options.viewName
13155
- });
13156
- }
13157
- constructor(optionsOrColumns, v2Options) {
13158
- if (this.isTableV1(optionsOrColumns)) {
13159
- this.initTableV1(optionsOrColumns);
13160
- }
13161
- else {
13162
- this.initTableV2(optionsOrColumns, v2Options);
13163
- }
13164
- }
13165
- copyWithName(name) {
13166
- return new Table({
13167
- ...this.options,
13168
- name
13169
- });
13170
- }
13171
- isTableV1(arg) {
13172
- return 'columns' in arg && Array.isArray(arg.columns);
13173
- }
13174
- initTableV1(options) {
13175
- this.options = {
13176
- ...options,
13177
- indexes: options.indexes || []
13178
- };
13179
- this.applyDefaultOptions();
13180
- }
13181
- initTableV2(columns, options) {
13182
- const convertedColumns = Object.entries(columns).map(([name, columnInfo]) => new Column({ name, type: columnInfo.type }));
13183
- const convertedIndexes = Object.entries(options?.indexes ?? {}).map(([name, columnNames]) => new Index({
13184
- name,
13185
- columns: columnNames.map((name) => new IndexedColumn({
13186
- name: name.replace(/^-/, ''),
13187
- ascending: !name.startsWith('-')
13188
- }))
13189
- }));
13190
- this.options = {
13191
- name: '',
13192
- columns: convertedColumns,
13193
- indexes: convertedIndexes,
13194
- viewName: options?.viewName,
13195
- insertOnly: options?.insertOnly,
13196
- localOnly: options?.localOnly,
13197
- trackPrevious: options?.trackPrevious,
13198
- trackMetadata: options?.trackMetadata,
13199
- ignoreEmptyUpdates: options?.ignoreEmptyUpdates
14202
+ * The output of this can be passed through `JSON.serialize` and then used in `powersync_create_raw_table_crud_trigger`
14203
+ * to define triggers for this table.
14204
+ */
14205
+ static rawTableToJson(table) {
14206
+ const serialized = {
14207
+ name: table.name,
14208
+ put: table.put,
14209
+ delete: table.delete,
14210
+ clear: table.clear
13200
14211
  };
13201
- this.applyDefaultOptions();
13202
- this._mappedColumns = columns;
13203
- }
13204
- applyDefaultOptions() {
13205
- this.options.insertOnly ??= DEFAULT_TABLE_OPTIONS.insertOnly;
13206
- this.options.localOnly ??= DEFAULT_TABLE_OPTIONS.localOnly;
13207
- this.options.trackPrevious ??= DEFAULT_TABLE_OPTIONS.trackPrevious;
13208
- this.options.trackMetadata ??= DEFAULT_TABLE_OPTIONS.trackMetadata;
13209
- this.options.ignoreEmptyUpdates ??= DEFAULT_TABLE_OPTIONS.ignoreEmptyUpdates;
13210
- }
13211
- get name() {
13212
- return this.options.name;
13213
- }
13214
- get viewNameOverride() {
13215
- return this.options.viewName;
13216
- }
13217
- get viewName() {
13218
- return this.viewNameOverride ?? this.name;
13219
- }
13220
- get columns() {
13221
- return this.options.columns;
13222
- }
13223
- get columnMap() {
13224
- return (this._mappedColumns ??
13225
- this.columns.reduce((hash, column) => {
13226
- hash[column.name] = { type: column.type ?? exports.ColumnType.TEXT };
13227
- return hash;
13228
- }, {}));
13229
- }
13230
- get indexes() {
13231
- return this.options.indexes ?? [];
13232
- }
13233
- get localOnly() {
13234
- return this.options.localOnly;
13235
- }
13236
- get insertOnly() {
13237
- return this.options.insertOnly;
13238
- }
13239
- get trackPrevious() {
13240
- return this.options.trackPrevious;
13241
- }
13242
- get trackMetadata() {
13243
- return this.options.trackMetadata;
13244
- }
13245
- get ignoreEmptyUpdates() {
13246
- return this.options.ignoreEmptyUpdates;
13247
- }
13248
- get internalName() {
13249
- if (this.options.localOnly) {
13250
- return `ps_data_local__${this.name}`;
14212
+ if ('schema' in table) {
14213
+ // We have schema options, those are flattened into the outer JSON object for the core extension.
14214
+ const schema = table.schema;
14215
+ serialized.table_name = schema.tableName ?? table.name;
14216
+ serialized.synced_columns = schema.syncedColumns;
14217
+ Object.assign(serialized, encodeTableOptions(table.schema));
13251
14218
  }
13252
- return `ps_data__${this.name}`;
13253
- }
13254
- get validName() {
13255
- if (InvalidSQLCharacters.test(this.name)) {
13256
- return false;
13257
- }
13258
- if (this.viewNameOverride != null && InvalidSQLCharacters.test(this.viewNameOverride)) {
13259
- return false;
13260
- }
13261
- return true;
13262
- }
13263
- validate() {
13264
- if (InvalidSQLCharacters.test(this.name)) {
13265
- throw new Error(`Invalid characters in table name: ${this.name}`);
13266
- }
13267
- if (this.viewNameOverride && InvalidSQLCharacters.test(this.viewNameOverride)) {
13268
- throw new Error(`Invalid characters in view name: ${this.viewNameOverride}`);
13269
- }
13270
- if (this.columns.length > MAX_AMOUNT_OF_COLUMNS) {
13271
- throw new Error(`Table has too many columns. The maximum number of columns is ${MAX_AMOUNT_OF_COLUMNS}.`);
13272
- }
13273
- if (this.trackMetadata && this.localOnly) {
13274
- throw new Error(`Can't include metadata for local-only tables.`);
13275
- }
13276
- if (this.trackPrevious != false && this.localOnly) {
13277
- throw new Error(`Can't include old values for local-only tables.`);
13278
- }
13279
- const columnNames = new Set();
13280
- columnNames.add('id');
13281
- for (const column of this.columns) {
13282
- const { name: columnName } = column;
13283
- if (column.name === 'id') {
13284
- throw new Error(`An id column is automatically added, custom id columns are not supported`);
13285
- }
13286
- if (columnNames.has(columnName)) {
13287
- throw new Error(`Duplicate column ${columnName}`);
13288
- }
13289
- if (InvalidSQLCharacters.test(columnName)) {
13290
- throw new Error(`Invalid characters in column name: ${column.name}`);
13291
- }
13292
- columnNames.add(columnName);
13293
- }
13294
- const indexNames = new Set();
13295
- for (const index of this.indexes) {
13296
- if (indexNames.has(index.name)) {
13297
- throw new Error(`Duplicate index ${index.name}`);
13298
- }
13299
- if (InvalidSQLCharacters.test(index.name)) {
13300
- throw new Error(`Invalid characters in index name: ${index.name}`);
13301
- }
13302
- for (const column of index.columns) {
13303
- if (!columnNames.has(column.name)) {
13304
- throw new Error(`Column ${column.name} not found for index ${index.name}`);
13305
- }
13306
- }
13307
- indexNames.add(index.name);
13308
- }
13309
- }
13310
- toJSON() {
13311
- const trackPrevious = this.trackPrevious;
13312
- return {
13313
- name: this.name,
13314
- view_name: this.viewName,
13315
- local_only: this.localOnly,
13316
- insert_only: this.insertOnly,
13317
- include_old: trackPrevious && (trackPrevious.columns ?? true),
13318
- include_old_only_when_changed: typeof trackPrevious == 'object' && trackPrevious.onlyWhenChanged == true,
13319
- include_metadata: this.trackMetadata,
13320
- ignore_empty_update: this.ignoreEmptyUpdates,
13321
- columns: this.columns.map((c) => c.toJSON()),
13322
- indexes: this.indexes.map((e) => e.toJSON(this))
13323
- };
14219
+ return serialized;
13324
14220
  }
13325
14221
  }
13326
14222
 
@@ -13479,6 +14375,7 @@ const parseQuery = (query, parameters) => {
13479
14375
  return { sqlStatement, parameters: parameters };
13480
14376
  };
13481
14377
 
14378
+ exports.ATTACHMENT_TABLE = ATTACHMENT_TABLE;
13482
14379
  exports.AbortOperation = AbortOperation;
13483
14380
  exports.AbstractPowerSyncDatabase = AbstractPowerSyncDatabase;
13484
14381
  exports.AbstractPowerSyncDatabaseOpenFactory = AbstractPowerSyncDatabaseOpenFactory;
@@ -13486,6 +14383,10 @@ exports.AbstractQueryProcessor = AbstractQueryProcessor;
13486
14383
  exports.AbstractRemote = AbstractRemote;
13487
14384
  exports.AbstractStreamingSyncImplementation = AbstractStreamingSyncImplementation;
13488
14385
  exports.ArrayComparator = ArrayComparator;
14386
+ exports.AttachmentContext = AttachmentContext;
14387
+ exports.AttachmentQueue = AttachmentQueue;
14388
+ exports.AttachmentService = AttachmentService;
14389
+ exports.AttachmentTable = AttachmentTable;
13489
14390
  exports.BaseObserver = BaseObserver;
13490
14391
  exports.Column = Column;
13491
14392
  exports.ConnectionClosedError = ConnectionClosedError;
@@ -13528,17 +14429,18 @@ exports.MEMORY_TRIGGER_CLAIM_MANAGER = MEMORY_TRIGGER_CLAIM_MANAGER;
13528
14429
  exports.OnChangeQueryProcessor = OnChangeQueryProcessor;
13529
14430
  exports.OpType = OpType;
13530
14431
  exports.OplogEntry = OplogEntry;
13531
- exports.RawTable = RawTable;
13532
14432
  exports.Schema = Schema;
13533
14433
  exports.SqliteBucketStorage = SqliteBucketStorage;
13534
14434
  exports.SyncDataBatch = SyncDataBatch;
13535
14435
  exports.SyncDataBucket = SyncDataBucket;
13536
14436
  exports.SyncProgress = SyncProgress;
13537
14437
  exports.SyncStatus = SyncStatus;
14438
+ exports.SyncingService = SyncingService;
13538
14439
  exports.Table = Table;
13539
14440
  exports.TableV2 = TableV2;
13540
14441
  exports.TriggerManagerImpl = TriggerManagerImpl;
13541
14442
  exports.UploadQueueStats = UploadQueueStats;
14443
+ exports.attachmentFromSql = attachmentFromSql;
13542
14444
  exports.column = column;
13543
14445
  exports.compilableQueryWatch = compilableQueryWatch;
13544
14446
  exports.createBaseLogger = createBaseLogger;
@@ -13557,6 +14459,7 @@ exports.isStreamingSyncCheckpointDiff = isStreamingSyncCheckpointDiff;
13557
14459
  exports.isStreamingSyncCheckpointPartiallyComplete = isStreamingSyncCheckpointPartiallyComplete;
13558
14460
  exports.isStreamingSyncData = isStreamingSyncData;
13559
14461
  exports.isSyncNewCheckpointRequest = isSyncNewCheckpointRequest;
14462
+ exports.mutexRunExclusive = mutexRunExclusive;
13560
14463
  exports.parseQuery = parseQuery;
13561
14464
  exports.runOnSchemaChange = runOnSchemaChange;
13562
14465
  exports.sanitizeSQL = sanitizeSQL;