@powersync/service-module-mongodb 0.13.2 → 0.15.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 +49 -0
- package/dist/api/MongoRouteAPIAdapter.js +2 -2
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/replication/ChangeStream.js +8 -6
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/MongoManager.js +9 -8
- package/dist/replication/MongoManager.js.map +1 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/types.js.map +1 -1
- package/package.json +9 -9
- package/src/api/MongoRouteAPIAdapter.ts +2 -2
- package/src/replication/ChangeStream.ts +8 -6
- package/src/replication/MongoManager.ts +10 -8
- package/src/types/types.ts +3 -0
- package/test/src/change_stream.test.ts +20 -18
- package/test/src/change_stream_utils.ts +62 -18
- package/test/src/chunked_snapshot.test.ts +8 -4
- package/test/src/resume.test.ts +11 -7
- package/test/src/resuming_snapshots.test.ts +9 -5
- package/test/src/slow_tests.test.ts +8 -6
- package/test/src/util.ts +35 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,12 +3,11 @@ import { setTimeout } from 'node:timers/promises';
|
|
|
3
3
|
import { describe, expect, test, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
6
|
-
import { storage } from '@powersync/service-core';
|
|
7
6
|
import { test_utils } from '@powersync/service-core-tests';
|
|
8
7
|
|
|
9
8
|
import { PostImagesOption } from '@module/types/types.js';
|
|
10
9
|
import { ChangeStreamTestContext } from './change_stream_utils.js';
|
|
11
|
-
import { describeWithStorage } from './util.js';
|
|
10
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
12
11
|
|
|
13
12
|
const BASIC_SYNC_RULES = `
|
|
14
13
|
bucket_definitions:
|
|
@@ -21,9 +20,12 @@ describe('change stream', () => {
|
|
|
21
20
|
describeWithStorage({ timeout: 20_000 }, defineChangeStreamTests);
|
|
22
21
|
});
|
|
23
22
|
|
|
24
|
-
function defineChangeStreamTests(factory:
|
|
23
|
+
function defineChangeStreamTests({ factory, storageVersion }: StorageVersionTestContext) {
|
|
24
|
+
const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
|
|
25
|
+
return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
|
|
26
|
+
};
|
|
25
27
|
test('replicating basic values', async () => {
|
|
26
|
-
await using context = await
|
|
28
|
+
await using context = await openContext({
|
|
27
29
|
mongoOptions: { postImages: PostImagesOption.READ_ONLY }
|
|
28
30
|
});
|
|
29
31
|
const { db } = context;
|
|
@@ -59,7 +61,7 @@ bucket_definitions:
|
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
test('replicating wildcard', async () => {
|
|
62
|
-
await using context = await
|
|
64
|
+
await using context = await openContext();
|
|
63
65
|
const { db } = context;
|
|
64
66
|
await context.updateSyncRules(`
|
|
65
67
|
bucket_definitions:
|
|
@@ -91,7 +93,7 @@ bucket_definitions:
|
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
test('updateLookup - no fullDocument available', async () => {
|
|
94
|
-
await using context = await
|
|
96
|
+
await using context = await openContext({
|
|
95
97
|
mongoOptions: { postImages: PostImagesOption.OFF }
|
|
96
98
|
});
|
|
97
99
|
const { db, client } = context;
|
|
@@ -137,7 +139,7 @@ bucket_definitions:
|
|
|
137
139
|
test('postImages - autoConfigure', async () => {
|
|
138
140
|
// Similar to the above test, but with postImages enabled.
|
|
139
141
|
// This resolves the consistency issue.
|
|
140
|
-
await using context = await
|
|
142
|
+
await using context = await openContext({
|
|
141
143
|
mongoOptions: { postImages: PostImagesOption.AUTO_CONFIGURE }
|
|
142
144
|
});
|
|
143
145
|
const { db, client } = context;
|
|
@@ -185,7 +187,7 @@ bucket_definitions:
|
|
|
185
187
|
test('postImages - on', async () => {
|
|
186
188
|
// Similar to postImages - autoConfigure, but does not auto-configure.
|
|
187
189
|
// changeStreamPreAndPostImages must be manually configured.
|
|
188
|
-
await using context = await
|
|
190
|
+
await using context = await openContext({
|
|
189
191
|
mongoOptions: { postImages: PostImagesOption.READ_ONLY }
|
|
190
192
|
});
|
|
191
193
|
const { db, client } = context;
|
|
@@ -230,7 +232,7 @@ bucket_definitions:
|
|
|
230
232
|
});
|
|
231
233
|
|
|
232
234
|
test('replicating case sensitive table', async () => {
|
|
233
|
-
await using context = await
|
|
235
|
+
await using context = await openContext();
|
|
234
236
|
const { db } = context;
|
|
235
237
|
await context.updateSyncRules(`
|
|
236
238
|
bucket_definitions:
|
|
@@ -254,7 +256,7 @@ bucket_definitions:
|
|
|
254
256
|
});
|
|
255
257
|
|
|
256
258
|
test('replicating large values', async () => {
|
|
257
|
-
await using context = await
|
|
259
|
+
await using context = await openContext();
|
|
258
260
|
const { db } = context;
|
|
259
261
|
await context.updateSyncRules(`
|
|
260
262
|
bucket_definitions:
|
|
@@ -285,7 +287,7 @@ bucket_definitions:
|
|
|
285
287
|
});
|
|
286
288
|
|
|
287
289
|
test('replicating dropCollection', async () => {
|
|
288
|
-
await using context = await
|
|
290
|
+
await using context = await openContext();
|
|
289
291
|
const { db } = context;
|
|
290
292
|
const syncRuleContent = `
|
|
291
293
|
bucket_definitions:
|
|
@@ -317,7 +319,7 @@ bucket_definitions:
|
|
|
317
319
|
});
|
|
318
320
|
|
|
319
321
|
test('replicating renameCollection', async () => {
|
|
320
|
-
await using context = await
|
|
322
|
+
await using context = await openContext();
|
|
321
323
|
const { db } = context;
|
|
322
324
|
const syncRuleContent = `
|
|
323
325
|
bucket_definitions:
|
|
@@ -348,7 +350,7 @@ bucket_definitions:
|
|
|
348
350
|
});
|
|
349
351
|
|
|
350
352
|
test('initial sync', async () => {
|
|
351
|
-
await using context = await
|
|
353
|
+
await using context = await openContext();
|
|
352
354
|
const { db } = context;
|
|
353
355
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
354
356
|
|
|
@@ -373,7 +375,7 @@ bucket_definitions:
|
|
|
373
375
|
// MongoServerError: PlanExecutor error during aggregation :: caused by :: BSONObj size: 33554925 (0x20001ED) is invalid.
|
|
374
376
|
// Size must be between 0 and 16793600(16MB)
|
|
375
377
|
|
|
376
|
-
await using context = await
|
|
378
|
+
await using context = await openContext();
|
|
377
379
|
await context.updateSyncRules(`bucket_definitions:
|
|
378
380
|
global:
|
|
379
381
|
data:
|
|
@@ -422,7 +424,7 @@ bucket_definitions:
|
|
|
422
424
|
});
|
|
423
425
|
|
|
424
426
|
test('collection not in sync rules', async () => {
|
|
425
|
-
await using context = await
|
|
427
|
+
await using context = await openContext();
|
|
426
428
|
const { db } = context;
|
|
427
429
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
428
430
|
|
|
@@ -439,7 +441,7 @@ bucket_definitions:
|
|
|
439
441
|
});
|
|
440
442
|
|
|
441
443
|
test('postImages - new collection with postImages enabled', async () => {
|
|
442
|
-
await using context = await
|
|
444
|
+
await using context = await openContext({
|
|
443
445
|
mongoOptions: { postImages: PostImagesOption.AUTO_CONFIGURE }
|
|
444
446
|
});
|
|
445
447
|
const { db } = context;
|
|
@@ -472,7 +474,7 @@ bucket_definitions:
|
|
|
472
474
|
});
|
|
473
475
|
|
|
474
476
|
test('postImages - new collection with postImages disabled', async () => {
|
|
475
|
-
await using context = await
|
|
477
|
+
await using context = await openContext({
|
|
476
478
|
mongoOptions: { postImages: PostImagesOption.AUTO_CONFIGURE }
|
|
477
479
|
});
|
|
478
480
|
const { db } = context;
|
|
@@ -502,7 +504,7 @@ bucket_definitions:
|
|
|
502
504
|
});
|
|
503
505
|
|
|
504
506
|
test('recover from error', async () => {
|
|
505
|
-
await using context = await
|
|
507
|
+
await using context = await openContext();
|
|
506
508
|
const { db } = context;
|
|
507
509
|
await context.updateSyncRules(`
|
|
508
510
|
bucket_definitions:
|
|
@@ -4,13 +4,18 @@ import {
|
|
|
4
4
|
createCoreReplicationMetrics,
|
|
5
5
|
initializeCoreReplicationMetrics,
|
|
6
6
|
InternalOpId,
|
|
7
|
+
LEGACY_STORAGE_VERSION,
|
|
7
8
|
OplogEntry,
|
|
8
9
|
ProtocolOpId,
|
|
9
10
|
ReplicationCheckpoint,
|
|
11
|
+
storage,
|
|
12
|
+
STORAGE_VERSION_CONFIG,
|
|
10
13
|
SyncRulesBucketStorage,
|
|
11
|
-
TestStorageOptions
|
|
14
|
+
TestStorageOptions,
|
|
15
|
+
updateSyncRulesFromYaml,
|
|
16
|
+
utils
|
|
12
17
|
} from '@powersync/service-core';
|
|
13
|
-
import { METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
|
|
18
|
+
import { bucketRequest, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
|
|
14
19
|
|
|
15
20
|
import { ChangeStream, ChangeStreamOptions } from '@module/replication/ChangeStream.js';
|
|
16
21
|
import { MongoManager } from '@module/replication/MongoManager.js';
|
|
@@ -22,7 +27,9 @@ import { clearTestDb, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
|
22
27
|
export class ChangeStreamTestContext {
|
|
23
28
|
private _walStream?: ChangeStream;
|
|
24
29
|
private abortController = new AbortController();
|
|
25
|
-
private streamPromise?: Promise<void
|
|
30
|
+
private streamPromise?: Promise<PromiseSettledResult<void>>;
|
|
31
|
+
private syncRulesId?: number;
|
|
32
|
+
private syncRulesContent?: storage.PersistedSyncRulesContent;
|
|
26
33
|
public storage?: SyncRulesBucketStorage;
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -35,6 +42,7 @@ export class ChangeStreamTestContext {
|
|
|
35
42
|
factory: (options: TestStorageOptions) => Promise<BucketStorageFactory>,
|
|
36
43
|
options?: {
|
|
37
44
|
doNotClear?: boolean;
|
|
45
|
+
storageVersion?: number;
|
|
38
46
|
mongoOptions?: Partial<NormalizedMongoConnectionConfig>;
|
|
39
47
|
streamOptions?: Partial<ChangeStreamOptions>;
|
|
40
48
|
}
|
|
@@ -45,13 +53,19 @@ export class ChangeStreamTestContext {
|
|
|
45
53
|
if (!options?.doNotClear) {
|
|
46
54
|
await clearTestDb(connectionManager.db);
|
|
47
55
|
}
|
|
48
|
-
|
|
56
|
+
|
|
57
|
+
const storageVersion = options?.storageVersion ?? LEGACY_STORAGE_VERSION;
|
|
58
|
+
const versionedBuckets = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false;
|
|
59
|
+
|
|
60
|
+
return new ChangeStreamTestContext(f, connectionManager, options?.streamOptions, storageVersion, versionedBuckets);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
constructor(
|
|
52
64
|
public factory: BucketStorageFactory,
|
|
53
65
|
public connectionManager: MongoManager,
|
|
54
|
-
private streamOptions
|
|
66
|
+
private streamOptions: Partial<ChangeStreamOptions> = {},
|
|
67
|
+
private storageVersion: number = LEGACY_STORAGE_VERSION,
|
|
68
|
+
private versionedBuckets: boolean = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false
|
|
55
69
|
) {
|
|
56
70
|
createCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
57
71
|
initializeCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
@@ -88,7 +102,11 @@ export class ChangeStreamTestContext {
|
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
async updateSyncRules(content: string) {
|
|
91
|
-
const syncRules = await this.factory.updateSyncRules(
|
|
105
|
+
const syncRules = await this.factory.updateSyncRules(
|
|
106
|
+
updateSyncRulesFromYaml(content, { validate: true, storageVersion: this.storageVersion })
|
|
107
|
+
);
|
|
108
|
+
this.syncRulesId = syncRules.id;
|
|
109
|
+
this.syncRulesContent = syncRules;
|
|
92
110
|
this.storage = this.factory.getInstance(syncRules);
|
|
93
111
|
return this.storage!;
|
|
94
112
|
}
|
|
@@ -99,11 +117,20 @@ export class ChangeStreamTestContext {
|
|
|
99
117
|
throw new Error(`Next sync rules not available`);
|
|
100
118
|
}
|
|
101
119
|
|
|
120
|
+
this.syncRulesId = syncRules.id;
|
|
121
|
+
this.syncRulesContent = syncRules;
|
|
102
122
|
this.storage = this.factory.getInstance(syncRules);
|
|
103
123
|
return this.storage!;
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
|
|
126
|
+
private getSyncRulesContent(): storage.PersistedSyncRulesContent {
|
|
127
|
+
if (this.syncRulesContent == null) {
|
|
128
|
+
throw new Error('Sync rules not configured - call updateSyncRules() first');
|
|
129
|
+
}
|
|
130
|
+
return this.syncRulesContent;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get streamer() {
|
|
107
134
|
if (this.storage == null) {
|
|
108
135
|
throw new Error('updateSyncRules() first');
|
|
109
136
|
}
|
|
@@ -125,7 +152,7 @@ export class ChangeStreamTestContext {
|
|
|
125
152
|
}
|
|
126
153
|
|
|
127
154
|
async replicateSnapshot() {
|
|
128
|
-
await this.
|
|
155
|
+
await this.streamer.initReplication();
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
/**
|
|
@@ -143,13 +170,21 @@ export class ChangeStreamTestContext {
|
|
|
143
170
|
}
|
|
144
171
|
|
|
145
172
|
startStreaming() {
|
|
146
|
-
|
|
173
|
+
this.streamPromise = this.streamer
|
|
174
|
+
.streamChanges()
|
|
175
|
+
.then(() => ({ status: 'fulfilled', value: undefined }) satisfies PromiseFulfilledResult<void>)
|
|
176
|
+
.catch((reason) => ({ status: 'rejected', reason }) satisfies PromiseRejectedResult);
|
|
177
|
+
return this.streamPromise;
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
async getCheckpoint(options?: { timeout?: number }) {
|
|
150
181
|
let checkpoint = await Promise.race([
|
|
151
182
|
getClientCheckpoint(this.client, this.db, this.factory, { timeout: options?.timeout ?? 15_000 }),
|
|
152
|
-
this.streamPromise
|
|
183
|
+
this.streamPromise?.then((e) => {
|
|
184
|
+
if (e.status == 'rejected') {
|
|
185
|
+
throw e.reason;
|
|
186
|
+
}
|
|
187
|
+
})
|
|
153
188
|
]);
|
|
154
189
|
if (checkpoint == null) {
|
|
155
190
|
// This indicates an issue with the test setup - streamingPromise completed instead
|
|
@@ -161,7 +196,8 @@ export class ChangeStreamTestContext {
|
|
|
161
196
|
|
|
162
197
|
async getBucketsDataBatch(buckets: Record<string, InternalOpId>, options?: { timeout?: number }) {
|
|
163
198
|
let checkpoint = await this.getCheckpoint(options);
|
|
164
|
-
const
|
|
199
|
+
const syncRules = this.getSyncRulesContent();
|
|
200
|
+
const map = Object.entries(buckets).map(([bucket, start]) => bucketRequest(syncRules, bucket, start));
|
|
165
201
|
return test_utils.fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
|
|
166
202
|
}
|
|
167
203
|
|
|
@@ -170,8 +206,9 @@ export class ChangeStreamTestContext {
|
|
|
170
206
|
if (typeof start == 'string') {
|
|
171
207
|
start = BigInt(start);
|
|
172
208
|
}
|
|
209
|
+
const syncRules = this.getSyncRulesContent();
|
|
173
210
|
const checkpoint = await this.getCheckpoint(options);
|
|
174
|
-
|
|
211
|
+
let map = [bucketRequest(syncRules, bucket, start)];
|
|
175
212
|
let data: OplogEntry[] = [];
|
|
176
213
|
while (true) {
|
|
177
214
|
const batch = this.storage!.getBucketDataBatch(checkpoint, map);
|
|
@@ -181,20 +218,27 @@ export class ChangeStreamTestContext {
|
|
|
181
218
|
if (batches.length == 0 || !batches[0]!.chunkData.has_more) {
|
|
182
219
|
break;
|
|
183
220
|
}
|
|
184
|
-
map
|
|
221
|
+
map = [bucketRequest(syncRules, bucket, BigInt(batches[0]!.chunkData.next_after))];
|
|
185
222
|
}
|
|
186
223
|
return data;
|
|
187
224
|
}
|
|
188
225
|
|
|
189
|
-
async getChecksums(buckets: string[], options?: { timeout?: number }) {
|
|
226
|
+
async getChecksums(buckets: string[], options?: { timeout?: number }): Promise<utils.ChecksumMap> {
|
|
190
227
|
let checkpoint = await this.getCheckpoint(options);
|
|
191
|
-
|
|
228
|
+
const syncRules = this.getSyncRulesContent();
|
|
229
|
+
const versionedBuckets = buckets.map((bucket) => bucketRequest(syncRules, bucket, 0n));
|
|
230
|
+
const checksums = await this.storage!.getChecksums(checkpoint, versionedBuckets);
|
|
231
|
+
|
|
232
|
+
const unversioned: utils.ChecksumMap = new Map();
|
|
233
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
234
|
+
unversioned.set(buckets[i], checksums.get(versionedBuckets[i].bucket)!);
|
|
235
|
+
}
|
|
236
|
+
return unversioned;
|
|
192
237
|
}
|
|
193
238
|
|
|
194
239
|
async getChecksum(bucket: string, options?: { timeout?: number }) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return map.get(bucket);
|
|
240
|
+
const checksums = await this.getChecksums([bucket], options);
|
|
241
|
+
return checksums.get(bucket);
|
|
198
242
|
}
|
|
199
243
|
}
|
|
200
244
|
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { reduceBucket
|
|
2
|
+
import { reduceBucket } from '@powersync/service-core';
|
|
3
3
|
import { METRICS_HELPER } from '@powersync/service-core-tests';
|
|
4
4
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
5
5
|
import { SqliteJsonValue } from '@powersync/service-sync-rules';
|
|
6
6
|
import * as timers from 'timers/promises';
|
|
7
7
|
import { describe, expect, test } from 'vitest';
|
|
8
8
|
import { ChangeStreamTestContext } from './change_stream_utils.js';
|
|
9
|
-
import { describeWithStorage } from './util.js';
|
|
9
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
10
10
|
|
|
11
11
|
describe('chunked snapshots', () => {
|
|
12
12
|
describeWithStorage({ timeout: 120_000 }, defineBatchTests);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
function defineBatchTests(factory:
|
|
15
|
+
function defineBatchTests({ factory, storageVersion }: StorageVersionTestContext) {
|
|
16
|
+
const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
|
|
17
|
+
return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
|
|
18
|
+
};
|
|
19
|
+
|
|
16
20
|
// This is not as sensitive to the id type as postgres, but we still test a couple of cases
|
|
17
21
|
test('chunked snapshot (int32)', async () => {
|
|
18
22
|
await testChunkedSnapshot({
|
|
@@ -93,7 +97,7 @@ function defineBatchTests(factory: TestStorageFactory) {
|
|
|
93
97
|
const idToSqlite = options.idToSqlite ?? ((n) => n);
|
|
94
98
|
const idToString = (id: any) => String(idToSqlite(id));
|
|
95
99
|
|
|
96
|
-
await using context = await
|
|
100
|
+
await using context = await openContext({
|
|
97
101
|
// We need to use a smaller chunk size here, so that we can run a query in between chunks
|
|
98
102
|
streamOptions: { snapshotChunkLength: 100 }
|
|
99
103
|
});
|
package/test/src/resume.test.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { ChangeStreamInvalidatedError } from '@module/replication/ChangeStream.js';
|
|
2
2
|
import { MongoManager } from '@module/replication/MongoManager.js';
|
|
3
3
|
import { normalizeConnectionConfig } from '@module/types/types.js';
|
|
4
|
-
import { BucketStorageFactory, TestStorageOptions } from '@powersync/service-core';
|
|
5
4
|
import { describe, expect, test } from 'vitest';
|
|
6
5
|
import { ChangeStreamTestContext } from './change_stream_utils.js';
|
|
7
6
|
import { env } from './env.js';
|
|
8
|
-
import { describeWithStorage } from './util.js';
|
|
7
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
9
8
|
|
|
10
9
|
describe('mongodb resuming replication', () => {
|
|
11
10
|
describeWithStorage({}, defineResumeTest);
|
|
12
11
|
});
|
|
13
12
|
|
|
14
|
-
function defineResumeTest(
|
|
13
|
+
function defineResumeTest({ factory: factoryGenerator, storageVersion }: StorageVersionTestContext) {
|
|
14
|
+
const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
|
|
15
|
+
return ChangeStreamTestContext.open(factoryGenerator, { ...options, storageVersion });
|
|
16
|
+
};
|
|
17
|
+
|
|
15
18
|
test('resuming with a different source database', async () => {
|
|
16
|
-
await using context = await
|
|
19
|
+
await using context = await openContext();
|
|
17
20
|
const { db } = context;
|
|
18
21
|
|
|
19
22
|
await context.updateSyncRules(/* yaml */
|
|
@@ -53,13 +56,14 @@ function defineResumeTest(factoryGenerator: (options?: TestStorageOptions) => Pr
|
|
|
53
56
|
const factory = await factoryGenerator({ doNotClear: true });
|
|
54
57
|
|
|
55
58
|
// Create a new context without updating the sync rules
|
|
56
|
-
await using context2 = new ChangeStreamTestContext(factory, connectionManager);
|
|
59
|
+
await using context2 = new ChangeStreamTestContext(factory, connectionManager, {}, storageVersion);
|
|
57
60
|
const activeContent = await factory.getActiveSyncRulesContent();
|
|
58
61
|
context2.storage = factory.getInstance(activeContent!);
|
|
59
62
|
|
|
60
63
|
// If this test times out, it likely didn't throw the expected error here.
|
|
61
|
-
const
|
|
64
|
+
const result = await context2.startStreaming();
|
|
62
65
|
// The ChangeStreamReplicationJob will detect this and throw a ChangeStreamInvalidatedError
|
|
63
|
-
expect(
|
|
66
|
+
expect(result.status).toEqual('rejected');
|
|
67
|
+
expect((result as PromiseRejectedResult).reason).toBeInstanceOf(ChangeStreamInvalidatedError);
|
|
64
68
|
});
|
|
65
69
|
}
|
|
@@ -8,19 +8,19 @@ import { env } from './env.js';
|
|
|
8
8
|
import { describeWithStorage } from './util.js';
|
|
9
9
|
|
|
10
10
|
describe.skipIf(!(env.CI || env.SLOW_TESTS))('batch replication', function () {
|
|
11
|
-
describeWithStorage({ timeout: 240_000 }, function (factory) {
|
|
11
|
+
describeWithStorage({ timeout: 240_000 }, function ({ factory, storageVersion }) {
|
|
12
12
|
test('resuming initial replication (1)', async () => {
|
|
13
13
|
// Stop early - likely to not include deleted row in first replication attempt.
|
|
14
|
-
await testResumingReplication(factory, 2000);
|
|
14
|
+
await testResumingReplication(factory, storageVersion, 2000);
|
|
15
15
|
});
|
|
16
16
|
test('resuming initial replication (2)', async () => {
|
|
17
17
|
// Stop late - likely to include deleted row in first replication attempt.
|
|
18
|
-
await testResumingReplication(factory, 8000);
|
|
18
|
+
await testResumingReplication(factory, storageVersion, 8000);
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
async function testResumingReplication(factory: TestStorageFactory, stopAfter: number) {
|
|
23
|
+
async function testResumingReplication(factory: TestStorageFactory, storageVersion: number, stopAfter: number) {
|
|
24
24
|
// This tests interrupting and then resuming initial replication.
|
|
25
25
|
// We interrupt replication after test_data1 has fully replicated, and
|
|
26
26
|
// test_data2 has partially replicated.
|
|
@@ -35,7 +35,10 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
35
35
|
let startRowCount: number;
|
|
36
36
|
|
|
37
37
|
{
|
|
38
|
-
await using context = await ChangeStreamTestContext.open(factory, {
|
|
38
|
+
await using context = await ChangeStreamTestContext.open(factory, {
|
|
39
|
+
storageVersion,
|
|
40
|
+
streamOptions: { snapshotChunkLength: 1000 }
|
|
41
|
+
});
|
|
39
42
|
|
|
40
43
|
await context.updateSyncRules(`bucket_definitions:
|
|
41
44
|
global:
|
|
@@ -87,6 +90,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
87
90
|
// Bypass the usual "clear db on factory open" step.
|
|
88
91
|
await using context2 = await ChangeStreamTestContext.open(factory, {
|
|
89
92
|
doNotClear: true,
|
|
93
|
+
storageVersion,
|
|
90
94
|
streamOptions: { snapshotChunkLength: 1000 }
|
|
91
95
|
});
|
|
92
96
|
|
|
@@ -2,19 +2,21 @@ import { setTimeout } from 'node:timers/promises';
|
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
5
|
-
import { storage } from '@powersync/service-core';
|
|
6
|
-
|
|
7
5
|
import { ChangeStreamTestContext, setSnapshotHistorySeconds } from './change_stream_utils.js';
|
|
8
6
|
import { env } from './env.js';
|
|
9
|
-
import { describeWithStorage } from './util.js';
|
|
7
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
10
8
|
|
|
11
9
|
describe.runIf(env.CI || env.SLOW_TESTS)('change stream slow tests', { timeout: 60_000 }, function () {
|
|
12
10
|
describeWithStorage({}, defineSlowTests);
|
|
13
11
|
});
|
|
14
12
|
|
|
15
|
-
function defineSlowTests(factory:
|
|
13
|
+
function defineSlowTests({ factory, storageVersion }: StorageVersionTestContext) {
|
|
14
|
+
const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
|
|
15
|
+
return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
|
|
16
|
+
};
|
|
17
|
+
|
|
16
18
|
test('replicating snapshot with lots of data', async () => {
|
|
17
|
-
await using context = await
|
|
19
|
+
await using context = await openContext();
|
|
18
20
|
// Test with low minSnapshotHistoryWindowInSeconds, to trigger:
|
|
19
21
|
// > Read timestamp .. is older than the oldest available timestamp.
|
|
20
22
|
// This happened when we had {snapshot: true} in the initial
|
|
@@ -52,7 +54,7 @@ bucket_definitions:
|
|
|
52
54
|
// changestream), we may miss updates, which this test would
|
|
53
55
|
// hopefully catch.
|
|
54
56
|
|
|
55
|
-
await using context = await
|
|
57
|
+
await using context = await openContext();
|
|
56
58
|
const { db } = context;
|
|
57
59
|
await context.updateSyncRules(`
|
|
58
60
|
bucket_definitions:
|
package/test/src/util.ts
CHANGED
|
@@ -3,9 +3,14 @@ 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
|
-
|
|
6
|
+
import {
|
|
7
|
+
BSON_DESERIALIZE_DATA_OPTIONS,
|
|
8
|
+
SUPPORTED_STORAGE_VERSIONS,
|
|
9
|
+
TestStorageConfig,
|
|
10
|
+
TestStorageFactory
|
|
11
|
+
} from '@powersync/service-core';
|
|
8
12
|
import { describe, TestOptions } from 'vitest';
|
|
13
|
+
import { env } from './env.js';
|
|
9
14
|
|
|
10
15
|
export const TEST_URI = env.MONGO_TEST_DATA_URL;
|
|
11
16
|
|
|
@@ -19,18 +24,38 @@ export const INITIALIZED_MONGO_STORAGE_FACTORY = mongo_storage.test_utils.mongoT
|
|
|
19
24
|
isCI: env.CI
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.
|
|
27
|
+
export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.postgresTestSetup({
|
|
23
28
|
url: env.PG_STORAGE_TEST_URL
|
|
24
29
|
});
|
|
25
30
|
|
|
26
|
-
export
|
|
27
|
-
describe.skipIf(!env.TEST_MONGO_STORAGE)(`mongodb storage`, options, function () {
|
|
28
|
-
fn(INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
29
|
-
});
|
|
31
|
+
export const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS;
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
export interface StorageVersionTestContext {
|
|
34
|
+
factory: TestStorageFactory;
|
|
35
|
+
storageVersion: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function describeWithStorage(options: TestOptions, fn: (context: StorageVersionTestContext) => void) {
|
|
39
|
+
const describeFactory = (storageName: string, config: TestStorageConfig) => {
|
|
40
|
+
describe(`${storageName} storage`, options, function () {
|
|
41
|
+
for (const storageVersion of TEST_STORAGE_VERSIONS) {
|
|
42
|
+
describe(`storage v${storageVersion}`, function () {
|
|
43
|
+
fn({
|
|
44
|
+
factory: config.factory,
|
|
45
|
+
storageVersion
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (env.TEST_MONGO_STORAGE) {
|
|
53
|
+
describeFactory('mongodb', INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (env.TEST_POSTGRES_STORAGE) {
|
|
57
|
+
describeFactory('postgres', INITIALIZED_POSTGRES_STORAGE_FACTORY);
|
|
58
|
+
}
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
export async function clearTestDb(db: mongo.Db) {
|