brass-runtime 1.15.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.
Files changed (209) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +673 -136
  3. package/dist/agent/cli/main.cjs +40 -35
  4. package/dist/agent/cli/main.js +9 -4
  5. package/dist/agent/cli/main.mjs +9 -4
  6. package/dist/agent/index.cjs +8 -4
  7. package/dist/agent/index.d.ts +1 -1
  8. package/dist/agent/index.js +7 -3
  9. package/dist/agent/index.mjs +7 -3
  10. package/dist/chunk-2HQTDLHF.mjs +683 -0
  11. package/dist/chunk-36I3M4UC.mjs +370 -0
  12. package/dist/chunk-3AYM6WPJ.js +1629 -0
  13. package/dist/chunk-3LOYJFRR.cjs +300 -0
  14. package/dist/chunk-3RG5ZIWI.js +10 -0
  15. package/dist/chunk-3Y2RIUMM.js +300 -0
  16. package/dist/{chunk-VEZNF5GZ.cjs → chunk-4ROBZFL6.cjs} +130 -126
  17. package/dist/{chunk-3QMOKAS5.js → chunk-52OB2ROS.js} +9 -5
  18. package/dist/chunk-52PPNNI4.cjs +416 -0
  19. package/dist/chunk-5EC274J5.cjs +2874 -0
  20. package/dist/chunk-5QC7LRZ3.js +229 -0
  21. package/dist/chunk-5VRJNBLZ.mjs +2874 -0
  22. package/dist/chunk-62AZW6UT.cjs +313 -0
  23. package/dist/chunk-6IXXWIUM.js +683 -0
  24. package/dist/chunk-74ZTY6CP.js +2871 -0
  25. package/dist/chunk-76YMRMH2.cjs +777 -0
  26. package/dist/chunk-7CMJS3QE.mjs +2871 -0
  27. package/dist/{chunk-4NHES7VK.mjs → chunk-7JIJOVCT.js} +27 -13
  28. package/dist/chunk-A2OM6NEH.mjs +194 -0
  29. package/dist/chunk-AGR5B2BC.cjs +683 -0
  30. package/dist/chunk-AVNQLJ5V.js +777 -0
  31. package/dist/chunk-B33ICAKP.js +313 -0
  32. package/dist/{chunk-ELOOF35R.mjs → chunk-B5JD23U7.mjs} +1 -1
  33. package/dist/chunk-BABBZK4Y.js +2024 -0
  34. package/dist/chunk-C3MDXTRZ.js +354 -0
  35. package/dist/chunk-CIZFIMK5.js +2193 -0
  36. package/dist/chunk-CZIVE6NT.cjs +354 -0
  37. package/dist/chunk-DNFJLJMW.mjs +354 -0
  38. package/dist/chunk-DNFO2EIZ.mjs +777 -0
  39. package/dist/chunk-EJ6BPYVR.mjs +416 -0
  40. package/dist/chunk-ENKODRU3.cjs +2193 -0
  41. package/dist/chunk-EOC4UHBS.mjs +229 -0
  42. package/dist/{chunk-BMH5AV44.js → chunk-FH2X7BVP.js} +756 -440
  43. package/dist/{chunk-PPUXIH5R.js → chunk-FHQGHPMO.mjs} +27 -13
  44. package/dist/{chunk-TGIFUAK4.cjs → chunk-GLE2WY7Z.cjs} +951 -635
  45. package/dist/{chunk-BDF4AMWX.mjs → chunk-GYM3LLGS.mjs} +756 -440
  46. package/dist/chunk-HLWLMW2F.mjs +2024 -0
  47. package/dist/chunk-JF5WGYJJ.cjs +194 -0
  48. package/dist/chunk-KH4SYAOS.mjs +1629 -0
  49. package/dist/chunk-KN32XNTH.mjs +313 -0
  50. package/dist/chunk-KQLYONSE.cjs +2871 -0
  51. package/dist/{chunk-STVLQ3XD.cjs → chunk-KZJQ723N.cjs} +92 -78
  52. package/dist/chunk-L2SYFEBS.js +194 -0
  53. package/dist/chunk-L6VB5N7Q.cjs +104 -0
  54. package/dist/{chunk-K6M7MDZ4.mjs → chunk-MBEJI5HF.mjs} +9 -5
  55. package/dist/chunk-MIIYDLGM.js +2874 -0
  56. package/dist/chunk-MOO4L7F4.mjs +104 -0
  57. package/dist/chunk-MT3OWDPC.mjs +2193 -0
  58. package/dist/chunk-MVGUEJ5Z.cjs +370 -0
  59. package/dist/chunk-OBGZSXTJ.cjs +10 -0
  60. package/dist/chunk-PD4EJTQC.cjs +229 -0
  61. package/dist/chunk-PWC3RBQE.mjs +300 -0
  62. package/dist/chunk-Q2I37RP3.cjs +1629 -0
  63. package/dist/chunk-RKGKFN2A.js +416 -0
  64. package/dist/{chunk-R3R2FVLG.cjs → chunk-SA6HUJVI.cjs} +5 -5
  65. package/dist/chunk-TRM4JUZQ.js +104 -0
  66. package/dist/chunk-UB4B6OFY.js +370 -0
  67. package/dist/{chunk-TO7IKXYT.js → chunk-UCUBNWM2.js} +1 -1
  68. package/dist/chunk-VN44DYYT.cjs +2024 -0
  69. package/dist/chunk-Y6FXYEAI.mjs +10 -0
  70. package/dist/client-CZHU674n.d.ts +820 -0
  71. package/dist/core/index.cjs +198 -4
  72. package/dist/core/index.d.ts +311 -212
  73. package/dist/core/index.js +237 -43
  74. package/dist/core/index.mjs +237 -43
  75. package/dist/{effect-CMOQKX8y.d.ts → effect-DIUHZ9IN.d.ts} +195 -1
  76. package/dist/effectRunner-CFLC32IK.cjs +8 -0
  77. package/dist/effectRunner-L4S7IPT3.js +8 -0
  78. package/dist/effectRunner-NNGG75QA.mjs +8 -0
  79. package/dist/http/index.cjs +1227 -2971
  80. package/dist/http/index.d.ts +826 -280
  81. package/dist/http/index.js +1089 -2833
  82. package/dist/http/index.mjs +1089 -2833
  83. package/dist/http/testing.cjs +161 -0
  84. package/dist/http/testing.d.ts +43 -0
  85. package/dist/http/testing.js +161 -0
  86. package/dist/http/testing.mjs +161 -0
  87. package/dist/index.cjs +486 -250
  88. package/dist/index.d.ts +87 -95
  89. package/dist/index.js +391 -155
  90. package/dist/index.mjs +391 -155
  91. package/dist/observability/index.cjs +162 -0
  92. package/dist/observability/index.d.ts +152 -0
  93. package/dist/observability/index.js +162 -0
  94. package/dist/observability/index.mjs +162 -0
  95. package/dist/perf/cli.cjs +401 -0
  96. package/dist/perf/cli.d.ts +1 -0
  97. package/dist/perf/cli.js +401 -0
  98. package/dist/perf/cli.mjs +401 -0
  99. package/dist/perf/index.cjs +141 -0
  100. package/dist/perf/index.d.ts +483 -0
  101. package/dist/perf/index.js +141 -0
  102. package/dist/perf/index.mjs +141 -0
  103. package/dist/schedule-CK3Ml_7p.d.ts +259 -0
  104. package/dist/schema/index.cjs +29 -0
  105. package/dist/schema/index.d.ts +179 -0
  106. package/dist/schema/index.js +29 -0
  107. package/dist/schema/index.mjs +29 -0
  108. package/dist/server-GJPg8ZSG.d.ts +675 -0
  109. package/dist/{stream-FQm9h4Mg.d.ts → stream-B4oK9JFP.d.ts} +1 -1
  110. package/dist/tracer-Hwt1cl7h.d.ts +189 -0
  111. package/dist/tracing-DqbTKGcf.d.ts +148 -0
  112. package/docs/ARCHITECTURE.md +292 -0
  113. package/docs/README.md +63 -0
  114. package/docs/adr/0001-ai-context-pack.md +32 -0
  115. package/docs/agent-apply-mode.md +104 -0
  116. package/docs/agent-approvals.md +110 -0
  117. package/docs/agent-batch.md +185 -0
  118. package/docs/agent-boundaries.md +112 -0
  119. package/docs/agent-chat-sessions.md +160 -0
  120. package/docs/agent-ci.md +17 -0
  121. package/docs/agent-cli.md +405 -0
  122. package/docs/agent-config.md +480 -0
  123. package/docs/agent-context-discovery.md +159 -0
  124. package/docs/agent-copilot-like-dx.md +126 -0
  125. package/docs/agent-declarative-optimized-planning.md +138 -0
  126. package/docs/agent-dx.md +224 -0
  127. package/docs/agent-env-files.md +126 -0
  128. package/docs/agent-follow-up-context.md +43 -0
  129. package/docs/agent-global-usage.md +180 -0
  130. package/docs/agent-init.md +109 -0
  131. package/docs/agent-install-and-configure.md +516 -0
  132. package/docs/agent-language-workspace-ux.md +99 -0
  133. package/docs/agent-llm-adapters.md +123 -0
  134. package/docs/agent-local-install.md +190 -0
  135. package/docs/agent-local-tests.md +51 -0
  136. package/docs/agent-observability.md +155 -0
  137. package/docs/agent-patch-quality-loop.md +162 -0
  138. package/docs/agent-presets.md +22 -0
  139. package/docs/agent-project-commands.md +237 -0
  140. package/docs/agent-project-intelligence.md +156 -0
  141. package/docs/agent-redaction.md +18 -0
  142. package/docs/agent-release-readiness.md +76 -0
  143. package/docs/agent-rollback-safety.md +162 -0
  144. package/docs/agent-rollback.md +23 -0
  145. package/docs/agent-run-artifacts.md +16 -0
  146. package/docs/agent-vscode-auto-discovery.md +137 -0
  147. package/docs/agent-vscode-batch-runner.md +100 -0
  148. package/docs/agent-vscode-chat-layout.md +90 -0
  149. package/docs/agent-vscode-clean-install.md +147 -0
  150. package/docs/agent-vscode-code-actions.md +70 -0
  151. package/docs/agent-vscode-diff-preview.md +45 -0
  152. package/docs/agent-vscode-inline-assist.md +56 -0
  153. package/docs/agent-vscode-install.md +186 -0
  154. package/docs/agent-vscode-model-setup.md +97 -0
  155. package/docs/agent-vscode-patch-preview.md +92 -0
  156. package/docs/agent-vscode-problems.md +79 -0
  157. package/docs/agent-vscode-project-dashboard.md +106 -0
  158. package/docs/agent-vscode-run-history.md +92 -0
  159. package/docs/agent-vscode-ux.md +73 -0
  160. package/docs/ai/INVARIANTS.md +84 -0
  161. package/docs/ai/PROJECT_MAP.md +338 -0
  162. package/docs/ai/PUBLIC_API.md +336 -0
  163. package/docs/ai/VALIDATION_MATRIX.md +67 -0
  164. package/docs/api-polish.md +37 -0
  165. package/docs/cancellation.md +162 -0
  166. package/docs/coverage.md +46 -0
  167. package/docs/getting-started.md +159 -0
  168. package/docs/guides/README.md +40 -0
  169. package/docs/guides/circuit-breaker.md +89 -0
  170. package/docs/guides/error-handling.md +91 -0
  171. package/docs/guides/getting-started.md +107 -0
  172. package/docs/guides/layers.md +189 -0
  173. package/docs/guides/metrics.md +101 -0
  174. package/docs/guides/resource-management.md +141 -0
  175. package/docs/guides/retry.md +215 -0
  176. package/docs/guides/semaphore.md +66 -0
  177. package/docs/guides/streams.md +117 -0
  178. package/docs/guides/supervisors.md +98 -0
  179. package/docs/guides/testing.md +162 -0
  180. package/docs/guides/tracing.md +71 -0
  181. package/docs/http-recipes.md +399 -0
  182. package/docs/http.md +749 -0
  183. package/docs/modules.md +285 -0
  184. package/docs/observability-collector-smoke.md +31 -0
  185. package/docs/observability-framework-examples.md +98 -0
  186. package/docs/observability.md +542 -0
  187. package/docs/otel-collector-smoke.yaml +27 -0
  188. package/docs/performance-profiler.md +199 -0
  189. package/docs/production-readiness.md +73 -0
  190. package/docs/recipes/README.md +12 -0
  191. package/docs/recipes/http-server.md +45 -0
  192. package/docs/recipes/layers.md +44 -0
  193. package/docs/recipes/performance.md +47 -0
  194. package/docs/recipes/runtime.md +41 -0
  195. package/docs/recipes/testing.md +41 -0
  196. package/docs/release.md +53 -0
  197. package/docs/wasm-bounded-queues.md +44 -0
  198. package/docs/wasm-engine-observability-benchmarks.md +85 -0
  199. package/docs/wasm-fiber-engine.md +117 -0
  200. package/docs/wasm-scheduler-state-machine.md +122 -0
  201. package/docs/wasm-stream-chunks.md +54 -0
  202. package/package.json +48 -2
  203. package/dist/chunk-AR22SXML.js +0 -1043
  204. package/dist/chunk-BDYEENHT.js +0 -224
  205. package/dist/chunk-JFPU5GQI.mjs +0 -1043
  206. package/dist/chunk-MS34J5LY.cjs +0 -224
  207. package/dist/chunk-UMAZLXAB.mjs +0 -224
  208. package/dist/chunk-XPZNXSVN.cjs +0 -1043
  209. package/dist/tracing-DNT9jEbr.d.ts +0 -106
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
+ ```