@powersync/service-module-mongodb 0.0.0-dev-20261007110800 → 0.0.0-dev-20261015071128

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 (47) hide show
  1. package/CHANGELOG.md +13 -11
  2. package/benchmarks/env.ts +11 -0
  3. package/benchmarks/replication_branch_throughput.benchmark.ts +240 -0
  4. package/dist/api/MongoRouteAPIAdapter.d.ts +5 -0
  5. package/dist/api/MongoRouteAPIAdapter.js +11 -4
  6. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  7. package/dist/module/MongoModule.js +1 -0
  8. package/dist/module/MongoModule.js.map +1 -1
  9. package/dist/replication/ChangeStream.d.ts +2 -1
  10. package/dist/replication/ChangeStream.js +164 -150
  11. package/dist/replication/ChangeStream.js.map +1 -1
  12. package/dist/replication/ChangeStreamReplicationJob.js +17 -9
  13. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  14. package/dist/replication/MongoReplicationQueryProvider.d.ts +9 -11
  15. package/dist/replication/MongoReplicationQueryProvider.js.map +1 -1
  16. package/dist/replication/MongoReplicationStream.d.ts +37 -48
  17. package/dist/replication/MongoReplicationStream.js +157 -30
  18. package/dist/replication/MongoReplicationStream.js.map +1 -1
  19. package/dist/replication/MongoReplicationStreamTypes.d.ts +157 -0
  20. package/dist/replication/MongoReplicationStreamTypes.js +19 -0
  21. package/dist/replication/MongoReplicationStreamTypes.js.map +1 -0
  22. package/dist/replication/MongoSnapshotter.js +45 -25
  23. package/dist/replication/MongoSnapshotter.js.map +1 -1
  24. package/dist/replication/replication-index.d.ts +1 -0
  25. package/dist/replication/replication-index.js +1 -0
  26. package/dist/replication/replication-index.js.map +1 -1
  27. package/package.json +11 -10
  28. package/src/api/MongoRouteAPIAdapter.ts +12 -5
  29. package/src/module/MongoModule.ts +1 -0
  30. package/src/replication/ChangeStream.ts +183 -173
  31. package/src/replication/ChangeStreamReplicationJob.ts +16 -9
  32. package/src/replication/MongoReplicationQueryProvider.ts +9 -11
  33. package/src/replication/MongoReplicationStream.ts +196 -83
  34. package/src/replication/MongoReplicationStreamTypes.ts +169 -0
  35. package/src/replication/MongoSnapshotter.ts +49 -28
  36. package/src/replication/replication-index.ts +1 -0
  37. package/test/src/change_event_memory.test.ts +33 -0
  38. package/test/src/change_stream_abort.test.ts +110 -0
  39. package/test/src/filtering_example.test.ts +65 -0
  40. package/test/src/filtering_lifecycle.test.ts +101 -0
  41. package/test/src/query_provider_availability.test.ts +1 -0
  42. package/test/src/query_provider_diagnostics.test.ts +55 -0
  43. package/test/src/replication_stream.test.ts +411 -34
  44. package/test/src/stream_progress.test.ts +62 -44
  45. package/test/tsconfig.json +2 -2
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/vitest.benchmark.config.ts +15 -0
@@ -1,5 +1,10 @@
1
1
  import { DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER } from '@module/replication/MongoReplicationQueryProvider.js';
2
- import { MongoReplicationStreamItem } from '@module/replication/MongoReplicationStream.js';
2
+ import {
3
+ createMongoReplicationStreamBatch,
4
+ filterMongoReplicationStream
5
+ } from '@module/replication/MongoReplicationStream.js';
6
+ import type { MongoReplicationStreamBatch } from '@module/replication/MongoReplicationStreamTypes.js';
7
+ import { MongoReplicationFilterResult } from '@module/replication/MongoReplicationStreamTypes.js';
3
8
  import { describe, expect, test, vi } from 'vitest';
4
9
  import { openChangeStreamTestContext } from './change_stream_test_setup.js';
5
10
  import { DATABASE_TYPE, DatabaseType } from './DatabaseType.js';
@@ -41,31 +46,33 @@ describe.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('MongoDB durable adapt
41
46
  const provider = {
42
47
  ...DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER,
43
48
  openChangeStream: ({ open }: Parameters<typeof DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER.openChangeStream>[0]) =>
44
- (async function* (): AsyncGenerator<MongoReplicationStreamItem> {
49
+ (async function* (): AsyncGenerator<MongoReplicationStreamBatch> {
45
50
  let retained = false;
46
- let filtered = 0;
47
- for await (const item of open({})) {
48
- if (item.type == 'change' && 'documentKey' in item.event && item.event.ns.coll == 'documents') {
49
- if (String(item.event.documentKey._id) != '1') {
50
- filtered++;
51
- continue;
51
+ const filteredStream = filterMongoReplicationStream({
52
+ batches: open({}),
53
+ filter: (event) => {
54
+ if (event.ns.coll == 'documents') {
55
+ if (String(event.documentKey._id) != '1') {
56
+ return MongoReplicationFilterResult.EXCLUDE;
57
+ }
58
+ retained = true;
52
59
  }
53
- retained = true;
54
- } else if (item.type == 'progress') {
55
- // open({}) uses the shared reader, which emits progress with the batch's resume token
56
- // after consuming a MongoDB batch, provided no split event is incomplete. This is a
57
- // reader-generated boundary, not a MongoDB change event or a result of filtering.
58
- // If a boundary arrives after retaining document 1 but before excluding any later
59
- // documents, suppress it until a boundary also covers excluded changes. Forwarding
60
- // that boundary triggers the injected flush failure with document 1 still pending,
61
- // verifying that its write must be durable before the resume token can be saved.
62
- if (retained && filtered == 0) continue;
63
- yield { ...item, filteredCount: filtered };
64
- retained = false;
65
- filtered = 0;
66
- continue;
60
+ return MongoReplicationFilterResult.KEEP;
67
61
  }
68
- yield item;
62
+ });
63
+ for await (const batch of filteredStream) {
64
+ yield createMongoReplicationStreamBatch({
65
+ events: batch.events,
66
+ resolveResumeToken: () => {
67
+ const progress = batch.getResumeToken();
68
+ if (progress == null || (retained && batch.metadata.filteredCount == 0)) {
69
+ return undefined;
70
+ }
71
+ retained = false;
72
+ return progress;
73
+ },
74
+ resolveMetadata: () => batch.metadata
75
+ });
69
76
  }
70
77
  })()
71
78
  };
@@ -224,32 +231,43 @@ describe.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('MongoDB durable adapt
224
231
  createReplicationQueryProvider: () => ({
225
232
  ...DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER,
226
233
  openChangeStream({ open }) {
227
- return (async function* (): AsyncGenerator<MongoReplicationStreamItem> {
228
- let count = 0;
229
- for await (const item of open({})) {
230
- // Drop data events but keep the shared reader's batch boundaries and any control
231
- // events. Report how many complete changes each forwarded boundary excludes.
232
- if (item.type == 'change' && 'ns' in item.event && item.event.ns.coll == 'documents') {
233
- count++;
234
- continue;
235
- }
236
- if (item.type == 'progress' && count > 0) {
237
- filtered += count;
238
- beforeFailure = (await context.storage!.getStatus()).resumeLsn!;
239
- // The adapter error prevents delivery of the boundary. The flush error allows
240
- // delivery but fails storage processing before its resume token can be saved.
241
- if (scenario == 'adapter failure') throw new Error('injected adapter failure');
242
- failFlush = scenario == 'flush failure';
243
- yield { ...item, filteredCount: count };
244
- count = 0;
245
- // next() is requested only after this module has handled the progress item. Inspect actual
234
+ return (async function* (): AsyncGenerator<MongoReplicationStreamBatch> {
235
+ for await (const batch of open({})) {
236
+ let count = 0;
237
+ const positionBeforeBatch = (await context.storage!.getStatus()).resumeLsn!;
238
+ const events = (function* () {
239
+ for (const change of batch.events) {
240
+ if ('ns' in change.event && change.event.ns.coll == 'documents') {
241
+ count++;
242
+ continue;
243
+ }
244
+ yield change;
245
+ }
246
+ })();
247
+ let forwardedProgress = false;
248
+ yield createMongoReplicationStreamBatch({
249
+ events: events,
250
+ resolveResumeToken: () => {
251
+ const progress = batch.getResumeToken();
252
+ if (progress == null || count == 0) {
253
+ return progress;
254
+ }
255
+ filtered += count;
256
+ beforeFailure = positionBeforeBatch;
257
+ if (scenario == 'adapter failure') throw new Error('injected adapter failure');
258
+ failFlush = scenario == 'flush failure';
259
+ forwardedProgress = true;
260
+ return progress;
261
+ },
262
+ resolveMetadata: () => ({ filteredCount: count })
263
+ });
264
+ if (forwardedProgress) {
265
+ // next() is requested only after this module has handled the batch, including its resume token. Inspect actual
246
266
  // persisted state here, without sleeps or waiting for a later checkpoint event.
247
267
  savedPositions.push((await context.storage!.getStatus()).resumeLsn!);
248
268
  // End the successful case deliberately while the later checkpoint marker is
249
269
  // still pending, so assertions observe progress independently of a commit.
250
270
  if (savedPositions.length == 3) throw new Error('stopped after three safe boundaries');
251
- } else {
252
- yield item;
253
271
  }
254
272
  }
255
273
  })();
@@ -6,9 +6,9 @@
6
6
  "@module/*": ["../src/*"],
7
7
  "@core-tests/*": ["../../../packages/service-core/test/src/*"]
8
8
  },
9
- "rootDir": "src"
9
+ "rootDir": ".."
10
10
  },
11
- "include": ["src"],
11
+ "include": ["src", "../benchmarks"],
12
12
  "references": [
13
13
  {
14
14
  "path": "../"