brass-runtime 1.17.0 → 1.18.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.
Files changed (120) hide show
  1. package/README.md +36 -3
  2. package/dist/agent/cli/main.cjs +31 -32
  3. package/dist/agent/cli/main.js +3 -4
  4. package/dist/agent/cli/main.mjs +3 -4
  5. package/dist/agent/index.cjs +4 -5
  6. package/dist/agent/index.d.ts +1 -1
  7. package/dist/agent/index.js +3 -4
  8. package/dist/agent/index.mjs +3 -4
  9. package/dist/{chunk-GYM3LLGS.mjs → chunk-2QNREG6K.mjs} +188 -5
  10. package/dist/{chunk-4ROBZFL6.cjs → chunk-2SLT3X6G.cjs} +6 -8
  11. package/dist/{chunk-KZJQ723N.cjs → chunk-3PFZGP23.cjs} +13 -15
  12. package/dist/{chunk-VWIPB6I5.js → chunk-3PHU7FWS.js} +528 -23
  13. package/dist/{chunk-BKK77SBA.js → chunk-4YQHPIWJ.js} +32 -11
  14. package/dist/chunk-5XADBMSU.cjs +33 -0
  15. package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
  16. package/dist/chunk-7TKI527D.cjs +123 -0
  17. package/dist/{chunk-AGR5B2BC.cjs → chunk-7TXQJFZX.cjs} +564 -12
  18. package/dist/{chunk-RKGKFN2A.js → chunk-AADFFVYS.js} +1 -1
  19. package/dist/{chunk-52PPNNI4.cjs → chunk-AJMKZXRB.cjs} +2 -2
  20. package/dist/{chunk-3AYM6WPJ.js → chunk-BG5RNEA2.js} +20 -299
  21. package/dist/{chunk-2HQTDLHF.mjs → chunk-ELLF55ER.mjs} +555 -3
  22. package/dist/{chunk-EOC4UHBS.mjs → chunk-G5JTCFMI.mjs} +2 -2
  23. package/dist/chunk-H5GYX7RZ.js +6126 -0
  24. package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
  25. package/dist/{chunk-6IXXWIUM.js → chunk-IBRHSH5H.js} +555 -3
  26. package/dist/{chunk-Q2I37RP3.cjs → chunk-IFRBVMWJ.cjs} +44 -323
  27. package/dist/{chunk-52OB2ROS.js → chunk-ITG6I7ZS.js} +2 -4
  28. package/dist/chunk-ITZQ526U.mjs +33 -0
  29. package/dist/{chunk-7JIJOVCT.js → chunk-JH4GI3DW.js} +2 -4
  30. package/dist/{chunk-F6XWZQY4.cjs → chunk-KHACHFBQ.cjs} +583 -78
  31. package/dist/{chunk-4P2HHGAX.mjs → chunk-KRYP6CAE.mjs} +32 -11
  32. package/dist/chunk-KTGDLBLD.mjs +123 -0
  33. package/dist/{chunk-WBGRHGBP.cjs → chunk-LXBU5E77.cjs} +114 -93
  34. package/dist/{chunk-PD4EJTQC.cjs → chunk-N6QNSTWD.cjs} +5 -5
  35. package/dist/{chunk-6RY2FFN4.mjs → chunk-OI4ESUMC.mjs} +9 -11
  36. package/dist/{chunk-EJ6BPYVR.mjs → chunk-OT2TESZU.mjs} +1 -1
  37. package/dist/{chunk-7X3K5RMS.js → chunk-PSEU65ND.js} +9 -11
  38. package/dist/{chunk-SK7UZRNI.mjs → chunk-QCOLAHU3.mjs} +528 -23
  39. package/dist/{chunk-KH4SYAOS.mjs → chunk-QZ6QFJNM.mjs} +20 -299
  40. package/dist/{chunk-MBEJI5HF.mjs → chunk-R6WDSZA6.mjs} +2 -4
  41. package/dist/{chunk-FHQGHPMO.mjs → chunk-RREBJX2S.mjs} +2 -4
  42. package/dist/{chunk-5QC7LRZ3.js → chunk-S4HHFUYP.js} +2 -2
  43. package/dist/{chunk-GLE2WY7Z.cjs → chunk-SSQJKDN3.cjs} +194 -11
  44. package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
  45. package/dist/chunk-VIFA4DPN.cjs +6126 -0
  46. package/dist/chunk-W6WR37HN.js +33 -0
  47. package/dist/{chunk-FH2X7BVP.js → chunk-XSAHV5HQ.js} +188 -5
  48. package/dist/chunk-YM3EDNYD.js +123 -0
  49. package/dist/{chunk-7ZPEZ57L.cjs → chunk-YTX2JYYP.cjs} +18 -20
  50. package/dist/chunk-Z3PSSXP3.mjs +6126 -0
  51. package/dist/core/index.cjs +31 -9
  52. package/dist/core/index.d.ts +19 -152
  53. package/dist/core/index.js +80 -58
  54. package/dist/core/index.mjs +80 -58
  55. package/dist/defaultClient-DhpCQW9m.d.ts +1623 -0
  56. package/dist/{effect-DIUHZ9IN.d.ts → effect-CtUDl5M5.d.ts} +1 -1
  57. package/dist/http/index.cjs +202 -59
  58. package/dist/http/index.d.ts +55 -819
  59. package/dist/http/index.js +216 -73
  60. package/dist/http/index.mjs +216 -73
  61. package/dist/http/testing.cjs +31 -10
  62. package/dist/http/testing.d.ts +16 -5
  63. package/dist/http/testing.js +29 -8
  64. package/dist/http/testing.mjs +29 -8
  65. package/dist/index.cjs +110 -88
  66. package/dist/index.d.ts +9 -8
  67. package/dist/index.js +81 -59
  68. package/dist/index.mjs +81 -59
  69. package/dist/{schedule-CK3Ml_7p.d.ts → layer-BalPI6cN.d.ts} +176 -2
  70. package/dist/observability/index.cjs +20 -7
  71. package/dist/observability/index.d.ts +32 -8
  72. package/dist/observability/index.js +19 -6
  73. package/dist/observability/index.mjs +19 -6
  74. package/dist/perf/cli.cjs +26 -28
  75. package/dist/perf/cli.js +11 -13
  76. package/dist/perf/cli.mjs +11 -13
  77. package/dist/perf/index.cjs +13 -15
  78. package/dist/perf/index.js +11 -13
  79. package/dist/perf/index.mjs +11 -13
  80. package/dist/schema/index.cjs +2 -2
  81. package/dist/schema/index.js +1 -1
  82. package/dist/schema/index.mjs +1 -1
  83. package/dist/{server-D6JZ15_e.d.ts → server-C1zVmqE6.d.ts} +5 -5
  84. package/dist/{stream-B4oK9JFP.d.ts → stream-Bb4FTejt.d.ts} +1 -1
  85. package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DzfuE6um.d.ts} +2 -2
  86. package/dist/{tracing-DqbTKGcf.d.ts → tracing-BABA5arE.d.ts} +1 -1
  87. package/docs/README.md +2 -0
  88. package/docs/ai/PUBLIC_API.md +28 -7
  89. package/docs/articles/brass-runtime-http-observability.md +467 -0
  90. package/docs/frameworks/angular.md +51 -0
  91. package/docs/frameworks/express.md +58 -0
  92. package/docs/frameworks/fastify.md +49 -0
  93. package/docs/frameworks/nestjs.md +53 -0
  94. package/docs/frameworks/nextjs.md +55 -0
  95. package/docs/frameworks/react.md +44 -0
  96. package/docs/frameworks/vanilla.md +56 -0
  97. package/docs/guides/layers.md +130 -0
  98. package/docs/http-recipes.md +31 -1
  99. package/docs/http.md +50 -1
  100. package/docs/observability.md +132 -0
  101. package/docs/performance-profiler.md +6 -2
  102. package/docs/recipes/layers.md +46 -2
  103. package/docs/recipes/testing.md +25 -0
  104. package/package.json +4 -1
  105. package/dist/chunk-3LOYJFRR.cjs +0 -300
  106. package/dist/chunk-3Y2RIUMM.js +0 -300
  107. package/dist/chunk-5EC274J5.cjs +0 -2874
  108. package/dist/chunk-5VRJNBLZ.mjs +0 -2874
  109. package/dist/chunk-62AZW6UT.cjs +0 -313
  110. package/dist/chunk-74ZTY6CP.js +0 -2871
  111. package/dist/chunk-7CMJS3QE.mjs +0 -2871
  112. package/dist/chunk-A2OM6NEH.mjs +0 -194
  113. package/dist/chunk-B33ICAKP.js +0 -313
  114. package/dist/chunk-JF5WGYJJ.cjs +0 -194
  115. package/dist/chunk-KN32XNTH.mjs +0 -313
  116. package/dist/chunk-KQLYONSE.cjs +0 -2871
  117. package/dist/chunk-L2SYFEBS.js +0 -194
  118. package/dist/chunk-MIIYDLGM.js +0 -2874
  119. package/dist/chunk-PWC3RBQE.mjs +0 -300
  120. package/dist/client-CZHU674n.d.ts +0 -820
@@ -145,3 +145,58 @@ export async function POST(
145
145
  For Client Components, reuse the React provider recipe and set
146
146
  `otlpEndpoint: "/api/otel"`.
147
147
 
148
+ ## Layer Variant
149
+
150
+ For Route Handlers, keep a server-only layer singleton and close it from your
151
+ process lifecycle when the platform exposes one:
152
+
153
+ ```ts
154
+ // app/lib/brass.server.ts
155
+ import "server-only";
156
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
157
+ import { s } from "brass-runtime/schema";
158
+ import { HttpClientService } from "brass-runtime/http";
159
+ import {
160
+ ObservabilityService,
161
+ makeObservabilityLayer,
162
+ makeObservedRuntimeLayer,
163
+ makeObservedHttpClientLayer,
164
+ makeOtlpOptions,
165
+ } from "brass-runtime/observability";
166
+
167
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
168
+ const AppLayer = Layer.composeAll(
169
+ makeConfigLayer(Config, s.object({
170
+ serviceName: s.nonEmptyString(),
171
+ apiBaseUrl: s.url(),
172
+ otlpEndpoint: s.url(),
173
+ }), {
174
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-next",
175
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
176
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
177
+ }),
178
+ makeObservabilityLayer((ctx) => {
179
+ const config = ctx.unsafeGet(Config);
180
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
181
+ }),
182
+ makeObservedRuntimeLayer(),
183
+ makeObservedHttpClientLayer((ctx) => ({
184
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
185
+ preset: "production",
186
+ })),
187
+ );
188
+
189
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
190
+
191
+ export const brass = {
192
+ observability: built.service.unsafeGet(ObservabilityService),
193
+ runtime: built.service.unsafeGet(RuntimeService),
194
+ http: built.service.unsafeGet(HttpClientService),
195
+ shutdown: () => Runtime.make({}).toPromise(built.close()),
196
+ };
197
+ ```
198
+
199
+ ## Runnable Example
200
+
201
+ A minimal runnable app lives at
202
+ [examples/nextjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nextjs).
@@ -137,3 +137,47 @@ export function Profile() {
137
137
  Point `/api/otel` to a server route that owns the collector credentials. The
138
138
  Next.js recipe includes one proxy example; the same idea works in any BFF.
139
139
 
140
+ ## Layer Variant
141
+
142
+ The provider can build a layer graph instead of manually constructing each
143
+ service. In browser apps, use a same-origin OTLP proxy and close the layer from
144
+ the React cleanup:
145
+
146
+ ```tsx
147
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
148
+ import { s } from "brass-runtime/schema";
149
+ import { HttpClientService } from "brass-runtime/http";
150
+ import {
151
+ makeObservabilityLayer,
152
+ makeObservedRuntimeLayer,
153
+ makeObservedHttpClientLayer,
154
+ makeOtlpOptions,
155
+ } from "brass-runtime/observability";
156
+
157
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
158
+ const AppLayer = Layer.composeAll(
159
+ makeConfigLayer(Config, s.object({
160
+ serviceName: s.nonEmptyString(),
161
+ apiBaseUrl: s.string(),
162
+ otlpEndpoint: s.string(),
163
+ }), { serviceName: "shop-react", apiBaseUrl: "/api", otlpEndpoint: "/api/otel" }),
164
+ makeObservabilityLayer((ctx) => {
165
+ const config = ctx.unsafeGet(Config);
166
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
167
+ }),
168
+ makeObservedRuntimeLayer(),
169
+ makeObservedHttpClientLayer((ctx) => ({ baseUrl: ctx.unsafeGet(Config).apiBaseUrl, preset: "balanced" })),
170
+ );
171
+
172
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
173
+ const brass = {
174
+ runtime: built.service.unsafeGet(RuntimeService),
175
+ http: built.service.unsafeGet(HttpClientService),
176
+ shutdown: () => Runtime.make({}).toPromise(built.close()),
177
+ };
178
+ ```
179
+
180
+ ## Runnable Example
181
+
182
+ A minimal runnable app lives at
183
+ [examples/react](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/react).
@@ -222,3 +222,59 @@ process.once("SIGTERM", async () => {
222
222
  server.listen(process.env.PORT ?? 3000);
223
223
  ```
224
224
 
225
+ ## Layer Variant
226
+
227
+ For scripts, CLIs, workers, or small Node servers, a layer graph keeps startup
228
+ and teardown explicit without a framework:
229
+
230
+ ```ts
231
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
232
+ import { s } from "brass-runtime/schema";
233
+ import { HttpClientService } from "brass-runtime/http";
234
+ import {
235
+ ObservabilityService,
236
+ makeObservabilityLayer,
237
+ makeObservedRuntimeLayer,
238
+ makeObservedHttpClientLayer,
239
+ makeOtlpOptions,
240
+ } from "brass-runtime/observability";
241
+
242
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
243
+ const AppLayer = Layer.composeAll(
244
+ makeConfigLayer(Config, s.object({
245
+ serviceName: s.nonEmptyString(),
246
+ apiBaseUrl: s.url(),
247
+ otlpEndpoint: s.url(),
248
+ }), {
249
+ serviceName: "worker",
250
+ apiBaseUrl: "https://users-api.internal",
251
+ otlpEndpoint: "http://grafana-alloy:4318",
252
+ }),
253
+ makeObservabilityLayer((ctx) => {
254
+ const config = ctx.unsafeGet(Config);
255
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
256
+ }),
257
+ makeObservedRuntimeLayer(),
258
+ makeObservedHttpClientLayer((ctx) => ({
259
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
260
+ preset: "production",
261
+ })),
262
+ );
263
+
264
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
265
+ const runtime = built.service.unsafeGet(RuntimeService);
266
+ const http = built.service.unsafeGet(HttpClientService);
267
+ const observability = built.service.unsafeGet(ObservabilityService);
268
+
269
+ try {
270
+ await runtime.toPromise(http.getJson("/users/42"));
271
+ console.log(observability.prometheus.export());
272
+ } finally {
273
+ await Runtime.make({}).toPromise(built.close());
274
+ }
275
+ ```
276
+
277
+ ## Runnable Example
278
+
279
+ A minimal runnable app lives at
280
+ [examples/vanilla](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/vanilla).
@@ -111,6 +111,136 @@ await runtime.toPromise(
111
111
  );
112
112
  ```
113
113
 
114
+ For wider independent graphs, prefer `Layer.all(...)` / `mergeAll(...)` over
115
+ deeply nested `merge(...)` calls:
116
+
117
+ ```ts
118
+ const AppLayer = Layer.all(ConfigLayer, DbLayer, CacheLayer);
119
+ ```
120
+
121
+ For ordered graphs where each context layer reads services produced by previous
122
+ layers, use `Layer.composeAll(...)`:
123
+
124
+ ```ts
125
+ const AppLayer = Layer.composeAll(ConfigLayer, DbLayer, RepoLayer);
126
+ ```
127
+
128
+ ## Accessing Services
129
+
130
+ Use `Layer.use(...)` or `Layer.useAll(...)` when a program should consume
131
+ services without manually calling `ctx.unsafeGet(...)`.
132
+
133
+ ```ts
134
+ import { Layer, asyncSucceed } from "brass-runtime";
135
+
136
+ const Config = Layer.tag<{ readonly baseUrl: string }>("Config");
137
+ const Http = Layer.tag<{ readonly get: (path: string) => string }>("Http");
138
+
139
+ const ConfigLayer = Layer.value(Config, { baseUrl: "https://api.example.com" });
140
+ const HttpLayer = Layer.effect(Http, (ctx) => {
141
+ const config = ctx.unsafeGet(Config);
142
+ return asyncSucceed({
143
+ get: (path) => `${config.baseUrl}${path}`,
144
+ });
145
+ });
146
+
147
+ const AppLayer = Layer.composeAll(ConfigLayer, HttpLayer);
148
+
149
+ await runtime.toPromise(
150
+ Layer.provideContext(
151
+ AppLayer,
152
+ Layer.useAll({ config: Config, http: Http }, ({ config, http }) =>
153
+ asyncSucceed(http.get(`/users?origin=${config.baseUrl}`)),
154
+ ),
155
+ ),
156
+ );
157
+ ```
158
+
159
+ `getService(tag)` and `getServices({ ...tags })` remain available when a program
160
+ is directly evaluated with a `LayerContext` as its environment. `Layer.use(...)`
161
+ and `Layer.useAll(...)` are better fits for `Layer.provideContext(...)`.
162
+
163
+ ## Application Graphs
164
+
165
+ Application modules can model config, observability, and HTTP clients as
166
+ services. That keeps framework code focused on wiring and lets tests swap any
167
+ piece with a smaller layer.
168
+
169
+ ```ts
170
+ import { Runtime, Layer, makeConfigLayer } from "brass-runtime/core";
171
+ import { s } from "brass-runtime/schema";
172
+ import { defineHttpPolicyPresets, HttpClientService } from "brass-runtime/http";
173
+ import {
174
+ makeObservabilityLayer,
175
+ makeObservedRuntimeLayer,
176
+ makeObservedHttpClientLayer,
177
+ makeOtlpOptions,
178
+ } from "brass-runtime/observability";
179
+
180
+ type AppConfig = {
181
+ readonly serviceName: string;
182
+ readonly apiBaseUrl: string;
183
+ readonly otlpEndpoint: string;
184
+ };
185
+
186
+ const AppConfig = Layer.tag<AppConfig>("AppConfig");
187
+ const AppConfigSchema = s.object({
188
+ serviceName: s.nonEmptyString(),
189
+ apiBaseUrl: s.url(),
190
+ otlpEndpoint: s.url(),
191
+ });
192
+
193
+ const policyPresets = defineHttpPolicyPresets({
194
+ readModel: { lane: "read-model", priority: 3, retry: { maxRetries: 2 } },
195
+ command: { lane: "command", priority: 1, retry: false },
196
+ });
197
+
198
+ const ConfigLayer = makeConfigLayer(AppConfig, AppConfigSchema, {
199
+ serviceName: "orders-api",
200
+ apiBaseUrl: "https://users-api.internal",
201
+ otlpEndpoint: "http://grafana-alloy:4318",
202
+ });
203
+
204
+ const ObservabilityLayer = makeObservabilityLayer((ctx) => {
205
+ const config = ctx.unsafeGet(AppConfig);
206
+ return {
207
+ serviceName: config.serviceName,
208
+ otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }),
209
+ flushIntervalMs: 10_000,
210
+ autoStart: true,
211
+ };
212
+ });
213
+
214
+ const HttpLayer = makeObservedHttpClientLayer((ctx) => {
215
+ const config = ctx.unsafeGet(AppConfig);
216
+ return {
217
+ baseUrl: config.apiBaseUrl,
218
+ preset: "production",
219
+ policyPresets,
220
+ };
221
+ }, {
222
+ httpObservability: {
223
+ policy: { labelKeys: ["preset", "lane"] },
224
+ },
225
+ });
226
+
227
+ const AppLayer = Layer.composeAll(
228
+ ConfigLayer,
229
+ ObservabilityLayer,
230
+ makeObservedRuntimeLayer(),
231
+ HttpLayer,
232
+ );
233
+
234
+ const program = Layer.use(HttpClientService, (http) =>
235
+ http.getJson("/users/42", { policy: "readModel" }),
236
+ );
237
+
238
+ const runtime = Runtime.make({});
239
+ const response = await runtime.toPromise(
240
+ Layer.provideContext(AppLayer, program),
241
+ );
242
+ ```
243
+
114
244
  ## Scoped Builds
115
245
 
116
246
  Use `buildLayer` or `Layer.build` when a caller wants manual lifecycle control.
@@ -72,6 +72,35 @@ configs automatically. `.json()` infers Axios/Fetch-shaped responses. Use
72
72
  `.json((res) => res.payload, (res) => ({ status: res.code }))` when the
73
73
  external client uses a different response shape.
74
74
 
75
+ ## Node Proxy Transport
76
+
77
+ ```ts
78
+ import { toPromise } from "brass-runtime";
79
+ import { makeNodeHttpProxyClient } from "brass-runtime/http";
80
+
81
+ const http = makeNodeHttpProxyClient({
82
+ baseUrl: "https://api.example.com",
83
+ nodeTransport: {
84
+ maxSockets: 512,
85
+ maxFreeSockets: 512,
86
+ },
87
+ pool: {
88
+ key: "origin",
89
+ concurrency: 512,
90
+ maxQueue: 512,
91
+ },
92
+ });
93
+
94
+ await http.getJson("/users/1").unsafeRunPromise();
95
+ await toPromise(http.shutdown(), {});
96
+ ```
97
+
98
+ Use this in Node BFF/proxy services when benchmark evidence shows the default
99
+ `fetch` backend is the bottleneck. The factory uses
100
+ `preset: "highThroughputProxy"` and performs the final I/O through
101
+ `node:http` / `node:https` keep-alive agents while keeping Brass cancellation,
102
+ pooling, stats, policy, and observability.
103
+
75
104
  ## Named Policy Presets
76
105
 
77
106
  ```ts
@@ -363,7 +392,8 @@ try {
363
392
  `preset: "production"` is the explicit name for the full default stack:
364
393
  timeout, priority, retry, dedup, adaptive limiter, safe-method response cache,
365
394
  response compression, stats, and shutdown. `preset: "default"` is the same
366
- stack kept for compatibility.
395
+ stack kept for compatibility. `preset: "highThroughputProxy"` is the explicit
396
+ hot BFF/proxy preset, and `preset: "proxy"` is its shorter compatibility alias.
367
397
 
368
398
  Construction-time validation catches invalid setup before traffic starts:
369
399
 
package/docs/http.md CHANGED
@@ -205,6 +205,28 @@ That keeps timeout, pool/adaptive limiter, stats, retry, cache, deduplication
205
205
  and cancellation in Brass while letting the final I/O backend be `fetch`,
206
206
  Axios, undici, a test double, or an internal client.
207
207
 
208
+ For Node BFF/proxy workloads where `fetch`/Undici is the limiting cost, Brass
209
+ also ships a first-party `node:http` / `node:https` transport and a Node-only
210
+ factory for the recommended high-throughput proxy shape:
211
+
212
+ ```ts
213
+ import { toPromise } from "brass-runtime";
214
+ import { makeNodeHttpProxyClient } from "brass-runtime/http";
215
+
216
+ const http = makeNodeHttpProxyClient({
217
+ baseUrl: "https://api.example.com",
218
+ nodeTransport: {
219
+ maxSockets: 512,
220
+ maxFreeSockets: 512,
221
+ },
222
+ });
223
+
224
+ await toPromise(http.shutdown(), {}); // closes owned Node agents
225
+ ```
226
+
227
+ Use this path only in Node services. Browser and edge runtimes should keep the
228
+ default `fetch` transport or inject their platform client.
229
+
208
230
  ```ts
209
231
  import {
210
232
  makeDefaultHttpClient,
@@ -358,7 +380,10 @@ Named presets are available as `conservative`, `balanced`, and `aggressive`.
358
380
  The default HTTP client uses `balanced` for `preset: "balanced"` and
359
381
  `aggressive` for `preset: "default"` / `preset: "production"`.
360
382
  `production` is the explicit name for the full production-ready default stack;
361
- `default` remains as the compatibility name. Use `adaptiveLimiterPresets` or
383
+ `default` remains as the compatibility name. Use
384
+ `preset: "highThroughputProxy"` for high-throughput BFF/proxy paths where
385
+ Brass should not add priority/adaptive queues or timeout timers by default;
386
+ `preset: "proxy"` is the shorter compatibility alias. Use `adaptiveLimiterPresets` or
362
387
  `makeAdaptiveLimiterConfig(preset, overrides)` when you want a documented
363
388
  adaptive limiter baseline with a few local overrides.
364
389
 
@@ -371,6 +396,25 @@ attributes receive `preset`, `lane`, `poolKey`, `dedupKey`, `priority`, and retr
371
396
  overrides automatically, while metric labels stay opt-in through
372
397
  `policy.labelKeys` to avoid accidental high-cardinality metrics.
373
398
 
399
+ For application graphs, the HTTP subpath also exposes a DI layer helper. The
400
+ layer owns the default client lifecycle and calls `shutdown()` when released:
401
+
402
+ ```ts
403
+ import { Layer } from "brass-runtime/core";
404
+ import { HttpClientService, makeDefaultHttpClientLayer } from "brass-runtime/http";
405
+
406
+ const Config = Layer.tag<{ readonly apiBaseUrl: string }>("Config");
407
+
408
+ const HttpLayer = makeDefaultHttpClientLayer((ctx) => ({
409
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
410
+ preset: "production",
411
+ }));
412
+
413
+ const program = Layer.use(HttpClientService, (http) =>
414
+ http.getJson("/users/42"),
415
+ );
416
+ ```
417
+
374
418
  See [`http-recipes.md`](http-recipes.md) for typed API client, testing,
375
419
  observability, retry, adaptive limiter, and config validation recipes.
376
420
 
@@ -490,6 +534,7 @@ adopters' tests:
490
534
  ```ts
491
535
  import {
492
536
  makeJsonHttpResponse,
537
+ makeMockDefaultHttpClientLayer,
493
538
  makeMockHttpClient,
494
539
  runHttpEffect,
495
540
  withMockFetch,
@@ -497,6 +542,10 @@ import {
497
542
 
498
543
  const mock = makeMockHttpClient((req) => makeJsonHttpResponse({ url: req.url }));
499
544
  const wire = await runHttpEffect(mock({ method: "GET", url: "/users/1" }));
545
+
546
+ const MockHttpLayer = makeMockDefaultHttpClientLayer((req) =>
547
+ makeJsonHttpResponse({ url: req.url }),
548
+ );
500
549
  ```
501
550
 
502
551
  ---
@@ -236,6 +236,91 @@ The exporters are dependency-free:
236
236
  - `makeObservability` returns `hooks`, `env`, `metrics`, `tracer`, exporters,
237
237
  plus `flush()`, `start()`, `stop()`, and `shutdown()`.
238
238
 
239
+ ### High-TPS HTTP proxy path
240
+
241
+ For BFF/proxy paths where p99 matters more than full per-request tracing, keep
242
+ HTTP client metrics separate from global runtime hooks. Runtime hooks are useful
243
+ for fiber/span/log visibility, but on a hot proxy path they add work to every
244
+ effect execution.
245
+
246
+ ```ts
247
+ import { Runtime } from "brass-runtime/core";
248
+ import { makeDefaultHttpClient } from "brass-runtime/http";
249
+ import { makeObservability, withHttpObservability } from "brass-runtime/observability";
250
+
251
+ const observability = makeObservability({
252
+ metrics: false,
253
+ logs: false,
254
+ traces: false,
255
+ autoStart: false,
256
+ });
257
+
258
+ // Deliberately no observability hooks on this runtime.
259
+ const runtime = Runtime.make({});
260
+
261
+ const http = makeDefaultHttpClient({
262
+ baseUrl: "https://api.example.com",
263
+ preset: "highThroughputProxy",
264
+ middleware: [
265
+ withHttpObservability({
266
+ metrics: observability.metrics,
267
+ logs: false,
268
+ spans: false,
269
+ adaptiveLimiter: false,
270
+ injectTraceHeaders: false,
271
+ includeHostLabel: false,
272
+ route: "/downstream/:id",
273
+ }),
274
+ ],
275
+ });
276
+
277
+ await runtime.toPromise(http.getJson("/downstream/123"));
278
+ ```
279
+
280
+ Use stable `route` values instead of raw URLs, and enable `includeHostLabel`
281
+ only when the client really talks to multiple downstream hosts. If you need
282
+ traces on the same path, prefer HTTP-only sampled spans through `spanSink`
283
+ instead of global runtime hooks. Use a separate fully observed runtime/client
284
+ for diagnostic flows that need every fiber event.
285
+
286
+ For a sampled span path with lower per-request overhead, keep runtime metrics
287
+ off, avoid runtime hooks, avoid per-request HTTP span events, and only inject
288
+ `traceparent` when a downstream needs propagation:
289
+
290
+ ```ts
291
+ const traced = makeObservability({
292
+ metrics: false,
293
+ logs: false,
294
+ sampling: 0.001,
295
+ autoStart: false,
296
+ });
297
+
298
+ const tracedRuntime = new Runtime({
299
+ env: traced.env,
300
+ });
301
+
302
+ const tracedHttp = makeDefaultHttpClient({
303
+ baseUrl: "https://api.example.com",
304
+ preset: "highThroughputProxy",
305
+ middleware: [
306
+ withHttpObservability({
307
+ metrics: traced.metrics,
308
+ logs: false,
309
+ spans: { events: false, sampleRate: 0.001 },
310
+ spanSink: traced.tracer,
311
+ adaptiveLimiter: false,
312
+ injectTraceHeaders: false,
313
+ includeHostLabel: false,
314
+ route: "/downstream/:id",
315
+ }),
316
+ ],
317
+ });
318
+ ```
319
+
320
+ `sampleRate` lets the HTTP middleware skip tracing before touching the current
321
+ fiber/runtime on unsampled requests. This keeps p99 low on hot proxy paths while
322
+ still retaining a sampled trace stream.
323
+
239
324
  ### HTTP policy observability
240
325
 
241
326
  ```ts
@@ -430,6 +515,53 @@ const http = makeDefaultHttpClient({
430
515
  });
431
516
  ```
432
517
 
518
+ For applications that use Brass layers, observability can own both the
519
+ observability lifecycle and an observed HTTP client:
520
+
521
+ ```ts
522
+ import { Layer } from "brass-runtime/core";
523
+ import { HttpClientService } from "brass-runtime/http";
524
+ import {
525
+ makeObservabilityLayer,
526
+ makeObservedRuntimeLayer,
527
+ makeObservedHttpClientLayer,
528
+ makeOtlpOptions,
529
+ } from "brass-runtime/observability";
530
+
531
+ const Config = Layer.tag<{
532
+ readonly serviceName: string;
533
+ readonly apiBaseUrl: string;
534
+ readonly otlpEndpoint: string;
535
+ }>("Config");
536
+
537
+ const ConfigLayer = Layer.value(Config, {
538
+ serviceName: "orders-api",
539
+ apiBaseUrl: "https://users-api.internal",
540
+ otlpEndpoint: "http://grafana-alloy:4318",
541
+ });
542
+
543
+ const ObservabilityLayer = makeObservabilityLayer((ctx) => {
544
+ const config = ctx.unsafeGet(Config);
545
+ return {
546
+ serviceName: config.serviceName,
547
+ otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }),
548
+ flushIntervalMs: 10_000,
549
+ autoStart: true,
550
+ };
551
+ });
552
+
553
+ const HttpLayer = makeObservedHttpClientLayer((ctx) => ({
554
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
555
+ preset: "production",
556
+ }));
557
+
558
+ const AppLayer = Layer.composeAll(ConfigLayer, ObservabilityLayer, makeObservedRuntimeLayer(), HttpLayer);
559
+
560
+ const program = Layer.use(HttpClientService, (http) =>
561
+ http.getJson("/users/42"),
562
+ );
563
+ ```
564
+
433
565
  Sampling can be configured globally, by ratio, or with rules:
434
566
 
435
567
  ```ts
@@ -128,6 +128,9 @@ HTTP layer profile:
128
128
  - `node-http-text`
129
129
  - `wire-raw`
130
130
  - `default-minimal-json`
131
+ - `default-proxy-json`
132
+ - `default-proxy-node-json`
133
+ - `high-throughput-proxy-node-json`
131
134
  - `default-balanced-no-adaptive-json`
132
135
  - `default-balanced-json`
133
136
  - `default-json`
@@ -141,8 +144,9 @@ HTTP long-run memory lab:
141
144
 
142
145
  - defaults to `forceGc: true`, so use `node --expose-gc` for the strongest
143
146
  retained-memory signal
144
- - compares node transport, wire raw, minimal, balanced without adaptive,
145
- balanced, default, and default+observability variants
147
+ - compares node transport, wire raw, minimal, proxy, proxy+node transport,
148
+ high-throughput proxy+node transport, balanced without adaptive, balanced,
149
+ default, and default+observability variants
146
150
  - reports heap/rss totals, max p99, mean throughput, errors, and
147
151
  `heapDeltaPer10kRequestsMb`
148
152
  - highlights whether memory is `ok`, `watch`, `critical`, or `unknown-gc`
@@ -32,8 +32,9 @@ const RepoLayer = Layer.effect(Repo, (ctx: LayerContext) => {
32
32
  const AppLayer = Layer.compose(ConfigLayer, RepoLayer);
33
33
 
34
34
  const userUrl = await runPromise(
35
- provideContext(AppLayer, (ctx) =>
36
- asyncSucceed(ctx.unsafeGet(Repo).findUser("u1")),
35
+ provideContext(
36
+ AppLayer,
37
+ Layer.use(Repo, (repo) => asyncSucceed(repo.findUser("u1"))),
37
38
  ),
38
39
  );
39
40
 
@@ -42,3 +43,46 @@ console.log(userUrl);
42
43
 
43
44
  Missing services throw `MissingLayerServiceError`; use `formatLayerError` when
44
45
  surfacing the message.
46
+
47
+ For independent layers, `Layer.all(...)` keeps composition readable. For
48
+ ordered context graphs where later layers read earlier services, use
49
+ `Layer.composeAll(...)`. `Layer.useAll(...)` reads multiple services without
50
+ manual context access:
51
+
52
+ ```ts
53
+ const Logger = defineService<{ readonly info: (message: string) => void }>("Logger");
54
+ const LoggerLayer = Layer.value(Logger, console);
55
+
56
+ const AppLayer2 = Layer.composeAll(ConfigLayer, RepoLayer, LoggerLayer);
57
+
58
+ await runPromise(
59
+ provideContext(
60
+ AppLayer2,
61
+ Layer.useAll({ repo: Repo, logger: Logger }, ({ repo, logger }) => {
62
+ logger.info("loading user");
63
+ return asyncSucceed(repo.findUser("u1"));
64
+ }),
65
+ ),
66
+ );
67
+ ```
68
+
69
+ For app wiring, prefer the focused helpers:
70
+
71
+ ```ts
72
+ import { RuntimeService, makeConfigLayer, makeRuntimeLayer, makeTestLayer } from "brass-runtime";
73
+ import { s } from "brass-runtime/schema";
74
+
75
+ const ConfigSchema = s.object({ baseUrl: s.url() });
76
+
77
+ const ConfigLayer2 = makeConfigLayer(Config, ConfigSchema, {
78
+ baseUrl: "https://api.example.com",
79
+ });
80
+
81
+ const RuntimeLayer = makeRuntimeLayer((ctx) => ({
82
+ config: ctx.unsafeGet(Config),
83
+ }));
84
+
85
+ const TestConfigLayer = makeTestLayer(Config, {
86
+ baseUrl: "https://test.example.com",
87
+ });
88
+ ```
@@ -33,6 +33,31 @@ const response = await runHttpEffect(client({ method: "GET", url: "/health" }));
33
33
  console.log(response.status);
34
34
  ```
35
35
 
36
+ Layer-based applications can swap services with `makeTestLayer`,
37
+ `makeTestLayers`, or a mock default HTTP client layer:
38
+
39
+ ```ts
40
+ import { Layer, makeTestLayer } from "brass-runtime/core";
41
+ import { HttpClientService } from "brass-runtime/http";
42
+ import {
43
+ makeJsonHttpResponse,
44
+ makeMockDefaultHttpClientLayer,
45
+ } from "brass-runtime/http/testing";
46
+
47
+ const Config = Layer.tag<{ readonly baseUrl: string }>("Config");
48
+
49
+ const TestLayer = Layer.composeAll(
50
+ makeTestLayer(Config, { baseUrl: "test://users" }),
51
+ makeMockDefaultHttpClientLayer((req) =>
52
+ makeJsonHttpResponse({ url: req.url }),
53
+ ),
54
+ );
55
+
56
+ const program = Layer.use(HttpClientService, (http) =>
57
+ http.getJson("/users/1"),
58
+ );
59
+ ```
60
+
36
61
  For typed failures, assert on `Exit` instead of catching thrown values.
37
62
 
38
63
  ```ts