@checkstack/healthcheck-tcp-backend 0.1.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @checkstack/healthcheck-tcp-backend
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
8
+
9
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
10
+
11
+ **Key changes:**
12
+
13
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
14
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
15
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
16
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
17
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
18
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
19
+
20
+ **Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [3dd1914]
25
+ - @checkstack/backend-api@0.7.0
26
+
27
+ ## 0.1.14
28
+
29
+ ### Patch Changes
30
+
31
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
32
+
33
+ ### Breaking Changes (Internal)
34
+
35
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
36
+
37
+ ### New Features
38
+
39
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
40
+ - `mergeCounter()` - track occurrences
41
+ - `mergeAverage()` - track sum/count, compute avg
42
+ - `mergeRate()` - track success/total, compute %
43
+ - `mergeMinMax()` - track min/max values
44
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
45
+
46
+ ### Improvements
47
+
48
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
49
+ - Prepares for real-time hourly aggregation without batch accumulation
50
+
51
+ - Updated dependencies [f676e11]
52
+ - Updated dependencies [48c2080]
53
+ - @checkstack/common@0.6.2
54
+ - @checkstack/backend-api@0.6.0
55
+ - @checkstack/healthcheck-common@0.8.2
56
+
3
57
  ## 0.1.13
4
58
 
5
59
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-tcp-backend",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,14 +9,14 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.1",
13
- "@checkstack/common": "0.6.0",
14
- "@checkstack/healthcheck-common": "0.7.0"
12
+ "@checkstack/backend-api": "0.5.2",
13
+ "@checkstack/common": "0.6.1",
14
+ "@checkstack/healthcheck-common": "0.8.1"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/bun": "^1.0.0",
18
18
  "typescript": "^5.0.0",
19
- "@checkstack/tsconfig": "0.0.2",
20
- "@checkstack/scripts": "0.1.0"
19
+ "@checkstack/tsconfig": "0.0.3",
20
+ "@checkstack/scripts": "0.1.1"
21
21
  }
22
22
  }
@@ -65,7 +65,7 @@ describe("BannerCollector", () => {
65
65
  });
66
66
  });
67
67
 
68
- describe("aggregateResult", () => {
68
+ describe("mergeResult", () => {
69
69
  it("should calculate average read time and banner rate", () => {
70
70
  const collector = new BannerCollector();
71
71
  const runs = [
@@ -87,10 +87,11 @@ describe("BannerCollector", () => {
87
87
  },
88
88
  ];
89
89
 
90
- const aggregated = collector.aggregateResult(runs);
90
+ let aggregated = collector.mergeResult(undefined, runs[0]);
91
+ aggregated = collector.mergeResult(aggregated, runs[1]);
91
92
 
92
- expect(aggregated.avgReadTimeMs).toBe(75);
93
- expect(aggregated.bannerRate).toBe(50);
93
+ expect(aggregated.avgReadTimeMs.avg).toBe(75);
94
+ expect(aggregated.bannerRate.rate).toBe(50);
94
95
  });
95
96
  });
96
97
 
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ mergeRate,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -50,20 +56,24 @@ const bannerResultSchema = healthResultSchema({
50
56
 
51
57
  export type BannerResult = z.infer<typeof bannerResultSchema>;
52
58
 
53
- const bannerAggregatedSchema = healthResultSchema({
54
- avgReadTimeMs: healthResultNumber({
59
+ // Aggregated result fields definition
60
+ const bannerAggregatedFields = {
61
+ avgReadTimeMs: aggregatedAverage({
55
62
  "x-chart-type": "line",
56
63
  "x-chart-label": "Avg Read Time",
57
64
  "x-chart-unit": "ms",
58
65
  }),
59
- bannerRate: healthResultNumber({
66
+ bannerRate: aggregatedRate({
60
67
  "x-chart-type": "gauge",
61
68
  "x-chart-label": "Banner Rate",
62
69
  "x-chart-unit": "%",
63
70
  }),
64
- });
71
+ };
65
72
 
66
- export type BannerAggregatedResult = z.infer<typeof bannerAggregatedSchema>;
73
+ // Type inferred from field definitions
74
+ export type BannerAggregatedResult = InferAggregatedResult<
75
+ typeof bannerAggregatedFields
76
+ >;
67
77
 
68
78
  // ============================================================================
69
79
  // BANNER COLLECTOR
@@ -73,15 +83,12 @@ export type BannerAggregatedResult = z.infer<typeof bannerAggregatedSchema>;
73
83
  * Built-in TCP banner collector.
74
84
  * Reads the initial banner/greeting from a TCP server.
75
85
  */
76
- export class BannerCollector
77
- implements
78
- CollectorStrategy<
79
- TcpTransportClient,
80
- BannerConfig,
81
- BannerResult,
82
- BannerAggregatedResult
83
- >
84
- {
86
+ export class BannerCollector implements CollectorStrategy<
87
+ TcpTransportClient,
88
+ BannerConfig,
89
+ BannerResult,
90
+ BannerAggregatedResult
91
+ > {
85
92
  id = "banner";
86
93
  displayName = "TCP Banner";
87
94
  description = "Read the initial banner/greeting from the server";
@@ -92,9 +99,9 @@ export class BannerCollector
92
99
 
93
100
  config = new Versioned({ version: 1, schema: bannerConfigSchema });
94
101
  result = new Versioned({ version: 1, schema: bannerResultSchema });
95
- aggregatedResult = new Versioned({
102
+ aggregatedResult = new VersionedAggregated({
96
103
  version: 1,
97
- schema: bannerAggregatedSchema,
104
+ fields: bannerAggregatedFields,
98
105
  });
99
106
 
100
107
  async execute({
@@ -123,28 +130,18 @@ export class BannerCollector
123
130
  };
124
131
  }
125
132
 
126
- aggregateResult(
127
- runs: HealthCheckRunForAggregation<BannerResult>[]
133
+ mergeResult(
134
+ existing: BannerAggregatedResult | undefined,
135
+ run: HealthCheckRunForAggregation<BannerResult>,
128
136
  ): BannerAggregatedResult {
129
- const times = runs
130
- .map((r) => r.metadata?.readTimeMs)
131
- .filter((v): v is number => typeof v === "number");
132
-
133
- const hasBanners = runs
134
- .map((r) => r.metadata?.hasBanner)
135
- .filter((v): v is boolean => typeof v === "boolean");
136
-
137
- const bannerCount = hasBanners.filter(Boolean).length;
137
+ const metadata = run.metadata;
138
138
 
139
139
  return {
140
- avgReadTimeMs:
141
- times.length > 0
142
- ? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
143
- : 0,
144
- bannerRate:
145
- hasBanners.length > 0
146
- ? Math.round((bannerCount / hasBanners.length) * 100)
147
- : 0,
140
+ avgReadTimeMs: mergeAverage(
141
+ existing?.avgReadTimeMs,
142
+ metadata?.readTimeMs,
143
+ ),
144
+ bannerRate: mergeRate(existing?.bannerRate, metadata?.hasBanner),
148
145
  };
149
146
  }
150
147
  }
@@ -90,7 +90,7 @@ describe("TcpHealthCheckStrategy", () => {
90
90
  });
91
91
  });
92
92
 
93
- describe("aggregateResult", () => {
93
+ describe("mergeResult", () => {
94
94
  it("should calculate averages correctly", () => {
95
95
  const strategy = new TcpHealthCheckStrategy();
96
96
  const runs = [
@@ -118,11 +118,12 @@ describe("TcpHealthCheckStrategy", () => {
118
118
  },
119
119
  ];
120
120
 
121
- const aggregated = strategy.aggregateResult(runs);
121
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
122
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
122
123
 
123
- expect(aggregated.avgConnectionTime).toBe(15);
124
- expect(aggregated.successRate).toBe(100);
125
- expect(aggregated.errorCount).toBe(0);
124
+ expect(aggregated.avgConnectionTime.avg).toBe(15);
125
+ expect(aggregated.successRate.rate).toBe(100);
126
+ expect(aggregated.errorCount.count).toBe(0);
126
127
  });
127
128
 
128
129
  it("should count errors and calculate success rate", () => {
@@ -153,10 +154,11 @@ describe("TcpHealthCheckStrategy", () => {
153
154
  },
154
155
  ];
155
156
 
156
- const aggregated = strategy.aggregateResult(runs);
157
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
158
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
157
159
 
158
- expect(aggregated.successRate).toBe(50);
159
- expect(aggregated.errorCount).toBe(1);
160
+ expect(aggregated.successRate.rate).toBe(50);
161
+ expect(aggregated.errorCount.count).toBe(1);
160
162
  });
161
163
  });
162
164
  });
package/src/strategy.ts CHANGED
@@ -2,8 +2,16 @@ import {
2
2
  HealthCheckStrategy,
3
3
  HealthCheckRunForAggregation,
4
4
  Versioned,
5
+ VersionedAggregated,
6
+ aggregatedAverage,
7
+ aggregatedRate,
8
+ aggregatedCounter,
9
+ mergeAverage,
10
+ mergeRate,
11
+ mergeCounter,
5
12
  z,
6
13
  type ConnectedClient,
14
+ type InferAggregatedResult,
7
15
  } from "@checkstack/backend-api";
8
16
  import {
9
17
  healthResultBoolean,
@@ -70,27 +78,25 @@ const tcpResultSchema = healthResultSchema({
70
78
 
71
79
  type TcpResult = z.infer<typeof tcpResultSchema>;
72
80
 
73
- /**
74
- * Aggregated metadata for buckets.
75
- */
76
- const tcpAggregatedSchema = healthResultSchema({
77
- avgConnectionTime: healthResultNumber({
81
+ /** Aggregated field definitions for bucket merging */
82
+ const tcpAggregatedFields = {
83
+ avgConnectionTime: aggregatedAverage({
78
84
  "x-chart-type": "line",
79
85
  "x-chart-label": "Avg Connection Time",
80
86
  "x-chart-unit": "ms",
81
87
  }),
82
- successRate: healthResultNumber({
88
+ successRate: aggregatedRate({
83
89
  "x-chart-type": "gauge",
84
90
  "x-chart-label": "Success Rate",
85
91
  "x-chart-unit": "%",
86
92
  }),
87
- errorCount: healthResultNumber({
93
+ errorCount: aggregatedCounter({
88
94
  "x-chart-type": "counter",
89
95
  "x-chart-label": "Errors",
90
96
  }),
91
- });
97
+ };
92
98
 
93
- type TcpAggregatedResult = z.infer<typeof tcpAggregatedSchema>;
99
+ type TcpAggregatedResult = InferAggregatedResult<typeof tcpAggregatedFields>;
94
100
 
95
101
  // ============================================================================
96
102
  // SOCKET INTERFACE (for testability)
@@ -159,15 +165,12 @@ const defaultSocketFactory: SocketFactory = () => {
159
165
  // STRATEGY
160
166
  // ============================================================================
161
167
 
162
- export class TcpHealthCheckStrategy
163
- implements
164
- HealthCheckStrategy<
165
- TcpConfig,
166
- TcpTransportClient,
167
- TcpResult,
168
- TcpAggregatedResult
169
- >
170
- {
168
+ export class TcpHealthCheckStrategy implements HealthCheckStrategy<
169
+ TcpConfig,
170
+ TcpTransportClient,
171
+ TcpResult,
172
+ typeof tcpAggregatedFields
173
+ > {
171
174
  id = "tcp";
172
175
  displayName = "TCP Health Check";
173
176
  description = "TCP port connectivity check with optional banner grab";
@@ -208,45 +211,33 @@ export class TcpHealthCheckStrategy
208
211
  ],
209
212
  });
210
213
 
211
- aggregatedResult: Versioned<TcpAggregatedResult> = new Versioned({
214
+ aggregatedResult = new VersionedAggregated({
212
215
  version: 1,
213
- schema: tcpAggregatedSchema,
216
+ fields: tcpAggregatedFields,
214
217
  });
215
218
 
216
- aggregateResult(
217
- runs: HealthCheckRunForAggregation<TcpResult>[]
219
+ mergeResult(
220
+ existing: TcpAggregatedResult | undefined,
221
+ run: HealthCheckRunForAggregation<TcpResult>,
218
222
  ): TcpAggregatedResult {
219
- const validRuns = runs.filter((r) => r.metadata);
220
-
221
- if (validRuns.length === 0) {
222
- return { avgConnectionTime: 0, successRate: 0, errorCount: 0 };
223
- }
224
-
225
- const connectionTimes = validRuns
226
- .map((r) => r.metadata?.connectionTimeMs)
227
- .filter((t): t is number => typeof t === "number");
223
+ const metadata = run.metadata;
228
224
 
229
- const avgConnectionTime =
230
- connectionTimes.length > 0
231
- ? Math.round(
232
- connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
233
- )
234
- : 0;
225
+ const avgConnectionTime = mergeAverage(
226
+ existing?.avgConnectionTime,
227
+ metadata?.connectionTimeMs,
228
+ );
235
229
 
236
- const successCount = validRuns.filter(
237
- (r) => r.metadata?.connected === true
238
- ).length;
239
- const successRate = Math.round((successCount / validRuns.length) * 100);
230
+ const isSuccess = metadata?.connected ?? false;
231
+ const successRate = mergeRate(existing?.successRate, isSuccess);
240
232
 
241
- const errorCount = validRuns.filter(
242
- (r) => r.metadata?.error !== undefined
243
- ).length;
233
+ const hasError = metadata?.error !== undefined;
234
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
244
235
 
245
236
  return { avgConnectionTime, successRate, errorCount };
246
237
  }
247
238
 
248
239
  async createClient(
249
- config: TcpConfig
240
+ config: TcpConfig,
250
241
  ): Promise<ConnectedClient<TcpTransportClient>> {
251
242
  const validatedConfig = this.config.validate(config);
252
243
  const socket = this.socketFactory();