@powersync/service-module-mongodb 0.0.0-dev-20260909133214 → 0.0.0-dev-20260929095716
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 +38 -6
- package/ci/test-connection.yaml +12 -0
- package/dist/api/MongoRouteAPIAdapter.d.ts +2 -2
- package/dist/api/MongoRouteAPIAdapter.js +73 -143
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/api/infer-collection-schema.d.ts +12 -0
- package/dist/api/infer-collection-schema.js +142 -0
- package/dist/api/infer-collection-schema.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/module/MongoModule.d.ts +1 -2
- package/dist/module/MongoModule.js +0 -1
- package/dist/module/MongoModule.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +10 -2
- package/dist/replication/ChangeStream.js +228 -317
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.d.ts +4 -1
- package/dist/replication/ChangeStreamReplicationJob.js +4 -1
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/ChangeStreamReplicator.d.ts +3 -0
- package/dist/replication/ChangeStreamReplicator.js +3 -0
- package/dist/replication/ChangeStreamReplicator.js.map +1 -1
- package/dist/replication/MongoRelation.js +5 -2
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/MongoReplicationQueryProvider.d.ts +87 -0
- package/dist/replication/MongoReplicationQueryProvider.js +23 -0
- package/dist/replication/MongoReplicationQueryProvider.js.map +1 -0
- package/dist/replication/MongoReplicationStream.d.ts +85 -0
- package/dist/replication/MongoReplicationStream.js +101 -0
- package/dist/replication/MongoReplicationStream.js.map +1 -0
- package/dist/replication/MongoSnapshotQuery.d.ts +2 -0
- package/dist/replication/MongoSnapshotQuery.js +12 -2
- package/dist/replication/MongoSnapshotQuery.js.map +1 -1
- package/dist/replication/MongoSnapshotter.d.ts +7 -2
- package/dist/replication/MongoSnapshotter.js +58 -85
- package/dist/replication/MongoSnapshotter.js.map +1 -1
- package/dist/replication/RawChangeStream.d.ts +1 -1
- package/dist/replication/RawChangeStream.js +1 -1
- package/dist/replication/RawChangeStream.js.map +1 -1
- package/dist/replication/SourceRowConverter.d.ts +1 -0
- package/dist/replication/SourceRowConverter.js +4 -2
- package/dist/replication/SourceRowConverter.js.map +1 -1
- package/dist/replication/bufferToSqlite.d.ts +3 -1
- package/dist/replication/bufferToSqlite.js +35 -23
- package/dist/replication/bufferToSqlite.js.map +1 -1
- package/dist/replication/replication-index.d.ts +6 -0
- package/dist/replication/replication-index.js +6 -0
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/test-utils/ChangeStreamTestContext.d.ts +131 -0
- package/dist/test-utils/ChangeStreamTestContext.js +391 -0
- package/dist/test-utils/ChangeStreamTestContext.js.map +1 -0
- package/dist/test-utils/test-utils-index.d.ts +1 -0
- package/dist/test-utils/test-utils-index.js +2 -0
- package/dist/test-utils/test-utils-index.js.map +1 -0
- package/package.json +8 -8
- package/src/api/MongoRouteAPIAdapter.ts +79 -127
- package/src/api/infer-collection-schema.ts +153 -0
- package/src/index.ts +1 -0
- package/src/module/MongoModule.ts +1 -3
- package/src/replication/ChangeStream.ts +253 -359
- package/src/replication/ChangeStreamReplicationJob.ts +7 -2
- package/src/replication/ChangeStreamReplicator.ts +5 -0
- package/src/replication/MongoRelation.ts +8 -1
- package/src/replication/MongoReplicationQueryProvider.ts +108 -0
- package/src/replication/MongoReplicationStream.ts +201 -0
- package/src/replication/MongoSnapshotQuery.ts +19 -3
- package/src/replication/MongoSnapshotter.ts +70 -92
- package/src/replication/RawChangeStream.ts +2 -2
- package/src/replication/SourceRowConverter.ts +4 -2
- package/src/replication/bufferToSqlite.ts +43 -23
- package/src/replication/replication-index.ts +6 -0
- package/src/test-utils/ChangeStreamTestContext.ts +520 -0
- package/src/test-utils/test-utils-index.ts +1 -0
- package/test/src/buffer_to_sqlite.test.ts +9 -0
- package/test/src/change_stream.test.ts +3 -3
- package/test/src/change_stream_test_setup.ts +38 -0
- package/test/src/chunked_snapshot.test.ts +3 -3
- package/test/src/documentdb_mode.test.ts +3 -3
- package/test/src/replication_stream.test.ts +311 -0
- package/test/src/resume.test.ts +10 -4
- package/test/src/resuming_snapshots.test.ts +3 -3
- package/test/src/schema.test.ts +223 -0
- package/test/src/slow_tests.test.ts +3 -3
- package/test/src/snapshot_query_provider.test.ts +95 -0
- package/test/src/stream_progress.test.ts +312 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/test/src/change_stream_utils.ts +0 -375
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { ReplicationAbortedError } from '@powersync/lib-services-framework';
|
|
3
|
-
import {
|
|
4
|
-
BucketStorageFactory,
|
|
5
|
-
createCoreReplicationMetrics,
|
|
6
|
-
initializeCoreReplicationMetrics,
|
|
7
|
-
InternalOpId,
|
|
8
|
-
isBatchEnd,
|
|
9
|
-
LEGACY_STORAGE_VERSION,
|
|
10
|
-
OplogEntry,
|
|
11
|
-
ProtocolOpId,
|
|
12
|
-
ReplicationCheckpoint,
|
|
13
|
-
settledPromise,
|
|
14
|
-
storage,
|
|
15
|
-
SyncRulesBucketStorage,
|
|
16
|
-
TestStorageOptions,
|
|
17
|
-
unsettledPromise,
|
|
18
|
-
updateSyncRulesFromYaml,
|
|
19
|
-
utils
|
|
20
|
-
} from '@powersync/service-core';
|
|
21
|
-
import { bucketRequest, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
|
|
22
|
-
|
|
23
|
-
import { SentinelLSN } from '@module/common/SentinelLSN.js';
|
|
24
|
-
import { ChangeStream, ChangeStreamOptions } from '@module/replication/ChangeStream.js';
|
|
25
|
-
import { MongoManager } from '@module/replication/MongoManager.js';
|
|
26
|
-
import {
|
|
27
|
-
createCheckpoint,
|
|
28
|
-
createSentinelCheckpointLsn,
|
|
29
|
-
STANDALONE_CHECKPOINT_ID
|
|
30
|
-
} from '@module/replication/MongoRelation.js';
|
|
31
|
-
import { detectDocumentDb } from '@module/replication/replication-utils.js';
|
|
32
|
-
import { NormalizedMongoConnectionConfig } from '@module/types/types.js';
|
|
33
|
-
|
|
34
|
-
import { clearTestDb, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
35
|
-
|
|
36
|
-
export class ChangeStreamTestContext {
|
|
37
|
-
private _walStream?: ChangeStream;
|
|
38
|
-
private abortController = new AbortController();
|
|
39
|
-
private settledReplicationPromise?: Promise<PromiseSettledResult<void>>;
|
|
40
|
-
private syncRulesContent?: storage.PersistedSyncConfigContent;
|
|
41
|
-
public storage?: SyncRulesBucketStorage;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Tests operating on the mongo change stream need to configure the stream and manage asynchronous
|
|
45
|
-
* replication, which gets a little tricky.
|
|
46
|
-
*
|
|
47
|
-
* This configures all the context, and tears it down afterwards.
|
|
48
|
-
*/
|
|
49
|
-
static async open(
|
|
50
|
-
factory: (options: TestStorageOptions) => Promise<BucketStorageFactory>,
|
|
51
|
-
options?: {
|
|
52
|
-
doNotClear?: boolean;
|
|
53
|
-
storageVersion?: number;
|
|
54
|
-
mongoOptions?: Partial<NormalizedMongoConnectionConfig>;
|
|
55
|
-
streamOptions?: Partial<ChangeStreamOptions>;
|
|
56
|
-
/**
|
|
57
|
-
* Optional override for tests that need to force a mode. By default the
|
|
58
|
-
* test context detects DocumentDB from the hello response.
|
|
59
|
-
*/
|
|
60
|
-
documentDbMode?: boolean;
|
|
61
|
-
}
|
|
62
|
-
) {
|
|
63
|
-
const f = await factory({ doNotClear: options?.doNotClear });
|
|
64
|
-
const connectionManager = new MongoManager({ ...TEST_CONNECTION_OPTIONS, ...options?.mongoOptions });
|
|
65
|
-
|
|
66
|
-
if (!options?.doNotClear) {
|
|
67
|
-
await clearTestDb(connectionManager.db);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const storageVersion = options?.storageVersion ?? LEGACY_STORAGE_VERSION;
|
|
71
|
-
const documentDbMode = options?.documentDbMode ?? (await detectDocumentDb(connectionManager.db));
|
|
72
|
-
|
|
73
|
-
return new ChangeStreamTestContext(f, connectionManager, options?.streamOptions, storageVersion, documentDbMode);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
constructor(
|
|
77
|
-
public factory: BucketStorageFactory,
|
|
78
|
-
public connectionManager: MongoManager,
|
|
79
|
-
private streamOptions: Partial<ChangeStreamOptions> = {},
|
|
80
|
-
private storageVersion: number = LEGACY_STORAGE_VERSION,
|
|
81
|
-
private documentDbMode: boolean = false
|
|
82
|
-
) {
|
|
83
|
-
createCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
84
|
-
initializeCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Abort snapshot and/or replication, without actively closing connections.
|
|
89
|
-
*/
|
|
90
|
-
abort(cause?: Error) {
|
|
91
|
-
this.abortController.abort(cause);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async dispose() {
|
|
95
|
-
this.abort(new Error('Disposing test context'));
|
|
96
|
-
await this.settledReplicationPromise;
|
|
97
|
-
await this.factory[Symbol.asyncDispose]();
|
|
98
|
-
await this.connectionManager.end();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async [Symbol.asyncDispose]() {
|
|
102
|
-
await this.dispose();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get client() {
|
|
106
|
-
return this.connectionManager.client;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
get db() {
|
|
110
|
-
return this.connectionManager.db;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
get connectionTag() {
|
|
114
|
-
return this.connectionManager.connectionTag;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async updateSyncRules(content: string) {
|
|
118
|
-
const replicationStream = await this.factory.updateSyncRules(
|
|
119
|
-
updateSyncRulesFromYaml(content, { validate: true, storageVersion: this.storageVersion })
|
|
120
|
-
);
|
|
121
|
-
this.syncRulesContent = replicationStream.syncConfigContent[0];
|
|
122
|
-
this.storage = this.factory.getInstance(replicationStream);
|
|
123
|
-
return this.storage!;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async loadNextSyncRules() {
|
|
127
|
-
const syncConfig = await this.factory.getDeployingSyncConfig();
|
|
128
|
-
if (syncConfig == null) {
|
|
129
|
-
throw new Error(`Next sync config not available`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
this.syncRulesContent = syncConfig.content;
|
|
133
|
-
this.storage = syncConfig.storage;
|
|
134
|
-
return this.storage!;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async loadActiveSyncRules() {
|
|
138
|
-
const syncConfig = await this.factory.getActiveSyncConfig();
|
|
139
|
-
if (syncConfig == null) {
|
|
140
|
-
throw new Error(`Active sync config not found`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
this.syncRulesContent = syncConfig.content;
|
|
144
|
-
this.storage = syncConfig.storage;
|
|
145
|
-
return this.storage!;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private getSyncConfigContent(): storage.PersistedSyncConfigContent {
|
|
149
|
-
if (this.syncRulesContent == null) {
|
|
150
|
-
throw new Error('Sync config not configured - call updateSyncRules() first');
|
|
151
|
-
}
|
|
152
|
-
return this.syncRulesContent;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
get streamer() {
|
|
156
|
-
if (this.storage == null) {
|
|
157
|
-
throw new Error('updateSyncRules() first');
|
|
158
|
-
}
|
|
159
|
-
if (this._walStream) {
|
|
160
|
-
return this._walStream;
|
|
161
|
-
}
|
|
162
|
-
const options: ChangeStreamOptions = {
|
|
163
|
-
storage: this.storage,
|
|
164
|
-
metrics: METRICS_HELPER.metricsEngine,
|
|
165
|
-
connections: this.connectionManager,
|
|
166
|
-
abort_signal: this.abortController.signal,
|
|
167
|
-
// Specifically reduce this from the default for tests on MongoDB <= 6.0, otherwise it can take
|
|
168
|
-
// a long time to abort the stream.
|
|
169
|
-
maxAwaitTimeMS: this.streamOptions?.maxAwaitTimeMS ?? 200,
|
|
170
|
-
snapshotChunkLength: this.streamOptions?.snapshotChunkLength,
|
|
171
|
-
keepaliveIntervalMs: this.streamOptions?.keepaliveIntervalMs,
|
|
172
|
-
storageHooks: this.streamOptions?.storageHooks,
|
|
173
|
-
snapshotHooks: this.streamOptions?.snapshotHooks,
|
|
174
|
-
logger: this.streamOptions?.logger
|
|
175
|
-
};
|
|
176
|
-
this._walStream = new ChangeStream(options);
|
|
177
|
-
return this._walStream!;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async replicateSnapshot() {
|
|
181
|
-
this.settledReplicationPromise ??= settledPromise(this.streamer.replicate());
|
|
182
|
-
try {
|
|
183
|
-
await Promise.race([unsettledPromise(this.settledReplicationPromise), this.streamer.waitForInitialSnapshot()]);
|
|
184
|
-
} catch (e) {
|
|
185
|
-
if (e instanceof ReplicationAbortedError && e.cause != null) {
|
|
186
|
-
throw e.cause;
|
|
187
|
-
}
|
|
188
|
-
throw e;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* A snapshot is not consistent until streaming replication has caught up.
|
|
194
|
-
* We simulate that for tests.
|
|
195
|
-
* Do not use if there are any writes performed while doing the snapshot, as that
|
|
196
|
-
* would result in inconsistent data.
|
|
197
|
-
*/
|
|
198
|
-
async markSnapshotConsistent() {
|
|
199
|
-
let checkpoint: string;
|
|
200
|
-
if (this.documentDbMode) {
|
|
201
|
-
const sentinelCheckpoint = SentinelLSN.fromSerialized(await createSentinelCheckpointLsn(this.client, this.db));
|
|
202
|
-
const status = await this.storage!.getStatus();
|
|
203
|
-
const resumeFrom = status.resumeLsn;
|
|
204
|
-
const resumeToken = resumeFrom ? SentinelLSN.fromSerialized(resumeFrom).resumeToken : null;
|
|
205
|
-
|
|
206
|
-
// This helper artificially marks the snapshot as consistent without
|
|
207
|
-
// waiting for the stream to observe the sentinel. Keep the sentinel as the
|
|
208
|
-
// comparable position, but carry forward the existing snapshot resume
|
|
209
|
-
// token so later DocumentDB streaming still resumes from a real token.
|
|
210
|
-
checkpoint = new SentinelLSN({
|
|
211
|
-
sentinel: sentinelCheckpoint.sentinel,
|
|
212
|
-
resume_token: resumeToken
|
|
213
|
-
}).comparable;
|
|
214
|
-
} else {
|
|
215
|
-
checkpoint = await createCheckpoint(this.db, STANDALONE_CHECKPOINT_ID);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
await using writer = await this.storage!.createWriter(test_utils.BATCH_OPTIONS);
|
|
219
|
-
await writer.keepalive(checkpoint);
|
|
220
|
-
await writer.flush();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
startStreaming() {
|
|
224
|
-
this.settledReplicationPromise ??= settledPromise(this.streamer.replicate());
|
|
225
|
-
return this.settledReplicationPromise;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async getCheckpoint(options?: { timeout?: number }) {
|
|
229
|
-
let checkpoint = await Promise.race([
|
|
230
|
-
getClientCheckpoint(this.client, this.db, this.factory, { timeout: options?.timeout ?? 15_000 }),
|
|
231
|
-
this.settledReplicationPromise == null ? undefined : unsettledPromise(this.settledReplicationPromise)
|
|
232
|
-
]);
|
|
233
|
-
if (checkpoint == null) {
|
|
234
|
-
// This indicates an issue with the test setup - streamingPromise completed instead
|
|
235
|
-
// of getClientCheckpoint()
|
|
236
|
-
throw new Error('Test failure - streamingPromise completed');
|
|
237
|
-
}
|
|
238
|
-
return checkpoint;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async getBucketsDataBatch(buckets: Record<string, InternalOpId>, options?: { timeout?: number }) {
|
|
242
|
-
let checkpoint = await this.getCheckpoint(options);
|
|
243
|
-
const syncConfigContent = this.getSyncConfigContent();
|
|
244
|
-
const map = Object.entries(buckets).map(([bucket, start]) => bucketRequest(syncConfigContent, bucket, start));
|
|
245
|
-
return test_utils.fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async getBucketData(bucket: string, start?: ProtocolOpId | InternalOpId | undefined, options?: { timeout?: number }) {
|
|
249
|
-
const checkpoint = await this.getCheckpoint(options);
|
|
250
|
-
return this.getBucketDataAtCheckpoint(bucket, checkpoint, start);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async getBucketDataAtLatestCheckpoint(bucket: string, start?: ProtocolOpId | InternalOpId | undefined) {
|
|
254
|
-
if (this.storage == null) {
|
|
255
|
-
throw new Error('updateSyncRules() first');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const checkpoint = await this.storage.getCheckpoint();
|
|
259
|
-
return this.getBucketDataAtCheckpoint(bucket, checkpoint, start);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async getBucketDataAtCheckpoint(
|
|
263
|
-
bucket: string,
|
|
264
|
-
checkpoint: ReplicationCheckpoint,
|
|
265
|
-
start?: ProtocolOpId | InternalOpId | undefined
|
|
266
|
-
) {
|
|
267
|
-
start ??= 0n;
|
|
268
|
-
if (typeof start == 'string') {
|
|
269
|
-
start = BigInt(start);
|
|
270
|
-
}
|
|
271
|
-
const syncConfigContent = this.getSyncConfigContent();
|
|
272
|
-
let map = [bucketRequest(syncConfigContent, bucket, start)];
|
|
273
|
-
let data: OplogEntry[] = [];
|
|
274
|
-
while (true) {
|
|
275
|
-
const batch = this.storage!.getBucketDataBatch(checkpoint, map);
|
|
276
|
-
|
|
277
|
-
const chunks = await test_utils.fromAsync(batch);
|
|
278
|
-
if (chunks.length == 0) {
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
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
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return data;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async getChecksums(buckets: string[], options?: { timeout?: number }): Promise<utils.ChecksumMap> {
|
|
299
|
-
let checkpoint = await this.getCheckpoint(options);
|
|
300
|
-
const syncConfigContent = this.getSyncConfigContent();
|
|
301
|
-
const versionedBuckets = buckets.map((bucket) => bucketRequest(syncConfigContent, bucket, 0n));
|
|
302
|
-
const checksums = await this.storage!.getChecksums(checkpoint, versionedBuckets);
|
|
303
|
-
|
|
304
|
-
const unversioned: utils.ChecksumMap = new Map();
|
|
305
|
-
for (let i = 0; i < buckets.length; i++) {
|
|
306
|
-
unversioned.set(buckets[i], checksums.get(versionedBuckets[i].bucket)!);
|
|
307
|
-
}
|
|
308
|
-
return unversioned;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async getChecksum(bucket: string, options?: { timeout?: number }) {
|
|
312
|
-
const checksums = await this.getChecksums([bucket], options);
|
|
313
|
-
return checksums.get(bucket);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export async function getClientCheckpoint(
|
|
318
|
-
client: mongo.MongoClient,
|
|
319
|
-
db: mongo.Db,
|
|
320
|
-
storageFactory: BucketStorageFactory,
|
|
321
|
-
options?: { timeout?: number; documentDbMode?: boolean; storage?: SyncRulesBucketStorage }
|
|
322
|
-
): Promise<ReplicationCheckpoint> {
|
|
323
|
-
const start = Date.now();
|
|
324
|
-
const documentDbMode = options?.documentDbMode ?? (await detectDocumentDb(db));
|
|
325
|
-
|
|
326
|
-
const lsn = documentDbMode
|
|
327
|
-
? await createSentinelCheckpointLsn(client, db)
|
|
328
|
-
: await createCheckpoint(db, STANDALONE_CHECKPOINT_ID);
|
|
329
|
-
// This old API needs a persisted checkpoint id.
|
|
330
|
-
// Since we don't use LSNs anymore, the only way to get that is to wait.
|
|
331
|
-
|
|
332
|
-
const timeout = options?.timeout ?? 50_000;
|
|
333
|
-
// DocumentDB: the streaming loop skips standalone checkpoint events while a
|
|
334
|
-
// batch barrier is pending (see ChangeStream.ts), so a single sentinel bump
|
|
335
|
-
// can be missed on an idle stream with no later event to advance the
|
|
336
|
-
// checkpoint. Periodically re-bump the sentinel so a standalone event
|
|
337
|
-
// eventually commits past `lsn`, mirroring getSnapshotLsn's retry loop.
|
|
338
|
-
const NUDGE_INTERVAL_MS = 1000;
|
|
339
|
-
let lastNudge = Date.now();
|
|
340
|
-
let lastCp: ReplicationCheckpoint | null = null;
|
|
341
|
-
|
|
342
|
-
while (Date.now() - start < timeout) {
|
|
343
|
-
const storage = (await storageFactory.getActiveSyncConfig())?.storage;
|
|
344
|
-
const cp = await storage?.getCheckpoint();
|
|
345
|
-
if (cp != null) {
|
|
346
|
-
lastCp = cp;
|
|
347
|
-
if (cp.lsn && cp.lsn >= lsn) {
|
|
348
|
-
return cp;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (documentDbMode && Date.now() - lastNudge >= NUDGE_INTERVAL_MS) {
|
|
353
|
-
await createSentinelCheckpointLsn(client, db);
|
|
354
|
-
lastNudge = Date.now();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
throw new Error(`Timeout while waiting for checkpoint ${lsn}. Last checkpoint: ${lastCp?.lsn}`);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export async function setSnapshotHistorySeconds(client: mongo.MongoClient, seconds: number) {
|
|
364
|
-
const { minSnapshotHistoryWindowInSeconds: currentValue } = await client
|
|
365
|
-
.db('admin')
|
|
366
|
-
.command({ getParameter: 1, minSnapshotHistoryWindowInSeconds: 1 });
|
|
367
|
-
|
|
368
|
-
await client.db('admin').command({ setParameter: 1, minSnapshotHistoryWindowInSeconds: seconds });
|
|
369
|
-
|
|
370
|
-
return {
|
|
371
|
-
async [Symbol.asyncDispose]() {
|
|
372
|
-
await client.db('admin').command({ setParameter: 1, minSnapshotHistoryWindowInSeconds: currentValue });
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
}
|