brass-runtime 1.17.0 → 1.18.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 (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-7X3K5RMS.js → chunk-22HZQG5F.js} +9 -11
  10. package/dist/{chunk-GLE2WY7Z.cjs → chunk-2JHJ4YHS.cjs} +417 -124
  11. package/dist/{chunk-Q2I37RP3.cjs → chunk-2OW6IFY2.cjs} +44 -323
  12. package/dist/{chunk-7ZPEZ57L.cjs → chunk-5LC7V2OZ.cjs} +18 -20
  13. package/dist/{chunk-AGR5B2BC.cjs → chunk-5RZ7YITF.cjs} +564 -12
  14. package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
  15. package/dist/{chunk-EJ6BPYVR.mjs → chunk-6V2AWT4R.mjs} +1 -1
  16. package/dist/{chunk-3AYM6WPJ.js → chunk-7DU7IQHK.js} +20 -299
  17. package/dist/{chunk-SK7UZRNI.mjs → chunk-7GBJYOX7.mjs} +528 -23
  18. package/dist/chunk-7TKI527D.cjs +123 -0
  19. package/dist/{chunk-52OB2ROS.js → chunk-7VQLEN37.js} +2 -4
  20. package/dist/{chunk-KH4SYAOS.mjs → chunk-B5FKOLTB.mjs} +20 -299
  21. package/dist/{chunk-FHQGHPMO.mjs → chunk-BC6Q6BCO.mjs} +2 -4
  22. package/dist/{chunk-4P2HHGAX.mjs → chunk-COOW7BJX.mjs} +32 -11
  23. package/dist/{chunk-2HQTDLHF.mjs → chunk-EEN5OTCR.mjs} +555 -3
  24. package/dist/{chunk-KZJQ723N.cjs → chunk-EICAJDNX.cjs} +13 -15
  25. package/dist/chunk-ELIECDYN.cjs +33 -0
  26. package/dist/{chunk-GYM3LLGS.mjs → chunk-H626ZTDZ.mjs} +399 -106
  27. package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
  28. package/dist/{chunk-7JIJOVCT.js → chunk-IPSMXUWA.js} +2 -4
  29. package/dist/{chunk-4ROBZFL6.cjs → chunk-J6DUHITE.cjs} +6 -8
  30. package/dist/{chunk-6RY2FFN4.mjs → chunk-JWIEMBE6.mjs} +9 -11
  31. package/dist/{chunk-PD4EJTQC.cjs → chunk-KNTJ7FQB.cjs} +5 -5
  32. package/dist/chunk-KTGDLBLD.mjs +123 -0
  33. package/dist/chunk-LSYQ3C2M.js +33 -0
  34. package/dist/{chunk-RKGKFN2A.js → chunk-OW5VHAOE.js} +1 -1
  35. package/dist/{chunk-EOC4UHBS.mjs → chunk-RBHNOKH4.mjs} +2 -2
  36. package/dist/{chunk-6IXXWIUM.js → chunk-S4HXADU4.js} +555 -3
  37. package/dist/{chunk-FH2X7BVP.js → chunk-TTSPIU3U.js} +399 -106
  38. package/dist/{chunk-5QC7LRZ3.js → chunk-UAKAF32U.js} +2 -2
  39. package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
  40. package/dist/{chunk-MBEJI5HF.mjs → chunk-WCBNXPN6.mjs} +2 -4
  41. package/dist/{chunk-52PPNNI4.cjs → chunk-WGE2FEZE.cjs} +2 -2
  42. package/dist/{chunk-WBGRHGBP.cjs → chunk-WI7GZF3B.cjs} +114 -93
  43. package/dist/chunk-WUDHOZIH.js +6234 -0
  44. package/dist/{chunk-F6XWZQY4.cjs → chunk-WVSZOPGQ.cjs} +583 -78
  45. package/dist/chunk-XPIMJQYS.cjs +6234 -0
  46. package/dist/{chunk-VWIPB6I5.js → chunk-YGR2IN4R.js} +528 -23
  47. package/dist/chunk-YM3EDNYD.js +123 -0
  48. package/dist/chunk-YWLLH27R.mjs +33 -0
  49. package/dist/{chunk-BKK77SBA.js → chunk-YZ5LQ32F.js} +32 -11
  50. package/dist/chunk-Z3ZZMQUZ.mjs +6234 -0
  51. package/dist/core/index.cjs +37 -9
  52. package/dist/core/index.d.ts +19 -152
  53. package/dist/core/index.js +86 -58
  54. package/dist/core/index.mjs +86 -58
  55. package/dist/defaultClient-Cid0JoUR.d.ts +1648 -0
  56. package/dist/{effect-DIUHZ9IN.d.ts → effect-DnGUuhw6.d.ts} +22 -1
  57. package/dist/http/index.cjs +206 -59
  58. package/dist/http/index.d.ts +55 -819
  59. package/dist/http/index.js +220 -73
  60. package/dist/http/index.mjs +220 -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 +116 -88
  66. package/dist/index.d.ts +9 -8
  67. package/dist/index.js +87 -59
  68. package/dist/index.mjs +87 -59
  69. package/dist/{schedule-CK3Ml_7p.d.ts → layer-D2LFcBVx.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-Bf1zNYZk.d.ts} +5 -5
  84. package/dist/{stream-B4oK9JFP.d.ts → stream-I7bkvF7a.d.ts} +1 -1
  85. package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DF83nLn6.d.ts} +2 -2
  86. package/dist/{tracing-DqbTKGcf.d.ts → tracing-CWV4gT0u.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 +6 -2
  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
@@ -122,3 +122,52 @@ process.once("SIGTERM", async () => {
122
122
 
123
123
  Runnable repo example: `src/examples/observabilityFastify.ts`.
124
124
 
125
+ ## Layer Variant
126
+
127
+ Fastify can use the same app graph shape as Express. Build it once during
128
+ startup, read services from the produced `LayerContext`, and close the layer in
129
+ `onClose`/shutdown:
130
+
131
+ ```ts
132
+ import { Layer, Runtime, RuntimeService, makeConfigLayer } from "brass-runtime/core";
133
+ import { s } from "brass-runtime/schema";
134
+ import { HttpClientService } from "brass-runtime/http";
135
+ import {
136
+ ObservabilityService,
137
+ makeObservabilityLayer,
138
+ makeObservedRuntimeLayer,
139
+ makeObservedHttpClientLayer,
140
+ makeOtlpOptions,
141
+ } from "brass-runtime/observability";
142
+
143
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
144
+
145
+ const AppLayer = Layer.composeAll(
146
+ makeConfigLayer(Config, s.object({
147
+ serviceName: s.nonEmptyString(),
148
+ apiBaseUrl: s.url(),
149
+ otlpEndpoint: s.url(),
150
+ }), {
151
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "shop-fastify",
152
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
153
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
154
+ }),
155
+ makeObservabilityLayer((ctx) => {
156
+ const config = ctx.unsafeGet(Config);
157
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
158
+ }),
159
+ makeObservedRuntimeLayer(),
160
+ makeObservedHttpClientLayer((ctx) => ({
161
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
162
+ preset: "production",
163
+ })),
164
+ );
165
+
166
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
167
+ app.decorate("brass", {
168
+ observability: built.service.unsafeGet(ObservabilityService),
169
+ runtime: built.service.unsafeGet(RuntimeService),
170
+ http: built.service.unsafeGet(HttpClientService),
171
+ });
172
+ app.addHook("onClose", () => Runtime.make({}).toPromise(built.close()));
173
+ ```
@@ -270,6 +270,55 @@ npm install --save-dev @nestjs/core @nestjs/common @nestjs/platform-express refl
270
270
  npm run example:observability:nest
271
271
  ```
272
272
 
273
+ ## Layer Variant
274
+
275
+ Nest providers can also be backed by one Brass layer graph. This keeps
276
+ validation, runtime hooks, HTTP observability, and shutdown in one place:
277
+
278
+ ```ts
279
+ import { Runtime, Layer, RuntimeService, makeConfigLayer } from "brass-runtime/core";
280
+ import { s } from "brass-runtime/schema";
281
+ import { HttpClientService } from "brass-runtime/http";
282
+ import {
283
+ ObservabilityService,
284
+ makeObservabilityLayer,
285
+ makeObservedRuntimeLayer,
286
+ makeObservedHttpClientLayer,
287
+ makeOtlpOptions,
288
+ } from "brass-runtime/observability";
289
+
290
+ const Config = Layer.tag<{ serviceName: string; apiBaseUrl: string; otlpEndpoint: string }>("Config");
291
+ const AppLayer = Layer.composeAll(
292
+ makeConfigLayer(Config, s.object({
293
+ serviceName: s.nonEmptyString(),
294
+ apiBaseUrl: s.url(),
295
+ otlpEndpoint: s.url(),
296
+ }), {
297
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "orders-api",
298
+ apiBaseUrl: process.env.USERS_API_BASE_URL ?? "https://users-api.internal",
299
+ otlpEndpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
300
+ }),
301
+ makeObservabilityLayer((ctx) => {
302
+ const config = ctx.unsafeGet(Config);
303
+ return { serviceName: config.serviceName, otlp: makeOtlpOptions({ endpoint: config.otlpEndpoint }) };
304
+ }),
305
+ makeObservedRuntimeLayer(),
306
+ makeObservedHttpClientLayer((ctx) => ({
307
+ baseUrl: ctx.unsafeGet(Config).apiBaseUrl,
308
+ preset: "production",
309
+ })),
310
+ );
311
+
312
+ const built = await Runtime.make({}).toPromise(Layer.build(AppLayer));
313
+
314
+ export const brassProviders = [
315
+ { provide: BRASS_OBSERVABILITY, useValue: built.service.unsafeGet(ObservabilityService) },
316
+ { provide: BRASS_RUNTIME, useValue: built.service.unsafeGet(RuntimeService) },
317
+ { provide: BRASS_HTTP, useValue: built.service.unsafeGet(HttpClientService) },
318
+ { provide: BRASS_LAYER_CLOSE, useValue: () => Runtime.make({}).toPromise(built.close()) },
319
+ ];
320
+ ```
321
+
273
322
  It uses a fake OTLP `fetch` by default so it can run without a collector. To
274
323
  send to Grafana/Alloy instead:
275
324
 
@@ -280,3 +329,7 @@ GRAFANA_OTLP_AUTHORIZATION='Basic <token>' \
280
329
  npm run example:observability:nest
281
330
  ```
282
331
 
332
+ ## Runnable Example
333
+
334
+ A minimal runnable app lives at
335
+ [examples/nestjs](https://github.com/BaldrVivaldelli/brass-runtime/tree/main/examples/nestjs).
@@ -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