@powersync/service-module-mongodb 0.15.3 → 0.16.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 (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/api/MongoRouteAPIAdapter.js +2 -2
  3. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  4. package/dist/replication/ChangeStream.d.ts +6 -6
  5. package/dist/replication/ChangeStream.js +300 -322
  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/ChangeStreamReplicator.d.ts +1 -1
  10. package/dist/replication/ChangeStreamReplicator.js +1 -1
  11. package/dist/replication/ChangeStreamReplicator.js.map +1 -1
  12. package/dist/replication/JsonBufferWriter.d.ts +80 -0
  13. package/dist/replication/JsonBufferWriter.js +342 -0
  14. package/dist/replication/JsonBufferWriter.js.map +1 -0
  15. package/dist/replication/MongoManager.d.ts +1 -1
  16. package/dist/replication/MongoManager.js +1 -1
  17. package/dist/replication/MongoManager.js.map +1 -1
  18. package/dist/replication/MongoRelation.js +4 -0
  19. package/dist/replication/MongoRelation.js.map +1 -1
  20. package/dist/replication/MongoSnapshotQuery.d.ts +1 -1
  21. package/dist/replication/MongoSnapshotQuery.js +6 -3
  22. package/dist/replication/MongoSnapshotQuery.js.map +1 -1
  23. package/dist/replication/RawChangeStream.d.ts +55 -0
  24. package/dist/replication/RawChangeStream.js +322 -0
  25. package/dist/replication/RawChangeStream.js.map +1 -0
  26. package/dist/replication/SourceRowConverter.d.ts +46 -0
  27. package/dist/replication/SourceRowConverter.js +42 -0
  28. package/dist/replication/SourceRowConverter.js.map +1 -0
  29. package/dist/replication/bufferToSqlite.d.ts +43 -0
  30. package/dist/replication/bufferToSqlite.js +740 -0
  31. package/dist/replication/bufferToSqlite.js.map +1 -0
  32. package/dist/replication/internal-mongodb-utils.d.ts +0 -12
  33. package/dist/replication/internal-mongodb-utils.js +0 -54
  34. package/dist/replication/internal-mongodb-utils.js.map +1 -1
  35. package/dist/replication/replication-index.d.ts +4 -2
  36. package/dist/replication/replication-index.js +4 -2
  37. package/dist/replication/replication-index.js.map +1 -1
  38. package/dist/replication/replication-utils.d.ts +1 -1
  39. package/dist/types/types.js.map +1 -1
  40. package/package.json +11 -11
  41. package/scripts/benchmark-change-document-json.mts +358 -0
  42. package/scripts/benchmark-change-document.mts +370 -0
  43. package/src/api/MongoRouteAPIAdapter.ts +2 -2
  44. package/src/replication/ChangeStream.ts +348 -371
  45. package/src/replication/ChangeStreamReplicationJob.ts +2 -2
  46. package/src/replication/ChangeStreamReplicator.ts +2 -5
  47. package/src/replication/JsonBufferWriter.ts +390 -0
  48. package/src/replication/MongoManager.ts +2 -2
  49. package/src/replication/MongoRelation.ts +5 -2
  50. package/src/replication/MongoSnapshotQuery.ts +8 -5
  51. package/src/replication/RawChangeStream.ts +460 -0
  52. package/src/replication/SourceRowConverter.ts +65 -0
  53. package/src/replication/bufferToSqlite.ts +944 -0
  54. package/src/replication/internal-mongodb-utils.ts +0 -66
  55. package/src/replication/replication-index.ts +4 -2
  56. package/src/replication/replication-utils.ts +2 -2
  57. package/src/types/types.ts +1 -1
  58. package/test/src/buffer_to_sqlite.test.ts +1146 -0
  59. package/test/src/change_stream.test.ts +49 -3
  60. package/test/src/change_stream_utils.ts +4 -10
  61. package/test/src/mongo_test.test.ts +66 -64
  62. package/test/src/parse_document_id.test.ts +54 -0
  63. package/test/src/raw_change_stream.test.ts +547 -0
  64. package/test/src/resume.test.ts +12 -2
  65. package/test/src/util.ts +56 -3
  66. package/test/tsconfig.json +0 -1
  67. package/tsconfig.scripts.json +13 -0
  68. package/tsconfig.tsbuildinfo +1 -1
  69. 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> {
@@ -1,10 +1,7 @@
1
- import { storage, replication } from '@powersync/service-core';
1
+ import { replication, storage } from '@powersync/service-core';
2
+ import { MongoModule } from '../module/MongoModule.js';
2
3
  import { ChangeStreamReplicationJob } from './ChangeStreamReplicationJob.js';
3
4
  import { ConnectionManagerFactory } from './ConnectionManagerFactory.js';
4
- import { MongoErrorRateLimiter } from './MongoErrorRateLimiter.js';
5
- import { MongoModule } from '../module/MongoModule.js';
6
- import { MongoLSN } from '../common/MongoLSN.js';
7
- import { timestampToDate } from './replication-utils.js';
8
5
 
9
6
  export interface ChangeStreamReplicatorOptions extends replication.AbstractReplicatorOptions {
10
7
  connectionFactory: ConnectionManagerFactory;
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
 
3
- import { NormalizedMongoConnectionConfig } from '../types/types.js';
4
- import { BSON_DESERIALIZE_DATA_OPTIONS, POWERSYNC_VERSION } from '@powersync/service-core';
5
3
  import { BaseObserver } from '@powersync/lib-services-framework';
4
+ import { BSON_DESERIALIZE_DATA_OPTIONS, POWERSYNC_VERSION } from '@powersync/service-core';
5
+ import { NormalizedMongoConnectionConfig } from '../types/types.js';
6
6
 
7
7
  export interface MongoManagerListener {
8
8
  onEnded(): void;
@@ -6,10 +6,10 @@ import {
6
6
  CustomArray,
7
7
  CustomObject,
8
8
  CustomSqliteValue,
9
+ DateTimeSourceOptions,
10
+ DateTimeValue,
9
11
  SqliteInputRow,
10
12
  SqliteInputValue,
11
- DateTimeValue,
12
- DateTimeSourceOptions,
13
13
  TimeValuePrecision
14
14
  } from '@powersync/service-sync-rules';
15
15
 
@@ -115,6 +115,9 @@ function filterJsonData(data: any, context: CompatibilityContext, depth = 0): an
115
115
  } else if (typeof data == 'number') {
116
116
  if (autoBigNum && Number.isInteger(data)) {
117
117
  return BigInt(data);
118
+ } else if (!Number.isFinite(data)) {
119
+ // Only finite numbers can be represented in JSON.
120
+ return null;
118
121
  } else {
119
122
  return data;
120
123
  }
@@ -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> {