@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716

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 (213) hide show
  1. package/CHANGELOG.md +129 -7
  2. package/dist/storage/MongoBucketStorage.d.ts +42 -5
  3. package/dist/storage/MongoBucketStorage.js +404 -215
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
  6. package/dist/storage/implementation/MongoBucketBatch.js +169 -98
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +21 -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/MongoIdSequence.d.ts +10 -1
  12. package/dist/storage/implementation/MongoIdSequence.js +14 -1
  13. package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
  14. package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
  15. package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
  16. package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  18. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +35 -2
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  29. package/dist/storage/implementation/MongoStorageProvider.js +8 -4
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  38. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  39. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  43. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  44. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  45. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
  48. package/dist/storage/implementation/common/PersistedBatch.js +57 -14
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  53. package/dist/storage/implementation/db.d.ts +9 -1
  54. package/dist/storage/implementation/db.js +28 -1
  55. package/dist/storage/implementation/db.js.map +1 -1
  56. package/dist/storage/implementation/models.d.ts +35 -0
  57. package/dist/storage/implementation/models.js.map +1 -1
  58. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +11 -0
  59. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
  60. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  61. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
  62. package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
  63. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  64. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  65. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  66. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +66 -17
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  70. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  71. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/models.js +1 -0
  74. package/dist/storage/implementation/v1/models.js.map +1 -1
  75. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  76. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  77. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +14 -2
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
  82. package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
  83. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  85. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  86. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  94. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  95. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  96. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
  97. package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
  98. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  99. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  100. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  102. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  103. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  104. package/dist/storage/implementation/v3/compact-utils.d.ts +108 -0
  105. package/dist/storage/implementation/v3/compact-utils.js +223 -0
  106. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  107. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  108. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  109. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +90 -6
  111. package/dist/storage/implementation/v3/models.js.map +1 -1
  112. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  113. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
  114. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  115. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  116. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
  117. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
  118. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  119. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
  120. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
  121. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  122. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  123. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
  124. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  125. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  126. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  127. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  128. package/dist/storage/storage-index.d.ts +2 -1
  129. package/dist/storage/storage-index.js +1 -0
  130. package/dist/storage/storage-index.js.map +1 -1
  131. package/dist/types/types.d.ts +28 -1
  132. package/dist/types/types.js +28 -7
  133. package/dist/types/types.js.map +1 -1
  134. package/dist/utils/test-utils.d.ts +4 -2
  135. package/dist/utils/test-utils.js +7 -4
  136. package/dist/utils/test-utils.js.map +1 -1
  137. package/dist/utils/util.js +3 -1
  138. package/dist/utils/util.js.map +1 -1
  139. package/package.json +10 -9
  140. package/src/storage/MongoBucketStorage.ts +545 -277
  141. package/src/storage/implementation/MongoBucketBatch.ts +258 -124
  142. package/src/storage/implementation/MongoCompactor.ts +37 -346
  143. package/src/storage/implementation/MongoIdSequence.ts +21 -1
  144. package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
  145. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  146. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
  147. package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
  148. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
  149. package/src/storage/implementation/MongoStorageProvider.ts +16 -4
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
  152. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  153. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
  154. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  155. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  156. package/src/storage/implementation/common/PersistedBatch.ts +76 -20
  157. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  158. package/src/storage/implementation/db.ts +36 -2
  159. package/src/storage/implementation/models.ts +39 -0
  160. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
  161. package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
  162. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  163. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
  164. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  165. package/src/storage/implementation/v1/models.ts +1 -0
  166. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  167. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +339 -247
  168. package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
  169. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  170. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  171. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
  172. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  173. package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
  174. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  175. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  176. package/src/storage/implementation/v3/compact-utils.ts +329 -0
  177. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  178. package/src/storage/implementation/v3/models.ts +97 -4
  179. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
  180. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  181. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
  182. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
  183. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
  184. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  185. package/src/storage/storage-index.ts +2 -0
  186. package/src/types/types.ts +42 -7
  187. package/src/utils/test-utils.ts +9 -5
  188. package/src/utils/util.ts +3 -1
  189. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  190. package/test/src/__snapshots__/storage_sync.test.ts.snap +794 -205
  191. package/test/src/chunk_compaction_config.test.ts +38 -0
  192. package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
  193. package/test/src/compact-utils.test.ts +251 -0
  194. package/test/src/concurrent_writers.test.ts +599 -0
  195. package/test/src/connection_config.test.ts +159 -0
  196. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  197. package/test/src/helpers/concurrentWriter.mjs +29 -0
  198. package/test/src/helpers/s3TestFactory.ts +12 -12
  199. package/test/src/object_storage_usage.test.ts +268 -0
  200. package/test/src/parameter_compacting_v1.test.ts +120 -0
  201. package/test/src/parameter_compacting_v3.test.ts +192 -0
  202. package/test/src/parameter_compaction_fence.test.ts +276 -0
  203. package/test/src/storage.test.ts +280 -24
  204. package/test/src/storage_compacting.test.ts +1097 -253
  205. package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
  206. package/test/src/storage_s3_checksums.test.ts +6 -7
  207. package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
  208. package/test/src/storage_s3_reading.test.ts +294 -6
  209. package/test/src/storage_s3_writing.test.ts +4 -3
  210. package/test/src/storage_sync.test.ts +412 -80
  211. package/test/src/storeCurrentData.test.ts +5 -5
  212. package/test/src/util.ts +1 -1
  213. package/tsconfig.tsbuildinfo +1 -1
@@ -20,13 +20,17 @@ export abstract class MongoPersistedSyncConfigContentBase extends storage.Persis
20
20
  options: Omit<storage.PersistedSyncConfigContentData, 'syncConfigId'> & {
21
21
  mapping: SingleSyncConfigBucketDefinitionMapping;
22
22
  syncConfigId: bson.ObjectId | null;
23
- }
23
+ },
24
+ syncConfigParser: storage.SyncConfigParser
24
25
  ) {
25
26
  const { mapping, syncConfigId, ...base } = options;
26
- super({
27
- ...base,
28
- syncConfigId: syncConfigId?.toHexString() ?? null
29
- });
27
+ super(
28
+ {
29
+ ...base,
30
+ syncConfigId: syncConfigId?.toHexString() ?? null
31
+ },
32
+ syncConfigParser
33
+ );
30
34
  this.mapping = mapping;
31
35
  this.syncConfigObjectId = syncConfigId;
32
36
  }
@@ -60,18 +64,23 @@ export abstract class MongoPersistedSyncConfigContentBase extends storage.Persis
60
64
  }
61
65
  }
62
66
  export class MongoPersistedSyncConfigContentV1 extends MongoPersistedSyncConfigContentBase {
63
- constructor(db: PowerSyncMongo, doc: SyncRuleDocumentV1) {
64
- super(db, {
65
- replicationStreamId: doc._id,
66
- sync_rules_content: doc.content,
67
- compiled_plan: doc.serialized_plan ?? null,
68
- // Handle legacy values
69
- replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
70
- storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION,
71
- mapping: new SingleSyncConfigBucketDefinitionMapping(),
72
- syncConfigId: null,
73
- syncConfigState: doc.state
74
- });
67
+ constructor(db: PowerSyncMongo, doc: SyncRuleDocumentV1, syncConfigParser: storage.SyncConfigParser) {
68
+ super(
69
+ db,
70
+ {
71
+ replicationStreamId: doc._id,
72
+ sync_rules_content: doc.content,
73
+ compiled_plan: doc.serialized_plan ?? null,
74
+ // Handle legacy values
75
+ replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
76
+ storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION,
77
+ mapping: new SingleSyncConfigBucketDefinitionMapping(),
78
+ syncConfigId: null,
79
+ syncConfigState: doc.state,
80
+ version_label: doc.version_label ?? undefined
81
+ },
82
+ syncConfigParser
83
+ );
75
84
  }
76
85
 
77
86
  async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
@@ -86,22 +95,32 @@ export class MongoPersistedSyncConfigContentV1 extends MongoPersistedSyncConfigC
86
95
  export class MongoPersistedSyncConfigContentV3 extends MongoPersistedSyncConfigContentBase {
87
96
  declare public readonly syncConfigObjectId: bson.ObjectId;
88
97
 
89
- constructor(db: PowerSyncMongo, doc: ReplicationStreamDocumentV3, config: SyncConfigDefinition) {
98
+ constructor(
99
+ db: PowerSyncMongo,
100
+ doc: ReplicationStreamDocumentV3,
101
+ config: SyncConfigDefinition,
102
+ syncConfigParser: storage.SyncConfigParser
103
+ ) {
90
104
  const state = doc.sync_configs.find((c) => c._id.equals(config._id));
91
105
  if (state == null) {
92
106
  throw new ServiceAssertionError(`Cannot find sync config ${config._id} in replication stream ${doc._id}`);
93
107
  }
94
- super(db, {
95
- replicationStreamId: doc._id,
96
- sync_rules_content: config.content,
97
- compiled_plan: config.serialized_plan ?? null,
98
-
99
- replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
100
- storageVersion: doc.storage_version,
101
- mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(config.rule_mapping),
102
- syncConfigId: config._id,
103
- syncConfigState: state.state
104
- });
108
+ super(
109
+ db,
110
+ {
111
+ replicationStreamId: doc._id,
112
+ sync_rules_content: config.content,
113
+ compiled_plan: config.serialized_plan ?? null,
114
+
115
+ replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
116
+ storageVersion: doc.storage_version,
117
+ mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(config.rule_mapping),
118
+ syncConfigId: config._id,
119
+ syncConfigState: state.state,
120
+ version_label: config.version_label
121
+ },
122
+ syncConfigParser
123
+ );
105
124
  }
106
125
 
107
126
  async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
@@ -1,7 +1,11 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
3
3
  import { POWERSYNC_VERSION, storage } from '@powersync/service-core';
4
- import { MongoStorageConfig, normalizeClearBatchThrottleRate } from '../../types/types.js';
4
+ import {
5
+ MongoStorageConfig,
6
+ normalizeChunkCompactionConcurrency,
7
+ normalizeClearBatchThrottleRate
8
+ } from '../../types/types.js';
5
9
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
6
10
  import { MongoReportStorage } from '../MongoReportStorage.js';
7
11
  import { PowerSyncMongo } from './db.js';
@@ -24,6 +28,10 @@ export class MongoStorageProvider implements storage.StorageProvider {
24
28
  }
25
29
 
26
30
  const decodedConfig = MongoStorageConfig.decode(storage as any);
31
+ const chunkCompactionConcurrency = normalizeChunkCompactionConcurrency(
32
+ decodedConfig.chunk_compaction_concurrency,
33
+ decodedConfig.object_storage != null
34
+ );
27
35
 
28
36
  let objectStorage: ObjectStorage | undefined;
29
37
  if (decodedConfig.object_storage?.type === 's3') {
@@ -37,7 +45,8 @@ export class MongoStorageProvider implements storage.StorageProvider {
37
45
  forcePathStyle: decodedConfig.object_storage.force_path_style,
38
46
  accessKeyId: decodedConfig.object_storage.access_key_id,
39
47
  secretAccessKey: decodedConfig.object_storage.secret_access_key,
40
- concurrencyLimit: decodedConfig.object_storage.concurrency_limit
48
+ concurrencyLimit: decodedConfig.object_storage.concurrency_limit,
49
+ defaultsMode: decodedConfig.object_storage.defaults_mode
41
50
  });
42
51
  }
43
52
 
@@ -46,6 +55,8 @@ export class MongoStorageProvider implements storage.StorageProvider {
46
55
  maxPoolSize: resolvedConfig.storage.max_pool_size ?? 8
47
56
  });
48
57
 
58
+ const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
59
+
49
60
  let shuttingDown = false;
50
61
 
51
62
  // Explicitly connect on startup.
@@ -55,7 +66,6 @@ export class MongoStorageProvider implements storage.StorageProvider {
55
66
  // Errors here will cause the process to exit.
56
67
  await client.connect();
57
68
 
58
- const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
59
69
  const readPreference =
60
70
  decodedConfig.bulk_read_preference == null
61
71
  ? undefined
@@ -64,11 +74,13 @@ export class MongoStorageProvider implements storage.StorageProvider {
64
74
  // 90 is the minimum value.
65
75
  maxStalenessSeconds: decodedConfig.bulk_read_preference == 'primary' ? undefined : 90
66
76
  });
67
- const syncStorageFactory = new MongoBucketStorage(database, {
77
+ const syncStorageFactory = new MongoBucketStorage(database, options.syncConfigParser, {
78
+ chunkCompactionConcurrency,
68
79
  replicationStreamNamePrefix: resolvedConfig.slot_name_prefix,
69
80
  readPreference,
70
81
  clearBatchThrottleRate: normalizeClearBatchThrottleRate(decodedConfig.clear_batch_throttle_rate),
71
82
  checksumCacheTtlMs: resolvedConfig.api_parameters.bucket_count_cache_ttl_minutes * 60_000,
83
+ defaultStorageVersion: decodedConfig.default_storage_version,
72
84
  // Right now, only MongoDB source databases supports incremental reprocessing.
73
85
  // Remove this filter when we support it for other source databases.
74
86
  // This assumes a single source connection - revisit if we ever support multiple connections.
@@ -2,6 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import {
4
4
  BaseObserver,
5
+ logger as defaultLogger,
5
6
  DO_NOT_LOG,
6
7
  Logger,
7
8
  ReplicationAbortedError,
@@ -11,14 +12,15 @@ import {
11
12
  BroadcastIterable,
12
13
  CHECKPOINT_INVALIDATE_ALL,
13
14
  CheckpointChanges,
15
+ CompactInitialReplicationOptions,
16
+ CompactInitialReplicationResults,
14
17
  GetCheckpointChangesOptions,
15
18
  InternalOpId,
16
19
  mergeAsyncIterables,
17
- PopulateChecksumCacheOptions,
18
- PopulateChecksumCacheResults,
19
20
  ReplicationCheckpoint,
20
21
  ReplicationStreamStorageIds,
21
22
  storage,
23
+ SyncRuleState,
22
24
  utils,
23
25
  WatchWriteCheckpointOptions
24
26
  } from '@powersync/service-core';
@@ -28,6 +30,10 @@ import { LRUCache } from 'lru-cache';
28
30
  import * as timers from 'timers/promises';
29
31
  import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../../types/types.js';
30
32
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
33
+ import {
34
+ MongoGetCheckpointChangesOptions,
35
+ MongoSyncBucketStorageCheckpoint
36
+ } from './common/MongoSyncBucketStorageCheckpoint.js';
31
37
  import { DEFAULT_INLINE_THRESHOLD_BYTES } from './common/PersistedBatch.js';
32
38
  import type { VersionedPowerSyncMongo } from './db.js';
33
39
  import { StorageConfig } from './models.js';
@@ -37,7 +43,8 @@ import { MongoCompactOptions, MongoCompactor } from './MongoCompactor.js';
37
43
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
38
44
  import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
39
45
  import { MongoPersistedReplicationStream } from './MongoPersistedReplicationStream.js';
40
- import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
46
+ import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
47
+ import { MongoCheckpointAPIOptions, MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
41
48
  import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
42
49
 
43
50
  export interface MongoSyncBucketStorageOptions {
@@ -50,6 +57,16 @@ export interface MongoSyncBucketStorageOptions {
50
57
  inlineThresholdBytes?: number;
51
58
  }
52
59
 
60
+ /**
61
+ * The stream state read for a checkpoint. All fields must come from the same snapshot.
62
+ */
63
+ export interface MongoCheckpointState {
64
+ checkpoint: InternalOpId;
65
+ lsn: string | null;
66
+ /** See {@link MongoSyncBucketStorageCheckpoint.parameterChangesInvalidBefore}. */
67
+ parameterChangesInvalidBefore: InternalOpId;
68
+ }
69
+
53
70
  interface InternalCheckpointChanges extends CheckpointChanges {
54
71
  updatedWriteCheckpoints: Map<string, bigint>;
55
72
  invalidateWriteCheckpoints: boolean;
@@ -93,6 +110,10 @@ export abstract class MongoSyncBucketStorage
93
110
  public readonly readPreference: mongo.ReadPreference | undefined;
94
111
  public readonly clearBatchThrottleRate: number;
95
112
  #storageInitialized = false;
113
+ /**
114
+ * Required for writing, optional for reading.
115
+ */
116
+ private readonly replicationLock: MongoSyncRulesLock | null;
96
117
 
97
118
  constructor(
98
119
  public readonly factory: MongoBucketStorage,
@@ -103,6 +124,7 @@ export abstract class MongoSyncBucketStorage
103
124
  options: MongoSyncBucketStorageOptions
104
125
  ) {
105
126
  super();
127
+ this.replicationLock = replicationStream.current_lock;
106
128
  this.storageConfig = options.storageConfig;
107
129
  this.objectStorage = options.objectStorage;
108
130
  // Keep small chunks inline in MongoDB rather than offloading them to S3.
@@ -112,12 +134,27 @@ export abstract class MongoSyncBucketStorage
112
134
  this.clearBatchThrottleRate = options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE;
113
135
  this.db = factory.db.versioned(this.storageConfig);
114
136
  this.checksums = this.createMongoChecksums(options);
115
- this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
137
+ this.writeCheckpointAPI = this.createWriteCheckpointAPI({
116
138
  db: this.db,
117
- mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
118
- sync_rules_id: replicationStreamId
139
+ writeCheckpointMode: { mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED },
140
+ replicationStreamId
119
141
  });
120
- this.logger = replicationStream.logger;
142
+
143
+ // Include both replication stream name and sync config version labels in log prefix.
144
+ // Examples:
145
+ // No sync config with a label: [stream]
146
+ // All sync configs have a label: [stream][v5,v6]
147
+ // Mixed: [stream][?,v6]
148
+ const hasVersionLabel = replicationStream.syncConfigContent.some((config) => config.version_label != null);
149
+ const versionLabels = hasVersionLabel
150
+ ? replicationStream.syncConfigContent.map((config) => config.version_label ?? '?')
151
+ : [];
152
+ this.logger =
153
+ versionLabels.length == 0
154
+ ? replicationStream.logger
155
+ : defaultLogger.child({
156
+ prefix: `[${replicationStreamName}][${versionLabels.join(',')}] `
157
+ });
121
158
  }
122
159
 
123
160
  /**
@@ -128,11 +165,20 @@ export abstract class MongoSyncBucketStorage
128
165
  abstract createMongoCompactor(options: MongoCompactOptions): MongoCompactor;
129
166
 
130
167
  protected abstract createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums;
168
+ protected createWriteCheckpointAPI(options: MongoCheckpointAPIOptions): MongoWriteCheckpointAPI {
169
+ return new MongoWriteCheckpointAPI(options);
170
+ }
171
+
131
172
  protected abstract createMongoParameterCompactor(
132
173
  checkpoint: InternalOpId,
133
174
  options: storage.CompactOptions
134
175
  ): MongoParameterCompactor;
135
176
 
177
+ /** MongoDB parameter compaction uses a persisted operation-id cursor. */
178
+ public supportsIncrementalParameterCompaction(): boolean {
179
+ return true;
180
+ }
181
+
136
182
  get writeCheckpointMode() {
137
183
  return this.writeCheckpointAPI.writeCheckpointMode;
138
184
  }
@@ -144,8 +190,8 @@ export abstract class MongoSyncBucketStorage
144
190
  return this.replicationStream.storageIds;
145
191
  }
146
192
 
147
- setWriteCheckpointMode(mode: storage.WriteCheckpointMode): void {
148
- this.writeCheckpointAPI.setWriteCheckpointMode(mode);
193
+ setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
194
+ this.writeCheckpointAPI.setWriteCheckpointMode(config);
149
195
  }
150
196
 
151
197
  createManagedWriteCheckpoints(
@@ -178,9 +224,7 @@ export abstract class MongoSyncBucketStorage
178
224
  return (await this.getCheckpointInternal()) ?? new EmptyReplicationCheckpoint();
179
225
  }
180
226
 
181
- protected abstract fetchCheckpointState(
182
- session: mongo.ClientSession
183
- ): Promise<{ checkpoint: bigint; lsn: string | null } | null>;
227
+ protected abstract fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null>;
184
228
 
185
229
  async getCheckpointInternal(): Promise<storage.ReplicationCheckpoint | null> {
186
230
  return await this.db.client.withSession({ snapshot: true }, async (session) => {
@@ -197,7 +241,14 @@ export abstract class MongoSyncBucketStorage
197
241
  if (clusterTime == null) {
198
242
  throw new ServiceAssertionError('Missing clusterTime in getCheckpoint()');
199
243
  }
200
- return new MongoReplicationCheckpoint(this, state.checkpoint, state.lsn, snapshotTime, clusterTime);
244
+ return new MongoReplicationCheckpoint(
245
+ this,
246
+ state.checkpoint,
247
+ state.lsn,
248
+ snapshotTime,
249
+ clusterTime,
250
+ state.parameterChangesInvalidBefore
251
+ );
201
252
  });
202
253
  }
203
254
 
@@ -224,23 +275,36 @@ export abstract class MongoSyncBucketStorage
224
275
  * The version-independent part of the batch options.
225
276
  */
226
277
  protected writerBatchOptions(options: storage.CreateWriterOptions): Omit<MongoBucketBatchOptions, 'resumeFromLsn'> {
278
+ const replicationLock = this.requireReplicationLock();
227
279
  return {
228
280
  logger: options.logger ?? this.logger,
229
281
  db: this.db,
230
282
  parsedSyncConfig: this.getParsedSyncConfigSet(options),
231
283
  replicationStreamId: this.replicationStreamId,
232
284
  replicationStreamName: this.replicationStreamName,
285
+ replicationLock,
286
+ opIdAllocator: this.factory.getOpIdAllocator(this.replicationStream, replicationLock),
233
287
  storeCurrentData: options.storeCurrentData,
234
288
  skipExistingRows: options.skipExistingRows ?? false,
235
289
  markRecordUnavailable: options.markRecordUnavailable,
236
290
  hooks: options.hooks,
237
291
  tracer: options.tracer,
292
+ signal: options.signal,
238
293
  objectStorage: this.objectStorage,
239
294
  inlineThresholdBytes: this.inlineThresholdBytes
240
295
  };
241
296
  }
242
297
 
298
+ private requireReplicationLock(): MongoSyncRulesLock {
299
+ if (this.replicationLock == null) {
300
+ throw new ServiceAssertionError('A replication lease is required to create a writer');
301
+ }
302
+ this.replicationLock.throwIfAborted();
303
+ return this.replicationLock;
304
+ }
305
+
243
306
  async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
307
+ this.requireReplicationLock();
244
308
  await this.initializeStorage();
245
309
 
246
310
  const writer = await this.createWriterImpl(options);
@@ -329,6 +393,7 @@ export abstract class MongoSyncBucketStorage
329
393
  protected abstract clearBucketState(signal?: AbortSignal): Promise<void>;
330
394
 
331
395
  protected abstract clearSourceTables(signal?: AbortSignal): Promise<void>;
396
+ protected async clearCustomCheckpointRequests(_signal?: AbortSignal): Promise<void> {}
332
397
  protected abstract clearSyncRuleState(): Promise<void>;
333
398
 
334
399
  async clear(options?: storage.ClearStorageOptions): Promise<void> {
@@ -338,6 +403,7 @@ export abstract class MongoSyncBucketStorage
338
403
  throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
339
404
  }
340
405
 
406
+ this.factory.discardOpIdAllocator(this.replicationStreamId);
341
407
  await this.clearSyncRuleState();
342
408
 
343
409
  await this.clearBucketData(signal);
@@ -345,6 +411,7 @@ export abstract class MongoSyncBucketStorage
345
411
  await this.clearSourceRecords(signal);
346
412
  await this.clearBucketState(signal);
347
413
  await this.clearSourceTables(signal);
414
+ await this.clearCustomCheckpointRequests(signal);
348
415
 
349
416
  this.#storageInitialized = false;
350
417
  }
@@ -367,43 +434,30 @@ export abstract class MongoSyncBucketStorage
367
434
  async compact(options?: storage.CompactOptions) {
368
435
  let maxOpId = options?.maxOpId;
369
436
  if (maxOpId == null) {
437
+ // For PROCESSING streams, this will be undefined.
370
438
  const checkpoint = await this.getCheckpointInternal();
371
439
  maxOpId = checkpoint?.checkpoint ?? undefined;
372
440
  }
373
441
  await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
374
442
 
375
- if (maxOpId != null && options?.compactParameterData) {
376
- await this.createMongoParameterCompactor(maxOpId, options).compact();
443
+ if (maxOpId != null && options?.compactParameterData && this.replicationStream.state == SyncRuleState.ACTIVE) {
444
+ // Use the stream-scoped logger, matching bucket compaction above.
445
+ await this.createMongoParameterCompactor(maxOpId, { ...options, logger: this.logger }).compact();
377
446
  }
378
447
  }
379
448
 
449
+ abstract compactInitialReplication(
450
+ options: CompactInitialReplicationOptions
451
+ ): Promise<CompactInitialReplicationResults>;
452
+
380
453
  /**
381
454
  * The highest op id persisted for this stream, whether or not covered by a checkpoint.
382
455
  *
383
- * Used as the default `maxOpId` for {@link populatePersistentChecksumCache}, which runs after
456
+ * Used as the default `maxOpId` for {@link compactInitialReplication}, which runs after
384
457
  * initial replication but before the first checkpoint exists.
385
458
  */
386
459
  protected abstract fetchPersistedOpHead(): Promise<InternalOpId | null>;
387
460
 
388
- async populatePersistentChecksumCache(options: PopulateChecksumCacheOptions): Promise<PopulateChecksumCacheResults> {
389
- this.logger.info(`Populating persistent checksum cache...`);
390
- const start = Date.now();
391
- const maxOpId = options.maxOpId ?? (await this.fetchPersistedOpHead()) ?? undefined;
392
- const compactor = this.createMongoCompactor({
393
- ...options,
394
- maxOpId,
395
- memoryLimitMB: 0,
396
- logger: this.logger
397
- });
398
-
399
- const result = await compactor.populateChecksums({
400
- minBucketChanges: options.minBucketChanges ?? 10
401
- });
402
- const duration = Date.now() - start;
403
- this.logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
404
- return result;
405
- }
406
-
407
461
  private async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<ReplicationCheckpoint> {
408
462
  if (signal.aborted) {
409
463
  return;
@@ -580,13 +634,31 @@ export abstract class MongoSyncBucketStorage
580
634
  }
581
635
 
582
636
  protected abstract getParameterBucketChangesImpl(
583
- options: GetCheckpointChangesOptions
637
+ options: MongoGetCheckpointChangesOptions
584
638
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>>;
585
639
 
586
640
  private async getParameterBucketChanges(
587
641
  options: GetCheckpointChangesOptions
588
642
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
589
- return this.getParameterBucketChangesImpl(options);
643
+ const nextCheckpoint = requireMongoCheckpoint(options.nextCheckpoint);
644
+ if (options.lastCheckpoint.checkpoint < nextCheckpoint.parameterChangesInvalidBefore) {
645
+ // Parameter compaction may have deleted parameter entries in the range we'd have to query
646
+ // to find the individual changed lookups. Invalidate all parameter buckets instead.
647
+ //
648
+ // The fence is committed before the first delete of a compaction pass, and captured in the
649
+ // same snapshot as the checkpoint, so a checkpoint that could miss a deleted entry always
650
+ // observes the fence as well.
651
+ return {
652
+ invalidateParameterBuckets: true,
653
+ updatedParameterLookups: new Set<string>()
654
+ };
655
+ }
656
+ // The query below runs at the checkpoint snapshot, which still sees entries deleted by a
657
+ // compaction pass that started after the snapshot.
658
+ return this.getParameterBucketChangesImpl({
659
+ lastCheckpoint: options.lastCheckpoint,
660
+ nextCheckpoint
661
+ });
590
662
  }
591
663
 
592
664
  private checkpointChangesCache = new LRUCache<
@@ -611,7 +683,10 @@ export abstract class MongoSyncBucketStorage
611
683
  });
612
684
 
613
685
  async getCheckpointChanges(options: GetCheckpointChangesOptions): Promise<InternalCheckpointChanges> {
614
- const key = `${options.lastCheckpoint.checkpoint}_${options.lastCheckpoint.lsn}__${options.nextCheckpoint.checkpoint}_${options.nextCheckpoint.lsn}`;
686
+ // The invalidation fence is part of the identity: the same checkpoint pair read before and
687
+ // after a compaction pass produces different results (specific lookups vs. invalidate-all).
688
+ const fence = requireMongoCheckpoint(options.nextCheckpoint).parameterChangesInvalidBefore;
689
+ const key = `${options.lastCheckpoint.checkpoint}_${options.lastCheckpoint.lsn}__${options.nextCheckpoint.checkpoint}_${options.nextCheckpoint.lsn}_${fence}`;
615
690
  const result = await this.checkpointChangesCache.fetch(key, { context: { options } });
616
691
  return result!;
617
692
  }
@@ -629,7 +704,19 @@ export abstract class MongoSyncBucketStorage
629
704
  }
630
705
  }
631
706
 
632
- class MongoReplicationCheckpoint implements ReplicationCheckpoint {
707
+ /**
708
+ * We don't support any other constructions of ReplicationCheckpoint.
709
+ */
710
+ function requireMongoCheckpoint(checkpoint: ReplicationCheckpoint): MongoReplicationCheckpoint {
711
+ if (!(checkpoint instanceof MongoReplicationCheckpoint)) {
712
+ throw new ServiceAssertionError(
713
+ `Checkpoint changes require a checkpoint from getCheckpointInternal(), got ${checkpoint.constructor.name}`
714
+ );
715
+ }
716
+ return checkpoint;
717
+ }
718
+
719
+ class MongoReplicationCheckpoint implements MongoSyncBucketStorageCheckpoint {
633
720
  #storage: MongoSyncBucketStorage;
634
721
 
635
722
  constructor(
@@ -637,7 +724,9 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
637
724
  public readonly checkpoint: InternalOpId,
638
725
  public readonly lsn: string | null,
639
726
  public snapshotTime: mongo.Timestamp,
640
- public clusterTime: mongo.ClusterTime
727
+ public clusterTime: mongo.ClusterTime,
728
+ /** Captured in the same snapshot as the checkpoint. */
729
+ public readonly parameterChangesInvalidBefore: InternalOpId
641
730
  ) {
642
731
  this.#storage = storage;
643
732
  }
@@ -647,6 +736,10 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
647
736
  }
648
737
  }
649
738
 
739
+ /**
740
+ * Used when no checkpoint has been persisted yet. This has no snapshot or invalidation fence, so
741
+ * it cannot be used for checkpoint change detection - see {@link requireMongoCheckpoint}.
742
+ */
650
743
  class EmptyReplicationCheckpoint implements ReplicationCheckpoint {
651
744
  readonly checkpoint: InternalOpId = 0n;
652
745
  readonly lsn: string | null = null;
@@ -1,15 +1,23 @@
1
1
  import crypto from 'crypto';
2
2
 
3
3
  import { mongo } from '@powersync/lib-service-mongodb';
4
- import { ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
5
- import { storage } from '@powersync/service-core';
4
+ import { ErrorCode, Logger, ReplicationAbortedError, ServiceError } from '@powersync/lib-services-framework';
5
+ import { storage, utils } from '@powersync/service-core';
6
6
  import { VersionedPowerSyncMongo } from './db.js';
7
7
 
8
+ const LOCK_DURATION_MS = 60 * 1000;
9
+
8
10
  /**
9
11
  * Manages a lock on a replication stream document, so that only one process
10
12
  * processes that replication stream at a time.
11
13
  */
12
14
  export class MongoSyncRulesLock implements storage.ReplicationLock {
15
+ /** FIFO admission shared by this lease's writers; database fencing still enforces ownership. */
16
+ readonly writerMutex = new utils.Mutex();
17
+
18
+ private readonly abort = new AbortController();
19
+ readonly signal = this.abort.signal;
20
+
13
21
  private readonly refreshInterval: NodeJS.Timeout;
14
22
 
15
23
  /**
@@ -22,15 +30,20 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
22
30
  ): Promise<MongoSyncRulesLock> {
23
31
  const lockId = crypto.randomBytes(8).toString('hex');
24
32
  const doc = await db.sync_rules.findOneAndUpdate(
25
- { _id: sync_rules.replicationStreamId, $or: [{ lock: null }, { 'lock.expires_at': { $lt: new Date() } }] },
26
33
  {
27
- $set: {
28
- lock: {
29
- id: lockId,
30
- expires_at: new Date(Date.now() + 60 * 1000)
34
+ _id: sync_rules.replicationStreamId,
35
+ $or: [{ lock: null }, { $expr: { $lt: ['$lock.expires_at', '$$NOW'] } }]
36
+ },
37
+ [
38
+ {
39
+ $set: {
40
+ lock: {
41
+ id: lockId,
42
+ expires_at: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: LOCK_DURATION_MS } }
43
+ }
31
44
  }
32
45
  }
33
- },
46
+ ],
34
47
  {
35
48
  projection: { lock: 1 },
36
49
  returnDocument: 'before',
@@ -60,13 +73,14 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
60
73
  constructor(
61
74
  private db: VersionedPowerSyncMongo,
62
75
  public sync_rules_id: number,
63
- private lock_id: string,
76
+ public readonly lock_id: string,
64
77
  private logger: Logger
65
78
  ) {
66
79
  this.refreshInterval = setInterval(async () => {
67
80
  try {
68
81
  await this.refresh();
69
82
  } catch (e) {
83
+ this.abort.abort(e);
70
84
  this.logger.error('Failed to refresh lock', e);
71
85
  clearInterval(this.refreshInterval);
72
86
  }
@@ -74,6 +88,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
74
88
  }
75
89
 
76
90
  async release(): Promise<void> {
91
+ this.abort.abort(new Error('Replication lock released'));
77
92
  clearInterval(this.refreshInterval);
78
93
  const result = await this.db.sync_rules.updateOne(
79
94
  {
@@ -90,15 +105,66 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
90
105
  }
91
106
  }
92
107
 
108
+ throwIfAborted(): void {
109
+ this.signal.throwIfAborted();
110
+ }
111
+
112
+ static ownerFilter(streamId: number, lock: MongoSyncRulesLock) {
113
+ lock.throwIfAborted();
114
+ return { _id: streamId, 'lock.id': lock.lock_id };
115
+ }
116
+
117
+ static heartbeatUpdate() {
118
+ // Always change the existing heartbeat, even for writes in the same
119
+ // millisecond. A no-op update is not sufficient for a transactional fence.
120
+ return {
121
+ last_keepalive_ts: {
122
+ $max: ['$$NOW', { $add: [{ $ifNull: ['$last_keepalive_ts', new Date(0)] }, 1] }]
123
+ }
124
+ };
125
+ }
126
+
127
+ static assertOwned<T>(document: T | null): T {
128
+ if (document == null) {
129
+ throw new ReplicationAbortedError('Replication writer no longer owns the stream');
130
+ }
131
+ return document;
132
+ }
133
+
134
+ /**
135
+ * A write, not just an ownership read: takeover conflicts with every transaction
136
+ * that publishes under this owner. Every writer must hold a stream lease.
137
+ */
138
+ static async fence(
139
+ db: VersionedPowerSyncMongo,
140
+ streamId: number,
141
+ lock: MongoSyncRulesLock,
142
+ session: mongo.ClientSession,
143
+ projection: mongo.Document = { last_persisted_op: 1, last_checkpoint: 1, keepalive_op: 1 }
144
+ ) {
145
+ const doc = await db.sync_rules.findOneAndUpdate(
146
+ this.ownerFilter(streamId, lock),
147
+ [{ $set: this.heartbeatUpdate() }],
148
+ { session, returnDocument: 'after', projection }
149
+ );
150
+ return this.assertOwned(doc);
151
+ }
152
+
93
153
  private async refresh(): Promise<void> {
94
154
  const result = await this.db.sync_rules.findOneAndUpdate(
95
155
  {
96
156
  _id: this.sync_rules_id,
97
157
  'lock.id': this.lock_id
98
158
  },
99
- {
100
- $set: { 'lock.expires_at': new Date(Date.now() + 60 * 1000) }
101
- },
159
+ [
160
+ {
161
+ $set: {
162
+ 'lock.expires_at': {
163
+ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: LOCK_DURATION_MS }
164
+ }
165
+ }
166
+ }
167
+ ],
102
168
  { returnDocument: 'after' }
103
169
  );
104
170
  if (result == null) {