@powersync/common 1.41.0 → 1.42.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 (183) hide show
  1. package/dist/bundle.cjs +10820 -22
  2. package/dist/bundle.cjs.map +1 -0
  3. package/dist/bundle.mjs +10741 -22
  4. package/dist/bundle.mjs.map +1 -0
  5. package/dist/bundle.node.cjs +10820 -0
  6. package/dist/bundle.node.cjs.map +1 -0
  7. package/dist/bundle.node.mjs +10741 -0
  8. package/dist/bundle.node.mjs.map +1 -0
  9. package/dist/index.d.cts +77 -13
  10. package/lib/client/AbstractPowerSyncDatabase.js +1 -0
  11. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -0
  12. package/lib/client/AbstractPowerSyncOpenFactory.js +1 -0
  13. package/lib/client/AbstractPowerSyncOpenFactory.js.map +1 -0
  14. package/lib/client/ConnectionManager.js +1 -0
  15. package/lib/client/ConnectionManager.js.map +1 -0
  16. package/lib/client/CustomQuery.js +1 -0
  17. package/lib/client/CustomQuery.js.map +1 -0
  18. package/lib/client/Query.js +1 -0
  19. package/lib/client/Query.js.map +1 -0
  20. package/lib/client/SQLOpenFactory.js +1 -0
  21. package/lib/client/SQLOpenFactory.js.map +1 -0
  22. package/lib/client/compilableQueryWatch.js +1 -0
  23. package/lib/client/compilableQueryWatch.js.map +1 -0
  24. package/lib/client/connection/PowerSyncBackendConnector.js +1 -0
  25. package/lib/client/connection/PowerSyncBackendConnector.js.map +1 -0
  26. package/lib/client/connection/PowerSyncCredentials.js +1 -0
  27. package/lib/client/connection/PowerSyncCredentials.js.map +1 -0
  28. package/lib/client/constants.js +1 -0
  29. package/lib/client/constants.js.map +1 -0
  30. package/lib/client/runOnSchemaChange.js +1 -0
  31. package/lib/client/runOnSchemaChange.js.map +1 -0
  32. package/lib/client/sync/bucket/BucketStorageAdapter.js +1 -0
  33. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -0
  34. package/lib/client/sync/bucket/CrudBatch.js +1 -0
  35. package/lib/client/sync/bucket/CrudBatch.js.map +1 -0
  36. package/lib/client/sync/bucket/CrudEntry.js +1 -0
  37. package/lib/client/sync/bucket/CrudEntry.js.map +1 -0
  38. package/lib/client/sync/bucket/CrudTransaction.js +1 -0
  39. package/lib/client/sync/bucket/CrudTransaction.js.map +1 -0
  40. package/lib/client/sync/bucket/OpType.js +1 -0
  41. package/lib/client/sync/bucket/OpType.js.map +1 -0
  42. package/lib/client/sync/bucket/OplogEntry.js +1 -0
  43. package/lib/client/sync/bucket/OplogEntry.js.map +1 -0
  44. package/lib/client/sync/bucket/SqliteBucketStorage.js +1 -0
  45. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -0
  46. package/lib/client/sync/bucket/SyncDataBatch.js +1 -0
  47. package/lib/client/sync/bucket/SyncDataBatch.js.map +1 -0
  48. package/lib/client/sync/bucket/SyncDataBucket.js +1 -0
  49. package/lib/client/sync/bucket/SyncDataBucket.js.map +1 -0
  50. package/lib/client/sync/stream/AbstractRemote.d.ts +5 -0
  51. package/lib/client/sync/stream/AbstractRemote.js +19 -6
  52. package/lib/client/sync/stream/AbstractRemote.js.map +1 -0
  53. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +1 -0
  54. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -0
  55. package/lib/client/sync/stream/WebsocketClientTransport.js +1 -0
  56. package/lib/client/sync/stream/WebsocketClientTransport.js.map +1 -0
  57. package/lib/client/sync/stream/core-instruction.js +1 -0
  58. package/lib/client/sync/stream/core-instruction.js.map +1 -0
  59. package/lib/client/sync/stream/streaming-sync-types.js +1 -0
  60. package/lib/client/sync/stream/streaming-sync-types.js.map +1 -0
  61. package/lib/client/sync/sync-streams.js +1 -0
  62. package/lib/client/sync/sync-streams.js.map +1 -0
  63. package/lib/client/triggers/TriggerManager.d.ts +71 -12
  64. package/lib/client/triggers/TriggerManager.js +1 -0
  65. package/lib/client/triggers/TriggerManager.js.map +1 -0
  66. package/lib/client/triggers/TriggerManagerImpl.js +11 -5
  67. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -0
  68. package/lib/client/triggers/sanitizeSQL.js +1 -0
  69. package/lib/client/triggers/sanitizeSQL.js.map +1 -0
  70. package/lib/client/watched/GetAllQuery.js +1 -0
  71. package/lib/client/watched/GetAllQuery.js.map +1 -0
  72. package/lib/client/watched/WatchedQuery.js +1 -0
  73. package/lib/client/watched/WatchedQuery.js.map +1 -0
  74. package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -0
  75. package/lib/client/watched/processors/AbstractQueryProcessor.js.map +1 -0
  76. package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -0
  77. package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -0
  78. package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -0
  79. package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -0
  80. package/lib/client/watched/processors/comparators.js +1 -0
  81. package/lib/client/watched/processors/comparators.js.map +1 -0
  82. package/lib/db/DBAdapter.js +1 -0
  83. package/lib/db/DBAdapter.js.map +1 -0
  84. package/lib/db/crud/SyncProgress.js +1 -0
  85. package/lib/db/crud/SyncProgress.js.map +1 -0
  86. package/lib/db/crud/SyncStatus.js +1 -0
  87. package/lib/db/crud/SyncStatus.js.map +1 -0
  88. package/lib/db/crud/UploadQueueStatus.js +1 -0
  89. package/lib/db/crud/UploadQueueStatus.js.map +1 -0
  90. package/lib/db/schema/Column.js +1 -0
  91. package/lib/db/schema/Column.js.map +1 -0
  92. package/lib/db/schema/Index.js +1 -0
  93. package/lib/db/schema/Index.js.map +1 -0
  94. package/lib/db/schema/IndexedColumn.js +1 -0
  95. package/lib/db/schema/IndexedColumn.js.map +1 -0
  96. package/lib/db/schema/RawTable.js +1 -0
  97. package/lib/db/schema/RawTable.js.map +1 -0
  98. package/lib/db/schema/Schema.js +1 -0
  99. package/lib/db/schema/Schema.js.map +1 -0
  100. package/lib/db/schema/Table.js +1 -0
  101. package/lib/db/schema/Table.js.map +1 -0
  102. package/lib/db/schema/TableV2.js +1 -0
  103. package/lib/db/schema/TableV2.js.map +1 -0
  104. package/lib/index.js +1 -0
  105. package/lib/index.js.map +1 -0
  106. package/lib/types/types.js +1 -0
  107. package/lib/types/types.js.map +1 -0
  108. package/lib/utils/AbortOperation.js +1 -0
  109. package/lib/utils/AbortOperation.js.map +1 -0
  110. package/lib/utils/BaseObserver.js +1 -0
  111. package/lib/utils/BaseObserver.js.map +1 -0
  112. package/lib/utils/ControlledExecutor.js +1 -0
  113. package/lib/utils/ControlledExecutor.js.map +1 -0
  114. package/lib/utils/DataStream.js +1 -0
  115. package/lib/utils/DataStream.js.map +1 -0
  116. package/lib/utils/Logger.js +1 -0
  117. package/lib/utils/Logger.js.map +1 -0
  118. package/lib/utils/MetaBaseObserver.js +1 -0
  119. package/lib/utils/MetaBaseObserver.js.map +1 -0
  120. package/lib/utils/async.js +1 -0
  121. package/lib/utils/async.js.map +1 -0
  122. package/lib/utils/mutex.js +1 -0
  123. package/lib/utils/mutex.js.map +1 -0
  124. package/lib/utils/parseQuery.js +1 -0
  125. package/lib/utils/parseQuery.js.map +1 -0
  126. package/package.json +23 -15
  127. package/src/client/AbstractPowerSyncDatabase.ts +1343 -0
  128. package/src/client/AbstractPowerSyncOpenFactory.ts +39 -0
  129. package/src/client/ConnectionManager.ts +402 -0
  130. package/src/client/CustomQuery.ts +56 -0
  131. package/src/client/Query.ts +106 -0
  132. package/src/client/SQLOpenFactory.ts +55 -0
  133. package/src/client/compilableQueryWatch.ts +55 -0
  134. package/src/client/connection/PowerSyncBackendConnector.ts +25 -0
  135. package/src/client/connection/PowerSyncCredentials.ts +5 -0
  136. package/src/client/constants.ts +1 -0
  137. package/src/client/runOnSchemaChange.ts +31 -0
  138. package/src/client/sync/bucket/BucketStorageAdapter.ts +118 -0
  139. package/src/client/sync/bucket/CrudBatch.ts +21 -0
  140. package/src/client/sync/bucket/CrudEntry.ts +172 -0
  141. package/src/client/sync/bucket/CrudTransaction.ts +21 -0
  142. package/src/client/sync/bucket/OpType.ts +23 -0
  143. package/src/client/sync/bucket/OplogEntry.ts +50 -0
  144. package/src/client/sync/bucket/SqliteBucketStorage.ts +395 -0
  145. package/src/client/sync/bucket/SyncDataBatch.ts +11 -0
  146. package/src/client/sync/bucket/SyncDataBucket.ts +49 -0
  147. package/src/client/sync/stream/AbstractRemote.ts +636 -0
  148. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +1258 -0
  149. package/src/client/sync/stream/WebsocketClientTransport.ts +80 -0
  150. package/src/client/sync/stream/core-instruction.ts +99 -0
  151. package/src/client/sync/stream/streaming-sync-types.ts +205 -0
  152. package/src/client/sync/sync-streams.ts +107 -0
  153. package/src/client/triggers/TriggerManager.ts +451 -0
  154. package/src/client/triggers/TriggerManagerImpl.ts +320 -0
  155. package/src/client/triggers/sanitizeSQL.ts +66 -0
  156. package/src/client/watched/GetAllQuery.ts +46 -0
  157. package/src/client/watched/WatchedQuery.ts +121 -0
  158. package/src/client/watched/processors/AbstractQueryProcessor.ts +226 -0
  159. package/src/client/watched/processors/DifferentialQueryProcessor.ts +305 -0
  160. package/src/client/watched/processors/OnChangeQueryProcessor.ts +122 -0
  161. package/src/client/watched/processors/comparators.ts +57 -0
  162. package/src/db/DBAdapter.ts +134 -0
  163. package/src/db/crud/SyncProgress.ts +100 -0
  164. package/src/db/crud/SyncStatus.ts +308 -0
  165. package/src/db/crud/UploadQueueStatus.ts +20 -0
  166. package/src/db/schema/Column.ts +60 -0
  167. package/src/db/schema/Index.ts +39 -0
  168. package/src/db/schema/IndexedColumn.ts +42 -0
  169. package/src/db/schema/RawTable.ts +67 -0
  170. package/src/db/schema/Schema.ts +76 -0
  171. package/src/db/schema/Table.ts +359 -0
  172. package/src/db/schema/TableV2.ts +9 -0
  173. package/src/index.ts +52 -0
  174. package/src/types/types.ts +9 -0
  175. package/src/utils/AbortOperation.ts +17 -0
  176. package/src/utils/BaseObserver.ts +41 -0
  177. package/src/utils/ControlledExecutor.ts +72 -0
  178. package/src/utils/DataStream.ts +211 -0
  179. package/src/utils/Logger.ts +47 -0
  180. package/src/utils/MetaBaseObserver.ts +81 -0
  181. package/src/utils/async.ts +61 -0
  182. package/src/utils/mutex.ts +34 -0
  183. package/src/utils/parseQuery.ts +25 -0
@@ -0,0 +1,395 @@
1
+ import Logger, { ILogger } from 'js-logger';
2
+ import { DBAdapter, extractTableUpdates, Transaction } from '../../../db/DBAdapter.js';
3
+ import { BaseObserver } from '../../../utils/BaseObserver.js';
4
+ import { MAX_OP_ID } from '../../constants.js';
5
+ import {
6
+ BucketChecksum,
7
+ BucketOperationProgress,
8
+ BucketState,
9
+ BucketStorageAdapter,
10
+ BucketStorageListener,
11
+ Checkpoint,
12
+ PowerSyncControlCommand,
13
+ PSInternalTable,
14
+ SyncLocalDatabaseResult
15
+ } from './BucketStorageAdapter.js';
16
+ import { CrudBatch } from './CrudBatch.js';
17
+ import { CrudEntry, CrudEntryJSON } from './CrudEntry.js';
18
+ import { SyncDataBatch } from './SyncDataBatch.js';
19
+
20
+ export class SqliteBucketStorage extends BaseObserver<BucketStorageListener> implements BucketStorageAdapter {
21
+ public tableNames: Set<string>;
22
+ private _hasCompletedSync: boolean;
23
+ private updateListener: () => void;
24
+ private _clientId?: Promise<string>;
25
+
26
+ constructor(
27
+ private db: DBAdapter,
28
+ private logger: ILogger = Logger.get('SqliteBucketStorage')
29
+ ) {
30
+ super();
31
+ this._hasCompletedSync = false;
32
+ this.tableNames = new Set();
33
+ this.updateListener = db.registerListener({
34
+ tablesUpdated: (update) => {
35
+ const tables = extractTableUpdates(update);
36
+ if (tables.includes(PSInternalTable.CRUD)) {
37
+ this.iterateListeners((l) => l.crudUpdate?.());
38
+ }
39
+ }
40
+ });
41
+ }
42
+
43
+ async init() {
44
+ this._hasCompletedSync = false;
45
+ const existingTableRows = await this.db.getAll<{ name: string }>(
46
+ `SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data_*'`
47
+ );
48
+ for (const row of existingTableRows ?? []) {
49
+ this.tableNames.add(row.name);
50
+ }
51
+ }
52
+
53
+ async dispose() {
54
+ this.updateListener?.();
55
+ }
56
+
57
+ async _getClientId() {
58
+ const row = await this.db.get<{ client_id: string }>('SELECT powersync_client_id() as client_id');
59
+ return row['client_id'];
60
+ }
61
+
62
+ getClientId() {
63
+ if (this._clientId == null) {
64
+ this._clientId = this._getClientId();
65
+ }
66
+ return this._clientId!;
67
+ }
68
+
69
+ getMaxOpId() {
70
+ return MAX_OP_ID;
71
+ }
72
+
73
+ /**
74
+ * Reset any caches.
75
+ */
76
+ startSession(): void {}
77
+
78
+ async getBucketStates(): Promise<BucketState[]> {
79
+ const result = await this.db.getAll<BucketState>(
80
+ "SELECT name as bucket, cast(last_op as TEXT) as op_id FROM ps_buckets WHERE pending_delete = 0 AND name != '$local'"
81
+ );
82
+ return result;
83
+ }
84
+
85
+ async getBucketOperationProgress(): Promise<BucketOperationProgress> {
86
+ const rows = await this.db.getAll<{ name: string; count_at_last: number; count_since_last: number }>(
87
+ 'SELECT name, count_at_last, count_since_last FROM ps_buckets'
88
+ );
89
+ return Object.fromEntries(rows.map((r) => [r.name, { atLast: r.count_at_last, sinceLast: r.count_since_last }]));
90
+ }
91
+
92
+ async saveSyncData(batch: SyncDataBatch, fixedKeyFormat: boolean = false) {
93
+ await this.writeTransaction(async (tx) => {
94
+ for (const b of batch.buckets) {
95
+ await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
96
+ 'save',
97
+ JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
98
+ ]);
99
+ this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
100
+ }
101
+ });
102
+ }
103
+
104
+ async removeBuckets(buckets: string[]): Promise<void> {
105
+ for (const bucket of buckets) {
106
+ await this.deleteBucket(bucket);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Mark a bucket for deletion.
112
+ */
113
+ private async deleteBucket(bucket: string) {
114
+ await this.writeTransaction(async (tx) => {
115
+ await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
116
+ });
117
+
118
+ this.logger.debug(`Done deleting bucket ${bucket}`);
119
+ }
120
+
121
+ async hasCompletedSync() {
122
+ if (this._hasCompletedSync) {
123
+ return true;
124
+ }
125
+ const r = await this.db.get<{ synced_at: string | null }>(`SELECT powersync_last_synced_at() as synced_at`);
126
+ const completed = r.synced_at != null;
127
+ if (completed) {
128
+ this._hasCompletedSync = true;
129
+ }
130
+ return completed;
131
+ }
132
+
133
+ async syncLocalDatabase(checkpoint: Checkpoint, priority?: number): Promise<SyncLocalDatabaseResult> {
134
+ const r = await this.validateChecksums(checkpoint, priority);
135
+ if (!r.checkpointValid) {
136
+ this.logger.error('Checksums failed for', r.checkpointFailures);
137
+ for (const b of r.checkpointFailures ?? []) {
138
+ await this.deleteBucket(b);
139
+ }
140
+ return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
141
+ }
142
+ if (priority == null) {
143
+ this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
144
+ } else {
145
+ this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
146
+ }
147
+
148
+ let buckets = checkpoint.buckets;
149
+ if (priority !== undefined) {
150
+ buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
151
+ }
152
+ const bucketNames = buckets.map((b) => b.bucket);
153
+ await this.writeTransaction(async (tx) => {
154
+ await tx.execute(`UPDATE ps_buckets SET last_op = ? WHERE name IN (SELECT json_each.value FROM json_each(?))`, [
155
+ checkpoint.last_op_id,
156
+ JSON.stringify(bucketNames)
157
+ ]);
158
+
159
+ if (priority == null && checkpoint.write_checkpoint) {
160
+ await tx.execute("UPDATE ps_buckets SET last_op = ? WHERE name = '$local'", [checkpoint.write_checkpoint]);
161
+ }
162
+ });
163
+
164
+ const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
165
+ if (!valid) {
166
+ return { ready: false, checkpointValid: true };
167
+ }
168
+
169
+ return {
170
+ ready: true,
171
+ checkpointValid: true
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Atomically update the local state to the current checkpoint.
177
+ *
178
+ * This includes creating new tables, dropping old tables, and copying data over from the oplog.
179
+ */
180
+ private async updateObjectsFromBuckets(checkpoint: Checkpoint, priority: number | undefined) {
181
+ let arg = '';
182
+ if (priority !== undefined) {
183
+ const affectedBuckets: string[] = [];
184
+ for (const desc of checkpoint.buckets) {
185
+ if (hasMatchingPriority(priority, desc)) {
186
+ affectedBuckets.push(desc.bucket);
187
+ }
188
+ }
189
+
190
+ arg = JSON.stringify({ priority, buckets: affectedBuckets });
191
+ }
192
+
193
+ return this.writeTransaction(async (tx) => {
194
+ const { insertId: result } = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
195
+ 'sync_local',
196
+ arg
197
+ ]);
198
+ if (result == 1) {
199
+ if (priority == null) {
200
+ const bucketToCount = Object.fromEntries(checkpoint.buckets.map((b) => [b.bucket, b.count]));
201
+ // The two parameters could be replaced with one, but: https://github.com/powersync-ja/better-sqlite3/pull/6
202
+ const jsonBucketCount = JSON.stringify(bucketToCount);
203
+ await tx.execute(
204
+ "UPDATE ps_buckets SET count_since_last = 0, count_at_last = ?->name WHERE name != '$local' AND ?->name IS NOT NULL",
205
+ [jsonBucketCount, jsonBucketCount]
206
+ );
207
+ }
208
+
209
+ return true;
210
+ } else {
211
+ return false;
212
+ }
213
+ });
214
+ }
215
+
216
+ async validateChecksums(checkpoint: Checkpoint, priority: number | undefined): Promise<SyncLocalDatabaseResult> {
217
+ if (priority !== undefined) {
218
+ // Only validate the buckets within the priority we care about
219
+ const newBuckets = checkpoint.buckets.filter((cs) => hasMatchingPriority(priority, cs));
220
+ checkpoint = { ...checkpoint, buckets: newBuckets };
221
+ }
222
+
223
+ const rs = await this.db.execute('SELECT powersync_validate_checkpoint(?) as result', [
224
+ JSON.stringify({ ...checkpoint })
225
+ ]);
226
+
227
+ const resultItem = rs.rows?.item(0);
228
+ if (!resultItem) {
229
+ return {
230
+ checkpointValid: false,
231
+ ready: false,
232
+ checkpointFailures: []
233
+ };
234
+ }
235
+
236
+ const result = JSON.parse(resultItem['result']);
237
+
238
+ if (result['valid']) {
239
+ return { ready: true, checkpointValid: true };
240
+ } else {
241
+ return {
242
+ checkpointValid: false,
243
+ ready: false,
244
+ checkpointFailures: result['failed_buckets']
245
+ };
246
+ }
247
+ }
248
+
249
+ async updateLocalTarget(cb: () => Promise<string>): Promise<boolean> {
250
+ const rs1 = await this.db.getAll(
251
+ "SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)",
252
+ [MAX_OP_ID]
253
+ );
254
+ if (!rs1.length) {
255
+ // Nothing to update
256
+ return false;
257
+ }
258
+ const rs = await this.db.getAll<{ seq: number }>("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
259
+ if (!rs.length) {
260
+ // Nothing to update
261
+ return false;
262
+ }
263
+
264
+ const seqBefore: number = rs[0]['seq'];
265
+
266
+ const opId = await cb();
267
+
268
+ return this.writeTransaction(async (tx) => {
269
+ const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
270
+ if (anyData.rows?.length) {
271
+ // if isNotEmpty
272
+ this.logger.debug(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`);
273
+ return false;
274
+ }
275
+
276
+ const rs = await tx.execute("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
277
+ if (!rs.rows?.length) {
278
+ // assert isNotEmpty
279
+ throw new Error('SQLite Sequence should not be empty');
280
+ }
281
+
282
+ const seqAfter: number = rs.rows?.item(0)['seq'];
283
+ if (seqAfter != seqBefore) {
284
+ this.logger.debug(
285
+ `New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)`
286
+ );
287
+
288
+ // New crud data may have been uploaded since we got the checkpoint. Abort.
289
+ return false;
290
+ }
291
+
292
+ this.logger.debug(`Updating target write checkpoint to ${opId}`);
293
+ await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]);
294
+ return true;
295
+ });
296
+ }
297
+
298
+ async nextCrudItem(): Promise<CrudEntry | undefined> {
299
+ const next = await this.db.getOptional<CrudEntryJSON>('SELECT * FROM ps_crud ORDER BY id ASC LIMIT 1');
300
+ if (!next) {
301
+ return;
302
+ }
303
+ return CrudEntry.fromRow(next);
304
+ }
305
+
306
+ async hasCrud(): Promise<boolean> {
307
+ const anyData = await this.db.getOptional('SELECT 1 FROM ps_crud LIMIT 1');
308
+ return !!anyData;
309
+ }
310
+
311
+ /**
312
+ * Get a batch of objects to send to the server.
313
+ * When the objects are successfully sent to the server, call .complete()
314
+ */
315
+ async getCrudBatch(limit: number = 100): Promise<CrudBatch | null> {
316
+ if (!(await this.hasCrud())) {
317
+ return null;
318
+ }
319
+
320
+ const crudResult = await this.db.getAll<CrudEntryJSON>('SELECT * FROM ps_crud ORDER BY id ASC LIMIT ?', [limit]);
321
+
322
+ const all: CrudEntry[] = [];
323
+ for (const row of crudResult) {
324
+ all.push(CrudEntry.fromRow(row));
325
+ }
326
+
327
+ if (all.length === 0) {
328
+ return null;
329
+ }
330
+
331
+ const last = all[all.length - 1];
332
+ return {
333
+ crud: all,
334
+ haveMore: true,
335
+ complete: async (writeCheckpoint?: string) => {
336
+ return this.writeTransaction(async (tx) => {
337
+ await tx.execute('DELETE FROM ps_crud WHERE id <= ?', [last.clientId]);
338
+ if (writeCheckpoint) {
339
+ const crudResult = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
340
+ if (crudResult.rows?.length) {
341
+ await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [
342
+ writeCheckpoint
343
+ ]);
344
+ }
345
+ } else {
346
+ await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [
347
+ this.getMaxOpId()
348
+ ]);
349
+ }
350
+ });
351
+ }
352
+ };
353
+ }
354
+
355
+ async writeTransaction<T>(callback: (tx: Transaction) => Promise<T>, options?: { timeoutMs: number }): Promise<T> {
356
+ return this.db.writeTransaction(callback, options);
357
+ }
358
+
359
+ /**
360
+ * Set a target checkpoint.
361
+ */
362
+ async setTargetCheckpoint(checkpoint: Checkpoint) {
363
+ // No-op for now
364
+ }
365
+
366
+ async control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string> {
367
+ return await this.writeTransaction(async (tx) => {
368
+ const [[raw]] = await tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload]);
369
+ return raw;
370
+ });
371
+ }
372
+
373
+ async hasMigratedSubkeys(): Promise<boolean> {
374
+ const { r } = await this.db.get<{ r: number }>('SELECT EXISTS(SELECT * FROM ps_kv WHERE key = ?) as r', [
375
+ SqliteBucketStorage._subkeyMigrationKey
376
+ ]);
377
+ return r != 0;
378
+ }
379
+
380
+ async migrateToFixedSubkeys(): Promise<void> {
381
+ await this.writeTransaction(async (tx) => {
382
+ await tx.execute('UPDATE ps_oplog SET key = powersync_remove_duplicate_key_encoding(key);');
383
+ await tx.execute('INSERT OR REPLACE INTO ps_kv (key, value) VALUES (?, ?);', [
384
+ SqliteBucketStorage._subkeyMigrationKey,
385
+ '1'
386
+ ]);
387
+ });
388
+ }
389
+
390
+ static _subkeyMigrationKey = 'powersync_js_migrated_subkeys';
391
+ }
392
+
393
+ function hasMatchingPriority(priority: number, bucket: BucketChecksum) {
394
+ return bucket.priority != null && bucket.priority <= priority;
395
+ }
@@ -0,0 +1,11 @@
1
+ import { SyncDataBucket } from './SyncDataBucket.js';
2
+
3
+ // TODO JSON
4
+
5
+ export class SyncDataBatch {
6
+ static fromJSON(json: any) {
7
+ return new SyncDataBatch(json.buckets.map((bucket: any) => SyncDataBucket.fromRow(bucket)));
8
+ }
9
+
10
+ constructor(public buckets: SyncDataBucket[]) {}
11
+ }
@@ -0,0 +1,49 @@
1
+ import { OpId } from './CrudEntry.js';
2
+ import { OplogEntry, OplogEntryJSON } from './OplogEntry.js';
3
+
4
+ export type SyncDataBucketJSON = {
5
+ bucket: string;
6
+ has_more?: boolean;
7
+ after?: string;
8
+ next_after?: string;
9
+ data: OplogEntryJSON[];
10
+ };
11
+
12
+ export class SyncDataBucket {
13
+ static fromRow(row: SyncDataBucketJSON) {
14
+ return new SyncDataBucket(
15
+ row.bucket,
16
+ row.data.map((entry) => OplogEntry.fromRow(entry)),
17
+ row.has_more ?? false,
18
+ row.after,
19
+ row.next_after
20
+ );
21
+ }
22
+
23
+ constructor(
24
+ public bucket: string,
25
+ public data: OplogEntry[],
26
+ /**
27
+ * True if the response does not contain all the data for this bucket, and another request must be made.
28
+ */
29
+ public has_more: boolean,
30
+ /**
31
+ * The `after` specified in the request.
32
+ */
33
+ public after?: OpId,
34
+ /**
35
+ * Use this for the next request.
36
+ */
37
+ public next_after?: OpId
38
+ ) {}
39
+
40
+ toJSON(fixedKeyEncoding = false): SyncDataBucketJSON {
41
+ return {
42
+ bucket: this.bucket,
43
+ has_more: this.has_more,
44
+ after: this.after,
45
+ next_after: this.next_after,
46
+ data: this.data.map((entry) => entry.toJSON(fixedKeyEncoding))
47
+ };
48
+ }
49
+ }