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.
- package/CHANGELOG.md +17 -0
- package/README.md +673 -136
- package/dist/agent/cli/main.cjs +40 -35
- package/dist/agent/cli/main.js +9 -4
- package/dist/agent/cli/main.mjs +9 -4
- package/dist/agent/index.cjs +8 -4
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +7 -3
- package/dist/agent/index.mjs +7 -3
- package/dist/chunk-2HQTDLHF.mjs +683 -0
- package/dist/chunk-36I3M4UC.mjs +370 -0
- package/dist/chunk-3AYM6WPJ.js +1629 -0
- package/dist/chunk-3LOYJFRR.cjs +300 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3Y2RIUMM.js +300 -0
- package/dist/{chunk-VEZNF5GZ.cjs → chunk-4ROBZFL6.cjs} +130 -126
- package/dist/{chunk-3QMOKAS5.js → chunk-52OB2ROS.js} +9 -5
- package/dist/chunk-52PPNNI4.cjs +416 -0
- package/dist/chunk-5EC274J5.cjs +2874 -0
- package/dist/chunk-5QC7LRZ3.js +229 -0
- package/dist/chunk-5VRJNBLZ.mjs +2874 -0
- package/dist/chunk-62AZW6UT.cjs +313 -0
- package/dist/chunk-6IXXWIUM.js +683 -0
- package/dist/chunk-74ZTY6CP.js +2871 -0
- package/dist/chunk-76YMRMH2.cjs +777 -0
- package/dist/chunk-7CMJS3QE.mjs +2871 -0
- package/dist/{chunk-4NHES7VK.mjs → chunk-7JIJOVCT.js} +27 -13
- package/dist/chunk-A2OM6NEH.mjs +194 -0
- package/dist/chunk-AGR5B2BC.cjs +683 -0
- package/dist/chunk-AVNQLJ5V.js +777 -0
- package/dist/chunk-B33ICAKP.js +313 -0
- package/dist/{chunk-ELOOF35R.mjs → chunk-B5JD23U7.mjs} +1 -1
- package/dist/chunk-BABBZK4Y.js +2024 -0
- package/dist/chunk-C3MDXTRZ.js +354 -0
- package/dist/chunk-CIZFIMK5.js +2193 -0
- package/dist/chunk-CZIVE6NT.cjs +354 -0
- package/dist/chunk-DNFJLJMW.mjs +354 -0
- package/dist/chunk-DNFO2EIZ.mjs +777 -0
- package/dist/chunk-EJ6BPYVR.mjs +416 -0
- package/dist/chunk-ENKODRU3.cjs +2193 -0
- package/dist/chunk-EOC4UHBS.mjs +229 -0
- package/dist/{chunk-BMH5AV44.js → chunk-FH2X7BVP.js} +756 -440
- package/dist/{chunk-PPUXIH5R.js → chunk-FHQGHPMO.mjs} +27 -13
- package/dist/{chunk-TGIFUAK4.cjs → chunk-GLE2WY7Z.cjs} +951 -635
- package/dist/{chunk-BDF4AMWX.mjs → chunk-GYM3LLGS.mjs} +756 -440
- package/dist/chunk-HLWLMW2F.mjs +2024 -0
- package/dist/chunk-JF5WGYJJ.cjs +194 -0
- package/dist/chunk-KH4SYAOS.mjs +1629 -0
- package/dist/chunk-KN32XNTH.mjs +313 -0
- package/dist/chunk-KQLYONSE.cjs +2871 -0
- package/dist/{chunk-STVLQ3XD.cjs → chunk-KZJQ723N.cjs} +92 -78
- package/dist/chunk-L2SYFEBS.js +194 -0
- package/dist/chunk-L6VB5N7Q.cjs +104 -0
- package/dist/{chunk-K6M7MDZ4.mjs → chunk-MBEJI5HF.mjs} +9 -5
- package/dist/chunk-MIIYDLGM.js +2874 -0
- package/dist/chunk-MOO4L7F4.mjs +104 -0
- package/dist/chunk-MT3OWDPC.mjs +2193 -0
- package/dist/chunk-MVGUEJ5Z.cjs +370 -0
- package/dist/chunk-OBGZSXTJ.cjs +10 -0
- package/dist/chunk-PD4EJTQC.cjs +229 -0
- package/dist/chunk-PWC3RBQE.mjs +300 -0
- package/dist/chunk-Q2I37RP3.cjs +1629 -0
- package/dist/chunk-RKGKFN2A.js +416 -0
- package/dist/{chunk-R3R2FVLG.cjs → chunk-SA6HUJVI.cjs} +5 -5
- package/dist/chunk-TRM4JUZQ.js +104 -0
- package/dist/chunk-UB4B6OFY.js +370 -0
- package/dist/{chunk-TO7IKXYT.js → chunk-UCUBNWM2.js} +1 -1
- package/dist/chunk-VN44DYYT.cjs +2024 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/client-CZHU674n.d.ts +820 -0
- package/dist/core/index.cjs +198 -4
- package/dist/core/index.d.ts +311 -212
- package/dist/core/index.js +237 -43
- package/dist/core/index.mjs +237 -43
- package/dist/{effect-CMOQKX8y.d.ts → effect-DIUHZ9IN.d.ts} +195 -1
- package/dist/effectRunner-CFLC32IK.cjs +8 -0
- package/dist/effectRunner-L4S7IPT3.js +8 -0
- package/dist/effectRunner-NNGG75QA.mjs +8 -0
- package/dist/http/index.cjs +1227 -2971
- package/dist/http/index.d.ts +826 -280
- package/dist/http/index.js +1089 -2833
- package/dist/http/index.mjs +1089 -2833
- package/dist/http/testing.cjs +161 -0
- package/dist/http/testing.d.ts +43 -0
- package/dist/http/testing.js +161 -0
- package/dist/http/testing.mjs +161 -0
- package/dist/index.cjs +486 -250
- package/dist/index.d.ts +87 -95
- package/dist/index.js +391 -155
- package/dist/index.mjs +391 -155
- package/dist/observability/index.cjs +162 -0
- package/dist/observability/index.d.ts +152 -0
- package/dist/observability/index.js +162 -0
- package/dist/observability/index.mjs +162 -0
- package/dist/perf/cli.cjs +401 -0
- package/dist/perf/cli.d.ts +1 -0
- package/dist/perf/cli.js +401 -0
- package/dist/perf/cli.mjs +401 -0
- package/dist/perf/index.cjs +141 -0
- package/dist/perf/index.d.ts +483 -0
- package/dist/perf/index.js +141 -0
- package/dist/perf/index.mjs +141 -0
- package/dist/schedule-CK3Ml_7p.d.ts +259 -0
- package/dist/schema/index.cjs +29 -0
- package/dist/schema/index.d.ts +179 -0
- package/dist/schema/index.js +29 -0
- package/dist/schema/index.mjs +29 -0
- package/dist/server-GJPg8ZSG.d.ts +675 -0
- package/dist/{stream-FQm9h4Mg.d.ts → stream-B4oK9JFP.d.ts} +1 -1
- package/dist/tracer-Hwt1cl7h.d.ts +189 -0
- package/dist/tracing-DqbTKGcf.d.ts +148 -0
- package/docs/ARCHITECTURE.md +292 -0
- package/docs/README.md +63 -0
- package/docs/adr/0001-ai-context-pack.md +32 -0
- package/docs/agent-apply-mode.md +104 -0
- package/docs/agent-approvals.md +110 -0
- package/docs/agent-batch.md +185 -0
- package/docs/agent-boundaries.md +112 -0
- package/docs/agent-chat-sessions.md +160 -0
- package/docs/agent-ci.md +17 -0
- package/docs/agent-cli.md +405 -0
- package/docs/agent-config.md +480 -0
- package/docs/agent-context-discovery.md +159 -0
- package/docs/agent-copilot-like-dx.md +126 -0
- package/docs/agent-declarative-optimized-planning.md +138 -0
- package/docs/agent-dx.md +224 -0
- package/docs/agent-env-files.md +126 -0
- package/docs/agent-follow-up-context.md +43 -0
- package/docs/agent-global-usage.md +180 -0
- package/docs/agent-init.md +109 -0
- package/docs/agent-install-and-configure.md +516 -0
- package/docs/agent-language-workspace-ux.md +99 -0
- package/docs/agent-llm-adapters.md +123 -0
- package/docs/agent-local-install.md +190 -0
- package/docs/agent-local-tests.md +51 -0
- package/docs/agent-observability.md +155 -0
- package/docs/agent-patch-quality-loop.md +162 -0
- package/docs/agent-presets.md +22 -0
- package/docs/agent-project-commands.md +237 -0
- package/docs/agent-project-intelligence.md +156 -0
- package/docs/agent-redaction.md +18 -0
- package/docs/agent-release-readiness.md +76 -0
- package/docs/agent-rollback-safety.md +162 -0
- package/docs/agent-rollback.md +23 -0
- package/docs/agent-run-artifacts.md +16 -0
- package/docs/agent-vscode-auto-discovery.md +137 -0
- package/docs/agent-vscode-batch-runner.md +100 -0
- package/docs/agent-vscode-chat-layout.md +90 -0
- package/docs/agent-vscode-clean-install.md +147 -0
- package/docs/agent-vscode-code-actions.md +70 -0
- package/docs/agent-vscode-diff-preview.md +45 -0
- package/docs/agent-vscode-inline-assist.md +56 -0
- package/docs/agent-vscode-install.md +186 -0
- package/docs/agent-vscode-model-setup.md +97 -0
- package/docs/agent-vscode-patch-preview.md +92 -0
- package/docs/agent-vscode-problems.md +79 -0
- package/docs/agent-vscode-project-dashboard.md +106 -0
- package/docs/agent-vscode-run-history.md +92 -0
- package/docs/agent-vscode-ux.md +73 -0
- package/docs/ai/INVARIANTS.md +84 -0
- package/docs/ai/PROJECT_MAP.md +338 -0
- package/docs/ai/PUBLIC_API.md +336 -0
- package/docs/ai/VALIDATION_MATRIX.md +67 -0
- package/docs/api-polish.md +37 -0
- package/docs/cancellation.md +162 -0
- package/docs/coverage.md +46 -0
- package/docs/getting-started.md +159 -0
- package/docs/guides/README.md +40 -0
- package/docs/guides/circuit-breaker.md +89 -0
- package/docs/guides/error-handling.md +91 -0
- package/docs/guides/getting-started.md +107 -0
- package/docs/guides/layers.md +189 -0
- package/docs/guides/metrics.md +101 -0
- package/docs/guides/resource-management.md +141 -0
- package/docs/guides/retry.md +215 -0
- package/docs/guides/semaphore.md +66 -0
- package/docs/guides/streams.md +117 -0
- package/docs/guides/supervisors.md +98 -0
- package/docs/guides/testing.md +162 -0
- package/docs/guides/tracing.md +71 -0
- package/docs/http-recipes.md +399 -0
- package/docs/http.md +749 -0
- package/docs/modules.md +285 -0
- package/docs/observability-collector-smoke.md +31 -0
- package/docs/observability-framework-examples.md +98 -0
- package/docs/observability.md +542 -0
- package/docs/otel-collector-smoke.yaml +27 -0
- package/docs/performance-profiler.md +199 -0
- package/docs/production-readiness.md +73 -0
- package/docs/recipes/README.md +12 -0
- package/docs/recipes/http-server.md +45 -0
- package/docs/recipes/layers.md +44 -0
- package/docs/recipes/performance.md +47 -0
- package/docs/recipes/runtime.md +41 -0
- package/docs/recipes/testing.md +41 -0
- package/docs/release.md +53 -0
- package/docs/wasm-bounded-queues.md +44 -0
- package/docs/wasm-engine-observability-benchmarks.md +85 -0
- package/docs/wasm-fiber-engine.md +117 -0
- package/docs/wasm-scheduler-state-machine.md +122 -0
- package/docs/wasm-stream-chunks.md +54 -0
- package/package.json +48 -2
- package/dist/chunk-AR22SXML.js +0 -1043
- package/dist/chunk-BDYEENHT.js +0 -224
- package/dist/chunk-JFPU5GQI.mjs +0 -1043
- package/dist/chunk-MS34J5LY.cjs +0 -224
- package/dist/chunk-UMAZLXAB.mjs +0 -224
- package/dist/chunk-XPZNXSVN.cjs +0 -1043
- package/dist/tracing-DNT9jEbr.d.ts +0 -106
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# 🧵 Brass Runtime — Getting Started
|
|
2
|
+
|
|
3
|
+
`brass-runtime` es un runtime funcional y cooperativo para JavaScript/TypeScript inspirado en modelos como **ZIO**, **Effect** y **structured concurrency**.
|
|
4
|
+
|
|
5
|
+
Su objetivo es permitir escribir lógica **pura, cancelable y composable**, sin perder la posibilidad de ejecutarla fácilmente desde código imperativo.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Instalación
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install brass-runtime
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## 🧠 Conceptos clave
|
|
18
|
+
|
|
19
|
+
### `Async<R, E, A>`
|
|
20
|
+
Un `Async` representa un **cálculo perezoso** que:
|
|
21
|
+
|
|
22
|
+
- puede requerir un entorno (`R`)
|
|
23
|
+
- puede fallar con un error (`E`)
|
|
24
|
+
- puede producir un valor (`A`)
|
|
25
|
+
- **no se ejecuta automáticamente**
|
|
26
|
+
|
|
27
|
+
Nada corre hasta que vos lo pedís explícitamente.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Ejemplo rápido
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { makeHttp } from "brass-runtime/http";
|
|
35
|
+
import { toPromise } from "brass-runtime";
|
|
36
|
+
|
|
37
|
+
const http = makeHttp({
|
|
38
|
+
baseUrl: "https://jsonplaceholder.typicode.com",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const effect = http.get("/posts/1");
|
|
43
|
+
|
|
44
|
+
const result = await toPromise(effect, {});
|
|
45
|
+
console.log(result.status);
|
|
46
|
+
console.log(result.bodyText);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🧩 ¿Por qué `toPromise`?
|
|
55
|
+
|
|
56
|
+
`toPromise` es el **puente entre el mundo funcional y el mundo imperativo**.
|
|
57
|
+
|
|
58
|
+
- `Async` es perezoso → no ejecuta nada por sí solo
|
|
59
|
+
- `toPromise` ejecuta el efecto en el runtime
|
|
60
|
+
- devuelve una `Promise` estándar
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const result = await toPromise(effect, env);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Internamente:
|
|
67
|
+
- crea un *fiber*
|
|
68
|
+
- lo ejecuta en el scheduler
|
|
69
|
+
- espera el resultado
|
|
70
|
+
- lo transforma en `Promise`
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## ⚙️ Estructura mental del runtime
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Async ---> Fiber ---> Scheduler ---> Resultado
|
|
78
|
+
| | |
|
|
79
|
+
| | +-- controla ejecución
|
|
80
|
+
| +-- maneja estado y cancelación
|
|
81
|
+
+-- describe el cómputo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Nada se ejecuta hasta que:
|
|
85
|
+
```ts
|
|
86
|
+
toPromise(effect, env)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🌐 Ejemplo HTTP completo
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { makeHttp } from "brass-runtime/http";
|
|
95
|
+
import { toPromise } from "brass-runtime";
|
|
96
|
+
|
|
97
|
+
const http = makeHttp({
|
|
98
|
+
baseUrl: "https://jsonplaceholder.typicode.com",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const effect = http.get("/posts/1");
|
|
103
|
+
|
|
104
|
+
const result = await toPromise(effect, {});
|
|
105
|
+
console.log("Status:", result.status);
|
|
106
|
+
console.log("Body:", result.bodyText);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🧩 ¿Por qué no usar `fetch` directamente?
|
|
115
|
+
|
|
116
|
+
Porque `Async` te da:
|
|
117
|
+
|
|
118
|
+
- Cancelación estructurada
|
|
119
|
+
- Composición funcional
|
|
120
|
+
- Control explícito de ejecución
|
|
121
|
+
- Testing determinístico
|
|
122
|
+
- Integración con fibras y scopes
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 🧠 Regla de oro
|
|
127
|
+
|
|
128
|
+
> **Los efectos no se ejecutan solos.**
|
|
129
|
+
> Se describen con `Async`, y se ejecutan solo con `toPromise` (o un runner equivalente).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 🧪 Testing
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { toPromise } from "brass-runtime";
|
|
137
|
+
|
|
138
|
+
test("fetch works", async () => {
|
|
139
|
+
const result = await toPromise(http.get("/posts/1"), {});
|
|
140
|
+
expect(result.status).toBe(200);
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 🧭 Próximos pasos
|
|
147
|
+
|
|
148
|
+
- Composición (`map`, `flatMap`)
|
|
149
|
+
- Cancelación con `AbortSignal`
|
|
150
|
+
- `race`, `timeout`, `retry`
|
|
151
|
+
- Integración con React / Bun / Workers
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Next
|
|
157
|
+
|
|
158
|
+
- Learn how interruption and `Scope` work: [Cancellation & interruption](./cancellation.md)
|
|
159
|
+
- Enable logging/tracing with hooks: [Observability](./observability.md)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# brass-runtime Guides
|
|
2
|
+
|
|
3
|
+
Guides for each feature of brass-runtime, organized by topic.
|
|
4
|
+
|
|
5
|
+
## Core
|
|
6
|
+
|
|
7
|
+
- [Getting Started](./getting-started.md) — Install, first effect, basic patterns
|
|
8
|
+
- [Effects & Fibers](./effects-and-fibers.md) — The effect system, fibers, and concurrency
|
|
9
|
+
- [Error Handling](./error-handling.md) — Typed errors, catchTag, recovery patterns
|
|
10
|
+
- [Resource Management](./resource-management.md) — bracket, ensuring, managed resources
|
|
11
|
+
|
|
12
|
+
## Concurrency
|
|
13
|
+
|
|
14
|
+
- [Streams & Pipelines](./streams.md) — ZStream, operators, fusion
|
|
15
|
+
- [Queue & Hub](./queue-and-hub.md) — Bounded queues, pub/sub
|
|
16
|
+
- [Semaphore & Rate Limiting](./semaphore.md) — Concurrency control
|
|
17
|
+
- [Circuit Breaker](./circuit-breaker.md) — Failure protection
|
|
18
|
+
- [Supervisors](./supervisors.md) — Restart and escalation policies for child fibers
|
|
19
|
+
|
|
20
|
+
## Resilience
|
|
21
|
+
|
|
22
|
+
- [Retry & Backoff](./retry.md) — retry, retryWithBackoff, Schedule
|
|
23
|
+
- [Timeout](./timeout.md) — Effect timeouts with cancellation
|
|
24
|
+
|
|
25
|
+
## Infrastructure
|
|
26
|
+
|
|
27
|
+
- [Layers (DI)](./layers.md) — Dependency injection with lifecycle
|
|
28
|
+
- [Ref (Shared State)](./ref.md) — Mutable state across fibers
|
|
29
|
+
- [Scheduler](./scheduler.md) — Task scheduling, lanes, budgets
|
|
30
|
+
- [Worker Pool](./worker-pool.md) — CPU-intensive offloading
|
|
31
|
+
|
|
32
|
+
## Observability
|
|
33
|
+
|
|
34
|
+
- [Tracing](./tracing.md) — OpenTelemetry-compatible spans
|
|
35
|
+
- [Metrics](./metrics.md) — Counters, gauges, histograms
|
|
36
|
+
- [Graceful Shutdown](./shutdown.md) — Clean process termination
|
|
37
|
+
|
|
38
|
+
## Testing
|
|
39
|
+
|
|
40
|
+
- [Testing Utilities](./testing.md) — TestRuntime, assertions, helpers
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Circuit Breaker
|
|
2
|
+
|
|
3
|
+
Protect against cascading failures when downstream services are unhealthy.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
CLOSED → (failures exceed threshold) → OPEN → (timeout expires) → HALF-OPEN
|
|
9
|
+
↑ |
|
|
10
|
+
└──────────── (probe succeeds) ──────────────────────────────────────┘
|
|
11
|
+
(probe fails) → back to OPEN
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Basic usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { makeCircuitBreaker } from "brass-runtime";
|
|
18
|
+
|
|
19
|
+
const breaker = makeCircuitBreaker({
|
|
20
|
+
failureThreshold: 5, // open after 5 consecutive failures
|
|
21
|
+
resetTimeoutMs: 30_000, // try again after 30 seconds
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Protect any effect
|
|
25
|
+
const result = await run(breaker.protect(callPaymentService()));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const breaker = makeCircuitBreaker({
|
|
32
|
+
failureThreshold: 3, // trips after 3 failures
|
|
33
|
+
resetTimeoutMs: 10_000, // 10s cooldown
|
|
34
|
+
successThreshold: 2, // need 2 successes in half-open to close
|
|
35
|
+
|
|
36
|
+
// Only count certain errors as failures
|
|
37
|
+
isFailure: (error) => {
|
|
38
|
+
if (error._tag === "NotFound") return false; // 404 is not a failure
|
|
39
|
+
if (error._tag === "BadRequest") return false; // client error, not service
|
|
40
|
+
return true; // everything else counts
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Observe state transitions
|
|
44
|
+
onStateChange: (from, to) => {
|
|
45
|
+
metrics.counter("circuit_breaker_transitions", { from, to }).increment();
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## With retry
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { makeCircuitBreaker, retryWithBackoff } from "brass-runtime";
|
|
54
|
+
|
|
55
|
+
const breaker = makeCircuitBreaker({ failureThreshold: 5 });
|
|
56
|
+
|
|
57
|
+
// Retry, but stop if circuit opens
|
|
58
|
+
const resilient = retryWithBackoff(
|
|
59
|
+
breaker.protect(callService()),
|
|
60
|
+
{
|
|
61
|
+
maxRetries: 3,
|
|
62
|
+
shouldRetry: (error) => {
|
|
63
|
+
// Don't retry if circuit is open — it'll just fail fast anyway
|
|
64
|
+
if (error._tag === "CircuitBreakerOpen") return false;
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Monitoring
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const stats = breaker.stats();
|
|
75
|
+
// {
|
|
76
|
+
// state: "closed",
|
|
77
|
+
// failures: 2,
|
|
78
|
+
// successes: 0,
|
|
79
|
+
// totalRequests: 150,
|
|
80
|
+
// totalFailures: 5,
|
|
81
|
+
// totalSuccesses: 143,
|
|
82
|
+
// totalRejected: 2,
|
|
83
|
+
// lastFailureTime: 1699000000000,
|
|
84
|
+
// lastSuccessTime: 1699000001000,
|
|
85
|
+
// }
|
|
86
|
+
|
|
87
|
+
// Manual reset (e.g., after deploying a fix)
|
|
88
|
+
breaker.reset();
|
|
89
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
brass-runtime provides typed error handling with discriminated unions, enabling exhaustive pattern matching and type-safe recovery.
|
|
4
|
+
|
|
5
|
+
## Tagged Errors
|
|
6
|
+
|
|
7
|
+
Define errors as discriminated unions:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
type AppError =
|
|
11
|
+
| { _tag: "NetworkError"; url: string; status: number }
|
|
12
|
+
| { _tag: "TimeoutError"; ms: number }
|
|
13
|
+
| { _tag: "NotFound"; id: string };
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## catchTag — Handle specific errors
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { catchTag } from "brass-runtime";
|
|
20
|
+
|
|
21
|
+
const result = catchTag(
|
|
22
|
+
fetchUser(id), // Async<R, AppError, User>
|
|
23
|
+
"NotFound", // catch only NotFound
|
|
24
|
+
(e) => asyncSucceed(defaultUser) // e is narrowed to { _tag: "NotFound"; id: string }
|
|
25
|
+
);
|
|
26
|
+
// Result type: Async<R, NetworkError | TimeoutError, User>
|
|
27
|
+
// NotFound is removed from the error type!
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## catchTags — Handle multiple errors at once
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { catchTags } from "brass-runtime";
|
|
34
|
+
|
|
35
|
+
const result = catchTags(fetchUser(id), {
|
|
36
|
+
NotFound: (e) => asyncSucceed(defaultUser),
|
|
37
|
+
TimeoutError: (e) => retryWithBackoff(fetchUser(id)),
|
|
38
|
+
});
|
|
39
|
+
// Only NetworkError remains in the error channel
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## tagError — Wrap untyped errors
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { tagError } from "brass-runtime";
|
|
46
|
+
|
|
47
|
+
// Wrap a fetch error with a tag
|
|
48
|
+
const typed = tagError(
|
|
49
|
+
rawFetch(url),
|
|
50
|
+
"NetworkError",
|
|
51
|
+
(e) => ({ url, message: String(e) })
|
|
52
|
+
);
|
|
53
|
+
// Error type: { _tag: "NetworkError"; url: string; message: string }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## orElse — Fallback on any error
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { orElse } from "brass-runtime";
|
|
60
|
+
|
|
61
|
+
const result = orElse(
|
|
62
|
+
primaryDataSource(),
|
|
63
|
+
(error) => fallbackDataSource()
|
|
64
|
+
);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## mapError — Transform errors
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { mapError } from "brass-runtime";
|
|
71
|
+
|
|
72
|
+
const wrapped = mapError(
|
|
73
|
+
effect,
|
|
74
|
+
(e) => ({ _tag: "ServiceError" as const, cause: e })
|
|
75
|
+
);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Combining with retry
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { catchTag, retryWithBackoff } from "brass-runtime";
|
|
82
|
+
|
|
83
|
+
const resilient = catchTag(
|
|
84
|
+
retryWithBackoff(fetchData(), {
|
|
85
|
+
maxRetries: 3,
|
|
86
|
+
shouldRetry: (e) => e._tag === "NetworkError",
|
|
87
|
+
}),
|
|
88
|
+
"TimeoutError",
|
|
89
|
+
() => asyncSucceed(cachedData)
|
|
90
|
+
);
|
|
91
|
+
```
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install brass-runtime
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Your first effect
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Runtime, asyncSucceed, asyncFlatMap, asyncFail } from "brass-runtime";
|
|
13
|
+
|
|
14
|
+
// Create a runtime
|
|
15
|
+
const runtime = Runtime.make({});
|
|
16
|
+
|
|
17
|
+
// Effects are values — they describe what to do, not how
|
|
18
|
+
const greet = asyncSucceed("Hello, brass!");
|
|
19
|
+
|
|
20
|
+
// Run the effect
|
|
21
|
+
const result = await runtime.toPromise(greet);
|
|
22
|
+
console.log(result); // "Hello, brass!"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Composing effects
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { asyncSucceed, asyncFlatMap, asyncFail, asyncFold } from "brass-runtime";
|
|
29
|
+
|
|
30
|
+
// Chain effects with flatMap
|
|
31
|
+
const program = asyncFlatMap(
|
|
32
|
+
asyncSucceed(21),
|
|
33
|
+
(n) => asyncSucceed(n * 2)
|
|
34
|
+
);
|
|
35
|
+
// Result: 42
|
|
36
|
+
|
|
37
|
+
// Handle errors
|
|
38
|
+
const safe = asyncFold(
|
|
39
|
+
asyncFail("oops"),
|
|
40
|
+
(error) => asyncSucceed(`recovered from: ${error}`),
|
|
41
|
+
(value) => asyncSucceed(value)
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Async operations
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { async } from "brass-runtime";
|
|
49
|
+
|
|
50
|
+
// Wrap any callback-based API
|
|
51
|
+
const fetchData = async((_env, cb) => {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
|
|
54
|
+
fetch("https://api.example.com/data", { signal: controller.signal })
|
|
55
|
+
.then(res => res.json())
|
|
56
|
+
.then(data => cb({ _tag: "Success", value: data }))
|
|
57
|
+
.catch(err => cb({ _tag: "Failure", cause: { _tag: "Fail", error: err } }));
|
|
58
|
+
|
|
59
|
+
// Return a canceler
|
|
60
|
+
return () => controller.abort();
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Concurrency with fibers
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Runtime, asyncSucceed, asyncFlatMap } from "brass-runtime";
|
|
68
|
+
|
|
69
|
+
const runtime = Runtime.make({});
|
|
70
|
+
|
|
71
|
+
// Fork runs an effect in a new fiber (lightweight thread)
|
|
72
|
+
const fiber = runtime.fork(longRunningEffect);
|
|
73
|
+
|
|
74
|
+
// Join waits for the fiber to complete
|
|
75
|
+
fiber.join((exit) => {
|
|
76
|
+
if (exit._tag === "Success") console.log(exit.value);
|
|
77
|
+
else console.log("Failed:", exit.cause);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Interrupt cancels a fiber
|
|
81
|
+
fiber.interrupt();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Streams
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { fromArray, collectStream, via, mapP, filterP, andThen } from "brass-runtime";
|
|
88
|
+
|
|
89
|
+
// Create a stream from an array
|
|
90
|
+
const numbers = fromArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
91
|
+
|
|
92
|
+
// Build a pipeline (auto-fused for performance)
|
|
93
|
+
const pipeline = andThen(
|
|
94
|
+
mapP((x: number) => x * 2),
|
|
95
|
+
filterP((x: number) => x > 10)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Apply and collect
|
|
99
|
+
const result = await runtime.toPromise(collectStream(via(numbers, pipeline)));
|
|
100
|
+
// [12, 14, 16, 18, 20]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## What's next?
|
|
104
|
+
|
|
105
|
+
- [Effects & Fibers](./effects-and-fibers.md) — Deep dive into the effect system
|
|
106
|
+
- [Error Handling](./error-handling.md) — Typed errors and recovery
|
|
107
|
+
- [Streams & Pipelines](./streams.md) — Stream processing with fusion
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Layers (Dependency Injection)
|
|
2
|
+
|
|
3
|
+
Layers describe how to build services, wire dependencies, and release resources.
|
|
4
|
+
They are lazy values: acquisition happens only when a runtime evaluates
|
|
5
|
+
`provideLayer`, `provideLayerContext`, `buildLayer`, or the `Layer.*` aliases.
|
|
6
|
+
|
|
7
|
+
Layer 2.0 adds three pieces on top of the original API:
|
|
8
|
+
|
|
9
|
+
- typed `ServiceTag<A>` keys
|
|
10
|
+
- immutable `LayerContext` service maps
|
|
11
|
+
- scoped builds with memoization and idempotent finalizers
|
|
12
|
+
|
|
13
|
+
## Typed Contexts
|
|
14
|
+
|
|
15
|
+
Use tags when a layer graph needs more than one service or when a service should
|
|
16
|
+
be retrieved by capability instead of object shape.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { type Async, Layer, Runtime, asyncSucceed, asyncSync } from "brass-runtime";
|
|
20
|
+
|
|
21
|
+
const finalizer = (run: () => void): Async<unknown, never, void> =>
|
|
22
|
+
asyncSync(() => run()) as Async<unknown, never, void>;
|
|
23
|
+
|
|
24
|
+
type Config = { readonly dbUrl: string };
|
|
25
|
+
type Db = { readonly query: (sql: string) => string };
|
|
26
|
+
|
|
27
|
+
const Config = Layer.tag<Config>("Config");
|
|
28
|
+
const Db = Layer.tag<Db>("Db");
|
|
29
|
+
|
|
30
|
+
const ConfigLayer = Layer.value(Config, { dbUrl: "postgres://local" });
|
|
31
|
+
|
|
32
|
+
const DbLayer = Layer.effect(
|
|
33
|
+
Db,
|
|
34
|
+
(ctx) => {
|
|
35
|
+
const config = ctx.unsafeGet(Config);
|
|
36
|
+
return asyncSucceed({
|
|
37
|
+
query: (sql) => `${sql} on ${config.dbUrl}`,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
(db) => finalizer(() => {
|
|
41
|
+
db.query("close");
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const AppLayer = Layer.compose(ConfigLayer, DbLayer);
|
|
46
|
+
|
|
47
|
+
const runtime = Runtime.make({});
|
|
48
|
+
const result = await runtime.toPromise(
|
|
49
|
+
Layer.provideContext(AppLayer, (ctx) =>
|
|
50
|
+
asyncSucceed(ctx.unsafeGet(Db).query("select 1")),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`LayerContext` is immutable. `add` and `merge` return new contexts, and
|
|
56
|
+
right-hand services win when two contexts contain the same tag.
|
|
57
|
+
|
|
58
|
+
## Plain Services
|
|
59
|
+
|
|
60
|
+
The original layer API is still supported for simple service shapes.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { layer, layerFrom, composeLayer, provideLayer, asyncSucceed, asyncSync } from "brass-runtime";
|
|
64
|
+
|
|
65
|
+
type Config = { readonly dbUrl: string };
|
|
66
|
+
type Db = { readonly query: (sql: string) => string };
|
|
67
|
+
|
|
68
|
+
const ConfigLayer = layer(() => asyncSucceed<Config>({
|
|
69
|
+
dbUrl: "postgres://local",
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const DbLayer = layerFrom<Config>()(
|
|
73
|
+
(config) => asyncSucceed<Db>({
|
|
74
|
+
query: (sql) => `${sql} on ${config.dbUrl}`,
|
|
75
|
+
}),
|
|
76
|
+
() => asyncSync(() => {
|
|
77
|
+
// close the connection pool here
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const AppLayer = composeLayer(ConfigLayer, DbLayer);
|
|
82
|
+
|
|
83
|
+
await runtime.toPromise(
|
|
84
|
+
provideLayer(AppLayer, (db) => asyncSucceed(db.query("select 1"))),
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Merging
|
|
89
|
+
|
|
90
|
+
`mergeLayer` combines independent layers. Plain object services are merged with
|
|
91
|
+
object spread; `LayerContext` services are merged by tag.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Layer, mergeLayer, asyncSucceed } from "brass-runtime";
|
|
95
|
+
|
|
96
|
+
const Db = Layer.tag<{ readonly query: (sql: string) => string }>("Db");
|
|
97
|
+
const Cache = Layer.tag<{ readonly get: (key: string) => string | undefined }>("Cache");
|
|
98
|
+
|
|
99
|
+
const DbLayer = Layer.effect(Db, () => asyncSucceed({ query: (sql) => sql }));
|
|
100
|
+
const CacheLayer = Layer.effect(Cache, () => asyncSucceed({ get: () => undefined }));
|
|
101
|
+
|
|
102
|
+
const InfraLayer = mergeLayer(DbLayer, CacheLayer);
|
|
103
|
+
|
|
104
|
+
await runtime.toPromise(
|
|
105
|
+
Layer.provideContext(InfraLayer, (ctx) =>
|
|
106
|
+
asyncSucceed({
|
|
107
|
+
db: ctx.unsafeGet(Db),
|
|
108
|
+
cache: ctx.unsafeGet(Cache),
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Scoped Builds
|
|
115
|
+
|
|
116
|
+
Use `buildLayer` or `Layer.build` when a caller wants manual lifecycle control.
|
|
117
|
+
The returned scope memoizes shared layer instances during a build, releases in
|
|
118
|
+
reverse acquisition order, and makes `close()` idempotent.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Layer, asyncSucceed } from "brass-runtime";
|
|
122
|
+
|
|
123
|
+
const built = await runtime.toPromise(Layer.build(InfraLayer));
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await runtime.toPromise(
|
|
127
|
+
built.use((ctx) => asyncSucceed(ctx.unsafeGet(Db).query("select 1"))),
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
await runtime.toPromise(built.close());
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
For advanced graph assembly, create an explicit scope:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const scope = Layer.scope();
|
|
138
|
+
|
|
139
|
+
const db = await runtime.toPromise(scope.get(DbLayer));
|
|
140
|
+
const sameDb = await runtime.toPromise(scope.get(DbLayer));
|
|
141
|
+
|
|
142
|
+
db === sameDb; // true for the same layer object within the same scope
|
|
143
|
+
|
|
144
|
+
await runtime.toPromise(scope.close());
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
After a scope is closed, further `scope.get(...)` calls fail.
|
|
148
|
+
|
|
149
|
+
## Patterns
|
|
150
|
+
|
|
151
|
+
### Singleton Service
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const Logger = Layer.tag<Console>("Logger");
|
|
155
|
+
const LoggerLayer = Layer.value(Logger, console);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Service With Health Check
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const DbLayer = Layer.effect(
|
|
162
|
+
Db,
|
|
163
|
+
() => asyncSync(() => {
|
|
164
|
+
const pool = createPool();
|
|
165
|
+
pool.query("select 1");
|
|
166
|
+
return pool;
|
|
167
|
+
}),
|
|
168
|
+
(pool) => asyncSync(() => {
|
|
169
|
+
pool.close();
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Test Doubles
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const RealDbLayer = Layer.effect(Db, () => asyncSucceed(createPool()));
|
|
178
|
+
|
|
179
|
+
const MockDbLayer = Layer.value(Db, {
|
|
180
|
+
query: (sql: string) => `mock:${sql}`,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const result = await runtime.toPromise(
|
|
184
|
+
Layer.provideContext(
|
|
185
|
+
process.env.NODE_ENV === "test" ? MockDbLayer : RealDbLayer,
|
|
186
|
+
(ctx) => asyncSucceed(ctx.unsafeGet(Db).query("select *")),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
```
|