@powersync/service-module-mongodb-storage 0.16.0 → 0.17.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 +34 -0
- package/dist/storage/MongoBucketStorage.d.ts +6 -4
- package/dist/storage/MongoBucketStorage.js +110 -36
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +4 -6
- package/dist/storage/implementation/BucketDefinitionMapping.js +3 -3
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
- package/dist/storage/implementation/CheckpointState.d.ts +20 -0
- package/dist/storage/implementation/CheckpointState.js +31 -0
- package/dist/storage/implementation/CheckpointState.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +33 -22
- package/dist/storage/implementation/MongoBucketBatch.js +45 -271
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +2 -1
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +4 -4
- package/dist/storage/implementation/MongoPersistedSyncRules.js +11 -8
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +19 -5
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +53 -19
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +21 -10
- package/dist/storage/implementation/MongoSyncBucketStorage.js +18 -163
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +7 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
- package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -2
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +10 -2
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +31 -47
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +385 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +16 -7
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +77 -6
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.d.ts +12 -1
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +17 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +429 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +1 -1
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +1 -2
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +29 -7
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +117 -16
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +3 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +61 -3
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/package.json +6 -6
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
- package/src/storage/MongoBucketStorage.ts +166 -44
- package/src/storage/implementation/BucketDefinitionMapping.ts +12 -9
- package/src/storage/implementation/CheckpointState.ts +59 -0
- package/src/storage/implementation/MongoBucketBatch.ts +81 -355
- package/src/storage/implementation/MongoChecksums.ts +2 -1
- package/src/storage/implementation/MongoCompactor.ts +1 -1
- package/src/storage/implementation/MongoPersistedSyncRules.ts +13 -7
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +69 -24
- package/src/storage/implementation/MongoSyncBucketStorage.ts +40 -215
- package/src/storage/implementation/MongoSyncRulesLock.ts +9 -3
- package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
- package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
- package/src/storage/implementation/common/PersistedBatch.ts +2 -2
- package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +2 -2
- package/src/storage/implementation/db.ts +5 -2
- package/src/storage/implementation/models.ts +35 -58
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +478 -1
- package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +111 -16
- package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
- package/src/storage/implementation/v1/models.ts +15 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +564 -1
- package/src/storage/implementation/v3/MongoCompactorV3.ts +1 -1
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +1 -2
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +150 -22
- package/src/storage/implementation/v3/PersistedBatchV3.ts +1 -2
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +7 -2
- package/src/storage/implementation/v3/models.ts +70 -2
- package/test/src/storage_sync.test.ts +422 -6
- package/test/src/storeCurrentData.test.ts +211 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { ColumnDescriptor, storage } from '@powersync/service-core';
|
|
4
|
+
import { HydratedSyncConfig, MatchingSources } from '@powersync/service-sync-rules';
|
|
5
|
+
import * as bson from 'bson';
|
|
3
6
|
import { mongoTableId } from '../../../utils/util.js';
|
|
7
|
+
import { calculateCheckpointState } from '../CheckpointState.js';
|
|
4
8
|
import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
|
|
9
|
+
import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
|
|
5
10
|
import { PersistedBatch } from '../common/PersistedBatch.js';
|
|
6
11
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
7
12
|
import { PersistedBatchV3 } from './PersistedBatchV3.js';
|
|
8
13
|
import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
|
|
9
14
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
15
|
+
import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
|
|
16
|
+
|
|
17
|
+
function sameStringArray(left: string[], right: string[]) {
|
|
18
|
+
return left.length == right.length && left.every((value, index) => value == right[index]);
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
12
22
|
declare public readonly db: VersionedPowerSyncMongoV3;
|
|
13
23
|
|
|
14
24
|
private readonly store: SourceRecordStore;
|
|
25
|
+
private readonly syncConfigId: bson.ObjectId;
|
|
26
|
+
private needsActivationV3 = true;
|
|
27
|
+
private lastWaitingLogThrottledV3 = 0;
|
|
15
28
|
|
|
16
29
|
constructor(options: MongoBucketBatchOptions) {
|
|
17
30
|
super(options);
|
|
31
|
+
if (options.syncConfigId == null) {
|
|
32
|
+
throw new ReplicationAssertionError('Missing sync config id for v3 batch');
|
|
33
|
+
}
|
|
34
|
+
this.syncConfigId = options.syncConfigId;
|
|
18
35
|
this.store = new SourceRecordStoreV3(this.db, this.group_id, this.mapping);
|
|
19
36
|
}
|
|
20
37
|
|
|
@@ -41,4 +58,550 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
41
58
|
});
|
|
42
59
|
}
|
|
43
60
|
}
|
|
61
|
+
|
|
62
|
+
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
63
|
+
const ref = options.source;
|
|
64
|
+
const syncRules = options.syncRules ?? this.sync_rules;
|
|
65
|
+
const matchingSources = syncRules.getMatchingSources(ref);
|
|
66
|
+
|
|
67
|
+
const { connection_id, source } = options;
|
|
68
|
+
const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
|
|
69
|
+
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
70
|
+
name: column.name,
|
|
71
|
+
type: column.type,
|
|
72
|
+
type_oid: column.typeId
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
let result: storage.ResolveTablesResult | null = null;
|
|
76
|
+
const initializeSourceRecordsFor: bson.ObjectId[] = [];
|
|
77
|
+
|
|
78
|
+
await this.db.client.withSession(async (session) => {
|
|
79
|
+
const col = this.db.commonSourceTables(this.group_id);
|
|
80
|
+
const exactFilter: Record<string, unknown> = {
|
|
81
|
+
connection_id,
|
|
82
|
+
schema_name: schema,
|
|
83
|
+
table_name: name,
|
|
84
|
+
replica_id_columns2: normalizedReplicaIdColumns
|
|
85
|
+
};
|
|
86
|
+
if (objectId != null) {
|
|
87
|
+
exactFilter.relation_id = objectId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const exactDocs = (await col.find(exactFilter, { session }).toArray()) as SourceTableDocumentV3[];
|
|
91
|
+
const bucketSourceById = new Map(
|
|
92
|
+
matchingSources.bucketDataSources.map((source) => [this.mapping.bucketSourceId(source), source] as const)
|
|
93
|
+
);
|
|
94
|
+
const parameterLookupSourceById = new Map(
|
|
95
|
+
matchingSources.parameterLookupSources.map(
|
|
96
|
+
(source) => [this.mapping.parameterLookupId(source), source] as const
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
const desiredBucketIds = new Set(bucketSourceById.keys());
|
|
100
|
+
const desiredLookupIds = new Set(parameterLookupSourceById.keys());
|
|
101
|
+
const desiredHasMembership = desiredBucketIds.size > 0 || desiredLookupIds.size > 0;
|
|
102
|
+
const triggersEvent = syncRules.tableTriggersEvent(ref);
|
|
103
|
+
|
|
104
|
+
const coveredBucketIds = new Set<string>();
|
|
105
|
+
const coveredLookupIds = new Set<string>();
|
|
106
|
+
const retainedDocIds: bson.ObjectId[] = [];
|
|
107
|
+
const tables: storage.SourceTable[] = [];
|
|
108
|
+
let retainedEventOnlyTable = false;
|
|
109
|
+
|
|
110
|
+
for (const doc of exactDocs) {
|
|
111
|
+
const bucketDataSourceIds = doc.bucket_data_source_ids.filter((id) => desiredBucketIds.has(id));
|
|
112
|
+
const parameterLookupSourceIds = doc.parameter_lookup_source_ids.filter((id) => desiredLookupIds.has(id));
|
|
113
|
+
const coversDesiredMembership = bucketDataSourceIds.length > 0 || parameterLookupSourceIds.length > 0;
|
|
114
|
+
const coversEventOnlyTable = !desiredHasMembership && triggersEvent && !retainedEventOnlyTable;
|
|
115
|
+
|
|
116
|
+
for (const id of bucketDataSourceIds) {
|
|
117
|
+
coveredBucketIds.add(id);
|
|
118
|
+
}
|
|
119
|
+
for (const id of parameterLookupSourceIds) {
|
|
120
|
+
coveredLookupIds.add(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const updates: Partial<SourceTableDocumentV3> = {};
|
|
124
|
+
if (
|
|
125
|
+
!sameStringArray(doc.bucket_data_source_ids, bucketDataSourceIds) ||
|
|
126
|
+
!sameStringArray(doc.parameter_lookup_source_ids, parameterLookupSourceIds)
|
|
127
|
+
) {
|
|
128
|
+
updates.bucket_data_source_ids = bucketDataSourceIds;
|
|
129
|
+
updates.parameter_lookup_source_ids = parameterLookupSourceIds;
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(updates).length > 0) {
|
|
132
|
+
await col.updateOne({ _id: doc._id }, { $set: updates }, { session });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (coversDesiredMembership || coversEventOnlyTable) {
|
|
136
|
+
if (coversEventOnlyTable) {
|
|
137
|
+
retainedEventOnlyTable = true;
|
|
138
|
+
}
|
|
139
|
+
retainedDocIds.push(doc._id);
|
|
140
|
+
const table = this.sourceTableFromDocument(
|
|
141
|
+
{
|
|
142
|
+
...doc,
|
|
143
|
+
bucket_data_source_ids: bucketDataSourceIds,
|
|
144
|
+
parameter_lookup_source_ids: parameterLookupSourceIds
|
|
145
|
+
},
|
|
146
|
+
connectionTag,
|
|
147
|
+
syncRules,
|
|
148
|
+
{
|
|
149
|
+
bucketDataSources: bucketDataSourceIds.map((id) => bucketSourceById.get(id)!),
|
|
150
|
+
parameterLookupSources: parameterLookupSourceIds.map((id) => parameterLookupSourceById.get(id)!)
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
table.storeCurrentData = sendsCompleteRows !== true;
|
|
154
|
+
tables.push(table);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const uncoveredBucketIds = [...desiredBucketIds].filter((id) => !coveredBucketIds.has(id));
|
|
159
|
+
const uncoveredLookupIds = [...desiredLookupIds].filter((id) => !coveredLookupIds.has(id));
|
|
160
|
+
|
|
161
|
+
if (uncoveredBucketIds.length > 0 || uncoveredLookupIds.length > 0 || (triggersEvent && tables.length == 0)) {
|
|
162
|
+
const id = options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId();
|
|
163
|
+
const sourceTable = new storage.SourceTable({
|
|
164
|
+
id,
|
|
165
|
+
ref,
|
|
166
|
+
objectId,
|
|
167
|
+
replicaIdColumns,
|
|
168
|
+
snapshotComplete: false,
|
|
169
|
+
bucketDataSources: uncoveredBucketIds.map((id) => bucketSourceById.get(id)!),
|
|
170
|
+
parameterLookupSources: uncoveredLookupIds.map((id) => parameterLookupSourceById.get(id)!)
|
|
171
|
+
});
|
|
172
|
+
sourceTable.syncData = uncoveredBucketIds.length > 0;
|
|
173
|
+
sourceTable.syncParameters = uncoveredLookupIds.length > 0;
|
|
174
|
+
sourceTable.syncEvent = triggersEvent;
|
|
175
|
+
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
176
|
+
|
|
177
|
+
const createDoc: SourceTableDocumentV3 = {
|
|
178
|
+
_id: id,
|
|
179
|
+
connection_id,
|
|
180
|
+
relation_id: objectId,
|
|
181
|
+
schema_name: schema,
|
|
182
|
+
table_name: name,
|
|
183
|
+
replica_id_columns: null,
|
|
184
|
+
replica_id_columns2: normalizedReplicaIdColumns,
|
|
185
|
+
snapshot_done: false,
|
|
186
|
+
snapshot_status: undefined,
|
|
187
|
+
bucket_data_source_ids: uncoveredBucketIds,
|
|
188
|
+
parameter_lookup_source_ids: uncoveredLookupIds
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
await col.insertOne(createDoc, { session });
|
|
192
|
+
initializeSourceRecordsFor.push(createDoc._id);
|
|
193
|
+
retainedDocIds.push(createDoc._id);
|
|
194
|
+
tables.push(sourceTable);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const conflictFilter = [{ schema_name: schema, table_name: name }] as Record<string, unknown>[];
|
|
198
|
+
if (objectId != null) {
|
|
199
|
+
conflictFilter.push({ relation_id: objectId });
|
|
200
|
+
}
|
|
201
|
+
const dropTables = await col
|
|
202
|
+
.find(
|
|
203
|
+
{
|
|
204
|
+
connection_id,
|
|
205
|
+
_id: { $nin: retainedDocIds },
|
|
206
|
+
$or: conflictFilter
|
|
207
|
+
},
|
|
208
|
+
{ session }
|
|
209
|
+
)
|
|
210
|
+
.toArray();
|
|
211
|
+
|
|
212
|
+
result = {
|
|
213
|
+
tables,
|
|
214
|
+
dropTables: dropTables.map((doc) =>
|
|
215
|
+
this.sourceTableFromDocument(doc as SourceTableDocumentV3, connectionTag, syncRules)
|
|
216
|
+
)
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
for (const sourceTableId of initializeSourceRecordsFor) {
|
|
221
|
+
await this.db.initializeSourceRecordsCollection(this.group_id, sourceTableId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result!;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private sourceTableFromDocument(
|
|
228
|
+
doc: SourceTableDocumentV3,
|
|
229
|
+
connectionTag: string,
|
|
230
|
+
syncRules: HydratedSyncConfig,
|
|
231
|
+
memberships?: MatchingSources
|
|
232
|
+
): storage.SourceTable {
|
|
233
|
+
const resolvedMemberships = memberships ?? this.sourceTableMembershipsFromDocument(doc, syncRules);
|
|
234
|
+
const table = new storage.SourceTable({
|
|
235
|
+
id: doc._id,
|
|
236
|
+
ref: {
|
|
237
|
+
connectionTag,
|
|
238
|
+
schema: doc.schema_name,
|
|
239
|
+
name: doc.table_name
|
|
240
|
+
},
|
|
241
|
+
objectId: doc.relation_id,
|
|
242
|
+
replicaIdColumns: doc.replica_id_columns2!.map(
|
|
243
|
+
(c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
|
|
244
|
+
),
|
|
245
|
+
snapshotComplete: doc.snapshot_done ?? true,
|
|
246
|
+
bucketDataSources: resolvedMemberships.bucketDataSources,
|
|
247
|
+
parameterLookupSources: resolvedMemberships.parameterLookupSources
|
|
248
|
+
});
|
|
249
|
+
table.syncData = table.bucketDataSources.length > 0;
|
|
250
|
+
table.syncParameters = table.parameterLookupSources.length > 0;
|
|
251
|
+
table.syncEvent = syncRules.tableTriggersEvent(table.ref);
|
|
252
|
+
table.snapshotStatus =
|
|
253
|
+
doc.snapshot_status == null
|
|
254
|
+
? undefined
|
|
255
|
+
: {
|
|
256
|
+
lastKey: doc.snapshot_status.last_key?.buffer ?? null,
|
|
257
|
+
totalEstimatedCount: doc.snapshot_status.total_estimated_count,
|
|
258
|
+
replicatedCount: doc.snapshot_status.replicated_count
|
|
259
|
+
};
|
|
260
|
+
return table;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private sourceTableMembershipsFromDocument(
|
|
264
|
+
doc: SourceTableDocumentV3,
|
|
265
|
+
syncRules: HydratedSyncConfig
|
|
266
|
+
): MatchingSources {
|
|
267
|
+
const bucketDataSourceIds = new Set(doc.bucket_data_source_ids);
|
|
268
|
+
const parameterLookupSourceIds = new Set(doc.parameter_lookup_source_ids);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
bucketDataSources: syncRules.bucketDataSources.filter((source) =>
|
|
272
|
+
bucketDataSourceIds.has(this.mapping.bucketSourceId(source))
|
|
273
|
+
),
|
|
274
|
+
parameterLookupSources: syncRules.bucketParameterLookupSources.filter((source) =>
|
|
275
|
+
parameterLookupSourceIds.has(this.mapping.parameterLookupId(source))
|
|
276
|
+
)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
|
|
281
|
+
const doc = (await this.db
|
|
282
|
+
.commonSourceTables(this.group_id)
|
|
283
|
+
.findOne({ _id: mongoTableId(table.id) }, { session: this.session })) as SourceTableDocumentV3 | null;
|
|
284
|
+
if (doc == null) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return this.sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
292
|
+
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
293
|
+
|
|
294
|
+
await this.flush(options);
|
|
295
|
+
|
|
296
|
+
const now = new Date();
|
|
297
|
+
|
|
298
|
+
await this.db.write_checkpoints.updateMany(
|
|
299
|
+
{
|
|
300
|
+
processed_at_lsn: null,
|
|
301
|
+
'lsns.1': { $lte: lsn }
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
$set: {
|
|
305
|
+
processed_at_lsn: lsn
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
session: this.session
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const preUpdateDocument = await this.db.sync_rules.findOne(
|
|
314
|
+
{
|
|
315
|
+
_id: this.group_id,
|
|
316
|
+
'sync_configs._id': this.syncConfigId
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
session: this.session,
|
|
320
|
+
projection: {
|
|
321
|
+
snapshot_lsn: 1,
|
|
322
|
+
sync_configs: {
|
|
323
|
+
$elemMatch: {
|
|
324
|
+
_id: this.syncConfigId
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const state = (preUpdateDocument as ReplicationStreamDocumentV3)?.sync_configs?.[0];
|
|
332
|
+
if (state == null) {
|
|
333
|
+
throw new ReplicationAssertionError(
|
|
334
|
+
`Failed to update checkpoint - no matching sync_config for _id: ${this.group_id}/${this.syncConfigId.toHexString()}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const checkpointState = calculateCheckpointState({
|
|
339
|
+
lsn,
|
|
340
|
+
snapshotDone: state.snapshot_done === true,
|
|
341
|
+
lastCheckpointLsn: state.last_checkpoint_lsn,
|
|
342
|
+
noCheckpointBefore: state.no_checkpoint_before,
|
|
343
|
+
keepaliveOp: state.keepalive_op == null ? null : BigInt(state.keepalive_op),
|
|
344
|
+
lastCheckpoint: state.last_checkpoint,
|
|
345
|
+
persistedOp: this.persisted_op,
|
|
346
|
+
createEmptyCheckpoints
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const updateSet: Record<string, any> = {
|
|
350
|
+
last_keepalive_ts: now,
|
|
351
|
+
last_fatal_error: null,
|
|
352
|
+
last_fatal_error_ts: null,
|
|
353
|
+
'sync_configs.$[config].keepalive_op': checkpointState.newKeepaliveOp,
|
|
354
|
+
'sync_configs.$[config].last_checkpoint': checkpointState.newLastCheckpoint
|
|
355
|
+
};
|
|
356
|
+
if (checkpointState.checkpointCreated) {
|
|
357
|
+
updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
|
|
358
|
+
updateSet['snapshot_lsn'] = null;
|
|
359
|
+
updateSet['last_checkpoint_ts'] = now;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await this.db.sync_rules.updateOne(
|
|
363
|
+
{
|
|
364
|
+
_id: this.group_id,
|
|
365
|
+
'sync_configs._id': this.syncConfigId
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
$set: updateSet
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
session: this.session,
|
|
372
|
+
arrayFilters: [{ 'config._id': this.syncConfigId }]
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (checkpointState.checkpointBlocked) {
|
|
377
|
+
if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
|
|
378
|
+
this.logger.info(
|
|
379
|
+
`Waiting before creating checkpoint, currently at ${lsn} / ${checkpointState.newKeepaliveOp}. Current state: ${JSON.stringify(
|
|
380
|
+
{
|
|
381
|
+
snapshot_done: state.snapshot_done,
|
|
382
|
+
last_checkpoint_lsn: state.last_checkpoint_lsn,
|
|
383
|
+
no_checkpoint_before: state.no_checkpoint_before
|
|
384
|
+
}
|
|
385
|
+
)}`
|
|
386
|
+
);
|
|
387
|
+
this.lastWaitingLogThrottledV3 = Date.now();
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
if (checkpointState.checkpointCreated) {
|
|
391
|
+
this.logger.debug(`Created checkpoint at ${lsn} / ${checkpointState.newLastCheckpoint}`);
|
|
392
|
+
}
|
|
393
|
+
await this.autoActivateV3(lsn);
|
|
394
|
+
await this.db.notifyCheckpoint();
|
|
395
|
+
this.persisted_op = null;
|
|
396
|
+
this.last_checkpoint_lsn = lsn;
|
|
397
|
+
if (checkpointState.newLastCheckpoint != null) {
|
|
398
|
+
await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
checkpointBlocked: checkpointState.checkpointBlocked,
|
|
403
|
+
checkpointCreated: checkpointState.checkpointCreated
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async keepalive(lsn: string): Promise<storage.CheckpointResult> {
|
|
408
|
+
return await this.commit(lsn, { createEmptyCheckpoints: true });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async setResumeLsn(lsn: string): Promise<void> {
|
|
412
|
+
await this.db.sync_rules.updateOne(
|
|
413
|
+
{
|
|
414
|
+
_id: this.group_id,
|
|
415
|
+
'sync_configs._id': this.syncConfigId
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
$set: {
|
|
419
|
+
snapshot_lsn: lsn
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
{ session: this.session }
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private async autoActivateV3(lsn: string): Promise<void> {
|
|
427
|
+
if (!this.needsActivationV3) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const session = this.session;
|
|
432
|
+
let activated = false;
|
|
433
|
+
await session.withTransaction(async () => {
|
|
434
|
+
const doc = await this.db.sync_rules.findOne(
|
|
435
|
+
{
|
|
436
|
+
_id: this.group_id,
|
|
437
|
+
'sync_configs._id': this.syncConfigId
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
session,
|
|
441
|
+
projection: {
|
|
442
|
+
state: 1,
|
|
443
|
+
sync_configs: {
|
|
444
|
+
$elemMatch: {
|
|
445
|
+
_id: this.syncConfigId
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
const state = (doc as ReplicationStreamDocumentV3)?.sync_configs?.[0];
|
|
452
|
+
if (
|
|
453
|
+
doc &&
|
|
454
|
+
doc.state == storage.SyncRuleState.PROCESSING &&
|
|
455
|
+
state?.state == storage.SyncRuleState.PROCESSING &&
|
|
456
|
+
state.snapshot_done &&
|
|
457
|
+
state.last_checkpoint != null
|
|
458
|
+
) {
|
|
459
|
+
await this.db.sync_rules.updateOne(
|
|
460
|
+
{
|
|
461
|
+
_id: this.group_id,
|
|
462
|
+
'sync_configs._id': this.syncConfigId
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
$set: {
|
|
466
|
+
state: storage.SyncRuleState.ACTIVE,
|
|
467
|
+
'sync_configs.$[config].state': storage.SyncRuleState.ACTIVE
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
session,
|
|
472
|
+
arrayFilters: [{ 'config._id': this.syncConfigId }]
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
await this.db.sync_rules.updateMany(
|
|
477
|
+
{
|
|
478
|
+
_id: { $ne: this.group_id },
|
|
479
|
+
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
480
|
+
},
|
|
481
|
+
syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
|
|
482
|
+
{ session }
|
|
483
|
+
);
|
|
484
|
+
activated = true;
|
|
485
|
+
} else if (doc?.state != storage.SyncRuleState.PROCESSING) {
|
|
486
|
+
this.needsActivationV3 = false;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
if (activated) {
|
|
490
|
+
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
491
|
+
await this.db.notifyCheckpoint();
|
|
492
|
+
this.needsActivationV3 = false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
|
|
497
|
+
await this.db.sync_rules.updateOne(
|
|
498
|
+
{
|
|
499
|
+
_id: this.group_id,
|
|
500
|
+
'sync_configs._id': this.syncConfigId
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
$set: {
|
|
504
|
+
'sync_configs.$[config].snapshot_done': true,
|
|
505
|
+
last_keepalive_ts: new Date()
|
|
506
|
+
},
|
|
507
|
+
$max: {
|
|
508
|
+
'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
session: this.session,
|
|
513
|
+
arrayFilters: [{ 'config._id': this.syncConfigId }]
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
|
|
519
|
+
await this.withTransaction(async () => {
|
|
520
|
+
// Protect against race conditions
|
|
521
|
+
const count = await this.db.sourceTablesV3(this.group_id).countDocuments(
|
|
522
|
+
{
|
|
523
|
+
snapshot_done: false
|
|
524
|
+
},
|
|
525
|
+
{ session: this.session }
|
|
526
|
+
);
|
|
527
|
+
if (count > 0) {
|
|
528
|
+
if (options?.throwOnConflict ?? true) {
|
|
529
|
+
throw new ReplicationAssertionError(
|
|
530
|
+
`Cannot mark snapshot done while ${count} source table${count == 1 ? '' : 's'} still require snapshotting`
|
|
531
|
+
);
|
|
532
|
+
} else {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await this.markAllSnapshotDone(no_checkpoint_before_lsn);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async markTableSnapshotRequired(_table: storage.SourceTable): Promise<void> {
|
|
542
|
+
await this.db.sync_rules.updateOne(
|
|
543
|
+
{
|
|
544
|
+
_id: this.group_id,
|
|
545
|
+
'sync_configs._id': this.syncConfigId
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
$set: {
|
|
549
|
+
'sync_configs.$[config].snapshot_done': false
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
session: this.session,
|
|
554
|
+
arrayFilters: [{ 'config._id': this.syncConfigId }]
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async markTableSnapshotDone(
|
|
560
|
+
tables: storage.SourceTable[],
|
|
561
|
+
no_checkpoint_before_lsn?: string
|
|
562
|
+
): Promise<storage.SourceTable[]> {
|
|
563
|
+
const session = this.session;
|
|
564
|
+
const ids = tables.map((table) => mongoTableId(table.id));
|
|
565
|
+
|
|
566
|
+
await this.withTransaction(async () => {
|
|
567
|
+
await this.db.commonSourceTables(this.group_id).updateMany(
|
|
568
|
+
{ _id: { $in: ids } },
|
|
569
|
+
{
|
|
570
|
+
$set: {
|
|
571
|
+
snapshot_done: true
|
|
572
|
+
},
|
|
573
|
+
$unset: {
|
|
574
|
+
snapshot_status: 1
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
{ session }
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
if (no_checkpoint_before_lsn != null) {
|
|
581
|
+
await this.db.sync_rules.updateOne(
|
|
582
|
+
{
|
|
583
|
+
_id: this.group_id,
|
|
584
|
+
'sync_configs._id': this.syncConfigId
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
$set: {
|
|
588
|
+
last_keepalive_ts: new Date()
|
|
589
|
+
},
|
|
590
|
+
$max: {
|
|
591
|
+
'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
session: this.session,
|
|
596
|
+
arrayFilters: [{ 'config._id': this.syncConfigId }]
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
return tables.map((table) => {
|
|
602
|
+
const copy = table.clone();
|
|
603
|
+
copy.snapshotComplete = true;
|
|
604
|
+
return copy;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
44
607
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { storage } from '@powersync/service-core';
|
|
4
|
-
import { BucketDefinitionId } from '
|
|
4
|
+
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
5
|
import { SingleBucketStore } from '../common/SingleBucketStore.js';
|
|
6
6
|
import { BucketStateDocumentBase } from '../models.js';
|
|
7
7
|
import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { deserializeParameterLookup } from '@powersync/service-core';
|
|
2
|
-
import { ScopedParameterLookup, SqliteJsonValue } from '@powersync/service-sync-rules';
|
|
2
|
+
import { ParameterIndexId, ScopedParameterLookup, SqliteJsonValue } from '@powersync/service-sync-rules';
|
|
3
3
|
import * as bson from 'bson';
|
|
4
|
-
import { ParameterIndexId } from '../BucketDefinitionMapping.js';
|
|
5
4
|
|
|
6
5
|
export function serializeParameterLookupV3(lookup: ScopedParameterLookup): bson.Binary {
|
|
7
6
|
return new bson.Binary(bson.serialize({ l: lookup.values.slice(2) }));
|