@powersync/service-module-mongodb-storage 0.20.0 → 0.22.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 +62 -0
- package/dist/storage/MongoBucketStorage.d.ts +36 -3
- package/dist/storage/MongoBucketStorage.js +372 -207
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +17 -5
- package/dist/storage/implementation/MongoBucketBatch.js +44 -17
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- 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 +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +7 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- 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 +49 -6
- 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 +27 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +31 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +33 -4
- 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 +65 -16
- 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 +10 -2
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +100 -26
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +82 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +698 -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 +91 -31
- 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 +76 -42
- 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 +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -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 +84 -4
- 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 +6 -6
- package/dist/storage/implementation/v3/source-table-utils.js +30 -37
- 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.js +1 -0
- 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 +507 -269
- package/src/storage/implementation/MongoBucketBatch.ts +67 -24
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +15 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- 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 +66 -10
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
- package/src/storage/implementation/db.ts +35 -2
- package/src/storage/implementation/models.ts +34 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +46 -6
- 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 +87 -27
- 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 +138 -34
- package/src/storage/implementation/v3/MongoCompactorV3.ts +958 -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 +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +93 -48
- 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 +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +91 -3
- 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 +39 -41
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +42 -7
- package/src/utils/test-utils.ts +1 -0
- 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 +605 -16
- package/test/src/chunk_compaction_config.test.ts +38 -0
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +267 -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 +273 -22
- package/test/src/storage_compacting.test.ts +968 -246
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +290 -2
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +336 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MongoStorageConfig, normalizeChunkCompactionConcurrency } from '@module/types/types.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('chunk compaction concurrency configuration', () => {
|
|
5
|
+
test('defaults to two workers', () => {
|
|
6
|
+
expect(normalizeChunkCompactionConcurrency(undefined)).toBe(2);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('defaults to four workers with object storage', () => {
|
|
10
|
+
expect(normalizeChunkCompactionConcurrency(undefined, true)).toBe(4);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test.each([false, true])('preserves an explicit override with object storage %s', (hasObjectStorage) => {
|
|
14
|
+
expect(normalizeChunkCompactionConcurrency(3, hasObjectStorage)).toBe(3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('decodes a configured worker count', () => {
|
|
18
|
+
const config = MongoStorageConfig.decode({
|
|
19
|
+
type: 'mongodb',
|
|
20
|
+
uri: 'mongodb://localhost:27017/powersync',
|
|
21
|
+
chunk_compaction_concurrency: 4
|
|
22
|
+
});
|
|
23
|
+
expect(normalizeChunkCompactionConcurrency(config.chunk_compaction_concurrency)).toBe(4);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.each([1, 64])('accepts boundary worker count %s', (value) => {
|
|
27
|
+
expect(normalizeChunkCompactionConcurrency(value)).toBe(value);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test.each([0, -1, 1.5, 65, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])(
|
|
31
|
+
'rejects invalid worker count %s',
|
|
32
|
+
(value) => {
|
|
33
|
+
expect(() => normalizeChunkCompactionConcurrency(value)).toThrow(
|
|
34
|
+
'storage.chunk_compaction_concurrency must be an integer between 1 and 64'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -324,7 +324,7 @@ streams:
|
|
|
324
324
|
test('drops current_data when a table becomes event-only but is kept by a live event', async () => {
|
|
325
325
|
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
326
326
|
|
|
327
|
-
// The first config syncs `todos` data and also
|
|
327
|
+
// The first config syncs `todos` data and also evaluates the audit event for its rows.
|
|
328
328
|
const first = await factory.updateSyncRules(
|
|
329
329
|
updateSyncRulesFromYaml(
|
|
330
330
|
`
|
|
@@ -360,15 +360,22 @@ event_definitions:
|
|
|
360
360
|
},
|
|
361
361
|
afterReplicaId: test_utils.rid('todo-1')
|
|
362
362
|
});
|
|
363
|
+
const eventDefinitionId = [...resolved.tables[0].eventDefinitionIds!][0];
|
|
364
|
+
firstWriter.addCustomWriteCheckpoint({
|
|
365
|
+
user_id: 'user-1',
|
|
366
|
+
checkpoint: 1n,
|
|
367
|
+
event_id: eventDefinitionId
|
|
368
|
+
});
|
|
363
369
|
await firstWriter.markAllSnapshotDone('1/1');
|
|
364
370
|
await firstWriter.commit('1/1');
|
|
365
371
|
|
|
366
372
|
const sourceTableId = resolved.tables[0].id as bson.ObjectId;
|
|
367
373
|
const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
|
|
368
374
|
const ownerDefinitionId = first.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
).
|
|
375
|
+
const initialSourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
|
|
376
|
+
expect(initialSourceTable?.bucket_data_source_ids).toEqual([ownerDefinitionId]);
|
|
377
|
+
expect(initialSourceTable?.event_definition_ids).toHaveLength(1);
|
|
378
|
+
const eventDefinitionIds = initialSourceTable!.event_definition_ids;
|
|
372
379
|
// The prior snapshot is persisted in current_data.
|
|
373
380
|
expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
|
|
374
381
|
|
|
@@ -414,13 +421,22 @@ event_definitions:
|
|
|
414
421
|
sourceTablesDeleted: 0
|
|
415
422
|
});
|
|
416
423
|
|
|
417
|
-
// The source table row survives (the live event still needs it), narrowed to empty
|
|
418
|
-
// memberships, but its
|
|
424
|
+
// The source table row survives (the live event still needs it), narrowed to empty data and
|
|
425
|
+
// parameter memberships, but its event membership remains and current_data is dropped.
|
|
419
426
|
const sourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
|
|
420
427
|
expect(sourceTable).not.toBeNull();
|
|
421
428
|
expect(sourceTable?.bucket_data_source_ids).toEqual([]);
|
|
422
429
|
expect(sourceTable?.parameter_lookup_source_ids).toEqual([]);
|
|
430
|
+
expect(sourceTable?.event_definition_ids).toEqual(eventDefinitionIds);
|
|
423
431
|
expect(await collectionExists(db, sourceRecordsCollection)).toBe(false);
|
|
432
|
+
await expect(
|
|
433
|
+
db
|
|
434
|
+
.customCheckpointRequests({
|
|
435
|
+
replicationStreamId: first.replicationStreamId,
|
|
436
|
+
eventId: eventDefinitionId
|
|
437
|
+
})
|
|
438
|
+
.findOne({ user_id: 'user-1' })
|
|
439
|
+
).resolves.not.toBeNull();
|
|
424
440
|
|
|
425
441
|
const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
|
|
426
442
|
expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
|
|
@@ -429,9 +445,9 @@ event_definitions:
|
|
|
429
445
|
test('cleans up event-only source tables no longer triggered by a live sync config', async () => {
|
|
430
446
|
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
431
447
|
|
|
432
|
-
// The first config syncs `todos` and additionally
|
|
433
|
-
// `audit_log` table is referenced only by the event trigger, so its source table
|
|
434
|
-
//
|
|
448
|
+
// The first config syncs `todos` and additionally evaluates the audit event for `audit_log`. The
|
|
449
|
+
// `audit_log` table is referenced only by the event trigger, so its source table has only an
|
|
450
|
+
// event membership (an event-only table).
|
|
435
451
|
const first = await factory.updateSyncRules(
|
|
436
452
|
updateSyncRulesFromYaml(
|
|
437
453
|
`
|
|
@@ -485,21 +501,27 @@ event_definitions:
|
|
|
485
501
|
},
|
|
486
502
|
afterReplicaId: test_utils.rid('audit-1')
|
|
487
503
|
});
|
|
504
|
+
const auditEventDefinitionId = [...auditTable.eventDefinitionIds!][0];
|
|
505
|
+
firstWriter.addCustomWriteCheckpoint({
|
|
506
|
+
user_id: 'audit-user',
|
|
507
|
+
checkpoint: 1n,
|
|
508
|
+
event_id: auditEventDefinitionId
|
|
509
|
+
});
|
|
488
510
|
await firstWriter.markAllSnapshotDone('1/1');
|
|
489
511
|
await firstWriter.commit('1/1');
|
|
490
512
|
|
|
491
513
|
const todosTableId = todosTable.id as bson.ObjectId;
|
|
492
514
|
const auditTableId = auditTable.id as bson.ObjectId;
|
|
493
|
-
// The event-only table
|
|
494
|
-
// never selects it.
|
|
515
|
+
// The event-only table has empty bucket and parameter memberships, plus its event id.
|
|
495
516
|
const auditSourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: auditTableId });
|
|
496
517
|
expect(auditSourceTable?.bucket_data_source_ids).toEqual([]);
|
|
497
518
|
expect(auditSourceTable?.parameter_lookup_source_ids).toEqual([]);
|
|
519
|
+
expect(auditSourceTable?.event_definition_ids).toHaveLength(1);
|
|
498
520
|
const auditRecordsCollection = db.sourceRecords(first.replicationStreamId, auditTableId).collectionName;
|
|
499
521
|
|
|
500
522
|
// The second (active) config keeps the same `by_owner` stream but drops the audit event.
|
|
501
|
-
// The shared bucket definition stays in use,
|
|
502
|
-
//
|
|
523
|
+
// The shared bucket definition stays in use, while the unused event id selects this table for
|
|
524
|
+
// membership cleanup.
|
|
503
525
|
const second = await factory.updateSyncRules(
|
|
504
526
|
updateSyncRulesFromYaml(
|
|
505
527
|
`
|
|
@@ -537,6 +559,11 @@ streams:
|
|
|
537
559
|
// The orphaned event-only table and its source records are removed...
|
|
538
560
|
expect(await db.sourceTables(first.replicationStreamId).countDocuments({ _id: auditTableId })).toBe(0);
|
|
539
561
|
expect(await collectionExists(db, auditRecordsCollection)).toBe(false);
|
|
562
|
+
const auditCheckpointCollection = db.customCheckpointRequests({
|
|
563
|
+
replicationStreamId: first.replicationStreamId,
|
|
564
|
+
eventId: auditEventDefinitionId
|
|
565
|
+
}).collectionName;
|
|
566
|
+
expect(await collectionExists(db, auditCheckpointCollection)).toBe(false);
|
|
540
567
|
// ...while the data table still used by the live config is retained.
|
|
541
568
|
expect(await db.sourceTables(first.replicationStreamId).countDocuments({ _id: todosTableId })).toBe(1);
|
|
542
569
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCompactionDelta,
|
|
3
|
+
applyStatsReplacement,
|
|
4
|
+
bucketStats,
|
|
5
|
+
chooseCompactionKind,
|
|
6
|
+
combineAdjacentStats,
|
|
7
|
+
combineChunkStats,
|
|
8
|
+
CompactIntervalConfig,
|
|
9
|
+
CompactionKind,
|
|
10
|
+
emptyBucketStats,
|
|
11
|
+
forcedCompactionKind,
|
|
12
|
+
fullCompactionCheckAt,
|
|
13
|
+
statsForDocument,
|
|
14
|
+
statsForDocuments
|
|
15
|
+
} from '@module/storage/implementation/v3/compact-utils.js';
|
|
16
|
+
import { BucketStateDocumentV3 } from '@module/storage/implementation/v3/models.js';
|
|
17
|
+
import { describe, expect, test } from 'vitest';
|
|
18
|
+
|
|
19
|
+
const INTERVAL_CONFIG: CompactIntervalConfig = {
|
|
20
|
+
minCompactChunkIntervalMs: 1_000,
|
|
21
|
+
minCompactFullIntervalMs: 10_000,
|
|
22
|
+
maxCompactFullIntervalMs: 100_000
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function bucketState(overrides: Partial<BucketStateDocumentV3> = {}): BucketStateDocumentV3 {
|
|
26
|
+
return {
|
|
27
|
+
_id: { d: 'definition', b: 'bucket[]' },
|
|
28
|
+
last_op: 10n,
|
|
29
|
+
next_compact_check: new Date(0),
|
|
30
|
+
first_uncompacted_write: new Date(0),
|
|
31
|
+
bucket_stats: { count: 10, bytes: 100n, chunks: 1 },
|
|
32
|
+
...overrides
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compactedState(overrides: Partial<NonNullable<BucketStateDocumentV3['compacted_state']>> = {}) {
|
|
37
|
+
return {
|
|
38
|
+
op_id: 5n,
|
|
39
|
+
checksum: 50n,
|
|
40
|
+
count: 5,
|
|
41
|
+
bytes: 50n,
|
|
42
|
+
chunks: 1,
|
|
43
|
+
at: new Date(0),
|
|
44
|
+
...overrides
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('V3 compact utilities', () => {
|
|
49
|
+
test.each([
|
|
50
|
+
{
|
|
51
|
+
name: 'uses the minimum interval before the first full compact',
|
|
52
|
+
state: bucketState(),
|
|
53
|
+
expected: new Date(10_000)
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'scales the interval by the uncompacted row ratio',
|
|
57
|
+
state: bucketState({
|
|
58
|
+
bucket_stats: { count: 100, bytes: 100n, chunks: 1 },
|
|
59
|
+
last_full_compact: { op_id: 90n, count: 90, puts: 90, at: new Date(0) }
|
|
60
|
+
}),
|
|
61
|
+
expected: new Date(100_000)
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'uses the maximum interval when there are no uncompacted rows',
|
|
65
|
+
state: bucketState({
|
|
66
|
+
bucket_stats: { count: 100, bytes: 100n, chunks: 1 },
|
|
67
|
+
last_full_compact: { op_id: 10n, count: 100, puts: 100, at: new Date(0) }
|
|
68
|
+
}),
|
|
69
|
+
expected: new Date(100_000)
|
|
70
|
+
}
|
|
71
|
+
])('$name', ({ state, expected }) => {
|
|
72
|
+
expect(fullCompactionCheckAt(state, INTERVAL_CONFIG)).toEqual(expected);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('rejects a scheduled bucket without an uncompacted write', () => {
|
|
76
|
+
expect(() => fullCompactionCheckAt(bucketState({ first_uncompacted_write: undefined }), INTERVAL_CONFIG)).toThrow(
|
|
77
|
+
'Scheduled V3 bucket bucket[] has no first uncompacted write'
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('chooses a full compact when its check is due', () => {
|
|
82
|
+
const decision = chooseCompactionKind(
|
|
83
|
+
bucketState({ bucket_stats: { count: 10, bytes: 100n, chunks: 10 } }),
|
|
84
|
+
new Date(10_000),
|
|
85
|
+
INTERVAL_CONFIG
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(decision.kind).toBe(CompactionKind.Full);
|
|
89
|
+
expect(decision.nextCompactCheck).toEqual({
|
|
90
|
+
$min: [new Date(70_000), { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: 1_000 } }]
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('chooses a chunk compact after enough chunks and the minimum interval', () => {
|
|
95
|
+
const decision = chooseCompactionKind(
|
|
96
|
+
bucketState({
|
|
97
|
+
bucket_stats: { count: 10, bytes: 100n, chunks: 9 },
|
|
98
|
+
compacted_state: compactedState({ chunks: 1 })
|
|
99
|
+
}),
|
|
100
|
+
new Date(5_000),
|
|
101
|
+
INTERVAL_CONFIG
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(decision.kind).toBe(CompactionKind.Chunks);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('waits when the chunk interval has not elapsed', () => {
|
|
108
|
+
const decision = chooseCompactionKind(
|
|
109
|
+
bucketState({
|
|
110
|
+
bucket_stats: { count: 10, bytes: 100n, chunks: 9 },
|
|
111
|
+
compacted_state: compactedState({ chunks: 1, at: new Date(4_500) })
|
|
112
|
+
}),
|
|
113
|
+
new Date(5_000),
|
|
114
|
+
INTERVAL_CONFIG
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(decision.kind).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('schedules only the full check when too few chunks were added', () => {
|
|
121
|
+
const decision = chooseCompactionKind(
|
|
122
|
+
bucketState({ bucket_stats: { count: 7, bytes: 70n, chunks: 7 } }),
|
|
123
|
+
new Date(5_000),
|
|
124
|
+
INTERVAL_CONFIG
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(decision).toEqual({ kind: null, nextCompactCheck: new Date(70_000) });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test.each([
|
|
131
|
+
{
|
|
132
|
+
name: 'does not force an unspecified kind',
|
|
133
|
+
forceKind: undefined,
|
|
134
|
+
compactedOpId: undefined,
|
|
135
|
+
maxOpIdCap: undefined,
|
|
136
|
+
expected: null
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'forces work without previous compacted state',
|
|
140
|
+
forceKind: CompactionKind.Chunks,
|
|
141
|
+
compactedOpId: undefined,
|
|
142
|
+
maxOpIdCap: undefined,
|
|
143
|
+
expected: CompactionKind.Chunks
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'skips work already compacted through the cap',
|
|
147
|
+
forceKind: CompactionKind.Chunks,
|
|
148
|
+
compactedOpId: 5n,
|
|
149
|
+
maxOpIdCap: 5n,
|
|
150
|
+
expected: null
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'skips work already compacted through the bucket head',
|
|
154
|
+
forceKind: CompactionKind.Chunks,
|
|
155
|
+
compactedOpId: 10n,
|
|
156
|
+
maxOpIdCap: undefined,
|
|
157
|
+
expected: null
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'forces work beyond the compacted state',
|
|
161
|
+
forceKind: CompactionKind.Chunks,
|
|
162
|
+
compactedOpId: 5n,
|
|
163
|
+
maxOpIdCap: 6n,
|
|
164
|
+
expected: CompactionKind.Chunks
|
|
165
|
+
}
|
|
166
|
+
])('$name', ({ forceKind, compactedOpId, maxOpIdCap, expected }) => {
|
|
167
|
+
const state = bucketState({
|
|
168
|
+
compacted_state: compactedOpId == null ? undefined : compactedState({ op_id: compactedOpId })
|
|
169
|
+
});
|
|
170
|
+
expect(forcedCompactionKind(state, forceKind, { maxOpIdCap })).toBe(expected);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('derives and combines bucket statistics', () => {
|
|
174
|
+
const state = bucketState({ bucket_stats: { count: 12, bytes: 120n, chunks: 3 } });
|
|
175
|
+
expect(bucketStats(state)).toEqual({ count: 12, bytes: 120n, chunks: 3 });
|
|
176
|
+
expect(emptyBucketStats()).toEqual({ count: 0, bytes: 0n, chunks: 0, checksum: 0 });
|
|
177
|
+
expect(statsForDocument({ count: 2, size: 20, checksum: 10n })).toEqual({
|
|
178
|
+
count: 2,
|
|
179
|
+
bytes: 20n,
|
|
180
|
+
chunks: 1,
|
|
181
|
+
checksum: 10
|
|
182
|
+
});
|
|
183
|
+
expect(
|
|
184
|
+
statsForDocuments([
|
|
185
|
+
{ count: 2, size: 20, checksum: 10n },
|
|
186
|
+
{ count: 3, size: 30, checksum: 20n }
|
|
187
|
+
])
|
|
188
|
+
).toEqual({ count: 5, bytes: 50n, chunks: 2, checksum: 30 });
|
|
189
|
+
expect(
|
|
190
|
+
combineAdjacentStats(
|
|
191
|
+
{ count: 2, bytes: 20n, chunks: 1, checksum: 10 },
|
|
192
|
+
{ count: 3, bytes: 30n, chunks: 2, checksum: 20 }
|
|
193
|
+
)
|
|
194
|
+
).toEqual({ count: 5, bytes: 50n, chunks: 3, checksum: 30 });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('replaces an overlapping chunk in cached statistics', () => {
|
|
198
|
+
expect(
|
|
199
|
+
combineChunkStats(
|
|
200
|
+
compactedState({ count: 10, bytes: 100n, chunks: 4, checksum: 100n }),
|
|
201
|
+
{ count: 6, bytes: 60n, chunks: 2, checksum: 50 },
|
|
202
|
+
{ count: 3, bytes: 30n, chunks: 1, checksum: 30 }
|
|
203
|
+
)
|
|
204
|
+
).toEqual({ count: 13, bytes: 130n, chunks: 5, checksum: 120 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('applies compacted statistics as a delta to total bucket statistics', () => {
|
|
208
|
+
expect(
|
|
209
|
+
applyCompactionDelta(
|
|
210
|
+
{ count: 20, bytes: 200n, chunks: 8 },
|
|
211
|
+
{ count: 5, bytes: 50n, chunks: 3, checksum: 10 },
|
|
212
|
+
{ count: 3, bytes: 30n, chunks: 1, checksum: 10 }
|
|
213
|
+
)
|
|
214
|
+
).toEqual({ count: 18, bytes: 180n, chunks: 6 });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('applies a stored range replacement to statistics including its checksum', () => {
|
|
218
|
+
expect(
|
|
219
|
+
applyStatsReplacement(
|
|
220
|
+
{ count: 20, bytes: 200n, chunks: 8, checksum: 100 },
|
|
221
|
+
{ count: 5, bytes: 50n, chunks: 3, checksum: 40 },
|
|
222
|
+
{ count: 3, bytes: 30n, chunks: 1, checksum: 20 }
|
|
223
|
+
)
|
|
224
|
+
).toEqual({ count: 18, bytes: 180n, chunks: 6, checksum: 80 });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ObjectStorage,
|
|
3
|
+
ObjectStorageOperationOptions,
|
|
3
4
|
ObjectStoragePutMetadata
|
|
4
5
|
} from '@module/storage/implementation/v3/object-storage/ObjectStorage.js';
|
|
5
6
|
|
|
@@ -9,13 +10,19 @@ export class MemoryObjectStorage implements ObjectStorage {
|
|
|
9
10
|
*/
|
|
10
11
|
public readonly store = new Map<string, { data: Uint8Array; metadata: ObjectStoragePutMetadata }>();
|
|
11
12
|
|
|
12
|
-
async put(
|
|
13
|
+
async put(
|
|
14
|
+
path: string,
|
|
15
|
+
data: Uint8Array,
|
|
16
|
+
metadata: ObjectStoragePutMetadata,
|
|
17
|
+
options?: ObjectStorageOperationOptions
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
options?.signal?.throwIfAborted();
|
|
13
20
|
this.store.set(path, { data, metadata: metadata });
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
async get(
|
|
17
24
|
path: string,
|
|
18
|
-
options?:
|
|
25
|
+
options?: ObjectStorageOperationOptions
|
|
19
26
|
): Promise<{ data: Uint8Array; metadata: ObjectStoragePutMetadata }> {
|
|
20
27
|
const signal = options?.signal;
|
|
21
28
|
signal?.throwIfAborted();
|
|
@@ -26,7 +33,7 @@ export class MemoryObjectStorage implements ObjectStorage {
|
|
|
26
33
|
return data;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
async *list(prefix: string, options?:
|
|
36
|
+
async *list(prefix: string, options?: ObjectStorageOperationOptions): AsyncIterable<string> {
|
|
30
37
|
const signal = options?.signal;
|
|
31
38
|
for (const path of this.store.keys()) {
|
|
32
39
|
signal?.throwIfAborted();
|
|
@@ -36,18 +43,19 @@ export class MemoryObjectStorage implements ObjectStorage {
|
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
async delete(paths: string[]): Promise<void> {
|
|
46
|
+
async delete(paths: string[], options?: ObjectStorageOperationOptions): Promise<void> {
|
|
47
|
+
options?.signal?.throwIfAborted();
|
|
40
48
|
for (const p of paths) {
|
|
41
49
|
this.store.delete(p);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
async deletePrefix(prefix: string, options?:
|
|
53
|
+
async deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{ objectCount: number }> {
|
|
46
54
|
const paths: string[] = [];
|
|
47
55
|
for await (const path of this.list(prefix, options)) {
|
|
48
56
|
paths.push(path);
|
|
49
57
|
}
|
|
50
|
-
await this.delete(paths);
|
|
58
|
+
await this.delete(paths, options);
|
|
51
59
|
return { objectCount: paths.length };
|
|
52
60
|
}
|
|
53
61
|
}
|