@onrails/result 0.4.0 → 0.4.2
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/DESIGN.md +1 -1
- package/README.md +168 -12
- package/RECIPES.md +11 -9
- package/dist/async-fns.cjs +52 -0
- package/dist/async-fns.cjs.map +1 -0
- package/dist/async-fns.d.cts +143 -0
- package/dist/async-fns.d.ts +143 -0
- package/dist/async-fns.js +42 -0
- package/dist/async-fns.js.map +1 -0
- package/dist/async-lift.d.cts +109 -0
- package/dist/async-lift.d.ts +109 -0
- package/dist/{async-Ci6WSL5-.d.ts → async.d.cts} +45 -58
- package/dist/{async-DcXR7k9K.d.cts → async.d.ts} +45 -58
- package/dist/collections.d.cts +105 -0
- package/dist/collections.d.ts +105 -0
- package/dist/compat/neverthrow.cjs +57 -45
- package/dist/compat/neverthrow.cjs.map +1 -1
- package/dist/compat/neverthrow.d.cts +21 -24
- package/dist/compat/neverthrow.d.ts +21 -24
- package/dist/compat/neverthrow.js +56 -44
- package/dist/compat/neverthrow.js.map +1 -1
- package/dist/extra.cjs +3 -1
- package/dist/extra.cjs.map +1 -1
- package/dist/extra.d.cts +11 -14
- package/dist/extra.d.ts +11 -14
- package/dist/extra.js +3 -1
- package/dist/extra.js.map +1 -1
- package/dist/fluent.cjs +15 -13
- package/dist/fluent.cjs.map +1 -1
- package/dist/fluent.d.cts +4 -7
- package/dist/fluent.d.ts +4 -7
- package/dist/fluent.js +15 -13
- package/dist/fluent.js.map +1 -1
- package/dist/index.cjs +77 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -490
- package/dist/index.d.ts +28 -490
- package/dist/index.js +77 -53
- package/dist/index.js.map +1 -1
- package/dist/internal/ctor.d.cts +11 -0
- package/dist/internal/ctor.d.ts +11 -0
- package/dist/internal/dual.d.cts +42 -0
- package/dist/internal/dual.d.ts +42 -0
- package/dist/internal/infer.d.cts +40 -0
- package/dist/internal/infer.d.ts +40 -0
- package/dist/internal/payload.d.cts +17 -0
- package/dist/internal/payload.d.ts +17 -0
- package/dist/internal/to-error.d.cts +2 -0
- package/dist/internal/to-error.d.ts +2 -0
- package/dist/pipe.cjs +8 -0
- package/dist/pipe.cjs.map +1 -1
- package/dist/pipe.d.cts +58 -11
- package/dist/pipe.d.ts +58 -11
- package/dist/pipe.js +8 -1
- package/dist/pipe.js.map +1 -1
- package/dist/railway.cjs +87 -63
- package/dist/railway.cjs.map +1 -1
- package/dist/railway.d.cts +49 -19
- package/dist/railway.d.ts +49 -19
- package/dist/railway.js +87 -63
- package/dist/railway.js.map +1 -1
- package/dist/result.d.cts +317 -0
- package/dist/result.d.ts +317 -0
- package/dist/try-gen.cjs +3 -3
- package/dist/try-gen.cjs.map +1 -1
- package/dist/try-gen.d.cts +4 -7
- package/dist/try-gen.d.ts +4 -7
- package/dist/try-gen.js +3 -3
- package/dist/try-gen.js.map +1 -1
- package/dist/{types-BQ9vv0nD.d.cts → types.d.cts} +6 -8
- package/dist/{types-BQ9vv0nD.d.ts → types.d.ts} +6 -8
- package/package.json +69 -24
- package/skills/railway-do-notation/SKILL.md +9 -9
package/DESIGN.md
CHANGED
package/README.md
CHANGED
|
@@ -11,9 +11,13 @@ bun add @onrails/result
|
|
|
11
11
|
## Quick start (value-first — best inference)
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { err, flatMap, match, ok, trySync } from "@onrails/result";
|
|
14
|
+
import { err, flatMap, fromThrowable, match, ok, trySync } from "@onrails/result";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Run it now — the sync mirror of `tryAsync`.
|
|
17
|
+
const parsed = trySync(() => JSON.parse(raw)); // Result<unknown, Error>
|
|
18
|
+
|
|
19
|
+
// Or lift the throwing function once and reuse it.
|
|
20
|
+
const parse = fromThrowable(
|
|
17
21
|
(raw: string) => JSON.parse(raw),
|
|
18
22
|
(e) => ({ kind: "parse" as const, message: String(e) }),
|
|
19
23
|
);
|
|
@@ -21,24 +25,56 @@ const parse = trySync(
|
|
|
21
25
|
const pipeline = flatMap(parse('{"v":1}'), (data) => ok(data.v));
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
Every transform is dual-form: data-first `flatMap(r, fn)` (best inference) or curried `flatMap(fn)(r)` for `pipe`/`flow`. Long chains: `fluent()` from `@onrails/result/fluent`.
|
|
25
|
-
|
|
26
28
|
For worked examples of multi-step pipelines, parser builders, validator ladders, and parallel sub-workflows see [RECIPES.md](./RECIPES.md).
|
|
27
29
|
|
|
28
|
-
##
|
|
30
|
+
## The default path
|
|
31
|
+
|
|
32
|
+
Two forms cover almost everything. Learn these; treat the rest of this README as reference for the cases they do not fit.
|
|
33
|
+
|
|
34
|
+
| Sync | data-first module functions — `flatMap(r, fn)`, `map`, `match` |
|
|
35
|
+
| ----- | -------------------------------------------------------------- |
|
|
36
|
+
| Async | `ResultAsync` dot-chaining — `ra.flatMap(fn).map(fn).match(…)` |
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// sync
|
|
40
|
+
const name = match(
|
|
41
|
+
flatMap(parseConfig(raw), (cfg) => (cfg.name ? ok(cfg.name) : err({ kind: "missing" }))),
|
|
42
|
+
(n) => n,
|
|
43
|
+
(e) => `<${e.kind}>`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// async — `.flatMap` also accepts a plain sync `Result`, so no manual lifting
|
|
47
|
+
const count = await loadUser(id).flatMap(loadOrders).map((os) => os.length).unwrapOr(0);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Data-first gives the best inference, and `ResultAsync.flatMap` takes either carrier, so mixed sync/async chains need no lift step.
|
|
51
|
+
|
|
52
|
+
## Other syntaxes (opt-in)
|
|
53
|
+
|
|
54
|
+
Same operations, different shape. Reach for one only when it removes real nesting or naming pain — none of them unlock behavior the default path lacks.
|
|
29
55
|
|
|
30
56
|
| Shape | Reach for |
|
|
31
57
|
| ---------------------------------- | -------------------------------------------------------------------- |
|
|
32
|
-
| One or two sync steps | `flatMapResult`, `mapResult`, `match` |
|
|
33
|
-
| One or two async steps | `ResultAsync.flatMap`, `asyncAfter` |
|
|
34
58
|
| Long sync chain, value-first | `pipe(r, map(...), flatMap(...), ...)` |
|
|
59
|
+
| Long async chain, value-first | `pipe(ra, RA.map(...), RA.flatMap(...))` from `@onrails/result/async` |
|
|
35
60
|
| Long sync chain, dot-style preferred | `fluent(r)` from `@onrails/result/fluent` |
|
|
36
61
|
| Reusable composed function | `flow(...)` from `@onrails/result/pipe` |
|
|
37
62
|
| Several named sync/async steps | `Railway.*` builder from `@onrails/result/railway` |
|
|
38
63
|
| Linear sync with early-return feel | `tryGen` + `$` from `@onrails/result/try-gen` |
|
|
64
|
+
|
|
65
|
+
Every transform is dual-form: data-first `flatMap(r, fn)` or curried `flatMap(fn)(r)` for `pipe` / `flow`.
|
|
66
|
+
|
|
67
|
+
## Boundaries and collections
|
|
68
|
+
|
|
69
|
+
These are not alternative syntaxes — they are the entry points into the railway.
|
|
70
|
+
|
|
71
|
+
| Shape | Reach for |
|
|
72
|
+
| ---------------------------------- | -------------------------------------------------------------------- |
|
|
39
73
|
| Independent validations, accumulated failures | `validateAll` / `validateTuple` from `@onrails/result` |
|
|
40
74
|
| Sync → async lift, keep error type | `fromResult`, `asyncAfter` (do **not** use `fromAsync` here) |
|
|
41
75
|
| `Promise<Result<…>>` boundary lift | `fromAsync` / `tryAsync` |
|
|
76
|
+
| Throwing sync call, run it now | `trySync(() => f())` — mirrors `tryAsync(promise)` |
|
|
77
|
+
| Throwing sync function, reusable | `fromThrowable(f, onThrow)` — neverthrow's `Result.fromThrowable` |
|
|
42
78
|
|
|
43
79
|
Rule of thumb: pick the smallest tool that removes nesting. Reach for `Railway` only when named context replaces positional tuple plumbing.
|
|
44
80
|
|
|
@@ -60,7 +96,7 @@ Use `asyncAfter` for the common "validate synchronously, then run async IO" shap
|
|
|
60
96
|
import { asyncAfter, tryAsync, trySync } from "@onrails/result";
|
|
61
97
|
|
|
62
98
|
return asyncAfter(
|
|
63
|
-
trySync(() => ArtifactSchema.parse(artifact), toError)
|
|
99
|
+
trySync(() => ArtifactSchema.parse(artifact), toError),
|
|
64
100
|
(validated) =>
|
|
65
101
|
tryAsync(
|
|
66
102
|
getDb()
|
|
@@ -94,6 +130,42 @@ type BotError =
|
|
|
94
130
|
|
|
95
131
|
Helpers: `@onrails/result/extra` — `hasKind`, `mapErrKind`, `declareErrors`, `UnionErrors`, `AccumulateErrors`.
|
|
96
132
|
|
|
133
|
+
### Literals stay narrow
|
|
134
|
+
|
|
135
|
+
`ok` and `err` lock an inline literal instead of widening it, so a tagged error keeps its `kind` without `as const`:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
err({ kind: "not_found", id }); // Result<never, { readonly kind: "not_found"; readonly id: string }>
|
|
139
|
+
ok("ready"); // Result<"ready", never>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Locking is inference-only — a value that already has a type is left alone (`ok(name)` where `name: string` is still `Result<string, never>`), and an array literal payload stays assignable to a mutable array at every depth (`ok([1, 2])` satisfies `Result<number[], E>`).
|
|
143
|
+
|
|
144
|
+
Generic code needs no type arguments — an unresolved `T` passes through unchanged, whatever its constraint:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const lift = <T, E>(value: T): Result<T, E> => ok(value);
|
|
148
|
+
const toAsync = <T, E>(r: Result<T, E>): ResultAsync<T, E> =>
|
|
149
|
+
isErr(r) ? errAsync(r.error) : okAsync(r.value);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Passing **both** type arguments (`ok<T, E>(value)`) opts out of locking.
|
|
153
|
+
|
|
154
|
+
Array-literal locking needs TypeScript ≥ 5.3. Older compilers widen array literals (`ok([1, 2])` is `Result<number[], never>`) and lock every other literal as usual.
|
|
155
|
+
|
|
156
|
+
### Naming the error channel
|
|
157
|
+
|
|
158
|
+
One explicit type argument on `err` names the **error**:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
type AppError = { kind: "parse" } | { kind: "io" };
|
|
162
|
+
|
|
163
|
+
err<AppError>({ kind: "io" }); // Result<never, AppError>
|
|
164
|
+
err<number, AppError>({ kind: "io" }); // Result<number, AppError> — neverthrow's T, E order
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The two-argument form keeps neverthrow's order. `errAsync` / `ResultAsync.err` behave the same way.
|
|
168
|
+
|
|
97
169
|
## Async interop — `fromAsync`
|
|
98
170
|
|
|
99
171
|
Lift `async` handlers that return `Result` without leaking `Promise<Result<…>>`:
|
|
@@ -132,6 +204,20 @@ import { match } from "ts-pattern";
|
|
|
132
204
|
R.match(result, onOk, onErr);
|
|
133
205
|
```
|
|
134
206
|
|
|
207
|
+
For an object-form, self-labelling fold, use `matchTag` from [`@onrails/pattern`](../pattern/README.md) — it dispatches on `_tag` and requires one branch per tag, so the branches read as names instead of argument positions:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { matchTag } from "@onrails/pattern";
|
|
211
|
+
|
|
212
|
+
const label = (r: Result<number, string>) =>
|
|
213
|
+
matchTag(r, {
|
|
214
|
+
Ok: (v) => `ok:${v.value}`, // v: Ok<number> — the member, not the payload
|
|
215
|
+
Err: (e) => `err:${e.error}`, // e: Err<string>
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Note the difference: `match` hands each branch the *payload*, `matchTag` hands it the narrowed *member*.
|
|
220
|
+
|
|
135
221
|
`unwrapOk` and `unwrapErr` are test/assertion helpers. Prefer `match`, `isOk`, or `isErr` in production control flow.
|
|
136
222
|
|
|
137
223
|
```ts
|
|
@@ -163,7 +249,7 @@ import { ResultAsync } from "@onrails/result";
|
|
|
163
249
|
const combined = ResultAsync.combineTuple([
|
|
164
250
|
loadSettings(),
|
|
165
251
|
loadModelCatalog(),
|
|
166
|
-
]
|
|
252
|
+
]);
|
|
167
253
|
|
|
168
254
|
const dto = combined.map(([settings, catalog]) =>
|
|
169
255
|
buildDto(settings, catalog),
|
|
@@ -172,6 +258,25 @@ const dto = combined.map(([settings, catalog]) =>
|
|
|
172
258
|
|
|
173
259
|
When TS only infers the first error in a generator-style flow, use `declareErrors<E1 | E2>()` from `/extra`.
|
|
174
260
|
|
|
261
|
+
## Async pipelines — `@onrails/result/async`
|
|
262
|
+
|
|
263
|
+
`ResultAsync` is a class, so dot-chaining is always available and stays the shortest form for a one-off chain. When you want the sync side's *other* two syntaxes on the async carrier — value-first `pipe`, or point-free `flow` — import the data-last twins as a namespace:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import * as RA from "@onrails/result/async";
|
|
267
|
+
import { pipe } from "@onrails/result";
|
|
268
|
+
import { flow } from "@onrails/result/pipe";
|
|
269
|
+
|
|
270
|
+
// value-first
|
|
271
|
+
pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
272
|
+
// ResultAsync<number, NotFound | DbError>
|
|
273
|
+
|
|
274
|
+
// reusable point-free pipeline — no value yet
|
|
275
|
+
const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Every `ResultAsync` method has a twin here — `map`, `mapErr`, `bimap`, `flatMap`, `recover`, `tap`, `tapErr`, `match`, `unwrapOr` — each in both data-first and curried form. The terminals (`match`, `unwrapOr`) resolve to a plain `Promise`, ending the pipeline.
|
|
279
|
+
|
|
175
280
|
## `Railway` — named service workflows
|
|
176
281
|
|
|
177
282
|
Use `Railway` from `@onrails/result/railway` when a service workflow has several named sync/async steps and would otherwise need manual context-carrying objects:
|
|
@@ -195,7 +300,15 @@ const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), t
|
|
|
195
300
|
|
|
196
301
|
Sync-only workflows return `Result<T, E>`. The first `fromPromise`, `fromAsync`, or `parallel` step upgrades the output to `ResultAsync<T, E>`.
|
|
197
302
|
|
|
198
|
-
|
|
303
|
+
Step keys must be unique. Reusing one is a compile error naming the offending key — previously it overwrote at runtime while the type silently collapsed to `never`:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
Railway.empty()
|
|
307
|
+
.derive("x", () => 1)
|
|
308
|
+
.derive("x", () => "two"); // ✗ key "x" already exists in the workflow context
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMap`) for one or two steps where a builder would add ceremony.
|
|
199
312
|
|
|
200
313
|
To share steps across workflows, extract plain functions of the context and plug them in via `.fromResult` / `.fromAsync`:
|
|
201
314
|
|
|
@@ -211,6 +324,8 @@ const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), t
|
|
|
211
324
|
|
|
212
325
|
## Pipe
|
|
213
326
|
|
|
327
|
+
`pipe` and `flow` both live in `@onrails/result/pipe`; `pipe` is also re-exported from the package root.
|
|
328
|
+
|
|
214
329
|
```ts
|
|
215
330
|
import { pipe } from "@onrails/result";
|
|
216
331
|
import { flow } from "@onrails/result/pipe";
|
|
@@ -233,10 +348,50 @@ const parseUserName = flow(
|
|
|
233
348
|
parseUserName(raw);
|
|
234
349
|
```
|
|
235
350
|
|
|
351
|
+
Both cap at **12 steps**. That cap is deliberate: a single recursive variadic signature would remove it, but it cannot give each step a contextual type from the previous step's output, so lambda parameters degrade to `unknown` (`map((cfg) => cfg.user)` stops inferring). Longer chains nest — `pipe(pipe(v, ...), ...)` — or factor a segment into a named `flow`.
|
|
352
|
+
|
|
236
353
|
## ESLint
|
|
237
354
|
|
|
238
355
|
`@onrails/eslint-plugin` — warns on `Promise<Result<…>>` and `_unsafeUnwrap*`.
|
|
239
356
|
|
|
357
|
+
## Breaking changes
|
|
358
|
+
|
|
359
|
+
### `ok` / `err` lock inline literals
|
|
360
|
+
|
|
361
|
+
Inline literals no longer widen: `ok("a")` is `Result<"a", never>` and `err({ kind: "io" })` carries `{ readonly kind: "io" }`. Annotated values and generic type parameters are unaffected. One call shape needs a change:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
// a type-level assertion that expected the widened type
|
|
365
|
+
expectType<TypeEqual<typeof ok(1), Result<number, never>>>(true); // before
|
|
366
|
+
expectType<TypeEqual<typeof value, Result<1, never>>>(true); // after
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### One type argument on `err` now names the error
|
|
370
|
+
|
|
371
|
+
`err<AppError>(e)` used to set the *`Ok`* parameter and leave the error at `unknown`. It now returns `Result<never, AppError>`. Call sites that meant the old reading fail to compile; use `err<AppError, unknown>(e)` to restore it, or drop the argument and let inference lock the literal. The compat shim (`@onrails/result/compat/neverthrow`) is unchanged.
|
|
372
|
+
|
|
373
|
+
### `trySync` is now eager; the lazy form is `fromThrowable`
|
|
374
|
+
|
|
375
|
+
`trySync` used to lift a throwing *function* and return a wrapper, while
|
|
376
|
+
`tryAsync` took a *value* and ran immediately — same prefix, opposite contract.
|
|
377
|
+
The two now agree, and the lifting form takes neverthrow's own name.
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// before
|
|
381
|
+
const parse = trySync(JSON.parse, toErr);
|
|
382
|
+
parse(raw);
|
|
383
|
+
trySync(() => Schema.parse(input), toErr)(); // note the trailing ()
|
|
384
|
+
|
|
385
|
+
// after
|
|
386
|
+
const parse = fromThrowable(JSON.parse, toErr);
|
|
387
|
+
parse(raw);
|
|
388
|
+
trySync(() => Schema.parse(input), toErr); // runs now, returns Result
|
|
389
|
+
trySync(() => Schema.parse(input)); // Result<T, Error> — onThrow optional
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Mechanical migration: rename `trySync` → `fromThrowable` everywhere, then drop
|
|
393
|
+
the trailing `()` on any call site that immediately invoked the wrapper.
|
|
394
|
+
|
|
240
395
|
## Migration from neverthrow
|
|
241
396
|
|
|
242
397
|
See [@onrails/codemod](../codemod/README.md) for the automated codemod, and the **Compat surface** notes below.
|
|
@@ -250,7 +405,7 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
|
|
|
250
405
|
- `Result` / `ResultAsync` are class-shaped (`CompatResult` / `CompatResultAsync`).
|
|
251
406
|
- `await ra` resolves to a `CompatResult<T, E>` (thenable), so `.isOk()`, `.value`, `.error`, `.match()`, `.unwrapOr()` all work without an extra `.resolve()` call.
|
|
252
407
|
- `andThen` / `chain` / `flatMap` / `orElse` accept any of `CompatResultAsync` / `ResultAsync` / `CompatResult` / tagged `Result` returns and union the error type.
|
|
253
|
-
- Supported: `andThen`, `asyncAndThen`, `chain`, `flatMap`, `
|
|
408
|
+
- Supported: `andThen`, `asyncAndThen`, `chain`, `flatMap`, `map`, `mapErr`, `orElse`, `match`, `unwrapOr`, `isOk`, `isErr`, `andTee`, `orTee`, `Result.combine`, `Result.fromThrowable`, `ResultAsync.combine`, `ResultAsync.fromPromise`, `ResultAsync.fromSafePromise`, `ResultAsync.fromThrowable`, `_unsafeUnwrap` / `_unsafeUnwrapErr`.
|
|
254
409
|
- Treat the compat surface as a migration step, not the destination — once a package migrates, switch its imports to `@onrails/result` and `@onrails/result/fluent`.
|
|
255
410
|
|
|
256
411
|
## Subpaths
|
|
@@ -258,9 +413,10 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
|
|
|
258
413
|
| Path | Contents |
|
|
259
414
|
|------|----------|
|
|
260
415
|
| `@onrails/result` | Core + interop exports |
|
|
416
|
+
| `@onrails/result/async` | Data-last twins of the `ResultAsync` methods (for `pipe` / `flow`) |
|
|
261
417
|
| `@onrails/result/fluent` | `fluent()` |
|
|
262
418
|
| `@onrails/result/extra` | Error-type utilities |
|
|
263
|
-
| `@onrails/result/pipe` | `flow` (
|
|
419
|
+
| `@onrails/result/pipe` | `pipe` (value-first) and `flow` (point-free), up to 12 steps |
|
|
264
420
|
| `@onrails/result/railway` | `Railway` named-context workflow builder |
|
|
265
421
|
| `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
|
|
266
422
|
| `@onrails/result/compat/neverthrow` | Migration shim |
|
package/RECIPES.md
CHANGED
|
@@ -54,7 +54,7 @@ const stepResult = tryGen(() => {
|
|
|
54
54
|
const user = $(authenticate(req));
|
|
55
55
|
const post = $(fetchPost(postId));
|
|
56
56
|
if (post.authorId !== user.id && !user.isAdmin) {
|
|
57
|
-
return err({ kind: "unauthorized"
|
|
57
|
+
return err({ kind: "unauthorized" });
|
|
58
58
|
}
|
|
59
59
|
return ok(post);
|
|
60
60
|
});
|
|
@@ -81,15 +81,17 @@ Define a parser factory once, instantiate per schema.
|
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
import { flow } from "@onrails/result/pipe";
|
|
84
|
-
import { flatMap,
|
|
84
|
+
import { flatMap, fromThrowable, type Result } from "@onrails/result";
|
|
85
85
|
|
|
86
86
|
type ParseError = { kind: "parse"; message: string };
|
|
87
87
|
type SchemaError = { kind: "schema"; field: string };
|
|
88
88
|
|
|
89
89
|
const parseJsonWith = <T>(schema: { parse: (x: unknown) => T }) =>
|
|
90
90
|
flow(
|
|
91
|
-
|
|
92
|
-
flatMap(
|
|
91
|
+
fromThrowable(JSON.parse, (e): ParseError => ({ kind: "parse", message: String(e) })),
|
|
92
|
+
flatMap(
|
|
93
|
+
fromThrowable(schema.parse, (e): SchemaError => ({ kind: "schema", field: String(e) })),
|
|
94
|
+
),
|
|
93
95
|
);
|
|
94
96
|
|
|
95
97
|
const parseUser = parseJsonWith(UserSchema);
|
|
@@ -115,7 +117,7 @@ const saveOrder = (rawJson: string) =>
|
|
|
115
117
|
pipe(
|
|
116
118
|
rawJson,
|
|
117
119
|
parseUser, // Result<User, ParseError | SchemaError>
|
|
118
|
-
flatMap((u) => (u.active ? ok(u) : err({ kind: "inactive"
|
|
120
|
+
flatMap((u) => (u.active ? ok(u) : err({ kind: "inactive" }))),
|
|
119
121
|
mapErr((e) => ({ kind: "input" as const, cause: e })),
|
|
120
122
|
tap((u) => log.info({ msg: "validated", userId: u.id })),
|
|
121
123
|
asyncAfter((u) =>
|
|
@@ -208,7 +210,7 @@ const buildSummary = (userId: string) =>
|
|
|
208
210
|
loadProfile(userId), // ResultAsync<Profile, ProfileError>
|
|
209
211
|
loadRecentMetrics(userId), // ResultAsync<Metrics, MetricsError>
|
|
210
212
|
loadFeatureFlags(userId), // ResultAsync<Flags, never>
|
|
211
|
-
]
|
|
213
|
+
]),
|
|
212
214
|
(combined) =>
|
|
213
215
|
combined.map(([profile, metrics, flags]) => ({
|
|
214
216
|
userId,
|
|
@@ -237,10 +239,10 @@ type CharsError = { kind: "chars"; bad: string };
|
|
|
237
239
|
type TooShortError = { kind: "too_short"; min: number };
|
|
238
240
|
|
|
239
241
|
const requireMin = (min: number) => (s: string) =>
|
|
240
|
-
s.length >= min ? ok(s) : err({ kind: "len"
|
|
242
|
+
s.length >= min ? ok(s) : err({ kind: "len", min });
|
|
241
243
|
|
|
242
244
|
const requireAscii = (s: string) =>
|
|
243
|
-
/^[\x20-\x7e]*$/.test(s) ? ok(s) : err({ kind: "chars"
|
|
245
|
+
/^[\x20-\x7e]*$/.test(s) ? ok(s) : err({ kind: "chars", bad: s });
|
|
244
246
|
|
|
245
247
|
const validateUsername = flow(
|
|
246
248
|
(raw: string) => ok(raw.trim()),
|
|
@@ -248,7 +250,7 @@ const validateUsername = flow(
|
|
|
248
250
|
flatMap(requireAscii),
|
|
249
251
|
recover(
|
|
250
252
|
(e: LengthError | CharsError): Result<string, TooShortError | CharsError> =>
|
|
251
|
-
e.kind === "len" ? err({ kind: "too_short"
|
|
253
|
+
e.kind === "len" ? err({ kind: "too_short", min: e.min }) : err(e),
|
|
252
254
|
),
|
|
253
255
|
);
|
|
254
256
|
// (raw: string) => Result<string, TooShortError | CharsError>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/internal/dual.ts
|
|
4
|
+
var dual = (arity, body) => {
|
|
5
|
+
const run = body;
|
|
6
|
+
const dispatcher = (...args) => args.length >= arity ? run(...args) : (self) => run(self, ...args);
|
|
7
|
+
return dispatcher;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/async-fns.ts
|
|
11
|
+
var map = dual(2, (ra, fn) => ra.map(fn));
|
|
12
|
+
var mapErr = dual(
|
|
13
|
+
2,
|
|
14
|
+
(ra, fn) => ra.mapErr(fn)
|
|
15
|
+
);
|
|
16
|
+
var bimap = dual(
|
|
17
|
+
3,
|
|
18
|
+
(ra, onOk, onErr) => ra.bimap(onOk, onErr)
|
|
19
|
+
);
|
|
20
|
+
var flatMap = dual(
|
|
21
|
+
2,
|
|
22
|
+
(ra, fn) => ra.flatMap(fn)
|
|
23
|
+
);
|
|
24
|
+
var recover = dual(
|
|
25
|
+
2,
|
|
26
|
+
(ra, fn) => ra.recover(fn)
|
|
27
|
+
);
|
|
28
|
+
var tap = dual(2, (ra, fn) => ra.tap(fn));
|
|
29
|
+
var tapErr = dual(
|
|
30
|
+
2,
|
|
31
|
+
(ra, fn) => ra.tapErr(fn)
|
|
32
|
+
);
|
|
33
|
+
var match = dual(
|
|
34
|
+
3,
|
|
35
|
+
(ra, onOk, onErr) => ra.match(onOk, onErr)
|
|
36
|
+
);
|
|
37
|
+
var unwrapOr = dual(
|
|
38
|
+
2,
|
|
39
|
+
(ra, defaultValue) => ra.unwrapOr(defaultValue)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
exports.bimap = bimap;
|
|
43
|
+
exports.flatMap = flatMap;
|
|
44
|
+
exports.map = map;
|
|
45
|
+
exports.mapErr = mapErr;
|
|
46
|
+
exports.match = match;
|
|
47
|
+
exports.recover = recover;
|
|
48
|
+
exports.tap = tap;
|
|
49
|
+
exports.tapErr = tapErr;
|
|
50
|
+
exports.unwrapOr = unwrapOr;
|
|
51
|
+
//# sourceMappingURL=async-fns.cjs.map
|
|
52
|
+
//# sourceMappingURL=async-fns.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/dual.ts","../src/async-fns.ts"],"names":[],"mappings":";;;AA0CO,IAAM,IAAA,GAAO,CAAI,KAAA,EAAe,IAAA,KAA2C;AAMhF,EAAA,MAAM,GAAA,GAAM,IAAA;AACZ,EAAA,MAAM,UAAA,GAAa,CAAA,GAAI,IAAA,KACrB,IAAA,CAAK,UAAU,KAAA,GAAQ,GAAA,CAAI,GAAG,IAAI,IAAI,CAAC,IAAA,KAAkB,GAAA,CAAI,IAAA,EAAM,GAAG,IAAI,CAAA;AAG5E,EAAA,OAAO,UAAA;AACT,CAAA;;;ACbO,IAAM,GAAA,GAGT,KAAK,CAAA,EAAG,CAAU,IAAuB,EAAA,KAA2C,EAAA,CAAG,GAAA,CAAI,EAAE,CAAC;AAW3F,IAAM,MAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAU,EAAA,EAAuB,EAAA,KAA2C,EAAA,CAAG,OAAO,EAAE;AAC1F;AAUO,IAAM,KAAA,GAUT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CACE,EAAA,EACA,IAAA,EACA,UACsB,EAAA,CAAG,KAAA,CAAM,MAAM,KAAK;AAC9C;AAWO,IAAM,OAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAa,EAAA,EAAuB,EAAA,KAClC,EAAA,CAAG,QAAQ,EAAE;AACjB;AAWO,IAAM,OAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAU,EAAA,EAAuB,EAAA,KAC/B,EAAA,CAAG,QAAQ,EAAE;AACjB;AAUO,IAAM,GAAA,GAGT,KAAK,CAAA,EAAG,CAAO,IAAuB,EAAA,KAA8C,EAAA,CAAG,GAAA,CAAI,EAAE,CAAC;AAU3F,IAAM,MAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAO,EAAA,EAAuB,EAAA,KAA8C,EAAA,CAAG,OAAO,EAAE;AAC1F;AAWO,IAAM,KAAA,GAUT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CACE,EAAA,EACA,IAAA,EACA,UACqB,EAAA,CAAG,KAAA,CAAM,MAAM,KAAK;AAC7C;AAWO,IAAM,QAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAU,EAAA,EAAuB,YAAA,KAAoC,EAAA,CAAG,SAAS,YAAY;AAC/F","file":"async-fns.cjs","sourcesContent":["/**\n * @onrails/result internal — owning module for the **dual-form idiom**.\n *\n * Every dual-form transform in this package (`map`, `flatMap`, `match`, …)\n * accepts either shape:\n *\n * data-first: `map(result, fn)` — full arity, runs immediately\n * curried: `map(fn)(result)` — arity minus the leading `self`, returns\n * a unary function awaiting the data\n *\n * {@link dual} is the single arity dispatcher those transforms derive from;\n * hand-rolled `args.length` checks are not allowed elsewhere.\n *\n * NOTE: a twin copy of this module lives at `packages/maybe/src/internal/dual.ts`\n * — keep the two in sync when editing.\n *\n * Internal only: not listed in package.json `exports` or tsup entries.\n */\n\n/**\n * Derive a dual-form (data-first / curried) function from a single\n * data-first `body`.\n *\n * When called with `args.length >= arity` the wrapper runs `body(...args)`\n * (data-first). With fewer arguments it returns `(self) => body(self, ...args)`\n * — the curried, data-last-`self` form used with `pipe`/`flow`.\n *\n * The public type is supplied entirely by the caller: annotate the exported\n * const with the exact overload set and let `F` be inferred from it.\n *\n * @example\n * ```ts\n * export const map: {\n * <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E>;\n * <T, U>(fn: (value: T) => U): <E>(result: Result<T, E>) => Result<U, E>;\n * } = dual(\n * 2,\n * <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>\n * isOk(result) ? ok(fn(result.value)) : err(result.error),\n * );\n * ```\n */\nexport const dual = <F>(arity: number, body: (...args: never[]) => unknown): F => {\n // `(...args: never[]) => unknown` is the top function type, so every typed\n // body is assignable to the parameter — but it cannot be invoked with\n // `unknown` arguments. Re-view it through the variadic shape the dispatcher\n // actually forwards; the constraint is that `body` only ever receives the\n // arguments the caller's overload annotation admitted.\n const run = body as (...args: readonly unknown[]) => unknown;\n const dispatcher = (...args: readonly unknown[]): unknown =>\n args.length >= arity ? run(...args) : (self: unknown) => run(self, ...args);\n // The caller's overload annotation on the exported const supplies the real\n // public type; internally the dispatcher stays `unknown`-typed.\n return dispatcher as F;\n};\n","/**\n * `@onrails/result/async` — data-last twins of the {@link ResultAsync} methods,\n * so an async railway composes with `pipe` / `flow` the same way a sync one does.\n *\n * `ResultAsync` is a class, so dot-chaining is always available and remains the\n * shortest form for a one-off chain. Reach for this module when you want the\n * sync side's other two syntaxes on the async carrier:\n *\n * ```ts\n * import * as RA from \"@onrails/result/async\";\n * import { pipe } from \"@onrails/result\";\n * import { flow } from \"@onrails/result/pipe\";\n *\n * // value-first pipe\n * pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));\n *\n * // reusable point-free pipeline — no value yet\n * const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));\n * ```\n *\n * Every export mirrors one `ResultAsync` method exactly, in both forms:\n * data-first (`RA.map(ra, fn)`) and curried (`RA.map(fn)`). Import as a\n * namespace — the names deliberately match the sync core, and `test/parity.spec.ts`\n * enforces that the mirror stays complete.\n */\n\nimport type { ResultAsync } from \"./async.js\";\nimport { dual } from \"./internal/dual.js\";\nimport type { Result } from \"./types.js\";\n\n/** A step may hand back either carrier; the async one absorbs a sync `Result`. */\ntype Step<U, F> = ResultAsync<U, F> | Result<U, F>;\n\n/**\n * Transforms the `Ok` value, passing `Err` through unchanged.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), map((u) => u.name)); // ResultAsync<string, LoadError>\n * ```\n */\nexport const map: {\n <T, U, E>(ra: ResultAsync<T, E>, fn: (value: T) => U): ResultAsync<U, E>;\n <T, U>(fn: (value: T) => U): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E>;\n} = dual(2, <T, U, E>(ra: ResultAsync<T, E>, fn: (value: T) => U): ResultAsync<U, E> => ra.map(fn));\n\n/**\n * Transforms the `Err` value, passing `Ok` through unchanged — useful for\n * unifying heterogeneous failures into one app-level union.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), mapErr((e): AppError => ({ kind: \"load\", cause: e })));\n * ```\n */\nexport const mapErr: {\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => F): ResultAsync<T, F>;\n <E, F>(fn: (error: E) => F): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, F>;\n} = dual(\n 2,\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => F): ResultAsync<T, F> => ra.mapErr(fn),\n);\n\n/**\n * Transforms both tracks in one step. Exactly one of `onOk` / `onErr` runs.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), bimap(toDto, (e): AppError => ({ kind: \"load\", cause: e })));\n * ```\n */\nexport const bimap: {\n <T, U, E, F>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U,\n onErr: (error: E) => F,\n ): ResultAsync<U, F>;\n <T, U, E, F>(\n onOk: (value: T) => U,\n onErr: (error: E) => F,\n ): (ra: ResultAsync<T, E>) => ResultAsync<U, F>;\n} = dual(\n 3,\n <T, U, E, F>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U,\n onErr: (error: E) => F,\n ): ResultAsync<U, F> => ra.bimap(onOk, onErr),\n);\n\n/**\n * Canonical bind — chains a step returning a `ResultAsync` or a sync `Result`,\n * short-circuiting on `Err`. Error types accumulate (`E | F`).\n *\n * @example\n * ```ts\n * pipe(authenticate(req), flatMap((user) => loadProfile(user.id)));\n * ```\n */\nexport const flatMap: {\n <T, U, E, F>(ra: ResultAsync<T, E>, fn: (value: T) => Step<U, F>): ResultAsync<U, E | F>;\n <T, U, F>(fn: (value: T) => Step<U, F>): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E | F>;\n} = dual(\n 2,\n <T, U, E, F>(ra: ResultAsync<T, E>, fn: (value: T) => Step<U, F>): ResultAsync<U, E | F> =>\n ra.flatMap(fn),\n);\n\n/**\n * Error-track bind — runs `fn` only on `Err`, letting a failed workflow\n * recover to `Ok` or remap the failure.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), recover((e) => (e === \"notFound\" ? okAsync(guest) : errAsync(e))));\n * ```\n */\nexport const recover: {\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => Step<T, F>): ResultAsync<T, F>;\n <T, E, F>(fn: (error: E) => Step<T, F>): (ra: ResultAsync<T, E>) => ResultAsync<T, F>;\n} = dual(\n 2,\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => Step<T, F>): ResultAsync<T, F> =>\n ra.recover(fn),\n);\n\n/**\n * Observes the `Ok` value for side effects without changing it.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), tap((u) => log.info({ msg: \"loaded\", id: u.id })));\n * ```\n */\nexport const tap: {\n <T, E>(ra: ResultAsync<T, E>, fn: (value: T) => void): ResultAsync<T, E>;\n <T>(fn: (value: T) => void): <E>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;\n} = dual(2, <T, E>(ra: ResultAsync<T, E>, fn: (value: T) => void): ResultAsync<T, E> => ra.tap(fn));\n\n/**\n * Observes the `Err` value for side effects without changing it.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), tapErr((e) => metrics.inc(\"user.load.fail\", { kind: e.kind })));\n * ```\n */\nexport const tapErr: {\n <T, E>(ra: ResultAsync<T, E>, fn: (error: E) => void): ResultAsync<T, E>;\n <E>(fn: (error: E) => void): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;\n} = dual(\n 2,\n <T, E>(ra: ResultAsync<T, E>, fn: (error: E) => void): ResultAsync<T, E> => ra.tapErr(fn),\n);\n\n/**\n * Terminal collapse — folds both tracks into a single awaited value, settling\n * the carrier exactly once. Ends the pipeline, so it returns a plain `Promise`.\n *\n * @example\n * ```ts\n * const status = await pipe(save(row), match(() => 200, (e) => (e.kind === \"conflict\" ? 409 : 500)));\n * ```\n */\nexport const match: {\n <T, E, U1, U2 = U1>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U1,\n onErr: (error: E) => U2,\n ): Promise<U1 | U2>;\n <T, E, U1, U2 = U1>(\n onOk: (value: T) => U1,\n onErr: (error: E) => U2,\n ): (ra: ResultAsync<T, E>) => Promise<U1 | U2>;\n} = dual(\n 3,\n <T, E, U1, U2>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U1,\n onErr: (error: E) => U2,\n ): Promise<U1 | U2> => ra.match(onOk, onErr),\n);\n\n/**\n * Terminal — resolves to the `Ok` value, or `defaultValue` on `Err`. The\n * fallback may differ from `T`; the result widens to `T | U`.\n *\n * @example\n * ```ts\n * const profile = await pipe(loadProfile(id), unwrapOr(guestProfile));\n * ```\n */\nexport const unwrapOr: {\n <T, E, U>(ra: ResultAsync<T, E>, defaultValue: U): Promise<T | U>;\n <U>(defaultValue: U): <T, E>(ra: ResultAsync<T, E>) => Promise<T | U>;\n} = dual(\n 2,\n <T, E, U>(ra: ResultAsync<T, E>, defaultValue: U): Promise<T | U> => ra.unwrapOr(defaultValue),\n);\n"]}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@onrails/result/async` — data-last twins of the {@link ResultAsync} methods,
|
|
3
|
+
* so an async railway composes with `pipe` / `flow` the same way a sync one does.
|
|
4
|
+
*
|
|
5
|
+
* `ResultAsync` is a class, so dot-chaining is always available and remains the
|
|
6
|
+
* shortest form for a one-off chain. Reach for this module when you want the
|
|
7
|
+
* sync side's other two syntaxes on the async carrier:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import * as RA from "@onrails/result/async";
|
|
11
|
+
* import { pipe } from "@onrails/result";
|
|
12
|
+
* import { flow } from "@onrails/result/pipe";
|
|
13
|
+
*
|
|
14
|
+
* // value-first pipe
|
|
15
|
+
* pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
16
|
+
*
|
|
17
|
+
* // reusable point-free pipeline — no value yet
|
|
18
|
+
* const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Every export mirrors one `ResultAsync` method exactly, in both forms:
|
|
22
|
+
* data-first (`RA.map(ra, fn)`) and curried (`RA.map(fn)`). Import as a
|
|
23
|
+
* namespace — the names deliberately match the sync core, and `test/parity.spec.ts`
|
|
24
|
+
* enforces that the mirror stays complete.
|
|
25
|
+
*/
|
|
26
|
+
import type { ResultAsync } from "./async.js";
|
|
27
|
+
import type { Result } from "./types.js";
|
|
28
|
+
/** A step may hand back either carrier; the async one absorbs a sync `Result`. */
|
|
29
|
+
type Step<U, F> = ResultAsync<U, F> | Result<U, F>;
|
|
30
|
+
/**
|
|
31
|
+
* Transforms the `Ok` value, passing `Err` through unchanged.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* pipe(loadUser(id), map((u) => u.name)); // ResultAsync<string, LoadError>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const map: {
|
|
39
|
+
<T, U, E>(ra: ResultAsync<T, E>, fn: (value: T) => U): ResultAsync<U, E>;
|
|
40
|
+
<T, U>(fn: (value: T) => U): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Transforms the `Err` value, passing `Ok` through unchanged — useful for
|
|
44
|
+
* unifying heterogeneous failures into one app-level union.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* pipe(loadUser(id), mapErr((e): AppError => ({ kind: "load", cause: e })));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const mapErr: {
|
|
52
|
+
<T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => F): ResultAsync<T, F>;
|
|
53
|
+
<E, F>(fn: (error: E) => F): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, F>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Transforms both tracks in one step. Exactly one of `onOk` / `onErr` runs.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* pipe(loadUser(id), bimap(toDto, (e): AppError => ({ kind: "load", cause: e })));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const bimap: {
|
|
64
|
+
<T, U, E, F>(ra: ResultAsync<T, E>, onOk: (value: T) => U, onErr: (error: E) => F): ResultAsync<U, F>;
|
|
65
|
+
<T, U, E, F>(onOk: (value: T) => U, onErr: (error: E) => F): (ra: ResultAsync<T, E>) => ResultAsync<U, F>;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Canonical bind — chains a step returning a `ResultAsync` or a sync `Result`,
|
|
69
|
+
* short-circuiting on `Err`. Error types accumulate (`E | F`).
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* pipe(authenticate(req), flatMap((user) => loadProfile(user.id)));
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const flatMap: {
|
|
77
|
+
<T, U, E, F>(ra: ResultAsync<T, E>, fn: (value: T) => Step<U, F>): ResultAsync<U, E | F>;
|
|
78
|
+
<T, U, F>(fn: (value: T) => Step<U, F>): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E | F>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Error-track bind — runs `fn` only on `Err`, letting a failed workflow
|
|
82
|
+
* recover to `Ok` or remap the failure.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* pipe(loadUser(id), recover((e) => (e === "notFound" ? okAsync(guest) : errAsync(e))));
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare const recover: {
|
|
90
|
+
<T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => Step<T, F>): ResultAsync<T, F>;
|
|
91
|
+
<T, E, F>(fn: (error: E) => Step<T, F>): (ra: ResultAsync<T, E>) => ResultAsync<T, F>;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Observes the `Ok` value for side effects without changing it.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* pipe(loadUser(id), tap((u) => log.info({ msg: "loaded", id: u.id })));
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare const tap: {
|
|
102
|
+
<T, E>(ra: ResultAsync<T, E>, fn: (value: T) => void): ResultAsync<T, E>;
|
|
103
|
+
<T>(fn: (value: T) => void): <E>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Observes the `Err` value for side effects without changing it.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* pipe(loadUser(id), tapErr((e) => metrics.inc("user.load.fail", { kind: e.kind })));
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare const tapErr: {
|
|
114
|
+
<T, E>(ra: ResultAsync<T, E>, fn: (error: E) => void): ResultAsync<T, E>;
|
|
115
|
+
<E>(fn: (error: E) => void): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Terminal collapse — folds both tracks into a single awaited value, settling
|
|
119
|
+
* the carrier exactly once. Ends the pipeline, so it returns a plain `Promise`.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const status = await pipe(save(row), match(() => 200, (e) => (e.kind === "conflict" ? 409 : 500)));
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare const match: {
|
|
127
|
+
<T, E, U1, U2 = U1>(ra: ResultAsync<T, E>, onOk: (value: T) => U1, onErr: (error: E) => U2): Promise<U1 | U2>;
|
|
128
|
+
<T, E, U1, U2 = U1>(onOk: (value: T) => U1, onErr: (error: E) => U2): (ra: ResultAsync<T, E>) => Promise<U1 | U2>;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Terminal — resolves to the `Ok` value, or `defaultValue` on `Err`. The
|
|
132
|
+
* fallback may differ from `T`; the result widens to `T | U`.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const profile = await pipe(loadProfile(id), unwrapOr(guestProfile));
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export declare const unwrapOr: {
|
|
140
|
+
<T, E, U>(ra: ResultAsync<T, E>, defaultValue: U): Promise<T | U>;
|
|
141
|
+
<U>(defaultValue: U): <T, E>(ra: ResultAsync<T, E>) => Promise<T | U>;
|
|
142
|
+
};
|
|
143
|
+
export {};
|