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,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
|
+
```
|