@powersync/service-module-mongodb 0.15.4 → 0.17.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 (64) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/api/MongoRouteAPIAdapter.js +12 -21
  3. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  4. package/dist/replication/ChangeStream.d.ts +23 -42
  5. package/dist/replication/ChangeStream.js +363 -600
  6. package/dist/replication/ChangeStream.js.map +1 -1
  7. package/dist/replication/ChangeStreamReplicationJob.js +2 -2
  8. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  9. package/dist/replication/JsonBufferWriter.d.ts +80 -0
  10. package/dist/replication/JsonBufferWriter.js +342 -0
  11. package/dist/replication/JsonBufferWriter.js.map +1 -0
  12. package/dist/replication/MongoRelation.d.ts +1 -1
  13. package/dist/replication/MongoRelation.js +45 -21
  14. package/dist/replication/MongoRelation.js.map +1 -1
  15. package/dist/replication/MongoSnapshotQuery.d.ts +1 -1
  16. package/dist/replication/MongoSnapshotQuery.js +6 -3
  17. package/dist/replication/MongoSnapshotQuery.js.map +1 -1
  18. package/dist/replication/MongoSnapshotter.d.ts +81 -0
  19. package/dist/replication/MongoSnapshotter.js +594 -0
  20. package/dist/replication/MongoSnapshotter.js.map +1 -0
  21. package/dist/replication/RawChangeStream.d.ts +55 -0
  22. package/dist/replication/RawChangeStream.js +322 -0
  23. package/dist/replication/RawChangeStream.js.map +1 -0
  24. package/dist/replication/SourceRowConverter.d.ts +46 -0
  25. package/dist/replication/SourceRowConverter.js +42 -0
  26. package/dist/replication/SourceRowConverter.js.map +1 -0
  27. package/dist/replication/bufferToSqlite.d.ts +43 -0
  28. package/dist/replication/bufferToSqlite.js +740 -0
  29. package/dist/replication/bufferToSqlite.js.map +1 -0
  30. package/dist/replication/internal-mongodb-utils.d.ts +0 -12
  31. package/dist/replication/internal-mongodb-utils.js +0 -54
  32. package/dist/replication/internal-mongodb-utils.js.map +1 -1
  33. package/dist/replication/replication-index.d.ts +2 -0
  34. package/dist/replication/replication-index.js +2 -0
  35. package/dist/replication/replication-index.js.map +1 -1
  36. package/package.json +11 -11
  37. package/scripts/benchmark-change-document-json.mts +358 -0
  38. package/scripts/benchmark-change-document.mts +370 -0
  39. package/src/api/MongoRouteAPIAdapter.ts +13 -21
  40. package/src/replication/ChangeStream.ts +421 -720
  41. package/src/replication/ChangeStreamReplicationJob.ts +2 -2
  42. package/src/replication/JsonBufferWriter.ts +390 -0
  43. package/src/replication/MongoRelation.ts +54 -25
  44. package/src/replication/MongoSnapshotQuery.ts +8 -5
  45. package/src/replication/MongoSnapshotter.ts +729 -0
  46. package/src/replication/RawChangeStream.ts +460 -0
  47. package/src/replication/SourceRowConverter.ts +65 -0
  48. package/src/replication/bufferToSqlite.ts +944 -0
  49. package/src/replication/internal-mongodb-utils.ts +0 -65
  50. package/src/replication/replication-index.ts +2 -0
  51. package/test/src/buffer_to_sqlite.test.ts +1146 -0
  52. package/test/src/change_stream.test.ts +259 -19
  53. package/test/src/change_stream_utils.ts +28 -27
  54. package/test/src/checkpoint_retry.test.ts +131 -0
  55. package/test/src/mongo_test.test.ts +66 -64
  56. package/test/src/parse_document_id.test.ts +54 -0
  57. package/test/src/raw_change_stream.test.ts +547 -0
  58. package/test/src/resume.test.ts +12 -2
  59. package/test/src/resuming_snapshots.test.ts +10 -6
  60. package/test/src/util.ts +56 -3
  61. package/test/tsconfig.json +0 -1
  62. package/tsconfig.scripts.json +13 -0
  63. package/tsconfig.tsbuildinfo +1 -1
  64. package/test/src/internal_mongodb_utils.test.ts +0 -103
@@ -1,4 +1,4 @@
1
- import { container, logger as defaultLogger } from '@powersync/lib-services-framework';
1
+ import { container } from '@powersync/lib-services-framework';
2
2
  import { replication } from '@powersync/service-core';
3
3
 
4
4
  import { ChangeStream, ChangeStreamInvalidatedError } from './ChangeStream.js';
@@ -16,7 +16,7 @@ export class ChangeStreamReplicationJob extends replication.AbstractReplicationJ
16
16
  super(options);
17
17
  this.connectionFactory = options.connectionFactory;
18
18
  // We use a custom formatter to process the prefix
19
- this.logger = defaultLogger.child({ prefix: `[powersync_${this.storage.group_id}] ` });
19
+ this.logger = options.storage.logger;
20
20
  }
21
21
 
22
22
  async cleanUp(): Promise<void> {
@@ -0,0 +1,390 @@
1
+ const JSON_BUFFER_INITIAL_CAPACITY = 1024 * 1024;
2
+ const HEX_LOWER_BYTES = Buffer.from('0123456789abcdef', 'ascii');
3
+ export const BYTE_DQUOTE = 0x22; // "
4
+ export const BYTE_BACKSLASH = 0x5c; // \
5
+ export const BYTE_BACKSPACE = 0x08; // \b
6
+ export const BYTE_TAB = 0x09; // \t
7
+ export const BYTE_NEWLINE = 0x0a; // \n
8
+ export const BYTE_FORM_FEED = 0x0c; // \f
9
+ export const BYTE_CARRIAGE_RETURN = 0x0d; // \r
10
+ export const BYTE_LBRACE = 0x7b; // {
11
+ export const BYTE_RBRACE = 0x7d; // }
12
+ export const BYTE_LBRACKET = 0x5b; // [
13
+ export const BYTE_RBRACKET = 0x5d; // ]
14
+ export const BYTE_COMMA = 0x2c; // ,
15
+ export const BYTE_COLON = 0x3a; // :
16
+ export const BYTE_ZERO = 0x30; // 0
17
+ export const BYTE_ONE = 0x31; // 1
18
+ export const BYTE_T = 0x54; // T
19
+ export const BYTE_SPACE = 0x20; // ' '
20
+ const BYTE_DASH = 0x2d; // -
21
+ const BYTE_DOT = 0x2e; // .
22
+ const BYTE_Z = 0x5a; // Z
23
+ const BYTE_B = 0x62; // b
24
+ const BYTE_T_LOWER = 0x74; // t
25
+ const BYTE_N = 0x6e; // n
26
+ const BYTE_F = 0x66; // f
27
+ const BYTE_R = 0x72; // r
28
+ const BYTE_U = 0x75; // u
29
+
30
+ /**
31
+ * Low-level class to generate JSON.
32
+ *
33
+ * This writes to a Buffer, which can then be decoded as a string.
34
+ */
35
+ export class JsonBufferWriter {
36
+ /**
37
+ * The raw buffer. Capacity can increase, but is never decreased.
38
+ */
39
+ private buffer: Buffer;
40
+
41
+ /**
42
+ * length of data written to the buffer.
43
+ */
44
+ private length = 0;
45
+
46
+ constructor(capacity = JSON_BUFFER_INITIAL_CAPACITY) {
47
+ // In theory allocUnsafe could be fine. But in case there are any bugs where this.length
48
+ // is not updated correctly, that could lead to leaking data from memory.
49
+ // Buffer.alloc makes sure no data is leaked if a bug like that is hit.
50
+ // The same applies to ensureCapacity, and for the same reason we zero out changes in truncate().
51
+ this.buffer = Buffer.alloc(capacity);
52
+ }
53
+
54
+ /**
55
+ * Resets the length, equivalent to truncate(0).
56
+ */
57
+ reset() {
58
+ this.truncate(0);
59
+ }
60
+
61
+ toString() {
62
+ return this.buffer.toString('utf8', 0, this.length);
63
+ }
64
+
65
+ getLength() {
66
+ return this.length;
67
+ }
68
+
69
+ truncate(length: number) {
70
+ // Safely reset data
71
+ this.buffer.fill(0, length, this.length);
72
+ this.length = length;
73
+ }
74
+
75
+ /**
76
+ * Write a single raw byte.
77
+ *
78
+ * Caller is responsible for ensuring this produces valid JSON.
79
+ */
80
+ writeByte(value: number) {
81
+ this.ensureCapacity(1);
82
+ this.buffer[this.length++] = value;
83
+ }
84
+
85
+ /**
86
+ * Write raw ascii string - one byte per character. No quoting or escaping is performed.
87
+ *
88
+ * Caller is responsible for ensuring this produces valid JSON.
89
+ */
90
+ writeAscii(text: string) {
91
+ const length = text.length;
92
+ this.ensureCapacity(length);
93
+ this.buffer.write(text, this.length, length, 'ascii');
94
+ this.length += length;
95
+ }
96
+
97
+ /**
98
+ * Write UTF-8 string. No quoting or escaping is performed.
99
+ *
100
+ * Caller is responsible for ensuring this produces valid JSON.
101
+ */
102
+ writeUtf8(text: string) {
103
+ const length = Buffer.byteLength(text);
104
+ this.ensureCapacity(length);
105
+ this.buffer.write(text, this.length, length, 'utf8');
106
+ this.length += length;
107
+ }
108
+
109
+ /**
110
+ * Quote and write a string, escaping characters as needed.
111
+ */
112
+ writeQuotedJsonString(text: string) {
113
+ this.writeByte(BYTE_DQUOTE);
114
+ let start = 0;
115
+
116
+ for (let i = 0; i < text.length; i++) {
117
+ const ch = text.charCodeAt(i);
118
+ let escaped: string | undefined;
119
+
120
+ switch (ch) {
121
+ case BYTE_DQUOTE:
122
+ escaped = '\\"';
123
+ break;
124
+ case BYTE_BACKSLASH:
125
+ escaped = '\\\\';
126
+ break;
127
+ case BYTE_BACKSPACE:
128
+ escaped = '\\b';
129
+ break;
130
+ case BYTE_TAB:
131
+ escaped = '\\t';
132
+ break;
133
+ case BYTE_NEWLINE:
134
+ escaped = '\\n';
135
+ break;
136
+ case BYTE_FORM_FEED:
137
+ escaped = '\\f';
138
+ break;
139
+ case BYTE_CARRIAGE_RETURN:
140
+ escaped = '\\r';
141
+ break;
142
+ default:
143
+ if (ch < 0x20) {
144
+ escaped = `\\u${ch.toString(16).padStart(4, '0')}`;
145
+ }
146
+ }
147
+
148
+ if (escaped == null) {
149
+ continue;
150
+ }
151
+
152
+ if (start < i) {
153
+ this.writeUtf8(text.slice(start, i));
154
+ }
155
+ this.writeAscii(escaped);
156
+ start = i + 1;
157
+ }
158
+
159
+ if (start < text.length) {
160
+ this.writeUtf8(text.slice(start));
161
+ }
162
+
163
+ this.writeByte(BYTE_DQUOTE);
164
+ }
165
+
166
+ /**
167
+ * Quotes and write an UTF-8 string from a source buffer, escaping characters as needed.
168
+ *
169
+ * @param bytes source buffer
170
+ * @param start start offset, inclusive
171
+ * @param end end offset, exclusive
172
+ */
173
+ writeQuotedUtf8Slice(bytes: Buffer, start: number, end: number): void {
174
+ let firstEscape = -1;
175
+ for (let index = start; index < end; index++) {
176
+ const value = bytes[index];
177
+ if (value < 0x20 || value === BYTE_DQUOTE || value === BYTE_BACKSLASH) {
178
+ firstEscape = index;
179
+ break;
180
+ }
181
+ }
182
+
183
+ const rawLength = end - start;
184
+ let length = this.length;
185
+ this.ensureCapacity(rawLength + 2);
186
+ let buffer = this.buffer;
187
+
188
+ buffer[length++] = BYTE_DQUOTE;
189
+
190
+ if (firstEscape < 0) {
191
+ bytes.copy(buffer, length, start, end);
192
+ length += rawLength;
193
+ buffer[length++] = BYTE_DQUOTE;
194
+ this.length = length;
195
+ return;
196
+ }
197
+
198
+ if (firstEscape > start) {
199
+ bytes.copy(buffer, length, start, firstEscape);
200
+ length += firstEscape - start;
201
+ }
202
+
203
+ let chunkStart = firstEscape;
204
+ for (let index = firstEscape; index < end; index++) {
205
+ const value = bytes[index];
206
+ if (value >= 0x20 && value !== BYTE_DQUOTE && value !== BYTE_BACKSLASH) {
207
+ continue;
208
+ }
209
+
210
+ if (chunkStart < index) {
211
+ const chunkLength = index - chunkStart;
212
+ this.length = length;
213
+ this.ensureCapacity(chunkLength + 6);
214
+ buffer = this.buffer;
215
+ bytes.copy(buffer, length, chunkStart, index);
216
+ length += chunkLength;
217
+ } else {
218
+ this.length = length;
219
+ this.ensureCapacity(6);
220
+ buffer = this.buffer;
221
+ }
222
+
223
+ switch (value) {
224
+ case BYTE_DQUOTE:
225
+ buffer[length++] = BYTE_BACKSLASH;
226
+ buffer[length++] = BYTE_DQUOTE;
227
+ break;
228
+ case BYTE_BACKSLASH:
229
+ buffer[length++] = BYTE_BACKSLASH;
230
+ buffer[length++] = BYTE_BACKSLASH;
231
+ break;
232
+ case BYTE_BACKSPACE:
233
+ buffer[length++] = BYTE_BACKSLASH;
234
+ buffer[length++] = BYTE_B;
235
+ break;
236
+ case BYTE_TAB:
237
+ buffer[length++] = BYTE_BACKSLASH;
238
+ buffer[length++] = BYTE_T_LOWER;
239
+ break;
240
+ case BYTE_NEWLINE:
241
+ buffer[length++] = BYTE_BACKSLASH;
242
+ buffer[length++] = BYTE_N;
243
+ break;
244
+ case BYTE_FORM_FEED:
245
+ buffer[length++] = BYTE_BACKSLASH;
246
+ buffer[length++] = BYTE_F;
247
+ break;
248
+ case BYTE_CARRIAGE_RETURN:
249
+ buffer[length++] = BYTE_BACKSLASH;
250
+ buffer[length++] = BYTE_R;
251
+ break;
252
+ default:
253
+ buffer[length++] = BYTE_BACKSLASH;
254
+ buffer[length++] = BYTE_U;
255
+ buffer[length++] = BYTE_ZERO;
256
+ buffer[length++] = BYTE_ZERO;
257
+ buffer[length++] = HEX_LOWER_BYTES[value >> 4];
258
+ buffer[length++] = HEX_LOWER_BYTES[value & 0x0f];
259
+ break;
260
+ }
261
+
262
+ chunkStart = index + 1;
263
+ }
264
+
265
+ if (chunkStart < end) {
266
+ const chunkLength = end - chunkStart;
267
+ this.length = length;
268
+ this.ensureCapacity(chunkLength + 1);
269
+ buffer = this.buffer;
270
+ bytes.copy(buffer, length, chunkStart, end);
271
+ length += chunkLength;
272
+ } else {
273
+ this.length = length;
274
+ this.ensureCapacity(1);
275
+ buffer = this.buffer;
276
+ }
277
+
278
+ buffer[length++] = BYTE_DQUOTE;
279
+ this.length = length;
280
+ }
281
+
282
+ /**
283
+ * Quote and write bytes as hex.
284
+ */
285
+ writeQuotedHexLower(bytes: Buffer, start: number, length: number) {
286
+ this.ensureCapacity(length * 2 + 2);
287
+ this.buffer[this.length++] = BYTE_DQUOTE;
288
+ for (let index = start; index < start + length; index++) {
289
+ const value = bytes[index];
290
+ this.buffer[this.length++] = HEX_LOWER_BYTES[value >> 4];
291
+ this.buffer[this.length++] = HEX_LOWER_BYTES[value & 0x0f];
292
+ }
293
+ this.buffer[this.length++] = BYTE_DQUOTE;
294
+ }
295
+
296
+ /**
297
+ * Quote and write 16 UUID bytes in canonical lower-case form.
298
+ */
299
+ writeQuotedUuid(bytes: Buffer, start: number) {
300
+ this.ensureCapacity(38);
301
+ const buffer = this.buffer;
302
+ let length = this.length;
303
+ buffer[length++] = BYTE_DQUOTE;
304
+ for (let index = 0; index < 16; index++) {
305
+ if (index === 4 || index === 6 || index === 8 || index === 10) {
306
+ buffer[length++] = BYTE_DASH;
307
+ }
308
+ const value = bytes[start + index];
309
+ buffer[length++] = HEX_LOWER_BYTES[value >> 4];
310
+ buffer[length++] = HEX_LOWER_BYTES[value & 0x0f];
311
+ }
312
+ buffer[length++] = BYTE_DQUOTE;
313
+ this.length = length;
314
+ }
315
+
316
+ private ensureCapacity(extra: number) {
317
+ const required = this.length + extra;
318
+ if (required <= this.buffer.length) {
319
+ return;
320
+ }
321
+
322
+ let nextLength = this.buffer.length;
323
+ while (nextLength < required) {
324
+ nextLength *= 2;
325
+ }
326
+
327
+ const next = Buffer.alloc(nextLength);
328
+ this.buffer.copy(next, 0, 0, this.length);
329
+ this.buffer = next;
330
+ }
331
+
332
+ writeDateTime(
333
+ year: number,
334
+ month: number,
335
+ day: number,
336
+ hour: number,
337
+ minute: number,
338
+ second: number,
339
+ millisecond: number,
340
+ quoted: boolean,
341
+ separator: number,
342
+ includeMilliseconds: boolean
343
+ ) {
344
+ // A more specific value would be this:
345
+ // (quoted ? 2 : 0) + 20 + (includeMilliseconds ? 4 : 1)
346
+ // But there is no harm in over-allocating by 6 bytes.
347
+ this.ensureCapacity(26);
348
+ const buffer = this.buffer;
349
+ let offset = this.length;
350
+
351
+ if (quoted) {
352
+ buffer[offset++] = BYTE_DQUOTE;
353
+ }
354
+
355
+ buffer[offset++] = BYTE_ZERO + ((year / 1000) | 0);
356
+ buffer[offset++] = BYTE_ZERO + (((year / 100) | 0) % 10);
357
+ buffer[offset++] = BYTE_ZERO + (((year / 10) | 0) % 10);
358
+ buffer[offset++] = BYTE_ZERO + (year % 10);
359
+ buffer[offset++] = BYTE_DASH;
360
+ buffer[offset++] = BYTE_ZERO + ((month / 10) | 0);
361
+ buffer[offset++] = BYTE_ZERO + (month % 10);
362
+ buffer[offset++] = BYTE_DASH;
363
+ buffer[offset++] = BYTE_ZERO + ((day / 10) | 0);
364
+ buffer[offset++] = BYTE_ZERO + (day % 10);
365
+ buffer[offset++] = separator;
366
+ buffer[offset++] = BYTE_ZERO + ((hour / 10) | 0);
367
+ buffer[offset++] = BYTE_ZERO + (hour % 10);
368
+ buffer[offset++] = BYTE_COLON;
369
+ buffer[offset++] = BYTE_ZERO + ((minute / 10) | 0);
370
+ buffer[offset++] = BYTE_ZERO + (minute % 10);
371
+ buffer[offset++] = BYTE_COLON;
372
+ buffer[offset++] = BYTE_ZERO + ((second / 10) | 0);
373
+ buffer[offset++] = BYTE_ZERO + (second % 10);
374
+
375
+ if (includeMilliseconds) {
376
+ buffer[offset++] = BYTE_DOT;
377
+ buffer[offset++] = BYTE_ZERO + ((millisecond / 100) | 0);
378
+ buffer[offset++] = BYTE_ZERO + (((millisecond / 10) | 0) % 10);
379
+ buffer[offset++] = BYTE_ZERO + (millisecond % 10);
380
+ }
381
+
382
+ buffer[offset++] = BYTE_Z;
383
+
384
+ if (quoted) {
385
+ buffer[offset++] = BYTE_DQUOTE;
386
+ }
387
+
388
+ this.length = offset;
389
+ }
390
+ }
@@ -13,12 +13,15 @@ import {
13
13
  TimeValuePrecision
14
14
  } from '@powersync/service-sync-rules';
15
15
 
16
- import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
16
+ import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
17
17
  import { MongoLSN } from '../common/MongoLSN.js';
18
- import { CHECKPOINTS_COLLECTION } from './replication-utils.js';
19
18
 
20
- export function getMongoRelation(source: mongo.ChangeStreamNameSpace): storage.SourceEntityDescriptor {
19
+ export function getMongoRelation(
20
+ source: mongo.ChangeStreamNameSpace,
21
+ connectionTag: string
22
+ ): storage.SourceEntityDescriptor {
21
23
  return {
24
+ connectionTag,
22
25
  name: source.coll,
23
26
  schema: source.db,
24
27
  // Not relevant for MongoDB - we use db + coll name as the identifier
@@ -115,6 +118,9 @@ function filterJsonData(data: any, context: CompatibilityContext, depth = 0): an
115
118
  } else if (typeof data == 'number') {
116
119
  if (autoBigNum && Number.isInteger(data)) {
117
120
  return BigInt(data);
121
+ } else if (!Number.isFinite(data)) {
122
+ // Only finite numbers can be represented in JSON.
123
+ return null;
118
124
  } else {
119
125
  return data;
120
126
  }
@@ -172,30 +178,53 @@ export async function createCheckpoint(
172
178
  db: mongo.Db,
173
179
  id: mongo.ObjectId | string
174
180
  ): Promise<string> {
175
- const session = client.startSession();
176
- try {
177
- // We use an unique id per process, and clear documents on startup.
178
- // This is so that we can filter events for our own process only, and ignore
179
- // events from other processes.
180
- await db.collection(CHECKPOINTS_COLLECTION).findOneAndUpdate(
181
- {
182
- _id: id as any
183
- },
184
- {
185
- $inc: { i: 1 }
186
- },
187
- {
188
- upsert: true,
189
- returnDocument: 'after',
190
- session
181
+ const TRIES = 2;
182
+ for (let i = 0; i < TRIES; i++) {
183
+ try {
184
+ return await createCheckpointInner(client, db, id);
185
+ } catch (e) {
186
+ if (i < TRIES - 1) {
187
+ logger.warn(`Failed to create checkpoint on attempt ${i + 1}`, e);
188
+ } else {
189
+ throw e;
191
190
  }
192
- );
193
- const time = session.operationTime!;
194
- // TODO: Use the above when we support custom write checkpoints
195
- return new MongoLSN({ timestamp: time }).comparable;
196
- } finally {
197
- await session.endSession();
191
+ }
192
+ }
193
+ throw new ServiceAssertionError(`Unreachable code`);
194
+ }
195
+
196
+ async function createCheckpointInner(
197
+ client: mongo.MongoClient,
198
+ db: mongo.Db,
199
+ id: mongo.ObjectId | string
200
+ ): Promise<string> {
201
+ // We use an unique id per process, and clear documents on startup.
202
+ // This is so that we can filter events for our own process only, and ignore
203
+ // events from other processes.
204
+
205
+ // We use a command instead of a regular update to avoid auto retries on writes.
206
+ // An auto retry on the write can trigger a weird edge case where the change stream event
207
+ // has the clusterTime of the first write, while the returned operation time is for the second no-op write.
208
+ // Instead, we do manual retries, which does not have the same write de-duplication logic.
209
+ // A sentinal-based approach would be better here, but that is a much bigger change.
210
+
211
+ const response = await db.command({
212
+ findAndModify: '_powersync_checkpoints',
213
+ query: {
214
+ _id: id as any
215
+ },
216
+ new: true,
217
+ upsert: true,
218
+ update: {
219
+ $inc: { i: 1 }
220
+ }
221
+ });
222
+
223
+ const time = response.operationTime as mongo.Timestamp | undefined;
224
+ if (time == null) {
225
+ throw new ServiceError(ErrorCode.PSYNC_S1004, `clusterTime not available for checkpoint`);
198
226
  }
227
+ return new MongoLSN({ timestamp: time }).comparable;
199
228
  }
200
229
 
201
230
  const mongoTimeOptions: DateTimeSourceOptions = {
@@ -1,6 +1,7 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
3
  import { bson } from '@powersync/service-core';
4
+ import { parseDocumentId } from './bufferToSqlite.js';
4
5
  import { getCursorBatchBytes } from './internal-mongodb-utils.js';
5
6
 
6
7
  /**
@@ -23,7 +24,7 @@ export class ChunkedSnapshotQuery implements AsyncDisposable {
23
24
  }
24
25
 
25
26
  async nextChunk(): Promise<
26
- { docs: mongo.Document[]; lastKey: Uint8Array; bytes: number } | { docs: []; lastKey: null; bytes: 0 }
27
+ { docs: Buffer[]; lastKey: Uint8Array; bytes: number } | { docs: []; lastKey: null; bytes: 0 }
27
28
  > {
28
29
  let cursor = this.lastCursor;
29
30
  let newCursor = false;
@@ -46,7 +47,8 @@ export class ChunkedSnapshotQuery implements AsyncDisposable {
46
47
  // batchSize is 1 more than limit to auto-close the cursor.
47
48
  // See https://github.com/mongodb/node-mongodb-native/pull/4580
48
49
  batchSize: this.batchSize + 1,
49
- sort: { _id: 1 }
50
+ sort: { _id: 1 },
51
+ raw: true
50
52
  });
51
53
  newCursor = true;
52
54
  }
@@ -62,14 +64,15 @@ export class ChunkedSnapshotQuery implements AsyncDisposable {
62
64
  }
63
65
  }
64
66
  const bytes = getCursorBatchBytes(cursor);
65
- const docBatch = cursor.readBufferedDocuments();
67
+ const docBatch = cursor.readBufferedDocuments() as Buffer[];
66
68
  this.lastCursor = cursor;
67
69
  if (docBatch.length == 0) {
68
70
  throw new ReplicationAssertionError(`MongoDB snapshot query returned an empty batch, but hasNext() was true.`);
69
71
  }
70
- const lastKey = docBatch[docBatch.length - 1]._id;
72
+ const lastDoc = docBatch[docBatch.length - 1];
73
+ const { id: lastKey, idBuffer } = parseDocumentId(lastDoc);
71
74
  this.lastKey = lastKey;
72
- return { docs: docBatch, lastKey: bson.serialize({ _id: lastKey }), bytes };
75
+ return { docs: docBatch, lastKey: idBuffer, bytes };
73
76
  }
74
77
 
75
78
  async [Symbol.asyncDispose](): Promise<void> {