@powersync/common 0.0.0-dev-20260630144038 → 0.0.0-dev-20261015114518

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 (48) hide show
  1. package/lib/attachments/AttachmentContext.js +2 -1
  2. package/lib/attachments/AttachmentContext.js.map +1 -1
  3. package/lib/attachments/AttachmentQueue.d.ts +98 -42
  4. package/lib/attachments/AttachmentQueue.js +103 -22
  5. package/lib/attachments/AttachmentQueue.js.map +1 -1
  6. package/lib/attachments/AttachmentTransportAdapter.d.ts +47 -0
  7. package/lib/attachments/AttachmentTransportAdapter.js +2 -0
  8. package/lib/attachments/AttachmentTransportAdapter.js.map +1 -0
  9. package/lib/attachments/BufferedAttachmentTransport.d.ts +24 -0
  10. package/lib/attachments/BufferedAttachmentTransport.js +32 -0
  11. package/lib/attachments/BufferedAttachmentTransport.js.map +1 -0
  12. package/lib/attachments/LocalStorageAdapter.d.ts +19 -0
  13. package/lib/attachments/Schema.d.ts +21 -2
  14. package/lib/attachments/Schema.js +14 -10
  15. package/lib/attachments/Schema.js.map +1 -1
  16. package/lib/attachments/SyncingService.d.ts +21 -8
  17. package/lib/attachments/SyncingService.js +59 -30
  18. package/lib/attachments/SyncingService.js.map +1 -1
  19. package/lib/client/CommonPowerSyncDatabase.d.ts +25 -1
  20. package/lib/client/connection/PowerSyncBackendConnector.d.ts +11 -0
  21. package/lib/client/connection/PowerSyncCredentials.d.ts +1 -1
  22. package/lib/client/runOnSchemaChange.js +2 -0
  23. package/lib/client/runOnSchemaChange.js.map +1 -1
  24. package/lib/client/sync/CheckpointRequest.d.ts +32 -0
  25. package/lib/client/sync/CheckpointRequest.js +2 -0
  26. package/lib/client/sync/CheckpointRequest.js.map +1 -0
  27. package/lib/client/sync/options.d.ts +42 -0
  28. package/lib/client/sync/options.js.map +1 -1
  29. package/lib/db/crud/SyncStatus.d.ts +1 -1
  30. package/lib/index.d.ts +3 -1
  31. package/lib/index.js +2 -0
  32. package/lib/index.js.map +1 -1
  33. package/package.json +4 -3
  34. package/src/attachments/AttachmentContext.ts +2 -1
  35. package/src/attachments/AttachmentQueue.ts +176 -68
  36. package/src/attachments/AttachmentTransportAdapter.ts +49 -0
  37. package/src/attachments/BufferedAttachmentTransport.ts +37 -0
  38. package/src/attachments/LocalStorageAdapter.ts +20 -0
  39. package/src/attachments/Schema.ts +29 -20
  40. package/src/attachments/SyncingService.ts +67 -34
  41. package/src/client/CommonPowerSyncDatabase.ts +26 -1
  42. package/src/client/connection/PowerSyncBackendConnector.ts +12 -0
  43. package/src/client/connection/PowerSyncCredentials.ts +1 -1
  44. package/src/client/runOnSchemaChange.ts +2 -0
  45. package/src/client/sync/CheckpointRequest.ts +31 -0
  46. package/src/client/sync/options.ts +44 -0
  47. package/src/db/crud/SyncStatus.ts +1 -1
  48. package/src/index.ts +9 -1
@@ -1,10 +1,13 @@
1
1
  import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js';
2
+ import { Mutex } from '../utils/mutex.js';
2
3
  import { LogLevels, PowerSyncLogger } from '../utils/Logger.js';
3
4
  import { Transaction } from '../db/DBAdapter.js';
4
5
  import { AttachmentContext } from './AttachmentContext.js';
5
6
  import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
6
7
  import { AttachmentService } from './AttachmentService.js';
7
- import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js';
8
+ import { AttachmentTransportAdapter } from './AttachmentTransportAdapter.js';
9
+ import { BufferedAttachmentTransport } from './BufferedAttachmentTransport.js';
10
+ import { AttachmentData, LocalStorageAdapter, StreamingLocalStorageAdapter } from './LocalStorageAdapter.js';
8
11
  import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
9
12
  import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js';
10
13
  import { SyncingService } from './SyncingService.js';
@@ -12,24 +15,22 @@ import { WatchedAttachmentItem } from './WatchedAttachmentItem.js';
12
15
  import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js';
13
16
 
14
17
  /**
15
- * Configuration options for {@link AttachmentQueue}.
18
+ * Fields common to every {@link AttachmentQueueOptions} variant.
16
19
  *
17
20
  * @experimental
18
21
  * @alpha This is currently experimental and may change without a major version bump.
19
22
  */
20
- export interface AttachmentQueueOptions {
23
+ export interface BaseAttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
21
24
  /**
22
25
  * PowerSync database instance
23
26
  */
24
27
  db: CommonPowerSyncDatabase;
25
28
  /**
26
- * Remote storage adapter for upload/download operations
27
- */
28
- remoteStorage: RemoteStorageAdapter;
29
- /**
30
- * Local storage adapter for file persistence
29
+ * Local storage adapter for file persistence. Its type determines whether
30
+ * {@link AttachmentQueue.saveFileFromUri} is available (a
31
+ * {@link StreamingLocalStorageAdapter} enables it).
31
32
  */
32
- localStorage: LocalStorageAdapter;
33
+ localStorage: TLocal;
33
34
  /**
34
35
  * Callback for monitoring attachment changes in your data model
35
36
  */
@@ -63,6 +64,52 @@ export interface AttachmentQueueOptions {
63
64
  errorHandler?: AttachmentErrorHandler;
64
65
  }
65
66
 
67
+ /**
68
+ * Configuration options for {@link AttachmentQueue}.
69
+ *
70
+ * Provide **exactly one** remote mechanism:
71
+ * - `remoteStorage` — a {@link RemoteStorageAdapter}, wrapped in the default
72
+ * default buffered transport that delegates upload/download/delete to it.
73
+ * - `transportAdapter` — an {@link AttachmentTransportAdapter} that owns all remote
74
+ * operations directly (e.g. a native file-URI implementation for buffer-free
75
+ * transfer of large files). No `remoteStorage` is needed in this case.
76
+ *
77
+ * Supplying both, or neither, is a type error.
78
+ *
79
+ * @experimental
80
+ * @alpha This is currently experimental and may change without a major version bump.
81
+ */
82
+ export type AttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> =
83
+ BaseAttachmentQueueOptions<TLocal> &
84
+ (
85
+ | { remoteStorage: RemoteStorageAdapter; transportAdapter?: never }
86
+ | { transportAdapter: AttachmentTransportAdapter; remoteStorage?: never }
87
+ );
88
+
89
+ /**
90
+ * Fields shared by {@link AttachmentQueue.saveFile} and {@link AttachmentQueue.saveFileFromUri}.
91
+ *
92
+ * @alpha
93
+ */
94
+ export interface SaveAttachmentOptions {
95
+ /** File extension (e.g., 'jpg', 'pdf') */
96
+ fileExtension: string;
97
+ /** MIME type of the file (e.g., 'image/jpeg') */
98
+ mediaType?: string;
99
+ /** Optional metadata to associate with the attachment */
100
+ metaData?: string;
101
+ /** Optional custom ID. If not provided, a UUID will be generated */
102
+ id?: string;
103
+ /**
104
+ * Optional callback to execute additional database operations within the same transaction as the
105
+ * attachment creation.
106
+ */
107
+ updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise<void>;
108
+ }
109
+
110
+ /** How the file bytes reach managed storage when creating an upload attachment. */
111
+ type AttachmentSource = { kind: 'data'; data: AttachmentData } | { kind: 'uri'; localUri: string };
112
+
66
113
  /**
67
114
  * AttachmentQueue manages the lifecycle and synchronization of attachments
68
115
  * between local and remote storage.
@@ -72,7 +119,7 @@ export interface AttachmentQueueOptions {
72
119
  * @experimental
73
120
  * @alpha This is currently experimental and may change without a major version bump.
74
121
  */
75
- export class AttachmentQueue {
122
+ export class AttachmentQueue<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
76
123
  /** Timer for periodic synchronization operations */
77
124
  private periodicSyncTimer?: ReturnType<typeof setInterval>;
78
125
 
@@ -80,10 +127,7 @@ export class AttachmentQueue {
80
127
  private readonly syncingService: SyncingService;
81
128
 
82
129
  /** Adapter for local file storage operations */
83
- readonly localStorage: LocalStorageAdapter;
84
-
85
- /** Adapter for remote file storage operations */
86
- readonly remoteStorage: RemoteStorageAdapter;
130
+ readonly localStorage: TLocal;
87
131
 
88
132
  /**
89
133
  * Callback function to watch for changes in attachment references in your data model.
@@ -134,6 +178,21 @@ export class AttachmentQueue {
134
178
 
135
179
  private watchAttachmentsAbortController!: AbortController;
136
180
 
181
+ /**
182
+ * Serializes concurrent `syncStorage()` triggers (periodic timer, watch-onDiff,
183
+ * status-changed). Held across the whole batch, but only contended by other
184
+ * sync triggers — foreground `saveFile` / `deleteFile` / watched-attachment
185
+ * processing don't take this lock and proceed in parallel via the
186
+ * `AttachmentService` mutex, which is acquired only briefly per row.
187
+ */
188
+ private syncLoopMutex: Mutex;
189
+
190
+ /**
191
+ * Aborted by `stopSync()` to interrupt an in-flight batch within one
192
+ * attachment's processing time. Polled between rows by `SyncingService`.
193
+ */
194
+ private syncAbortController?: AbortController;
195
+
137
196
  /**
138
197
  * Creates a new AttachmentQueue instance.
139
198
  *
@@ -143,6 +202,7 @@ export class AttachmentQueue {
143
202
  db,
144
203
  localStorage,
145
204
  remoteStorage,
205
+ transportAdapter,
146
206
  watchAttachments,
147
207
  logger,
148
208
  tableName = ATTACHMENT_TABLE,
@@ -151,9 +211,9 @@ export class AttachmentQueue {
151
211
  downloadAttachments = true,
152
212
  archivedCacheLimit = 100,
153
213
  errorHandler
154
- }: AttachmentQueueOptions) {
214
+ }: AttachmentQueueOptions<TLocal>) {
155
215
  this.db = db;
156
- this.remoteStorage = remoteStorage;
216
+ this.syncLoopMutex = db.createMutex();
157
217
  this.localStorage = localStorage;
158
218
  this.watchAttachments = watchAttachments;
159
219
  this.tableName = tableName;
@@ -163,10 +223,13 @@ export class AttachmentQueue {
163
223
  this.downloadAttachments = downloadAttachments;
164
224
  this.logger = logger ?? db.logger;
165
225
  this.attachmentService = new AttachmentService(db, this.logger, tableName, archivedCacheLimit);
226
+
227
+ const transport = transportAdapter ?? new BufferedAttachmentTransport(localStorage, remoteStorage!);
228
+
166
229
  this.syncingService = new SyncingService(
167
230
  this.attachmentService,
168
231
  localStorage,
169
- remoteStorage,
232
+ transport,
170
233
  this.logger,
171
234
  errorHandler
172
235
  );
@@ -192,17 +255,24 @@ export class AttachmentQueue {
192
255
  * - Handles state transitions for archived and new attachments
193
256
  */
194
257
  async startSync(): Promise<void> {
195
- await this.stopSync();
196
-
197
- this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
198
- throttleMs: this.syncThrottleDuration
199
- });
258
+ this.syncAbortController?.abort();
259
+ const syncAbortController = (this.syncAbortController = new AbortController());
260
+ await this.stopSyncAfterAbort();
200
261
 
201
262
  // immediately invoke the sync storage to initialize local storage
202
263
  await this.localStorage.initialize();
203
264
 
204
265
  await this.verifyAttachments();
205
266
 
267
+ // Skip rest of setup if a stopSync() aborted this call during the async setup steps.
268
+ if (syncAbortController.signal.aborted) {
269
+ return;
270
+ }
271
+
272
+ this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({
273
+ throttleMs: this.syncThrottleDuration
274
+ });
275
+
206
276
  // Sync storage periodically
207
277
  this.periodicSyncTimer = setInterval(async () => {
208
278
  await this.syncStorage();
@@ -324,22 +394,48 @@ export class AttachmentQueue {
324
394
  *
325
395
  * This is called automatically at regular intervals when sync is started,
326
396
  * but can also be called manually to trigger an immediate sync.
397
+ *
398
+ * Concurrent invocations are serialized via `syncLoopMutex`.
327
399
  */
328
400
  async syncStorage(): Promise<void> {
329
- await this.attachmentService.withContext(async (ctx) => {
330
- const activeAttachments = await ctx.getActiveAttachments();
331
- await this.localStorage.initialize();
332
- await this.syncingService.processAttachments(activeAttachments, ctx);
333
- await this.syncingService.deleteArchivedAttachments(ctx);
334
- });
401
+ const signal = this.syncAbortController?.signal;
402
+ // We have a signal from startSync() to stopSync(), so treat the absence of one like an aborted sync.
403
+ if (signal == null || signal?.aborted) return;
404
+
405
+ try {
406
+ await this.syncLoopMutex.runExclusive(async () => {
407
+ const activeAttachments = await this.attachmentService.withContext((ctx) => ctx.getActiveAttachments());
408
+ await this.localStorage.initialize();
409
+
410
+ await this.syncingService.processAttachments(activeAttachments, { signal });
411
+
412
+ if (signal.aborted) return;
413
+
414
+ await this.attachmentService.withContext((ctx) => this.syncingService.deleteArchivedAttachments(ctx));
415
+ }, signal);
416
+ } catch (error) {
417
+ // A queued batch's acquire rejects when `stopSync` aborts — expected, not an error.
418
+ if (signal.aborted) return;
419
+ throw error;
420
+ }
335
421
  }
336
422
 
337
423
  /**
338
424
  * Stops the attachment synchronization process.
339
425
  *
340
- * Clears the periodic sync timer and closes all active attachment watchers.
426
+ * Clears the periodic sync timer, closes all active attachment watchers, and
427
+ * aborts any in-flight `syncStorage()` call so it exits within one
428
+ * attachment's processing time instead of running the batch to completion.
341
429
  */
342
430
  async stopSync(): Promise<void> {
431
+ if (this.syncAbortController) {
432
+ this.syncAbortController.abort();
433
+ this.syncAbortController = undefined;
434
+ }
435
+ return this.stopSyncAfterAbort();
436
+ }
437
+
438
+ private async stopSyncAfterAbort(): Promise<void> {
343
439
  clearInterval(this.periodicSyncTimer);
344
440
  this.periodicSyncTimer = undefined;
345
441
  if (this.watchActiveAttachments) await this.watchActiveAttachments.close();
@@ -367,49 +463,30 @@ export class AttachmentQueue {
367
463
  return this.attachmentService.withContext(callback);
368
464
  }
369
465
  /**
370
- * Saves a file to local storage and queues it for upload to remote storage.
371
- *
372
- * @param options - File save options
373
- * @returns Promise resolving to the created attachment record
466
+ * Creates a `QUEUED_UPLOAD` attachment record, placing the file at the managed
467
+ * `localUri` from the given `source`, and persists the record
468
+ * alongside the caller's `updateHook` in a single transaction.
374
469
  */
375
- async saveFile({
376
- data,
377
- fileExtension,
378
- mediaType,
379
- metaData,
380
- id,
381
- updateHook
382
- }: {
383
- /**
384
- * The file data as ArrayBuffer, Blob, or base64 string
385
- */
386
- data: AttachmentData;
387
- /**
388
- * File extension (e.g., 'jpg', 'pdf')
389
- */
390
- fileExtension: string;
391
- /**
392
- * MIME type of the file (e.g., 'image/jpeg')
393
- */
394
- mediaType?: string;
395
- /**
396
- * Optional metadata to associate with the attachment
397
- */
398
- metaData?: string;
399
- /**
400
- * Optional custom ID. If not provided, a UUID will be generated
401
- */
402
- id?: string;
403
- /**
404
- * Optional callback to execute additional database operations within the same transaction as the attachment
405
- * creation.
406
- */
407
- updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise<void>;
408
- }): Promise<AttachmentRecord> {
470
+ private async createUploadAttachment(
471
+ { fileExtension, mediaType, metaData, id, updateHook }: SaveAttachmentOptions,
472
+ source: AttachmentSource
473
+ ): Promise<AttachmentRecord> {
409
474
  const resolvedId = id ?? (await this.generateAttachmentId());
410
475
  const filename = `${resolvedId}.${fileExtension}`;
411
476
  const localUri = this.localStorage.getLocalUri(filename);
412
- const size = await this.localStorage.saveFile(localUri, data);
477
+
478
+ let size: number;
479
+ if (source.kind === 'data') {
480
+ size = await this.localStorage.saveFile(localUri, source.data);
481
+ } else {
482
+ // saveFileFromUri is only exposed for streaming-capable local adapters; guard at
483
+ // runtime too for plain-JS callers.
484
+ const localStorage = this.localStorage as Partial<StreamingLocalStorageAdapter>;
485
+ if (!localStorage.moveFile) {
486
+ throw new Error('The configured local storage adapter does not support moveFile, required by saveFileFromUri.');
487
+ }
488
+ size = await localStorage.moveFile(source.localUri, localUri);
489
+ }
413
490
 
414
491
  const attachment: AttachmentRecord = {
415
492
  id: resolvedId,
@@ -433,6 +510,37 @@ export class AttachmentQueue {
433
510
  return attachment;
434
511
  }
435
512
 
513
+ /**
514
+ * Saves in-memory file data to local storage and queues it for upload.
515
+ *
516
+ * @param options - File data plus {@link SaveAttachmentOptions}
517
+ * @returns Promise resolving to the created attachment record
518
+ */
519
+ async saveFile(options: SaveAttachmentOptions & { data: AttachmentData }): Promise<AttachmentRecord> {
520
+ return this.createUploadAttachment(options, { kind: 'data', data: options.data });
521
+ }
522
+
523
+ /**
524
+ * Registers a file that already exists on disk and queues it for upload, moving it
525
+ * into managed storage without loading it into memory.
526
+ *
527
+ * Prefer this over {@link AttachmentQueue.saveFile} for large, app-originated files
528
+ * (recordings, videos): it avoids reading the file into an `ArrayBuffer` just to write
529
+ * it back to disk. Requires the local storage adapter to implement `moveFile`.
530
+ *
531
+ * Only available when the queue is configured with a {@link StreamingLocalStorageAdapter}
532
+ * (one that implements `moveFile`).
533
+ *
534
+ * @param options - The existing file's `localUri` plus {@link SaveAttachmentOptions}
535
+ * @returns Promise resolving to the created attachment record
536
+ */
537
+ async saveFileFromUri(
538
+ this: AttachmentQueue<StreamingLocalStorageAdapter>,
539
+ options: SaveAttachmentOptions & { localUri: string }
540
+ ): Promise<AttachmentRecord> {
541
+ return this.createUploadAttachment(options, { kind: 'uri', localUri: options.localUri });
542
+ }
543
+
436
544
  async deleteFile({
437
545
  id,
438
546
  updateHook
@@ -0,0 +1,49 @@
1
+ import { AttachmentRecord } from './Schema.js';
2
+
3
+ /**
4
+ * An {@link AttachmentRecord} that is guaranteed to have a `localUri`.
5
+ *
6
+ * The syncing service assigns `localUri` before invoking a transport download,
7
+ * so implementations always receive both the metadata and the destination path.
8
+ *
9
+ * @alpha
10
+ */
11
+ export type LocatedAttachmentRecord = AttachmentRecord & { localUri: string };
12
+
13
+ /**
14
+ * AttachmentTransportAdapter owns all remote-side operations for an attachment —
15
+ * transfer (upload/download) and delete — as single operations.
16
+ *
17
+ * A transport owns the entire transfer, letting implementations pick the most
18
+ * efficient mechanism available (buffer, stream, or a platform-native file-URI
19
+ * upload/download API). On platforms like React Native this allows large files to
20
+ * be transferred without ever materializing them in the JS heap.
21
+ *
22
+ * The default transport composes the local and remote storage adapters. Provide a custom transport (via
23
+ * `AttachmentQueue`'s `transportAdapter` option) to own the whole remote side; in
24
+ * that case a separate `remoteStorage` is not required.
25
+ *
26
+ * @experimental
27
+ * @alpha This is currently experimental and may change without a major version bump.
28
+ */
29
+ export interface AttachmentTransportAdapter {
30
+ /**
31
+ * Uploads the attachment's local file to remote storage.
32
+ * @param attachment - The attachment to upload. `localUri` points at the source file.
33
+ */
34
+ upload(attachment: LocatedAttachmentRecord): Promise<void>;
35
+
36
+ /**
37
+ * Downloads the remote file into `attachment.localUri`.
38
+ * @param attachment - The attachment to download. `localUri` is the destination path,
39
+ * assigned by the syncing service before this call.
40
+ */
41
+ download(attachment: LocatedAttachmentRecord): Promise<void>;
42
+
43
+ /**
44
+ * Deletes the attachment's file from remote storage. Local file removal is handled
45
+ * separately by the syncing service.
46
+ * @param attachment - The attachment to delete.
47
+ */
48
+ delete(attachment: AttachmentRecord): Promise<void>;
49
+ }
@@ -0,0 +1,37 @@
1
+ import { AttachmentTransportAdapter, LocatedAttachmentRecord } from './AttachmentTransportAdapter.js';
2
+ import { LocalStorageAdapter } from './LocalStorageAdapter.js';
3
+ import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
4
+ import { AttachmentRecord } from './Schema.js';
5
+
6
+ /**
7
+ * Default {@link AttachmentTransportAdapter}, composing the local and remote
8
+ * storage adapters.
9
+ *
10
+ * The full file body is materialized as an `ArrayBuffer` in JS memory between the
11
+ * two adapter calls. This is fine for small files but can cause memory pressure on
12
+ * large ones — environments that support native transfer should provide a custom
13
+ * transport instead.
14
+ *
15
+ * @experimental
16
+ * @alpha This is currently experimental and may change without a major version bump.
17
+ */
18
+ export class BufferedAttachmentTransport implements AttachmentTransportAdapter {
19
+ constructor(
20
+ private localStorage: LocalStorageAdapter,
21
+ private remoteStorage: RemoteStorageAdapter
22
+ ) {}
23
+
24
+ async upload(attachment: LocatedAttachmentRecord): Promise<void> {
25
+ const fileData = await this.localStorage.readFile(attachment.localUri);
26
+ await this.remoteStorage.uploadFile(fileData, attachment);
27
+ }
28
+
29
+ async download(attachment: LocatedAttachmentRecord): Promise<void> {
30
+ const fileData = await this.remoteStorage.downloadFile(attachment);
31
+ await this.localStorage.saveFile(attachment.localUri, fileData);
32
+ }
33
+
34
+ async delete(attachment: AttachmentRecord): Promise<void> {
35
+ await this.remoteStorage.deleteFile(attachment);
36
+ }
37
+ }
@@ -76,3 +76,23 @@ export interface LocalStorageAdapter {
76
76
  */
77
77
  getLocalUri(filename: string): string;
78
78
  }
79
+
80
+ /**
81
+ * A {@link LocalStorageAdapter} that can relocate a file into managed storage without
82
+ * loading it into memory. Required for {@link AttachmentQueue.saveFileFromUri}; only
83
+ * queues configured with a streaming-capable local adapter expose that method.
84
+ *
85
+ * @experimental
86
+ * @alpha This is currently experimental and may change without a major version bump.
87
+ */
88
+ export interface StreamingLocalStorageAdapter extends LocalStorageAdapter {
89
+ /**
90
+ * Moves a file into managed storage without loading it into memory.
91
+ * Overwrites any existing file at the target. When source and target are the same
92
+ * path, this is a no-op that just reports the size.
93
+ * @param sourceUri - Path of the existing file
94
+ * @param targetUri - Destination path within managed storage
95
+ * @returns Number of bytes in the moved file
96
+ */
97
+ moveFile(sourceUri: string, targetUri: string): Promise<number>;
98
+ }
@@ -1,5 +1,5 @@
1
1
  import { column } from '../db/schema/Column.js';
2
- import { Table } from '../db/schema/Table.js';
2
+ import { RowType, Table } from '../db/schema/Table.js';
3
3
  import { TableOptions } from '../db/schema/Table.js';
4
4
 
5
5
  /**
@@ -66,30 +66,39 @@ export enum AttachmentState {
66
66
  */
67
67
  export interface AttachmentTableOptions extends Omit<TableOptions, 'name' | 'columns'> {}
68
68
 
69
+ /**
70
+ * @alpha
71
+ */
72
+ export const ATTACHMENT_TABLE_COLUMNS = {
73
+ filename: column.text,
74
+ local_uri: column.text,
75
+ timestamp: column.integer,
76
+ size: column.integer,
77
+ media_type: column.text,
78
+ state: column.integer, // Corresponds to AttachmentState
79
+ has_synced: column.integer,
80
+ meta_data: column.text
81
+ };
82
+
69
83
  /**
70
84
  * AttachmentTable defines the schema for the attachment queue table.
71
85
  *
72
86
  * @alpha
73
87
  */
74
- export class AttachmentTable extends Table {
88
+ export class AttachmentTable extends Table<typeof ATTACHMENT_TABLE_COLUMNS> {
75
89
  constructor(options?: AttachmentTableOptions) {
76
- super(
77
- {
78
- filename: column.text,
79
- local_uri: column.text,
80
- timestamp: column.integer,
81
- size: column.integer,
82
- media_type: column.text,
83
- state: column.integer, // Corresponds to AttachmentState
84
- has_synced: column.integer,
85
- meta_data: column.text
86
- },
87
- {
88
- ...options,
89
- viewName: options?.viewName ?? ATTACHMENT_TABLE,
90
- localOnly: true,
91
- insertOnly: false
92
- }
93
- );
90
+ super(ATTACHMENT_TABLE_COLUMNS, {
91
+ ...options,
92
+ viewName: options?.viewName ?? ATTACHMENT_TABLE,
93
+ localOnly: true,
94
+ insertOnly: false
95
+ });
94
96
  }
95
97
  }
98
+
99
+ /**
100
+ * AttachmentTableRecord represents the row type of the attachment table.
101
+ *
102
+ * @alpha
103
+ */
104
+ export type AttachmentTableRecord = RowType<AttachmentTable>;