brass-runtime 1.16.0 → 1.16.1
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/CHANGELOG.md +17 -0
- package/README.md +283 -18
- package/dist/agent/cli/main.cjs +38 -38
- package/dist/agent/cli/main.js +6 -6
- package/dist/agent/cli/main.mjs +6 -6
- package/dist/agent/index.cjs +7 -7
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +6 -6
- package/dist/agent/index.mjs +6 -6
- package/dist/chunk-2HQTDLHF.mjs +683 -0
- package/dist/chunk-36I3M4UC.mjs +370 -0
- package/dist/{chunk-QY5FKYEQ.js → chunk-3AYM6WPJ.js} +570 -51
- package/dist/chunk-3LOYJFRR.cjs +300 -0
- package/dist/chunk-3Y2RIUMM.js +300 -0
- package/dist/{chunk-N6VHMOWB.cjs → chunk-4ROBZFL6.cjs} +128 -128
- package/dist/{chunk-NC5SDRYE.js → chunk-52OB2ROS.js} +4 -4
- package/dist/{chunk-JX3LZQJH.cjs → chunk-52PPNNI4.cjs} +82 -20
- package/dist/{chunk-5YOQOXEQ.cjs → chunk-5EC274J5.cjs} +676 -293
- package/dist/chunk-5QC7LRZ3.js +229 -0
- package/dist/{chunk-7TL2LHQJ.js → chunk-5VRJNBLZ.mjs} +524 -141
- package/dist/chunk-62AZW6UT.cjs +313 -0
- package/dist/chunk-6IXXWIUM.js +683 -0
- package/dist/chunk-74ZTY6CP.js +2871 -0
- package/dist/chunk-76YMRMH2.cjs +777 -0
- package/dist/chunk-7CMJS3QE.mjs +2871 -0
- package/dist/{chunk-2WC63LJK.mjs → chunk-7JIJOVCT.js} +20 -10
- package/dist/{chunk-FM4W4QPL.js → chunk-A2OM6NEH.mjs} +5 -4
- package/dist/chunk-AGR5B2BC.cjs +683 -0
- package/dist/chunk-AVNQLJ5V.js +777 -0
- package/dist/chunk-B33ICAKP.js +313 -0
- package/dist/{chunk-J3H54ZRV.mjs → chunk-B5JD23U7.mjs} +1 -1
- package/dist/chunk-BABBZK4Y.js +2024 -0
- package/dist/{chunk-U5KWK3PX.mjs → chunk-C3MDXTRZ.js} +11 -0
- package/dist/{chunk-F5EUMJL7.mjs → chunk-CIZFIMK5.js} +55 -5
- package/dist/{chunk-SPUEME2B.cjs → chunk-CZIVE6NT.cjs} +12 -1
- package/dist/{chunk-TDVMADDN.js → chunk-DNFJLJMW.mjs} +11 -0
- package/dist/chunk-DNFO2EIZ.mjs +777 -0
- package/dist/{chunk-XDZOO4L5.js → chunk-EJ6BPYVR.mjs} +79 -17
- package/dist/{chunk-JNFRRJYH.cjs → chunk-ENKODRU3.cjs} +242 -192
- package/dist/chunk-EOC4UHBS.mjs +229 -0
- package/dist/{chunk-7LVI2GIN.js → chunk-FH2X7BVP.js} +507 -72
- package/dist/{chunk-OOGJ73B6.js → chunk-FHQGHPMO.mjs} +20 -10
- package/dist/{chunk-WQ5QNU5R.cjs → chunk-GLE2WY7Z.cjs} +652 -217
- package/dist/{chunk-G6IQOE4P.mjs → chunk-GYM3LLGS.mjs} +507 -72
- package/dist/chunk-HLWLMW2F.mjs +2024 -0
- package/dist/{chunk-TVN5I4U6.cjs → chunk-JF5WGYJJ.cjs} +25 -24
- package/dist/{chunk-CY33PGEX.mjs → chunk-KH4SYAOS.mjs} +570 -51
- package/dist/chunk-KN32XNTH.mjs +313 -0
- package/dist/chunk-KQLYONSE.cjs +2871 -0
- package/dist/{chunk-7HUOJA4W.cjs → chunk-KZJQ723N.cjs} +90 -80
- package/dist/{chunk-CCKHV5BT.mjs → chunk-L2SYFEBS.js} +5 -4
- package/dist/{chunk-IJT6RRQ5.cjs → chunk-L6VB5N7Q.cjs} +20 -9
- package/dist/{chunk-ZGLD4TVZ.mjs → chunk-MBEJI5HF.mjs} +4 -4
- package/dist/{chunk-PRWCB3QL.mjs → chunk-MIIYDLGM.js} +524 -141
- package/dist/{chunk-H55LI6WY.js → chunk-MOO4L7F4.mjs} +15 -4
- package/dist/{chunk-7XOPAB5Q.js → chunk-MT3OWDPC.mjs} +55 -5
- package/dist/chunk-MVGUEJ5Z.cjs +370 -0
- package/dist/chunk-PD4EJTQC.cjs +229 -0
- package/dist/chunk-PWC3RBQE.mjs +300 -0
- package/dist/{chunk-MWXMNYJS.cjs → chunk-Q2I37RP3.cjs} +643 -124
- package/dist/{chunk-VFIUZG7J.mjs → chunk-RKGKFN2A.js} +79 -17
- package/dist/{chunk-NYL4D7SK.cjs → chunk-SA6HUJVI.cjs} +5 -5
- package/dist/{chunk-K2T3DV26.mjs → chunk-TRM4JUZQ.js} +15 -4
- package/dist/chunk-UB4B6OFY.js +370 -0
- package/dist/{chunk-G3XGCZDQ.js → chunk-UCUBNWM2.js} +1 -1
- package/dist/chunk-VN44DYYT.cjs +2024 -0
- package/dist/{client-CtFmoDvM.d.ts → client-CZHU674n.d.ts} +211 -36
- package/dist/core/index.cjs +135 -9
- package/dist/core/index.d.ts +238 -33
- package/dist/core/index.js +155 -29
- package/dist/core/index.mjs +155 -29
- package/dist/{effect-CGNl5Rqp.d.ts → effect-DIUHZ9IN.d.ts} +89 -1
- package/dist/effectRunner-CFLC32IK.cjs +8 -0
- package/dist/{effectRunner-A4CHJXJI.js → effectRunner-L4S7IPT3.js} +2 -2
- package/dist/{effectRunner-OPUF6QRN.mjs → effectRunner-NNGG75QA.mjs} +2 -2
- package/dist/http/index.cjs +324 -2986
- package/dist/http/index.d.ts +54 -68
- package/dist/http/index.js +238 -2900
- package/dist/http/index.mjs +238 -2900
- package/dist/http/testing.cjs +14 -12
- package/dist/http/testing.d.ts +5 -4
- package/dist/http/testing.js +10 -8
- package/dist/http/testing.mjs +10 -8
- package/dist/index.cjs +423 -255
- package/dist/index.d.ts +87 -69
- package/dist/index.js +301 -133
- package/dist/index.mjs +301 -133
- package/dist/observability/index.cjs +16 -531
- package/dist/observability/index.d.ts +81 -8
- package/dist/observability/index.js +23 -538
- package/dist/observability/index.mjs +23 -538
- package/dist/perf/cli.cjs +401 -0
- package/dist/perf/cli.d.ts +1 -0
- package/dist/perf/cli.js +401 -0
- package/dist/perf/cli.mjs +401 -0
- package/dist/perf/index.cjs +141 -0
- package/dist/perf/index.d.ts +483 -0
- package/dist/perf/index.js +141 -0
- package/dist/perf/index.mjs +141 -0
- package/dist/schedule-CK3Ml_7p.d.ts +259 -0
- package/dist/schema/index.cjs +6 -2
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.js +5 -1
- package/dist/schema/index.mjs +5 -1
- package/dist/{server-C8hDXA74.d.ts → server-GJPg8ZSG.d.ts} +4 -3
- package/dist/{stream-dvSs0QS5.d.ts → stream-B4oK9JFP.d.ts} +1 -1
- package/dist/{tracer-B5tRH9H7.d.ts → tracer-Hwt1cl7h.d.ts} +13 -54
- package/dist/{tracing-Dt9S_6V8.d.ts → tracing-DqbTKGcf.d.ts} +1 -1
- package/docs/ARCHITECTURE.md +292 -0
- package/docs/README.md +63 -0
- package/docs/adr/0001-ai-context-pack.md +32 -0
- package/docs/agent-apply-mode.md +104 -0
- package/docs/agent-approvals.md +110 -0
- package/docs/agent-batch.md +185 -0
- package/docs/agent-boundaries.md +112 -0
- package/docs/agent-chat-sessions.md +160 -0
- package/docs/agent-ci.md +17 -0
- package/docs/agent-cli.md +405 -0
- package/docs/agent-config.md +480 -0
- package/docs/agent-context-discovery.md +159 -0
- package/docs/agent-copilot-like-dx.md +126 -0
- package/docs/agent-declarative-optimized-planning.md +138 -0
- package/docs/agent-dx.md +224 -0
- package/docs/agent-env-files.md +126 -0
- package/docs/agent-follow-up-context.md +43 -0
- package/docs/agent-global-usage.md +180 -0
- package/docs/agent-init.md +109 -0
- package/docs/agent-install-and-configure.md +516 -0
- package/docs/agent-language-workspace-ux.md +99 -0
- package/docs/agent-llm-adapters.md +123 -0
- package/docs/agent-local-install.md +190 -0
- package/docs/agent-local-tests.md +51 -0
- package/docs/agent-observability.md +155 -0
- package/docs/agent-patch-quality-loop.md +162 -0
- package/docs/agent-presets.md +22 -0
- package/docs/agent-project-commands.md +237 -0
- package/docs/agent-project-intelligence.md +156 -0
- package/docs/agent-redaction.md +18 -0
- package/docs/agent-release-readiness.md +76 -0
- package/docs/agent-rollback-safety.md +162 -0
- package/docs/agent-rollback.md +23 -0
- package/docs/agent-run-artifacts.md +16 -0
- package/docs/agent-vscode-auto-discovery.md +137 -0
- package/docs/agent-vscode-batch-runner.md +100 -0
- package/docs/agent-vscode-chat-layout.md +90 -0
- package/docs/agent-vscode-clean-install.md +147 -0
- package/docs/agent-vscode-code-actions.md +70 -0
- package/docs/agent-vscode-diff-preview.md +45 -0
- package/docs/agent-vscode-inline-assist.md +56 -0
- package/docs/agent-vscode-install.md +186 -0
- package/docs/agent-vscode-model-setup.md +97 -0
- package/docs/agent-vscode-patch-preview.md +92 -0
- package/docs/agent-vscode-problems.md +79 -0
- package/docs/agent-vscode-project-dashboard.md +106 -0
- package/docs/agent-vscode-run-history.md +92 -0
- package/docs/agent-vscode-ux.md +73 -0
- package/docs/ai/INVARIANTS.md +84 -0
- package/docs/ai/PROJECT_MAP.md +338 -0
- package/docs/ai/PUBLIC_API.md +336 -0
- package/docs/ai/VALIDATION_MATRIX.md +67 -0
- package/docs/api-polish.md +37 -0
- package/docs/cancellation.md +162 -0
- package/docs/coverage.md +46 -0
- package/docs/getting-started.md +159 -0
- package/docs/guides/README.md +40 -0
- package/docs/guides/circuit-breaker.md +89 -0
- package/docs/guides/error-handling.md +91 -0
- package/docs/guides/getting-started.md +107 -0
- package/docs/guides/layers.md +189 -0
- package/docs/guides/metrics.md +101 -0
- package/docs/guides/resource-management.md +141 -0
- package/docs/guides/retry.md +215 -0
- package/docs/guides/semaphore.md +66 -0
- package/docs/guides/streams.md +117 -0
- package/docs/guides/supervisors.md +98 -0
- package/docs/guides/testing.md +162 -0
- package/docs/guides/tracing.md +71 -0
- package/docs/http-recipes.md +399 -0
- package/docs/http.md +749 -0
- package/docs/modules.md +285 -0
- package/docs/observability-collector-smoke.md +31 -0
- package/docs/observability-framework-examples.md +98 -0
- package/docs/observability.md +542 -0
- package/docs/otel-collector-smoke.yaml +27 -0
- package/docs/performance-profiler.md +199 -0
- package/docs/production-readiness.md +73 -0
- package/docs/recipes/README.md +12 -0
- package/docs/recipes/http-server.md +45 -0
- package/docs/recipes/layers.md +44 -0
- package/docs/recipes/performance.md +47 -0
- package/docs/recipes/runtime.md +41 -0
- package/docs/recipes/testing.md +41 -0
- package/docs/release.md +53 -0
- package/docs/wasm-bounded-queues.md +44 -0
- package/docs/wasm-engine-observability-benchmarks.md +85 -0
- package/docs/wasm-fiber-engine.md +117 -0
- package/docs/wasm-scheduler-state-machine.md +122 -0
- package/docs/wasm-stream-chunks.md +54 -0
- package/package.json +22 -2
- package/dist/chunk-45F7OKGT.cjs +0 -104
- package/dist/chunk-7V4KY4RL.mjs +0 -104
- package/dist/chunk-DJQ7OMMB.cjs +0 -144
- package/dist/chunk-GOV47PPB.mjs +0 -552
- package/dist/chunk-JF4XXPZ5.cjs +0 -552
- package/dist/chunk-KCPT2D6G.js +0 -552
- package/dist/chunk-NOYZIMUJ.mjs +0 -144
- package/dist/chunk-PNVFW245.js +0 -144
- package/dist/chunk-ROJC3NBJ.js +0 -104
- package/dist/effectRunner-3ZHAD3LE.cjs +0 -8
- package/dist/schedule-Fque9Abz.d.ts +0 -70
package/docs/http.md
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# 🌐 brass-http — ZIO-style HTTP client for brass-runtime
|
|
2
|
+
|
|
3
|
+
`brass-http` is a small, composable HTTP client built on top of **brass-runtime**.
|
|
4
|
+
It follows the same design principles as ZIO HTTP:
|
|
5
|
+
|
|
6
|
+
- **Lazy & declarative**: requests are values, nothing runs until executed.
|
|
7
|
+
- **Async without Promises as semantics**: async is modeled explicitly via the runtime.
|
|
8
|
+
- **Cancelable**: requests cooperate with fiber interruption (`AbortController`).
|
|
9
|
+
- **Layered**: wire / content / metadata are cleanly separated.
|
|
10
|
+
- **Middleware-friendly**: logging, metrics, retries can be added without touching core logic.
|
|
11
|
+
|
|
12
|
+
This is **not a wrapper around `fetch` promises** — it is an effectful HTTP client integrated with the fiber runtime.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Design overview
|
|
17
|
+
|
|
18
|
+
The HTTP client is split into **three conceptual layers**:
|
|
19
|
+
|
|
20
|
+
Recommended entry points:
|
|
21
|
+
|
|
22
|
+
- `httpClient` is the default DX for most callers: text/JSON helpers, retry middleware, and `.toPromise`.
|
|
23
|
+
- `makeHttpClient` / `makeLifecycleClient` are the production-oriented clients when you need cache, deduplication, priority queues, retry, lifecycle events, stats, or `cancelAll`.
|
|
24
|
+
- `makeHttp` / `makeHttpStream` are low-level wire clients for middleware authors and tests.
|
|
25
|
+
- `httpClientWithMeta` is a compatibility/DX helper for responses that should carry request metadata.
|
|
26
|
+
|
|
27
|
+
### 1) Wire layer (transport)
|
|
28
|
+
|
|
29
|
+
Lowest level. Talks to an effect-based transport. The default transport uses
|
|
30
|
+
`fetch`, but callers can provide their own backend.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Async<R, HttpError, HttpWireResponse>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Includes:
|
|
37
|
+
- status
|
|
38
|
+
- headers
|
|
39
|
+
- raw body text
|
|
40
|
+
- timing (`ms`)
|
|
41
|
+
- status text
|
|
42
|
+
|
|
43
|
+
This layer knows **nothing** about JSON, parsing, or domain models.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 2) Content layer (default DX)
|
|
48
|
+
|
|
49
|
+
Maps wire responses into typed content.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
HttpResponse<A> = {
|
|
53
|
+
status: number
|
|
54
|
+
headers: Record<string, string>
|
|
55
|
+
body: A
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Helpers:
|
|
60
|
+
- `getText`
|
|
61
|
+
- `getJson<A>`
|
|
62
|
+
|
|
63
|
+
No metadata, no timing, no transport concerns.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### 3) Meta / observability layer (optional middleware)
|
|
68
|
+
|
|
69
|
+
Enriches content responses with metadata:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
HttpResponseWithMeta<A> = HttpResponse<A> & {
|
|
73
|
+
meta: {
|
|
74
|
+
statusText: string
|
|
75
|
+
ms: number
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Applied via `withMeta(client)` — never forced.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Basic usage (no meta)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { httpClient } from "brass-runtime/http";
|
|
88
|
+
import { toPromise } from "brass-runtime";
|
|
89
|
+
|
|
90
|
+
type Post = {
|
|
91
|
+
id: number;
|
|
92
|
+
title: string;
|
|
93
|
+
body: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const http = httpClient({
|
|
97
|
+
baseUrl: "https://jsonplaceholder.typicode.com",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const effect = http.getJson<Post>("/posts/1");
|
|
101
|
+
|
|
102
|
+
// Nothing happens yet (lazy)
|
|
103
|
+
|
|
104
|
+
const result = await toPromise(effect, {});
|
|
105
|
+
|
|
106
|
+
console.log(result.status);
|
|
107
|
+
console.log(result.body.title);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Dependency-free JSON schemas
|
|
111
|
+
|
|
112
|
+
`brass-runtime/schema` includes a small schema DSL so callers can validate JSON
|
|
113
|
+
responses without bringing Zod, Valibot, or any runtime dependency. HTTP
|
|
114
|
+
reexports `s` for convenience.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { makeDefaultHttpClient, s } from "brass-runtime/http";
|
|
118
|
+
|
|
119
|
+
const Post = s.object({
|
|
120
|
+
id: s.number({ int: true }),
|
|
121
|
+
title: s.string({ minLength: 1 }),
|
|
122
|
+
tags: s.array(s.string()).optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const http = makeDefaultHttpClient({
|
|
126
|
+
baseUrl: "https://api.example.com",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await http.getJson("/posts/1", { schema: Post }).unsafeRunPromise();
|
|
130
|
+
console.log(result.body.title);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Validation failures reject with `{ _tag: "ValidationError", message, body,
|
|
134
|
+
issues }`, where each issue includes the schema path that failed.
|
|
135
|
+
|
|
136
|
+
This makes the HTTP layer more than a typed transport wrapper: response schemas,
|
|
137
|
+
request-body schemas, construction-time config validation, cancellation,
|
|
138
|
+
retry/cache/dedup/compression, and observability all stay in the same lazy
|
|
139
|
+
effect pipeline. Callers can adopt schema validation without adding a second
|
|
140
|
+
runtime validator dependency.
|
|
141
|
+
|
|
142
|
+
Request bodies can be validated before a request is sent:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const CreatePost = s.object({
|
|
146
|
+
title: s.string({ minLength: 1 }),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await http.postJson(
|
|
150
|
+
"/posts",
|
|
151
|
+
{ title: "Hello" },
|
|
152
|
+
{ bodySchema: CreatePost, schema: Post }
|
|
153
|
+
).unsafeRunPromise();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The body argument is inferred from `bodySchema`. If `bodySchema` fails, the
|
|
157
|
+
effect rejects with `phase: "request"` and the transport is never called.
|
|
158
|
+
|
|
159
|
+
HTTP also exports error helpers and normalizers:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import {
|
|
163
|
+
formatHttpError,
|
|
164
|
+
isRetryableHttpError,
|
|
165
|
+
isTimeoutHttpError,
|
|
166
|
+
isValidationError,
|
|
167
|
+
matchHttpError,
|
|
168
|
+
toHttpError,
|
|
169
|
+
} from "brass-runtime/http";
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await http.getJson("/posts/1", { schema: Post }).unsafeRunPromise();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (isValidationError(error)) console.error(error.issues);
|
|
175
|
+
if (isTimeoutHttpError(error)) console.error("timeout/backpressure timeout");
|
|
176
|
+
if (isRetryableHttpError(error)) console.error("safe to retry later");
|
|
177
|
+
console.error(formatHttpError(error));
|
|
178
|
+
matchHttpError(error, {
|
|
179
|
+
Timeout: (err) => console.error(err.timeoutMs),
|
|
180
|
+
PoolClosed: (err) => console.error(err.key),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const mapped = toHttpError(axiosError); // understands AbortError, timeout codes, Axios-like response.status
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The same schema module can be used outside HTTP:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { s } from "brass-runtime/schema";
|
|
191
|
+
|
|
192
|
+
const Config = s.object({ port: s.int({ min: 1 }), callbackUrl: s.url() });
|
|
193
|
+
const parsed = Config.parse({ port: 3000, callbackUrl: "https://example.com/cb" });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Custom Transports
|
|
197
|
+
|
|
198
|
+
The wire client uses `fetch` by default, but the transport boundary is now an
|
|
199
|
+
effect. `makeHttp`, `httpClient`, `makeLifecycleClient`, and
|
|
200
|
+
`makeDefaultHttpClient` accept `transport`, a function from normalized
|
|
201
|
+
`HttpRequest` + resolved `URL` + `AbortSignal` to
|
|
202
|
+
`Async<unknown, HttpError, HttpWireResponse>`.
|
|
203
|
+
|
|
204
|
+
That keeps timeout, pool/adaptive limiter, stats, retry, cache, deduplication
|
|
205
|
+
and cancellation in Brass while letting the final I/O backend be `fetch`,
|
|
206
|
+
Axios, undici, a test double, or an internal client.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import {
|
|
210
|
+
makeDefaultHttpClient,
|
|
211
|
+
makePromiseHttpTransport,
|
|
212
|
+
promiseHttpTransport,
|
|
213
|
+
normalizeHttpHeaders,
|
|
214
|
+
} from "brass-runtime/http";
|
|
215
|
+
|
|
216
|
+
const transport = makePromiseHttpTransport({
|
|
217
|
+
request: ({ request, url, signal }) =>
|
|
218
|
+
myHttpLibrary.request({
|
|
219
|
+
url: url.toString(),
|
|
220
|
+
method: request.method,
|
|
221
|
+
headers: request.headers,
|
|
222
|
+
body: request.body,
|
|
223
|
+
signal,
|
|
224
|
+
}),
|
|
225
|
+
response: (res) => ({
|
|
226
|
+
status: res.status,
|
|
227
|
+
statusText: res.statusText ?? "",
|
|
228
|
+
headers: normalizeHttpHeaders(res.headers),
|
|
229
|
+
bodyText: typeof res.data === "string" ? res.data : JSON.stringify(res.data),
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const http = makeDefaultHttpClient({
|
|
234
|
+
baseUrl: "https://api.example.com",
|
|
235
|
+
transport,
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For Axios-style clients, the consuming app owns the dependency and only injects
|
|
240
|
+
the adapter. The fluent builder covers the common `status` / `statusText` /
|
|
241
|
+
`headers` / `data` response shape:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const transport = promiseHttpTransport()
|
|
245
|
+
.requestConfig(({ request, url }) => ({
|
|
246
|
+
url: url.toString(),
|
|
247
|
+
method: request.method,
|
|
248
|
+
headers: request.headers,
|
|
249
|
+
data: request.body,
|
|
250
|
+
responseType: "json",
|
|
251
|
+
}))
|
|
252
|
+
.send((config) => axiosInstance.request(config))
|
|
253
|
+
.json();
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Brass injects the `AbortSignal` into object configs before `send`, so the
|
|
257
|
+
runtime still owns real cancellation without making callers spell out
|
|
258
|
+
`signal`. Promise transports use `toHttpError` by default, so Axios-like
|
|
259
|
+
timeouts, aborts, and `response.status` failures become typed `HttpError`
|
|
260
|
+
values.
|
|
261
|
+
|
|
262
|
+
If the external client has a different shape, keep the same fluent order and
|
|
263
|
+
map only the pieces that differ:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const transport = promiseHttpTransport()
|
|
267
|
+
.requestConfig(({ request, url }) => ({ method: request.method, url }))
|
|
268
|
+
.send((config) => internalClient.send(config))
|
|
269
|
+
.json((res) => res.payload, (res) => ({
|
|
270
|
+
status: res.code,
|
|
271
|
+
statusText: res.message,
|
|
272
|
+
headers: res.headerMap,
|
|
273
|
+
transportMeta: { upstream: res.node },
|
|
274
|
+
}));
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Per-request execution knobs live under `policy`, so they compose with all
|
|
278
|
+
transports and lifecycle middleware:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
await http.getJson("/users", {
|
|
282
|
+
policy: {
|
|
283
|
+
priority: 1,
|
|
284
|
+
dedupKey: "users:list",
|
|
285
|
+
poolKey: "users-api",
|
|
286
|
+
retry: false,
|
|
287
|
+
},
|
|
288
|
+
}).unsafeRunPromise();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
For repeated intent, define named presets once and reference them per request:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const policies = defineHttpPolicyPresets({
|
|
295
|
+
readModel: {
|
|
296
|
+
lane: "read-model",
|
|
297
|
+
poolKey: "users-api",
|
|
298
|
+
priority: 2,
|
|
299
|
+
retry: { maxRetries: 2, baseDelayMs: 50 },
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const http = makeDefaultHttpClient({
|
|
304
|
+
baseUrl: "https://api.example.com",
|
|
305
|
+
policyPresets: policies,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await http.getJson("/users/1", {
|
|
309
|
+
policy: { preset: "readModel", dedupKey: "users:1" },
|
|
310
|
+
}).unsafeRunPromise();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`DefaultHttpClientConfig`, `LifecycleClientConfig`, and
|
|
314
|
+
`HttpObservabilityOptions` are validated at construction boundaries with
|
|
315
|
+
`ConfigValidationError`. Invalid policy preset fields, compression encodings,
|
|
316
|
+
and observability policy label keys fail before the first request.
|
|
317
|
+
|
|
318
|
+
## Adaptive Limiter
|
|
319
|
+
|
|
320
|
+
`adaptiveLimiter` keeps per-key state bounded with `stateTtlMs` and supports
|
|
321
|
+
explicit ramp-up controls:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { makeAdaptiveLimiterConfig, makeHttp } from "brass-runtime/http";
|
|
325
|
+
|
|
326
|
+
const http = makeHttp({
|
|
327
|
+
adaptiveLimiter: makeAdaptiveLimiterConfig("balanced", {
|
|
328
|
+
maxLimit: 256,
|
|
329
|
+
stateTtlMs: 300_000,
|
|
330
|
+
warmupRequests: 25,
|
|
331
|
+
decreaseCooldownSamples: 3,
|
|
332
|
+
historySize: 64,
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
console.log(http.adaptiveLimiter?.dump());
|
|
337
|
+
console.log(http.adaptiveLimiter?.history("https://api.example.com"));
|
|
338
|
+
http.shutdown?.();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`destroy()`/`shutdown()` clears queue-timeout and TTL timers, rejects queued
|
|
342
|
+
waiters with `PoolClosed`, and drops limiter state. Circuit breaker feedback can
|
|
343
|
+
call `markCircuitOpen(key)` directly; `withCircuitBreaker` also forwards open
|
|
344
|
+
signals when it receives an `adaptiveLimiter` or wraps a `makeHttp` client that
|
|
345
|
+
owns one.
|
|
346
|
+
For changing latency floors, `baselineStrategy` can use the exact min, P5, or a
|
|
347
|
+
low-percentile EMA. Diagnostics expose per-key limit-change `history`, current
|
|
348
|
+
utilization, throughput, rejection rate, and monotonic activity timestamps plus
|
|
349
|
+
wall-clock timestamps for logs.
|
|
350
|
+
`windowDecayFactor` weights percentiles toward recent samples, while
|
|
351
|
+
`errorWeight` blends 5xx/failure rate into the gradient so fast-failing
|
|
352
|
+
downstreams can lower concurrency before latency rises. The internal limiter
|
|
353
|
+
queue can honor request priority and, when full, evict lower-priority waiters;
|
|
354
|
+
sustained `PoolRejected` errors may include `retryAfterMs` as a client-side
|
|
355
|
+
backoff hint.
|
|
356
|
+
|
|
357
|
+
Named presets are available as `conservative`, `balanced`, and `aggressive`.
|
|
358
|
+
The default HTTP client uses `balanced` for `preset: "balanced"` and
|
|
359
|
+
`aggressive` for `preset: "default"` / `preset: "production"`.
|
|
360
|
+
`production` is the explicit name for the full production-ready default stack;
|
|
361
|
+
`default` remains as the compatibility name. Use `adaptiveLimiterPresets` or
|
|
362
|
+
`makeAdaptiveLimiterConfig(preset, overrides)` when you want a documented
|
|
363
|
+
adaptive limiter baseline with a few local overrides.
|
|
364
|
+
|
|
365
|
+
When `withHttpObservability` wraps a client that owns an adaptive limiter, it
|
|
366
|
+
records limiter gauges such as limit, in-flight, queue depth, utilization,
|
|
367
|
+
error rate, request/completion rate, rejection rate, and state count. The same
|
|
368
|
+
snapshot is attached to HTTP client span events.
|
|
369
|
+
The middleware also reads structured per-request `policy`. Logs and span
|
|
370
|
+
attributes receive `preset`, `lane`, `poolKey`, `dedupKey`, `priority`, and retry
|
|
371
|
+
overrides automatically, while metric labels stay opt-in through
|
|
372
|
+
`policy.labelKeys` to avoid accidental high-cardinality metrics.
|
|
373
|
+
|
|
374
|
+
See [`http-recipes.md`](http-recipes.md) for typed API client, testing,
|
|
375
|
+
observability, retry, adaptive limiter, and config validation recipes.
|
|
376
|
+
|
|
377
|
+
## HTTP Server
|
|
378
|
+
|
|
379
|
+
`brass-runtime/http` includes a first server MVP for Node. It uses a simple
|
|
380
|
+
router, effect-based middleware, first-party schema validation for
|
|
381
|
+
`params`/`query`/`body`/`response`, and optional observability integration.
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import {
|
|
385
|
+
Async,
|
|
386
|
+
Runtime,
|
|
387
|
+
} from "brass-runtime/core";
|
|
388
|
+
import {
|
|
389
|
+
json,
|
|
390
|
+
makeHttpRouter,
|
|
391
|
+
makeNodeHttpServer,
|
|
392
|
+
route,
|
|
393
|
+
s,
|
|
394
|
+
} from "brass-runtime/http";
|
|
395
|
+
import { makeObservability } from "brass-runtime/observability";
|
|
396
|
+
|
|
397
|
+
const Params = s.object({ id: s.nonEmptyString() });
|
|
398
|
+
const Response = s.object({ id: s.string(), ok: s.boolean() });
|
|
399
|
+
|
|
400
|
+
const router = makeHttpRouter([
|
|
401
|
+
route("GET", "/users/:id", {
|
|
402
|
+
params: Params,
|
|
403
|
+
response: Response,
|
|
404
|
+
}, (ctx) => Async.succeed(json({ id: ctx.params.id, ok: true }))),
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
const obs = makeObservability({ logs: false });
|
|
408
|
+
const runtime = new Runtime({ env: {} });
|
|
409
|
+
const server = await runtime.toPromise(makeNodeHttpServer({
|
|
410
|
+
router,
|
|
411
|
+
observability: obs,
|
|
412
|
+
port: 3000,
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
console.log(server.url());
|
|
416
|
+
await server.close();
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Path params are inferred from the route string even without a params schema:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
route("GET", "/users/:id/books/:bookId", (ctx) =>
|
|
423
|
+
Async.succeed(json({
|
|
424
|
+
userId: ctx.params.id,
|
|
425
|
+
bookId: ctx.params.bookId,
|
|
426
|
+
}))
|
|
427
|
+
);
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`makeNodeHttpServerResource` exposes the same adapter as a `Resource` for
|
|
431
|
+
scoped lifecycle. Release uses a schedule-driven graceful shutdown poll and
|
|
432
|
+
can force-close remaining connections after `gracefulShutdownMs`.
|
|
433
|
+
For declarative server lifecycle, routers also expose `listen()`:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
await runtime.toPromise(
|
|
437
|
+
router.listen({
|
|
438
|
+
host: "127.0.0.1",
|
|
439
|
+
port: 3000,
|
|
440
|
+
observability: obs,
|
|
441
|
+
}).use((server) =>
|
|
442
|
+
Async.succeed(console.log(server.url()))
|
|
443
|
+
)
|
|
444
|
+
);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Runtime health/readiness probes can be mounted as ordinary routes. They reuse
|
|
448
|
+
the observability health model and return `200` when ready or `503` when not:
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import {
|
|
452
|
+
makeHttpRouter,
|
|
453
|
+
makeRuntimeHealthRoute,
|
|
454
|
+
makeRuntimeReadinessRoute,
|
|
455
|
+
} from "brass-runtime/http";
|
|
456
|
+
|
|
457
|
+
const router = makeHttpRouter([
|
|
458
|
+
makeRuntimeHealthRoute({ runtime, registry: runtime.registry }),
|
|
459
|
+
makeRuntimeReadinessRoute({
|
|
460
|
+
runtime,
|
|
461
|
+
registry: runtime.registry,
|
|
462
|
+
adaptiveLimiters: { api: limiter },
|
|
463
|
+
readiness: { failOnDegraded: true },
|
|
464
|
+
}),
|
|
465
|
+
]);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Builder API
|
|
469
|
+
|
|
470
|
+
For discoverability, the default client also has a fluent builder:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { httpClientBuilder } from "brass-runtime/http";
|
|
474
|
+
|
|
475
|
+
const http = httpClientBuilder()
|
|
476
|
+
.baseUrl("https://api.example.com")
|
|
477
|
+
.production()
|
|
478
|
+
.balancedLimiter({ maxLimit: 128 })
|
|
479
|
+
.header("authorization", `Bearer ${token}`)
|
|
480
|
+
.cache({ ttlSeconds: 30, maxEntries: 512 })
|
|
481
|
+
.retry({ maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 })
|
|
482
|
+
.build();
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Test helpers subpath
|
|
486
|
+
|
|
487
|
+
The `brass-runtime/http/testing` subpath exposes dependency-free helpers for
|
|
488
|
+
adopters' tests:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
import {
|
|
492
|
+
makeJsonHttpResponse,
|
|
493
|
+
makeMockHttpClient,
|
|
494
|
+
runHttpEffect,
|
|
495
|
+
withMockFetch,
|
|
496
|
+
} from "brass-runtime/http/testing";
|
|
497
|
+
|
|
498
|
+
const mock = makeMockHttpClient((req) => makeJsonHttpResponse({ url: req.url }));
|
|
499
|
+
const wire = await runHttpEffect(mock({ method: "GET", url: "/users/1" }));
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## With metadata (observability)
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
import { httpClientWithMeta } from "../http/httpClient";
|
|
508
|
+
|
|
509
|
+
const http = httpClientWithMeta({
|
|
510
|
+
baseUrl: "https://jsonplaceholder.typicode.com",
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const res = await toPromise(http.getJson<Post>("/posts/1"), {});
|
|
514
|
+
|
|
515
|
+
console.log(res.status);
|
|
516
|
+
console.log(res.meta.ms);
|
|
517
|
+
console.log(res.body.title);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Metadata is **opt-in**, not baked into the core.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Lifecycle client
|
|
525
|
+
|
|
526
|
+
Use `makeHttpClient` (alias of `makeLifecycleClient`) when request lifecycle behavior is part of the contract:
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
import { makeHttpClient } from "brass-runtime/http";
|
|
530
|
+
import { toPromise } from "brass-runtime";
|
|
531
|
+
|
|
532
|
+
const http = makeHttpClient({
|
|
533
|
+
baseUrl: "https://api.example.com",
|
|
534
|
+
dedup: {},
|
|
535
|
+
cache: { ttlSeconds: 60, maxEntries: 512 },
|
|
536
|
+
priority: { concurrency: 8 },
|
|
537
|
+
retry: { maxRetries: 2, baseDelayMs: 50, maxDelayMs: 500 },
|
|
538
|
+
onEvent: (event) => {
|
|
539
|
+
console.log(event.type, event.cacheKey ?? event.priority ?? event.attempt ?? "");
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const res = await toPromise(http({ method: "GET", url: "/users/1" }), {});
|
|
544
|
+
console.log(res.status, http.stats().cacheHits);
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
`stats()` reports wire counters plus lifecycle counters for cache hits/misses,
|
|
548
|
+
dedup hits/active groups, queue depth, retry attempts, and request
|
|
549
|
+
success/failure totals.
|
|
550
|
+
`cancelAll()` aborts active requests through the same `AbortController` path used
|
|
551
|
+
by fiber interruption.
|
|
552
|
+
|
|
553
|
+
The stable composition order is:
|
|
554
|
+
|
|
555
|
+
```txt
|
|
556
|
+
wire -> priority -> retry -> cache -> dedup -> lifecycle tracking
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Raw wire access (escape hatch)
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
const wire = await toPromise(http.get("/posts/1"), {});
|
|
565
|
+
|
|
566
|
+
console.log(wire.status);
|
|
567
|
+
console.log(wire.bodyText);
|
|
568
|
+
console.log(wire.ms);
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Cancellation & interruption
|
|
574
|
+
|
|
575
|
+
All requests are **cooperatively cancelable**.
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
const fiber = fork(http.getJson<Post>("/posts/1"), {});
|
|
579
|
+
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
fiber.interrupt(); // aborts the underlying transport
|
|
582
|
+
}, 50);
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Middleware model (ZIO-style)
|
|
588
|
+
|
|
589
|
+
Clients are **just functions**:
|
|
590
|
+
|
|
591
|
+
```
|
|
592
|
+
type HttpClient = (req: HttpRequest) =>
|
|
593
|
+
Async<unknown, HttpError, HttpWireResponse>;
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Status
|
|
599
|
+
|
|
600
|
+
Experimental but stable enough to use and evolve.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Timeout, pool y retry budget
|
|
605
|
+
|
|
606
|
+
`makeHttp`, `httpClient` y `httpClientStream` aceptan controles de fase 2:
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
const http = httpClient({
|
|
610
|
+
baseUrl: "https://api.example.com",
|
|
611
|
+
timeoutMs: 2_000,
|
|
612
|
+
pool: {
|
|
613
|
+
concurrency: 32,
|
|
614
|
+
maxQueue: 128,
|
|
615
|
+
queueTimeoutMs: 100,
|
|
616
|
+
key: "origin",
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`timeoutMs` cubre espera de pool, transporte y lectura de body en respuestas no-streaming.
|
|
622
|
+
|
|
623
|
+
El pool permite rechazar temprano en vez de dejar requests vivos hasta un `504`:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
pool: { concurrency: 16, maxQueue: 0 } // fail-fast
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Errores nuevos:
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
{ _tag: "Timeout", timeoutMs, phase: "request", message }
|
|
633
|
+
{ _tag: "PoolRejected", key, limit, message, retryAfterMs? }
|
|
634
|
+
{ _tag: "PoolTimeout", key, timeoutMs, message }
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Retry ahora puede tener budget total:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
http.withRetry({
|
|
641
|
+
maxRetries: 2,
|
|
642
|
+
baseDelayMs: 25,
|
|
643
|
+
maxDelayMs: 250,
|
|
644
|
+
maxElapsedMs: 800,
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
El retry default reintenta `Timeout`, `PoolTimeout` y `FetchError` sin status o
|
|
649
|
+
con status retriable (`408`, `429`, `5xx` relevantes). No reintenta `Abort`,
|
|
650
|
+
`BadUrl`, `PoolRejected` ni `FetchError` con status no retriable como `404`.
|
|
651
|
+
|
|
652
|
+
Stats:
|
|
653
|
+
|
|
654
|
+
```ts
|
|
655
|
+
console.log(http.stats());
|
|
656
|
+
console.log(http.wire.stats());
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Y desde runtime:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
import { abortablePromiseStats } from "../core/runtime/runtime";
|
|
663
|
+
console.log(abortablePromiseStats());
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## HTTP feature middlewares
|
|
669
|
+
|
|
670
|
+
Estas features viven en la capa HTTP y se componen como middleware sobre el wire client.
|
|
671
|
+
|
|
672
|
+
### Response compression
|
|
673
|
+
|
|
674
|
+
`makeResponseCompressionMiddleware` agrega `Accept-Encoding` cuando falta y descomprime respuestas con `Content-Encoding` soportado.
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
import { httpClient, makeResponseCompressionMiddleware } from "brass-runtime/http";
|
|
678
|
+
|
|
679
|
+
const compression = makeResponseCompressionMiddleware({
|
|
680
|
+
encodings: ["br", "gzip", "deflate"],
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const http = httpClient({ baseUrl: "https://api.example.com" })
|
|
684
|
+
.with(compression.middleware);
|
|
685
|
+
|
|
686
|
+
const res = await http.getText("/data").toPromise({});
|
|
687
|
+
console.log(res.body);
|
|
688
|
+
console.log(compression.stats());
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Request compression
|
|
692
|
+
|
|
693
|
+
`makeRequestCompressionMiddleware` comprime bodies salientes de `POST`, `PUT` y `PATCH` cuando superan `minBytes`.
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
import { httpClient, makeRequestCompressionMiddleware } from "brass-runtime/http";
|
|
697
|
+
|
|
698
|
+
const requestCompression = makeRequestCompressionMiddleware({
|
|
699
|
+
encoding: "gzip",
|
|
700
|
+
minBytes: 1024,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const http = httpClient({ baseUrl: "https://api.example.com" })
|
|
704
|
+
.with(requestCompression.middleware);
|
|
705
|
+
|
|
706
|
+
await http.post("/upload", largeBody).toPromise({});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Request batching
|
|
710
|
+
|
|
711
|
+
El batching es server-specific: Brass agrupa requests y vos definis como se encodea el batch y como se divide la respuesta.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
import { httpClient, withRequestBatching } from "brass-runtime/http";
|
|
715
|
+
|
|
716
|
+
const http = httpClient({ baseUrl: "https://api.example.com" })
|
|
717
|
+
.with(withRequestBatching({
|
|
718
|
+
key: () => "users",
|
|
719
|
+
maxBatchSize: 16,
|
|
720
|
+
maxWaitMs: 5,
|
|
721
|
+
encode: (requests) => ({
|
|
722
|
+
method: "POST",
|
|
723
|
+
url: "/batch",
|
|
724
|
+
headers: { "content-type": "application/json" },
|
|
725
|
+
body: JSON.stringify(requests.map((req) => ({ method: req.method, url: req.url }))),
|
|
726
|
+
}),
|
|
727
|
+
decode: (response) => {
|
|
728
|
+
const bodies = JSON.parse(response.bodyText) as unknown[];
|
|
729
|
+
return bodies.map((body) => ({ ...response, bodyText: JSON.stringify(body) }));
|
|
730
|
+
},
|
|
731
|
+
}));
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Connection pre-warming
|
|
735
|
+
|
|
736
|
+
`prewarmConnections` ejecuta requests livianos, por defecto `HEAD`, para preparar conexiones antes del trafico real. Tambien existe `withConnectionPrewarming` para calentar el origen en el primer request.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
import { toPromise } from "brass-runtime";
|
|
740
|
+
import { prewarmConnections } from "brass-runtime/http";
|
|
741
|
+
|
|
742
|
+
await toPromise(
|
|
743
|
+
prewarmConnections({
|
|
744
|
+
baseUrl: "https://api.example.com",
|
|
745
|
+
urls: ["/health"],
|
|
746
|
+
}),
|
|
747
|
+
{}
|
|
748
|
+
);
|
|
749
|
+
```
|