@powersync/service-module-mongodb 0.19.0 → 0.20.1

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 (35) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/module/MongoModule.js +2 -1
  3. package/dist/module/MongoModule.js.map +1 -1
  4. package/dist/replication/ChangeStream.d.ts +8 -0
  5. package/dist/replication/ChangeStream.js +69 -10
  6. package/dist/replication/ChangeStream.js.map +1 -1
  7. package/dist/replication/ChangeStreamReplicationJob.js +2 -1
  8. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  9. package/dist/replication/MongoSnapshotter.js +5 -0
  10. package/dist/replication/MongoSnapshotter.js.map +1 -1
  11. package/dist/replication/RawChangeStream.js +1 -11
  12. package/dist/replication/RawChangeStream.js.map +1 -1
  13. package/dist/replication/checkpoints/CheckpointImplementation.d.ts +4 -1
  14. package/dist/replication/checkpoints/SentinelCheckpointImplementation.d.ts +4 -1
  15. package/dist/replication/checkpoints/SentinelCheckpointImplementation.js +10 -8
  16. package/dist/replication/checkpoints/SentinelCheckpointImplementation.js.map +1 -1
  17. package/dist/replication/checkpoints/TimestampCheckpointImplementation.d.ts +4 -1
  18. package/dist/replication/checkpoints/TimestampCheckpointImplementation.js +23 -20
  19. package/dist/replication/checkpoints/TimestampCheckpointImplementation.js.map +1 -1
  20. package/dist/types/types.d.ts +5 -0
  21. package/dist/types/types.js +20 -2
  22. package/dist/types/types.js.map +1 -1
  23. package/package.json +9 -9
  24. package/src/module/MongoModule.ts +2 -1
  25. package/src/replication/ChangeStream.ts +73 -12
  26. package/src/replication/ChangeStreamReplicationJob.ts +2 -1
  27. package/src/replication/MongoSnapshotter.ts +10 -0
  28. package/src/replication/RawChangeStream.ts +1 -15
  29. package/src/replication/checkpoints/CheckpointImplementation.ts +1 -1
  30. package/src/replication/checkpoints/SentinelCheckpointImplementation.ts +11 -9
  31. package/src/replication/checkpoints/TimestampCheckpointImplementation.ts +27 -24
  32. package/src/types/types.ts +27 -2
  33. package/test/src/change_stream_utils.ts +16 -4
  34. package/test/src/config.test.ts +34 -0
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { MongoConnectionParams } from '@powersync/lib-service-mongodb/types';
2
2
  import * as lib_mongo from '@powersync/lib-service-mongodb/types';
3
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
3
4
  import * as service_types from '@powersync/service-types';
4
5
  import { LookupFunction } from 'node:net';
5
6
  import * as t from 'ts-codec';
@@ -40,6 +41,10 @@ export enum PostImagesOption {
40
41
  READ_ONLY = 'read_only'
41
42
  }
42
43
 
44
+ const DEFAULT_PING_INTERVAL_SECONDS = 60;
45
+ const MIN_PING_INTERVAL_SECONDS = 5;
46
+ const MAX_PING_INTERVAL_SECONDS = 60;
47
+
43
48
  export interface NormalizedMongoConnectionConfig {
44
49
  id: string;
45
50
  tag: string;
@@ -54,13 +59,19 @@ export interface NormalizedMongoConnectionConfig {
54
59
 
55
60
  postImages: PostImagesOption;
56
61
 
62
+ heartbeat_interval_seconds: number;
63
+
57
64
  connectionParams: MongoConnectionParams;
58
65
  }
59
66
 
60
67
  export const MongoConnectionConfig = service_types.configFile.DataSourceConfig.and(lib_mongo.BaseMongoConfig).and(
61
68
  t.object({
62
69
  // Replication specific settings
63
- post_images: t.literal('off').or(t.literal('auto_configure')).or(t.literal('read_only')).optional()
70
+ post_images: t.literal('off').or(t.literal('auto_configure')).or(t.literal('read_only')).optional(),
71
+ /**
72
+ * Interval in seconds between source connection heartbeats. Null or omitted defaults to 60 seconds.
73
+ */
74
+ heartbeat_interval_seconds: t.number.or(t.Null).optional()
64
75
  })
65
76
  );
66
77
 
@@ -87,6 +98,20 @@ export function normalizeConnectionConfig(options: MongoConnectionConfigDecoded)
87
98
  ...base,
88
99
  id: options.id ?? 'default',
89
100
  tag: options.tag ?? 'default',
90
- postImages: (options.post_images as PostImagesOption | undefined) ?? PostImagesOption.OFF
101
+ postImages: (options.post_images as PostImagesOption | undefined) ?? PostImagesOption.OFF,
102
+ heartbeat_interval_seconds: normalizeHeartbeatInterval(options.heartbeat_interval_seconds)
91
103
  };
92
104
  }
105
+
106
+ function normalizeHeartbeatInterval(value: number | null | undefined): number {
107
+ if (value == null) {
108
+ return DEFAULT_PING_INTERVAL_SECONDS;
109
+ }
110
+ if (!Number.isFinite(value) || value < MIN_PING_INTERVAL_SECONDS || value > MAX_PING_INTERVAL_SECONDS) {
111
+ throw new ServiceError(
112
+ ErrorCode.PSYNC_S1109,
113
+ `MongoDB connection: heartbeat_interval_seconds must be between ${MIN_PING_INTERVAL_SECONDS} and ${MAX_PING_INTERVAL_SECONDS} seconds; null or omitted defaults to ${DEFAULT_PING_INTERVAL_SECONDS}`
114
+ );
115
+ }
116
+ return value;
117
+ }
@@ -5,6 +5,7 @@ import {
5
5
  createCoreReplicationMetrics,
6
6
  initializeCoreReplicationMetrics,
7
7
  InternalOpId,
8
+ isBatchEnd,
8
9
  LEGACY_STORAGE_VERSION,
9
10
  OplogEntry,
10
11
  ProtocolOpId,
@@ -273,12 +274,23 @@ export class ChangeStreamTestContext {
273
274
  while (true) {
274
275
  const batch = this.storage!.getBucketDataBatch(checkpoint, map);
275
276
 
276
- const batches = await test_utils.fromAsync(batch);
277
- data = data.concat(batches[0]?.chunkData.data ?? []);
278
- if (batches.length == 0 || !batches[0]!.chunkData.has_more) {
277
+ const chunks = await test_utils.fromAsync(batch);
278
+ if (chunks.length == 0) {
279
279
  break;
280
280
  }
281
- map = [bucketRequest(syncConfigContent, bucket, BigInt(batches[0]!.chunkData.next_after))];
281
+ for (let chunk of chunks) {
282
+ if (isBatchEnd(chunk)) {
283
+ if (!chunk.hasMore) {
284
+ return data;
285
+ }
286
+ } else {
287
+ data = data.concat(chunk.chunkData.data ?? []);
288
+ map = [bucketRequest(syncConfigContent, bucket, BigInt(chunk.chunkData.next_after))];
289
+ if (!chunk.chunkData.has_more) {
290
+ return data;
291
+ }
292
+ }
293
+ }
282
294
  }
283
295
  return data;
284
296
  }
@@ -0,0 +1,34 @@
1
+ import { normalizeConnectionConfig } from '@module/types/types.js';
2
+ import { describe, expect, test } from 'vitest';
3
+
4
+ const BASE_CONFIG = {
5
+ type: 'mongodb' as const,
6
+ uri: 'mongodb://localhost:27017/powersync_test'
7
+ };
8
+
9
+ describe('MongoDB connection config', () => {
10
+ test('defaults heartbeat_interval_seconds to 60 seconds', () => {
11
+ expect(normalizeConnectionConfig(BASE_CONFIG).heartbeat_interval_seconds).toBe(60);
12
+ });
13
+
14
+ test('defaults a null heartbeat_interval_seconds to 60 seconds', () => {
15
+ expect(
16
+ normalizeConnectionConfig({ ...BASE_CONFIG, heartbeat_interval_seconds: null }).heartbeat_interval_seconds
17
+ ).toBe(60);
18
+ });
19
+
20
+ test('allows the MongoDB maximum heartbeat interval', () => {
21
+ expect(
22
+ normalizeConnectionConfig({ ...BASE_CONFIG, heartbeat_interval_seconds: 60 }).heartbeat_interval_seconds
23
+ ).toBe(60);
24
+ });
25
+
26
+ test.each([0, 4, 61, Number.NaN, Number.POSITIVE_INFINITY])(
27
+ 'rejects invalid heartbeat_interval_seconds: %s',
28
+ (heartbeat_interval_seconds) => {
29
+ expect(() => normalizeConnectionConfig({ ...BASE_CONFIG, heartbeat_interval_seconds })).toThrow(
30
+ 'heartbeat_interval_seconds'
31
+ );
32
+ }
33
+ );
34
+ });