@git-stunts/alfred 0.2.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/LICENSE +190 -0
- package/README.md +332 -0
- package/package.json +55 -0
- package/src/compose.js +153 -0
- package/src/errors.js +63 -0
- package/src/index.d.ts +136 -0
- package/src/index.js +29 -0
- package/src/policies/bulkhead.js +145 -0
- package/src/policies/circuit-breaker.js +189 -0
- package/src/policies/retry.js +170 -0
- package/src/policies/timeout.js +83 -0
- package/src/policy.js +271 -0
- package/src/telemetry.js +75 -0
- package/src/testing.d.ts +136 -0
- package/src/testing.js +8 -0
- package/src/utils/clock.js +109 -0
- package/src/utils/jitter.js +74 -0
package/src/testing.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
retries?: number;
|
|
3
|
+
delay?: number;
|
|
4
|
+
maxDelay?: number;
|
|
5
|
+
backoff?: 'constant' | 'linear' | 'exponential';
|
|
6
|
+
jitter?: 'none' | 'full' | 'equal' | 'decorrelated';
|
|
7
|
+
shouldRetry?: (error: Error) => boolean;
|
|
8
|
+
onRetry?: (error: Error, attempt: number, delay: number) => void;
|
|
9
|
+
telemetry?: TelemetrySink;
|
|
10
|
+
clock?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CircuitBreakerOptions {
|
|
14
|
+
threshold: number;
|
|
15
|
+
duration: number;
|
|
16
|
+
successThreshold?: number;
|
|
17
|
+
shouldTrip?: (error: Error) => boolean;
|
|
18
|
+
onOpen?: () => void;
|
|
19
|
+
onClose?: () => void;
|
|
20
|
+
onHalfOpen?: () => void;
|
|
21
|
+
telemetry?: TelemetrySink;
|
|
22
|
+
clock?: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TimeoutOptions {
|
|
26
|
+
onTimeout?: (elapsed: number) => void;
|
|
27
|
+
telemetry?: TelemetrySink;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BulkheadOptions {
|
|
31
|
+
limit: number;
|
|
32
|
+
queueLimit?: number;
|
|
33
|
+
telemetry?: TelemetrySink;
|
|
34
|
+
clock?: any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TelemetryEvent {
|
|
38
|
+
type: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
[key: string]: any;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TelemetrySink {
|
|
44
|
+
emit(event: TelemetryEvent): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class InMemorySink implements TelemetrySink {
|
|
48
|
+
events: TelemetryEvent[];
|
|
49
|
+
emit(event: TelemetryEvent): void;
|
|
50
|
+
clear(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ConsoleSink implements TelemetrySink {
|
|
54
|
+
emit(event: TelemetryEvent): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class NoopSink implements TelemetrySink {
|
|
58
|
+
emit(event: TelemetryEvent): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class MultiSink implements TelemetrySink {
|
|
62
|
+
constructor(sinks: TelemetrySink[]);
|
|
63
|
+
emit(event: TelemetryEvent): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class RetryExhaustedError extends Error {
|
|
67
|
+
attempts: number;
|
|
68
|
+
cause: Error;
|
|
69
|
+
constructor(attempts: number, cause: Error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class CircuitOpenError extends Error {
|
|
73
|
+
openedAt: Date;
|
|
74
|
+
failureCount: number;
|
|
75
|
+
constructor(openedAt: Date, failureCount: number);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class TimeoutError extends Error {
|
|
79
|
+
timeout: number;
|
|
80
|
+
elapsed: number;
|
|
81
|
+
constructor(timeout: number, elapsed: number);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class BulkheadRejectedError extends Error {
|
|
85
|
+
limit: number;
|
|
86
|
+
queueLimit: number;
|
|
87
|
+
constructor(limit: number, queueLimit: number);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function retry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
91
|
+
|
|
92
|
+
export interface CircuitBreaker {
|
|
93
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
94
|
+
readonly state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function circuitBreaker(options: CircuitBreakerOptions): CircuitBreaker;
|
|
98
|
+
|
|
99
|
+
export function timeout<T>(ms: number, fn: ((signal: AbortSignal) => Promise<T>) | (() => Promise<T>), options?: TimeoutOptions): Promise<T>;
|
|
100
|
+
|
|
101
|
+
export interface Bulkhead {
|
|
102
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
103
|
+
readonly stats: { active: number; pending: number; available: number };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function bulkhead(options: BulkheadOptions): Bulkhead;
|
|
107
|
+
|
|
108
|
+
export function compose(...policies: any[]): { execute<T>(fn: () => Promise<T>): Promise<T> };
|
|
109
|
+
export function fallback(primary: any, secondary: any): { execute<T>(fn: () => Promise<T>): Promise<T> };
|
|
110
|
+
export function race(primary: any, secondary: any): { execute<T>(fn: () => Promise<T>): Promise<T> };
|
|
111
|
+
|
|
112
|
+
export class Policy {
|
|
113
|
+
constructor(executor: (fn: () => Promise<any>) => Promise<any>);
|
|
114
|
+
static retry(options?: RetryOptions): Policy;
|
|
115
|
+
static circuitBreaker(options: CircuitBreakerOptions): Policy;
|
|
116
|
+
static timeout(ms: number, options?: TimeoutOptions): Policy;
|
|
117
|
+
static bulkhead(options: BulkheadOptions): Policy;
|
|
118
|
+
static noop(): Policy;
|
|
119
|
+
|
|
120
|
+
wrap(otherPolicy: Policy): Policy;
|
|
121
|
+
or(otherPolicy: Policy): Policy;
|
|
122
|
+
race(otherPolicy: Policy): Policy;
|
|
123
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class SystemClock {
|
|
127
|
+
now(): number;
|
|
128
|
+
sleep(ms: number): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class TestClock {
|
|
132
|
+
now(): number;
|
|
133
|
+
sleep(ms: number): Promise<void>;
|
|
134
|
+
tick(ms?: number): Promise<void>;
|
|
135
|
+
advance(ms: number): Promise<void>;
|
|
136
|
+
}
|
package/src/testing.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System clock using real time.
|
|
3
|
+
*/
|
|
4
|
+
export class SystemClock {
|
|
5
|
+
now() {
|
|
6
|
+
return Date.now();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async sleep(ms) {
|
|
10
|
+
return new Promise(resolve => {
|
|
11
|
+
const timer = setTimeout(resolve, ms);
|
|
12
|
+
if (typeof timer === 'object' && typeof timer.unref === 'function') {
|
|
13
|
+
timer.unref();
|
|
14
|
+
} else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
|
|
15
|
+
// Deno returns a number ID
|
|
16
|
+
Deno.unrefTimer(timer);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Test clock for deterministic tests.
|
|
24
|
+
* Allows manual control of time progression.
|
|
25
|
+
*/
|
|
26
|
+
export class TestClock {
|
|
27
|
+
constructor() {
|
|
28
|
+
this._time = 0;
|
|
29
|
+
this._pendingTimers = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
now() {
|
|
33
|
+
return this._time;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a sleep promise that resolves when time is advanced.
|
|
38
|
+
* @param {number} ms - Milliseconds to sleep
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
sleep(ms) {
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
this._pendingTimers.push({
|
|
44
|
+
triggerAt: this._time + ms,
|
|
45
|
+
resolve
|
|
46
|
+
});
|
|
47
|
+
// Sort by trigger time
|
|
48
|
+
this._pendingTimers.sort((a, b) => a.triggerAt - b.triggerAt);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Advances time and resolves any pending timers.
|
|
54
|
+
* @param {number} ms - Milliseconds to advance
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async advance(ms) {
|
|
58
|
+
const targetTime = this._time + ms;
|
|
59
|
+
|
|
60
|
+
while (this._pendingTimers.length > 0) {
|
|
61
|
+
const next = this._pendingTimers[0];
|
|
62
|
+
if (next.triggerAt > targetTime) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._time = next.triggerAt;
|
|
67
|
+
this._pendingTimers.shift();
|
|
68
|
+
next.resolve();
|
|
69
|
+
|
|
70
|
+
// Yield to allow async code to run
|
|
71
|
+
await Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._time = targetTime;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Process any timers ready at current time.
|
|
79
|
+
* @param {number} [ms=0] - Optional additional time to add
|
|
80
|
+
* @returns {Promise<void>}
|
|
81
|
+
*/
|
|
82
|
+
async tick(ms = 0) {
|
|
83
|
+
await this.advance(ms);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sets absolute time.
|
|
88
|
+
* @param {number} time - Time in milliseconds
|
|
89
|
+
*/
|
|
90
|
+
setTime(time) {
|
|
91
|
+
this._time = time;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns number of pending timers.
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
get pendingCount() {
|
|
99
|
+
return this._pendingTimers.length;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clears all pending timers.
|
|
104
|
+
*/
|
|
105
|
+
reset() {
|
|
106
|
+
this._time = 0;
|
|
107
|
+
this._pendingTimers = [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jitter strategies to prevent thundering herd problems.
|
|
3
|
+
*
|
|
4
|
+
* @see https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* No jitter - returns delay unchanged.
|
|
9
|
+
* @param {number} delay - Base delay
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
export function noJitter(delay) {
|
|
13
|
+
return delay;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Full jitter - random delay between 0 and calculated delay.
|
|
18
|
+
* Most aggressive randomization, best for reducing collisions.
|
|
19
|
+
* @param {number} delay - Base delay
|
|
20
|
+
* @param {() => number} [random=Math.random] - Random function (0-1)
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
export function fullJitter(delay, random = Math.random) {
|
|
24
|
+
return Math.floor(random() * delay);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Equal jitter - delay between 50% and 100% of calculated delay.
|
|
29
|
+
* Balances spread with guaranteed minimum delay.
|
|
30
|
+
* @param {number} delay - Base delay
|
|
31
|
+
* @param {() => number} [random=Math.random] - Random function (0-1)
|
|
32
|
+
* @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
export function equalJitter(delay, random = Math.random) {
|
|
35
|
+
const half = delay / 2;
|
|
36
|
+
return Math.floor(half + random() * half);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decorrelated jitter - AWS-style stateful jitter.
|
|
41
|
+
* Each delay is random between base delay and 3x previous delay.
|
|
42
|
+
* Creates a random walk with bounds.
|
|
43
|
+
* @param {number} baseDelay - Minimum delay
|
|
44
|
+
* @param {number} prevDelay - Previous delay (or baseDelay for first)
|
|
45
|
+
* @param {number} maxDelay - Maximum delay cap
|
|
46
|
+
* @param {() => number} [random=Math.random] - Random function (0-1)
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
// eslint-disable-next-line max-params
|
|
50
|
+
export function decorrelatedJitter(baseDelay, prevDelay, maxDelay, random = Math.random) {
|
|
51
|
+
const next = Math.floor(random() * (prevDelay * 3 - baseDelay)) + baseDelay;
|
|
52
|
+
return Math.min(next, maxDelay);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a jitter function based on strategy name.
|
|
57
|
+
* @param {'none' | 'full' | 'equal' | 'decorrelated'} strategy
|
|
58
|
+
* @param {() => number} [random=Math.random]
|
|
59
|
+
* @returns {(delay: number, prevDelay?: number, maxDelay?: number) => number}
|
|
60
|
+
*/
|
|
61
|
+
export function createJitter(strategy, random = Math.random) {
|
|
62
|
+
switch (strategy) {
|
|
63
|
+
case 'full':
|
|
64
|
+
return (delay) => fullJitter(delay, random);
|
|
65
|
+
case 'equal':
|
|
66
|
+
return (delay) => equalJitter(delay, random);
|
|
67
|
+
case 'decorrelated':
|
|
68
|
+
return (delay, prevDelay, maxDelay) =>
|
|
69
|
+
decorrelatedJitter(delay, prevDelay || delay, maxDelay || delay * 10, random);
|
|
70
|
+
case 'none':
|
|
71
|
+
default:
|
|
72
|
+
return noJitter;
|
|
73
|
+
}
|
|
74
|
+
}
|