@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.
- package/README.md +571 -166
- package/dist/adt/index.d.mts +1 -1
- package/dist/adt/index.mjs +1 -1
- package/dist/adt-CPG_sa8q.mjs +2 -0
- package/dist/adt-CPG_sa8q.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-B4WfexUL.d.mts +57 -0
- package/dist/index-B4WfexUL.d.mts.map +1 -0
- 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-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-DUki2Bcp.d.mts} +54 -45
- package/dist/index-DUki2Bcp.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-CJ-aaQe8.mjs +2 -0
- package/dist/schema-CJ-aaQe8.mjs.map +1 -0
- package/dist/schema.shared-Bjyroa6b.mjs +2 -0
- package/dist/schema.shared-Bjyroa6b.mjs.map +1 -0
- package/dist/schema.types-CBEXCwfs.d.mts +60 -0
- package/dist/schema.types-CBEXCwfs.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,188 @@ 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
|
|
244
|
+
type SearchInput = {
|
|
245
|
+
q: string
|
|
246
|
+
limit: number
|
|
247
|
+
}
|
|
301
248
|
|
|
302
|
-
|
|
249
|
+
const hasQuery = (input: SearchInput) => input.q.trim().length > 0
|
|
250
|
+
const hasSafeLimit = (input: SearchInput) => input.limit > 0 && input.limit <= 100
|
|
303
251
|
|
|
304
|
-
|
|
252
|
+
const isSearchInput = Predicate.and<SearchInput>(hasQuery, hasSafeLimit)
|
|
305
253
|
|
|
306
|
-
|
|
307
|
-
|
|
254
|
+
const canSearch = isSearchInput({ q: "books", limit: 20 })
|
|
255
|
+
```
|
|
308
256
|
|
|
309
|
-
|
|
257
|
+
### Schema
|
|
310
258
|
|
|
311
|
-
|
|
312
|
-
|
|
259
|
+
Schema wraps Standard Schema-compatible validators for two production use cases:
|
|
260
|
+
boundary parsing and sync-only refinement.
|
|
313
261
|
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
####
|
|
268
|
+
#### Boundary Parsing Example
|
|
319
269
|
|
|
320
270
|
```ts
|
|
321
|
-
import {
|
|
271
|
+
import { Result, Schema } from "@nicolastoulemont/std"
|
|
272
|
+
import { z } from "zod"
|
|
322
273
|
|
|
323
|
-
|
|
274
|
+
type Ticket = {
|
|
275
|
+
channel: "chat" | "email"
|
|
276
|
+
chatId?: string | null
|
|
277
|
+
metadata?: {
|
|
278
|
+
conversationId?: string | null
|
|
279
|
+
} | null
|
|
280
|
+
}
|
|
324
281
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
333
|
-
await imageQueue.shutdown({ mode: "drain" })
|
|
334
|
-
```
|
|
298
|
+
const parseChatTicket = Schema.parse(ChatTicketSchema)
|
|
335
299
|
|
|
336
|
-
|
|
300
|
+
const result = parseChatTicket({
|
|
301
|
+
channel: "chat",
|
|
302
|
+
chatId: "chat_123",
|
|
303
|
+
metadata: { conversationId: "conv_123" },
|
|
304
|
+
})
|
|
337
305
|
|
|
338
|
-
|
|
306
|
+
if (Result.isOk(result)) {
|
|
307
|
+
result.value.metadata.conversationId
|
|
308
|
+
}
|
|
309
|
+
```
|
|
339
310
|
|
|
340
|
-
####
|
|
311
|
+
#### In-Memory Refinement Example
|
|
341
312
|
|
|
342
313
|
```ts
|
|
343
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
326
|
+
type ChatTicketFields = {
|
|
327
|
+
channel: "chat"
|
|
328
|
+
chatId: string
|
|
329
|
+
}
|
|
354
330
|
|
|
355
|
-
|
|
356
|
-
|
|
331
|
+
const ChatTicketSchema: Schema.SyncSchema<PersistedTicket, ChatTicketFields> = z.object({
|
|
332
|
+
channel: z.literal("chat"),
|
|
333
|
+
chatId: z.string(),
|
|
334
|
+
})
|
|
357
335
|
|
|
358
|
-
|
|
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
|
-
|
|
338
|
+
const isChatTicket = Schema.refine(ChatTicketSchema)
|
|
373
339
|
|
|
374
|
-
|
|
375
|
-
})
|
|
340
|
+
declare const ticket: PersistedTicket
|
|
376
341
|
|
|
377
|
-
|
|
342
|
+
if (isChatTicket(ticket)) {
|
|
343
|
+
ticket.id
|
|
344
|
+
ticket.chatId.toUpperCase()
|
|
345
|
+
}
|
|
378
346
|
```
|
|
379
347
|
|
|
380
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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 {
|
|
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)
|
|
929
|
+
const result = pipe(10, (n) => n + 1, toLabel)
|
|
525
930
|
```
|
|
526
931
|
|
|
527
932
|
#### Real-World Example
|