@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 +328 -0
- package/dist/chunk-5RHNUUOY.js +245 -0
- package/dist/chunk-5RHNUUOY.js.map +1 -0
- package/dist/chunk-GZWQJKYR.cjs +253 -0
- package/dist/chunk-GZWQJKYR.cjs.map +1 -0
- package/dist/index.cjs +29 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/nestjs/index.cjs +78 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +32 -0
- package/dist/nestjs/index.d.ts +32 -0
- package/dist/nestjs/index.js +76 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/types-x0zPV-KD.d.cts +71 -0
- package/dist/types-x0zPV-KD.d.ts +71 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @backendkit-labs/http-client
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@backendkit-labs/http-client)
|
|
4
|
+
[](https://github.com/backendkit-dev/backendkit-monorepo/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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"]}
|