@checkstack/backend-api 0.5.2 → 0.7.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 +54 -0
- package/package.json +7 -6
- package/src/aggregated-result.test.ts +277 -0
- package/src/aggregated-result.ts +473 -0
- package/src/collector-strategy.ts +17 -6
- package/src/health-check.ts +27 -18
- package/src/incremental-aggregation.test.ts +243 -0
- package/src/incremental-aggregation.ts +308 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
mergeCounter,
|
|
4
|
+
mergeAverage,
|
|
5
|
+
mergeRate,
|
|
6
|
+
mergeMinMax,
|
|
7
|
+
mergeCounterStates,
|
|
8
|
+
mergeAverageStates,
|
|
9
|
+
mergeRateStates,
|
|
10
|
+
mergeMinMaxStates,
|
|
11
|
+
} from "./incremental-aggregation";
|
|
12
|
+
|
|
13
|
+
describe("mergeCounter", () => {
|
|
14
|
+
it("creates new counter from undefined", () => {
|
|
15
|
+
const result = mergeCounter(undefined, true);
|
|
16
|
+
expect(result).toEqual({ _type: "counter", count: 1 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("increments existing counter with true", () => {
|
|
20
|
+
const result = mergeCounter({ count: 5 }, true);
|
|
21
|
+
expect(result).toEqual({ _type: "counter", count: 6 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("does not increment with false", () => {
|
|
25
|
+
const result = mergeCounter({ count: 5 }, false);
|
|
26
|
+
expect(result).toEqual({ _type: "counter", count: 5 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("accepts numeric increment", () => {
|
|
30
|
+
const result = mergeCounter({ count: 10 }, 3);
|
|
31
|
+
expect(result).toEqual({ _type: "counter", count: 13 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("handles zero increment", () => {
|
|
35
|
+
const result = mergeCounter({ count: 10 }, 0);
|
|
36
|
+
expect(result).toEqual({ _type: "counter", count: 10 });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("mergeAverage", () => {
|
|
41
|
+
it("creates new average from undefined with value", () => {
|
|
42
|
+
const result = mergeAverage(undefined, 100);
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
_type: "average",
|
|
45
|
+
_sum: 100,
|
|
46
|
+
_count: 1,
|
|
47
|
+
avg: 100,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns initial state when undefined value passed to undefined", () => {
|
|
52
|
+
const result = mergeAverage(undefined, undefined);
|
|
53
|
+
expect(result).toEqual({ _type: "average", _sum: 0, _count: 0, avg: 0 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("correctly computes average across multiple values", () => {
|
|
57
|
+
let state = mergeAverage(undefined, 100);
|
|
58
|
+
state = mergeAverage(state, 200);
|
|
59
|
+
state = mergeAverage(state, 300);
|
|
60
|
+
expect(state).toEqual({ _type: "average", _sum: 600, _count: 3, avg: 200 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("skips undefined values without affecting count", () => {
|
|
64
|
+
let state = mergeAverage(undefined, 100);
|
|
65
|
+
state = mergeAverage(state, undefined);
|
|
66
|
+
state = mergeAverage(state, 200);
|
|
67
|
+
expect(state).toEqual({ _type: "average", _sum: 300, _count: 2, avg: 150 });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rounds average to 1 decimal place", () => {
|
|
71
|
+
let state = mergeAverage(undefined, 100);
|
|
72
|
+
state = mergeAverage(state, 101);
|
|
73
|
+
// (100 + 101) / 2 = 100.5, rounds to 1 decimal place
|
|
74
|
+
expect(state.avg).toBe(100.5);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("mergeRate", () => {
|
|
79
|
+
it("creates new rate from undefined with success", () => {
|
|
80
|
+
const result = mergeRate(undefined, true);
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
_type: "rate",
|
|
83
|
+
_success: 1,
|
|
84
|
+
_total: 1,
|
|
85
|
+
rate: 100,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("creates new rate from undefined with failure", () => {
|
|
90
|
+
const result = mergeRate(undefined, false);
|
|
91
|
+
expect(result).toEqual({ _type: "rate", _success: 0, _total: 1, rate: 0 });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns initial state when undefined value passed to undefined", () => {
|
|
95
|
+
const result = mergeRate(undefined, undefined);
|
|
96
|
+
expect(result).toEqual({ _type: "rate", _success: 0, _total: 0, rate: 0 });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("correctly computes rate across multiple values", () => {
|
|
100
|
+
let state = mergeRate(undefined, true);
|
|
101
|
+
state = mergeRate(state, true);
|
|
102
|
+
state = mergeRate(state, false);
|
|
103
|
+
state = mergeRate(state, true);
|
|
104
|
+
// 3/4 = 75%
|
|
105
|
+
expect(state).toEqual({ _type: "rate", _success: 3, _total: 4, rate: 75 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("skips undefined values without affecting totals", () => {
|
|
109
|
+
let state = mergeRate(undefined, true);
|
|
110
|
+
state = mergeRate(state, undefined);
|
|
111
|
+
state = mergeRate(state, false);
|
|
112
|
+
expect(state).toEqual({ _type: "rate", _success: 1, _total: 2, rate: 50 });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("mergeMinMax", () => {
|
|
117
|
+
it("creates new min/max from undefined", () => {
|
|
118
|
+
const result = mergeMinMax(undefined, 50);
|
|
119
|
+
expect(result).toEqual({ _type: "minmax", min: 50, max: 50 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns initial state when undefined value passed to undefined", () => {
|
|
123
|
+
const result = mergeMinMax(undefined, undefined);
|
|
124
|
+
expect(result).toEqual({ _type: "minmax", min: 0, max: 0 });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("updates min when new value is lower", () => {
|
|
128
|
+
let state = mergeMinMax(undefined, 50);
|
|
129
|
+
state = mergeMinMax(state, 30);
|
|
130
|
+
expect(state).toEqual({ _type: "minmax", min: 30, max: 50 });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("updates max when new value is higher", () => {
|
|
134
|
+
let state = mergeMinMax(undefined, 50);
|
|
135
|
+
state = mergeMinMax(state, 80);
|
|
136
|
+
expect(state).toEqual({ _type: "minmax", min: 50, max: 80 });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("updates both when appropriate", () => {
|
|
140
|
+
let state = mergeMinMax(undefined, 50);
|
|
141
|
+
state = mergeMinMax(state, 20);
|
|
142
|
+
state = mergeMinMax(state, 100);
|
|
143
|
+
state = mergeMinMax(state, 60);
|
|
144
|
+
expect(state).toEqual({ _type: "minmax", min: 20, max: 100 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("skips undefined values", () => {
|
|
148
|
+
let state = mergeMinMax(undefined, 50);
|
|
149
|
+
state = mergeMinMax(state, undefined);
|
|
150
|
+
state = mergeMinMax(state, 30);
|
|
151
|
+
expect(state).toEqual({ _type: "minmax", min: 30, max: 50 });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("handles negative values", () => {
|
|
155
|
+
let state = mergeMinMax(undefined, -10);
|
|
156
|
+
state = mergeMinMax(state, -50);
|
|
157
|
+
state = mergeMinMax(state, -5);
|
|
158
|
+
expect(state).toEqual({ _type: "minmax", min: -50, max: -5 });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// State-Merge Utilities (for combining pre-aggregated states)
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
describe("mergeCounterStates", () => {
|
|
167
|
+
it("combines two counter states", () => {
|
|
168
|
+
const result = mergeCounterStates({ count: 5 }, { count: 3 });
|
|
169
|
+
expect(result).toEqual({ _type: "counter", count: 8 });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("handles zero counts", () => {
|
|
173
|
+
const result = mergeCounterStates({ count: 0 }, { count: 10 });
|
|
174
|
+
expect(result).toEqual({ _type: "counter", count: 10 });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("mergeAverageStates", () => {
|
|
179
|
+
it("correctly merges two average states", () => {
|
|
180
|
+
const a = { _sum: 100, _count: 2, avg: 50 };
|
|
181
|
+
const b = { _sum: 200, _count: 2, avg: 100 };
|
|
182
|
+
const result = mergeAverageStates(a, b);
|
|
183
|
+
expect(result).toEqual({ _type: "average", _sum: 300, _count: 4, avg: 75 });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("handles unequal counts", () => {
|
|
187
|
+
const a = { _sum: 100, _count: 1, avg: 100 };
|
|
188
|
+
const b = { _sum: 200, _count: 4, avg: 50 };
|
|
189
|
+
const result = mergeAverageStates(a, b);
|
|
190
|
+
// (100 + 200) / 5 = 60
|
|
191
|
+
expect(result).toEqual({ _type: "average", _sum: 300, _count: 5, avg: 60 });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("handles zero count", () => {
|
|
195
|
+
const a = { _sum: 0, _count: 0, avg: 0 };
|
|
196
|
+
const b = { _sum: 100, _count: 2, avg: 50 };
|
|
197
|
+
const result = mergeAverageStates(a, b);
|
|
198
|
+
expect(result).toEqual({ _type: "average", _sum: 100, _count: 2, avg: 50 });
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("mergeRateStates", () => {
|
|
203
|
+
it("correctly merges two rate states", () => {
|
|
204
|
+
const a = { _success: 3, _total: 4, rate: 75 };
|
|
205
|
+
const b = { _success: 7, _total: 10, rate: 70 };
|
|
206
|
+
const result = mergeRateStates(a, b);
|
|
207
|
+
// 10/14 ≈ 71.4% -> rounds to 71
|
|
208
|
+
expect(result).toEqual({
|
|
209
|
+
_type: "rate",
|
|
210
|
+
_success: 10,
|
|
211
|
+
_total: 14,
|
|
212
|
+
rate: 71,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("handles zero total", () => {
|
|
217
|
+
const a = { _success: 0, _total: 0, rate: 0 };
|
|
218
|
+
const b = { _success: 5, _total: 10, rate: 50 };
|
|
219
|
+
const result = mergeRateStates(a, b);
|
|
220
|
+
expect(result).toEqual({
|
|
221
|
+
_type: "rate",
|
|
222
|
+
_success: 5,
|
|
223
|
+
_total: 10,
|
|
224
|
+
rate: 50,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("mergeMinMaxStates", () => {
|
|
230
|
+
it("takes min of mins and max of maxes", () => {
|
|
231
|
+
const a = { min: 10, max: 50 };
|
|
232
|
+
const b = { min: 5, max: 100 };
|
|
233
|
+
const result = mergeMinMaxStates(a, b);
|
|
234
|
+
expect(result).toEqual({ _type: "minmax", min: 5, max: 100 });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("handles overlapping ranges", () => {
|
|
238
|
+
const a = { min: 20, max: 60 };
|
|
239
|
+
const b = { min: 40, max: 80 };
|
|
240
|
+
const result = mergeMinMaxStates(a, b);
|
|
241
|
+
expect(result).toEqual({ _type: "minmax", min: 20, max: 80 });
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
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 (with required _type)
|
|
9
|
+
* - TypeScript types: State (with _type) and StateInput (without _type)
|
|
10
|
+
* - A merge function that always outputs the _type discriminator
|
|
11
|
+
*
|
|
12
|
+
* Strategy/collector implementations provide data WITHOUT _type.
|
|
13
|
+
* The merge functions add _type automatically.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// COUNTER PATTERN
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Zod schema for accumulated counter state.
|
|
22
|
+
* _type is required for reliable type detection.
|
|
23
|
+
*/
|
|
24
|
+
export const counterStateSchema = z.object({
|
|
25
|
+
_type: z.literal("counter"),
|
|
26
|
+
count: z.number(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Counter state with required _type discriminator (derived from schema).
|
|
31
|
+
*/
|
|
32
|
+
export type CounterState = z.infer<typeof counterStateSchema>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Counter state input type - without _type (for strategy/collector implementations).
|
|
36
|
+
*/
|
|
37
|
+
export type CounterStateInput = Omit<CounterState, "_type">;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Incrementally merge a counter.
|
|
41
|
+
* Use for tracking occurrences (errorCount, requestCount, etc.)
|
|
42
|
+
*
|
|
43
|
+
* @param existing - Previous counter state (undefined for first run)
|
|
44
|
+
* @param increment - Value to add (boolean true = 1, false = 0, or direct number)
|
|
45
|
+
*/
|
|
46
|
+
export function mergeCounter(
|
|
47
|
+
existing: CounterStateInput | undefined,
|
|
48
|
+
increment: boolean | number,
|
|
49
|
+
): CounterState {
|
|
50
|
+
const value =
|
|
51
|
+
typeof increment === "boolean" ? (increment ? 1 : 0) : increment;
|
|
52
|
+
return {
|
|
53
|
+
_type: "counter",
|
|
54
|
+
count: (existing?.count ?? 0) + value,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// AVERAGE PATTERN
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Zod schema for accumulated average state.
|
|
64
|
+
* Internal `_sum` and `_count` fields enable accurate averaging.
|
|
65
|
+
* _type is required for reliable type detection.
|
|
66
|
+
*/
|
|
67
|
+
export const averageStateSchema = z.object({
|
|
68
|
+
_type: z.literal("average"),
|
|
69
|
+
/** Internal: sum of all values */
|
|
70
|
+
_sum: z.number(),
|
|
71
|
+
/** Internal: count of values */
|
|
72
|
+
_count: z.number(),
|
|
73
|
+
/** Computed average (rounded) */
|
|
74
|
+
avg: z.number(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Average state with required _type discriminator (derived from schema).
|
|
79
|
+
*/
|
|
80
|
+
export type AverageState = z.infer<typeof averageStateSchema>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Average state input type - without _type (for strategy/collector implementations).
|
|
84
|
+
*/
|
|
85
|
+
export type AverageStateInput = Omit<AverageState, "_type">;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Incrementally merge an average.
|
|
89
|
+
* Use for tracking averages (avgResponseTimeMs, avgExecutionTimeMs, etc.)
|
|
90
|
+
*
|
|
91
|
+
* @param existing - Previous average state (undefined for first run)
|
|
92
|
+
* @param value - New value to incorporate (undefined skipped)
|
|
93
|
+
*/
|
|
94
|
+
export function mergeAverage(
|
|
95
|
+
existing: AverageStateInput | undefined,
|
|
96
|
+
value: number | undefined,
|
|
97
|
+
): AverageState {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
// No new value, return existing with _type or initial state
|
|
100
|
+
return {
|
|
101
|
+
_type: "average",
|
|
102
|
+
_sum: existing?._sum ?? 0,
|
|
103
|
+
_count: existing?._count ?? 0,
|
|
104
|
+
avg: existing?.avg ?? 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sum = (existing?._sum ?? 0) + value;
|
|
109
|
+
const count = (existing?._count ?? 0) + 1;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
_type: "average",
|
|
113
|
+
_sum: sum,
|
|
114
|
+
_count: count,
|
|
115
|
+
// Round to 1 decimal place to preserve precision for float metrics (e.g., load averages)
|
|
116
|
+
avg: Math.round((sum / count) * 10) / 10,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// RATE PATTERN
|
|
122
|
+
// =============================================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Zod schema for accumulated rate state (percentage).
|
|
126
|
+
* Internal `_success` and `_total` fields enable accurate rate calculation.
|
|
127
|
+
* _type is required for reliable type detection.
|
|
128
|
+
*/
|
|
129
|
+
export const rateStateSchema = z.object({
|
|
130
|
+
_type: z.literal("rate"),
|
|
131
|
+
/** Internal: count of successes */
|
|
132
|
+
_success: z.number(),
|
|
133
|
+
/** Internal: total count */
|
|
134
|
+
_total: z.number(),
|
|
135
|
+
/** Computed rate as percentage (0-100, rounded) */
|
|
136
|
+
rate: z.number(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Rate state with required _type discriminator (derived from schema).
|
|
141
|
+
*/
|
|
142
|
+
export type RateState = z.infer<typeof rateStateSchema>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Rate state input type - without _type (for strategy/collector implementations).
|
|
146
|
+
*/
|
|
147
|
+
export type RateStateInput = Omit<RateState, "_type">;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Incrementally merge a rate (percentage).
|
|
151
|
+
* Use for tracking success rates, availability percentages, etc.
|
|
152
|
+
*
|
|
153
|
+
* @param existing - Previous rate state (undefined for first run)
|
|
154
|
+
* @param success - Whether this run was successful (undefined skipped)
|
|
155
|
+
*/
|
|
156
|
+
export function mergeRate(
|
|
157
|
+
existing: RateStateInput | undefined,
|
|
158
|
+
success: boolean | undefined,
|
|
159
|
+
): RateState {
|
|
160
|
+
if (success === undefined) {
|
|
161
|
+
// No new value, return existing with _type or initial state
|
|
162
|
+
return {
|
|
163
|
+
_type: "rate",
|
|
164
|
+
_success: existing?._success ?? 0,
|
|
165
|
+
_total: existing?._total ?? 0,
|
|
166
|
+
rate: existing?.rate ?? 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const successCount = (existing?._success ?? 0) + (success ? 1 : 0);
|
|
171
|
+
const total = (existing?._total ?? 0) + 1;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
_type: "rate",
|
|
175
|
+
_success: successCount,
|
|
176
|
+
_total: total,
|
|
177
|
+
rate: Math.round((successCount / total) * 100),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// MINMAX PATTERN
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Zod schema for accumulated min/max state.
|
|
187
|
+
* _type is required for reliable type detection.
|
|
188
|
+
*/
|
|
189
|
+
export const minMaxStateSchema = z.object({
|
|
190
|
+
_type: z.literal("minmax"),
|
|
191
|
+
min: z.number(),
|
|
192
|
+
max: z.number(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* MinMax state with required _type discriminator (derived from schema).
|
|
197
|
+
*/
|
|
198
|
+
export type MinMaxState = z.infer<typeof minMaxStateSchema>;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* MinMax state input type - without _type (for strategy/collector implementations).
|
|
202
|
+
*/
|
|
203
|
+
export type MinMaxStateInput = Omit<MinMaxState, "_type">;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Incrementally merge min/max values.
|
|
207
|
+
* Use for tracking min/max latency, memory, etc.
|
|
208
|
+
*
|
|
209
|
+
* @param existing - Previous min/max state (undefined for first run)
|
|
210
|
+
* @param value - New value to incorporate (undefined skipped)
|
|
211
|
+
*/
|
|
212
|
+
export function mergeMinMax(
|
|
213
|
+
existing: MinMaxStateInput | undefined,
|
|
214
|
+
value: number | undefined,
|
|
215
|
+
): MinMaxState {
|
|
216
|
+
if (value === undefined) {
|
|
217
|
+
// No new value, return existing with _type or initial state
|
|
218
|
+
return {
|
|
219
|
+
_type: "minmax",
|
|
220
|
+
min: existing?.min ?? 0,
|
|
221
|
+
max: existing?.max ?? 0,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (existing === undefined) {
|
|
226
|
+
// First value
|
|
227
|
+
return { _type: "minmax", min: value, max: value };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
_type: "minmax",
|
|
232
|
+
min: Math.min(existing.min, value),
|
|
233
|
+
max: Math.max(existing.max, value),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// STATE-MERGE UTILITIES - For merging two pre-aggregated states
|
|
239
|
+
// =============================================================================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Merge two CounterStates.
|
|
243
|
+
* Used when combining pre-aggregated buckets (e.g., in combineBuckets).
|
|
244
|
+
* Always includes _type discriminator for reliable type detection.
|
|
245
|
+
*/
|
|
246
|
+
export function mergeCounterStates(
|
|
247
|
+
a: CounterStateInput,
|
|
248
|
+
b: CounterStateInput,
|
|
249
|
+
): CounterState {
|
|
250
|
+
return {
|
|
251
|
+
_type: "counter",
|
|
252
|
+
count: a.count + b.count,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Merge two AverageStates.
|
|
258
|
+
* Used when combining pre-aggregated buckets.
|
|
259
|
+
* Always includes _type discriminator for reliable type detection.
|
|
260
|
+
*/
|
|
261
|
+
export function mergeAverageStates(
|
|
262
|
+
a: AverageStateInput,
|
|
263
|
+
b: AverageStateInput,
|
|
264
|
+
): AverageState {
|
|
265
|
+
const sum = a._sum + b._sum;
|
|
266
|
+
const count = a._count + b._count;
|
|
267
|
+
return {
|
|
268
|
+
_type: "average",
|
|
269
|
+
_sum: sum,
|
|
270
|
+
_count: count,
|
|
271
|
+
avg: count > 0 ? Math.round((sum / count) * 10) / 10 : 0,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Merge two RateStates.
|
|
277
|
+
* Used when combining pre-aggregated buckets.
|
|
278
|
+
* Always includes _type discriminator for reliable type detection.
|
|
279
|
+
*/
|
|
280
|
+
export function mergeRateStates(
|
|
281
|
+
a: RateStateInput,
|
|
282
|
+
b: RateStateInput,
|
|
283
|
+
): RateState {
|
|
284
|
+
const success = a._success + b._success;
|
|
285
|
+
const total = a._total + b._total;
|
|
286
|
+
return {
|
|
287
|
+
_type: "rate",
|
|
288
|
+
_success: success,
|
|
289
|
+
_total: total,
|
|
290
|
+
rate: total > 0 ? Math.round((success / total) * 100) : 0,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Merge two MinMaxStates.
|
|
296
|
+
* Used when combining pre-aggregated buckets.
|
|
297
|
+
* Always includes _type discriminator for reliable type detection.
|
|
298
|
+
*/
|
|
299
|
+
export function mergeMinMaxStates(
|
|
300
|
+
a: MinMaxStateInput,
|
|
301
|
+
b: MinMaxStateInput,
|
|
302
|
+
): MinMaxState {
|
|
303
|
+
return {
|
|
304
|
+
_type: "minmax",
|
|
305
|
+
min: Math.min(a.min, b.min),
|
|
306
|
+
max: Math.max(a.max, b.max),
|
|
307
|
+
};
|
|
308
|
+
}
|
package/src/index.ts
CHANGED