@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +6 -4
  3. package/dist/storage/MongoBucketStorage.js +110 -36
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +4 -6
  6. package/dist/storage/implementation/BucketDefinitionMapping.js +3 -3
  7. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
  8. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  9. package/dist/storage/implementation/CheckpointState.js +31 -0
  10. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  11. package/dist/storage/implementation/MongoBucketBatch.d.ts +33 -22
  12. package/dist/storage/implementation/MongoBucketBatch.js +45 -271
  13. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  14. package/dist/storage/implementation/MongoChecksums.d.ts +2 -1
  15. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  16. package/dist/storage/implementation/MongoCompactor.d.ts +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +4 -4
  18. package/dist/storage/implementation/MongoPersistedSyncRules.js +11 -8
  19. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -1
  20. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +19 -5
  21. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +53 -19
  22. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  23. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +21 -10
  24. package/dist/storage/implementation/MongoSyncBucketStorage.js +18 -163
  25. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  26. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  27. package/dist/storage/implementation/MongoSyncRulesLock.js +7 -3
  28. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  32. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -2
  34. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  35. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  36. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  37. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  38. package/dist/storage/implementation/db.d.ts +10 -2
  39. package/dist/storage/implementation/db.js.map +1 -1
  40. package/dist/storage/implementation/models.d.ts +31 -47
  41. package/dist/storage/implementation/models.js.map +1 -1
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +385 -0
  44. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  45. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  46. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +16 -7
  47. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +77 -6
  48. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  49. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  50. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  51. package/dist/storage/implementation/v1/models.d.ts +12 -1
  52. package/dist/storage/implementation/v1/models.js.map +1 -1
  53. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +17 -0
  54. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +429 -0
  55. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  56. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +1 -1
  57. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +1 -2
  58. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -1
  59. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +29 -7
  60. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +117 -16
  61. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  62. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -2
  63. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +3 -2
  65. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
  66. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  67. package/dist/storage/implementation/v3/models.d.ts +61 -3
  68. package/dist/storage/implementation/v3/models.js.map +1 -1
  69. package/package.json +6 -6
  70. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  71. package/src/storage/MongoBucketStorage.ts +166 -44
  72. package/src/storage/implementation/BucketDefinitionMapping.ts +12 -9
  73. package/src/storage/implementation/CheckpointState.ts +59 -0
  74. package/src/storage/implementation/MongoBucketBatch.ts +81 -355
  75. package/src/storage/implementation/MongoChecksums.ts +2 -1
  76. package/src/storage/implementation/MongoCompactor.ts +1 -1
  77. package/src/storage/implementation/MongoPersistedSyncRules.ts +13 -7
  78. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +69 -24
  79. package/src/storage/implementation/MongoSyncBucketStorage.ts +40 -215
  80. package/src/storage/implementation/MongoSyncRulesLock.ts +9 -3
  81. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  82. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  83. package/src/storage/implementation/common/PersistedBatch.ts +2 -2
  84. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  85. package/src/storage/implementation/createMongoSyncBucketStorage.ts +2 -2
  86. package/src/storage/implementation/db.ts +5 -2
  87. package/src/storage/implementation/models.ts +35 -58
  88. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +478 -1
  89. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  90. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +111 -16
  91. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  92. package/src/storage/implementation/v1/models.ts +15 -0
  93. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +564 -1
  94. package/src/storage/implementation/v3/MongoCompactorV3.ts +1 -1
  95. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +1 -2
  96. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +150 -22
  97. package/src/storage/implementation/v3/PersistedBatchV3.ts +1 -2
  98. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +7 -2
  99. package/src/storage/implementation/v3/models.ts +70 -2
  100. package/test/src/storage_sync.test.ts +422 -6
  101. package/test/src/storeCurrentData.test.ts +211 -0
  102. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { HydratedSyncRules, SqlEventDescriptor, SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
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: HydratedSyncRules;
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
- private readonly sync_rules: HydratedSyncRules;
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
- private readonly skipExistingRows: boolean;
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
- private last_checkpoint_lsn: string | null = null;
108
+ protected last_checkpoint_lsn: string | null = null;
99
109
 
100
- private persisted_op: InternalOpId | null = null;
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 (this.storeCurrentData && !this.skipExistingRows) {
214
- // We skip this step if we don't store current_data, since the sizes will
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
- const sizeLookups = batch.batch.map((r) => ({
228
- sourceTableId: mongoTableId(r.record.sourceTable.id),
229
- replicaId: r.beforeId
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(this.storeCurrentData, after!)) {
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 (this.storeCurrentData && result.data != null) {
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 && !this.storeCurrentData) {
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(this.storeCurrentData, after)) {
508
+ if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
462
509
  // Insert or update
463
510
  if (sourceTable.syncData) {
464
- const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
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
- private async withTransaction(cb: () => Promise<void>) {
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(this.storeCurrentData, after) ? after : undefined,
933
- before: before && utils.isCompleteRow(this.storeCurrentData, before) ? before : undefined
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, BucketDefinitionMapping } from './BucketDefinitionMapping.js';
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';