@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716
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.
- package/CHANGELOG.md +129 -7
- package/dist/storage/MongoBucketStorage.d.ts +42 -5
- package/dist/storage/MongoBucketStorage.js +404 -215
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
- package/dist/storage/implementation/MongoBucketBatch.js +169 -98
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
- package/dist/storage/implementation/MongoIdSequence.js +14 -1
- package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
- package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
- package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +35 -2
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +8 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
- package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
- package/dist/storage/implementation/MongoWriteBatch.js +149 -0
- package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
- package/dist/storage/implementation/common/PersistedBatch.js +57 -14
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +9 -1
- package/dist/storage/implementation/db.js +28 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +35 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +11 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
- package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +66 -17
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
- package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +14 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
- package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +108 -0
- package/dist/storage/implementation/v3/compact-utils.js +223 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +90 -6
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
- package/dist/storage/implementation/v3/source-table-utils.js +49 -57
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +28 -1
- package/dist/types/types.js +28 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +4 -2
- package/dist/utils/test-utils.js +7 -4
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +545 -277
- package/src/storage/implementation/MongoBucketBatch.ts +258 -124
- package/src/storage/implementation/MongoCompactor.ts +37 -346
- package/src/storage/implementation/MongoIdSequence.ts +21 -1
- package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
- package/src/storage/implementation/MongoStorageProvider.ts +16 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
- package/src/storage/implementation/MongoWriteBatch.ts +172 -0
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +76 -20
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +36 -2
- package/src/storage/implementation/models.ts +39 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
- package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
- package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +339 -247
- package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +329 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +97 -4
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +9 -5
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +794 -205
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
- package/test/src/compact-utils.test.ts +251 -0
- package/test/src/concurrent_writers.test.ts +599 -0
- package/test/src/connection_config.test.ts +159 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/helpers/concurrentWriter.mjs +29 -0
- package/test/src/helpers/s3TestFactory.ts +12 -12
- package/test/src/object_storage_usage.test.ts +268 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +280 -24
- package/test/src/storage_compacting.test.ts +1097 -253
- package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
- package/test/src/storage_s3_checksums.test.ts +6 -7
- package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
- package/test/src/storage_s3_reading.test.ts +294 -6
- package/test/src/storage_s3_writing.test.ts +4 -3
- package/test/src/storage_sync.test.ts +412 -80
- package/test/src/storeCurrentData.test.ts +5 -5
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -20,13 +20,17 @@ export abstract class MongoPersistedSyncConfigContentBase extends storage.Persis
|
|
|
20
20
|
options: Omit<storage.PersistedSyncConfigContentData, 'syncConfigId'> & {
|
|
21
21
|
mapping: SingleSyncConfigBucketDefinitionMapping;
|
|
22
22
|
syncConfigId: bson.ObjectId | null;
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
syncConfigParser: storage.SyncConfigParser
|
|
24
25
|
) {
|
|
25
26
|
const { mapping, syncConfigId, ...base } = options;
|
|
26
|
-
super(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
super(
|
|
28
|
+
{
|
|
29
|
+
...base,
|
|
30
|
+
syncConfigId: syncConfigId?.toHexString() ?? null
|
|
31
|
+
},
|
|
32
|
+
syncConfigParser
|
|
33
|
+
);
|
|
30
34
|
this.mapping = mapping;
|
|
31
35
|
this.syncConfigObjectId = syncConfigId;
|
|
32
36
|
}
|
|
@@ -60,18 +64,23 @@ export abstract class MongoPersistedSyncConfigContentBase extends storage.Persis
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
export class MongoPersistedSyncConfigContentV1 extends MongoPersistedSyncConfigContentBase {
|
|
63
|
-
constructor(db: PowerSyncMongo, doc: SyncRuleDocumentV1) {
|
|
64
|
-
super(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
constructor(db: PowerSyncMongo, doc: SyncRuleDocumentV1, syncConfigParser: storage.SyncConfigParser) {
|
|
68
|
+
super(
|
|
69
|
+
db,
|
|
70
|
+
{
|
|
71
|
+
replicationStreamId: doc._id,
|
|
72
|
+
sync_rules_content: doc.content,
|
|
73
|
+
compiled_plan: doc.serialized_plan ?? null,
|
|
74
|
+
// Handle legacy values
|
|
75
|
+
replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
|
|
76
|
+
storageVersion: doc.storage_version ?? storage.LEGACY_STORAGE_VERSION,
|
|
77
|
+
mapping: new SingleSyncConfigBucketDefinitionMapping(),
|
|
78
|
+
syncConfigId: null,
|
|
79
|
+
syncConfigState: doc.state,
|
|
80
|
+
version_label: doc.version_label ?? undefined
|
|
81
|
+
},
|
|
82
|
+
syncConfigParser
|
|
83
|
+
);
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
|
|
@@ -86,22 +95,32 @@ export class MongoPersistedSyncConfigContentV1 extends MongoPersistedSyncConfigC
|
|
|
86
95
|
export class MongoPersistedSyncConfigContentV3 extends MongoPersistedSyncConfigContentBase {
|
|
87
96
|
declare public readonly syncConfigObjectId: bson.ObjectId;
|
|
88
97
|
|
|
89
|
-
constructor(
|
|
98
|
+
constructor(
|
|
99
|
+
db: PowerSyncMongo,
|
|
100
|
+
doc: ReplicationStreamDocumentV3,
|
|
101
|
+
config: SyncConfigDefinition,
|
|
102
|
+
syncConfigParser: storage.SyncConfigParser
|
|
103
|
+
) {
|
|
90
104
|
const state = doc.sync_configs.find((c) => c._id.equals(config._id));
|
|
91
105
|
if (state == null) {
|
|
92
106
|
throw new ServiceAssertionError(`Cannot find sync config ${config._id} in replication stream ${doc._id}`);
|
|
93
107
|
}
|
|
94
|
-
super(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
super(
|
|
109
|
+
db,
|
|
110
|
+
{
|
|
111
|
+
replicationStreamId: doc._id,
|
|
112
|
+
sync_rules_content: config.content,
|
|
113
|
+
compiled_plan: config.serialized_plan ?? null,
|
|
114
|
+
|
|
115
|
+
replicationStreamName: doc.slot_name ?? `powersync_${doc._id}`,
|
|
116
|
+
storageVersion: doc.storage_version,
|
|
117
|
+
mapping: SingleSyncConfigBucketDefinitionMapping.fromPersistedMapping(config.rule_mapping),
|
|
118
|
+
syncConfigId: config._id,
|
|
119
|
+
syncConfigState: state.state,
|
|
120
|
+
version_label: config.version_label
|
|
121
|
+
},
|
|
122
|
+
syncConfigParser
|
|
123
|
+
);
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { POWERSYNC_VERSION, storage } from '@powersync/service-core';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MongoStorageConfig,
|
|
6
|
+
normalizeChunkCompactionConcurrency,
|
|
7
|
+
normalizeClearBatchThrottleRate
|
|
8
|
+
} from '../../types/types.js';
|
|
5
9
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
6
10
|
import { MongoReportStorage } from '../MongoReportStorage.js';
|
|
7
11
|
import { PowerSyncMongo } from './db.js';
|
|
@@ -24,6 +28,10 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
const decodedConfig = MongoStorageConfig.decode(storage as any);
|
|
31
|
+
const chunkCompactionConcurrency = normalizeChunkCompactionConcurrency(
|
|
32
|
+
decodedConfig.chunk_compaction_concurrency,
|
|
33
|
+
decodedConfig.object_storage != null
|
|
34
|
+
);
|
|
27
35
|
|
|
28
36
|
let objectStorage: ObjectStorage | undefined;
|
|
29
37
|
if (decodedConfig.object_storage?.type === 's3') {
|
|
@@ -37,7 +45,8 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
37
45
|
forcePathStyle: decodedConfig.object_storage.force_path_style,
|
|
38
46
|
accessKeyId: decodedConfig.object_storage.access_key_id,
|
|
39
47
|
secretAccessKey: decodedConfig.object_storage.secret_access_key,
|
|
40
|
-
concurrencyLimit: decodedConfig.object_storage.concurrency_limit
|
|
48
|
+
concurrencyLimit: decodedConfig.object_storage.concurrency_limit,
|
|
49
|
+
defaultsMode: decodedConfig.object_storage.defaults_mode
|
|
41
50
|
});
|
|
42
51
|
}
|
|
43
52
|
|
|
@@ -46,6 +55,8 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
46
55
|
maxPoolSize: resolvedConfig.storage.max_pool_size ?? 8
|
|
47
56
|
});
|
|
48
57
|
|
|
58
|
+
const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
|
|
59
|
+
|
|
49
60
|
let shuttingDown = false;
|
|
50
61
|
|
|
51
62
|
// Explicitly connect on startup.
|
|
@@ -55,7 +66,6 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
55
66
|
// Errors here will cause the process to exit.
|
|
56
67
|
await client.connect();
|
|
57
68
|
|
|
58
|
-
const database = new PowerSyncMongo(client, { database: resolvedConfig.storage.database });
|
|
59
69
|
const readPreference =
|
|
60
70
|
decodedConfig.bulk_read_preference == null
|
|
61
71
|
? undefined
|
|
@@ -64,11 +74,13 @@ export class MongoStorageProvider implements storage.StorageProvider {
|
|
|
64
74
|
// 90 is the minimum value.
|
|
65
75
|
maxStalenessSeconds: decodedConfig.bulk_read_preference == 'primary' ? undefined : 90
|
|
66
76
|
});
|
|
67
|
-
const syncStorageFactory = new MongoBucketStorage(database, {
|
|
77
|
+
const syncStorageFactory = new MongoBucketStorage(database, options.syncConfigParser, {
|
|
78
|
+
chunkCompactionConcurrency,
|
|
68
79
|
replicationStreamNamePrefix: resolvedConfig.slot_name_prefix,
|
|
69
80
|
readPreference,
|
|
70
81
|
clearBatchThrottleRate: normalizeClearBatchThrottleRate(decodedConfig.clear_batch_throttle_rate),
|
|
71
82
|
checksumCacheTtlMs: resolvedConfig.api_parameters.bucket_count_cache_ttl_minutes * 60_000,
|
|
83
|
+
defaultStorageVersion: decodedConfig.default_storage_version,
|
|
72
84
|
// Right now, only MongoDB source databases supports incremental reprocessing.
|
|
73
85
|
// Remove this filter when we support it for other source databases.
|
|
74
86
|
// This assumes a single source connection - revisit if we ever support multiple connections.
|
|
@@ -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,8 @@ 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 {
|
|
46
|
+
import { MongoSyncRulesLock } from './MongoSyncRulesLock.js';
|
|
47
|
+
import { MongoCheckpointAPIOptions, MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
41
48
|
import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
|
|
42
49
|
|
|
43
50
|
export interface MongoSyncBucketStorageOptions {
|
|
@@ -50,6 +57,16 @@ export interface MongoSyncBucketStorageOptions {
|
|
|
50
57
|
inlineThresholdBytes?: number;
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The stream state read for a checkpoint. All fields must come from the same snapshot.
|
|
62
|
+
*/
|
|
63
|
+
export interface MongoCheckpointState {
|
|
64
|
+
checkpoint: InternalOpId;
|
|
65
|
+
lsn: string | null;
|
|
66
|
+
/** See {@link MongoSyncBucketStorageCheckpoint.parameterChangesInvalidBefore}. */
|
|
67
|
+
parameterChangesInvalidBefore: InternalOpId;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
interface InternalCheckpointChanges extends CheckpointChanges {
|
|
54
71
|
updatedWriteCheckpoints: Map<string, bigint>;
|
|
55
72
|
invalidateWriteCheckpoints: boolean;
|
|
@@ -93,6 +110,10 @@ export abstract class MongoSyncBucketStorage
|
|
|
93
110
|
public readonly readPreference: mongo.ReadPreference | undefined;
|
|
94
111
|
public readonly clearBatchThrottleRate: number;
|
|
95
112
|
#storageInitialized = false;
|
|
113
|
+
/**
|
|
114
|
+
* Required for writing, optional for reading.
|
|
115
|
+
*/
|
|
116
|
+
private readonly replicationLock: MongoSyncRulesLock | null;
|
|
96
117
|
|
|
97
118
|
constructor(
|
|
98
119
|
public readonly factory: MongoBucketStorage,
|
|
@@ -103,6 +124,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
103
124
|
options: MongoSyncBucketStorageOptions
|
|
104
125
|
) {
|
|
105
126
|
super();
|
|
127
|
+
this.replicationLock = replicationStream.current_lock;
|
|
106
128
|
this.storageConfig = options.storageConfig;
|
|
107
129
|
this.objectStorage = options.objectStorage;
|
|
108
130
|
// Keep small chunks inline in MongoDB rather than offloading them to S3.
|
|
@@ -112,12 +134,27 @@ export abstract class MongoSyncBucketStorage
|
|
|
112
134
|
this.clearBatchThrottleRate = options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE;
|
|
113
135
|
this.db = factory.db.versioned(this.storageConfig);
|
|
114
136
|
this.checksums = this.createMongoChecksums(options);
|
|
115
|
-
this.writeCheckpointAPI =
|
|
137
|
+
this.writeCheckpointAPI = this.createWriteCheckpointAPI({
|
|
116
138
|
db: this.db,
|
|
117
|
-
mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED,
|
|
118
|
-
|
|
139
|
+
writeCheckpointMode: { mode: writeCheckpointMode ?? storage.WriteCheckpointMode.MANAGED },
|
|
140
|
+
replicationStreamId
|
|
119
141
|
});
|
|
120
|
-
|
|
142
|
+
|
|
143
|
+
// Include both replication stream name and sync config version labels in log prefix.
|
|
144
|
+
// Examples:
|
|
145
|
+
// No sync config with a label: [stream]
|
|
146
|
+
// All sync configs have a label: [stream][v5,v6]
|
|
147
|
+
// Mixed: [stream][?,v6]
|
|
148
|
+
const hasVersionLabel = replicationStream.syncConfigContent.some((config) => config.version_label != null);
|
|
149
|
+
const versionLabels = hasVersionLabel
|
|
150
|
+
? replicationStream.syncConfigContent.map((config) => config.version_label ?? '?')
|
|
151
|
+
: [];
|
|
152
|
+
this.logger =
|
|
153
|
+
versionLabels.length == 0
|
|
154
|
+
? replicationStream.logger
|
|
155
|
+
: defaultLogger.child({
|
|
156
|
+
prefix: `[${replicationStreamName}][${versionLabels.join(',')}] `
|
|
157
|
+
});
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
/**
|
|
@@ -128,11 +165,20 @@ export abstract class MongoSyncBucketStorage
|
|
|
128
165
|
abstract createMongoCompactor(options: MongoCompactOptions): MongoCompactor;
|
|
129
166
|
|
|
130
167
|
protected abstract createMongoChecksums(options: MongoSyncBucketStorageOptions): MongoChecksums;
|
|
168
|
+
protected createWriteCheckpointAPI(options: MongoCheckpointAPIOptions): MongoWriteCheckpointAPI {
|
|
169
|
+
return new MongoWriteCheckpointAPI(options);
|
|
170
|
+
}
|
|
171
|
+
|
|
131
172
|
protected abstract createMongoParameterCompactor(
|
|
132
173
|
checkpoint: InternalOpId,
|
|
133
174
|
options: storage.CompactOptions
|
|
134
175
|
): MongoParameterCompactor;
|
|
135
176
|
|
|
177
|
+
/** MongoDB parameter compaction uses a persisted operation-id cursor. */
|
|
178
|
+
public supportsIncrementalParameterCompaction(): boolean {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
get writeCheckpointMode() {
|
|
137
183
|
return this.writeCheckpointAPI.writeCheckpointMode;
|
|
138
184
|
}
|
|
@@ -144,8 +190,8 @@ export abstract class MongoSyncBucketStorage
|
|
|
144
190
|
return this.replicationStream.storageIds;
|
|
145
191
|
}
|
|
146
192
|
|
|
147
|
-
setWriteCheckpointMode(
|
|
148
|
-
this.writeCheckpointAPI.setWriteCheckpointMode(
|
|
193
|
+
setWriteCheckpointMode(config: storage.WriteCheckpointModeConfig): void {
|
|
194
|
+
this.writeCheckpointAPI.setWriteCheckpointMode(config);
|
|
149
195
|
}
|
|
150
196
|
|
|
151
197
|
createManagedWriteCheckpoints(
|
|
@@ -178,9 +224,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
178
224
|
return (await this.getCheckpointInternal()) ?? new EmptyReplicationCheckpoint();
|
|
179
225
|
}
|
|
180
226
|
|
|
181
|
-
protected abstract fetchCheckpointState(
|
|
182
|
-
session: mongo.ClientSession
|
|
183
|
-
): Promise<{ checkpoint: bigint; lsn: string | null } | null>;
|
|
227
|
+
protected abstract fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null>;
|
|
184
228
|
|
|
185
229
|
async getCheckpointInternal(): Promise<storage.ReplicationCheckpoint | null> {
|
|
186
230
|
return await this.db.client.withSession({ snapshot: true }, async (session) => {
|
|
@@ -197,7 +241,14 @@ export abstract class MongoSyncBucketStorage
|
|
|
197
241
|
if (clusterTime == null) {
|
|
198
242
|
throw new ServiceAssertionError('Missing clusterTime in getCheckpoint()');
|
|
199
243
|
}
|
|
200
|
-
return new MongoReplicationCheckpoint(
|
|
244
|
+
return new MongoReplicationCheckpoint(
|
|
245
|
+
this,
|
|
246
|
+
state.checkpoint,
|
|
247
|
+
state.lsn,
|
|
248
|
+
snapshotTime,
|
|
249
|
+
clusterTime,
|
|
250
|
+
state.parameterChangesInvalidBefore
|
|
251
|
+
);
|
|
201
252
|
});
|
|
202
253
|
}
|
|
203
254
|
|
|
@@ -224,23 +275,36 @@ export abstract class MongoSyncBucketStorage
|
|
|
224
275
|
* The version-independent part of the batch options.
|
|
225
276
|
*/
|
|
226
277
|
protected writerBatchOptions(options: storage.CreateWriterOptions): Omit<MongoBucketBatchOptions, 'resumeFromLsn'> {
|
|
278
|
+
const replicationLock = this.requireReplicationLock();
|
|
227
279
|
return {
|
|
228
280
|
logger: options.logger ?? this.logger,
|
|
229
281
|
db: this.db,
|
|
230
282
|
parsedSyncConfig: this.getParsedSyncConfigSet(options),
|
|
231
283
|
replicationStreamId: this.replicationStreamId,
|
|
232
284
|
replicationStreamName: this.replicationStreamName,
|
|
285
|
+
replicationLock,
|
|
286
|
+
opIdAllocator: this.factory.getOpIdAllocator(this.replicationStream, replicationLock),
|
|
233
287
|
storeCurrentData: options.storeCurrentData,
|
|
234
288
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
235
289
|
markRecordUnavailable: options.markRecordUnavailable,
|
|
236
290
|
hooks: options.hooks,
|
|
237
291
|
tracer: options.tracer,
|
|
292
|
+
signal: options.signal,
|
|
238
293
|
objectStorage: this.objectStorage,
|
|
239
294
|
inlineThresholdBytes: this.inlineThresholdBytes
|
|
240
295
|
};
|
|
241
296
|
}
|
|
242
297
|
|
|
298
|
+
private requireReplicationLock(): MongoSyncRulesLock {
|
|
299
|
+
if (this.replicationLock == null) {
|
|
300
|
+
throw new ServiceAssertionError('A replication lease is required to create a writer');
|
|
301
|
+
}
|
|
302
|
+
this.replicationLock.throwIfAborted();
|
|
303
|
+
return this.replicationLock;
|
|
304
|
+
}
|
|
305
|
+
|
|
243
306
|
async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
|
|
307
|
+
this.requireReplicationLock();
|
|
244
308
|
await this.initializeStorage();
|
|
245
309
|
|
|
246
310
|
const writer = await this.createWriterImpl(options);
|
|
@@ -329,6 +393,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
329
393
|
protected abstract clearBucketState(signal?: AbortSignal): Promise<void>;
|
|
330
394
|
|
|
331
395
|
protected abstract clearSourceTables(signal?: AbortSignal): Promise<void>;
|
|
396
|
+
protected async clearCustomCheckpointRequests(_signal?: AbortSignal): Promise<void> {}
|
|
332
397
|
protected abstract clearSyncRuleState(): Promise<void>;
|
|
333
398
|
|
|
334
399
|
async clear(options?: storage.ClearStorageOptions): Promise<void> {
|
|
@@ -338,6 +403,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
338
403
|
throw new ReplicationAbortedError('Aborted clearing data', signal.reason);
|
|
339
404
|
}
|
|
340
405
|
|
|
406
|
+
this.factory.discardOpIdAllocator(this.replicationStreamId);
|
|
341
407
|
await this.clearSyncRuleState();
|
|
342
408
|
|
|
343
409
|
await this.clearBucketData(signal);
|
|
@@ -345,6 +411,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
345
411
|
await this.clearSourceRecords(signal);
|
|
346
412
|
await this.clearBucketState(signal);
|
|
347
413
|
await this.clearSourceTables(signal);
|
|
414
|
+
await this.clearCustomCheckpointRequests(signal);
|
|
348
415
|
|
|
349
416
|
this.#storageInitialized = false;
|
|
350
417
|
}
|
|
@@ -367,43 +434,30 @@ export abstract class MongoSyncBucketStorage
|
|
|
367
434
|
async compact(options?: storage.CompactOptions) {
|
|
368
435
|
let maxOpId = options?.maxOpId;
|
|
369
436
|
if (maxOpId == null) {
|
|
437
|
+
// For PROCESSING streams, this will be undefined.
|
|
370
438
|
const checkpoint = await this.getCheckpointInternal();
|
|
371
439
|
maxOpId = checkpoint?.checkpoint ?? undefined;
|
|
372
440
|
}
|
|
373
441
|
await this.createMongoCompactor({ ...options, maxOpId, logger: this.logger }).compact();
|
|
374
442
|
|
|
375
|
-
if (maxOpId != null && options?.compactParameterData) {
|
|
376
|
-
|
|
443
|
+
if (maxOpId != null && options?.compactParameterData && this.replicationStream.state == SyncRuleState.ACTIVE) {
|
|
444
|
+
// Use the stream-scoped logger, matching bucket compaction above.
|
|
445
|
+
await this.createMongoParameterCompactor(maxOpId, { ...options, logger: this.logger }).compact();
|
|
377
446
|
}
|
|
378
447
|
}
|
|
379
448
|
|
|
449
|
+
abstract compactInitialReplication(
|
|
450
|
+
options: CompactInitialReplicationOptions
|
|
451
|
+
): Promise<CompactInitialReplicationResults>;
|
|
452
|
+
|
|
380
453
|
/**
|
|
381
454
|
* The highest op id persisted for this stream, whether or not covered by a checkpoint.
|
|
382
455
|
*
|
|
383
|
-
* Used as the default `maxOpId` for {@link
|
|
456
|
+
* Used as the default `maxOpId` for {@link compactInitialReplication}, which runs after
|
|
384
457
|
* initial replication but before the first checkpoint exists.
|
|
385
458
|
*/
|
|
386
459
|
protected abstract fetchPersistedOpHead(): Promise<InternalOpId | null>;
|
|
387
460
|
|
|
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
461
|
private async *watchActiveCheckpoint(signal: AbortSignal): AsyncIterable<ReplicationCheckpoint> {
|
|
408
462
|
if (signal.aborted) {
|
|
409
463
|
return;
|
|
@@ -580,13 +634,31 @@ export abstract class MongoSyncBucketStorage
|
|
|
580
634
|
}
|
|
581
635
|
|
|
582
636
|
protected abstract getParameterBucketChangesImpl(
|
|
583
|
-
options:
|
|
637
|
+
options: MongoGetCheckpointChangesOptions
|
|
584
638
|
): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>>;
|
|
585
639
|
|
|
586
640
|
private async getParameterBucketChanges(
|
|
587
641
|
options: GetCheckpointChangesOptions
|
|
588
642
|
): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
|
|
589
|
-
|
|
643
|
+
const nextCheckpoint = requireMongoCheckpoint(options.nextCheckpoint);
|
|
644
|
+
if (options.lastCheckpoint.checkpoint < nextCheckpoint.parameterChangesInvalidBefore) {
|
|
645
|
+
// Parameter compaction may have deleted parameter entries in the range we'd have to query
|
|
646
|
+
// to find the individual changed lookups. Invalidate all parameter buckets instead.
|
|
647
|
+
//
|
|
648
|
+
// The fence is committed before the first delete of a compaction pass, and captured in the
|
|
649
|
+
// same snapshot as the checkpoint, so a checkpoint that could miss a deleted entry always
|
|
650
|
+
// observes the fence as well.
|
|
651
|
+
return {
|
|
652
|
+
invalidateParameterBuckets: true,
|
|
653
|
+
updatedParameterLookups: new Set<string>()
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// The query below runs at the checkpoint snapshot, which still sees entries deleted by a
|
|
657
|
+
// compaction pass that started after the snapshot.
|
|
658
|
+
return this.getParameterBucketChangesImpl({
|
|
659
|
+
lastCheckpoint: options.lastCheckpoint,
|
|
660
|
+
nextCheckpoint
|
|
661
|
+
});
|
|
590
662
|
}
|
|
591
663
|
|
|
592
664
|
private checkpointChangesCache = new LRUCache<
|
|
@@ -611,7 +683,10 @@ export abstract class MongoSyncBucketStorage
|
|
|
611
683
|
});
|
|
612
684
|
|
|
613
685
|
async getCheckpointChanges(options: GetCheckpointChangesOptions): Promise<InternalCheckpointChanges> {
|
|
614
|
-
|
|
686
|
+
// The invalidation fence is part of the identity: the same checkpoint pair read before and
|
|
687
|
+
// after a compaction pass produces different results (specific lookups vs. invalidate-all).
|
|
688
|
+
const fence = requireMongoCheckpoint(options.nextCheckpoint).parameterChangesInvalidBefore;
|
|
689
|
+
const key = `${options.lastCheckpoint.checkpoint}_${options.lastCheckpoint.lsn}__${options.nextCheckpoint.checkpoint}_${options.nextCheckpoint.lsn}_${fence}`;
|
|
615
690
|
const result = await this.checkpointChangesCache.fetch(key, { context: { options } });
|
|
616
691
|
return result!;
|
|
617
692
|
}
|
|
@@ -629,7 +704,19 @@ export abstract class MongoSyncBucketStorage
|
|
|
629
704
|
}
|
|
630
705
|
}
|
|
631
706
|
|
|
632
|
-
|
|
707
|
+
/**
|
|
708
|
+
* We don't support any other constructions of ReplicationCheckpoint.
|
|
709
|
+
*/
|
|
710
|
+
function requireMongoCheckpoint(checkpoint: ReplicationCheckpoint): MongoReplicationCheckpoint {
|
|
711
|
+
if (!(checkpoint instanceof MongoReplicationCheckpoint)) {
|
|
712
|
+
throw new ServiceAssertionError(
|
|
713
|
+
`Checkpoint changes require a checkpoint from getCheckpointInternal(), got ${checkpoint.constructor.name}`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
return checkpoint;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
class MongoReplicationCheckpoint implements MongoSyncBucketStorageCheckpoint {
|
|
633
720
|
#storage: MongoSyncBucketStorage;
|
|
634
721
|
|
|
635
722
|
constructor(
|
|
@@ -637,7 +724,9 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
|
|
|
637
724
|
public readonly checkpoint: InternalOpId,
|
|
638
725
|
public readonly lsn: string | null,
|
|
639
726
|
public snapshotTime: mongo.Timestamp,
|
|
640
|
-
public clusterTime: mongo.ClusterTime
|
|
727
|
+
public clusterTime: mongo.ClusterTime,
|
|
728
|
+
/** Captured in the same snapshot as the checkpoint. */
|
|
729
|
+
public readonly parameterChangesInvalidBefore: InternalOpId
|
|
641
730
|
) {
|
|
642
731
|
this.#storage = storage;
|
|
643
732
|
}
|
|
@@ -647,6 +736,10 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
|
|
|
647
736
|
}
|
|
648
737
|
}
|
|
649
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Used when no checkpoint has been persisted yet. This has no snapshot or invalidation fence, so
|
|
741
|
+
* it cannot be used for checkpoint change detection - see {@link requireMongoCheckpoint}.
|
|
742
|
+
*/
|
|
650
743
|
class EmptyReplicationCheckpoint implements ReplicationCheckpoint {
|
|
651
744
|
readonly checkpoint: InternalOpId = 0n;
|
|
652
745
|
readonly lsn: string | null = null;
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
|
|
3
3
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
4
|
-
import { ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
|
|
5
|
-
import { storage } from '@powersync/service-core';
|
|
4
|
+
import { ErrorCode, Logger, ReplicationAbortedError, ServiceError } from '@powersync/lib-services-framework';
|
|
5
|
+
import { storage, utils } 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.
|
|
11
13
|
*/
|
|
12
14
|
export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
15
|
+
/** FIFO admission shared by this lease's writers; database fencing still enforces ownership. */
|
|
16
|
+
readonly writerMutex = new utils.Mutex();
|
|
17
|
+
|
|
18
|
+
private readonly abort = new AbortController();
|
|
19
|
+
readonly signal = this.abort.signal;
|
|
20
|
+
|
|
13
21
|
private readonly refreshInterval: NodeJS.Timeout;
|
|
14
22
|
|
|
15
23
|
/**
|
|
@@ -22,15 +30,20 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
22
30
|
): Promise<MongoSyncRulesLock> {
|
|
23
31
|
const lockId = crypto.randomBytes(8).toString('hex');
|
|
24
32
|
const doc = await db.sync_rules.findOneAndUpdate(
|
|
25
|
-
{ _id: sync_rules.replicationStreamId, $or: [{ lock: null }, { 'lock.expires_at': { $lt: new Date() } }] },
|
|
26
33
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
_id: sync_rules.replicationStreamId,
|
|
35
|
+
$or: [{ lock: null }, { $expr: { $lt: ['$lock.expires_at', '$$NOW'] } }]
|
|
36
|
+
},
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
$set: {
|
|
40
|
+
lock: {
|
|
41
|
+
id: lockId,
|
|
42
|
+
expires_at: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: LOCK_DURATION_MS } }
|
|
43
|
+
}
|
|
31
44
|
}
|
|
32
45
|
}
|
|
33
|
-
|
|
46
|
+
],
|
|
34
47
|
{
|
|
35
48
|
projection: { lock: 1 },
|
|
36
49
|
returnDocument: 'before',
|
|
@@ -60,13 +73,14 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
60
73
|
constructor(
|
|
61
74
|
private db: VersionedPowerSyncMongo,
|
|
62
75
|
public sync_rules_id: number,
|
|
63
|
-
|
|
76
|
+
public readonly lock_id: string,
|
|
64
77
|
private logger: Logger
|
|
65
78
|
) {
|
|
66
79
|
this.refreshInterval = setInterval(async () => {
|
|
67
80
|
try {
|
|
68
81
|
await this.refresh();
|
|
69
82
|
} catch (e) {
|
|
83
|
+
this.abort.abort(e);
|
|
70
84
|
this.logger.error('Failed to refresh lock', e);
|
|
71
85
|
clearInterval(this.refreshInterval);
|
|
72
86
|
}
|
|
@@ -74,6 +88,7 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
async release(): Promise<void> {
|
|
91
|
+
this.abort.abort(new Error('Replication lock released'));
|
|
77
92
|
clearInterval(this.refreshInterval);
|
|
78
93
|
const result = await this.db.sync_rules.updateOne(
|
|
79
94
|
{
|
|
@@ -90,15 +105,66 @@ export class MongoSyncRulesLock implements storage.ReplicationLock {
|
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
throwIfAborted(): void {
|
|
109
|
+
this.signal.throwIfAborted();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static ownerFilter(streamId: number, lock: MongoSyncRulesLock) {
|
|
113
|
+
lock.throwIfAborted();
|
|
114
|
+
return { _id: streamId, 'lock.id': lock.lock_id };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static heartbeatUpdate() {
|
|
118
|
+
// Always change the existing heartbeat, even for writes in the same
|
|
119
|
+
// millisecond. A no-op update is not sufficient for a transactional fence.
|
|
120
|
+
return {
|
|
121
|
+
last_keepalive_ts: {
|
|
122
|
+
$max: ['$$NOW', { $add: [{ $ifNull: ['$last_keepalive_ts', new Date(0)] }, 1] }]
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static assertOwned<T>(document: T | null): T {
|
|
128
|
+
if (document == null) {
|
|
129
|
+
throw new ReplicationAbortedError('Replication writer no longer owns the stream');
|
|
130
|
+
}
|
|
131
|
+
return document;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A write, not just an ownership read: takeover conflicts with every transaction
|
|
136
|
+
* that publishes under this owner. Every writer must hold a stream lease.
|
|
137
|
+
*/
|
|
138
|
+
static async fence(
|
|
139
|
+
db: VersionedPowerSyncMongo,
|
|
140
|
+
streamId: number,
|
|
141
|
+
lock: MongoSyncRulesLock,
|
|
142
|
+
session: mongo.ClientSession,
|
|
143
|
+
projection: mongo.Document = { last_persisted_op: 1, last_checkpoint: 1, keepalive_op: 1 }
|
|
144
|
+
) {
|
|
145
|
+
const doc = await db.sync_rules.findOneAndUpdate(
|
|
146
|
+
this.ownerFilter(streamId, lock),
|
|
147
|
+
[{ $set: this.heartbeatUpdate() }],
|
|
148
|
+
{ session, returnDocument: 'after', projection }
|
|
149
|
+
);
|
|
150
|
+
return this.assertOwned(doc);
|
|
151
|
+
}
|
|
152
|
+
|
|
93
153
|
private async refresh(): Promise<void> {
|
|
94
154
|
const result = await this.db.sync_rules.findOneAndUpdate(
|
|
95
155
|
{
|
|
96
156
|
_id: this.sync_rules_id,
|
|
97
157
|
'lock.id': this.lock_id
|
|
98
158
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
159
|
+
[
|
|
160
|
+
{
|
|
161
|
+
$set: {
|
|
162
|
+
'lock.expires_at': {
|
|
163
|
+
$dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: LOCK_DURATION_MS }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
],
|
|
102
168
|
{ returnDocument: 'after' }
|
|
103
169
|
);
|
|
104
170
|
if (result == null) {
|