brass-runtime 1.16.0 → 1.16.1
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 +283 -18
- 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-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-74ZTY6CP.js +2871 -0
- package/dist/chunk-76YMRMH2.cjs +777 -0
- package/dist/chunk-7CMJS3QE.mjs +2871 -0
- package/dist/{chunk-2WC63LJK.mjs → chunk-7JIJOVCT.js} +20 -10
- package/dist/{chunk-FM4W4QPL.js → chunk-A2OM6NEH.mjs} +5 -4
- package/dist/chunk-AGR5B2BC.cjs +683 -0
- package/dist/chunk-AVNQLJ5V.js +777 -0
- package/dist/chunk-B33ICAKP.js +313 -0
- package/dist/{chunk-J3H54ZRV.mjs → chunk-B5JD23U7.mjs} +1 -1
- package/dist/chunk-BABBZK4Y.js +2024 -0
- package/dist/{chunk-U5KWK3PX.mjs → chunk-C3MDXTRZ.js} +11 -0
- package/dist/{chunk-F5EUMJL7.mjs → chunk-CIZFIMK5.js} +55 -5
- package/dist/{chunk-SPUEME2B.cjs → chunk-CZIVE6NT.cjs} +12 -1
- package/dist/{chunk-TDVMADDN.js → chunk-DNFJLJMW.mjs} +11 -0
- package/dist/chunk-DNFO2EIZ.mjs +777 -0
- package/dist/{chunk-XDZOO4L5.js → chunk-EJ6BPYVR.mjs} +79 -17
- package/dist/{chunk-JNFRRJYH.cjs → chunk-ENKODRU3.cjs} +242 -192
- package/dist/chunk-EOC4UHBS.mjs +229 -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-HLWLMW2F.mjs +2024 -0
- 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-7XOPAB5Q.js → chunk-MT3OWDPC.mjs} +55 -5
- 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-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-VN44DYYT.cjs +2024 -0
- 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 +16 -531
- package/dist/observability/index.d.ts +81 -8
- package/dist/observability/index.js +23 -538
- package/dist/observability/index.mjs +23 -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-GJPg8ZSG.d.ts} +4 -3
- 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 +63 -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 +336 -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/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/observability-collector-smoke.md +31 -0
- package/docs/observability-framework-examples.md +98 -0
- package/docs/observability.md +542 -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,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
|
+
```
|
|
@@ -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
|