@powersync/service-module-mongodb-storage 0.19.0 → 0.21.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 (176) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +31 -3
  3. package/dist/storage/MongoBucketStorage.js +365 -206
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
  6. package/dist/storage/implementation/MongoBucketBatch.js +35 -9
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +3 -1
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  32. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
  34. package/dist/storage/implementation/common/PersistedBatch.js +43 -2
  35. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  36. package/dist/storage/implementation/db.d.ts +4 -1
  37. package/dist/storage/implementation/db.js +14 -1
  38. package/dist/storage/implementation/db.js.map +1 -1
  39. package/dist/storage/implementation/models.d.ts +35 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
  45. package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
  46. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  48. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  49. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  51. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  52. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/models.js +1 -0
  54. package/dist/storage/implementation/v1/models.js.map +1 -1
  55. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  56. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  57. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  58. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
  59. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
  60. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
  62. package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
  63. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  65. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  66. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  68. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  69. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  71. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  72. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  74. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  75. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
  77. package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
  78. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  80. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  81. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  83. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  84. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  85. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  86. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  87. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  88. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  89. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  90. package/dist/storage/implementation/v3/models.d.ts +89 -5
  91. package/dist/storage/implementation/v3/models.js.map +1 -1
  92. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  93. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
  94. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  95. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  96. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
  97. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
  98. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  99. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  100. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  101. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  102. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  103. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
  104. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  105. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  106. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  107. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  108. package/dist/storage/storage-index.d.ts +2 -1
  109. package/dist/storage/storage-index.js +1 -0
  110. package/dist/storage/storage-index.js.map +1 -1
  111. package/dist/types/types.d.ts +23 -1
  112. package/dist/types/types.js +16 -7
  113. package/dist/types/types.js.map +1 -1
  114. package/dist/utils/util.js +3 -1
  115. package/dist/utils/util.js.map +1 -1
  116. package/package.json +10 -9
  117. package/src/storage/MongoBucketStorage.ts +495 -268
  118. package/src/storage/implementation/MongoBucketBatch.ts +54 -14
  119. package/src/storage/implementation/MongoCompactor.ts +35 -346
  120. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  121. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  122. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  123. package/src/storage/implementation/MongoStorageProvider.ts +3 -1
  124. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  125. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  126. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
  127. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  128. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  129. package/src/storage/implementation/common/PersistedBatch.ts +54 -2
  130. package/src/storage/implementation/db.ts +21 -2
  131. package/src/storage/implementation/models.ts +38 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
  133. package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
  134. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  135. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  136. package/src/storage/implementation/v1/models.ts +1 -0
  137. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  138. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
  139. package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
  140. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  141. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  142. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  143. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  144. package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
  145. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  146. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  147. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  148. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  149. package/src/storage/implementation/v3/models.ts +96 -3
  150. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
  151. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  152. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
  153. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  154. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
  155. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  156. package/src/storage/storage-index.ts +2 -0
  157. package/src/types/types.ts +24 -7
  158. package/src/utils/util.ts +3 -1
  159. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  160. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  161. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  162. package/test/src/compact-utils.test.ts +226 -0
  163. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  164. package/test/src/object_storage_usage.test.ts +255 -0
  165. package/test/src/parameter_compacting_v1.test.ts +120 -0
  166. package/test/src/parameter_compacting_v3.test.ts +192 -0
  167. package/test/src/parameter_compaction_fence.test.ts +276 -0
  168. package/test/src/storage.test.ts +273 -22
  169. package/test/src/storage_compacting.test.ts +714 -244
  170. package/test/src/storage_s3_checksums.test.ts +3 -4
  171. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  172. package/test/src/storage_s3_reading.test.ts +243 -1
  173. package/test/src/storage_s3_writing.test.ts +1 -0
  174. package/test/src/storage_sync.test.ts +365 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,152 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import { CompactionLeaseLostError } from '../MongoCompactor.js';
4
+ import { BucketStateDocumentV3 } from './models.js';
5
+
6
+ const LEASE_RENEW_INTERVAL_MS = 60 * 1000;
7
+
8
+ /**
9
+ * Filters for buckets that have no lease or an expired lease.
10
+ */
11
+ export const AVAILABLE_LEASE_EXPR = {
12
+ // $$NOW is evaluated by MongoDB, avoiding lease expiry races due
13
+ // to clocks on separate compact workers.
14
+ $expr: {
15
+ $or: [{ $eq: [{ $type: '$compact_lease' }, 'missing'] }, { $lte: ['$compact_lease.expires_at', '$$NOW'] }]
16
+ }
17
+ };
18
+
19
+ /**
20
+ * Owns one V3 bucket-compaction lease, including its server-time renewal and
21
+ * the owner-fenced operations which release it.
22
+ *
23
+ * This is intended as a way to reduce redundant work if multiple jobs attempt to
24
+ * compact the same bucket concurrently, but it's not an absolute safety guarantee.
25
+ * The individual operations must still be designed to be safe with concurrent compacting.
26
+ */
27
+ export class CompactionLease implements AsyncDisposable {
28
+ readonly startedAt: Date;
29
+ readonly lastOp: InternalOpId;
30
+
31
+ private timer: NodeJS.Timeout | undefined;
32
+ private renewalInFlight = false;
33
+ private renewalError: unknown;
34
+ private finalizing = false;
35
+ private finished = false;
36
+
37
+ private constructor(
38
+ private readonly collection: mongo.Collection<BucketStateDocumentV3>,
39
+ readonly state: BucketStateDocumentV3,
40
+ readonly id: mongo.ObjectId,
41
+ private readonly durationMs: number
42
+ ) {
43
+ this.startedAt = new Date(state.compact_lease!.expires_at.getTime() - durationMs);
44
+ this.lastOp = state.last_op;
45
+ }
46
+
47
+ static async claim(
48
+ collection: mongo.Collection<BucketStateDocumentV3>,
49
+ filter: mongo.Filter<BucketStateDocumentV3>,
50
+ sort: mongo.Sort | undefined,
51
+ durationMs: number
52
+ ): Promise<CompactionLease | null> {
53
+ const id = new mongo.ObjectId();
54
+ const state = await collection.findOneAndUpdate(
55
+ {
56
+ $and: [filter, AVAILABLE_LEASE_EXPR]
57
+ },
58
+ [
59
+ {
60
+ $set: {
61
+ compact_lease: {
62
+ id,
63
+ expires_at: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: durationMs } }
64
+ }
65
+ }
66
+ }
67
+ ],
68
+ { sort, returnDocument: 'after' }
69
+ );
70
+ return state == null ? null : new CompactionLease(collection, state, id, durationMs);
71
+ }
72
+
73
+ startRenewal() {
74
+ const interval = Math.max(1, Math.min(LEASE_RENEW_INTERVAL_MS, Math.floor(this.durationMs / 2)));
75
+ this.timer = setInterval(() => {
76
+ if (this.renewalInFlight) {
77
+ return;
78
+ }
79
+ this.renewalInFlight = true;
80
+ void this.renew()
81
+ .catch((error) => {
82
+ this.renewalError = error;
83
+ })
84
+ .finally(() => {
85
+ this.renewalInFlight = false;
86
+ });
87
+ }, interval);
88
+ this.timer.unref();
89
+ }
90
+
91
+ async throwIfLost() {
92
+ if (this.renewalError != null) {
93
+ throw this.renewalError;
94
+ }
95
+ }
96
+
97
+ async reschedule(nextCompactCheck: mongo.Document) {
98
+ await this.finish([{ $set: { next_compact_check: nextCompactCheck } }, { $unset: 'compact_lease' }]);
99
+ }
100
+
101
+ async finalize(update: mongo.Document) {
102
+ await this.finish([{ $set: update }, { $unset: 'compact_lease' }]);
103
+ }
104
+
105
+ async [Symbol.asyncDispose]() {
106
+ if (this.timer != null) {
107
+ clearInterval(this.timer);
108
+ this.timer = undefined;
109
+ }
110
+ if (this.finished) {
111
+ return;
112
+ }
113
+ this.finalizing = true;
114
+ await this.collection.updateOne(this.filter, { $unset: { compact_lease: '' } });
115
+ this.finished = true;
116
+ }
117
+
118
+ private get filter(): mongo.Filter<BucketStateDocumentV3> {
119
+ return { _id: this.state._id, 'compact_lease.id': this.id };
120
+ }
121
+
122
+ private async renew() {
123
+ const result = await this.collection.updateOne(this.filter, [
124
+ {
125
+ $set: {
126
+ 'compact_lease.expires_at': {
127
+ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.durationMs }
128
+ }
129
+ }
130
+ }
131
+ ]);
132
+ // A finalization in progress owns the authoritative fenced result.
133
+ if (result.matchedCount != 1 && !this.finalizing) {
134
+ throw new CompactionLeaseLostError(`Lost compaction lease for bucket ${this.state._id.b}`);
135
+ }
136
+ // A successful renewal confirms that a transient failure has passed. A
137
+ // lease-loss error remains sticky: it means another worker may own it.
138
+ if (!(this.renewalError instanceof CompactionLeaseLostError)) {
139
+ this.renewalError = undefined;
140
+ }
141
+ }
142
+
143
+ private async finish(update: mongo.Document[]) {
144
+ await this.throwIfLost();
145
+ this.finalizing = true;
146
+ const result = await this.collection.updateOne(this.filter, update);
147
+ if (result.matchedCount != 1) {
148
+ throw new CompactionLeaseLostError(`Lost compaction lease for bucket ${this.state._id.b}`);
149
+ }
150
+ this.finished = true;
151
+ }
152
+ }
@@ -1,21 +1,21 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
- import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
- import { storage } from '@powersync/service-core';
2
+ import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
3
+ import { InternalOpId, storage } from '@powersync/service-core';
4
+ import { EventDefinitionId } from '@powersync/service-sync-rules';
4
5
  import * as bson from 'bson';
5
6
  import { mongoTableId } from '../../../utils/util.js';
6
7
  import { canCheckpointState } from '../CheckpointState.js';
7
8
  import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
8
9
  import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
9
- import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
10
+ import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
10
11
  import { PersistedBatch } from '../common/PersistedBatch.js';
11
12
  import { SourceRecordStore } from '../common/SourceRecordStore.js';
12
13
  import { PersistedBatchV3 } from './PersistedBatchV3.js';
13
14
  import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
14
15
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
15
- import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
16
+ import { CustomCheckpointRequestDocumentV3, ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
16
17
  import {
17
18
  createNewSourceTable,
18
- designateEventCarrier,
19
19
  overlappingSourceTableFilter,
20
20
  planSourceTableReconciliation,
21
21
  sourceTableDesiredResolution,
@@ -27,6 +27,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
27
27
  declare readonly db: VersionedPowerSyncMongoV3;
28
28
 
29
29
  private readonly store: SourceRecordStore;
30
+ /** Avoid repeating idempotent createIndexes calls for every checkpoint flush. */
31
+ private readonly initializedCustomCheckpointEventIds = new Set<EventDefinitionId>();
32
+ // Used only to validate supplied checkpoint ids; it does not initialize collections.
33
+ private readonly knownEventIds: ReadonlySet<EventDefinitionId>;
30
34
  private readonly syncConfigIds: bson.ObjectId[];
31
35
  private needsActivationV3 = true;
32
36
  private lastWaitingLogThrottledV3 = 0;
@@ -38,6 +42,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
38
42
  throw new ReplicationAssertionError('Missing sync config id for v3 batch');
39
43
  }
40
44
  this.syncConfigIds = syncConfigIds;
45
+ this.knownEventIds = new Set(this.mapping.allEventDefinitionIds());
41
46
  this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
42
47
  }
43
48
 
@@ -45,7 +50,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
45
50
  return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
46
51
  logger: this.logger,
47
52
  objectStorage: this.options.objectStorage,
48
- inlineThresholdBytes: this.options.inlineThresholdBytes
53
+ inlineThresholdBytes: this.options.inlineThresholdBytes,
54
+ signal: this.options.signal,
55
+ objectStorageUsageWriterId: this.objectStorageUsageWriterId
49
56
  });
50
57
  }
51
58
 
@@ -90,7 +97,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
90
97
  this.syncConfigIds.map((id) => id.toHexString()),
91
98
  table.ref,
92
99
  bucketDataSourceIds,
93
- parameterLookupSourceIds
100
+ parameterLookupSourceIds,
101
+ [...table.eventDefinitionIds!]
94
102
  )
95
103
  );
96
104
  }
@@ -132,7 +140,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
132
140
  .sourceRecords(this.replicationStreamId, mongoTableId(table.id))
133
141
  .drop()
134
142
  .catch((error) => {
135
- if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
143
+ if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
136
144
  return;
137
145
  }
138
146
  throw error;
@@ -146,25 +154,19 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
146
154
  const parsedOverride = options.parsedSyncConfig as MongoParsedSyncConfigSet | undefined;
147
155
  const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
148
156
  const mapping = parsedOverride?.mapping ?? this.mapping;
157
+ const eventById = (parsedOverride ?? this.options.parsedSyncConfig).eventById;
149
158
 
150
159
  const { connection_id, source } = options;
151
- const context: SourceTableReconciliationContext = {
152
- connectionId: connection_id,
153
- connectionTag: source.connectionTag,
154
- identity: {
155
- schema: source.schema,
156
- name: source.name,
157
- objectId: source.objectId,
158
- replicaIdColumns: source.replicaIdColumns.map((column) => ({
159
- name: column.name,
160
- type: column.type,
161
- type_oid: column.typeId
162
- }))
163
- },
164
- storeCurrentData: source.sendsCompleteRows !== true,
165
- syncConfig,
166
- mapping,
167
- desired: sourceTableDesiredResolution(syncConfig, source, mapping)
160
+ const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
161
+ const identity = {
162
+ schema: source.schema,
163
+ name: source.name,
164
+ objectId: source.objectId,
165
+ replicaIdColumns: source.replicaIdColumns.map((column) => ({
166
+ name: column.name,
167
+ type: column.type,
168
+ type_oid: column.typeId
169
+ }))
168
170
  };
169
171
 
170
172
  let result: storage.ResolveTablesResult | null = null;
@@ -174,14 +176,36 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
174
176
  await session.withTransaction(async () => {
175
177
  const col = this.db.sourceTables(this.replicationStreamId);
176
178
 
177
- // Fetch every persisted source-table doc that can overlap this physical table.
178
- // Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
179
+ // Find records that overlap by name or relation id.
179
180
  const candidateDocs = await col
180
- .find(overlappingSourceTableFilter(connection_id, context.identity), { session })
181
+ .find(overlappingSourceTableFilter(connection_id, identity), { session })
181
182
  .toArray();
182
183
 
183
- // Pure planning: which docs to retain (and narrow), whether a new doc is needed for
184
- // uncovered memberships, and which docs conflict with the current identity.
184
+ const candidateTables = candidateDocs.map((doc) =>
185
+ sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping, eventById)
186
+ );
187
+ const candidates = candidateTables.map((table) => table.clone());
188
+ const resolution = await reconcile({ source, candidates });
189
+ storage.validateSourceTableCandidateResolution(candidates, resolution);
190
+
191
+ // Persist metadata from the reconciler without mutating the queried documents.
192
+ for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
193
+ await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
194
+ }
195
+
196
+ const context: SourceTableReconciliationContext = {
197
+ connectionId: connection_id,
198
+ connectionTag: source.connectionTag,
199
+ identity,
200
+ storeCurrentData: source.sendsCompleteRows !== true,
201
+ syncConfig,
202
+ mapping,
203
+ desired: sourceTableDesiredResolution(syncConfig, source, mapping, eventById),
204
+ sourceCompatibleTables: resolution.compatibleTables,
205
+ newTableSourceMetadata: resolution.newTableValues.sourceMetadata
206
+ };
207
+
208
+ // Plan record reuse, membership changes, creation, and removal.
185
209
  const plan = planSourceTableReconciliation(candidateDocs, context);
186
210
 
187
211
  // Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
@@ -194,7 +218,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
194
218
  {
195
219
  $set: {
196
220
  bucket_data_source_ids: update.memberships.bucketDataSourceIds,
197
- parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
221
+ parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds,
222
+ event_definition_ids: update.memberships.eventDefinitionIds
198
223
  }
199
224
  },
200
225
  { session }
@@ -212,12 +237,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
212
237
  plan.tables.push(table);
213
238
  }
214
239
 
215
- // If memberships are split across multiple source tables, only one may fire events.
216
- designateEventCarrier(plan.tables, context.desired.triggersEvent);
217
-
218
240
  result = {
219
241
  tables: plan.tables,
220
- dropTables: plan.dropDocs.map((doc) => sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping))
242
+ dropTables: plan.dropDocs.map((doc) =>
243
+ sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping, eventById)
244
+ )
221
245
  };
222
246
  });
223
247
 
@@ -232,12 +256,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
232
256
  return null;
233
257
  }
234
258
 
235
- const refreshed = sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules, this.mapping);
236
- // The event-carrier designation is decided per resolveTables result and not persisted -
237
- // preserve the caller's designation instead of recomputing it from the ref, so that
238
- // refreshing a non-carrier table does not make it fire events.
239
- refreshed.syncEvent = table.syncEvent;
240
- return refreshed;
259
+ return sourceTableFromDocument(
260
+ doc,
261
+ table.ref.connectionTag,
262
+ this.sync_rules,
263
+ this.mapping,
264
+ this.options.parsedSyncConfig.eventById
265
+ );
241
266
  }
242
267
 
243
268
  async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
@@ -504,7 +529,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
504
529
  _id: { $ne: this.replicationStreamId },
505
530
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
506
531
  },
507
- syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
532
+ stopReplicationStreamPipeline(),
508
533
  { session }
509
534
  );
510
535
  activated = true;
@@ -668,4 +693,96 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
668
693
  return copy;
669
694
  });
670
695
  }
696
+
697
+ protected override async batchCreateCustomWriteCheckpoints(
698
+ session: lib_mongo.mongo.ClientSession,
699
+ opId: InternalOpId
700
+ ): Promise<void> {
701
+ if (this.write_checkpoint_batch.length == 0) {
702
+ return;
703
+ }
704
+
705
+ const checkpointsByEvent = Map.groupBy(this.write_checkpoint_batch, (checkpoint) =>
706
+ this.validateCustomCheckpointEventId(checkpoint.event_id)
707
+ );
708
+
709
+ for (const [eventId, checkpoints] of checkpointsByEvent) {
710
+ await this.batchCreateEventCustomWriteCheckpoints(session, opId, eventId, checkpoints);
711
+ }
712
+ }
713
+
714
+ protected override async prepareCustomWriteCheckpoints(): Promise<void> {
715
+ // Most events never produce custom checkpoints, so create collections lazily
716
+ // only for the event ids present in this batch. This hook runs before the
717
+ // replication transaction because MongoDB cannot create indexes within it.
718
+ const eventIds = new Set(
719
+ this.write_checkpoint_batch.map((checkpoint) => this.validateCustomCheckpointEventId(checkpoint.event_id))
720
+ );
721
+ for (const eventId of eventIds) {
722
+ if (this.initializedCustomCheckpointEventIds.has(eventId)) {
723
+ continue;
724
+ }
725
+ await this.db.initializeCustomCheckpointRequestsCollection({
726
+ replicationStreamId: this.replicationStreamId,
727
+ eventId
728
+ });
729
+ this.initializedCustomCheckpointEventIds.add(eventId);
730
+ }
731
+ }
732
+
733
+ private validateCustomCheckpointEventId(eventId: EventDefinitionId | undefined): EventDefinitionId {
734
+ if (eventId == null) {
735
+ throw new ServiceAssertionError('V3 custom checkpoints require an event definition id');
736
+ }
737
+
738
+ if (!this.knownEventIds.has(eventId)) {
739
+ throw new ServiceAssertionError(`Unknown custom checkpoint event definition ${eventId}`);
740
+ }
741
+ return eventId;
742
+ }
743
+
744
+ private async batchCreateEventCustomWriteCheckpoints(
745
+ session: lib_mongo.mongo.ClientSession,
746
+ opId: InternalOpId,
747
+ eventId: EventDefinitionId,
748
+ checkpoints: storage.CustomWriteCheckpointOptions[]
749
+ ): Promise<void> {
750
+ await this.db
751
+ .customCheckpointRequests({
752
+ eventId,
753
+ replicationStreamId: this.replicationStreamId
754
+ })
755
+ .bulkWrite(
756
+ checkpoints.map((checkpointOptions) => {
757
+ const set: Partial<CustomCheckpointRequestDocumentV3> = {
758
+ user_id: checkpointOptions.user_id,
759
+ checkpoint: checkpointOptions.checkpoint,
760
+ op_id: opId
761
+ };
762
+ if (checkpointOptions.checkpoint_requested_at != null) {
763
+ set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
764
+ }
765
+
766
+ return {
767
+ updateOne: {
768
+ filter: {
769
+ user_id: checkpointOptions.user_id
770
+ },
771
+ update: {
772
+ $set: set,
773
+ ...(checkpointOptions.checkpoint_requested_at == null
774
+ ? {
775
+ $unset: {
776
+ checkpoint_requested_at: 1
777
+ }
778
+ }
779
+ : {})
780
+ },
781
+ upsert: true
782
+ }
783
+ };
784
+ }),
785
+ { session }
786
+ );
787
+ }
671
788
  }