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