@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,5 +1,5 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import { HydratedSyncConfig, SqlEventDescriptor, SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
|
|
3
3
|
import * as bson from 'bson';
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -13,14 +13,12 @@ import {
|
|
|
13
13
|
} from '@powersync/lib-services-framework';
|
|
14
14
|
import {
|
|
15
15
|
BucketStorageMarkRecordUnavailable,
|
|
16
|
-
CheckpointResult,
|
|
17
16
|
deserializeBson,
|
|
18
17
|
InternalOpId,
|
|
19
18
|
isCompleteRow,
|
|
20
19
|
PerformanceTracer,
|
|
21
20
|
SaveOperationTag,
|
|
22
21
|
storage,
|
|
23
|
-
SyncRuleState,
|
|
24
22
|
utils
|
|
25
23
|
} from '@powersync/service-core';
|
|
26
24
|
import * as timers from 'node:timers/promises';
|
|
@@ -29,7 +27,6 @@ import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
|
|
|
29
27
|
import { PersistedBatch } from './common/PersistedBatch.js';
|
|
30
28
|
import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
|
|
31
29
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
32
|
-
import { SyncRuleDocument } from './models.js';
|
|
33
30
|
import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
|
|
34
31
|
import { MongoIdSequence } from './MongoIdSequence.js';
|
|
35
32
|
import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
|
|
@@ -44,9 +41,10 @@ const replicationMutex = new utils.Mutex();
|
|
|
44
41
|
|
|
45
42
|
export interface MongoBucketBatchOptions {
|
|
46
43
|
db: VersionedPowerSyncMongo;
|
|
47
|
-
syncRules:
|
|
44
|
+
syncRules: HydratedSyncConfig;
|
|
48
45
|
groupId: number;
|
|
49
46
|
slotName: string;
|
|
47
|
+
syncConfigId?: bson.ObjectId | null;
|
|
50
48
|
lastCheckpointLsn: string | null;
|
|
51
49
|
keepaliveOp: InternalOpId | null;
|
|
52
50
|
resumeFromLsn: string | null;
|
|
@@ -58,6 +56,7 @@ export interface MongoBucketBatchOptions {
|
|
|
58
56
|
skipExistingRows: boolean;
|
|
59
57
|
|
|
60
58
|
markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
59
|
+
hooks: storage.StorageHooks | undefined;
|
|
61
60
|
|
|
62
61
|
logger: Logger;
|
|
63
62
|
tracer?: PerformanceTracer<'storage' | 'evaluate'>;
|
|
@@ -67,23 +66,34 @@ export abstract class MongoBucketBatch
|
|
|
67
66
|
extends BaseObserver<storage.BucketBatchStorageListener>
|
|
68
67
|
implements storage.BucketStorageBatch
|
|
69
68
|
{
|
|
69
|
+
protected readonly options: MongoBucketBatchOptions;
|
|
70
70
|
protected logger: Logger;
|
|
71
71
|
|
|
72
72
|
private readonly client: mongo.MongoClient;
|
|
73
73
|
public readonly db: VersionedPowerSyncMongo;
|
|
74
74
|
public readonly session: mongo.ClientSession;
|
|
75
|
-
|
|
75
|
+
protected readonly sync_rules: HydratedSyncConfig;
|
|
76
76
|
|
|
77
77
|
protected readonly group_id: number;
|
|
78
78
|
|
|
79
79
|
private readonly slot_name: string;
|
|
80
|
+
/**
|
|
81
|
+
* Source-level setting for whether raw row data should be stored in current_data.
|
|
82
|
+
*
|
|
83
|
+
* Some sources always send complete rows (MongoDB, MySQL with binlog_row_image=full),
|
|
84
|
+
* in which case this is false for the whole batch. For sources where it depends on the
|
|
85
|
+
* table (Postgres REPLICA IDENTITY), this is true and the decision is refined per-table
|
|
86
|
+
* via SourceTable.storeCurrentData. The effective per-record value is the conjunction of
|
|
87
|
+
* the two.
|
|
88
|
+
*/
|
|
80
89
|
private readonly storeCurrentData: boolean;
|
|
81
|
-
|
|
90
|
+
public readonly skipExistingRows: boolean;
|
|
82
91
|
protected readonly mapping: BucketDefinitionMapping;
|
|
83
92
|
|
|
84
93
|
private batch: OperationBatch | null = null;
|
|
85
94
|
private write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
|
|
86
95
|
private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
96
|
+
private hooks: storage.StorageHooks | undefined;
|
|
87
97
|
private clearedError = false;
|
|
88
98
|
|
|
89
99
|
private tracer: PerformanceTracer<'storage' | 'evaluate'>;
|
|
@@ -95,9 +105,9 @@ export abstract class MongoBucketBatch
|
|
|
95
105
|
* 1. A commit LSN.
|
|
96
106
|
* 2. A keepalive message LSN.
|
|
97
107
|
*/
|
|
98
|
-
|
|
108
|
+
protected last_checkpoint_lsn: string | null = null;
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
protected persisted_op: InternalOpId | null = null;
|
|
101
111
|
|
|
102
112
|
/**
|
|
103
113
|
* Last written op, if any. This may not reflect a consistent checkpoint.
|
|
@@ -116,11 +126,10 @@ export abstract class MongoBucketBatch
|
|
|
116
126
|
*/
|
|
117
127
|
public resumeFromLsn: string | null = null;
|
|
118
128
|
|
|
119
|
-
private needsActivation = true;
|
|
120
|
-
|
|
121
129
|
constructor(options: MongoBucketBatchOptions) {
|
|
122
130
|
super();
|
|
123
131
|
this.logger = options.logger;
|
|
132
|
+
this.options = options;
|
|
124
133
|
this.client = options.db.client;
|
|
125
134
|
this.db = options.db;
|
|
126
135
|
this.group_id = options.groupId;
|
|
@@ -133,6 +142,7 @@ export abstract class MongoBucketBatch
|
|
|
133
142
|
this.mapping = options.mapping;
|
|
134
143
|
this.skipExistingRows = options.skipExistingRows;
|
|
135
144
|
this.markRecordUnavailable = options.markRecordUnavailable;
|
|
145
|
+
this.hooks = options.hooks;
|
|
136
146
|
this.batch = new OperationBatch();
|
|
137
147
|
|
|
138
148
|
this.persisted_op = options.keepaliveOp ?? null;
|
|
@@ -150,12 +160,33 @@ export abstract class MongoBucketBatch
|
|
|
150
160
|
return this.last_checkpoint_lsn;
|
|
151
161
|
}
|
|
152
162
|
|
|
163
|
+
abstract resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult>;
|
|
164
|
+
|
|
153
165
|
protected abstract createPersistedBatch(writtenSize: number): PersistedBatch;
|
|
154
166
|
|
|
155
167
|
protected abstract get sourceRecordStore(): SourceRecordStore;
|
|
156
168
|
|
|
157
169
|
protected abstract cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]): Promise<void>;
|
|
158
170
|
|
|
171
|
+
abstract commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult>;
|
|
172
|
+
|
|
173
|
+
abstract keepalive(lsn: string): Promise<storage.CheckpointResult>;
|
|
174
|
+
|
|
175
|
+
abstract setResumeLsn(lsn: string): Promise<void>;
|
|
176
|
+
|
|
177
|
+
abstract getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null>;
|
|
178
|
+
|
|
179
|
+
abstract markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void>;
|
|
180
|
+
|
|
181
|
+
abstract markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
|
|
182
|
+
|
|
183
|
+
abstract markTableSnapshotRequired(table: storage.SourceTable): Promise<void>;
|
|
184
|
+
|
|
185
|
+
abstract markTableSnapshotDone(
|
|
186
|
+
tables: storage.SourceTable[],
|
|
187
|
+
no_checkpoint_before_lsn?: string
|
|
188
|
+
): Promise<storage.SourceTable[]>;
|
|
189
|
+
|
|
159
190
|
async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
|
|
160
191
|
let result: storage.FlushedResult | null = null;
|
|
161
192
|
// One flush may be split over multiple transactions.
|
|
@@ -176,6 +207,8 @@ export abstract class MongoBucketBatch
|
|
|
176
207
|
|
|
177
208
|
using _ = this.tracer.span('storage', 'flush');
|
|
178
209
|
|
|
210
|
+
await this.hooks?.beforeBatchFlush?.(this);
|
|
211
|
+
|
|
179
212
|
await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
|
|
180
213
|
if (batch != null) {
|
|
181
214
|
resumeBatch = await this.replicateBatch(session, batch, opSeq, options);
|
|
@@ -199,6 +232,7 @@ export abstract class MongoBucketBatch
|
|
|
199
232
|
|
|
200
233
|
this.persisted_op = last_op;
|
|
201
234
|
this.last_flushed_op = last_op;
|
|
235
|
+
await this.hooks?.afterBatchFlush?.(this);
|
|
202
236
|
return { flushed_op: last_op };
|
|
203
237
|
}
|
|
204
238
|
|
|
@@ -210,8 +244,12 @@ export abstract class MongoBucketBatch
|
|
|
210
244
|
): Promise<OperationBatch | null> {
|
|
211
245
|
let sizes: Map<string, number> | undefined = undefined;
|
|
212
246
|
using _ = this.tracer.span('storage', 'replicate_batch');
|
|
213
|
-
if
|
|
214
|
-
|
|
247
|
+
// Only look up current_data sizes if the batch stores current_data and at least one
|
|
248
|
+
// table in it does too (per-table can disable it, e.g. Postgres REPLICA IDENTITY FULL).
|
|
249
|
+
const anyTableStoresCurrentData =
|
|
250
|
+
this.storeCurrentData && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
|
|
251
|
+
if (anyTableStoresCurrentData && !this.skipExistingRows) {
|
|
252
|
+
// We skip this step if no tables store current_data, since the sizes will
|
|
215
253
|
// always be small in that case.
|
|
216
254
|
|
|
217
255
|
// With skipExistingRows, we don't load the full documents into memory,
|
|
@@ -224,10 +262,14 @@ export abstract class MongoBucketBatch
|
|
|
224
262
|
// (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
|
|
225
263
|
// the order of processing, which then becomes really tricky to manage.
|
|
226
264
|
// This now takes 2+ queries, but doesn't have any issues with order of operations.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
265
|
+
// Within this branch this.storeCurrentData is true, so the per-table flag is the
|
|
266
|
+
// effective value - only look up sizes for tables that actually store current_data.
|
|
267
|
+
const sizeLookups = batch.batch
|
|
268
|
+
.filter((r) => r.record.sourceTable.storeCurrentData)
|
|
269
|
+
.map((r) => ({
|
|
270
|
+
sourceTableId: mongoTableId(r.record.sourceTable.id),
|
|
271
|
+
replicaId: r.beforeId
|
|
272
|
+
}));
|
|
231
273
|
|
|
232
274
|
sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
|
|
233
275
|
}
|
|
@@ -323,6 +365,9 @@ export abstract class MongoBucketBatch
|
|
|
323
365
|
const afterId = operation.afterId;
|
|
324
366
|
let after = record.after;
|
|
325
367
|
const sourceTable = record.sourceTable;
|
|
368
|
+
// Effective per-record flag: store current_data only if both the batch (source-level,
|
|
369
|
+
// e.g. Postgres) and the table (e.g. non-FULL replica identity) require it.
|
|
370
|
+
const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
|
|
326
371
|
|
|
327
372
|
let existing_buckets: LoadedSourceRecord['buckets'] = [];
|
|
328
373
|
let new_buckets: LoadedSourceRecord['buckets'] = [];
|
|
@@ -351,7 +396,7 @@ export abstract class MongoBucketBatch
|
|
|
351
396
|
// Not an error if we re-apply a transaction
|
|
352
397
|
existing_buckets = [];
|
|
353
398
|
existing_lookups = [];
|
|
354
|
-
if (!isCompleteRow(
|
|
399
|
+
if (!isCompleteRow(storeCurrentData, after!)) {
|
|
355
400
|
if (this.markRecordUnavailable != null) {
|
|
356
401
|
// This will trigger a "resnapshot" of the record.
|
|
357
402
|
// This is not relevant if storeCurrentData is false, since we'll get the full row
|
|
@@ -367,7 +412,7 @@ export abstract class MongoBucketBatch
|
|
|
367
412
|
} else {
|
|
368
413
|
existing_buckets = result.buckets;
|
|
369
414
|
existing_lookups = result.lookups;
|
|
370
|
-
if (
|
|
415
|
+
if (storeCurrentData && result.data != null) {
|
|
371
416
|
const data = deserializeBson(result.data.buffer) as SqliteRow;
|
|
372
417
|
after = storage.mergeToast<SqliteValue>(after!, data);
|
|
373
418
|
}
|
|
@@ -378,7 +423,9 @@ export abstract class MongoBucketBatch
|
|
|
378
423
|
// Not an error if we re-apply a transaction
|
|
379
424
|
existing_buckets = [];
|
|
380
425
|
existing_lookups = [];
|
|
381
|
-
// Log to help with debugging if there was a consistency issue
|
|
426
|
+
// Log to help with debugging if there was a consistency issue.
|
|
427
|
+
// Gate on the batch-level flag: FULL tables (per-record flag false) still get a
|
|
428
|
+
// current_data entry, so a missing record on DELETE is meaningful for them too.
|
|
382
429
|
if (this.storeCurrentData && this.markRecordUnavailable == null) {
|
|
383
430
|
this.logger.warn(
|
|
384
431
|
`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
|
|
@@ -391,7 +438,7 @@ export abstract class MongoBucketBatch
|
|
|
391
438
|
}
|
|
392
439
|
|
|
393
440
|
let afterData: bson.Binary | null = null;
|
|
394
|
-
if (afterId != null && !
|
|
441
|
+
if (afterId != null && !storeCurrentData) {
|
|
395
442
|
afterData = null;
|
|
396
443
|
} else if (afterId != null) {
|
|
397
444
|
try {
|
|
@@ -458,13 +505,15 @@ export abstract class MongoBucketBatch
|
|
|
458
505
|
// However, it will be valid by the end of the transaction.
|
|
459
506
|
//
|
|
460
507
|
// In this case, we don't save the op, but we do save the current data.
|
|
461
|
-
if (afterId && after && utils.isCompleteRow(
|
|
508
|
+
if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
|
|
462
509
|
// Insert or update
|
|
463
510
|
if (sourceTable.syncData) {
|
|
464
|
-
const { results
|
|
511
|
+
const { results, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
|
|
465
512
|
record: after,
|
|
466
|
-
sourceTable
|
|
513
|
+
sourceTable: sourceTable.ref,
|
|
514
|
+
bucketDataSources: sourceTable.bucketDataSources
|
|
467
515
|
});
|
|
516
|
+
const evaluated = results;
|
|
468
517
|
|
|
469
518
|
for (let error of syncErrors) {
|
|
470
519
|
container.reporter.captureMessage(
|
|
@@ -496,8 +545,9 @@ export abstract class MongoBucketBatch
|
|
|
496
545
|
if (sourceTable.syncParameters) {
|
|
497
546
|
// Parameters
|
|
498
547
|
const { results: paramEvaluated, errors: paramErrors } = this.sync_rules.evaluateParameterRowWithErrors(
|
|
499
|
-
sourceTable,
|
|
500
|
-
after
|
|
548
|
+
sourceTable.ref,
|
|
549
|
+
after,
|
|
550
|
+
{ parameterLookupSources: sourceTable.parameterLookupSources }
|
|
501
551
|
);
|
|
502
552
|
|
|
503
553
|
for (let error of paramErrors) {
|
|
@@ -559,7 +609,7 @@ export abstract class MongoBucketBatch
|
|
|
559
609
|
return result;
|
|
560
610
|
}
|
|
561
611
|
|
|
562
|
-
|
|
612
|
+
protected async withTransaction(cb: () => Promise<void>) {
|
|
563
613
|
using lockSpan = this.tracer.span('storage', 'internal_lock');
|
|
564
614
|
await replicationMutex.exclusiveLock(async () => {
|
|
565
615
|
lockSpan.end();
|
|
@@ -669,259 +719,9 @@ export abstract class MongoBucketBatch
|
|
|
669
719
|
await this[Symbol.asyncDispose]();
|
|
670
720
|
}
|
|
671
721
|
|
|
672
|
-
private lastWaitingLogThottled = 0;
|
|
673
|
-
|
|
674
|
-
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<CheckpointResult> {
|
|
675
|
-
const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
|
|
676
|
-
|
|
677
|
-
await this.flush(options);
|
|
678
|
-
|
|
679
|
-
const now = new Date();
|
|
680
|
-
|
|
681
|
-
// Mark relevant write checkpoints as "processed".
|
|
682
|
-
// This makes it easier to identify write checkpoints that are "valid" in order.
|
|
683
|
-
await this.db.write_checkpoints.updateMany(
|
|
684
|
-
{
|
|
685
|
-
processed_at_lsn: null,
|
|
686
|
-
'lsns.1': { $lte: lsn }
|
|
687
|
-
},
|
|
688
|
-
{
|
|
689
|
-
$set: {
|
|
690
|
-
processed_at_lsn: lsn
|
|
691
|
-
}
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
session: this.session
|
|
695
|
-
}
|
|
696
|
-
);
|
|
697
|
-
|
|
698
|
-
const can_checkpoint = {
|
|
699
|
-
$and: [
|
|
700
|
-
{ $eq: ['$snapshot_done', true] },
|
|
701
|
-
{
|
|
702
|
-
$or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
|
|
703
|
-
},
|
|
704
|
-
{
|
|
705
|
-
$or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
|
|
706
|
-
}
|
|
707
|
-
]
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
const new_keepalive_op = {
|
|
711
|
-
$cond: [
|
|
712
|
-
can_checkpoint,
|
|
713
|
-
{ $literal: null },
|
|
714
|
-
{
|
|
715
|
-
$toString: {
|
|
716
|
-
$max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
]
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
const new_last_checkpoint = {
|
|
723
|
-
$cond: [
|
|
724
|
-
can_checkpoint,
|
|
725
|
-
{
|
|
726
|
-
$max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
|
|
727
|
-
},
|
|
728
|
-
'$last_checkpoint'
|
|
729
|
-
]
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
// For this query, we need to handle multiple cases, depending on the state:
|
|
733
|
-
// 1. Normal commit - advance last_checkpoint to this.persisted_op.
|
|
734
|
-
// 2. Commit delayed by no_checkpoint_before due to snapshot. In this case we only advance keepalive_op.
|
|
735
|
-
// 3. Commit with no new data - here may may set last_checkpoint = keepalive_op, if a delayed commit is relevant.
|
|
736
|
-
// We want to do as much as possible in a single atomic database operation, which makes this somewhat complex.
|
|
737
|
-
let preUpdateDocument = await this.db.sync_rules.findOneAndUpdate(
|
|
738
|
-
{ _id: this.group_id },
|
|
739
|
-
[
|
|
740
|
-
{
|
|
741
|
-
$set: {
|
|
742
|
-
_can_checkpoint: can_checkpoint,
|
|
743
|
-
_not_empty: createEmptyCheckpoints
|
|
744
|
-
? true
|
|
745
|
-
: {
|
|
746
|
-
$or: [
|
|
747
|
-
{ $literal: createEmptyCheckpoints },
|
|
748
|
-
{ $ne: ['$keepalive_op', new_keepalive_op] },
|
|
749
|
-
{ $ne: ['$last_checkpoint', new_last_checkpoint] }
|
|
750
|
-
]
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
},
|
|
754
|
-
{
|
|
755
|
-
$set: {
|
|
756
|
-
last_checkpoint_lsn: {
|
|
757
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
|
|
758
|
-
},
|
|
759
|
-
last_checkpoint_ts: {
|
|
760
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
|
|
761
|
-
},
|
|
762
|
-
last_keepalive_ts: { $literal: now },
|
|
763
|
-
last_fatal_error: { $literal: null },
|
|
764
|
-
last_fatal_error_ts: { $literal: null },
|
|
765
|
-
keepalive_op: new_keepalive_op,
|
|
766
|
-
last_checkpoint: new_last_checkpoint,
|
|
767
|
-
// Unset snapshot_lsn on checkpoint
|
|
768
|
-
snapshot_lsn: {
|
|
769
|
-
$cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
$unset: ['_can_checkpoint', '_not_empty']
|
|
775
|
-
}
|
|
776
|
-
],
|
|
777
|
-
{
|
|
778
|
-
session: this.session,
|
|
779
|
-
// We return the before document, so that we can check the previous state to determine if a checkpoint was actually created or if we were blocked by snapshot/no_checkpoint_before.
|
|
780
|
-
returnDocument: 'before',
|
|
781
|
-
projection: {
|
|
782
|
-
snapshot_done: 1,
|
|
783
|
-
last_checkpoint_lsn: 1,
|
|
784
|
-
no_checkpoint_before: 1,
|
|
785
|
-
keepalive_op: 1,
|
|
786
|
-
last_checkpoint: 1
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
);
|
|
790
|
-
|
|
791
|
-
if (preUpdateDocument == null) {
|
|
792
|
-
throw new ReplicationAssertionError(
|
|
793
|
-
'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// This re-implements the same logic as in the pipeline, to determine what was actually updated.
|
|
798
|
-
// Unfortunately we cannot return these from the pipeline directly, so we need to re-implement the logic.
|
|
799
|
-
const canCheckpoint =
|
|
800
|
-
preUpdateDocument.snapshot_done === true &&
|
|
801
|
-
(preUpdateDocument.last_checkpoint_lsn == null || preUpdateDocument.last_checkpoint_lsn <= lsn) &&
|
|
802
|
-
(preUpdateDocument.no_checkpoint_before == null || preUpdateDocument.no_checkpoint_before <= lsn);
|
|
803
|
-
|
|
804
|
-
const keepaliveOp = preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op);
|
|
805
|
-
const maxKeepalive = [keepaliveOp ?? 0n, this.persisted_op ?? 0n, 0n].reduce((a, b) => (a > b ? a : b));
|
|
806
|
-
const newKeepaliveOp = canCheckpoint ? null : maxKeepalive.toString();
|
|
807
|
-
const newLastCheckpoint = canCheckpoint
|
|
808
|
-
? [preUpdateDocument.last_checkpoint ?? 0n, this.persisted_op ?? 0n, keepaliveOp ?? 0n, 0n].reduce((a, b) =>
|
|
809
|
-
a > b ? a : b
|
|
810
|
-
)
|
|
811
|
-
: preUpdateDocument.last_checkpoint;
|
|
812
|
-
const notEmpty =
|
|
813
|
-
createEmptyCheckpoints ||
|
|
814
|
-
preUpdateDocument.keepalive_op !== newKeepaliveOp ||
|
|
815
|
-
preUpdateDocument.last_checkpoint !== newLastCheckpoint;
|
|
816
|
-
const checkpointCreated = canCheckpoint && notEmpty;
|
|
817
|
-
|
|
818
|
-
const checkpointBlocked = !canCheckpoint;
|
|
819
|
-
|
|
820
|
-
if (checkpointBlocked) {
|
|
821
|
-
// Failed on snapshot_done or no_checkpoint_before.
|
|
822
|
-
if (Date.now() - this.lastWaitingLogThottled > 5_000) {
|
|
823
|
-
this.logger.info(
|
|
824
|
-
`Waiting before creating checkpoint, currently at ${lsn} / ${preUpdateDocument.keepalive_op}. Current state: ${JSON.stringify(
|
|
825
|
-
{
|
|
826
|
-
snapshot_done: preUpdateDocument.snapshot_done,
|
|
827
|
-
last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
|
|
828
|
-
no_checkpoint_before: preUpdateDocument.no_checkpoint_before
|
|
829
|
-
}
|
|
830
|
-
)}`
|
|
831
|
-
);
|
|
832
|
-
this.lastWaitingLogThottled = Date.now();
|
|
833
|
-
}
|
|
834
|
-
} else {
|
|
835
|
-
if (checkpointCreated) {
|
|
836
|
-
this.logger.debug(`Created checkpoint at ${lsn} / ${newLastCheckpoint}`);
|
|
837
|
-
}
|
|
838
|
-
await this.autoActivate(lsn);
|
|
839
|
-
await this.db.notifyCheckpoint();
|
|
840
|
-
this.persisted_op = null;
|
|
841
|
-
this.last_checkpoint_lsn = lsn;
|
|
842
|
-
if (newLastCheckpoint != null) {
|
|
843
|
-
await this.sourceRecordStore.postCommitCleanup(newLastCheckpoint, this.logger);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
return { checkpointBlocked, checkpointCreated };
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Switch from processing -> active if relevant.
|
|
851
|
-
*
|
|
852
|
-
* Called on new commits.
|
|
853
|
-
*/
|
|
854
|
-
private async autoActivate(lsn: string) {
|
|
855
|
-
if (!this.needsActivation) {
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// Activate the batch, so it can start processing.
|
|
860
|
-
// This is done automatically when the first save() is called.
|
|
861
|
-
|
|
862
|
-
const session = this.session;
|
|
863
|
-
let activated = false;
|
|
864
|
-
await session.withTransaction(async () => {
|
|
865
|
-
const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
|
|
866
|
-
if (doc && doc.state == SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
|
|
867
|
-
await this.db.sync_rules.updateOne(
|
|
868
|
-
{
|
|
869
|
-
_id: this.group_id
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
$set: {
|
|
873
|
-
state: storage.SyncRuleState.ACTIVE
|
|
874
|
-
}
|
|
875
|
-
},
|
|
876
|
-
{ session }
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
await this.db.sync_rules.updateMany(
|
|
880
|
-
{
|
|
881
|
-
_id: { $ne: this.group_id },
|
|
882
|
-
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
883
|
-
},
|
|
884
|
-
{
|
|
885
|
-
$set: {
|
|
886
|
-
state: storage.SyncRuleState.STOP
|
|
887
|
-
}
|
|
888
|
-
},
|
|
889
|
-
{ session }
|
|
890
|
-
);
|
|
891
|
-
activated = true;
|
|
892
|
-
} else if (doc?.state != SyncRuleState.PROCESSING) {
|
|
893
|
-
this.needsActivation = false;
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
if (activated) {
|
|
897
|
-
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
898
|
-
await this.db.notifyCheckpoint();
|
|
899
|
-
this.needsActivation = false;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
async keepalive(lsn: string): Promise<CheckpointResult> {
|
|
904
|
-
return await this.commit(lsn, { createEmptyCheckpoints: true });
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
async setResumeLsn(lsn: string): Promise<void> {
|
|
908
|
-
const update: Partial<SyncRuleDocument> = {
|
|
909
|
-
snapshot_lsn: lsn
|
|
910
|
-
};
|
|
911
|
-
|
|
912
|
-
await this.db.sync_rules.updateOne(
|
|
913
|
-
{
|
|
914
|
-
_id: this.group_id
|
|
915
|
-
},
|
|
916
|
-
{
|
|
917
|
-
$set: update
|
|
918
|
-
},
|
|
919
|
-
{ session: this.session }
|
|
920
|
-
);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
722
|
async save(record: storage.SaveOptions): Promise<storage.FlushedResult | null> {
|
|
924
723
|
const { after, before, sourceTable, tag } = record;
|
|
724
|
+
const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
|
|
925
725
|
for (const event of this.getTableEvents(sourceTable)) {
|
|
926
726
|
this.iterateListeners((cb) =>
|
|
927
727
|
cb.replicationEvent?.({
|
|
@@ -929,8 +729,8 @@ export abstract class MongoBucketBatch
|
|
|
929
729
|
table: sourceTable,
|
|
930
730
|
data: {
|
|
931
731
|
op: tag,
|
|
932
|
-
after: after && utils.isCompleteRow(
|
|
933
|
-
before: before && utils.isCompleteRow(
|
|
732
|
+
after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
|
|
733
|
+
before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
|
|
934
734
|
},
|
|
935
735
|
event
|
|
936
736
|
})
|
|
@@ -1071,80 +871,6 @@ export abstract class MongoBucketBatch
|
|
|
1071
871
|
return copy;
|
|
1072
872
|
}
|
|
1073
873
|
|
|
1074
|
-
async markAllSnapshotDone(no_checkpoint_before_lsn: string) {
|
|
1075
|
-
await this.db.sync_rules.updateOne(
|
|
1076
|
-
{
|
|
1077
|
-
_id: this.group_id
|
|
1078
|
-
},
|
|
1079
|
-
{
|
|
1080
|
-
$set: {
|
|
1081
|
-
snapshot_done: true,
|
|
1082
|
-
last_keepalive_ts: new Date()
|
|
1083
|
-
},
|
|
1084
|
-
$max: {
|
|
1085
|
-
no_checkpoint_before: no_checkpoint_before_lsn
|
|
1086
|
-
}
|
|
1087
|
-
},
|
|
1088
|
-
{ session: this.session }
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
|
|
1093
|
-
await this.db.sync_rules.updateOne(
|
|
1094
|
-
{
|
|
1095
|
-
_id: this.group_id
|
|
1096
|
-
},
|
|
1097
|
-
{
|
|
1098
|
-
$set: {
|
|
1099
|
-
snapshot_done: false
|
|
1100
|
-
}
|
|
1101
|
-
},
|
|
1102
|
-
{ session: this.session }
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
async markTableSnapshotDone(tables: storage.SourceTable[], no_checkpoint_before_lsn?: string) {
|
|
1107
|
-
const session = this.session;
|
|
1108
|
-
const ids = tables.map((table) => mongoTableId(table.id));
|
|
1109
|
-
|
|
1110
|
-
await this.withTransaction(async () => {
|
|
1111
|
-
await this.db.commonSourceTables(this.group_id).updateMany(
|
|
1112
|
-
{ _id: { $in: ids } },
|
|
1113
|
-
{
|
|
1114
|
-
$set: {
|
|
1115
|
-
snapshot_done: true
|
|
1116
|
-
},
|
|
1117
|
-
$unset: {
|
|
1118
|
-
snapshot_status: 1
|
|
1119
|
-
}
|
|
1120
|
-
},
|
|
1121
|
-
{ session }
|
|
1122
|
-
);
|
|
1123
|
-
|
|
1124
|
-
if (no_checkpoint_before_lsn != null) {
|
|
1125
|
-
await this.db.sync_rules.updateOne(
|
|
1126
|
-
{
|
|
1127
|
-
_id: this.group_id
|
|
1128
|
-
},
|
|
1129
|
-
{
|
|
1130
|
-
$set: {
|
|
1131
|
-
last_keepalive_ts: new Date()
|
|
1132
|
-
},
|
|
1133
|
-
$max: {
|
|
1134
|
-
no_checkpoint_before: no_checkpoint_before_lsn
|
|
1135
|
-
}
|
|
1136
|
-
},
|
|
1137
|
-
{ session: this.session }
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
return tables.map((table) => {
|
|
1142
|
-
const copy = table.clone();
|
|
1143
|
-
copy.snapshotComplete = true;
|
|
1144
|
-
return copy;
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
874
|
protected async clearError(): Promise<void> {
|
|
1149
875
|
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
1150
876
|
if (this.clearedError) {
|
|
@@ -1170,7 +896,7 @@ export abstract class MongoBucketBatch
|
|
|
1170
896
|
*/
|
|
1171
897
|
protected getTableEvents(table: storage.SourceTable): SqlEventDescriptor[] {
|
|
1172
898
|
return this.sync_rules.eventDescriptors.filter((evt) =>
|
|
1173
|
-
[...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table))
|
|
899
|
+
[...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref))
|
|
1174
900
|
);
|
|
1175
901
|
}
|
|
1176
902
|
}
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
16
16
|
|
|
17
17
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
18
|
-
import { BucketDefinitionId
|
|
18
|
+
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
19
|
+
import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
|
|
19
20
|
import { BucketDataDocumentBase, StorageConfig } from './models.js';
|
|
20
21
|
|
|
21
22
|
export interface FetchPartialBucketChecksumV3 {
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
storage,
|
|
14
14
|
utils
|
|
15
15
|
} from '@powersync/service-core';
|
|
16
|
+
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
16
17
|
|
|
17
|
-
import { BucketDefinitionId } from './BucketDefinitionMapping.js';
|
|
18
18
|
import { BucketDataDoc, BucketKey } from './common/BucketDataDoc.js';
|
|
19
19
|
import { BucketDataDocumentGeneric, SingleBucketStore } from './common/SingleBucketStore.js';
|
|
20
20
|
import type { VersionedPowerSyncMongo } from './db.js';
|