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,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
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Metrics
|
|
2
|
+
|
|
3
|
+
Lightweight metrics collection with counters, gauges, and histograms.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { makeMetrics } from "brass-runtime";
|
|
9
|
+
|
|
10
|
+
const metrics = makeMetrics();
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Counters
|
|
14
|
+
|
|
15
|
+
Monotonically increasing values (requests, errors, events):
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const requestCount = metrics.counter("http_requests_total", { method: "GET" });
|
|
19
|
+
requestCount.increment();
|
|
20
|
+
requestCount.increment(5); // increment by 5
|
|
21
|
+
|
|
22
|
+
console.log(requestCount.value()); // 6
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Gauges
|
|
26
|
+
|
|
27
|
+
Values that go up and down (connections, queue depth):
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const activeConns = metrics.gauge("active_connections");
|
|
31
|
+
activeConns.set(10);
|
|
32
|
+
activeConns.increment(); // 11
|
|
33
|
+
activeConns.decrement(3); // 8
|
|
34
|
+
|
|
35
|
+
console.log(activeConns.value()); // 8
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Histograms
|
|
39
|
+
|
|
40
|
+
Distribution of values (latency, sizes):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const latency = metrics.histogram("request_duration_ms", [1, 5, 10, 25, 50, 100, 250, 500, 1000]);
|
|
44
|
+
|
|
45
|
+
latency.observe(42.5);
|
|
46
|
+
latency.observe(3.2);
|
|
47
|
+
latency.observe(150);
|
|
48
|
+
|
|
49
|
+
// Get percentiles
|
|
50
|
+
console.log(latency.percentile(50)); // p50
|
|
51
|
+
console.log(latency.percentile(95)); // p95
|
|
52
|
+
console.log(latency.percentile(99)); // p99
|
|
53
|
+
|
|
54
|
+
// Get bucket distribution
|
|
55
|
+
const buckets = latency.buckets();
|
|
56
|
+
// { boundaries: [...], counts: [...], sum: 195.7, count: 3, min: 3.2, max: 150 }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Snapshot (export all metrics)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const snapshot = metrics.snapshot();
|
|
63
|
+
// {
|
|
64
|
+
// counters: [{ name: "http_requests_total", labels: { method: "GET" }, value: 6 }],
|
|
65
|
+
// gauges: [{ name: "active_connections", labels: {}, value: 8 }],
|
|
66
|
+
// histograms: [{ name: "request_duration_ms", labels: {}, buckets: {...} }],
|
|
67
|
+
// }
|
|
68
|
+
|
|
69
|
+
// Export to Prometheus format, CloudWatch, etc.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## With effects
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const requestCounter = metrics.counter("requests");
|
|
76
|
+
const latencyHist = metrics.histogram("latency_ms");
|
|
77
|
+
|
|
78
|
+
const instrumented = <R, E, A>(name: string, effect: Async<R, E, A>): Async<R, E, A> => {
|
|
79
|
+
const start = performance.now();
|
|
80
|
+
requestCounter.increment();
|
|
81
|
+
|
|
82
|
+
return asyncFold(
|
|
83
|
+
effect,
|
|
84
|
+
(error) => {
|
|
85
|
+
latencyHist.observe(performance.now() - start);
|
|
86
|
+
metrics.counter("errors", { operation: name }).increment();
|
|
87
|
+
return asyncFail(error);
|
|
88
|
+
},
|
|
89
|
+
(value) => {
|
|
90
|
+
latencyHist.observe(performance.now() - start);
|
|
91
|
+
return asyncSucceed(value);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Reset (for testing)
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
metrics.reset(); // clears all counters, gauges, histograms
|
|
101
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Resource Management
|
|
2
|
+
|
|
3
|
+
brass-runtime guarantees resource cleanup even when effects fail or fibers are interrupted.
|
|
4
|
+
|
|
5
|
+
## bracket — Acquire/Use/Release
|
|
6
|
+
|
|
7
|
+
The fundamental pattern for safe resource management:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { bracket } from "brass-runtime";
|
|
11
|
+
|
|
12
|
+
const result = await run(
|
|
13
|
+
bracket(
|
|
14
|
+
// Acquire: open the resource
|
|
15
|
+
openDatabaseConnection(),
|
|
16
|
+
|
|
17
|
+
// Use: work with the resource
|
|
18
|
+
(conn) => conn.query("SELECT * FROM users"),
|
|
19
|
+
|
|
20
|
+
// Release: always runs, even on failure/interruption
|
|
21
|
+
(conn, exit) => conn.close()
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Guarantees:**
|
|
27
|
+
- If `acquire` fails, `release` is never called
|
|
28
|
+
- If `use` fails or is interrupted, `release` still runs
|
|
29
|
+
- Errors in `release` are swallowed (the `use` result propagates)
|
|
30
|
+
|
|
31
|
+
## ensuring — Attach a finalizer
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { ensuring } from "brass-runtime";
|
|
35
|
+
|
|
36
|
+
const result = await run(
|
|
37
|
+
ensuring(
|
|
38
|
+
doWork(),
|
|
39
|
+
(exit) => {
|
|
40
|
+
// Always runs after doWork completes
|
|
41
|
+
if (exit._tag === "Success") logSuccess(exit.value);
|
|
42
|
+
else logFailure(exit.cause);
|
|
43
|
+
return unit();
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## managed — Reusable resource descriptors
|
|
50
|
+
|
|
51
|
+
Define a resource once, use it many times:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { managed, useManaged } from "brass-runtime";
|
|
55
|
+
|
|
56
|
+
// Define the resource (acquire + release)
|
|
57
|
+
const dbPool = managed(
|
|
58
|
+
asyncSucceed(createPool({ max: 10 })),
|
|
59
|
+
(pool) => { pool.close(); return unit(); }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Use it — each call acquires a fresh instance
|
|
63
|
+
const users = await run(useManaged(dbPool, (pool) => pool.query("SELECT *")));
|
|
64
|
+
const orders = await run(useManaged(dbPool, (pool) => pool.query("SELECT *")));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Resource — Composable scoped resources
|
|
68
|
+
|
|
69
|
+
`Resource` is the higher-level acquire/use/release descriptor. It composes with
|
|
70
|
+
`map`, `flatMap`, `zip`, and `Resource.all`, and releases nested resources in
|
|
71
|
+
reverse acquisition order.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Resource, makeResource, useResource } from "brass-runtime";
|
|
75
|
+
|
|
76
|
+
const db = makeResource(
|
|
77
|
+
openDatabasePool(),
|
|
78
|
+
(pool, _exit) => pool.close()
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const cache = makeResource(
|
|
82
|
+
openCacheClient(),
|
|
83
|
+
(client, _exit) => client.disconnect()
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const services = Resource.all([db, cache] as const);
|
|
87
|
+
|
|
88
|
+
const users = await run(
|
|
89
|
+
useResource(services, ([pool, client]) =>
|
|
90
|
+
loadUsers(pool, client)
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`Resource.fromManaged(managedValue)` bridges older `managed` descriptors into
|
|
96
|
+
the composable API.
|
|
97
|
+
|
|
98
|
+
## managedAll — Compose multiple resources
|
|
99
|
+
|
|
100
|
+
Acquire in order, release in reverse (LIFO):
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { managedAll, useManaged } from "brass-runtime";
|
|
104
|
+
|
|
105
|
+
const dbPool = managed(createPool(), (p) => p.close());
|
|
106
|
+
const cache = managed(createRedis(), (r) => r.disconnect());
|
|
107
|
+
const storage = managed(createS3Client(), (s) => s.destroy());
|
|
108
|
+
|
|
109
|
+
// All three acquired in order, released in reverse
|
|
110
|
+
const resources = managedAll([dbPool, cache, storage]);
|
|
111
|
+
|
|
112
|
+
const result = await run(
|
|
113
|
+
useManaged(resources, ([db, redis, s3]) => {
|
|
114
|
+
// Use all three services
|
|
115
|
+
return processData(db, redis, s3);
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
// s3 released first, then redis, then db
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## With Semaphore (connection limiting)
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { makeSemaphore, bracket } from "brass-runtime";
|
|
125
|
+
|
|
126
|
+
const poolSem = makeSemaphore(10); // max 10 connections
|
|
127
|
+
|
|
128
|
+
const withConnection = (work: (conn: Connection) => Async<any, any, any>) =>
|
|
129
|
+
poolSem.withPermit(
|
|
130
|
+
bracket(
|
|
131
|
+
openConnection(),
|
|
132
|
+
work,
|
|
133
|
+
(conn) => conn.close()
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// At most 10 concurrent connections
|
|
138
|
+
await Promise.all(
|
|
139
|
+
userIds.map(id => run(withConnection(conn => conn.query(id))))
|
|
140
|
+
);
|
|
141
|
+
```
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Retry & Backoff
|
|
2
|
+
|
|
3
|
+
brass-runtime provides multiple retry strategies, from simple to composable.
|
|
4
|
+
|
|
5
|
+
## Quick retry (no delay)
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { retryN } from "brass-runtime";
|
|
9
|
+
|
|
10
|
+
// Retry up to 3 times with no delay
|
|
11
|
+
const result = await run(retryN(fetchData(), 3));
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Exponential backoff with jitter
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { retryWithBackoff } from "brass-runtime";
|
|
18
|
+
|
|
19
|
+
const result = await run(retryWithBackoff(callApi(), {
|
|
20
|
+
maxRetries: 5,
|
|
21
|
+
baseDelayMs: 100, // first retry: random 0-100ms
|
|
22
|
+
maxDelayMs: 10_000, // cap at 10s
|
|
23
|
+
maxElapsedMs: 60_000, // total budget: 60s
|
|
24
|
+
shouldRetry: (error) => error._tag !== "NotFound", // don't retry 404s
|
|
25
|
+
}));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Full control with retry()
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { retry } from "brass-runtime";
|
|
32
|
+
|
|
33
|
+
const result = await run(retry(effect, {
|
|
34
|
+
maxRetries: 10,
|
|
35
|
+
baseDelayMs: 50,
|
|
36
|
+
maxDelayMs: 5000,
|
|
37
|
+
maxElapsedMs: 30_000,
|
|
38
|
+
jitter: "full", // or "none" for deterministic delays
|
|
39
|
+
shouldRetry: (error, attempt) => {
|
|
40
|
+
if (error._tag === "RateLimit") return true;
|
|
41
|
+
if (attempt > 5) return false;
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Schedule 2.0
|
|
48
|
+
|
|
49
|
+
For advanced use cases, use `Schedule` values. Schedules are declarative,
|
|
50
|
+
stateful only when driven, and runtime-clock aware when used by
|
|
51
|
+
`retryWithSchedule`, `repeatWithSchedule`, HTTP retry, supervisors, and server
|
|
52
|
+
shutdown polling.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { exponential, intersect, maxElapsed, recurs, retryWithSchedule } from "brass-runtime";
|
|
56
|
+
|
|
57
|
+
// Retry 5 times with exponential backoff, but stop after 30s total
|
|
58
|
+
const policy = maxElapsed(
|
|
59
|
+
intersect(recurs(5), exponential(100, 5000)),
|
|
60
|
+
30_000,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const result = await run(retryWithSchedule(effect, policy));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### ScheduleDriver
|
|
67
|
+
|
|
68
|
+
Use a driver when you want to manually step a schedule, inspect state, reset it,
|
|
69
|
+
or emit observability events.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { Schedule } from "brass-runtime";
|
|
73
|
+
|
|
74
|
+
const policy = Schedule.named(
|
|
75
|
+
"api.retry",
|
|
76
|
+
Schedule.jitter(Schedule.exponential(100, 5_000), { factor: 0.2 }),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const driver = Schedule.driver(policy, {
|
|
80
|
+
onDecision: (event) => {
|
|
81
|
+
console.log(event.name, event.attempt, event.decision.delayMs);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const next = driver.next({ status: 503 });
|
|
86
|
+
|
|
87
|
+
if (next.continue) {
|
|
88
|
+
console.log(`wait ${next.delayMs}ms before trying again`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
driver.snapshot();
|
|
92
|
+
driver.reset();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Schedule combinators
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import {
|
|
99
|
+
Schedule,
|
|
100
|
+
andThen,
|
|
101
|
+
elapsed,
|
|
102
|
+
exponential,
|
|
103
|
+
fibonacci,
|
|
104
|
+
fixed,
|
|
105
|
+
forever,
|
|
106
|
+
jitter,
|
|
107
|
+
jittered,
|
|
108
|
+
linear,
|
|
109
|
+
maxDelay,
|
|
110
|
+
maxElapsed,
|
|
111
|
+
never,
|
|
112
|
+
once,
|
|
113
|
+
recurs,
|
|
114
|
+
spaced,
|
|
115
|
+
take,
|
|
116
|
+
tapDecision,
|
|
117
|
+
untilInput,
|
|
118
|
+
untilOutput,
|
|
119
|
+
windowed,
|
|
120
|
+
whileOutput,
|
|
121
|
+
} from "brass-runtime";
|
|
122
|
+
|
|
123
|
+
// Fixed delay between retries
|
|
124
|
+
const fixed5s = fixed(5000);
|
|
125
|
+
|
|
126
|
+
// Alias for fixed delay
|
|
127
|
+
const every5s = spaced(5000);
|
|
128
|
+
|
|
129
|
+
// One-shot / forever / never policies
|
|
130
|
+
const oneRetry = once();
|
|
131
|
+
const always = forever();
|
|
132
|
+
const stop = never();
|
|
133
|
+
|
|
134
|
+
// Exponential: 100ms, 200ms, 400ms, 800ms... capped at 10s
|
|
135
|
+
const expo = exponential(100, 10_000);
|
|
136
|
+
|
|
137
|
+
// Linear: 100ms, 200ms, 300ms... capped at 10s
|
|
138
|
+
const lin = linear(100, 10_000);
|
|
139
|
+
|
|
140
|
+
// Fibonacci: 100ms, 100ms, 200ms, 300ms, 500ms... capped at 10s
|
|
141
|
+
const fib = fibonacci(100, 10_000);
|
|
142
|
+
|
|
143
|
+
// Full jitter: random in [0, exponential_delay]
|
|
144
|
+
const fullJitter = jittered(100, 10_000);
|
|
145
|
+
|
|
146
|
+
// Jitter any schedule by +/-20%
|
|
147
|
+
const softJitter = jitter(expo, { factor: 0.2 });
|
|
148
|
+
|
|
149
|
+
// Stop after N attempts
|
|
150
|
+
const limited = take(expo, 5);
|
|
151
|
+
|
|
152
|
+
// Cap delay or total elapsed runtime-clock budget
|
|
153
|
+
const cappedDelay = maxDelay(expo, 2_000);
|
|
154
|
+
const budgeted = maxElapsed(expo, 30_000);
|
|
155
|
+
const elapsedOnly = elapsed(30_000);
|
|
156
|
+
|
|
157
|
+
// Reset schedule state when the burst window expires
|
|
158
|
+
const rolling = windowed(recurs(3), 60_000);
|
|
159
|
+
|
|
160
|
+
// Stop based on inputs or outputs
|
|
161
|
+
const untilReady = untilInput<{ status: string }>((input) => input.status === "ready");
|
|
162
|
+
const whileSmall = whileOutput(linear(10), (attempt) => attempt < 5);
|
|
163
|
+
const untilAttempt5 = untilOutput(linear(10), (attempt) => attempt >= 5);
|
|
164
|
+
|
|
165
|
+
// Attach observability without changing retry semantics
|
|
166
|
+
const observed = tapDecision(Schedule.named("db.retry", expo), (event) => {
|
|
167
|
+
console.log(event.name, event.attempt, event.decision.delayMs);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// First use fast retries, then switch to slow
|
|
171
|
+
const staged = andThen(take(fixed(100), 3), exponential(1000, 30_000));
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Schedules also plug into HTTP retry middleware, so retry, polling, and
|
|
175
|
+
supervisor restarts can share the same timing model:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { Schedule, exponential, jitter } from "brass-runtime";
|
|
179
|
+
import { withRetry } from "brass-runtime/http";
|
|
180
|
+
|
|
181
|
+
const retry = withRetry({
|
|
182
|
+
maxRetries: 5,
|
|
183
|
+
baseDelayMs: 100,
|
|
184
|
+
maxDelayMs: 5_000,
|
|
185
|
+
schedule: Schedule.named(
|
|
186
|
+
"users-api.retry",
|
|
187
|
+
jitter(exponential(100, 5_000), { factor: 0.2 }),
|
|
188
|
+
),
|
|
189
|
+
onScheduleDecision: (event) => {
|
|
190
|
+
console.log(event.name, event.attempt, event.decision.delayMs);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Repeat (not just retry)
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { repeatWithSchedule, fixed } from "brass-runtime";
|
|
199
|
+
|
|
200
|
+
// Poll every 5 seconds
|
|
201
|
+
const poller = repeatWithSchedule(checkStatus(), fixed(5000));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## With Circuit Breaker
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { makeCircuitBreaker, retryWithBackoff } from "brass-runtime";
|
|
208
|
+
|
|
209
|
+
const breaker = makeCircuitBreaker({ failureThreshold: 5 });
|
|
210
|
+
|
|
211
|
+
const resilient = retryWithBackoff(
|
|
212
|
+
breaker.protect(callService()),
|
|
213
|
+
{ maxRetries: 3, shouldRetry: (e) => e._tag !== "CircuitBreakerOpen" }
|
|
214
|
+
);
|
|
215
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Semaphore & Rate Limiting
|
|
2
|
+
|
|
3
|
+
Control concurrency with counting semaphores.
|
|
4
|
+
|
|
5
|
+
## Basic usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { makeSemaphore } from "brass-runtime";
|
|
9
|
+
|
|
10
|
+
// Allow at most 5 concurrent operations
|
|
11
|
+
const sem = makeSemaphore(5);
|
|
12
|
+
|
|
13
|
+
// Automatic acquire/release
|
|
14
|
+
const result = await run(sem.withPermit(callExternalApi()));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Rate limiting API calls
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const apiLimiter = makeSemaphore(10); // max 10 concurrent requests
|
|
21
|
+
|
|
22
|
+
async function fetchAll(urls: string[]) {
|
|
23
|
+
return Promise.all(
|
|
24
|
+
urls.map(url => run(apiLimiter.withPermit(fetch(url))))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Database connection limiting
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const connPool = makeSemaphore(20); // max 20 DB connections
|
|
33
|
+
|
|
34
|
+
const query = (sql: string) =>
|
|
35
|
+
connPool.withPermit(
|
|
36
|
+
bracket(
|
|
37
|
+
openConnection(),
|
|
38
|
+
(conn) => conn.execute(sql),
|
|
39
|
+
(conn) => conn.release()
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Manual acquire/release
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const sem = makeSemaphore(1); // mutex
|
|
48
|
+
|
|
49
|
+
await run(sem.acquire());
|
|
50
|
+
try {
|
|
51
|
+
// Critical section — only one fiber at a time
|
|
52
|
+
await doExclusiveWork();
|
|
53
|
+
} finally {
|
|
54
|
+
sem.release();
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Monitoring
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const sem = makeSemaphore(10);
|
|
62
|
+
|
|
63
|
+
console.log(sem.available()); // permits left (0-10)
|
|
64
|
+
console.log(sem.waiting()); // fibers waiting for a permit
|
|
65
|
+
console.log(sem.capacity); // total permits (10)
|
|
66
|
+
```
|