brass-runtime 1.16.0 → 1.17.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.
- package/CHANGELOG.md +17 -0
- package/README.md +287 -23
- package/dist/agent/cli/main.cjs +38 -38
- package/dist/agent/cli/main.js +6 -6
- package/dist/agent/cli/main.mjs +6 -6
- package/dist/agent/index.cjs +7 -7
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +6 -6
- package/dist/agent/index.mjs +6 -6
- package/dist/chunk-2HQTDLHF.mjs +683 -0
- package/dist/chunk-36I3M4UC.mjs +370 -0
- package/dist/{chunk-QY5FKYEQ.js → chunk-3AYM6WPJ.js} +570 -51
- package/dist/chunk-3LOYJFRR.cjs +300 -0
- package/dist/chunk-3Y2RIUMM.js +300 -0
- package/dist/{chunk-7XOPAB5Q.js → chunk-4P2HHGAX.mjs} +83 -5
- package/dist/{chunk-N6VHMOWB.cjs → chunk-4ROBZFL6.cjs} +128 -128
- package/dist/{chunk-NC5SDRYE.js → chunk-52OB2ROS.js} +4 -4
- package/dist/{chunk-JX3LZQJH.cjs → chunk-52PPNNI4.cjs} +82 -20
- package/dist/{chunk-5YOQOXEQ.cjs → chunk-5EC274J5.cjs} +676 -293
- package/dist/chunk-5QC7LRZ3.js +229 -0
- package/dist/{chunk-7TL2LHQJ.js → chunk-5VRJNBLZ.mjs} +524 -141
- package/dist/chunk-62AZW6UT.cjs +313 -0
- package/dist/chunk-6IXXWIUM.js +683 -0
- package/dist/chunk-6RY2FFN4.mjs +2024 -0
- package/dist/chunk-74ZTY6CP.js +2871 -0
- package/dist/chunk-7CMJS3QE.mjs +2871 -0
- package/dist/{chunk-2WC63LJK.mjs → chunk-7JIJOVCT.js} +20 -10
- package/dist/chunk-7X3K5RMS.js +2024 -0
- package/dist/chunk-7ZPEZ57L.cjs +2024 -0
- package/dist/{chunk-FM4W4QPL.js → chunk-A2OM6NEH.mjs} +5 -4
- package/dist/chunk-AGR5B2BC.cjs +683 -0
- package/dist/chunk-B33ICAKP.js +313 -0
- package/dist/{chunk-J3H54ZRV.mjs → chunk-B5JD23U7.mjs} +1 -1
- package/dist/{chunk-F5EUMJL7.mjs → chunk-BKK77SBA.js} +83 -5
- package/dist/{chunk-U5KWK3PX.mjs → chunk-C3MDXTRZ.js} +11 -0
- package/dist/{chunk-SPUEME2B.cjs → chunk-CZIVE6NT.cjs} +12 -1
- package/dist/{chunk-TDVMADDN.js → chunk-DNFJLJMW.mjs} +11 -0
- package/dist/{chunk-XDZOO4L5.js → chunk-EJ6BPYVR.mjs} +79 -17
- package/dist/chunk-EOC4UHBS.mjs +229 -0
- package/dist/chunk-F6XWZQY4.cjs +777 -0
- package/dist/{chunk-7LVI2GIN.js → chunk-FH2X7BVP.js} +507 -72
- package/dist/{chunk-OOGJ73B6.js → chunk-FHQGHPMO.mjs} +20 -10
- package/dist/{chunk-WQ5QNU5R.cjs → chunk-GLE2WY7Z.cjs} +652 -217
- package/dist/{chunk-G6IQOE4P.mjs → chunk-GYM3LLGS.mjs} +507 -72
- package/dist/{chunk-TVN5I4U6.cjs → chunk-JF5WGYJJ.cjs} +25 -24
- package/dist/{chunk-CY33PGEX.mjs → chunk-KH4SYAOS.mjs} +570 -51
- package/dist/chunk-KN32XNTH.mjs +313 -0
- package/dist/chunk-KQLYONSE.cjs +2871 -0
- package/dist/{chunk-7HUOJA4W.cjs → chunk-KZJQ723N.cjs} +90 -80
- package/dist/{chunk-CCKHV5BT.mjs → chunk-L2SYFEBS.js} +5 -4
- package/dist/{chunk-IJT6RRQ5.cjs → chunk-L6VB5N7Q.cjs} +20 -9
- package/dist/{chunk-ZGLD4TVZ.mjs → chunk-MBEJI5HF.mjs} +4 -4
- package/dist/{chunk-PRWCB3QL.mjs → chunk-MIIYDLGM.js} +524 -141
- package/dist/{chunk-H55LI6WY.js → chunk-MOO4L7F4.mjs} +15 -4
- package/dist/chunk-MVGUEJ5Z.cjs +370 -0
- package/dist/chunk-PD4EJTQC.cjs +229 -0
- package/dist/chunk-PWC3RBQE.mjs +300 -0
- package/dist/{chunk-MWXMNYJS.cjs → chunk-Q2I37RP3.cjs} +643 -124
- package/dist/{chunk-VFIUZG7J.mjs → chunk-RKGKFN2A.js} +79 -17
- package/dist/{chunk-NYL4D7SK.cjs → chunk-SA6HUJVI.cjs} +5 -5
- package/dist/chunk-SK7UZRNI.mjs +777 -0
- package/dist/{chunk-K2T3DV26.mjs → chunk-TRM4JUZQ.js} +15 -4
- package/dist/chunk-UB4B6OFY.js +370 -0
- package/dist/{chunk-G3XGCZDQ.js → chunk-UCUBNWM2.js} +1 -1
- package/dist/chunk-VWIPB6I5.js +777 -0
- package/dist/{chunk-JNFRRJYH.cjs → chunk-WBGRHGBP.cjs} +270 -192
- package/dist/{client-CtFmoDvM.d.ts → client-CZHU674n.d.ts} +211 -36
- package/dist/core/index.cjs +135 -9
- package/dist/core/index.d.ts +238 -33
- package/dist/core/index.js +155 -29
- package/dist/core/index.mjs +155 -29
- package/dist/{effect-CGNl5Rqp.d.ts → effect-DIUHZ9IN.d.ts} +89 -1
- package/dist/effectRunner-CFLC32IK.cjs +8 -0
- package/dist/{effectRunner-A4CHJXJI.js → effectRunner-L4S7IPT3.js} +2 -2
- package/dist/{effectRunner-OPUF6QRN.mjs → effectRunner-NNGG75QA.mjs} +2 -2
- package/dist/http/index.cjs +324 -2986
- package/dist/http/index.d.ts +54 -68
- package/dist/http/index.js +238 -2900
- package/dist/http/index.mjs +238 -2900
- package/dist/http/testing.cjs +14 -12
- package/dist/http/testing.d.ts +5 -4
- package/dist/http/testing.js +10 -8
- package/dist/http/testing.mjs +10 -8
- package/dist/index.cjs +423 -255
- package/dist/index.d.ts +87 -69
- package/dist/index.js +301 -133
- package/dist/index.mjs +301 -133
- package/dist/observability/index.cjs +18 -531
- package/dist/observability/index.d.ts +81 -8
- package/dist/observability/index.js +25 -538
- package/dist/observability/index.mjs +25 -538
- 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 +6 -2
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.js +5 -1
- package/dist/schema/index.mjs +5 -1
- package/dist/{server-C8hDXA74.d.ts → server-D6JZ15_e.d.ts} +16 -4
- package/dist/{stream-dvSs0QS5.d.ts → stream-B4oK9JFP.d.ts} +1 -1
- package/dist/{tracer-B5tRH9H7.d.ts → tracer-Hwt1cl7h.d.ts} +13 -54
- package/dist/{tracing-Dt9S_6V8.d.ts → tracing-DqbTKGcf.d.ts} +1 -1
- package/docs/ARCHITECTURE.md +292 -0
- package/docs/README.md +65 -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 +339 -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/framework-integrations.md +38 -0
- package/docs/frameworks/angular.md +153 -0
- package/docs/frameworks/express.md +125 -0
- package/docs/frameworks/fastify.md +124 -0
- package/docs/frameworks/nestjs.md +282 -0
- package/docs/frameworks/nextjs.md +147 -0
- package/docs/frameworks/react.md +139 -0
- package/docs/frameworks/vanilla.md +224 -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/nestjs.md +6 -0
- package/docs/observability-collector-smoke.md +31 -0
- package/docs/observability-framework-examples.md +110 -0
- package/docs/observability.md +649 -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 +22 -2
- package/dist/chunk-45F7OKGT.cjs +0 -104
- package/dist/chunk-7V4KY4RL.mjs +0 -104
- package/dist/chunk-DJQ7OMMB.cjs +0 -144
- package/dist/chunk-GOV47PPB.mjs +0 -552
- package/dist/chunk-JF4XXPZ5.cjs +0 -552
- package/dist/chunk-KCPT2D6G.js +0 -552
- package/dist/chunk-NOYZIMUJ.mjs +0 -144
- package/dist/chunk-PNVFW245.js +0 -144
- package/dist/chunk-ROJC3NBJ.js +0 -104
- package/dist/effectRunner-3ZHAD3LE.cjs +0 -8
- package/dist/schedule-Fque9Abz.d.ts +0 -70
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Vanilla integration
|
|
2
|
+
|
|
3
|
+
Use this when an app does not have a framework-level DI or lifecycle system.
|
|
4
|
+
The same shape works for plain browser TypeScript, Node HTTP handlers, CLIs,
|
|
5
|
+
and small services.
|
|
6
|
+
|
|
7
|
+
## Browser
|
|
8
|
+
|
|
9
|
+
Browser apps should send telemetry to a same-origin proxy. Do not ship Grafana
|
|
10
|
+
Cloud or collector credentials to the client.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// brass.browser.ts
|
|
14
|
+
import { Runtime } from "brass-runtime/core";
|
|
15
|
+
import {
|
|
16
|
+
defineHttpPolicyPresets,
|
|
17
|
+
makeDefaultHttpClient,
|
|
18
|
+
} from "brass-runtime/http";
|
|
19
|
+
import {
|
|
20
|
+
makeObservability,
|
|
21
|
+
makeOtlpOptions,
|
|
22
|
+
withHttpObservability,
|
|
23
|
+
} from "brass-runtime/observability";
|
|
24
|
+
|
|
25
|
+
const policyPresets = defineHttpPolicyPresets({
|
|
26
|
+
readModel: {
|
|
27
|
+
lane: "read-model",
|
|
28
|
+
priority: 3,
|
|
29
|
+
retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
|
|
30
|
+
},
|
|
31
|
+
command: {
|
|
32
|
+
lane: "command",
|
|
33
|
+
priority: 1,
|
|
34
|
+
retry: false,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const brass = (() => {
|
|
39
|
+
const observability = makeObservability({
|
|
40
|
+
serviceName: "shop-web",
|
|
41
|
+
resource: { "deployment.environment": "browser" },
|
|
42
|
+
logs: false,
|
|
43
|
+
sampling: { ratio: 0.1, respectRemoteSampled: true, forceSampleOnError: true },
|
|
44
|
+
redaction: {},
|
|
45
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
46
|
+
otlp: makeOtlpOptions({
|
|
47
|
+
endpoint: "/api/otel",
|
|
48
|
+
timeoutMs: 10_000,
|
|
49
|
+
retry: { attempts: 2, initialDelayMs: 100, maxDelayMs: 1_000 },
|
|
50
|
+
pipeline: { maxQueueSize: 2_000, batchSize: 128, dropPolicy: "drop-oldest" },
|
|
51
|
+
}),
|
|
52
|
+
flushIntervalMs: 15_000,
|
|
53
|
+
autoStart: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const runtime = new Runtime({
|
|
57
|
+
env: observability.env,
|
|
58
|
+
hooks: observability.hooks,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const http = makeDefaultHttpClient({
|
|
62
|
+
baseUrl: "/api",
|
|
63
|
+
preset: "balanced",
|
|
64
|
+
timeoutMs: 5_000,
|
|
65
|
+
policyPresets,
|
|
66
|
+
middleware: [withHttpObservability(observability)],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
observability,
|
|
71
|
+
runtime,
|
|
72
|
+
http,
|
|
73
|
+
shutdown: async () => {
|
|
74
|
+
await http.shutdown();
|
|
75
|
+
await observability.shutdown();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
})();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use it from ordinary browser code:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
type User = {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
readonly name: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export async function loadCurrentUser(): Promise<User> {
|
|
90
|
+
const response = await brass.runtime.toPromise(
|
|
91
|
+
brass.http.getJson<User>("/users/me", {
|
|
92
|
+
policy: "readModel",
|
|
93
|
+
timeoutMs: 2_000,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return response.body;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.addEventListener("pagehide", () => {
|
|
101
|
+
void brass.shutdown();
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Node
|
|
106
|
+
|
|
107
|
+
On the server, Brass can send directly to a collector because credentials stay
|
|
108
|
+
inside the trusted process.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// brass.node.ts
|
|
112
|
+
import { Runtime } from "brass-runtime/core";
|
|
113
|
+
import {
|
|
114
|
+
defineHttpPolicyPresets,
|
|
115
|
+
makeDefaultHttpClient,
|
|
116
|
+
} from "brass-runtime/http";
|
|
117
|
+
import {
|
|
118
|
+
makeObservability,
|
|
119
|
+
makeOtlpOptions,
|
|
120
|
+
withHttpObservability,
|
|
121
|
+
} from "brass-runtime/observability";
|
|
122
|
+
|
|
123
|
+
const policyPresets = defineHttpPolicyPresets({
|
|
124
|
+
readModel: {
|
|
125
|
+
lane: "read-model",
|
|
126
|
+
priority: 3,
|
|
127
|
+
retry: { maxRetries: 2, baseDelayMs: 100, maxDelayMs: 1_000 },
|
|
128
|
+
},
|
|
129
|
+
command: {
|
|
130
|
+
lane: "command",
|
|
131
|
+
priority: 1,
|
|
132
|
+
retry: false,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const brass = (() => {
|
|
137
|
+
const observability = makeObservability({
|
|
138
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "vanilla-node",
|
|
139
|
+
serviceVersion: process.env.OTEL_SERVICE_VERSION,
|
|
140
|
+
resource: {
|
|
141
|
+
"deployment.environment": process.env.NODE_ENV ?? "development",
|
|
142
|
+
},
|
|
143
|
+
logs: { minLevel: "info" },
|
|
144
|
+
sampling: { ratio: 0.25, respectRemoteSampled: true, forceSampleOnError: true },
|
|
145
|
+
redaction: {},
|
|
146
|
+
cardinality: { maxValuesPerLabel: 100 },
|
|
147
|
+
otlp: makeOtlpOptions({
|
|
148
|
+
endpoint: process.env.GRAFANA_OTLP_ENDPOINT ?? "http://grafana-alloy:4318",
|
|
149
|
+
headers: process.env.GRAFANA_OTLP_AUTHORIZATION
|
|
150
|
+
? { Authorization: process.env.GRAFANA_OTLP_AUTHORIZATION }
|
|
151
|
+
: undefined,
|
|
152
|
+
timeoutMs: 10_000,
|
|
153
|
+
retry: { attempts: 3, initialDelayMs: 100, maxDelayMs: 2_000 },
|
|
154
|
+
pipeline: {
|
|
155
|
+
maxQueueSize: 10_000,
|
|
156
|
+
batchSize: 512,
|
|
157
|
+
dropPolicy: "drop-oldest",
|
|
158
|
+
shutdownTimeoutMs: 10_000,
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
flushIntervalMs: 10_000,
|
|
162
|
+
autoStart: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const runtime = new Runtime({
|
|
166
|
+
env: observability.env,
|
|
167
|
+
hooks: observability.hooks,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const http = makeDefaultHttpClient({
|
|
171
|
+
baseUrl: process.env.API_BASE_URL ?? "https://api.internal",
|
|
172
|
+
preset: "production",
|
|
173
|
+
timeoutMs: 5_000,
|
|
174
|
+
policyPresets,
|
|
175
|
+
middleware: [withHttpObservability(observability)],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
observability,
|
|
180
|
+
runtime,
|
|
181
|
+
http,
|
|
182
|
+
shutdown: async () => {
|
|
183
|
+
await http.shutdown();
|
|
184
|
+
await observability.shutdown();
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
})();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use the Node request adapter when you receive `IncomingMessage`-like requests:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { createServer } from "node:http";
|
|
194
|
+
import {
|
|
195
|
+
makeNodeRequestObservabilityContext,
|
|
196
|
+
} from "brass-runtime/observability";
|
|
197
|
+
import { brass } from "./brass.node";
|
|
198
|
+
|
|
199
|
+
const server = createServer(async (req, res) => {
|
|
200
|
+
const ctx = makeNodeRequestObservabilityContext(brass.observability, req, {
|
|
201
|
+
route: req.url?.startsWith("/users/") ? "/users/:id" : req.url,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const response = await ctx.run(
|
|
205
|
+
ctx.withRequestSpan(
|
|
206
|
+
brass.http.getJson("/users/42", {
|
|
207
|
+
policy: "readModel",
|
|
208
|
+
timeoutMs: 2_000,
|
|
209
|
+
}),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
res.setHeader("content-type", "application/json");
|
|
214
|
+
res.end(JSON.stringify(response.body));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
process.once("SIGTERM", async () => {
|
|
218
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
219
|
+
await brass.shutdown();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
server.listen(process.env.PORT ?? 3000);
|
|
223
|
+
```
|
|
224
|
+
|
|
@@ -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
|