@powersync/service-module-mongodb-storage 0.16.0 → 0.18.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 (206) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +35 -15
  3. package/dist/storage/MongoBucketStorage.js +309 -86
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  6. package/dist/storage/implementation/CheckpointState.js +31 -0
  7. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +69 -39
  9. package/dist/storage/implementation/MongoBucketBatch.js +104 -313
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +11 -9
  12. package/dist/storage/implementation/MongoChecksums.js +3 -111
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +3 -1
  15. package/dist/storage/implementation/MongoCompactor.js +18 -9
  16. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  17. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  18. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  19. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  20. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  21. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  25. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  27. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  28. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  29. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  30. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +121 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +50 -21
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +62 -199
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +9 -5
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  41. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  42. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -3
  48. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  53. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  54. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  55. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  56. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  57. package/dist/storage/implementation/db.d.ts +10 -2
  58. package/dist/storage/implementation/db.js +9 -14
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +25 -50
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +23 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +403 -2
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +22 -15
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +100 -27
  70. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +21 -1
  77. package/dist/storage/implementation/v1/models.js.map +1 -1
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +21 -0
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +459 -3
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  82. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +20 -3
  85. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  86. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +50 -16
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +292 -75
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -3
  94. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  95. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  96. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  97. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +16 -14
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +28 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +111 -10
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  114. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  115. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  116. package/dist/storage/storage-index.d.ts +4 -4
  117. package/dist/storage/storage-index.js +4 -4
  118. package/dist/storage/storage-index.js.map +1 -1
  119. package/dist/types/types.d.ts +2 -2
  120. package/dist/utils/test-utils.d.ts +1 -2
  121. package/dist/utils/test-utils.js +5 -1
  122. package/dist/utils/test-utils.js.map +1 -1
  123. package/dist/utils/util.d.ts +1 -1
  124. package/dist/utils/util.js +3 -2
  125. package/dist/utils/util.js.map +1 -1
  126. package/package.json +9 -19
  127. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  128. package/src/storage/MongoBucketStorage.ts +464 -95
  129. package/src/storage/implementation/CheckpointState.ts +59 -0
  130. package/src/storage/implementation/MongoBucketBatch.ts +159 -405
  131. package/src/storage/implementation/MongoChecksums.ts +7 -138
  132. package/src/storage/implementation/MongoCompactor.ts +20 -10
  133. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  134. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  135. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  136. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  137. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +152 -0
  138. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  139. package/src/storage/implementation/MongoSyncBucketStorage.ts +89 -250
  140. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -5
  141. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  142. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  143. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  144. package/src/storage/implementation/common/PersistedBatch.ts +8 -4
  145. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  146. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  147. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  148. package/src/storage/implementation/db.ts +16 -21
  149. package/src/storage/implementation/models.ts +28 -61
  150. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +503 -5
  151. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  152. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  153. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +150 -49
  154. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  155. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  156. package/src/storage/implementation/v1/models.ts +25 -0
  157. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +611 -4
  158. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  159. package/src/storage/implementation/v3/MongoCompactorV3.ts +647 -30
  160. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  161. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +397 -111
  162. package/src/storage/implementation/v3/PersistedBatchV3.ts +63 -38
  163. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  164. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  165. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +48 -38
  166. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  167. package/src/storage/implementation/v3/chunking.ts +38 -0
  168. package/src/storage/implementation/v3/models.ts +150 -44
  169. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  170. package/src/storage/storage-index.ts +12 -4
  171. package/src/utils/test-utils.ts +6 -3
  172. package/src/utils/util.ts +3 -2
  173. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  174. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  175. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  176. package/test/src/storage.test.ts +11 -11
  177. package/test/src/storage_compacting.test.ts +1956 -6
  178. package/test/src/storage_sync.test.ts +1429 -42
  179. package/test/src/storeCurrentData.test.ts +221 -0
  180. package/test/src/util.ts +2 -1
  181. package/tsconfig.json +1 -4
  182. package/tsconfig.tsbuildinfo +1 -1
  183. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -17
  184. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  185. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  186. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  187. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -64
  188. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  189. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -15
  190. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -42
  191. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  192. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  193. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  194. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  195. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  196. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  197. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  198. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -5
  199. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  200. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  201. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -72
  202. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -76
  203. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -56
  204. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  205. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  206. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -12
@@ -1,22 +1,48 @@
1
- import { GetIntanceOptions, storage } from '@powersync/service-core';
2
-
3
- import { DO_NOT_LOG, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
1
+ import {
2
+ GetIntanceOptions,
3
+ LEGACY_STORAGE_VERSION,
4
+ SingleSyncConfigBucketDefinitionMapping,
5
+ storage
6
+ } from '@powersync/service-core';
7
+
8
+ import { DO_NOT_LOG, ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
4
9
  import { v4 as uuid } from 'uuid';
5
10
 
6
11
  import * as lib_mongo from '@powersync/lib-service-mongodb';
7
12
  import { mongo } from '@powersync/lib-service-mongodb';
8
13
 
9
- import { generateSlotName } from '../utils/util.js';
10
- import { BucketDefinitionMapping } from './implementation/BucketDefinitionMapping.js';
14
+ import {
15
+ describeIncrementalSyncConfigUpdate,
16
+ formatIncrementalSyncConfigUpdateLog,
17
+ isCompatible
18
+ } from '@powersync/service-core';
19
+ import { ObjectId } from 'bson';
20
+ import { generateReplicationStreamName } from '../utils/util.js';
11
21
  import type { MongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
12
22
  import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
13
23
  import { PowerSyncMongo } from './implementation/db.js';
14
- import { getMongoStorageConfig, SyncRuleDocument } from './implementation/models.js';
24
+ import { getMongoStorageConfig, StorageConfig, SyncRuleDocumentBase } from './implementation/models.js';
15
25
  import { MongoChecksumOptions } from './implementation/MongoChecksums.js';
16
- import { MongoPersistedSyncRulesContent } from './implementation/MongoPersistedSyncRulesContent.js';
26
+ import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
27
+ import { syncRuleStateUpdatePipeline } from './implementation/SyncRuleStateUpdate.js';
28
+ import { SyncRuleDocumentV1 } from './implementation/v1/models.js';
29
+ import { VersionedPowerSyncMongoV3 } from './implementation/v3/VersionedPowerSyncMongoV3.js';
30
+ import { ReplicationStreamDocumentV3, SyncConfigDefinition, SyncRuleConfigStateV3 } from './storage-index.js';
17
31
 
18
32
  export interface MongoBucketStorageOptions {
19
- checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig' | 'mapping'>;
33
+ /**
34
+ * Prefix for replication stream name and Postgres logical replication slot name.
35
+ */
36
+ replicationStreamNamePrefix: string;
37
+ checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
38
+ /**
39
+ * Reuse a compatible active replication stream by appending a new sync config.
40
+ *
41
+ * This currently requires source replication support. MongoDB sources can process multiple
42
+ * sync configs in one replication stream, but other source connectors still expect a single
43
+ * sync config per stream.
44
+ */
45
+ supportsMultipleSyncConfigs?: boolean;
20
46
  }
21
47
 
22
48
  export class MongoBucketStorage extends storage.BucketStorageFactory {
@@ -24,8 +50,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
24
50
 
25
51
  private readonly client: mongo.MongoClient;
26
52
  private readonly session: mongo.ClientSession;
27
- // TODO: This is still Postgres specific and needs to be reworked
28
- public readonly slot_name_prefix: string;
53
+ public readonly replicationStreamNamePrefix: string;
29
54
 
30
55
  private activeStorageCache: MongoSyncBucketStorage | undefined;
31
56
 
@@ -33,51 +58,54 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
33
58
 
34
59
  constructor(
35
60
  db: PowerSyncMongo,
36
- options: {
37
- slot_name_prefix: string;
38
- },
39
- private internalOptions?: MongoBucketStorageOptions
61
+ private options: MongoBucketStorageOptions
40
62
  ) {
41
63
  super();
42
64
  this.client = db.client;
43
65
  this.db = db;
44
66
  this.session = this.client.startSession();
45
- this.slot_name_prefix = options.slot_name_prefix;
67
+ this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
46
68
  }
47
69
 
48
70
  async [Symbol.asyncDispose]() {
49
71
  // No-op
50
72
  }
51
73
 
52
- getInstance(syncRules: storage.PersistedSyncRulesContent, options?: GetIntanceOptions): MongoSyncBucketStorage {
53
- let { id, slot_name } = syncRules;
54
- if ((typeof id as any) == 'bigint') {
55
- id = Number(id);
74
+ getInstance(
75
+ replicationStream: storage.PersistedReplicationStream,
76
+ options?: GetIntanceOptions
77
+ ): MongoSyncBucketStorage {
78
+ if (!(replicationStream instanceof MongoPersistedReplicationStream)) {
79
+ throw new Error(`Expected MongoPersistedReplicationStream`);
80
+ }
81
+ let { replicationStreamId, replicationStreamName } = replicationStream;
82
+ if ((typeof replicationStreamId as any) == 'bigint') {
83
+ replicationStreamId = Number(replicationStreamId);
56
84
  }
57
- const storageConfig = (syncRules as MongoPersistedSyncRulesContent).getStorageConfig();
58
- const storage = createMongoSyncBucketStorage(
85
+ const storageConfig = replicationStream.getStorageConfig();
86
+ const syncRuleStorage = createMongoSyncBucketStorage(
59
87
  this,
60
- id,
61
- syncRules as MongoPersistedSyncRulesContent,
62
- slot_name,
88
+ replicationStreamId,
89
+ replicationStream,
90
+ replicationStreamName,
63
91
  undefined,
64
92
  {
65
- ...this.internalOptions,
93
+ checksumOptions: this.options.checksumOptions,
66
94
  storageConfig
67
95
  }
68
96
  );
69
97
  if (!options?.skipLifecycleHooks) {
70
- this.iterateListeners((cb) => cb.syncStorageCreated?.(storage));
98
+ this.iterateListeners((cb) => cb.syncStorageCreated?.(syncRuleStorage));
71
99
  }
72
100
 
73
- storage.registerListener({
101
+ syncRuleStorage.registerListener({
74
102
  batchStarted: (batch) => {
75
103
  batch.registerListener({
76
104
  replicationEvent: (payload) => this.iterateListeners((cb) => cb.replicationEvent?.(payload))
77
105
  });
78
106
  }
79
107
  });
80
- return storage;
108
+ return syncRuleStorage;
81
109
  }
82
110
 
83
111
  async getSystemIdentifier(): Promise<storage.BucketStorageSystemIdentifier> {
@@ -97,29 +125,25 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
97
125
  };
98
126
  }
99
127
 
100
- async restartReplication(sync_rules_group_id: number) {
101
- const next = await this.getNextSyncRulesContent();
102
- const active = await this.getActiveSyncRulesContent();
128
+ async restartReplication(replicationStreamId: number) {
129
+ const next = await this.getDeployingSyncConfig();
130
+ const active = await this.getActiveSyncConfig();
103
131
 
104
- if (next != null && next.id == sync_rules_group_id) {
132
+ if (next != null && next.content.replicationStreamId == replicationStreamId) {
105
133
  // We need to redo the "next" replication stream
106
- await this.updateSyncRules(next.asUpdateOptions());
134
+ await this.updateSyncRules(next.content.asUpdateOptions());
107
135
  // Pro-actively stop replicating
108
136
  await this.db.sync_rules.updateOne(
109
137
  {
110
- _id: next.id,
138
+ _id: next.content.replicationStreamId,
111
139
  state: storage.SyncRuleState.PROCESSING
112
140
  },
113
- {
114
- $set: {
115
- state: storage.SyncRuleState.STOP
116
- }
117
- }
141
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP)
118
142
  );
119
143
  await this.db.notifyCheckpoint();
120
- } else if (next == null && active?.id == sync_rules_group_id) {
144
+ } else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
121
145
  // Slot removed for "active" replication stream, while there is no "next" one.
122
- await this.updateSyncRules(active.asUpdateOptions());
146
+ await this.updateSyncRules(active.content.asUpdateOptions());
123
147
 
124
148
  // In this case we keep the old one as active for clients, so that that existing clients
125
149
  // can still get the latest data while we replicate the new ones.
@@ -127,48 +151,340 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
127
151
 
128
152
  await this.db.sync_rules.updateOne(
129
153
  {
130
- _id: active.id,
154
+ _id: active.content.replicationStreamId,
131
155
  state: storage.SyncRuleState.ACTIVE
132
156
  },
133
- {
134
- $set: {
135
- state: storage.SyncRuleState.ERRORED
136
- }
137
- }
157
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED)
138
158
  );
139
159
  await this.db.notifyCheckpoint();
140
- } else if (next != null && active?.id == sync_rules_group_id) {
160
+ } else if (next != null && active?.content.replicationStreamId == replicationStreamId) {
141
161
  // Already have next replication stream, but need to stop replicating the active one.
142
162
 
143
163
  await this.db.sync_rules.updateOne(
144
164
  {
145
- _id: active.id,
165
+ _id: active.content.replicationStreamId,
146
166
  state: storage.SyncRuleState.ACTIVE
147
167
  },
168
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED)
169
+ );
170
+ await this.db.notifyCheckpoint();
171
+ }
172
+ }
173
+
174
+ private async updateSyncRulesV3(
175
+ options: storage.UpdateSyncRulesOptions,
176
+ storageVersion: number,
177
+ storageConfig: StorageConfig
178
+ ): Promise<MongoPersistedReplicationStream> {
179
+ let rules: MongoPersistedReplicationStream | undefined = undefined;
180
+ const versioned = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
181
+
182
+ const session = this.session;
183
+
184
+ await session.withTransaction(async () => {
185
+ const active = await this.db.sync_rules.findOne<ReplicationStreamDocumentV3>(
148
186
  {
149
- $set: {
150
- state: storage.SyncRuleState.ERRORED
187
+ state: storage.SyncRuleState.ACTIVE,
188
+ storage_version: storageVersion
189
+ },
190
+ { session, sort: { _id: -1 }, limit: 1 }
191
+ );
192
+ if (active != null) {
193
+ const existingConfigDocs = await this.loadSyncConfigDefinitions(versioned, active, session);
194
+
195
+ if (
196
+ this.options.supportsMultipleSyncConfigs &&
197
+ isCompatible(
198
+ existingConfigDocs.map((d) => d.serialized_plan ?? null),
199
+ options.config,
200
+ logger
201
+ )
202
+ ) {
203
+ logger.info(`Using incremental reprocessing`);
204
+ await this.db.sync_rules.updateMany(
205
+ {
206
+ state: storage.SyncRuleState.PROCESSING
207
+ },
208
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
209
+ { session }
210
+ );
211
+ await this.stopEmbeddedDeployingConfigs(active, session);
212
+ rules = await this.appendSyncConfigToStream({
213
+ versioned,
214
+ existing: active,
215
+ existingConfigDocs,
216
+ options,
217
+ storageVersion,
218
+ session
219
+ });
220
+ return;
221
+ }
222
+
223
+ await this.stopEmbeddedDeployingConfigs(active, session);
224
+ }
225
+
226
+ // Only have a single replication stream with PROCESSING.
227
+ await this.db.sync_rules.updateMany(
228
+ {
229
+ state: storage.SyncRuleState.PROCESSING
230
+ },
231
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
232
+ { session }
233
+ );
234
+
235
+ const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
236
+ {
237
+ _id: 'sync_rules'
238
+ },
239
+ {
240
+ $inc: {
241
+ op_id: 1n
151
242
  }
243
+ },
244
+ {
245
+ upsert: true,
246
+ returnDocument: 'after',
247
+ session
152
248
  }
153
249
  );
154
- await this.db.notifyCheckpoint();
250
+
251
+ const id = Number(id_doc!.op_id);
252
+ const replicationStreamName = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
253
+
254
+ const mapping =
255
+ options.config.plan == null
256
+ ? // For legacy sync rules and streams, use the parsed config directly to create a mapping
257
+ SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(options.config.parsed)
258
+ : // For new sync streams, always use the serialized version
259
+ SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingFromSerializedPlans(
260
+ [],
261
+ options.config.plan.plan,
262
+ []
263
+ );
264
+
265
+ const syncConfigDoc: SyncConfigDefinition = {
266
+ _id: new ObjectId(),
267
+ replication_stream_id: id,
268
+ created_at: new Date(),
269
+ storage_version: storageVersion,
270
+ content: options.config.yaml,
271
+ serialized_plan: options.config.plan,
272
+ rule_mapping: mapping.serialize()
273
+ };
274
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
275
+
276
+ const doc: ReplicationStreamDocumentV3 = {
277
+ _id: id,
278
+ storage_version: storageVersion,
279
+ sync_configs: [
280
+ {
281
+ _id: syncConfigDoc._id,
282
+ state: storage.SyncRuleState.PROCESSING,
283
+ last_checkpoint: null,
284
+ last_checkpoint_lsn: null,
285
+ no_checkpoint_before: null,
286
+ snapshot_done: false
287
+ }
288
+ ],
289
+ snapshot_lsn: undefined,
290
+ state: storage.SyncRuleState.PROCESSING,
291
+ slot_name: replicationStreamName,
292
+ last_checkpoint_ts: null,
293
+ last_fatal_error: null,
294
+ last_fatal_error_ts: null,
295
+ last_keepalive_ts: null
296
+ };
297
+
298
+ await this.db.sync_rules.insertOne(doc, { session });
299
+ rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
300
+ if (options.lock) {
301
+ // The lock is persisted on rules.current_lock
302
+ await rules.lock(session);
303
+ }
304
+ });
305
+
306
+ // Notify only after the transaction has committed, so listeners cannot observe pre-commit state.
307
+ await this.db.notifyCheckpoint();
308
+
309
+ return rules!;
310
+ }
311
+
312
+ private async loadSyncConfigDefinitions(
313
+ versioned: VersionedPowerSyncMongoV3,
314
+ existing: ReplicationStreamDocumentV3,
315
+ session: mongo.ClientSession
316
+ ) {
317
+ const activeConfigIds = existing.sync_configs
318
+ .filter((config) => config.state == storage.SyncRuleState.ACTIVE)
319
+ .map((config) => config._id);
320
+
321
+ return versioned.syncConfigDefinitions
322
+ .find(
323
+ {
324
+ _id: { $in: activeConfigIds }
325
+ },
326
+ { session }
327
+ )
328
+ .toArray();
329
+ }
330
+
331
+ /**
332
+ * Load _all_ definition mappings for a replication stream - used as a base to generate new ids.
333
+ */
334
+ private async loadHistoricalSyncConfigRuleMappings(
335
+ versioned: VersionedPowerSyncMongoV3,
336
+ replicationStreamId: number,
337
+ session: mongo.ClientSession
338
+ ) {
339
+ return versioned.syncConfigDefinitions
340
+ .find(
341
+ {
342
+ replication_stream_id: replicationStreamId
343
+ },
344
+ {
345
+ session,
346
+ projection: {
347
+ rule_mapping: 1
348
+ }
349
+ }
350
+ )
351
+ .toArray();
352
+ }
353
+
354
+ private async stopEmbeddedDeployingConfigs(existing: ReplicationStreamDocumentV3, session: mongo.ClientSession) {
355
+ const deployingConfigs = existing.sync_configs
356
+ .filter((config) => config.state == storage.SyncRuleState.PROCESSING)
357
+ .map((config) => config._id);
358
+ if (deployingConfigs.length == 0) {
359
+ return;
360
+ }
361
+
362
+ await this.db.sync_rules.updateOne(
363
+ {
364
+ _id: existing._id,
365
+ 'sync_configs._id': { $in: deployingConfigs }
366
+ },
367
+ {
368
+ $set: {
369
+ 'sync_configs.$[config].state': storage.SyncRuleState.STOP
370
+ }
371
+ },
372
+ {
373
+ session,
374
+ arrayFilters: [{ 'config._id': { $in: deployingConfigs } }]
375
+ }
376
+ );
377
+ }
378
+
379
+ private logIncrementalDefinitionChanges(changes: ReturnType<typeof describeIncrementalSyncConfigUpdate>) {
380
+ logger.info(`Incremental reprocessing sync config update:\n${formatIncrementalSyncConfigUpdateLog(changes)}`);
381
+ }
382
+
383
+ private async appendSyncConfigToStream(options: {
384
+ versioned: VersionedPowerSyncMongoV3;
385
+ existing: ReplicationStreamDocumentV3;
386
+ existingConfigDocs: SyncConfigDefinition[];
387
+ options: storage.UpdateSyncRulesOptions;
388
+ storageVersion: number;
389
+ session: mongo.ClientSession;
390
+ }): Promise<MongoPersistedReplicationStream> {
391
+ const { versioned, existing, existingConfigDocs, options: updateOptions, storageVersion, session } = options;
392
+ const compatibleConfigs = existingConfigDocs.map((doc) => ({
393
+ plan: doc.serialized_plan!.plan,
394
+ mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
395
+ }));
396
+ const historicalRuleMappings = await this.loadHistoricalSyncConfigRuleMappings(versioned, existing._id, session);
397
+ const reservedMappings = historicalRuleMappings.map((doc) =>
398
+ SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
399
+ );
400
+ const mappingResult = SingleSyncConfigBucketDefinitionMapping.constructIncrementalMappingWithChanges(
401
+ compatibleConfigs,
402
+ updateOptions.config.plan!.plan,
403
+ reservedMappings
404
+ );
405
+ const mapping = mappingResult.mapping;
406
+ this.logIncrementalDefinitionChanges(
407
+ describeIncrementalSyncConfigUpdate({
408
+ activeMappings: existingConfigDocs.map((doc) =>
409
+ SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(doc.rule_mapping)
410
+ ),
411
+ newMapping: mapping,
412
+ newSyncConfig: updateOptions.config.parsed,
413
+ mappingChanges: mappingResult.changes
414
+ })
415
+ );
416
+
417
+ const syncConfigDoc: SyncConfigDefinition = {
418
+ _id: new ObjectId(),
419
+ replication_stream_id: existing._id,
420
+ created_at: new Date(),
421
+ storage_version: storageVersion,
422
+ content: updateOptions.config.yaml,
423
+ serialized_plan: updateOptions.config.plan,
424
+ rule_mapping: mapping.serialize()
425
+ };
426
+ await versioned.syncConfigDefinitions.insertOne(syncConfigDoc, { session });
427
+ const syncConfigState: SyncRuleConfigStateV3 = {
428
+ _id: syncConfigDoc._id,
429
+ state: storage.SyncRuleState.PROCESSING,
430
+ last_checkpoint: null,
431
+ last_checkpoint_lsn: null,
432
+ no_checkpoint_before: null,
433
+ snapshot_done: false
434
+ };
435
+
436
+ await this.db.sync_rules.updateOne(
437
+ { _id: existing._id },
438
+ {
439
+ $push: {
440
+ sync_configs: syncConfigState
441
+ },
442
+ $set: {
443
+ last_fatal_error: null,
444
+ last_fatal_error_ts: null
445
+ }
446
+ },
447
+ { session }
448
+ );
449
+ const syncConfigStates = [
450
+ ...existing.sync_configs.filter((config) => config.state == storage.SyncRuleState.ACTIVE),
451
+ syncConfigState
452
+ ];
453
+ const stream = new MongoPersistedReplicationStream(
454
+ this.db,
455
+ {
456
+ ...existing,
457
+ sync_configs: syncConfigStates
458
+ },
459
+ [...existingConfigDocs, syncConfigDoc]
460
+ );
461
+ if (updateOptions.lock) {
462
+ await stream.lock(session);
155
463
  }
464
+ return stream;
156
465
  }
157
466
 
158
- async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<MongoPersistedSyncRulesContent> {
467
+ async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<MongoPersistedReplicationStream> {
159
468
  const storageVersion =
160
469
  options.storageVersion ?? options.config.parsed.config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
470
+
161
471
  const storageConfig = getMongoStorageConfig(storageVersion);
472
+ if (storageConfig.incrementalReprocessing) {
473
+ return this.updateSyncRulesV3(options, storageVersion, storageConfig);
474
+ }
162
475
 
163
- let rules: MongoPersistedSyncRulesContent | undefined = undefined;
476
+ let rules: MongoPersistedReplicationStream | undefined = undefined;
164
477
 
165
- await this.session.withTransaction(async () => {
478
+ const session = this.session;
479
+
480
+ await session.withTransaction(async () => {
166
481
  // Only have a single replication stream with PROCESSING.
167
482
  await this.db.sync_rules.updateMany(
168
483
  {
169
484
  state: storage.SyncRuleState.PROCESSING
170
485
  },
171
- { $set: { state: storage.SyncRuleState.STOP } }
486
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
487
+ { session }
172
488
  );
173
489
 
174
490
  const id_doc = await this.db.op_id_sequence.findOneAndUpdate(
@@ -182,14 +498,15 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
182
498
  },
183
499
  {
184
500
  upsert: true,
185
- returnDocument: 'after'
501
+ returnDocument: 'after',
502
+ session
186
503
  }
187
504
  );
188
505
 
189
506
  const id = Number(id_doc!.op_id);
190
- const slot_name = generateSlotName(this.slot_name_prefix, id);
507
+ const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
191
508
 
192
- const doc: SyncRuleDocument = {
509
+ const doc: SyncRuleDocumentV1 = {
193
510
  _id: id,
194
511
  storage_version: storageVersion,
195
512
  content: options.config.yaml,
@@ -207,89 +524,141 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
207
524
  last_fatal_error_ts: null,
208
525
  last_keepalive_ts: null
209
526
  };
210
- if (storageConfig.incrementalReprocessing) {
211
- const parsed = options.config.parsed;
212
- doc.rule_mapping = BucketDefinitionMapping.fromParsedSyncRules(parsed).serialize();
213
- }
214
- await this.db.sync_rules.insertOne(doc);
527
+
528
+ await this.db.sync_rules.insertOne(doc, { session });
215
529
  await this.db.notifyCheckpoint();
216
- rules = new MongoPersistedSyncRulesContent(this.db, doc);
530
+ rules = new MongoPersistedReplicationStream(this.db, doc);
217
531
  if (options.lock) {
218
- const lock = await rules.lock();
532
+ // The lock is persisted on rules.current_lock
533
+ await rules.lock(session);
219
534
  }
220
535
  });
221
536
 
222
537
  return rules!;
223
538
  }
224
539
 
225
- async getActiveSyncRulesContent(): Promise<MongoPersistedSyncRulesContent | null> {
540
+ async getActiveSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
226
541
  const doc = await this.db.sync_rules.findOne(
227
542
  {
228
543
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
229
544
  },
230
545
  { sort: { _id: -1 }, limit: 1 }
231
546
  );
547
+
548
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED], {
549
+ cacheActiveStorage: true
550
+ });
551
+ }
552
+
553
+ private async replicationStreamFromDoc(doc: SyncRuleDocumentBase | null, stateFilter: storage.SyncRuleState[]) {
232
554
  if (doc == null) {
233
555
  return null;
234
556
  }
557
+ const storageConfig = getMongoStorageConfig(doc.storage_version ?? LEGACY_STORAGE_VERSION);
558
+
559
+ if (storageConfig.incrementalReprocessing) {
560
+ const v3 = doc as ReplicationStreamDocumentV3;
561
+ const matching = v3.sync_configs.filter((c) => stateFilter.includes(c.state));
562
+ if (matching.length == 0) {
563
+ return null;
564
+ }
565
+
566
+ // TODO: cache the config. It could specifically help for the main replication loop
567
+ // that checks for active replication streams.
568
+ // It is not a major bottleneck though, since it only runs once every couple of seconds at most.
569
+ const db = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
570
+ const syncConfigDocs = await db.syncConfigDefinitions
571
+ .find({
572
+ _id: { $in: matching.map((config) => config._id) }
573
+ })
574
+ .toArray();
575
+
576
+ if (syncConfigDocs.length == 0) {
577
+ return null;
578
+ }
579
+ return new MongoPersistedReplicationStream(this.db, v3, syncConfigDocs);
580
+ }
235
581
 
236
- return new MongoPersistedSyncRulesContent(this.db, doc);
582
+ return new MongoPersistedReplicationStream(this.db, doc as SyncRuleDocumentV1);
237
583
  }
238
584
 
239
- async getNextSyncRulesContent(): Promise<MongoPersistedSyncRulesContent | null> {
585
+ async getDeployingSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
240
586
  const doc = await this.db.sync_rules.findOne(
241
587
  {
242
- state: storage.SyncRuleState.PROCESSING
588
+ $or: [{ state: storage.SyncRuleState.PROCESSING }, { 'sync_configs.state': storage.SyncRuleState.PROCESSING }]
243
589
  },
244
590
  { sort: { _id: -1 }, limit: 1 }
245
591
  );
246
- if (doc == null) {
247
- return null;
248
- }
249
592
 
250
- return new MongoPersistedSyncRulesContent(this.db, doc);
593
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING]);
251
594
  }
252
595
 
253
- async getReplicatingSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
596
+ async getReplicatingReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
254
597
  const docs = await this.db.sync_rules
255
598
  .find({
256
599
  state: { $in: [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE] }
257
600
  })
258
601
  .toArray();
259
602
 
260
- return docs.map((doc) => {
261
- return new MongoPersistedSyncRulesContent(this.db, doc);
262
- });
603
+ return (
604
+ await Promise.all(
605
+ docs.map((doc) => {
606
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
607
+ })
608
+ )
609
+ ).filter((r) => r != null);
263
610
  }
264
611
 
265
- async getStoppedSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
612
+ async getStoppedReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
266
613
  const docs = await this.db.sync_rules
267
614
  .find({
268
615
  state: storage.SyncRuleState.STOP
269
616
  })
270
617
  .toArray();
271
618
 
272
- return docs.map((doc) => {
273
- return new MongoPersistedSyncRulesContent(this.db, doc);
274
- });
619
+ return (
620
+ await Promise.all(
621
+ docs.map((doc) => {
622
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.STOP]);
623
+ })
624
+ )
625
+ ).filter((d) => d != null);
275
626
  }
276
627
 
277
- async getActiveStorage(): Promise<MongoSyncBucketStorage | null> {
278
- const content = await this.getActiveSyncRulesContent();
279
- if (content == null) {
628
+ private async resolvedSyncConfigFromDoc(
629
+ doc: SyncRuleDocumentBase | null,
630
+ stateFilter: storage.SyncRuleState[],
631
+ options: { cacheActiveStorage?: boolean } = {}
632
+ ): Promise<storage.ResolvedSyncConfig | null> {
633
+ const stream = await this.replicationStreamFromDoc(doc, stateFilter);
634
+ if (stream == null) {
280
635
  return null;
281
636
  }
282
637
 
283
- // It is important that this instance is cached.
284
- // Not for the instance construction itself, but to ensure that internal caches on the instance
285
- // are re-used properly.
286
- if (this.activeStorageCache?.group_id == content.id) {
287
- return this.activeStorageCache;
288
- } else {
289
- const instance = this.getInstance(content);
290
- this.activeStorageCache = instance;
291
- return instance;
292
- }
638
+ const content = stream.syncConfigContent[0];
639
+ const thisFactory = this;
640
+
641
+ return {
642
+ content,
643
+ replicationStream: stream,
644
+ get storage() {
645
+ // It is important that this instance is cached.
646
+ // Not for the instance construction itself, but to ensure that internal caches on the instance
647
+ // are re-used properly.
648
+ if (
649
+ options.cacheActiveStorage &&
650
+ thisFactory.activeStorageCache?.replicationStream.replicationJobId == stream.replicationJobId
651
+ ) {
652
+ return thisFactory.activeStorageCache;
653
+ }
654
+
655
+ const instance = thisFactory.getInstance(stream);
656
+ if (options.cacheActiveStorage) {
657
+ thisFactory.activeStorageCache = instance;
658
+ }
659
+ return instance;
660
+ }
661
+ };
293
662
  }
294
663
 
295
664
  async getStorageMetrics(): Promise<storage.StorageMetrics> {