brass-runtime 1.15.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.
Files changed (209) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +673 -136
  3. package/dist/agent/cli/main.cjs +40 -35
  4. package/dist/agent/cli/main.js +9 -4
  5. package/dist/agent/cli/main.mjs +9 -4
  6. package/dist/agent/index.cjs +8 -4
  7. package/dist/agent/index.d.ts +1 -1
  8. package/dist/agent/index.js +7 -3
  9. package/dist/agent/index.mjs +7 -3
  10. package/dist/chunk-2HQTDLHF.mjs +683 -0
  11. package/dist/chunk-36I3M4UC.mjs +370 -0
  12. package/dist/chunk-3AYM6WPJ.js +1629 -0
  13. package/dist/chunk-3LOYJFRR.cjs +300 -0
  14. package/dist/chunk-3RG5ZIWI.js +10 -0
  15. package/dist/chunk-3Y2RIUMM.js +300 -0
  16. package/dist/{chunk-VEZNF5GZ.cjs → chunk-4ROBZFL6.cjs} +130 -126
  17. package/dist/{chunk-3QMOKAS5.js → chunk-52OB2ROS.js} +9 -5
  18. package/dist/chunk-52PPNNI4.cjs +416 -0
  19. package/dist/chunk-5EC274J5.cjs +2874 -0
  20. package/dist/chunk-5QC7LRZ3.js +229 -0
  21. package/dist/chunk-5VRJNBLZ.mjs +2874 -0
  22. package/dist/chunk-62AZW6UT.cjs +313 -0
  23. package/dist/chunk-6IXXWIUM.js +683 -0
  24. package/dist/chunk-74ZTY6CP.js +2871 -0
  25. package/dist/chunk-76YMRMH2.cjs +777 -0
  26. package/dist/chunk-7CMJS3QE.mjs +2871 -0
  27. package/dist/{chunk-4NHES7VK.mjs → chunk-7JIJOVCT.js} +27 -13
  28. package/dist/chunk-A2OM6NEH.mjs +194 -0
  29. package/dist/chunk-AGR5B2BC.cjs +683 -0
  30. package/dist/chunk-AVNQLJ5V.js +777 -0
  31. package/dist/chunk-B33ICAKP.js +313 -0
  32. package/dist/{chunk-ELOOF35R.mjs → chunk-B5JD23U7.mjs} +1 -1
  33. package/dist/chunk-BABBZK4Y.js +2024 -0
  34. package/dist/chunk-C3MDXTRZ.js +354 -0
  35. package/dist/chunk-CIZFIMK5.js +2193 -0
  36. package/dist/chunk-CZIVE6NT.cjs +354 -0
  37. package/dist/chunk-DNFJLJMW.mjs +354 -0
  38. package/dist/chunk-DNFO2EIZ.mjs +777 -0
  39. package/dist/chunk-EJ6BPYVR.mjs +416 -0
  40. package/dist/chunk-ENKODRU3.cjs +2193 -0
  41. package/dist/chunk-EOC4UHBS.mjs +229 -0
  42. package/dist/{chunk-BMH5AV44.js → chunk-FH2X7BVP.js} +756 -440
  43. package/dist/{chunk-PPUXIH5R.js → chunk-FHQGHPMO.mjs} +27 -13
  44. package/dist/{chunk-TGIFUAK4.cjs → chunk-GLE2WY7Z.cjs} +951 -635
  45. package/dist/{chunk-BDF4AMWX.mjs → chunk-GYM3LLGS.mjs} +756 -440
  46. package/dist/chunk-HLWLMW2F.mjs +2024 -0
  47. package/dist/chunk-JF5WGYJJ.cjs +194 -0
  48. package/dist/chunk-KH4SYAOS.mjs +1629 -0
  49. package/dist/chunk-KN32XNTH.mjs +313 -0
  50. package/dist/chunk-KQLYONSE.cjs +2871 -0
  51. package/dist/{chunk-STVLQ3XD.cjs → chunk-KZJQ723N.cjs} +92 -78
  52. package/dist/chunk-L2SYFEBS.js +194 -0
  53. package/dist/chunk-L6VB5N7Q.cjs +104 -0
  54. package/dist/{chunk-K6M7MDZ4.mjs → chunk-MBEJI5HF.mjs} +9 -5
  55. package/dist/chunk-MIIYDLGM.js +2874 -0
  56. package/dist/chunk-MOO4L7F4.mjs +104 -0
  57. package/dist/chunk-MT3OWDPC.mjs +2193 -0
  58. package/dist/chunk-MVGUEJ5Z.cjs +370 -0
  59. package/dist/chunk-OBGZSXTJ.cjs +10 -0
  60. package/dist/chunk-PD4EJTQC.cjs +229 -0
  61. package/dist/chunk-PWC3RBQE.mjs +300 -0
  62. package/dist/chunk-Q2I37RP3.cjs +1629 -0
  63. package/dist/chunk-RKGKFN2A.js +416 -0
  64. package/dist/{chunk-R3R2FVLG.cjs → chunk-SA6HUJVI.cjs} +5 -5
  65. package/dist/chunk-TRM4JUZQ.js +104 -0
  66. package/dist/chunk-UB4B6OFY.js +370 -0
  67. package/dist/{chunk-TO7IKXYT.js → chunk-UCUBNWM2.js} +1 -1
  68. package/dist/chunk-VN44DYYT.cjs +2024 -0
  69. package/dist/chunk-Y6FXYEAI.mjs +10 -0
  70. package/dist/client-CZHU674n.d.ts +820 -0
  71. package/dist/core/index.cjs +198 -4
  72. package/dist/core/index.d.ts +311 -212
  73. package/dist/core/index.js +237 -43
  74. package/dist/core/index.mjs +237 -43
  75. package/dist/{effect-CMOQKX8y.d.ts → effect-DIUHZ9IN.d.ts} +195 -1
  76. package/dist/effectRunner-CFLC32IK.cjs +8 -0
  77. package/dist/effectRunner-L4S7IPT3.js +8 -0
  78. package/dist/effectRunner-NNGG75QA.mjs +8 -0
  79. package/dist/http/index.cjs +1227 -2971
  80. package/dist/http/index.d.ts +826 -280
  81. package/dist/http/index.js +1089 -2833
  82. package/dist/http/index.mjs +1089 -2833
  83. package/dist/http/testing.cjs +161 -0
  84. package/dist/http/testing.d.ts +43 -0
  85. package/dist/http/testing.js +161 -0
  86. package/dist/http/testing.mjs +161 -0
  87. package/dist/index.cjs +486 -250
  88. package/dist/index.d.ts +87 -95
  89. package/dist/index.js +391 -155
  90. package/dist/index.mjs +391 -155
  91. package/dist/observability/index.cjs +162 -0
  92. package/dist/observability/index.d.ts +152 -0
  93. package/dist/observability/index.js +162 -0
  94. package/dist/observability/index.mjs +162 -0
  95. package/dist/perf/cli.cjs +401 -0
  96. package/dist/perf/cli.d.ts +1 -0
  97. package/dist/perf/cli.js +401 -0
  98. package/dist/perf/cli.mjs +401 -0
  99. package/dist/perf/index.cjs +141 -0
  100. package/dist/perf/index.d.ts +483 -0
  101. package/dist/perf/index.js +141 -0
  102. package/dist/perf/index.mjs +141 -0
  103. package/dist/schedule-CK3Ml_7p.d.ts +259 -0
  104. package/dist/schema/index.cjs +29 -0
  105. package/dist/schema/index.d.ts +179 -0
  106. package/dist/schema/index.js +29 -0
  107. package/dist/schema/index.mjs +29 -0
  108. package/dist/server-GJPg8ZSG.d.ts +675 -0
  109. package/dist/{stream-FQm9h4Mg.d.ts → stream-B4oK9JFP.d.ts} +1 -1
  110. package/dist/tracer-Hwt1cl7h.d.ts +189 -0
  111. package/dist/tracing-DqbTKGcf.d.ts +148 -0
  112. package/docs/ARCHITECTURE.md +292 -0
  113. package/docs/README.md +63 -0
  114. package/docs/adr/0001-ai-context-pack.md +32 -0
  115. package/docs/agent-apply-mode.md +104 -0
  116. package/docs/agent-approvals.md +110 -0
  117. package/docs/agent-batch.md +185 -0
  118. package/docs/agent-boundaries.md +112 -0
  119. package/docs/agent-chat-sessions.md +160 -0
  120. package/docs/agent-ci.md +17 -0
  121. package/docs/agent-cli.md +405 -0
  122. package/docs/agent-config.md +480 -0
  123. package/docs/agent-context-discovery.md +159 -0
  124. package/docs/agent-copilot-like-dx.md +126 -0
  125. package/docs/agent-declarative-optimized-planning.md +138 -0
  126. package/docs/agent-dx.md +224 -0
  127. package/docs/agent-env-files.md +126 -0
  128. package/docs/agent-follow-up-context.md +43 -0
  129. package/docs/agent-global-usage.md +180 -0
  130. package/docs/agent-init.md +109 -0
  131. package/docs/agent-install-and-configure.md +516 -0
  132. package/docs/agent-language-workspace-ux.md +99 -0
  133. package/docs/agent-llm-adapters.md +123 -0
  134. package/docs/agent-local-install.md +190 -0
  135. package/docs/agent-local-tests.md +51 -0
  136. package/docs/agent-observability.md +155 -0
  137. package/docs/agent-patch-quality-loop.md +162 -0
  138. package/docs/agent-presets.md +22 -0
  139. package/docs/agent-project-commands.md +237 -0
  140. package/docs/agent-project-intelligence.md +156 -0
  141. package/docs/agent-redaction.md +18 -0
  142. package/docs/agent-release-readiness.md +76 -0
  143. package/docs/agent-rollback-safety.md +162 -0
  144. package/docs/agent-rollback.md +23 -0
  145. package/docs/agent-run-artifacts.md +16 -0
  146. package/docs/agent-vscode-auto-discovery.md +137 -0
  147. package/docs/agent-vscode-batch-runner.md +100 -0
  148. package/docs/agent-vscode-chat-layout.md +90 -0
  149. package/docs/agent-vscode-clean-install.md +147 -0
  150. package/docs/agent-vscode-code-actions.md +70 -0
  151. package/docs/agent-vscode-diff-preview.md +45 -0
  152. package/docs/agent-vscode-inline-assist.md +56 -0
  153. package/docs/agent-vscode-install.md +186 -0
  154. package/docs/agent-vscode-model-setup.md +97 -0
  155. package/docs/agent-vscode-patch-preview.md +92 -0
  156. package/docs/agent-vscode-problems.md +79 -0
  157. package/docs/agent-vscode-project-dashboard.md +106 -0
  158. package/docs/agent-vscode-run-history.md +92 -0
  159. package/docs/agent-vscode-ux.md +73 -0
  160. package/docs/ai/INVARIANTS.md +84 -0
  161. package/docs/ai/PROJECT_MAP.md +338 -0
  162. package/docs/ai/PUBLIC_API.md +336 -0
  163. package/docs/ai/VALIDATION_MATRIX.md +67 -0
  164. package/docs/api-polish.md +37 -0
  165. package/docs/cancellation.md +162 -0
  166. package/docs/coverage.md +46 -0
  167. package/docs/getting-started.md +159 -0
  168. package/docs/guides/README.md +40 -0
  169. package/docs/guides/circuit-breaker.md +89 -0
  170. package/docs/guides/error-handling.md +91 -0
  171. package/docs/guides/getting-started.md +107 -0
  172. package/docs/guides/layers.md +189 -0
  173. package/docs/guides/metrics.md +101 -0
  174. package/docs/guides/resource-management.md +141 -0
  175. package/docs/guides/retry.md +215 -0
  176. package/docs/guides/semaphore.md +66 -0
  177. package/docs/guides/streams.md +117 -0
  178. package/docs/guides/supervisors.md +98 -0
  179. package/docs/guides/testing.md +162 -0
  180. package/docs/guides/tracing.md +71 -0
  181. package/docs/http-recipes.md +399 -0
  182. package/docs/http.md +749 -0
  183. package/docs/modules.md +285 -0
  184. package/docs/observability-collector-smoke.md +31 -0
  185. package/docs/observability-framework-examples.md +98 -0
  186. package/docs/observability.md +542 -0
  187. package/docs/otel-collector-smoke.yaml +27 -0
  188. package/docs/performance-profiler.md +199 -0
  189. package/docs/production-readiness.md +73 -0
  190. package/docs/recipes/README.md +12 -0
  191. package/docs/recipes/http-server.md +45 -0
  192. package/docs/recipes/layers.md +44 -0
  193. package/docs/recipes/performance.md +47 -0
  194. package/docs/recipes/runtime.md +41 -0
  195. package/docs/recipes/testing.md +41 -0
  196. package/docs/release.md +53 -0
  197. package/docs/wasm-bounded-queues.md +44 -0
  198. package/docs/wasm-engine-observability-benchmarks.md +85 -0
  199. package/docs/wasm-fiber-engine.md +117 -0
  200. package/docs/wasm-scheduler-state-machine.md +122 -0
  201. package/docs/wasm-stream-chunks.md +54 -0
  202. package/package.json +48 -2
  203. package/dist/chunk-AR22SXML.js +0 -1043
  204. package/dist/chunk-BDYEENHT.js +0 -224
  205. package/dist/chunk-JFPU5GQI.mjs +0 -1043
  206. package/dist/chunk-MS34J5LY.cjs +0 -224
  207. package/dist/chunk-UMAZLXAB.mjs +0 -224
  208. package/dist/chunk-XPZNXSVN.cjs +0 -1043
  209. package/dist/tracing-DNT9jEbr.d.ts +0 -106
@@ -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