@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +23 -13
  2. package/dist/storage/MongoBucketStorage.d.ts +1 -1
  3. package/dist/storage/MongoBucketStorage.js +7 -7
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +5 -3
  6. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  7. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +1 -2
  8. package/dist/storage/implementation/MongoPersistedReplicationStream.js +4 -7
  9. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
  10. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
  11. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +6 -6
  12. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  13. package/dist/storage/implementation/MongoStorageProvider.js +1 -1
  14. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  15. package/dist/storage/implementation/v3/MongoCompactorV3.js +5 -0
  16. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  17. package/dist/storage/implementation/v3/compact-utils.d.ts +3 -1
  18. package/dist/storage/implementation/v3/compact-utils.js.map +1 -1
  19. package/dist/utils/test-utils.d.ts +1 -2
  20. package/dist/utils/test-utils.js +1 -2
  21. package/dist/utils/test-utils.js.map +1 -1
  22. package/package.json +7 -7
  23. package/src/storage/MongoBucketStorage.ts +6 -8
  24. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +9 -2
  25. package/src/storage/implementation/MongoPersistedReplicationStream.ts +4 -12
  26. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +31 -48
  27. package/src/storage/implementation/MongoStorageProvider.ts +1 -1
  28. package/src/storage/implementation/v3/MongoCompactorV3.ts +6 -0
  29. package/src/storage/implementation/v3/compact-utils.ts +3 -1
  30. package/src/utils/test-utils.ts +2 -3
  31. package/test/src/source_table_config.test.ts +19 -61
  32. package/test/src/storage_compacting.test.ts +37 -0
  33. package/test/src/storage_sync.test.ts +3 -4
  34. package/tsconfig.tsbuildinfo +1 -1
@@ -1,17 +1,13 @@
1
1
  import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
2
- import { SqlSyncConfigParser, updateSyncRulesFromConfig } from '@powersync/service-core';
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
- # Sync config fixture.
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
- # Sync config fixture.
18
+ # sync config fixture.
23
19
  config:
24
20
  edition: 3
25
21
  source_table_options:
26
22
  orders:
27
- sample: 10
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
- // A small module-owned option exercises persistence without introducing MongoDB filter syntax into core.
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 module options across closing storage and loading its compiled plan', async () => {
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(true).factory();
68
- const parsed = factory.syncConfigParser.parseContent(CONFIGURED, test_utils.PARSE_OPTIONS);
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(true).factory({ doNotClear: true });
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('rejects saved options if the module is absent after restart', async () => {
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(factory.syncConfigParser.parseContent(STREAMS, test_utils.PARSE_OPTIONS), {
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(true).factory();
69
+ await using factory = await storageFactory().factory();
112
70
  const deploy = (yaml: string) =>
113
71
  factory.updateSyncRules(
114
- updateSyncRulesFromConfig(factory.syncConfigParser.parseContent(yaml, test_utils.PARSE_OPTIONS), {
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('sample: 10', 'sample: 20'));
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
- expect(parsed.errors).toEqual([]);
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
  /**