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.
Files changed (219) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +287 -23
  3. package/dist/agent/cli/main.cjs +38 -38
  4. package/dist/agent/cli/main.js +6 -6
  5. package/dist/agent/cli/main.mjs +6 -6
  6. package/dist/agent/index.cjs +7 -7
  7. package/dist/agent/index.d.ts +1 -1
  8. package/dist/agent/index.js +6 -6
  9. package/dist/agent/index.mjs +6 -6
  10. package/dist/chunk-2HQTDLHF.mjs +683 -0
  11. package/dist/chunk-36I3M4UC.mjs +370 -0
  12. package/dist/{chunk-QY5FKYEQ.js → chunk-3AYM6WPJ.js} +570 -51
  13. package/dist/chunk-3LOYJFRR.cjs +300 -0
  14. package/dist/chunk-3Y2RIUMM.js +300 -0
  15. package/dist/{chunk-7XOPAB5Q.js → chunk-4P2HHGAX.mjs} +83 -5
  16. package/dist/{chunk-N6VHMOWB.cjs → chunk-4ROBZFL6.cjs} +128 -128
  17. package/dist/{chunk-NC5SDRYE.js → chunk-52OB2ROS.js} +4 -4
  18. package/dist/{chunk-JX3LZQJH.cjs → chunk-52PPNNI4.cjs} +82 -20
  19. package/dist/{chunk-5YOQOXEQ.cjs → chunk-5EC274J5.cjs} +676 -293
  20. package/dist/chunk-5QC7LRZ3.js +229 -0
  21. package/dist/{chunk-7TL2LHQJ.js → chunk-5VRJNBLZ.mjs} +524 -141
  22. package/dist/chunk-62AZW6UT.cjs +313 -0
  23. package/dist/chunk-6IXXWIUM.js +683 -0
  24. package/dist/chunk-6RY2FFN4.mjs +2024 -0
  25. package/dist/chunk-74ZTY6CP.js +2871 -0
  26. package/dist/chunk-7CMJS3QE.mjs +2871 -0
  27. package/dist/{chunk-2WC63LJK.mjs → chunk-7JIJOVCT.js} +20 -10
  28. package/dist/chunk-7X3K5RMS.js +2024 -0
  29. package/dist/chunk-7ZPEZ57L.cjs +2024 -0
  30. package/dist/{chunk-FM4W4QPL.js → chunk-A2OM6NEH.mjs} +5 -4
  31. package/dist/chunk-AGR5B2BC.cjs +683 -0
  32. package/dist/chunk-B33ICAKP.js +313 -0
  33. package/dist/{chunk-J3H54ZRV.mjs → chunk-B5JD23U7.mjs} +1 -1
  34. package/dist/{chunk-F5EUMJL7.mjs → chunk-BKK77SBA.js} +83 -5
  35. package/dist/{chunk-U5KWK3PX.mjs → chunk-C3MDXTRZ.js} +11 -0
  36. package/dist/{chunk-SPUEME2B.cjs → chunk-CZIVE6NT.cjs} +12 -1
  37. package/dist/{chunk-TDVMADDN.js → chunk-DNFJLJMW.mjs} +11 -0
  38. package/dist/{chunk-XDZOO4L5.js → chunk-EJ6BPYVR.mjs} +79 -17
  39. package/dist/chunk-EOC4UHBS.mjs +229 -0
  40. package/dist/chunk-F6XWZQY4.cjs +777 -0
  41. package/dist/{chunk-7LVI2GIN.js → chunk-FH2X7BVP.js} +507 -72
  42. package/dist/{chunk-OOGJ73B6.js → chunk-FHQGHPMO.mjs} +20 -10
  43. package/dist/{chunk-WQ5QNU5R.cjs → chunk-GLE2WY7Z.cjs} +652 -217
  44. package/dist/{chunk-G6IQOE4P.mjs → chunk-GYM3LLGS.mjs} +507 -72
  45. package/dist/{chunk-TVN5I4U6.cjs → chunk-JF5WGYJJ.cjs} +25 -24
  46. package/dist/{chunk-CY33PGEX.mjs → chunk-KH4SYAOS.mjs} +570 -51
  47. package/dist/chunk-KN32XNTH.mjs +313 -0
  48. package/dist/chunk-KQLYONSE.cjs +2871 -0
  49. package/dist/{chunk-7HUOJA4W.cjs → chunk-KZJQ723N.cjs} +90 -80
  50. package/dist/{chunk-CCKHV5BT.mjs → chunk-L2SYFEBS.js} +5 -4
  51. package/dist/{chunk-IJT6RRQ5.cjs → chunk-L6VB5N7Q.cjs} +20 -9
  52. package/dist/{chunk-ZGLD4TVZ.mjs → chunk-MBEJI5HF.mjs} +4 -4
  53. package/dist/{chunk-PRWCB3QL.mjs → chunk-MIIYDLGM.js} +524 -141
  54. package/dist/{chunk-H55LI6WY.js → chunk-MOO4L7F4.mjs} +15 -4
  55. package/dist/chunk-MVGUEJ5Z.cjs +370 -0
  56. package/dist/chunk-PD4EJTQC.cjs +229 -0
  57. package/dist/chunk-PWC3RBQE.mjs +300 -0
  58. package/dist/{chunk-MWXMNYJS.cjs → chunk-Q2I37RP3.cjs} +643 -124
  59. package/dist/{chunk-VFIUZG7J.mjs → chunk-RKGKFN2A.js} +79 -17
  60. package/dist/{chunk-NYL4D7SK.cjs → chunk-SA6HUJVI.cjs} +5 -5
  61. package/dist/chunk-SK7UZRNI.mjs +777 -0
  62. package/dist/{chunk-K2T3DV26.mjs → chunk-TRM4JUZQ.js} +15 -4
  63. package/dist/chunk-UB4B6OFY.js +370 -0
  64. package/dist/{chunk-G3XGCZDQ.js → chunk-UCUBNWM2.js} +1 -1
  65. package/dist/chunk-VWIPB6I5.js +777 -0
  66. package/dist/{chunk-JNFRRJYH.cjs → chunk-WBGRHGBP.cjs} +270 -192
  67. package/dist/{client-CtFmoDvM.d.ts → client-CZHU674n.d.ts} +211 -36
  68. package/dist/core/index.cjs +135 -9
  69. package/dist/core/index.d.ts +238 -33
  70. package/dist/core/index.js +155 -29
  71. package/dist/core/index.mjs +155 -29
  72. package/dist/{effect-CGNl5Rqp.d.ts → effect-DIUHZ9IN.d.ts} +89 -1
  73. package/dist/effectRunner-CFLC32IK.cjs +8 -0
  74. package/dist/{effectRunner-A4CHJXJI.js → effectRunner-L4S7IPT3.js} +2 -2
  75. package/dist/{effectRunner-OPUF6QRN.mjs → effectRunner-NNGG75QA.mjs} +2 -2
  76. package/dist/http/index.cjs +324 -2986
  77. package/dist/http/index.d.ts +54 -68
  78. package/dist/http/index.js +238 -2900
  79. package/dist/http/index.mjs +238 -2900
  80. package/dist/http/testing.cjs +14 -12
  81. package/dist/http/testing.d.ts +5 -4
  82. package/dist/http/testing.js +10 -8
  83. package/dist/http/testing.mjs +10 -8
  84. package/dist/index.cjs +423 -255
  85. package/dist/index.d.ts +87 -69
  86. package/dist/index.js +301 -133
  87. package/dist/index.mjs +301 -133
  88. package/dist/observability/index.cjs +18 -531
  89. package/dist/observability/index.d.ts +81 -8
  90. package/dist/observability/index.js +25 -538
  91. package/dist/observability/index.mjs +25 -538
  92. package/dist/perf/cli.cjs +401 -0
  93. package/dist/perf/cli.d.ts +1 -0
  94. package/dist/perf/cli.js +401 -0
  95. package/dist/perf/cli.mjs +401 -0
  96. package/dist/perf/index.cjs +141 -0
  97. package/dist/perf/index.d.ts +483 -0
  98. package/dist/perf/index.js +141 -0
  99. package/dist/perf/index.mjs +141 -0
  100. package/dist/schedule-CK3Ml_7p.d.ts +259 -0
  101. package/dist/schema/index.cjs +6 -2
  102. package/dist/schema/index.d.ts +3 -1
  103. package/dist/schema/index.js +5 -1
  104. package/dist/schema/index.mjs +5 -1
  105. package/dist/{server-C8hDXA74.d.ts → server-D6JZ15_e.d.ts} +16 -4
  106. package/dist/{stream-dvSs0QS5.d.ts → stream-B4oK9JFP.d.ts} +1 -1
  107. package/dist/{tracer-B5tRH9H7.d.ts → tracer-Hwt1cl7h.d.ts} +13 -54
  108. package/dist/{tracing-Dt9S_6V8.d.ts → tracing-DqbTKGcf.d.ts} +1 -1
  109. package/docs/ARCHITECTURE.md +292 -0
  110. package/docs/README.md +65 -0
  111. package/docs/adr/0001-ai-context-pack.md +32 -0
  112. package/docs/agent-apply-mode.md +104 -0
  113. package/docs/agent-approvals.md +110 -0
  114. package/docs/agent-batch.md +185 -0
  115. package/docs/agent-boundaries.md +112 -0
  116. package/docs/agent-chat-sessions.md +160 -0
  117. package/docs/agent-ci.md +17 -0
  118. package/docs/agent-cli.md +405 -0
  119. package/docs/agent-config.md +480 -0
  120. package/docs/agent-context-discovery.md +159 -0
  121. package/docs/agent-copilot-like-dx.md +126 -0
  122. package/docs/agent-declarative-optimized-planning.md +138 -0
  123. package/docs/agent-dx.md +224 -0
  124. package/docs/agent-env-files.md +126 -0
  125. package/docs/agent-follow-up-context.md +43 -0
  126. package/docs/agent-global-usage.md +180 -0
  127. package/docs/agent-init.md +109 -0
  128. package/docs/agent-install-and-configure.md +516 -0
  129. package/docs/agent-language-workspace-ux.md +99 -0
  130. package/docs/agent-llm-adapters.md +123 -0
  131. package/docs/agent-local-install.md +190 -0
  132. package/docs/agent-local-tests.md +51 -0
  133. package/docs/agent-observability.md +155 -0
  134. package/docs/agent-patch-quality-loop.md +162 -0
  135. package/docs/agent-presets.md +22 -0
  136. package/docs/agent-project-commands.md +237 -0
  137. package/docs/agent-project-intelligence.md +156 -0
  138. package/docs/agent-redaction.md +18 -0
  139. package/docs/agent-release-readiness.md +76 -0
  140. package/docs/agent-rollback-safety.md +162 -0
  141. package/docs/agent-rollback.md +23 -0
  142. package/docs/agent-run-artifacts.md +16 -0
  143. package/docs/agent-vscode-auto-discovery.md +137 -0
  144. package/docs/agent-vscode-batch-runner.md +100 -0
  145. package/docs/agent-vscode-chat-layout.md +90 -0
  146. package/docs/agent-vscode-clean-install.md +147 -0
  147. package/docs/agent-vscode-code-actions.md +70 -0
  148. package/docs/agent-vscode-diff-preview.md +45 -0
  149. package/docs/agent-vscode-inline-assist.md +56 -0
  150. package/docs/agent-vscode-install.md +186 -0
  151. package/docs/agent-vscode-model-setup.md +97 -0
  152. package/docs/agent-vscode-patch-preview.md +92 -0
  153. package/docs/agent-vscode-problems.md +79 -0
  154. package/docs/agent-vscode-project-dashboard.md +106 -0
  155. package/docs/agent-vscode-run-history.md +92 -0
  156. package/docs/agent-vscode-ux.md +73 -0
  157. package/docs/ai/INVARIANTS.md +84 -0
  158. package/docs/ai/PROJECT_MAP.md +338 -0
  159. package/docs/ai/PUBLIC_API.md +339 -0
  160. package/docs/ai/VALIDATION_MATRIX.md +67 -0
  161. package/docs/api-polish.md +37 -0
  162. package/docs/cancellation.md +162 -0
  163. package/docs/coverage.md +46 -0
  164. package/docs/framework-integrations.md +38 -0
  165. package/docs/frameworks/angular.md +153 -0
  166. package/docs/frameworks/express.md +125 -0
  167. package/docs/frameworks/fastify.md +124 -0
  168. package/docs/frameworks/nestjs.md +282 -0
  169. package/docs/frameworks/nextjs.md +147 -0
  170. package/docs/frameworks/react.md +139 -0
  171. package/docs/frameworks/vanilla.md +224 -0
  172. package/docs/getting-started.md +159 -0
  173. package/docs/guides/README.md +40 -0
  174. package/docs/guides/circuit-breaker.md +89 -0
  175. package/docs/guides/error-handling.md +91 -0
  176. package/docs/guides/getting-started.md +107 -0
  177. package/docs/guides/layers.md +189 -0
  178. package/docs/guides/metrics.md +101 -0
  179. package/docs/guides/resource-management.md +141 -0
  180. package/docs/guides/retry.md +215 -0
  181. package/docs/guides/semaphore.md +66 -0
  182. package/docs/guides/streams.md +117 -0
  183. package/docs/guides/supervisors.md +98 -0
  184. package/docs/guides/testing.md +162 -0
  185. package/docs/guides/tracing.md +71 -0
  186. package/docs/http-recipes.md +399 -0
  187. package/docs/http.md +749 -0
  188. package/docs/modules.md +285 -0
  189. package/docs/nestjs.md +6 -0
  190. package/docs/observability-collector-smoke.md +31 -0
  191. package/docs/observability-framework-examples.md +110 -0
  192. package/docs/observability.md +649 -0
  193. package/docs/otel-collector-smoke.yaml +27 -0
  194. package/docs/performance-profiler.md +199 -0
  195. package/docs/production-readiness.md +73 -0
  196. package/docs/recipes/README.md +12 -0
  197. package/docs/recipes/http-server.md +45 -0
  198. package/docs/recipes/layers.md +44 -0
  199. package/docs/recipes/performance.md +47 -0
  200. package/docs/recipes/runtime.md +41 -0
  201. package/docs/recipes/testing.md +41 -0
  202. package/docs/release.md +53 -0
  203. package/docs/wasm-bounded-queues.md +44 -0
  204. package/docs/wasm-engine-observability-benchmarks.md +85 -0
  205. package/docs/wasm-fiber-engine.md +117 -0
  206. package/docs/wasm-scheduler-state-machine.md +122 -0
  207. package/docs/wasm-stream-chunks.md +54 -0
  208. package/package.json +22 -2
  209. package/dist/chunk-45F7OKGT.cjs +0 -104
  210. package/dist/chunk-7V4KY4RL.mjs +0 -104
  211. package/dist/chunk-DJQ7OMMB.cjs +0 -144
  212. package/dist/chunk-GOV47PPB.mjs +0 -552
  213. package/dist/chunk-JF4XXPZ5.cjs +0 -552
  214. package/dist/chunk-KCPT2D6G.js +0 -552
  215. package/dist/chunk-NOYZIMUJ.mjs +0 -144
  216. package/dist/chunk-PNVFW245.js +0 -144
  217. package/dist/chunk-ROJC3NBJ.js +0 -104
  218. package/dist/effectRunner-3ZHAD3LE.cjs +0 -8
  219. 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
+ ```