@nicolastoulemont/std 0.7.2 → 0.8.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/README.md +571 -166
  2. package/dist/adt/index.d.mts +1 -1
  3. package/dist/adt/index.mjs +1 -1
  4. package/dist/adt-CPG_sa8q.mjs +2 -0
  5. package/dist/adt-CPG_sa8q.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-B4WfexUL.d.mts +57 -0
  53. package/dist/index-B4WfexUL.d.mts.map +1 -0
  54. package/dist/{index-DXbYlSnB.d.mts → index-BA0EsFxS.d.mts} +5 -74
  55. package/dist/index-BA0EsFxS.d.mts.map +1 -0
  56. package/dist/{index-UzMbg1dh.d.mts → index-BqJ1GWAF.d.mts} +20 -56
  57. package/dist/index-BqJ1GWAF.d.mts.map +1 -0
  58. package/dist/index-BsPOcZk9.d.mts +96 -0
  59. package/dist/index-BsPOcZk9.d.mts.map +1 -0
  60. package/dist/{index-DEAWPlcI.d.mts → index-CIvNgjsx.d.mts} +24 -56
  61. package/dist/index-CIvNgjsx.d.mts.map +1 -0
  62. package/dist/{index-B_iY5tq0.d.mts → index-CNTYbcY9.d.mts} +1 -21
  63. package/dist/index-CNTYbcY9.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-DUki2Bcp.d.mts} +54 -45
  73. package/dist/index-DUki2Bcp.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-CJ-aaQe8.mjs +2 -0
  141. package/dist/schema-CJ-aaQe8.mjs.map +1 -0
  142. package/dist/schema.shared-Bjyroa6b.mjs +2 -0
  143. package/dist/schema.shared-Bjyroa6b.mjs.map +1 -0
  144. package/dist/schema.types-CBEXCwfs.d.mts +60 -0
  145. package/dist/schema.types-CBEXCwfs.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,188 @@ 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
244
+ type SearchInput = {
245
+ q: string
246
+ limit: number
247
+ }
301
248
 
302
- Queue provides a standalone FIFO task queue with configurable concurrency, backpressure (bounded mode), and lifecycle controls.
249
+ const hasQuery = (input: SearchInput) => input.q.trim().length > 0
250
+ const hasSafeLimit = (input: SearchInput) => input.limit > 0 && input.limit <= 100
303
251
 
304
- #### Abstract Example
252
+ const isSearchInput = Predicate.and<SearchInput>(hasQuery, hasSafeLimit)
305
253
 
306
- ```ts
307
- import { Queue } from "@nicolastoulemont/std"
254
+ const canSearch = isSearchInput({ q: "books", limit: 20 })
255
+ ```
308
256
 
309
- const queue = Queue.make({ concurrency: 2 })
257
+ ### Schema
310
258
 
311
- const first = queue.enqueue(() => 1)
312
- const second = queue.enqueue(async () => 2)
259
+ Schema wraps Standard Schema-compatible validators for two production use cases:
260
+ boundary parsing and sync-only refinement.
313
261
 
314
- await queue.awaitIdle()
315
- await queue.shutdown({ mode: "drain" })
316
- ```
262
+ Use `Schema.parse` at I/O boundaries when a broad external type hides smaller implicit subtypes.
263
+ Use `Schema.refine` for in-memory narrowing when the schema validates only part of a broader value and you want to preserve the rest of the original shape.
264
+ Use `Schema.is` when the narrowed type should exactly match the schema output.
265
+ Use `Schema.Refine<Base, typeof schema>` for a reusable preserved-shape narrowed type, and `Schema.Infer<typeof schema>` for the exact schema output type.
266
+ Only use `Schema.refine` with sync schemas that prove properties already present on the original value. Transforms, defaults, and coercions should continue to use `Schema.parse`.
317
267
 
318
- #### Real-World Example
268
+ #### Boundary Parsing Example
319
269
 
320
270
  ```ts
321
- import { Queue } from "@nicolastoulemont/std"
271
+ import { Result, Schema } from "@nicolastoulemont/std"
272
+ import { z } from "zod"
322
273
 
323
- const imageQueue = Queue.bounded(100, { concurrency: 4 })
274
+ type Ticket = {
275
+ channel: "chat" | "email"
276
+ chatId?: string | null
277
+ metadata?: {
278
+ conversationId?: string | null
279
+ } | null
280
+ }
324
281
 
325
- const tasks = imageUrls.map((url) =>
326
- imageQueue.enqueue(async ({ signal }) => {
327
- const response = await fetch(url, { signal })
328
- return response.arrayBuffer()
282
+ type ChatTicket = {
283
+ channel: "chat"
284
+ chatId: string
285
+ metadata: {
286
+ conversationId: string
287
+ }
288
+ }
289
+
290
+ const ChatTicketSchema: Schema.SyncRefinementSchema<Ticket, ChatTicket> = z.object({
291
+ channel: z.literal("chat"),
292
+ chatId: z.string(),
293
+ metadata: z.object({
294
+ conversationId: z.string(),
329
295
  }),
330
- )
296
+ })
331
297
 
332
- const buffers = await Promise.all(tasks)
333
- await imageQueue.shutdown({ mode: "drain" })
334
- ```
298
+ const parseChatTicket = Schema.parse(ChatTicketSchema)
335
299
 
336
- ### Multithread
300
+ const result = parseChatTicket({
301
+ channel: "chat",
302
+ chatId: "chat_123",
303
+ metadata: { conversationId: "conv_123" },
304
+ })
337
305
 
338
- Multithread runs self-contained callbacks in worker threads using a Result-first API while remaining yieldable in `Fx.gen`.
306
+ if (Result.isOk(result)) {
307
+ result.value.metadata.conversationId
308
+ }
309
+ ```
339
310
 
340
- #### Abstract Example
311
+ #### In-Memory Refinement Example
341
312
 
342
313
  ```ts
343
- import { Multithread } from "@nicolastoulemont/std"
344
-
345
- const op = Multithread.run((input: string, ctx) => {
346
- ctx.throwIfCancelled()
347
- return input.toUpperCase()
348
- }, "hello")
314
+ import { Schema } from "@nicolastoulemont/std"
315
+ import { z } from "zod"
349
316
 
350
- const result = await op.result()
351
- ```
317
+ type PersistedTicket = {
318
+ id: string
319
+ channel: "chat" | "email"
320
+ chatId?: string | null
321
+ metadata?: {
322
+ conversationId?: string | null
323
+ } | null
324
+ }
352
325
 
353
- #### Real-World Example
326
+ type ChatTicketFields = {
327
+ channel: "chat"
328
+ chatId: string
329
+ }
354
330
 
355
- ```ts
356
- import { Fx, Multithread } from "@nicolastoulemont/std"
331
+ const ChatTicketSchema: Schema.SyncSchema<PersistedTicket, ChatTicketFields> = z.object({
332
+ channel: z.literal("chat"),
333
+ chatId: z.string(),
334
+ })
357
335
 
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
- )
336
+ type ChatTicket = Schema.Refine<PersistedTicket, typeof ChatTicketSchema>
371
337
 
372
- const preferred = yield* Multithread.firstSuccess([Multithread.run(() => "cache"), Multithread.run(() => "database")])
338
+ const isChatTicket = Schema.refine(ChatTicketSchema)
373
339
 
374
- return { records, preferred }
375
- })
340
+ declare const ticket: PersistedTicket
376
341
 
377
- const exit = await Fx.run(program)
342
+ if (isChatTicket(ticket)) {
343
+ ticket.id
344
+ ticket.chatId.toUpperCase()
345
+ }
378
346
  ```
379
347
 
380
- Multithread cancellation is cooperative. `abort()` always cancels logically, and worker code can stop early by calling `ctx.throwIfCancelled()`.
348
+ Use `Schema.is` instead when the narrowed type should be the schema output itself rather than `Base & Output<typeof schema>`.
381
349
 
382
350
  ### Adt
383
351
 
384
- Adt provides schema-backed tagged variants so you can model domain state with exhaustive pattern matching.
352
+ Adt builds tagged unions backed by any Standard Schema-compatible validator.
353
+ The examples below use `zod`, but the same API works with `valibot`, `arktype`, and other libraries that implement the Standard Schema contract.
385
354
 
386
355
  #### Abstract Example
387
356
 
388
357
  ```ts
389
- import { Adt, type AdtInfer } from "@nicolastoulemont/std"
358
+ import { Adt } from "@nicolastoulemont/std"
390
359
  import { z } from "zod"
391
360
 
392
361
  const Shape = Adt.union("Shape", {
393
362
  Circle: z.object({ radius: z.number() }),
394
363
  Square: z.object({ side: z.number() }),
395
364
  })
396
- type Shape = AdtInfer<typeof Shape>
365
+
366
+ type Shape = Adt.Infer<typeof Shape>
397
367
 
398
368
  const describeShape = (shape: Shape) =>
399
369
  Adt.match(shape, {
@@ -405,7 +375,7 @@ const describeShape = (shape: Shape) =>
405
375
  #### Real-World Example
406
376
 
407
377
  ```ts
408
- import { Adt, type AdtInfer } from "@nicolastoulemont/std"
378
+ import { Adt } from "@nicolastoulemont/std"
409
379
  import { z } from "zod"
410
380
 
411
381
  const OrderState = Adt.union("OrderState", {
@@ -413,7 +383,8 @@ const OrderState = Adt.union("OrderState", {
413
383
  Confirmed: z.object({ id: z.string(), paymentId: z.string() }),
414
384
  Shipped: z.object({ id: z.string(), trackingId: z.string() }),
415
385
  })
416
- type OrderState = AdtInfer<typeof OrderState>
386
+
387
+ type OrderState = Adt.Infer<typeof OrderState>
417
388
 
418
389
  const badgeLabel = (state: OrderState) =>
419
390
  Adt.match(state, {
@@ -425,7 +396,8 @@ const badgeLabel = (state: OrderState) =>
425
396
 
426
397
  ### Data
427
398
 
428
- Data creates immutable structural value objects (`Data.struct`, `Data.tuple`, `Data.array`, `Data.tagged`) with stable equality and hashing semantics.
399
+ Data creates immutable structural value objects with stable equality and hashing semantics.
400
+ Use it when you want value semantics for tuples, arrays, tagged records, or custom error types.
429
401
 
430
402
  #### Abstract Example
431
403
 
@@ -454,6 +426,7 @@ if (previous.equals(next)) {
454
426
  ### Order
455
427
 
456
428
  Order provides composable comparators and immutable sorting helpers.
429
+ Use `Order.string` for deterministic lexicographic ordering and `Order.collator(...)` when locale rules or numeric string sorting matter.
457
430
 
458
431
  #### Abstract Example
459
432
 
@@ -484,6 +457,8 @@ const sorted = Order.sort(
484
457
  ```ts
485
458
  import { Order } from "@nicolastoulemont/std"
486
459
 
460
+ const collator = new Intl.Collator("en", { numeric: true })
461
+
487
462
  type Product = {
488
463
  id: string
489
464
  category: string
@@ -491,7 +466,7 @@ type Product = {
491
466
  rating: number
492
467
  }
493
468
 
494
- const byCategory = Order.by(Order.string, (product: Product) => product.category)
469
+ const byCategory = Order.by(Order.collator(collator), (product: Product) => product.category)
495
470
  const byPrice = Order.by(Order.number, (product: Product) => product.price)
496
471
  const byRatingDesc = Order.reverse(Order.by(Order.number, (product: Product) => product.rating))
497
472
 
@@ -506,14 +481,444 @@ const products: Product[] = [
506
481
  const sorted = sortProducts(products)
507
482
  ```
508
483
 
484
+ ### Context
485
+
486
+ Context is the typed immutable service map used by `Fx`, `Layer`, and `Provide`.
487
+ Use it directly when you want to assemble dependencies without building a layer first.
488
+
489
+ #### Abstract Example
490
+
491
+ ```ts
492
+ import { Context, Service, pipe } from "@nicolastoulemont/std"
493
+
494
+ const Logger = Service.tag<{ log: (message: string) => void }>("Logger")
495
+ const Clock = Service.tag<{ now: () => number }>("Clock")
496
+
497
+ const ctx = pipe(Context.make(Logger, { log: () => undefined }), Context.add(Clock, { now: () => 123 }))
498
+
499
+ const now = Context.get(ctx, Clock).now()
500
+ ```
501
+
502
+ #### Real-World Example
503
+
504
+ ```ts
505
+ import { Context, Service, pipe } from "@nicolastoulemont/std"
506
+
507
+ const Config = Service.tag<{ apiBaseUrl: string }>("Config")
508
+ const Request = Service.tag<{ id: string }>("Request")
509
+
510
+ const base = Context.make(Config, { apiBaseUrl: "https://api.example.com" })
511
+ const requestCtx = pipe(base, Context.add(Request, { id: "req_123" }))
512
+
513
+ const requestId = Context.get(requestCtx, Request).id
514
+ ```
515
+
516
+ ### Service
517
+
518
+ Service defines typed dependency tags that can be yielded inside `Fx.gen`.
519
+ Use `Service.tag(...)` for interface-only tags and `Service.Service<...>()("...")` when you want a class-style service tag.
520
+
521
+ #### Abstract Example
522
+
523
+ ```ts
524
+ import { Fx, Provide, Service } from "@nicolastoulemont/std"
525
+
526
+ const Clock = Service.tag<{ now: () => number }>("Clock")
527
+
528
+ const program = Fx.gen(function* () {
529
+ return (yield* Clock).now()
530
+ })
531
+
532
+ const exit = Fx.run(Provide.service(Clock, { now: () => 123 })(program))
533
+ ```
534
+
535
+ #### Real-World Example
536
+
537
+ ```ts
538
+ import { Fx, Provide, Service } from "@nicolastoulemont/std"
539
+
540
+ const Logger = Service.Service<{ info: (message: string) => void }>()("Logger")
541
+
542
+ const program = Fx.gen(function* () {
543
+ const logger = yield* Logger
544
+ logger.info("starting request")
545
+ return "ok"
546
+ })
547
+
548
+ const exit = Fx.run(
549
+ Provide.service(Logger, {
550
+ info: (message) => console.log(message),
551
+ })(program),
552
+ )
553
+ ```
554
+
555
+ ### Layer
556
+
557
+ Layer builds services, composes service graphs, and models dependency construction separately from program execution.
558
+ Use it when the service itself has dependencies, can fail, or needs scoped cleanup.
559
+
560
+ #### Abstract Example
561
+
562
+ ```ts
563
+ import { Fx, Layer, Provide, Service } from "@nicolastoulemont/std"
564
+
565
+ const Port = Service.tag<number>("Port")
566
+ const PortLive = Layer.ok(Port, 3000)
567
+
568
+ const program = Fx.gen(function* () {
569
+ return yield* Port
570
+ })
571
+
572
+ const exit = Fx.run(Provide.layer(PortLive)(program))
573
+ ```
574
+
575
+ #### Real-World Example
576
+
577
+ ```ts
578
+ import { Fx, Layer, Provide, Service } from "@nicolastoulemont/std"
579
+
580
+ const Config = Service.tag<{ baseUrl: string }>("Config")
581
+ const Client = Service.tag<{ get: (path: string) => string }>("Client")
582
+
583
+ const ConfigLive = Layer.ok(Config, { baseUrl: "https://api.example.com" })
584
+
585
+ const ClientLive = Layer.fx(Client)(
586
+ Fx.gen(function* () {
587
+ const config = yield* Config
588
+ return {
589
+ get: (path: string) => `${config.baseUrl}${path}`,
590
+ }
591
+ }),
592
+ )
593
+
594
+ const Live = Layer.provide(ConfigLive)(ClientLive)
595
+
596
+ const program = Fx.gen(function* () {
597
+ return (yield* Client).get("/users")
598
+ })
599
+
600
+ const exit = Fx.run(Provide.layer(Live)(program))
601
+ ```
602
+
603
+ ### Provide
604
+
605
+ Provide resolves `Fx` requirements using a service, a context, or a fully-built layer.
606
+ It is the last step that turns a dependency-requiring effect into a runnable one.
607
+
608
+ #### Abstract Example
609
+
610
+ ```ts
611
+ import { Fx, Provide, Service } from "@nicolastoulemont/std"
612
+
613
+ const Port = Service.tag<number>("Port")
614
+
615
+ const readPort = Fx.gen(function* () {
616
+ return yield* Port
617
+ })
618
+
619
+ const exit = Fx.run(Provide.service(Port, 3000)(readPort))
620
+ ```
621
+
622
+ #### Real-World Example
623
+
624
+ ```ts
625
+ import { Context, Fx, Provide, Service, pipe } from "@nicolastoulemont/std"
626
+
627
+ const Config = Service.tag<{ baseUrl: string }>("Config")
628
+ const Logger = Service.tag<{ info: (message: string) => void }>("Logger")
629
+
630
+ const ctx = pipe(
631
+ Context.make(Config, { baseUrl: "https://api.example.com" }),
632
+ Context.add(Logger, { info: (message) => console.log(message) }),
633
+ )
634
+
635
+ const program = Fx.gen(function* () {
636
+ const config = yield* Config
637
+ const logger = yield* Logger
638
+ logger.info(`Calling ${config.baseUrl}`)
639
+ return config.baseUrl
640
+ })
641
+
642
+ const exit = Fx.run(Provide.context(ctx)(program))
643
+ ```
644
+
645
+ ### Fx
646
+
647
+ Fx models generator-based effects with typed dependencies, typed failures, and sync or async execution.
648
+ It is the center of the effectful part of the library, and it composes naturally with `Result`, `Option`, `Layer`, `Provide`, and `Service`.
649
+
650
+ #### Abstract Example
651
+
652
+ ```ts
653
+ import { Fx, Layer, Provide, Service, pipe } from "@nicolastoulemont/std"
654
+
655
+ const Clock = Service.tag<{ now: () => number }>("Clock")
656
+ const ClockLive = Layer.ok(Clock, { now: () => Date.now() })
657
+
658
+ const program = Fx.gen(function* () {
659
+ const clock = yield* Clock
660
+ return clock.now()
661
+ })
662
+
663
+ const exit = Fx.run(pipe(program, Provide.layer(ClockLive)))
664
+
665
+ const timestamp = Fx.match(exit, {
666
+ Ok: (ok) => ok.value,
667
+ Err: () => 0,
668
+ Defect: () => 0,
669
+ })
670
+ ```
671
+
672
+ #### Real-World Example
673
+
674
+ ```ts
675
+ import { Data, Fx, Layer, Provide, Result, Service, pipe } from "@nicolastoulemont/std"
676
+
677
+ const Api = Service.tag<{ postOrder: (input: { sku: string; qty: number }) => Promise<{ orderId: string }> }>("Api")
678
+
679
+ const ApiLive = Layer.ok(Api, {
680
+ postOrder: async () => ({ orderId: "ord_42" }),
681
+ })
682
+
683
+ class InvalidQuantityError extends Data.TaggedError("InvalidQuantityError")<{ qty: number }> {}
684
+
685
+ const submitOrder = Fx.gen(function* (payload: { sku?: string; qty: number }) {
686
+ const api = yield* Api
687
+ const sku = yield* Fx.option(payload.sku)
688
+ const validQty = yield* Result.filter(
689
+ Result.ok(payload.qty),
690
+ (qty) => qty > 0,
691
+ (qty) => new InvalidQuantityError({ qty }),
692
+ )
693
+ return yield* Fx.try(() => api.postOrder({ sku, qty: validQty }))
694
+ })
695
+
696
+ const exit = Fx.run(pipe(submitOrder({ sku: "book-1", qty: 2 }), Provide.layer(ApiLive)))
697
+ ```
698
+
699
+ ### Duration
700
+
701
+ Duration provides fixed-size, millisecond-backed values for retries, timeouts, and config-style inputs.
702
+
703
+ #### Abstract Example
704
+
705
+ ```ts
706
+ import { Duration, Result } from "@nicolastoulemont/std"
707
+
708
+ const timeout = Duration.seconds(30)
709
+
710
+ const parsed = Duration.parse("5 minutes")
711
+
712
+ const timeoutMs = Result.match(parsed, {
713
+ Ok: Duration.toMillis,
714
+ Err: (error) => error._tag,
715
+ })
716
+ ```
717
+
718
+ #### Real-World Example
719
+
720
+ ```ts
721
+ import { Duration, Schedule } from "@nicolastoulemont/std"
722
+
723
+ const retry = Schedule.fixed({
724
+ times: 3,
725
+ delayMs: Duration.seconds(1),
726
+ })
727
+
728
+ const backoff = Schedule.exponential({
729
+ times: 5,
730
+ baseDelayMs: "0.5 seconds",
731
+ maxDelayMs: "10 seconds",
732
+ })
733
+ ```
734
+
735
+ ### Schedule
736
+
737
+ Schedule describes retry policies for `Fx.retry`.
738
+ Use `recurs` for immediate retries, `fixed` for constant delays, and `exponential` for backoff.
739
+
740
+ #### Abstract Example
741
+
742
+ ```ts
743
+ import { Schedule } from "@nicolastoulemont/std"
744
+
745
+ const schedule = Schedule.recurs(2)
746
+ const delay = schedule.delayForAttempt(1)
747
+ ```
748
+
749
+ #### Real-World Example
750
+
751
+ ```ts
752
+ import { Duration, Fx, Result, Schedule } from "@nicolastoulemont/std"
753
+
754
+ let attempts = 0
755
+
756
+ const flaky = Fx.gen(function* () {
757
+ attempts += 1
758
+ if (attempts < 3) {
759
+ return yield* Result.err("temporary" as const)
760
+ }
761
+ return "ok"
762
+ })
763
+
764
+ const exit = await Fx.run(
765
+ Fx.retry(
766
+ flaky,
767
+ Schedule.exponential({
768
+ times: 5,
769
+ baseDelayMs: Duration.millis(100),
770
+ maxDelayMs: "1 seconds",
771
+ }),
772
+ ),
773
+ )
774
+ ```
775
+
776
+ ### Scope
777
+
778
+ Scope manages finalizers and nested resource lifecycles.
779
+ It is mostly used by `Layer.scoped` and `Provide.layer`, but you can use it directly when you want explicit cleanup semantics.
780
+
781
+ #### Abstract Example
782
+
783
+ ```ts
784
+ import { Fx, Result, Scope } from "@nicolastoulemont/std"
785
+
786
+ let released = false
787
+
788
+ const scope = Scope.make()
789
+
790
+ Fx.run(
791
+ scope.addFinalizer(() =>
792
+ Fx.gen(function* () {
793
+ released = true
794
+ }),
795
+ ),
796
+ )
797
+
798
+ Fx.run(scope.close(Result.ok(undefined)))
799
+ ```
800
+
801
+ #### Real-World Example
802
+
803
+ ```ts
804
+ import { Fx, Result, Scope } from "@nicolastoulemont/std"
805
+
806
+ const events: string[] = []
807
+
808
+ const root = Scope.make()
809
+ const child = root.fork()
810
+
811
+ Fx.run(
812
+ root.addFinalizer(() =>
813
+ Fx.gen(function* () {
814
+ events.push("root")
815
+ }),
816
+ ),
817
+ )
818
+
819
+ Fx.run(
820
+ child.addFinalizer(() =>
821
+ Fx.gen(function* () {
822
+ events.push("child")
823
+ }),
824
+ ),
825
+ )
826
+
827
+ Fx.run(root.close(Result.ok(undefined)))
828
+ ```
829
+
830
+ ### Queue
831
+
832
+ Queue provides a standalone FIFO task queue with configurable concurrency, backpressure, and lifecycle controls.
833
+ Use it when you want bounded async work without adopting the full `Fx` model.
834
+
835
+ #### Abstract Example
836
+
837
+ ```ts
838
+ import { Queue } from "@nicolastoulemont/std"
839
+
840
+ const queue = Queue.make({ concurrency: 2 })
841
+
842
+ const first = queue.enqueue(() => 1)
843
+ const second = queue.enqueue(async () => 2)
844
+
845
+ await queue.awaitIdle()
846
+ await queue.shutdown({ mode: "drain" })
847
+ ```
848
+
849
+ #### Real-World Example
850
+
851
+ ```ts
852
+ import { Queue } from "@nicolastoulemont/std"
853
+
854
+ const imageQueue = Queue.bounded(100, { concurrency: 4 })
855
+
856
+ const tasks = imageUrls.map((url) =>
857
+ imageQueue.enqueue(async ({ signal }) => {
858
+ const response = await fetch(url, { signal })
859
+ return response.arrayBuffer()
860
+ }),
861
+ )
862
+
863
+ const buffers = await Promise.all(tasks)
864
+ await imageQueue.shutdown({ mode: "drain" })
865
+ ```
866
+
867
+ ### Multithread
868
+
869
+ Multithread runs self-contained callbacks in worker threads using a Result-first API while remaining yieldable in `Fx.gen`.
870
+ It requires the optional `multithreading` dependency at runtime.
871
+
872
+ #### Abstract Example
873
+
874
+ ```ts
875
+ import { Multithread } from "@nicolastoulemont/std"
876
+
877
+ const op = Multithread.run((input: string, ctx) => {
878
+ ctx.throwIfCancelled()
879
+ return input.toUpperCase()
880
+ }, "hello")
881
+
882
+ const result = await op.result()
883
+ ```
884
+
885
+ #### Real-World Example
886
+
887
+ ```ts
888
+ import { Fx, Multithread } from "@nicolastoulemont/std"
889
+
890
+ const program = Fx.gen(async function* () {
891
+ const records = yield* Multithread.map(
892
+ ['{"id":"1","email":"a@example.com"}', '{"id":"2","email":"b@example.com"}'],
893
+ (line, _index, ctx) => {
894
+ ctx.throwIfCancelled()
895
+ try {
896
+ return JSON.parse(line) as { id: string; email: string }
897
+ } catch {
898
+ return { _tag: "Err" as const, error: { _tag: "ParseError" as const, line } }
899
+ }
900
+ },
901
+ { parallelism: 4 },
902
+ )
903
+
904
+ const preferred = yield* Multithread.firstSuccess([Multithread.run(() => "cache"), Multithread.run(() => "database")])
905
+
906
+ return { records, preferred }
907
+ })
908
+
909
+ const exit = await Fx.run(program)
910
+ ```
911
+
912
+ Multithread cancellation is cooperative. `abort()` always cancels logically, and worker code can stop early by calling `ctx.throwIfCancelled()`.
913
+
509
914
  ### pipe / flow
510
915
 
511
- pipe and flow compose sync/async transformations into readable, type-inferred data pipelines.
916
+ `pipe` and `flow` compose sync or async transformations into readable, type-inferred data pipelines.
512
917
 
513
918
  #### Abstract Example
514
919
 
515
920
  ```ts
516
- import { pipe, flow } from "@nicolastoulemont/std"
921
+ import { flow, pipe } from "@nicolastoulemont/std"
517
922
 
518
923
  const toLabel = flow(
519
924
  (n: number) => n * 2,
@@ -521,7 +926,7 @@ const toLabel = flow(
521
926
  (s) => `value:${s}`,
522
927
  )
523
928
 
524
- const result = pipe(10, (n) => n + 1, toLabel) // "value:22"
929
+ const result = pipe(10, (n) => n + 1, toLabel)
525
930
  ```
526
931
 
527
932
  #### Real-World Example