@powersync/service-module-mongodb-storage 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +31 -3
  3. package/dist/storage/MongoBucketStorage.js +365 -206
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
  6. package/dist/storage/implementation/MongoBucketBatch.js +35 -9
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +3 -1
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  30. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  31. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  32. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  33. package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
  34. package/dist/storage/implementation/common/PersistedBatch.js +43 -2
  35. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  36. package/dist/storage/implementation/db.d.ts +4 -1
  37. package/dist/storage/implementation/db.js +14 -1
  38. package/dist/storage/implementation/db.js.map +1 -1
  39. package/dist/storage/implementation/models.d.ts +31 -0
  40. package/dist/storage/implementation/models.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
  42. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  43. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
  45. package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
  46. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  48. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  49. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  51. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  52. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/models.js +1 -0
  54. package/dist/storage/implementation/v1/models.js.map +1 -1
  55. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  56. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  57. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  58. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
  59. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +90 -19
  60. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
  62. package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
  63. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  64. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  65. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  66. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  68. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  69. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  71. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  72. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  74. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  75. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
  77. package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
  78. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  80. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  81. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  83. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  84. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  85. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  86. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  87. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  88. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  89. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  90. package/dist/storage/implementation/v3/models.d.ts +84 -4
  91. package/dist/storage/implementation/v3/models.js.map +1 -1
  92. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  93. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
  94. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  95. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  96. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
  97. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
  98. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  99. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
  100. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
  101. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  102. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  103. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
  104. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  105. package/dist/storage/implementation/v3/source-table-utils.d.ts +6 -6
  106. package/dist/storage/implementation/v3/source-table-utils.js +30 -37
  107. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  108. package/dist/storage/storage-index.d.ts +2 -1
  109. package/dist/storage/storage-index.js +1 -0
  110. package/dist/storage/storage-index.js.map +1 -1
  111. package/dist/types/types.d.ts +23 -1
  112. package/dist/types/types.js +16 -7
  113. package/dist/types/types.js.map +1 -1
  114. package/dist/utils/util.js +3 -1
  115. package/dist/utils/util.js.map +1 -1
  116. package/package.json +10 -9
  117. package/src/storage/MongoBucketStorage.ts +495 -268
  118. package/src/storage/implementation/MongoBucketBatch.ts +54 -14
  119. package/src/storage/implementation/MongoCompactor.ts +35 -346
  120. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  121. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  122. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  123. package/src/storage/implementation/MongoStorageProvider.ts +3 -1
  124. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  125. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  126. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
  127. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  128. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  129. package/src/storage/implementation/common/PersistedBatch.ts +54 -2
  130. package/src/storage/implementation/db.ts +21 -2
  131. package/src/storage/implementation/models.ts +34 -0
  132. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +31 -0
  133. package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
  134. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  135. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  136. package/src/storage/implementation/v1/models.ts +1 -0
  137. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  138. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +124 -22
  139. package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
  140. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  141. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  142. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  143. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  144. package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
  145. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  146. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  147. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  148. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  149. package/src/storage/implementation/v3/models.ts +91 -3
  150. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
  151. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  152. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
  153. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
  154. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
  155. package/src/storage/implementation/v3/source-table-utils.ts +39 -41
  156. package/src/storage/storage-index.ts +2 -0
  157. package/src/types/types.ts +24 -7
  158. package/src/utils/util.ts +3 -1
  159. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  160. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  161. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  162. package/test/src/compact-utils.test.ts +226 -0
  163. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  164. package/test/src/object_storage_usage.test.ts +255 -0
  165. package/test/src/parameter_compacting_v1.test.ts +120 -0
  166. package/test/src/parameter_compacting_v3.test.ts +192 -0
  167. package/test/src/parameter_compaction_fence.test.ts +276 -0
  168. package/test/src/storage.test.ts +273 -22
  169. package/test/src/storage_compacting.test.ts +714 -244
  170. package/test/src/storage_s3_checksums.test.ts +3 -4
  171. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  172. package/test/src/storage_s3_reading.test.ts +243 -1
  173. package/test/src/storage_s3_writing.test.ts +1 -0
  174. package/test/src/storage_sync.test.ts +336 -41
  175. package/test/src/util.ts +1 -1
  176. package/tsconfig.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import {
4
4
  BaseObserver,
5
+ logger as defaultLogger,
5
6
  DO_NOT_LOG,
6
7
  Logger,
7
8
  ReplicationAbortedError,
@@ -11,14 +12,15 @@ import {
11
12
  BroadcastIterable,
12
13
  CHECKPOINT_INVALIDATE_ALL,
13
14
  CheckpointChanges,
15
+ CompactInitialReplicationOptions,
16
+ CompactInitialReplicationResults,
14
17
  GetCheckpointChangesOptions,
15
18
  InternalOpId,
16
19
  mergeAsyncIterables,
17
- PopulateChecksumCacheOptions,
18
- PopulateChecksumCacheResults,
19
20
  ReplicationCheckpoint,
20
21
  ReplicationStreamStorageIds,
21
22
  storage,
23
+ SyncRuleState,
22
24
  utils,
23
25
  WatchWriteCheckpointOptions
24
26
  } from '@powersync/service-core';
@@ -28,6 +30,10 @@ import { LRUCache } from 'lru-cache';
28
30
  import * as timers from 'timers/promises';
29
31
  import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../../types/types.js';
30
32
  import { MongoBucketStorage } from '../MongoBucketStorage.js';
33
+ import {
34
+ MongoGetCheckpointChangesOptions,
35
+ MongoSyncBucketStorageCheckpoint
36
+ } from './common/MongoSyncBucketStorageCheckpoint.js';
31
37
  import { DEFAULT_INLINE_THRESHOLD_BYTES } from './common/PersistedBatch.js';
32
38
  import type { VersionedPowerSyncMongo } from './db.js';
33
39
  import { StorageConfig } from './models.js';
@@ -37,7 +43,7 @@ import { MongoCompactOptions, MongoCompactor } from './MongoCompactor.js';
37
43
  import { MongoParameterCompactor } from './MongoParameterCompactor.js';
38
44
  import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
39
45
  import { MongoPersistedReplicationStream } from './MongoPersistedReplicationStream.js';
40
- import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
46
+ import { MongoCheckpointAPIOptions, MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
41
47
  import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
42
48
 
43
49
  export interface MongoSyncBucketStorageOptions {
@@ -50,6 +56,16 @@ export interface MongoSyncBucketStorageOptions {
50
56
  inlineThresholdBytes?: number;
51
57
  }
52
58
 
59
+ /**
60
+ * The stream state read for a checkpoint. All fields must come from the same snapshot.
61
+ */
62
+ export interface MongoCheckpointState {
63
+ checkpoint: InternalOpId;
64
+ lsn: string | null;
65
+ /** See {@link MongoSyncBucketStorageCheckpoint.parameterChangesInvalidBefore}. */
66
+ parameterChangesInvalidBefore: InternalOpId;
67
+ }
68
+
53
69
  interface InternalCheckpointChanges extends CheckpointChanges {
54
70
  updatedWriteCheckpoints: Map<string, bigint>;
55
71
  invalidateWriteCheckpoints: boolean;
@@ -112,12 +128,27 @@ export abstract class MongoSyncBucketStorage
112
128
  this.clearBatchThrottleRate = options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE;
113
129
  this.db = factory.db.versioned(this.storageConfig);
114
130
  this.checksums = this.createMongoChecksums(options);
115
- this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
131
+ this.writeCheckpointAPI = this.createWriteCheckpointAPI({
116
132
  db: this.db,
117
- mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
118
- sync_rules_id: replicationStreamId
133
+ writeCheckpointMode: { mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED },
134
+ replicationStreamId
119
135
  });
120
- this.logger = replicationStream.logger;
136
+
137
+ // Include both replication stream name and sync config version labels in log prefix.
138
+ // Examples:
139
+ // No sync config with a label: [stream]
140
+ // All sync configs have a label: [stream][v5,v6]
141
+ // Mixed: [stream][?,v6]
142
+ const hasVersionLabel = replicationStream.syncConfigContent.some((config) => config.version_label != null);
143
+ const versionLabels = hasVersionLabel
144
+ ? replicationStream.syncConfigContent.map((config) => config.version_label ?? '?')
145
+ : [];
146
+ this.logger =
147
+ versionLabels.length == 0
148
+ ? replicationStream.logger
149
+ : defaultLogger.child({
150
+ prefix: `[${replicationStreamName}][${versionLabels.join(',')}] `
151
+ });
121
152
  }
122
153
 
123
154
  /**
@@ -128,11 +159,20 @@ export abstract class MongoSyncBucketStorage
128
159
  abstract createMongoCompactor(options: MongoCompactOptions): MongoCompactor;
129
160
 
130
161
  protected abstract createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums;
162
+ protected createWriteCheckpointAPI(options: MongoCheckpointAPIOptions): MongoWriteCheckpointAPI {
163
+ return new MongoWriteCheckpointAPI(options);
164
+ }
165
+
131
166
  protected abstract createMongoParameterCompactor(
132
167
  checkpoint: InternalOpId,
133
168
  options: storage.CompactOptions
134
169
  ): MongoParameterCompactor;
135
170
 
171
+ /** MongoDB parameter compaction uses a persisted operation-id cursor. */
172
+ public supportsIncrementalParameterCompaction(): boolean {
173
+ return true;
174
+ }
175
+
136
176
  get writeCheckpointMode() {
137
177
  return this.writeCheckpointAPI.writeCheckpointMode;
138
178
  }
@@ -144,8 +184,8 @@ export abstract class MongoSyncBucketStorage
144
184
  return this.replicationStream.storageIds;
145
185
  }
146
186
 
147
- setWriteCheckpointMode(mode: storage.WriteCheckpointMode): void {
148
- this.writeCheckpointAPI.setWriteCheckpointMode(mode);
187
+ setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
188
+ this.writeCheckpointAPI.setWriteCheckpointMode(config);
149
189
  }
150
190
 
151
191
  createManagedWriteCheckpoints(
@@ -178,9 +218,7 @@ export abstract class MongoSyncBucketStorage
178
218
  return (await this.getCheckpointInternal()) ?? new EmptyReplicationCheckpoint();
179
219
  }
180
220
 
181
- protected abstract fetchCheckpointState(
182
- session: mongo.ClientSession
183
- ): Promise<{ checkpoint: bigint; lsn: string | null } | null>;
221
+ protected abstract fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null>;
184
222
 
185
223
  async getCheckpointInternal(): Promise<storage.ReplicationCheckpoint | null> {
186
224
  return await this.db.client.withSession({ snapshot: true }, async (session) => {
@@ -197,7 +235,14 @@ export abstract class MongoSyncBucketStorage
197
235
  if (clusterTime == null) {
198
236
  throw new ServiceAssertionError('Missing clusterTime in getCheckpoint()');
199
237
  }
200
- return new MongoReplicationCheckpoint(this, state.checkpoint, state.lsn, snapshotTime, clusterTime);
238
+ return new MongoReplicationCheckpoint(
239
+ this,
240
+ state.checkpoint,
241
+ state.lsn,
242
+ snapshotTime,
243
+ clusterTime,
244
+ state.parameterChangesInvalidBefore
245
+ );
201
246
  });
202
247
  }
203
248
 
@@ -235,6 +280,7 @@ export abstract class MongoSyncBucketStorage
235
280
  markRecordUnavailable: options.markRecordUnavailable,
236
281
  hooks: options.hooks,
237
282
  tracer: options.tracer,
283
+ signal: options.signal,
238
284
  objectStorage: this.objectStorage,
239
285
  inlineThresholdBytes: this.inlineThresholdBytes
240
286
  };
@@ -329,6 +375,7 @@ export abstract class MongoSyncBucketStorage
329
375
  protected abstract clearBucketState(signal?: AbortSignal): Promise<void>;
330
376
 
331
377
  protected abstract clearSourceTables(signal?: AbortSignal): Promise<void>;
378
+ protected async clearCustomCheckpointRequests(_signal?: AbortSignal): Promise<void> {}
332
379
  protected abstract clearSyncRuleState(): Promise<void>;
333
380
 
334
381
  async clear(options?: storage.ClearStorageOptions): Promise<void> {
@@ -345,6 +392,7 @@ export abstract class MongoSyncBucketStorage
345
392
  await this.clearSourceRecords(signal);
346
393
  await this.clearBucketState(signal);
347
394
  await this.clearSourceTables(signal);
395
+ await this.clearCustomCheckpointRequests(signal);
348
396
 
349
397
  this.#storageInitialized = false;
350
398
  }
@@ -367,43 +415,30 @@ export abstract class MongoSyncBucketStorage
367
415
  async compact(options?: storage.CompactOptions) {
368
416
  let maxOpId = options?.maxOpId;
369
417
  if (maxOpId == null) {
418
+ // For PROCESSING streams, this will be undefined.
370
419
  const checkpoint = await this.getCheckpointInternal();
371
420
  maxOpId = checkpoint?.checkpoint ?? undefined;
372
421
  }
373
422
  await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
374
423
 
375
- if (maxOpId != null && options?.compactParameterData) {
376
- await this.createMongoParameterCompactor(maxOpId, options).compact();
424
+ if (maxOpId != null && options?.compactParameterData && this.replicationStream.state == SyncRuleState.ACTIVE) {
425
+ // Use the stream-scoped logger, matching bucket compaction above.
426
+ await this.createMongoParameterCompactor(maxOpId, { ...options, logger: this.logger }).compact();
377
427
  }
378
428
  }
379
429
 
430
+ abstract compactInitialReplication(
431
+ options: CompactInitialReplicationOptions
432
+ ): Promise<CompactInitialReplicationResults>;
433
+
380
434
  /**
381
435
  * The highest op id persisted for this stream, whether or not covered by a checkpoint.
382
436
  *
383
- * Used as the default `maxOpId` for {@link populatePersistentChecksumCache}, which runs after
437
+ * Used as the default `maxOpId` for {@link compactInitialReplication}, which runs after
384
438
  * initial replication but before the first checkpoint exists.
385
439
  */
386
440
  protected abstract fetchPersistedOpHead(): Promise<InternalOpId | null>;
387
441
 
388
- async populatePersistentChecksumCache(options: PopulateChecksumCacheOptions): Promise<PopulateChecksumCacheResults> {
389
- this.logger.info(`Populating persistent checksum cache...`);
390
- const start = Date.now();
391
- const maxOpId = options.maxOpId ?? (await this.fetchPersistedOpHead()) ?? undefined;
392
- const compactor = this.createMongoCompactor({
393
- ...options,
394
- maxOpId,
395
- memoryLimitMB: 0,
396
- logger: this.logger
397
- });
398
-
399
- const result = await compactor.populateChecksums({
400
- minBucketChanges: options.minBucketChanges ?? 10
401
- });
402
- const duration = Date.now() - start;
403
- this.logger.info(`Populated persistent checksum cache in ${(duration / 1000).toFixed(1)}s`);
404
- return result;
405
- }
406
-
407
442
  private async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<ReplicationCheckpoint> {
408
443
  if (signal.aborted) {
409
444
  return;
@@ -580,13 +615,31 @@ export abstract class MongoSyncBucketStorage
580
615
  }
581
616
 
582
617
  protected abstract getParameterBucketChangesImpl(
583
- options: GetCheckpointChangesOptions
618
+ options: MongoGetCheckpointChangesOptions
584
619
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>>;
585
620
 
586
621
  private async getParameterBucketChanges(
587
622
  options: GetCheckpointChangesOptions
588
623
  ): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
589
- return this.getParameterBucketChangesImpl(options);
624
+ const nextCheckpoint = requireMongoCheckpoint(options.nextCheckpoint);
625
+ if (options.lastCheckpoint.checkpoint < nextCheckpoint.parameterChangesInvalidBefore) {
626
+ // Parameter compaction may have deleted parameter entries in the range we'd have to query
627
+ // to find the individual changed lookups. Invalidate all parameter buckets instead.
628
+ //
629
+ // The fence is committed before the first delete of a compaction pass, and captured in the
630
+ // same snapshot as the checkpoint, so a checkpoint that could miss a deleted entry always
631
+ // observes the fence as well.
632
+ return {
633
+ invalidateParameterBuckets: true,
634
+ updatedParameterLookups: new Set<string>()
635
+ };
636
+ }
637
+ // The query below runs at the checkpoint snapshot, which still sees entries deleted by a
638
+ // compaction pass that started after the snapshot.
639
+ return this.getParameterBucketChangesImpl({
640
+ lastCheckpoint: options.lastCheckpoint,
641
+ nextCheckpoint
642
+ });
590
643
  }
591
644
 
592
645
  private checkpointChangesCache = new LRUCache<
@@ -611,7 +664,10 @@ export abstract class MongoSyncBucketStorage
611
664
  });
612
665
 
613
666
  async getCheckpointChanges(options: GetCheckpointChangesOptions): Promise<InternalCheckpointChanges> {
614
- const key = `${options.lastCheckpoint.checkpoint}_${options.lastCheckpoint.lsn}__${options.nextCheckpoint.checkpoint}_${options.nextCheckpoint.lsn}`;
667
+ // The invalidation fence is part of the identity: the same checkpoint pair read before and
668
+ // after a compaction pass produces different results (specific lookups vs. invalidate-all).
669
+ const fence = requireMongoCheckpoint(options.nextCheckpoint).parameterChangesInvalidBefore;
670
+ const key = `${options.lastCheckpoint.checkpoint}_${options.lastCheckpoint.lsn}__${options.nextCheckpoint.checkpoint}_${options.nextCheckpoint.lsn}_${fence}`;
615
671
  const result = await this.checkpointChangesCache.fetch(key, { context: { options } });
616
672
  return result!;
617
673
  }
@@ -629,7 +685,19 @@ export abstract class MongoSyncBucketStorage
629
685
  }
630
686
  }
631
687
 
632
- class MongoReplicationCheckpoint implements ReplicationCheckpoint {
688
+ /**
689
+ * We don't support any other constructions of ReplicationCheckpoint.
690
+ */
691
+ function requireMongoCheckpoint(checkpoint: ReplicationCheckpoint): MongoReplicationCheckpoint {
692
+ if (!(checkpoint instanceof MongoReplicationCheckpoint)) {
693
+ throw new ServiceAssertionError(
694
+ `Checkpoint changes require a checkpoint from getCheckpointInternal(), got ${checkpoint.constructor.name}`
695
+ );
696
+ }
697
+ return checkpoint;
698
+ }
699
+
700
+ class MongoReplicationCheckpoint implements MongoSyncBucketStorageCheckpoint {
633
701
  #storage: MongoSyncBucketStorage;
634
702
 
635
703
  constructor(
@@ -637,7 +705,9 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
637
705
  public readonly checkpoint: InternalOpId,
638
706
  public readonly lsn: string | null,
639
707
  public snapshotTime: mongo.Timestamp,
640
- public clusterTime: mongo.ClusterTime
708
+ public clusterTime: mongo.ClusterTime,
709
+ /** Captured in the same snapshot as the checkpoint. */
710
+ public readonly parameterChangesInvalidBefore: InternalOpId
641
711
  ) {
642
712
  this.#storage = storage;
643
713
  }
@@ -647,6 +717,10 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
647
717
  }
648
718
  }
649
719
 
720
+ /**
721
+ * Used when no checkpoint has been persisted yet. This has no snapshot or invalidation fence, so
722
+ * it cannot be used for checkpoint change detection - see {@link requireMongoCheckpoint}.
723
+ */
650
724
  class EmptyReplicationCheckpoint implements ReplicationCheckpoint {
651
725
  readonly checkpoint: InternalOpId = 0n;
652
726
  readonly lsn: string | null = null;
@@ -5,6 +5,8 @@ import { ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framewo
5
5
  import { storage } from '@powersync/service-core';
6
6
  import { VersionedPowerSyncMongo } from './db.js';
7
7
 
8
+ const LOCK_DURATION_MS = 60 * 1000;
9
+
8
10
  /**
9
11
  * Manages a lock on a replication stream document, so that only one process
10
12
  * processes that replication stream at a time.
@@ -22,15 +24,20 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
22
24
  ): Promise<MongoSyncRulesLock> {
23
25
  const lockId = crypto.randomBytes(8).toString('hex');
24
26
  const doc = await db.sync_rules.findOneAndUpdate(
25
- { _id: sync_rules.replicationStreamId, $or: [{ lock: null }, { 'lock.expires_at': { $lt: new Date() } }] },
26
27
  {
27
- $set: {
28
- lock: {
29
- id: lockId,
30
- expires_at: new Date(Date.now() + 60 * 1000)
28
+ _id: sync_rules.replicationStreamId,
29
+ $or: [{ lock: null }, { $expr: { $lt: ['$lock.expires_at', '$$NOW'] } }]
30
+ },
31
+ [
32
+ {
33
+ $set: {
34
+ lock: {
35
+ id: lockId,
36
+ expires_at: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: LOCK_DURATION_MS } }
37
+ }
31
38
  }
32
39
  }
33
- },
40
+ ],
34
41
  {
35
42
  projection: { lock: 1 },
36
43
  returnDocument: 'before',
@@ -96,9 +103,15 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
96
103
  _id: this.sync_rules_id,
97
104
  'lock.id': this.lock_id
98
105
  },
99
- {
100
- $set: { 'lock.expires_at': new Date(Date.now() + 60 * 1000) }
101
- },
106
+ [
107
+ {
108
+ $set: {
109
+ 'lock.expires_at': {
110
+ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: LOCK_DURATION_MS }
111
+ }
112
+ }
113
+ }
114
+ ],
102
115
  { returnDocument: 'after' }
103
116
  );
104
117
  if (result == null) {
@@ -1,29 +1,31 @@
1
- import { mongo } from '@powersync/lib-service-mongodb';
2
1
  import * as framework from '@powersync/lib-services-framework';
3
- import { GetCheckpointChangesOptions, InternalOpId, storage } from '@powersync/service-core';
2
+ import { GetCheckpointChangesOptions, storage } from '@powersync/service-core';
4
3
  import { VersionedPowerSyncMongo } from './db.js';
5
4
 
6
5
  export type MongoCheckpointAPIOptions = {
7
6
  db: VersionedPowerSyncMongo;
8
- mode: storage.WriteCheckpointMode;
9
- sync_rules_id: number;
7
+ writeCheckpointMode: storage.WriteCheckpointModeConfig;
8
+ replicationStreamId: number;
10
9
  };
11
10
 
12
11
  export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
13
12
  readonly db: VersionedPowerSyncMongo;
13
+ readonly replicationStreamId: number;
14
+
14
15
  private _mode: storage.WriteCheckpointMode;
15
16
 
16
17
  constructor(options: MongoCheckpointAPIOptions) {
17
18
  this.db = options.db;
18
- this._mode = options.mode;
19
+ this._mode = options.writeCheckpointMode.mode;
20
+ this.replicationStreamId = options.replicationStreamId;
19
21
  }
20
22
 
21
23
  get writeCheckpointMode() {
22
24
  return this._mode;
23
25
  }
24
26
 
25
- setWriteCheckpointMode(mode: storage.WriteCheckpointMode): void {
26
- this._mode = mode;
27
+ setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
28
+ this._mode = config.mode;
27
29
  }
28
30
 
29
31
  async createManagedWriteCheckpoints(
@@ -247,6 +249,7 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
247
249
  }
248
250
 
249
251
  protected async lastCustomWriteCheckpoint(filters: storage.CustomWriteCheckpointFilters) {
252
+ // Legacy V1/V2 implementation.
250
253
  const { user_id, sync_rules_id } = filters;
251
254
  const lastWriteCheckpoint = await this.db.custom_write_checkpoints.findOne({
252
255
  user_id,
@@ -301,8 +304,9 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
301
304
  };
302
305
  }
303
306
 
304
- private async getCustomWriteCheckpointChanges(options: GetCheckpointChangesOptions) {
307
+ protected async getCustomWriteCheckpointChanges(options: GetCheckpointChangesOptions) {
305
308
  const limit = 1000;
309
+ // Legacy V1/V2 implementation.
306
310
  const changes = await this.db.custom_write_checkpoints
307
311
  .find(
308
312
  {
@@ -332,39 +336,3 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
332
336
  };
333
337
  }
334
338
  }
335
-
336
- export async function batchCreateCustomWriteCheckpoints(
337
- db: VersionedPowerSyncMongo,
338
- session: mongo.ClientSession,
339
- checkpoints: storage.CustomWriteCheckpointOptions[],
340
- opId: InternalOpId
341
- ): Promise<void> {
342
- if (checkpoints.length == 0) {
343
- return;
344
- }
345
-
346
- await db.custom_write_checkpoints.bulkWrite(
347
- checkpoints.map((checkpointOptions) => {
348
- const set: Record<string, unknown> = {
349
- checkpoint: checkpointOptions.checkpoint,
350
- sync_rules_id: checkpointOptions.sync_rules_id,
351
- op_id: opId
352
- };
353
- if (checkpointOptions.checkpoint_requested_at != null) {
354
- set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
355
- }
356
-
357
- return {
358
- updateOne: {
359
- filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
360
- update: {
361
- $set: set,
362
- ...(checkpointOptions.checkpoint_requested_at == null ? { $unset: { checkpoint_requested_at: 1 } } : {})
363
- },
364
- upsert: true
365
- }
366
- };
367
- }),
368
- { session }
369
- );
370
- }
@@ -2,21 +2,28 @@ import { mongo } from '@powersync/lib-service-mongodb';
2
2
  import { storage } from '@powersync/service-core';
3
3
 
4
4
  /**
5
- * Update pipeline to update replication stream status, covering all storage versions.
5
+ * Update pipeline to stop a deploying replication stream, covering all storage versions.
6
6
  *
7
7
  * Roughly equivalent to:
8
8
  * $set: {
9
- * state: state,
10
- * 'sync_configs.$[].state': state
9
+ * state: STOP,
10
+ * 'sync_configs.$[].state': STOP
11
11
  * }
12
12
  *
13
13
  * The difference is that this also handles v1 storage cases, where `sync_configs` is not present.
14
+ *
15
+ * This can be used for specific use cases:
16
+ * 1. Stop an existing PROCESSING stream, when it is being replaced by a new PROCESSING one.
17
+ * 2. Stop an existing ACTIVE or ERRORED stream, when it is being replaced by a new ACTIVE one.
18
+ *
19
+ * Do not use this when an ACTIVE stream transitions to ERRORED, and is being replaced by a new PROCESSING one.
20
+ * In that case, we need to maintain the ERRORED state, which this pipeline does not cover.
14
21
  */
15
- export function syncRuleStateUpdatePipeline(state: storage.SyncRuleState): mongo.Document[] {
22
+ export function stopReplicationStreamPipeline(): mongo.Document[] {
16
23
  return [
17
24
  {
18
25
  $set: {
19
- state,
26
+ state: storage.SyncRuleState.STOP,
20
27
  sync_configs: {
21
28
  $cond: [
22
29
  { $isArray: '$sync_configs' },
@@ -25,7 +32,7 @@ export function syncRuleStateUpdatePipeline(state: storage.SyncRuleState): mongo
25
32
  input: '$sync_configs',
26
33
  as: 'config',
27
34
  in: {
28
- $mergeObjects: ['$$config', { state }]
35
+ $mergeObjects: ['$$config', { state: storage.SyncRuleState.STOP }]
29
36
  }
30
37
  }
31
38
  },
@@ -1,9 +1,31 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { InternalOpId } from '@powersync/service-core';
2
+ import { InternalOpId, ReplicationCheckpoint } from '@powersync/service-core';
3
3
  import * as bson from 'bson';
4
4
 
5
- export interface MongoSyncBucketStorageCheckpoint {
5
+ export interface MongoSyncBucketStorageCheckpoint extends ReplicationCheckpoint {
6
6
  checkpoint: InternalOpId;
7
7
  snapshotTime: bson.Timestamp;
8
8
  clusterTime: mongo.ClusterTime;
9
+
10
+ /**
11
+ * The stream's `parameter_compaction.checkpoint_changes_invalid_before` boundary, read in the
12
+ * same snapshot as {@link checkpoint} and {@link snapshotTime}.
13
+ *
14
+ * Parameter compaction may have deleted parameter entries below this boundary, so checkpoint
15
+ * change detection cannot enumerate individual lookup changes from a checkpoint below it.
16
+ *
17
+ * 0n for streams that have never been compacted.
18
+ */
19
+ parameterChangesInvalidBefore: InternalOpId;
20
+ }
21
+
22
+ /**
23
+ * MongoDB-specific version of GetCheckpointChangesOptions.
24
+ *
25
+ * The next checkpoint carries the snapshot and invalidation boundary that change detection must
26
+ * be evaluated against. Only `checkpoint` is used from the previous one.
27
+ */
28
+ export interface MongoGetCheckpointChangesOptions {
29
+ lastCheckpoint: ReplicationCheckpoint;
30
+ nextCheckpoint: MongoSyncBucketStorageCheckpoint;
9
31
  }
@@ -65,6 +65,11 @@ export interface PersistedBatchOptions {
65
65
  logger?: Logger;
66
66
  objectStorage?: ObjectStorage;
67
67
  inlineThresholdBytes?: number;
68
+ /**
69
+ * Aborts in-flight object storage uploads when replication stops.
70
+ */
71
+ signal?: AbortSignal;
72
+ objectStorageUsageWriterId?: string;
68
73
  }
69
74
 
70
75
  /**
@@ -81,6 +86,8 @@ export abstract class PersistedBatch {
81
86
 
82
87
  protected readonly objectStorage?: ObjectStorage;
83
88
  protected readonly inlineThresholdBytes: number = DEFAULT_INLINE_THRESHOLD_BYTES;
89
+ protected readonly signal?: AbortSignal;
90
+ protected readonly objectStorageUsageWriterId?: string;
84
91
 
85
92
  /**
86
93
  * For debug logging only.
@@ -102,6 +109,8 @@ export abstract class PersistedBatch {
102
109
  this.currentSize = writtenSize;
103
110
  this.logger = options?.logger ?? defaultLogger;
104
111
  this.objectStorage = options?.objectStorage;
112
+ this.signal = options?.signal;
113
+ this.objectStorageUsageWriterId = options?.objectStorageUsageWriterId;
105
114
  if (options?.inlineThresholdBytes != null) {
106
115
  this.inlineThresholdBytes = options.inlineThresholdBytes;
107
116
  }
@@ -124,7 +133,8 @@ export abstract class PersistedBatch {
124
133
  remaining_buckets.set(currentBucketKey(mapped), mapped);
125
134
  }
126
135
 
127
- const dchecksum = BigInt(utils.hashDelete(replicaIdToSubkey(options.table.id, options.sourceKey)));
136
+ const subkey = this.persistedSubkey(options.table.id, options.sourceKey);
137
+ const dchecksum = BigInt(utils.hashDelete(subkey));
128
138
 
129
139
  for (const evaluated of options.evaluated) {
130
140
  const definitionId = this.getBucketDefinitionId(evaluated.source);
@@ -159,6 +169,7 @@ export abstract class PersistedBatch {
159
169
  bucket: evaluated.bucket,
160
170
  sourceTableId: options.table.id,
161
171
  sourceKey: options.sourceKey,
172
+ subkey,
162
173
  table: evaluated.table,
163
174
  rowId: evaluated.id,
164
175
  checksum: BigInt(checksum),
@@ -181,6 +192,7 @@ export abstract class PersistedBatch {
181
192
  op_id,
182
193
  sourceTableId: options.table.id,
183
194
  sourceKey: options.sourceKey,
195
+ subkey,
184
196
  table: bucket.table,
185
197
  rowId: bucket.id,
186
198
  checksum: dchecksum
@@ -217,6 +229,14 @@ export abstract class PersistedBatch {
217
229
  protected abstract checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId;
218
230
  protected abstract getBucketDefinitionId(bucketSource: BucketDataSource): BucketDefinitionId;
219
231
 
232
+ /**
233
+ * New bucket operations persist protocol subkeys during replication. Older
234
+ * V1 documents may not have one, so sync reads retain a fallback.
235
+ */
236
+ protected persistedSubkey(table: storage.SourceTableId, key: storage.ReplicaId): string {
237
+ return replicaIdToSubkey(table, key);
238
+ }
239
+
220
240
  protected get bucketDataCount(): number {
221
241
  return this.bucketData.length;
222
242
  }
@@ -234,17 +254,45 @@ export abstract class PersistedBatch {
234
254
  bucket,
235
255
  lastOp: op_id,
236
256
  incrementCount: 1,
237
- incrementBytes: bytes
257
+ incrementBytes: bytes,
258
+ incrementChunks: 0
238
259
  });
239
260
  }
240
261
  }
241
262
 
263
+ /**
264
+ * V3 persists operations in chunks. Keep this separate from incrementBucket:
265
+ * operation counts are known while evaluating rows, while chunk counts and
266
+ * exact persisted bytes are only known after the writer has chunked a flush.
267
+ */
268
+ protected incrementBucketPersistedChunk(definitionId: BucketDefinitionId, bucket: string, bytes: number) {
269
+ const key = `${definitionId ?? ''}:${bucket}`;
270
+ const existingState = this.bucketStates.get(key);
271
+ if (existingState != null) {
272
+ existingState.incrementChunks += 1;
273
+ existingState.incrementBytes += bytes;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * V3's compactor calculates byte deltas from persisted chunk metadata.
279
+ * Replace the writer's per-operation estimate with those exact sizes before
280
+ * flushing the corresponding bucket state.
281
+ */
282
+ protected resetBucketPersistedBytes(definitionId: BucketDefinitionId, bucket: string) {
283
+ const state = this.bucketStates.get(`${definitionId ?? ''}:${bucket}`);
284
+ if (state != null) {
285
+ state.incrementBytes = 0;
286
+ }
287
+ }
288
+
242
289
  protected addBucketDataPut(options: {
243
290
  op_id: InternalOpId;
244
291
  bucketKey: BucketKey;
245
292
  bucket: string;
246
293
  sourceTableId: storage.SourceTable['id'];
247
294
  sourceKey: storage.ReplicaId;
295
+ subkey: string;
248
296
  table: string;
249
297
  rowId: string;
250
298
  checksum: bigint;
@@ -256,6 +304,7 @@ export abstract class PersistedBatch {
256
304
  op: 'PUT',
257
305
  source_table: mongoTableId(options.sourceTableId),
258
306
  source_key: options.sourceKey,
307
+ subkey: options.subkey,
259
308
  table: options.table,
260
309
  row_id: options.rowId,
261
310
  checksum: options.checksum,
@@ -268,6 +317,7 @@ export abstract class PersistedBatch {
268
317
  bucketKey: BucketKey;
269
318
  sourceTableId: storage.SourceTable['id'];
270
319
  sourceKey: storage.ReplicaId;
320
+ subkey: string;
271
321
  table: string;
272
322
  rowId: string;
273
323
  checksum: bigint;
@@ -278,6 +328,7 @@ export abstract class PersistedBatch {
278
328
  op: 'REMOVE',
279
329
  source_table: mongoTableId(options.sourceTableId),
280
330
  source_key: options.sourceKey,
331
+ subkey: options.subkey,
281
332
  table: options.table,
282
333
  row_id: options.rowId,
283
334
  checksum: options.checksum,
@@ -377,4 +428,5 @@ export interface BucketStateUpdate {
377
428
  lastOp: InternalOpId;
378
429
  incrementCount: number;
379
430
  incrementBytes: number;
431
+ incrementChunks: number;
380
432
  }