@checkstack/healthcheck-tcp-backend 0.1.13 → 0.1.14

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,35 @@
1
1
  # @checkstack/healthcheck-tcp-backend
2
2
 
3
+ ## 0.1.14
4
+
5
+ ### Patch Changes
6
+
7
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
8
+
9
+ ### Breaking Changes (Internal)
10
+
11
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
12
+
13
+ ### New Features
14
+
15
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
16
+ - `mergeCounter()` - track occurrences
17
+ - `mergeAverage()` - track sum/count, compute avg
18
+ - `mergeRate()` - track success/total, compute %
19
+ - `mergeMinMax()` - track min/max values
20
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
21
+
22
+ ### Improvements
23
+
24
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
25
+ - Prepares for real-time hourly aggregation without batch accumulation
26
+
27
+ - Updated dependencies [f676e11]
28
+ - Updated dependencies [48c2080]
29
+ - @checkstack/common@0.6.2
30
+ - @checkstack/backend-api@0.6.0
31
+ - @checkstack/healthcheck-common@0.8.2
32
+
3
33
  ## 0.1.13
4
34
 
5
35
  ### 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.1.14",
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,7 +87,8 @@ 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
93
  expect(aggregated.avgReadTimeMs).toBe(75);
93
94
  expect(aggregated.bannerRate).toBe(50);
@@ -4,6 +4,12 @@ import {
4
4
  type HealthCheckRunForAggregation,
5
5
  type CollectorResult,
6
6
  type CollectorStrategy,
7
+ mergeAverage,
8
+ averageStateSchema,
9
+ mergeRate,
10
+ rateStateSchema,
11
+ type AverageState,
12
+ type RateState,
7
13
  } from "@checkstack/backend-api";
8
14
  import {
9
15
  healthResultNumber,
@@ -50,7 +56,7 @@ const bannerResultSchema = healthResultSchema({
50
56
 
51
57
  export type BannerResult = z.infer<typeof bannerResultSchema>;
52
58
 
53
- const bannerAggregatedSchema = healthResultSchema({
59
+ const bannerAggregatedDisplaySchema = healthResultSchema({
54
60
  avgReadTimeMs: healthResultNumber({
55
61
  "x-chart-type": "line",
56
62
  "x-chart-label": "Avg Read Time",
@@ -63,6 +69,17 @@ const bannerAggregatedSchema = healthResultSchema({
63
69
  }),
64
70
  });
65
71
 
72
+ const bannerAggregatedInternalSchema = z.object({
73
+ _readTime: averageStateSchema
74
+ .optional(),
75
+ _banner: rateStateSchema
76
+ .optional(),
77
+ });
78
+
79
+ const bannerAggregatedSchema = bannerAggregatedDisplaySchema.merge(
80
+ bannerAggregatedInternalSchema,
81
+ );
82
+
66
83
  export type BannerAggregatedResult = z.infer<typeof bannerAggregatedSchema>;
67
84
 
68
85
  // ============================================================================
@@ -73,15 +90,12 @@ export type BannerAggregatedResult = z.infer<typeof bannerAggregatedSchema>;
73
90
  * Built-in TCP banner collector.
74
91
  * Reads the initial banner/greeting from a TCP server.
75
92
  */
76
- export class BannerCollector
77
- implements
78
- CollectorStrategy<
79
- TcpTransportClient,
80
- BannerConfig,
81
- BannerResult,
82
- BannerAggregatedResult
83
- >
84
- {
93
+ export class BannerCollector implements CollectorStrategy<
94
+ TcpTransportClient,
95
+ BannerConfig,
96
+ BannerResult,
97
+ BannerAggregatedResult
98
+ > {
85
99
  id = "banner";
86
100
  displayName = "TCP Banner";
87
101
  description = "Read the initial banner/greeting from the server";
@@ -123,28 +137,27 @@ export class BannerCollector
123
137
  };
124
138
  }
125
139
 
126
- aggregateResult(
127
- runs: HealthCheckRunForAggregation<BannerResult>[]
140
+ mergeResult(
141
+ existing: BannerAggregatedResult | undefined,
142
+ run: HealthCheckRunForAggregation<BannerResult>,
128
143
  ): BannerAggregatedResult {
129
- const times = runs
130
- .map((r) => r.metadata?.readTimeMs)
131
- .filter((v): v is number => typeof v === "number");
144
+ const metadata = run.metadata;
132
145
 
133
- const hasBanners = runs
134
- .map((r) => r.metadata?.hasBanner)
135
- .filter((v): v is boolean => typeof v === "boolean");
146
+ const readTimeState = mergeAverage(
147
+ existing?._readTime as AverageState | undefined,
148
+ metadata?.readTimeMs,
149
+ );
136
150
 
137
- const bannerCount = hasBanners.filter(Boolean).length;
151
+ const bannerState = mergeRate(
152
+ existing?._banner as RateState | undefined,
153
+ metadata?.hasBanner,
154
+ );
138
155
 
139
156
  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,
157
+ avgReadTimeMs: readTimeState.avg,
158
+ bannerRate: bannerState.rate,
159
+ _readTime: readTimeState,
160
+ _banner: bannerState,
148
161
  };
149
162
  }
150
163
  }
@@ -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,7 +118,8 @@ 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
124
  expect(aggregated.avgConnectionTime).toBe(15);
124
125
  expect(aggregated.successRate).toBe(100);
@@ -153,7 +154,8 @@ 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
160
  expect(aggregated.successRate).toBe(50);
159
161
  expect(aggregated.errorCount).toBe(1);
package/src/strategy.ts CHANGED
@@ -4,6 +4,15 @@ import {
4
4
  Versioned,
5
5
  z,
6
6
  type ConnectedClient,
7
+ mergeAverage,
8
+ averageStateSchema,
9
+ mergeRate,
10
+ rateStateSchema,
11
+ mergeCounter,
12
+ counterStateSchema,
13
+ type AverageState,
14
+ type RateState,
15
+ type CounterState,
7
16
  } from "@checkstack/backend-api";
8
17
  import {
9
18
  healthResultBoolean,
@@ -73,7 +82,8 @@ type TcpResult = z.infer<typeof tcpResultSchema>;
73
82
  /**
74
83
  * Aggregated metadata for buckets.
75
84
  */
76
- const tcpAggregatedSchema = healthResultSchema({
85
+ // UI-visible aggregated fields
86
+ const tcpAggregatedDisplaySchema = healthResultSchema({
77
87
  avgConnectionTime: healthResultNumber({
78
88
  "x-chart-type": "line",
79
89
  "x-chart-label": "Avg Connection Time",
@@ -90,6 +100,19 @@ const tcpAggregatedSchema = healthResultSchema({
90
100
  }),
91
101
  });
92
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
+ );
115
+
93
116
  type TcpAggregatedResult = z.infer<typeof tcpAggregatedSchema>;
94
117
 
95
118
  // ============================================================================
@@ -159,15 +182,12 @@ const defaultSocketFactory: SocketFactory = () => {
159
182
  // STRATEGY
160
183
  // ============================================================================
161
184
 
162
- export class TcpHealthCheckStrategy
163
- implements
164
- HealthCheckStrategy<
165
- TcpConfig,
166
- TcpTransportClient,
167
- TcpResult,
168
- TcpAggregatedResult
169
- >
170
- {
185
+ export class TcpHealthCheckStrategy implements HealthCheckStrategy<
186
+ TcpConfig,
187
+ TcpTransportClient,
188
+ TcpResult,
189
+ TcpAggregatedResult
190
+ > {
171
191
  id = "tcp";
172
192
  displayName = "TCP Health Check";
173
193
  description = "TCP port connectivity check with optional banner grab";
@@ -213,40 +233,39 @@ export class TcpHealthCheckStrategy
213
233
  schema: tcpAggregatedSchema,
214
234
  });
215
235
 
216
- aggregateResult(
217
- runs: HealthCheckRunForAggregation<TcpResult>[]
236
+ mergeResult(
237
+ existing: TcpAggregatedResult | undefined,
238
+ run: HealthCheckRunForAggregation<TcpResult>,
218
239
  ): TcpAggregatedResult {
219
- const validRuns = runs.filter((r) => r.metadata);
240
+ const metadata = run.metadata;
220
241
 
221
- if (validRuns.length === 0) {
222
- return { avgConnectionTime: 0, successRate: 0, errorCount: 0 };
223
- }
242
+ const connectionTimeState = mergeAverage(
243
+ existing?._connectionTime as AverageState | undefined,
244
+ metadata?.connectionTimeMs,
245
+ );
224
246
 
225
- const connectionTimes = validRuns
226
- .map((r) => r.metadata?.connectionTimeMs)
227
- .filter((t): t is number => typeof t === "number");
247
+ const successState = mergeRate(
248
+ existing?._success as RateState | undefined,
249
+ metadata?.connected,
250
+ );
228
251
 
229
- const avgConnectionTime =
230
- connectionTimes.length > 0
231
- ? Math.round(
232
- connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length
233
- )
234
- : 0;
252
+ const errorState = mergeCounter(
253
+ existing?._errors as CounterState | undefined,
254
+ metadata?.error !== undefined,
255
+ );
235
256
 
236
- const successCount = validRuns.filter(
237
- (r) => r.metadata?.connected === true
238
- ).length;
239
- const successRate = Math.round((successCount / validRuns.length) * 100);
240
-
241
- const errorCount = validRuns.filter(
242
- (r) => r.metadata?.error !== undefined
243
- ).length;
244
-
245
- return { avgConnectionTime, successRate, errorCount };
257
+ return {
258
+ avgConnectionTime: connectionTimeState.avg,
259
+ successRate: successState.rate,
260
+ errorCount: errorState.count,
261
+ _connectionTime: connectionTimeState,
262
+ _success: successState,
263
+ _errors: errorState,
264
+ };
246
265
  }
247
266
 
248
267
  async createClient(
249
- config: TcpConfig
268
+ config: TcpConfig,
250
269
  ): Promise<ConnectedClient<TcpTransportClient>> {
251
270
  const validatedConfig = this.config.validate(config);
252
271
  const socket = this.socketFactory();