@powersync/service-module-mongodb 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214

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 (77) hide show
  1. package/CHANGELOG.md +153 -5
  2. package/dist/api/MongoRouteAPIAdapter.d.ts +4 -2
  3. package/dist/api/MongoRouteAPIAdapter.js +23 -32
  4. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  5. package/dist/common/SentinelLSN.d.ts +37 -0
  6. package/dist/common/SentinelLSN.js +59 -0
  7. package/dist/common/SentinelLSN.js.map +1 -0
  8. package/dist/module/MongoModule.js +2 -1
  9. package/dist/module/MongoModule.js.map +1 -1
  10. package/dist/replication/ChangeStream.d.ts +24 -0
  11. package/dist/replication/ChangeStream.js +203 -67
  12. package/dist/replication/ChangeStream.js.map +1 -1
  13. package/dist/replication/ChangeStreamReplicationJob.js +3 -2
  14. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  15. package/dist/replication/ChangeStreamReplicator.js +1 -1
  16. package/dist/replication/ChangeStreamReplicator.js.map +1 -1
  17. package/dist/replication/MongoRelation.d.ts +36 -1
  18. package/dist/replication/MongoRelation.js +133 -18
  19. package/dist/replication/MongoRelation.js.map +1 -1
  20. package/dist/replication/MongoSnapshotter.d.ts +9 -1
  21. package/dist/replication/MongoSnapshotter.js +129 -67
  22. package/dist/replication/MongoSnapshotter.js.map +1 -1
  23. package/dist/replication/RawChangeStream.d.ts +4 -1
  24. package/dist/replication/RawChangeStream.js +6 -10
  25. package/dist/replication/RawChangeStream.js.map +1 -1
  26. package/dist/replication/checkpoints/CheckpointImplementation.d.ts +135 -0
  27. package/dist/replication/checkpoints/CheckpointImplementation.js +22 -0
  28. package/dist/replication/checkpoints/CheckpointImplementation.js.map +1 -0
  29. package/dist/replication/checkpoints/SentinelCheckpointImplementation.d.ts +58 -0
  30. package/dist/replication/checkpoints/SentinelCheckpointImplementation.js +224 -0
  31. package/dist/replication/checkpoints/SentinelCheckpointImplementation.js.map +1 -0
  32. package/dist/replication/checkpoints/TimestampCheckpointImplementation.d.ts +27 -0
  33. package/dist/replication/checkpoints/TimestampCheckpointImplementation.js +116 -0
  34. package/dist/replication/checkpoints/TimestampCheckpointImplementation.js.map +1 -0
  35. package/dist/replication/checkpoints/create-checkpoint-implementation.d.ts +6 -0
  36. package/dist/replication/checkpoints/create-checkpoint-implementation.js +10 -0
  37. package/dist/replication/checkpoints/create-checkpoint-implementation.js.map +1 -0
  38. package/dist/replication/replication-utils.d.ts +6 -0
  39. package/dist/replication/replication-utils.js +19 -2
  40. package/dist/replication/replication-utils.js.map +1 -1
  41. package/dist/types/types.d.ts +7 -2
  42. package/dist/types/types.js +20 -2
  43. package/dist/types/types.js.map +1 -1
  44. package/package.json +11 -21
  45. package/src/api/MongoRouteAPIAdapter.ts +28 -39
  46. package/src/common/SentinelLSN.ts +78 -0
  47. package/src/module/MongoModule.ts +2 -1
  48. package/src/replication/ChangeStream.ts +223 -78
  49. package/src/replication/ChangeStreamReplicationJob.ts +3 -2
  50. package/src/replication/ChangeStreamReplicator.ts +1 -1
  51. package/src/replication/MongoRelation.ts +153 -21
  52. package/src/replication/MongoSnapshotter.ts +148 -73
  53. package/src/replication/RawChangeStream.ts +27 -32
  54. package/src/replication/checkpoints/CheckpointImplementation.ts +167 -0
  55. package/src/replication/checkpoints/SentinelCheckpointImplementation.ts +269 -0
  56. package/src/replication/checkpoints/TimestampCheckpointImplementation.ts +145 -0
  57. package/src/replication/checkpoints/create-checkpoint-implementation.ts +14 -0
  58. package/src/replication/replication-utils.ts +23 -2
  59. package/src/types/types.ts +27 -2
  60. package/test/DOCUMENTDB_TESTING.md +115 -0
  61. package/test/src/DatabaseType.ts +25 -0
  62. package/test/src/change_stream.test.ts +180 -71
  63. package/test/src/change_stream_utils.ts +117 -29
  64. package/test/src/checkpoint_retry.test.ts +134 -0
  65. package/test/src/config.test.ts +34 -0
  66. package/test/src/documentdb_helpers.test.ts +124 -0
  67. package/test/src/documentdb_mode.test.ts +1039 -0
  68. package/test/src/mongo_test.test.ts +15 -5
  69. package/test/src/raw_change_stream.test.ts +147 -126
  70. package/test/src/resume.test.ts +2 -2
  71. package/test/src/resume_token.test.ts +30 -0
  72. package/test/src/slow_tests.test.ts +4 -1
  73. package/test/src/test-timeouts.ts +23 -0
  74. package/test/src/util.ts +14 -32
  75. package/tsconfig.json +1 -4
  76. package/tsconfig.scripts.json +1 -2
  77. package/tsconfig.tsbuildinfo +1 -1
@@ -2,13 +2,30 @@ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
2
2
  import { PostImagesOption } from '../types/types.js';
3
3
  export const CHECKPOINTS_COLLECTION = '_powersync_checkpoints';
4
4
  const REQUIRED_CHECKPOINT_PERMISSIONS = ['find', 'insert', 'update', 'remove', 'changeStream', 'createCollection'];
5
+ /**
6
+ * Whether a `hello` response indicates Azure DocumentDB (formerly Azure Cosmos
7
+ * DB for MongoDB vCore), which reports `documentdb_versions` in the `internal`
8
+ * section.
9
+ */
10
+ function isDocumentDbHello(hello) {
11
+ return hello.internal?.documentdb_versions != null;
12
+ }
13
+ /**
14
+ * Detect whether the connected server is DocumentDB. DocumentDB lacks usable
15
+ * clusterTime/operationTime and uses the sentinel checkpoint implementation.
16
+ */
17
+ export async function detectDocumentDb(db) {
18
+ const hello = await db.command({ hello: 1 });
19
+ return isDocumentDbHello(hello);
20
+ }
5
21
  export async function checkSourceConfiguration(connectionManager) {
6
22
  const db = connectionManager.db;
7
23
  const hello = await db.command({ hello: 1 });
8
- if (hello.msg == 'isdbgrid') {
24
+ const isDocumentDb = isDocumentDbHello(hello);
25
+ if (hello.msg == 'isdbgrid' && !isDocumentDb) {
9
26
  throw new ServiceError(ErrorCode.PSYNC_S1341, 'Sharded MongoDB Clusters are not supported yet (including MongoDB Serverless instances).');
10
27
  }
11
- else if (hello.setName == null) {
28
+ else if (hello.setName == null && !isDocumentDb) {
12
29
  throw new ServiceError(ErrorCode.PSYNC_S1342, 'Standalone MongoDB instances are not supported - use a replicaset.');
13
30
  }
14
31
  // https://www.mongodb.com/docs/manual/reference/command/connectionStatus/
@@ -1 +1 @@
1
- {"version":3,"file":"replication-utils.js","sourceRoot":"","sources":["../../src/replication/replication-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"replication-utils.js","sourceRoot":"","sources":["../../src/replication/replication-utils.ts"],"names":[],"mappings":"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"}
@@ -43,6 +43,7 @@ export interface NormalizedMongoConnectionConfig {
43
43
  password?: string;
44
44
  lookup?: LookupFunction;
45
45
  postImages: PostImagesOption;
46
+ heartbeat_interval_seconds: number;
46
47
  connectionParams: MongoConnectionParams;
47
48
  }
48
49
  export declare const MongoConnectionConfig: t.Intersection<t.Codec<{
@@ -51,8 +52,8 @@ export declare const MongoConnectionConfig: t.Intersection<t.Codec<{
51
52
  tag?: string | undefined;
52
53
  debug_api?: boolean | undefined;
53
54
  } & {
54
- uri: string;
55
55
  type: "mongodb";
56
+ uri: string;
56
57
  database?: string | undefined;
57
58
  username?: string | undefined;
58
59
  password?: string | undefined;
@@ -63,14 +64,18 @@ export declare const MongoConnectionConfig: t.Intersection<t.Codec<{
63
64
  tag?: string | undefined;
64
65
  debug_api?: boolean | undefined;
65
66
  } & {
66
- uri: string;
67
67
  type: "mongodb";
68
+ uri: string;
68
69
  database?: string | undefined;
69
70
  username?: string | undefined;
70
71
  password?: string | undefined;
71
72
  reject_ip_ranges?: string[] | undefined;
72
73
  }, string, t.CodecProps>, t.ObjectCodec<{
73
74
  post_images: t.OptionalCodec<t.Codec<"off" | "auto_configure" | "read_only", "off" | "auto_configure" | "read_only", string, t.CodecProps>>;
75
+ /**
76
+ * Interval in seconds between source connection heartbeats. Null or omitted defaults to 60 seconds.
77
+ */
78
+ heartbeat_interval_seconds: t.OptionalCodec<t.Codec<number | null, number | null, string, t.CodecProps>>;
74
79
  }>>;
75
80
  /**
76
81
  * Config input specified when starting services
@@ -1,4 +1,5 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb/types';
2
+ import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
2
3
  import * as service_types from '@powersync/service-types';
3
4
  import * as t from 'ts-codec';
4
5
  export var PostImagesOption;
@@ -35,9 +36,16 @@ export var PostImagesOption;
35
36
  */
36
37
  PostImagesOption["READ_ONLY"] = "read_only";
37
38
  })(PostImagesOption || (PostImagesOption = {}));
39
+ const DEFAULT_PING_INTERVAL_SECONDS = 60;
40
+ const MIN_PING_INTERVAL_SECONDS = 5;
41
+ const MAX_PING_INTERVAL_SECONDS = 60;
38
42
  export const MongoConnectionConfig = service_types.configFile.DataSourceConfig.and(lib_mongo.BaseMongoConfig).and(t.object({
39
43
  // Replication specific settings
40
- post_images: t.literal('off').or(t.literal('auto_configure')).or(t.literal('read_only')).optional()
44
+ post_images: service_types.enumLiteral('off', 'auto_configure', 'read_only').optional(),
45
+ /**
46
+ * Interval in seconds between source connection heartbeats. Null or omitted defaults to 60 seconds.
47
+ */
48
+ heartbeat_interval_seconds: service_types.orNull(t.number).optional()
41
49
  }));
42
50
  /**
43
51
  * Validate and normalize connection options.
@@ -50,7 +58,17 @@ export function normalizeConnectionConfig(options) {
50
58
  ...base,
51
59
  id: options.id ?? 'default',
52
60
  tag: options.tag ?? 'default',
53
- postImages: options.post_images ?? PostImagesOption.OFF
61
+ postImages: options.post_images ?? PostImagesOption.OFF,
62
+ heartbeat_interval_seconds: normalizeHeartbeatInterval(options.heartbeat_interval_seconds)
54
63
  };
55
64
  }
65
+ function normalizeHeartbeatInterval(value) {
66
+ if (value == null) {
67
+ return DEFAULT_PING_INTERVAL_SECONDS;
68
+ }
69
+ if (!Number.isFinite(value) || value < MIN_PING_INTERVAL_SECONDS || value > MAX_PING_INTERVAL_SECONDS) {
70
+ throw new ServiceError(ErrorCode.PSYNC_S1109, `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}`);
71
+ }
72
+ return value;
73
+ }
56
74
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,sCAAsC,CAAC;AAClE,OAAO,KAAK,aAAa,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,MAAM,CAAN,IAAY,gBAkCX;AAlCD,WAAY,gBAAgB;IAC1B;;;;;;;OAOG;IACH,+BAAW,CAAA;IAEX;;;;;;;OAOG;IACH,qDAAiC,CAAA;IAEjC;;;;;;;;;;;OAWG;IACH,2CAAuB,CAAA;AACzB,CAAC,EAlCW,gBAAgB,KAAhB,gBAAgB,QAkC3B;AAmBD,MAAM,CAAC,MAAM,qBAAqB,GAAG,aAAa,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC,GAAG,CAC/G,CAAC,CAAC,MAAM,CAAC;IACP,gCAAgC;IAChC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE;CACpG,CAAC,CACH,CAAC;AAaF;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAqC;IAC7E,MAAM,IAAI,GAAG,SAAS,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAErD,OAAO;QACL,GAAG,IAAI;QACP,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,SAAS;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,SAAS;QAC7B,UAAU,EAAG,OAAO,CAAC,WAA4C,IAAI,gBAAgB,CAAC,GAAG;KAC1F,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,SAAS,MAAM,sCAAsC,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,KAAK,aAAa,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B,MAAM,CAAN,IAAY,gBAkCX;AAlCD,WAAY,gBAAgB;IAC1B;;;;;;;OAOG;IACH,+BAAW,CAAA;IAEX;;;;;;;OAOG;IACH,qDAAiC,CAAA;IAEjC;;;;;;;;;;;OAWG;IACH,2CAAuB,CAAA;AACzB,CAAC,EAlCW,gBAAgB,KAAhB,gBAAgB,QAkC3B;AAED,MAAM,6BAA6B,GAAG,EAAE,CAAC;AACzC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AACpC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAqBrC,MAAM,CAAC,MAAM,qBAAqB,GAAG,aAAa,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC,GAAG,CAC/G,CAAC,CAAC,MAAM,CAAC;IACP,gCAAgC;IAChC,WAAW,EAAE,aAAa,CAAC,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,WAAW,CAAC,CAAC,QAAQ,EAAE;IACvF;;OAEG;IACH,0BAA0B,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;CACtE,CAAC,CACH,CAAC;AAaF;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAqC;IAC7E,MAAM,IAAI,GAAG,SAAS,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAErD,OAAO;QACL,GAAG,IAAI;QACP,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,SAAS;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,SAAS;QAC7B,UAAU,EAAG,OAAO,CAAC,WAA4C,IAAI,gBAAgB,CAAC,GAAG;QACzF,0BAA0B,EAAE,0BAA0B,CAAC,OAAO,CAAC,0BAA0B,CAAC;KAC3F,CAAC;AACJ,CAAC;AAED,SAAS,0BAA0B,CAAC,KAAgC;IAClE,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,6BAA6B,CAAC;IACvC,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,yBAAyB,IAAI,KAAK,GAAG,yBAAyB,EAAE,CAAC;QACtG,MAAM,IAAI,YAAY,CACpB,SAAS,CAAC,WAAW,EACrB,kEAAkE,yBAAyB,QAAQ,yBAAyB,yCAAyC,6BAA6B,EAAE,CACrM,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/package.json CHANGED
@@ -1,41 +1,31 @@
1
1
  {
2
2
  "name": "@powersync/service-module-mongodb",
3
3
  "repository": "https://github.com/powersync-ja/powersync-service",
4
- "types": "dist/index.d.ts",
5
- "version": "0.0.0-dev-20260602073133",
6
- "main": "dist/index.js",
4
+ "version": "0.0.0-dev-20260909133214",
7
5
  "license": "FSL-1.1-ALv2",
8
6
  "type": "module",
9
7
  "publishConfig": {
10
8
  "access": "public"
11
9
  },
12
10
  "exports": {
13
- ".": {
14
- "import": "./dist/index.js",
15
- "require": "./dist/index.js",
16
- "default": "./dist/index.js"
17
- },
18
- "./types": {
19
- "import": "./dist/types/types.js",
20
- "require": "./dist/types/types.js",
21
- "default": "./dist/types/types.js"
22
- }
11
+ ".": "./dist/index.js",
12
+ "./types": "./dist/types/types.js"
23
13
  },
24
14
  "dependencies": {
25
15
  "bson": "^6.10.4",
26
16
  "ts-codec": "^1.3.0",
27
17
  "uuid": "^14.0.0",
28
- "@powersync/lib-service-mongodb": "0.0.0-dev-20260602073133",
29
- "@powersync/lib-services-framework": "0.0.0-dev-20260602073133",
30
- "@powersync/service-core": "0.0.0-dev-20260602073133",
18
+ "@powersync/lib-service-mongodb": "0.0.0-dev-20260909133214",
19
+ "@powersync/lib-services-framework": "0.0.0-dev-20260909133214",
20
+ "@powersync/service-core": "0.0.0-dev-20260909133214",
31
21
  "@powersync/service-jsonbig": "0.17.13",
32
- "@powersync/service-sync-rules": "0.0.0-dev-20260602073133",
33
- "@powersync/service-types": "0.15.2"
22
+ "@powersync/service-sync-rules": "0.0.0-dev-20260909133214",
23
+ "@powersync/service-types": "0.18.0"
34
24
  },
35
25
  "devDependencies": {
36
- "@powersync/service-core-tests": "0.0.0-dev-20260602073133",
37
- "@powersync/service-module-mongodb-storage": "0.0.0-dev-20260602073133",
38
- "@powersync/service-module-postgres-storage": "0.0.0-dev-20260602073133"
26
+ "@powersync/service-core-tests": "0.0.0-dev-20260909133214",
27
+ "@powersync/service-module-mongodb-storage": "0.0.0-dev-20260909133214",
28
+ "@powersync/service-module-postgres-storage": "0.0.0-dev-20260909133214"
39
29
  },
40
30
  "scripts": {
41
31
  "build": "tsc -b",
@@ -1,14 +1,15 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
- import { api, ParseSyncRulesOptions, ReplicationHeadCallback } from '@powersync/service-core';
3
+ import { api, ParseSyncConfigOptions, ReplicationHeadCallback } from '@powersync/service-core';
4
4
  import * as sync_rules from '@powersync/service-sync-rules';
5
5
  import * as service_types from '@powersync/service-types';
6
6
 
7
- import { ServiceAssertionError } from '@powersync/lib-services-framework';
8
- import { MongoLSN } from '../common/MongoLSN.js';
7
+ import { logger } from '@powersync/lib-services-framework';
8
+ import { CheckpointImplementation } from '../replication/checkpoints/CheckpointImplementation.js';
9
+ import { createCheckpointImplementation } from '../replication/checkpoints/create-checkpoint-implementation.js';
9
10
  import { MongoManager } from '../replication/MongoManager.js';
10
- import { constructAfterRecord, STANDALONE_CHECKPOINT_ID } from '../replication/MongoRelation.js';
11
- import { CHECKPOINTS_COLLECTION } from '../replication/replication-utils.js';
11
+ import { constructAfterRecord } from '../replication/MongoRelation.js';
12
+ import { CHECKPOINTS_COLLECTION, detectDocumentDb } from '../replication/replication-utils.js';
12
13
  import * as types from '../types/types.js';
13
14
  import { escapeRegExp } from '../utils.js';
14
15
 
@@ -19,6 +20,8 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
19
20
  connectionTag: string;
20
21
  defaultSchema: string;
21
22
 
23
+ private checkpointImplementation: CheckpointImplementation | null = null;
24
+
22
25
  constructor(protected config: types.ResolvedConnectionConfig) {
23
26
  const manager = new MongoManager(config);
24
27
  this.client = manager.client;
@@ -27,7 +30,7 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
27
30
  this.connectionTag = config.tag ?? sync_rules.DEFAULT_TAG;
28
31
  }
29
32
 
30
- getParseSyncRulesOptions(): ParseSyncRulesOptions {
33
+ getParseSyncRulesOptions(): ParseSyncConfigOptions {
31
34
  return {
32
35
  defaultSchema: this.defaultSchema
33
36
  };
@@ -199,41 +202,27 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
199
202
  }
200
203
 
201
204
  async createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T> {
202
- const session = this.client.startSession();
203
- try {
204
- await this.db.command({ hello: 1 }, { session });
205
- const head = session.clusterTime?.clusterTime;
206
- if (head == null) {
207
- throw new ServiceAssertionError(`clusterTime not available for write checkpoint`);
208
- }
209
-
210
- const r = await callback(new MongoLSN({ timestamp: head }).comparable);
211
-
212
- // Trigger a change on the changestream.
213
- await this.db.collection(CHECKPOINTS_COLLECTION).findOneAndUpdate(
214
- {
215
- _id: STANDALONE_CHECKPOINT_ID as any
216
- },
217
- {
218
- $inc: { i: 1 }
219
- },
220
- {
221
- upsert: true,
222
- returnDocument: 'after',
223
- session
224
- }
225
- );
226
- const time = session.operationTime!;
227
- if (time == null) {
228
- throw new ServiceAssertionError(`operationTime not available for write checkpoint`);
229
- } else if (time.lt(head)) {
230
- throw new ServiceAssertionError(`operationTime must be > clusterTime`);
231
- }
205
+ const checkpointImplementation = await this.getCheckpointImplementation();
206
+ return checkpointImplementation.createReplicationHead(callback);
207
+ }
232
208
 
233
- return r;
234
- } finally {
235
- await session.endSession();
209
+ private async getCheckpointImplementation(): Promise<CheckpointImplementation> {
210
+ if (this.checkpointImplementation == null) {
211
+ const isDocumentDb = await detectDocumentDb(this.db);
212
+ this.checkpointImplementation = createCheckpointImplementation(isDocumentDb, {
213
+ client: this.client,
214
+ db: this.db,
215
+ // The adapter never streams, so it has no real barrier document. This
216
+ // random id is only safe because the adapter only ever calls
217
+ // createReplicationHead, which produces a standalone (stream_id = null)
218
+ // head that every real ChangeStream observes. It must NOT be used for a
219
+ // batch barrier (createBatchCheckpoint stamps this id), or the head would
220
+ // become an own-barrier of a phantom stream that nothing resolves.
221
+ checkpointStreamId: new mongo.ObjectId(),
222
+ logger
223
+ });
236
224
  }
225
+ return this.checkpointImplementation;
237
226
  }
238
227
 
239
228
  async getConnectionSchema(): Promise<service_types.DatabaseSchema[]> {
@@ -0,0 +1,78 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { storage } from '@powersync/service-core';
3
+
4
+ export type SentinelLSNSpecification = {
5
+ sentinel: bigint;
6
+ /**
7
+ * Resume tokens are opaque on sentinel-based sources (e.g. DocumentDB). The
8
+ * sentinel component is the comparable position; this token is only used to
9
+ * resume the change stream.
10
+ */
11
+ resume_token?: mongo.ResumeToken | null;
12
+ };
13
+
14
+ const DELIMINATOR = '|';
15
+
16
+ /**
17
+ * Width of the sentinel coordinate, matching a MongoDB cluster timestamp
18
+ * (64-bit → 16 hex chars). The sentinel counter is a 64-bit `Long`, so this is
19
+ * sufficient and never truncates.
20
+ */
21
+ const SENTINEL_HEX_LENGTH = 16;
22
+
23
+ /**
24
+ * LSN for sentinel-based checkpointing (sources without a usable clusterTime,
25
+ * e.g. DocumentDB). The ordered coordinate is a monotonic sentinel counter; the
26
+ * opaque resume token is carried alongside it only for `resumeAfter`.
27
+ *
28
+ * The coordinate is serialized as a 16-hex-char value with the **same shape as
29
+ * a MongoDB timestamp LSN** ({@link MongoLSN}): the high 32 bits resemble epoch
30
+ * seconds and the low 32 bits an increment. This is deliberate — it makes
31
+ * sentinel LSNs directly string-comparable with timestamp LSNs, so a sentinel
32
+ * coordinate (seeded at the current epoch seconds; see createSentinelCheckpointLsn)
33
+ * always sorts **above** any real-timestamp LSN issued in the past.
34
+ *
35
+ * It only *resembles* a timestamp. The value is a synthetic monotonic counter,
36
+ * not a real cluster time, and must never be used as one (e.g. it is never fed
37
+ * to `startAtOperationTime`; the sentinel implementation resumes purely via the
38
+ * resume token).
39
+ */
40
+ export class SentinelLSN {
41
+ static ZERO = new SentinelLSN({ sentinel: 0n });
42
+
43
+ static fromSerialized(comparable: string): SentinelLSN {
44
+ const [sentinelString, resumeString] = comparable.split(DELIMINATOR);
45
+
46
+ return new SentinelLSN({
47
+ sentinel: BigInt(`0x${sentinelString}`),
48
+ resume_token: resumeString ? storage.deserializeBson(Buffer.from(resumeString, 'base64')).resumeToken : null
49
+ });
50
+ }
51
+
52
+ constructor(protected options: SentinelLSNSpecification) {}
53
+
54
+ get sentinel() {
55
+ return this.options.sentinel;
56
+ }
57
+
58
+ get resumeToken() {
59
+ return this.options.resume_token;
60
+ }
61
+
62
+ get comparable() {
63
+ // padStart(16) of the combined 64-bit value yields the identical string to
64
+ // MongoLSN's high(8)+low(8) formatting, keeping the two formats comparable.
65
+ const sentinel = this.sentinel.toString(16).padStart(SENTINEL_HEX_LENGTH, '0');
66
+ const segments = [sentinel];
67
+
68
+ if (this.resumeToken) {
69
+ segments.push(storage.serializeBson({ resumeToken: this.resumeToken }).toString('base64'));
70
+ }
71
+
72
+ return segments.join(DELIMINATOR);
73
+ }
74
+
75
+ toString() {
76
+ return this.comparable;
77
+ }
78
+ }
@@ -39,7 +39,8 @@ export class MongoModule extends replication.ReplicationModule<types.MongoConnec
39
39
  storageEngine: context.storageEngine,
40
40
  metricsEngine: context.metricsEngine,
41
41
  connectionFactory: connectionFactory,
42
- rateLimiter: new MongoErrorRateLimiter()
42
+ rateLimiter: new MongoErrorRateLimiter(),
43
+ heartbeatIntervalSeconds: normalisedConfig.heartbeat_interval_seconds
43
44
  });
44
45
  }
45
46