@nicolastoulemont/std 0.7.2 → 0.8.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 (209) hide show
  1. package/README.md +570 -168
  2. package/dist/adt/index.d.mts +1 -1
  3. package/dist/adt/index.mjs +1 -1
  4. package/dist/adt-CzdkjlUM.mjs +2 -0
  5. package/dist/adt-CzdkjlUM.mjs.map +1 -0
  6. package/dist/brand/index.d.mts +1 -1
  7. package/dist/brand/index.mjs +1 -1
  8. package/dist/brand-DZgGDrAe.mjs +2 -0
  9. package/dist/brand-DZgGDrAe.mjs.map +1 -0
  10. package/dist/brand.types-B3NDX1vo.d.mts +62 -0
  11. package/dist/brand.types-B3NDX1vo.d.mts.map +1 -0
  12. package/dist/context/index.d.mts +1 -1
  13. package/dist/context/index.mjs +1 -1
  14. package/dist/{context-CCHj1nab.mjs → context-0xDbwtpx.mjs} +2 -2
  15. package/dist/context-0xDbwtpx.mjs.map +1 -0
  16. package/dist/{context-r8ESJiFn.d.mts → context-B2dWloPl.d.mts} +2 -18
  17. package/dist/context-B2dWloPl.d.mts.map +1 -0
  18. package/dist/data/index.d.mts +1 -1
  19. package/dist/data/index.mjs +1 -1
  20. package/dist/{data-BLXO4XwS.mjs → data-BHYPdqWZ.mjs} +2 -2
  21. package/dist/{data-BLXO4XwS.mjs.map → data-BHYPdqWZ.mjs.map} +1 -1
  22. package/dist/{discriminator.types-CTURejXz.d.mts → discriminator.types-C-ygT2S1.d.mts} +1 -1
  23. package/dist/discriminator.types-C-ygT2S1.d.mts.map +1 -0
  24. package/dist/{dual-CZhzZslG.mjs → dual-fN6OUwN_.mjs} +1 -1
  25. package/dist/{dual-CZhzZslG.mjs.map → dual-fN6OUwN_.mjs.map} +1 -1
  26. package/dist/duration/index.d.mts +2 -0
  27. package/dist/duration/index.mjs +1 -0
  28. package/dist/duration-CYoDHcOR.mjs +2 -0
  29. package/dist/duration-CYoDHcOR.mjs.map +1 -0
  30. package/dist/either/index.d.mts +1 -1
  31. package/dist/either/index.mjs +1 -1
  32. package/dist/{either-BMLPfvMl.mjs → either-G7uOu4Ar.mjs} +2 -2
  33. package/dist/either-G7uOu4Ar.mjs.map +1 -0
  34. package/dist/{equality-CoyUHWh9.mjs → equality-BX6BUidG.mjs} +1 -1
  35. package/dist/{equality-CoyUHWh9.mjs.map → equality-BX6BUidG.mjs.map} +1 -1
  36. package/dist/{flow-D8_tllWl.mjs → flow-CNyLsPGb.mjs} +1 -1
  37. package/dist/flow-CNyLsPGb.mjs.map +1 -0
  38. package/dist/functions/index.d.mts +1 -1
  39. package/dist/functions/index.mjs +1 -1
  40. package/dist/functions-ByAk682_.mjs +2 -0
  41. package/dist/functions-ByAk682_.mjs.map +1 -0
  42. package/dist/fx/index.d.mts +1 -1
  43. package/dist/fx/index.mjs +1 -1
  44. package/dist/fx-DUXDxwsU.mjs +2 -0
  45. package/dist/fx-DUXDxwsU.mjs.map +1 -0
  46. package/dist/{fx.runtime-DclEDyjY.mjs → fx.runtime-jQxh77s3.mjs} +2 -2
  47. package/dist/{fx.runtime-DclEDyjY.mjs.map → fx.runtime-jQxh77s3.mjs.map} +1 -1
  48. package/dist/{fx.types-DeEWEltG.d.mts → fx.types-BdN1EWxr.d.mts} +1 -1
  49. package/dist/{fx.types-DeEWEltG.d.mts.map → fx.types-BdN1EWxr.d.mts.map} +1 -1
  50. package/dist/{fx.types-Bg-Mmdm5.mjs → fx.types-DyQVgTS8.mjs} +1 -1
  51. package/dist/{fx.types-Bg-Mmdm5.mjs.map → fx.types-DyQVgTS8.mjs.map} +1 -1
  52. package/dist/{index-DXbYlSnB.d.mts → index-BA0EsFxS.d.mts} +5 -74
  53. package/dist/index-BA0EsFxS.d.mts.map +1 -0
  54. package/dist/{index-UzMbg1dh.d.mts → index-BqJ1GWAF.d.mts} +20 -56
  55. package/dist/index-BqJ1GWAF.d.mts.map +1 -0
  56. package/dist/index-BsPOcZk9.d.mts +96 -0
  57. package/dist/index-BsPOcZk9.d.mts.map +1 -0
  58. package/dist/{index-DEAWPlcI.d.mts → index-CIvNgjsx.d.mts} +24 -56
  59. package/dist/index-CIvNgjsx.d.mts.map +1 -0
  60. package/dist/{index-B_iY5tq0.d.mts → index-CNTYbcY9.d.mts} +1 -21
  61. package/dist/index-CNTYbcY9.d.mts.map +1 -0
  62. package/dist/index-Ctg7XUOs.d.mts +36 -0
  63. package/dist/index-Ctg7XUOs.d.mts.map +1 -0
  64. package/dist/{index-Cq2IFito.d.mts → index-Ctqe1fD1.d.mts} +3 -17
  65. package/dist/index-Ctqe1fD1.d.mts.map +1 -0
  66. package/dist/{index-B_wWGszy.d.mts → index-D7mFNjot.d.mts} +1 -5
  67. package/dist/index-D7mFNjot.d.mts.map +1 -0
  68. package/dist/{index-CUZn-ohG.d.mts → index-D8rDE60Y.d.mts} +23 -54
  69. package/dist/index-D8rDE60Y.d.mts.map +1 -0
  70. package/dist/{index-By6dNRc4.d.mts → index-DR7hzXU4.d.mts} +3 -23
  71. package/dist/{index-By6dNRc4.d.mts.map → index-DR7hzXU4.d.mts.map} +1 -1
  72. package/dist/{index-DKS1g1oC.d.mts → index-DfQGXBQI.d.mts} +54 -45
  73. package/dist/index-DfQGXBQI.d.mts.map +1 -0
  74. package/dist/{index-BNQ9xSAz.d.mts → index-MsJqfQu0.d.mts} +64 -70
  75. package/dist/index-MsJqfQu0.d.mts.map +1 -0
  76. package/dist/{index-CGiLfREk.d.mts → index-UINIHFuh.d.mts} +39 -15
  77. package/dist/index-UINIHFuh.d.mts.map +1 -0
  78. package/dist/{index-BiiE8NS7.d.mts → index-crtzMG48.d.mts} +14 -23
  79. package/dist/index-crtzMG48.d.mts.map +1 -0
  80. package/dist/{index-B1-tBzc0.d.mts → index-dCRymj_g.d.mts} +23 -71
  81. package/dist/{index-B1-tBzc0.d.mts.map → index-dCRymj_g.d.mts.map} +1 -1
  82. package/dist/index-uE3S3Krx.d.mts +245 -0
  83. package/dist/index-uE3S3Krx.d.mts.map +1 -0
  84. package/dist/index.d.mts +21 -19
  85. package/dist/index.mjs +1 -1
  86. package/dist/layer/index.d.mts +1 -1
  87. package/dist/layer/index.mjs +1 -1
  88. package/dist/{layer-BttmtDrs.mjs → layer-CKtH7TRL.mjs} +2 -2
  89. package/dist/layer-CKtH7TRL.mjs.map +1 -0
  90. package/dist/{layer.types-DgpCIsk_.d.mts → layer.types-BB0MrvLg.d.mts} +4 -4
  91. package/dist/{layer.types-DgpCIsk_.d.mts.map → layer.types-BB0MrvLg.d.mts.map} +1 -1
  92. package/dist/multithread/index.d.mts +1 -1
  93. package/dist/multithread/index.mjs +1 -1
  94. package/dist/{multithread-xUUh4eLn.mjs → multithread-Cyc8Bz45.mjs} +2 -2
  95. package/dist/multithread-Cyc8Bz45.mjs.map +1 -0
  96. package/dist/option/index.d.mts +1 -1
  97. package/dist/option/index.mjs +1 -1
  98. package/dist/{option-Tfbo4wty.mjs → option-C2iCxAuJ.mjs} +2 -2
  99. package/dist/option-C2iCxAuJ.mjs.map +1 -0
  100. package/dist/{option.types-D1mm0zUb.mjs → option.types-CbY_swma.mjs} +1 -1
  101. package/dist/{option.types-D1mm0zUb.mjs.map → option.types-CbY_swma.mjs.map} +1 -1
  102. package/dist/{option.types-qPevEZQd.d.mts → option.types-D9hrKcfa.d.mts} +3 -3
  103. package/dist/{option.types-qPevEZQd.d.mts.map → option.types-D9hrKcfa.d.mts.map} +1 -1
  104. package/dist/order/index.d.mts +1 -1
  105. package/dist/order/index.mjs +1 -1
  106. package/dist/order-BXOBEKvB.mjs +2 -0
  107. package/dist/order-BXOBEKvB.mjs.map +1 -0
  108. package/dist/{pipeable-rfqacPxZ.d.mts → pipeable-BIrevC0D.d.mts} +1 -1
  109. package/dist/{pipeable-rfqacPxZ.d.mts.map → pipeable-BIrevC0D.d.mts.map} +1 -1
  110. package/dist/pipeable-Dp1_23zH.mjs +2 -0
  111. package/dist/{pipeable-COGyGMUV.mjs.map → pipeable-Dp1_23zH.mjs.map} +1 -1
  112. package/dist/predicate/index.d.mts +1 -1
  113. package/dist/predicate/index.mjs +1 -1
  114. package/dist/{predicate-DUhhQqWY.mjs → predicate-D_1SsIi4.mjs} +2 -2
  115. package/dist/predicate-D_1SsIi4.mjs.map +1 -0
  116. package/dist/provide/index.d.mts +1 -1
  117. package/dist/provide/index.mjs +1 -1
  118. package/dist/{provide-BmSM3Ruy.mjs → provide--yZE8x-n.mjs} +2 -2
  119. package/dist/provide--yZE8x-n.mjs.map +1 -0
  120. package/dist/queue/index.d.mts +1 -1
  121. package/dist/queue/index.mjs +1 -1
  122. package/dist/{queue-Sg6KJerl.mjs → queue-apiEOlRD.mjs} +2 -2
  123. package/dist/queue-apiEOlRD.mjs.map +1 -0
  124. package/dist/{queue.types-CD2LOu37.d.mts → queue.types-B-l5XYbU.d.mts} +1 -1
  125. package/dist/{queue.types-CD2LOu37.d.mts.map → queue.types-B-l5XYbU.d.mts.map} +1 -1
  126. package/dist/result/index.d.mts +1 -1
  127. package/dist/result/index.mjs +1 -1
  128. package/dist/{result-BEzV0DYC.mjs → result-D3VY0qBG.mjs} +2 -2
  129. package/dist/result-D3VY0qBG.mjs.map +1 -0
  130. package/dist/{result.types-_xDAei3-.d.mts → result.types-BKzChyWY.d.mts} +3 -3
  131. package/dist/{result.types-_xDAei3-.d.mts.map → result.types-BKzChyWY.d.mts.map} +1 -1
  132. package/dist/schedule/index.d.mts +1 -1
  133. package/dist/schedule/index.mjs +1 -1
  134. package/dist/schedule-B9K_2Z21.d.mts +183 -0
  135. package/dist/schedule-B9K_2Z21.d.mts.map +1 -0
  136. package/dist/schedule-C6iN3oMt.mjs +2 -0
  137. package/dist/schedule-C6iN3oMt.mjs.map +1 -0
  138. package/dist/schema/index.d.mts +2 -0
  139. package/dist/schema/index.mjs +1 -0
  140. package/dist/schema-D87TVF_b.mjs +2 -0
  141. package/dist/schema-D87TVF_b.mjs.map +1 -0
  142. package/dist/schema.shared-CI4eydjX.mjs +2 -0
  143. package/dist/schema.shared-CI4eydjX.mjs.map +1 -0
  144. package/dist/schema.types-CFzzx4bw.d.mts +45 -0
  145. package/dist/schema.types-CFzzx4bw.d.mts.map +1 -0
  146. package/dist/scope/index.d.mts +1 -1
  147. package/dist/scope/index.mjs +1 -1
  148. package/dist/{scope-CZdp4wKX.d.mts → scope-CuM3CzwG.d.mts} +3 -9
  149. package/dist/scope-CuM3CzwG.d.mts.map +1 -0
  150. package/dist/{scope-D_kzd1nT.mjs → scope-gVt4PESc.mjs} +2 -2
  151. package/dist/scope-gVt4PESc.mjs.map +1 -0
  152. package/dist/service/index.d.mts +1 -1
  153. package/dist/service/index.mjs +1 -1
  154. package/dist/{service-3PYQTUdH.mjs → service-CWAIEH46.mjs} +2 -2
  155. package/dist/service-CWAIEH46.mjs.map +1 -0
  156. package/dist/{service-DrXU7KJG.d.mts → service-D8mr0wwg.d.mts} +2 -8
  157. package/dist/service-D8mr0wwg.d.mts.map +1 -0
  158. package/dist/{service-resolution-C19smeaO.mjs → service-resolution-BefYr4nR.mjs} +1 -1
  159. package/dist/{service-resolution-C19smeaO.mjs.map → service-resolution-BefYr4nR.mjs.map} +1 -1
  160. package/package.json +9 -1
  161. package/dist/adt-DajUZvJe.mjs +0 -2
  162. package/dist/adt-DajUZvJe.mjs.map +0 -1
  163. package/dist/brand-Bia3Vj6l.mjs +0 -2
  164. package/dist/brand-Bia3Vj6l.mjs.map +0 -1
  165. package/dist/context-CCHj1nab.mjs.map +0 -1
  166. package/dist/context-r8ESJiFn.d.mts.map +0 -1
  167. package/dist/data.tagged-error.types-CGiKD-ES.d.mts +0 -29
  168. package/dist/data.tagged-error.types-CGiKD-ES.d.mts.map +0 -1
  169. package/dist/discriminator.types-CTURejXz.d.mts.map +0 -1
  170. package/dist/either-BMLPfvMl.mjs.map +0 -1
  171. package/dist/flow-D8_tllWl.mjs.map +0 -1
  172. package/dist/functions-BkevX2Dw.mjs +0 -2
  173. package/dist/functions-BkevX2Dw.mjs.map +0 -1
  174. package/dist/fx-K-a9Smhn.mjs +0 -2
  175. package/dist/fx-K-a9Smhn.mjs.map +0 -1
  176. package/dist/index-7Lv982Om.d.mts +0 -217
  177. package/dist/index-7Lv982Om.d.mts.map +0 -1
  178. package/dist/index-BNQ9xSAz.d.mts.map +0 -1
  179. package/dist/index-B_iY5tq0.d.mts.map +0 -1
  180. package/dist/index-B_wWGszy.d.mts.map +0 -1
  181. package/dist/index-BiiE8NS7.d.mts.map +0 -1
  182. package/dist/index-CGiLfREk.d.mts.map +0 -1
  183. package/dist/index-CUZn-ohG.d.mts.map +0 -1
  184. package/dist/index-Cq2IFito.d.mts.map +0 -1
  185. package/dist/index-DEAWPlcI.d.mts.map +0 -1
  186. package/dist/index-DKS1g1oC.d.mts.map +0 -1
  187. package/dist/index-DXbYlSnB.d.mts.map +0 -1
  188. package/dist/index-UzMbg1dh.d.mts.map +0 -1
  189. package/dist/layer-BttmtDrs.mjs.map +0 -1
  190. package/dist/multithread-xUUh4eLn.mjs.map +0 -1
  191. package/dist/option-Tfbo4wty.mjs.map +0 -1
  192. package/dist/order-D5c4QChk.mjs +0 -2
  193. package/dist/order-D5c4QChk.mjs.map +0 -1
  194. package/dist/pipeable-COGyGMUV.mjs +0 -2
  195. package/dist/predicate-DUhhQqWY.mjs.map +0 -1
  196. package/dist/provide-BmSM3Ruy.mjs.map +0 -1
  197. package/dist/queue-Sg6KJerl.mjs.map +0 -1
  198. package/dist/result-BEzV0DYC.mjs.map +0 -1
  199. package/dist/schedule-C6tjcJ1O.mjs +0 -2
  200. package/dist/schedule-C6tjcJ1O.mjs.map +0 -1
  201. package/dist/schedule-DlX2Dg69.d.mts +0 -144
  202. package/dist/schedule-DlX2Dg69.d.mts.map +0 -1
  203. package/dist/scope-CZdp4wKX.d.mts.map +0 -1
  204. package/dist/scope-D_kzd1nT.mjs.map +0 -1
  205. package/dist/service-3PYQTUdH.mjs.map +0 -1
  206. package/dist/service-DrXU7KJG.d.mts.map +0 -1
  207. /package/dist/{chunk-C934ptG5.mjs → chunk-oQKkju2G.mjs} +0 -0
  208. /package/dist/{option-CBCwzF0L.mjs → option-CXXiA1w-.mjs} +0 -0
  209. /package/dist/{result-B5WbPg8C.mjs → result-xFLfwriM.mjs} +0 -0
package/README.md CHANGED
@@ -4,18 +4,29 @@
4
4
 
5
5
  `@nicolastoulemont/std` is a functional TypeScript toolkit for modeling domain data, handling failures explicitly, and composing sync or async workflows.
6
6
  It is designed for application code where clear control flow, predictable typing, and dependency-aware orchestration matter.
7
- The API is small, pipe-friendly, and built around practical primitives you can combine incrementally.
7
+ The API is pipe-friendly, namespace-oriented, and built from small primitives you can combine incrementally.
8
8
 
9
9
  ## Installation
10
10
 
11
+ Install the base package:
12
+
11
13
  ```bash
12
14
  pnpm add @nicolastoulemont/std
13
15
  ```
14
16
 
17
+ Install optional extras used by some modules and examples:
18
+
19
+ ```bash
20
+ pnpm add @nicolastoulemont/std zod multithreading
21
+ ```
22
+
23
+ - `zod` is optional. The ADT examples below use it, but `Adt` accepts any Standard Schema-compatible validator, including `zod`, `valibot`, `arktype`, and similar libraries.
24
+ - `multithreading` is optional. It is only required if you want to use the `Multithread` module.
25
+
15
26
  ## Quick Start
16
27
 
17
28
  ```ts
18
- import { Result, Data, pipe } from "@nicolastoulemont/std"
29
+ import { Data, Result, pipe } from "@nicolastoulemont/std"
19
30
 
20
31
  class InvalidPortError extends Data.TaggedError("InvalidPortError")<{ input: string }> {}
21
32
 
@@ -33,7 +44,6 @@ const parsePort = (input: string) =>
33
44
  import { Fx, Layer, Provide, Service, pipe } from "@nicolastoulemont/std"
34
45
 
35
46
  const Config = Service.tag<{ baseUrl: string }>("Config")
36
-
37
47
  const ConfigLive = Layer.ok(Config, { baseUrl: "https://api.example.com" })
38
48
 
39
49
  const program = Fx.gen(function* () {
@@ -54,12 +64,12 @@ const response = Fx.match(exit, {
54
64
 
55
65
  ### Result
56
66
 
57
- Result models success/failure with typed errors so transformations stay explicit and composable.
67
+ Result models success and failure with typed errors so transformations stay explicit and composable.
58
68
 
59
69
  #### Abstract Example
60
70
 
61
71
  ```ts
62
- import { Result, Data, pipe } from "@nicolastoulemont/std"
72
+ import { Data, Result, pipe } from "@nicolastoulemont/std"
63
73
 
64
74
  class NotPositiveIntegerError extends Data.TaggedError("NotPositiveIntegerError")<{ input: string }> {}
65
75
 
@@ -78,7 +88,7 @@ const parsePositiveInt = (input: string) => {
78
88
  #### Real-World Example
79
89
 
80
90
  ```ts
81
- import { Result, Data, pipe } from "@nicolastoulemont/std"
91
+ import { Data, Result, pipe } from "@nicolastoulemont/std"
82
92
 
83
93
  class ValidationError extends Data.TaggedError("ValidationError")<{ message: string }> {}
84
94
  class ConflictError extends Data.TaggedError("ConflictError")<{ message: string }> {}
@@ -97,7 +107,7 @@ const signup = (email: string) => pipe(validateEmail(email), Result.flatMap(crea
97
107
 
98
108
  ### Option
99
109
 
100
- Option models optional presence/absence when missing data is expected and not an error condition.
110
+ Option models optional presence and absence when missing data is expected and not an error condition.
101
111
 
102
112
  #### Abstract Example
103
113
 
@@ -136,7 +146,7 @@ const readPagination = (query: URLSearchParams) => ({
136
146
 
137
147
  ### Either
138
148
 
139
- Either models two valid branches where both sides are meaningful outcomes rather than success versus failure.
149
+ Either models two meaningful branches where both sides are valid outcomes rather than success versus failure.
140
150
 
141
151
  #### Abstract Example
142
152
 
@@ -172,228 +182,185 @@ const responseMeta = (id: string) =>
172
182
  )
173
183
  ```
174
184
 
175
- ### Fx
185
+ ### Brand
176
186
 
177
- Fx models generator-based effects with typed dependencies and short-circuiting typed failures.
187
+ Brand adds nominal typing to primitives and other values without changing their runtime representation.
188
+ Use `Brand.make` when the source is already trusted, and `Brand.refine` when you want a validated branded value wrapped in `Result`.
178
189
 
179
190
  #### Abstract Example
180
191
 
181
192
  ```ts
182
- import { Fx, Layer, Provide, Service, pipe } from "@nicolastoulemont/std"
193
+ import { Brand } from "@nicolastoulemont/std"
183
194
 
184
- const Clock = Service.tag<{ now: () => number }>("Clock")
185
- const ClockLive = Layer.ok(Clock, { now: () => Date.now() })
195
+ type Port = Brand.Branded<number, "Port">
186
196
 
187
- const program = Fx.gen(function* () {
188
- const clock = yield* Clock
189
- return clock.now()
190
- })
197
+ const toPort = Brand.refine<Port>((value) => Number.isInteger(value) && value > 0 && value <= 65_535, "Invalid port")
191
198
 
192
- const exit = Fx.run(pipe(program, Provide.layer(ClockLive)))
193
-
194
- const timestamp = Fx.match(exit, {
195
- Ok: (ok) => ok.value,
196
- Err: () => 0,
197
- Defect: () => 0,
198
- })
199
+ const parsed = toPort(3000)
199
200
  ```
200
201
 
201
202
  #### Real-World Example
202
203
 
203
204
  ```ts
204
- import { Fx, Layer, Result, Data, Provide, Service, pipe } from "@nicolastoulemont/std"
205
+ import { Brand, Result, pipe } from "@nicolastoulemont/std"
205
206
 
206
- const Api = Service.tag<{ postOrder: (input: { sku: string; qty: number }) => Promise<{ orderId: string }> }>("Api")
207
- const ApiLive = Layer.ok(Api, {
208
- postOrder: async () => ({ orderId: "ord_42" }),
209
- })
207
+ type Email = Brand.Branded<string, "Email">
210
208
 
211
- class InvalidQuantityError extends Data.TaggedError("InvalidQuantityError")<{ qty: number }> {}
209
+ const toEmail = Brand.refine<Email>(
210
+ (value) => value.includes("@"),
211
+ (value) => `Invalid email: ${value}`,
212
+ )
212
213
 
213
- const submitOrder = Fx.gen(function* (payload: { sku?: string; qty: number }) {
214
- const api = yield* Api
215
- const sku = yield* Fx.option(payload.sku)
216
- const validQty = yield* Result.filter(
217
- Result.ok(payload.qty),
218
- (qty) => qty > 0,
219
- (qty) => new InvalidQuantityError({ qty }),
214
+ const register = (input: { email: string }) =>
215
+ pipe(
216
+ Result.ok(input.email),
217
+ Result.flatMap(toEmail),
218
+ Result.map((email) => ({ email })),
220
219
  )
221
- return yield* Fx.try(() => api.postOrder({ sku, qty: validQty }))
222
- })
223
-
224
- const exit = Fx.run(pipe(submitOrder({ sku: "book-1", qty: 2 }), Provide.layer(ApiLive)))
225
-
226
- const httpResponse = Fx.match(exit, {
227
- Ok: (ok) => ({ status: 201, body: ok.value }),
228
- Err: (err) => ({ status: 400, body: String(err.error) }),
229
- Defect: () => ({ status: 500, body: "Unexpected defect" }),
230
- })
231
220
  ```
232
221
 
233
- #### Retry Example
222
+ ### Predicate
234
223
 
235
- ```ts
236
- import { Fx, Result, Schedule } from "@nicolastoulemont/std"
224
+ Predicate provides small composable boolean predicates and refinements for filtering, narrowing, and request validation.
237
225
 
238
- let attempts = 0
239
-
240
- const flaky = Fx.gen(function* () {
241
- attempts += 1
242
- if (attempts < 3) {
243
- return yield* Result.err("temporary" as const)
244
- }
245
- return "ok"
246
- })
247
-
248
- const exit = Fx.run(Fx.retry(flaky, Schedule.recurs(5)))
249
- ```
250
-
251
- #### Nested Retry with Dependencies
226
+ #### Abstract Example
252
227
 
253
228
  ```ts
254
- import { Fx, Layer, Result, Schedule, pipe, Provide, Service } from "@nicolastoulemont/std"
255
-
256
- type ConfigService = { baseUrl: string }
257
- const Config = Service.tag<ConfigService>("Config")
258
- const ConfigLive = Layer.ok(Config, { baseUrl: "https://api.example.com" })
229
+ import { Predicate } from "@nicolastoulemont/std"
259
230
 
260
- let attempts = 0
261
-
262
- const inner = Fx.retry(
263
- Fx.gen(function* () {
264
- const config = yield* Config
265
- attempts += 1
266
- if (attempts < 2) {
267
- return yield* Result.err("temporary" as const)
268
- }
269
- return config.baseUrl
270
- }),
271
- Schedule.recurs(2),
231
+ const isPositiveEven = Predicate.and<number>(
232
+ (n) => n > 0,
233
+ (n) => n % 2 === 0,
272
234
  )
273
235
 
274
- const program = Fx.gen(function* () {
275
- const baseUrl = yield* inner
276
- return `ready:${baseUrl}`
277
- })
278
-
279
- const exit = Fx.run(pipe(program, Provide.layer(ConfigLive)))
236
+ const ok = isPositiveEven(4)
280
237
  ```
281
238
 
282
- #### Concurrent Traversal with Fx.forEach
239
+ #### Real-World Example
283
240
 
284
241
  ```ts
285
- import { Fx } from "@nicolastoulemont/std"
242
+ import { Predicate } from "@nicolastoulemont/std"
286
243
 
287
- const loadUsers = Fx.forEach(
288
- ["u1", "u2", "u3"],
289
- (id) =>
290
- Fx.gen(async function* () {
291
- const response = await fetch(`/api/users/${id}`)
292
- return yield* Fx.try(() => response.json())
293
- }),
294
- { concurrency: 2 },
295
- )
296
-
297
- const exit = await Fx.run(loadUsers)
298
- ```
299
-
300
- ### Queue
301
-
302
- Queue provides a standalone FIFO task queue with configurable concurrency, backpressure (bounded mode), and lifecycle controls.
303
-
304
- #### Abstract Example
305
-
306
- ```ts
307
- import { Queue } from "@nicolastoulemont/std"
244
+ type SearchInput = {
245
+ q: string
246
+ limit: number
247
+ }
308
248
 
309
- const queue = Queue.make({ concurrency: 2 })
249
+ const hasQuery = (input: SearchInput) => input.q.trim().length > 0
250
+ const hasSafeLimit = (input: SearchInput) => input.limit > 0 && input.limit <= 100
310
251
 
311
- const first = queue.enqueue(() => 1)
312
- const second = queue.enqueue(async () => 2)
252
+ const isSearchInput = Predicate.and<SearchInput>(hasQuery, hasSafeLimit)
313
253
 
314
- await queue.awaitIdle()
315
- await queue.shutdown({ mode: "drain" })
254
+ const canSearch = isSearchInput({ q: "books", limit: 20 })
316
255
  ```
317
256
 
318
- #### Real-World Example
257
+ ### Schema
319
258
 
320
- ```ts
321
- import { Queue } from "@nicolastoulemont/std"
259
+ Schema wraps Standard Schema-compatible validators for two production use cases:
260
+ boundary parsing and sync-only refinement.
322
261
 
323
- const imageQueue = Queue.bounded(100, { concurrency: 4 })
262
+ Use `Schema.parse` at I/O boundaries when a broad external type hides smaller implicit subtypes.
263
+ Use `Schema.is` only when the schema is a true refinement of an in-memory value and does not rely on transforms, defaults, or coercions.
324
264
 
325
- const tasks = imageUrls.map((url) =>
326
- imageQueue.enqueue(async ({ signal }) => {
327
- const response = await fetch(url, { signal })
328
- return response.arrayBuffer()
329
- }),
330
- )
265
+ #### Boundary Parsing Example
331
266
 
332
- const buffers = await Promise.all(tasks)
333
- await imageQueue.shutdown({ mode: "drain" })
334
- ```
267
+ ```ts
268
+ import { Result, Schema } from "@nicolastoulemont/std"
269
+ import { z } from "zod"
335
270
 
336
- ### Multithread
271
+ type Ticket = {
272
+ channel: "chat" | "email"
273
+ chatId?: string | null
274
+ metadata?: {
275
+ conversationId?: string | null
276
+ } | null
277
+ }
337
278
 
338
- Multithread runs self-contained callbacks in worker threads using a Result-first API while remaining yieldable in `Fx.gen`.
279
+ type ChatTicket = {
280
+ channel: "chat"
281
+ chatId: string
282
+ metadata: {
283
+ conversationId: string
284
+ }
285
+ }
339
286
 
340
- #### Abstract Example
287
+ const ChatTicketSchema: Schema.SyncRefinementSchema<Ticket, ChatTicket> = z.object({
288
+ channel: z.literal("chat"),
289
+ chatId: z.string(),
290
+ metadata: z.object({
291
+ conversationId: z.string(),
292
+ }),
293
+ })
341
294
 
342
- ```ts
343
- import { Multithread } from "@nicolastoulemont/std"
295
+ const parseChatTicket = Schema.parse(ChatTicketSchema)
344
296
 
345
- const op = Multithread.run((input: string, ctx) => {
346
- ctx.throwIfCancelled()
347
- return input.toUpperCase()
348
- }, "hello")
297
+ const result = parseChatTicket({
298
+ channel: "chat",
299
+ chatId: "chat_123",
300
+ metadata: { conversationId: "conv_123" },
301
+ })
349
302
 
350
- const result = await op.result()
303
+ if (Result.isOk(result)) {
304
+ result.value.metadata.conversationId
305
+ }
351
306
  ```
352
307
 
353
- #### Real-World Example
308
+ #### In-Memory Refinement Example
354
309
 
355
310
  ```ts
356
- import { Fx, Multithread } from "@nicolastoulemont/std"
311
+ import { Schema } from "@nicolastoulemont/std"
312
+ import { z } from "zod"
357
313
 
358
- const program = Fx.gen(async function* () {
359
- const records = yield* Multithread.map(
360
- ['{"id":"1","email":"a@example.com"}', '{"id":"2","email":"b@example.com"}'],
361
- (line, _index, ctx) => {
362
- ctx.throwIfCancelled()
363
- try {
364
- return JSON.parse(line) as { id: string; email: string }
365
- } catch {
366
- return { _tag: "Err" as const, error: { _tag: "ParseError" as const, line } }
367
- }
368
- },
369
- { parallelism: 4 },
370
- )
314
+ type Ticket = {
315
+ channel: "chat" | "email"
316
+ chatId?: string | null
317
+ metadata?: {
318
+ conversationId?: string | null
319
+ } | null
320
+ }
371
321
 
372
- const preferred = yield* Multithread.firstSuccess([Multithread.run(() => "cache"), Multithread.run(() => "database")])
322
+ type ChatTicket = {
323
+ channel: "chat"
324
+ chatId: string
325
+ metadata: {
326
+ conversationId: string
327
+ }
328
+ }
373
329
 
374
- return { records, preferred }
330
+ const ChatTicketSchema: Schema.SyncRefinementSchema<Ticket, ChatTicket> = z.object({
331
+ channel: z.literal("chat"),
332
+ chatId: z.string(),
333
+ metadata: z.object({
334
+ conversationId: z.string(),
335
+ }),
375
336
  })
376
337
 
377
- const exit = await Fx.run(program)
378
- ```
338
+ const isChatTicket = Schema.is(ChatTicketSchema)
379
339
 
380
- Multithread cancellation is cooperative. `abort()` always cancels logically, and worker code can stop early by calling `ctx.throwIfCancelled()`.
340
+ declare const ticket: Ticket
341
+
342
+ if (isChatTicket(ticket)) {
343
+ ticket.metadata.conversationId
344
+ }
345
+ ```
381
346
 
382
347
  ### Adt
383
348
 
384
- Adt provides schema-backed tagged variants so you can model domain state with exhaustive pattern matching.
349
+ Adt builds tagged unions backed by any Standard Schema-compatible validator.
350
+ The examples below use `zod`, but the same API works with `valibot`, `arktype`, and other libraries that implement the Standard Schema contract.
385
351
 
386
352
  #### Abstract Example
387
353
 
388
354
  ```ts
389
- import { Adt, type AdtInfer } from "@nicolastoulemont/std"
355
+ import { Adt } from "@nicolastoulemont/std"
390
356
  import { z } from "zod"
391
357
 
392
358
  const Shape = Adt.union("Shape", {
393
359
  Circle: z.object({ radius: z.number() }),
394
360
  Square: z.object({ side: z.number() }),
395
361
  })
396
- type Shape = AdtInfer<typeof Shape>
362
+
363
+ type Shape = Adt.Infer<typeof Shape>
397
364
 
398
365
  const describeShape = (shape: Shape) =>
399
366
  Adt.match(shape, {
@@ -405,7 +372,7 @@ const describeShape = (shape: Shape) =>
405
372
  #### Real-World Example
406
373
 
407
374
  ```ts
408
- import { Adt, type AdtInfer } from "@nicolastoulemont/std"
375
+ import { Adt } from "@nicolastoulemont/std"
409
376
  import { z } from "zod"
410
377
 
411
378
  const OrderState = Adt.union("OrderState", {
@@ -413,7 +380,8 @@ const OrderState = Adt.union("OrderState", {
413
380
  Confirmed: z.object({ id: z.string(), paymentId: z.string() }),
414
381
  Shipped: z.object({ id: z.string(), trackingId: z.string() }),
415
382
  })
416
- type OrderState = AdtInfer<typeof OrderState>
383
+
384
+ type OrderState = Adt.Infer<typeof OrderState>
417
385
 
418
386
  const badgeLabel = (state: OrderState) =>
419
387
  Adt.match(state, {
@@ -425,7 +393,8 @@ const badgeLabel = (state: OrderState) =>
425
393
 
426
394
  ### Data
427
395
 
428
- Data creates immutable structural value objects (`Data.struct`, `Data.tuple`, `Data.array`, `Data.tagged`) with stable equality and hashing semantics.
396
+ Data creates immutable structural value objects with stable equality and hashing semantics.
397
+ Use it when you want value semantics for tuples, arrays, tagged records, or custom error types.
429
398
 
430
399
  #### Abstract Example
431
400
 
@@ -454,6 +423,7 @@ if (previous.equals(next)) {
454
423
  ### Order
455
424
 
456
425
  Order provides composable comparators and immutable sorting helpers.
426
+ Use `Order.string` for deterministic lexicographic ordering and `Order.collator(...)` when locale rules or numeric string sorting matter.
457
427
 
458
428
  #### Abstract Example
459
429
 
@@ -484,6 +454,8 @@ const sorted = Order.sort(
484
454
  ```ts
485
455
  import { Order } from "@nicolastoulemont/std"
486
456
 
457
+ const collator = new Intl.Collator("en", { numeric: true })
458
+
487
459
  type Product = {
488
460
  id: string
489
461
  category: string
@@ -491,7 +463,7 @@ type Product = {
491
463
  rating: number
492
464
  }
493
465
 
494
- const byCategory = Order.by(Order.string, (product: Product) => product.category)
466
+ const byCategory = Order.by(Order.collator(collator), (product: Product) => product.category)
495
467
  const byPrice = Order.by(Order.number, (product: Product) => product.price)
496
468
  const byRatingDesc = Order.reverse(Order.by(Order.number, (product: Product) => product.rating))
497
469
 
@@ -506,14 +478,444 @@ const products: Product[] = [
506
478
  const sorted = sortProducts(products)
507
479
  ```
508
480
 
481
+ ### Context
482
+
483
+ Context is the typed immutable service map used by `Fx`, `Layer`, and `Provide`.
484
+ Use it directly when you want to assemble dependencies without building a layer first.
485
+
486
+ #### Abstract Example
487
+
488
+ ```ts
489
+ import { Context, Service, pipe } from "@nicolastoulemont/std"
490
+
491
+ const Logger = Service.tag<{ log: (message: string) => void }>("Logger")
492
+ const Clock = Service.tag<{ now: () => number }>("Clock")
493
+
494
+ const ctx = pipe(Context.make(Logger, { log: () => undefined }), Context.add(Clock, { now: () => 123 }))
495
+
496
+ const now = Context.get(ctx, Clock).now()
497
+ ```
498
+
499
+ #### Real-World Example
500
+
501
+ ```ts
502
+ import { Context, Service, pipe } from "@nicolastoulemont/std"
503
+
504
+ const Config = Service.tag<{ apiBaseUrl: string }>("Config")
505
+ const Request = Service.tag<{ id: string }>("Request")
506
+
507
+ const base = Context.make(Config, { apiBaseUrl: "https://api.example.com" })
508
+ const requestCtx = pipe(base, Context.add(Request, { id: "req_123" }))
509
+
510
+ const requestId = Context.get(requestCtx, Request).id
511
+ ```
512
+
513
+ ### Service
514
+
515
+ Service defines typed dependency tags that can be yielded inside `Fx.gen`.
516
+ Use `Service.tag(...)` for interface-only tags and `Service.Service<...>()("...")` when you want a class-style service tag.
517
+
518
+ #### Abstract Example
519
+
520
+ ```ts
521
+ import { Fx, Provide, Service } from "@nicolastoulemont/std"
522
+
523
+ const Clock = Service.tag<{ now: () => number }>("Clock")
524
+
525
+ const program = Fx.gen(function* () {
526
+ return (yield* Clock).now()
527
+ })
528
+
529
+ const exit = Fx.run(Provide.service(Clock, { now: () => 123 })(program))
530
+ ```
531
+
532
+ #### Real-World Example
533
+
534
+ ```ts
535
+ import { Fx, Provide, Service } from "@nicolastoulemont/std"
536
+
537
+ const Logger = Service.Service<{ info: (message: string) => void }>()("Logger")
538
+
539
+ const program = Fx.gen(function* () {
540
+ const logger = yield* Logger
541
+ logger.info("starting request")
542
+ return "ok"
543
+ })
544
+
545
+ const exit = Fx.run(
546
+ Provide.service(Logger, {
547
+ info: (message) => console.log(message),
548
+ })(program),
549
+ )
550
+ ```
551
+
552
+ ### Layer
553
+
554
+ Layer builds services, composes service graphs, and models dependency construction separately from program execution.
555
+ Use it when the service itself has dependencies, can fail, or needs scoped cleanup.
556
+
557
+ #### Abstract Example
558
+
559
+ ```ts
560
+ import { Fx, Layer, Provide, Service } from "@nicolastoulemont/std"
561
+
562
+ const Port = Service.tag<number>("Port")
563
+ const PortLive = Layer.ok(Port, 3000)
564
+
565
+ const program = Fx.gen(function* () {
566
+ return yield* Port
567
+ })
568
+
569
+ const exit = Fx.run(Provide.layer(PortLive)(program))
570
+ ```
571
+
572
+ #### Real-World Example
573
+
574
+ ```ts
575
+ import { Fx, Layer, Provide, Service } from "@nicolastoulemont/std"
576
+
577
+ const Config = Service.tag<{ baseUrl: string }>("Config")
578
+ const Client = Service.tag<{ get: (path: string) => string }>("Client")
579
+
580
+ const ConfigLive = Layer.ok(Config, { baseUrl: "https://api.example.com" })
581
+
582
+ const ClientLive = Layer.fx(Client)(
583
+ Fx.gen(function* () {
584
+ const config = yield* Config
585
+ return {
586
+ get: (path: string) => `${config.baseUrl}${path}`,
587
+ }
588
+ }),
589
+ )
590
+
591
+ const Live = Layer.provide(ConfigLive)(ClientLive)
592
+
593
+ const program = Fx.gen(function* () {
594
+ return (yield* Client).get("/users")
595
+ })
596
+
597
+ const exit = Fx.run(Provide.layer(Live)(program))
598
+ ```
599
+
600
+ ### Provide
601
+
602
+ Provide resolves `Fx` requirements using a service, a context, or a fully-built layer.
603
+ It is the last step that turns a dependency-requiring effect into a runnable one.
604
+
605
+ #### Abstract Example
606
+
607
+ ```ts
608
+ import { Fx, Provide, Service } from "@nicolastoulemont/std"
609
+
610
+ const Port = Service.tag<number>("Port")
611
+
612
+ const readPort = Fx.gen(function* () {
613
+ return yield* Port
614
+ })
615
+
616
+ const exit = Fx.run(Provide.service(Port, 3000)(readPort))
617
+ ```
618
+
619
+ #### Real-World Example
620
+
621
+ ```ts
622
+ import { Context, Fx, Provide, Service, pipe } from "@nicolastoulemont/std"
623
+
624
+ const Config = Service.tag<{ baseUrl: string }>("Config")
625
+ const Logger = Service.tag<{ info: (message: string) => void }>("Logger")
626
+
627
+ const ctx = pipe(
628
+ Context.make(Config, { baseUrl: "https://api.example.com" }),
629
+ Context.add(Logger, { info: (message) => console.log(message) }),
630
+ )
631
+
632
+ const program = Fx.gen(function* () {
633
+ const config = yield* Config
634
+ const logger = yield* Logger
635
+ logger.info(`Calling ${config.baseUrl}`)
636
+ return config.baseUrl
637
+ })
638
+
639
+ const exit = Fx.run(Provide.context(ctx)(program))
640
+ ```
641
+
642
+ ### Fx
643
+
644
+ Fx models generator-based effects with typed dependencies, typed failures, and sync or async execution.
645
+ It is the center of the effectful part of the library, and it composes naturally with `Result`, `Option`, `Layer`, `Provide`, and `Service`.
646
+
647
+ #### Abstract Example
648
+
649
+ ```ts
650
+ import { Fx, Layer, Provide, Service, pipe } from "@nicolastoulemont/std"
651
+
652
+ const Clock = Service.tag<{ now: () => number }>("Clock")
653
+ const ClockLive = Layer.ok(Clock, { now: () => Date.now() })
654
+
655
+ const program = Fx.gen(function* () {
656
+ const clock = yield* Clock
657
+ return clock.now()
658
+ })
659
+
660
+ const exit = Fx.run(pipe(program, Provide.layer(ClockLive)))
661
+
662
+ const timestamp = Fx.match(exit, {
663
+ Ok: (ok) => ok.value,
664
+ Err: () => 0,
665
+ Defect: () => 0,
666
+ })
667
+ ```
668
+
669
+ #### Real-World Example
670
+
671
+ ```ts
672
+ import { Data, Fx, Layer, Provide, Result, Service, pipe } from "@nicolastoulemont/std"
673
+
674
+ const Api = Service.tag<{ postOrder: (input: { sku: string; qty: number }) => Promise<{ orderId: string }> }>("Api")
675
+
676
+ const ApiLive = Layer.ok(Api, {
677
+ postOrder: async () => ({ orderId: "ord_42" }),
678
+ })
679
+
680
+ class InvalidQuantityError extends Data.TaggedError("InvalidQuantityError")<{ qty: number }> {}
681
+
682
+ const submitOrder = Fx.gen(function* (payload: { sku?: string; qty: number }) {
683
+ const api = yield* Api
684
+ const sku = yield* Fx.option(payload.sku)
685
+ const validQty = yield* Result.filter(
686
+ Result.ok(payload.qty),
687
+ (qty) => qty > 0,
688
+ (qty) => new InvalidQuantityError({ qty }),
689
+ )
690
+ return yield* Fx.try(() => api.postOrder({ sku, qty: validQty }))
691
+ })
692
+
693
+ const exit = Fx.run(pipe(submitOrder({ sku: "book-1", qty: 2 }), Provide.layer(ApiLive)))
694
+ ```
695
+
696
+ ### Duration
697
+
698
+ Duration provides fixed-size, millisecond-backed values for retries, timeouts, and config-style inputs.
699
+
700
+ #### Abstract Example
701
+
702
+ ```ts
703
+ import { Duration, Result } from "@nicolastoulemont/std"
704
+
705
+ const timeout = Duration.seconds(30)
706
+
707
+ const parsed = Duration.parse("5 minutes")
708
+
709
+ const timeoutMs = Result.match(parsed, {
710
+ Ok: Duration.toMillis,
711
+ Err: (error) => error._tag,
712
+ })
713
+ ```
714
+
715
+ #### Real-World Example
716
+
717
+ ```ts
718
+ import { Duration, Schedule } from "@nicolastoulemont/std"
719
+
720
+ const retry = Schedule.fixed({
721
+ times: 3,
722
+ delayMs: Duration.seconds(1),
723
+ })
724
+
725
+ const backoff = Schedule.exponential({
726
+ times: 5,
727
+ baseDelayMs: "0.5 seconds",
728
+ maxDelayMs: "10 seconds",
729
+ })
730
+ ```
731
+
732
+ ### Schedule
733
+
734
+ Schedule describes retry policies for `Fx.retry`.
735
+ Use `recurs` for immediate retries, `fixed` for constant delays, and `exponential` for backoff.
736
+
737
+ #### Abstract Example
738
+
739
+ ```ts
740
+ import { Schedule } from "@nicolastoulemont/std"
741
+
742
+ const schedule = Schedule.recurs(2)
743
+ const delay = schedule.delayForAttempt(1)
744
+ ```
745
+
746
+ #### Real-World Example
747
+
748
+ ```ts
749
+ import { Duration, Fx, Result, Schedule } from "@nicolastoulemont/std"
750
+
751
+ let attempts = 0
752
+
753
+ const flaky = Fx.gen(function* () {
754
+ attempts += 1
755
+ if (attempts < 3) {
756
+ return yield* Result.err("temporary" as const)
757
+ }
758
+ return "ok"
759
+ })
760
+
761
+ const exit = await Fx.run(
762
+ Fx.retry(
763
+ flaky,
764
+ Schedule.exponential({
765
+ times: 5,
766
+ baseDelayMs: Duration.millis(100),
767
+ maxDelayMs: "1 seconds",
768
+ }),
769
+ ),
770
+ )
771
+ ```
772
+
773
+ ### Scope
774
+
775
+ Scope manages finalizers and nested resource lifecycles.
776
+ It is mostly used by `Layer.scoped` and `Provide.layer`, but you can use it directly when you want explicit cleanup semantics.
777
+
778
+ #### Abstract Example
779
+
780
+ ```ts
781
+ import { Fx, Result, Scope } from "@nicolastoulemont/std"
782
+
783
+ let released = false
784
+
785
+ const scope = Scope.make()
786
+
787
+ Fx.run(
788
+ scope.addFinalizer(() =>
789
+ Fx.gen(function* () {
790
+ released = true
791
+ }),
792
+ ),
793
+ )
794
+
795
+ Fx.run(scope.close(Result.ok(undefined)))
796
+ ```
797
+
798
+ #### Real-World Example
799
+
800
+ ```ts
801
+ import { Fx, Result, Scope } from "@nicolastoulemont/std"
802
+
803
+ const events: string[] = []
804
+
805
+ const root = Scope.make()
806
+ const child = root.fork()
807
+
808
+ Fx.run(
809
+ root.addFinalizer(() =>
810
+ Fx.gen(function* () {
811
+ events.push("root")
812
+ }),
813
+ ),
814
+ )
815
+
816
+ Fx.run(
817
+ child.addFinalizer(() =>
818
+ Fx.gen(function* () {
819
+ events.push("child")
820
+ }),
821
+ ),
822
+ )
823
+
824
+ Fx.run(root.close(Result.ok(undefined)))
825
+ ```
826
+
827
+ ### Queue
828
+
829
+ Queue provides a standalone FIFO task queue with configurable concurrency, backpressure, and lifecycle controls.
830
+ Use it when you want bounded async work without adopting the full `Fx` model.
831
+
832
+ #### Abstract Example
833
+
834
+ ```ts
835
+ import { Queue } from "@nicolastoulemont/std"
836
+
837
+ const queue = Queue.make({ concurrency: 2 })
838
+
839
+ const first = queue.enqueue(() => 1)
840
+ const second = queue.enqueue(async () => 2)
841
+
842
+ await queue.awaitIdle()
843
+ await queue.shutdown({ mode: "drain" })
844
+ ```
845
+
846
+ #### Real-World Example
847
+
848
+ ```ts
849
+ import { Queue } from "@nicolastoulemont/std"
850
+
851
+ const imageQueue = Queue.bounded(100, { concurrency: 4 })
852
+
853
+ const tasks = imageUrls.map((url) =>
854
+ imageQueue.enqueue(async ({ signal }) => {
855
+ const response = await fetch(url, { signal })
856
+ return response.arrayBuffer()
857
+ }),
858
+ )
859
+
860
+ const buffers = await Promise.all(tasks)
861
+ await imageQueue.shutdown({ mode: "drain" })
862
+ ```
863
+
864
+ ### Multithread
865
+
866
+ Multithread runs self-contained callbacks in worker threads using a Result-first API while remaining yieldable in `Fx.gen`.
867
+ It requires the optional `multithreading` dependency at runtime.
868
+
869
+ #### Abstract Example
870
+
871
+ ```ts
872
+ import { Multithread } from "@nicolastoulemont/std"
873
+
874
+ const op = Multithread.run((input: string, ctx) => {
875
+ ctx.throwIfCancelled()
876
+ return input.toUpperCase()
877
+ }, "hello")
878
+
879
+ const result = await op.result()
880
+ ```
881
+
882
+ #### Real-World Example
883
+
884
+ ```ts
885
+ import { Fx, Multithread } from "@nicolastoulemont/std"
886
+
887
+ const program = Fx.gen(async function* () {
888
+ const records = yield* Multithread.map(
889
+ ['{"id":"1","email":"a@example.com"}', '{"id":"2","email":"b@example.com"}'],
890
+ (line, _index, ctx) => {
891
+ ctx.throwIfCancelled()
892
+ try {
893
+ return JSON.parse(line) as { id: string; email: string }
894
+ } catch {
895
+ return { _tag: "Err" as const, error: { _tag: "ParseError" as const, line } }
896
+ }
897
+ },
898
+ { parallelism: 4 },
899
+ )
900
+
901
+ const preferred = yield* Multithread.firstSuccess([Multithread.run(() => "cache"), Multithread.run(() => "database")])
902
+
903
+ return { records, preferred }
904
+ })
905
+
906
+ const exit = await Fx.run(program)
907
+ ```
908
+
909
+ Multithread cancellation is cooperative. `abort()` always cancels logically, and worker code can stop early by calling `ctx.throwIfCancelled()`.
910
+
509
911
  ### pipe / flow
510
912
 
511
- pipe and flow compose sync/async transformations into readable, type-inferred data pipelines.
913
+ `pipe` and `flow` compose sync or async transformations into readable, type-inferred data pipelines.
512
914
 
513
915
  #### Abstract Example
514
916
 
515
917
  ```ts
516
- import { pipe, flow } from "@nicolastoulemont/std"
918
+ import { flow, pipe } from "@nicolastoulemont/std"
517
919
 
518
920
  const toLabel = flow(
519
921
  (n: number) => n * 2,
@@ -521,7 +923,7 @@ const toLabel = flow(
521
923
  (s) => `value:${s}`,
522
924
  )
523
925
 
524
- const result = pipe(10, (n) => n + 1, toLabel) // "value:22"
926
+ const result = pipe(10, (n) => n + 1, toLabel)
525
927
  ```
526
928
 
527
929
  #### Real-World Example