@checkstack/backend-api 0.6.0 → 0.8.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.
@@ -4,58 +4,67 @@ import {
4
4
  mergeAverage,
5
5
  mergeRate,
6
6
  mergeMinMax,
7
+ mergeCounterStates,
8
+ mergeAverageStates,
9
+ mergeRateStates,
10
+ mergeMinMaxStates,
7
11
  } from "./incremental-aggregation";
8
12
 
9
13
  describe("mergeCounter", () => {
10
14
  it("creates new counter from undefined", () => {
11
15
  const result = mergeCounter(undefined, true);
12
- expect(result).toEqual({ count: 1 });
16
+ expect(result).toEqual({ _type: "counter", count: 1 });
13
17
  });
14
18
 
15
19
  it("increments existing counter with true", () => {
16
20
  const result = mergeCounter({ count: 5 }, true);
17
- expect(result).toEqual({ count: 6 });
21
+ expect(result).toEqual({ _type: "counter", count: 6 });
18
22
  });
19
23
 
20
24
  it("does not increment with false", () => {
21
25
  const result = mergeCounter({ count: 5 }, false);
22
- expect(result).toEqual({ count: 5 });
26
+ expect(result).toEqual({ _type: "counter", count: 5 });
23
27
  });
24
28
 
25
29
  it("accepts numeric increment", () => {
26
30
  const result = mergeCounter({ count: 10 }, 3);
27
- expect(result).toEqual({ count: 13 });
31
+ expect(result).toEqual({ _type: "counter", count: 13 });
28
32
  });
29
33
 
30
34
  it("handles zero increment", () => {
31
35
  const result = mergeCounter({ count: 10 }, 0);
32
- expect(result).toEqual({ count: 10 });
36
+ expect(result).toEqual({ _type: "counter", count: 10 });
33
37
  });
34
38
  });
35
39
 
36
40
  describe("mergeAverage", () => {
37
41
  it("creates new average from undefined with value", () => {
38
42
  const result = mergeAverage(undefined, 100);
39
- expect(result).toEqual({ _sum: 100, _count: 1, avg: 100 });
43
+ expect(result).toEqual({
44
+ _type: "average",
45
+ _sum: 100,
46
+ _count: 1,
47
+ avg: 100,
48
+ });
40
49
  });
41
50
 
42
51
  it("returns initial state when undefined value passed to undefined", () => {
43
52
  const result = mergeAverage(undefined, undefined);
44
- expect(result).toEqual({ _sum: 0, _count: 0, avg: 0 });
53
+ expect(result).toEqual({ _type: "average", _sum: 0, _count: 0, avg: 0 });
45
54
  });
46
55
 
47
56
  it("correctly computes average across multiple values", () => {
48
57
  let state = mergeAverage(undefined, 100);
49
58
  state = mergeAverage(state, 200);
50
59
  state = mergeAverage(state, 300);
51
- expect(state).toEqual({ _sum: 600, _count: 3, avg: 200 });
60
+ expect(state).toEqual({ _type: "average", _sum: 600, _count: 3, avg: 200 });
52
61
  });
53
62
 
54
63
  it("skips undefined values without affecting count", () => {
55
64
  let state = mergeAverage(undefined, 100);
56
65
  state = mergeAverage(state, undefined);
57
66
  state = mergeAverage(state, 200);
58
- expect(state).toEqual({ _sum: 300, _count: 2, avg: 150 });
67
+ expect(state).toEqual({ _type: "average", _sum: 300, _count: 2, avg: 150 });
59
68
  });
60
69
 
61
70
  it("rounds average to 1 decimal place", () => {
@@ -69,17 +78,22 @@ describe("mergeAverage", () => {
69
78
  describe("mergeRate", () => {
70
79
  it("creates new rate from undefined with success", () => {
71
80
  const result = mergeRate(undefined, true);
72
- expect(result).toEqual({ _success: 1, _total: 1, rate: 100 });
81
+ expect(result).toEqual({
82
+ _type: "rate",
83
+ _success: 1,
84
+ _total: 1,
85
+ rate: 100,
86
+ });
73
87
  });
74
88
 
75
89
  it("creates new rate from undefined with failure", () => {
76
90
  const result = mergeRate(undefined, false);
77
- expect(result).toEqual({ _success: 0, _total: 1, rate: 0 });
91
+ expect(result).toEqual({ _type: "rate", _success: 0, _total: 1, rate: 0 });
78
92
  });
79
93
 
80
94
  it("returns initial state when undefined value passed to undefined", () => {
81
95
  const result = mergeRate(undefined, undefined);
82
- expect(result).toEqual({ _success: 0, _total: 0, rate: 0 });
96
+ expect(result).toEqual({ _type: "rate", _success: 0, _total: 0, rate: 0 });
83
97
  });
84
98
 
85
99
  it("correctly computes rate across multiple values", () => {
@@ -88,38 +102,38 @@ describe("mergeRate", () => {
88
102
  state = mergeRate(state, false);
89
103
  state = mergeRate(state, true);
90
104
  // 3/4 = 75%
91
- expect(state).toEqual({ _success: 3, _total: 4, rate: 75 });
105
+ expect(state).toEqual({ _type: "rate", _success: 3, _total: 4, rate: 75 });
92
106
  });
93
107
 
94
108
  it("skips undefined values without affecting totals", () => {
95
109
  let state = mergeRate(undefined, true);
96
110
  state = mergeRate(state, undefined);
97
111
  state = mergeRate(state, false);
98
- expect(state).toEqual({ _success: 1, _total: 2, rate: 50 });
112
+ expect(state).toEqual({ _type: "rate", _success: 1, _total: 2, rate: 50 });
99
113
  });
100
114
  });
101
115
 
102
116
  describe("mergeMinMax", () => {
103
117
  it("creates new min/max from undefined", () => {
104
118
  const result = mergeMinMax(undefined, 50);
105
- expect(result).toEqual({ min: 50, max: 50 });
119
+ expect(result).toEqual({ _type: "minmax", min: 50, max: 50 });
106
120
  });
107
121
 
108
122
  it("returns initial state when undefined value passed to undefined", () => {
109
123
  const result = mergeMinMax(undefined, undefined);
110
- expect(result).toEqual({ min: 0, max: 0 });
124
+ expect(result).toEqual({ _type: "minmax", min: 0, max: 0 });
111
125
  });
112
126
 
113
127
  it("updates min when new value is lower", () => {
114
128
  let state = mergeMinMax(undefined, 50);
115
129
  state = mergeMinMax(state, 30);
116
- expect(state).toEqual({ min: 30, max: 50 });
130
+ expect(state).toEqual({ _type: "minmax", min: 30, max: 50 });
117
131
  });
118
132
 
119
133
  it("updates max when new value is higher", () => {
120
134
  let state = mergeMinMax(undefined, 50);
121
135
  state = mergeMinMax(state, 80);
122
- expect(state).toEqual({ min: 50, max: 80 });
136
+ expect(state).toEqual({ _type: "minmax", min: 50, max: 80 });
123
137
  });
124
138
 
125
139
  it("updates both when appropriate", () => {
@@ -127,20 +141,103 @@ describe("mergeMinMax", () => {
127
141
  state = mergeMinMax(state, 20);
128
142
  state = mergeMinMax(state, 100);
129
143
  state = mergeMinMax(state, 60);
130
- expect(state).toEqual({ min: 20, max: 100 });
144
+ expect(state).toEqual({ _type: "minmax", min: 20, max: 100 });
131
145
  });
132
146
 
133
147
  it("skips undefined values", () => {
134
148
  let state = mergeMinMax(undefined, 50);
135
149
  state = mergeMinMax(state, undefined);
136
150
  state = mergeMinMax(state, 30);
137
- expect(state).toEqual({ min: 30, max: 50 });
151
+ expect(state).toEqual({ _type: "minmax", min: 30, max: 50 });
138
152
  });
139
153
 
140
154
  it("handles negative values", () => {
141
155
  let state = mergeMinMax(undefined, -10);
142
156
  state = mergeMinMax(state, -50);
143
157
  state = mergeMinMax(state, -5);
144
- expect(state).toEqual({ min: -50, max: -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 });
145
242
  });
146
243
  });
@@ -5,25 +5,37 @@ import { z } from "zod";
5
5
  * These utilities enable O(1) memory aggregation without storing raw data.
6
6
  *
7
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
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.
11
14
  */
12
15
 
13
- // ===== Counter Pattern =====
16
+ // =============================================================================
17
+ // COUNTER PATTERN
18
+ // =============================================================================
14
19
 
15
20
  /**
16
21
  * Zod schema for accumulated counter state.
22
+ * _type is required for reliable type detection.
17
23
  */
18
24
  export const counterStateSchema = z.object({
25
+ _type: z.literal("counter"),
19
26
  count: z.number(),
20
27
  });
21
28
 
22
29
  /**
23
- * Accumulated counter state.
30
+ * Counter state with required _type discriminator (derived from schema).
24
31
  */
25
32
  export type CounterState = z.infer<typeof counterStateSchema>;
26
33
 
34
+ /**
35
+ * Counter state input type - without _type (for strategy/collector implementations).
36
+ */
37
+ export type CounterStateInput = Omit<CounterState, "_type">;
38
+
27
39
  /**
28
40
  * Incrementally merge a counter.
29
41
  * Use for tracking occurrences (errorCount, requestCount, etc.)
@@ -32,23 +44,28 @@ export type CounterState = z.infer<typeof counterStateSchema>;
32
44
  * @param increment - Value to add (boolean true = 1, false = 0, or direct number)
33
45
  */
34
46
  export function mergeCounter(
35
- existing: CounterState | undefined,
47
+ existing: CounterStateInput | undefined,
36
48
  increment: boolean | number,
37
49
  ): CounterState {
38
50
  const value =
39
51
  typeof increment === "boolean" ? (increment ? 1 : 0) : increment;
40
52
  return {
53
+ _type: "counter",
41
54
  count: (existing?.count ?? 0) + value,
42
55
  };
43
56
  }
44
57
 
45
- // ===== Average Pattern =====
58
+ // =============================================================================
59
+ // AVERAGE PATTERN
60
+ // =============================================================================
46
61
 
47
62
  /**
48
63
  * Zod schema for accumulated average state.
49
64
  * Internal `_sum` and `_count` fields enable accurate averaging.
65
+ * _type is required for reliable type detection.
50
66
  */
51
67
  export const averageStateSchema = z.object({
68
+ _type: z.literal("average"),
52
69
  /** Internal: sum of all values */
53
70
  _sum: z.number(),
54
71
  /** Internal: count of values */
@@ -58,10 +75,15 @@ export const averageStateSchema = z.object({
58
75
  });
59
76
 
60
77
  /**
61
- * Accumulated average state.
78
+ * Average state with required _type discriminator (derived from schema).
62
79
  */
63
80
  export type AverageState = z.infer<typeof averageStateSchema>;
64
81
 
82
+ /**
83
+ * Average state input type - without _type (for strategy/collector implementations).
84
+ */
85
+ export type AverageStateInput = Omit<AverageState, "_type">;
86
+
65
87
  /**
66
88
  * Incrementally merge an average.
67
89
  * Use for tracking averages (avgResponseTimeMs, avgExecutionTimeMs, etc.)
@@ -70,18 +92,24 @@ export type AverageState = z.infer<typeof averageStateSchema>;
70
92
  * @param value - New value to incorporate (undefined skipped)
71
93
  */
72
94
  export function mergeAverage(
73
- existing: AverageState | undefined,
95
+ existing: AverageStateInput | undefined,
74
96
  value: number | undefined,
75
97
  ): AverageState {
76
98
  if (value === undefined) {
77
- // No new value, return existing or initial state
78
- return existing ?? { _sum: 0, _count: 0, avg: 0 };
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
+ };
79
106
  }
80
107
 
81
108
  const sum = (existing?._sum ?? 0) + value;
82
109
  const count = (existing?._count ?? 0) + 1;
83
110
 
84
111
  return {
112
+ _type: "average",
85
113
  _sum: sum,
86
114
  _count: count,
87
115
  // Round to 1 decimal place to preserve precision for float metrics (e.g., load averages)
@@ -89,13 +117,17 @@ export function mergeAverage(
89
117
  };
90
118
  }
91
119
 
92
- // ===== Rate Pattern =====
120
+ // =============================================================================
121
+ // RATE PATTERN
122
+ // =============================================================================
93
123
 
94
124
  /**
95
125
  * Zod schema for accumulated rate state (percentage).
96
126
  * Internal `_success` and `_total` fields enable accurate rate calculation.
127
+ * _type is required for reliable type detection.
97
128
  */
98
129
  export const rateStateSchema = z.object({
130
+ _type: z.literal("rate"),
99
131
  /** Internal: count of successes */
100
132
  _success: z.number(),
101
133
  /** Internal: total count */
@@ -105,10 +137,15 @@ export const rateStateSchema = z.object({
105
137
  });
106
138
 
107
139
  /**
108
- * Accumulated rate state (percentage).
140
+ * Rate state with required _type discriminator (derived from schema).
109
141
  */
110
142
  export type RateState = z.infer<typeof rateStateSchema>;
111
143
 
144
+ /**
145
+ * Rate state input type - without _type (for strategy/collector implementations).
146
+ */
147
+ export type RateStateInput = Omit<RateState, "_type">;
148
+
112
149
  /**
113
150
  * Incrementally merge a rate (percentage).
114
151
  * Use for tracking success rates, availability percentages, etc.
@@ -117,39 +154,54 @@ export type RateState = z.infer<typeof rateStateSchema>;
117
154
  * @param success - Whether this run was successful (undefined skipped)
118
155
  */
119
156
  export function mergeRate(
120
- existing: RateState | undefined,
157
+ existing: RateStateInput | undefined,
121
158
  success: boolean | undefined,
122
159
  ): RateState {
123
160
  if (success === undefined) {
124
- // No new value, return existing or initial state
125
- return existing ?? { _success: 0, _total: 0, rate: 0 };
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
+ };
126
168
  }
127
169
 
128
170
  const successCount = (existing?._success ?? 0) + (success ? 1 : 0);
129
171
  const total = (existing?._total ?? 0) + 1;
130
172
 
131
173
  return {
174
+ _type: "rate",
132
175
  _success: successCount,
133
176
  _total: total,
134
177
  rate: Math.round((successCount / total) * 100),
135
178
  };
136
179
  }
137
180
 
138
- // ===== MinMax Pattern =====
181
+ // =============================================================================
182
+ // MINMAX PATTERN
183
+ // =============================================================================
139
184
 
140
185
  /**
141
186
  * Zod schema for accumulated min/max state.
187
+ * _type is required for reliable type detection.
142
188
  */
143
189
  export const minMaxStateSchema = z.object({
190
+ _type: z.literal("minmax"),
144
191
  min: z.number(),
145
192
  max: z.number(),
146
193
  });
147
194
 
148
195
  /**
149
- * Accumulated min/max state.
196
+ * MinMax state with required _type discriminator (derived from schema).
150
197
  */
151
198
  export type MinMaxState = z.infer<typeof minMaxStateSchema>;
152
199
 
200
+ /**
201
+ * MinMax state input type - without _type (for strategy/collector implementations).
202
+ */
203
+ export type MinMaxStateInput = Omit<MinMaxState, "_type">;
204
+
153
205
  /**
154
206
  * Incrementally merge min/max values.
155
207
  * Use for tracking min/max latency, memory, etc.
@@ -158,21 +210,99 @@ export type MinMaxState = z.infer<typeof minMaxStateSchema>;
158
210
  * @param value - New value to incorporate (undefined skipped)
159
211
  */
160
212
  export function mergeMinMax(
161
- existing: MinMaxState | undefined,
213
+ existing: MinMaxStateInput | undefined,
162
214
  value: number | undefined,
163
215
  ): MinMaxState {
164
216
  if (value === undefined) {
165
- // No new value, return existing or initial state
166
- return existing ?? { min: 0, max: 0 };
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
+ };
167
223
  }
168
224
 
169
225
  if (existing === undefined) {
170
226
  // First value
171
- return { min: value, max: value };
227
+ return { _type: "minmax", min: value, max: value };
172
228
  }
173
229
 
174
230
  return {
231
+ _type: "minmax",
175
232
  min: Math.min(existing.min, value),
176
233
  max: Math.max(existing.max, value),
177
234
  };
178
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
@@ -3,6 +3,7 @@ export * from "./extension-point";
3
3
  export * from "./core-services";
4
4
  export * from "./plugin-system";
5
5
  export * from "./health-check";
6
+ export * from "./base-strategy-config";
6
7
  export * from "./auth-strategy";
7
8
  export * from "./zod-config";
8
9
  export * from "./encryption";
@@ -25,3 +26,4 @@ export * from "./transport-client";
25
26
  export * from "./collector-strategy";
26
27
  export * from "./collector-registry";
27
28
  export * from "./incremental-aggregation";
29
+ export * from "./aggregated-result";