@powersync/common 1.57.3 → 2.1.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 (229) hide show
  1. package/lib/attachments/AttachmentContext.d.ts +5 -5
  2. package/lib/attachments/AttachmentContext.js +6 -2
  3. package/lib/attachments/AttachmentContext.js.map +1 -1
  4. package/lib/attachments/AttachmentQueue.d.ts +85 -47
  5. package/lib/attachments/AttachmentQueue.js +55 -18
  6. package/lib/attachments/AttachmentQueue.js.map +1 -1
  7. package/lib/attachments/AttachmentService.d.ts +3 -3
  8. package/lib/attachments/AttachmentService.js +4 -3
  9. package/lib/attachments/AttachmentService.js.map +1 -1
  10. package/lib/attachments/AttachmentTransportAdapter.d.ts +47 -0
  11. package/lib/attachments/AttachmentTransportAdapter.js +2 -0
  12. package/lib/attachments/AttachmentTransportAdapter.js.map +1 -0
  13. package/lib/attachments/BufferedAttachmentTransport.d.ts +24 -0
  14. package/lib/attachments/BufferedAttachmentTransport.js +32 -0
  15. package/lib/attachments/BufferedAttachmentTransport.js.map +1 -0
  16. package/lib/attachments/LocalStorageAdapter.d.ts +19 -0
  17. package/lib/attachments/Schema.d.ts +2 -2
  18. package/lib/attachments/SyncingService.d.ts +8 -5
  19. package/lib/attachments/SyncingService.js +31 -15
  20. package/lib/attachments/SyncingService.js.map +1 -1
  21. package/lib/client/{AbstractPowerSyncDatabase.d.ts → CommonPowerSyncDatabase.d.ts} +56 -232
  22. package/lib/client/CommonPowerSyncDatabase.js +2 -0
  23. package/lib/client/CommonPowerSyncDatabase.js.map +1 -0
  24. package/lib/client/SQLOpenFactory.d.ts +35 -13
  25. package/lib/client/SQLOpenFactory.js +1 -25
  26. package/lib/client/SQLOpenFactory.js.map +1 -1
  27. package/lib/client/compilableQueryWatch.d.ts +2 -2
  28. package/lib/client/compilableQueryWatch.js +1 -2
  29. package/lib/client/compilableQueryWatch.js.map +1 -1
  30. package/lib/client/connection/PowerSyncBackendConnector.d.ts +3 -3
  31. package/lib/client/runOnSchemaChange.d.ts +2 -2
  32. package/lib/client/runOnSchemaChange.js.map +1 -1
  33. package/lib/client/sync/bucket/CrudEntry.d.ts +3 -34
  34. package/lib/client/sync/bucket/CrudEntry.js +0 -99
  35. package/lib/client/sync/bucket/CrudEntry.js.map +1 -1
  36. package/lib/client/sync/options.d.ts +65 -0
  37. package/lib/client/sync/options.js +25 -0
  38. package/lib/client/sync/options.js.map +1 -0
  39. package/lib/client/sync/sync-streams.d.ts +1 -1
  40. package/lib/client/triggers/TriggerManager.d.ts +9 -38
  41. package/lib/client/watched/GetAllQuery.d.ts +3 -3
  42. package/lib/client/watched/GetAllQuery.js +1 -1
  43. package/lib/client/watched/GetAllQuery.js.map +1 -1
  44. package/lib/client/watched/WatchedQuery.d.ts +2 -10
  45. package/lib/client/watched/WatchedQuery.js +0 -11
  46. package/lib/client/watched/WatchedQuery.js.map +1 -1
  47. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +0 -50
  48. package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -176
  49. package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -1
  50. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +0 -22
  51. package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -82
  52. package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -1
  53. package/lib/db/DBAdapter.d.ts +72 -154
  54. package/lib/db/DBAdapter.js +70 -108
  55. package/lib/db/DBAdapter.js.map +1 -1
  56. package/lib/db/QueryResult.d.ts +104 -0
  57. package/lib/db/QueryResult.js +96 -0
  58. package/lib/db/QueryResult.js.map +1 -0
  59. package/lib/db/crud/SyncProgress.d.ts +1 -13
  60. package/lib/db/crud/SyncProgress.js +1 -62
  61. package/lib/db/crud/SyncProgress.js.map +1 -1
  62. package/lib/db/crud/SyncStatus.d.ts +32 -66
  63. package/lib/db/crud/SyncStatus.js +1 -253
  64. package/lib/db/crud/SyncStatus.js.map +1 -1
  65. package/lib/db/schema/Column.d.ts +0 -7
  66. package/lib/db/schema/Column.js +0 -7
  67. package/lib/db/schema/Column.js.map +1 -1
  68. package/lib/db/schema/Index.d.ts +2 -6
  69. package/lib/db/schema/Index.js +1 -4
  70. package/lib/db/schema/Index.js.map +1 -1
  71. package/lib/db/schema/IndexedColumn.d.ts +2 -6
  72. package/lib/db/schema/IndexedColumn.js +1 -4
  73. package/lib/db/schema/IndexedColumn.js.map +1 -1
  74. package/lib/db/schema/Schema.d.ts +4 -28
  75. package/lib/db/schema/Schema.js.map +1 -1
  76. package/lib/db/schema/Table.d.ts +50 -96
  77. package/lib/db/schema/Table.js +83 -91
  78. package/lib/db/schema/Table.js.map +1 -1
  79. package/lib/index.d.ts +6 -21
  80. package/lib/index.js +6 -22
  81. package/lib/index.js.map +1 -1
  82. package/lib/utils/BaseObserver.d.ts +1 -1
  83. package/lib/utils/BaseObserver.js +1 -1
  84. package/lib/utils/Logger.d.ts +52 -23
  85. package/lib/utils/Logger.js +35 -33
  86. package/lib/utils/Logger.js.map +1 -1
  87. package/lib/utils/MetaBaseObserver.d.ts +11 -11
  88. package/lib/utils/MetaBaseObserver.js +1 -50
  89. package/lib/utils/MetaBaseObserver.js.map +1 -1
  90. package/lib/utils/mutex.d.ts +3 -53
  91. package/lib/utils/mutex.js +1 -156
  92. package/lib/utils/mutex.js.map +1 -1
  93. package/package.json +8 -45
  94. package/src/attachments/AttachmentContext.ts +11 -10
  95. package/src/attachments/AttachmentQueue.ts +129 -70
  96. package/src/attachments/AttachmentService.ts +7 -6
  97. package/src/attachments/AttachmentTransportAdapter.ts +49 -0
  98. package/src/attachments/BufferedAttachmentTransport.ts +37 -0
  99. package/src/attachments/LocalStorageAdapter.ts +20 -0
  100. package/src/attachments/Schema.ts +2 -2
  101. package/src/attachments/SyncingService.ts +34 -20
  102. package/src/client/CommonPowerSyncDatabase.ts +540 -0
  103. package/src/client/SQLOpenFactory.ts +38 -22
  104. package/src/client/compilableQueryWatch.ts +3 -4
  105. package/src/client/connection/PowerSyncBackendConnector.ts +3 -3
  106. package/src/client/runOnSchemaChange.ts +2 -2
  107. package/src/client/sync/bucket/CrudEntry.ts +4 -104
  108. package/src/client/sync/options.ts +77 -0
  109. package/src/client/sync/sync-streams.ts +1 -1
  110. package/src/client/triggers/TriggerManager.ts +10 -41
  111. package/src/client/watched/GetAllQuery.ts +3 -3
  112. package/src/client/watched/WatchedQuery.ts +2 -15
  113. package/src/client/watched/processors/DifferentialQueryProcessor.ts +0 -223
  114. package/src/client/watched/processors/OnChangeQueryProcessor.ts +0 -111
  115. package/src/db/DBAdapter.ts +141 -226
  116. package/src/db/QueryResult.ts +195 -0
  117. package/src/db/crud/SyncProgress.ts +2 -42
  118. package/src/db/crud/SyncStatus.ts +45 -220
  119. package/src/db/schema/Column.ts +0 -8
  120. package/src/db/schema/Index.ts +3 -6
  121. package/src/db/schema/IndexedColumn.ts +3 -6
  122. package/src/db/schema/Schema.ts +4 -4
  123. package/src/db/schema/Table.ts +111 -186
  124. package/src/index.ts +6 -21
  125. package/src/utils/BaseObserver.ts +1 -1
  126. package/src/utils/Logger.ts +80 -42
  127. package/src/utils/MetaBaseObserver.ts +14 -60
  128. package/src/utils/mutex.ts +4 -201
  129. package/dist/bundle.cjs +0 -14398
  130. package/dist/bundle.cjs.map +0 -1
  131. package/dist/bundle.mjs +0 -14317
  132. package/dist/bundle.mjs.map +0 -1
  133. package/dist/bundle.node.cjs +0 -12029
  134. package/dist/bundle.node.cjs.map +0 -1
  135. package/dist/bundle.node.mjs +0 -11948
  136. package/dist/bundle.node.mjs.map +0 -1
  137. package/dist/index.d.cts +0 -4651
  138. package/legacy/sync_protocol.d.ts +0 -103
  139. package/lib/client/AbstractPowerSyncDatabase.js +0 -990
  140. package/lib/client/AbstractPowerSyncDatabase.js.map +0 -1
  141. package/lib/client/AbstractPowerSyncOpenFactory.d.ts +0 -28
  142. package/lib/client/AbstractPowerSyncOpenFactory.js +0 -28
  143. package/lib/client/AbstractPowerSyncOpenFactory.js.map +0 -1
  144. package/lib/client/ConnectionManager.d.ts +0 -115
  145. package/lib/client/ConnectionManager.js +0 -294
  146. package/lib/client/ConnectionManager.js.map +0 -1
  147. package/lib/client/CustomQuery.d.ts +0 -22
  148. package/lib/client/CustomQuery.js +0 -43
  149. package/lib/client/CustomQuery.js.map +0 -1
  150. package/lib/client/constants.d.ts +0 -4
  151. package/lib/client/constants.js +0 -5
  152. package/lib/client/constants.js.map +0 -1
  153. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +0 -56
  154. package/lib/client/sync/bucket/BucketStorageAdapter.js +0 -29
  155. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +0 -1
  156. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +0 -37
  157. package/lib/client/sync/bucket/SqliteBucketStorage.js +0 -165
  158. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +0 -1
  159. package/lib/client/sync/stream/AbstractRemote.d.ts +0 -180
  160. package/lib/client/sync/stream/AbstractRemote.js +0 -508
  161. package/lib/client/sync/stream/AbstractRemote.js.map +0 -1
  162. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +0 -265
  163. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +0 -706
  164. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +0 -1
  165. package/lib/client/sync/stream/WebsocketClientTransport.d.ts +0 -15
  166. package/lib/client/sync/stream/WebsocketClientTransport.js +0 -62
  167. package/lib/client/sync/stream/WebsocketClientTransport.js.map +0 -1
  168. package/lib/client/sync/stream/core-instruction.d.ts +0 -77
  169. package/lib/client/sync/stream/core-instruction.js +0 -30
  170. package/lib/client/sync/stream/core-instruction.js.map +0 -1
  171. package/lib/client/triggers/MemoryTriggerClaimManager.d.ts +0 -6
  172. package/lib/client/triggers/MemoryTriggerClaimManager.js +0 -21
  173. package/lib/client/triggers/MemoryTriggerClaimManager.js.map +0 -1
  174. package/lib/client/triggers/TriggerManagerImpl.d.ts +0 -39
  175. package/lib/client/triggers/TriggerManagerImpl.js +0 -410
  176. package/lib/client/triggers/TriggerManagerImpl.js.map +0 -1
  177. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +0 -68
  178. package/lib/client/watched/processors/AbstractQueryProcessor.js +0 -151
  179. package/lib/client/watched/processors/AbstractQueryProcessor.js.map +0 -1
  180. package/lib/db/ConnectionClosedError.d.ts +0 -12
  181. package/lib/db/ConnectionClosedError.js +0 -23
  182. package/lib/db/ConnectionClosedError.js.map +0 -1
  183. package/lib/db/schema/TableV2.d.ts +0 -11
  184. package/lib/db/schema/TableV2.js +0 -11
  185. package/lib/db/schema/TableV2.js.map +0 -1
  186. package/lib/utils/AbortOperation.d.ts +0 -11
  187. package/lib/utils/AbortOperation.js +0 -21
  188. package/lib/utils/AbortOperation.js.map +0 -1
  189. package/lib/utils/ControlledExecutor.d.ts +0 -31
  190. package/lib/utils/ControlledExecutor.js +0 -58
  191. package/lib/utils/ControlledExecutor.js.map +0 -1
  192. package/lib/utils/async.d.ts +0 -46
  193. package/lib/utils/async.js +0 -147
  194. package/lib/utils/async.js.map +0 -1
  195. package/lib/utils/compatibility.d.ts +0 -8
  196. package/lib/utils/compatibility.js +0 -9
  197. package/lib/utils/compatibility.js.map +0 -1
  198. package/lib/utils/parseQuery.d.ts +0 -12
  199. package/lib/utils/parseQuery.js +0 -20
  200. package/lib/utils/parseQuery.js.map +0 -1
  201. package/lib/utils/queue.d.ts +0 -16
  202. package/lib/utils/queue.js +0 -42
  203. package/lib/utils/queue.js.map +0 -1
  204. package/lib/utils/stream_transform.d.ts +0 -41
  205. package/lib/utils/stream_transform.js +0 -211
  206. package/lib/utils/stream_transform.js.map +0 -1
  207. package/src/client/AbstractPowerSyncDatabase.ts +0 -1408
  208. package/src/client/AbstractPowerSyncOpenFactory.ts +0 -45
  209. package/src/client/ConnectionManager.ts +0 -405
  210. package/src/client/CustomQuery.ts +0 -56
  211. package/src/client/constants.ts +0 -4
  212. package/src/client/sync/bucket/BucketStorageAdapter.ts +0 -65
  213. package/src/client/sync/bucket/SqliteBucketStorage.ts +0 -202
  214. package/src/client/sync/stream/AbstractRemote.ts +0 -648
  215. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +0 -971
  216. package/src/client/sync/stream/WebsocketClientTransport.ts +0 -82
  217. package/src/client/sync/stream/core-instruction.ts +0 -109
  218. package/src/client/triggers/MemoryTriggerClaimManager.ts +0 -25
  219. package/src/client/triggers/TriggerManagerImpl.ts +0 -501
  220. package/src/client/watched/processors/AbstractQueryProcessor.ts +0 -226
  221. package/src/db/ConnectionClosedError.ts +0 -25
  222. package/src/db/schema/TableV2.ts +0 -11
  223. package/src/utils/AbortOperation.ts +0 -19
  224. package/src/utils/ControlledExecutor.ts +0 -81
  225. package/src/utils/async.ts +0 -185
  226. package/src/utils/compatibility.ts +0 -9
  227. package/src/utils/parseQuery.ts +0 -31
  228. package/src/utils/queue.ts +0 -48
  229. package/src/utils/stream_transform.ts +0 -260
@@ -1,37 +1,36 @@
1
- import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js';
2
- import { DEFAULT_WATCH_THROTTLE_MS } from '../client/watched/WatchedQuery.js';
3
1
  import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js';
4
2
  import { Mutex } from '../utils/mutex.js';
3
+ import { LogLevels, PowerSyncLogger } from '../utils/Logger.js';
5
4
  import { Transaction } from '../db/DBAdapter.js';
6
- import { ILogger } from '../utils/Logger.js';
7
5
  import { AttachmentContext } from './AttachmentContext.js';
8
6
  import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
9
7
  import { AttachmentService } from './AttachmentService.js';
10
- 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';
11
11
  import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
12
12
  import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js';
13
13
  import { SyncingService } from './SyncingService.js';
14
14
  import { WatchedAttachmentItem } from './WatchedAttachmentItem.js';
15
+ import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js';
15
16
 
16
17
  /**
17
- * Configuration options for {@link AttachmentQueue}.
18
+ * Fields common to every {@link AttachmentQueueOptions} variant.
18
19
  *
19
20
  * @experimental
20
21
  * @alpha This is currently experimental and may change without a major version bump.
21
22
  */
22
- export interface AttachmentQueueOptions {
23
+ export interface BaseAttachmentQueueOptions<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
23
24
  /**
24
25
  * PowerSync database instance
25
26
  */
26
- db: AbstractPowerSyncDatabase;
27
- /**
28
- * Remote storage adapter for upload/download operations
29
- */
30
- remoteStorage: RemoteStorageAdapter;
27
+ db: CommonPowerSyncDatabase;
31
28
  /**
32
- * 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).
33
32
  */
34
- localStorage: LocalStorageAdapter;
33
+ localStorage: TLocal;
35
34
  /**
36
35
  * Callback for monitoring attachment changes in your data model
37
36
  */
@@ -43,7 +42,7 @@ export interface AttachmentQueueOptions {
43
42
  /**
44
43
  * Logger instance. Defaults to db.logger
45
44
  */
46
- logger?: ILogger;
45
+ logger?: PowerSyncLogger;
47
46
  /**
48
47
  * Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
49
48
  */
@@ -65,6 +64,52 @@ export interface AttachmentQueueOptions {
65
64
  errorHandler?: AttachmentErrorHandler;
66
65
  }
67
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
+
68
113
  /**
69
114
  * AttachmentQueue manages the lifecycle and synchronization of attachments
70
115
  * between local and remote storage.
@@ -74,7 +119,7 @@ export interface AttachmentQueueOptions {
74
119
  * @experimental
75
120
  * @alpha This is currently experimental and may change without a major version bump.
76
121
  */
77
- export class AttachmentQueue {
122
+ export class AttachmentQueue<TLocal extends LocalStorageAdapter = LocalStorageAdapter> {
78
123
  /** Timer for periodic synchronization operations */
79
124
  private periodicSyncTimer?: ReturnType<typeof setInterval>;
80
125
 
@@ -82,10 +127,7 @@ export class AttachmentQueue {
82
127
  private readonly syncingService: SyncingService;
83
128
 
84
129
  /** Adapter for local file storage operations */
85
- readonly localStorage: LocalStorageAdapter;
86
-
87
- /** Adapter for remote file storage operations */
88
- readonly remoteStorage: RemoteStorageAdapter;
130
+ readonly localStorage: TLocal;
89
131
 
90
132
  /**
91
133
  * Callback function to watch for changes in attachment references in your data model.
@@ -103,7 +145,7 @@ export class AttachmentQueue {
103
145
  readonly tableName: string;
104
146
 
105
147
  /** Logger instance for diagnostic information */
106
- readonly logger: ILogger;
148
+ readonly logger: PowerSyncLogger;
107
149
 
108
150
  /** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
109
151
  * failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
@@ -115,7 +157,7 @@ export class AttachmentQueue {
115
157
  * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
116
158
  * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
117
159
  * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
118
- readonly syncThrottleDuration: number;
160
+ readonly syncThrottleDuration?: number;
119
161
 
120
162
  /** Whether to automatically download remote attachments. Default: true */
121
163
  readonly downloadAttachments: boolean = true;
@@ -127,7 +169,7 @@ export class AttachmentQueue {
127
169
  private readonly attachmentService: AttachmentService;
128
170
 
129
171
  /** PowerSync database instance */
130
- private readonly db: AbstractPowerSyncDatabase;
172
+ private readonly db: CommonPowerSyncDatabase;
131
173
 
132
174
  /** Cleanup function for status change listener */
133
175
  private statusListenerDispose?: () => void;
@@ -143,7 +185,7 @@ export class AttachmentQueue {
143
185
  * processing don't take this lock and proceed in parallel via the
144
186
  * `AttachmentService` mutex, which is acquired only briefly per row.
145
187
  */
146
- private syncLoopMutex = new Mutex();
188
+ private syncLoopMutex: Mutex;
147
189
 
148
190
  /**
149
191
  * Aborted by `stopSync()` to interrupt an in-flight batch within one
@@ -160,17 +202,18 @@ export class AttachmentQueue {
160
202
  db,
161
203
  localStorage,
162
204
  remoteStorage,
205
+ transportAdapter,
163
206
  watchAttachments,
164
207
  logger,
165
208
  tableName = ATTACHMENT_TABLE,
166
209
  syncIntervalMs = 30 * 1000,
167
- syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS,
210
+ syncThrottleDuration,
168
211
  downloadAttachments = true,
169
212
  archivedCacheLimit = 100,
170
213
  errorHandler
171
- }: AttachmentQueueOptions) {
214
+ }: AttachmentQueueOptions<TLocal>) {
172
215
  this.db = db;
173
- this.remoteStorage = remoteStorage;
216
+ this.syncLoopMutex = db.createMutex();
174
217
  this.localStorage = localStorage;
175
218
  this.watchAttachments = watchAttachments;
176
219
  this.tableName = tableName;
@@ -180,10 +223,13 @@ export class AttachmentQueue {
180
223
  this.downloadAttachments = downloadAttachments;
181
224
  this.logger = logger ?? db.logger;
182
225
  this.attachmentService = new AttachmentService(db, this.logger, tableName, archivedCacheLimit);
226
+
227
+ const transport = transportAdapter ?? new BufferedAttachmentTransport(localStorage, remoteStorage!);
228
+
183
229
  this.syncingService = new SyncingService(
184
230
  this.attachmentService,
185
231
  localStorage,
186
- remoteStorage,
232
+ transport,
187
233
  this.logger,
188
234
  errorHandler
189
235
  );
@@ -239,7 +285,7 @@ export class AttachmentQueue {
239
285
  if (status.connected) {
240
286
  // Device came online, process attachments immediately
241
287
  this.syncStorage().catch((error) => {
242
- this.logger.error('Error syncing storage on connection:', error);
288
+ this.logger.log({ level: LogLevels.error, message: 'Error syncing storage on connection', error });
243
289
  });
244
290
  }
245
291
  }
@@ -348,7 +394,8 @@ export class AttachmentQueue {
348
394
  */
349
395
  async syncStorage(): Promise<void> {
350
396
  const signal = this.syncAbortController?.signal;
351
- if (signal?.aborted) return;
397
+ // We have a signal from startSync() to stopSync(), so treat the absence of one like an aborted sync.
398
+ if (signal == null || signal?.aborted) return;
352
399
 
353
400
  try {
354
401
  await this.syncLoopMutex.runExclusive(async () => {
@@ -357,13 +404,13 @@ export class AttachmentQueue {
357
404
 
358
405
  await this.syncingService.processAttachments(activeAttachments, { signal });
359
406
 
360
- if (signal?.aborted) return;
407
+ if (signal.aborted) return;
361
408
 
362
409
  await this.attachmentService.withContext((ctx) => this.syncingService.deleteArchivedAttachments(ctx));
363
410
  }, signal);
364
411
  } catch (error) {
365
412
  // A queued batch's acquire rejects when `stopSync` aborts — expected, not an error.
366
- if (signal?.aborted) return;
413
+ if (signal.aborted) return;
367
414
  throw error;
368
415
  }
369
416
  }
@@ -407,49 +454,30 @@ export class AttachmentQueue {
407
454
  return this.attachmentService.withContext(callback);
408
455
  }
409
456
  /**
410
- * Saves a file to local storage and queues it for upload to remote storage.
411
- *
412
- * @param options - File save options
413
- * @returns Promise resolving to the created attachment record
457
+ * Creates a `QUEUED_UPLOAD` attachment record, placing the file at the managed
458
+ * `localUri` from the given `source`, and persists the record
459
+ * alongside the caller's `updateHook` in a single transaction.
414
460
  */
415
- async saveFile({
416
- data,
417
- fileExtension,
418
- mediaType,
419
- metaData,
420
- id,
421
- updateHook
422
- }: {
423
- /**
424
- * The file data as ArrayBuffer, Blob, or base64 string
425
- */
426
- data: AttachmentData;
427
- /**
428
- * File extension (e.g., 'jpg', 'pdf')
429
- */
430
- fileExtension: string;
431
- /**
432
- * MIME type of the file (e.g., 'image/jpeg')
433
- */
434
- mediaType?: string;
435
- /**
436
- * Optional metadata to associate with the attachment
437
- */
438
- metaData?: string;
439
- /**
440
- * Optional custom ID. If not provided, a UUID will be generated
441
- */
442
- id?: string;
443
- /**
444
- * Optional callback to execute additional database operations within the same transaction as the attachment
445
- * creation.
446
- */
447
- updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise<void>;
448
- }): Promise<AttachmentRecord> {
461
+ private async createUploadAttachment(
462
+ { fileExtension, mediaType, metaData, id, updateHook }: SaveAttachmentOptions,
463
+ source: AttachmentSource
464
+ ): Promise<AttachmentRecord> {
449
465
  const resolvedId = id ?? (await this.generateAttachmentId());
450
466
  const filename = `${resolvedId}.${fileExtension}`;
451
467
  const localUri = this.localStorage.getLocalUri(filename);
452
- const size = await this.localStorage.saveFile(localUri, data);
468
+
469
+ let size: number;
470
+ if (source.kind === 'data') {
471
+ size = await this.localStorage.saveFile(localUri, source.data);
472
+ } else {
473
+ // saveFileFromUri is only exposed for streaming-capable local adapters; guard at
474
+ // runtime too for plain-JS callers.
475
+ const localStorage = this.localStorage as Partial<StreamingLocalStorageAdapter>;
476
+ if (!localStorage.moveFile) {
477
+ throw new Error('The configured local storage adapter does not support moveFile, required by saveFileFromUri.');
478
+ }
479
+ size = await localStorage.moveFile(source.localUri, localUri);
480
+ }
453
481
 
454
482
  const attachment: AttachmentRecord = {
455
483
  id: resolvedId,
@@ -473,6 +501,37 @@ export class AttachmentQueue {
473
501
  return attachment;
474
502
  }
475
503
 
504
+ /**
505
+ * Saves in-memory file data to local storage and queues it for upload.
506
+ *
507
+ * @param options - File data plus {@link SaveAttachmentOptions}
508
+ * @returns Promise resolving to the created attachment record
509
+ */
510
+ async saveFile(options: SaveAttachmentOptions & { data: AttachmentData }): Promise<AttachmentRecord> {
511
+ return this.createUploadAttachment(options, { kind: 'data', data: options.data });
512
+ }
513
+
514
+ /**
515
+ * Registers a file that already exists on disk and queues it for upload, moving it
516
+ * into managed storage without loading it into memory.
517
+ *
518
+ * Prefer this over {@link AttachmentQueue.saveFile} for large, app-originated files
519
+ * (recordings, videos): it avoids reading the file into an `ArrayBuffer` just to write
520
+ * it back to disk. Requires the local storage adapter to implement `moveFile`.
521
+ *
522
+ * Only available when the queue is configured with a {@link StreamingLocalStorageAdapter}
523
+ * (one that implements `moveFile`).
524
+ *
525
+ * @param options - The existing file's `localUri` plus {@link SaveAttachmentOptions}
526
+ * @returns Promise resolving to the created attachment record
527
+ */
528
+ async saveFileFromUri(
529
+ this: AttachmentQueue<StreamingLocalStorageAdapter>,
530
+ options: SaveAttachmentOptions & { localUri: string }
531
+ ): Promise<AttachmentRecord> {
532
+ return this.createUploadAttachment(options, { kind: 'uri', localUri: options.localUri });
533
+ }
534
+
476
535
  async deleteFile({
477
536
  id,
478
537
  updateHook
@@ -1,6 +1,6 @@
1
- import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js';
1
+ import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js';
2
2
  import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js';
3
- import { ILogger } from '../utils/Logger.js';
3
+ import { PowerSyncLogger, LogLevels } from '../utils/Logger.js';
4
4
  import { Mutex } from '../utils/mutex.js';
5
5
  import { AttachmentContext } from './AttachmentContext.js';
6
6
  import { AttachmentRecord, AttachmentState } from './Schema.js';
@@ -11,15 +11,16 @@ import { AttachmentRecord, AttachmentState } from './Schema.js';
11
11
  * @internal
12
12
  */
13
13
  export class AttachmentService {
14
- private mutex = new Mutex();
14
+ private mutex: Mutex;
15
15
  private context: AttachmentContext;
16
16
 
17
17
  constructor(
18
- private db: AbstractPowerSyncDatabase,
19
- private logger: ILogger,
18
+ private db: CommonPowerSyncDatabase,
19
+ private logger: PowerSyncLogger,
20
20
  private tableName: string = 'attachments',
21
21
  archivedCacheLimit: number = 100
22
22
  ) {
23
+ this.mutex = db.createMutex();
23
24
  this.context = new AttachmentContext(db, tableName, logger, archivedCacheLimit);
24
25
  }
25
26
 
@@ -28,7 +29,7 @@ export class AttachmentService {
28
29
  * @returns Watch query that emits changes for queued uploads, downloads, and deletes
29
30
  */
30
31
  watchActiveAttachments({ throttleMs }: { throttleMs?: number } = {}): DifferentialWatchedQuery<AttachmentRecord> {
31
- this.logger.info('Watching active attachments...');
32
+ this.logger.log({ level: LogLevels.info, message: 'Watching active attachments...' });
32
33
  const watch = this.db
33
34
  .query<AttachmentRecord>({
34
35
  sql: /* sql */ `
@@ -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,6 +1,6 @@
1
1
  import { column } from '../db/schema/Column.js';
2
2
  import { RowType, Table } from '../db/schema/Table.js';
3
- import { TableV2Options } from '../db/schema/Table.js';
3
+ import { TableOptions } from '../db/schema/Table.js';
4
4
 
5
5
  /**
6
6
  * The default name of the local table storing attachment data.
@@ -64,7 +64,7 @@ export enum AttachmentState {
64
64
  /**
65
65
  * @alpha
66
66
  */
67
- export interface AttachmentTableOptions extends Omit<TableV2Options, 'name' | 'columns'> {}
67
+ export interface AttachmentTableOptions extends Omit<TableOptions, 'name' | 'columns'> {}
68
68
 
69
69
  /**
70
70
  * @alpha
@@ -1,7 +1,7 @@
1
- import { ILogger } from '../utils/Logger.js';
1
+ import { LogLevels, PowerSyncLogger } from '../utils/Logger.js';
2
2
  import { AttachmentService } from './AttachmentService.js';
3
+ import { AttachmentTransportAdapter } from './AttachmentTransportAdapter.js';
3
4
  import { LocalStorageAdapter } from './LocalStorageAdapter.js';
4
- import { RemoteStorageAdapter } from './RemoteStorageAdapter.js';
5
5
  import { AttachmentRecord, AttachmentState } from './Schema.js';
6
6
  import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
7
7
  import { AttachmentContext } from './AttachmentContext.js';
@@ -10,25 +10,28 @@ import { AttachmentContext } from './AttachmentContext.js';
10
10
  * Orchestrates attachment synchronization between local and remote storage.
11
11
  * Handles uploads, downloads, deletions, and state transitions.
12
12
  *
13
+ * Remote operations (upload/download/delete) go through the {@link AttachmentTransportAdapter};
14
+ * local file operations use the {@link LocalStorageAdapter}.
15
+ *
13
16
  * @internal
14
17
  */
15
18
  export class SyncingService {
16
19
  private attachmentService: AttachmentService;
17
20
  private localStorage: LocalStorageAdapter;
18
- private remoteStorage: RemoteStorageAdapter;
19
- private logger: ILogger;
21
+ private transport: AttachmentTransportAdapter;
22
+ private logger: PowerSyncLogger;
20
23
  private errorHandler?: AttachmentErrorHandler;
21
24
 
22
25
  constructor(
23
26
  attachmentService: AttachmentService,
24
27
  localStorage: LocalStorageAdapter,
25
- remoteStorage: RemoteStorageAdapter,
26
- logger: ILogger,
28
+ transport: AttachmentTransportAdapter,
29
+ logger: PowerSyncLogger,
27
30
  errorHandler?: AttachmentErrorHandler
28
31
  ) {
29
32
  this.attachmentService = attachmentService;
30
33
  this.localStorage = localStorage;
31
- this.remoteStorage = remoteStorage;
34
+ this.transport = transport;
32
35
  this.logger = logger;
33
36
  this.errorHandler = errorHandler;
34
37
  }
@@ -56,11 +59,17 @@ export class SyncingService {
56
59
  }
57
60
  ): Promise<void> {
58
61
  const signal = options?.signal;
59
- this.logger.info(`Starting processAttachments with ${attachments.length} attachments`);
62
+ this.logger.log({
63
+ level: LogLevels.info,
64
+ message: `Starting processAttachments with ${attachments.length} attachments`
65
+ });
60
66
 
61
67
  for (const attachment of attachments) {
62
68
  if (signal?.aborted) {
63
- this.logger.info('Sync cancelled; stopping iteration early');
69
+ this.logger.log({
70
+ level: LogLevels.info,
71
+ message: 'Sync cancelled; stopping iteration early'
72
+ });
64
73
  return;
65
74
  }
66
75
 
@@ -84,7 +93,11 @@ export class SyncingService {
84
93
 
85
94
  await this.attachmentService.withContext((ctx) => ctx.saveAttachments([updated]));
86
95
  } catch (error) {
87
- this.logger.warn(`Error during sync for ${attachment.id}`, error);
96
+ this.logger.log({
97
+ level: LogLevels.warn,
98
+ message: `Error during sync for ${attachment.id}`,
99
+ error
100
+ });
88
101
  }
89
102
  }
90
103
  }
@@ -98,14 +111,13 @@ export class SyncingService {
98
111
  * @throws Error if the attachment has no localUri
99
112
  */
100
113
  async uploadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
101
- this.logger.info(`Uploading attachment ${attachment.filename}`);
114
+ this.logger.log({ level: LogLevels.info, message: `Uploading attachment ${attachment.filename}` });
102
115
  try {
103
116
  if (attachment.localUri == null) {
104
117
  throw new Error(`No localUri for attachment ${attachment.id}`);
105
118
  }
106
119
 
107
- const fileBlob = await this.localStorage.readFile(attachment.localUri);
108
- await this.remoteStorage.uploadFile(fileBlob, attachment);
120
+ await this.transport.upload({ ...attachment, localUri: attachment.localUri });
109
121
 
110
122
  return {
111
123
  ...attachment,
@@ -127,19 +139,17 @@ export class SyncingService {
127
139
 
128
140
  /**
129
141
  * Downloads an attachment from remote storage to local storage.
130
- * Retrieves the file, converts to base64, and saves locally.
142
+ * The destination `localUri` is assigned here and the transport writes the file to it.
131
143
  * On success, marks as SYNCED. On failure, defers to error handler or archives.
132
144
  *
133
145
  * @param attachment - The attachment record to download
134
146
  * @returns Updated attachment record with local URI and new state
135
147
  */
136
148
  async downloadAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
137
- this.logger.info(`Downloading attachment ${attachment.filename}`);
149
+ this.logger.log({ level: LogLevels.info, message: `Downloading attachment ${attachment.filename}` });
138
150
  try {
139
- const fileData = await this.remoteStorage.downloadFile(attachment);
140
-
141
151
  const localUri = this.localStorage.getLocalUri(attachment.filename);
142
- await this.localStorage.saveFile(localUri, fileData);
152
+ await this.transport.download({ ...attachment, localUri });
143
153
 
144
154
  return {
145
155
  ...attachment,
@@ -171,7 +181,7 @@ export class SyncingService {
171
181
  */
172
182
  async deleteAttachment(attachment: AttachmentRecord, context: AttachmentContext): Promise<AttachmentRecord> {
173
183
  try {
174
- await this.remoteStorage.deleteFile(attachment);
184
+ await this.transport.delete(attachment);
175
185
  if (attachment.localUri) {
176
186
  await this.localStorage.deleteFile(attachment.localUri);
177
187
  }
@@ -206,7 +216,11 @@ export class SyncingService {
206
216
  try {
207
217
  await this.localStorage.deleteFile(attachment.localUri);
208
218
  } catch (error) {
209
- this.logger.error('Error deleting local file for archived attachment', error);
219
+ this.logger.log({
220
+ level: LogLevels.error,
221
+ message: 'Error deleting local file for archived attachment',
222
+ error
223
+ });
210
224
  }
211
225
  }
212
226
  }