@powersync/service-module-mongodb 0.4.2 → 0.5.1
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 +27 -0
- package/dist/api/MongoRouteAPIAdapter.d.ts +2 -1
- package/dist/api/MongoRouteAPIAdapter.js +39 -0
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/common/MongoLSN.d.ts +31 -0
- package/dist/common/MongoLSN.js +47 -0
- package/dist/common/MongoLSN.js.map +1 -0
- package/dist/module/MongoModule.js +2 -2
- package/dist/module/MongoModule.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +3 -3
- package/dist/replication/ChangeStream.js +66 -22
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js +5 -4
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/ChangeStreamReplicator.js +1 -0
- package/dist/replication/ChangeStreamReplicator.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.js +2 -0
- package/dist/replication/ConnectionManagerFactory.js.map +1 -1
- package/dist/replication/MongoErrorRateLimiter.js +5 -7
- package/dist/replication/MongoErrorRateLimiter.js.map +1 -1
- package/dist/replication/MongoManager.js +10 -4
- package/dist/replication/MongoManager.js.map +1 -1
- package/dist/replication/MongoRelation.d.ts +0 -2
- package/dist/replication/MongoRelation.js +3 -15
- package/dist/replication/MongoRelation.js.map +1 -1
- package/package.json +10 -10
- package/src/api/MongoRouteAPIAdapter.ts +41 -1
- package/src/common/MongoLSN.ts +74 -0
- package/src/replication/ChangeStream.ts +65 -28
- package/src/replication/ChangeStreamReplicationJob.ts +4 -4
- package/src/replication/MongoRelation.ts +3 -17
- package/test/src/change_stream_utils.ts +2 -2
- package/test/src/resume.test.ts +152 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -58,7 +58,7 @@ export class ChangeStreamTestContext {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async updateSyncRules(content: string) {
|
|
61
|
-
const syncRules = await this.factory.updateSyncRules({ content: content });
|
|
61
|
+
const syncRules = await this.factory.updateSyncRules({ content: content, validate: true });
|
|
62
62
|
this.storage = this.factory.getInstance(syncRules);
|
|
63
63
|
return this.storage!;
|
|
64
64
|
}
|
|
@@ -85,7 +85,7 @@ export class ChangeStreamTestContext {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
startStreaming() {
|
|
88
|
-
this.streamPromise = this.walStream.streamChanges();
|
|
88
|
+
return (this.streamPromise = this.walStream.streamChanges());
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
async getCheckpoint(options?: { timeout?: number }) {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { MongoLSN, ZERO_LSN } from '@module/common/MongoLSN.js';
|
|
2
|
+
|
|
3
|
+
import { MongoManager } from '@module/replication/MongoManager.js';
|
|
4
|
+
import { normalizeConnectionConfig } from '@module/types/types.js';
|
|
5
|
+
import { isMongoServerError, mongo } from '@powersync/lib-service-mongodb';
|
|
6
|
+
import { BucketStorageFactory, TestStorageOptions } from '@powersync/service-core';
|
|
7
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
8
|
+
import { ChangeStreamTestContext } from './change_stream_utils.js';
|
|
9
|
+
import { env } from './env.js';
|
|
10
|
+
import { INITIALIZED_MONGO_STORAGE_FACTORY, INITIALIZED_POSTGRES_STORAGE_FACTORY } from './util.js';
|
|
11
|
+
|
|
12
|
+
describe('mongo lsn', () => {
|
|
13
|
+
test('LSN with resume tokens should be comparable', () => {
|
|
14
|
+
// Values without a resume token should be comparable
|
|
15
|
+
expect(
|
|
16
|
+
new MongoLSN({
|
|
17
|
+
timestamp: mongo.Timestamp.fromNumber(1)
|
|
18
|
+
}).comparable <
|
|
19
|
+
new MongoLSN({
|
|
20
|
+
timestamp: mongo.Timestamp.fromNumber(10)
|
|
21
|
+
}).comparable
|
|
22
|
+
).true;
|
|
23
|
+
|
|
24
|
+
// Values with resume tokens should correctly compare
|
|
25
|
+
expect(
|
|
26
|
+
new MongoLSN({
|
|
27
|
+
timestamp: mongo.Timestamp.fromNumber(1),
|
|
28
|
+
resume_token: { _data: 'resume1' }
|
|
29
|
+
}).comparable <
|
|
30
|
+
new MongoLSN({
|
|
31
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
32
|
+
resume_token: { _data: 'resume2' }
|
|
33
|
+
}).comparable
|
|
34
|
+
).true;
|
|
35
|
+
|
|
36
|
+
// The resume token should not affect comparison
|
|
37
|
+
expect(
|
|
38
|
+
new MongoLSN({
|
|
39
|
+
timestamp: mongo.Timestamp.fromNumber(1),
|
|
40
|
+
resume_token: { _data: '2' }
|
|
41
|
+
}).comparable <
|
|
42
|
+
new MongoLSN({
|
|
43
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
44
|
+
resume_token: { _data: '1' }
|
|
45
|
+
}).comparable
|
|
46
|
+
).true;
|
|
47
|
+
|
|
48
|
+
// Resume token should not be required for comparison
|
|
49
|
+
expect(
|
|
50
|
+
new MongoLSN({
|
|
51
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
52
|
+
resume_token: { _data: '2' }
|
|
53
|
+
}).comparable > // Switching the order to test this case
|
|
54
|
+
new MongoLSN({
|
|
55
|
+
timestamp: mongo.Timestamp.fromNumber(9)
|
|
56
|
+
}).comparable
|
|
57
|
+
).true;
|
|
58
|
+
|
|
59
|
+
// Comparison should be backwards compatible with old LSNs
|
|
60
|
+
expect(
|
|
61
|
+
new MongoLSN({
|
|
62
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
63
|
+
resume_token: { _data: '2' }
|
|
64
|
+
}).comparable > ZERO_LSN
|
|
65
|
+
).true;
|
|
66
|
+
expect(
|
|
67
|
+
new MongoLSN({
|
|
68
|
+
timestamp: mongo.Timestamp.fromNumber(10),
|
|
69
|
+
resume_token: { _data: '2' }
|
|
70
|
+
}).comparable >
|
|
71
|
+
new MongoLSN({
|
|
72
|
+
timestamp: mongo.Timestamp.fromNumber(1)
|
|
73
|
+
}).comparable.split('|')[0] // Simulate an old LSN
|
|
74
|
+
).true;
|
|
75
|
+
expect(
|
|
76
|
+
new MongoLSN({
|
|
77
|
+
timestamp: mongo.Timestamp.fromNumber(1),
|
|
78
|
+
resume_token: { _data: '2' }
|
|
79
|
+
}).comparable <
|
|
80
|
+
new MongoLSN({
|
|
81
|
+
timestamp: mongo.Timestamp.fromNumber(10)
|
|
82
|
+
}).comparable.split('|')[0] // Simulate an old LSN
|
|
83
|
+
).true;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe.skipIf(!env.TEST_MONGO_STORAGE)('MongoDB resume - mongo storage', () => {
|
|
88
|
+
defineResumeTest(INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe.skipIf(!env.TEST_POSTGRES_STORAGE)('MongoDB resume - postgres storage', () => {
|
|
92
|
+
defineResumeTest(INITIALIZED_POSTGRES_STORAGE_FACTORY);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function defineResumeTest(factoryGenerator: (options?: TestStorageOptions) => Promise<BucketStorageFactory>) {
|
|
96
|
+
test('resuming with a different source database', async () => {
|
|
97
|
+
await using context = await ChangeStreamTestContext.open(factoryGenerator);
|
|
98
|
+
const { db } = context;
|
|
99
|
+
|
|
100
|
+
await context.updateSyncRules(/* yaml */
|
|
101
|
+
` bucket_definitions:
|
|
102
|
+
global:
|
|
103
|
+
data:
|
|
104
|
+
- SELECT _id as id, description, num FROM "test_data"`);
|
|
105
|
+
|
|
106
|
+
await context.replicateSnapshot();
|
|
107
|
+
|
|
108
|
+
context.startStreaming();
|
|
109
|
+
|
|
110
|
+
const collection = db.collection('test_data');
|
|
111
|
+
await collection.insertOne({ description: 'test1', num: 1152921504606846976n });
|
|
112
|
+
|
|
113
|
+
// Wait for the item above to be replicated. The commit should store a resume token.
|
|
114
|
+
await vi.waitFor(
|
|
115
|
+
async () => {
|
|
116
|
+
const checkpoint = await context.storage?.getCheckpoint();
|
|
117
|
+
expect(MongoLSN.fromSerialized(checkpoint!.lsn!).resumeToken).exist;
|
|
118
|
+
},
|
|
119
|
+
{ timeout: 5000 }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Done with this context for now
|
|
123
|
+
await context.dispose();
|
|
124
|
+
|
|
125
|
+
// Use the provided MongoDB url to connect to a different source database
|
|
126
|
+
const originalUrl = env.MONGO_TEST_URL;
|
|
127
|
+
// Change this to a different database
|
|
128
|
+
const url = new URL(originalUrl);
|
|
129
|
+
const parts = url.pathname.split('/');
|
|
130
|
+
parts[1] = 'differentDB'; // Replace the database name
|
|
131
|
+
url.pathname = parts.join('/');
|
|
132
|
+
|
|
133
|
+
// Point to a new source DB
|
|
134
|
+
const connectionManager = new MongoManager(
|
|
135
|
+
normalizeConnectionConfig({
|
|
136
|
+
type: 'mongodb',
|
|
137
|
+
uri: url.toString()
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
const factory = await factoryGenerator({ doNotClear: true });
|
|
141
|
+
|
|
142
|
+
// Create a new context without updating the sync rules
|
|
143
|
+
await using context2 = new ChangeStreamTestContext(factory, connectionManager);
|
|
144
|
+
const activeContent = await factory.getActiveSyncRulesContent();
|
|
145
|
+
context2.storage = factory.getInstance(activeContent!);
|
|
146
|
+
|
|
147
|
+
const error = await context2.startStreaming().catch((ex) => ex);
|
|
148
|
+
expect(error).exist;
|
|
149
|
+
// The ChangeStreamReplicationJob will detect this and throw a ChangeStreamInvalidatedError
|
|
150
|
+
expect(isMongoServerError(error) && error.hasErrorLabel('NonResumableChangeStreamError'));
|
|
151
|
+
});
|
|
152
|
+
}
|