@powersync/service-module-mongodb-storage 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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 +119 -6
  2. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
  3. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
  4. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +3 -3
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -1
  7. package/dist/storage/MongoBucketStorage.d.ts +5 -3
  8. package/dist/storage/MongoBucketStorage.js +53 -26
  9. package/dist/storage/MongoBucketStorage.js.map +1 -1
  10. package/dist/storage/MongoReportStorage.js.map +1 -1
  11. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
  12. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  13. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  14. package/dist/storage/implementation/MongoBucketBatch.d.ts +25 -21
  15. package/dist/storage/implementation/MongoBucketBatch.js +256 -210
  16. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  17. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  18. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  19. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  20. package/dist/storage/implementation/MongoChecksums.d.ts +30 -19
  21. package/dist/storage/implementation/MongoChecksums.js +13 -71
  22. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  23. package/dist/storage/implementation/MongoCompactor.d.ts +99 -53
  24. package/dist/storage/implementation/MongoCompactor.js +265 -303
  25. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  26. package/dist/storage/implementation/MongoParameterCompactor.d.ts +12 -7
  27. package/dist/storage/implementation/MongoParameterCompactor.js +24 -9
  28. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  29. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  30. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  33. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +11 -7
  34. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  35. package/dist/storage/implementation/MongoStorageProvider.js +1 -1
  36. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +54 -30
  38. package/dist/storage/implementation/MongoSyncBucketStorage.js +118 -472
  39. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  40. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +7 -5
  41. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  42. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  43. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
  44. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  45. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  46. package/dist/storage/implementation/OperationBatch.js +3 -2
  47. package/dist/storage/implementation/OperationBatch.js.map +1 -1
  48. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  49. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  50. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  51. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  52. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  53. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  54. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  55. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  56. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  57. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  58. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  59. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  60. package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
  61. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  62. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  63. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  64. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  65. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  66. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  67. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  68. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  69. package/dist/storage/implementation/db.d.ts +38 -4
  70. package/dist/storage/implementation/db.js +80 -1
  71. package/dist/storage/implementation/db.js.map +1 -1
  72. package/dist/storage/implementation/models.d.ts +66 -25
  73. package/dist/storage/implementation/models.js +22 -2
  74. package/dist/storage/implementation/models.js.map +1 -1
  75. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
  76. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  77. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  78. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  79. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  80. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  81. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  82. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  83. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  84. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  85. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  86. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  87. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
  88. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +269 -0
  89. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
  91. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  92. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  93. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  94. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  95. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  96. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  97. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  98. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  99. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  100. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  101. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  102. package/dist/storage/implementation/v1/models.d.ts +34 -0
  103. package/dist/storage/implementation/v1/models.js +37 -0
  104. package/dist/storage/implementation/v1/models.js.map +1 -0
  105. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
  106. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
  107. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  108. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  109. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  110. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  111. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  112. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  113. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  114. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  115. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  116. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  117. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
  118. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  119. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  120. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
  121. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +404 -0
  122. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
  124. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  125. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  126. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  127. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  128. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  129. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  130. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  131. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  132. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
  133. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
  134. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  135. package/dist/storage/implementation/v3/models.d.ts +43 -0
  136. package/dist/storage/implementation/v3/models.js +34 -0
  137. package/dist/storage/implementation/v3/models.js.map +1 -0
  138. package/dist/storage/storage-index.d.ts +8 -5
  139. package/dist/storage/storage-index.js +8 -5
  140. package/dist/storage/storage-index.js.map +1 -1
  141. package/dist/utils/test-utils.d.ts +4 -1
  142. package/dist/utils/test-utils.js +15 -12
  143. package/dist/utils/test-utils.js.map +1 -1
  144. package/dist/utils/util.d.ts +13 -5
  145. package/dist/utils/util.js +40 -5
  146. package/dist/utils/util.js.map +1 -1
  147. package/package.json +9 -9
  148. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  149. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
  150. package/src/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
  151. package/src/storage/MongoBucketStorage.ts +111 -55
  152. package/src/storage/MongoReportStorage.ts +2 -2
  153. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  154. package/src/storage/implementation/MongoBucketBatch.ts +322 -270
  155. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  156. package/src/storage/implementation/MongoChecksums.ts +54 -75
  157. package/src/storage/implementation/MongoCompactor.ts +411 -412
  158. package/src/storage/implementation/MongoParameterCompactor.ts +52 -26
  159. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  160. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +19 -10
  161. package/src/storage/implementation/MongoStorageProvider.ts +1 -1
  162. package/src/storage/implementation/MongoSyncBucketStorage.ts +206 -466
  163. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -16
  164. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +7 -5
  165. package/src/storage/implementation/OperationBatch.ts +3 -2
  166. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  167. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  168. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  169. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  170. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  171. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  172. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  173. package/src/storage/implementation/db.ts +115 -8
  174. package/src/storage/implementation/models.ts +88 -28
  175. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  176. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  177. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  178. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  179. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +432 -0
  180. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  181. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  182. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  183. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  184. package/src/storage/implementation/v1/models.ts +84 -0
  185. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  186. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  187. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  188. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  189. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  190. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +545 -0
  191. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  192. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  193. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  194. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  195. package/src/storage/implementation/v3/models.ts +96 -0
  196. package/src/storage/storage-index.ts +8 -5
  197. package/src/utils/test-utils.ts +15 -12
  198. package/src/utils/util.ts +53 -9
  199. package/test/src/__snapshots__/storage.test.ts.snap +201 -0
  200. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  201. package/test/src/__snapshots__/storage_sync.test.ts.snap +1393 -16
  202. package/test/src/connection-report-storage.test.ts +3 -3
  203. package/test/src/setup.ts +1 -1
  204. package/test/src/storage.test.ts +10 -8
  205. package/test/src/storage_compacting.test.ts +146 -46
  206. package/test/src/storage_sync.test.ts +404 -56
  207. package/test/src/util.ts +3 -3
  208. package/test/tsconfig.json +0 -1
  209. package/tsconfig.tsbuildinfo +1 -1
  210. package/dist/storage/implementation/PersistedBatch.d.ts +0 -64
  211. package/dist/storage/implementation/PersistedBatch.js +0 -323
  212. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  213. package/src/storage/implementation/PersistedBatch.ts +0 -401
@@ -2,7 +2,8 @@ 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,
5
+ DO_NOT_LOG,
6
+ Logger,
6
7
  ReplicationAbortedError,
7
8
  ServiceAssertionError
8
9
  } from '@powersync/lib-services-framework';
@@ -10,47 +11,43 @@ import {
10
11
  BroadcastIterable,
11
12
  CHECKPOINT_INVALIDATE_ALL,
12
13
  CheckpointChanges,
13
- deserializeParameterLookup,
14
14
  GetCheckpointChangesOptions,
15
15
  InternalOpId,
16
- internalToExternalOpId,
17
16
  maxLsn,
18
17
  mergeAsyncIterables,
19
18
  PopulateChecksumCacheOptions,
20
19
  PopulateChecksumCacheResults,
21
- ProtocolOpId,
22
20
  ReplicationCheckpoint,
23
21
  storage,
24
22
  utils,
25
23
  WatchWriteCheckpointOptions
26
24
  } from '@powersync/service-core';
27
- import { JSONBig } from '@powersync/service-jsonbig';
28
25
  import { HydratedSyncRules, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
29
26
  import * as bson from 'bson';
30
27
  import { LRUCache } from 'lru-cache';
31
28
  import * as timers from 'timers/promises';
32
- import { idPrefixFilter, mapOpEntry, readSingleBatch, setSessionSnapshotTime } from '../../utils/util.js';
29
+ import { retryOnMongoMaxTimeMSExpired } from '../../utils/util.js';
33
30
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
34
- import { PowerSyncMongo } from './db.js';
35
- import {
36
- BucketDataDocument,
37
- BucketDataKey,
38
- BucketStateDocument,
39
- SourceKey,
40
- SourceTableDocument,
41
- StorageConfig
42
- } from './models.js';
43
- import { MongoBucketBatch } from './MongoBucketBatch.js';
31
+ import { MongoSyncBucketStorageContext } from './common/MongoSyncBucketStorageContext.js';
32
+ import type { VersionedPowerSyncMongo } from './db.js';
33
+ import { CommonSourceTableDocument, StorageConfig } from './models.js';
34
+ import { MongoBucketBatchOptions } from './MongoBucketBatch.js';
44
35
  import { MongoChecksumOptions, MongoChecksums } from './MongoChecksums.js';
45
- import { MongoCompactor } from './MongoCompactor.js';
36
+ import { MongoCompactOptions, MongoCompactor } from './MongoCompactor.js';
46
37
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
38
+ import { MongoPersistedSyncRulesContent } from './MongoPersistedSyncRulesContent.js';
47
39
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
48
40
 
49
41
  export interface MongoSyncBucketStorageOptions {
50
- checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
42
+ checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig' | 'mapping'>;
51
43
  storageConfig: StorageConfig;
52
44
  }
53
45
 
46
+ interface InternalCheckpointChanges extends CheckpointChanges {
47
+ updatedWriteCheckpoints: Map<string, bigint>;
48
+ invalidateWriteCheckpoints: boolean;
49
+ }
50
+
54
51
  /**
55
52
  * Only keep checkpoints around for a minute, before fetching a fresh one.
56
53
  *
@@ -62,41 +59,68 @@ export interface MongoSyncBucketStorageOptions {
62
59
  */
63
60
  const CHECKPOINT_TIMEOUT_MS = 60_000;
64
61
 
65
- export class MongoSyncBucketStorage
62
+ export abstract class MongoSyncBucketStorage
66
63
  extends BaseObserver<storage.SyncRulesBucketStorageListener>
67
64
  implements storage.SyncRulesBucketStorage
68
65
  {
69
- private readonly db: PowerSyncMongo;
66
+ readonly db: VersionedPowerSyncMongo;
67
+ [DO_NOT_LOG] = true;
68
+
70
69
  readonly checksums: MongoChecksums;
71
70
 
72
71
  private parsedSyncRulesCache: { parsed: HydratedSyncRules; options: storage.ParseSyncRulesOptions } | undefined;
73
72
  private writeCheckpointAPI: MongoWriteCheckpointAPI;
73
+ public readonly logger: Logger;
74
+ #storageInitialized = false;
74
75
 
75
76
  constructor(
76
77
  public readonly factory: MongoBucketStorage,
77
78
  public readonly group_id: number,
78
- private readonly sync_rules: storage.PersistedSyncRulesContent,
79
+ protected readonly sync_rules: MongoPersistedSyncRulesContent,
79
80
  public readonly slot_name: string,
80
81
  writeCheckpointMode: storage.WriteCheckpointMode | undefined,
81
82
  options: MongoSyncBucketStorageOptions
82
83
  ) {
83
84
  super();
84
- this.db = factory.db;
85
- this.checksums = new MongoChecksums(this.db, this.group_id, {
86
- ...options.checksumOptions,
87
- storageConfig: options?.storageConfig
88
- });
85
+ this.db = factory.db.versioned(sync_rules.getStorageConfig());
86
+ this.checksums = this.createMongoChecksums(options);
89
87
  this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
90
88
  db: this.db,
91
89
  mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
92
90
  sync_rules_id: group_id
93
91
  });
92
+ this.logger = sync_rules.logger;
94
93
  }
95
94
 
95
+ /**
96
+ * Not for external use - public here for tests only.
97
+ *
98
+ * @internal
99
+ */
100
+ abstract createMongoCompactor(options: MongoCompactOptions): MongoCompactor;
101
+
102
+ protected abstract createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums;
103
+ protected abstract createMongoParameterCompactor(
104
+ checkpoint: InternalOpId,
105
+ options: storage.CompactOptions
106
+ ): MongoParameterCompactor;
107
+
96
108
  get writeCheckpointMode() {
97
109
  return this.writeCheckpointAPI.writeCheckpointMode;
98
110
  }
99
111
 
112
+ get mapping() {
113
+ return this.sync_rules.mapping;
114
+ }
115
+
116
+ protected get versionContext(): MongoSyncBucketStorageContext {
117
+ return {
118
+ db: this.db,
119
+ group_id: this.group_id,
120
+ mapping: this.mapping
121
+ };
122
+ }
123
+
100
124
  setWriteCheckpointMode(mode: storage.WriteCheckpointMode): void {
101
125
  this.writeCheckpointAPI.setWriteCheckpointMode(mode);
102
126
  }
@@ -114,10 +138,6 @@ export class MongoSyncBucketStorage
114
138
 
115
139
  getParsedSyncRules(options: storage.ParseSyncRulesOptions): HydratedSyncRules {
116
140
  const { parsed, options: cachedOptions } = this.parsedSyncRulesCache ?? {};
117
- /**
118
- * Check if the cached sync rules, if present, had the same options.
119
- * Parse sync rules if the options are different or if there is no cached value.
120
- */
121
141
  if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
122
142
  this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncRules(), options };
123
143
  }
@@ -139,26 +159,15 @@ export class MongoSyncBucketStorage
139
159
  }
140
160
  );
141
161
  if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
142
- // Sync rules not active - return null
143
162
  return null;
144
163
  }
145
164
 
146
- // Specifically using operationTime instead of clusterTime
147
- // There are 3 fields in the response:
148
- // 1. operationTime, not exposed for snapshot sessions (used for causal consistency)
149
- // 2. clusterTime (used for connection management)
150
- // 3. atClusterTime, which is session.snapshotTime
151
- // We use atClusterTime, to match the driver's internal snapshot handling.
152
- // There are cases where clusterTime > operationTime and atClusterTime,
153
- // which could cause snapshot queries using this as the snapshotTime to timeout.
154
- // This was specifically observed on MongoDB 6.0 and 7.0.
155
165
  const snapshotTime = (session as any).snapshotTime as bson.Timestamp | undefined;
156
166
  if (snapshotTime == null) {
157
167
  throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
158
168
  }
159
169
  return new MongoReplicationCheckpoint(
160
170
  this,
161
- // null/0n is a valid checkpoint in some cases, for example if the initial snapshot was empty
162
171
  doc.last_checkpoint ?? 0n,
163
172
  doc.last_checkpoint_lsn ?? null,
164
173
  snapshotTime
@@ -166,10 +175,23 @@ export class MongoSyncBucketStorage
166
175
  });
167
176
  }
168
177
 
169
- async startBatch(
170
- options: storage.StartBatchOptions,
171
- callback: (batch: storage.BucketStorageBatch) => Promise<void>
172
- ): Promise<storage.FlushedResult | null> {
178
+ protected abstract initializeVersionStorage(): Promise<void>;
179
+
180
+ private async initializeStorage() {
181
+ if (this.#storageInitialized) {
182
+ return;
183
+ }
184
+
185
+ await this.db.initializeStreamStorage(this.group_id);
186
+ await this.initializeVersionStorage();
187
+ this.#storageInitialized = true;
188
+ }
189
+
190
+ protected abstract createWriterImpl(batchOptions: MongoBucketBatchOptions): storage.BucketStorageBatch;
191
+
192
+ async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
193
+ await this.initializeStorage();
194
+
173
195
  const doc = await this.db.sync_rules.findOne(
174
196
  {
175
197
  _id: this.group_id
@@ -178,31 +200,46 @@ export class MongoSyncBucketStorage
178
200
  );
179
201
  const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
180
202
 
181
- await using batch = new MongoBucketBatch({
182
- logger: options.logger,
203
+ const batchOptions: MongoBucketBatchOptions = {
204
+ logger: options.logger ?? this.logger,
183
205
  db: this.db,
184
206
  syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
207
+ mapping: this.sync_rules.mapping,
185
208
  groupId: this.group_id,
186
209
  slotName: this.slot_name,
187
210
  lastCheckpointLsn: checkpoint_lsn,
188
211
  resumeFromLsn: maxLsn(checkpoint_lsn, doc?.snapshot_lsn),
189
- noCheckpointBeforeLsn: doc?.no_checkpoint_before ?? options.zeroLSN,
190
212
  keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
191
213
  storeCurrentData: options.storeCurrentData,
192
214
  skipExistingRows: options.skipExistingRows ?? false,
193
- markRecordUnavailable: options.markRecordUnavailable
194
- });
195
- this.iterateListeners((cb) => cb.batchStarted?.(batch));
196
-
197
- await callback(batch);
198
- await batch.flush();
199
- if (batch.last_flushed_op != null) {
200
- return { flushed_op: batch.last_flushed_op };
201
- } else {
202
- return null;
203
- }
215
+ markRecordUnavailable: options.markRecordUnavailable,
216
+ tracer: options.tracer
217
+ };
218
+ const writer = this.createWriterImpl(batchOptions);
219
+ this.iterateListeners((cb) => cb.batchStarted?.(writer));
220
+ return writer;
221
+ }
222
+
223
+ async startBatch(
224
+ options: storage.CreateWriterOptions,
225
+ callback: (batch: storage.BucketStorageBatch) => Promise<void>
226
+ ): Promise<storage.FlushedResult | null> {
227
+ await using writer = await this.createWriter(options);
228
+ await callback(writer);
229
+ await writer.flush();
230
+ return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
204
231
  }
205
232
 
233
+ protected abstract sourceTableBaseId(): Partial<CommonSourceTableDocument>;
234
+
235
+ protected abstract augmentCreatedSourceTableDocument(
236
+ createDoc: CommonSourceTableDocument,
237
+ options: storage.ResolveTableOptions,
238
+ candidateSourceTable: storage.SourceTable
239
+ ): void;
240
+
241
+ protected abstract initializeResolvedSourceRecords(sourceTableId: bson.ObjectId): Promise<void>;
242
+
206
243
  async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
207
244
  const { group_id, connection_id, connection_tag, entity_descriptor } = options;
208
245
 
@@ -214,23 +251,36 @@ export class MongoSyncBucketStorage
214
251
  type_oid: column.typeId
215
252
  }));
216
253
  let result: storage.ResolveTableResult | null = null;
254
+ let initializeSourceRecordsFor: bson.ObjectId | null = null;
255
+
256
+ const baseId = this.sourceTableBaseId();
217
257
  await this.db.client.withSession(async (session) => {
218
- const col = this.db.source_tables;
219
- let filter: Partial<SourceTableDocument> = {
220
- group_id: group_id,
258
+ const col = this.db.commonSourceTables(group_id);
259
+ let filter: Partial<CommonSourceTableDocument> = {
260
+ ...baseId,
221
261
  connection_id: connection_id,
222
262
  schema_name: schema,
223
263
  table_name: name,
224
264
  replica_id_columns2: normalizedReplicaIdColumns
225
265
  };
266
+
226
267
  if (objectId != null) {
227
268
  filter.relation_id = objectId;
228
269
  }
229
270
  let doc = await col.findOne(filter, { session });
230
271
  if (doc == null) {
231
- doc = {
232
- _id: new bson.ObjectId(),
233
- group_id: group_id,
272
+ const candidateSourceTable = new storage.SourceTable({
273
+ id: new bson.ObjectId(),
274
+ connectionTag: connection_tag,
275
+ objectId: objectId,
276
+ schema: schema,
277
+ name: name,
278
+ replicaIdColumns: replicaIdColumns,
279
+ snapshotComplete: false
280
+ });
281
+ const createDoc: CommonSourceTableDocument = {
282
+ _id: candidateSourceTable.id as bson.ObjectId,
283
+ ...(baseId as any),
234
284
  connection_id: connection_id,
235
285
  relation_id: objectId,
236
286
  schema_name: schema,
@@ -240,8 +290,11 @@ export class MongoSyncBucketStorage
240
290
  snapshot_done: false,
241
291
  snapshot_status: undefined
242
292
  };
293
+ this.augmentCreatedSourceTableDocument(createDoc, options, candidateSourceTable);
294
+ doc = createDoc;
243
295
 
244
296
  await col.insertOne(doc, { session });
297
+ initializeSourceRecordsFor = doc._id;
245
298
  }
246
299
  const sourceTable = new storage.SourceTable({
247
300
  id: doc._id,
@@ -265,16 +318,14 @@ export class MongoSyncBucketStorage
265
318
  };
266
319
 
267
320
  let dropTables: storage.SourceTable[] = [];
268
- // Detect tables that are either renamed, or have different replica_id_columns
269
321
  let truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
270
322
  if (objectId != null) {
271
- // Only detect renames if the source uses relation ids.
272
323
  truncateFilter.push({ relation_id: objectId });
273
324
  }
274
325
  const truncate = await col
275
326
  .find(
276
327
  {
277
- group_id: group_id,
328
+ ...baseId,
278
329
  connection_id: connection_id,
279
330
  _id: { $ne: doc._id },
280
331
  $or: truncateFilter
@@ -301,225 +352,44 @@ export class MongoSyncBucketStorage
301
352
  dropTables: dropTables
302
353
  };
303
354
  });
355
+ if (initializeSourceRecordsFor != null) {
356
+ await this.initializeResolvedSourceRecords(initializeSourceRecordsFor);
357
+ }
304
358
  return result!;
305
359
  }
306
360
 
361
+ protected abstract getParameterSetsImpl(
362
+ checkpoint: MongoReplicationCheckpoint,
363
+ lookups: ScopedParameterLookup[],
364
+ limit: number
365
+ ): Promise<SqliteJsonRow[]>;
366
+
307
367
  async getParameterSets(
308
368
  checkpoint: MongoReplicationCheckpoint,
309
- lookups: ScopedParameterLookup[]
369
+ lookups: ScopedParameterLookup[],
370
+ limit: number
310
371
  ): Promise<SqliteJsonRow[]> {
311
- return this.db.client.withSession({ snapshot: true }, async (session) => {
312
- // Set the session's snapshot time to the checkpoint's snapshot time.
313
- // An alternative would be to create the session when the checkpoint is created, but managing
314
- // the session lifetime would become more complex.
315
- // Starting and ending sessions are cheap (synchronous when no transactions are used),
316
- // so this should be fine.
317
- // This is a roundabout way of setting {readConcern: {atClusterTime: clusterTime}}, since
318
- // that is not exposed directly by the driver.
319
- // Future versions of the driver may change the snapshotTime behavior, so we need tests to
320
- // validate that this works as expected. We test this in the compacting tests.
321
- setSessionSnapshotTime(session, checkpoint.snapshotTime);
322
- const lookupFilter = lookups.map((lookup) => {
323
- return storage.serializeLookup(lookup);
324
- });
325
- // This query does not use indexes super efficiently, apart from the lookup filter.
326
- // From some experimentation I could do individual lookups more efficient using an index
327
- // on {'key.g': 1, lookup: 1, 'key.t': 1, 'key.k': 1, _id: -1},
328
- // but could not do the same using $group.
329
- // For now, just rely on compacting to remove extraneous data.
330
- // For a description of the data format, see the `/docs/parameters-lookups.md` file.
331
- const rows = await this.db.bucket_parameters
332
- .aggregate(
333
- [
334
- {
335
- $match: {
336
- 'key.g': this.group_id,
337
- lookup: { $in: lookupFilter },
338
- _id: { $lte: checkpoint.checkpoint }
339
- }
340
- },
341
- {
342
- $sort: {
343
- _id: -1
344
- }
345
- },
346
- {
347
- $group: {
348
- _id: { key: '$key', lookup: '$lookup' },
349
- bucket_parameters: {
350
- $first: '$bucket_parameters'
351
- }
352
- }
353
- }
354
- ],
355
- {
356
- session,
357
- readConcern: 'snapshot',
358
- // Limit the time for the operation to complete, to avoid getting connection timeouts
359
- maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
360
- }
361
- )
362
- .toArray()
363
- .catch((e) => {
364
- throw lib_mongo.mapQueryError(e, 'while evaluating parameter queries');
365
- });
366
- const groupedParameters = rows.map((row) => {
367
- return row.bucket_parameters;
368
- });
369
- return groupedParameters.flat();
370
- });
372
+ return this.getParameterSetsImpl(checkpoint, lookups, limit);
371
373
  }
372
374
 
375
+ protected abstract getBucketDataBatchImpl(
376
+ checkpoint: utils.InternalOpId,
377
+ dataBuckets: storage.BucketDataRequest[],
378
+ options?: storage.BucketDataBatchOptions
379
+ ): AsyncIterable<storage.SyncBucketDataChunk>;
380
+
373
381
  async *getBucketDataBatch(
374
382
  checkpoint: utils.InternalOpId,
375
- dataBuckets: Map<string, InternalOpId>,
383
+ dataBuckets: storage.BucketDataRequest[],
376
384
  options?: storage.BucketDataBatchOptions
377
385
  ): AsyncIterable<storage.SyncBucketDataChunk> {
378
- if (dataBuckets.size == 0) {
379
- return;
380
- }
381
- let filters: mongo.Filter<BucketDataDocument>[] = [];
382
-
383
- if (checkpoint == null) {
384
- throw new ServiceAssertionError('checkpoint is null');
385
- }
386
- const end = checkpoint;
387
- for (let [name, start] of dataBuckets.entries()) {
388
- filters.push({
389
- _id: {
390
- $gt: {
391
- g: this.group_id,
392
- b: name,
393
- o: start
394
- },
395
- $lte: {
396
- g: this.group_id,
397
- b: name,
398
- o: end as any
399
- }
400
- }
401
- });
402
- }
403
-
404
- // Internal naming:
405
- // We do a query for one "batch", which may consist of multiple "chunks".
406
- // Each chunk is limited to single bucket, and is limited in length and size.
407
- // There are also overall batch length and size limits.
408
-
409
- const batchLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
410
- const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
411
-
412
- const cursor = this.db.bucket_data.find(
413
- {
414
- $or: filters
415
- },
416
- {
417
- session: undefined,
418
- sort: { _id: 1 },
419
- limit: batchLimit,
420
- // Increase batch size above the default 101, so that we can fill an entire batch in
421
- // one go.
422
- // batchSize is 1 more than limit to auto-close the cursor.
423
- // See https://github.com/mongodb/node-mongodb-native/pull/4580
424
- batchSize: batchLimit + 1,
425
- // Raw mode is returns an array of Buffer instead of parsed documents.
426
- // We use it so that:
427
- // 1. We can calculate the document size accurately without serializing again.
428
- // 2. We can delay parsing the results until it's needed.
429
- // We manually use bson.deserialize below
430
- raw: true,
431
-
432
- // Limit the time for the operation to complete, to avoid getting connection timeouts
433
- maxTimeMS: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS
434
- }
435
- ) as unknown as mongo.FindCursor<Buffer>;
436
-
437
- // We want to limit results to a single batch to avoid high memory usage.
438
- // This approach uses MongoDB's batch limits to limit the data here, which limits
439
- // to the lower of the batch count and size limits.
440
- // This is similar to using `singleBatch: true` in the find options, but allows
441
- // detecting "hasMore".
442
- let { data, hasMore: batchHasMore } = await readSingleBatch(cursor).catch((e) => {
443
- throw lib_mongo.mapQueryError(e, 'while reading bucket data');
444
- });
445
- if (data.length == batchLimit) {
446
- // Limit reached - could have more data, despite the cursor being drained.
447
- batchHasMore = true;
448
- }
449
-
450
- let chunkSizeBytes = 0;
451
- let currentChunk: utils.SyncBucketData | null = null;
452
- let targetOp: InternalOpId | null = null;
453
-
454
- // Ordered by _id, meaning buckets are grouped together
455
- for (let rawData of data) {
456
- const row = bson.deserialize(rawData, storage.BSON_DESERIALIZE_INTERNAL_OPTIONS) as BucketDataDocument;
457
- const bucket = row._id.b;
458
-
459
- if (currentChunk == null || currentChunk.bucket != bucket || chunkSizeBytes >= chunkSizeLimitBytes) {
460
- // We need to start a new chunk
461
- let start: ProtocolOpId | undefined = undefined;
462
- if (currentChunk != null) {
463
- // There is an existing chunk we need to yield
464
- if (currentChunk.bucket == bucket) {
465
- // Current and new chunk have the same bucket, so need has_more on the current one.
466
- // If currentChunk.bucket != bucket, then we reached the end of the previous bucket,
467
- // and has_more = false in that case.
468
- currentChunk.has_more = true;
469
- start = currentChunk.next_after;
470
- }
471
-
472
- const yieldChunk = currentChunk;
473
- currentChunk = null;
474
- chunkSizeBytes = 0;
475
- yield { chunkData: yieldChunk, targetOp: targetOp };
476
- targetOp = null;
477
- }
478
-
479
- if (start == null) {
480
- const startOpId = dataBuckets.get(bucket);
481
- if (startOpId == null) {
482
- throw new ServiceAssertionError(`data for unexpected bucket: ${bucket}`);
483
- }
484
- start = internalToExternalOpId(startOpId);
485
- }
486
- currentChunk = {
487
- bucket,
488
- after: start,
489
- has_more: false,
490
- data: [],
491
- next_after: start
492
- };
493
- targetOp = null;
494
- }
495
-
496
- const entry = mapOpEntry(row);
497
-
498
- if (row.target_op != null) {
499
- // MOVE, CLEAR
500
- if (targetOp == null || row.target_op > targetOp) {
501
- targetOp = row.target_op;
502
- }
503
- }
504
-
505
- currentChunk.data.push(entry);
506
- currentChunk.next_after = entry.op_id;
507
-
508
- chunkSizeBytes += rawData.byteLength;
509
- }
510
-
511
- if (currentChunk != null) {
512
- const yieldChunk = currentChunk;
513
- currentChunk = null;
514
- // This is the final chunk in the batch.
515
- // There may be more data if and only if the batch we retrieved isn't complete.
516
- yieldChunk.has_more = batchHasMore;
517
- yield { chunkData: yieldChunk, targetOp: targetOp };
518
- targetOp = null;
519
- }
386
+ yield* this.getBucketDataBatchImpl(checkpoint, dataBuckets, options);
520
387
  }
521
388
 
522
- async getChecksums(checkpoint: utils.InternalOpId, buckets: string[]): Promise<utils.ChecksumMap> {
389
+ async getChecksums(
390
+ checkpoint: utils.InternalOpId,
391
+ buckets: storage.BucketChecksumRequest[]
392
+ ): Promise<utils.ChecksumMap> {
523
393
  return this.checksums.getChecksums(checkpoint, buckets);
524
394
  }
525
395
 
@@ -528,7 +398,6 @@ export class MongoSyncBucketStorage
528
398
  }
529
399
 
530
400
  async terminate(options?: storage.TerminateOptions) {
531
- // Default is to clear the storage except when explicitly requested not to.
532
401
  if (!options || options?.clearStorage) {
533
402
  await this.clear(options);
534
403
  }
@@ -562,7 +431,7 @@ export class MongoSyncBucketStorage
562
431
  }
563
432
  );
564
433
  if (doc == null) {
565
- throw new ServiceAssertionError('Cannot find sync rules status');
434
+ throw new ServiceAssertionError('Cannot find replication stream status');
566
435
  }
567
436
 
568
437
  return {
@@ -573,32 +442,22 @@ export class MongoSyncBucketStorage
573
442
  };
574
443
  }
575
444
 
445
+ protected abstract clearBucketData(signal?: AbortSignal): Promise<void>;
446
+
447
+ protected abstract clearParameterIndexes(signal?: AbortSignal): Promise<void>;
448
+
449
+ protected abstract clearSourceRecords(signal?: AbortSignal): Promise<void>;
450
+
451
+ protected abstract clearBucketState(signal?: AbortSignal): Promise<void>;
452
+
453
+ protected abstract clearSourceTables(signal?: AbortSignal): Promise<void>;
454
+
576
455
  async clear(options?: storage.ClearStorageOptions): Promise<void> {
577
- while (true) {
578
- if (options?.signal?.aborted) {
579
- throw new ReplicationAbortedError('Aborted clearing data');
580
- }
581
- try {
582
- await this.clearIteration();
456
+ const signal = options?.signal;
583
457
 
584
- logger.info(`${this.slot_name} Done clearing data`);
585
- return;
586
- } catch (e: unknown) {
587
- if (lib_mongo.isMongoServerError(e) && e.codeName == 'MaxTimeMSExpired') {
588
- logger.info(
589
- `${this.slot_name} Cleared batch of data in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`
590
- );
591
- await timers.setTimeout(lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5);
592
- } else {
593
- throw e;
594
- }
595
- }
458
+ if (signal?.aborted) {
459
+ throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
596
460
  }
597
- }
598
-
599
- private async clearIteration(): Promise<void> {
600
- // Individual operations here may time out with the maxTimeMS option.
601
- // It is expected to still make progress, and continue on the next try.
602
461
 
603
462
  await this.db.sync_rules.updateOne(
604
463
  {
@@ -618,39 +477,31 @@ export class MongoSyncBucketStorage
618
477
  },
619
478
  { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
620
479
  );
621
- await this.db.bucket_data.deleteMany(
622
- {
623
- _id: idPrefixFilter<BucketDataKey>({ g: this.group_id }, ['b', 'o'])
624
- },
625
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
626
- );
627
- await this.db.bucket_parameters.deleteMany(
628
- {
629
- 'key.g': this.group_id
630
- },
631
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
632
- );
633
480
 
634
- await this.db.current_data.deleteMany(
635
- {
636
- _id: idPrefixFilter<SourceKey>({ g: this.group_id }, ['t', 'k'])
637
- },
638
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
639
- );
481
+ await this.clearBucketData(signal);
482
+ await this.clearParameterIndexes(signal);
483
+ await this.clearSourceRecords(signal);
484
+ await this.clearBucketState(signal);
485
+ await this.clearSourceTables(signal);
640
486
 
641
- await this.db.bucket_state.deleteMany(
642
- {
643
- _id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.group_id }, ['b'])
644
- },
645
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
646
- );
487
+ this.#storageInitialized = false;
488
+ }
647
489
 
648
- await this.db.source_tables.deleteMany(
649
- {
650
- group_id: this.group_id
651
- },
652
- { maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
653
- );
490
+ protected async clearDeleteMany(
491
+ label: string,
492
+ operation: () => Promise<mongo.DeleteResult>,
493
+ signal?: AbortSignal
494
+ ): Promise<void> {
495
+ await retryOnMongoMaxTimeMSExpired(operation, {
496
+ signal,
497
+ abortMessage: 'Aborted clearing data',
498
+ retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5,
499
+ onRetry: () => {
500
+ this.logger.info(
501
+ `Cleared batch of ${label} in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`
502
+ );
503
+ }
504
+ });
654
505
  }
655
506
 
656
507
  async reportError(e: any): Promise<void> {
@@ -674,100 +525,68 @@ export class MongoSyncBucketStorage
674
525
  const checkpoint = await this.getCheckpointInternal();
675
526
  maxOpId = checkpoint?.checkpoint ?? undefined;
676
527
  }
677
- await new MongoCompactor(this, this.db, { ...options, maxOpId }).compact();
528
+ await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
678
529
 
679
530
  if (maxOpId != null && options?.compactParameterData) {
680
- await new MongoParameterCompactor(this.db, this.group_id, maxOpId, options).compact();
531
+ await this.createMongoParameterCompactor(maxOpId, options).compact();
681
532
  }
682
533
  }
683
534
 
684
535
  async populatePersistentChecksumCache(options: PopulateChecksumCacheOptions): Promise<PopulateChecksumCacheResults> {
685
- logger.info(`Populating persistent checksum cache...`);
536
+ this.logger.info(`Populating persistent checksum cache...`);
686
537
  const start = Date.now();
687
- // We do a minimal compact here.
688
- // We can optimize this in the future.
689
- const compactor = new MongoCompactor(this, this.db, {
538
+ const compactor = this.createMongoCompactor({
690
539
  ...options,
691
- // Don't track updates for MOVE compacting
692
- memoryLimitMB: 0
540
+ memoryLimitMB: 0,
541
+ logger: this.logger
693
542
  });
694
543
 
695
544
  const result = await compactor.populateChecksums({
696
- // There are cases with millions of small buckets, in which case it can take very long to
697
- // populate the checksums, with minimal benefit. We skip the small buckets here.
698
545
  minBucketChanges: options.minBucketChanges ?? 10
699
546
  });
700
547
  const duration = Date.now() - start;
701
- logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
548
+ this.logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
702
549
  return result;
703
550
  }
704
551
 
705
- /**
706
- * Instance-wide watch on the latest available checkpoint (op_id + lsn).
707
- */
708
552
  private async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<ReplicationCheckpoint> {
709
553
  if (signal.aborted) {
710
554
  return;
711
555
  }
712
556
 
713
- // If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS) before we get another checkpoint,
714
- // to avoid stale checkpoint snapshots. This is what checkpointTimeoutStream() is for.
715
- // Essentially, even if there are no actual checkpoint changes, we want a new snapshotTime every minute or so,
716
- // to ensure that any new clients connecting will get a valid snapshotTime.
717
557
  const stream = mergeAsyncIterables(
718
558
  [this.checkpointChangesStream(signal), this.checkpointTimeoutStream(signal)],
719
559
  signal
720
560
  );
721
561
 
722
- // We only watch changes to the active sync rules.
723
- // If it changes to inactive, we abort and restart with the new sync rules.
724
562
  for await (const _ of stream) {
725
563
  if (signal.aborted) {
726
- // Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
727
564
  break;
728
565
  }
729
566
 
730
567
  const op = await this.getCheckpointInternal();
731
568
  if (op == null) {
732
- // Sync rules have changed - abort and restart.
733
- // We do a soft close of the stream here - no error
734
569
  break;
735
570
  }
736
571
 
737
- // Previously, we only yielded when the checkpoint or lsn changed.
738
- // However, we always want to use the latest snapshotTime, so we skip that filtering here.
739
- // That filtering could be added in the per-user streams if needed, but in general the capped collection
740
- // should already only contain useful changes in most cases.
741
572
  yield op;
742
573
  }
743
574
  }
744
575
 
745
- // Nothing is done here until a subscriber starts to iterate
746
576
  private readonly sharedIter = new BroadcastIterable((signal) => {
747
577
  return this.watchActiveCheckpoint(signal);
748
578
  });
749
579
 
750
- /**
751
- * User-specific watch on the latest checkpoint and/or write checkpoint.
752
- */
753
580
  async *watchCheckpointChanges(options: WatchWriteCheckpointOptions): AsyncIterable<storage.StorageCheckpointUpdate> {
754
581
  let lastCheckpoint: ReplicationCheckpoint | null = null;
755
582
 
756
583
  const iter = this.sharedIter[Symbol.asyncIterator](options.signal);
757
584
 
758
585
  let writeCheckpoint: bigint | null = null;
759
- // true if we queried the initial write checkpoint, even if it doesn't exist
760
586
  let queriedInitialWriteCheckpoint = false;
761
587
 
762
588
  for await (const nextCheckpoint of iter) {
763
- // lsn changes are not important by itself.
764
- // What is important is:
765
- // 1. checkpoint (op_id) changes.
766
- // 2. write checkpoint changes for the specific user
767
-
768
589
  if (nextCheckpoint.lsn != null && !queriedInitialWriteCheckpoint) {
769
- // Lookup the first write checkpoint for the user when we can.
770
- // There will not actually be one in all cases.
771
590
  writeCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
772
591
  sync_rules_id: this.group_id,
773
592
  user_id: options.user_id,
@@ -783,15 +602,11 @@ export class MongoSyncBucketStorage
783
602
  lastCheckpoint.checkpoint == nextCheckpoint.checkpoint &&
784
603
  lastCheckpoint.lsn == nextCheckpoint.lsn
785
604
  ) {
786
- // No change - wait for next one
787
- // In some cases, many LSNs may be produced in a short time.
788
- // Add a delay to throttle the loop a bit.
789
605
  await timers.setTimeout(20 + 10 * Math.random());
790
606
  continue;
791
607
  }
792
608
 
793
609
  if (lastCheckpoint == null) {
794
- // First message for this stream - "INVALIDATE_ALL" means it will lookup all data
795
610
  yield {
796
611
  base: nextCheckpoint,
797
612
  writeCheckpoint,
@@ -805,8 +620,6 @@ export class MongoSyncBucketStorage
805
620
 
806
621
  let updatedWriteCheckpoint = updates.updatedWriteCheckpoints.get(options.user_id) ?? null;
807
622
  if (updates.invalidateWriteCheckpoints) {
808
- // Invalidated means there were too many updates to track the individual ones,
809
- // so we switch to "polling" (querying directly in each stream).
810
623
  updatedWriteCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
811
624
  sync_rules_id: this.group_id,
812
625
  user_id: options.user_id,
@@ -817,8 +630,6 @@ export class MongoSyncBucketStorage
817
630
  }
818
631
  if (updatedWriteCheckpoint != null && (writeCheckpoint == null || updatedWriteCheckpoint > writeCheckpoint)) {
819
632
  writeCheckpoint = updatedWriteCheckpoint;
820
- // If it happened that we haven't queried a write checkpoint at this point,
821
- // then we don't need to anymore, since we got an updated one.
822
633
  queriedInitialWriteCheckpoint = true;
823
634
  }
824
635
 
@@ -838,12 +649,6 @@ export class MongoSyncBucketStorage
838
649
  }
839
650
  }
840
651
 
841
- /**
842
- * This watches the checkpoint_events capped collection for new documents inserted,
843
- * and yields whenever one or more documents are inserted.
844
- *
845
- * The actual checkpoint must be queried on the sync_rules collection after this.
846
- */
847
652
  private async *checkpointChangesStream(signal: AbortSignal): AsyncGenerator<void> {
848
653
  if (signal.aborted) {
849
654
  return;
@@ -862,17 +667,13 @@ export class MongoSyncBucketStorage
862
667
  cursor.close().catch(() => {});
863
668
  });
864
669
 
865
- // Yield once on start, regardless of whether there are documents in the cursor.
866
- // This is to ensure that the first iteration of the generator yields immediately.
867
670
  yield;
868
671
 
869
672
  try {
870
673
  while (!signal.aborted) {
871
674
  const doc = await cursor.tryNext().catch((e) => {
872
675
  if (lib_mongo.isMongoServerError(e) && e.codeName === 'CappedPositionLost') {
873
- // Cursor position lost, potentially due to a high rate of notifications
874
676
  cursor = query();
875
- // Treat as an event found, before querying the new cursor again
876
677
  return {};
877
678
  } else {
878
679
  return Promise.reject(e);
@@ -881,8 +682,6 @@ export class MongoSyncBucketStorage
881
682
  if (cursor.closed) {
882
683
  return;
883
684
  }
884
- // Skip buffered documents, if any. We don't care about the contents,
885
- // we only want to know when new documents are inserted.
886
685
  cursor.readBufferedDocuments();
887
686
  if (doc != null) {
888
687
  yield;
@@ -904,7 +703,6 @@ export class MongoSyncBucketStorage
904
703
  await timers.setTimeout(CHECKPOINT_TIMEOUT_MS, undefined, { signal });
905
704
  } catch (e) {
906
705
  if (e.name == 'AbortError') {
907
- // This is how we typically abort this stream, when all listeners are done
908
706
  return;
909
707
  }
910
708
  throw e;
@@ -916,94 +714,37 @@ export class MongoSyncBucketStorage
916
714
  }
917
715
  }
918
716
 
717
+ protected abstract getDataBucketChangesImpl(
718
+ options: GetCheckpointChangesOptions
719
+ ): Promise<Pick<CheckpointChanges, 'updatedDataBuckets' | 'invalidateDataBuckets'>>;
720
+
919
721
  private async getDataBucketChanges(
920
722
  options: GetCheckpointChangesOptions
921
723
  ): Promise<Pick<CheckpointChanges, 'updatedDataBuckets' | 'invalidateDataBuckets'>> {
922
- const limit = 1000;
923
- const bucketStateUpdates = await this.db.bucket_state
924
- .find(
925
- {
926
- // We have an index on (_id.g, last_op).
927
- '_id.g': this.group_id,
928
- last_op: { $gt: options.lastCheckpoint.checkpoint }
929
- },
930
- {
931
- projection: {
932
- '_id.b': 1
933
- },
934
- limit: limit + 1,
935
- // batchSize is 1 more than limit to auto-close the cursor.
936
- // See https://github.com/mongodb/node-mongodb-native/pull/4580
937
- batchSize: limit + 2,
938
- singleBatch: true
939
- }
940
- )
941
- .toArray();
942
-
943
- const buckets = bucketStateUpdates.map((doc) => doc._id.b);
944
- const invalidateDataBuckets = buckets.length > limit;
945
-
946
- return {
947
- invalidateDataBuckets: invalidateDataBuckets,
948
- updatedDataBuckets: invalidateDataBuckets ? new Set<string>() : new Set(buckets)
949
- };
724
+ return this.getDataBucketChangesImpl(options);
950
725
  }
951
726
 
727
+ protected abstract getParameterBucketChangesImpl(
728
+ options: GetCheckpointChangesOptions
729
+ ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>>;
730
+
952
731
  private async getParameterBucketChanges(
953
732
  options: GetCheckpointChangesOptions
954
733
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
955
- const limit = 1000;
956
- const parameterUpdates = await this.db.bucket_parameters
957
- .find(
958
- {
959
- _id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint },
960
- 'key.g': this.group_id
961
- },
962
- {
963
- projection: {
964
- lookup: 1
965
- },
966
- limit: limit + 1,
967
- // batchSize is 1 more than limit to auto-close the cursor.
968
- // See https://github.com/mongodb/node-mongodb-native/pull/4580
969
- batchSize: limit + 2,
970
- singleBatch: true
971
- }
972
- )
973
- .toArray();
974
- const invalidateParameterUpdates = parameterUpdates.length > limit;
975
-
976
- return {
977
- invalidateParameterBuckets: invalidateParameterUpdates,
978
- updatedParameterLookups: invalidateParameterUpdates
979
- ? new Set<string>()
980
- : new Set<string>(parameterUpdates.map((p) => JSONBig.stringify(deserializeParameterLookup(p.lookup))))
981
- };
734
+ return this.getParameterBucketChangesImpl(options);
982
735
  }
983
736
 
984
- // If we processed all connections together for each checkpoint, we could do a single lookup for all connections.
985
- // In practice, specific connections may fall behind. So instead, we just cache the results of each specific lookup.
986
- // TODO (later):
987
- // We can optimize this by implementing it like ChecksumCache: We can use partial cache results to do
988
- // more efficient lookups in some cases.
989
737
  private checkpointChangesCache = new LRUCache<
990
738
  string,
991
739
  InternalCheckpointChanges,
992
740
  { options: GetCheckpointChangesOptions }
993
741
  >({
994
- // Limit to 50 cache entries, or 10MB, whichever comes first.
995
- // Some rough calculations:
996
- // If we process 10 checkpoints per second, and a connection may be 2 seconds behind, we could have
997
- // up to 20 relevant checkpoints. That gives us 20*20 = 400 potentially-relevant cache entries.
998
- // That is a worst-case scenario, so we don't actually store that many. In real life, the cache keys
999
- // would likely be clustered around a few values, rather than spread over all 400 potential values.
1000
742
  max: 50,
1001
743
  maxSize: 12 * 1024 * 1024,
1002
744
  sizeCalculation: (value: InternalCheckpointChanges) => {
1003
- // Estimate of memory usage
1004
745
  const paramSize = [...value.updatedParameterLookups].reduce<number>((a, b) => a + b.length, 0);
1005
746
  const bucketSize = [...value.updatedDataBuckets].reduce<number>((a, b) => a + b.length, 0);
1006
- const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30; // estiamte for user_id + bigint
747
+ const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
1007
748
  return 100 + paramSize + bucketSize + writeCheckpointSize;
1008
749
  },
1009
750
  fetchMethod: async (_key, _staleValue, options) => {
@@ -1030,21 +771,20 @@ export class MongoSyncBucketStorage
1030
771
  }
1031
772
  }
1032
773
 
1033
- interface InternalCheckpointChanges extends CheckpointChanges {
1034
- updatedWriteCheckpoints: Map<string, bigint>;
1035
- invalidateWriteCheckpoints: boolean;
1036
- }
1037
-
1038
774
  class MongoReplicationCheckpoint implements ReplicationCheckpoint {
775
+ #storage: MongoSyncBucketStorage;
776
+
1039
777
  constructor(
1040
- private storage: MongoSyncBucketStorage,
778
+ storage: MongoSyncBucketStorage,
1041
779
  public readonly checkpoint: InternalOpId,
1042
780
  public readonly lsn: string | null,
1043
781
  public snapshotTime: mongo.Timestamp
1044
- ) {}
782
+ ) {
783
+ this.#storage = storage;
784
+ }
1045
785
 
1046
- async getParameterSets(lookups: ScopedParameterLookup[]): Promise<SqliteJsonRow[]> {
1047
- return this.storage.getParameterSets(this, lookups);
786
+ async getParameterSets(lookups: ScopedParameterLookup[], limit: number): Promise<SqliteJsonRow[]> {
787
+ return this.#storage.getParameterSets(this, lookups, limit);
1048
788
  }
1049
789
  }
1050
790
 
@@ -1052,7 +792,7 @@ class EmptyReplicationCheckpoint implements ReplicationCheckpoint {
1052
792
  readonly checkpoint: InternalOpId = 0n;
1053
793
  readonly lsn: string | null = null;
1054
794
 
1055
- async getParameterSets(lookups: ScopedParameterLookup[]): Promise<SqliteJsonRow[]> {
795
+ async getParameterSets(_lookups: ScopedParameterLookup[]): Promise<SqliteJsonRow[]> {
1056
796
  return [];
1057
797
  }
1058
798
  }