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

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