@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.
Files changed (230) hide show
  1. package/CHANGELOG.md +152 -4
  2. package/dist/api/RouteAPI.d.ts +42 -14
  3. package/dist/api/diagnostics.d.ts +12 -1
  4. package/dist/api/diagnostics.js +36 -22
  5. package/dist/api/diagnostics.js.map +1 -1
  6. package/dist/entry/commands/compact-action.js +30 -22
  7. package/dist/entry/commands/compact-action.js.map +1 -1
  8. package/dist/metrics/metrics-interfaces.d.ts +5 -1
  9. package/dist/metrics/metrics-interfaces.js.map +1 -1
  10. package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.d.ts +36 -0
  11. package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js +70 -0
  12. package/dist/metrics/open-telemetry/MetricAttributeFilteringExporter.js.map +1 -0
  13. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +6 -1
  14. package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
  15. package/dist/metrics/open-telemetry/util.js +7 -3
  16. package/dist/metrics/open-telemetry/util.js.map +1 -1
  17. package/dist/modules/AbstractModule.d.ts +6 -2
  18. package/dist/modules/AbstractModule.js.map +1 -1
  19. package/dist/replication/AbstractReplicator.d.ts +31 -7
  20. package/dist/replication/AbstractReplicator.js +168 -74
  21. package/dist/replication/AbstractReplicator.js.map +1 -1
  22. package/dist/replication/ReplicationModule.d.ts +3 -3
  23. package/dist/replication/ReplicationModule.js.map +1 -1
  24. package/dist/routes/RouterEngine.d.ts +1 -0
  25. package/dist/routes/RouterEngine.js +6 -1
  26. package/dist/routes/RouterEngine.js.map +1 -1
  27. package/dist/routes/configure-fastify.d.ts +104 -56
  28. package/dist/routes/configure-fastify.js +15 -2
  29. package/dist/routes/configure-fastify.js.map +1 -1
  30. package/dist/routes/endpoints/admin.d.ts +42 -24
  31. package/dist/routes/endpoints/admin.js +49 -36
  32. package/dist/routes/endpoints/admin.js.map +1 -1
  33. package/dist/routes/endpoints/checkpointing.d.ts +62 -0
  34. package/dist/routes/endpoints/checkpointing.js +65 -7
  35. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  36. package/dist/routes/endpoints/socket-route.js +3 -2
  37. package/dist/routes/endpoints/socket-route.js.map +1 -1
  38. package/dist/routes/endpoints/sync-rules.js +25 -14
  39. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  40. package/dist/routes/endpoints/sync-stream.js +1 -1
  41. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  42. package/dist/runner/teardown.js +8 -8
  43. package/dist/runner/teardown.js.map +1 -1
  44. package/dist/storage/BucketStorageBatch.d.ts +64 -17
  45. package/dist/storage/BucketStorageBatch.js.map +1 -1
  46. package/dist/storage/BucketStorageFactory.d.ts +50 -31
  47. package/dist/storage/BucketStorageFactory.js +10 -20
  48. package/dist/storage/BucketStorageFactory.js.map +1 -1
  49. package/dist/storage/CheckpointChecksumInvalidatedError.d.ts +12 -0
  50. package/dist/storage/CheckpointChecksumInvalidatedError.js +17 -0
  51. package/dist/storage/CheckpointChecksumInvalidatedError.js.map +1 -0
  52. package/dist/storage/ChecksumCache.d.ts +11 -6
  53. package/dist/storage/ChecksumCache.js +5 -4
  54. package/dist/storage/ChecksumCache.js.map +1 -1
  55. package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
  56. package/dist/storage/ParsedSyncConfigSet.js +2 -0
  57. package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
  58. package/dist/storage/PersistedReplicationStream.d.ts +32 -0
  59. package/dist/storage/PersistedReplicationStream.js +26 -0
  60. package/dist/storage/PersistedReplicationStream.js.map +1 -0
  61. package/dist/storage/PersistedSyncConfigContent.d.ts +65 -0
  62. package/dist/storage/PersistedSyncConfigContent.js +138 -0
  63. package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
  64. package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
  65. package/dist/storage/PersistedSyncConfigStatus.js +2 -0
  66. package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
  67. package/dist/storage/ReplicationEventPayload.d.ts +6 -1
  68. package/dist/storage/SourceEntity.d.ts +13 -2
  69. package/dist/storage/SourceTable.d.ts +53 -4
  70. package/dist/storage/SourceTable.js +62 -7
  71. package/dist/storage/SourceTable.js.map +1 -1
  72. package/dist/storage/SourceTableReconciler.d.ts +83 -0
  73. package/dist/storage/SourceTableReconciler.js +95 -0
  74. package/dist/storage/SourceTableReconciler.js.map +1 -0
  75. package/dist/storage/StorageVersionConfig.d.ts +4 -0
  76. package/dist/storage/StorageVersionConfig.js +10 -1
  77. package/dist/storage/StorageVersionConfig.js.map +1 -1
  78. package/dist/storage/SyncRulesBucketStorage.d.ts +162 -26
  79. package/dist/storage/SyncRulesBucketStorage.js +3 -0
  80. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  81. package/dist/storage/WriteCheckpointAPI.d.ts +85 -6
  82. package/dist/storage/WriteCheckpointAPI.js +33 -0
  83. package/dist/storage/WriteCheckpointAPI.js.map +1 -1
  84. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +133 -0
  85. package/dist/storage/implementation/BucketDefinitionMapping.js +351 -0
  86. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  87. package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
  88. package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
  89. package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
  90. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
  91. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +70 -0
  92. package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
  93. package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
  94. package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
  95. package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
  96. package/dist/storage/storage-index.d.ts +10 -1
  97. package/dist/storage/storage-index.js +10 -1
  98. package/dist/storage/storage-index.js.map +1 -1
  99. package/dist/storage/storage-metrics.js +120 -4
  100. package/dist/storage/storage-metrics.js.map +1 -1
  101. package/dist/sync/BucketChecksumState.d.ts +12 -2
  102. package/dist/sync/BucketChecksumState.js +69 -21
  103. package/dist/sync/BucketChecksumState.js.map +1 -1
  104. package/dist/sync/RequestTracker.d.ts +7 -0
  105. package/dist/sync/RequestTracker.js +12 -0
  106. package/dist/sync/RequestTracker.js.map +1 -1
  107. package/dist/sync/sync.js +184 -43
  108. package/dist/sync/sync.js.map +1 -1
  109. package/dist/sync/util.d.ts +5 -0
  110. package/dist/sync/util.js +19 -1
  111. package/dist/sync/util.js.map +1 -1
  112. package/dist/system/ServiceContext.d.ts +2 -0
  113. package/dist/system/ServiceContext.js +2 -0
  114. package/dist/system/ServiceContext.js.map +1 -1
  115. package/dist/tracing/PerformanceTracer.d.ts +23 -1
  116. package/dist/tracing/PerformanceTracer.js +11 -0
  117. package/dist/tracing/PerformanceTracer.js.map +1 -1
  118. package/dist/util/checkpointing.d.ts +4 -8
  119. package/dist/util/checkpointing.js +1 -16
  120. package/dist/util/checkpointing.js.map +1 -1
  121. package/dist/util/config/compound-config-collector.js +23 -2
  122. package/dist/util/config/compound-config-collector.js.map +1 -1
  123. package/dist/util/config/defaults.d.ts +2 -0
  124. package/dist/util/config/defaults.js +2 -0
  125. package/dist/util/config/defaults.js.map +1 -1
  126. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +1 -0
  127. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
  128. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -0
  129. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  130. package/dist/util/config/sync-rules/sync-rules-provider.d.ts +2 -0
  131. package/dist/util/config/sync-rules/sync-rules-provider.js +3 -0
  132. package/dist/util/config/sync-rules/sync-rules-provider.js.map +1 -1
  133. package/dist/util/config/types.d.ts +4 -1
  134. package/dist/util/lsn.d.ts +4 -0
  135. package/dist/util/lsn.js +10 -0
  136. package/dist/util/lsn.js.map +1 -1
  137. package/dist/util/protocol-types.d.ts +4 -4
  138. package/dist/util/protocol-types.js +3 -2
  139. package/dist/util/protocol-types.js.map +1 -1
  140. package/dist/util/util-index.d.ts +1 -0
  141. package/dist/util/util-index.js +1 -0
  142. package/dist/util/util-index.js.map +1 -1
  143. package/dist/util/utils.d.ts +3 -2
  144. package/dist/util/utils.js +17 -2
  145. package/dist/util/utils.js.map +1 -1
  146. package/dist/util/write-checkpoint-batcher.d.ts +19 -0
  147. package/dist/util/write-checkpoint-batcher.js +100 -0
  148. package/dist/util/write-checkpoint-batcher.js.map +1 -0
  149. package/package.json +7 -7
  150. package/src/api/RouteAPI.ts +43 -14
  151. package/src/api/diagnostics.ts +45 -26
  152. package/src/entry/commands/compact-action.ts +37 -23
  153. package/src/metrics/metrics-interfaces.ts +6 -1
  154. package/src/metrics/open-telemetry/MetricAttributeFilteringExporter.ts +85 -0
  155. package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +7 -2
  156. package/src/metrics/open-telemetry/util.ts +7 -3
  157. package/src/modules/AbstractModule.ts +6 -2
  158. package/src/replication/AbstractReplicator.ts +212 -78
  159. package/src/replication/ReplicationModule.ts +2 -3
  160. package/src/routes/RouterEngine.ts +7 -1
  161. package/src/routes/configure-fastify.ts +25 -2
  162. package/src/routes/endpoints/admin.ts +75 -51
  163. package/src/routes/endpoints/checkpointing.ts +79 -8
  164. package/src/routes/endpoints/socket-route.ts +3 -2
  165. package/src/routes/endpoints/sync-rules.ts +26 -14
  166. package/src/routes/endpoints/sync-stream.ts +1 -1
  167. package/src/runner/teardown.ts +8 -8
  168. package/src/storage/BucketStorageBatch.ts +65 -18
  169. package/src/storage/BucketStorageFactory.ts +73 -46
  170. package/src/storage/CheckpointChecksumInvalidatedError.ts +17 -0
  171. package/src/storage/ChecksumCache.ts +27 -14
  172. package/src/storage/ParsedSyncConfigSet.ts +25 -0
  173. package/src/storage/PersistedReplicationStream.ts +55 -0
  174. package/src/storage/PersistedSyncConfigContent.ts +207 -0
  175. package/src/storage/PersistedSyncConfigStatus.ts +14 -0
  176. package/src/storage/ReplicationEventPayload.ts +6 -1
  177. package/src/storage/SourceEntity.ts +12 -2
  178. package/src/storage/SourceTable.ts +96 -8
  179. package/src/storage/SourceTableReconciler.ts +188 -0
  180. package/src/storage/StorageVersionConfig.ts +11 -1
  181. package/src/storage/SyncRulesBucketStorage.ts +189 -27
  182. package/src/storage/WriteCheckpointAPI.ts +140 -6
  183. package/src/storage/implementation/BucketDefinitionMapping.ts +539 -0
  184. package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
  185. package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +126 -0
  186. package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
  187. package/src/storage/storage-index.ts +11 -1
  188. package/src/storage/storage-metrics.ts +154 -8
  189. package/src/sync/BucketChecksumState.ts +105 -21
  190. package/src/sync/RequestTracker.ts +20 -0
  191. package/src/sync/sync.ts +214 -50
  192. package/src/sync/util.ts +33 -8
  193. package/src/system/ServiceContext.ts +7 -0
  194. package/src/tracing/PerformanceTracer.ts +41 -1
  195. package/src/util/checkpointing.ts +4 -22
  196. package/src/util/config/compound-config-collector.ts +31 -1
  197. package/src/util/config/defaults.ts +2 -0
  198. package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +1 -0
  199. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -0
  200. package/src/util/config/sync-rules/sync-rules-provider.ts +5 -0
  201. package/src/util/config/types.ts +4 -1
  202. package/src/util/lsn.ts +9 -0
  203. package/src/util/protocol-types.ts +3 -2
  204. package/src/util/util-index.ts +1 -0
  205. package/src/util/utils.ts +16 -2
  206. package/src/util/write-checkpoint-batcher.ts +142 -0
  207. package/test/src/AbstractReplicator.test.ts +177 -0
  208. package/test/src/MetricAttributeFilteringExporter.test.ts +96 -0
  209. package/test/src/PersistedSyncConfigContent.test.ts +152 -0
  210. package/test/src/checksum_cache.test.ts +22 -0
  211. package/test/src/config.test.ts +142 -0
  212. package/test/src/diagnostics.test.ts +87 -34
  213. package/test/src/module-loader.test.ts +33 -1
  214. package/test/src/routes/admin.test.ts +188 -18
  215. package/test/src/routes/checkpointing.test.ts +54 -0
  216. package/test/src/routes/mocks.ts +6 -2
  217. package/test/src/routes/stream.test.ts +123 -2
  218. package/test/src/source-table-reconciler.test.ts +244 -0
  219. package/test/src/storage/SourceTable.test.ts +92 -0
  220. package/test/src/storage-metrics.test.ts +148 -0
  221. package/test/src/sync/BucketChecksumState.test.ts +98 -12
  222. package/test/src/sync/util.test.ts +28 -1
  223. package/test/src/util/checkpointing.test.ts +249 -0
  224. package/test/src/util/utils.test.ts +23 -0
  225. package/tsconfig.json +1 -4
  226. package/tsconfig.tsbuildinfo +1 -1
  227. package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
  228. package/dist/storage/PersistedSyncRulesContent.js +0 -100
  229. package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
  230. 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('Buckets by definition:');
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('limits bucket breakdown to top 10 definitions', async () => {
1161
- // Create sync streams with 15 different definitions with dynamic parameters
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 <= 15; 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: 10,
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: 30 }, (_, i) => [i.toString(), BigInt(i)]))
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: 15 (limit of 10)');
1300
+ ).rejects.toThrow('Too many buckets: 105 (limit of 100)');
1216
1301
 
1217
- // Verify only top 10 are shown
1302
+ // Verify only the first 100 are shown.
1218
1303
  const errorMessage = errorMessages[0];
1219
- expect(errorMessage).toContain('... and 5 more results from 5 definitions');
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 definitions are listed (should be exactly 10)
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(10);
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: InternalOpId, buckets: { bucket: string }[]): Promise<ChecksumMap> {
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
  });