@powersync/service-module-mongodb 0.18.2 → 0.19.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.
- package/CHANGELOG.md +16 -0
- package/dist/api/MongoRouteAPIAdapter.d.ts +2 -0
- 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 +15 -0
- package/dist/replication/ChangeStream.js +105 -44
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/MongoRelation.d.ts +36 -1
- package/dist/replication/MongoRelation.js +102 -6
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/MongoSnapshotter.d.ts +9 -0
- package/dist/replication/MongoSnapshotter.js +108 -42
- package/dist/replication/MongoSnapshotter.js.map +1 -1
- package/dist/replication/RawChangeStream.d.ts +12 -1
- package/dist/replication/RawChangeStream.js +30 -7
- 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 +222 -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 +113 -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/package.json +8 -8
- package/src/api/MongoRouteAPIAdapter.ts +26 -37
- package/src/common/SentinelLSN.ts +78 -0
- package/src/replication/ChangeStream.ts +118 -50
- package/src/replication/MongoRelation.ts +124 -14
- package/src/replication/MongoSnapshotter.ts +122 -47
- package/src/replication/RawChangeStream.ts +64 -28
- package/src/replication/checkpoints/CheckpointImplementation.ts +167 -0
- package/src/replication/checkpoints/SentinelCheckpointImplementation.ts +267 -0
- package/src/replication/checkpoints/TimestampCheckpointImplementation.ts +142 -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 +97 -65
- package/test/src/change_stream_utils.ts +83 -7
- package/test/src/checkpoint_retry.test.ts +5 -2
- 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_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 +1 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,8 +12,13 @@ import { parseDocumentId } from '@module/replication/bufferToSqlite.js';
|
|
|
12
12
|
import { ChangeStreamBatch, parseChangeDocument, rawChangeStream } from '@module/replication/RawChangeStream.js';
|
|
13
13
|
import { DirectSourceRowConverter } from '@module/replication/SourceRowConverter.js';
|
|
14
14
|
import { PostImagesOption } from '@module/types/types.js';
|
|
15
|
+
import { DATABASE_TYPE, DatabaseType } from './DatabaseType.js';
|
|
15
16
|
import { clearTestDb, connectMongoData, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
16
17
|
|
|
18
|
+
// DocumentDB does not support the (deprecated) mapReduce command, which
|
|
19
|
+
// insertUndefined() relies on to produce BSON `undefined` values. It also does
|
|
20
|
+
// not support database-level change streams (production always uses
|
|
21
|
+
// cluster-level streams on DocumentDB). Tests depending on either are skipped.
|
|
17
22
|
describe('mongo data types', () => {
|
|
18
23
|
// These test the full data cycle by writing to mongodb, then checking the change stream and direct collection queries.
|
|
19
24
|
// More direct tests directly on the BSON values are in buffer_to_sqlite.test.ts.
|
|
@@ -244,7 +249,8 @@ describe('mongo data types', () => {
|
|
|
244
249
|
});
|
|
245
250
|
}
|
|
246
251
|
|
|
247
|
-
|
|
252
|
+
// DocumentDB: requires mapReduce (insertUndefined)
|
|
253
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('test direct queries', async () => {
|
|
248
254
|
const { db, client } = await connectMongoData();
|
|
249
255
|
|
|
250
256
|
const collection = db.collection('test_data');
|
|
@@ -265,7 +271,8 @@ describe('mongo data types', () => {
|
|
|
265
271
|
}
|
|
266
272
|
});
|
|
267
273
|
|
|
268
|
-
|
|
274
|
+
// DocumentDB: requires mapReduce (insertUndefined)
|
|
275
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('test direct queries - arrays', async () => {
|
|
269
276
|
const { db, client } = await connectMongoData();
|
|
270
277
|
const collection = db.collection('test_data_arrays');
|
|
271
278
|
try {
|
|
@@ -286,7 +293,8 @@ describe('mongo data types', () => {
|
|
|
286
293
|
}
|
|
287
294
|
});
|
|
288
295
|
|
|
289
|
-
|
|
296
|
+
// DocumentDB: requires a database-level change stream and mapReduce
|
|
297
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('test replication', async () => {
|
|
290
298
|
// With MongoDB, replication uses the exact same document format
|
|
291
299
|
// as normal queries. We test it anyway.
|
|
292
300
|
const { db, client } = await connectMongoData();
|
|
@@ -311,7 +319,8 @@ describe('mongo data types', () => {
|
|
|
311
319
|
}
|
|
312
320
|
});
|
|
313
321
|
|
|
314
|
-
|
|
322
|
+
// DocumentDB: requires a database-level change stream and mapReduce
|
|
323
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('test replication - arrays', async () => {
|
|
315
324
|
const { db, client } = await connectMongoData();
|
|
316
325
|
const collection = db.collection('test_data');
|
|
317
326
|
try {
|
|
@@ -334,7 +343,8 @@ describe('mongo data types', () => {
|
|
|
334
343
|
}
|
|
335
344
|
});
|
|
336
345
|
|
|
337
|
-
|
|
346
|
+
// DocumentDB: requires mapReduce (insertUndefined)
|
|
347
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('connection schema', async () => {
|
|
338
348
|
await using adapter = new MongoRouteAPIAdapter({
|
|
339
349
|
type: 'mongodb',
|
|
340
350
|
...TEST_CONNECTION_OPTIONS
|
|
@@ -4,19 +4,28 @@ import { ChangeStreamBatch, namespaceCollection, rawChangeStream } from '@module
|
|
|
4
4
|
import { getCursorBatchBytes } from '@module/replication/replication-index.js';
|
|
5
5
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
6
6
|
import { bson } from '@powersync/service-core';
|
|
7
|
+
import { DATABASE_TYPE, DatabaseType } from './DatabaseType.js';
|
|
8
|
+
import { testTimeout } from './test-timeouts.js';
|
|
7
9
|
import { clearTestDb, connectMongoData, requireFailCommand } from './util.js';
|
|
8
10
|
|
|
11
|
+
// DocumentDB only supports cluster-level change streams — collection- and
|
|
12
|
+
// database-level streams fail with NamespaceNotFound ("Collection not found"),
|
|
13
|
+
// which is why production always uses cluster-level streams on DocumentDB.
|
|
14
|
+
// The cluster-level size tracking test is also skipped: its strict batching
|
|
15
|
+
// assertions (exact batch counts, immediate event delivery per readAll pass)
|
|
16
|
+
// are racy against a remote DocumentDB cluster with multi-second event latency.
|
|
17
|
+
// Byte tracking on DocumentDB is exercised indirectly by documentdb_mode.test.ts.
|
|
9
18
|
describe('internal mongodb utils', () => {
|
|
10
19
|
// The implementation relies on internal APIs, so we verify this works as expected for various types of change streams.
|
|
11
|
-
test('collection change stream size tracking', async () => {
|
|
20
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('collection change stream size tracking', async () => {
|
|
12
21
|
await testChangeStreamBsonBytes('collection');
|
|
13
22
|
});
|
|
14
23
|
|
|
15
|
-
test('db change stream size tracking', async () => {
|
|
24
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('db change stream size tracking', async () => {
|
|
16
25
|
await testChangeStreamBsonBytes('db');
|
|
17
26
|
});
|
|
18
27
|
|
|
19
|
-
test('cluster change stream size tracking', async () => {
|
|
28
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('cluster change stream size tracking', async () => {
|
|
20
29
|
await testChangeStreamBsonBytes('cluster');
|
|
21
30
|
});
|
|
22
31
|
|
|
@@ -44,144 +53,215 @@ describe('internal mongodb utils', () => {
|
|
|
44
53
|
expect(totalBytes).toBeLessThan(1200);
|
|
45
54
|
});
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// DocumentDB does not support database-level change streams (rawChangeStream on a db); only cluster-level.
|
|
57
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)(
|
|
58
|
+
'uses separate aggregate and getMore command options',
|
|
59
|
+
async () => {
|
|
60
|
+
const { db, client } = await connectMongoData({ monitorCommands: true });
|
|
61
|
+
await using _ = { [Symbol.asyncDispose]: async () => await client.close() };
|
|
62
|
+
await clearTestDb(db);
|
|
63
|
+
const collection = db.collection('test_data');
|
|
64
|
+
|
|
65
|
+
const started: any[] = [];
|
|
66
|
+
client.on('commandStarted', (event) => {
|
|
67
|
+
if (event.commandName == 'aggregate' || event.commandName == 'getMore') {
|
|
68
|
+
started.push(event);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
const stream = rawChangeStream(
|
|
73
|
+
db,
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
$changeStream: {
|
|
77
|
+
fullDocument: 'updateLookup'
|
|
78
|
+
}
|
|
66
79
|
}
|
|
80
|
+
],
|
|
81
|
+
{
|
|
82
|
+
batchSize: 10,
|
|
83
|
+
maxAwaitTimeMS: 50,
|
|
84
|
+
maxTimeMS: 1_000
|
|
67
85
|
}
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
batchSize: 10,
|
|
71
|
-
maxAwaitTimeMS: 50,
|
|
72
|
-
maxTimeMS: 1_000
|
|
73
|
-
}
|
|
74
|
-
);
|
|
86
|
+
);
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
await stream.next();
|
|
89
|
+
await collection.insertOne({ test: 1 });
|
|
90
|
+
const nextBatch = await readUntilNonEmptyBatch(stream);
|
|
91
|
+
await stream.return?.();
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
expect(nextBatch.events).toHaveLength(1);
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
const aggregate = started.find((event) => event.commandName == 'aggregate');
|
|
96
|
+
const getMore = started.find((event) => event.commandName == 'getMore');
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
expect(aggregate?.command.cursor?.batchSize).toEqual(1);
|
|
99
|
+
expect(aggregate?.command.maxTimeMS).toEqual(1_000);
|
|
100
|
+
expect(getMore?.command.batchSize).toEqual(10);
|
|
101
|
+
expect(getMore?.command.maxTimeMS).toEqual(50);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
test(
|
|
106
|
+
'keeps getMore maxTimeMS when client-side maxAwaitTimeMS is enabled',
|
|
107
|
+
{ timeout: testTimeout(30_000, { cloudOverride: 150_000 }) },
|
|
108
|
+
async () => {
|
|
109
|
+
const { db, client } = await connectMongoData({ monitorCommands: true });
|
|
110
|
+
await using _ = { [Symbol.asyncDispose]: async () => await client.close() };
|
|
111
|
+
await clearTestDb(db);
|
|
112
|
+
const collection = db.collection('test_data');
|
|
113
|
+
// Keep the local Mongo path fast, but give Azure DocumentDB enough time
|
|
114
|
+
// for slow cloud change-stream delivery when maxTimeMS is sent to getMore.
|
|
115
|
+
const maxAwaitTimeMS = testTimeout(50, { cloudOverride: 10_000 });
|
|
116
|
+
|
|
117
|
+
const started: any[] = [];
|
|
118
|
+
client.on('commandStarted', (event) => {
|
|
119
|
+
if (event.commandName == 'aggregate' || event.commandName == 'getMore') {
|
|
120
|
+
started.push(event);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
91
123
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
const stream = rawChangeStream(
|
|
125
|
+
DATABASE_TYPE == DatabaseType.DOCUMENTDB ? client.db('admin') : db,
|
|
126
|
+
[
|
|
127
|
+
{
|
|
128
|
+
$changeStream: {
|
|
129
|
+
fullDocument: 'updateLookup',
|
|
130
|
+
...(DATABASE_TYPE == DatabaseType.DOCUMENTDB ? { allChangesForCluster: true } : {})
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
...(DATABASE_TYPE == DatabaseType.DOCUMENTDB
|
|
134
|
+
? [
|
|
135
|
+
{
|
|
136
|
+
$match: {
|
|
137
|
+
'ns.db': db.databaseName,
|
|
138
|
+
'ns.coll': collection.collectionName
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
: [])
|
|
143
|
+
],
|
|
144
|
+
{
|
|
145
|
+
batchSize: 10,
|
|
146
|
+
maxAwaitTimeMS,
|
|
147
|
+
clientSideMaxAwaitTimeMS: true,
|
|
148
|
+
maxTimeMS: 1_000
|
|
149
|
+
}
|
|
150
|
+
);
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
await stream.next();
|
|
153
|
+
await collection.insertOne({ test: 1 });
|
|
154
|
+
const nextBatch = await readUntilNonEmptyBatch(stream);
|
|
155
|
+
await stream.return?.();
|
|
99
156
|
|
|
100
|
-
|
|
101
|
-
client.on('commandStarted', (event) => {
|
|
102
|
-
if (event.commandName == 'aggregate' || event.commandName == 'getMore') {
|
|
103
|
-
started.push(event);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
157
|
+
expect(nextBatch.events).toHaveLength(1);
|
|
106
158
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
159
|
+
const aggregate = started.find((event) => event.commandName == 'aggregate');
|
|
160
|
+
const getMore = started.find((event) => event.commandName == 'getMore');
|
|
161
|
+
|
|
162
|
+
expect(aggregate?.command.maxTimeMS).toEqual(1_000);
|
|
163
|
+
expect(getMore?.command.maxTimeMS).toEqual(maxAwaitTimeMS);
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// This test uses configureFailPoint to inject a getMore timeout. Azure
|
|
168
|
+
// DocumentDB does not support configureFailPoint.
|
|
169
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)(
|
|
170
|
+
'should resume on MaxTimeMSExpired from getMore',
|
|
171
|
+
async (ctx) => {
|
|
172
|
+
const { db, client } = await connectMongoData({ monitorCommands: true });
|
|
173
|
+
await using _ = { [Symbol.asyncDispose]: async () => await client.close() };
|
|
174
|
+
await using failCommand = await requireFailCommand(client, ctx);
|
|
175
|
+
|
|
176
|
+
await clearTestDb(db);
|
|
177
|
+
const collection = db.collection('test_data');
|
|
178
|
+
|
|
179
|
+
const started: any[] = [];
|
|
180
|
+
client.on('commandStarted', (event) => {
|
|
181
|
+
if (event.commandName == 'aggregate' || event.commandName == 'getMore') {
|
|
182
|
+
started.push(event);
|
|
114
183
|
}
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
batchSize: 3,
|
|
118
|
-
maxAwaitTimeMS: 50,
|
|
119
|
-
maxTimeMS: 1_000
|
|
120
|
-
// To get more details when debugging, enable the logger:
|
|
121
|
-
// logger: logger
|
|
122
|
-
}
|
|
123
|
-
);
|
|
184
|
+
});
|
|
124
185
|
|
|
125
|
-
|
|
186
|
+
const stream = rawChangeStream(
|
|
187
|
+
db,
|
|
188
|
+
[
|
|
189
|
+
{
|
|
190
|
+
$changeStream: {
|
|
191
|
+
fullDocument: 'updateLookup'
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
{
|
|
196
|
+
batchSize: 3,
|
|
197
|
+
maxAwaitTimeMS: 50,
|
|
198
|
+
maxTimeMS: 1_000
|
|
199
|
+
// To get more details when debugging, enable the logger:
|
|
200
|
+
// logger: logger
|
|
201
|
+
}
|
|
202
|
+
);
|
|
126
203
|
|
|
127
|
-
|
|
128
|
-
mode: { times: 1 },
|
|
129
|
-
data: {
|
|
130
|
-
failCommands: ['getMore'],
|
|
131
|
-
errorCode: 50 // MaxTimeMSExpired
|
|
132
|
-
}
|
|
133
|
-
});
|
|
204
|
+
await stream.next();
|
|
134
205
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
206
|
+
await failCommand.configure({
|
|
207
|
+
mode: { times: 1 },
|
|
208
|
+
data: {
|
|
209
|
+
failCommands: ['getMore'],
|
|
210
|
+
errorCode: 50 // MaxTimeMSExpired
|
|
211
|
+
}
|
|
212
|
+
});
|
|
138
213
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
143
|
-
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
144
|
-
{ test: 1 }
|
|
145
|
-
]);
|
|
146
|
-
}
|
|
147
|
-
{
|
|
148
|
-
// This will be a getMore with batchSize 2
|
|
149
|
-
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
150
|
-
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
151
|
-
{ test: 2 },
|
|
152
|
-
{ test: 3 }
|
|
153
|
-
]);
|
|
154
|
-
}
|
|
155
|
-
{
|
|
156
|
-
// At this point, this batch size is at the original size of 3 again.
|
|
157
|
-
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
158
|
-
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
159
|
-
{ test: 4 },
|
|
160
|
-
{ test: 5 },
|
|
161
|
-
{ test: 6 }
|
|
162
|
-
]);
|
|
163
|
-
}
|
|
164
|
-
{
|
|
165
|
-
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
166
|
-
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
167
|
-
{ test: 7 },
|
|
168
|
-
{ test: 8 }
|
|
169
|
-
]);
|
|
170
|
-
}
|
|
214
|
+
for (let i = 1; i <= 8; i++) {
|
|
215
|
+
await collection.insertOne({ test: i });
|
|
216
|
+
}
|
|
171
217
|
|
|
172
|
-
|
|
173
|
-
expect(aggregateCommands.length).toBeGreaterThanOrEqual(2);
|
|
174
|
-
expect(aggregateCommands[0].command.pipeline).toEqual([
|
|
218
|
+
// Test the exponentially-increasing batch size after the retry
|
|
175
219
|
{
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
220
|
+
// This will fail the getMore, then retry with aggregate with batchSize 1
|
|
221
|
+
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
222
|
+
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
223
|
+
{ test: 1 }
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
{
|
|
227
|
+
// This will be a getMore with batchSize 2
|
|
228
|
+
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
229
|
+
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
230
|
+
{ test: 2 },
|
|
231
|
+
{ test: 3 }
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
{
|
|
235
|
+
// At this point, this batch size is at the original size of 3 again.
|
|
236
|
+
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
237
|
+
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
238
|
+
{ test: 4 },
|
|
239
|
+
{ test: 5 },
|
|
240
|
+
{ test: 6 }
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
{
|
|
244
|
+
const batch = await readUntilNonEmptyBatch(stream, 10);
|
|
245
|
+
expect(batch.events.map((e) => bson.deserialize(e, { useBigInt64: true }).fullDocument)).toMatchObject([
|
|
246
|
+
{ test: 7 },
|
|
247
|
+
{ test: 8 }
|
|
248
|
+
]);
|
|
179
249
|
}
|
|
180
|
-
]);
|
|
181
|
-
expect(aggregateCommands[1].command.pipeline[0]?.$changeStream?.resumeAfter).toBeDefined();
|
|
182
250
|
|
|
183
|
-
|
|
184
|
-
|
|
251
|
+
const aggregateCommands = started.filter((event) => event.commandName == 'aggregate');
|
|
252
|
+
expect(aggregateCommands.length).toBeGreaterThanOrEqual(2);
|
|
253
|
+
expect(aggregateCommands[0].command.pipeline).toEqual([
|
|
254
|
+
{
|
|
255
|
+
$changeStream: {
|
|
256
|
+
fullDocument: 'updateLookup'
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
]);
|
|
260
|
+
expect(aggregateCommands[1].command.pipeline[0]?.$changeStream?.resumeAfter).toBeDefined();
|
|
261
|
+
|
|
262
|
+
await stream?.return().catch(() => {});
|
|
263
|
+
}
|
|
264
|
+
);
|
|
185
265
|
|
|
186
266
|
async function testChangeStreamBsonBytes(type: 'db' | 'collection' | 'cluster') {
|
|
187
267
|
const { db, client } = await connectMongoData();
|
|
@@ -260,7 +340,8 @@ describe('internal mongodb utils', () => {
|
|
|
260
340
|
expect(totalBytes).toBeLessThan(8000);
|
|
261
341
|
}
|
|
262
342
|
|
|
263
|
-
|
|
343
|
+
// DocumentDB: database-level change streams and $currentOp are not supported
|
|
344
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('should resume on missing cursor (1)', async () => {
|
|
264
345
|
// Many resumable errors are difficult to simulate, but CursorNotFound is easy.
|
|
265
346
|
|
|
266
347
|
const { db, client } = await connectMongoData();
|
|
@@ -313,7 +394,8 @@ describe('internal mongodb utils', () => {
|
|
|
313
394
|
expect(readDocs.map((doc) => doc.fullDocument)).toMatchObject([{ test: 1 }, { test: 2 }, { test: 3 }, { test: 4 }]);
|
|
314
395
|
});
|
|
315
396
|
|
|
316
|
-
|
|
397
|
+
// DocumentDB: database-level change streams and $currentOp are not supported
|
|
398
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('should resume on missing cursor (2)', async () => {
|
|
317
399
|
const { db, client } = await connectMongoData();
|
|
318
400
|
await using _ = { [Symbol.asyncDispose]: async () => await client.close() };
|
|
319
401
|
await clearTestDb(db);
|
|
@@ -387,7 +469,8 @@ describe('internal mongodb utils', () => {
|
|
|
387
469
|
expect(readDocs.map((doc) => doc.fullDocument)).toMatchObject([{ test: 1 }, { test: 2 }, { test: 3 }, { test: 4 }]);
|
|
388
470
|
});
|
|
389
471
|
|
|
390
|
-
|
|
472
|
+
// DocumentDB: database-level change streams are not supported
|
|
473
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('should cleanly abort a stream between events', async () => {
|
|
391
474
|
const { db, client } = await connectMongoData();
|
|
392
475
|
const abortController = new AbortController();
|
|
393
476
|
await using _ = { [Symbol.asyncDispose]: async () => await client.close() };
|
|
@@ -437,7 +520,8 @@ describe('internal mongodb utils', () => {
|
|
|
437
520
|
expect(readDocs.map((doc) => doc.fullDocument)).toMatchObject([{ test: 1 }, { test: 2 }]);
|
|
438
521
|
});
|
|
439
522
|
|
|
440
|
-
|
|
523
|
+
// DocumentDB: database-level change streams are not supported
|
|
524
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('should cleanly abort a stream in an event', async () => {
|
|
441
525
|
const { db, client } = await connectMongoData();
|
|
442
526
|
const abortController = new AbortController();
|
|
443
527
|
await using _ = { [Symbol.asyncDispose]: async () => await client.close() };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MongoLSN, parseResumeTokenTimestamp, ZERO_LSN } from '@module/common/MongoLSN.js';
|
|
2
|
+
import { SentinelLSN } from '@module/common/SentinelLSN.js';
|
|
2
3
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
3
4
|
import { describe, expect, it, test } from 'vitest';
|
|
4
5
|
|
|
@@ -109,3 +110,32 @@ describe('mongo lsn', () => {
|
|
|
109
110
|
).toBe(true);
|
|
110
111
|
});
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
describe('documentdb lsn', () => {
|
|
115
|
+
test('uses sentinel as the comparable prefix', () => {
|
|
116
|
+
expect(new SentinelLSN({ sentinel: 9n }).comparable < new SentinelLSN({ sentinel: 10n }).comparable).toBe(true);
|
|
117
|
+
expect(new SentinelLSN({ sentinel: 10n }).comparable > new SentinelLSN({ sentinel: 9n }).comparable).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('resume token round-trips without changing the sentinel', () => {
|
|
121
|
+
const lsn = new SentinelLSN({
|
|
122
|
+
sentinel: 42n,
|
|
123
|
+
resume_token: { _data: 'MzowOjM1MjY4MTIwOTEyOjA6MzUyNjgxMjA5MTI6MTowOg==' }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const parsed = SentinelLSN.fromSerialized(lsn.comparable);
|
|
127
|
+
|
|
128
|
+
expect(parsed.sentinel).toEqual(42n);
|
|
129
|
+
expect(parsed.resumeToken).toEqual({ _data: 'MzowOjM1MjY4MTIwOTEyOjA6MzUyNjgxMjA5MTI6MTowOg==' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('write checkpoint LSN without resume token compares below observed stream LSN for the same sentinel', () => {
|
|
133
|
+
const writeCheckpointHead = new SentinelLSN({ sentinel: 42n }).comparable;
|
|
134
|
+
const observedStreamHead = new SentinelLSN({
|
|
135
|
+
sentinel: 42n,
|
|
136
|
+
resume_token: { _data: 'resume' }
|
|
137
|
+
}).comparable;
|
|
138
|
+
|
|
139
|
+
expect(observedStreamHead >= writeCheckpointHead).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
5
5
|
import { ChangeStreamTestContext, setSnapshotHistorySeconds } from './change_stream_utils.js';
|
|
6
|
+
import { DATABASE_TYPE, DatabaseType } from './DatabaseType.js';
|
|
6
7
|
import { env } from './env.js';
|
|
7
8
|
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
8
9
|
|
|
@@ -15,7 +16,9 @@ function defineSlowTests({ factory, storageVersion }: StorageVersionTestContext)
|
|
|
15
16
|
return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
test
|
|
19
|
+
// This test uses getParameter/setParameter for minSnapshotHistoryWindowInSeconds,
|
|
20
|
+
// which Azure DocumentDB does not support.
|
|
21
|
+
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)('replicating snapshot with lots of data', async () => {
|
|
19
22
|
await using context = await openContext();
|
|
20
23
|
// Test with low minSnapshotHistoryWindowInSeconds, to trigger:
|
|
21
24
|
// > Read timestamp .. is older than the oldest available timestamp.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DATABASE_TYPE, DatabaseType } from './DatabaseType.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scale factor applied to a default test timeout on cloud DocumentDB when no
|
|
5
|
+
* explicit cloud override is given to {@link testTimeout}. Cloud DocumentDB has
|
|
6
|
+
* 10-30s change-stream propagation spikes, so timeouts there need headroom.
|
|
7
|
+
* Override with the `TEST_TIMEOUT_MULTIPLIER` env var; defaults to 6.
|
|
8
|
+
*/
|
|
9
|
+
export const TEST_TIMEOUT_MULTIPLIER = Number(process.env.TEST_TIMEOUT_MULTIPLIER ?? '6');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a test timeout in milliseconds for the current backend.
|
|
13
|
+
*
|
|
14
|
+
* - Fast backend (local / standard MongoDB): uses `defaultMs` as-is.
|
|
15
|
+
* - Cloud DocumentDB: uses `cloudOverride` when provided, otherwise scales
|
|
16
|
+
* `defaultMs` by {@link TEST_TIMEOUT_MULTIPLIER}.
|
|
17
|
+
*/
|
|
18
|
+
export function testTimeout(defaultMs: number, options?: { cloudOverride?: number }): number {
|
|
19
|
+
if (DATABASE_TYPE !== DatabaseType.DOCUMENTDB) {
|
|
20
|
+
return defaultMs;
|
|
21
|
+
}
|
|
22
|
+
return options?.cloudOverride ?? Math.ceil(defaultMs * TEST_TIMEOUT_MULTIPLIER);
|
|
23
|
+
}
|
package/test/src/util.ts
CHANGED
|
@@ -71,8 +71,7 @@ export async function connectMongoData(options: mongo.MongoClientOptions = {}) {
|
|
|
71
71
|
...BSON_DESERIALIZE_DATA_OPTIONS,
|
|
72
72
|
...options
|
|
73
73
|
});
|
|
74
|
-
|
|
75
|
-
return { client, db: client.db(dbname) };
|
|
74
|
+
return { client, db: client.db(TEST_CONNECTION_OPTIONS.database) };
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|