@dunx/http 2.3.1 → 2.5.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.
@@ -1,10 +1,10 @@
1
1
  import { type RoutePath } from './marker.js';
2
- import type { Input, RouteSchemas } from './schema.js';
2
+ import type { Input, Returns, RouteSchemas } from './schema.js';
3
3
  type ControllerTarget = abstract new (...args: never[]) => object;
4
4
  export declare const Controller: (prefix?: string) => <T extends ControllerTarget>(target: T) => T;
5
- export declare const Get: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
6
- export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
7
- export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
8
- export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
9
- export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O) => <M extends (input: Input<O>) => unknown>(value: M, _context: ClassMethodDecoratorContext) => M;
5
+ export declare const Get: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "GET">>(value: H, _context: ClassMethodDecoratorContext) => H;
6
+ export declare const Post: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "POST">>(value: H, _context: ClassMethodDecoratorContext) => H;
7
+ export declare const Put: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "PUT">>(value: H, _context: ClassMethodDecoratorContext) => H;
8
+ export declare const Patch: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "PATCH">>(value: H, _context: ClassMethodDecoratorContext) => H;
9
+ export declare const Delete: <const O extends RouteSchemas>(path?: RoutePath, options?: O | undefined) => <H extends (input: Input<O>) => Returns<O, "DELETE">>(value: H, _context: ClassMethodDecoratorContext) => H;
10
10
  export {};
@@ -1,5 +1,18 @@
1
+ import { HttpStatusCode } from '../server/status.js';
1
2
  import type { RouteSchemas } from './schema.js';
2
3
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
4
+ /**
5
+ * The success status a route answers with when `options.status` is absent.
6
+ * `buildRoutes` and `@dunx/openapi`'s `statusOf` both read it, so the rule is
7
+ * stated once rather than in each of them.
8
+ */
9
+ export declare const defaultStatusFor: (method: HttpMethod) => number;
10
+ /**
11
+ * The type-level twin of {@link defaultStatusFor}, derived from the same constants
12
+ * so the two cannot drift. `Returns` needs it to know which `response` entry a
13
+ * handler is being held to.
14
+ */
15
+ export type DefaultStatus<M extends HttpMethod> = M extends 'POST' ? typeof HttpStatusCode.CREATED : typeof HttpStatusCode.OK;
3
16
  /**
4
17
  * A literal path, or a thunk read at **discovery** rather than at decoration.
5
18
  *
@@ -1,4 +1,5 @@
1
1
  import type { BunRequest } from 'bun';
2
+ import type { DefaultStatus, HttpMethod } from './marker.js';
2
3
  /**
3
4
  * Standard Schema v1, restated rather than depended on. The spec is an
4
5
  * *interface*, not a runtime: `@standard-schema/spec` ships nothing but these
@@ -33,6 +34,14 @@ export interface StandardSchemaIssue {
33
34
  }
34
35
  /** The validated output of a schema - `InferOutput<typeof CreateNote>` is `Note`. */
35
36
  export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer Out> ? Out : never;
37
+ /**
38
+ * A JSON Schema, as JSON. OpenAPI 3.1 embeds draft 2020-12 verbatim.
39
+ *
40
+ * Declared here rather than in `@dunx/openapi` because {@link RouteSchemas} names
41
+ * it and that package depends on this one, so this is the lowest common owner.
42
+ * `@dunx/openapi` re-exports it.
43
+ */
44
+ export type JsonSchema = Readonly<Record<string, unknown>>;
36
45
  /**
37
46
  * The second argument to `@Get`/`@Post`/... Declaring a schema is what makes the
38
47
  * matching `input` field appear, get parsed, and get validated; omitting one
@@ -57,14 +66,30 @@ export interface RouteSchemas {
57
66
  * } as const satisfies RouteSchemas;
58
67
  * ```
59
68
  *
60
- * **Never validated.** It documents the response; it does not enforce it.
61
- * Running a validation pass over every response body would be a per-request
62
- * cost paid for a documentation feature, which is the wrong trade - the
63
- * handler's own return type is what checks the answer, at compile time and for
64
- * free. Nothing in the request path reads this key.
69
+ * **Never validated at runtime, checked at compile time.** Running a validation
70
+ * pass over every response body would be a per-request cost paid for a
71
+ * documentation feature. The handler's own return type carries the check
72
+ * instead: the verb decorators constrain it against the entry for the success
73
+ * status, so a handler answering with a different shape is a `TS1241` naming
74
+ * the mismatched property. See {@link Returns}. Nothing in the request path
75
+ * reads this key.
76
+ *
77
+ * A plain {@link JsonSchema} is accepted here too, and only here: a JSON Schema
78
+ * needs no conversion, so documenting a response costs no validator. `$id` names
79
+ * it, hoisting it into `components/schemas` the way `.meta({ id })` does for a
80
+ * zod schema. `body`, `query` and `params` still take a Standard Schema, because
81
+ * those are parsed.
82
+ *
83
+ * ```ts
84
+ * response: {
85
+ * 200: Object.freeze({ $id: 'Pong', type: 'object' }),
86
+ * }
87
+ * ```
65
88
  */
66
- readonly response?: Readonly<Record<number, StandardSchemaV1>>;
89
+ readonly response?: ResponseMap;
67
90
  }
91
+ /** `response` keyed by status code. Named so {@link Returns} can constrain it. */
92
+ export type ResponseMap = Readonly<Record<number, StandardSchemaV1 | JsonSchema>>;
68
93
  /**
69
94
  * The handler's parameter type, derived from its own options object. It has to be
70
95
  * written out - a standard method decorator can *check* a parameter's type but
@@ -98,6 +123,58 @@ export type Input<O extends RouteSchemas> = {
98
123
  } ? {
99
124
  readonly params: InferOutput<P>;
100
125
  } : unknown);
126
+ /**
127
+ * The status a handler's return type is held to: an explicit `options.status`,
128
+ * else the verb's default. Widened to `number` without `as const`, which is what
129
+ * turns the check off rather than misapplying it.
130
+ */
131
+ type SuccessStatus<O extends RouteSchemas, M extends HttpMethod> = O extends {
132
+ status: infer S extends number;
133
+ } ? S : DefaultStatus<M>;
134
+ /**
135
+ * A plain {@link JsonSchema} carries no type to infer, so it becomes `unknown` and
136
+ * absorbs whatever the handler returns. That is the escape hatch for a response
137
+ * whose shape no schema value describes.
138
+ */
139
+ type Declared<S> = [InferOutput<S>] extends [never] ? unknown : Serialised<InferOutput<S>>;
140
+ /**
141
+ * The declared shape as JSON will present it, which is the same shape with every
142
+ * array made readonly.
143
+ *
144
+ * `z.array()` infers a mutable `T[]`, and `readonly T[]` is not assignable to it -
145
+ * so a repository method returning `readonly User[]`, the correct signature for
146
+ * something that must not be mutated, would fail against a document it satisfies.
147
+ * Mutability does not survive `Response.json`, so it is not part of the contract.
148
+ *
149
+ * Only arrays need the rewrite; TypeScript already ignores a property's `readonly`
150
+ * modifier when checking assignability. The object branch is how nested arrays are
151
+ * reached, and functions are returned untouched because mapping over one would
152
+ * discard its call signature.
153
+ */
154
+ type Serialised<T> = T extends readonly (infer E)[] ? readonly Serialised<E>[] : T extends (...args: never[]) => unknown ? T : T extends object ? {
155
+ readonly [K in keyof T]: Serialised<T[K]>;
156
+ } : T;
157
+ /**
158
+ * What a handler may return, given its own options object and its verb.
159
+ *
160
+ * A route decorator can *check* a handler's type but cannot *infer* it
161
+ * (docs/architecture/constraints.md), and that cuts both ways: this is the return
162
+ * half of the same guarantee `Input<O>` gives the parameter. Declaring
163
+ * `response: { 200: User }` stops being documentation a handler can contradict.
164
+ *
165
+ * `Response` is always allowed - it is the escape hatch `buildRoutes` passes
166
+ * through untouched. So is a promise of either. Nothing is checked when the
167
+ * success status has no `response` entry.
168
+ */
169
+ export type Returns<O extends RouteSchemas, M extends HttpMethod> = SuccessBody<O, M> | Response | Promise<SuccessBody<O, M> | Response>;
170
+ /**
171
+ * `infer R extends ResponseMap` is load bearing: without the constraint the
172
+ * narrowed `O` inside the branch is `{ response: R } & O`, whose `response` no
173
+ * longer satisfies `RouteSchemas`, and `SuccessStatus<O, M>` fails with `TS2344`.
174
+ */
175
+ type SuccessBody<O extends RouteSchemas, M extends HttpMethod> = O extends {
176
+ response: infer R extends ResponseMap;
177
+ } ? SuccessStatus<O, M> extends keyof R ? Declared<R[SuccessStatus<O, M>]> : unknown : unknown;
101
178
  /** What the framework actually hands a handler; `Input<O>` is its typed view. */
102
179
  export interface RouteInput {
103
180
  readonly req: BunRequest;
@@ -105,3 +182,4 @@ export interface RouteInput {
105
182
  readonly query?: unknown;
106
183
  readonly params?: unknown;
107
184
  }
185
+ export {};
@@ -11,6 +11,16 @@ export interface RouteContext {
11
11
  readonly handler: string;
12
12
  readonly method: HttpMethod;
13
13
  readonly path: string;
14
+ /**
15
+ * Whether this route declares a `body` schema, and therefore whether the input
16
+ * reader will parse the body and record it for anything else that wants it.
17
+ *
18
+ * Resolved here because it is a property of the route, known when the table is
19
+ * built. `RequestLoggingMiddleware` reads it to decide once - not per request -
20
+ * whether logging the body needs a `Request.clone()`, which is the single most
21
+ * expensive thing it can do. See `raw-body.ts`.
22
+ */
23
+ readonly parsesBody: boolean;
14
24
  get<T>(key: MetaKey<T>): T | undefined;
15
25
  }
16
26
  /**
@@ -0,0 +1,21 @@
1
+ import type { BunRequest } from 'bun';
2
+ export declare class RawBody {
3
+ /**
4
+ * Called by `RequestLoggingMiddleware` before the chain runs, when it intends to
5
+ * log this body and the route is going to parse it anyway.
6
+ */
7
+ static want(req: BunRequest): void;
8
+ /** Whether the body reader should buffer the text on the way past. */
9
+ static wanted(req: BunRequest): boolean;
10
+ /**
11
+ * Called by the body reader with the text it buffered, **before** validating it.
12
+ *
13
+ * Before, on purpose, and for two reasons. Validation applies defaults, coerces
14
+ * and strips unknown keys, so the validated object is not what the caller sent.
15
+ * And a body that fails to parse at all still has text - which is the case the
16
+ * clone used to cover and the reason this holds text rather than a value.
17
+ */
18
+ static record(req: BunRequest, text: string): void;
19
+ /** The buffered text, or `undefined` when nothing read one. */
20
+ static read(req: BunRequest): string | undefined;
21
+ }
@@ -8,15 +8,35 @@ export interface RequestLoggingOptions {
8
8
  /**
9
9
  * Log the request body. Default **`false`**.
10
10
  *
11
- * Reading it means `req.clone().text()` - a second copy of every payload,
12
- * buffered and parsed, on the hot path. Measured on the `validate` scenario in
13
- * `internal/bench`, turning both body options on costs roughly two thirds of the
14
- * throughput. It is also the field most likely to contain a password.
11
+ * **What it costs depends on whether the route declares a `body` schema**, and by
12
+ * a factor of fifteen. `bun run logging:bodies` in `internal/bench`, round-robin
13
+ * over 3 runs against `POST /validate`:
15
14
  *
16
- * Turn it on in development, where seeing the payload is the point.
15
+ * | setting | us/req | vs the default |
16
+ * | ---------------------------------------- | -----: | -------------: |
17
+ * | `requestLogging: false` | 12.80 | -4.45 us |
18
+ * | the shipped default, both bodies off | 17.25 | - |
19
+ * | **`requestBody: true`, schema route** | **19.12** | **+1.87 us** |
20
+ * | `responseBody: true` | 19.80 | +2.55 us |
21
+ * | both bodies, schema route | 20.03 | +2.78 us |
22
+ * | **`requestBody: true`, no schema** | **46.06** | **+28.81 us** |
23
+ *
24
+ * A route with a schema has already had its body buffered by the input reader, so
25
+ * the logger reads that text and nothing is cloned. A route without one leaves the
26
+ * logger to `req.clone()`, and cloning a request whose body is an unread network
27
+ * stream is the entire cost - not the second `JSON.parse`, which is 0.32 us.
28
+ * `raw-body.ts` has that decomposition.
29
+ *
30
+ * It is the field most likely to contain a password. Turn it on in development,
31
+ * where seeing the payload is the point.
17
32
  */
18
33
  readonly requestBody?: boolean;
19
- /** Log the response body. Default **`false`** - same clone-and-buffer cost. */
34
+ /**
35
+ * Log the response body. Default **`false`**, +2.55 us - see the table above.
36
+ *
37
+ * No equivalent trick here and none needed: a response is already a materialised
38
+ * string by the time this clones it, which is why it was never the expensive half.
39
+ */
20
40
  readonly responseBody?: boolean;
21
41
  /**
22
42
  * Paths to skip entirely - a health check polled every second, say.
@@ -77,6 +97,20 @@ export interface RequestLoggingOptions {
77
97
  * is for.
78
98
  */
79
99
  readonly correlate?: boolean;
100
+ /**
101
+ * Adopt W3C Trace Context, so `traceId`, `spanId` and `parentSpanId` join
102
+ * `requestId` on every line the request writes. Default **`false`**.
103
+ *
104
+ * On, an inbound `traceparent` is honoured and this service becomes a child
105
+ * span of the caller's; off, nothing reads the header and nothing is minted.
106
+ * It costs a header read and 8 random bytes per request, which is not worth
107
+ * paying in a service with nothing to correlate against - and `requestId`
108
+ * already spans two dunx services on its own.
109
+ *
110
+ * `@dunx/http/client` sends the adopted trace upstream, so turning this on at
111
+ * both ends is what makes one trace cover both.
112
+ */
113
+ readonly trace?: boolean;
80
114
  }
81
115
  /**
82
116
  * One structured entry per request, carrying the request and its response.
@@ -1,6 +1,6 @@
1
1
  import { type Ctor, type ModuleRef } from '@dunx/core';
2
2
  import type { DiscoveredRoute } from '../route/discover.js';
3
- import type { HttpMethod } from '../route/marker.js';
3
+ import { type HttpMethod } from '../route/marker.js';
4
4
  import type { UpgradeHandler } from '../ws/adapter.js';
5
5
  import { type CorsOptions } from './cors.js';
6
6
  import { type ErrorMapper } from './errors.js';
@@ -0,0 +1,51 @@
1
+ export declare const TRACEPARENT_HEADER = "traceparent";
2
+ export declare const TRACESTATE_HEADER = "tracestate";
3
+ export interface Trace {
4
+ /** 32 hex digits, shared by every span in the trace. */
5
+ readonly traceId: string;
6
+ /** 16 hex digits identifying this server's work on this request. */
7
+ readonly spanId: string;
8
+ /** The caller's span, when one arrived in `traceparent`. */
9
+ readonly parentSpanId?: string;
10
+ /** Two hex digits. Bit 0 is `sampled`. */
11
+ readonly flags: string;
12
+ /** `tracestate` verbatim, when one arrived. Vendor data this server does not read. */
13
+ readonly state?: string;
14
+ }
15
+ /**
16
+ * W3C Trace Context, propagated across services.
17
+ *
18
+ * The whole of it is one header parsed and one header written. There is no
19
+ * exporter, no sampler and no dependency: what this buys is that every log line a
20
+ * request writes carries the same `traceId` the service upstream logged, so the
21
+ * two can be joined without either of them running a collector.
22
+ *
23
+ * `@dunx/http` does not turn this on by itself - `requestLogging: { trace: true }`
24
+ * does. Adopting a trace costs a header read and 8 random bytes on every request,
25
+ * which is not worth spending in a service that has nothing to correlate with.
26
+ */
27
+ export declare class TraceContext {
28
+ #private;
29
+ /**
30
+ * The inbound `traceparent`, or a fresh trace.
31
+ *
32
+ * A malformed header is discarded rather than repaired, which is what the
33
+ * standard requires: an unparseable `traceparent` means the caller's trace is
34
+ * unknown, not that this request has none. Version `ff` is invalid, and a
35
+ * higher version keeps its first four fields and drops the rest, so a future
36
+ * format still propagates through this service instead of being dropped.
37
+ *
38
+ * When nothing arrives, `traceId` is the request id with its hyphens removed -
39
+ * a UUID is 16 bytes, which is exactly a trace id, and reusing it means one
40
+ * identifier in two spellings rather than a second `crypto` call per request.
41
+ */
42
+ static adopt(req: Request, requestId: string): Trace;
43
+ /** The trace adopted for this request, if one was. */
44
+ static of(req: Request): Trace | undefined;
45
+ /**
46
+ * The `traceparent` to send upstream. This server's span becomes the callee's
47
+ * parent, so the two link without inventing a span nothing logged.
48
+ */
49
+ static header(trace: Pick<Trace, 'traceId' | 'spanId' | 'flags'>): string;
50
+ static sampled(trace: Pick<Trace, 'flags'>): boolean;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/http",
3
- "version": "2.3.1",
3
+ "version": "2.5.0",
4
4
  "description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
5
5
  "keywords": [
6
6
  "bun",
@@ -58,7 +58,7 @@
58
58
  "@dunx/core": "workspace:*"
59
59
  },
60
60
  "peerDependencies": {
61
- "@dunx/core": "^2.3.1",
61
+ "@dunx/core": "^2.5.0",
62
62
  "@types/bun": ">=1.3.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
@@ -67,6 +67,6 @@
67
67
  }
68
68
  },
69
69
  "engines": {
70
- "bun": ">=1.3.0"
70
+ "bun": ">=1.4.0"
71
71
  }
72
72
  }
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/server/status.ts"],
4
- "sourcesContent": [
5
- "/**\n * Frozen object plus an indexed-access union, not an `enum`. An enum emits a\n * runtime object that no other syntax can produce, which is why the repo bans it -\n * see CLAUDE.md. This gives the same `HttpStatusCode.NOT_FOUND` ergonomics, a\n * narrower type, and erases cleanly.\n */\nexport const HttpStatusCode = Object.freeze({\n OK: 200,\n CREATED: 201,\n ACCEPTED: 202,\n NO_CONTENT: 204,\n MOVED_PERMANENTLY: 301,\n FOUND: 302,\n NOT_MODIFIED: 304,\n TEMPORARY_REDIRECT: 307,\n PERMANENT_REDIRECT: 308,\n BAD_REQUEST: 400,\n UNAUTHORIZED: 401,\n PAYMENT_REQUIRED: 402,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n METHOD_NOT_ALLOWED: 405,\n NOT_ACCEPTABLE: 406,\n REQUEST_TIMEOUT: 408,\n CONFLICT: 409,\n GONE: 410,\n PRECONDITION_FAILED: 412,\n PAYLOAD_TOO_LARGE: 413,\n URI_TOO_LONG: 414,\n UNSUPPORTED_MEDIA_TYPE: 415,\n IM_A_TEAPOT: 418,\n UNPROCESSABLE_ENTITY: 422,\n TOO_MANY_REQUESTS: 429,\n INTERNAL_SERVER_ERROR: 500,\n NOT_IMPLEMENTED: 501,\n BAD_GATEWAY: 502,\n SERVICE_UNAVAILABLE: 503,\n GATEWAY_TIMEOUT: 504,\n} as const);\n\n/** The status numbers: `200 | 201 | ...`. */\nexport type HttpStatusCode =\n (typeof HttpStatusCode)[keyof typeof HttpStatusCode];\n\n/** The names: `'OK' | 'CREATED' | ...`. */\nexport type HttpStatusName = keyof typeof HttpStatusCode;\n"
6
- ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMO,IAAM,iBAAiB,OAAO,OAAO;AAAA,EAC1C,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,OAAO;AAAA,EACP,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,MAAM;AAAA,EACN,qBAAqB;AAAA,EACrB,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,wBAAwB;AAAA,EACxB,aAAa;AAAA,EACb,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,uBAAuB;AAAA,EACvB,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,qBAAqB;AAAA,EACrB,iBAAiB;AACnB,CAAU;",
8
- "debugId": "463DAE47FB741BC964756E2164756E21",
9
- "names": []
10
- }
@@ -1,15 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/client/errors.ts", "../src/client/json.ts", "../src/client/options.ts", "../src/client/retry.ts", "../src/client/module.ts", "../src/client/service.ts"],
4
- "sourcesContent": [
5
- "import { AppError } from '@dunx/core';\n\n/**\n * Any non-2xx response from an outbound call, carrying the parsed body.\n *\n * **Deliberately not an `HttpError`.** `HttpError` is the inbound contract - the\n * default error mapper reads its `status` and answers the caller with it - so an\n * upstream 401 arriving as an `HttpError(401)` would make this service reply 401,\n * telling *its* client \"you are unauthorized\" when what actually happened is that\n * this service could not authenticate upstream. Extending `AppError` instead means\n * an unhandled upstream failure surfaces as a 500, which is the honest default, and\n * a caller who knows better maps it:\n *\n * ```ts\n * try {\n * return await this.http.get(url);\n * } catch (error) {\n * if (error instanceof FetchError && error.status === 404) return null;\n * throw new HttpError(HttpStatusCode.BAD_GATEWAY, 'upstream unavailable');\n * }\n * ```\n */\nexport class FetchError extends AppError {\n override readonly name = 'FetchError';\n\n constructor(\n readonly status: number,\n readonly statusText: string,\n /** The response body, parsed as JSON when it was, else text, else undefined. */\n readonly body: unknown,\n readonly response: {\n readonly method: string;\n readonly url: string;\n readonly headers: Headers;\n },\n ) {\n super(\n `HTTP ${status} ${statusText} from ${response.method} ${response.url}`,\n );\n }\n}\nObject.defineProperty(FetchError, Symbol.for('dunx.deps'), {\n value: () => [{ unresolved: \"readonly status: number\" }, { unresolved: \"readonly statusText: string\" }, { unresolved: \"readonly body: unknown\" }, { unresolved: \"readonly response: {\\n readonly method: string;\\n readonly url: string;\\n readonly headers: Headers;\\n }\" }],\n});\n\n/**\n * The request never produced a response: DNS failure, connection refused, TLS\n * rejection, or the timeout firing. `fetch` reports these as a `TypeError` or an\n * `AbortError`, neither of which says which call died.\n *\n * Separate from {@link FetchError} because there is no status to branch on and the\n * retry decision is different: a transport failure is worth retrying by default,\n * while a 400 never is.\n */\nexport class FetchTransportError extends AppError {\n override readonly name = 'FetchTransportError';\n\n constructor(\n readonly response: { readonly method: string; readonly url: string },\n /** True when the timeout or the caller's signal aborted it. */\n readonly aborted: boolean,\n options?: ErrorOptions,\n ) {\n super(\n `${response.method} ${response.url} failed: ${\n aborted ? 'aborted' : 'transport error'\n }`,\n options,\n );\n }\n}\nObject.defineProperty(FetchTransportError, Symbol.for('dunx.deps'), {\n value: () => [{ unresolved: \"readonly response: { readonly method: string; readonly url: string }\" }, { unresolved: \"readonly aborted: boolean\" }, ErrorOptions],\n});\n",
6
- "/**\n * A `JSON.stringify` that survives a cycle. **For logging only.**\n *\n * Never for a request body. The implementation this was ported from used it for\n * both, so a circular payload was *sent* upstream as `\"[Circular]\"` - a wrong body\n * that reads as a successful call and comes back as someone else's 400. A body goes\n * through plain `JSON.stringify`, which throws, because a cycle there is a bug in\n * the caller and should say so.\n */\nexport const safeStringify = (value: unknown): string => {\n const seen = new WeakSet<object>();\n return JSON.stringify(value, (_key, entry: unknown) => {\n if (typeof entry === 'object' && entry !== null) {\n if (seen.has(entry)) return '[Circular]';\n seen.add(entry);\n }\n return entry;\n });\n};\n\n/**\n * A plain object: `{}`, `Object.create(null)`, or a JSON-parsed value. Anything\n * with its own prototype - `Date`, `Map`, `Error`, a class instance - is not one.\n *\n * The prototype check rather than the reference's `typeof === 'object' && !Array\n * && !(instanceof Error)`, which answered `true` for a `Date` and for every class\n * instance, so \"is this a plain object\" did not mean what it said. Body routing\n * does not use this - see {@link isJsonBody} - so tightening it changes no\n * behaviour beyond making the predicate honest.\n */\nexport const isPlainObject = (\n value: unknown,\n): value is Record<string, unknown> => {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value) as object | null;\n return proto === Object.prototype || proto === null;\n};\n\n/**\n * Whether a payload should be JSON-encoded, or handed to `fetch` as-is.\n *\n * `fetch` already knows what to do with a `BodyInit` - it sets the boundary for a\n * `FormData`, the content type for a `URLSearchParams`, streams a `ReadableStream`\n * - so the only question is whether this value is one. Everything else, including\n * a `Date` or a class instance, is JSON: that is what `JSON.stringify` is for.\n *\n * Listed explicitly rather than inferred from `isPlainObject`, because the two\n * questions have different answers. `new Date()` is not a plain object but is\n * JSON-encodable; a `Blob` is neither.\n */\nexport const isJsonBody = (payload: unknown): boolean => {\n if (payload === null || payload === undefined) return false;\n if (typeof payload !== 'object') return typeof payload !== 'string';\n return !(\n payload instanceof FormData ||\n payload instanceof URLSearchParams ||\n payload instanceof Blob ||\n payload instanceof ArrayBuffer ||\n payload instanceof ReadableStream ||\n ArrayBuffer.isView(payload)\n );\n};\n",
7
- "import type { RetryOptions } from './retry.js';\n\n/**\n * Named `HttpClientOptions`, not `HttpOptions`: the server half already exports\n * that from `@dunx/http` for `HttpFactory.create`, and two things called\n * `HttpOptions` meaning opposite directions of traffic is the confusion this\n * subpath exists to avoid.\n */\nexport interface HttpClientOptionsInit {\n /**\n * Prefixed to a relative `path`. With it, calls name a path; without it, every\n * call passes a whole url.\n */\n readonly baseUrl?: string | URL;\n /** Per-request budget, enforced with `AbortSignal.timeout`. @default 30000 */\n readonly timeoutMs?: number;\n /** Sent on every request, under anything a call sets itself. */\n readonly headers?: Readonly<Record<string, string>>;\n readonly retry?: RetryOptions<unknown>;\n /**\n * Forward the inbound request id to the upstream, so one trace spans both\n * services. `true` uses `x-request-id`; a string names the header. Read from\n * `RequestContext`, so it only carries when there is a request in scope.\n *\n * @default true\n */\n readonly propagateRequestId?: boolean | string;\n /** Bound as its own token, so a second client can be injected by name. */\n readonly name?: string;\n /**\n * Bun-only `fetch` extensions, passed straight through. None of these exist on\n * Node's fetch, and they are the reason an outbound client on Bun can do things a\n * ported one cannot: talk through a proxy, pin a certificate, or reach a unix\n * socket, with no dependency.\n */\n readonly proxy?: string;\n readonly tls?: Bun.TLSOptions;\n readonly unix?: string;\n /** @default true - Bun decompresses by default. */\n readonly decompress?: boolean;\n /** Bun's own request/response tracing on stderr. Never on in production. */\n readonly verbose?: boolean;\n}\n\nexport const DEFAULT_REQUEST_ID_HEADER = 'x-request-id';\n\n/**\n * The resolved options, as a class so it is both the injection token and the type\n * a factory annotates - the same trick `RedisOptions` and `ConfigService` use.\n */\nexport class HttpClientOptions {\n readonly baseUrl: string | undefined;\n readonly timeoutMs: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly retry: RetryOptions<unknown>;\n readonly requestIdHeader: string | undefined;\n readonly name: string | undefined;\n readonly fetchOptions: Readonly<Record<string, unknown>>;\n\n constructor(init: HttpClientOptionsInit = {}) {\n this.baseUrl =\n init.baseUrl === undefined ? undefined : String(init.baseUrl);\n this.timeoutMs = init.timeoutMs ?? 30_000;\n this.headers = init.headers ?? {};\n this.retry = init.retry ?? {};\n this.name = init.name;\n\n const propagate = init.propagateRequestId ?? true;\n this.requestIdHeader =\n propagate === false\n ? undefined\n : propagate === true\n ? DEFAULT_REQUEST_ID_HEADER\n : propagate;\n\n // Only the keys actually set: `exactOptionalPropertyTypes` means passing\n // `proxy: undefined` is not the same as omitting it, and Bun reads presence.\n this.fetchOptions = Object.fromEntries(\n (\n [\n ['proxy', init.proxy],\n ['tls', init.tls],\n ['unix', init.unix],\n ['decompress', init.decompress],\n ['verbose', init.verbose],\n ] as const\n ).filter(([, value]) => value !== undefined),\n );\n }\n}\nObject.defineProperty(HttpClientOptions, Symbol.for('dunx.deps'), {\n value: () => [{ unresolved: \"init: HttpClientOptionsInit = {}\" }],\n});\n",
8
- "import { HttpStatusCode } from '../server/status.js';\nimport { FetchError, FetchTransportError } from './errors.js';\n\n/**\n * Retry, backoff and `Retry-After`, with no dependency.\n *\n * `crypto.getRandomValues` supplies the jitter. `Math.random` is what the source\n * this was ported from used and is banned repo-wide for anything that matters -\n * jitter matters, because decorrelating retries is the whole reason it exists. The\n * alternative, `@arkv/rng`, is a 64 KB WebAssembly PRNG, which is a lot of weight\n * to put in every deployment of the most-imported package to choose a number of\n * milliseconds. `crypto.getRandomValues` is a Web standard Bun implements natively,\n * is a CSPRNG, and costs nothing.\n */\nconst uniform = (): number => {\n const buffer = new Uint32Array(1);\n crypto.getRandomValues(buffer);\n // 2**32 rather than 0xffffffff, so the result is [0, 1) and never exactly 1.\n return (buffer[0] ?? 0) / 2 ** 32;\n};\n\nexport interface BackoffOptions {\n /** Base delay, doubled each attempt. */\n readonly baseMs: number;\n /** @default 2 */\n readonly power?: number;\n /** Upper bound of the random component added to each delay. @default 1000 */\n readonly jitterMs?: number;\n /** @default 30000 */\n readonly maxMs?: number;\n}\n\n/** `base * power^attempt + jitter`, capped. `attempt` is 0 for the first retry. */\nexport const backoffDelay = (\n attempt: number,\n { baseMs, power = 2, jitterMs = 1000, maxMs = 30_000 }: BackoffOptions,\n): number => Math.min(baseMs * power ** attempt + uniform() * jitterMs, maxMs);\n\n/**\n * The wait an upstream asked for, in ms, or undefined.\n *\n * RFC 9110 allows either a delay in seconds or an HTTP date, and both appear in\n * the wild - GitHub sends seconds, some CDNs send a date. Ignoring the header, as\n * the reference did, means retrying straight back into a rate limit that had just\n * told you exactly how long to wait.\n */\nexport const retryAfterMs = (\n headers: Headers,\n now: number = Date.now(),\n): number | undefined => {\n const header = headers.get('retry-after');\n if (header === null) return undefined;\n\n const seconds = Number(header);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n\n const at = Date.parse(header);\n return Number.isNaN(at) ? undefined : Math.max(0, at - now);\n};\n\n/**\n * Statuses worth trying again: a server that failed, one that is overloaded, and\n * one that timed out. Deliberately narrower than the source, which also retried\n * 409 and 422 - both of those are the server rejecting the *request*, and sending\n * it again unchanged gets the same answer.\n */\nexport const isRetryableStatus = (status: number): boolean =>\n status >= HttpStatusCode.INTERNAL_SERVER_ERROR ||\n status === HttpStatusCode.REQUEST_TIMEOUT ||\n status === HttpStatusCode.TOO_MANY_REQUESTS;\n\nexport interface RetryOptions<T> {\n /** Retries *after* the first attempt, so 3 means up to 4 calls. @default 3 */\n readonly maxRetries?: number;\n /** @default 1000 */\n readonly retryDelayMs?: number;\n readonly backoff?: Omit<BackoffOptions, 'baseMs'>;\n /** @default isRetryableStatus */\n readonly shouldRetryOnStatus?: (status: number) => boolean;\n /** Honour a `Retry-After` header over the computed backoff. @default true */\n readonly respectRetryAfter?: boolean;\n readonly onAttempt?: (attempt: number, isRetry: boolean) => void;\n readonly onError?: (\n error: unknown,\n attempt: number,\n willRetry: boolean,\n ) => void;\n readonly onSuccess?: (result: T, attempt: number) => void;\n}\n\n/**\n * Whether an error is worth another attempt, and how long to wait first.\n *\n * An abort is never retried: the caller's signal fired or the timeout expired, and\n * both mean the budget for this call is spent. A transport failure is retried,\n * because a refused connection is the case retrying exists for.\n */\nconst decide = <T>(\n error: unknown,\n attempt: number,\n options: RetryOptions<T>,\n): { readonly retry: boolean; readonly delayMs: number } => {\n const {\n retryDelayMs = 1000,\n backoff,\n shouldRetryOnStatus = isRetryableStatus,\n respectRetryAfter = true,\n } = options;\n const computed = backoffDelay(attempt, { baseMs: retryDelayMs, ...backoff });\n\n if (error instanceof FetchTransportError) {\n return { retry: !error.aborted, delayMs: computed };\n }\n\n if (error instanceof FetchError) {\n if (!shouldRetryOnStatus(error.status)) return { retry: false, delayMs: 0 };\n const asked = respectRetryAfter\n ? retryAfterMs(error.response.headers)\n : undefined;\n // Still capped by the backoff ceiling: an upstream asking for an hour should\n // not park a request handler for an hour.\n const maxMs = backoff?.maxMs ?? 30_000;\n return {\n retry: true,\n delayMs: asked === undefined ? computed : Math.min(asked, maxMs),\n };\n }\n\n // Something other than a fetch failure - a JSON parse, a callback throwing.\n // Retried, matching the source, because a non-HTTP error carries no verdict.\n return { retry: true, delayMs: computed };\n};\n\n/**\n * Runs `operation`, retrying per `options`.\n *\n * `Bun.sleep` rather than a `setTimeout` promise: it is the runtime's own timer and\n * needs no wrapper.\n */\nexport const executeWithRetry = async <T>(\n operation: () => Promise<T> | T,\n options: RetryOptions<T> = {},\n): Promise<T> => {\n const { maxRetries = 3, onAttempt, onError, onSuccess } = options;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt += 1) {\n onAttempt?.(attempt + 1, attempt > 0);\n try {\n const result = await operation();\n onSuccess?.(result, attempt + 1);\n return result;\n } catch (error) {\n lastError = error;\n const { retry, delayMs } = decide(error, attempt, options);\n const willRetry = retry && attempt < maxRetries;\n onError?.(error, attempt + 1, willRetry);\n\n if (!willRetry) throw error;\n await Bun.sleep(delayMs);\n }\n }\n\n // Unreachable: the loop either returns or throws. Kept so the signature does not\n // need `T | undefined`.\n throw lastError;\n};\n",
9
- "import {\n Logger,\n provide,\n RequestContext,\n token,\n type Deps,\n type DynamicModule,\n type FactoryProvider,\n type Token,\n} from '@dunx/core';\nimport { HttpClientOptions, type HttpClientOptionsInit } from './options.js';\nimport { HttpService } from './service.js';\n\nconst tokens = new Map<string, Token<HttpService>>();\n\n/**\n * The token a named client is bound to.\n *\n * Memoised, because `token()` returns a fresh object every call - without this the\n * module and the consumer would hold different tokens for `'stripe'` and the lookup\n * would miss. Same name in, same token out.\n *\n * A `Token` is not a constructor type, so a named client cannot be a constructor\n * parameter. Reach it with `inject()` in a field initialiser:\n *\n * ```ts\n * class Payments {\n * readonly stripe = inject(httpClient('stripe'));\n * }\n * ```\n */\nexport const httpClient = (name: string): Token<HttpService> => {\n const existing = tokens.get(name);\n if (existing) return existing;\n const created = token<HttpService>(`HttpService(${name})`);\n tokens.set(name, created);\n return created;\n};\n\nconst serviceFrom = (\n target: Token<HttpService> | typeof HttpService,\n optionsToken: Token<HttpClientOptions> | typeof HttpClientOptions,\n) =>\n provide(target, {\n useFactory: (\n options: HttpClientOptions,\n logger: Logger,\n context: RequestContext,\n ) => new HttpService(options, logger, context),\n inject: [optionsToken, Logger, RequestContext] as const,\n });\n\n/**\n * A named client binds its own options token, so two of them do not collide on\n * `HttpClientOptions` - the flat container reports that as a duplicate binding.\n */\nconst namedModule = (\n name: string,\n options: HttpClientOptions | FactoryProvider<HttpClientOptions, Deps>,\n): DynamicModule => {\n const optionsToken = token<HttpClientOptions>(`HttpClientOptions(${name})`);\n const optionsProvider =\n options instanceof HttpClientOptions\n ? provide(optionsToken, { useValue: options })\n : provide(optionsToken, options);\n\n return {\n module: HttpModule,\n exports: [optionsToken, httpClient(name)],\n providers: [optionsProvider, serviceFrom(httpClient(name), optionsToken)],\n };\n};\n\n/**\n * The outbound half of `@dunx/http`.\n *\n * Named `HttpModule` and `HttpService` under the `./client` subpath rather than in\n * the root barrel, where `HttpFactory` already means the inbound direction. The\n * subpath is what keeps the name unambiguous at the import site:\n *\n * ```ts\n * import { HttpFactory } from '@dunx/http'; // serving\n * import { HttpModule } from '@dunx/http/client'; // calling out\n * ```\n *\n * It depends on `Logger` and `RequestContext`, both of which core always binds, so\n * it works in an app that imported no logging module at all.\n */\nexport class HttpModule {\n /**\n * Binds `HttpService` and `HttpClientOptions`, or `httpClient(init.name)` alone\n * when `name` is set - a named registration deliberately does not also claim\n * `HttpService`, so several upstreams can coexist alongside one default.\n */\n static forRoot(init: HttpClientOptionsInit = {}): DynamicModule {\n const options = new HttpClientOptions(init);\n if (options.name !== undefined) return namedModule(options.name, options);\n\n return {\n module: HttpModule,\n exports: [HttpClientOptions, HttpService],\n providers: [\n provide(HttpClientOptions, { useValue: options }),\n serviceFrom(HttpService, HttpClientOptions),\n ],\n };\n }\n\n /**\n * `forRoot` with the options behind a factory, which is the one thing a\n * zero-argument `forRoot` cannot do: read the base url or the timeout off\n * `ConfigService`.\n *\n * There is no separate async machinery - the container resolves eagerly and\n * awaits factories before any constructor runs, so awaited config is settled by\n * the time anything is built.\n *\n * ```ts\n * HttpModule.forRootAsync({\n * useFactory: (config: AppConfigService) => ({\n * baseUrl: config.get('upstream').url,\n * }),\n * inject: [AppConfigService],\n * });\n * ```\n *\n * `name` is a parameter rather than a field of the awaited init, because the\n * token has to exist before the factory runs.\n */\n static forRootAsync(\n load: () => HttpClientOptionsInit | Promise<HttpClientOptionsInit>,\n name?: string,\n ): DynamicModule;\n static forRootAsync<const D extends Deps>(\n config: FactoryProvider<HttpClientOptionsInit, D>,\n name?: string,\n ): DynamicModule;\n static forRootAsync(\n source:\n | (() => HttpClientOptionsInit | Promise<HttpClientOptionsInit>)\n | FactoryProvider<HttpClientOptionsInit, Deps>,\n name?: string,\n ): DynamicModule {\n const load = typeof source === 'function' ? source : source.useFactory;\n const inject = typeof source === 'function' ? [] : (source.inject ?? []);\n const useFactory = async (\n ...deps: readonly unknown[]\n ): Promise<HttpClientOptions> => new HttpClientOptions(await load(...deps));\n\n if (name !== undefined) {\n return namedModule(name, { useFactory, inject } as FactoryProvider<\n HttpClientOptions,\n Deps\n >);\n }\n\n return {\n module: HttpModule,\n exports: [HttpClientOptions, HttpService],\n providers: [\n provide(HttpClientOptions, { useFactory, inject } as FactoryProvider<\n HttpClientOptions,\n Deps\n >),\n serviceFrom(HttpService, HttpClientOptions),\n ],\n };\n }\n}\n",
10
- "import { Logger, RequestContext } from '@dunx/core';\nimport { UrlHelper, type ParamsType } from '@arkv/shared';\nimport type { HttpMethod } from '../route/marker.js';\nimport { FetchError, FetchTransportError } from './errors.js';\nimport { isJsonBody, safeStringify } from './json.js';\nimport { HttpClientOptions } from './options.js';\nimport { executeWithRetry, type RetryOptions } from './retry.js';\n\n/** The client speaks two more verbs than a route can declare. */\nexport type RequestMethod = HttpMethod | 'HEAD' | 'OPTIONS';\n\n/**\n * `@arkv/shared`'s own param type, imported rather than restated - a local copy\n * would drift from what `buildUrl` actually accepts, which is how `null` ended up\n * in the first draft of this file and `interpolate` would never have seen it.\n */\ntype Params = ParamsType;\n\n/**\n * `fetch`'s own body type, derived from its signature. `BodyInit` is not a global\n * here: the root tsconfig sets `lib: [\"ESNext\"]` with no DOM, so the name does not\n * exist even though the value does. Reading it off `typeof fetch` needs no lib and\n * cannot disagree with the runtime.\n */\ntype FetchBody = NonNullable<NonNullable<Parameters<typeof fetch>[1]>['body']>;\n\nexport type HeaderFactory = (params: {\n /** Unix seconds, which is what every HMAC scheme signs. */\n readonly timestamp: number;\n readonly method: RequestMethod;\n /** `pathname + search`, the part such schemes sign. */\n readonly requestPath: string;\n /** The serialised body, or `''`. */\n readonly body: string;\n}) => Record<string, string>;\n\nexport interface RequestConfig<TRequest = unknown, TResponse = unknown> {\n readonly method: RequestMethod;\n /** Absolute, or relative to `baseUrl`. Omit when `baseUrl` plus `path` is enough. */\n readonly url?: string | URL;\n readonly payload?: TRequest;\n readonly headers?: Readonly<Record<string, string>>;\n /** Appended to the base, with `{param}` interpolated from `pathParams`. */\n readonly path?: string;\n readonly pathParams?: Params;\n readonly queryParams?: Params;\n /** Overrides the client's default budget. */\n readonly timeoutMs?: number;\n /** Called once per attempt, so a signature covers the body it is sent with. */\n readonly headerFactory?: HeaderFactory;\n /** Merged into the async context for this call, so its logs carry it. */\n readonly flow?: string;\n readonly retry?: RetryOptions<TResponse>;\n /** Cancels the call. Combined with the timeout, whichever fires first. */\n readonly signal?: AbortSignal;\n}\n\ntype BaseOptions<TRequest, TResponse> = Omit<\n RequestConfig<TRequest, TResponse>,\n 'method' | 'url' | 'payload'\n>;\n\n/**\n * What `send` reads. Narrower than `RequestConfig` on purpose: `RetryOptions<T>` is\n * invariant in `T` - its `onSuccess` takes a `T` and its callbacks return one - so a\n * `RequestConfig<_, TResponse>` is not assignable to a `RequestConfig<_, unknown>`.\n * `send` never touches `retry`, so leaving it out is both true and assignable.\n */\ninterface SendConfig {\n readonly method: RequestMethod;\n readonly headers?: Readonly<Record<string, string>>;\n readonly timeoutMs?: number;\n readonly headerFactory?: HeaderFactory;\n readonly signal?: AbortSignal;\n}\n\n/**\n * A `fetch` client with a per-request timeout, retry with backoff, request-id\n * propagation and one log line per call.\n *\n * `fetch` and nothing else: it is a Web standard Bun implements natively, so there\n * is no client dependency to justify - which is also why `axios` and `node-fetch`\n * are banned repo-wide. What this adds over calling `fetch` yourself is the parts\n * every caller otherwise reimplements slightly differently: the timeout, the\n * retry policy, `Retry-After`, url building, and a failure that says which call\n * failed.\n *\n * Extends `UrlHelper` from `@arkv/shared`, so `buildUrl` and `interpolate` are\n * available on the service, and there is one implementation of them across the\n * owner's projects rather than a fork per repo.\n */\nexport class HttpService extends UrlHelper {\n constructor(\n private readonly options: HttpClientOptions,\n private readonly logger: Logger,\n private readonly requestContext: RequestContext,\n ) {\n super();\n }\n\n async request<TRequest = unknown, TResponse = unknown>(\n config: RequestConfig<TRequest, TResponse>,\n ): Promise<TResponse> {\n const url = this.urlFor(config);\n const startedAt = Date.now();\n let attempts = 0;\n let status: number | undefined;\n\n /**\n * Serialised **once**, outside the retry loop. A body does not change between\n * attempts - only the signature over it does, and `headerFactory` gets a fresh\n * timestamp per attempt from `send`.\n *\n * Doing it inside meant a caller's own `JSON.stringify` failure, a circular\n * payload, was treated as a retryable error: three attempts and eight seconds of\n * backoff before surfacing a bug that no amount of retrying could fix. It also\n * re-serialised a large body on every attempt.\n */\n const { body, serialised } = this.bodyFor(config.payload);\n\n /**\n * A stream body is consumed by the first attempt, so a second would send an\n * empty one. Retrying is switched off rather than left to fail as a confusing\n * \"body already used\" on the retry.\n */\n const replayable = !(config.payload instanceof ReadableStream);\n\n const attempt = async (): Promise<TResponse> => {\n attempts += 1;\n const response = await this.send(config, url, body, serialised);\n status = response.status;\n\n if (!response.ok) {\n throw new FetchError(\n response.status,\n response.statusText,\n await readBody(response),\n {\n method: config.method,\n url: url.href,\n headers: response.headers,\n },\n );\n }\n\n return (await readBody(response)) as TResponse;\n };\n\n const describe = (): string => `${config.method} ${url.href}`;\n\n try {\n const result = await this.requestContext.runWithContext(\n {\n ...(config.flow === undefined ? {} : { flow: config.flow }),\n event: config.path ?? url.pathname,\n },\n () =>\n executeWithRetry(attempt, {\n ...this.options.retry,\n ...config.retry,\n ...(replayable ? {} : { maxRetries: 0 }),\n } as RetryOptions<TResponse>),\n );\n\n this.logger.debug(`${describe()} succeeded`, {\n status,\n attempts,\n elapsedMs: Date.now() - startedAt,\n });\n return result;\n } catch (error) {\n this.logger.error(`${describe()} failed`, {\n // `safeStringify`, not the error object: an upstream body can carry a cycle\n // and this is the one place that must not throw while reporting a throw.\n err: safeStringify(describeError(error)),\n attempts,\n elapsedMs: Date.now() - startedAt,\n });\n throw error;\n }\n }\n\n get<TResponse = unknown>(\n url?: string | URL,\n options?: BaseOptions<never, TResponse>,\n ): Promise<TResponse> {\n return this.request<never, TResponse>({\n method: 'GET',\n ...options,\n ...urlOf(url),\n });\n }\n\n post<TRequest = unknown, TResponse = unknown>(\n url?: string | URL,\n payload?: TRequest,\n options?: BaseOptions<TRequest, TResponse>,\n ): Promise<TResponse> {\n return this.request<TRequest, TResponse>({\n method: 'POST',\n ...options,\n ...urlOf(url),\n ...(payload === undefined ? {} : { payload }),\n });\n }\n\n put<TRequest = unknown, TResponse = unknown>(\n url?: string | URL,\n payload?: TRequest,\n options?: BaseOptions<TRequest, TResponse>,\n ): Promise<TResponse> {\n return this.request<TRequest, TResponse>({\n method: 'PUT',\n ...options,\n ...urlOf(url),\n ...(payload === undefined ? {} : { payload }),\n });\n }\n\n patch<TRequest = unknown, TResponse = unknown>(\n url?: string | URL,\n payload?: TRequest,\n options?: BaseOptions<TRequest, TResponse>,\n ): Promise<TResponse> {\n return this.request<TRequest, TResponse>({\n method: 'PATCH',\n ...options,\n ...urlOf(url),\n ...(payload === undefined ? {} : { payload }),\n });\n }\n\n delete<TResponse = unknown>(\n url?: string | URL,\n options?: BaseOptions<never, TResponse>,\n ): Promise<TResponse> {\n return this.request<never, TResponse>({\n method: 'DELETE',\n ...options,\n ...urlOf(url),\n });\n }\n\n /**\n * Yields each `data:` payload of a Server-Sent-Events response, consuming the\n * terminating `[DONE]` sentinel rather than yielding it.\n *\n * **No retry**, deliberately: a partially consumed stream cannot be replayed, so\n * retrying would re-deliver events the caller has already seen. The timeout\n * covers the connect only - it is dropped once headers arrive, or a long-lived\n * stream would be cut off mid-flight.\n *\n * Hand-rolled rather than delegated: Bun exposes no `EventSource` global and no\n * SSE parser, which was measured rather than assumed.\n */\n async *streamSse<TRequest = unknown>(\n config: Omit<RequestConfig<TRequest>, 'method' | 'retry'> & {\n readonly method?: 'GET' | 'POST';\n },\n ): AsyncGenerator<string> {\n const url = this.urlFor(config);\n const method = config.method ?? 'POST';\n const startedAt = Date.now();\n const { body, serialised } = this.bodyFor(config.payload);\n\n const response = await this.send(\n { ...config, method },\n url,\n body,\n serialised,\n 'text/event-stream',\n );\n\n if (!response.ok || response.body === null) {\n throw new FetchError(\n response.status,\n response.statusText,\n await readBody(response),\n { method, url: url.href, headers: response.headers },\n );\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n // Async iteration, not `getReader()`: it acquires the reader and releases it\n // on completion, on `break`, and on the `return` below when `[DONE]` arrives -\n // which is the case the manual form needed `releaseLock()` in a `finally` for.\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n\n let newline = buffer.indexOf('\\n');\n while (newline !== -1) {\n const line = buffer.slice(0, newline).trim();\n buffer = buffer.slice(newline + 1);\n newline = buffer.indexOf('\\n');\n\n if (!line.startsWith('data:')) continue;\n const data = line.slice(5).trim();\n if (data === '[DONE]') return;\n yield data;\n }\n }\n } finally {\n this.logger.debug(`SSE ${method} ${url.href} closed`, {\n elapsedMs: Date.now() - startedAt,\n });\n }\n }\n\n /**\n * Resolves the target, accepting the three forms a caller actually reaches for:\n * an absolute url, a path relative to `baseUrl`, or `baseUrl` plus an explicit\n * `path`.\n *\n * `get('/users')` is the one worth calling out. A relative first argument is what\n * every HTTP client takes once a base url exists, and passing it straight to\n * `buildUrl` throws `ERR_INVALID_URL` from inside `new URL()` - a message naming\n * neither the call nor the missing base. So a first argument that is not an\n * absolute url is treated as the path, which is what it reads as.\n *\n * `URL.canParse` decides, rather than a regex over `//` or `:` - it is the same\n * parser `new URL` uses, so the two cannot disagree.\n */\n private urlFor(config: {\n readonly url?: string | URL;\n readonly path?: string;\n readonly pathParams?: Params;\n readonly queryParams?: Params;\n }): URL {\n const given = config.url === undefined ? undefined : String(config.url);\n const absolute =\n given !== undefined && given !== '' && URL.canParse(given)\n ? given\n : undefined;\n const relative = given === '' || absolute !== undefined ? undefined : given;\n const base = absolute ?? this.options.baseUrl;\n\n if (base === undefined) {\n throw new FetchTransportError(\n { method: 'GET', url: given ?? config.path ?? '(none)' },\n false,\n {\n cause: new Error(\n 'No url to call. Pass an absolute url, or set baseUrl on ' +\n 'HttpModule.forRoot and pass a path.',\n ),\n },\n );\n }\n\n // An explicit `path` wins over a relative first argument, so a call cannot\n // silently request two different paths.\n const path = config.path ?? relative;\n\n return this.buildUrl({\n base,\n ...(path === undefined ? {} : { path }),\n ...(config.pathParams === undefined\n ? {}\n : { pathParams: config.pathParams }),\n ...(config.queryParams === undefined\n ? {}\n : { queryParams: config.queryParams }),\n });\n }\n\n /** `serialised` is what a `headerFactory` signs, and is `''` for no body. */\n private bodyFor(payload: unknown): {\n body: FetchBody | undefined;\n serialised: string;\n json: boolean;\n } {\n if (payload === undefined || payload === null) {\n return { body: undefined, serialised: '', json: false };\n }\n if (!isJsonBody(payload)) {\n return { body: payload as FetchBody, serialised: '', json: false };\n }\n // Plain `JSON.stringify`, deliberately not `safeStringify`: a cycle here must\n // throw rather than be sent upstream as \"[Circular]\".\n const serialised = JSON.stringify(payload);\n return { body: serialised, serialised, json: true };\n }\n\n private async send(\n config: SendConfig,\n url: URL,\n body: FetchBody | undefined,\n serialised: string,\n accept = 'application/json',\n ): Promise<Response> {\n const requestId =\n this.options.requestIdHeader === undefined\n ? undefined\n : this.requestContext.getContext().requestId;\n\n const headers: Record<string, string> = {\n accept,\n ...(serialised === '' ? {} : { 'content-type': 'application/json' }),\n ...this.options.headers,\n ...(requestId === undefined || this.options.requestIdHeader === undefined\n ? {}\n : { [this.options.requestIdHeader]: requestId }),\n ...config.headerFactory?.({\n timestamp: Math.floor(Date.now() / 1000),\n method: config.method,\n requestPath: url.pathname + url.search,\n body: serialised,\n }),\n ...config.headers,\n };\n\n /**\n * `AbortSignal.timeout` plus `AbortSignal.any`, rather than an\n * `AbortController` with a `setTimeout` and a `clearTimeout` in a `finally`.\n * Both are Web standards Bun implements, the timer is the runtime's to cancel,\n * and combining the caller's signal with the budget is one call instead of a\n * second listener that has to be removed.\n */\n const timeoutMs = config.timeoutMs ?? this.options.timeoutMs;\n const signals = [\n ...(timeoutMs > 0 ? [AbortSignal.timeout(timeoutMs)] : []),\n ...(config.signal === undefined ? [] : [config.signal]),\n ];\n\n try {\n return await fetch(url.href, {\n method: config.method,\n headers,\n ...(body === undefined ? {} : { body }),\n ...(signals.length === 0 ? {} : { signal: AbortSignal.any(signals) }),\n ...this.options.fetchOptions,\n });\n } catch (error) {\n // `fetch` reports a refused connection, a DNS failure and an abort all as\n // exceptions with nothing naming the call. Wrapped so the message does.\n const aborted =\n error instanceof Error &&\n (error.name === 'AbortError' || error.name === 'TimeoutError');\n throw new FetchTransportError(\n { method: config.method, url: url.href },\n aborted,\n { cause: error },\n );\n }\n }\n}\nObject.defineProperty(HttpService, Symbol.for('dunx.deps'), {\n value: () => [HttpClientOptions, Logger, RequestContext],\n});\n\nconst urlOf = (url?: string | URL): { url?: string | URL } =>\n url === undefined ? {} : { url };\n\n/** JSON when the upstream said so or the body parses; text otherwise; undefined for empty. */\nconst readBody = async (response: Response): Promise<unknown> => {\n const text = await response.text().catch(() => '');\n if (text === '') return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n};\n\nconst describeError = (error: unknown): Record<string, unknown> => {\n if (error instanceof FetchError) {\n return {\n name: error.name,\n message: error.message,\n status: error.status,\n body: error.body,\n };\n }\n if (error instanceof Error) {\n return { name: error.name, message: error.message };\n }\n return { message: String(error) };\n};\n"
11
- ],
12
- "mappings": "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",
13
- "debugId": "9448CCC7AF6FC82264756E2164756E21",
14
- "names": []
15
- }