@powersync/service-module-mongodb-storage 0.0.0-dev-20261005120023 → 0.0.0-dev-20261015071128
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 +23 -13
- package/dist/storage/MongoBucketStorage.d.ts +1 -1
- package/dist/storage/MongoBucketStorage.js +7 -7
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +5 -3
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +1 -2
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +4 -7
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +6 -6
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.js +5 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +3 -1
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -1
- package/dist/utils/test-utils.d.ts +1 -2
- package/dist/utils/test-utils.js +1 -2
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/MongoBucketStorage.ts +6 -8
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +9 -2
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +4 -12
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +31 -48
- package/src/storage/implementation/MongoStorageProvider.ts +1 -1
- package/src/storage/implementation/v3/MongoCompactorV3.ts +6 -0
- package/src/storage/implementation/v3/compact-utils.ts +3 -1
- package/src/utils/test-utils.ts +2 -3
- package/test/src/source_table_config.test.ts +19 -61
- package/test/src/storage_compacting.test.ts +37 -0
- package/test/src/storage_sync.test.ts +3 -4
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
2
|
-
import {
|
|
2
|
+
import { updateSyncRulesFromConfig } from '@powersync/service-core';
|
|
3
3
|
import { test_utils } from '@powersync/service-core-tests';
|
|
4
|
-
import {
|
|
5
|
-
AdditionalSyncConfigParser,
|
|
6
|
-
normalizeSourceTableConfig,
|
|
7
|
-
PrecompiledSyncConfig
|
|
8
|
-
} from '@powersync/service-sync-rules';
|
|
4
|
+
import { PrecompiledSyncConfig, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
9
5
|
import { describe, expect, test } from 'vitest';
|
|
10
6
|
import { env } from './env.js';
|
|
11
7
|
import { TEST_STORAGE_VERSIONS } from './util.js';
|
|
12
8
|
|
|
13
9
|
const STREAMS = /* yaml */ `
|
|
14
|
-
#
|
|
10
|
+
# sync config fixture.
|
|
15
11
|
config:
|
|
16
12
|
edition: 3
|
|
17
13
|
streams:
|
|
@@ -19,86 +15,48 @@ const STREAMS = /* yaml */ `
|
|
|
19
15
|
query: SELECT * FROM orders
|
|
20
16
|
`;
|
|
21
17
|
const CONFIGURED = /* yaml */ `
|
|
22
|
-
#
|
|
18
|
+
# sync config fixture.
|
|
23
19
|
config:
|
|
24
20
|
edition: 3
|
|
25
21
|
source_table_options:
|
|
26
22
|
orders:
|
|
27
|
-
|
|
23
|
+
mongodb_filter_expression: { $eq: ['$$doc.active', true] }
|
|
28
24
|
streams:
|
|
29
25
|
orders:
|
|
30
26
|
query: SELECT * FROM orders
|
|
31
27
|
`;
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
const TABLE_OPTIONS: AdditionalSyncConfigParser = {
|
|
35
|
-
id: 'example.tables',
|
|
36
|
-
extendJsonSchema({ schema }) {
|
|
37
|
-
const options = (schema.properties as any).config.properties.source_table_options.additionalProperties;
|
|
38
|
-
options.properties.sample = { type: 'number' };
|
|
39
|
-
},
|
|
40
|
-
parse({ config, context }) {
|
|
41
|
-
const sourceTables = normalizeSourceTableConfig(
|
|
42
|
-
(config as { config?: { source_table_options?: unknown } }).config?.source_table_options
|
|
43
|
-
);
|
|
44
|
-
for (const [table, options] of Object.entries(sourceTables)) {
|
|
45
|
-
if (!Object.hasOwn(options!, 'sample')) continue;
|
|
46
|
-
context.parsedConfig.sourceTableConfig = { ...context.parsedConfig.sourceTableConfig, [table]: options };
|
|
47
|
-
const { plan } = context.parsedConfig as PrecompiledSyncConfig;
|
|
48
|
-
plan.moduleData = { ...plan.moduleData, ['example.tables']: null };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function storageFactory(withModule: boolean) {
|
|
54
|
-
const syncConfigParser = new SqlSyncConfigParser(withModule ? [TABLE_OPTIONS] : []);
|
|
29
|
+
function storageFactory() {
|
|
55
30
|
return mongoTestStorageFactoryGenerator({
|
|
56
31
|
url: env.MONGO_TEST_URL,
|
|
57
32
|
isCI: env.CI,
|
|
58
|
-
supportsMultipleSyncConfigs: true
|
|
59
|
-
syncConfigParser
|
|
33
|
+
supportsMultipleSyncConfigs: true
|
|
60
34
|
});
|
|
61
35
|
}
|
|
62
36
|
|
|
63
37
|
describe.for(TEST_STORAGE_VERSIONS)('source-table config with storage v%i', (storageVersion) => {
|
|
64
|
-
test('preserves
|
|
38
|
+
test('preserves source-table options across closing storage and loading its compiled plan', async () => {
|
|
65
39
|
let expected;
|
|
66
40
|
{
|
|
67
|
-
await using factory = await storageFactory(
|
|
68
|
-
const parsed =
|
|
69
|
-
expected = parsed.config.sourceTableConfig;
|
|
41
|
+
await using factory = await storageFactory().factory();
|
|
42
|
+
const parsed = SqlSyncRules.fromYaml(CONFIGURED, test_utils.PARSE_OPTIONS);
|
|
43
|
+
expected = (parsed.config as PrecompiledSyncConfig).plan.sourceTableConfig;
|
|
70
44
|
await factory.updateSyncRules(updateSyncRulesFromConfig(parsed, { storageVersion }));
|
|
71
45
|
}
|
|
72
46
|
|
|
73
47
|
// Reopen the same database without clearing it, as a service restart would.
|
|
74
|
-
await using reopened = await storageFactory(
|
|
48
|
+
await using reopened = await storageFactory().factory({ doNotClear: true });
|
|
75
49
|
const deploying = (await reopened.getDeployingSyncConfig())!;
|
|
76
50
|
expect(deploying.content.compiled_plan?.plan.version).toBe(3);
|
|
77
51
|
const parsed = deploying.content.parsed(test_utils.PARSE_OPTIONS);
|
|
78
|
-
expect(parsed.syncConfigs[0].config.sourceTableConfig).toEqual(expected);
|
|
52
|
+
expect((parsed.syncConfigs[0].config as PrecompiledSyncConfig).plan.sourceTableConfig).toEqual(expected);
|
|
79
53
|
expect(parsed.hydratedSyncConfig.sourceTableConfig).toEqual(expected);
|
|
80
54
|
});
|
|
81
55
|
|
|
82
|
-
test('
|
|
83
|
-
|
|
84
|
-
await using factory = await storageFactory(true).factory();
|
|
85
|
-
await factory.updateSyncRules(
|
|
86
|
-
updateSyncRulesFromConfig(factory.syncConfigParser.parseContent(CONFIGURED, test_utils.PARSE_OPTIONS), {
|
|
87
|
-
storageVersion
|
|
88
|
-
})
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
await using reopened = await storageFactory(false).factory({ doNotClear: true });
|
|
92
|
-
const deploying = (await reopened.getDeployingSyncConfig())!;
|
|
93
|
-
expect(() => deploying.content.parsed(test_utils.PARSE_OPTIONS)).toThrow(
|
|
94
|
-
'Missing required sync config parsers: example.tables'
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('continues loading unfiltered plans without an additional parser', async () => {
|
|
99
|
-
await using factory = await storageFactory(false).factory();
|
|
56
|
+
test('continues loading unfiltered plans without source options', async () => {
|
|
57
|
+
await using factory = await storageFactory().factory();
|
|
100
58
|
await factory.updateSyncRules(
|
|
101
|
-
updateSyncRulesFromConfig(
|
|
59
|
+
updateSyncRulesFromConfig(SqlSyncRules.fromYaml(STREAMS, test_utils.PARSE_OPTIONS), {
|
|
102
60
|
storageVersion
|
|
103
61
|
})
|
|
104
62
|
);
|
|
@@ -108,10 +66,10 @@ describe.for(TEST_STORAGE_VERSIONS)('source-table config with storage v%i', (sto
|
|
|
108
66
|
});
|
|
109
67
|
|
|
110
68
|
test('starts replacement processing when source-table options change', async () => {
|
|
111
|
-
await using factory = await storageFactory(
|
|
69
|
+
await using factory = await storageFactory().factory();
|
|
112
70
|
const deploy = (yaml: string) =>
|
|
113
71
|
factory.updateSyncRules(
|
|
114
|
-
updateSyncRulesFromConfig(
|
|
72
|
+
updateSyncRulesFromConfig(SqlSyncRules.fromYaml(yaml, test_utils.PARSE_OPTIONS), {
|
|
115
73
|
storageVersion
|
|
116
74
|
})
|
|
117
75
|
);
|
|
@@ -132,7 +90,7 @@ describe.for(TEST_STORAGE_VERSIONS)('source-table config with storage v%i', (sto
|
|
|
132
90
|
await writer.markAllSnapshotDone('2/1');
|
|
133
91
|
await writer.commit('2/1');
|
|
134
92
|
}
|
|
135
|
-
const changed = await deploy(CONFIGURED.replace('
|
|
93
|
+
const changed = await deploy(CONFIGURED.replace('true', 'false'));
|
|
136
94
|
expect(changed.replicationStreamId).not.toBe(first.replicationStreamId);
|
|
137
95
|
expect((await factory.getActiveSyncConfig())?.content.replicationStreamId).toBe(first.replicationStreamId);
|
|
138
96
|
});
|
|
@@ -2615,6 +2615,43 @@ bucket_definitions:
|
|
|
2615
2615
|
});
|
|
2616
2616
|
});
|
|
2617
2617
|
|
|
2618
|
+
test('full compaction releases superseded input payloads across read batches', async () => {
|
|
2619
|
+
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
2620
|
+
const documents = Array.from({ length: 6 }, (_, index) =>
|
|
2621
|
+
serializeBucketData(BUCKET, [makeOp(index + 1, 'A', 'x'.repeat(256 * 1024), ctx, sourceTableId)])
|
|
2622
|
+
);
|
|
2623
|
+
const latest = makeOp(7, 'A', 'latest', ctx, sourceTableId);
|
|
2624
|
+
documents.push(serializeBucketData(BUCKET, [latest]));
|
|
2625
|
+
await insertDocs(collection, documents);
|
|
2626
|
+
await insertBucketState(bucketStateCollection, ctx.definitionId, 7n);
|
|
2627
|
+
|
|
2628
|
+
const compactor = bucketStorage.createMongoCompactor({
|
|
2629
|
+
compactBuckets: [BUCKET],
|
|
2630
|
+
maxOpId: 7n,
|
|
2631
|
+
moveBatchQueryLimit: 2
|
|
2632
|
+
});
|
|
2633
|
+
const originalFlush = (compactor as any).flushCompactionGroup.bind(compactor);
|
|
2634
|
+
const flushSpy = vi.spyOn(compactor as any, 'flushCompactionGroup').mockImplementation(async (...args: any[]) => {
|
|
2635
|
+
const group = args[1];
|
|
2636
|
+
// All seven inputs merge into one small output across four read batches.
|
|
2637
|
+
// Keeping their original ops here would retain every superseded payload.
|
|
2638
|
+
expect(group.inputs).toHaveLength(7);
|
|
2639
|
+
expect(group.inputs.every((input: BucketDataDocumentV3) => input.ops == null)).toBe(true);
|
|
2640
|
+
expect(group.ops.filter((op: BucketDataDoc) => op.op === 'MOVE')).toHaveLength(6);
|
|
2641
|
+
return originalFlush(...args);
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
await expect(compactor.compact()).resolves.toBe(1);
|
|
2645
|
+
expect(flushSpy).toHaveBeenCalledOnce();
|
|
2646
|
+
const compacted = await collection.find({ '_id.b': BUCKET }).toArray();
|
|
2647
|
+
const ops = compacted.flatMap((document) => document.ops!);
|
|
2648
|
+
expect(ops.map((op) => op.op)).toEqual(['CLEAR', 'PUT']);
|
|
2649
|
+
expect(ops[1]).toMatchObject({ o: latest.o, data: latest.data });
|
|
2650
|
+
expect(compacted.reduce((sum, document) => sum + document.checksum, 0n)).toBe(
|
|
2651
|
+
documents.reduce((sum, document) => sum + document.checksum, 0n)
|
|
2652
|
+
);
|
|
2653
|
+
});
|
|
2654
|
+
|
|
2618
2655
|
test('7. cross-batch seen map overflow - dedup continues across batches', async () => {
|
|
2619
2656
|
const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
|
|
2620
2657
|
|
|
@@ -73,7 +73,8 @@ function objectIdGenerator(id: string) {
|
|
|
73
73
|
*/
|
|
74
74
|
function parsedSyncConfigSetFor(yaml: string, storageVersion: number) {
|
|
75
75
|
const parsed = SqlSyncRules.fromYaml(yaml, test_utils.PARSE_OPTIONS);
|
|
76
|
-
|
|
76
|
+
// These fixtures are legacy Sync Rules, which always report a deprecation warning.
|
|
77
|
+
expect(parsed.errors.filter((error) => error.type !== 'warning')).toEqual([]);
|
|
77
78
|
return new MongoParsedSyncConfigSet(1, getMongoStorageConfig(storageVersion), 'test_slot', [
|
|
78
79
|
{
|
|
79
80
|
syncConfigId: new bson.ObjectId().toHexString(),
|
|
@@ -99,9 +100,7 @@ async function getMongoSyncConfigContents(factory: storage.BucketStorageFactory,
|
|
|
99
100
|
})
|
|
100
101
|
.toArray();
|
|
101
102
|
|
|
102
|
-
return syncConfigDocs.map(
|
|
103
|
-
(config) => new MongoPersistedSyncConfigContentV3(mongoFactory.db, doc, config, mongoFactory.syncConfigParser)
|
|
104
|
-
);
|
|
103
|
+
return syncConfigDocs.map((config) => new MongoPersistedSyncConfigContentV3(mongoFactory.db, doc, config));
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
/**
|