@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.
@@ -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,8 @@
1
+ /**
2
+ * @fileoverview Testing utilities for @git-stunts/alfred.
3
+ * Provides tools for deterministic testing of resilience policies.
4
+ */
5
+
6
+ // @ts-self-types="./testing.d.ts"
7
+
8
+ export { TestClock } from './utils/clock.js';
@@ -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
+ }