@powersync/service-module-mongodb-storage 0.0.0-dev-20260313100403 → 0.0.0-dev-20260515144844

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