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,117 @@
1
+ # Streams & Pipelines
2
+
3
+ brass-runtime provides ZIO-style streams with automatic fusion for high performance.
4
+
5
+ ## Creating streams
6
+
7
+ ```ts
8
+ import { fromArray, emptyStream, fromPull } from "brass-runtime";
9
+
10
+ // From an array (optimized: uses FromArray node, no per-element overhead)
11
+ const numbers = fromArray([1, 2, 3, 4, 5]);
12
+
13
+ // Empty stream
14
+ const empty = emptyStream();
15
+ ```
16
+
17
+ ## Pipelines (transformers)
18
+
19
+ Pipelines are reusable stream transformers:
20
+
21
+ ```ts
22
+ import { mapP, filterP, takeP, dropP, andThen, via } from "brass-runtime";
23
+
24
+ // Single operators
25
+ const doubled = mapP((x: number) => x * 2);
26
+ const evens = filterP((x: number) => x % 2 === 0);
27
+ const first10 = takeP(10);
28
+ const skipFirst5 = dropP(5);
29
+
30
+ // Compose pipelines (auto-fused!)
31
+ const pipeline = andThen(
32
+ andThen(doubled, evens),
33
+ first10
34
+ );
35
+
36
+ // Apply to a stream
37
+ const result = await run(collectStream(via(fromArray(data), pipeline)));
38
+ ```
39
+
40
+ ## Stream Fusion
41
+
42
+ When you compose pure operators with `andThen`, brass-runtime automatically fuses them into a single loop — no intermediate fibers, no per-element scheduling overhead.
43
+
44
+ ```ts
45
+ // This pipeline:
46
+ const pipeline = andThen(
47
+ andThen(mapP(x => x * 2), filterP(x => x > 10)),
48
+ takeP(100)
49
+ );
50
+
51
+ // Is executed as a single tight loop equivalent to:
52
+ // for (const x of input) {
53
+ // const mapped = x * 2;
54
+ // if (mapped > 10) { output.push(mapped); if (output.length >= 100) break; }
55
+ // }
56
+ ```
57
+
58
+ **Performance:** 10,000 elements through map+filter in **0.25ms** (vs 84ms without fusion).
59
+
60
+ ## Stream Operators
61
+
62
+ ```ts
63
+ import { zip, zipWith, scan, interleave, take, drop, throttle, debounce } from "brass-runtime";
64
+
65
+ // Zip two streams element-by-element
66
+ const pairs = zip(names, ages); // ["Alice", 30], ["Bob", 25], ...
67
+
68
+ // Running accumulator
69
+ const runningSum = scan(numbers, 0, (acc, n) => acc + n);
70
+ // 0, 1, 3, 6, 10, 15, ...
71
+
72
+ // Interleave (alternate elements)
73
+ const mixed = interleave(evens, odds);
74
+
75
+ // Rate limiting
76
+ const throttled = throttle(events, 1000); // max 1 per second
77
+ const debounced = debounce(keystrokes, 300); // emit after 300ms silence
78
+ ```
79
+
80
+ ## Collecting results
81
+
82
+ ```ts
83
+ import { collectStream } from "brass-runtime";
84
+
85
+ // Collect all elements into an array
86
+ const all = await run(collectStream(stream));
87
+
88
+ // With pipeline
89
+ const filtered = await run(collectStream(via(stream, pipeline)));
90
+ ```
91
+
92
+ ## Queue-based streams
93
+
94
+ ```ts
95
+ import { bounded } from "brass-runtime";
96
+
97
+ const queue = await run(bounded(100, "backpressure"));
98
+
99
+ // Producer
100
+ for (const item of items) {
101
+ await run(queue.offer(item));
102
+ }
103
+
104
+ // Consumer
105
+ const value = await run(queue.take());
106
+
107
+ // Batch operations
108
+ const results = await run(queue.offerBatch(items));
109
+ const batch = await run(queue.takeBatch(50));
110
+ ```
111
+
112
+ ## Performance tips
113
+
114
+ 1. **Use `andThen` for composition** — triggers automatic fusion
115
+ 2. **Use `fromArray` for known data** — uses optimized FromArray node
116
+ 3. **Use `via` instead of calling pipeline directly** — enables fusion fast-path
117
+ 4. **Batch queue operations** — `offerBatch`/`takeBatch` for bulk work
@@ -0,0 +1,98 @@
1
+ # Supervisors
2
+
3
+ Supervisors let a runtime own groups of child fibers with explicit restart and
4
+ escalation policy. Use them for long-lived workers, polling loops, connection
5
+ refreshers, and other background work that should fail in a predictable shape.
6
+
7
+ ```ts
8
+ import { Runtime, fixed, joinSupervised, makeSupervisor } from "brass-runtime";
9
+
10
+ const runtime = Runtime.make({});
11
+ const supervisor = makeSupervisor(runtime, {
12
+ strategy: "one-for-one",
13
+ restart: {
14
+ mode: "on-failure",
15
+ maxRestarts: 5,
16
+ withinMs: 60_000,
17
+ schedule: fixed(250),
18
+ },
19
+ });
20
+
21
+ const worker = supervisor.start({
22
+ name: "token-refresh",
23
+ effect: () => refreshTokenLoop(),
24
+ });
25
+
26
+ await runtime.toPromise(joinSupervised(worker));
27
+ ```
28
+
29
+ ## Strategies
30
+
31
+ - `one-for-one`: restart only the child that failed.
32
+ - `all-for-one`: interrupt and restart siblings when one child fails.
33
+
34
+ ## Restart Policy
35
+
36
+ `restart` can be `"never"`, `"always"`, `"on-failure"`, or an object:
37
+
38
+ ```ts
39
+ const supervisor = makeSupervisor(runtime, {
40
+ restart: {
41
+ mode: "on-failure",
42
+ maxRestarts: 10,
43
+ withinMs: 30_000,
44
+ delayMs: ({ restartCount }) => Math.min(1_000, restartCount * 100),
45
+ },
46
+ });
47
+ ```
48
+
49
+ For reusable timing behavior, pass a `Schedule`:
50
+
51
+ ```ts
52
+ import { exponential, jitter, makeSupervisor } from "brass-runtime";
53
+
54
+ const supervisor = makeSupervisor(runtime, {
55
+ restart: {
56
+ mode: "on-failure",
57
+ schedule: jitter(exponential(100, 5_000), { factor: 0.2 }),
58
+ },
59
+ });
60
+ ```
61
+
62
+ ## Escalation
63
+
64
+ When the restart budget is exhausted, escalation controls what happens next:
65
+
66
+ - `shutdown` interrupts sibling fibers.
67
+ - `ignore` completes only the failed child and leaves siblings running.
68
+
69
+ ```ts
70
+ const supervisor = makeSupervisor(runtime, {
71
+ strategy: "all-for-one",
72
+ restart: { mode: "on-failure", maxRestarts: 3 },
73
+ escalation: "shutdown",
74
+ });
75
+ ```
76
+
77
+ ## Observability
78
+
79
+ Supervisors emit runtime events through the existing `RuntimeHooks` pipeline:
80
+
81
+ - `supervisor.child.start`
82
+ - `supervisor.child.end`
83
+ - `supervisor.child.restart`
84
+ - `supervisor.child.escalate`
85
+ - `supervisor.shutdown`
86
+
87
+ Attach an `EventBus`, metrics sink, structured logger, or observability preset
88
+ the same way you do for fibers/scopes.
89
+
90
+ ## Shutdown
91
+
92
+ Call `shutdown()` during graceful process termination. It cancels pending
93
+ restart timers, interrupts running children, and completes when current child
94
+ fibers have observed interruption.
95
+
96
+ ```ts
97
+ await runtime.toPromise(supervisor.shutdown());
98
+ ```
@@ -0,0 +1,162 @@
1
+ # Testing Utilities
2
+
3
+ brass-runtime provides helpers for testing effects deterministically.
4
+
5
+ ## TestRuntime
6
+
7
+ ```ts
8
+ import { makeTestRuntime } from "brass-runtime";
9
+
10
+ const { runtime, run, runExit, clock, advance, flushAll } = makeTestRuntime();
11
+
12
+ // run() returns the value (throws on failure)
13
+ const value = await run(myEffect);
14
+
15
+ // runExit() returns the full Exit (never throws)
16
+ const exit = await runExit(myEffect);
17
+ if (exit._tag === "Success") console.log(exit.value);
18
+ else console.log(exit.cause);
19
+ ```
20
+
21
+ `makeTestRuntime()` uses the TypeScript runtime engine with:
22
+
23
+ - `TestScheduler`, a deterministic scheduler you can inspect and flush.
24
+ - `TestClock`, a virtual clock used by `sleep`, `timeout`, retry backoff,
25
+ `delayedEffect`, and `Runtime.delay`.
26
+ - The same fiber interpreter as production TS mode.
27
+
28
+ The scheduler auto-flushes by default for ergonomic tests. Disable that when
29
+ you need to inspect queued work:
30
+
31
+ ```ts
32
+ import { makeTestRuntime, succeed } from "brass-runtime";
33
+
34
+ const { runtime, scheduler, flushAll } = makeTestRuntime({}, { autoFlush: false });
35
+
36
+ const pending = runtime.toPromise(succeed("ok"));
37
+ expect(scheduler.size()).toBe(1);
38
+
39
+ flushAll();
40
+ await expect(pending).resolves.toBe("ok");
41
+ ```
42
+
43
+ ## Virtual Time
44
+
45
+ ```ts
46
+ import { makeTestRuntime, sleep, timeout, neverEffect } from "brass-runtime";
47
+
48
+ const { run, runExit, clock, advance } = makeTestRuntime();
49
+
50
+ const sleeping = run(sleep(1_000));
51
+ expect(clock.pendingTimers()).toHaveLength(1);
52
+
53
+ advance(1_000);
54
+ await sleeping;
55
+
56
+ const timedOut = runExit(timeout(neverEffect(), 50));
57
+ advance(50);
58
+ expect(await timedOut).toMatchObject({
59
+ _tag: "Failure",
60
+ cause: { _tag: "Fail", error: { _tag: "TimeoutError", ms: 50 } },
61
+ });
62
+ ```
63
+
64
+ ## Assertion helpers
65
+
66
+ ```ts
67
+ import { assertSucceeds, assertFails, assertFailsWith, assertCompletesWithin } from "brass-runtime";
68
+
69
+ // Assert success with specific value
70
+ await assertSucceeds(myEffect, 42);
71
+
72
+ // Assert failure with specific error
73
+ await assertFails(myEffect, "not found");
74
+
75
+ // Assert failure matching a predicate
76
+ await assertFailsWith(myEffect, (e) => e._tag === "NetworkError");
77
+
78
+ // Assert effect completes within time limit
79
+ const result = await assertCompletesWithin(myEffect, 100); // max 100ms
80
+ ```
81
+
82
+ ## Test effect builders
83
+
84
+ ```ts
85
+ import { flakyEffect, delayedEffect, neverEffect } from "brass-runtime";
86
+
87
+ // Fails N times, then succeeds (for testing retry)
88
+ const flaky = flakyEffect(3, "success!", "temporary error");
89
+ // Call 1: fails with "temporary error"
90
+ // Call 2: fails with "temporary error"
91
+ // Call 3: fails with "temporary error"
92
+ // Call 4: succeeds with "success!"
93
+
94
+ // Completes after a delay (for testing timeouts)
95
+ const slow = delayedEffect(500, "done");
96
+
97
+ // Never completes (for testing interruption)
98
+ const hanging = neverEffect();
99
+ ```
100
+
101
+ ## Testing retry logic
102
+
103
+ ```ts
104
+ import { flakyEffect, retryN, makeTestRuntime } from "brass-runtime";
105
+
106
+ const { run } = makeTestRuntime();
107
+
108
+ it("retries and eventually succeeds", async () => {
109
+ const effect = retryN(flakyEffect(2, "ok", "fail"), 3);
110
+ const result = await run(effect);
111
+ expect(result).toBe("ok");
112
+ });
113
+
114
+ it("fails after exhausting retries", async () => {
115
+ const effect = retryN(flakyEffect(10, "ok", "fail"), 2);
116
+ const exit = await runExit(effect);
117
+ expect(exit._tag).toBe("Failure");
118
+ });
119
+ ```
120
+
121
+ ## Testing timeouts
122
+
123
+ ```ts
124
+ import { timeout, delayedEffect, neverEffect, makeTestRuntime } from "brass-runtime";
125
+
126
+ const { run, advance } = makeTestRuntime();
127
+
128
+ it("succeeds before timeout", async () => {
129
+ const result = run(timeout(delayedEffect(10, "fast"), 1000));
130
+ advance(10);
131
+ await expect(result).resolves.toBe("fast");
132
+ });
133
+
134
+ it("times out on slow effect", async () => {
135
+ const result = run(timeout(neverEffect(), 50));
136
+ advance(50);
137
+ await expect(result).rejects.toMatchObject({ _tag: "TimeoutError", ms: 50 });
138
+ });
139
+ ```
140
+
141
+ ## Testing concurrency
142
+
143
+ ```ts
144
+ import { makeSemaphore, makeTestRuntime, delayedEffect } from "brass-runtime";
145
+
146
+ const { run } = makeTestRuntime();
147
+
148
+ it("limits concurrency", async () => {
149
+ const sem = makeSemaphore(2);
150
+ let maxConcurrent = 0;
151
+ let current = 0;
152
+
153
+ const task = sem.withPermit(async((_env, cb) => {
154
+ current++;
155
+ maxConcurrent = Math.max(maxConcurrent, current);
156
+ setTimeout(() => { current--; cb({ _tag: "Success", value: undefined }); }, 10);
157
+ }));
158
+
159
+ await Promise.all([run(task), run(task), run(task), run(task)]);
160
+ expect(maxConcurrent).toBeLessThanOrEqual(2);
161
+ });
162
+ ```
@@ -0,0 +1,71 @@
1
+ # Tracing
2
+
3
+ OpenTelemetry-compatible span generation for effects.
4
+
5
+ ## Setup
6
+
7
+ ```ts
8
+ import { makeTracer } from "brass-runtime";
9
+
10
+ const tracer = makeTracer({
11
+ serviceName: "my-api",
12
+ sampleRate: 1.0, // sample everything (use 0.1 for 10% in production)
13
+ onSpanEnd: (span) => {
14
+ // Export to your backend (Jaeger, Zipkin, etc.)
15
+ console.log(`[${span.status}] ${span.name}: ${span.endTime! - span.startTime}ms`);
16
+ },
17
+ });
18
+ ```
19
+
20
+ ## Wrapping effects in spans
21
+
22
+ ```ts
23
+ // Wrap any effect in a span
24
+ const result = await run(
25
+ tracer.span("fetchUser", fetchUser(userId), { userId })
26
+ );
27
+
28
+ // Nested spans
29
+ const result = await run(
30
+ tracer.span("handleRequest", asyncFlatMap(
31
+ tracer.span("validateInput", validate(input)),
32
+ (valid) => tracer.span("processData", process(valid))
33
+ ))
34
+ );
35
+ ```
36
+
37
+ ## Attributes
38
+
39
+ ```ts
40
+ tracer.span("db.query", dbQuery(sql), {
41
+ "db.system": "postgresql",
42
+ "db.statement": sql,
43
+ "db.name": "users",
44
+ });
45
+ ```
46
+
47
+ ## Inspecting spans (testing)
48
+
49
+ ```ts
50
+ const tracer = makeTracer({ serviceName: "test" });
51
+
52
+ await run(tracer.span("myOp", asyncSucceed(42)));
53
+
54
+ const spans = tracer.spans();
55
+ expect(spans[0].name).toBe("myOp");
56
+ expect(spans[0].status).toBe("ok");
57
+ expect(spans[0].endTime! - spans[0].startTime).toBeLessThan(10);
58
+
59
+ tracer.clear(); // reset for next test
60
+ ```
61
+
62
+ ## Error spans
63
+
64
+ ```ts
65
+ await run(tracer.span("failingOp", asyncFail("oops"))).catch(() => {});
66
+
67
+ const span = tracer.spans()[0];
68
+ expect(span.status).toBe("error");
69
+ expect(span.events[0].name).toBe("error");
70
+ expect(span.events[0].attributes!["error.message"]).toBe("oops");
71
+ ```