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