@powersync/service-core 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214
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 +152 -4
- package/dist/api/RouteAPI.d.ts +42 -14
- package/dist/api/diagnostics.d.ts +12 -1
- package/dist/api/diagnostics.js +36 -22
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +30 -22
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/metrics/metrics-interfaces.d.ts +5 -1
- package/dist/metrics/metrics-interfaces.js.map +1 -1
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.d.ts +36 -0
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js +70 -0
- package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js.map +1 -0
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +6 -1
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
- package/dist/metrics/open-telemetry/util.js +7 -3
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +6 -2
- package/dist/modules/AbstractModule.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +31 -7
- package/dist/replication/AbstractReplicator.js +168 -74
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +3 -3
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/routes/RouterEngine.d.ts +1 -0
- package/dist/routes/RouterEngine.js +6 -1
- package/dist/routes/RouterEngine.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +104 -56
- package/dist/routes/configure-fastify.js +15 -2
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/admin.d.ts +42 -24
- package/dist/routes/endpoints/admin.js +49 -36
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.d.ts +62 -0
- package/dist/routes/endpoints/checkpointing.js +65 -7
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +3 -2
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +25 -14
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +1 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/runner/teardown.js +8 -8
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +64 -17
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +50 -31
- package/dist/storage/BucketStorageFactory.js +10 -20
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/CheckpointChecksumInvalidatedError.d.ts +12 -0
- package/dist/storage/CheckpointChecksumInvalidatedError.js +17 -0
- package/dist/storage/CheckpointChecksumInvalidatedError.js.map +1 -0
- package/dist/storage/ChecksumCache.d.ts +11 -6
- package/dist/storage/ChecksumCache.js +5 -4
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
- package/dist/storage/ParsedSyncConfigSet.js +2 -0
- package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/PersistedReplicationStream.d.ts +32 -0
- package/dist/storage/PersistedReplicationStream.js +26 -0
- package/dist/storage/PersistedReplicationStream.js.map +1 -0
- package/dist/storage/PersistedSyncConfigContent.d.ts +65 -0
- package/dist/storage/PersistedSyncConfigContent.js +138 -0
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
- package/dist/storage/PersistedSyncConfigStatus.js +2 -0
- package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
- package/dist/storage/ReplicationEventPayload.d.ts +6 -1
- package/dist/storage/SourceEntity.d.ts +13 -2
- package/dist/storage/SourceTable.d.ts +53 -4
- package/dist/storage/SourceTable.js +62 -7
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SourceTableReconciler.d.ts +83 -0
- package/dist/storage/SourceTableReconciler.js +95 -0
- package/dist/storage/SourceTableReconciler.js.map +1 -0
- package/dist/storage/StorageVersionConfig.d.ts +4 -0
- package/dist/storage/StorageVersionConfig.js +10 -1
- package/dist/storage/StorageVersionConfig.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +162 -26
- package/dist/storage/SyncRulesBucketStorage.js +3 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +85 -6
- package/dist/storage/WriteCheckpointAPI.js +33 -0
- package/dist/storage/WriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +133 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +351 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +70 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
- package/dist/storage/storage-index.d.ts +10 -1
- package/dist/storage/storage-index.js +10 -1
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/storage/storage-metrics.js +120 -4
- package/dist/storage/storage-metrics.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +12 -2
- package/dist/sync/BucketChecksumState.js +69 -21
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +7 -0
- package/dist/sync/RequestTracker.js +12 -0
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.js +184 -43
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.d.ts +5 -0
- package/dist/sync/util.js +19 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +2 -0
- package/dist/system/ServiceContext.js +2 -0
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +23 -1
- package/dist/tracing/PerformanceTracer.js +11 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -1
- package/dist/util/checkpointing.d.ts +4 -8
- package/dist/util/checkpointing.js +1 -16
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +23 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/defaults.d.ts +2 -0
- package/dist/util/config/defaults.js +2 -0
- package/dist/util/config/defaults.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +1 -0
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -0
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/sync-rules-provider.d.ts +2 -0
- package/dist/util/config/sync-rules/sync-rules-provider.js +3 -0
- package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
- package/dist/util/config/types.d.ts +4 -1
- package/dist/util/lsn.d.ts +4 -0
- package/dist/util/lsn.js +10 -0
- package/dist/util/lsn.js.map +1 -1
- package/dist/util/protocol-types.d.ts +4 -4
- package/dist/util/protocol-types.js +3 -2
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/util-index.d.ts +1 -0
- package/dist/util/util-index.js +1 -0
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/utils.d.ts +3 -2
- package/dist/util/utils.js +17 -2
- package/dist/util/utils.js.map +1 -1
- package/dist/util/write-checkpoint-batcher.d.ts +19 -0
- package/dist/util/write-checkpoint-batcher.js +100 -0
- package/dist/util/write-checkpoint-batcher.js.map +1 -0
- package/package.json +7 -7
- package/src/api/RouteAPI.ts +43 -14
- package/src/api/diagnostics.ts +45 -26
- package/src/entry/commands/compact-action.ts +37 -23
- package/src/metrics/metrics-interfaces.ts +6 -1
- package/src/metrics/open-telemetry/MetricAttributeFilteringExporter.ts +85 -0
- package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +7 -2
- package/src/metrics/open-telemetry/util.ts +7 -3
- package/src/modules/AbstractModule.ts +6 -2
- package/src/replication/AbstractReplicator.ts +212 -78
- package/src/replication/ReplicationModule.ts +2 -3
- package/src/routes/RouterEngine.ts +7 -1
- package/src/routes/configure-fastify.ts +25 -2
- package/src/routes/endpoints/admin.ts +75 -51
- package/src/routes/endpoints/checkpointing.ts +79 -8
- package/src/routes/endpoints/socket-route.ts +3 -2
- package/src/routes/endpoints/sync-rules.ts +26 -14
- package/src/routes/endpoints/sync-stream.ts +1 -1
- package/src/runner/teardown.ts +8 -8
- package/src/storage/BucketStorageBatch.ts +65 -18
- package/src/storage/BucketStorageFactory.ts +73 -46
- package/src/storage/CheckpointChecksumInvalidatedError.ts +17 -0
- package/src/storage/ChecksumCache.ts +27 -14
- package/src/storage/ParsedSyncConfigSet.ts +25 -0
- package/src/storage/PersistedReplicationStream.ts +55 -0
- package/src/storage/PersistedSyncConfigContent.ts +207 -0
- package/src/storage/PersistedSyncConfigStatus.ts +14 -0
- package/src/storage/ReplicationEventPayload.ts +6 -1
- package/src/storage/SourceEntity.ts +12 -2
- package/src/storage/SourceTable.ts +96 -8
- package/src/storage/SourceTableReconciler.ts +188 -0
- package/src/storage/StorageVersionConfig.ts +11 -1
- package/src/storage/SyncRulesBucketStorage.ts +189 -27
- package/src/storage/WriteCheckpointAPI.ts +140 -6
- package/src/storage/implementation/BucketDefinitionMapping.ts +539 -0
- package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
- package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +126 -0
- package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
- package/src/storage/storage-index.ts +11 -1
- package/src/storage/storage-metrics.ts +154 -8
- package/src/sync/BucketChecksumState.ts +105 -21
- package/src/sync/RequestTracker.ts +20 -0
- package/src/sync/sync.ts +214 -50
- package/src/sync/util.ts +33 -8
- package/src/system/ServiceContext.ts +7 -0
- package/src/tracing/PerformanceTracer.ts +41 -1
- package/src/util/checkpointing.ts +4 -22
- package/src/util/config/compound-config-collector.ts +31 -1
- package/src/util/config/defaults.ts +2 -0
- package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +1 -0
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -0
- package/src/util/config/sync-rules/sync-rules-provider.ts +5 -0
- package/src/util/config/types.ts +4 -1
- package/src/util/lsn.ts +9 -0
- package/src/util/protocol-types.ts +3 -2
- package/src/util/util-index.ts +1 -0
- package/src/util/utils.ts +16 -2
- package/src/util/write-checkpoint-batcher.ts +142 -0
- package/test/src/AbstractReplicator.test.ts +177 -0
- package/test/src/MetricAttributeFilteringExporter.test.ts +96 -0
- package/test/src/PersistedSyncConfigContent.test.ts +152 -0
- package/test/src/checksum_cache.test.ts +22 -0
- package/test/src/config.test.ts +142 -0
- package/test/src/diagnostics.test.ts +87 -34
- package/test/src/module-loader.test.ts +33 -1
- package/test/src/routes/admin.test.ts +188 -18
- package/test/src/routes/checkpointing.test.ts +54 -0
- package/test/src/routes/mocks.ts +6 -2
- package/test/src/routes/stream.test.ts +123 -2
- package/test/src/source-table-reconciler.test.ts +244 -0
- package/test/src/storage/SourceTable.test.ts +92 -0
- package/test/src/storage-metrics.test.ts +148 -0
- package/test/src/sync/BucketChecksumState.test.ts +98 -12
- package/test/src/sync/util.test.ts +28 -1
- package/test/src/util/checkpointing.test.ts +249 -0
- package/test/src/util/utils.test.ts +23 -0
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
- package/dist/storage/PersistedSyncRulesContent.js +0 -100
- package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
- package/src/storage/PersistedSyncRulesContent.ts +0 -174
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import * as storage from '../../../src/storage/storage-index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a SourceTable with the current `ref`-based options shape.
|
|
6
|
+
* Data/parameter sources are not relevant to the storeCurrentData behaviour, so they default to empty.
|
|
7
|
+
*/
|
|
8
|
+
function makeTable(options: Partial<storage.SourceTableOptions> = {}): storage.SourceTable {
|
|
9
|
+
return new storage.SourceTable({
|
|
10
|
+
id: 'test-id',
|
|
11
|
+
ref: { connectionTag: 'test', schema: 'public', name: 'test_table' },
|
|
12
|
+
objectId: 123,
|
|
13
|
+
replicaIdColumns: [{ name: 'id' }],
|
|
14
|
+
snapshotComplete: true,
|
|
15
|
+
bucketDataSources: [],
|
|
16
|
+
parameterLookupSources: [],
|
|
17
|
+
...options
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SourceTable', () => {
|
|
22
|
+
describe('storeCurrentData property', () => {
|
|
23
|
+
test('defaults to true', () => {
|
|
24
|
+
const table = makeTable();
|
|
25
|
+
expect(table.storeCurrentData).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('can be set to false', () => {
|
|
29
|
+
const table = makeTable();
|
|
30
|
+
table.storeCurrentData = false;
|
|
31
|
+
expect(table.storeCurrentData).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('is preserved when cloning', () => {
|
|
35
|
+
const table = makeTable();
|
|
36
|
+
table.storeCurrentData = false;
|
|
37
|
+
const cloned = table.clone();
|
|
38
|
+
|
|
39
|
+
expect(cloned.storeCurrentData).toBe(false);
|
|
40
|
+
expect(cloned).not.toBe(table);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('clone preserves all properties including storeCurrentData', () => {
|
|
44
|
+
const table = makeTable({
|
|
45
|
+
replicaIdColumns: [{ name: 'id', type: 'int4' }],
|
|
46
|
+
snapshotComplete: false,
|
|
47
|
+
eventDefinitionIds: new Set(['event-1'])
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
table.syncData = false;
|
|
51
|
+
table.syncParameters = false;
|
|
52
|
+
table.syncEvent = false;
|
|
53
|
+
table.storeCurrentData = false;
|
|
54
|
+
table.snapshotStatus = {
|
|
55
|
+
totalEstimatedCount: 100,
|
|
56
|
+
replicatedCount: 50,
|
|
57
|
+
lastKey: Buffer.from('test')
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const cloned = table.clone();
|
|
61
|
+
|
|
62
|
+
expect(cloned.syncData).toBe(false);
|
|
63
|
+
expect(cloned.syncParameters).toBe(false);
|
|
64
|
+
expect(cloned.syncEvent).toBe(false);
|
|
65
|
+
expect(cloned.storeCurrentData).toBe(false);
|
|
66
|
+
expect(cloned.snapshotStatus).toEqual(table.snapshotStatus);
|
|
67
|
+
expect(cloned.eventDefinitionIds).toEqual(new Set(['event-1']));
|
|
68
|
+
expect(cloned.eventDefinitionIds).not.toBe(table.eventDefinitionIds);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('integration with other properties', () => {
|
|
73
|
+
test('storeCurrentData does not affect syncAny', () => {
|
|
74
|
+
const table = makeTable();
|
|
75
|
+
|
|
76
|
+
table.storeCurrentData = false;
|
|
77
|
+
table.syncData = true;
|
|
78
|
+
table.syncParameters = false;
|
|
79
|
+
table.syncEvent = false;
|
|
80
|
+
|
|
81
|
+
expect(table.syncAny).toBe(true); // Should still be true
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('storeCurrentData is independent of snapshot status', () => {
|
|
85
|
+
const table = makeTable({ snapshotComplete: false });
|
|
86
|
+
|
|
87
|
+
table.storeCurrentData = false;
|
|
88
|
+
expect(table.snapshotComplete).toBe(false);
|
|
89
|
+
expect(table.storeCurrentData).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { MetricsEngine } from '@/metrics/MetricsEngine.js';
|
|
2
|
+
import {
|
|
3
|
+
MetricMetadata,
|
|
4
|
+
MetricsFactory,
|
|
5
|
+
ObservableGauge,
|
|
6
|
+
ObservableGaugeObservation
|
|
7
|
+
} from '@/metrics/metrics-interfaces.js';
|
|
8
|
+
import { BucketStorageFactory, StorageMetrics, StorageSyncConfigMetrics } from '@/storage/BucketStorageFactory.js';
|
|
9
|
+
import { createCoreStorageMetrics, initializeCoreStorageMetrics } from '@/storage/storage-metrics.js';
|
|
10
|
+
import { StorageMetric } from '@powersync/service-types';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
type ValueProvider = () => Promise<number | ObservableGaugeObservation[] | undefined>;
|
|
14
|
+
|
|
15
|
+
function testEngine() {
|
|
16
|
+
const providers = new Map<string, ValueProvider>();
|
|
17
|
+
const factory: MetricsFactory = {
|
|
18
|
+
createCounter: () => ({ add: () => {} }),
|
|
19
|
+
createUpDownCounter: () => ({ add: () => {} }),
|
|
20
|
+
createObservableGauge: (metadata: MetricMetadata): ObservableGauge => ({
|
|
21
|
+
setValueProvider: (valueProvider) => providers.set(metadata.name, valueProvider)
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
const engine = new MetricsEngine({ factory, disable_telemetry_sharing: true });
|
|
25
|
+
createCoreStorageMetrics(engine);
|
|
26
|
+
return { engine, providers };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function syncConfigMetrics(id: string, state: string, bytes: number, versionLabel?: string): StorageSyncConfigMetrics {
|
|
30
|
+
return {
|
|
31
|
+
sync_config_id: id,
|
|
32
|
+
sync_config_state: state,
|
|
33
|
+
version_label: versionLabel,
|
|
34
|
+
attributed_bucket_data_bytes: bytes,
|
|
35
|
+
attributed_parameter_indexes_bytes: bytes,
|
|
36
|
+
attributed_source_records_bytes: bytes,
|
|
37
|
+
attributed_object_storage_bytes: bytes
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function storageMetrics(sync_config_metrics: StorageSyncConfigMetrics[]): StorageMetrics {
|
|
42
|
+
return {
|
|
43
|
+
operations_size_bytes: 0,
|
|
44
|
+
parameters_size_bytes: 0,
|
|
45
|
+
replication_size_bytes: 0,
|
|
46
|
+
object_storage_size_bytes: 0,
|
|
47
|
+
sync_config_metrics
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('storage metrics', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.useFakeTimers();
|
|
54
|
+
vi.setSystemTime(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reports retired sync configs as zero, then stops reporting them', async () => {
|
|
62
|
+
let current = storageMetrics([syncConfigMetrics('config-a', 'ACTIVE', 100)]);
|
|
63
|
+
const storage = {
|
|
64
|
+
getStorageMetrics: async () => current
|
|
65
|
+
} as unknown as BucketStorageFactory;
|
|
66
|
+
|
|
67
|
+
const { engine, providers } = testEngine();
|
|
68
|
+
initializeCoreStorageMetrics(engine, storage);
|
|
69
|
+
const provider = providers.get(StorageMetric.ATTRIBUTED_BUCKET_DATA_BYTES)!;
|
|
70
|
+
|
|
71
|
+
expect(await provider()).toEqual([
|
|
72
|
+
{ value: 100, attributes: { sync_config_id: 'config-a', sync_config_state: 'ACTIVE' } }
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// The sync config is pruned. Its series must go to zero rather than keep its last value,
|
|
76
|
+
// since the SDK carries the last reported value forward under cumulative temporality.
|
|
77
|
+
current = storageMetrics([syncConfigMetrics('config-b', 'ACTIVE', 50)]);
|
|
78
|
+
vi.setSystemTime(61_000);
|
|
79
|
+
|
|
80
|
+
expect(await provider()).toEqual([
|
|
81
|
+
{ value: 50, attributes: { sync_config_id: 'config-b', sync_config_state: 'ACTIVE' } },
|
|
82
|
+
{ value: 0, attributes: { sync_config_id: 'config-a', sync_config_state: 'ACTIVE' } }
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// Once every reader has had a chance to observe the zero, stop reporting it entirely.
|
|
86
|
+
vi.setSystemTime(61_000 + 16 * 60_000);
|
|
87
|
+
|
|
88
|
+
expect(await provider()).toEqual([
|
|
89
|
+
{ value: 50, attributes: { sync_config_id: 'config-b', sync_config_state: 'ACTIVE' } }
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('treats a state transition as a retired attribute set', async () => {
|
|
94
|
+
let current = storageMetrics([syncConfigMetrics('config-a', 'PROCESSING', 100, 'v1')]);
|
|
95
|
+
const storage = {
|
|
96
|
+
getStorageMetrics: async () => current
|
|
97
|
+
} as unknown as BucketStorageFactory;
|
|
98
|
+
|
|
99
|
+
const { engine, providers } = testEngine();
|
|
100
|
+
initializeCoreStorageMetrics(engine, storage);
|
|
101
|
+
const provider = providers.get(StorageMetric.ATTRIBUTED_SOURCE_RECORDS_BYTES)!;
|
|
102
|
+
|
|
103
|
+
await provider();
|
|
104
|
+
|
|
105
|
+
current = storageMetrics([syncConfigMetrics('config-a', 'ACTIVE', 120, 'v1')]);
|
|
106
|
+
vi.setSystemTime(61_000);
|
|
107
|
+
|
|
108
|
+
expect(await provider()).toEqual([
|
|
109
|
+
{
|
|
110
|
+
value: 120,
|
|
111
|
+
attributes: { sync_config_id: 'config-a', sync_config_state: 'ACTIVE', version_label: 'v1' }
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
value: 0,
|
|
115
|
+
attributes: { sync_config_id: 'config-a', sync_config_state: 'PROCESSING', version_label: 'v1' }
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not report anything as retired when reading the metrics fails', async () => {
|
|
121
|
+
let fail = false;
|
|
122
|
+
const storage = {
|
|
123
|
+
getStorageMetrics: async () => {
|
|
124
|
+
if (fail) {
|
|
125
|
+
throw new Error('storage unavailable');
|
|
126
|
+
}
|
|
127
|
+
return storageMetrics([syncConfigMetrics('config-a', 'ACTIVE', 100)]);
|
|
128
|
+
}
|
|
129
|
+
} as unknown as BucketStorageFactory;
|
|
130
|
+
|
|
131
|
+
const { engine, providers } = testEngine();
|
|
132
|
+
initializeCoreStorageMetrics(engine, storage);
|
|
133
|
+
const provider = providers.get(StorageMetric.ATTRIBUTED_OBJECT_STORAGE_BYTES)!;
|
|
134
|
+
|
|
135
|
+
await provider();
|
|
136
|
+
|
|
137
|
+
fail = true;
|
|
138
|
+
vi.setSystemTime(61_000);
|
|
139
|
+
expect(await provider()).toBeUndefined();
|
|
140
|
+
|
|
141
|
+
// The failure must not be interpreted as the sync config having been pruned.
|
|
142
|
+
fail = false;
|
|
143
|
+
vi.setSystemTime(122_000);
|
|
144
|
+
expect(await provider()).toEqual([
|
|
145
|
+
{ value: 100, attributes: { sync_config_id: 'config-a', sync_config_state: 'ACTIVE' } }
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -137,6 +137,7 @@ bucket_definitions:
|
|
|
137
137
|
priority: 3
|
|
138
138
|
}
|
|
139
139
|
]);
|
|
140
|
+
expect(line.bucketDataRequestHint).toEqual('bulk');
|
|
140
141
|
// This is the bucket data to be fetched
|
|
141
142
|
expect(bucketStarts(line.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 0n]]));
|
|
142
143
|
|
|
@@ -170,6 +171,45 @@ bucket_definitions:
|
|
|
170
171
|
}
|
|
171
172
|
});
|
|
172
173
|
expect(bucketStarts(line2.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 1n]]));
|
|
174
|
+
expect(line2.bucketDataRequestHint).toEqual('incremental');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('unfinished bulk bucket does not make checkpoint diff bulk', async () => {
|
|
178
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
179
|
+
storage.updateTestChecksum({ bucket: '1#global[]', checksum: 1, count: 1 });
|
|
180
|
+
|
|
181
|
+
const state = new BucketChecksumState({
|
|
182
|
+
syncContext,
|
|
183
|
+
syncRequest,
|
|
184
|
+
tokenPayload,
|
|
185
|
+
syncRules: SYNC_RULES_GLOBAL,
|
|
186
|
+
bucketStorage: storage
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const line = (await state.buildNextCheckpointLine({
|
|
190
|
+
base: storage.makeCheckpoint(1n),
|
|
191
|
+
writeCheckpoint: null,
|
|
192
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
193
|
+
}))!;
|
|
194
|
+
line.advance();
|
|
195
|
+
line.updateBucketPosition({ bucket: '1#global[]', nextAfter: 1n, hasMore: true });
|
|
196
|
+
|
|
197
|
+
storage.updateTestChecksum({ bucket: '1#global[]', checksum: 2, count: 2 });
|
|
198
|
+
|
|
199
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
200
|
+
base: storage.makeCheckpoint(2n),
|
|
201
|
+
writeCheckpoint: null,
|
|
202
|
+
update: {
|
|
203
|
+
updatedDataBuckets: new Set(['1#global[]']),
|
|
204
|
+
invalidateDataBuckets: false,
|
|
205
|
+
updatedParameterLookups: new Set(),
|
|
206
|
+
invalidateParameterBuckets: false
|
|
207
|
+
}
|
|
208
|
+
}))!;
|
|
209
|
+
line2.advance();
|
|
210
|
+
|
|
211
|
+
expect(line2.bucketDataRequestHint).toEqual('incremental');
|
|
212
|
+
expect(bucketStarts(line2.getFilteredBucketPositions())).toEqual(new Map([['1#global[]', 1n]]));
|
|
173
213
|
});
|
|
174
214
|
|
|
175
215
|
test('global bucket with initial state', async () => {
|
|
@@ -1143,7 +1183,7 @@ streams:
|
|
|
1143
1183
|
).rejects.toThrow('Too many buckets: 60 (limit of 50');
|
|
1144
1184
|
|
|
1145
1185
|
// Verify error log includes breakdown
|
|
1146
|
-
expect(errorMessages[0]).toContain('
|
|
1186
|
+
expect(errorMessages[0]).toContain('(limit of 50)\nBuckets by sync stream:');
|
|
1147
1187
|
expect(errorMessages[0]).toContain('projects: 30');
|
|
1148
1188
|
expect(errorMessages[0]).toContain('tasks: 20');
|
|
1149
1189
|
expect(errorMessages[0]).toContain('comments: 10');
|
|
@@ -1157,15 +1197,60 @@ streams:
|
|
|
1157
1197
|
});
|
|
1158
1198
|
});
|
|
1159
1199
|
|
|
1160
|
-
test('
|
|
1161
|
-
|
|
1200
|
+
test('labels the bucket breakdown as bucket definitions for legacy sync rules', async () => {
|
|
1201
|
+
const legacySyncRules = SqlSyncRules.fromYaml(
|
|
1202
|
+
`
|
|
1203
|
+
bucket_definitions:
|
|
1204
|
+
projects:
|
|
1205
|
+
parameters: SELECT id FROM projects WHERE user_id = request.user_id()
|
|
1206
|
+
data: []
|
|
1207
|
+
`,
|
|
1208
|
+
{ defaultSchema: 'public' }
|
|
1209
|
+
).config.hydrate({ hydrationState: versionedHydrationState(5), sqlite: nodeSqlite(sqlite) });
|
|
1210
|
+
|
|
1211
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
1212
|
+
const errorMessages: string[] = [];
|
|
1213
|
+
const state = new BucketChecksumState({
|
|
1214
|
+
syncContext: new SyncContext({
|
|
1215
|
+
maxBuckets: 50,
|
|
1216
|
+
maxParameterQueryResults: 100,
|
|
1217
|
+
maxDataFetchConcurrency: 10
|
|
1218
|
+
}),
|
|
1219
|
+
tokenPayload: new JwtPayload({ sub: 'u1' }),
|
|
1220
|
+
syncRequest,
|
|
1221
|
+
syncRules: legacySyncRules,
|
|
1222
|
+
bucketStorage: storage,
|
|
1223
|
+
logger: {
|
|
1224
|
+
info: () => {},
|
|
1225
|
+
error: (message: string) => errorMessages.push(message),
|
|
1226
|
+
warn: () => {},
|
|
1227
|
+
debug: () => {}
|
|
1228
|
+
} as any
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
await expect(
|
|
1232
|
+
state.buildNextCheckpointLine({
|
|
1233
|
+
base: storage.makeCheckpoint(1n, (lookups) => [
|
|
1234
|
+
{ lookup: lookups[0], rows: Array.from({ length: 60 }, (_, id) => ({ id })) }
|
|
1235
|
+
]),
|
|
1236
|
+
writeCheckpoint: null,
|
|
1237
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
1238
|
+
})
|
|
1239
|
+
).rejects.toThrow('Too many buckets: 60 (limit of 50)');
|
|
1240
|
+
|
|
1241
|
+
expect(errorMessages[0]).toContain('(limit of 50)\nBuckets by bucket definition:');
|
|
1242
|
+
expect(errorMessages[0]).toContain('projects: 60');
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
test('limits bucket breakdown to top 100 sync streams', async () => {
|
|
1246
|
+
// Create 105 sync streams with dynamic parameters.
|
|
1162
1247
|
let yamlDefinitions = `
|
|
1163
1248
|
config:
|
|
1164
1249
|
edition: 3
|
|
1165
1250
|
|
|
1166
1251
|
streams:
|
|
1167
1252
|
`;
|
|
1168
|
-
for (let i = 1; i <=
|
|
1253
|
+
for (let i = 1; i <= 105; i++) {
|
|
1169
1254
|
yamlDefinitions += ` def${i}:\n`;
|
|
1170
1255
|
yamlDefinitions += ` auto_subscribe: true\n`;
|
|
1171
1256
|
yamlDefinitions += ` query: SELECT * FROM tbl WHERE b = auth.parameter('${i}')\n`;
|
|
@@ -1189,7 +1274,7 @@ streams:
|
|
|
1189
1274
|
};
|
|
1190
1275
|
|
|
1191
1276
|
const smallContext = new SyncContext({
|
|
1192
|
-
maxBuckets:
|
|
1277
|
+
maxBuckets: 100,
|
|
1193
1278
|
maxParameterQueryResults: 10,
|
|
1194
1279
|
maxDataFetchConcurrency: 10
|
|
1195
1280
|
});
|
|
@@ -1198,7 +1283,7 @@ streams:
|
|
|
1198
1283
|
syncContext: smallContext,
|
|
1199
1284
|
tokenPayload: new JwtPayload({
|
|
1200
1285
|
sub: 'u1',
|
|
1201
|
-
...Object.fromEntries(Array.from({ length:
|
|
1286
|
+
...Object.fromEntries(Array.from({ length: 106 }, (_, i) => [i.toString(), BigInt(i)]))
|
|
1202
1287
|
}),
|
|
1203
1288
|
syncRequest,
|
|
1204
1289
|
syncRules: SYNC_RULES_MANY,
|
|
@@ -1212,15 +1297,16 @@ streams:
|
|
|
1212
1297
|
writeCheckpoint: null,
|
|
1213
1298
|
update: CHECKPOINT_INVALIDATE_ALL
|
|
1214
1299
|
})
|
|
1215
|
-
).rejects.toThrow('Too many buckets:
|
|
1300
|
+
).rejects.toThrow('Too many buckets: 105 (limit of 100)');
|
|
1216
1301
|
|
|
1217
|
-
// Verify only
|
|
1302
|
+
// Verify only the first 100 are shown.
|
|
1218
1303
|
const errorMessage = errorMessages[0];
|
|
1219
|
-
expect(errorMessage).toContain('
|
|
1304
|
+
expect(errorMessage).toContain('Buckets by sync stream:');
|
|
1305
|
+
expect(errorMessage).toContain('... and 5 more buckets from 5 sync streams');
|
|
1220
1306
|
|
|
1221
|
-
// Count how many
|
|
1307
|
+
// Count how many sync streams are listed (should be exactly 100).
|
|
1222
1308
|
const defMatches = errorMessage.match(/def\d+:/g);
|
|
1223
|
-
expect(defMatches?.length).toBe(
|
|
1309
|
+
expect(defMatches?.length).toBe(100);
|
|
1224
1310
|
});
|
|
1225
1311
|
});
|
|
1226
1312
|
});
|
|
@@ -1240,7 +1326,7 @@ class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
|
|
|
1240
1326
|
this.filter?.({ invalidate: true });
|
|
1241
1327
|
}
|
|
1242
1328
|
|
|
1243
|
-
async getChecksums(_checkpoint:
|
|
1329
|
+
async getChecksums(_checkpoint: ReplicationCheckpoint, buckets: { bucket: string }[]): Promise<ChecksumMap> {
|
|
1244
1330
|
return new Map<string, BucketChecksum>(
|
|
1245
1331
|
buckets.map(({ bucket }) => {
|
|
1246
1332
|
const checksum = this.state.get(bucket);
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import { acquireSemaphoreAbortable } from '@/index.js';
|
|
1
|
+
import { acquireSemaphoreAbortable, isAbortError } from '@/index.js';
|
|
2
2
|
import { Semaphore, SemaphoreInterface } from 'async-mutex';
|
|
3
|
+
import { AbortError } from 'ix/aborterror.js';
|
|
3
4
|
import { describe, expect, test, vi } from 'vitest';
|
|
4
5
|
|
|
6
|
+
describe('isAbortError', () => {
|
|
7
|
+
test('recognizes ix and native abort errors', () => {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
controller.abort();
|
|
10
|
+
|
|
11
|
+
expect(isAbortError(new AbortError())).toBe(true);
|
|
12
|
+
expect(isAbortError(controller.signal.reason)).toBe(true);
|
|
13
|
+
expect(isAbortError(new Error('not aborted'))).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
5
17
|
describe('acquireSemaphoreAbortable', () => {
|
|
6
18
|
test('can acquire', async () => {
|
|
7
19
|
const semaphore = new Semaphore(1);
|
|
@@ -31,4 +43,19 @@ describe('acquireSemaphoreAbortable', () => {
|
|
|
31
43
|
// Releasing the semaphore should not invoke resolve again
|
|
32
44
|
release();
|
|
33
45
|
});
|
|
46
|
+
|
|
47
|
+
test('does not wait when the signal is already aborted', async () => {
|
|
48
|
+
const semaphore = new Semaphore(1);
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
|
|
51
|
+
// Hold the only slot, so acquiring would block indefinitely.
|
|
52
|
+
const result = await acquireSemaphoreAbortable(semaphore, controller.signal);
|
|
53
|
+
expect(result).not.toBe('aborted');
|
|
54
|
+
const [, release] = result as [number, SemaphoreInterface.Releaser];
|
|
55
|
+
|
|
56
|
+
controller.abort();
|
|
57
|
+
expect(await acquireSemaphoreAbortable(semaphore, controller.signal)).toBe('aborted');
|
|
58
|
+
|
|
59
|
+
release();
|
|
60
|
+
});
|
|
34
61
|
});
|