@checkstack/healthcheck-tcp-backend 0.1.14 → 0.2.1

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,53 @@
1
1
  # @checkstack/healthcheck-tcp-backend
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 869b4ab: ## Health Check Execution Improvements
8
+
9
+ ### Breaking Changes (backend-api)
10
+
11
+ - `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
12
+
13
+ ### Features
14
+
15
+ - **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
16
+ - **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
17
+ - **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
18
+
19
+ ### Fixes
20
+
21
+ - Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
22
+ - Simplified registry type signatures by using default type parameters.
23
+
24
+ - Updated dependencies [869b4ab]
25
+ - @checkstack/backend-api@0.8.0
26
+
27
+ ## 0.2.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
32
+
33
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
34
+
35
+ **Key changes:**
36
+
37
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
38
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
39
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
40
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
41
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
42
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
43
+
44
+ **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.
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [3dd1914]
49
+ - @checkstack/backend-api@0.7.0
50
+
3
51
  ## 0.1.14
4
52
 
5
53
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-tcp-backend",
3
- "version": "0.1.14",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,9 +9,9 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.2",
13
- "@checkstack/common": "0.6.1",
14
- "@checkstack/healthcheck-common": "0.8.1"
12
+ "@checkstack/backend-api": "0.7.0",
13
+ "@checkstack/common": "0.6.2",
14
+ "@checkstack/healthcheck-common": "0.8.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/bun": "^1.0.0",
@@ -90,8 +90,8 @@ describe("BannerCollector", () => {
90
90
  let aggregated = collector.mergeResult(undefined, runs[0]);
91
91
  aggregated = collector.mergeResult(aggregated, runs[1]);
92
92
 
93
- expect(aggregated.avgReadTimeMs).toBe(75);
94
- expect(aggregated.bannerRate).toBe(50);
93
+ expect(aggregated.avgReadTimeMs.avg).toBe(75);
94
+ expect(aggregated.bannerRate.rate).toBe(50);
95
95
  });
96
96
  });
97
97
 
@@ -5,11 +5,11 @@ import {
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
7
  mergeAverage,
8
- averageStateSchema,
9
8
  mergeRate,
10
- rateStateSchema,
11
- type AverageState,
12
- type RateState,
9
+ VersionedAggregated,
10
+ aggregatedAverage,
11
+ aggregatedRate,
12
+ type InferAggregatedResult,
13
13
  } from "@checkstack/backend-api";
14
14
  import {
15
15
  healthResultNumber,
@@ -56,31 +56,24 @@ const bannerResultSchema = healthResultSchema({
56
56
 
57
57
  export type BannerResult = z.infer<typeof bannerResultSchema>;
58
58
 
59
- const bannerAggregatedDisplaySchema = healthResultSchema({
60
- avgReadTimeMs: healthResultNumber({
59
+ // Aggregated result fields definition
60
+ const bannerAggregatedFields = {
61
+ avgReadTimeMs: aggregatedAverage({
61
62
  "x-chart-type": "line",
62
63
  "x-chart-label": "Avg Read Time",
63
64
  "x-chart-unit": "ms",
64
65
  }),
65
- bannerRate: healthResultNumber({
66
+ bannerRate: aggregatedRate({
66
67
  "x-chart-type": "gauge",
67
68
  "x-chart-label": "Banner Rate",
68
69
  "x-chart-unit": "%",
69
70
  }),
70
- });
71
-
72
- const bannerAggregatedInternalSchema = z.object({
73
- _readTime: averageStateSchema
74
- .optional(),
75
- _banner: rateStateSchema
76
- .optional(),
77
- });
71
+ };
78
72
 
79
- const bannerAggregatedSchema = bannerAggregatedDisplaySchema.merge(
80
- bannerAggregatedInternalSchema,
81
- );
82
-
83
- export type BannerAggregatedResult = z.infer<typeof bannerAggregatedSchema>;
73
+ // Type inferred from field definitions
74
+ export type BannerAggregatedResult = InferAggregatedResult<
75
+ typeof bannerAggregatedFields
76
+ >;
84
77
 
85
78
  // ============================================================================
86
79
  // BANNER COLLECTOR
@@ -106,9 +99,9 @@ export class BannerCollector implements CollectorStrategy<
106
99
 
107
100
  config = new Versioned({ version: 1, schema: bannerConfigSchema });
108
101
  result = new Versioned({ version: 1, schema: bannerResultSchema });
109
- aggregatedResult = new Versioned({
102
+ aggregatedResult = new VersionedAggregated({
110
103
  version: 1,
111
- schema: bannerAggregatedSchema,
104
+ fields: bannerAggregatedFields,
112
105
  });
113
106
 
114
107
  async execute({
@@ -143,21 +136,12 @@ export class BannerCollector implements CollectorStrategy<
143
136
  ): BannerAggregatedResult {
144
137
  const metadata = run.metadata;
145
138
 
146
- const readTimeState = mergeAverage(
147
- existing?._readTime as AverageState | undefined,
148
- metadata?.readTimeMs,
149
- );
150
-
151
- const bannerState = mergeRate(
152
- existing?._banner as RateState | undefined,
153
- metadata?.hasBanner,
154
- );
155
-
156
139
  return {
157
- avgReadTimeMs: readTimeState.avg,
158
- bannerRate: bannerState.rate,
159
- _readTime: readTimeState,
160
- _banner: bannerState,
140
+ avgReadTimeMs: mergeAverage(
141
+ existing?.avgReadTimeMs,
142
+ metadata?.readTimeMs,
143
+ ),
144
+ bannerRate: mergeRate(existing?.bannerRate, metadata?.hasBanner),
161
145
  };
162
146
  }
163
147
  }
@@ -121,9 +121,9 @@ describe("TcpHealthCheckStrategy", () => {
121
121
  let aggregated = strategy.mergeResult(undefined, runs[0]);
122
122
  aggregated = strategy.mergeResult(aggregated, runs[1]);
123
123
 
124
- expect(aggregated.avgConnectionTime).toBe(15);
125
- expect(aggregated.successRate).toBe(100);
126
- 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);
127
127
  });
128
128
 
129
129
  it("should count errors and calculate success rate", () => {
@@ -157,8 +157,8 @@ describe("TcpHealthCheckStrategy", () => {
157
157
  let aggregated = strategy.mergeResult(undefined, runs[0]);
158
158
  aggregated = strategy.mergeResult(aggregated, runs[1]);
159
159
 
160
- expect(aggregated.successRate).toBe(50);
161
- expect(aggregated.errorCount).toBe(1);
160
+ expect(aggregated.successRate.rate).toBe(50);
161
+ expect(aggregated.errorCount.count).toBe(1);
162
162
  });
163
163
  });
164
164
  });
package/src/strategy.ts CHANGED
@@ -2,17 +2,17 @@ import {
2
2
  HealthCheckStrategy,
3
3
  HealthCheckRunForAggregation,
4
4
  Versioned,
5
- z,
6
- type ConnectedClient,
5
+ VersionedAggregated,
6
+ aggregatedAverage,
7
+ aggregatedRate,
8
+ aggregatedCounter,
7
9
  mergeAverage,
8
- averageStateSchema,
9
10
  mergeRate,
10
- rateStateSchema,
11
11
  mergeCounter,
12
- counterStateSchema,
13
- type AverageState,
14
- type RateState,
15
- type CounterState,
12
+ z,
13
+ type ConnectedClient,
14
+ type InferAggregatedResult,
15
+ baseStrategyConfigSchema,
16
16
  } from "@checkstack/backend-api";
17
17
  import {
18
18
  healthResultBoolean,
@@ -34,14 +34,9 @@ import type {
34
34
  * Configuration schema for TCP health checks.
35
35
  * Connection-only parameters - action params moved to BannerCollector.
36
36
  */
37
- export const tcpConfigSchema = z.object({
37
+ export const tcpConfigSchema = baseStrategyConfigSchema.extend({
38
38
  host: z.string().describe("Hostname or IP address"),
39
39
  port: z.number().int().min(1).max(65_535).describe("TCP port number"),
40
- timeout: z
41
- .number()
42
- .min(100)
43
- .default(5000)
44
- .describe("Connection timeout in milliseconds"),
45
40
  });
46
41
 
47
42
  export type TcpConfig = z.infer<typeof tcpConfigSchema>;
@@ -79,41 +74,25 @@ const tcpResultSchema = healthResultSchema({
79
74
 
80
75
  type TcpResult = z.infer<typeof tcpResultSchema>;
81
76
 
82
- /**
83
- * Aggregated metadata for buckets.
84
- */
85
- // UI-visible aggregated fields
86
- const tcpAggregatedDisplaySchema = healthResultSchema({
87
- avgConnectionTime: healthResultNumber({
77
+ /** Aggregated field definitions for bucket merging */
78
+ const tcpAggregatedFields = {
79
+ avgConnectionTime: aggregatedAverage({
88
80
  "x-chart-type": "line",
89
81
  "x-chart-label": "Avg Connection Time",
90
82
  "x-chart-unit": "ms",
91
83
  }),
92
- successRate: healthResultNumber({
84
+ successRate: aggregatedRate({
93
85
  "x-chart-type": "gauge",
94
86
  "x-chart-label": "Success Rate",
95
87
  "x-chart-unit": "%",
96
88
  }),
97
- errorCount: healthResultNumber({
89
+ errorCount: aggregatedCounter({
98
90
  "x-chart-type": "counter",
99
91
  "x-chart-label": "Errors",
100
92
  }),
101
- });
102
-
103
- // Internal state for incremental aggregation
104
- const tcpAggregatedInternalSchema = z.object({
105
- _connectionTime: averageStateSchema
106
- .optional(),
107
- _success: rateStateSchema
108
- .optional(),
109
- _errors: counterStateSchema.optional(),
110
- });
111
-
112
- const tcpAggregatedSchema = tcpAggregatedDisplaySchema.merge(
113
- tcpAggregatedInternalSchema,
114
- );
93
+ };
115
94
 
116
- type TcpAggregatedResult = z.infer<typeof tcpAggregatedSchema>;
95
+ type TcpAggregatedResult = InferAggregatedResult<typeof tcpAggregatedFields>;
117
96
 
118
97
  // ============================================================================
119
98
  // SOCKET INTERFACE (for testability)
@@ -186,7 +165,7 @@ export class TcpHealthCheckStrategy implements HealthCheckStrategy<
186
165
  TcpConfig,
187
166
  TcpTransportClient,
188
167
  TcpResult,
189
- TcpAggregatedResult
168
+ typeof tcpAggregatedFields
190
169
  > {
191
170
  id = "tcp";
192
171
  displayName = "TCP Health Check";
@@ -228,9 +207,9 @@ export class TcpHealthCheckStrategy implements HealthCheckStrategy<
228
207
  ],
229
208
  });
230
209
 
231
- aggregatedResult: Versioned<TcpAggregatedResult> = new Versioned({
210
+ aggregatedResult = new VersionedAggregated({
232
211
  version: 1,
233
- schema: tcpAggregatedSchema,
212
+ fields: tcpAggregatedFields,
234
213
  });
235
214
 
236
215
  mergeResult(
@@ -239,29 +218,18 @@ export class TcpHealthCheckStrategy implements HealthCheckStrategy<
239
218
  ): TcpAggregatedResult {
240
219
  const metadata = run.metadata;
241
220
 
242
- const connectionTimeState = mergeAverage(
243
- existing?._connectionTime as AverageState | undefined,
221
+ const avgConnectionTime = mergeAverage(
222
+ existing?.avgConnectionTime,
244
223
  metadata?.connectionTimeMs,
245
224
  );
246
225
 
247
- const successState = mergeRate(
248
- existing?._success as RateState | undefined,
249
- metadata?.connected,
250
- );
226
+ const isSuccess = metadata?.connected ?? false;
227
+ const successRate = mergeRate(existing?.successRate, isSuccess);
251
228
 
252
- const errorState = mergeCounter(
253
- existing?._errors as CounterState | undefined,
254
- metadata?.error !== undefined,
255
- );
229
+ const hasError = metadata?.error !== undefined;
230
+ const errorCount = mergeCounter(existing?.errorCount, hasError);
256
231
 
257
- return {
258
- avgConnectionTime: connectionTimeState.avg,
259
- successRate: successState.rate,
260
- errorCount: errorState.count,
261
- _connectionTime: connectionTimeState,
262
- _success: successState,
263
- _errors: errorState,
264
- };
232
+ return { avgConnectionTime, successRate, errorCount };
265
233
  }
266
234
 
267
235
  async createClient(