@checkstack/backend-api 0.5.1 → 0.6.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 +44 -0
- package/package.json +6 -6
- package/src/collector-strategy.ts +11 -4
- package/src/health-check.ts +15 -10
- package/src/incremental-aggregation.test.ts +146 -0
- package/src/incremental-aggregation.ts +178 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @checkstack/backend-api
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor 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
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [f676e11]
|
|
30
|
+
- @checkstack/common@0.6.2
|
|
31
|
+
- @checkstack/signal-common@0.1.6
|
|
32
|
+
- @checkstack/queue-api@0.2.3
|
|
33
|
+
|
|
34
|
+
## 0.5.2
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- 0b9fc58: Fix workspace:\* protocol resolution in published packages
|
|
39
|
+
|
|
40
|
+
Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
|
|
41
|
+
|
|
42
|
+
- Updated dependencies [0b9fc58]
|
|
43
|
+
- @checkstack/common@0.6.1
|
|
44
|
+
- @checkstack/queue-api@0.2.2
|
|
45
|
+
- @checkstack/signal-common@0.1.5
|
|
46
|
+
|
|
3
47
|
## 0.5.1
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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/common": "
|
|
13
|
-
"@checkstack/queue-api": "
|
|
14
|
-
"@checkstack/signal-common": "
|
|
12
|
+
"@checkstack/common": "0.6.1",
|
|
13
|
+
"@checkstack/queue-api": "0.2.2",
|
|
14
|
+
"@checkstack/signal-common": "0.1.5",
|
|
15
15
|
"@orpc/client": "^1.13.2",
|
|
16
16
|
"@orpc/openapi": "^1.13.2",
|
|
17
17
|
"@orpc/server": "^1.13.2",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/bun": "latest",
|
|
26
|
-
"@checkstack/tsconfig": "
|
|
27
|
-
"@checkstack/scripts": "
|
|
26
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
27
|
+
"@checkstack/scripts": "0.1.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"hono": "^4.0.0",
|
|
@@ -29,7 +29,7 @@ export interface CollectorStrategy<
|
|
|
29
29
|
TClient extends TransportClient<unknown, unknown>,
|
|
30
30
|
TConfig = unknown,
|
|
31
31
|
TResult = Record<string, unknown>,
|
|
32
|
-
TAggregated = Record<string, unknown
|
|
32
|
+
TAggregated = Record<string, unknown>,
|
|
33
33
|
> {
|
|
34
34
|
/** Unique identifier for this collector */
|
|
35
35
|
id: string;
|
|
@@ -76,8 +76,15 @@ export interface CollectorStrategy<
|
|
|
76
76
|
}): Promise<CollectorResult<TResult>>;
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
80
|
-
* Called
|
|
79
|
+
* Incrementally merge new run data into an existing aggregate.
|
|
80
|
+
* Called after each health check run for real-time aggregation.
|
|
81
|
+
*
|
|
82
|
+
* @param existing - Existing aggregated result (undefined for first run in bucket)
|
|
83
|
+
* @param newRun - Data from the new run to merge
|
|
84
|
+
* @returns Updated aggregated result
|
|
81
85
|
*/
|
|
82
|
-
|
|
86
|
+
mergeResult(
|
|
87
|
+
existing: TAggregated | undefined,
|
|
88
|
+
newRun: HealthCheckRunForAggregation<TResult>,
|
|
89
|
+
): TAggregated;
|
|
83
90
|
}
|
package/src/health-check.ts
CHANGED
|
@@ -13,10 +13,10 @@ export interface HealthCheckResult<TMetadata = Record<string, unknown>> {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Raw run data for aggregation (passed to
|
|
16
|
+
* Raw run data for aggregation (passed to mergeResult function).
|
|
17
17
|
*/
|
|
18
18
|
export interface HealthCheckRunForAggregation<
|
|
19
|
-
TResultMetadata = Record<string, unknown
|
|
19
|
+
TResultMetadata = Record<string, unknown>,
|
|
20
20
|
> {
|
|
21
21
|
status: "healthy" | "unhealthy" | "degraded";
|
|
22
22
|
latencyMs?: number;
|
|
@@ -27,7 +27,7 @@ export interface HealthCheckRunForAggregation<
|
|
|
27
27
|
* Connected transport client with cleanup capability.
|
|
28
28
|
*/
|
|
29
29
|
export interface ConnectedClient<
|
|
30
|
-
TClient extends TransportClient<unknown, unknown
|
|
30
|
+
TClient extends TransportClient<unknown, unknown>,
|
|
31
31
|
> {
|
|
32
32
|
/** The connected transport client */
|
|
33
33
|
client: TClient;
|
|
@@ -54,7 +54,7 @@ export interface HealthCheckStrategy<
|
|
|
54
54
|
unknown
|
|
55
55
|
>,
|
|
56
56
|
TResult = Record<string, unknown>,
|
|
57
|
-
TAggregatedResult = Record<string, unknown
|
|
57
|
+
TAggregatedResult = Record<string, unknown>,
|
|
58
58
|
> {
|
|
59
59
|
id: string;
|
|
60
60
|
displayName: string;
|
|
@@ -80,13 +80,18 @@ export interface HealthCheckStrategy<
|
|
|
80
80
|
createClient(config: TConfig): Promise<ConnectedClient<TClient>>;
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
* Called
|
|
83
|
+
* Incrementally merge new run data into an existing aggregate.
|
|
84
|
+
* Called after each health check run for real-time aggregation.
|
|
85
85
|
* Core metrics (counts, latency) are auto-calculated by platform.
|
|
86
86
|
* This function only handles strategy-specific result aggregation.
|
|
87
|
+
*
|
|
88
|
+
* @param existing - Existing aggregated result (undefined for first run in bucket)
|
|
89
|
+
* @param newRun - Data from the new run to merge
|
|
90
|
+
* @returns Updated aggregated result
|
|
87
91
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
mergeResult(
|
|
93
|
+
existing: TAggregatedResult | undefined,
|
|
94
|
+
newRun: HealthCheckRunForAggregation<TResult>,
|
|
90
95
|
): TAggregatedResult;
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -111,10 +116,10 @@ export interface HealthCheckRegistry {
|
|
|
111
116
|
TransportClient<unknown, unknown>,
|
|
112
117
|
unknown,
|
|
113
118
|
unknown
|
|
114
|
-
|
|
119
|
+
>,
|
|
115
120
|
): void;
|
|
116
121
|
getStrategy(
|
|
117
|
-
id: string
|
|
122
|
+
id: string,
|
|
118
123
|
):
|
|
119
124
|
| HealthCheckStrategy<
|
|
120
125
|
unknown,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
mergeCounter,
|
|
4
|
+
mergeAverage,
|
|
5
|
+
mergeRate,
|
|
6
|
+
mergeMinMax,
|
|
7
|
+
} from "./incremental-aggregation";
|
|
8
|
+
|
|
9
|
+
describe("mergeCounter", () => {
|
|
10
|
+
it("creates new counter from undefined", () => {
|
|
11
|
+
const result = mergeCounter(undefined, true);
|
|
12
|
+
expect(result).toEqual({ count: 1 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("increments existing counter with true", () => {
|
|
16
|
+
const result = mergeCounter({ count: 5 }, true);
|
|
17
|
+
expect(result).toEqual({ count: 6 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("does not increment with false", () => {
|
|
21
|
+
const result = mergeCounter({ count: 5 }, false);
|
|
22
|
+
expect(result).toEqual({ count: 5 });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("accepts numeric increment", () => {
|
|
26
|
+
const result = mergeCounter({ count: 10 }, 3);
|
|
27
|
+
expect(result).toEqual({ count: 13 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles zero increment", () => {
|
|
31
|
+
const result = mergeCounter({ count: 10 }, 0);
|
|
32
|
+
expect(result).toEqual({ count: 10 });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("mergeAverage", () => {
|
|
37
|
+
it("creates new average from undefined with value", () => {
|
|
38
|
+
const result = mergeAverage(undefined, 100);
|
|
39
|
+
expect(result).toEqual({ _sum: 100, _count: 1, avg: 100 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns initial state when undefined value passed to undefined", () => {
|
|
43
|
+
const result = mergeAverage(undefined, undefined);
|
|
44
|
+
expect(result).toEqual({ _sum: 0, _count: 0, avg: 0 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("correctly computes average across multiple values", () => {
|
|
48
|
+
let state = mergeAverage(undefined, 100);
|
|
49
|
+
state = mergeAverage(state, 200);
|
|
50
|
+
state = mergeAverage(state, 300);
|
|
51
|
+
expect(state).toEqual({ _sum: 600, _count: 3, avg: 200 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("skips undefined values without affecting count", () => {
|
|
55
|
+
let state = mergeAverage(undefined, 100);
|
|
56
|
+
state = mergeAverage(state, undefined);
|
|
57
|
+
state = mergeAverage(state, 200);
|
|
58
|
+
expect(state).toEqual({ _sum: 300, _count: 2, avg: 150 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rounds average to 1 decimal place", () => {
|
|
62
|
+
let state = mergeAverage(undefined, 100);
|
|
63
|
+
state = mergeAverage(state, 101);
|
|
64
|
+
// (100 + 101) / 2 = 100.5, rounds to 1 decimal place
|
|
65
|
+
expect(state.avg).toBe(100.5);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("mergeRate", () => {
|
|
70
|
+
it("creates new rate from undefined with success", () => {
|
|
71
|
+
const result = mergeRate(undefined, true);
|
|
72
|
+
expect(result).toEqual({ _success: 1, _total: 1, rate: 100 });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("creates new rate from undefined with failure", () => {
|
|
76
|
+
const result = mergeRate(undefined, false);
|
|
77
|
+
expect(result).toEqual({ _success: 0, _total: 1, rate: 0 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns initial state when undefined value passed to undefined", () => {
|
|
81
|
+
const result = mergeRate(undefined, undefined);
|
|
82
|
+
expect(result).toEqual({ _success: 0, _total: 0, rate: 0 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("correctly computes rate across multiple values", () => {
|
|
86
|
+
let state = mergeRate(undefined, true);
|
|
87
|
+
state = mergeRate(state, true);
|
|
88
|
+
state = mergeRate(state, false);
|
|
89
|
+
state = mergeRate(state, true);
|
|
90
|
+
// 3/4 = 75%
|
|
91
|
+
expect(state).toEqual({ _success: 3, _total: 4, rate: 75 });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("skips undefined values without affecting totals", () => {
|
|
95
|
+
let state = mergeRate(undefined, true);
|
|
96
|
+
state = mergeRate(state, undefined);
|
|
97
|
+
state = mergeRate(state, false);
|
|
98
|
+
expect(state).toEqual({ _success: 1, _total: 2, rate: 50 });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("mergeMinMax", () => {
|
|
103
|
+
it("creates new min/max from undefined", () => {
|
|
104
|
+
const result = mergeMinMax(undefined, 50);
|
|
105
|
+
expect(result).toEqual({ min: 50, max: 50 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns initial state when undefined value passed to undefined", () => {
|
|
109
|
+
const result = mergeMinMax(undefined, undefined);
|
|
110
|
+
expect(result).toEqual({ min: 0, max: 0 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("updates min when new value is lower", () => {
|
|
114
|
+
let state = mergeMinMax(undefined, 50);
|
|
115
|
+
state = mergeMinMax(state, 30);
|
|
116
|
+
expect(state).toEqual({ min: 30, max: 50 });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("updates max when new value is higher", () => {
|
|
120
|
+
let state = mergeMinMax(undefined, 50);
|
|
121
|
+
state = mergeMinMax(state, 80);
|
|
122
|
+
expect(state).toEqual({ min: 50, max: 80 });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("updates both when appropriate", () => {
|
|
126
|
+
let state = mergeMinMax(undefined, 50);
|
|
127
|
+
state = mergeMinMax(state, 20);
|
|
128
|
+
state = mergeMinMax(state, 100);
|
|
129
|
+
state = mergeMinMax(state, 60);
|
|
130
|
+
expect(state).toEqual({ min: 20, max: 100 });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("skips undefined values", () => {
|
|
134
|
+
let state = mergeMinMax(undefined, 50);
|
|
135
|
+
state = mergeMinMax(state, undefined);
|
|
136
|
+
state = mergeMinMax(state, 30);
|
|
137
|
+
expect(state).toEqual({ min: 30, max: 50 });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("handles negative values", () => {
|
|
141
|
+
let state = mergeMinMax(undefined, -10);
|
|
142
|
+
state = mergeMinMax(state, -50);
|
|
143
|
+
state = mergeMinMax(state, -5);
|
|
144
|
+
expect(state).toEqual({ min: -50, max: -5 });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Incremental aggregation utilities for real-time metrics.
|
|
5
|
+
* These utilities enable O(1) memory aggregation without storing raw data.
|
|
6
|
+
*
|
|
7
|
+
* Each pattern provides:
|
|
8
|
+
* - A Zod schema for validation/serialization
|
|
9
|
+
* - A TypeScript interface (inferred from schema)
|
|
10
|
+
* - A merge function for incremental updates
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ===== Counter Pattern =====
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Zod schema for accumulated counter state.
|
|
17
|
+
*/
|
|
18
|
+
export const counterStateSchema = z.object({
|
|
19
|
+
count: z.number(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Accumulated counter state.
|
|
24
|
+
*/
|
|
25
|
+
export type CounterState = z.infer<typeof counterStateSchema>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Incrementally merge a counter.
|
|
29
|
+
* Use for tracking occurrences (errorCount, requestCount, etc.)
|
|
30
|
+
*
|
|
31
|
+
* @param existing - Previous counter state (undefined for first run)
|
|
32
|
+
* @param increment - Value to add (boolean true = 1, false = 0, or direct number)
|
|
33
|
+
*/
|
|
34
|
+
export function mergeCounter(
|
|
35
|
+
existing: CounterState | undefined,
|
|
36
|
+
increment: boolean | number,
|
|
37
|
+
): CounterState {
|
|
38
|
+
const value =
|
|
39
|
+
typeof increment === "boolean" ? (increment ? 1 : 0) : increment;
|
|
40
|
+
return {
|
|
41
|
+
count: (existing?.count ?? 0) + value,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ===== Average Pattern =====
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Zod schema for accumulated average state.
|
|
49
|
+
* Internal `_sum` and `_count` fields enable accurate averaging.
|
|
50
|
+
*/
|
|
51
|
+
export const averageStateSchema = z.object({
|
|
52
|
+
/** Internal: sum of all values */
|
|
53
|
+
_sum: z.number(),
|
|
54
|
+
/** Internal: count of values */
|
|
55
|
+
_count: z.number(),
|
|
56
|
+
/** Computed average (rounded) */
|
|
57
|
+
avg: z.number(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Accumulated average state.
|
|
62
|
+
*/
|
|
63
|
+
export type AverageState = z.infer<typeof averageStateSchema>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Incrementally merge an average.
|
|
67
|
+
* Use for tracking averages (avgResponseTimeMs, avgExecutionTimeMs, etc.)
|
|
68
|
+
*
|
|
69
|
+
* @param existing - Previous average state (undefined for first run)
|
|
70
|
+
* @param value - New value to incorporate (undefined skipped)
|
|
71
|
+
*/
|
|
72
|
+
export function mergeAverage(
|
|
73
|
+
existing: AverageState | undefined,
|
|
74
|
+
value: number | undefined,
|
|
75
|
+
): AverageState {
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
// No new value, return existing or initial state
|
|
78
|
+
return existing ?? { _sum: 0, _count: 0, avg: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sum = (existing?._sum ?? 0) + value;
|
|
82
|
+
const count = (existing?._count ?? 0) + 1;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
_sum: sum,
|
|
86
|
+
_count: count,
|
|
87
|
+
// Round to 1 decimal place to preserve precision for float metrics (e.g., load averages)
|
|
88
|
+
avg: Math.round((sum / count) * 10) / 10,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ===== Rate Pattern =====
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Zod schema for accumulated rate state (percentage).
|
|
96
|
+
* Internal `_success` and `_total` fields enable accurate rate calculation.
|
|
97
|
+
*/
|
|
98
|
+
export const rateStateSchema = z.object({
|
|
99
|
+
/** Internal: count of successes */
|
|
100
|
+
_success: z.number(),
|
|
101
|
+
/** Internal: total count */
|
|
102
|
+
_total: z.number(),
|
|
103
|
+
/** Computed rate as percentage (0-100, rounded) */
|
|
104
|
+
rate: z.number(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Accumulated rate state (percentage).
|
|
109
|
+
*/
|
|
110
|
+
export type RateState = z.infer<typeof rateStateSchema>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Incrementally merge a rate (percentage).
|
|
114
|
+
* Use for tracking success rates, availability percentages, etc.
|
|
115
|
+
*
|
|
116
|
+
* @param existing - Previous rate state (undefined for first run)
|
|
117
|
+
* @param success - Whether this run was successful (undefined skipped)
|
|
118
|
+
*/
|
|
119
|
+
export function mergeRate(
|
|
120
|
+
existing: RateState | undefined,
|
|
121
|
+
success: boolean | undefined,
|
|
122
|
+
): RateState {
|
|
123
|
+
if (success === undefined) {
|
|
124
|
+
// No new value, return existing or initial state
|
|
125
|
+
return existing ?? { _success: 0, _total: 0, rate: 0 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const successCount = (existing?._success ?? 0) + (success ? 1 : 0);
|
|
129
|
+
const total = (existing?._total ?? 0) + 1;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
_success: successCount,
|
|
133
|
+
_total: total,
|
|
134
|
+
rate: Math.round((successCount / total) * 100),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ===== MinMax Pattern =====
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Zod schema for accumulated min/max state.
|
|
142
|
+
*/
|
|
143
|
+
export const minMaxStateSchema = z.object({
|
|
144
|
+
min: z.number(),
|
|
145
|
+
max: z.number(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Accumulated min/max state.
|
|
150
|
+
*/
|
|
151
|
+
export type MinMaxState = z.infer<typeof minMaxStateSchema>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Incrementally merge min/max values.
|
|
155
|
+
* Use for tracking min/max latency, memory, etc.
|
|
156
|
+
*
|
|
157
|
+
* @param existing - Previous min/max state (undefined for first run)
|
|
158
|
+
* @param value - New value to incorporate (undefined skipped)
|
|
159
|
+
*/
|
|
160
|
+
export function mergeMinMax(
|
|
161
|
+
existing: MinMaxState | undefined,
|
|
162
|
+
value: number | undefined,
|
|
163
|
+
): MinMaxState {
|
|
164
|
+
if (value === undefined) {
|
|
165
|
+
// No new value, return existing or initial state
|
|
166
|
+
return existing ?? { min: 0, max: 0 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (existing === undefined) {
|
|
170
|
+
// First value
|
|
171
|
+
return { min: value, max: value };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
min: Math.min(existing.min, value),
|
|
176
|
+
max: Math.max(existing.max, value),
|
|
177
|
+
};
|
|
178
|
+
}
|
package/src/index.ts
CHANGED