@powersync/service-module-mongodb 0.10.4 → 0.12.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 +61 -0
- package/LICENSE +3 -3
- package/dist/api/MongoRouteAPIAdapter.js +18 -2
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +15 -3
- package/dist/replication/ChangeStream.js +68 -22
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/MongoRelation.d.ts +3 -3
- package/dist/replication/MongoRelation.js +16 -15
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/replication-utils.js +3 -3
- package/dist/replication/replication-utils.js.map +1 -1
- package/package.json +11 -11
- package/src/api/MongoRouteAPIAdapter.ts +19 -19
- package/src/replication/ChangeStream.ts +74 -26
- package/src/replication/MongoRelation.ts +28 -18
- package/src/replication/replication-utils.ts +3 -3
- package/test/src/change_stream.test.ts +11 -6
- package/test/src/change_stream_utils.ts +20 -8
- package/test/src/mongo_test.test.ts +58 -16
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { storage } from '@powersync/service-core';
|
|
3
|
-
import {
|
|
3
|
+
import { JsonContainer } from '@powersync/service-jsonbig';
|
|
4
|
+
import { CustomArray, CustomObject, CustomSqliteValue, DateTimeValue } from '@powersync/service-sync-rules';
|
|
4
5
|
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
5
6
|
import { MongoLSN } from '../common/MongoLSN.js';
|
|
6
7
|
import { CHECKPOINTS_COLLECTION } from './replication-utils.js';
|
|
@@ -10,7 +11,7 @@ export function getMongoRelation(source) {
|
|
|
10
11
|
schema: source.db,
|
|
11
12
|
// Not relevant for MongoDB - we use db + coll name as the identifier
|
|
12
13
|
objectId: undefined,
|
|
13
|
-
|
|
14
|
+
replicaIdColumns: [{ name: '_id' }]
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
@@ -18,7 +19,7 @@ export function getMongoRelation(source) {
|
|
|
18
19
|
*/
|
|
19
20
|
export function getCacheIdentifier(source) {
|
|
20
21
|
if (source instanceof storage.SourceTable) {
|
|
21
|
-
return `${source.schema}.${source.
|
|
22
|
+
return `${source.schema}.${source.name}`;
|
|
22
23
|
}
|
|
23
24
|
return `${source.schema}.${source.name}`;
|
|
24
25
|
}
|
|
@@ -63,7 +64,8 @@ export function toMongoSyncRulesValue(data) {
|
|
|
63
64
|
return data.toHexString();
|
|
64
65
|
}
|
|
65
66
|
else if (data instanceof Date) {
|
|
66
|
-
|
|
67
|
+
const isoString = data.toISOString();
|
|
68
|
+
return new DateTimeValue(isoString);
|
|
67
69
|
}
|
|
68
70
|
else if (data instanceof mongo.Binary) {
|
|
69
71
|
return new Uint8Array(data.buffer);
|
|
@@ -81,8 +83,7 @@ export function toMongoSyncRulesValue(data) {
|
|
|
81
83
|
return JSON.stringify({ pattern: data.source, options: data.flags });
|
|
82
84
|
}
|
|
83
85
|
else if (Array.isArray(data)) {
|
|
84
|
-
|
|
85
|
-
return JSONBig.stringify(data.map((element) => filterJsonData(element)));
|
|
86
|
+
return new CustomArray(data, filterJsonData);
|
|
86
87
|
}
|
|
87
88
|
else if (data instanceof Uint8Array) {
|
|
88
89
|
return data;
|
|
@@ -91,18 +92,14 @@ export function toMongoSyncRulesValue(data) {
|
|
|
91
92
|
return data.toString();
|
|
92
93
|
}
|
|
93
94
|
else if (typeof data == 'object') {
|
|
94
|
-
|
|
95
|
-
for (let key of Object.keys(data)) {
|
|
96
|
-
record[key] = filterJsonData(data[key]);
|
|
97
|
-
}
|
|
98
|
-
return JSONBig.stringify(record);
|
|
95
|
+
return new CustomObject(data, filterJsonData);
|
|
99
96
|
}
|
|
100
97
|
else {
|
|
101
98
|
return null;
|
|
102
99
|
}
|
|
103
100
|
}
|
|
104
101
|
const DEPTH_LIMIT = 20;
|
|
105
|
-
function filterJsonData(data, depth = 0) {
|
|
102
|
+
function filterJsonData(data, context, depth = 0) {
|
|
106
103
|
const autoBigNum = true;
|
|
107
104
|
if (depth > DEPTH_LIMIT) {
|
|
108
105
|
// This is primarily to prevent infinite recursion
|
|
@@ -135,7 +132,8 @@ function filterJsonData(data, depth = 0) {
|
|
|
135
132
|
return data;
|
|
136
133
|
}
|
|
137
134
|
else if (data instanceof Date) {
|
|
138
|
-
|
|
135
|
+
const isoString = data.toISOString();
|
|
136
|
+
return new DateTimeValue(isoString).toSqliteValue(context);
|
|
139
137
|
}
|
|
140
138
|
else if (data instanceof mongo.ObjectId) {
|
|
141
139
|
return data.toHexString();
|
|
@@ -159,11 +157,14 @@ function filterJsonData(data, depth = 0) {
|
|
|
159
157
|
return { pattern: data.source, options: data.flags };
|
|
160
158
|
}
|
|
161
159
|
else if (Array.isArray(data)) {
|
|
162
|
-
return data.map((element) => filterJsonData(element, depth + 1));
|
|
160
|
+
return data.map((element) => filterJsonData(element, context, depth + 1));
|
|
163
161
|
}
|
|
164
162
|
else if (ArrayBuffer.isView(data)) {
|
|
165
163
|
return undefined;
|
|
166
164
|
}
|
|
165
|
+
else if (data instanceof CustomSqliteValue) {
|
|
166
|
+
return data.toSqliteValue(context);
|
|
167
|
+
}
|
|
167
168
|
else if (data instanceof JsonContainer) {
|
|
168
169
|
// Can be stringified directly when using our JSONBig implementation
|
|
169
170
|
return data;
|
|
@@ -171,7 +172,7 @@ function filterJsonData(data, depth = 0) {
|
|
|
171
172
|
else if (typeof data == 'object') {
|
|
172
173
|
let record = {};
|
|
173
174
|
for (let key of Object.keys(data)) {
|
|
174
|
-
record[key] = filterJsonData(data[key], depth + 1);
|
|
175
|
+
record[key] = filterJsonData(data[key], context, depth + 1);
|
|
175
176
|
}
|
|
176
177
|
return record;
|
|
177
178
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MongoRelation.js","sourceRoot":"","sources":["../../src/replication/MongoRelation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"MongoRelation.js","sourceRoot":"","sources":["../../src/replication/MongoRelation.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,17 +32,17 @@ export async function checkSourceConfiguration(connectionManager) {
|
|
|
32
32
|
const missingCheckpointActions = REQUIRED_CHECKPOINT_PERMISSIONS.filter((action) => !checkpointsActions.has(action));
|
|
33
33
|
if (missingCheckpointActions.length > 0) {
|
|
34
34
|
const fullName = `${db.databaseName}.${CHECKPOINTS_COLLECTION}`;
|
|
35
|
-
throw new ServiceError(ErrorCode.PSYNC_S1307, `MongoDB user does not have the required ${missingCheckpointActions.map((a) => `"${a}"`).join(', ')}
|
|
35
|
+
throw new ServiceError(ErrorCode.PSYNC_S1307, `MongoDB user does not have the required ${missingCheckpointActions.map((a) => `"${a}"`).join(', ')} privilege(s) on "${fullName}".`);
|
|
36
36
|
}
|
|
37
37
|
if (connectionManager.options.postImages == PostImagesOption.AUTO_CONFIGURE) {
|
|
38
38
|
// This checks that we have collMod on _any_ collection in the db.
|
|
39
39
|
// This is not a complete check, but does give a basic sanity-check for testing the connection.
|
|
40
40
|
if (!anyCollectionActions.has('collMod')) {
|
|
41
|
-
throw new ServiceError(ErrorCode.PSYNC_S1307, `MongoDB user does not have the required "collMod"
|
|
41
|
+
throw new ServiceError(ErrorCode.PSYNC_S1307, `MongoDB user does not have the required "collMod" privilege on "${db.databaseName}", required for "post_images: auto_configure".`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
if (!anyCollectionActions.has('listCollections')) {
|
|
45
|
-
throw new ServiceError(ErrorCode.PSYNC_S1307, `MongoDB user does not have the required "listCollections"
|
|
45
|
+
throw new ServiceError(ErrorCode.PSYNC_S1307, `MongoDB user does not have the required "listCollections" privilege on "${db.databaseName}".`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-utils.js","sourceRoot":"","sources":["../../src/replication/replication-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAE5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGrD,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE/D,MAAM,+BAA+B,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,kBAAkB,CAAC,CAAC;AAEnH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,iBAA+B;IAC5E,MAAM,EAAE,GAAG,iBAAiB,CAAC,EAAE,CAAC;IAEhC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7C,IAAI,KAAK,CAAC,GAAG,IAAI,UAAU,EAAE,CAAC;QAC5B,MAAM,IAAI,YAAY,CACpB,SAAS,CAAC,WAAW,EACrB,0FAA0F,CAC3F,CAAC;IACJ,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,IAAI,YAAY,CAAC,SAAS,CAAC,WAAW,EAAE,oEAAoE,CAAC,CAAC;IACtH,CAAC;IAED,0EAA0E;IAC1E,MAAM,gBAAgB,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IACzF,MAAM,WAAW,GAAG,gBAAgB,CAAC,QAAQ,EAAE,2BAG5C,CAAC;IACJ,IAAI,kBAAkB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC3C,IAAI,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAC;IAC7C,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,YAAY,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACvG,MAAM,aAAa,GAAG,WAAW,CAAC,MAAM,CACtC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,IAAI,sBAAsB,IAAI,CAAC,CAAC,QAAQ,EAAE,UAAU,IAAI,EAAE,CACvF,CAAC;QAEF,KAAK,IAAI,CAAC,IAAI,aAAa,EAAE,CAAC;YAC5B,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;gBACxB,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,KAAK,IAAI,CAAC,IAAI,WAAW,EAAE,CAAC;YAC1B,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;gBACxB,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;QAED,MAAM,wBAAwB,GAAG,+BAA+B,CAAC,MAAM,CACrE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAC5C,CAAC;QACF,IAAI,wBAAwB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,YAAY,IAAI,sBAAsB,EAAE,CAAC;YAChE,MAAM,IAAI,YAAY,CACpB,SAAS,CAAC,WAAW,EACrB,2CAA2C,wBAAwB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"replication-utils.js","sourceRoot":"","sources":["../../src/replication/replication-utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"name": "@powersync/service-module-mongodb",
|
|
3
3
|
"repository": "https://github.com/powersync-ja/powersync-service",
|
|
4
4
|
"types": "dist/index.d.ts",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.12.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
-
"license": "FSL-1.1-
|
|
7
|
+
"license": "FSL-1.1-ALv2",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"bson": "^6.10.3",
|
|
26
26
|
"ts-codec": "^1.3.0",
|
|
27
27
|
"uuid": "^11.1.0",
|
|
28
|
-
"@powersync/lib-service-mongodb": "0.6.
|
|
29
|
-
"@powersync/lib-services-framework": "0.7.
|
|
30
|
-
"@powersync/service-core": "1.
|
|
31
|
-
"@powersync/service-jsonbig": "0.17.
|
|
32
|
-
"@powersync/service-sync-rules": "0.
|
|
33
|
-
"@powersync/service-types": "0.
|
|
28
|
+
"@powersync/lib-service-mongodb": "0.6.4",
|
|
29
|
+
"@powersync/lib-services-framework": "0.7.3",
|
|
30
|
+
"@powersync/service-core": "1.15.0",
|
|
31
|
+
"@powersync/service-jsonbig": "0.17.11",
|
|
32
|
+
"@powersync/service-sync-rules": "0.29.0",
|
|
33
|
+
"@powersync/service-types": "0.13.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@powersync/service-core-tests": "0.
|
|
37
|
-
"@powersync/service-module-mongodb-storage": "0.
|
|
38
|
-
"@powersync/service-module-postgres-storage": "0.
|
|
36
|
+
"@powersync/service-core-tests": "0.12.0",
|
|
37
|
+
"@powersync/service-module-mongodb-storage": "0.12.0",
|
|
38
|
+
"@powersync/service-module-postgres-storage": "0.10.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -b",
|
|
@@ -5,7 +5,7 @@ import * as sync_rules from '@powersync/service-sync-rules';
|
|
|
5
5
|
import * as service_types from '@powersync/service-types';
|
|
6
6
|
|
|
7
7
|
import { MongoManager } from '../replication/MongoManager.js';
|
|
8
|
-
import { constructAfterRecord,
|
|
8
|
+
import { constructAfterRecord, STANDALONE_CHECKPOINT_ID } from '../replication/MongoRelation.js';
|
|
9
9
|
import { CHECKPOINTS_COLLECTION } from '../replication/replication-utils.js';
|
|
10
10
|
import * as types from '../types/types.js';
|
|
11
11
|
import { escapeRegExp } from '../utils.js';
|
|
@@ -137,15 +137,15 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
|
|
|
137
137
|
if (tablePattern.isWildcard) {
|
|
138
138
|
patternResult.tables = [];
|
|
139
139
|
for (let collection of collections) {
|
|
140
|
-
const sourceTable = new SourceTable(
|
|
141
|
-
0,
|
|
142
|
-
this.connectionTag,
|
|
143
|
-
collection.name,
|
|
144
|
-
schema,
|
|
145
|
-
collection.name,
|
|
146
|
-
[],
|
|
147
|
-
true
|
|
148
|
-
);
|
|
140
|
+
const sourceTable = new SourceTable({
|
|
141
|
+
id: 0,
|
|
142
|
+
connectionTag: this.connectionTag,
|
|
143
|
+
objectId: collection.name,
|
|
144
|
+
schema: schema,
|
|
145
|
+
name: collection.name,
|
|
146
|
+
replicaIdColumns: [],
|
|
147
|
+
snapshotComplete: true
|
|
148
|
+
});
|
|
149
149
|
let errors: service_types.ReplicationError[] = [];
|
|
150
150
|
if (collection.type == 'view') {
|
|
151
151
|
errors.push({ level: 'warning', message: `Collection ${schema}.${tablePattern.name} is a view` });
|
|
@@ -164,15 +164,15 @@ export class MongoRouteAPIAdapter implements api.RouteAPI {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
} else {
|
|
167
|
-
const sourceTable = new SourceTable(
|
|
168
|
-
0,
|
|
169
|
-
this.connectionTag,
|
|
170
|
-
tablePattern.name,
|
|
171
|
-
schema,
|
|
172
|
-
tablePattern.name,
|
|
173
|
-
[],
|
|
174
|
-
true
|
|
175
|
-
);
|
|
167
|
+
const sourceTable = new SourceTable({
|
|
168
|
+
id: 0,
|
|
169
|
+
connectionTag: this.connectionTag,
|
|
170
|
+
objectId: tablePattern.name,
|
|
171
|
+
schema: schema,
|
|
172
|
+
name: tablePattern.name,
|
|
173
|
+
replicaIdColumns: [],
|
|
174
|
+
snapshotComplete: true
|
|
175
|
+
});
|
|
176
176
|
|
|
177
177
|
const syncData = sqlSyncRules.tableSyncsData(sourceTable);
|
|
178
178
|
const syncParameters = sqlSyncRules.tableSyncsParameters(sourceTable);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ServiceError
|
|
11
11
|
} from '@powersync/lib-services-framework';
|
|
12
12
|
import {
|
|
13
|
+
InternalOpId,
|
|
13
14
|
MetricsEngine,
|
|
14
15
|
RelationCache,
|
|
15
16
|
SaveOperationTag,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
SourceTable,
|
|
18
19
|
storage
|
|
19
20
|
} from '@powersync/service-core';
|
|
20
|
-
import { DatabaseInputRow, SqliteRow, SqlSyncRules, TablePattern } from '@powersync/service-sync-rules';
|
|
21
|
+
import { DatabaseInputRow, SqliteInputRow, SqliteRow, SqlSyncRules, TablePattern } from '@powersync/service-sync-rules';
|
|
21
22
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
22
23
|
import { MongoLSN } from '../common/MongoLSN.js';
|
|
23
24
|
import { PostImagesOption } from '../types/types.js';
|
|
@@ -215,9 +216,14 @@ export class ChangeStream {
|
|
|
215
216
|
|
|
216
217
|
async estimatedCountNumber(table: storage.SourceTable): Promise<number> {
|
|
217
218
|
const db = this.client.db(table.schema);
|
|
218
|
-
return await db.collection(table.
|
|
219
|
+
return await db.collection(table.name).estimatedDocumentCount();
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* This gets a LSN before starting a snapshot, which we can resume streaming from after the snapshot.
|
|
224
|
+
*
|
|
225
|
+
* This LSN can survive initial replication restarts.
|
|
226
|
+
*/
|
|
221
227
|
private async getSnapshotLsn(): Promise<string> {
|
|
222
228
|
const hello = await this.defaultDb.command({ hello: 1 });
|
|
223
229
|
// Basic sanity check
|
|
@@ -292,6 +298,9 @@ export class ChangeStream {
|
|
|
292
298
|
);
|
|
293
299
|
}
|
|
294
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Given a snapshot LSN, validate that we can read from it, by opening a change stream.
|
|
303
|
+
*/
|
|
295
304
|
private async validateSnapshotLsn(lsn: string) {
|
|
296
305
|
await using streamManager = this.openChangeStream({ lsn: lsn, maxAwaitTimeMs: 0 });
|
|
297
306
|
const { stream } = streamManager;
|
|
@@ -309,7 +318,7 @@ export class ChangeStream {
|
|
|
309
318
|
const sourceTables = this.sync_rules.getSourceTables();
|
|
310
319
|
await this.client.connect();
|
|
311
320
|
|
|
312
|
-
await this.storage.startBatch(
|
|
321
|
+
const flushResult = await this.storage.startBatch(
|
|
313
322
|
{
|
|
314
323
|
logger: this.logger,
|
|
315
324
|
zeroLSN: MongoLSN.ZERO.comparable,
|
|
@@ -321,7 +330,7 @@ export class ChangeStream {
|
|
|
321
330
|
if (snapshotLsn == null) {
|
|
322
331
|
// First replication attempt - get a snapshot and store the timestamp
|
|
323
332
|
snapshotLsn = await this.getSnapshotLsn();
|
|
324
|
-
await batch.
|
|
333
|
+
await batch.setResumeLsn(snapshotLsn);
|
|
325
334
|
this.logger.info(`Marking snapshot at ${snapshotLsn}`);
|
|
326
335
|
} else {
|
|
327
336
|
this.logger.info(`Resuming snapshot at ${snapshotLsn}`);
|
|
@@ -359,13 +368,23 @@ export class ChangeStream {
|
|
|
359
368
|
await this.snapshotTable(batch, table);
|
|
360
369
|
await batch.markSnapshotDone([table], MongoLSN.ZERO.comparable);
|
|
361
370
|
|
|
362
|
-
|
|
371
|
+
this.touch();
|
|
363
372
|
}
|
|
364
373
|
|
|
365
|
-
|
|
374
|
+
// The checkpoint here is a marker - we need to replicate up to at least this
|
|
375
|
+
// point before the data can be considered consistent.
|
|
376
|
+
// We could do this for each individual table, but may as well just do it once for the entire snapshot.
|
|
377
|
+
const checkpoint = await createCheckpoint(this.client, this.defaultDb, STANDALONE_CHECKPOINT_ID);
|
|
378
|
+
await batch.markSnapshotDone([], checkpoint);
|
|
379
|
+
|
|
380
|
+
// This will not create a consistent checkpoint yet, but will persist the op.
|
|
381
|
+
// Actual checkpoint will be created when streaming replication caught up.
|
|
366
382
|
await batch.commit(snapshotLsn);
|
|
383
|
+
|
|
384
|
+
this.logger.info(`Snapshot done. Need to replicate from ${snapshotLsn} to ${checkpoint} to be consistent`);
|
|
367
385
|
}
|
|
368
386
|
);
|
|
387
|
+
return { lastOpId: flushResult?.flushed_op };
|
|
369
388
|
}
|
|
370
389
|
|
|
371
390
|
private async setupCheckpointsCollection() {
|
|
@@ -422,7 +441,7 @@ export class ChangeStream {
|
|
|
422
441
|
return { $match: { ns: { $in: $inFilters } }, multipleDatabases };
|
|
423
442
|
}
|
|
424
443
|
|
|
425
|
-
static *getQueryData(results: Iterable<DatabaseInputRow>): Generator<
|
|
444
|
+
static *getQueryData(results: Iterable<DatabaseInputRow>): Generator<SqliteInputRow> {
|
|
426
445
|
for (let row of results) {
|
|
427
446
|
yield constructAfterRecord(row);
|
|
428
447
|
}
|
|
@@ -432,7 +451,7 @@ export class ChangeStream {
|
|
|
432
451
|
const totalEstimatedCount = await this.estimatedCountNumber(table);
|
|
433
452
|
let at = table.snapshotStatus?.replicatedCount ?? 0;
|
|
434
453
|
const db = this.client.db(table.schema);
|
|
435
|
-
const collection = db.collection(table.
|
|
454
|
+
const collection = db.collection(table.name);
|
|
436
455
|
await using query = new ChunkedSnapshotQuery({
|
|
437
456
|
collection,
|
|
438
457
|
key: table.snapshotStatus?.lastKey,
|
|
@@ -492,7 +511,7 @@ export class ChangeStream {
|
|
|
492
511
|
this.logger.info(
|
|
493
512
|
`Replicating ${table.qualifiedName} ${table.formatSnapshotProgress()} in ${duration.toFixed(0)}ms`
|
|
494
513
|
);
|
|
495
|
-
|
|
514
|
+
this.touch();
|
|
496
515
|
}
|
|
497
516
|
// In case the loop was interrupted, make sure we await the last promise.
|
|
498
517
|
await nextChunkPromise;
|
|
@@ -656,7 +675,6 @@ export class ChangeStream {
|
|
|
656
675
|
try {
|
|
657
676
|
// If anything errors here, the entire replication process is halted, and
|
|
658
677
|
// all connections automatically closed, including this one.
|
|
659
|
-
|
|
660
678
|
await this.initReplication();
|
|
661
679
|
await this.streamChanges();
|
|
662
680
|
} catch (e) {
|
|
@@ -673,7 +691,15 @@ export class ChangeStream {
|
|
|
673
691
|
// Snapshot LSN is not present, so we need to start replication from scratch.
|
|
674
692
|
await this.storage.clear({ signal: this.abort_signal });
|
|
675
693
|
}
|
|
676
|
-
await this.initialReplication(result.snapshotLsn);
|
|
694
|
+
const { lastOpId } = await this.initialReplication(result.snapshotLsn);
|
|
695
|
+
if (lastOpId != null) {
|
|
696
|
+
// Populate the cache _after_ initial replication, but _before_ we switch to this sync rules.
|
|
697
|
+
await this.storage.populatePersistentChecksumCache({
|
|
698
|
+
signal: this.abort_signal,
|
|
699
|
+
// No checkpoint yet, but we do have the opId.
|
|
700
|
+
maxOpId: lastOpId
|
|
701
|
+
});
|
|
702
|
+
}
|
|
677
703
|
}
|
|
678
704
|
}
|
|
679
705
|
|
|
@@ -757,19 +783,20 @@ export class ChangeStream {
|
|
|
757
783
|
}
|
|
758
784
|
|
|
759
785
|
async streamChangesInternal() {
|
|
760
|
-
// Auto-activate as soon as initial replication is done
|
|
761
|
-
await this.storage.autoActivate();
|
|
762
|
-
|
|
763
786
|
await this.storage.startBatch(
|
|
764
787
|
{
|
|
765
788
|
logger: this.logger,
|
|
766
789
|
zeroLSN: MongoLSN.ZERO.comparable,
|
|
767
790
|
defaultSchema: this.defaultDb.databaseName,
|
|
791
|
+
// We get a complete postimage for every change, so we don't need to store the current data.
|
|
768
792
|
storeCurrentData: false
|
|
769
793
|
},
|
|
770
794
|
async (batch) => {
|
|
771
|
-
const {
|
|
772
|
-
|
|
795
|
+
const { resumeFromLsn } = batch;
|
|
796
|
+
if (resumeFromLsn == null) {
|
|
797
|
+
throw new ReplicationAssertionError(`No LSN found to resume from`);
|
|
798
|
+
}
|
|
799
|
+
const lastLsn = MongoLSN.fromSerialized(resumeFromLsn);
|
|
773
800
|
const startAfter = lastLsn?.timestamp;
|
|
774
801
|
|
|
775
802
|
// It is normal for this to be a minute or two old when there is a low volume
|
|
@@ -778,7 +805,7 @@ export class ChangeStream {
|
|
|
778
805
|
|
|
779
806
|
this.logger.info(`Resume streaming at ${startAfter?.inspect()} / ${lastLsn} | Token age: ${tokenAgeSeconds}s`);
|
|
780
807
|
|
|
781
|
-
await using streamManager = this.openChangeStream({ lsn:
|
|
808
|
+
await using streamManager = this.openChangeStream({ lsn: resumeFromLsn });
|
|
782
809
|
const { stream, filters } = streamManager;
|
|
783
810
|
if (this.abort_signal.aborted) {
|
|
784
811
|
await stream.close();
|
|
@@ -797,6 +824,7 @@ export class ChangeStream {
|
|
|
797
824
|
let splitDocument: mongo.ChangeStreamDocument | null = null;
|
|
798
825
|
|
|
799
826
|
let flexDbNameWorkaroundLogged = false;
|
|
827
|
+
let changesSinceLastCheckpoint = 0;
|
|
800
828
|
|
|
801
829
|
let lastEmptyResume = performance.now();
|
|
802
830
|
|
|
@@ -831,7 +859,7 @@ export class ChangeStream {
|
|
|
831
859
|
if (waitForCheckpointLsn == null && performance.now() - lastEmptyResume > 60_000) {
|
|
832
860
|
const { comparable: lsn, timestamp } = MongoLSN.fromResumeToken(stream.resumeToken);
|
|
833
861
|
await batch.keepalive(lsn);
|
|
834
|
-
|
|
862
|
+
this.touch();
|
|
835
863
|
lastEmptyResume = performance.now();
|
|
836
864
|
// Log the token update. This helps as a general "replication is still active" message in the logs.
|
|
837
865
|
// This token would typically be around 10s behind.
|
|
@@ -843,7 +871,7 @@ export class ChangeStream {
|
|
|
843
871
|
continue;
|
|
844
872
|
}
|
|
845
873
|
|
|
846
|
-
|
|
874
|
+
this.touch();
|
|
847
875
|
|
|
848
876
|
if (startAfter != null && originalChangeDocument.clusterTime?.lte(startAfter)) {
|
|
849
877
|
continue;
|
|
@@ -966,6 +994,7 @@ export class ChangeStream {
|
|
|
966
994
|
if (didCommit) {
|
|
967
995
|
this.oldestUncommittedChange = null;
|
|
968
996
|
this.isStartingReplication = false;
|
|
997
|
+
changesSinceLastCheckpoint = 0;
|
|
969
998
|
}
|
|
970
999
|
} else if (
|
|
971
1000
|
changeDocument.operationType == 'insert' ||
|
|
@@ -988,7 +1017,21 @@ export class ChangeStream {
|
|
|
988
1017
|
if (this.oldestUncommittedChange == null && changeDocument.clusterTime != null) {
|
|
989
1018
|
this.oldestUncommittedChange = timestampToDate(changeDocument.clusterTime);
|
|
990
1019
|
}
|
|
991
|
-
await this.writeChange(batch, table, changeDocument);
|
|
1020
|
+
const flushResult = await this.writeChange(batch, table, changeDocument);
|
|
1021
|
+
changesSinceLastCheckpoint += 1;
|
|
1022
|
+
if (flushResult != null && changesSinceLastCheckpoint >= 20_000) {
|
|
1023
|
+
// When we are catching up replication after an initial snapshot, there may be a very long delay
|
|
1024
|
+
// before we do a commit(). In that case, we need to periodically persist the resume LSN, so
|
|
1025
|
+
// we don't restart from scratch if we restart replication.
|
|
1026
|
+
// The same could apply if we need to catch up on replication after some downtime.
|
|
1027
|
+
const { comparable: lsn } = new MongoLSN({
|
|
1028
|
+
timestamp: changeDocument.clusterTime!,
|
|
1029
|
+
resume_token: changeDocument._id
|
|
1030
|
+
});
|
|
1031
|
+
this.logger.info(`Updating resume LSN to ${lsn} after ${changesSinceLastCheckpoint} changes`);
|
|
1032
|
+
await batch.setResumeLsn(lsn);
|
|
1033
|
+
changesSinceLastCheckpoint = 0;
|
|
1034
|
+
}
|
|
992
1035
|
}
|
|
993
1036
|
} else if (changeDocument.operationType == 'drop') {
|
|
994
1037
|
const rel = getMongoRelation(changeDocument.ns);
|
|
@@ -1036,13 +1079,18 @@ export class ChangeStream {
|
|
|
1036
1079
|
}
|
|
1037
1080
|
return Date.now() - this.oldestUncommittedChange.getTime();
|
|
1038
1081
|
}
|
|
1039
|
-
}
|
|
1040
1082
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1083
|
+
private lastTouchedAt = performance.now();
|
|
1084
|
+
|
|
1085
|
+
private touch() {
|
|
1086
|
+
if (performance.now() - this.lastTouchedAt > 1_000) {
|
|
1087
|
+
this.lastTouchedAt = performance.now();
|
|
1088
|
+
// Update the probes, but don't wait for it
|
|
1089
|
+
container.probes.touch().catch((e) => {
|
|
1090
|
+
this.logger.error(`Failed to touch the container probe: ${e.message}`, e);
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1046
1094
|
}
|
|
1047
1095
|
|
|
1048
1096
|
function mapChangeStreamError(e: any) {
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { storage } from '@powersync/service-core';
|
|
3
3
|
import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CompatibilityContext,
|
|
6
|
+
CustomArray,
|
|
7
|
+
CustomObject,
|
|
8
|
+
CustomSqliteValue,
|
|
9
|
+
DatabaseInputValue,
|
|
10
|
+
SqliteInputRow,
|
|
11
|
+
SqliteInputValue,
|
|
12
|
+
SqliteRow,
|
|
13
|
+
SqliteValue,
|
|
14
|
+
DateTimeValue
|
|
15
|
+
} from '@powersync/service-sync-rules';
|
|
5
16
|
|
|
6
17
|
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
7
18
|
import { MongoLSN } from '../common/MongoLSN.js';
|
|
@@ -13,7 +24,7 @@ export function getMongoRelation(source: mongo.ChangeStreamNameSpace): storage.S
|
|
|
13
24
|
schema: source.db,
|
|
14
25
|
// Not relevant for MongoDB - we use db + coll name as the identifier
|
|
15
26
|
objectId: undefined,
|
|
16
|
-
|
|
27
|
+
replicaIdColumns: [{ name: '_id' }]
|
|
17
28
|
} satisfies storage.SourceEntityDescriptor;
|
|
18
29
|
}
|
|
19
30
|
|
|
@@ -22,20 +33,20 @@ export function getMongoRelation(source: mongo.ChangeStreamNameSpace): storage.S
|
|
|
22
33
|
*/
|
|
23
34
|
export function getCacheIdentifier(source: storage.SourceEntityDescriptor | storage.SourceTable): string {
|
|
24
35
|
if (source instanceof storage.SourceTable) {
|
|
25
|
-
return `${source.schema}.${source.
|
|
36
|
+
return `${source.schema}.${source.name}`;
|
|
26
37
|
}
|
|
27
38
|
return `${source.schema}.${source.name}`;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
export function constructAfterRecord(document: mongo.Document):
|
|
31
|
-
let record:
|
|
41
|
+
export function constructAfterRecord(document: mongo.Document): SqliteInputRow {
|
|
42
|
+
let record: SqliteInputRow = {};
|
|
32
43
|
for (let key of Object.keys(document)) {
|
|
33
44
|
record[key] = toMongoSyncRulesValue(document[key]);
|
|
34
45
|
}
|
|
35
46
|
return record;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
export function toMongoSyncRulesValue(data: any):
|
|
49
|
+
export function toMongoSyncRulesValue(data: any): SqliteInputValue {
|
|
39
50
|
const autoBigNum = true;
|
|
40
51
|
if (data === null) {
|
|
41
52
|
return null;
|
|
@@ -60,7 +71,8 @@ export function toMongoSyncRulesValue(data: any): SqliteValue {
|
|
|
60
71
|
} else if (data instanceof mongo.UUID) {
|
|
61
72
|
return data.toHexString();
|
|
62
73
|
} else if (data instanceof Date) {
|
|
63
|
-
|
|
74
|
+
const isoString = data.toISOString();
|
|
75
|
+
return new DateTimeValue(isoString);
|
|
64
76
|
} else if (data instanceof mongo.Binary) {
|
|
65
77
|
return new Uint8Array(data.buffer);
|
|
66
78
|
} else if (data instanceof mongo.Long) {
|
|
@@ -72,18 +84,13 @@ export function toMongoSyncRulesValue(data: any): SqliteValue {
|
|
|
72
84
|
} else if (data instanceof RegExp) {
|
|
73
85
|
return JSON.stringify({ pattern: data.source, options: data.flags });
|
|
74
86
|
} else if (Array.isArray(data)) {
|
|
75
|
-
|
|
76
|
-
return JSONBig.stringify(data.map((element) => filterJsonData(element)));
|
|
87
|
+
return new CustomArray(data, filterJsonData);
|
|
77
88
|
} else if (data instanceof Uint8Array) {
|
|
78
89
|
return data;
|
|
79
90
|
} else if (data instanceof JsonContainer) {
|
|
80
91
|
return data.toString();
|
|
81
92
|
} else if (typeof data == 'object') {
|
|
82
|
-
|
|
83
|
-
for (let key of Object.keys(data)) {
|
|
84
|
-
record[key] = filterJsonData(data[key]);
|
|
85
|
-
}
|
|
86
|
-
return JSONBig.stringify(record);
|
|
93
|
+
return new CustomObject(data, filterJsonData);
|
|
87
94
|
} else {
|
|
88
95
|
return null;
|
|
89
96
|
}
|
|
@@ -91,7 +98,7 @@ export function toMongoSyncRulesValue(data: any): SqliteValue {
|
|
|
91
98
|
|
|
92
99
|
const DEPTH_LIMIT = 20;
|
|
93
100
|
|
|
94
|
-
function filterJsonData(data: any, depth = 0): any {
|
|
101
|
+
function filterJsonData(data: any, context: CompatibilityContext, depth = 0): any {
|
|
95
102
|
const autoBigNum = true;
|
|
96
103
|
if (depth > DEPTH_LIMIT) {
|
|
97
104
|
// This is primarily to prevent infinite recursion
|
|
@@ -117,7 +124,8 @@ function filterJsonData(data: any, depth = 0): any {
|
|
|
117
124
|
} else if (typeof data == 'bigint') {
|
|
118
125
|
return data;
|
|
119
126
|
} else if (data instanceof Date) {
|
|
120
|
-
|
|
127
|
+
const isoString = data.toISOString();
|
|
128
|
+
return new DateTimeValue(isoString).toSqliteValue(context);
|
|
121
129
|
} else if (data instanceof mongo.ObjectId) {
|
|
122
130
|
return data.toHexString();
|
|
123
131
|
} else if (data instanceof mongo.UUID) {
|
|
@@ -133,16 +141,18 @@ function filterJsonData(data: any, depth = 0): any {
|
|
|
133
141
|
} else if (data instanceof RegExp) {
|
|
134
142
|
return { pattern: data.source, options: data.flags };
|
|
135
143
|
} else if (Array.isArray(data)) {
|
|
136
|
-
return data.map((element) => filterJsonData(element, depth + 1));
|
|
144
|
+
return data.map((element) => filterJsonData(element, context, depth + 1));
|
|
137
145
|
} else if (ArrayBuffer.isView(data)) {
|
|
138
146
|
return undefined;
|
|
147
|
+
} else if (data instanceof CustomSqliteValue) {
|
|
148
|
+
return data.toSqliteValue(context);
|
|
139
149
|
} else if (data instanceof JsonContainer) {
|
|
140
150
|
// Can be stringified directly when using our JSONBig implementation
|
|
141
151
|
return data;
|
|
142
152
|
} else if (typeof data == 'object') {
|
|
143
153
|
let record: Record<string, any> = {};
|
|
144
154
|
for (let key of Object.keys(data)) {
|
|
145
|
-
record[key] = filterJsonData(data[key], depth + 1);
|
|
155
|
+
record[key] = filterJsonData(data[key], context, depth + 1);
|
|
146
156
|
}
|
|
147
157
|
return record;
|
|
148
158
|
} else {
|