@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,21 @@
1
+ import { BucketDefinitionId, ParameterIndexId } from '@powersync/service-sync-rules';
2
+ import { SingleSyncConfigBucketDefinitionMapping } from './BucketDefinitionMapping.js';
3
+
4
+ /**
5
+ * Persisted storage ids for a replication stream, derived from the rule_mapping documents
6
+ * of all its sync configs.
7
+ *
8
+ * This requires no parsed sync configs, and deliberately does not resolve individual
9
+ * sources - use the parsed-set-bound {@link BucketDefinitionMapping} for that.
10
+ */
11
+ export class ReplicationStreamStorageIds {
12
+ constructor(private readonly mappings: SingleSyncConfigBucketDefinitionMapping[]) {}
13
+
14
+ get bucketDefinitionIds(): BucketDefinitionId[] {
15
+ return [...new Set(this.mappings.flatMap((mapping) => mapping.allBucketDefinitionIds()))];
16
+ }
17
+
18
+ get parameterIndexIds(): ParameterIndexId[] {
19
+ return [...new Set(this.mappings.flatMap((mapping) => mapping.allParameterIndexIds()))];
20
+ }
21
+ }
@@ -2,16 +2,26 @@ export * from './bson.js';
2
2
  export * from './BucketStorage.js';
3
3
  export * from './BucketStorageBatch.js';
4
4
  export * from './BucketStorageFactory.js';
5
+ export * from './CheckpointChecksumInvalidatedError.js';
5
6
  export * from './ChecksumCache.js';
6
- export * from './PersistedSyncRulesContent.js';
7
+ export * from './ParsedSyncConfigSet.js';
8
+ export * from './PersistedReplicationStream.js';
9
+ export * from './PersistedSyncConfigContent.js';
10
+ export * from './PersistedSyncConfigStatus.js';
7
11
  export * from './ReplicationEventPayload.js';
8
12
  export * from './ReplicationLock.js';
9
13
  export * from './ReportStorage.js';
10
14
  export * from './SourceEntity.js';
11
15
  export * from './SourceTable.js';
16
+ export * from './SourceTableReconciler.js';
12
17
  export * from './storage-metrics.js';
13
18
  export * from './StorageEngine.js';
14
19
  export * from './StorageProvider.js';
15
20
  export * from './StorageVersionConfig.js';
16
21
  export * from './SyncRulesBucketStorage.js';
17
22
  export * from './WriteCheckpointAPI.js';
23
+
24
+ export * from './implementation/BucketDefinitionMapping.js';
25
+ export * from './implementation/IncrementalReprocessingCompatibility.js';
26
+ export * from './implementation/IncrementalReprocessingSyncConfigLog.js';
27
+ export * from './implementation/ReplicationStreamStorageIds.js';
@@ -1,7 +1,7 @@
1
1
  import { logger } from '@powersync/lib-services-framework';
2
2
  import { StorageMetric } from '@powersync/service-types';
3
3
  import { MetricsEngine } from '../metrics/MetricsEngine.js';
4
- import { BucketStorageFactory, StorageMetrics } from './BucketStorageFactory.js';
4
+ import { BucketStorageFactory, StorageMetrics, StorageSyncConfigMetrics } from './BucketStorageFactory.js';
5
5
 
6
6
  export function createCoreStorageMetrics(engine: MetricsEngine): void {
7
7
  engine.createObservableGauge({
@@ -9,59 +9,205 @@ export function createCoreStorageMetrics(engine: MetricsEngine): void {
9
9
  description: 'Size of current data stored in PowerSync',
10
10
  unit: 'bytes'
11
11
  });
12
+ engine.createObservableGauge({
13
+ name: StorageMetric.ATTRIBUTED_SOURCE_RECORDS_BYTES,
14
+ description: 'Size of current data stored in PowerSync by sync config',
15
+ unit: 'bytes'
16
+ });
12
17
 
13
18
  engine.createObservableGauge({
14
19
  name: StorageMetric.OPERATION_SIZE_BYTES,
15
20
  description: 'Size of operations stored in PowerSync',
16
21
  unit: 'bytes'
17
22
  });
23
+ engine.createObservableGauge({
24
+ name: StorageMetric.ATTRIBUTED_BUCKET_DATA_BYTES,
25
+ description: 'Size of operations stored in PowerSync by sync config',
26
+ unit: 'bytes'
27
+ });
18
28
 
19
29
  engine.createObservableGauge({
20
30
  name: StorageMetric.PARAMETER_SIZE_BYTES,
21
31
  description: 'Size of parameter data stored in PowerSync',
22
32
  unit: 'bytes'
23
33
  });
34
+ engine.createObservableGauge({
35
+ name: StorageMetric.ATTRIBUTED_PARAMETER_INDEXES_BYTES,
36
+ description: 'Size of parameter data stored in PowerSync by sync config',
37
+ unit: 'bytes'
38
+ });
39
+
40
+ engine.createObservableGauge({
41
+ name: StorageMetric.OBJECT_STORAGE_SIZE_BYTES,
42
+ description: 'Size of active object-storage references stored in PowerSync',
43
+ unit: 'bytes'
44
+ });
45
+ engine.createObservableGauge({
46
+ name: StorageMetric.ATTRIBUTED_OBJECT_STORAGE_BYTES,
47
+ description: 'Size of active object-storage references by sync config',
48
+ unit: 'bytes'
49
+ });
24
50
  }
25
51
 
26
52
  export function initializeCoreStorageMetrics(engine: MetricsEngine, storage: BucketStorageFactory): void {
27
53
  const replication_storage_size_bytes = engine.getObservableGauge(StorageMetric.REPLICATION_SIZE_BYTES);
54
+ const attributed_source_records_bytes = engine.getObservableGauge(StorageMetric.ATTRIBUTED_SOURCE_RECORDS_BYTES);
28
55
  const operation_storage_size_bytes = engine.getObservableGauge(StorageMetric.OPERATION_SIZE_BYTES);
56
+ const attributed_bucket_data_bytes = engine.getObservableGauge(StorageMetric.ATTRIBUTED_BUCKET_DATA_BYTES);
29
57
  const parameter_storage_size_bytes = engine.getObservableGauge(StorageMetric.PARAMETER_SIZE_BYTES);
58
+ const attributed_parameter_indexes_bytes = engine.getObservableGauge(
59
+ StorageMetric.ATTRIBUTED_PARAMETER_INDEXES_BYTES
60
+ );
61
+ const object_storage_size_bytes = engine.getObservableGauge(StorageMetric.OBJECT_STORAGE_SIZE_BYTES);
62
+ const attributed_object_storage_bytes = engine.getObservableGauge(StorageMetric.ATTRIBUTED_OBJECT_STORAGE_BYTES);
30
63
 
31
64
  const MINIMUM_INTERVAL = 60_000;
32
65
 
66
+ /**
67
+ * How long a sync config that is no longer present keeps being reported as zero.
68
+ *
69
+ * This must comfortably exceed the longest metric export interval, so that every configured
70
+ * reader observes the zero at least once. The OTLP reader exports every 5 minutes.
71
+ */
72
+ const RETIRED_SYNC_CONFIG_INTERVAL = 15 * 60_000;
73
+
74
+ type SyncConfigAttributes = {
75
+ sync_config_id: string;
76
+ sync_config_state: string;
77
+ version_label?: string;
78
+ };
79
+
80
+ /**
81
+ * Attribute sets reported at least once, along with the last time each was present in the
82
+ * storage metrics.
83
+ *
84
+ * These gauges are exported with cumulative temporality, which means the metrics SDK keeps
85
+ * re-exporting the last reported value for every attribute set it has seen, also after we stop
86
+ * reporting that set. Without this, a sync config that has since been pruned would keep
87
+ * reporting its last non-zero size indefinitely, so sums over these gauges would drift upwards
88
+ * permanently. Reporting zero for a while instead leaves the SDK carrying a zero forward.
89
+ */
90
+ const reportedSyncConfigs = new Map<string, { attributes: SyncConfigAttributes; lastSeen: number }>();
91
+
92
+ const syncConfigKey = (attributes: SyncConfigAttributes) =>
93
+ JSON.stringify([attributes.sync_config_id, attributes.sync_config_state, attributes.version_label]);
94
+
95
+ const syncConfigAttributes = (syncConfig: StorageSyncConfigMetrics): SyncConfigAttributes => ({
96
+ sync_config_id: syncConfig.sync_config_id,
97
+ sync_config_state: syncConfig.sync_config_state,
98
+ ...(syncConfig.version_label == null ? {} : { version_label: syncConfig.version_label })
99
+ });
100
+
101
+ const trackSyncConfigs = (metrics: StorageMetrics | null) => {
102
+ if (metrics == null) {
103
+ // Failed to read the metrics - this says nothing about which sync configs still exist.
104
+ return;
105
+ }
106
+ const now = Date.now();
107
+ for (const syncConfig of metrics.sync_config_metrics ?? []) {
108
+ const attributes = syncConfigAttributes(syncConfig);
109
+ reportedSyncConfigs.set(syncConfigKey(attributes), { attributes, lastSeen: now });
110
+ }
111
+ for (const [key, entry] of reportedSyncConfigs) {
112
+ if (now - entry.lastSeen > RETIRED_SYNC_CONFIG_INTERVAL) {
113
+ reportedSyncConfigs.delete(key);
114
+ }
115
+ }
116
+ };
117
+
33
118
  let cachedRequest: Promise<StorageMetrics | null> | undefined = undefined;
34
119
  let cacheTimestamp = 0;
35
120
 
36
121
  const getMetrics = () => {
37
122
  if (!cachedRequest || Date.now() - cacheTimestamp > MINIMUM_INTERVAL) {
38
- cachedRequest = storage.getStorageMetrics().catch((e) => {
39
- logger.error(`Failed to get storage metrics`, e);
40
- return null;
41
- });
123
+ cachedRequest = storage
124
+ .getStorageMetrics()
125
+ .catch((e) => {
126
+ logger.error(`Failed to get storage metrics`, e);
127
+ return null;
128
+ })
129
+ .then((metrics) => {
130
+ trackSyncConfigs(metrics);
131
+ return metrics;
132
+ });
42
133
  cacheTimestamp = Date.now();
43
134
  }
44
135
  return cachedRequest;
45
136
  };
46
137
 
138
+ type StorageSizeMetricKey =
139
+ | 'attributed_bucket_data_bytes'
140
+ | 'attributed_parameter_indexes_bytes'
141
+ | 'attributed_source_records_bytes'
142
+ | 'attributed_object_storage_bytes';
143
+
144
+ function nonNegative(value: number): number;
145
+ function nonNegative(value: number | undefined): number | undefined;
146
+ function nonNegative(value: number | undefined) {
147
+ return value == null ? value : Math.max(0, value);
148
+ }
149
+
150
+ const observationsForSyncConfigs = (metrics: StorageMetrics, key: StorageSizeMetricKey) => {
151
+ const observations: { value: number; attributes?: Record<string, string> }[] = [];
152
+ const present = new Set<string>();
153
+ for (const syncConfig of metrics.sync_config_metrics ?? []) {
154
+ const attributes = syncConfigAttributes(syncConfig);
155
+ present.add(syncConfigKey(attributes));
156
+ observations.push({ value: nonNegative(syncConfig[key]), attributes });
157
+ }
158
+ for (const entry of reportedSyncConfigs.values()) {
159
+ if (!present.has(syncConfigKey(entry.attributes))) {
160
+ observations.push({ value: 0, attributes: entry.attributes });
161
+ }
162
+ }
163
+ return observations;
164
+ };
165
+
47
166
  replication_storage_size_bytes.setValueProvider(async () => {
167
+ const metrics = await getMetrics();
168
+ return nonNegative(metrics?.replication_size_bytes);
169
+ });
170
+
171
+ attributed_source_records_bytes.setValueProvider(async () => {
48
172
  const metrics = await getMetrics();
49
173
  if (metrics) {
50
- return metrics.replication_size_bytes;
174
+ return observationsForSyncConfigs(metrics, 'attributed_source_records_bytes');
51
175
  }
52
176
  });
53
177
 
54
178
  operation_storage_size_bytes.setValueProvider(async () => {
179
+ const metrics = await getMetrics();
180
+ return nonNegative(metrics?.operations_size_bytes);
181
+ });
182
+
183
+ attributed_bucket_data_bytes.setValueProvider(async () => {
55
184
  const metrics = await getMetrics();
56
185
  if (metrics) {
57
- return metrics.operations_size_bytes;
186
+ return observationsForSyncConfigs(metrics, 'attributed_bucket_data_bytes');
58
187
  }
59
188
  });
60
189
 
61
190
  parameter_storage_size_bytes.setValueProvider(async () => {
191
+ const metrics = await getMetrics();
192
+ return nonNegative(metrics?.parameters_size_bytes);
193
+ });
194
+
195
+ attributed_parameter_indexes_bytes.setValueProvider(async () => {
196
+ const metrics = await getMetrics();
197
+ if (metrics) {
198
+ return observationsForSyncConfigs(metrics, 'attributed_parameter_indexes_bytes');
199
+ }
200
+ });
201
+
202
+ object_storage_size_bytes.setValueProvider(async () => {
203
+ const metrics = await getMetrics();
204
+ return nonNegative(metrics?.object_storage_size_bytes);
205
+ });
206
+
207
+ attributed_object_storage_bytes.setValueProvider(async () => {
62
208
  const metrics = await getMetrics();
63
209
  if (metrics) {
64
- return metrics.parameters_size_bytes;
210
+ return observationsForSyncConfigs(metrics, 'attributed_object_storage_bytes');
65
211
  }
66
212
  });
67
213
  }
@@ -2,6 +2,7 @@ import {
2
2
  BucketParameterQuerier,
3
3
  BucketPriority,
4
4
  BucketSource,
5
+ BucketSourceType,
5
6
  HydratedSyncConfig,
6
7
  mergeBuckets,
7
8
  QuerierError,
@@ -22,9 +23,26 @@ import {
22
23
  } from '@powersync/lib-services-framework';
23
24
  import { JwtPayload } from '../auth/JwtPayload.js';
24
25
  import { ParameterSetLimitExceededError } from '../storage/storage-index.js';
26
+ import { PerformanceTracer } from '../tracing/PerformanceTracer.js';
25
27
  import { SyncContext } from './SyncContext.js';
26
28
  import { getIntersection, hasIntersection } from './util.js';
27
29
 
30
+ export type SyncCheckpointTraceCategory =
31
+ // buildNextCheckpointLine, excluding parameter computation and checksum lookups
32
+ | 'checkpoint'
33
+ // Parameter lookups
34
+ | 'parameters'
35
+ // Checksum calculations
36
+ | 'checksum'
37
+ // Waiting for a data lock to become available
38
+ | 'acquiring_lock'
39
+ // Data fetches. Holds one data lock for this period.
40
+ | 'bucket_data'
41
+ // Sending data to client, may include serialization overhead. Holds one data lock for this period in most cases.
42
+ | 'sending'
43
+ // Time spent reading concurrent S3 requests, counted once per batch.
44
+ | 's3';
45
+
28
46
  export interface BucketChecksumStateOptions {
29
47
  syncContext: SyncContext;
30
48
  bucketStorage: BucketChecksumStateStorage;
@@ -60,6 +78,12 @@ export class BucketChecksumState {
60
78
  */
61
79
  private lastChecksums: util.ChecksumMap | null = null;
62
80
  private lastWriteCheckpoint: bigint | null = null;
81
+ /**
82
+ * The next storage checkpoint diff may be relative to a checksum-invalidated
83
+ * checkpoint that was never sent to the client. Re-check every bucket once
84
+ * instead of applying that diff to the last client-visible checksums.
85
+ */
86
+ private forceFullChecksumForNextCheckpoint = false;
63
87
  /**
64
88
  * Once we've sent the first full checkpoint line including all {@link util.Checkpoint.streams} that the user is
65
89
  * subscribed to, we keep an index of the stream names to their index in that array.
@@ -101,6 +125,10 @@ export class BucketChecksumState {
101
125
  }
102
126
  }
103
127
 
128
+ invalidateChecksumBaseline() {
129
+ this.forceFullChecksumForNextCheckpoint = true;
130
+ }
131
+
104
132
  /**
105
133
  * Build a new checkpoint line for an underlying storage checkpoint update if any buckets have changed.
106
134
  *
@@ -112,14 +140,20 @@ export class BucketChecksumState {
112
140
  * @param next The updated checkpoint
113
141
  * @returns A {@link CheckpointLine} if any of the buckets watched by this connected was updated, or otherwise `null`.
114
142
  */
115
- async buildNextCheckpointLine(next: storage.StorageCheckpointUpdate): Promise<CheckpointLine | null> {
143
+ async buildNextCheckpointLine(
144
+ next: storage.StorageCheckpointUpdate,
145
+ tracer?: PerformanceTracer<SyncCheckpointTraceCategory>
146
+ ): Promise<CheckpointLine | null> {
147
+ tracer ??= new PerformanceTracer<SyncCheckpointTraceCategory>(`sync checkpoint ${next.base.checkpoint}`);
148
+
116
149
  const { writeCheckpoint, base } = next;
117
150
  const userIdForLogs = this.parameterState.syncParams.userId;
118
151
 
119
152
  const storage = this.bucketStorage;
120
153
 
121
- const update = await this.parameterState.getCheckpointUpdate(next);
154
+ const update = await this.parameterState.getCheckpointUpdate(next, tracer);
122
155
  const { buckets: allBuckets, updatedBuckets, usedParameterResults } = update;
156
+ const forceFullChecksum = this.forceFullChecksumForNextCheckpoint;
123
157
 
124
158
  /** Set of all buckets in this checkpoint. */
125
159
  const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
@@ -144,8 +178,9 @@ export class BucketChecksumState {
144
178
  const count = bucketsByDefinition.get(definition) ?? 0;
145
179
  bucketsByDefinition.set(definition, count + 1);
146
180
  }
147
-
148
- const breakdown = formatBucketDefinitionBreakdown(bucketsByDefinition);
181
+ // Only 1 type is allowed per sync config
182
+ const bucketSourceType = this.parameterState.syncRules.bucketSourceDefinitions[0].type;
183
+ const breakdown = formatBucketDefinitionBreakdown(bucketsByDefinition, bucketSourceType);
149
184
  errorMessage += breakdown.message;
150
185
  logData.buckets_by_definition = breakdown.countsByDefinition;
151
186
 
@@ -154,13 +189,14 @@ export class BucketChecksumState {
154
189
  }
155
190
 
156
191
  let checksumMap: util.ChecksumMap;
157
- if (updatedBuckets != INVALIDATE_ALL_BUCKETS) {
192
+ if (!forceFullChecksum && updatedBuckets != INVALIDATE_ALL_BUCKETS) {
158
193
  if (this.lastChecksums == null) {
159
194
  throw new ServiceAssertionError(`Bucket diff received without existing checksums`);
160
195
  }
161
196
 
162
197
  // Re-check updated buckets only
163
198
  let checksumLookups: storage.BucketChecksumRequest[] = [];
199
+ let hasNewBucket = false;
164
200
 
165
201
  let newChecksums = new Map<string, util.BucketChecksum>();
166
202
  for (let desc of bucketDescriptionMap.values()) {
@@ -174,11 +210,17 @@ export class BucketChecksumState {
174
210
  newChecksums.set(desc.bucket, existing);
175
211
  } else {
176
212
  checksumLookups.push({ bucket: desc.bucket, source: desc.source });
213
+ hasNewBucket ||= !this.lastChecksums.has(desc.bucket);
177
214
  }
178
215
  }
179
216
 
180
217
  if (checksumLookups.length > 0) {
181
- let updatedChecksums = await storage.getChecksums(base.checkpoint, checksumLookups);
218
+ const requestHint: storage.BucketRequestHint = hasNewBucket ? 'bulk' : 'incremental';
219
+ let updatedChecksums: util.ChecksumMap;
220
+
221
+ using _ = tracer.span('checksum');
222
+ updatedChecksums = await storage.getChecksums(base, checksumLookups, { requestHint });
223
+
182
224
  for (let [bucket, value] of updatedChecksums.entries()) {
183
225
  newChecksums.set(bucket, value);
184
226
  }
@@ -186,12 +228,23 @@ export class BucketChecksumState {
186
228
  checksumMap = newChecksums;
187
229
  } else {
188
230
  // Re-check all buckets
189
- const bucketList = [...bucketDescriptionMap.values()].map((b) => ({ bucket: b.bucket, source: b.source }));
190
- checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
231
+ const hasNewBucket =
232
+ this.lastChecksums == null ||
233
+ [...bucketDescriptionMap.values()].some((b) => !this.lastChecksums!.has(b.bucket));
234
+ const requestHint: storage.BucketRequestHint = hasNewBucket ? 'bulk' : 'incremental';
235
+ const bucketList = [...bucketDescriptionMap.values()].map((b) => ({
236
+ bucket: b.bucket,
237
+ source: b.source
238
+ }));
239
+
240
+ using _ = tracer.span('checksum');
241
+ checksumMap = await storage.getChecksums(base, bucketList, { requestHint });
191
242
  }
192
243
 
193
244
  // Subset of buckets for which there may be new data in this batch.
194
245
  let bucketsToFetch: ResolvedBucket[];
246
+ let bucketDataRequestHint: storage.BucketRequestHint;
247
+ const newBucketDownloads = new Set<string>();
195
248
 
196
249
  let checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
197
250
 
@@ -210,6 +263,9 @@ export class BucketChecksumState {
210
263
  diff.updatedBuckets.length == 0
211
264
  ) {
212
265
  // No changes - don't send anything to the client
266
+ if (forceFullChecksum) {
267
+ this.forceFullChecksumForNextCheckpoint = false;
268
+ }
213
269
  return null;
214
270
  }
215
271
 
@@ -232,6 +288,12 @@ export class BucketChecksumState {
232
288
  bucketsToFetch = [...generateBucketsToFetch].map((b) => {
233
289
  return bucketDescriptionMap.get(b)!;
234
290
  });
291
+ for (const bucket of diff.updatedBuckets) {
292
+ if (!this.lastChecksums!.has(bucket.bucket)) {
293
+ newBucketDownloads.add(bucket.bucket);
294
+ }
295
+ }
296
+ bucketDataRequestHint = newBucketDownloads.size > 0 ? 'bulk' : 'incremental';
235
297
 
236
298
  deferredLog = () => {
237
299
  let message = `Updated checkpoint: ${base.checkpoint} | `;
@@ -280,6 +342,10 @@ export class BucketChecksumState {
280
342
  );
281
343
  };
282
344
  bucketsToFetch = allBuckets;
345
+ for (const bucket of bucketsToFetch) {
346
+ newBucketDownloads.add(bucket.bucket);
347
+ }
348
+ bucketDataRequestHint = 'bulk';
283
349
 
284
350
  const subscriptions: util.StreamDescription[] = [];
285
351
  const streamNameToIndex = new Map<string, number>();
@@ -321,6 +387,7 @@ export class BucketChecksumState {
321
387
  return {
322
388
  checkpointLine,
323
389
  bucketsToFetch,
390
+ bucketDataRequestHint,
324
391
  advance: () => {
325
392
  hasAdvanced = true;
326
393
  // bucketDataPositions must be updated in-place - it represents the current state of
@@ -353,6 +420,9 @@ export class BucketChecksumState {
353
420
  this.lastChecksums = checksumMap;
354
421
  this.lastWriteCheckpoint = writeCheckpoint;
355
422
  this.pendingBucketDownloads = pendingBucketDownloads;
423
+ if (forceFullChecksum) {
424
+ this.forceFullChecksumForNextCheckpoint = false;
425
+ }
356
426
  deferredLog();
357
427
  },
358
428
 
@@ -512,12 +582,15 @@ export class BucketParameterState {
512
582
  return (desc.subscribedToByDefault && this.includeDefaultStreams) || this.subscribedStreamNames.has(desc.name);
513
583
  }
514
584
 
515
- async getCheckpointUpdate(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate> {
585
+ async getCheckpointUpdate(
586
+ checkpoint: storage.StorageCheckpointUpdate,
587
+ tracer: PerformanceTracer<SyncCheckpointTraceCategory>
588
+ ): Promise<CheckpointUpdate> {
516
589
  const querier = this.querier;
517
590
  let update: CheckpointUpdate;
518
591
  if (querier.hasDynamicBuckets) {
519
592
  try {
520
- update = await this.getCheckpointUpdateDynamic(checkpoint);
593
+ update = await this.getCheckpointUpdateDynamic(checkpoint, tracer);
521
594
  } catch (e: unknown) {
522
595
  if (e instanceof ParameterSetLimitExceededError) {
523
596
  // Too many parameter results, create a breakdown of which streams are responsible for the most queries and
@@ -580,7 +653,10 @@ export class BucketParameterState {
580
653
  /**
581
654
  * For dynamic buckets, we need to re-query the list of buckets every time.
582
655
  */
583
- private async getCheckpointUpdateDynamic(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate> {
656
+ private async getCheckpointUpdateDynamic(
657
+ checkpoint: storage.StorageCheckpointUpdate,
658
+ tracer: PerformanceTracer<SyncCheckpointTraceCategory>
659
+ ): Promise<CheckpointUpdate> {
584
660
  const querier = this.querier;
585
661
  const staticBuckets = this.staticBuckets.values();
586
662
  const update = checkpoint.update;
@@ -636,6 +712,7 @@ export class BucketParameterState {
636
712
  }
637
713
 
638
714
  try {
715
+ using _ = tracer.span('parameters', definition);
639
716
  const results = await checkpoint.base.getParameterSets(lookups, remainingBudget);
640
717
  const numRows = results.reduce((a, b) => a + b.rows.length, 0);
641
718
 
@@ -692,6 +769,7 @@ export class BucketParameterState {
692
769
  export interface CheckpointLine {
693
770
  checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
694
771
  bucketsToFetch: ResolvedBucket[];
772
+ bucketDataRequestHint: storage.BucketRequestHint;
695
773
 
696
774
  /**
697
775
  * Call when a checkpoint line is being sent to a client, to update the internal state.
@@ -738,30 +816,36 @@ function logCheckpoint(
738
816
  }
739
817
 
740
818
  /**
741
- * Format a breakdown of dynamic bucket by sync stream definition.
819
+ * Format a breakdown of dynamic buckets by sync stream or legacy bucket definition.
742
820
  *
743
- * Sorts definitions by count (descending), includes the top 10, and returns both the
821
+ * Sorts definitions by count (descending), includes the top 100, and returns both the
744
822
  * formatted message string and the counts record suitable for structured log data.
745
823
  */
746
- function formatBucketDefinitionBreakdown(bucketsByDefinition: Map<string, number>): {
824
+ function formatBucketDefinitionBreakdown(
825
+ bucketsByDefinition: Map<string, number>,
826
+ bucketSourceType: BucketSourceType
827
+ ): {
747
828
  message: string;
748
829
  countsByDefinition: Record<string, number>;
749
830
  } {
750
- // Sort definitions by count (descending) and take top 10
831
+ const maxLoggedDefinitions = 100;
832
+
833
+ // Sort definitions by count (descending) and take the largest entries.
751
834
  const allSorted = Array.from(bucketsByDefinition.entries()).sort((a, b) => b[1] - a[1]);
752
- const sortedDefinitions = allSorted.slice(0, 10);
835
+ const sortedDefinitions = allSorted.slice(0, maxLoggedDefinitions);
753
836
 
754
- let message = '\Buckets by definition:';
837
+ const sourceLabel = bucketSourceType == BucketSourceType.SYNC_STREAM ? 'sync stream' : 'bucket definition';
838
+ let message = `\nBuckets by ${sourceLabel}:`;
755
839
  const countsByDefinition: Record<string, number> = {};
756
840
  for (const [definition, count] of sortedDefinitions) {
757
841
  message += `\n ${definition}: ${count}`;
758
842
  countsByDefinition[definition] = count;
759
843
  }
760
844
 
761
- if (allSorted.length > 10) {
762
- const remainingResults = allSorted.slice(10).reduce((sum, [, count]) => sum + count, 0);
763
- const remainingDefinitions = allSorted.length - 10;
764
- message += `\n ... and ${remainingResults} more results from ${remainingDefinitions} definitions`;
845
+ if (allSorted.length > maxLoggedDefinitions) {
846
+ const remainingResults = allSorted.slice(maxLoggedDefinitions).reduce((sum, [, count]) => sum + count, 0);
847
+ const remainingDefinitions = allSorted.length - maxLoggedDefinitions;
848
+ message += `\n ... and ${remainingResults} more buckets from ${remainingDefinitions} ${sourceLabel}s`;
765
849
  }
766
850
 
767
851
  return { message, countsByDefinition };
@@ -15,6 +15,8 @@ export class RequestTracker {
15
15
  largeBuckets: Record<string, number> = {};
16
16
 
17
17
  private encoding: string | undefined = undefined;
18
+ private lastCheckpointLogOperationsSynced = 0;
19
+ private lastCheckpointLogDataSyncedBytes = 0;
18
20
 
19
21
  constructor(private metrics: MetricsEngine) {
20
22
  this.metrics = metrics;
@@ -68,6 +70,19 @@ export class RequestTracker {
68
70
  encoding: this.encoding
69
71
  };
70
72
  }
73
+
74
+ getIncrementalCheckpointStats(): CheckpointLogStats {
75
+ const operationsSynced = this.operationsSynced - this.lastCheckpointLogOperationsSynced;
76
+ const dataSyncedBytes = this.dataSyncedBytes - this.lastCheckpointLogDataSyncedBytes;
77
+
78
+ this.lastCheckpointLogOperationsSynced = this.operationsSynced;
79
+ this.lastCheckpointLogDataSyncedBytes = this.dataSyncedBytes;
80
+
81
+ return {
82
+ operations_synced: operationsSynced,
83
+ data_synced_bytes: dataSyncedBytes
84
+ };
85
+ }
71
86
  }
72
87
 
73
88
  export interface OperationCounts {
@@ -83,6 +98,11 @@ export interface OperationsSentStats {
83
98
  total: number;
84
99
  }
85
100
 
101
+ export interface CheckpointLogStats {
102
+ operations_synced: number;
103
+ data_synced_bytes: number;
104
+ }
105
+
86
106
  export function statsForBatch(batch: SyncBucketData): OperationsSentStats {
87
107
  let put = 0;
88
108
  let remove = 0;