@powersync/service-module-mongodb-storage 0.17.0 → 0.18.1

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 (185) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +34 -16
  3. package/dist/storage/MongoBucketStorage.js +219 -70
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +42 -23
  6. package/dist/storage/implementation/MongoBucketBatch.js +61 -44
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoChecksums.d.ts +10 -9
  9. package/dist/storage/implementation/MongoChecksums.js +3 -111
  10. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  11. package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
  12. package/dist/storage/implementation/MongoCompactor.js +18 -9
  13. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  15. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  16. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  18. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +123 -0
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  29. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +41 -23
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +51 -43
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +3 -3
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  38. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  39. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  40. package/dist/storage/implementation/common/PersistedBatch.d.ts +1 -2
  41. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  42. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  43. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  44. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  45. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  46. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  47. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  48. package/dist/storage/implementation/db.js +9 -14
  49. package/dist/storage/implementation/db.js.map +1 -1
  50. package/dist/storage/implementation/models.d.ts +4 -13
  51. package/dist/storage/implementation/models.js.map +1 -1
  52. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
  53. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +42 -26
  54. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  55. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  56. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  57. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +15 -17
  58. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +47 -45
  59. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  60. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  61. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  62. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  63. package/dist/storage/implementation/v1/models.d.ts +10 -1
  64. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
  65. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +286 -259
  66. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  67. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  68. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  69. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +19 -2
  71. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  72. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  74. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  75. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  76. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +40 -28
  77. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +232 -116
  78. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -1
  80. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  81. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  83. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  85. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  86. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +13 -12
  88. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +29 -27
  89. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  90. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  91. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  92. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  93. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  94. package/dist/storage/implementation/v3/chunking.js +28 -0
  95. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  96. package/dist/storage/implementation/v3/models.d.ts +64 -21
  97. package/dist/storage/implementation/v3/models.js +11 -28
  98. package/dist/storage/implementation/v3/models.js.map +1 -1
  99. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  100. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  101. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  102. package/dist/storage/storage-index.d.ts +4 -4
  103. package/dist/storage/storage-index.js +4 -4
  104. package/dist/storage/storage-index.js.map +1 -1
  105. package/dist/types/types.d.ts +2 -2
  106. package/dist/utils/test-utils.d.ts +1 -2
  107. package/dist/utils/test-utils.js +5 -1
  108. package/dist/utils/test-utils.js.map +1 -1
  109. package/dist/utils/util.d.ts +1 -1
  110. package/dist/utils/util.js +3 -2
  111. package/dist/utils/util.js.map +1 -1
  112. package/package.json +9 -19
  113. package/src/storage/MongoBucketStorage.ts +334 -87
  114. package/src/storage/implementation/MongoBucketBatch.ts +86 -58
  115. package/src/storage/implementation/MongoChecksums.ts +6 -138
  116. package/src/storage/implementation/MongoCompactor.ts +19 -9
  117. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  118. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  119. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  120. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  121. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
  122. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  123. package/src/storage/implementation/MongoSyncBucketStorage.ts +70 -56
  124. package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
  125. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  126. package/src/storage/implementation/common/PersistedBatch.ts +7 -3
  127. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  128. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  129. package/src/storage/implementation/db.ts +11 -19
  130. package/src/storage/implementation/models.ts +4 -14
  131. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +55 -34
  132. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  133. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +70 -64
  134. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  135. package/src/storage/implementation/v1/models.ts +11 -1
  136. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +358 -314
  137. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  138. package/src/storage/implementation/v3/MongoCompactorV3.ts +646 -29
  139. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  140. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +315 -157
  141. package/src/storage/implementation/v3/PersistedBatchV3.ts +62 -36
  142. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  143. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  144. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +41 -36
  145. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  146. package/src/storage/implementation/v3/chunking.ts +38 -0
  147. package/src/storage/implementation/v3/models.ts +93 -55
  148. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  149. package/src/storage/storage-index.ts +12 -4
  150. package/src/utils/test-utils.ts +6 -3
  151. package/src/utils/util.ts +3 -2
  152. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  153. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  154. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  155. package/test/src/storage.test.ts +11 -11
  156. package/test/src/storage_compacting.test.ts +1956 -6
  157. package/test/src/storage_sync.test.ts +1077 -68
  158. package/test/src/storeCurrentData.test.ts +17 -7
  159. package/test/src/util.ts +2 -1
  160. package/tsconfig.json +1 -4
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  163. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  164. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  165. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  166. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  167. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  168. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  169. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  170. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  171. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  172. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  173. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  174. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  175. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  176. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  177. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  178. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  179. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  180. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  181. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  182. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  183. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  184. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  185. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
@@ -1,30 +1,48 @@
1
- import { GetIntanceOptions, LEGACY_STORAGE_VERSION, storage } from '@powersync/service-core';
1
+ import {
2
+ GetIntanceOptions,
3
+ LEGACY_STORAGE_VERSION,
4
+ SingleSyncConfigBucketDefinitionMapping,
5
+ storage
6
+ } from '@powersync/service-core';
2
7
 
3
- import { DO_NOT_LOG, ErrorCode, ServiceError } from '@powersync/lib-services-framework';
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
 
14
+ import {
15
+ describeIncrementalSyncConfigUpdate,
16
+ formatIncrementalSyncConfigUpdateLog,
17
+ isCompatible
18
+ } from '@powersync/service-core';
9
19
  import { ObjectId } from 'bson';
10
- import { generateSlotName } from '../utils/util.js';
11
- import { BucketDefinitionMapping } from './implementation/BucketDefinitionMapping.js';
20
+ import { generateReplicationStreamName } from '../utils/util.js';
12
21
  import type { MongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
13
22
  import { createMongoSyncBucketStorage } from './implementation/createMongoSyncBucketStorage.js';
14
23
  import { PowerSyncMongo } from './implementation/db.js';
15
24
  import { getMongoStorageConfig, StorageConfig, SyncRuleDocumentBase } from './implementation/models.js';
16
25
  import { MongoChecksumOptions } from './implementation/MongoChecksums.js';
17
- import {
18
- MongoPersistedSyncRulesContentV1,
19
- MongoPersistedSyncRulesContentV3
20
- } from './implementation/MongoPersistedSyncRulesContent.js';
26
+ import { MongoPersistedReplicationStream } from './implementation/MongoPersistedReplicationStream.js';
21
27
  import { syncRuleStateUpdatePipeline } from './implementation/SyncRuleStateUpdate.js';
22
28
  import { SyncRuleDocumentV1 } from './implementation/v1/models.js';
23
29
  import { VersionedPowerSyncMongoV3 } from './implementation/v3/VersionedPowerSyncMongoV3.js';
24
- import { ReplicationStreamDocumentV3, SyncConfigDefinition } from './storage-index.js';
30
+ import { ReplicationStreamDocumentV3, SyncConfigDefinition, SyncRuleConfigStateV3 } from './storage-index.js';
25
31
 
26
32
  export interface MongoBucketStorageOptions {
27
- 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;
28
46
  }
29
47
 
30
48
  export class MongoBucketStorage extends storage.BucketStorageFactory {
@@ -32,8 +50,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
32
50
 
33
51
  private readonly client: mongo.MongoClient;
34
52
  private readonly session: mongo.ClientSession;
35
- // TODO: This is still Postgres specific and needs to be reworked
36
- public readonly slot_name_prefix: string;
53
+ public readonly replicationStreamNamePrefix: string;
37
54
 
38
55
  private activeStorageCache: MongoSyncBucketStorage | undefined;
39
56
 
@@ -41,51 +58,54 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
41
58
 
42
59
  constructor(
43
60
  db: PowerSyncMongo,
44
- options: {
45
- slot_name_prefix: string;
46
- },
47
- private internalOptions?: MongoBucketStorageOptions
61
+ private options: MongoBucketStorageOptions
48
62
  ) {
49
63
  super();
50
64
  this.client = db.client;
51
65
  this.db = db;
52
66
  this.session = this.client.startSession();
53
- this.slot_name_prefix = options.slot_name_prefix;
67
+ this.replicationStreamNamePrefix = options.replicationStreamNamePrefix;
54
68
  }
55
69
 
56
70
  async [Symbol.asyncDispose]() {
57
71
  // No-op
58
72
  }
59
73
 
60
- getInstance(syncRules: storage.PersistedSyncRulesContent, options?: GetIntanceOptions): MongoSyncBucketStorage {
61
- let { id, slot_name } = syncRules;
62
- if ((typeof id as any) == 'bigint') {
63
- 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`);
64
80
  }
65
- const storageConfig = (syncRules as MongoPersistedSyncRulesContentV1).getStorageConfig();
66
- const storage = createMongoSyncBucketStorage(
81
+ let { replicationStreamId, replicationStreamName } = replicationStream;
82
+ if ((typeof replicationStreamId as any) == 'bigint') {
83
+ replicationStreamId = Number(replicationStreamId);
84
+ }
85
+ const storageConfig = replicationStream.getStorageConfig();
86
+ const syncRuleStorage = createMongoSyncBucketStorage(
67
87
  this,
68
- id,
69
- syncRules as MongoPersistedSyncRulesContentV1,
70
- slot_name,
88
+ replicationStreamId,
89
+ replicationStream,
90
+ replicationStreamName,
71
91
  undefined,
72
92
  {
73
- ...this.internalOptions,
93
+ checksumOptions: this.options.checksumOptions,
74
94
  storageConfig
75
95
  }
76
96
  );
77
97
  if (!options?.skipLifecycleHooks) {
78
- this.iterateListeners((cb) => cb.syncStorageCreated?.(storage));
98
+ this.iterateListeners((cb) => cb.syncStorageCreated?.(syncRuleStorage));
79
99
  }
80
100
 
81
- storage.registerListener({
101
+ syncRuleStorage.registerListener({
82
102
  batchStarted: (batch) => {
83
103
  batch.registerListener({
84
104
  replicationEvent: (payload) => this.iterateListeners((cb) => cb.replicationEvent?.(payload))
85
105
  });
86
106
  }
87
107
  });
88
- return storage;
108
+ return syncRuleStorage;
89
109
  }
90
110
 
91
111
  async getSystemIdentifier(): Promise<storage.BucketStorageSystemIdentifier> {
@@ -105,25 +125,25 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
105
125
  };
106
126
  }
107
127
 
108
- async restartReplication(sync_rules_group_id: number) {
109
- const next = await this.getNextSyncRulesContent();
110
- const active = await this.getActiveSyncRulesContent();
128
+ async restartReplication(replicationStreamId: number) {
129
+ const next = await this.getDeployingSyncConfig();
130
+ const active = await this.getActiveSyncConfig();
111
131
 
112
- if (next != null && next.id == sync_rules_group_id) {
132
+ if (next != null && next.content.replicationStreamId == replicationStreamId) {
113
133
  // We need to redo the "next" replication stream
114
- await this.updateSyncRules(next.asUpdateOptions());
134
+ await this.updateSyncRules(next.content.asUpdateOptions());
115
135
  // Pro-actively stop replicating
116
136
  await this.db.sync_rules.updateOne(
117
137
  {
118
- _id: next.id,
138
+ _id: next.content.replicationStreamId,
119
139
  state: storage.SyncRuleState.PROCESSING
120
140
  },
121
141
  syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP)
122
142
  );
123
143
  await this.db.notifyCheckpoint();
124
- } else if (next == null && active?.id == sync_rules_group_id) {
144
+ } else if (next == null && active?.content.replicationStreamId == replicationStreamId) {
125
145
  // Slot removed for "active" replication stream, while there is no "next" one.
126
- await this.updateSyncRules(active.asUpdateOptions());
146
+ await this.updateSyncRules(active.content.asUpdateOptions());
127
147
 
128
148
  // In this case we keep the old one as active for clients, so that that existing clients
129
149
  // can still get the latest data while we replicate the new ones.
@@ -131,18 +151,18 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
131
151
 
132
152
  await this.db.sync_rules.updateOne(
133
153
  {
134
- _id: active.id,
154
+ _id: active.content.replicationStreamId,
135
155
  state: storage.SyncRuleState.ACTIVE
136
156
  },
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
  },
148
168
  syncRuleStateUpdatePipeline(storage.SyncRuleState.ERRORED)
@@ -155,13 +175,54 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
155
175
  options: storage.UpdateSyncRulesOptions,
156
176
  storageVersion: number,
157
177
  storageConfig: StorageConfig
158
- ): Promise<MongoPersistedSyncRulesContentV3> {
159
- let rules: MongoPersistedSyncRulesContentV3 | undefined = undefined;
178
+ ): Promise<MongoPersistedReplicationStream> {
179
+ let rules: MongoPersistedReplicationStream | undefined = undefined;
160
180
  const versioned = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
161
181
 
162
182
  const session = this.session;
163
183
 
164
184
  await session.withTransaction(async () => {
185
+ const active = await this.db.sync_rules.findOne<ReplicationStreamDocumentV3>(
186
+ {
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
+
165
226
  // Only have a single replication stream with PROCESSING.
166
227
  await this.db.sync_rules.updateMany(
167
228
  {
@@ -188,9 +249,18 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
188
249
  );
189
250
 
190
251
  const id = Number(id_doc!.op_id);
191
- const slot_name = generateSlotName(this.slot_name_prefix, id);
192
-
193
- const mapping = BucketDefinitionMapping.fromParsedSyncRules(options.config.parsed);
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
+ );
194
264
 
195
265
  const syncConfigDoc: SyncConfigDefinition = {
196
266
  _id: new ObjectId(),
@@ -210,7 +280,6 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
210
280
  {
211
281
  _id: syncConfigDoc._id,
212
282
  state: storage.SyncRuleState.PROCESSING,
213
- keepalive_op: null,
214
283
  last_checkpoint: null,
215
284
  last_checkpoint_lsn: null,
216
285
  no_checkpoint_before: null,
@@ -219,7 +288,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
219
288
  ],
220
289
  snapshot_lsn: undefined,
221
290
  state: storage.SyncRuleState.PROCESSING,
222
- slot_name: slot_name,
291
+ slot_name: replicationStreamName,
223
292
  last_checkpoint_ts: null,
224
293
  last_fatal_error: null,
225
294
  last_fatal_error_ts: null,
@@ -227,20 +296,175 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
227
296
  };
228
297
 
229
298
  await this.db.sync_rules.insertOne(doc, { session });
230
- await this.db.notifyCheckpoint();
231
- rules = new MongoPersistedSyncRulesContentV3(this.db, doc, syncConfigDoc);
299
+ rules = new MongoPersistedReplicationStream(this.db, doc, [syncConfigDoc]);
232
300
  if (options.lock) {
233
301
  // The lock is persisted on rules.current_lock
234
302
  await rules.lock(session);
235
303
  }
236
304
  });
237
305
 
306
+ // Notify only after the transaction has committed, so listeners cannot observe pre-commit state.
307
+ await this.db.notifyCheckpoint();
308
+
238
309
  return rules!;
239
310
  }
240
311
 
241
- async updateSyncRules(
242
- options: storage.UpdateSyncRulesOptions
243
- ): Promise<MongoPersistedSyncRulesContentV1 | MongoPersistedSyncRulesContentV3> {
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
+ // The stream already exists, so an active replication job may already hold the stream lock.
462
+ // Deployment only persists the appended sync config; replication job locking is handled by
463
+ // the replicator.
464
+ return stream;
465
+ }
466
+
467
+ async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<MongoPersistedReplicationStream> {
244
468
  const storageVersion =
245
469
  options.storageVersion ?? options.config.parsed.config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
246
470
 
@@ -249,7 +473,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
249
473
  return this.updateSyncRulesV3(options, storageVersion, storageConfig);
250
474
  }
251
475
 
252
- let rules: MongoPersistedSyncRulesContentV1 | undefined = undefined;
476
+ let rules: MongoPersistedReplicationStream | undefined = undefined;
253
477
 
254
478
  const session = this.session;
255
479
 
@@ -280,7 +504,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
280
504
  );
281
505
 
282
506
  const id = Number(id_doc!.op_id);
283
- const slot_name = generateSlotName(this.slot_name_prefix, id);
507
+ const slot_name = generateReplicationStreamName(this.replicationStreamNamePrefix, id);
284
508
 
285
509
  const doc: SyncRuleDocumentV1 = {
286
510
  _id: id,
@@ -303,7 +527,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
303
527
 
304
528
  await this.db.sync_rules.insertOne(doc, { session });
305
529
  await this.db.notifyCheckpoint();
306
- rules = new MongoPersistedSyncRulesContentV1(this.db, doc);
530
+ rules = new MongoPersistedReplicationStream(this.db, doc);
307
531
  if (options.lock) {
308
532
  // The lock is persisted on rules.current_lock
309
533
  await rules.lock(session);
@@ -313,9 +537,7 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
313
537
  return rules!;
314
538
  }
315
539
 
316
- async getActiveSyncRulesContent(): Promise<
317
- MongoPersistedSyncRulesContentV1 | MongoPersistedSyncRulesContentV3 | null
318
- > {
540
+ async getActiveSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
319
541
  const doc = await this.db.sync_rules.findOne(
320
542
  {
321
543
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
@@ -323,10 +545,12 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
323
545
  { sort: { _id: -1 }, limit: 1 }
324
546
  );
325
547
 
326
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED]);
548
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED], {
549
+ cacheActiveStorage: true
550
+ });
327
551
  }
328
552
 
329
- private async getSyncRulesContent(doc: SyncRuleDocumentBase | null, stateFilter: storage.SyncRuleState[]) {
553
+ private async replicationStreamFromDoc(doc: SyncRuleDocumentBase | null, stateFilter: storage.SyncRuleState[]) {
330
554
  if (doc == null) {
331
555
  return null;
332
556
  }
@@ -334,8 +558,8 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
334
558
 
335
559
  if (storageConfig.incrementalReprocessing) {
336
560
  const v3 = doc as ReplicationStreamDocumentV3;
337
- const active = v3.sync_configs.find((c) => stateFilter.includes(c.state));
338
- if (active == null) {
561
+ const matching = v3.sync_configs.filter((c) => stateFilter.includes(c.state));
562
+ if (matching.length == 0) {
339
563
  return null;
340
564
  }
341
565
 
@@ -343,28 +567,33 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
343
567
  // that checks for active replication streams.
344
568
  // It is not a major bottleneck though, since it only runs once every couple of seconds at most.
345
569
  const db = this.db.versioned(storageConfig) as VersionedPowerSyncMongoV3;
346
- const syncConfigDoc = await db.syncConfigDefinitions.findOne({ _id: active._id });
347
- if (syncConfigDoc == null) {
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) {
348
577
  return null;
349
578
  }
350
- return new MongoPersistedSyncRulesContentV3(this.db, v3, syncConfigDoc);
579
+ return new MongoPersistedReplicationStream(this.db, v3, syncConfigDocs);
351
580
  }
352
581
 
353
- return new MongoPersistedSyncRulesContentV1(this.db, doc as SyncRuleDocumentV1);
582
+ return new MongoPersistedReplicationStream(this.db, doc as SyncRuleDocumentV1);
354
583
  }
355
584
 
356
- async getNextSyncRulesContent(): Promise<MongoPersistedSyncRulesContentV1 | MongoPersistedSyncRulesContentV3 | null> {
585
+ async getDeployingSyncConfig(): Promise<storage.ResolvedSyncConfig | null> {
357
586
  const doc = await this.db.sync_rules.findOne(
358
587
  {
359
- state: storage.SyncRuleState.PROCESSING
588
+ $or: [{ state: storage.SyncRuleState.PROCESSING }, { 'sync_configs.state': storage.SyncRuleState.PROCESSING }]
360
589
  },
361
590
  { sort: { _id: -1 }, limit: 1 }
362
591
  );
363
592
 
364
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.PROCESSING]);
593
+ return this.resolvedSyncConfigFromDoc(doc, [storage.SyncRuleState.PROCESSING]);
365
594
  }
366
595
 
367
- async getReplicatingSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
596
+ async getReplicatingReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
368
597
  const docs = await this.db.sync_rules
369
598
  .find({
370
599
  state: { $in: [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE] }
@@ -374,13 +603,13 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
374
603
  return (
375
604
  await Promise.all(
376
605
  docs.map((doc) => {
377
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
606
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE]);
378
607
  })
379
608
  )
380
609
  ).filter((r) => r != null);
381
610
  }
382
611
 
383
- async getStoppedSyncRules(): Promise<storage.PersistedSyncRulesContent[]> {
612
+ async getStoppedReplicationStreams(): Promise<storage.PersistedReplicationStream[]> {
384
613
  const docs = await this.db.sync_rules
385
614
  .find({
386
615
  state: storage.SyncRuleState.STOP
@@ -390,28 +619,46 @@ export class MongoBucketStorage extends storage.BucketStorageFactory {
390
619
  return (
391
620
  await Promise.all(
392
621
  docs.map((doc) => {
393
- return this.getSyncRulesContent(doc, [storage.SyncRuleState.STOP]);
622
+ return this.replicationStreamFromDoc(doc, [storage.SyncRuleState.STOP]);
394
623
  })
395
624
  )
396
625
  ).filter((d) => d != null);
397
626
  }
398
627
 
399
- async getActiveStorage(): Promise<MongoSyncBucketStorage | null> {
400
- const content = await this.getActiveSyncRulesContent();
401
- 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) {
402
635
  return null;
403
636
  }
404
637
 
405
- // It is important that this instance is cached.
406
- // Not for the instance construction itself, but to ensure that internal caches on the instance
407
- // are re-used properly.
408
- if (this.activeStorageCache?.group_id == content.id) {
409
- return this.activeStorageCache;
410
- } else {
411
- const instance = this.getInstance(content);
412
- this.activeStorageCache = instance;
413
- return instance;
414
- }
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
+ };
415
662
  }
416
663
 
417
664
  async getStorageMetrics(): Promise<storage.StorageMetrics> {