@powersync/service-module-mongodb 0.0.0-dev-20260803144812 → 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.
- package/CHANGELOG.md +63 -7
- package/dist/module/MongoModule.js +2 -1
- package/dist/module/MongoModule.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +8 -0
- package/dist/replication/ChangeStream.js +71 -14
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js +2 -1
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/MongoSnapshotter.js +6 -6
- package/dist/replication/MongoSnapshotter.js.map +1 -1
- package/dist/replication/RawChangeStream.d.ts +3 -11
- package/dist/replication/RawChangeStream.js +0 -17
- package/dist/replication/RawChangeStream.js.map +1 -1
- package/dist/replication/checkpoints/CheckpointImplementation.d.ts +4 -1
- package/dist/replication/checkpoints/SentinelCheckpointImplementation.d.ts +4 -1
- package/dist/replication/checkpoints/SentinelCheckpointImplementation.js +1 -1
- package/dist/replication/checkpoints/SentinelCheckpointImplementation.js.map +1 -1
- package/dist/replication/checkpoints/TimestampCheckpointImplementation.d.ts +4 -1
- package/dist/replication/checkpoints/TimestampCheckpointImplementation.js +2 -1
- package/dist/replication/checkpoints/TimestampCheckpointImplementation.js.map +1 -1
- package/dist/types/types.d.ts +5 -0
- package/dist/types/types.js +20 -2
- package/dist/types/types.js.map +1 -1
- package/package.json +9 -9
- package/src/module/MongoModule.ts +2 -1
- package/src/replication/ChangeStream.ts +75 -16
- package/src/replication/ChangeStreamReplicationJob.ts +2 -1
- package/src/replication/MongoSnapshotter.ts +6 -6
- package/src/replication/RawChangeStream.ts +3 -30
- package/src/replication/checkpoints/CheckpointImplementation.ts +1 -1
- package/src/replication/checkpoints/SentinelCheckpointImplementation.ts +2 -2
- package/src/replication/checkpoints/TimestampCheckpointImplementation.ts +3 -2
- package/src/types/types.ts +27 -2
- package/test/src/change_stream.test.ts +73 -0
- package/test/src/config.test.ts +34 -0
- package/test/src/documentdb_mode.test.ts +1 -2
- package/test/src/raw_change_stream.test.ts +0 -63
- package/test/src/util.ts +11 -29
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,14 +6,8 @@ import {
|
|
|
6
6
|
ReplicationAssertionError
|
|
7
7
|
} from '@powersync/lib-services-framework';
|
|
8
8
|
import { PerformanceTracer } from '@powersync/service-core';
|
|
9
|
-
import { performance } from 'node:perf_hooks';
|
|
10
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
11
9
|
import { ChangeStreamInvalidatedError } from './ChangeStream.js';
|
|
12
10
|
|
|
13
|
-
// Keep the DocumentDB idle-poll workaround from adding the full maxAwaitTimeMS
|
|
14
|
-
// as local latency when an update arrives just after an empty batch.
|
|
15
|
-
const CLIENT_SIDE_MAX_AWAIT_TIME_MS_DELAY_CAP_MS = 1_000;
|
|
16
|
-
|
|
17
11
|
export interface RawChangeStreamOptions {
|
|
18
12
|
signal?: AbortSignal;
|
|
19
13
|
|
|
@@ -21,21 +15,12 @@ export interface RawChangeStreamOptions {
|
|
|
21
15
|
* How long to wait for new data per batch (max time for long-polling).
|
|
22
16
|
* This is sent as maxTimeMS for the getMore command.
|
|
23
17
|
*
|
|
24
|
-
* A value of 0
|
|
25
|
-
*
|
|
18
|
+
* A value of 0 removes the explicit await limit and leaves the server's
|
|
19
|
+
* default awaitData behavior in effect; it does not make getMore return
|
|
20
|
+
* immediately. Snapshot probes use 0 for this purpose.
|
|
26
21
|
*/
|
|
27
22
|
maxAwaitTimeMS: number;
|
|
28
23
|
|
|
29
|
-
/**
|
|
30
|
-
* Also enforce maxAwaitTimeMS on the client for empty getMore batches.
|
|
31
|
-
*
|
|
32
|
-
* Azure DocumentDB currently returns idle getMore calls before maxTimeMS. When
|
|
33
|
-
* this is enabled, empty batches are delayed locally (capped at 1s) to avoid
|
|
34
|
-
* tight polling. We still send maxTimeMS so this remains compatible with
|
|
35
|
-
* servers that handle maxAwaitTimeMS correctly.
|
|
36
|
-
*/
|
|
37
|
-
clientSideMaxAwaitTimeMS?: boolean;
|
|
38
|
-
|
|
39
24
|
/**
|
|
40
25
|
* Timeout for the initial aggregate command.
|
|
41
26
|
*/
|
|
@@ -230,17 +215,12 @@ async function* rawChangeStreamInner(
|
|
|
230
215
|
options.signal?.throwIfAborted();
|
|
231
216
|
|
|
232
217
|
using commandSpan = options.tracer?.span('changestream', 'getmore');
|
|
233
|
-
const getMoreStartedAt = performance.now();
|
|
234
218
|
const getMoreCommand: mongo.Document = {
|
|
235
219
|
getMore: cursorId,
|
|
236
220
|
collection: nsCollection,
|
|
237
221
|
batchSize: batchSizer.next(),
|
|
238
222
|
maxTimeMS: options.maxAwaitTimeMS
|
|
239
223
|
};
|
|
240
|
-
// Azure DocumentDB currently returns empty getMore batches before
|
|
241
|
-
// maxTimeMS expires. Keep maxTimeMS for forward compatibility with the
|
|
242
|
-
// server-side behavior, and when client-side mode is enabled, enforce the
|
|
243
|
-
// capped idle wait locally for empty batches below.
|
|
244
224
|
const getMoreResult: mongo.Document = await db.command(getMoreCommand, { session, raw: true }).catch((e) => {
|
|
245
225
|
if (isMongoServerError(e) && e.codeName == 'CursorKilled') {
|
|
246
226
|
// This may be due to the killCursors command issued when aborting.
|
|
@@ -268,13 +248,6 @@ async function* rawChangeStreamInner(
|
|
|
268
248
|
// postBatchResumeToken is returned in MongoDB 4.0.7 and later, and we support 6.0+
|
|
269
249
|
throw new ReplicationAssertionError(`postBatchResumeToken from aggregate response`);
|
|
270
250
|
}
|
|
271
|
-
if (options.clientSideMaxAwaitTimeMS && nextBatch.length == 0) {
|
|
272
|
-
const remainingMaxAwaitTimeMS = Math.ceil(options.maxAwaitTimeMS - (performance.now() - getMoreStartedAt));
|
|
273
|
-
if (remainingMaxAwaitTimeMS > 0) {
|
|
274
|
-
const clientSideDelayMs = Math.min(remainingMaxAwaitTimeMS, CLIENT_SIDE_MAX_AWAIT_TIME_MS_DELAY_CAP_MS);
|
|
275
|
-
await delay(clientSideDelayMs, undefined, { signal: options.signal });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
251
|
yield {
|
|
279
252
|
events: nextBatch,
|
|
280
253
|
resumeToken: cursor.postBatchResumeToken,
|
|
@@ -128,7 +128,7 @@ export interface CheckpointImplementation {
|
|
|
128
128
|
* The timestamp implementation parses the timestamp embedded in the token.
|
|
129
129
|
* The sentinel implementation pairs the token with the current coordinate.
|
|
130
130
|
*/
|
|
131
|
-
lsnFromResumeToken(resumeToken: mongo.ResumeToken): string;
|
|
131
|
+
lsnFromResumeToken(resumeToken: mongo.ResumeToken): { lsn: string; timestamp: Date | null };
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Source-side replication head for write checkpoints. The LSN passed to the
|
|
@@ -108,11 +108,11 @@ export class SentinelCheckpointImplementation implements CheckpointImplementatio
|
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
lsnFromResumeToken(resumeToken: mongo.ResumeToken)
|
|
111
|
+
lsnFromResumeToken(resumeToken: mongo.ResumeToken) {
|
|
112
112
|
// Pair the bare token with the current coordinate. The coordinate is
|
|
113
113
|
// frozen between checkpoint events, so a per-batch resume marker only
|
|
114
114
|
// advances the token; that is all resumption needs (see the design doc).
|
|
115
|
-
return new SentinelLSN({ sentinel: this.position, resume_token: resumeToken }).comparable;
|
|
115
|
+
return { lsn: new SentinelLSN({ sentinel: this.position, resume_token: resumeToken }).comparable, timestamp: null };
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
async createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T> {
|
|
@@ -67,9 +67,10 @@ export class TimestampCheckpointImplementation implements CheckpointImplementati
|
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
lsnFromResumeToken(resumeToken: mongo.ResumeToken)
|
|
70
|
+
lsnFromResumeToken(resumeToken: mongo.ResumeToken) {
|
|
71
71
|
// The timestamp is embedded in the resume token.
|
|
72
|
-
|
|
72
|
+
const lsn = MongoLSN.fromResumeToken(resumeToken);
|
|
73
|
+
return { lsn: lsn.comparable, timestamp: timestampToDate(lsn.timestamp) };
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
async createReplicationHead<T>(callback: ReplicationHeadCallback<T>): Promise<T> {
|
package/src/types/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MongoConnectionParams } from '@powersync/lib-service-mongodb/types';
|
|
2
2
|
import * as lib_mongo from '@powersync/lib-service-mongodb/types';
|
|
3
|
+
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
3
4
|
import * as service_types from '@powersync/service-types';
|
|
4
5
|
import { LookupFunction } from 'node:net';
|
|
5
6
|
import * as t from 'ts-codec';
|
|
@@ -40,6 +41,10 @@ export enum PostImagesOption {
|
|
|
40
41
|
READ_ONLY = 'read_only'
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
const DEFAULT_PING_INTERVAL_SECONDS = 60;
|
|
45
|
+
const MIN_PING_INTERVAL_SECONDS = 5;
|
|
46
|
+
const MAX_PING_INTERVAL_SECONDS = 60;
|
|
47
|
+
|
|
43
48
|
export interface NormalizedMongoConnectionConfig {
|
|
44
49
|
id: string;
|
|
45
50
|
tag: string;
|
|
@@ -54,13 +59,19 @@ export interface NormalizedMongoConnectionConfig {
|
|
|
54
59
|
|
|
55
60
|
postImages: PostImagesOption;
|
|
56
61
|
|
|
62
|
+
heartbeat_interval_seconds: number;
|
|
63
|
+
|
|
57
64
|
connectionParams: MongoConnectionParams;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
export const MongoConnectionConfig = service_types.configFile.DataSourceConfig.and(lib_mongo.BaseMongoConfig).and(
|
|
61
68
|
t.object({
|
|
62
69
|
// Replication specific settings
|
|
63
|
-
post_images:
|
|
70
|
+
post_images: service_types.enumLiteral('off', 'auto_configure', 'read_only').optional(),
|
|
71
|
+
/**
|
|
72
|
+
* Interval in seconds between source connection heartbeats. Null or omitted defaults to 60 seconds.
|
|
73
|
+
*/
|
|
74
|
+
heartbeat_interval_seconds: service_types.orNull(t.number).optional()
|
|
64
75
|
})
|
|
65
76
|
);
|
|
66
77
|
|
|
@@ -87,6 +98,20 @@ export function normalizeConnectionConfig(options: MongoConnectionConfigDecoded)
|
|
|
87
98
|
...base,
|
|
88
99
|
id: options.id ?? 'default',
|
|
89
100
|
tag: options.tag ?? 'default',
|
|
90
|
-
postImages: (options.post_images as PostImagesOption | undefined) ?? PostImagesOption.OFF
|
|
101
|
+
postImages: (options.post_images as PostImagesOption | undefined) ?? PostImagesOption.OFF,
|
|
102
|
+
heartbeat_interval_seconds: normalizeHeartbeatInterval(options.heartbeat_interval_seconds)
|
|
91
103
|
};
|
|
92
104
|
}
|
|
105
|
+
|
|
106
|
+
function normalizeHeartbeatInterval(value: number | null | undefined): number {
|
|
107
|
+
if (value == null) {
|
|
108
|
+
return DEFAULT_PING_INTERVAL_SECONDS;
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isFinite(value) || value < MIN_PING_INTERVAL_SECONDS || value > MAX_PING_INTERVAL_SECONDS) {
|
|
111
|
+
throw new ServiceError(
|
|
112
|
+
ErrorCode.PSYNC_S1109,
|
|
113
|
+
`MongoDB connection: heartbeat_interval_seconds must be between ${MIN_PING_INTERVAL_SECONDS} and ${MAX_PING_INTERVAL_SECONDS} seconds; null or omitted defaults to ${DEFAULT_PING_INTERVAL_SECONDS}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
@@ -622,6 +622,79 @@ bucket_definitions:
|
|
|
622
622
|
expect(data).toMatchObject([test_utils.putOp('test_data', { id: test_id, description: 'test1' })]);
|
|
623
623
|
});
|
|
624
624
|
|
|
625
|
+
test.runIf(storageVersion >= 3)(
|
|
626
|
+
'does not resnapshot an unchanged event definition during incremental reprocessing',
|
|
627
|
+
async (testContext) => {
|
|
628
|
+
const eventDefinition = `
|
|
629
|
+
event_definitions:
|
|
630
|
+
checkpoint_requests:
|
|
631
|
+
payloads:
|
|
632
|
+
- SELECT user_id, checkpoint, client_id, CASE WHEN checkpoint_requested_at IS NULL THEN true ELSE false END AS is_legacy FROM checkpoints
|
|
633
|
+
`;
|
|
634
|
+
const syncConfig = (deploymentComment: string) => `
|
|
635
|
+
config:
|
|
636
|
+
edition: 3
|
|
637
|
+
storage_version: 3
|
|
638
|
+
# ${deploymentComment}
|
|
639
|
+
|
|
640
|
+
${eventDefinition}
|
|
641
|
+
streams:
|
|
642
|
+
global:
|
|
643
|
+
auto_subscribe: true
|
|
644
|
+
queries:
|
|
645
|
+
- SELECT *, _id AS id FROM lists
|
|
646
|
+
- SELECT *, _id AS id FROM todos
|
|
647
|
+
`;
|
|
648
|
+
|
|
649
|
+
let replicationStreamId: number;
|
|
650
|
+
{
|
|
651
|
+
await using context = await openContext();
|
|
652
|
+
const firstStorage = await context.updateSyncRules(syncConfig('initial deployment'));
|
|
653
|
+
if (
|
|
654
|
+
!('incrementalReprocessing' in firstStorage.storageConfig) ||
|
|
655
|
+
firstStorage.storageConfig.incrementalReprocessing !== true
|
|
656
|
+
) {
|
|
657
|
+
testContext.skip('storage does not support incremental reprocessing');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const { db } = context;
|
|
661
|
+
await db.collection('lists').insertOne({ name: 'list' });
|
|
662
|
+
await db.collection('todos').insertOne({ description: 'todo' });
|
|
663
|
+
await db.collection('checkpoints').insertOne({
|
|
664
|
+
user_id: 'user-1',
|
|
665
|
+
checkpoint: 1n,
|
|
666
|
+
client_id: 'client-1',
|
|
667
|
+
checkpoint_requested_at: null
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
replicationStreamId = firstStorage.replicationStreamId;
|
|
671
|
+
await context.replicateSnapshot();
|
|
672
|
+
context.startStreaming();
|
|
673
|
+
await context.getCheckpoint();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const resnapshottedCollections: string[] = [];
|
|
677
|
+
await using context = await openContext({
|
|
678
|
+
doNotClear: true,
|
|
679
|
+
streamOptions: {
|
|
680
|
+
snapshotHooks: {
|
|
681
|
+
beforeSnapshotStarted: async (table) => {
|
|
682
|
+
resnapshottedCollections.push(table.name);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const processingStorage = await context.updateSyncRules(syncConfig('redeployed without behavioral changes'));
|
|
689
|
+
expect(processingStorage.replicationStreamId).toBe(replicationStreamId);
|
|
690
|
+
expect(await context.factory.getDeployingSyncConfig()).not.toBeNull();
|
|
691
|
+
|
|
692
|
+
await context.replicateSnapshot();
|
|
693
|
+
|
|
694
|
+
expect(resnapshottedCollections).toEqual([]);
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
|
|
625
698
|
test('coalesces standalone checkpoints when backlog is buffered', async () => {
|
|
626
699
|
await using context = await openContext();
|
|
627
700
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { normalizeConnectionConfig } from '@module/types/types.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const BASE_CONFIG = {
|
|
5
|
+
type: 'mongodb' as const,
|
|
6
|
+
uri: 'mongodb://localhost:27017/powersync_test'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe('MongoDB connection config', () => {
|
|
10
|
+
test('defaults heartbeat_interval_seconds to 60 seconds', () => {
|
|
11
|
+
expect(normalizeConnectionConfig(BASE_CONFIG).heartbeat_interval_seconds).toBe(60);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('defaults a null heartbeat_interval_seconds to 60 seconds', () => {
|
|
15
|
+
expect(
|
|
16
|
+
normalizeConnectionConfig({ ...BASE_CONFIG, heartbeat_interval_seconds: null }).heartbeat_interval_seconds
|
|
17
|
+
).toBe(60);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('allows the MongoDB maximum heartbeat interval', () => {
|
|
21
|
+
expect(
|
|
22
|
+
normalizeConnectionConfig({ ...BASE_CONFIG, heartbeat_interval_seconds: 60 }).heartbeat_interval_seconds
|
|
23
|
+
).toBe(60);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.each([0, 4, 61, Number.NaN, Number.POSITIVE_INFINITY])(
|
|
27
|
+
'rejects invalid heartbeat_interval_seconds: %s',
|
|
28
|
+
(heartbeat_interval_seconds) => {
|
|
29
|
+
expect(() => normalizeConnectionConfig({ ...BASE_CONFIG, heartbeat_interval_seconds })).toThrow(
|
|
30
|
+
'heartbeat_interval_seconds'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -759,8 +759,7 @@ bucket_definitions:
|
|
|
759
759
|
expect(JSON.parse(lastOp.data as string)).toMatchObject({ description: 'after_keepalive' });
|
|
760
760
|
});
|
|
761
761
|
|
|
762
|
-
|
|
763
|
-
test.skip('respects maxAwaitTimeMS for idle getMore calls in documentDbMode', async () => {
|
|
762
|
+
test('respects maxAwaitTimeMS for idle getMore calls in documentDbMode', async () => {
|
|
764
763
|
const maxAwaitTimeMS = 2_000;
|
|
765
764
|
|
|
766
765
|
await using context = await openContext({
|
|
@@ -5,7 +5,6 @@ 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
7
|
import { DATABASE_TYPE, DatabaseType } from './DatabaseType.js';
|
|
8
|
-
import { testTimeout } from './test-timeouts.js';
|
|
9
8
|
import { clearTestDb, connectMongoData, requireFailCommand } from './util.js';
|
|
10
9
|
|
|
11
10
|
// DocumentDB only supports cluster-level change streams — collection- and
|
|
@@ -102,68 +101,6 @@ describe('internal mongodb utils', () => {
|
|
|
102
101
|
}
|
|
103
102
|
);
|
|
104
103
|
|
|
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
|
-
});
|
|
123
|
-
|
|
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
|
-
);
|
|
151
|
-
|
|
152
|
-
await stream.next();
|
|
153
|
-
await collection.insertOne({ test: 1 });
|
|
154
|
-
const nextBatch = await readUntilNonEmptyBatch(stream);
|
|
155
|
-
await stream.return?.();
|
|
156
|
-
|
|
157
|
-
expect(nextBatch.events).toHaveLength(1);
|
|
158
|
-
|
|
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
104
|
// This test uses configureFailPoint to inject a getMore timeout. Azure
|
|
168
105
|
// DocumentDB does not support configureFailPoint.
|
|
169
106
|
test.skipIf(DATABASE_TYPE == DatabaseType.DOCUMENTDB)(
|
package/test/src/util.ts
CHANGED
|
@@ -3,13 +3,9 @@ import * as mongo_storage from '@powersync/service-module-mongodb-storage';
|
|
|
3
3
|
import * as postgres_storage from '@powersync/service-module-postgres-storage';
|
|
4
4
|
|
|
5
5
|
import * as types from '@module/types/types.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
TestStorageConfig,
|
|
10
|
-
TestStorageFactory
|
|
11
|
-
} from '@powersync/service-core';
|
|
12
|
-
import { describe, TestContext, TestOptions } from 'vitest';
|
|
6
|
+
import { BSON_DESERIALIZE_DATA_OPTIONS, TestStorageFactory } from '@powersync/service-core';
|
|
7
|
+
import { describeStorageCombinations } from '@powersync/service-core-tests';
|
|
8
|
+
import { TestContext, TestOptions } from 'vitest';
|
|
13
9
|
import { env } from './env.js';
|
|
14
10
|
|
|
15
11
|
export const TEST_URI = env.MONGO_TEST_DATA_URL;
|
|
@@ -29,34 +25,20 @@ export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.
|
|
|
29
25
|
url: env.PG_STORAGE_TEST_URL
|
|
30
26
|
});
|
|
31
27
|
|
|
32
|
-
export const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS;
|
|
33
|
-
|
|
34
28
|
export interface StorageVersionTestContext {
|
|
35
29
|
factory: TestStorageFactory;
|
|
36
30
|
storageVersion: number;
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
export function describeWithStorage(options: TestOptions, fn: (context: StorageVersionTestContext) => void) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
if (env.TEST_MONGO_STORAGE) {
|
|
54
|
-
describeFactory('mongodb', INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (env.TEST_POSTGRES_STORAGE) {
|
|
58
|
-
describeFactory('postgres', INITIALIZED_POSTGRES_STORAGE_FACTORY);
|
|
59
|
-
}
|
|
34
|
+
describeStorageCombinations(
|
|
35
|
+
{
|
|
36
|
+
mongodb: env.TEST_MONGO_STORAGE ? INITIALIZED_MONGO_STORAGE_FACTORY : undefined,
|
|
37
|
+
postgres: env.TEST_POSTGRES_STORAGE ? INITIALIZED_POSTGRES_STORAGE_FACTORY : undefined
|
|
38
|
+
},
|
|
39
|
+
options,
|
|
40
|
+
fn
|
|
41
|
+
);
|
|
60
42
|
}
|
|
61
43
|
|
|
62
44
|
export async function clearTestDb(db: mongo.Db) {
|