@backendkit-labs/http-client 0.1.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 ADDED
@@ -0,0 +1,328 @@
1
+ # @backendkit-labs/http-client
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@backendkit-labs/http-client?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@backendkit-labs/http-client)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/backendkit-dev/backendkit-monorepo/ci.yml?style=flat-square&label=CI)](https://github.com/backendkit-dev/backendkit-monorepo/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/npm/l/@backendkit-labs/http-client?style=flat-square)](LICENSE)
6
+ [![Node](https://img.shields.io/node/v/@backendkit-labs/http-client?style=flat-square)](package.json)
7
+
8
+ > Production-grade HTTP client for Node.js — built on axios with circuit breaker, retry with exponential backoff, typed `Result<T, E>` responses, request cancellation, pre-request pipeline middleware, and optional NestJS DI integration.
9
+
10
+ Every method returns `Result<HttpResponse<T>, HttpClientError>` — no try/catch needed, no unhandled rejections, always typed.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @backendkit-labs/http-client axios
18
+ ```
19
+
20
+ NestJS peer dependencies (only for the `/nestjs` subpath):
21
+
22
+ ```bash
23
+ npm install @nestjs/common @nestjs/core rxjs
24
+ ```
25
+
26
+ ---
27
+
28
+ ## TypeScript Configuration
29
+
30
+ ### Subpath exports (`/nestjs`)
31
+
32
+ This package uses the `exports` field in `package.json` to expose the `/nestjs` subpath. TypeScript's ability to resolve it depends on the `moduleResolution` setting in your `tsconfig.json`.
33
+
34
+ **Modern resolution (recommended) — no extra config needed:**
35
+
36
+ ```json
37
+ {
38
+ "compilerOptions": {
39
+ "moduleResolution": "bundler"
40
+ }
41
+ }
42
+ ```
43
+
44
+ `"bundler"`, `"node16"`, and `"nodenext"` all understand the `exports` field natively.
45
+
46
+ **Legacy resolution (`"node"`) — add `paths` aliases:**
47
+
48
+ ```json
49
+ {
50
+ "compilerOptions": {
51
+ "moduleResolution": "node",
52
+ "paths": {
53
+ "@backendkit-labs/http-client/nestjs": [
54
+ "./node_modules/@backendkit-labs/http-client/dist/nestjs/index.d.ts"
55
+ ]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ **NestJS decorator support:**
62
+
63
+ ```json
64
+ {
65
+ "compilerOptions": {
66
+ "experimentalDecorators": true,
67
+ "emitDecoratorMetadata": true
68
+ }
69
+ }
70
+ ```
71
+
72
+ And in your `main.ts`, before anything else:
73
+
74
+ ```typescript
75
+ import 'reflect-metadata';
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Quick Start
81
+
82
+ ```typescript
83
+ import { HttpClient } from '@backendkit-labs/http-client';
84
+
85
+ const client = new HttpClient({
86
+ baseURL: 'https://api.example.com',
87
+ timeout: 5_000,
88
+ });
89
+
90
+ // All methods return Result<HttpResponse<T>, HttpClientError>
91
+ const result = await client.get<User[]>('/users');
92
+
93
+ if (result.ok) {
94
+ console.log(result.value.data); // User[]
95
+ console.log(result.value.status); // 200
96
+ } else {
97
+ console.error(result.error.type); // 'http' | 'network' | 'timeout' | 'cancelled' | 'circuit-open'
98
+ console.error(result.error.status); // 404, 500, etc. (for 'http' type)
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Configuration
105
+
106
+ ```typescript
107
+ const client = new HttpClient({
108
+ baseURL: 'https://api.example.com',
109
+ timeout: 10_000, // default: 10 000 ms
110
+ headers: { 'X-API-Key': 'secret' },
111
+
112
+ // Retry with exponential backoff + jitter
113
+ retry: {
114
+ attempts: 3, // retries after first failure
115
+ delayMs: 100,
116
+ maxDelayMs: 5_000,
117
+ jitter: true,
118
+ shouldRetry: (err) => err.type === 'network' || err.type === 'timeout',
119
+ },
120
+
121
+ // Circuit breaker
122
+ circuitBreaker: {
123
+ failureThreshold: 50, // % of calls that must fail to open the circuit
124
+ minimumCalls: 5, // minimum calls before evaluating thresholds
125
+ slidingWindowSize: 10,
126
+ openTimeoutMs: 30_000,
127
+ },
128
+
129
+ // Pre-request middleware steps
130
+ steps: [authStep, correlationIdStep],
131
+ });
132
+ ```
133
+
134
+ ---
135
+
136
+ ## HTTP Methods
137
+
138
+ All methods accept an optional `RequestConfig`:
139
+
140
+ ```typescript
141
+ interface RequestConfig {
142
+ headers?: Record<string, string>;
143
+ params?: Record<string, unknown>; // query string parameters
144
+ timeout?: number; // per-request override
145
+ cancelKey?: string; // key to cancel this request
146
+ correlationId?: string;
147
+ }
148
+ ```
149
+
150
+ ```typescript
151
+ client.get<T>(url, config?)
152
+ client.post<T>(url, data?, config?)
153
+ client.put<T>(url, data?, config?)
154
+ client.patch<T>(url, data?, config?)
155
+ client.delete<T>(url, config?)
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Error Types
161
+
162
+ ```typescript
163
+ type HttpErrorType = 'http' | 'network' | 'timeout' | 'cancelled' | 'circuit-open';
164
+
165
+ interface HttpClientError {
166
+ type: HttpErrorType;
167
+ message: string;
168
+ status?: number; // only for 'http'
169
+ data?: unknown; // response body, only for 'http'
170
+ cause?: unknown; // original axios error
171
+ }
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Request Cancellation
177
+
178
+ Register a `cancelKey` on the request and cancel by key at any time:
179
+
180
+ ```typescript
181
+ const promise = client.get('/long-poll', { cancelKey: 'my-poll' });
182
+
183
+ // Cancel a specific request
184
+ client.cancelRequest('my-poll');
185
+
186
+ // Cancel all in-flight requests
187
+ client.cancelAll();
188
+
189
+ const result = await promise;
190
+ if (!result.ok && result.error.type === 'cancelled') {
191
+ // handle cancellation
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Pipeline Middleware
198
+
199
+ Pre-request middleware steps transform the `HttpCtx` before each request. Steps are powered by `@backendkit-labs/pipeline`.
200
+
201
+ ```typescript
202
+ import type { PipelineStep, StepResult } from '@backendkit-labs/pipeline';
203
+ import { Ok } from '@backendkit-labs/pipeline';
204
+ import type { HttpCtx, HttpClientError } from '@backendkit-labs/http-client';
205
+
206
+ const authStep: PipelineStep<HttpCtx, HttpClientError> = {
207
+ stepName: 'auth',
208
+ async handle(ctx): Promise<StepResult<HttpCtx, HttpClientError>> {
209
+ const token = await tokenStore.get();
210
+ return Ok({ ...ctx, headers: { ...ctx.headers, Authorization: `Bearer ${token}` } });
211
+ },
212
+ };
213
+
214
+ const client = new HttpClient({ steps: [authStep] });
215
+ ```
216
+
217
+ A step can abort the request by returning `Err(...)`:
218
+
219
+ ```typescript
220
+ import { Err } from '@backendkit-labs/pipeline';
221
+
222
+ const rateLimitStep: PipelineStep<HttpCtx, HttpClientError> = {
223
+ stepName: 'rate-limit',
224
+ async handle(ctx): Promise<StepResult<HttpCtx, HttpClientError>> {
225
+ if (await rateLimiter.isExceeded()) {
226
+ return Err({ type: 'network', message: 'Rate limit exceeded' });
227
+ }
228
+ return Ok(ctx);
229
+ },
230
+ };
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Observability
236
+
237
+ ```typescript
238
+ // Snapshot of lifetime counters
239
+ client.getMetrics();
240
+ // → { requests, success, failed, cancelled, circuitOpen, retried }
241
+
242
+ // Circuit breaker state and counters
243
+ client.getCircuitBreakerState(); // 'closed' | 'open' | 'half_open' | undefined
244
+ client.getCircuitBreakerMetrics(); // detailed metrics or undefined
245
+ ```
246
+
247
+ ---
248
+
249
+ ## NestJS Integration
250
+
251
+ ### Module registration
252
+
253
+ ```typescript
254
+ // Define typed injection tokens
255
+ export const PRIMARY_API = defineHttpClient('primary-api');
256
+ export const PAYMENTS_API = defineHttpClient('payments-api');
257
+ ```
258
+
259
+ ```typescript
260
+ import { HttpClientModule } from '@backendkit-labs/http-client/nestjs';
261
+ import { PRIMARY_API, PAYMENTS_API } from './tokens';
262
+
263
+ @Module({
264
+ imports: [
265
+ HttpClientModule.forRoot({
266
+ clients: [
267
+ { token: PRIMARY_API, config: { baseURL: 'https://api.example.com', retry: { attempts: 3, delayMs: 100 } } },
268
+ { token: PAYMENTS_API, config: { baseURL: 'https://payments.example.com', circuitBreaker: { failureThreshold: 40, minimumCalls: 3 } } },
269
+ ],
270
+ }),
271
+ ],
272
+ })
273
+ export class AppModule {}
274
+ ```
275
+
276
+ ### Injection
277
+
278
+ ```typescript
279
+ import { InjectHttpClient } from '@backendkit-labs/http-client/nestjs';
280
+ import { PRIMARY_API } from './tokens';
281
+
282
+ @Injectable()
283
+ export class UserService {
284
+ constructor(
285
+ @InjectHttpClient(PRIMARY_API) private readonly http: HttpClient,
286
+ ) {}
287
+
288
+ async getUsers(): Promise<User[]> {
289
+ const result = await this.http.get<User[]>('/users');
290
+ if (!result.ok) throw new Error(result.error.message);
291
+ return result.value.data;
292
+ }
293
+ }
294
+ ```
295
+
296
+ ### Async module registration
297
+
298
+ ```typescript
299
+ HttpClientModule.forRootAsync({
300
+ imports: [ConfigModule],
301
+ inject: [ConfigService],
302
+ useFactory: (config: ConfigService) => ({
303
+ clients: [{
304
+ token: PRIMARY_API,
305
+ config: { baseURL: config.get('API_URL'), timeout: config.get('API_TIMEOUT') },
306
+ }],
307
+ }),
308
+ }),
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Named Clients
314
+
315
+ ```typescript
316
+ import { defineHttpClient, HttpClientToken } from '@backendkit-labs/http-client';
317
+
318
+ export const GITHUB_API: HttpClientToken = defineHttpClient('github-api');
319
+
320
+ // Provides the token's symbol as the NestJS DI token:
321
+ // Inject with @InjectHttpClient(GITHUB_API)
322
+ ```
323
+
324
+ ---
325
+
326
+ ## License
327
+
328
+ MIT
@@ -0,0 +1,245 @@
1
+ import axios2 from 'axios';
2
+ import { fail, ok } from '@backendkit-labs/result';
3
+ import { CircuitBreaker, isHttpServerError, DEFAULT_CIRCUIT_BREAKER_CONFIG, CircuitBreakerOpenError } from '@backendkit-labs/circuit-breaker';
4
+ import { Pipeline } from '@backendkit-labs/pipeline';
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __decorateClass = (decorators, target, key, kind) => {
9
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
10
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
11
+ if (decorator = decorators[i])
12
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
13
+ if (kind && result) __defProp(target, key, result);
14
+ return result;
15
+ };
16
+ var CancelManager = class {
17
+ tokens = /* @__PURE__ */ new Map();
18
+ getOrCreate(key) {
19
+ const existing = this.tokens.get(key);
20
+ if (existing) return existing;
21
+ const source = axios2.CancelToken.source();
22
+ this.tokens.set(key, source);
23
+ return source;
24
+ }
25
+ cancel(key, reason) {
26
+ const source = this.tokens.get(key);
27
+ if (source) {
28
+ source.cancel(reason ?? `Request ${key} cancelled`);
29
+ this.tokens.delete(key);
30
+ }
31
+ }
32
+ cancelAll() {
33
+ for (const [key, source] of this.tokens) {
34
+ source.cancel(`Request ${key} cancelled`);
35
+ }
36
+ this.tokens.clear();
37
+ }
38
+ delete(key) {
39
+ this.tokens.delete(key);
40
+ }
41
+ has(key) {
42
+ return this.tokens.has(key);
43
+ }
44
+ get size() {
45
+ return this.tokens.size;
46
+ }
47
+ };
48
+ var DEFAULT_RETRY = {
49
+ attempts: 0,
50
+ delayMs: 100,
51
+ maxDelayMs: 5e3,
52
+ jitter: true,
53
+ shouldRetry: (e) => e.type === "network" || e.type === "timeout" || e.type === "http" && (e.status ?? 0) >= 500
54
+ };
55
+ var HttpClient = class {
56
+ axiosInstance;
57
+ cb;
58
+ pipeline;
59
+ retry;
60
+ cancelMgr = new CancelManager();
61
+ _metrics = {
62
+ requests: 0,
63
+ success: 0,
64
+ failed: 0,
65
+ cancelled: 0,
66
+ circuitOpen: 0,
67
+ retried: 0
68
+ };
69
+ constructor(config = {}) {
70
+ this.axiosInstance = axios2.create({
71
+ baseURL: config.baseURL,
72
+ timeout: config.timeout ?? 1e4,
73
+ headers: config.headers
74
+ });
75
+ if (config.circuitBreaker) {
76
+ const { name: cbName, ...cbRest } = config.circuitBreaker;
77
+ this.cb = new CircuitBreaker({
78
+ name: cbName ?? "http-client",
79
+ ...DEFAULT_CIRCUIT_BREAKER_CONFIG,
80
+ isFailure: isHttpServerError,
81
+ ...cbRest
82
+ });
83
+ }
84
+ const p = new Pipeline();
85
+ for (const step of config.steps ?? []) p.pipe(step);
86
+ this.pipeline = p;
87
+ this.retry = config.retry ? { ...DEFAULT_RETRY, ...config.retry } : DEFAULT_RETRY;
88
+ }
89
+ // ── HTTP methods ──────────────────────────────────────────────────────────
90
+ get(url, config) {
91
+ return this.execute("GET", url, void 0, config);
92
+ }
93
+ post(url, data, config) {
94
+ return this.execute("POST", url, data, config);
95
+ }
96
+ put(url, data, config) {
97
+ return this.execute("PUT", url, data, config);
98
+ }
99
+ patch(url, data, config) {
100
+ return this.execute("PATCH", url, data, config);
101
+ }
102
+ delete(url, config) {
103
+ return this.execute("DELETE", url, void 0, config);
104
+ }
105
+ // ── Cancellation ──────────────────────────────────────────────────────────
106
+ cancelRequest(key) {
107
+ this.cancelMgr.cancel(key);
108
+ this._metrics.cancelled++;
109
+ }
110
+ cancelAll() {
111
+ const count = this.cancelMgr.size;
112
+ this.cancelMgr.cancelAll();
113
+ this._metrics.cancelled += count;
114
+ }
115
+ // ── Observability ─────────────────────────────────────────────────────────
116
+ getMetrics() {
117
+ return { ...this._metrics };
118
+ }
119
+ getCircuitBreakerState() {
120
+ return this.cb?.getState();
121
+ }
122
+ getCircuitBreakerMetrics() {
123
+ return this.cb?.getMetrics();
124
+ }
125
+ // ── Internal ──────────────────────────────────────────────────────────────
126
+ async execute(method, url, data, config = {}) {
127
+ this._metrics.requests++;
128
+ const initialCtx = {
129
+ url,
130
+ method,
131
+ data,
132
+ headers: config.headers ?? {},
133
+ params: config.params,
134
+ timeout: config.timeout,
135
+ cancelKey: config.cancelKey,
136
+ correlationId: config.correlationId
137
+ };
138
+ if (initialCtx.cancelKey) {
139
+ this.cancelMgr.getOrCreate(initialCtx.cancelKey);
140
+ }
141
+ const pipelineResult = await this.pipeline.run(initialCtx);
142
+ if (!pipelineResult.ok) {
143
+ this._metrics.failed++;
144
+ return fail(pipelineResult.error.cause);
145
+ }
146
+ const ctx = pipelineResult.value;
147
+ if (ctx.cancelKey && !this.cancelMgr.has(ctx.cancelKey)) {
148
+ this._metrics.failed++;
149
+ return fail({ type: "cancelled", message: `Request ${ctx.cancelKey} cancelled` });
150
+ }
151
+ const cancelToken = ctx.cancelKey ? this.cancelMgr.getOrCreate(ctx.cancelKey).token : void 0;
152
+ const rawCall = () => this.callAxios(ctx, cancelToken);
153
+ const cbCall = async () => {
154
+ if (!this.cb) return rawCall();
155
+ try {
156
+ return await this.cb.execute(rawCall);
157
+ } catch (e) {
158
+ if (e instanceof CircuitBreakerOpenError) {
159
+ this._metrics.circuitOpen++;
160
+ const err = { type: "circuit-open", message: e.message };
161
+ throw err;
162
+ }
163
+ throw e;
164
+ }
165
+ };
166
+ try {
167
+ const response = this.retry.attempts > 0 ? await this.withRetry(cbCall, this.retry) : await cbCall();
168
+ if (ctx.cancelKey) this.cancelMgr.delete(ctx.cancelKey);
169
+ this._metrics.success++;
170
+ return ok(response);
171
+ } catch (e) {
172
+ if (ctx.cancelKey) this.cancelMgr.delete(ctx.cancelKey);
173
+ const error = this.isHttpClientError(e) ? e : this.normalizeError(e);
174
+ if (error.type !== "circuit-open") this._metrics.failed++;
175
+ return fail(error);
176
+ }
177
+ }
178
+ async callAxios(ctx, cancelToken) {
179
+ const response = await this.axiosInstance.request({
180
+ url: ctx.url,
181
+ method: ctx.method,
182
+ data: ctx.data,
183
+ headers: ctx.headers,
184
+ params: ctx.params,
185
+ timeout: ctx.timeout,
186
+ cancelToken
187
+ });
188
+ return {
189
+ data: response.data,
190
+ status: response.status,
191
+ headers: response.headers
192
+ };
193
+ }
194
+ async withRetry(fn, config) {
195
+ let lastError;
196
+ for (let attempt = 0; attempt <= config.attempts; attempt++) {
197
+ try {
198
+ return await fn();
199
+ } catch (e) {
200
+ lastError = e;
201
+ if (attempt === config.attempts) break;
202
+ const error = this.isHttpClientError(e) ? e : this.normalizeError(e);
203
+ if (!config.shouldRetry(error, attempt)) break;
204
+ this._metrics.retried++;
205
+ const baseDelay = config.delayMs * Math.pow(2, attempt);
206
+ const jitter = config.jitter ? Math.random() * config.delayMs : 0;
207
+ const delay = Math.min(baseDelay + jitter, config.maxDelayMs);
208
+ await new Promise((resolve) => setTimeout(resolve, delay));
209
+ }
210
+ }
211
+ throw lastError;
212
+ }
213
+ normalizeError(error) {
214
+ if (axios2.isCancel(error)) {
215
+ return {
216
+ type: "cancelled",
217
+ message: error.message ?? "Request cancelled"
218
+ };
219
+ }
220
+ if (axios2.isAxiosError(error)) {
221
+ const code = error.code;
222
+ if (code === "ECONNABORTED" || code === "ETIMEDOUT") {
223
+ return { type: "timeout", message: error.message, cause: error };
224
+ }
225
+ if (!error.response) {
226
+ return { type: "network", message: error.message, cause: error };
227
+ }
228
+ return {
229
+ type: "http",
230
+ message: error.message,
231
+ status: error.response.status,
232
+ data: error.response.data,
233
+ cause: error
234
+ };
235
+ }
236
+ return { type: "network", message: "Unexpected error", cause: error };
237
+ }
238
+ isHttpClientError(e) {
239
+ return typeof e === "object" && e !== null && "type" in e && "message" in e && typeof e.type === "string";
240
+ }
241
+ };
242
+
243
+ export { CancelManager, HttpClient, __decorateClass };
244
+ //# sourceMappingURL=chunk-5RHNUUOY.js.map
245
+ //# sourceMappingURL=chunk-5RHNUUOY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/cancel-manager.ts","../src/core/http-client.ts"],"names":["axios"],"mappings":"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","file":"chunk-5RHNUUOY.js","sourcesContent":["import axios from 'axios';\nimport type { CancelTokenSource } from 'axios';\n\nexport class CancelManager {\n private readonly tokens = new Map<string, CancelTokenSource>();\n\n getOrCreate(key: string): CancelTokenSource {\n const existing = this.tokens.get(key);\n if (existing) return existing;\n const source = axios.CancelToken.source();\n this.tokens.set(key, source);\n return source;\n }\n\n cancel(key: string, reason?: string): void {\n const source = this.tokens.get(key);\n if (source) {\n source.cancel(reason ?? `Request ${key} cancelled`);\n this.tokens.delete(key);\n }\n }\n\n cancelAll(): void {\n for (const [key, source] of this.tokens) {\n source.cancel(`Request ${key} cancelled`);\n }\n this.tokens.clear();\n }\n\n delete(key: string): void {\n this.tokens.delete(key);\n }\n\n has(key: string): boolean {\n return this.tokens.has(key);\n }\n\n get size(): number {\n return this.tokens.size;\n }\n}\n","import axios from 'axios';\nimport type { AxiosInstance, CancelToken } from 'axios';\nimport { ok, fail } from '@backendkit-labs/result';\nimport type { Result } from '@backendkit-labs/result';\nimport { CircuitBreaker, CircuitBreakerOpenError, DEFAULT_CIRCUIT_BREAKER_CONFIG, isHttpServerError } from '@backendkit-labs/circuit-breaker';\nimport type { CircuitBreakerConfig } from '@backendkit-labs/circuit-breaker';\nimport { Pipeline } from '@backendkit-labs/pipeline';\nimport { CancelManager } from './cancel-manager.js';\nimport type {\n HttpClientConfig,\n HttpClientError,\n HttpCtx,\n HttpMetrics,\n HttpResponse,\n RequestConfig,\n RetryConfig,\n} from './types.js';\n\nconst DEFAULT_RETRY: Required<RetryConfig> = {\n attempts: 0,\n delayMs: 100,\n maxDelayMs: 5_000,\n jitter: true,\n shouldRetry: (e) =>\n e.type === 'network' || e.type === 'timeout' || (e.type === 'http' && (e.status ?? 0) >= 500),\n};\n\nexport class HttpClient {\n private readonly axiosInstance: AxiosInstance;\n private readonly cb: CircuitBreaker | undefined;\n private readonly pipeline: Pipeline<HttpCtx, HttpClientError>;\n private readonly retry: Required<RetryConfig>;\n private readonly cancelMgr = new CancelManager();\n private readonly _metrics: HttpMetrics = {\n requests: 0, success: 0, failed: 0, cancelled: 0, circuitOpen: 0, retried: 0,\n };\n\n constructor(config: HttpClientConfig = {}) {\n this.axiosInstance = axios.create({\n baseURL: config.baseURL,\n timeout: config.timeout ?? 10_000,\n headers: config.headers,\n });\n\n if (config.circuitBreaker) {\n const { name: cbName, ...cbRest } = config.circuitBreaker;\n this.cb = new CircuitBreaker({\n name: cbName ?? 'http-client',\n ...DEFAULT_CIRCUIT_BREAKER_CONFIG,\n isFailure: isHttpServerError,\n ...cbRest,\n } as CircuitBreakerConfig);\n }\n\n const p = new Pipeline<HttpCtx, HttpClientError>();\n for (const step of config.steps ?? []) p.pipe(step);\n this.pipeline = p;\n\n this.retry = config.retry\n ? { ...DEFAULT_RETRY, ...config.retry }\n : DEFAULT_RETRY;\n }\n\n // ── HTTP methods ──────────────────────────────────────────────────────────\n\n get<T>(url: string, config?: RequestConfig): Promise<Result<HttpResponse<T>, HttpClientError>> {\n return this.execute<T>('GET', url, undefined, config);\n }\n\n post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<Result<HttpResponse<T>, HttpClientError>> {\n return this.execute<T>('POST', url, data, config);\n }\n\n put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<Result<HttpResponse<T>, HttpClientError>> {\n return this.execute<T>('PUT', url, data, config);\n }\n\n patch<T>(url: string, data?: unknown, config?: RequestConfig): Promise<Result<HttpResponse<T>, HttpClientError>> {\n return this.execute<T>('PATCH', url, data, config);\n }\n\n delete<T>(url: string, config?: RequestConfig): Promise<Result<HttpResponse<T>, HttpClientError>> {\n return this.execute<T>('DELETE', url, undefined, config);\n }\n\n // ── Cancellation ──────────────────────────────────────────────────────────\n\n cancelRequest(key: string): void {\n this.cancelMgr.cancel(key);\n this._metrics.cancelled++;\n }\n\n cancelAll(): void {\n const count = this.cancelMgr.size;\n this.cancelMgr.cancelAll();\n this._metrics.cancelled += count;\n }\n\n // ── Observability ─────────────────────────────────────────────────────────\n\n getMetrics(): Readonly<HttpMetrics> {\n return { ...this._metrics };\n }\n\n getCircuitBreakerState() {\n return this.cb?.getState();\n }\n\n getCircuitBreakerMetrics() {\n return this.cb?.getMetrics();\n }\n\n // ── Internal ──────────────────────────────────────────────────────────────\n\n private async execute<T>(\n method: string,\n url: string,\n data: unknown,\n config: RequestConfig = {},\n ): Promise<Result<HttpResponse<T>, HttpClientError>> {\n this._metrics.requests++;\n\n const initialCtx: HttpCtx = {\n url,\n method,\n data,\n headers: config.headers ?? {},\n params: config.params,\n timeout: config.timeout,\n cancelKey: config.cancelKey,\n correlationId: config.correlationId,\n };\n\n // Pre-register cancel token synchronously so callers can cancel before the request fires\n if (initialCtx.cancelKey) {\n this.cancelMgr.getOrCreate(initialCtx.cancelKey);\n }\n\n // Pre-request middleware pipeline\n const pipelineResult = await this.pipeline.run(initialCtx);\n if (!pipelineResult.ok) {\n this._metrics.failed++;\n return fail(pipelineResult.error.cause);\n }\n\n const ctx = pipelineResult.value;\n\n // If the cancel key was cancelled during the pipeline, bail out immediately\n if (ctx.cancelKey && !this.cancelMgr.has(ctx.cancelKey)) {\n this._metrics.failed++;\n return fail({ type: 'cancelled', message: `Request ${ctx.cancelKey} cancelled` });\n }\n\n const cancelToken = ctx.cancelKey\n ? this.cancelMgr.getOrCreate(ctx.cancelKey).token\n : undefined;\n\n // Raw axios call — throws on any error\n const rawCall = () => this.callAxios<T>(ctx, cancelToken);\n\n // Circuit breaker wraps the raw call — throws CircuitBreakerOpenError when open\n const cbCall = async (): Promise<HttpResponse<T>> => {\n if (!this.cb) return rawCall();\n try {\n return await this.cb.execute(rawCall);\n } catch (e) {\n if (e instanceof CircuitBreakerOpenError) {\n this._metrics.circuitOpen++;\n const err: HttpClientError = { type: 'circuit-open', message: e.message };\n throw err;\n }\n throw e;\n }\n };\n\n try {\n const response = this.retry.attempts > 0\n ? await this.withRetry(cbCall, this.retry)\n : await cbCall();\n\n if (ctx.cancelKey) this.cancelMgr.delete(ctx.cancelKey);\n this._metrics.success++;\n return ok(response);\n } catch (e) {\n if (ctx.cancelKey) this.cancelMgr.delete(ctx.cancelKey);\n const error = this.isHttpClientError(e) ? e : this.normalizeError(e);\n if (error.type !== 'circuit-open') this._metrics.failed++;\n return fail(error);\n }\n }\n\n private async callAxios<T>(ctx: HttpCtx, cancelToken?: CancelToken): Promise<HttpResponse<T>> {\n const response = await this.axiosInstance.request<T>({\n url: ctx.url,\n method: ctx.method,\n data: ctx.data,\n headers: ctx.headers,\n params: ctx.params,\n timeout: ctx.timeout,\n cancelToken,\n });\n\n return {\n data: response.data,\n status: response.status,\n headers: response.headers as Record<string, string>,\n };\n }\n\n private async withRetry<T>(\n fn: () => Promise<T>,\n config: Required<RetryConfig>,\n ): Promise<T> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= config.attempts; attempt++) {\n try {\n return await fn();\n } catch (e) {\n lastError = e;\n\n if (attempt === config.attempts) break;\n\n const error = this.isHttpClientError(e) ? e : this.normalizeError(e);\n if (!config.shouldRetry(error, attempt)) break;\n\n this._metrics.retried++;\n\n const baseDelay = config.delayMs * Math.pow(2, attempt);\n const jitter = config.jitter ? Math.random() * config.delayMs : 0;\n const delay = Math.min(baseDelay + jitter, config.maxDelayMs);\n\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n\n throw lastError;\n }\n\n private normalizeError(error: unknown): HttpClientError {\n if (axios.isCancel(error)) {\n return {\n type: 'cancelled',\n message: (error as { message?: string }).message ?? 'Request cancelled',\n };\n }\n\n if (axios.isAxiosError(error)) {\n const code = error.code;\n if (code === 'ECONNABORTED' || code === 'ETIMEDOUT') {\n return { type: 'timeout', message: error.message, cause: error };\n }\n if (!error.response) {\n return { type: 'network', message: error.message, cause: error };\n }\n return {\n type: 'http',\n message: error.message,\n status: error.response.status,\n data: error.response.data,\n cause: error,\n };\n }\n\n return { type: 'network', message: 'Unexpected error', cause: error };\n }\n\n private isHttpClientError(e: unknown): e is HttpClientError {\n return (\n typeof e === 'object' &&\n e !== null &&\n 'type' in e &&\n 'message' in e &&\n typeof (e as HttpClientError).type === 'string'\n );\n }\n}\n"]}