@powersync/service-module-mongodb-storage 0.16.0 → 0.18.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 (206) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +35 -15
  3. package/dist/storage/MongoBucketStorage.js +309 -86
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  6. package/dist/storage/implementation/CheckpointState.js +31 -0
  7. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +69 -39
  9. package/dist/storage/implementation/MongoBucketBatch.js +104 -313
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +11 -9
  12. package/dist/storage/implementation/MongoChecksums.js +3 -111
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +3 -1
  15. package/dist/storage/implementation/MongoCompactor.js +18 -9
  16. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  17. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  18. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  19. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  20. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  21. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  25. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  27. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  28. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  29. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  30. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +121 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +50 -21
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +62 -199
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +9 -5
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  41. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  42. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -3
  48. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  53. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  54. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  55. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  56. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  57. package/dist/storage/implementation/db.d.ts +10 -2
  58. package/dist/storage/implementation/db.js +9 -14
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +25 -50
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +23 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +403 -2
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +22 -15
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +100 -27
  70. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +21 -1
  77. package/dist/storage/implementation/v1/models.js.map +1 -1
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +21 -0
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +459 -3
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  82. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +20 -3
  85. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  86. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +50 -16
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +292 -75
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -3
  94. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  95. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  96. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  97. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +16 -14
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +28 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +111 -10
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  114. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  115. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  116. package/dist/storage/storage-index.d.ts +4 -4
  117. package/dist/storage/storage-index.js +4 -4
  118. package/dist/storage/storage-index.js.map +1 -1
  119. package/dist/types/types.d.ts +2 -2
  120. package/dist/utils/test-utils.d.ts +1 -2
  121. package/dist/utils/test-utils.js +5 -1
  122. package/dist/utils/test-utils.js.map +1 -1
  123. package/dist/utils/util.d.ts +1 -1
  124. package/dist/utils/util.js +3 -2
  125. package/dist/utils/util.js.map +1 -1
  126. package/package.json +9 -19
  127. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  128. package/src/storage/MongoBucketStorage.ts +464 -95
  129. package/src/storage/implementation/CheckpointState.ts +59 -0
  130. package/src/storage/implementation/MongoBucketBatch.ts +159 -405
  131. package/src/storage/implementation/MongoChecksums.ts +7 -138
  132. package/src/storage/implementation/MongoCompactor.ts +20 -10
  133. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  134. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  135. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  136. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  137. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +152 -0
  138. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  139. package/src/storage/implementation/MongoSyncBucketStorage.ts +89 -250
  140. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -5
  141. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  142. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  143. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  144. package/src/storage/implementation/common/PersistedBatch.ts +8 -4
  145. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  146. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  147. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  148. package/src/storage/implementation/db.ts +16 -21
  149. package/src/storage/implementation/models.ts +28 -61
  150. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +503 -5
  151. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  152. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  153. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +150 -49
  154. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  155. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  156. package/src/storage/implementation/v1/models.ts +25 -0
  157. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +611 -4
  158. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  159. package/src/storage/implementation/v3/MongoCompactorV3.ts +647 -30
  160. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  161. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +397 -111
  162. package/src/storage/implementation/v3/PersistedBatchV3.ts +63 -38
  163. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  164. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  165. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +48 -38
  166. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  167. package/src/storage/implementation/v3/chunking.ts +38 -0
  168. package/src/storage/implementation/v3/models.ts +150 -44
  169. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  170. package/src/storage/storage-index.ts +12 -4
  171. package/src/utils/test-utils.ts +6 -3
  172. package/src/utils/util.ts +3 -2
  173. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  174. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  175. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  176. package/test/src/storage.test.ts +11 -11
  177. package/test/src/storage_compacting.test.ts +1956 -6
  178. package/test/src/storage_sync.test.ts +1429 -42
  179. package/test/src/storeCurrentData.test.ts +221 -0
  180. package/test/src/util.ts +2 -1
  181. package/tsconfig.json +1 -4
  182. package/tsconfig.tsbuildinfo +1 -1
  183. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -17
  184. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  185. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  186. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  187. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -64
  188. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  189. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -15
  190. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -42
  191. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  192. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  193. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  194. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  195. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  196. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  197. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  198. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -5
  199. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  200. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  201. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -72
  202. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -76
  203. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -56
  204. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  205. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  206. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -12
@@ -1,7 +1,7 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import * as bson from 'bson';
3
3
  import { BaseObserver, container, ErrorCode, errors, ReplicationAssertionError, ServiceError } from '@powersync/lib-services-framework';
4
- import { deserializeBson, isCompleteRow, PerformanceTracer, SaveOperationTag, storage, SyncRuleState, utils } from '@powersync/service-core';
4
+ import { deserializeBson, isCompleteRow, PerformanceTracer, SaveOperationTag, storage, utils } from '@powersync/service-core';
5
5
  import * as timers from 'node:timers/promises';
6
6
  import { mongoTableId } from '../../utils/util.js';
7
7
  import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
@@ -15,38 +15,40 @@ import { OperationBatch, RecordOperation } from './OperationBatch.js';
15
15
  // In the future, we can investigate allowing multiple replication streams operating independently.
16
16
  const replicationMutex = new utils.Mutex();
17
17
  export class MongoBucketBatch extends BaseObserver {
18
+ options;
18
19
  logger;
19
20
  client;
20
21
  db;
21
22
  session;
22
23
  sync_rules;
23
- group_id;
24
- slot_name;
24
+ replicationStreamId;
25
+ replicationStreamName;
26
+ /**
27
+ * Source-level setting for whether raw row data should be stored in current_data.
28
+ *
29
+ * Some sources always send complete rows (MongoDB, MySQL with binlog_row_image=full),
30
+ * in which case this is false for the whole batch. For sources where it depends on the
31
+ * table (Postgres REPLICA IDENTITY), this is true and the decision is refined per-table
32
+ * via SourceTable.storeCurrentData. The effective per-record value is the conjunction of
33
+ * the two.
34
+ */
25
35
  storeCurrentData;
26
36
  skipExistingRows;
27
37
  mapping;
28
38
  batch = null;
29
39
  write_checkpoint_batch = [];
30
40
  markRecordUnavailable;
41
+ hooks;
31
42
  clearedError = false;
32
43
  tracer;
33
- /**
34
- * Last LSN received associated with a checkpoint.
35
- *
36
- * This could be either:
37
- * 1. A commit LSN.
38
- * 2. A keepalive message LSN.
39
- */
40
- last_checkpoint_lsn = null;
41
- persisted_op = null;
42
44
  /**
43
45
  * Last written op, if any. This may not reflect a consistent checkpoint.
44
46
  */
45
47
  last_flushed_op = null;
46
48
  /**
47
- * lastCheckpointLsn is the last consistent commit.
49
+ * LSN to resume replication from.
48
50
  *
49
- * While that is generally a "safe" point to resume from, there are cases where we may want to resume from a different point:
51
+ * This is typically the last commit LSN, but there are cases where it differs:
50
52
  * 1. After an initial snapshot, we don't have a consistent commit yet, but need to resume from the snapshot LSN.
51
53
  * 2. If "no_checkpoint_before_lsn" is set far in advance, it may take a while to reach that point. We
52
54
  * may want to resume at incremental points before that.
@@ -54,35 +56,31 @@ export class MongoBucketBatch extends BaseObserver {
54
56
  * This is set when creating the batch, but may not be updated afterwards.
55
57
  */
56
58
  resumeFromLsn = null;
57
- needsActivation = true;
58
59
  constructor(options) {
59
60
  super();
60
61
  this.logger = options.logger;
62
+ this.options = options;
61
63
  this.client = options.db.client;
62
64
  this.db = options.db;
63
- this.group_id = options.groupId;
64
- this.last_checkpoint_lsn = options.lastCheckpointLsn;
65
+ this.replicationStreamId = options.replicationStreamId;
65
66
  this.resumeFromLsn = options.resumeFromLsn;
66
67
  this.session = this.client.startSession();
67
- this.slot_name = options.slotName;
68
- this.sync_rules = options.syncRules;
68
+ this.replicationStreamName = options.replicationStreamName;
69
+ this.sync_rules = options.parsedSyncConfig.hydratedSyncConfig;
69
70
  this.storeCurrentData = options.storeCurrentData;
70
- this.mapping = options.mapping;
71
+ this.mapping = options.parsedSyncConfig.mapping;
71
72
  this.skipExistingRows = options.skipExistingRows;
72
73
  this.markRecordUnavailable = options.markRecordUnavailable;
74
+ this.hooks = options.hooks;
73
75
  this.batch = new OperationBatch();
74
- this.persisted_op = options.keepaliveOp ?? null;
75
76
  this.tracer = options.tracer ?? new PerformanceTracer('MongoDB storage');
76
77
  }
77
78
  addCustomWriteCheckpoint(checkpoint) {
78
79
  this.write_checkpoint_batch.push({
79
80
  ...checkpoint,
80
- sync_rules_id: this.group_id
81
+ sync_rules_id: this.replicationStreamId
81
82
  });
82
83
  }
83
- get lastCheckpointLsn() {
84
- return this.last_checkpoint_lsn;
85
- }
86
84
  async flush(options) {
87
85
  let result = null;
88
86
  // One flush may be split over multiple transactions.
@@ -100,6 +98,7 @@ export class MongoBucketBatch extends BaseObserver {
100
98
  let last_op = null;
101
99
  let resumeBatch = null;
102
100
  using _ = this.tracer.span('storage', 'flush');
101
+ await this.hooks?.beforeBatchFlush?.(this);
103
102
  await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
104
103
  if (batch != null) {
105
104
  resumeBatch = await this.replicateBatch(session, batch, opSeq, options);
@@ -116,15 +115,19 @@ export class MongoBucketBatch extends BaseObserver {
116
115
  if (last_op == null) {
117
116
  throw new ReplicationAssertionError('Unexpected last_op == null');
118
117
  }
119
- this.persisted_op = last_op;
118
+ this.recordPersistedOp(last_op);
120
119
  this.last_flushed_op = last_op;
120
+ await this.hooks?.afterBatchFlush?.(this);
121
121
  return { flushed_op: last_op };
122
122
  }
123
123
  async replicateBatch(session, batch, op_seq, options) {
124
124
  let sizes = undefined;
125
125
  using _ = this.tracer.span('storage', 'replicate_batch');
126
- if (this.storeCurrentData && !this.skipExistingRows) {
127
- // We skip this step if we don't store current_data, since the sizes will
126
+ // Only look up current_data sizes if the batch stores current_data and at least one
127
+ // table in it does too (per-table can disable it, e.g. Postgres REPLICA IDENTITY FULL).
128
+ const anyTableStoresCurrentData = this.storeCurrentData && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
129
+ if (anyTableStoresCurrentData && !this.skipExistingRows) {
130
+ // We skip this step if no tables store current_data, since the sizes will
128
131
  // always be small in that case.
129
132
  // With skipExistingRows, we don't load the full documents into memory,
130
133
  // so we can also skip the size lookup step.
@@ -135,7 +138,11 @@ export class MongoBucketBatch extends BaseObserver {
135
138
  // (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
136
139
  // the order of processing, which then becomes really tricky to manage.
137
140
  // This now takes 2+ queries, but doesn't have any issues with order of operations.
138
- const sizeLookups = batch.batch.map((r) => ({
141
+ // Within this branch this.storeCurrentData is true, so the per-table flag is the
142
+ // effective value - only look up sizes for tables that actually store current_data.
143
+ const sizeLookups = batch.batch
144
+ .filter((r) => r.record.sourceTable.storeCurrentData)
145
+ .map((r) => ({
139
146
  sourceTableId: mongoTableId(r.record.sourceTable.id),
140
147
  replicaId: r.beforeId
141
148
  }));
@@ -217,6 +224,9 @@ export class MongoBucketBatch extends BaseObserver {
217
224
  const afterId = operation.afterId;
218
225
  let after = record.after;
219
226
  const sourceTable = record.sourceTable;
227
+ // Effective per-record flag: store current_data only if both the batch (source-level,
228
+ // e.g. Postgres) and the table (e.g. non-FULL replica identity) require it.
229
+ const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
220
230
  let existing_buckets = [];
221
231
  let new_buckets = [];
222
232
  let existing_lookups = [];
@@ -242,7 +252,7 @@ export class MongoBucketBatch extends BaseObserver {
242
252
  // Not an error if we re-apply a transaction
243
253
  existing_buckets = [];
244
254
  existing_lookups = [];
245
- if (!isCompleteRow(this.storeCurrentData, after)) {
255
+ if (!isCompleteRow(storeCurrentData, after)) {
246
256
  if (this.markRecordUnavailable != null) {
247
257
  // This will trigger a "resnapshot" of the record.
248
258
  // This is not relevant if storeCurrentData is false, since we'll get the full row
@@ -258,7 +268,7 @@ export class MongoBucketBatch extends BaseObserver {
258
268
  else {
259
269
  existing_buckets = result.buckets;
260
270
  existing_lookups = result.lookups;
261
- if (this.storeCurrentData && result.data != null) {
271
+ if (storeCurrentData && result.data != null) {
262
272
  const data = deserializeBson(result.data.buffer);
263
273
  after = storage.mergeToast(after, data);
264
274
  }
@@ -270,7 +280,9 @@ export class MongoBucketBatch extends BaseObserver {
270
280
  // Not an error if we re-apply a transaction
271
281
  existing_buckets = [];
272
282
  existing_lookups = [];
273
- // Log to help with debugging if there was a consistency issue
283
+ // Log to help with debugging if there was a consistency issue.
284
+ // Gate on the batch-level flag: FULL tables (per-record flag false) still get a
285
+ // current_data entry, so a missing record on DELETE is meaningful for them too.
274
286
  if (this.storeCurrentData && this.markRecordUnavailable == null) {
275
287
  this.logger.warn(`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`);
276
288
  }
@@ -281,7 +293,7 @@ export class MongoBucketBatch extends BaseObserver {
281
293
  }
282
294
  }
283
295
  let afterData = null;
284
- if (afterId != null && !this.storeCurrentData) {
296
+ if (afterId != null && !storeCurrentData) {
285
297
  afterData = null;
286
298
  }
287
299
  else if (afterId != null) {
@@ -302,7 +314,7 @@ export class MongoBucketBatch extends BaseObserver {
302
314
  container.reporter.captureMessage(`Data too big on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${e.message}`, {
303
315
  level: errors.ErrorSeverity.WARNING,
304
316
  metadata: {
305
- replication_slot: this.slot_name,
317
+ replication_slot: this.replicationStreamName,
306
318
  table: record.sourceTable.qualifiedName
307
319
  }
308
320
  });
@@ -341,18 +353,20 @@ export class MongoBucketBatch extends BaseObserver {
341
353
  // However, it will be valid by the end of the transaction.
342
354
  //
343
355
  // In this case, we don't save the op, but we do save the current data.
344
- if (afterId && after && utils.isCompleteRow(this.storeCurrentData, after)) {
356
+ if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
345
357
  // Insert or update
346
358
  if (sourceTable.syncData) {
347
- const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
359
+ const { results, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
348
360
  record: after,
349
- sourceTable
361
+ sourceTable: sourceTable.ref,
362
+ bucketDataSources: sourceTable.bucketDataSources
350
363
  });
364
+ const evaluated = results;
351
365
  for (let error of syncErrors) {
352
366
  container.reporter.captureMessage(`Failed to evaluate data query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
353
367
  level: errors.ErrorSeverity.WARNING,
354
368
  metadata: {
355
- replication_slot: this.slot_name,
369
+ replication_slot: this.replicationStreamName,
356
370
  table: record.sourceTable.qualifiedName
357
371
  }
358
372
  });
@@ -370,12 +384,12 @@ export class MongoBucketBatch extends BaseObserver {
370
384
  }
371
385
  if (sourceTable.syncParameters) {
372
386
  // Parameters
373
- const { results: paramEvaluated, errors: paramErrors } = this.sync_rules.evaluateParameterRowWithErrors(sourceTable, after);
387
+ const { results: paramEvaluated, errors: paramErrors } = this.sync_rules.evaluateParameterRowWithErrors(sourceTable.ref, after, { parameterLookupSources: sourceTable.parameterLookupSources });
374
388
  for (let error of paramErrors) {
375
389
  container.reporter.captureMessage(`Failed to evaluate parameter query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
376
390
  level: errors.ErrorSeverity.WARNING,
377
391
  metadata: {
378
- replication_slot: this.slot_name,
392
+ replication_slot: this.replicationStreamName,
379
393
  table: record.sourceTable.qualifiedName
380
394
  }
381
395
  });
@@ -481,17 +495,43 @@ export class MongoBucketBatch extends BaseObserver {
481
495
  session
482
496
  });
483
497
  await this.db.sync_rules.updateOne({
484
- _id: this.group_id
498
+ _id: this.replicationStreamId
485
499
  }, {
486
500
  $set: {
487
501
  last_keepalive_ts: new Date()
488
502
  }
489
503
  }, { session });
504
+ // Allow subclasses to persist additional flush-time state in the same transaction
505
+ // (e.g. v3 advances the stream-level last_persisted_op).
506
+ await this.onReplicationTransactionFlush(session, opSeq.last());
490
507
  // We don't notify checkpoint here - we don't make any checkpoint updates directly
491
508
  });
492
509
  }
510
+ /**
511
+ * Hook called inside the replication flush transaction, after ops have been persisted.
512
+ *
513
+ * The base implementation does nothing; v3 storage overrides this to `$max` the stream-level
514
+ * `last_persisted_op` durably within the same transaction.
515
+ */
516
+ async onReplicationTransactionFlush(_session, _lastOp) {
517
+ // No-op by default (v1 behaviour unchanged).
518
+ }
519
+ /**
520
+ * Called after a replication transaction has successfully committed, with the last persisted op id.
521
+ *
522
+ * v1 storage tracks this in memory to fold into the next checkpoint. v3 storage does not need it:
523
+ * the stream-level `last_persisted_op` is already advanced durably by
524
+ * {@link onReplicationTransactionFlush} within the same transaction, and checkpoints read it
525
+ * from the document.
526
+ *
527
+ * Keep calls to this adjacent to {@link withReplicationTransaction} usage - both must observe
528
+ * every path that persists ops.
529
+ */
530
+ recordPersistedOp(_lastOp) {
531
+ // No-op by default.
532
+ }
493
533
  async [Symbol.asyncDispose]() {
494
- if (this.batch != null || this.write_checkpoint_batch.length > 0) {
534
+ if (this.batch?.hasData() || this.write_checkpoint_batch.length > 0) {
495
535
  // We don't error here, since:
496
536
  // 1. In error states, this is expected (we can't distinguish between disposing after success or error).
497
537
  // 2. SuppressedError is messy to deal with.
@@ -503,221 +543,25 @@ export class MongoBucketBatch extends BaseObserver {
503
543
  async dispose() {
504
544
  await this[Symbol.asyncDispose]();
505
545
  }
506
- lastWaitingLogThottled = 0;
507
- async commit(lsn, options) {
508
- const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
509
- await this.flush(options);
510
- const now = new Date();
511
- // Mark relevant write checkpoints as "processed".
512
- // This makes it easier to identify write checkpoints that are "valid" in order.
513
- await this.db.write_checkpoints.updateMany({
514
- processed_at_lsn: null,
515
- 'lsns.1': { $lte: lsn }
516
- }, {
517
- $set: {
518
- processed_at_lsn: lsn
519
- }
520
- }, {
521
- session: this.session
522
- });
523
- const can_checkpoint = {
524
- $and: [
525
- { $eq: ['$snapshot_done', true] },
526
- {
527
- $or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
528
- },
529
- {
530
- $or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
531
- }
532
- ]
533
- };
534
- const new_keepalive_op = {
535
- $cond: [
536
- can_checkpoint,
537
- { $literal: null },
538
- {
539
- $toString: {
540
- $max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
541
- }
542
- }
543
- ]
544
- };
545
- const new_last_checkpoint = {
546
- $cond: [
547
- can_checkpoint,
548
- {
549
- $max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
550
- },
551
- '$last_checkpoint'
552
- ]
553
- };
554
- // For this query, we need to handle multiple cases, depending on the state:
555
- // 1. Normal commit - advance last_checkpoint to this.persisted_op.
556
- // 2. Commit delayed by no_checkpoint_before due to snapshot. In this case we only advance keepalive_op.
557
- // 3. Commit with no new data - here may may set last_checkpoint = keepalive_op, if a delayed commit is relevant.
558
- // We want to do as much as possible in a single atomic database operation, which makes this somewhat complex.
559
- let preUpdateDocument = await this.db.sync_rules.findOneAndUpdate({ _id: this.group_id }, [
560
- {
561
- $set: {
562
- _can_checkpoint: can_checkpoint,
563
- _not_empty: createEmptyCheckpoints
564
- ? true
565
- : {
566
- $or: [
567
- { $literal: createEmptyCheckpoints },
568
- { $ne: ['$keepalive_op', new_keepalive_op] },
569
- { $ne: ['$last_checkpoint', new_last_checkpoint] }
570
- ]
571
- }
572
- }
573
- },
574
- {
575
- $set: {
576
- last_checkpoint_lsn: {
577
- $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
578
- },
579
- last_checkpoint_ts: {
580
- $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
581
- },
582
- last_keepalive_ts: { $literal: now },
583
- last_fatal_error: { $literal: null },
584
- last_fatal_error_ts: { $literal: null },
585
- keepalive_op: new_keepalive_op,
586
- last_checkpoint: new_last_checkpoint,
587
- // Unset snapshot_lsn on checkpoint
588
- snapshot_lsn: {
589
- $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
590
- }
591
- }
592
- },
593
- {
594
- $unset: ['_can_checkpoint', '_not_empty']
595
- }
596
- ], {
597
- session: this.session,
598
- // 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.
599
- returnDocument: 'before',
600
- projection: {
601
- snapshot_done: 1,
602
- last_checkpoint_lsn: 1,
603
- no_checkpoint_before: 1,
604
- keepalive_op: 1,
605
- last_checkpoint: 1
606
- }
607
- });
608
- if (preUpdateDocument == null) {
609
- throw new ReplicationAssertionError('Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id);
610
- }
611
- // This re-implements the same logic as in the pipeline, to determine what was actually updated.
612
- // Unfortunately we cannot return these from the pipeline directly, so we need to re-implement the logic.
613
- const canCheckpoint = preUpdateDocument.snapshot_done === true &&
614
- (preUpdateDocument.last_checkpoint_lsn == null || preUpdateDocument.last_checkpoint_lsn <= lsn) &&
615
- (preUpdateDocument.no_checkpoint_before == null || preUpdateDocument.no_checkpoint_before <= lsn);
616
- const keepaliveOp = preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op);
617
- const maxKeepalive = [keepaliveOp ?? 0n, this.persisted_op ?? 0n, 0n].reduce((a, b) => (a > b ? a : b));
618
- const newKeepaliveOp = canCheckpoint ? null : maxKeepalive.toString();
619
- const newLastCheckpoint = canCheckpoint
620
- ? [preUpdateDocument.last_checkpoint ?? 0n, this.persisted_op ?? 0n, keepaliveOp ?? 0n, 0n].reduce((a, b) => a > b ? a : b)
621
- : preUpdateDocument.last_checkpoint;
622
- const notEmpty = createEmptyCheckpoints ||
623
- preUpdateDocument.keepalive_op !== newKeepaliveOp ||
624
- preUpdateDocument.last_checkpoint !== newLastCheckpoint;
625
- const checkpointCreated = canCheckpoint && notEmpty;
626
- const checkpointBlocked = !canCheckpoint;
627
- if (checkpointBlocked) {
628
- // Failed on snapshot_done or no_checkpoint_before.
629
- if (Date.now() - this.lastWaitingLogThottled > 5_000) {
630
- this.logger.info(`Waiting before creating checkpoint, currently at ${lsn} / ${preUpdateDocument.keepalive_op}. Current state: ${JSON.stringify({
631
- snapshot_done: preUpdateDocument.snapshot_done,
632
- last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
633
- no_checkpoint_before: preUpdateDocument.no_checkpoint_before
634
- })}`);
635
- this.lastWaitingLogThottled = Date.now();
636
- }
637
- }
638
- else {
639
- if (checkpointCreated) {
640
- this.logger.debug(`Created checkpoint at ${lsn} / ${newLastCheckpoint}`);
641
- }
642
- await this.autoActivate(lsn);
643
- await this.db.notifyCheckpoint();
644
- this.persisted_op = null;
645
- this.last_checkpoint_lsn = lsn;
646
- if (newLastCheckpoint != null) {
647
- await this.sourceRecordStore.postCommitCleanup(newLastCheckpoint, this.logger);
648
- }
649
- }
650
- return { checkpointBlocked, checkpointCreated };
651
- }
652
- /**
653
- * Switch from processing -> active if relevant.
654
- *
655
- * Called on new commits.
656
- */
657
- async autoActivate(lsn) {
658
- if (!this.needsActivation) {
659
- return;
660
- }
661
- // Activate the batch, so it can start processing.
662
- // This is done automatically when the first save() is called.
663
- const session = this.session;
664
- let activated = false;
665
- await session.withTransaction(async () => {
666
- const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
667
- if (doc && doc.state == SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
668
- await this.db.sync_rules.updateOne({
669
- _id: this.group_id
670
- }, {
671
- $set: {
672
- state: storage.SyncRuleState.ACTIVE
673
- }
674
- }, { session });
675
- await this.db.sync_rules.updateMany({
676
- _id: { $ne: this.group_id },
677
- state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
678
- }, {
679
- $set: {
680
- state: storage.SyncRuleState.STOP
681
- }
682
- }, { session });
683
- activated = true;
684
- }
685
- else if (doc?.state != SyncRuleState.PROCESSING) {
686
- this.needsActivation = false;
687
- }
688
- });
689
- if (activated) {
690
- this.logger.info(`Activated new replication stream at ${lsn}`);
691
- await this.db.notifyCheckpoint();
692
- this.needsActivation = false;
693
- }
694
- }
695
- async keepalive(lsn) {
696
- return await this.commit(lsn, { createEmptyCheckpoints: true });
697
- }
698
- async setResumeLsn(lsn) {
699
- const update = {
700
- snapshot_lsn: lsn
701
- };
702
- await this.db.sync_rules.updateOne({
703
- _id: this.group_id
704
- }, {
705
- $set: update
706
- }, { session: this.session });
707
- }
708
546
  async save(record) {
709
547
  const { after, before, sourceTable, tag } = record;
710
- for (const event of this.getTableEvents(sourceTable)) {
711
- this.iterateListeners((cb) => cb.replicationEvent?.({
712
- batch: this,
713
- table: sourceTable,
714
- data: {
715
- op: tag,
716
- after: after && utils.isCompleteRow(this.storeCurrentData, after) ? after : undefined,
717
- before: before && utils.isCompleteRow(this.storeCurrentData, before) ? before : undefined
718
- },
719
- event
720
- }));
548
+ const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
549
+ // syncEvent is the per-table designation from resolveTables. With v3 storage, multiple
550
+ // SourceTables can exist for the same ref, with a row change saved once per table -
551
+ // only the designated event carrier may fire events, so each event fires once per row.
552
+ if (sourceTable.syncEvent) {
553
+ for (const event of this.getTableEvents(sourceTable)) {
554
+ this.iterateListeners((cb) => cb.replicationEvent?.({
555
+ batch: this,
556
+ table: sourceTable,
557
+ data: {
558
+ op: tag,
559
+ after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
560
+ before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
561
+ },
562
+ event
563
+ }));
564
+ }
721
565
  }
722
566
  /**
723
567
  * Return if the table is just an event table
@@ -744,7 +588,7 @@ export class MongoBucketBatch extends BaseObserver {
744
588
  const result = await this.flush();
745
589
  await this.withTransaction(async () => {
746
590
  for (let table of sourceTables) {
747
- await this.db.commonSourceTables(this.group_id).deleteOne({ _id: mongoTableId(table.id) });
591
+ await this.db.commonSourceTables(this.replicationStreamId).deleteOne({ _id: mongoTableId(table.id) });
748
592
  }
749
593
  });
750
594
  await this.cleanupDroppedSourceTables(sourceTables);
@@ -757,7 +601,7 @@ export class MongoBucketBatch extends BaseObserver {
757
601
  last_op = await this.truncateSingle(table);
758
602
  }
759
603
  if (last_op) {
760
- this.persisted_op = last_op;
604
+ this.recordPersistedOp(last_op);
761
605
  return {
762
606
  flushed_op: last_op
763
607
  };
@@ -814,7 +658,7 @@ export class MongoBucketBatch extends BaseObserver {
814
658
  };
815
659
  copy.snapshotStatus = snapshotStatus;
816
660
  await this.withTransaction(async () => {
817
- await this.db.commonSourceTables(this.group_id).updateOne({ _id: mongoTableId(table.id) }, {
661
+ await this.db.commonSourceTables(this.replicationStreamId).updateOne({ _id: mongoTableId(table.id) }, {
818
662
  $set: {
819
663
  snapshot_status: {
820
664
  last_key: snapshotStatus.lastKey == null ? null : new bson.Binary(snapshotStatus.lastKey),
@@ -826,66 +670,13 @@ export class MongoBucketBatch extends BaseObserver {
826
670
  });
827
671
  return copy;
828
672
  }
829
- async markAllSnapshotDone(no_checkpoint_before_lsn) {
830
- await this.db.sync_rules.updateOne({
831
- _id: this.group_id
832
- }, {
833
- $set: {
834
- snapshot_done: true,
835
- last_keepalive_ts: new Date()
836
- },
837
- $max: {
838
- no_checkpoint_before: no_checkpoint_before_lsn
839
- }
840
- }, { session: this.session });
841
- }
842
- async markTableSnapshotRequired(table) {
843
- await this.db.sync_rules.updateOne({
844
- _id: this.group_id
845
- }, {
846
- $set: {
847
- snapshot_done: false
848
- }
849
- }, { session: this.session });
850
- }
851
- async markTableSnapshotDone(tables, no_checkpoint_before_lsn) {
852
- const session = this.session;
853
- const ids = tables.map((table) => mongoTableId(table.id));
854
- await this.withTransaction(async () => {
855
- await this.db.commonSourceTables(this.group_id).updateMany({ _id: { $in: ids } }, {
856
- $set: {
857
- snapshot_done: true
858
- },
859
- $unset: {
860
- snapshot_status: 1
861
- }
862
- }, { session });
863
- if (no_checkpoint_before_lsn != null) {
864
- await this.db.sync_rules.updateOne({
865
- _id: this.group_id
866
- }, {
867
- $set: {
868
- last_keepalive_ts: new Date()
869
- },
870
- $max: {
871
- no_checkpoint_before: no_checkpoint_before_lsn
872
- }
873
- }, { session: this.session });
874
- }
875
- });
876
- return tables.map((table) => {
877
- const copy = table.clone();
878
- copy.snapshotComplete = true;
879
- return copy;
880
- });
881
- }
882
673
  async clearError() {
883
674
  // No need to clear an error more than once per batch, since an error would always result in restarting the batch.
884
675
  if (this.clearedError) {
885
676
  return;
886
677
  }
887
678
  await this.db.sync_rules.updateOne({
888
- _id: this.group_id
679
+ _id: this.replicationStreamId
889
680
  }, {
890
681
  $set: {
891
682
  last_fatal_error: null,
@@ -898,7 +689,7 @@ export class MongoBucketBatch extends BaseObserver {
898
689
  * Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
899
690
  */
900
691
  getTableEvents(table) {
901
- return this.sync_rules.eventDescriptors.filter((evt) => [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table)));
692
+ return this.sync_rules.eventDescriptors.filter((evt) => [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref)));
902
693
  }
903
694
  }
904
695
  //# sourceMappingURL=MongoBucketBatch.js.map