@powersync/service-module-mongodb-storage 0.20.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 +45 -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 +31 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  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 +90 -19
  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 +84 -4
  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 +6 -6
  106. package/dist/storage/implementation/v3/source-table-utils.js +30 -37
  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 +34 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +31 -0
  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 +124 -22
  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 +91 -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 +39 -41
  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 +336 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -3,6 +3,8 @@ import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import { ServiceAssertionError } from '@powersync/lib-services-framework';
4
4
  import {
5
5
  CheckpointChanges,
6
+ CompactInitialReplicationOptions,
7
+ CompactInitialReplicationResults,
6
8
  deserializeParameterLookup,
7
9
  GetCheckpointChangesOptions,
8
10
  InternalOpId,
@@ -26,13 +28,20 @@ import {
26
28
  setSessionSnapshotTime
27
29
  } from '../../../utils/util.js';
28
30
  import { MongoBucketStorage } from '../../MongoBucketStorage.js';
29
- import { MongoSyncBucketStorageCheckpoint } from '../common/MongoSyncBucketStorageCheckpoint.js';
31
+ import {
32
+ MongoGetCheckpointChangesOptions,
33
+ MongoSyncBucketStorageCheckpoint
34
+ } from '../common/MongoSyncBucketStorageCheckpoint.js';
30
35
  import { SourceKey } from '../models.js';
31
36
  import { MongoChecksums } from '../MongoChecksums.js';
32
- import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
37
+ import { MongoCompactOptions } from '../MongoCompactor.js';
33
38
  import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
34
39
  import { MongoPersistedReplicationStream } from '../MongoPersistedReplicationStream.js';
35
- import { MongoSyncBucketStorage, MongoSyncBucketStorageOptions } from '../MongoSyncBucketStorage.js';
40
+ import {
41
+ MongoCheckpointState,
42
+ MongoSyncBucketStorage,
43
+ MongoSyncBucketStorageOptions
44
+ } from '../MongoSyncBucketStorage.js';
36
45
  import {
37
46
  BucketDataDocumentV1,
38
47
  BucketDataKeyV1,
@@ -100,14 +109,20 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
100
109
  });
101
110
  }
102
111
 
103
- protected async fetchCheckpointState(
104
- session: mongo.ClientSession
105
- ): Promise<{ checkpoint: bigint; lsn: string | null } | null> {
112
+ protected async fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null> {
106
113
  const doc = (await this.db.sync_rules.findOne(
107
114
  { _id: this.replicationStreamId },
108
115
  {
109
116
  session,
110
- projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
117
+ projection: {
118
+ _id: 1,
119
+ state: 1,
120
+ last_checkpoint: 1,
121
+ last_checkpoint_lsn: 1,
122
+ snapshot_done: 1,
123
+ // Must be read in the same snapshot as the checkpoint.
124
+ 'parameter_compaction.checkpoint_changes_invalid_before': 1
125
+ }
111
126
  }
112
127
  )) as SyncRuleDocumentV1;
113
128
  if (!doc?.snapshot_done || ![storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED].includes(doc.state)) {
@@ -115,7 +130,9 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
115
130
  }
116
131
  return {
117
132
  checkpoint: doc.last_checkpoint ?? 0n,
118
- lsn: doc.last_checkpoint_lsn ?? null
133
+ lsn: doc.last_checkpoint_lsn ?? null,
134
+ // Defaults to 0n for streams that have never been compacted.
135
+ parameterChangesInvalidBefore: doc.parameter_compaction?.checkpoint_changes_invalid_before ?? 0n
119
136
  };
120
137
  }
121
138
 
@@ -171,7 +188,8 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
171
188
  no_checkpoint_before: null
172
189
  },
173
190
  $unset: {
174
- snapshot_lsn: 1
191
+ snapshot_lsn: 1,
192
+ parameter_compaction: 1
175
193
  }
176
194
  },
177
195
  { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
@@ -186,10 +204,31 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
186
204
  });
187
205
  }
188
206
 
189
- createMongoCompactor(options: MongoCompactOptions): MongoCompactor {
207
+ createMongoCompactor(options: MongoCompactOptions): MongoCompactorV1 {
190
208
  return new MongoCompactorV1(this, this.db, options);
191
209
  }
192
210
 
211
+ async compactInitialReplication(
212
+ options: CompactInitialReplicationOptions
213
+ ): Promise<CompactInitialReplicationResults> {
214
+ this.logger.info(`Compacting after initial replication...`);
215
+ const start = Date.now();
216
+ const maxOpId = options.maxOpId ?? (await this.fetchPersistedOpHead()) ?? undefined;
217
+ const compactor = this.createMongoCompactor({
218
+ ...options,
219
+ maxOpId,
220
+ memoryLimitMB: 0,
221
+ logger: this.logger
222
+ });
223
+
224
+ const result = await compactor.populateChecksums({
225
+ minBucketChanges: options.minBucketChanges ?? 10
226
+ });
227
+ const duration = Date.now() - start;
228
+ this.logger.info(`Compacted after initial replication in ${(duration / 1000).toFixed(1)}s`);
229
+ return result;
230
+ }
231
+
193
232
  protected createMongoParameterCompactor(
194
233
  checkpoint: InternalOpId,
195
234
  options: storage.CompactOptions
@@ -303,7 +342,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
303
342
  }
304
343
 
305
344
  protected getParameterBucketChangesImpl(
306
- options: GetCheckpointChangesOptions
345
+ options: MongoGetCheckpointChangesOptions
307
346
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
308
347
  return getParameterBucketChangesV1(this.versionContext, options);
309
348
  }
@@ -548,27 +587,48 @@ export async function getDataBucketChangesV1(
548
587
  };
549
588
  }
550
589
 
590
+ /**
591
+ * Query the parameter entries changed between the two checkpoints, to determine which parameter
592
+ * lookups need to be re-evaluated.
593
+ *
594
+ * This runs at the next checkpoint's snapshot, so it still sees entries that parameter compaction
595
+ * deleted after that snapshot. Compaction that deleted entries before the snapshot is covered by
596
+ * the invalidation fence, checked before we get here.
597
+ */
551
598
  export async function getParameterBucketChangesV1(
552
599
  ctx: MongoSyncBucketStorageContextV1,
553
- options: GetCheckpointChangesOptions
600
+ options: MongoGetCheckpointChangesOptions
554
601
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
555
602
  const limit = 1000;
556
- const parameterUpdates = await ctx.db.parameterIndexV1
557
- .find(
558
- {
559
- _id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint },
560
- 'key.g': ctx.replicationStreamId
561
- },
562
- {
563
- projection: {
564
- lookup: 1
603
+ const parameterUpdates = await ctx.db.client.withSession({ snapshot: true }, async (session) => {
604
+ setSessionSnapshotTime(session, options.nextCheckpoint.snapshotTime);
605
+ return await ctx.db.parameterIndexV1
606
+ .find(
607
+ {
608
+ _id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint },
609
+ 'key.g': ctx.replicationStreamId
565
610
  },
566
- limit: limit + 1,
567
- batchSize: limit + 2,
568
- singleBatch: true
569
- }
570
- )
571
- .toArray();
611
+ {
612
+ session,
613
+ readConcern: 'snapshot',
614
+ projection: {
615
+ lookup: 1
616
+ },
617
+ limit: limit + 1,
618
+ batchSize: limit + 2,
619
+ singleBatch: true
620
+ }
621
+ )
622
+ .toArray()
623
+ .catch((e) => {
624
+ // Includes the case where the checkpoint snapshot has expired. Degrading to
625
+ // invalidateParameterBuckets would be safe in itself - it reads nothing - but it gains
626
+ // nothing: the caller responds to that by re-evaluating the parameter queries at this
627
+ // same snapshot, which fails too. The checkpoint has to be refetched instead, which is
628
+ // what the existing sync retry behavior does.
629
+ throw lib_mongo.mapQueryError(e, 'while querying parameter changes');
630
+ });
631
+ });
572
632
  const invalidateParameterUpdates = parameterUpdates.length > limit;
573
633
 
574
634
  return {
@@ -48,6 +48,7 @@ export function serializeBucketDataV1(document: BucketDataDoc): BucketDataDocume
48
48
  op: document.op,
49
49
  source_table: document.source_table,
50
50
  source_key: document.source_key,
51
+ subkey: document.subkey,
51
52
  table: document.table,
52
53
  row_id: document.row_id,
53
54
  checksum: document.checksum,
@@ -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,6 +154,7 @@ 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
160
  const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
@@ -173,7 +182,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
173
182
  .toArray();
174
183
 
175
184
  const candidateTables = candidateDocs.map((doc) =>
176
- sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping)
185
+ sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping, eventById)
177
186
  );
178
187
  const candidates = candidateTables.map((table) => table.clone());
179
188
  const resolution = await reconcile({ source, candidates });
@@ -191,7 +200,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
191
200
  storeCurrentData: source.sendsCompleteRows !== true,
192
201
  syncConfig,
193
202
  mapping,
194
- desired: sourceTableDesiredResolution(syncConfig, source, mapping),
203
+ desired: sourceTableDesiredResolution(syncConfig, source, mapping, eventById),
195
204
  sourceCompatibleTables: resolution.compatibleTables,
196
205
  newTableSourceMetadata: resolution.newTableValues.sourceMetadata
197
206
  };
@@ -209,7 +218,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
209
218
  {
210
219
  $set: {
211
220
  bucket_data_source_ids: update.memberships.bucketDataSourceIds,
212
- parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
221
+ parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds,
222
+ event_definition_ids: update.memberships.eventDefinitionIds
213
223
  }
214
224
  },
215
225
  { session }
@@ -227,12 +237,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
227
237
  plan.tables.push(table);
228
238
  }
229
239
 
230
- // If memberships are split across multiple source tables, only one may fire events.
231
- designateEventCarrier(plan.tables, context.desired.triggersEvent);
232
-
233
240
  result = {
234
241
  tables: plan.tables,
235
- 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
+ )
236
245
  };
237
246
  });
238
247
 
@@ -247,12 +256,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
247
256
  return null;
248
257
  }
249
258
 
250
- const refreshed = sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules, this.mapping);
251
- // The event-carrier designation is decided per resolveTables result and not persisted -
252
- // preserve the caller's designation instead of recomputing it from the ref, so that
253
- // refreshing a non-carrier table does not make it fire events.
254
- refreshed.syncEvent = table.syncEvent;
255
- return refreshed;
259
+ return sourceTableFromDocument(
260
+ doc,
261
+ table.ref.connectionTag,
262
+ this.sync_rules,
263
+ this.mapping,
264
+ this.options.parsedSyncConfig.eventById
265
+ );
256
266
  }
257
267
 
258
268
  async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
@@ -519,7 +529,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
519
529
  _id: { $ne: this.replicationStreamId },
520
530
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
521
531
  },
522
- syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
532
+ stopReplicationStreamPipeline(),
523
533
  { session }
524
534
  );
525
535
  activated = true;
@@ -683,4 +693,96 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
683
693
  return copy;
684
694
  });
685
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
+ }
686
788
  }