@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.
- package/README.md +570 -168
- package/dist/adt/index.d.mts +1 -1
- package/dist/adt/index.mjs +1 -1
- package/dist/adt-CzdkjlUM.mjs +2 -0
- package/dist/adt-CzdkjlUM.mjs.map +1 -0
- package/dist/brand/index.d.mts +1 -1
- package/dist/brand/index.mjs +1 -1
- package/dist/brand-DZgGDrAe.mjs +2 -0
- package/dist/brand-DZgGDrAe.mjs.map +1 -0
- package/dist/brand.types-B3NDX1vo.d.mts +62 -0
- package/dist/brand.types-B3NDX1vo.d.mts.map +1 -0
- package/dist/context/index.d.mts +1 -1
- package/dist/context/index.mjs +1 -1
- package/dist/{context-CCHj1nab.mjs → context-0xDbwtpx.mjs} +2 -2
- package/dist/context-0xDbwtpx.mjs.map +1 -0
- package/dist/{context-r8ESJiFn.d.mts → context-B2dWloPl.d.mts} +2 -18
- package/dist/context-B2dWloPl.d.mts.map +1 -0
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/{data-BLXO4XwS.mjs → data-BHYPdqWZ.mjs} +2 -2
- package/dist/{data-BLXO4XwS.mjs.map → data-BHYPdqWZ.mjs.map} +1 -1
- package/dist/{discriminator.types-CTURejXz.d.mts → discriminator.types-C-ygT2S1.d.mts} +1 -1
- package/dist/discriminator.types-C-ygT2S1.d.mts.map +1 -0
- package/dist/{dual-CZhzZslG.mjs → dual-fN6OUwN_.mjs} +1 -1
- package/dist/{dual-CZhzZslG.mjs.map → dual-fN6OUwN_.mjs.map} +1 -1
- package/dist/duration/index.d.mts +2 -0
- package/dist/duration/index.mjs +1 -0
- package/dist/duration-CYoDHcOR.mjs +2 -0
- package/dist/duration-CYoDHcOR.mjs.map +1 -0
- package/dist/either/index.d.mts +1 -1
- package/dist/either/index.mjs +1 -1
- package/dist/{either-BMLPfvMl.mjs → either-G7uOu4Ar.mjs} +2 -2
- package/dist/either-G7uOu4Ar.mjs.map +1 -0
- package/dist/{equality-CoyUHWh9.mjs → equality-BX6BUidG.mjs} +1 -1
- package/dist/{equality-CoyUHWh9.mjs.map → equality-BX6BUidG.mjs.map} +1 -1
- package/dist/{flow-D8_tllWl.mjs → flow-CNyLsPGb.mjs} +1 -1
- package/dist/flow-CNyLsPGb.mjs.map +1 -0
- package/dist/functions/index.d.mts +1 -1
- package/dist/functions/index.mjs +1 -1
- package/dist/functions-ByAk682_.mjs +2 -0
- package/dist/functions-ByAk682_.mjs.map +1 -0
- package/dist/fx/index.d.mts +1 -1
- package/dist/fx/index.mjs +1 -1
- package/dist/fx-DUXDxwsU.mjs +2 -0
- package/dist/fx-DUXDxwsU.mjs.map +1 -0
- package/dist/{fx.runtime-DclEDyjY.mjs → fx.runtime-jQxh77s3.mjs} +2 -2
- package/dist/{fx.runtime-DclEDyjY.mjs.map → fx.runtime-jQxh77s3.mjs.map} +1 -1
- package/dist/{fx.types-DeEWEltG.d.mts → fx.types-BdN1EWxr.d.mts} +1 -1
- package/dist/{fx.types-DeEWEltG.d.mts.map → fx.types-BdN1EWxr.d.mts.map} +1 -1
- package/dist/{fx.types-Bg-Mmdm5.mjs → fx.types-DyQVgTS8.mjs} +1 -1
- package/dist/{fx.types-Bg-Mmdm5.mjs.map → fx.types-DyQVgTS8.mjs.map} +1 -1
- package/dist/{index-DXbYlSnB.d.mts → index-BA0EsFxS.d.mts} +5 -74
- package/dist/index-BA0EsFxS.d.mts.map +1 -0
- package/dist/{index-UzMbg1dh.d.mts → index-BqJ1GWAF.d.mts} +20 -56
- package/dist/index-BqJ1GWAF.d.mts.map +1 -0
- package/dist/index-BsPOcZk9.d.mts +96 -0
- package/dist/index-BsPOcZk9.d.mts.map +1 -0
- package/dist/{index-DEAWPlcI.d.mts → index-CIvNgjsx.d.mts} +24 -56
- package/dist/index-CIvNgjsx.d.mts.map +1 -0
- package/dist/{index-B_iY5tq0.d.mts → index-CNTYbcY9.d.mts} +1 -21
- package/dist/index-CNTYbcY9.d.mts.map +1 -0
- package/dist/index-Ctg7XUOs.d.mts +36 -0
- package/dist/index-Ctg7XUOs.d.mts.map +1 -0
- package/dist/{index-Cq2IFito.d.mts → index-Ctqe1fD1.d.mts} +3 -17
- package/dist/index-Ctqe1fD1.d.mts.map +1 -0
- package/dist/{index-B_wWGszy.d.mts → index-D7mFNjot.d.mts} +1 -5
- package/dist/index-D7mFNjot.d.mts.map +1 -0
- package/dist/{index-CUZn-ohG.d.mts → index-D8rDE60Y.d.mts} +23 -54
- package/dist/index-D8rDE60Y.d.mts.map +1 -0
- package/dist/{index-By6dNRc4.d.mts → index-DR7hzXU4.d.mts} +3 -23
- package/dist/{index-By6dNRc4.d.mts.map → index-DR7hzXU4.d.mts.map} +1 -1
- package/dist/{index-DKS1g1oC.d.mts → index-DfQGXBQI.d.mts} +54 -45
- package/dist/index-DfQGXBQI.d.mts.map +1 -0
- package/dist/{index-BNQ9xSAz.d.mts → index-MsJqfQu0.d.mts} +64 -70
- package/dist/index-MsJqfQu0.d.mts.map +1 -0
- package/dist/{index-CGiLfREk.d.mts → index-UINIHFuh.d.mts} +39 -15
- package/dist/index-UINIHFuh.d.mts.map +1 -0
- package/dist/{index-BiiE8NS7.d.mts → index-crtzMG48.d.mts} +14 -23
- package/dist/index-crtzMG48.d.mts.map +1 -0
- package/dist/{index-B1-tBzc0.d.mts → index-dCRymj_g.d.mts} +23 -71
- package/dist/{index-B1-tBzc0.d.mts.map → index-dCRymj_g.d.mts.map} +1 -1
- package/dist/index-uE3S3Krx.d.mts +245 -0
- package/dist/index-uE3S3Krx.d.mts.map +1 -0
- package/dist/index.d.mts +21 -19
- package/dist/index.mjs +1 -1
- package/dist/layer/index.d.mts +1 -1
- package/dist/layer/index.mjs +1 -1
- package/dist/{layer-BttmtDrs.mjs → layer-CKtH7TRL.mjs} +2 -2
- package/dist/layer-CKtH7TRL.mjs.map +1 -0
- package/dist/{layer.types-DgpCIsk_.d.mts → layer.types-BB0MrvLg.d.mts} +4 -4
- package/dist/{layer.types-DgpCIsk_.d.mts.map → layer.types-BB0MrvLg.d.mts.map} +1 -1
- package/dist/multithread/index.d.mts +1 -1
- package/dist/multithread/index.mjs +1 -1
- package/dist/{multithread-xUUh4eLn.mjs → multithread-Cyc8Bz45.mjs} +2 -2
- package/dist/multithread-Cyc8Bz45.mjs.map +1 -0
- package/dist/option/index.d.mts +1 -1
- package/dist/option/index.mjs +1 -1
- package/dist/{option-Tfbo4wty.mjs → option-C2iCxAuJ.mjs} +2 -2
- package/dist/option-C2iCxAuJ.mjs.map +1 -0
- package/dist/{option.types-D1mm0zUb.mjs → option.types-CbY_swma.mjs} +1 -1
- package/dist/{option.types-D1mm0zUb.mjs.map → option.types-CbY_swma.mjs.map} +1 -1
- package/dist/{option.types-qPevEZQd.d.mts → option.types-D9hrKcfa.d.mts} +3 -3
- package/dist/{option.types-qPevEZQd.d.mts.map → option.types-D9hrKcfa.d.mts.map} +1 -1
- package/dist/order/index.d.mts +1 -1
- package/dist/order/index.mjs +1 -1
- package/dist/order-BXOBEKvB.mjs +2 -0
- package/dist/order-BXOBEKvB.mjs.map +1 -0
- package/dist/{pipeable-rfqacPxZ.d.mts → pipeable-BIrevC0D.d.mts} +1 -1
- package/dist/{pipeable-rfqacPxZ.d.mts.map → pipeable-BIrevC0D.d.mts.map} +1 -1
- package/dist/pipeable-Dp1_23zH.mjs +2 -0
- package/dist/{pipeable-COGyGMUV.mjs.map → pipeable-Dp1_23zH.mjs.map} +1 -1
- package/dist/predicate/index.d.mts +1 -1
- package/dist/predicate/index.mjs +1 -1
- package/dist/{predicate-DUhhQqWY.mjs → predicate-D_1SsIi4.mjs} +2 -2
- package/dist/predicate-D_1SsIi4.mjs.map +1 -0
- package/dist/provide/index.d.mts +1 -1
- package/dist/provide/index.mjs +1 -1
- package/dist/{provide-BmSM3Ruy.mjs → provide--yZE8x-n.mjs} +2 -2
- package/dist/provide--yZE8x-n.mjs.map +1 -0
- package/dist/queue/index.d.mts +1 -1
- package/dist/queue/index.mjs +1 -1
- package/dist/{queue-Sg6KJerl.mjs → queue-apiEOlRD.mjs} +2 -2
- package/dist/queue-apiEOlRD.mjs.map +1 -0
- package/dist/{queue.types-CD2LOu37.d.mts → queue.types-B-l5XYbU.d.mts} +1 -1
- package/dist/{queue.types-CD2LOu37.d.mts.map → queue.types-B-l5XYbU.d.mts.map} +1 -1
- package/dist/result/index.d.mts +1 -1
- package/dist/result/index.mjs +1 -1
- package/dist/{result-BEzV0DYC.mjs → result-D3VY0qBG.mjs} +2 -2
- package/dist/result-D3VY0qBG.mjs.map +1 -0
- package/dist/{result.types-_xDAei3-.d.mts → result.types-BKzChyWY.d.mts} +3 -3
- package/dist/{result.types-_xDAei3-.d.mts.map → result.types-BKzChyWY.d.mts.map} +1 -1
- package/dist/schedule/index.d.mts +1 -1
- package/dist/schedule/index.mjs +1 -1
- package/dist/schedule-B9K_2Z21.d.mts +183 -0
- package/dist/schedule-B9K_2Z21.d.mts.map +1 -0
- package/dist/schedule-C6iN3oMt.mjs +2 -0
- package/dist/schedule-C6iN3oMt.mjs.map +1 -0
- package/dist/schema/index.d.mts +2 -0
- package/dist/schema/index.mjs +1 -0
- package/dist/schema-D87TVF_b.mjs +2 -0
- package/dist/schema-D87TVF_b.mjs.map +1 -0
- package/dist/schema.shared-CI4eydjX.mjs +2 -0
- package/dist/schema.shared-CI4eydjX.mjs.map +1 -0
- package/dist/schema.types-CFzzx4bw.d.mts +45 -0
- package/dist/schema.types-CFzzx4bw.d.mts.map +1 -0
- package/dist/scope/index.d.mts +1 -1
- package/dist/scope/index.mjs +1 -1
- package/dist/{scope-CZdp4wKX.d.mts → scope-CuM3CzwG.d.mts} +3 -9
- package/dist/scope-CuM3CzwG.d.mts.map +1 -0
- package/dist/{scope-D_kzd1nT.mjs → scope-gVt4PESc.mjs} +2 -2
- package/dist/scope-gVt4PESc.mjs.map +1 -0
- package/dist/service/index.d.mts +1 -1
- package/dist/service/index.mjs +1 -1
- package/dist/{service-3PYQTUdH.mjs → service-CWAIEH46.mjs} +2 -2
- package/dist/service-CWAIEH46.mjs.map +1 -0
- package/dist/{service-DrXU7KJG.d.mts → service-D8mr0wwg.d.mts} +2 -8
- package/dist/service-D8mr0wwg.d.mts.map +1 -0
- package/dist/{service-resolution-C19smeaO.mjs → service-resolution-BefYr4nR.mjs} +1 -1
- package/dist/{service-resolution-C19smeaO.mjs.map → service-resolution-BefYr4nR.mjs.map} +1 -1
- package/package.json +9 -1
- package/dist/adt-DajUZvJe.mjs +0 -2
- package/dist/adt-DajUZvJe.mjs.map +0 -1
- package/dist/brand-Bia3Vj6l.mjs +0 -2
- package/dist/brand-Bia3Vj6l.mjs.map +0 -1
- package/dist/context-CCHj1nab.mjs.map +0 -1
- package/dist/context-r8ESJiFn.d.mts.map +0 -1
- package/dist/data.tagged-error.types-CGiKD-ES.d.mts +0 -29
- package/dist/data.tagged-error.types-CGiKD-ES.d.mts.map +0 -1
- package/dist/discriminator.types-CTURejXz.d.mts.map +0 -1
- package/dist/either-BMLPfvMl.mjs.map +0 -1
- package/dist/flow-D8_tllWl.mjs.map +0 -1
- package/dist/functions-BkevX2Dw.mjs +0 -2
- package/dist/functions-BkevX2Dw.mjs.map +0 -1
- package/dist/fx-K-a9Smhn.mjs +0 -2
- package/dist/fx-K-a9Smhn.mjs.map +0 -1
- package/dist/index-7Lv982Om.d.mts +0 -217
- package/dist/index-7Lv982Om.d.mts.map +0 -1
- package/dist/index-BNQ9xSAz.d.mts.map +0 -1
- package/dist/index-B_iY5tq0.d.mts.map +0 -1
- package/dist/index-B_wWGszy.d.mts.map +0 -1
- package/dist/index-BiiE8NS7.d.mts.map +0 -1
- package/dist/index-CGiLfREk.d.mts.map +0 -1
- package/dist/index-CUZn-ohG.d.mts.map +0 -1
- package/dist/index-Cq2IFito.d.mts.map +0 -1
- package/dist/index-DEAWPlcI.d.mts.map +0 -1
- package/dist/index-DKS1g1oC.d.mts.map +0 -1
- package/dist/index-DXbYlSnB.d.mts.map +0 -1
- package/dist/index-UzMbg1dh.d.mts.map +0 -1
- package/dist/layer-BttmtDrs.mjs.map +0 -1
- package/dist/multithread-xUUh4eLn.mjs.map +0 -1
- package/dist/option-Tfbo4wty.mjs.map +0 -1
- package/dist/order-D5c4QChk.mjs +0 -2
- package/dist/order-D5c4QChk.mjs.map +0 -1
- package/dist/pipeable-COGyGMUV.mjs +0 -2
- package/dist/predicate-DUhhQqWY.mjs.map +0 -1
- package/dist/provide-BmSM3Ruy.mjs.map +0 -1
- package/dist/queue-Sg6KJerl.mjs.map +0 -1
- package/dist/result-BEzV0DYC.mjs.map +0 -1
- package/dist/schedule-C6tjcJ1O.mjs +0 -2
- package/dist/schedule-C6tjcJ1O.mjs.map +0 -1
- package/dist/schedule-DlX2Dg69.d.mts +0 -144
- package/dist/schedule-DlX2Dg69.d.mts.map +0 -1
- package/dist/scope-CZdp4wKX.d.mts.map +0 -1
- package/dist/scope-D_kzd1nT.mjs.map +0 -1
- package/dist/service-3PYQTUdH.mjs.map +0 -1
- package/dist/service-DrXU7KJG.d.mts.map +0 -1
- /package/dist/{chunk-C934ptG5.mjs → chunk-oQKkju2G.mjs} +0 -0
- /package/dist/{option-CBCwzF0L.mjs → option-CXXiA1w-.mjs} +0 -0
- /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
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
###
|
|
185
|
+
### Brand
|
|
176
186
|
|
|
177
|
-
|
|
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 {
|
|
193
|
+
import { Brand } from "@nicolastoulemont/std"
|
|
183
194
|
|
|
184
|
-
|
|
185
|
-
const ClockLive = Layer.ok(Clock, { now: () => Date.now() })
|
|
195
|
+
type Port = Brand.Branded<number, "Port">
|
|
186
196
|
|
|
187
|
-
const
|
|
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
|
|
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 {
|
|
205
|
+
import { Brand, Result, pipe } from "@nicolastoulemont/std"
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
const ApiLive = Layer.ok(Api, {
|
|
208
|
-
postOrder: async () => ({ orderId: "ord_42" }),
|
|
209
|
-
})
|
|
207
|
+
type Email = Brand.Branded<string, "Email">
|
|
210
208
|
|
|
211
|
-
|
|
209
|
+
const toEmail = Brand.refine<Email>(
|
|
210
|
+
(value) => value.includes("@"),
|
|
211
|
+
(value) => `Invalid email: ${value}`,
|
|
212
|
+
)
|
|
212
213
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
Result.
|
|
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
|
-
|
|
222
|
+
### Predicate
|
|
234
223
|
|
|
235
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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
|
-
####
|
|
239
|
+
#### Real-World Example
|
|
283
240
|
|
|
284
241
|
```ts
|
|
285
|
-
import {
|
|
242
|
+
import { Predicate } from "@nicolastoulemont/std"
|
|
286
243
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
|
312
|
-
const second = queue.enqueue(async () => 2)
|
|
252
|
+
const isSearchInput = Predicate.and<SearchInput>(hasQuery, hasSafeLimit)
|
|
313
253
|
|
|
314
|
-
|
|
315
|
-
await queue.shutdown({ mode: "drain" })
|
|
254
|
+
const canSearch = isSearchInput({ q: "books", limit: 20 })
|
|
316
255
|
```
|
|
317
256
|
|
|
318
|
-
|
|
257
|
+
### Schema
|
|
319
258
|
|
|
320
|
-
|
|
321
|
-
|
|
259
|
+
Schema wraps Standard Schema-compatible validators for two production use cases:
|
|
260
|
+
boundary parsing and sync-only refinement.
|
|
322
261
|
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
267
|
+
```ts
|
|
268
|
+
import { Result, Schema } from "@nicolastoulemont/std"
|
|
269
|
+
import { z } from "zod"
|
|
335
270
|
|
|
336
|
-
|
|
271
|
+
type Ticket = {
|
|
272
|
+
channel: "chat" | "email"
|
|
273
|
+
chatId?: string | null
|
|
274
|
+
metadata?: {
|
|
275
|
+
conversationId?: string | null
|
|
276
|
+
} | null
|
|
277
|
+
}
|
|
337
278
|
|
|
338
|
-
|
|
279
|
+
type ChatTicket = {
|
|
280
|
+
channel: "chat"
|
|
281
|
+
chatId: string
|
|
282
|
+
metadata: {
|
|
283
|
+
conversationId: string
|
|
284
|
+
}
|
|
285
|
+
}
|
|
339
286
|
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
import { Multithread } from "@nicolastoulemont/std"
|
|
295
|
+
const parseChatTicket = Schema.parse(ChatTicketSchema)
|
|
344
296
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
297
|
+
const result = parseChatTicket({
|
|
298
|
+
channel: "chat",
|
|
299
|
+
chatId: "chat_123",
|
|
300
|
+
metadata: { conversationId: "conv_123" },
|
|
301
|
+
})
|
|
349
302
|
|
|
350
|
-
|
|
303
|
+
if (Result.isOk(result)) {
|
|
304
|
+
result.value.metadata.conversationId
|
|
305
|
+
}
|
|
351
306
|
```
|
|
352
307
|
|
|
353
|
-
####
|
|
308
|
+
#### In-Memory Refinement Example
|
|
354
309
|
|
|
355
310
|
```ts
|
|
356
|
-
import {
|
|
311
|
+
import { Schema } from "@nicolastoulemont/std"
|
|
312
|
+
import { z } from "zod"
|
|
357
313
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
322
|
+
type ChatTicket = {
|
|
323
|
+
channel: "chat"
|
|
324
|
+
chatId: string
|
|
325
|
+
metadata: {
|
|
326
|
+
conversationId: string
|
|
327
|
+
}
|
|
328
|
+
}
|
|
373
329
|
|
|
374
|
-
|
|
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
|
|
378
|
-
```
|
|
338
|
+
const isChatTicket = Schema.is(ChatTicketSchema)
|
|
379
339
|
|
|
380
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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 {
|
|
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)
|
|
926
|
+
const result = pipe(10, (n) => n + 1, toLabel)
|
|
525
927
|
```
|
|
526
928
|
|
|
527
929
|
#### Real-World Example
|