@powersync/service-module-mongodb-storage 0.18.2 → 0.19.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.
- package/CHANGELOG.md +48 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +2 -0
- package/dist/storage/implementation/MongoChecksums.js +3 -2
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -5
- package/dist/storage/implementation/MongoSyncBucketStorage.js +25 -14
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +16 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +20 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +3 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +109 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +38 -1
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoChecksums.ts +5 -2
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +34 -24
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +16 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +54 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +3 -1
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +123 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +40 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +95 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
2
|
+
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
3
|
+
import { bucketRequest, test_utils } from '@powersync/service-core-tests';
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { DEFAULT_INLINE_THRESHOLD_BYTES } from '../../src/storage/implementation/common/PersistedBatch.js';
|
|
6
|
+
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
7
|
+
import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
8
|
+
import { env } from './env.js';
|
|
9
|
+
import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
|
|
10
|
+
|
|
11
|
+
const SYNC_RULES_YAML = `
|
|
12
|
+
bucket_definitions:
|
|
13
|
+
global:
|
|
14
|
+
data:
|
|
15
|
+
- SELECT id, description FROM items
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function s3Factory(threshold?: number) {
|
|
19
|
+
const memoryStorage = new MemoryObjectStorage();
|
|
20
|
+
const factoryGen = mongoTestStorageFactoryGenerator({
|
|
21
|
+
url: env.MONGO_TEST_URL,
|
|
22
|
+
isCI: env.CI,
|
|
23
|
+
objectStorage: memoryStorage,
|
|
24
|
+
inlineThresholdBytes: threshold
|
|
25
|
+
});
|
|
26
|
+
return { memoryStorage, factoryGen };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Object storage inline threshold', () => {
|
|
30
|
+
test('stores small documents inline using the default threshold', async () => {
|
|
31
|
+
const { memoryStorage, factoryGen } = s3Factory();
|
|
32
|
+
await using factory = await factoryGen.factory();
|
|
33
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
34
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
35
|
+
expect(bucketStorage.inlineThresholdBytes).toBe(DEFAULT_INLINE_THRESHOLD_BYTES);
|
|
36
|
+
|
|
37
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
38
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
39
|
+
await writer.markAllSnapshotDone('1/1');
|
|
40
|
+
|
|
41
|
+
await writer.save({
|
|
42
|
+
sourceTable,
|
|
43
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
44
|
+
after: { id: 'small1', description: 'tiny' },
|
|
45
|
+
afterReplicaId: test_utils.rid('small1')
|
|
46
|
+
});
|
|
47
|
+
await writer.save({
|
|
48
|
+
sourceTable,
|
|
49
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
50
|
+
after: { id: 'small2', description: 'tiny' },
|
|
51
|
+
afterReplicaId: test_utils.rid('small2')
|
|
52
|
+
});
|
|
53
|
+
await writer.commit('1/1');
|
|
54
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
55
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
56
|
+
|
|
57
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
58
|
+
const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
59
|
+
const documents = await db
|
|
60
|
+
.bucketData(bucketStorage.replicationStreamId, definitionId)
|
|
61
|
+
.find({ '_id.b': request.bucket })
|
|
62
|
+
.toArray();
|
|
63
|
+
expect(documents).not.toHaveLength(0);
|
|
64
|
+
for (const document of documents) {
|
|
65
|
+
expect(document.size).toBeLessThanOrEqual(DEFAULT_INLINE_THRESHOLD_BYTES);
|
|
66
|
+
expect(document.ops).toBeDefined();
|
|
67
|
+
expect(document.storage_ref).toBeUndefined();
|
|
68
|
+
}
|
|
69
|
+
expect(memoryStorage.store.size).toBe(0);
|
|
70
|
+
|
|
71
|
+
const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
|
|
72
|
+
const data = test_utils.getBatchData(batch);
|
|
73
|
+
expect(data.map((op) => op.object_id)).toEqual(['small1', 'small2']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('stores documents above a configured threshold in object storage', async () => {
|
|
77
|
+
const { memoryStorage, factoryGen } = s3Factory(256);
|
|
78
|
+
await using factory = await factoryGen.factory();
|
|
79
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
80
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
81
|
+
expect(bucketStorage.inlineThresholdBytes).toBe(256);
|
|
82
|
+
|
|
83
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
84
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
85
|
+
await writer.markAllSnapshotDone('1/1');
|
|
86
|
+
|
|
87
|
+
await writer.save({
|
|
88
|
+
sourceTable,
|
|
89
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
90
|
+
after: { id: 'large', description: 'value'.repeat(500) },
|
|
91
|
+
afterReplicaId: test_utils.rid('large')
|
|
92
|
+
});
|
|
93
|
+
await writer.commit('1/1');
|
|
94
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
95
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
96
|
+
|
|
97
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
98
|
+
const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
99
|
+
const documents = await db
|
|
100
|
+
.bucketData(bucketStorage.replicationStreamId, definitionId)
|
|
101
|
+
.find({ '_id.b': request.bucket })
|
|
102
|
+
.toArray();
|
|
103
|
+
expect(documents).not.toHaveLength(0);
|
|
104
|
+
for (const document of documents) {
|
|
105
|
+
expect(document.size).toBeGreaterThan(256);
|
|
106
|
+
expect(document.ops).toBeUndefined();
|
|
107
|
+
expect(document.storage_ref).toBeDefined();
|
|
108
|
+
}
|
|
109
|
+
expect(new Set(memoryStorage.store.keys())).toEqual(
|
|
110
|
+
new Set(documents.map((document) => document.storage_ref!.path))
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
|
|
114
|
+
expect(test_utils.getBatchData(batch).map((op) => op.object_id)).toEqual(['large']);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addChecksums,
|
|
3
|
+
CheckpointChecksumInvalidatedError,
|
|
4
|
+
storage,
|
|
5
|
+
updateSyncRulesFromYaml
|
|
6
|
+
} from '@powersync/service-core';
|
|
7
|
+
import { bucketRequest, test_utils } from '@powersync/service-core-tests';
|
|
8
|
+
import { describe, expect, test } from 'vitest';
|
|
9
|
+
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
10
|
+
import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
11
|
+
import { env } from './env.js';
|
|
12
|
+
import { createS3TestStorageSuite } from './helpers/s3TestFactory.js';
|
|
13
|
+
|
|
14
|
+
const SYNC_RULES_YAML = `
|
|
15
|
+
bucket_definitions:
|
|
16
|
+
global:
|
|
17
|
+
data:
|
|
18
|
+
- SELECT id, description FROM items
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function s3Factory() {
|
|
22
|
+
const { objectStorage, factoryGen } = createS3TestStorageSuite({ url: env.MONGO_TEST_URL, isCI: env.CI });
|
|
23
|
+
return { memoryStorage: objectStorage, factoryGen };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('V3 checksums with S3 object storage', () => {
|
|
27
|
+
test('partial checksum with start straddling S3-backed document', async () => {
|
|
28
|
+
const { factoryGen } = s3Factory();
|
|
29
|
+
await using factory = await factoryGen.factory();
|
|
30
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
31
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
32
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
33
|
+
|
|
34
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
35
|
+
const bucket = request.bucket;
|
|
36
|
+
const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
37
|
+
|
|
38
|
+
// Write 6 ops through S3 writer — all land in one S3-backed doc
|
|
39
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
40
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
41
|
+
await writer.markAllSnapshotDone('1/1');
|
|
42
|
+
for (const id of ['A', 'B', 'C', 'D', 'E', 'F']) {
|
|
43
|
+
await writer.save({
|
|
44
|
+
sourceTable,
|
|
45
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
46
|
+
after: { id, description: id },
|
|
47
|
+
afterReplicaId: test_utils.rid(id)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
await writer.commit('1/1');
|
|
51
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
52
|
+
|
|
53
|
+
// Baseline: full checksum with no compacted_state
|
|
54
|
+
const full = (await bucketStorage.getChecksums(checkpoint, [request])).get(bucket)!;
|
|
55
|
+
|
|
56
|
+
// Set compacted_state.op_id = 3 to create a partial range starting after op 3.
|
|
57
|
+
// The doc has min_op=1, _id.o=6 so is_fully_included=false for the range (3, 6].
|
|
58
|
+
await db.bucketState(bucketStorage.replicationStreamId).updateOne(
|
|
59
|
+
{ _id: { d: definitionId, b: bucket } },
|
|
60
|
+
{
|
|
61
|
+
$set: {
|
|
62
|
+
last_op: 3n,
|
|
63
|
+
compacted_state: { op_id: 3n, count: 0, checksum: 0n, bytes: null },
|
|
64
|
+
estimate_since_compact: { count: 3, bytes: 100 }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{ upsert: true }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const partial = (await bucketStorage.getChecksums(checkpoint, [request])).get(bucket)!;
|
|
71
|
+
expect(partial.checksum).toBe(full.checksum);
|
|
72
|
+
expect(partial.count).toBe(full.count);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('partial checksum with end straddling S3-backed document invalidates the checkpoint', async () => {
|
|
76
|
+
const { factoryGen } = s3Factory();
|
|
77
|
+
await using factory = await factoryGen.factory();
|
|
78
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
79
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
80
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
81
|
+
|
|
82
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
83
|
+
const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
84
|
+
|
|
85
|
+
// Write several operations into one S3-backed document that straddles the
|
|
86
|
+
// requested checkpoint.
|
|
87
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
88
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
89
|
+
await writer.markAllSnapshotDone('1/1');
|
|
90
|
+
for (let i = 1; i <= 12; i++) {
|
|
91
|
+
const id = `row${i}`;
|
|
92
|
+
await writer.save({
|
|
93
|
+
sourceTable,
|
|
94
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
95
|
+
after: { id, description: `value${i}`.repeat(50) },
|
|
96
|
+
afterReplicaId: test_utils.rid(id)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
await writer.commit('1/1');
|
|
100
|
+
const documents = await db
|
|
101
|
+
.bucketData(bucketStorage.replicationStreamId, definitionId)
|
|
102
|
+
.find({ '_id.b': request.bucket })
|
|
103
|
+
.toArray();
|
|
104
|
+
expect(documents).toHaveLength(1);
|
|
105
|
+
expect(documents[0].storage_ref).toBeDefined();
|
|
106
|
+
expect(documents[0].ops).toBeUndefined();
|
|
107
|
+
|
|
108
|
+
// The document starts before op 7 and ends after it.
|
|
109
|
+
await expect(bucketStorage.getChecksums(test_utils.testCheckpoint(7n), [request])).rejects.toBeInstanceOf(
|
|
110
|
+
CheckpointChecksumInvalidatedError
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('checksum preserved after CLEAR-producing S3 compaction', async () => {
|
|
115
|
+
const { factoryGen } = s3Factory();
|
|
116
|
+
await using factory = await factoryGen.factory();
|
|
117
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
118
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
119
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
120
|
+
|
|
121
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
122
|
+
const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
123
|
+
|
|
124
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
125
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
|
|
126
|
+
await writer.markAllSnapshotDone('1/1');
|
|
127
|
+
|
|
128
|
+
// A@1, A@2 get superseded by A@4 (same replica_id for dedup). B@3 independent.
|
|
129
|
+
await writer.save({
|
|
130
|
+
sourceTable,
|
|
131
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
132
|
+
after: { id: 'A', description: 'v1' },
|
|
133
|
+
afterReplicaId: test_utils.rid('A')
|
|
134
|
+
});
|
|
135
|
+
await writer.save({
|
|
136
|
+
sourceTable,
|
|
137
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
138
|
+
after: { id: 'A', description: 'v2' },
|
|
139
|
+
afterReplicaId: test_utils.rid('A')
|
|
140
|
+
});
|
|
141
|
+
await writer.save({
|
|
142
|
+
sourceTable,
|
|
143
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
144
|
+
after: { id: 'B', description: 'beta' },
|
|
145
|
+
afterReplicaId: test_utils.rid('B')
|
|
146
|
+
});
|
|
147
|
+
await writer.save({
|
|
148
|
+
sourceTable,
|
|
149
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
150
|
+
after: { id: 'A', description: 'v4' },
|
|
151
|
+
afterReplicaId: test_utils.rid('A')
|
|
152
|
+
});
|
|
153
|
+
await writer.commit('1/1');
|
|
154
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
155
|
+
|
|
156
|
+
// Compact — produces CLEAR doc from collapsed MOVEs
|
|
157
|
+
await bucketStorage.compact({
|
|
158
|
+
maxOpId: checkpoint.checkpoint,
|
|
159
|
+
compactBuckets: [request.bucket],
|
|
160
|
+
clearBatchLimit: 200,
|
|
161
|
+
moveBatchLimit: 10,
|
|
162
|
+
moveBatchQueryLimit: 10,
|
|
163
|
+
moveBatchByteLimit: 1024,
|
|
164
|
+
minBucketChanges: 1,
|
|
165
|
+
minChangeRatio: 0
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Shift compacted_state.op_id back to before the CLEAR doc so getChecksums
|
|
169
|
+
// queries the pipeline (not just reads compacted_state directly).
|
|
170
|
+
await db
|
|
171
|
+
.bucketState(bucketStorage.replicationStreamId)
|
|
172
|
+
.updateOne({ _id: { d: definitionId, b: request.bucket } }, { $set: { 'compacted_state.op_id': 0n } });
|
|
173
|
+
|
|
174
|
+
// Ground truth: sum of doc-level checksum fields
|
|
175
|
+
const docs = await db
|
|
176
|
+
.bucketData(bucketStorage.replicationStreamId, definitionId)
|
|
177
|
+
.find({ '_id.b': request.bucket })
|
|
178
|
+
.toArray();
|
|
179
|
+
const groundTruth = docs.reduce((sum: number, d: any) => addChecksums(sum, Number(d.checksum)), 0);
|
|
180
|
+
expect(docs.some((doc) => doc.has_clear_op)).toBe(true);
|
|
181
|
+
expect(docs.every((doc) => doc.storage_ref != null && doc.ops == null)).toBe(true);
|
|
182
|
+
|
|
183
|
+
const checksum = (await bucketStorage.getChecksums(checkpoint, [request])).get(request.bucket)!;
|
|
184
|
+
expect(checksum.checksum).toBe(groundTruth);
|
|
185
|
+
|
|
186
|
+
// The two superseded A operations collapse into CLEAR.
|
|
187
|
+
const batchAfter = await test_utils.getBatchArray(bucketStorage.getBucketDataBatch(checkpoint, [request]));
|
|
188
|
+
const dataAfter = batchAfter.flatMap((chunk) => chunk.chunkData.data);
|
|
189
|
+
expect(dataAfter).toMatchObject([{ op: 'CLEAR' }, { object_id: 'B', op: 'PUT' }, { object_id: 'A', op: 'PUT' }]);
|
|
190
|
+
});
|
|
191
|
+
});
|