@powersync/service-module-mongodb-storage 0.15.4 → 0.16.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 (193) hide show
  1. package/CHANGELOG.md +35 -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 +2 -2
  7. package/dist/storage/MongoBucketStorage.js +47 -34
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -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/MongoBucketBatch.d.ts +16 -14
  13. package/dist/storage/implementation/MongoBucketBatch.js +80 -115
  14. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  15. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  16. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  17. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  18. package/dist/storage/implementation/MongoChecksums.d.ts +28 -17
  19. package/dist/storage/implementation/MongoChecksums.js +13 -72
  20. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  21. package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
  22. package/dist/storage/implementation/MongoCompactor.js +229 -296
  23. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  24. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  25. package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
  26. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  27. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  28. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  29. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  30. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  33. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +47 -29
  34. package/dist/storage/implementation/MongoSyncBucketStorage.js +94 -387
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  36. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
  37. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  38. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  39. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  41. package/dist/storage/implementation/OperationBatch.js +1 -1
  42. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  44. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  47. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  48. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  50. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  51. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  52. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  53. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  54. package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
  55. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  56. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  57. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  58. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  59. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  60. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  61. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  62. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  63. package/dist/storage/implementation/db.d.ts +32 -35
  64. package/dist/storage/implementation/db.js +77 -99
  65. package/dist/storage/implementation/db.js.map +1 -1
  66. package/dist/storage/implementation/models.d.ts +62 -33
  67. package/dist/storage/implementation/models.js +20 -1
  68. package/dist/storage/implementation/models.js.map +1 -1
  69. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
  70. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  71. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  72. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  73. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  74. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  75. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  76. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  77. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  78. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  79. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  80. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  81. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
  82. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
  83. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  84. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
  85. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  86. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  87. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  88. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  89. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  91. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  92. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  93. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  94. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  95. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  96. package/dist/storage/implementation/v1/models.d.ts +34 -0
  97. package/dist/storage/implementation/v1/models.js +37 -0
  98. package/dist/storage/implementation/v1/models.js.map +1 -0
  99. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
  100. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
  101. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  102. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  103. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  104. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  105. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  106. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  107. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  108. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  109. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  110. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  111. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
  112. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  113. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  114. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
  115. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
  116. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  117. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
  118. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  119. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  120. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  121. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  122. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  124. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  125. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  126. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
  127. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
  128. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  129. package/dist/storage/implementation/v3/models.d.ts +43 -0
  130. package/dist/storage/implementation/v3/models.js +34 -0
  131. package/dist/storage/implementation/v3/models.js.map +1 -0
  132. package/dist/storage/storage-index.d.ts +6 -3
  133. package/dist/storage/storage-index.js +6 -3
  134. package/dist/storage/storage-index.js.map +1 -1
  135. package/dist/utils/util.d.ts +10 -3
  136. package/dist/utils/util.js +24 -3
  137. package/dist/utils/util.js.map +1 -1
  138. package/package.json +9 -9
  139. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  140. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +6 -6
  141. package/src/storage/MongoBucketStorage.ts +92 -59
  142. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  143. package/src/storage/implementation/MongoBucketBatch.ts +110 -144
  144. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  145. package/src/storage/implementation/MongoChecksums.ts +52 -75
  146. package/src/storage/implementation/MongoCompactor.ts +374 -404
  147. package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
  148. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  149. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +17 -0
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +181 -455
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +11 -13
  152. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
  153. package/src/storage/implementation/OperationBatch.ts +1 -1
  154. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  155. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  156. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  157. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  158. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  159. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  160. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  161. package/src/storage/implementation/db.ts +105 -129
  162. package/src/storage/implementation/models.ts +82 -36
  163. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  164. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  165. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  166. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  167. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +448 -0
  168. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  169. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  170. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  171. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  172. package/src/storage/implementation/v1/models.ts +84 -0
  173. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  174. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  175. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  176. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  177. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  178. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
  179. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  180. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  181. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  182. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  183. package/src/storage/implementation/v3/models.ts +96 -0
  184. package/src/storage/storage-index.ts +6 -3
  185. package/src/utils/util.ts +34 -5
  186. package/test/src/storage_compacting.test.ts +57 -29
  187. package/test/src/storage_sync.test.ts +351 -5
  188. package/test/tsconfig.json +0 -1
  189. package/tsconfig.tsbuildinfo +1 -1
  190. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  191. package/dist/storage/implementation/PersistedBatch.js +0 -354
  192. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  193. package/src/storage/implementation/PersistedBatch.ts +0 -432
@@ -1,15 +1,10 @@
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';
2
+ import { BaseObserver, DO_NOT_LOG, ReplicationAbortedError, ServiceAssertionError } from '@powersync/lib-services-framework';
3
+ import { BroadcastIterable, CHECKPOINT_INVALIDATE_ALL, maxLsn, mergeAsyncIterables, storage } from '@powersync/service-core';
5
4
  import * as bson from 'bson';
6
5
  import { LRUCache } from 'lru-cache';
7
6
  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';
7
+ import { retryOnMongoMaxTimeMSExpired } from '../../utils/util.js';
13
8
  import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
14
9
  /**
15
10
  * Only keep checkpoints around for a minute, before fetching a fresh one.
@@ -26,11 +21,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
26
21
  group_id;
27
22
  sync_rules;
28
23
  slot_name;
29
- [DO_NOT_LOG] = true;
30
24
  db;
25
+ [DO_NOT_LOG] = true;
31
26
  checksums;
32
27
  parsedSyncRulesCache;
33
28
  writeCheckpointAPI;
29
+ logger;
30
+ #storageInitialized = false;
34
31
  constructor(factory, group_id, sync_rules, slot_name, writeCheckpointMode, options) {
35
32
  super();
36
33
  this.factory = factory;
@@ -38,19 +35,27 @@ export class MongoSyncBucketStorage extends BaseObserver {
38
35
  this.sync_rules = sync_rules;
39
36
  this.slot_name = slot_name;
40
37
  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
- });
38
+ this.checksums = this.createMongoChecksums(options);
45
39
  this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
46
40
  db: this.db,
47
41
  mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
48
42
  sync_rules_id: group_id
49
43
  });
44
+ this.logger = sync_rules.logger;
50
45
  }
51
46
  get writeCheckpointMode() {
52
47
  return this.writeCheckpointAPI.writeCheckpointMode;
53
48
  }
49
+ get mapping() {
50
+ return this.sync_rules.mapping;
51
+ }
52
+ get versionContext() {
53
+ return {
54
+ db: this.db,
55
+ group_id: this.group_id,
56
+ mapping: this.mapping
57
+ };
58
+ }
54
59
  setWriteCheckpointMode(mode) {
55
60
  this.writeCheckpointAPI.setWriteCheckpointMode(mode);
56
61
  }
@@ -65,10 +70,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
65
70
  }
66
71
  getParsedSyncRules(options) {
67
72
  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
73
  if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
73
74
  this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncRules(), options };
74
75
  }
@@ -84,36 +85,34 @@ export class MongoSyncBucketStorage extends BaseObserver {
84
85
  projection: { _id: 1, state: 1, last_checkpoint: 1, last_checkpoint_lsn: 1, snapshot_done: 1 }
85
86
  });
86
87
  if (!doc?.snapshot_done || !['ACTIVE', 'ERRORED'].includes(doc.state)) {
87
- // Sync rules not active - return null
88
88
  return null;
89
89
  }
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
90
  const snapshotTime = session.snapshotTime;
100
91
  if (snapshotTime == null) {
101
92
  throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
102
93
  }
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);
94
+ return new MongoReplicationCheckpoint(this, doc.last_checkpoint ?? 0n, doc.last_checkpoint_lsn ?? null, snapshotTime);
106
95
  });
107
96
  }
97
+ async initializeStorage() {
98
+ if (this.#storageInitialized) {
99
+ return;
100
+ }
101
+ await this.db.initializeStreamStorage(this.group_id);
102
+ await this.initializeVersionStorage();
103
+ this.#storageInitialized = true;
104
+ }
108
105
  async createWriter(options) {
106
+ await this.initializeStorage();
109
107
  const doc = await this.db.sync_rules.findOne({
110
108
  _id: this.group_id
111
109
  }, { projection: { last_checkpoint_lsn: 1, no_checkpoint_before: 1, keepalive_op: 1, snapshot_lsn: 1 } });
112
110
  const checkpoint_lsn = doc?.last_checkpoint_lsn ?? null;
113
- const writer = new MongoBucketBatch({
114
- logger: options.logger,
111
+ const batchOptions = {
112
+ logger: options.logger ?? this.logger,
115
113
  db: this.db,
116
114
  syncRules: this.sync_rules.parsed(options).hydratedSyncRules(),
115
+ mapping: this.sync_rules.mapping,
117
116
  groupId: this.group_id,
118
117
  slotName: this.slot_name,
119
118
  lastCheckpointLsn: checkpoint_lsn,
@@ -121,14 +120,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
121
120
  keepaliveOp: doc?.keepalive_op ? BigInt(doc.keepalive_op) : null,
122
121
  storeCurrentData: options.storeCurrentData,
123
122
  skipExistingRows: options.skipExistingRows ?? false,
124
- markRecordUnavailable: options.markRecordUnavailable
125
- });
123
+ markRecordUnavailable: options.markRecordUnavailable,
124
+ tracer: options.tracer
125
+ };
126
+ const writer = this.createWriterImpl(batchOptions);
126
127
  this.iterateListeners((cb) => cb.batchStarted?.(writer));
127
128
  return writer;
128
129
  }
129
- /**
130
- * @deprecated Use `createWriter()` with `await using` instead.
131
- */
132
130
  async startBatch(options, callback) {
133
131
  await using writer = await this.createWriter(options);
134
132
  await callback(writer);
@@ -144,10 +142,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
144
142
  type_oid: column.typeId
145
143
  }));
146
144
  let result = null;
145
+ let initializeSourceRecordsFor = null;
146
+ const baseId = this.sourceTableBaseId();
147
147
  await this.db.client.withSession(async (session) => {
148
- const col = this.db.source_tables;
148
+ const col = this.db.commonSourceTables(group_id);
149
149
  let filter = {
150
- group_id: group_id,
150
+ ...baseId,
151
151
  connection_id: connection_id,
152
152
  schema_name: schema,
153
153
  table_name: name,
@@ -158,9 +158,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
158
158
  }
159
159
  let doc = await col.findOne(filter, { session });
160
160
  if (doc == null) {
161
- doc = {
162
- _id: new bson.ObjectId(),
163
- group_id: group_id,
161
+ const candidateSourceTable = new storage.SourceTable({
162
+ id: new bson.ObjectId(),
163
+ connectionTag: connection_tag,
164
+ objectId: objectId,
165
+ schema: schema,
166
+ name: name,
167
+ replicaIdColumns: replicaIdColumns,
168
+ snapshotComplete: false
169
+ });
170
+ const createDoc = {
171
+ _id: candidateSourceTable.id,
172
+ ...baseId,
164
173
  connection_id: connection_id,
165
174
  relation_id: objectId,
166
175
  schema_name: schema,
@@ -170,7 +179,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
170
179
  snapshot_done: false,
171
180
  snapshot_status: undefined
172
181
  };
182
+ this.augmentCreatedSourceTableDocument(createDoc, options, candidateSourceTable);
183
+ doc = createDoc;
173
184
  await col.insertOne(doc, { session });
185
+ initializeSourceRecordsFor = doc._id;
174
186
  }
175
187
  const sourceTable = new storage.SourceTable({
176
188
  id: doc._id,
@@ -193,15 +205,13 @@ export class MongoSyncBucketStorage extends BaseObserver {
193
205
  replicatedCount: doc.snapshot_status.replicated_count
194
206
  };
195
207
  let dropTables = [];
196
- // Detect tables that are either renamed, or have different replica_id_columns
197
208
  let truncateFilter = [{ schema_name: schema, table_name: name }];
198
209
  if (objectId != null) {
199
- // Only detect renames if the source uses relation ids.
200
210
  truncateFilter.push({ relation_id: objectId });
201
211
  }
202
212
  const truncate = await col
203
213
  .find({
204
- group_id: group_id,
214
+ ...baseId,
205
215
  connection_id: connection_id,
206
216
  _id: { $ne: doc._id },
207
217
  $or: truncateFilter
@@ -221,192 +231,16 @@ export class MongoSyncBucketStorage extends BaseObserver {
221
231
  dropTables: dropTables
222
232
  };
223
233
  });
234
+ if (initializeSourceRecordsFor != null) {
235
+ await this.initializeResolvedSourceRecords(initializeSourceRecordsFor);
236
+ }
224
237
  return result;
225
238
  }
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
- });
239
+ async getParameterSets(checkpoint, lookups, limit) {
240
+ return this.getParameterSetsImpl(checkpoint, lookups, limit);
284
241
  }
285
242
  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
- }
243
+ yield* this.getBucketDataBatchImpl(checkpoint, dataBuckets, options);
410
244
  }
411
245
  async getChecksums(checkpoint, buckets) {
412
246
  return this.checksums.getChecksums(checkpoint, buckets);
@@ -415,7 +249,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
415
249
  this.checksums.clearCache();
416
250
  }
417
251
  async terminate(options) {
418
- // Default is to clear the storage except when explicitly requested not to.
419
252
  if (!options || options?.clearStorage) {
420
253
  await this.clear(options);
421
254
  }
@@ -442,7 +275,7 @@ export class MongoSyncBucketStorage extends BaseObserver {
442
275
  }
443
276
  });
444
277
  if (doc == null) {
445
- throw new ServiceAssertionError('Cannot find sync rules status');
278
+ throw new ServiceAssertionError('Cannot find replication stream status');
446
279
  }
447
280
  return {
448
281
  snapshot_done: doc.snapshot_done,
@@ -452,29 +285,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
452
285
  };
453
286
  }
454
287
  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
- }
288
+ const signal = options?.signal;
289
+ if (signal?.aborted) {
290
+ throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
473
291
  }
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
292
  await this.db.sync_rules.updateOne({
479
293
  _id: this.group_id
480
294
  }, {
@@ -489,21 +303,22 @@ export class MongoSyncBucketStorage extends BaseObserver {
489
303
  snapshot_lsn: 1
490
304
  }
491
305
  }, { 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 });
306
+ await this.clearBucketData(signal);
307
+ await this.clearParameterIndexes(signal);
308
+ await this.clearSourceRecords(signal);
309
+ await this.clearBucketState(signal);
310
+ await this.clearSourceTables(signal);
311
+ this.#storageInitialized = false;
312
+ }
313
+ async clearDeleteMany(label, operation, signal) {
314
+ await retryOnMongoMaxTimeMSExpired(operation, {
315
+ signal,
316
+ abortMessage: 'Aborted clearing data',
317
+ retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5,
318
+ onRetry: () => {
319
+ this.logger.info(`Cleared batch of ${label} in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`);
320
+ }
321
+ });
507
322
  }
508
323
  async reportError(e) {
509
324
  const message = String(e.message ?? 'Replication failure');
@@ -522,83 +337,52 @@ export class MongoSyncBucketStorage extends BaseObserver {
522
337
  const checkpoint = await this.getCheckpointInternal();
523
338
  maxOpId = checkpoint?.checkpoint ?? undefined;
524
339
  }
525
- await new MongoCompactor(this, this.db, { ...options, maxOpId }).compact();
340
+ await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
526
341
  if (maxOpId != null && options?.compactParameterData) {
527
- await new MongoParameterCompactor(this.db, this.group_id, maxOpId, options).compact();
342
+ await this.createMongoParameterCompactor(maxOpId, options).compact();
528
343
  }
529
344
  }
530
345
  async populatePersistentChecksumCache(options) {
531
- logger.info(`Populating persistent checksum cache...`);
346
+ this.logger.info(`Populating persistent checksum cache...`);
532
347
  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, {
348
+ const compactor = this.createMongoCompactor({
536
349
  ...options,
537
- // Don't track updates for MOVE compacting
538
- memoryLimitMB: 0
350
+ memoryLimitMB: 0,
351
+ logger: this.logger
539
352
  });
540
353
  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
354
  minBucketChanges: options.minBucketChanges ?? 10
544
355
  });
545
356
  const duration = Date.now() - start;
546
- logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
357
+ this.logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
547
358
  return result;
548
359
  }
549
- /**
550
- * Instance-wide watch on the latest available checkpoint (op_id + lsn).
551
- */
552
360
  async *watchActiveCheckpoint(signal) {
553
361
  if (signal.aborted) {
554
362
  return;
555
363
  }
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
364
  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
365
  for await (const _ of stream) {
564
366
  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
367
  break;
567
368
  }
568
369
  const op = await this.getCheckpointInternal();
569
370
  if (op == null) {
570
- // Sync rules have changed - abort and restart.
571
- // We do a soft close of the stream here - no error
572
371
  break;
573
372
  }
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
373
  yield op;
579
374
  }
580
375
  }
581
- // Nothing is done here until a subscriber starts to iterate
582
376
  sharedIter = new BroadcastIterable((signal) => {
583
377
  return this.watchActiveCheckpoint(signal);
584
378
  });
585
- /**
586
- * User-specific watch on the latest checkpoint and/or write checkpoint.
587
- */
588
379
  async *watchCheckpointChanges(options) {
589
380
  let lastCheckpoint = null;
590
381
  const iter = this.sharedIter[Symbol.asyncIterator](options.signal);
591
382
  let writeCheckpoint = null;
592
- // true if we queried the initial write checkpoint, even if it doesn't exist
593
383
  let queriedInitialWriteCheckpoint = false;
594
384
  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
385
  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
386
  writeCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
603
387
  sync_rules_id: this.group_id,
604
388
  user_id: options.user_id,
@@ -611,14 +395,10 @@ export class MongoSyncBucketStorage extends BaseObserver {
611
395
  if (lastCheckpoint != null &&
612
396
  lastCheckpoint.checkpoint == nextCheckpoint.checkpoint &&
613
397
  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
398
  await timers.setTimeout(20 + 10 * Math.random());
618
399
  continue;
619
400
  }
620
401
  if (lastCheckpoint == null) {
621
- // First message for this stream - "INVALIDATE_ALL" means it will lookup all data
622
402
  yield {
623
403
  base: nextCheckpoint,
624
404
  writeCheckpoint,
@@ -632,8 +412,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
632
412
  });
633
413
  let updatedWriteCheckpoint = updates.updatedWriteCheckpoints.get(options.user_id) ?? null;
634
414
  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
415
  updatedWriteCheckpoint = await this.writeCheckpointAPI.lastWriteCheckpoint({
638
416
  sync_rules_id: this.group_id,
639
417
  user_id: options.user_id,
@@ -644,8 +422,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
644
422
  }
645
423
  if (updatedWriteCheckpoint != null && (writeCheckpoint == null || updatedWriteCheckpoint > writeCheckpoint)) {
646
424
  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
425
  queriedInitialWriteCheckpoint = true;
650
426
  }
651
427
  yield {
@@ -662,12 +438,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
662
438
  lastCheckpoint = nextCheckpoint;
663
439
  }
664
440
  }
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
441
  async *checkpointChangesStream(signal) {
672
442
  if (signal.aborted) {
673
443
  return;
@@ -679,16 +449,12 @@ export class MongoSyncBucketStorage extends BaseObserver {
679
449
  signal.addEventListener('abort', () => {
680
450
  cursor.close().catch(() => { });
681
451
  });
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
452
  yield;
685
453
  try {
686
454
  while (!signal.aborted) {
687
455
  const doc = await cursor.tryNext().catch((e) => {
688
456
  if (lib_mongo.isMongoServerError(e) && e.codeName === 'CappedPositionLost') {
689
- // Cursor position lost, potentially due to a high rate of notifications
690
457
  cursor = query();
691
- // Treat as an event found, before querying the new cursor again
692
458
  return {};
693
459
  }
694
460
  else {
@@ -698,8 +464,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
698
464
  if (cursor.closed) {
699
465
  return;
700
466
  }
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
467
  cursor.readBufferedDocuments();
704
468
  if (doc != null) {
705
469
  yield;
@@ -723,7 +487,6 @@ export class MongoSyncBucketStorage extends BaseObserver {
723
487
  }
724
488
  catch (e) {
725
489
  if (e.name == 'AbortError') {
726
- // This is how we typically abort this stream, when all listeners are done
727
490
  return;
728
491
  }
729
492
  throw e;
@@ -734,74 +497,18 @@ export class MongoSyncBucketStorage extends BaseObserver {
734
497
  }
735
498
  }
736
499
  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
- };
500
+ return this.getDataBucketChangesImpl(options);
760
501
  }
761
502
  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
- };
503
+ return this.getParameterBucketChangesImpl(options);
785
504
  }
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
505
  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
506
  max: 50,
799
507
  maxSize: 12 * 1024 * 1024,
800
508
  sizeCalculation: (value) => {
801
- // Estimate of memory usage
802
509
  const paramSize = [...value.updatedParameterLookups].reduce((a, b) => a + b.length, 0);
803
510
  const bucketSize = [...value.updatedDataBuckets].reduce((a, b) => a + b.length, 0);
804
- const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30; // estiamte for user_id + bigint
511
+ const writeCheckpointSize = value.updatedWriteCheckpoints.size * 30;
805
512
  return 100 + paramSize + bucketSize + writeCheckpointSize;
806
513
  },
807
514
  fetchMethod: async (_key, _staleValue, options) => {
@@ -835,14 +542,14 @@ class MongoReplicationCheckpoint {
835
542
  this.snapshotTime = snapshotTime;
836
543
  this.#storage = storage;
837
544
  }
838
- async getParameterSets(lookups) {
839
- return this.#storage.getParameterSets(this, lookups);
545
+ async getParameterSets(lookups, limit) {
546
+ return this.#storage.getParameterSets(this, lookups, limit);
840
547
  }
841
548
  }
842
549
  class EmptyReplicationCheckpoint {
843
550
  checkpoint = 0n;
844
551
  lsn = null;
845
- async getParameterSets(lookups) {
552
+ async getParameterSets(_lookups) {
846
553
  return [];
847
554
  }
848
555
  }