@powersync/service-module-mongodb 0.0.0-dev-20260602073133 → 0.0.0-dev-20260803144812
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.
- package/CHANGELOG.md +97 -5
- package/dist/api/MongoRouteAPIAdapter.d.ts +4 -2
- package/dist/api/MongoRouteAPIAdapter.js +23 -32
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/common/SentinelLSN.d.ts +37 -0
- package/dist/common/SentinelLSN.js +59 -0
- package/dist/common/SentinelLSN.js.map +1 -0
- package/dist/replication/ChangeStream.d.ts +16 -0
- package/dist/replication/ChangeStream.js +143 -64
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/ChangeStreamReplicator.js +1 -1
- package/dist/replication/ChangeStreamReplicator.js.map +1 -1
- package/dist/replication/MongoRelation.d.ts +36 -1
- package/dist/replication/MongoRelation.js +133 -18
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/MongoSnapshotter.d.ts +9 -1
- package/dist/replication/MongoSnapshotter.js +128 -66
- package/dist/replication/MongoSnapshotter.js.map +1 -1
- package/dist/replication/RawChangeStream.d.ts +12 -1
- package/dist/replication/RawChangeStream.js +23 -10
- package/dist/replication/RawChangeStream.js.map +1 -1
- package/dist/replication/checkpoints/CheckpointImplementation.d.ts +132 -0
- package/dist/replication/checkpoints/CheckpointImplementation.js +22 -0
- package/dist/replication/checkpoints/CheckpointImplementation.js.map +1 -0
- package/dist/replication/checkpoints/SentinelCheckpointImplementation.d.ts +55 -0
- package/dist/replication/checkpoints/SentinelCheckpointImplementation.js +224 -0
- package/dist/replication/checkpoints/SentinelCheckpointImplementation.js.map +1 -0
- package/dist/replication/checkpoints/TimestampCheckpointImplementation.d.ts +24 -0
- package/dist/replication/checkpoints/TimestampCheckpointImplementation.js +115 -0
- package/dist/replication/checkpoints/TimestampCheckpointImplementation.js.map +1 -0
- package/dist/replication/checkpoints/create-checkpoint-implementation.d.ts +6 -0
- package/dist/replication/checkpoints/create-checkpoint-implementation.js +10 -0
- package/dist/replication/checkpoints/create-checkpoint-implementation.js.map +1 -0
- package/dist/replication/replication-utils.d.ts +6 -0
- package/dist/replication/replication-utils.js +19 -2
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +2 -2
- package/package.json +11 -21
- package/src/api/MongoRouteAPIAdapter.ts +28 -39
- package/src/common/SentinelLSN.ts +78 -0
- package/src/replication/ChangeStream.ts +159 -73
- package/src/replication/ChangeStreamReplicationJob.ts +1 -1
- package/src/replication/ChangeStreamReplicator.ts +1 -1
- package/src/replication/MongoRelation.ts +153 -21
- package/src/replication/MongoSnapshotter.ts +147 -72
- package/src/replication/RawChangeStream.ts +54 -32
- package/src/replication/checkpoints/CheckpointImplementation.ts +167 -0
- package/src/replication/checkpoints/SentinelCheckpointImplementation.ts +269 -0
- package/src/replication/checkpoints/TimestampCheckpointImplementation.ts +144 -0
- package/src/replication/checkpoints/create-checkpoint-implementation.ts +14 -0
- package/src/replication/replication-utils.ts +23 -2
- package/test/DOCUMENTDB_TESTING.md +115 -0
- package/test/src/DatabaseType.ts +25 -0
- package/test/src/change_stream.test.ts +107 -71
- package/test/src/change_stream_utils.ts +117 -29
- package/test/src/checkpoint_retry.test.ts +134 -0
- package/test/src/documentdb_helpers.test.ts +124 -0
- package/test/src/documentdb_mode.test.ts +1040 -0
- package/test/src/mongo_test.test.ts +15 -5
- package/test/src/raw_change_stream.test.ts +209 -125
- package/test/src/resume.test.ts +2 -2
- package/test/src/resume_token.test.ts +30 -0
- package/test/src/slow_tests.test.ts +4 -1
- package/test/src/test-timeouts.ts +23 -0
- package/test/src/util.ts +3 -3
- package/tsconfig.json +1 -4
- package/tsconfig.scripts.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { ReplicationHeadCallback, storage } from '@powersync/service-core';
|
|
4
|
+
import { MongoLSN } from '../../common/MongoLSN.js';
|
|
5
|
+
import { createCheckpoint, SENTINEL_CHECKPOINT_ID, STANDALONE_CHECKPOINT_ID } from '../MongoRelation.js';
|
|
6
|
+
import { CHECKPOINTS_COLLECTION, timestampToDate } from '../replication-utils.js';
|
|
7
|
+
import {
|
|
8
|
+
CheckpointEventApi,
|
|
9
|
+
CheckpointImplementation,
|
|
10
|
+
CheckpointImplementationContext,
|
|
11
|
+
getCheckpointId,
|
|
12
|
+
getEventTimestamp,
|
|
13
|
+
StreamResumePosition
|
|
14
|
+
} from './CheckpointImplementation.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Standard MongoDB checkpoint implementation. The ordered LSN coordinate is the oplog
|
|
18
|
+
* clusterTime — unique per operation, monotonic, and parseable from resume
|
|
19
|
+
* tokens. Barriers and event LSNs are plain comparable LSN strings.
|
|
20
|
+
*/
|
|
21
|
+
export class TimestampCheckpointImplementation implements CheckpointImplementation {
|
|
22
|
+
readonly zeroLsn = MongoLSN.ZERO.comparable;
|
|
23
|
+
|
|
24
|
+
constructor(private context: CheckpointImplementationContext) {}
|
|
25
|
+
|
|
26
|
+
parseResumePosition(lsn: string): StreamResumePosition {
|
|
27
|
+
const parsed = MongoLSN.fromSerialized(lsn);
|
|
28
|
+
return { resumeAfter: parsed.resumeToken ?? null, startAfter: parsed.timestamp };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
seedPosition(_lsn: string | null): void {
|
|
32
|
+
// The coordinate comes from each event's clusterTime; no state to seed.
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logResume(lsn: string): void {
|
|
36
|
+
const parsed = MongoLSN.fromSerialized(lsn);
|
|
37
|
+
// It is normal for this to be a minute or two old when there is a low volume
|
|
38
|
+
// of ChangeStream events.
|
|
39
|
+
const tokenAgeSeconds = Math.round((Date.now() - timestampToDate(parsed.timestamp).getTime()) / 1000);
|
|
40
|
+
this.context.logger.info(
|
|
41
|
+
`Resume streaming at ${parsed.timestamp.inspect()} / ${parsed} | Token age: ${tokenAgeSeconds}s`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async createStandaloneCheckpoint(): Promise<string> {
|
|
46
|
+
return createCheckpoint(this.context.db, STANDALONE_CHECKPOINT_ID);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async createBatchCheckpoint(): Promise<string> {
|
|
50
|
+
return createCheckpoint(this.context.db, this.context.checkpointStreamId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createFirstBarrier(): Promise<string | null> {
|
|
54
|
+
// The barrier marker is a comparable LSN; resume the snapshot stream from it.
|
|
55
|
+
return this.createBatchCheckpoint();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async keepalive(batch: storage.BucketStorageBatch, resumeToken: mongo.ResumeToken): Promise<void> {
|
|
59
|
+
// Parse the timestamp from the resume token. The ordered LSN prefix
|
|
60
|
+
// advances together with the token, so persisting is always safe.
|
|
61
|
+
const { comparable: lsn, timestamp } = MongoLSN.fromResumeToken(resumeToken);
|
|
62
|
+
await batch.keepalive(lsn);
|
|
63
|
+
// Log the token update. This helps as a general "replication is still active" message in the logs.
|
|
64
|
+
// This token would typically be around 10s behind.
|
|
65
|
+
this.context.logger.info(
|
|
66
|
+
`Idle change stream. Persisted resumeToken for ${timestampToDate(timestamp).toISOString()}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lsnFromResumeToken(resumeToken: mongo.ResumeToken): string {
|
|
71
|
+
// The timestamp is embedded in the resume token.
|
|
72
|
+
return MongoLSN.fromResumeToken(resumeToken).comparable;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T> {
|
|
76
|
+
const session = this.context.client.startSession();
|
|
77
|
+
try {
|
|
78
|
+
await this.context.db.command({ hello: 1 }, { session });
|
|
79
|
+
const head = session.clusterTime?.clusterTime;
|
|
80
|
+
if (head == null) {
|
|
81
|
+
throw new ServiceAssertionError(`clusterTime not available for write checkpoint`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { response, shouldAdvance } = await callback(new MongoLSN({ timestamp: head }).comparable);
|
|
85
|
+
|
|
86
|
+
if (shouldAdvance) {
|
|
87
|
+
// Trigger a change on the changestream, so that the write checkpoint
|
|
88
|
+
// is processed without waiting for other writes.
|
|
89
|
+
await this.context.db.collection(CHECKPOINTS_COLLECTION).findOneAndUpdate(
|
|
90
|
+
{
|
|
91
|
+
_id: STANDALONE_CHECKPOINT_ID as any
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
$inc: { i: 1 }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
upsert: true,
|
|
98
|
+
returnDocument: 'after',
|
|
99
|
+
session
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
const time = session.operationTime!;
|
|
103
|
+
if (time == null) {
|
|
104
|
+
throw new ServiceAssertionError(`operationTime not available for write checkpoint`);
|
|
105
|
+
} else if (time.lt(head)) {
|
|
106
|
+
throw new ServiceAssertionError(`operationTime must be > clusterTime`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return response;
|
|
111
|
+
} finally {
|
|
112
|
+
await session.endSession();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
readonly event: CheckpointEventApi = {
|
|
117
|
+
observe: (doc) => {
|
|
118
|
+
const checkpointId = getCheckpointId(doc);
|
|
119
|
+
if (checkpointId == null || checkpointId == SENTINEL_CHECKPOINT_ID) {
|
|
120
|
+
return 'foreign';
|
|
121
|
+
}
|
|
122
|
+
// The STANDALONE_CHECKPOINT_ID is only used for the TimestampCheckpointImplementation
|
|
123
|
+
if (checkpointId == STANDALONE_CHECKPOINT_ID) {
|
|
124
|
+
return 'standalone';
|
|
125
|
+
}
|
|
126
|
+
return this.context.checkpointStreamId.equals(checkpointId) ? 'own-barrier' : 'foreign';
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
lsn: (doc) => {
|
|
130
|
+
return new MongoLSN({
|
|
131
|
+
timestamp: getEventTimestamp(doc),
|
|
132
|
+
resume_token: doc._id
|
|
133
|
+
}).comparable;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
resolvesBarrier: (marker, doc) => {
|
|
137
|
+
// Barrier markers are comparable LSNs in this implementation.
|
|
138
|
+
return this.event.lsn(doc) >= marker;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// It's safe to clear the entire _powersync_checkpoints collection in this mode.
|
|
143
|
+
readonly checkpointClearFilter: mongo.Filter<mongo.Document> = {};
|
|
144
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CheckpointImplementation, CheckpointImplementationContext } from './CheckpointImplementation.js';
|
|
2
|
+
import { SentinelCheckpointImplementation } from './SentinelCheckpointImplementation.js';
|
|
3
|
+
import { TimestampCheckpointImplementation } from './TimestampCheckpointImplementation.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Select the checkpoint implementation for a source: sentinel-based for DocumentDB
|
|
7
|
+
* DB, clusterTime-based for standard MongoDB.
|
|
8
|
+
*/
|
|
9
|
+
export function createCheckpointImplementation(
|
|
10
|
+
isDocumentDb: boolean,
|
|
11
|
+
context: CheckpointImplementationContext
|
|
12
|
+
): CheckpointImplementation {
|
|
13
|
+
return isDocumentDb ? new SentinelCheckpointImplementation(context) : new TimestampCheckpointImplementation(context);
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
1
2
|
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
2
3
|
import * as bson from 'bson';
|
|
3
4
|
import { PostImagesOption } from '../types/types.js';
|
|
@@ -7,16 +8,36 @@ export const CHECKPOINTS_COLLECTION = '_powersync_checkpoints';
|
|
|
7
8
|
|
|
8
9
|
const REQUIRED_CHECKPOINT_PERMISSIONS = ['find', 'insert', 'update', 'remove', 'changeStream', 'createCollection'];
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Whether a `hello` response indicates Azure DocumentDB (formerly Azure Cosmos
|
|
13
|
+
* DB for MongoDB vCore), which reports `documentdb_versions` in the `internal`
|
|
14
|
+
* section.
|
|
15
|
+
*/
|
|
16
|
+
function isDocumentDbHello(hello: mongo.Document): boolean {
|
|
17
|
+
return hello.internal?.documentdb_versions != null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect whether the connected server is DocumentDB. DocumentDB lacks usable
|
|
22
|
+
* clusterTime/operationTime and uses the sentinel checkpoint implementation.
|
|
23
|
+
*/
|
|
24
|
+
export async function detectDocumentDb(db: mongo.Db): Promise<boolean> {
|
|
25
|
+
const hello = await db.command({ hello: 1 });
|
|
26
|
+
return isDocumentDbHello(hello);
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
export async function checkSourceConfiguration(connectionManager: MongoManager): Promise<void> {
|
|
11
30
|
const db = connectionManager.db;
|
|
12
31
|
|
|
13
32
|
const hello = await db.command({ hello: 1 });
|
|
14
|
-
|
|
33
|
+
const isDocumentDb = isDocumentDbHello(hello);
|
|
34
|
+
|
|
35
|
+
if (hello.msg == 'isdbgrid' && !isDocumentDb) {
|
|
15
36
|
throw new ServiceError(
|
|
16
37
|
ErrorCode.PSYNC_S1341,
|
|
17
38
|
'Sharded MongoDB Clusters are not supported yet (including MongoDB Serverless instances).'
|
|
18
39
|
);
|
|
19
|
-
} else if (hello.setName == null) {
|
|
40
|
+
} else if (hello.setName == null && !isDocumentDb) {
|
|
20
41
|
throw new ServiceError(ErrorCode.PSYNC_S1342, 'Standalone MongoDB instances are not supported - use a replicaset.');
|
|
21
42
|
}
|
|
22
43
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Running Tests Against DocumentDB
|
|
2
|
+
|
|
3
|
+
These instructions cover running the `module-mongodb` test suite against an Azure DocumentDB (formerly Cosmos DB for MongoDB vCore) cluster.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- An Azure DocumentDB (Cosmos DB for MongoDB vCore) cluster with change stream support
|
|
8
|
+
- Local PostgreSQL for PowerSync's internal storage (not the source database)
|
|
9
|
+
- The connection URI for the DocumentDB cluster
|
|
10
|
+
|
|
11
|
+
> **The open-source `documentdb-local` Docker image cannot be used for these tests.** The
|
|
12
|
+
> open-source DocumentDB engine does not implement change streams (`$changeStream is not
|
|
13
|
+
supported yet in native pipeline`), does not report the `documentdb_versions` `hello` field
|
|
14
|
+
> the suite uses for detection, and presents as `msg: isdbgrid`. An Azure-managed DocumentDB
|
|
15
|
+
> (vCore) cluster is required.
|
|
16
|
+
|
|
17
|
+
## Environment Variables
|
|
18
|
+
|
|
19
|
+
DocumentDB is detected automatically from the server: the test suite runs
|
|
20
|
+
`detectDocumentDb()` once at startup (see `DatabaseType.ts`) and gates the
|
|
21
|
+
DocumentDB-specific tests on the result. There is no separate enable flag — pointing
|
|
22
|
+
`MONGO_TEST_DATA_URL` at a DocumentDB cluster is what activates the DocumentDB tests.
|
|
23
|
+
|
|
24
|
+
| Variable | Required | Description |
|
|
25
|
+
| ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
26
|
+
| `MONGO_TEST_DATA_URL` | Yes | DocumentDB connection URI. Must include a database name in the path (see below). Pointing this at a DocumentDB cluster enables the DocumentDB tests. |
|
|
27
|
+
| `PG_STORAGE_TEST_URL` | No | PostgreSQL connection for PowerSync storage. Defaults to `postgres://postgres:postgres@localhost:5432/powersync_storage_test`. |
|
|
28
|
+
| `TEST_MONGO_STORAGE` | No | Set to `false` to skip MongoDB storage tests. Recommended when testing against DocumentDB to avoid using it as a storage backend. |
|
|
29
|
+
| `TEST_TIMEOUT_MULTIPLIER` | No | Scale factor `testTimeout()` applies to a default timeout on DocumentDB when no explicit cloud override is given. Defaults to **6**. Increase for a slow/remote cluster. |
|
|
30
|
+
|
|
31
|
+
### Connection URI format
|
|
32
|
+
|
|
33
|
+
The `MONGO_TEST_DATA_URL` must include a database name in the path. DocumentDB URIs typically don't have one, so you need to add it before the query string:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
# Original URI (no database):
|
|
37
|
+
mongodb+srv://user:pass@cluster.mongocluster.cosmos.azure.com/
|
|
38
|
+
|
|
39
|
+
# With database added:
|
|
40
|
+
mongodb+srv://user:pass@cluster.mongocluster.cosmos.azure.com/powersync_test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If your password contains special characters (`=`, `@`, `+`, `/`), they must be URL-encoded in the URI (e.g., `=` becomes `%3D`). DocumentDB auto-generated passwords often contain `=` (base64).
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
All commands run from the module directory: `modules/module-mongodb/`
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Run all DocumentDB tests (integration + unit helpers):
|
|
51
|
+
MONGO_TEST_DATA_URL="mongodb+srv://user:pass@cluster.mongocluster.cosmos.azure.com/powersync_test" \
|
|
52
|
+
TEST_MONGO_STORAGE=false \
|
|
53
|
+
npx vitest run documentdb --reporter=verbose
|
|
54
|
+
|
|
55
|
+
# Run only integration tests:
|
|
56
|
+
MONGO_TEST_DATA_URL="<uri>" \
|
|
57
|
+
TEST_MONGO_STORAGE=false \
|
|
58
|
+
npx vitest run documentdb_mode --reporter=verbose
|
|
59
|
+
|
|
60
|
+
# Run only unit helper tests (no DocumentDB cluster needed):
|
|
61
|
+
npx vitest run documentdb_helpers --reporter=verbose
|
|
62
|
+
|
|
63
|
+
# Run a specific test by name:
|
|
64
|
+
MONGO_TEST_DATA_URL="<uri>" \
|
|
65
|
+
TEST_MONGO_STORAGE=false \
|
|
66
|
+
npx vitest run documentdb_mode -t "resume after restart" --reporter=verbose
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If you have the URI in an environment variable (e.g., `$DOCUMENTDB_URI`), you can construct the test URL inline:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
DOCUMENTDB_TEST_URL=$(echo "$DOCUMENTDB_URI" | sed 's|\?|powersync_test?|')
|
|
73
|
+
MONGO_TEST_DATA_URL="$DOCUMENTDB_TEST_URL" \
|
|
74
|
+
TEST_MONGO_STORAGE=false \
|
|
75
|
+
npx vitest run documentdb --reporter=verbose
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## GitHub Actions
|
|
79
|
+
|
|
80
|
+
The `.github/workflows/documentdb-integration.yml` workflow runs these tests manually via `workflow_dispatch` only. Add a repository or organization secret named `AZURE_DOCUMENTDB_TEST_DATA_URL`, then dispatch the workflow. The workflow maps that secret to the test suite's `MONGO_TEST_DATA_URL` environment variable.
|
|
81
|
+
|
|
82
|
+
The URI must include a database name in the path. The tests clear/drop this database as part of setup, so use a dedicated test database and cluster.
|
|
83
|
+
|
|
84
|
+
The workflow starts a local MongoDB storage service, then runs the complete `modules/module-mongodb` `test` script against that storage backend. `MONGO_TEST_DATA_URL` is the remote DocumentDB source; `MONGO_TEST_URL` points at the local MongoDB storage test instance.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
TEST_MONGO_STORAGE=true
|
|
88
|
+
TEST_POSTGRES_STORAGE=false
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Test Files
|
|
92
|
+
|
|
93
|
+
| File | Requires DocumentDB | Description |
|
|
94
|
+
| ---------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
95
|
+
| `documentdb_mode.test.ts` | Yes | Integration tests: replication, sentinel checkpoints, write checkpoints, keepalive, resume. Skipped automatically unless `MONGO_TEST_DATA_URL` points at a DocumentDB cluster. |
|
|
96
|
+
| `documentdb_helpers.test.ts` | No (1 test needs MongoDB) | Unit tests: `getEventTimestamp`, sentinel parsing/matching, detection logic. Runs against any MongoDB or standalone. |
|
|
97
|
+
|
|
98
|
+
## What the Integration Tests Cover
|
|
99
|
+
|
|
100
|
+
Each integration test runs against 3 storage versions (v1, v2, v3) = 15 integration tests. Plus 15 unit tests in helpers = 30 total.
|
|
101
|
+
|
|
102
|
+
| Test | What it validates |
|
|
103
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------- |
|
|
104
|
+
| basic replication | Insert, update, delete through change stream with wallTime timestamps |
|
|
105
|
+
| sentinel checkpoint | Checkpoint created with `mode: 'sentinel'`, resolved by matching document content in the stream |
|
|
106
|
+
| keepalive | Stream idles past the keepalive interval without crashing on DocumentDB resume tokens |
|
|
107
|
+
| write checkpoint | Full `createReplicationHead` → sentinel → polling flow for client write consistency |
|
|
108
|
+
| data events not dropped | Verifies `.lte()` dedup guard is skipped — events in the same wall-clock second are not lost |
|
|
109
|
+
| resume after restart | Stop streaming, create new context, resume from stored token |
|
|
110
|
+
|
|
111
|
+
There is also a **characterization test**, `does not report collection drop and rename events`,
|
|
112
|
+
that documents the current limitation: it writes DDL plus a post-DDL marker, waits for the marker
|
|
113
|
+
(proving the stream caught up), then asserts no `drop` / `rename` events were delivered. It passes
|
|
114
|
+
today and **fails if a future DocumentDB engine starts delivering DDL events** — a signal to add
|
|
115
|
+
real drop/rename replication support.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { detectDocumentDb } from '@module/replication/replication-utils.js';
|
|
2
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
3
|
+
import { connectMongoData } from './util.js';
|
|
4
|
+
|
|
5
|
+
export enum DatabaseType {
|
|
6
|
+
DOCUMENTDB = 'DOCUMENTDB',
|
|
7
|
+
MONGODB = 'MONGODB'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let _databaseType: DatabaseType = DatabaseType.MONGODB;
|
|
11
|
+
|
|
12
|
+
// Detected once at import time. Close the client afterwards so this detection
|
|
13
|
+
// does not leak a connection: this module is imported by many test files, and
|
|
14
|
+
// the client created here is otherwise never closed, accumulating open handles
|
|
15
|
+
// in Vitest.
|
|
16
|
+
const { client, db } = await connectMongoData();
|
|
17
|
+
try {
|
|
18
|
+
_databaseType = (await detectDocumentDb(db)) ? DatabaseType.DOCUMENTDB : DatabaseType.MONGODB;
|
|
19
|
+
} catch (ex) {
|
|
20
|
+
logger.warn(`Could not determine MongoDB database type`, ex);
|
|
21
|
+
} finally {
|
|
22
|
+
await client.close().catch(() => {});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DATABASE_TYPE = _databaseType;
|