@onrails/result 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/DESIGN.md +1 -1
  2. package/README.md +165 -12
  3. package/RECIPES.md +6 -4
  4. package/dist/async-fns.cjs +52 -0
  5. package/dist/async-fns.cjs.map +1 -0
  6. package/dist/async-fns.d.cts +143 -0
  7. package/dist/async-fns.d.ts +143 -0
  8. package/dist/async-fns.js +42 -0
  9. package/dist/async-fns.js.map +1 -0
  10. package/dist/async-lift.d.cts +109 -0
  11. package/dist/async-lift.d.ts +109 -0
  12. package/dist/{async-Ci6WSL5-.d.ts → async.d.cts} +42 -58
  13. package/dist/{async-DcXR7k9K.d.cts → async.d.ts} +42 -58
  14. package/dist/collections.d.cts +105 -0
  15. package/dist/collections.d.ts +105 -0
  16. package/dist/compat/neverthrow.cjs +57 -45
  17. package/dist/compat/neverthrow.cjs.map +1 -1
  18. package/dist/compat/neverthrow.d.cts +21 -24
  19. package/dist/compat/neverthrow.d.ts +21 -24
  20. package/dist/compat/neverthrow.js +56 -44
  21. package/dist/compat/neverthrow.js.map +1 -1
  22. package/dist/extra.cjs +3 -1
  23. package/dist/extra.cjs.map +1 -1
  24. package/dist/extra.d.cts +11 -14
  25. package/dist/extra.d.ts +11 -14
  26. package/dist/extra.js +3 -1
  27. package/dist/extra.js.map +1 -1
  28. package/dist/fluent.cjs +15 -13
  29. package/dist/fluent.cjs.map +1 -1
  30. package/dist/fluent.d.cts +4 -7
  31. package/dist/fluent.d.ts +4 -7
  32. package/dist/fluent.js +15 -13
  33. package/dist/fluent.js.map +1 -1
  34. package/dist/index.cjs +77 -52
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +28 -490
  37. package/dist/index.d.ts +28 -490
  38. package/dist/index.js +77 -53
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal/ctor.d.cts +12 -0
  41. package/dist/internal/ctor.d.ts +12 -0
  42. package/dist/internal/dual.d.cts +42 -0
  43. package/dist/internal/dual.d.ts +42 -0
  44. package/dist/internal/infer.d.cts +40 -0
  45. package/dist/internal/infer.d.ts +40 -0
  46. package/dist/internal/lock.d.cts +16 -0
  47. package/dist/internal/lock.d.ts +16 -0
  48. package/dist/internal/to-error.d.cts +2 -0
  49. package/dist/internal/to-error.d.ts +2 -0
  50. package/dist/pipe.cjs +8 -0
  51. package/dist/pipe.cjs.map +1 -1
  52. package/dist/pipe.d.cts +58 -11
  53. package/dist/pipe.d.ts +58 -11
  54. package/dist/pipe.js +8 -1
  55. package/dist/pipe.js.map +1 -1
  56. package/dist/railway.cjs +87 -63
  57. package/dist/railway.cjs.map +1 -1
  58. package/dist/railway.d.cts +49 -19
  59. package/dist/railway.d.ts +49 -19
  60. package/dist/railway.js +87 -63
  61. package/dist/railway.js.map +1 -1
  62. package/dist/result.d.cts +315 -0
  63. package/dist/result.d.ts +315 -0
  64. package/dist/try-gen.cjs +3 -3
  65. package/dist/try-gen.cjs.map +1 -1
  66. package/dist/try-gen.d.cts +4 -7
  67. package/dist/try-gen.d.ts +4 -7
  68. package/dist/try-gen.js +3 -3
  69. package/dist/try-gen.js.map +1 -1
  70. package/dist/{types-BQ9vv0nD.d.cts → types.d.cts} +6 -8
  71. package/dist/{types-BQ9vv0nD.d.ts → types.d.ts} +6 -8
  72. package/package.json +69 -24
  73. package/skills/railway-do-notation/SKILL.md +9 -9
package/DESIGN.md CHANGED
@@ -40,7 +40,7 @@ Two distinct lift paths:
40
40
 
41
41
  ```ts
42
42
  asyncAfter(
43
- trySync(() => Schema.parse(input), toError)(),
43
+ trySync(() => Schema.parse(input), toError),
44
44
  (value) => tryAsync(save(value)),
45
45
  );
46
46
  ```
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
- const parse = trySync(
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
- ## When to use what
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,36 @@ 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 (`ok([1, 2])` satisfies `Result<number[], E>`).
143
+
144
+ Pass **both** type arguments to opt out. Generic code that has to produce `Result<T, E>` for an unresolved `T` needs that form:
145
+
146
+ ```ts
147
+ const lift = <T, E>(value: T): Result<T, E> => ok<T, E>(value);
148
+ ```
149
+
150
+ ### Naming the error channel
151
+
152
+ One explicit type argument on `err` names the **error**:
153
+
154
+ ```ts
155
+ type AppError = { kind: "parse" } | { kind: "io" };
156
+
157
+ err<AppError>({ kind: "io" }); // Result<never, AppError>
158
+ err<number, AppError>({ kind: "io" }); // Result<number, AppError> — neverthrow's T, E order
159
+ ```
160
+
161
+ The two-argument form keeps neverthrow's order. `errAsync` / `ResultAsync.err` behave the same way.
162
+
97
163
  ## Async interop — `fromAsync`
98
164
 
99
165
  Lift `async` handlers that return `Result` without leaking `Promise<Result<…>>`:
@@ -132,6 +198,20 @@ import { match } from "ts-pattern";
132
198
  R.match(result, onOk, onErr);
133
199
  ```
134
200
 
201
+ 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:
202
+
203
+ ```ts
204
+ import { matchTag } from "@onrails/pattern";
205
+
206
+ const label = (r: Result<number, string>) =>
207
+ matchTag(r, {
208
+ Ok: (v) => `ok:${v.value}`, // v: Ok<number> — the member, not the payload
209
+ Err: (e) => `err:${e.error}`, // e: Err<string>
210
+ });
211
+ ```
212
+
213
+ Note the difference: `match` hands each branch the *payload*, `matchTag` hands it the narrowed *member*.
214
+
135
215
  `unwrapOk` and `unwrapErr` are test/assertion helpers. Prefer `match`, `isOk`, or `isErr` in production control flow.
136
216
 
137
217
  ```ts
@@ -163,7 +243,7 @@ import { ResultAsync } from "@onrails/result";
163
243
  const combined = ResultAsync.combineTuple([
164
244
  loadSettings(),
165
245
  loadModelCatalog(),
166
- ] as const);
246
+ ]);
167
247
 
168
248
  const dto = combined.map(([settings, catalog]) =>
169
249
  buildDto(settings, catalog),
@@ -172,6 +252,25 @@ const dto = combined.map(([settings, catalog]) =>
172
252
 
173
253
  When TS only infers the first error in a generator-style flow, use `declareErrors<E1 | E2>()` from `/extra`.
174
254
 
255
+ ## Async pipelines — `@onrails/result/async`
256
+
257
+ `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:
258
+
259
+ ```ts
260
+ import * as RA from "@onrails/result/async";
261
+ import { pipe } from "@onrails/result";
262
+ import { flow } from "@onrails/result/pipe";
263
+
264
+ // value-first
265
+ pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));
266
+ // ResultAsync<number, NotFound | DbError>
267
+
268
+ // reusable point-free pipeline — no value yet
269
+ const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));
270
+ ```
271
+
272
+ 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.
273
+
175
274
  ## `Railway` — named service workflows
176
275
 
177
276
  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 +294,15 @@ const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), t
195
294
 
196
295
  Sync-only workflows return `Result<T, E>`. The first `fromPromise`, `fromAsync`, or `parallel` step upgrades the output to `ResultAsync<T, E>`.
197
296
 
198
- Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMapResult`) for one or two steps where a builder would add ceremony.
297
+ 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`:
298
+
299
+ ```ts
300
+ Railway.empty()
301
+ .derive("x", () => 1)
302
+ .derive("x", () => "two"); // ✗ key "x" already exists in the workflow context
303
+ ```
304
+
305
+ Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMap`) for one or two steps where a builder would add ceremony.
199
306
 
200
307
  To share steps across workflows, extract plain functions of the context and plug them in via `.fromResult` / `.fromAsync`:
201
308
 
@@ -211,6 +318,8 @@ const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), t
211
318
 
212
319
  ## Pipe
213
320
 
321
+ `pipe` and `flow` both live in `@onrails/result/pipe`; `pipe` is also re-exported from the package root.
322
+
214
323
  ```ts
215
324
  import { pipe } from "@onrails/result";
216
325
  import { flow } from "@onrails/result/pipe";
@@ -233,10 +342,53 @@ const parseUserName = flow(
233
342
  parseUserName(raw);
234
343
  ```
235
344
 
345
+ 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`.
346
+
236
347
  ## ESLint
237
348
 
238
349
  `@onrails/eslint-plugin` — warns on `Promise<Result<…>>` and `_unsafeUnwrap*`.
239
350
 
351
+ ## Breaking changes
352
+
353
+ ### `ok` / `err` lock inline literals
354
+
355
+ Inline literals no longer widen: `ok("a")` is `Result<"a", never>` and `err({ kind: "io" })` carries `{ readonly kind: "io" }`. Annotated values are unaffected. Two call shapes need a change:
356
+
357
+ ```ts
358
+ // generic code — pass both type arguments to opt out of locking
359
+ const lift = <T, E>(value: T): Result<T, E> => ok<T, E>(value);
360
+
361
+ // a type-level assertion that expected the widened type
362
+ expectType<TypeEqual<typeof ok(1), Result<number, never>>>(true); // before
363
+ expectType<TypeEqual<typeof value, Result<1, never>>>(true); // after
364
+ ```
365
+
366
+ ### One type argument on `err` now names the error
367
+
368
+ `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.
369
+
370
+ ### `trySync` is now eager; the lazy form is `fromThrowable`
371
+
372
+ `trySync` used to lift a throwing *function* and return a wrapper, while
373
+ `tryAsync` took a *value* and ran immediately — same prefix, opposite contract.
374
+ The two now agree, and the lifting form takes neverthrow's own name.
375
+
376
+ ```ts
377
+ // before
378
+ const parse = trySync(JSON.parse, toErr);
379
+ parse(raw);
380
+ trySync(() => Schema.parse(input), toErr)(); // note the trailing ()
381
+
382
+ // after
383
+ const parse = fromThrowable(JSON.parse, toErr);
384
+ parse(raw);
385
+ trySync(() => Schema.parse(input), toErr); // runs now, returns Result
386
+ trySync(() => Schema.parse(input)); // Result<T, Error> — onThrow optional
387
+ ```
388
+
389
+ Mechanical migration: rename `trySync` → `fromThrowable` everywhere, then drop
390
+ the trailing `()` on any call site that immediately invoked the wrapper.
391
+
240
392
  ## Migration from neverthrow
241
393
 
242
394
  See [@onrails/codemod](../codemod/README.md) for the automated codemod, and the **Compat surface** notes below.
@@ -250,7 +402,7 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
250
402
  - `Result` / `ResultAsync` are class-shaped (`CompatResult` / `CompatResultAsync`).
251
403
  - `await ra` resolves to a `CompatResult<T, E>` (thenable), so `.isOk()`, `.value`, `.error`, `.match()`, `.unwrapOr()` all work without an extra `.resolve()` call.
252
404
  - `andThen` / `chain` / `flatMap` / `orElse` accept any of `CompatResultAsync` / `ResultAsync` / `CompatResult` / tagged `Result` returns and union the error type.
253
- - Supported: `andThen`, `asyncAndThen`, `chain`, `flatMap`, `flatMapResult`, `andThenResult`, `map`, `mapErr`, `orElse`, `match`, `unwrapOr`, `isOk`, `isErr`, `andTee`, `orTee`, `Result.combine`, `Result.fromThrowable`, `ResultAsync.combine`, `ResultAsync.fromPromise`, `ResultAsync.fromSafePromise`, `ResultAsync.fromThrowable`, `_unsafeUnwrap` / `_unsafeUnwrapErr`.
405
+ - 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
406
  - 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
407
 
256
408
  ## Subpaths
@@ -258,9 +410,10 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
258
410
  | Path | Contents |
259
411
  |------|----------|
260
412
  | `@onrails/result` | Core + interop exports |
413
+ | `@onrails/result/async` | Data-last twins of the `ResultAsync` methods (for `pipe` / `flow`) |
261
414
  | `@onrails/result/fluent` | `fluent()` |
262
415
  | `@onrails/result/extra` | Error-type utilities |
263
- | `@onrails/result/pipe` | `flow` (variadic point-free composition) |
416
+ | `@onrails/result/pipe` | `pipe` (value-first) and `flow` (point-free), up to 12 steps |
264
417
  | `@onrails/result/railway` | `Railway` named-context workflow builder |
265
418
  | `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
266
419
  | `@onrails/result/compat/neverthrow` | Migration shim |
package/RECIPES.md CHANGED
@@ -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, trySync, type Result } from "@onrails/result";
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
- trySync(JSON.parse, (e): ParseError => ({ kind: "parse", message: String(e) })),
92
- flatMap(trySync(schema.parse, (e): SchemaError => ({ kind: "schema", field: String(e) }))),
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);
@@ -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
- ] as const),
213
+ ]),
212
214
  (combined) =>
213
215
  combined.map(([profile, metrics, flags]) => ({
214
216
  userId,
@@ -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 {};