@powersync/service-module-mongodb 0.4.1 → 0.5.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/api/MongoRouteAPIAdapter.js +5 -0
  3. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  4. package/dist/common/MongoLSN.d.ts +31 -0
  5. package/dist/common/MongoLSN.js +47 -0
  6. package/dist/common/MongoLSN.js.map +1 -0
  7. package/dist/module/MongoModule.js +2 -2
  8. package/dist/module/MongoModule.js.map +1 -1
  9. package/dist/replication/ChangeStream.d.ts +3 -3
  10. package/dist/replication/ChangeStream.js +66 -22
  11. package/dist/replication/ChangeStream.js.map +1 -1
  12. package/dist/replication/ChangeStreamReplicationJob.js +5 -4
  13. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  14. package/dist/replication/ChangeStreamReplicator.js +1 -0
  15. package/dist/replication/ChangeStreamReplicator.js.map +1 -1
  16. package/dist/replication/ConnectionManagerFactory.js +2 -0
  17. package/dist/replication/ConnectionManagerFactory.js.map +1 -1
  18. package/dist/replication/MongoErrorRateLimiter.js +5 -7
  19. package/dist/replication/MongoErrorRateLimiter.js.map +1 -1
  20. package/dist/replication/MongoManager.js +10 -4
  21. package/dist/replication/MongoManager.js.map +1 -1
  22. package/dist/replication/MongoRelation.d.ts +0 -2
  23. package/dist/replication/MongoRelation.js +3 -15
  24. package/dist/replication/MongoRelation.js.map +1 -1
  25. package/package.json +9 -9
  26. package/src/common/MongoLSN.ts +74 -0
  27. package/src/replication/ChangeStream.ts +65 -28
  28. package/src/replication/ChangeStreamReplicationJob.ts +4 -4
  29. package/src/replication/MongoRelation.ts +3 -17
  30. package/test/src/change_stream_utils.ts +2 -2
  31. package/test/src/resume.test.ts +152 -0
  32. package/tsconfig.tsbuildinfo +1 -1
@@ -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
+ }