brass-runtime
v1.21.0
Published
Effect runtime utilities for TypeScript
Downloads
3,045
Maintainers
Readme
brass-runtime
A ZIO-inspired effect runtime for TypeScript with structured concurrency, runtime diagnostics, pull-based streams, and a production-grade HTTP client.
Built without Promise/async/await as the primary semantic primitive. Effects are values — lazy, composable, and cancelable by default.
npm i brass-runtimeRunnable framework examples live in the repository examples. They are kept out of the npm package so installs stay small.
What it does
Runtime — algebraic effects, fibers, scopes, scheduler, interruptibility
regions, fiber-local refs, typed layers, semaphores, circuit breakers, rich
Cause<E> failures, metrics, tracing, and an opt-in flight recorder.
Streams — pull-based streams with backpressure, bounded buffers, hubs, pipelines, fusion optimization, and a small fluent DX facade.
HTTP — lazy/cancelable client and server primitives with typed routing, schema validation, health/readiness probes, adaptive concurrency, compression, batching, prewarm, cache, dedup, priority, retry, and observability.
Production signals — dependency-free schemas, Prometheus/OTLP exporters, structured logs, W3C trace propagation, sampling, redaction, bounded exporters, and explicit flush/shutdown.
Performance profiler — runtime primitives, HTTP layer comparison, memory retention reports, observability overhead, CLI/JSON output, and actionable recommendations.
Optional engine and tools — Rust/WASM-backed state machines plus the experimental Brass Agent CLI/VS Code workflow.
Philosophy
- Effects are values — lazy, composable, referentially transparent
- Async is explicit — no hidden Promise semantics
- Concurrency is structured — fibers, scopes, finalizers
- Side effects are interpreted — not executed eagerly
- Higher-level APIs are libraries — HTTP, streams, agent are built on top of core
Quick start
Run an effect
import { runPromise, succeed } from "brass-runtime";
const value = await runPromise(succeed(42));Use makeRuntime when you want explicit runtime options, and runExit when
you want the full Exit/Cause instead of a rejected promise.
Inspect failure causes
import { Cause, formatCause } from "brass-runtime";
const cause = Cause.then(
Cause.fail("database unavailable"),
Cause.both(Cause.interrupt(), Cause.die(new Error("release failed"))),
);
console.log(formatCause(cause));Cause<E> preserves typed failures, defects, interruptions, and sequential or
parallel composition (Then / Both) so diagnostics can explain what happened
without flattening every failure into a single thrown value.
Mask interruption
import { async, flatMap, succeed, uninterruptibleMask } from "brass-runtime";
const effect = uninterruptibleMask((restore) =>
flatMap(succeed("acquired"), (resource) =>
restore(async((_env, cb) => {
setTimeout(() => cb({ _tag: "Success", value: `used:${resource}` }), 10);
})),
),
);Use uninterruptible(effect) for critical regions and
uninterruptibleMask((restore) => ...) when only part of the region should be
interruptible again. Pending interruption is deferred until the protected region
exits; restored sub-effects remain cancelable.
Fiber-local context
import { Runtime, makeFiberRef } from "brass-runtime";
const requestId = makeFiberRef("anonymous");
const runtime = Runtime.make({});
const result = await runtime.toPromise(
requestId.locally("req-123", requestId.get()),
);FiberRef values are local to the running fiber, inherited by child fibers at
fork time, isolated from child mutations, and restored after locally regions
even when the region fails or is interrupted.
Explain runtime behavior
import { Runtime, async, makeRuntimeRecorder } from "brass-runtime";
const recorder = makeRuntimeRecorder({ maxEvents: 5000 });
const runtime = new Runtime({ env: {}, hooks: recorder.hooks });
await runtime.toPromise(async((_env, cb) => {
setTimeout(() => cb({ _tag: "Success", value: "ok" }), 10);
}));
console.log(recorder.explain());The flight recorder is opt-in and keeps a bounded ring buffer of runtime events: fiber start/end/suspend/resume, scopes, supervisor events, logs, spans, and trace context when available.
Recommended HTTP client
import { makeDefaultHttpClient, s } from "brass-runtime/http";
const User = s.object({
id: s.number({ int: true }),
name: s.string({ minLength: 1 }),
role: s.enum(["admin", "user"] as const).optional(),
});
const http = makeDefaultHttpClient({
baseUrl: "https://api.example.com",
headers: { accept: "application/json" },
});
const user = await http.getJson("/users/1", { schema: User }).unsafeRunPromise();
console.log(user.body.name);
console.log(http.stats());
console.log(http.compression?.stats());makeDefaultHttpClient is the batteries-included entrypoint: timeout,
deduplication, priority scheduling, retry, adaptive concurrency, safe-method
response cache, decompression, stats, cancelAll, and JSON/text helpers. Use
preset: "production" when you want that production-ready shape explicitly,
preset: "balanced" to skip the default cache, preset: "highThroughputProxy"
for hot BFF/proxy paths without lifecycle queues or Brass timers by default,
preset: "proxy" as the shorter compatibility alias, or preset: "minimal"
for a cheap wire client with the same helper API. preset: "default" remains
the same full preset for compatibility.
On Node BFF/proxy services, pair the proxy preset with the first-party
node:http transport when the default fetch backend is the bottleneck.
The Node-only factory below wires that recommended shape directly:
import { toPromise } from "brass-runtime";
import { makeNodeHttpProxyClient } from "brass-runtime/http";
const http = makeNodeHttpProxyClient({
baseUrl: "https://api.example.com",
nodeTransport: {
maxSockets: 512,
maxFreeSockets: 512,
},
});
await toPromise(http.shutdown(), {}); // also destroys owned Node agentsThe HTTP stack is meant to replace the usual fetch wrapper plus Zod/Valibot
glue: schemas are dependency-free, responses and request bodies are validated in
the same effect, config validation fails at construction time, and the client
still owns cancellation, retries, compression, observability, and adaptive
limits as one pipeline.
Custom Promise clients such as Axios can be injected without making the
consumer manage AbortSignal or Async plumbing:
import {
defineHttpPolicyPresets,
formatHttpError,
isRetryableHttpError,
makeDefaultHttpClient,
promiseHttpTransport,
} from "brass-runtime/http";
const transport = promiseHttpTransport()
.requestConfig(({ request, url }) => ({
url: url.toString(),
method: request.method,
headers: request.headers,
data: request.body,
responseType: "json",
}))
.send((config) => axiosInstance.request(config))
.json();
const axiosHttp = makeDefaultHttpClient({
baseUrl: "https://api.example.com",
transport,
});
try {
await axiosHttp.getJson("/users/1").unsafeRunPromise();
} catch (error) {
if (isRetryableHttpError(error)) {
console.warn("transient upstream failure");
}
console.error(formatHttpError(error));
}Brass injects the runtime AbortSignal into object configs before send and
normalizes external failures with toHttpError, including Axios-like
response.status, aborts, and common timeout codes.
Repeated execution intent can be named once with policy presets:
const policies = defineHttpPolicyPresets({
readModel: {
lane: "read-model",
poolKey: "users-api",
priority: 2,
retry: { maxRetries: 2, baseDelayMs: 50 },
},
});
const http = makeDefaultHttpClient({
baseUrl: "https://api.example.com",
policyPresets: policies,
});
await http.getJson("/users/1", {
policy: { preset: "readModel", dedupKey: "users:1" },
}).unsafeRunPromise();The default adaptive limiter uses the aggressive preset: warmup sample floor,
P5 baseline, error-rate signal, priority-aware queueing, jittered probes,
proportional headroom, capped decreases, and TTL-evicted per-key state.
Call shutdown() for explicit cleanup.
The same schema DSL is available outside HTTP:
import { Schema } from "brass-runtime/schema";
const Config = Schema.object({
port: Schema.int({ min: 1 }),
callbackUrl: Schema.url(),
});
const config = Config.parse({ port: 3000, callbackUrl: "https://example.com/cb" });Schemas can validate request bodies before the HTTP request is sent:
import { Schema } from "brass-runtime/schema";
const CreateUser = Schema.object({
name: Schema.string({ minLength: 1 }),
});
await http.postJson(
"/users",
{ name: "Ada" },
{ bodySchema: CreateUser, schema: User }
).unsafeRunPromise();The same validation machinery checks runtime, HTTP, and observability configs
at construction time, so invalid values fail with field paths like
$.otlp.pipeline.batchSize instead of surfacing later as ambiguous behavior.
HTTP server
import { asyncSucceed, asyncSync, runPromise, useResource } from "brass-runtime";
import { HttpServer, s } from "brass-runtime/http";
const User = s.object({
id: s.nonEmptyString(),
name: s.nonEmptyString(),
});
const router = HttpServer.router([
HttpServer.route("GET", "/users/:id", {
params: s.object({ id: s.nonEmptyString() }),
response: User,
}, (ctx) =>
asyncSucceed(HttpServer.json({
id: ctx.params.id,
name: "Ada",
})),
),
HttpServer.healthRoute(),
HttpServer.readinessRoute(),
]);
await runPromise(
useResource(
router.listen({ port: 3000 }),
(server) => asyncSync(() => {
console.log(server.url());
}),
),
);Routes infer :params from the path, optional schemas validate
params/query/body/response, middleware is effect-based, and the Node listener is
available as a managed resource for graceful shutdown.
Discoverable HTTP builder
import { httpClientBuilder } from "brass-runtime/http";
declare const token: string;
const http = httpClientBuilder()
.baseUrl("https://api.example.com")
.production()
.balancedLimiter({ maxLimit: 128 })
.header("authorization", `Bearer ${token}`)
.cache({ ttlSeconds: 30, maxEntries: 512 })
.retry({ maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 })
.build();HTTP test helpers
import {
makeJsonHttpResponse,
makeMockHttpClient,
runHttpEffect,
} from "brass-runtime/http/testing";
const mock = makeMockHttpClient((req) => makeJsonHttpResponse({ url: req.url }));
const response = await runHttpEffect(mock({ method: "GET", url: "/users/1" }));Domain-specific layers still fit in the same config:
const http = makeDefaultHttpClient({
baseUrl: "https://api.example.com",
batch: {
windowMs: 50,
maxBatchSize: 20,
batchKey: (req) => req.url.startsWith("/graphql") ? "graphql" : "",
batch: {
coalesce: (reqs) => ({ method: "POST", url: "/graphql/batch", body: JSON.stringify(reqs) }),
split: (res, reqs) => JSON.parse(res.bodyText),
},
},
});
const response = await http.getJson<User>("/users/1").unsafeRunPromise();Adaptive concurrency
import { makeAdaptiveLimiterConfig, makeHttp } from "brass-runtime/http";
const http = makeHttp({
adaptiveLimiter: makeAdaptiveLimiterConfig("balanced", {
maxLimit: 100,
stateTtlMs: 300_000,
warmupRequests: 20,
minSamples: 25,
decreaseCooldownSamples: 3,
decreaseThreshold: 0.6,
maxDecreaseRatio: 0.15,
historySize: 64,
onLimitChange: (event) => console.log(`limit: ${event.previousLimit} → ${event.newLimit}`),
}),
});
console.log(http.adaptiveLimiter?.dump());
console.log(http.adaptiveLimiter?.history("https://api.example.com"));
http.shutdown?.();More end-to-end examples live in docs/http-recipes.md.
Connection pre-warming
import { makePrewarmManager } from "brass-runtime/http";
const prewarm = makePrewarmManager({
origins: ["https://api.example.com", "https://cdn.example.com"],
keepAliveDurationMs: 55000,
autoRefresh: true,
});
await prewarm.warmAll();
// Subsequent requests skip TCP+TLS handshakeResponse compression
import { makeCompressionMiddleware, makeDefaultHttpClient } from "brass-runtime/http";
const { middleware, stats } = makeCompressionMiddleware({ encodings: ["br", "gzip"] });
const baseClient = makeDefaultHttpClient({
baseUrl: "https://api.example.com",
compression: false,
});
const client = baseClient.with(middleware);
// Responses are transparently decompressed (gzip, brotli, deflate)Production observability
import { Runtime, asyncSucceed } from "brass-runtime/core";
import {
makeObservability,
makeOtlpOptions,
runObservedHttpServerEffect,
withHttpObservability,
} from "brass-runtime/observability";
import { makeDefaultHttpClient } from "brass-runtime/http";
const obs = makeObservability({
serviceName: "api",
logs: { minLevel: "info" },
sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
redaction: {},
cardinality: { maxValuesPerLabel: 100 },
otlp: makeOtlpOptions({ endpoint: "http://collector:4318" }),
flushIntervalMs: 10_000,
});
const runtime = new Runtime({ env: obs.env, hooks: obs.hooks });
const client = makeDefaultHttpClient({
baseUrl: "https://api.example.com",
middleware: [withHttpObservability(obs)],
});
await runObservedHttpServerEffect(
obs,
{ method: "GET", route: "/users/:id" },
asyncSucceed("ok")
);
await runtime.toPromise(client.getText("/health"));
await obs.shutdown();HTTP client observability automatically reads adaptive limiter diagnostics when
the wrapped client owns a limiter, exposing gauges for current limit, queue
depth, utilization, error rate, request/completion rate, rejection rate, and
state count.
It also reads req.policy automatically: logs and span attributes include
preset, lane, poolKey, dedupKey, priority, and retry overrides when present.
Metric labels stay conservative by default; opt into stable labels with
withHttpObservability({ policy: { labelKeys: ["preset", "lane", "poolKey"] } }).
For hot proxy paths, keep HTTP metrics separate from runtime hooks:
makeObservability({ metrics: false, logs: false, traces: false }),
preset: "proxy", withHttpObservability({ spans: false, logs: false,
injectTraceHeaders: false, includeHostLabel: false }). For sampled spans on
the same path, avoid global runtime hooks and use
withHttpObservability({ spans: { events: false, sampleRate: 0.001 },
spanSink: observability.tracer, injectTraceHeaders: false }).
Performance profiler
npm run perf
npm run perf:json
npm run benchmark:perf
npm run perf:runtime:ab
npm run perf:runtime:soak
npm run perf:runtime:budget
npm run perf:http:memory
npm run perf:historyimport { runBrassPerformanceProfile } from "brass-runtime/perf";
const report = await runBrassPerformanceProfile({
http: {
calls: 20_000,
concurrency: 512,
delayMs: 2,
forceGc: true,
variants: ["default-json", "default-json-observed"],
},
});
console.log(report.recommendations);The profiler compares runtime primitives, runtime A/B variants, runtime-only
soak behavior, node:http, Brass wire/default HTTP clients, HTTP long-run
memory, observability overhead, history/baseline regressions, and memory deltas. Use
docs/performance-profiler.md for focused
commands and node --expose-gc runs.
Perf runs can be persisted and compared locally:
npm run perf -- --profile runtime-ab --record-history --save-baseline runtime-main
npm run perf -- --profile runtime-ab --compare-baseline runtime-main --fail-on-baseline-regressionFirst-release recipes
Copyable happy paths live in docs/recipes:
- runtime execution
- typed layers
- HTTP server
- testing
- performance baselines
Structured concurrency
import { Runtime, asyncSucceed, withScope } from "brass-runtime";
const runtime = Runtime.make({});
await runtime.toPromise(
withScope(runtime, (scope) => {
scope.fork(asyncSucceed("child"));
// scope close interrupts children + runs finalizers
})
);Streams
import { Runtime, Stream } from "brass-runtime";
const runtime = Runtime.make({});
const result = await Stream
.from([1, 2, 3, 4, 5])
.map((n) => n * 2)
.collect(runtime);
// [2, 4, 6, 8, 10]Package exports
| Import | Purpose |
|--------|---------|
| brass-runtime | Core runtime: effects, fibers, scheduler, streams, layers |
| brass-runtime/core | Stable core surface (preferred for new code) |
| brass-runtime/http | Default HTTP client factory, lifecycle middleware, compression, batching, prewarm, adaptive limiter |
| brass-runtime/http/testing | Dependency-free mock clients, mock fetch, response factories, and effect runner helpers |
| brass-runtime/schema | Dependency-free runtime schema DSL with type inference |
| brass-runtime/observability | Prometheus/OTLP exporters, logs, spans, trace propagation, request adapters |
| brass-runtime/perf | Runtime, HTTP, observability, memory, and baseline performance profiler |
| brass-runtime/agent | Brass Agent core (experimental) |
CLI: brass-agent
HTTP middleware pipeline
The lifecycle client composes middleware in this order (innermost to outermost):
Wire → Priority → Retry → Cache → Batch → DedupAdaptive limiting lives in the wire client, before lifecycle middleware.
makeDefaultHttpClient can then wrap the lifecycle stack with response
compression and caller middleware; caller middleware is outermost.
Each layer is independently optional. Set to false or omit to disable.
| Layer | What it does | |-------|-------------| | Adaptive Limiter | Gradient-based dynamic concurrency control per origin | | Priority | Priority queue for request scheduling (0-9 levels) | | Retry | Exponential backoff with circuit breaker awareness | | Cache | LRU + TTL + stale-while-revalidate | | Batch | Time-window request coalescing with split/distribute | | Dedup | Ref-counted in-flight request deduplication | | Compression | Transparent gzip/br/deflate decompression | | Prewarm | Proactive TCP+TLS connection establishment |
All layers emit lifecycle events, track stats, and support cancellation.
The recommended makeDefaultHttpClient factory wires the default preset
for you and accepts extra middleware, so observability can be attached with
middleware: [withHttpObservability(obs)] without coupling HTTP to exporters.
Per-request policy travels through that stack and is visible to observability
without being forwarded to the host transport.
WASM engine
Optional Rust/WASM-backed components for strict execution:
- Fiber engine state machine
- Scheduler queue
- Bounded queues
- Permit pool
- Retry planner
npm run build:wasm # requires wasm-packThe WASM engine never silently falls back to TypeScript — if you request WASM and it's unavailable, it fails explicitly.
Brass Agent (experimental)
A CLI-first coding agent built on the runtime. Inspects workspaces, discovers validation commands, gathers bounded context, asks an LLM for patches, and applies/rolls back changes under policy.
npm run agent:vscode:install # VS Code extension
brass-agent --doctor # check setup
brass-agent --init # initialize workspace
brass-agent --preset inspect # run inspectionDocs: Install · CLI · Project intelligence · VS Code
Testing
npm test # vitest suite
npm run test:types # TypeScript type checking
npm run test:coverage # coverage with baseline gate
npm run release:check # full release gate: types, tests, build, CJS, perf budgets
npm run benchmark # runtime, HTTP lifecycle, and 100k local HTTP concurrency
npm run benchmark:runtime # Runtime Performance Track
npm run benchmark:runtime:budget # Runtime Performance Track regression budget
npm run perf:runtime:ab # Runtime A/B Performance Lab
npm run perf:runtime:soak # Runtime-only soak profile
npm run perf:runtime:budget # Runtime profiler budget
npm run perf:http:memory # HTTP long-run memory lab
npm run benchmark -- http-concurrent # HTTP compare mode variants
node --expose-gc --import tsx src/benchmarks/runner.ts http-concurrent # HTTP memory/limiter diagnostics
npm run benchmark:adaptive
npm run benchmark:adaptive:soak
npm run benchmark:http:budget
npm run benchmark:http:soak
npm run benchmark:observability
npm run benchmark:observability:budget
npm run smoke:observability:collector # requires local OTEL collectorProperty-based tests use fast-check with 100+ iterations per property. Each HTTP middleware has dedicated property tests verifying correctness invariants.
Docs
- Getting Started
- Architecture
- Cancellation & Interruption
- Observability: Hooks & Tracing
- Observability framework examples
- Framework integrations
- NestJS integration
- Observability collector smoke
- HTTP module
- Production readiness
- Streams guide
- Testing guide
- WASM engine
Features
Runtime (core)
- [x] Sync effect values via
ZIO<R, E, A>aliases - [x] Algebraic async:
Async<R, E, A> - [x] Rich
Cause<E>failure trees with pretty printing - [x] Interruptibility regions with
uninterruptible/uninterruptibleMask - [x] Fiber-local refs with fork inheritance and scoped restoration
- [x] TS TestRuntime with deterministic scheduler and virtual clock
- [x] Schedule 2.0 with drivers, runtime-clock budgets, observability, and HTTP integration
- [x] Cooperative scheduler (observable, testable)
- [x] Fibers with interruption & finalizers
- [x] Structured scopes & resource safety
- [x] Runtime flight recorder for bounded execution traces
- [x] Layer 2.0 typed contexts, semaphores, circuit breakers
- [x] Metrics, tracing, runtime hooks
- [x] Worker pools
- [x] WASM engine (optional)
Streams
- [x] Pull-based streams with backpressure
- [x] Fluent
Stream/PipelineDX facade - [x] Bounded buffers, queues, hubs
- [x] Pipelines with fusion optimization
- [x] Stream merge, zip, broadcast
- [x] Chunks & batch operators
HTTP
- [x] Lazy, cancelable HTTP client
- [x] Effect-based Node HTTP server listener with resource lifecycle
- [x] Declarative typed routing with params/query/body/response schemas
- [x] Health/readiness routes backed by runtime health reports
- [x] Schema-validated JSON helpers
- [x] Discoverable builder API
- [x] Test helper subpath
- [x] Lifecycle client with middleware composition
- [x] Response cache (LRU + TTL + SWR)
- [x] Request deduplication (ref-counted)
- [x] Priority scheduling
- [x] Retry with exponential backoff
- [x] Response compression (gzip, br, deflate)
- [x] Request batching (time-window coalesce/split)
- [x] Connection pre-warming (probes, auto-refresh)
- [x] Adaptive concurrency (gradient-based)
- [x] Adaptive limiter presets, diagnostics, observability gauges, and soak benchmark
- [x] Circuit breaker
- [x] Tracing & validation
Schema
- [x] Dependency-free schema DSL
- [x] Type inference via
InferSchema - [x] Object, array, record, union, enum, literal, custom schemas
- [x] Optional, nullable, refine, transform
- [x] Path-rich validation issues
Observability
- [x] Runtime metrics sink and Prometheus exporter
- [x] OTLP JSON/HTTP exporters for metrics, traces, and logs
- [x] Structured logging with context propagation and redaction
- [x] W3C trace-context extract/inject helpers
- [x] Client and server HTTP observability helpers
- [x] Adaptive limiter metrics on HTTP client spans and Prometheus gauges
- [x] Sampling, force-sample-on-error, and bounded trace retention
- [x] Bounded exporter queues with retry, timeout, drop policy, and single-flight flush
- [x] Fetch/Node/Express/Fastify/Nest-style examples and collector smoke script
Design notes
- No hidden Promises — async is always modeled explicitly via
Async - Deterministic execution — scheduler is observable and testable
- Resource safety is structural — scopes guarantee cleanup
- Middleware composes via functions —
(next: HttpClientFn) => HttpClientFn - Cancellation propagates — ref-counted through the entire middleware stack
- Stats are frozen snapshots — no mutable state leaks to consumers
Contributing
- Runtime invariants matter — avoid sneaking Promises into core semantics
- Prefer libraries on top of the runtime over changes in core
- Add property tests when an invariant is broad
- Keep tests close to the changed module
- Small, focused PRs are welcome
License
MIT © 2025
