@git-stunts/alfred 0.2.0 → 0.3.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/README.md +70 -58
- package/package.json +2 -2
- package/src/compose.js +4 -4
- package/src/index.d.ts +249 -14
- package/src/index.js +8 -4
- package/src/policies/bulkhead.js +60 -32
- package/src/policies/circuit-breaker.js +22 -13
- package/src/policies/hedge.js +123 -0
- package/src/policies/retry.js +34 -24
- package/src/policies/timeout.js +8 -5
- package/src/policy.js +15 -12
- package/src/telemetry.js +84 -1
- package/src/testing.d.ts +14 -3
- package/src/utils/clock.js +3 -3
- package/src/utils/resolvable.js +10 -0
package/src/telemetry.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} TelemetryEvent
|
|
8
|
-
* @property {string} type - Event type (e.g. 'retry', 'circuit.open')
|
|
8
|
+
* @property {string} type - Event type (e.g. 'retry.failure', 'circuit.open')
|
|
9
9
|
* @property {number} timestamp - Event timestamp
|
|
10
|
+
* @property {Record<string, number>} [metrics] - Metric increments (counters)
|
|
10
11
|
* @property {Object} [metadata] - Additional event data
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -73,3 +74,85 @@ export class MultiSink {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sink that aggregates metrics in memory based on the `metrics` field in events.
|
|
80
|
+
* @implements {TelemetrySink}
|
|
81
|
+
*/
|
|
82
|
+
export class MetricsSink {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Processes a telemetry event and updates internal counters.
|
|
89
|
+
* @param {TelemetryEvent} event
|
|
90
|
+
*/
|
|
91
|
+
emit(event) {
|
|
92
|
+
const { duration, metrics } = event;
|
|
93
|
+
|
|
94
|
+
if (metrics && typeof metrics === 'object') {
|
|
95
|
+
this._updateMetrics(metrics);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof duration === 'number' && Number.isFinite(duration) && duration >= 0) {
|
|
99
|
+
this._updateLatency(duration);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_updateMetrics(metrics) {
|
|
104
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
105
|
+
if (typeof value !== 'number') {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const current = this.metrics[key];
|
|
110
|
+
if (current === undefined || typeof current === 'number') {
|
|
111
|
+
this.metrics[key] = (current || 0) + value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_updateLatency(ms) {
|
|
117
|
+
const { latency } = this.metrics;
|
|
118
|
+
latency.count++;
|
|
119
|
+
latency.sum += ms;
|
|
120
|
+
latency.min = Math.min(latency.min, ms);
|
|
121
|
+
latency.max = Math.max(latency.max, ms);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns a snapshot of the current metrics.
|
|
126
|
+
*/
|
|
127
|
+
get stats() {
|
|
128
|
+
const { latency, ...rest } = this.metrics;
|
|
129
|
+
const hasData = latency.count > 0;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
...rest,
|
|
133
|
+
latency: {
|
|
134
|
+
...latency,
|
|
135
|
+
min: hasData ? latency.min : 0,
|
|
136
|
+
max: hasData ? latency.max : 0,
|
|
137
|
+
avg: hasData ? latency.sum / latency.count : 0,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resets all metrics to zero.
|
|
144
|
+
*/
|
|
145
|
+
clear() {
|
|
146
|
+
this.metrics = {
|
|
147
|
+
retries: 0,
|
|
148
|
+
failures: 0,
|
|
149
|
+
successes: 0,
|
|
150
|
+
circuitBreaks: 0,
|
|
151
|
+
circuitRejections: 0,
|
|
152
|
+
bulkheadRejections: 0,
|
|
153
|
+
timeouts: 0,
|
|
154
|
+
hedges: 0,
|
|
155
|
+
latency: { count: 0, sum: 0, min: Infinity, max: 0 },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/testing.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface BulkheadOptions {
|
|
|
37
37
|
export interface TelemetryEvent {
|
|
38
38
|
type: string;
|
|
39
39
|
timestamp: number;
|
|
40
|
+
metrics?: Record<string, number>;
|
|
40
41
|
[key: string]: any;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -96,7 +97,11 @@ export interface CircuitBreaker {
|
|
|
96
97
|
|
|
97
98
|
export function circuitBreaker(options: CircuitBreakerOptions): CircuitBreaker;
|
|
98
99
|
|
|
99
|
-
export function timeout<T>(
|
|
100
|
+
export function timeout<T>(
|
|
101
|
+
ms: number,
|
|
102
|
+
fn: ((signal: AbortSignal) => Promise<T>) | (() => Promise<T>),
|
|
103
|
+
options?: TimeoutOptions
|
|
104
|
+
): Promise<T>;
|
|
100
105
|
|
|
101
106
|
export interface Bulkhead {
|
|
102
107
|
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
@@ -106,8 +111,14 @@ export interface Bulkhead {
|
|
|
106
111
|
export function bulkhead(options: BulkheadOptions): Bulkhead;
|
|
107
112
|
|
|
108
113
|
export function compose(...policies: any[]): { execute<T>(fn: () => Promise<T>): Promise<T> };
|
|
109
|
-
export function fallback(
|
|
110
|
-
|
|
114
|
+
export function fallback(
|
|
115
|
+
primary: any,
|
|
116
|
+
secondary: any
|
|
117
|
+
): { execute<T>(fn: () => Promise<T>): Promise<T> };
|
|
118
|
+
export function race(
|
|
119
|
+
primary: any,
|
|
120
|
+
secondary: any
|
|
121
|
+
): { execute<T>(fn: () => Promise<T>): Promise<T> };
|
|
111
122
|
|
|
112
123
|
export class Policy {
|
|
113
124
|
constructor(executor: (fn: () => Promise<any>) => Promise<any>);
|
package/src/utils/clock.js
CHANGED
|
@@ -7,7 +7,7 @@ export class SystemClock {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async sleep(ms) {
|
|
10
|
-
return new Promise(resolve => {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
11
|
const timer = setTimeout(resolve, ms);
|
|
12
12
|
if (typeof timer === 'object' && typeof timer.unref === 'function') {
|
|
13
13
|
timer.unref();
|
|
@@ -39,10 +39,10 @@ export class TestClock {
|
|
|
39
39
|
* @returns {Promise<void>}
|
|
40
40
|
*/
|
|
41
41
|
sleep(ms) {
|
|
42
|
-
return new Promise(resolve => {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
43
|
this._pendingTimers.push({
|
|
44
44
|
triggerAt: this._time + ms,
|
|
45
|
-
resolve
|
|
45
|
+
resolve,
|
|
46
46
|
});
|
|
47
47
|
// Sort by trigger time
|
|
48
48
|
this._pendingTimers.sort((a, b) => a.triggerAt - b.triggerAt);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a value that might be dynamic.
|
|
3
|
+
*
|
|
4
|
+
* @template T
|
|
5
|
+
* @param {T | (() => T)} value - The value or a function returning the value.
|
|
6
|
+
* @returns {T} The resolved value.
|
|
7
|
+
*/
|
|
8
|
+
export function resolve(value) {
|
|
9
|
+
return typeof value === 'function' ? value() : value;
|
|
10
|
+
}
|