@onrails/result 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/DESIGN.md +10 -18
  2. package/README.md +14 -59
  3. package/RECIPES.md +12 -25
  4. package/dist/{async-u8U5P2EH.d.cts → async-BR3AX0ck.d.cts} +54 -18
  5. package/dist/{async-1fIhfzFu.d.ts → async-SZ7BEl0g.d.ts} +54 -18
  6. package/dist/compat/neverthrow.cjs +39 -51
  7. package/dist/compat/neverthrow.cjs.map +1 -1
  8. package/dist/compat/neverthrow.d.cts +3 -3
  9. package/dist/compat/neverthrow.d.ts +3 -3
  10. package/dist/compat/neverthrow.js +39 -51
  11. package/dist/compat/neverthrow.js.map +1 -1
  12. package/dist/extra.cjs +1 -1
  13. package/dist/extra.cjs.map +1 -1
  14. package/dist/extra.d.cts +5 -11
  15. package/dist/extra.d.ts +5 -11
  16. package/dist/extra.js +1 -1
  17. package/dist/extra.js.map +1 -1
  18. package/dist/fluent.cjs +55 -27
  19. package/dist/fluent.cjs.map +1 -1
  20. package/dist/fluent.d.cts +37 -6
  21. package/dist/fluent.d.ts +37 -6
  22. package/dist/fluent.js +55 -27
  23. package/dist/fluent.js.map +1 -1
  24. package/dist/index.cjs +80 -94
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +146 -44
  27. package/dist/index.d.ts +146 -44
  28. package/dist/index.js +77 -94
  29. package/dist/index.js.map +1 -1
  30. package/dist/railway.cjs +98 -172
  31. package/dist/railway.cjs.map +1 -1
  32. package/dist/railway.d.cts +25 -179
  33. package/dist/railway.d.ts +25 -179
  34. package/dist/railway.js +99 -162
  35. package/dist/railway.js.map +1 -1
  36. package/dist/try-gen.cjs +1 -1
  37. package/dist/try-gen.cjs.map +1 -1
  38. package/dist/try-gen.js +1 -1
  39. package/dist/try-gen.js.map +1 -1
  40. package/package.json +1 -16
  41. package/skills/railway-do-notation/SKILL.md +29 -68
  42. package/skills/result-composition/SKILL.md +2 -4
  43. package/dist/$.cjs +0 -40
  44. package/dist/$.cjs.map +0 -1
  45. package/dist/$.js +0 -36
  46. package/dist/$.js.map +0 -1
  47. package/dist/_.d.cts +0 -2
  48. package/dist/_.d.ts +0 -2
  49. package/dist/interop.cjs +0 -433
  50. package/dist/interop.cjs.map +0 -1
  51. package/dist/interop.d.cts +0 -75
  52. package/dist/interop.d.ts +0 -75
  53. package/dist/interop.js +0 -429
  54. package/dist/interop.js.map +0 -1
  55. package/dist/validation.cjs +0 -53
  56. package/dist/validation.cjs.map +0 -1
  57. package/dist/validation.d.cts +0 -72
  58. package/dist/validation.d.ts +0 -72
  59. package/dist/validation.js +0 -48
  60. package/dist/validation.js.map +0 -1
package/dist/railway.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as ResultAsync } from './async-1fIhfzFu.js';
1
+ import { R as ResultAsync } from './async-SZ7BEl0g.js';
2
2
  import { R as Result } from './types-BQ9vv0nD.js';
3
3
 
4
4
  /**
@@ -11,20 +11,10 @@ type RailwayMode = "sync" | "async";
11
11
  * `Result<T, E>`, async mode → `ResultAsync<T, E>`.
12
12
  */
13
13
  type RailwayOutput<T, E, M extends RailwayMode> = M extends "async" ? ResultAsync<T, E> : Result<T, E>;
14
- /**
15
- * Initial context shape for a `railway` functional pipeline.
16
- * Wraps the raw input under the `input` key so subsequent named steps
17
- * can reference it via `ctx.input`.
18
- */
19
- type RailwayInput<I> = {
20
- readonly input: I;
21
- };
22
14
  type BranchFn<C extends object> = (ctx: C) => ResultAsync<unknown, unknown>;
23
15
  type BranchRecord = Record<string, (ctx: never) => ResultAsync<unknown, unknown>>;
24
16
  type BranchOk<R> = R extends (ctx: never) => ResultAsync<infer T, infer _E> ? T : never;
25
17
  type BranchErr<R> = R extends (ctx: never) => ResultAsync<infer _T, infer E> ? E : never;
26
- type BranchInput<R> = R extends (ctx: infer C) => ResultAsync<unknown, unknown> ? C : never;
27
- type ParallelInput<R extends BranchRecord> = BranchInput<R[keyof R]> & object;
28
18
  type ParallelOutput<R extends BranchRecord> = {
29
19
  [K in keyof R]: BranchOk<R[K]>;
30
20
  };
@@ -33,27 +23,6 @@ type ParallelError<R extends BranchRecord> = BranchErr<R[keyof R]>;
33
23
  * Named-context workflow builder. Each step appends a typed field to the
34
24
  * accumulating context object; the workflow tracks sync/async mode so the
35
25
  * final output type ({@link RailwayOutput}) is correct.
36
- *
37
- * Use `Railway` when a service workflow has 4+ named steps, mixed sync and
38
- * async boundaries, or independent async branches that should run in
39
- * parallel. For 1–2 step flows, prefer `flatMap` / `asyncAfter` directly.
40
- *
41
- * @example
42
- * ```ts
43
- * const summary = Railway
44
- * .fromSync("id", () => IdSchema.parse(raw), toError)
45
- * .fromPromise("row", ({ id }) => db.profiles.findFirst({ where: eq(profiles.id, id) }), toError)
46
- * .require("profile", "row", ({ id }) => ({ kind: "not_found" as const, id }))
47
- * .derive("normalized", ({ profile }) => normalizeProfile(profile))
48
- * .parallel({
49
- * artifacts: ({ normalized }) => loadArtifacts(normalized.id),
50
- * metrics: ({ normalized }) => loadMetrics(normalized.id),
51
- * })
52
- * .select(({ normalized, artifacts, metrics }) =>
53
- * toProfileSummary({ profile: normalized, artifacts, metrics }),
54
- * );
55
- * // ResultAsync<ProfileSummary, ParseError | DbError | NotFound>
56
- * ```
57
26
  */
58
27
  declare class Railway<C extends object, E, M extends RailwayMode> {
59
28
  private readonly state;
@@ -62,10 +31,7 @@ declare class Railway<C extends object, E, M extends RailwayMode> {
62
31
  static empty(): Railway<Record<never, never>, never, "sync">;
63
32
  /** Start a sync workflow with the given context as the initial state. */
64
33
  static context<C extends object>(context: C): Railway<C, never, "sync">;
65
- /**
66
- * Start a sync workflow with a throwing function — `onThrow` maps any
67
- * exception to a typed error.
68
- */
34
+ /** Start a sync workflow with a throwing function. */
69
35
  static fromSync<K extends string, T, E>(key: K, fn: () => T, onThrow: (error: unknown) => E): Railway<Record<K, T>, E, "sync">;
70
36
  /** Start a sync workflow with a `Result`-returning function. */
71
37
  static fromResult<K extends string, T, E>(key: K, fn: () => Result<T, E>): Railway<Record<K, T>, E, "sync">;
@@ -74,159 +40,39 @@ declare class Railway<C extends object, E, M extends RailwayMode> {
74
40
  /** Start an async workflow with a `ResultAsync`-returning function. */
75
41
  static fromAsync<K extends string, T, E>(key: K, fn: () => ResultAsync<T, E>): Railway<Record<K, T>, E, "async">;
76
42
  /**
77
- * Pure sync derivation — `fn` must not throw. Use {@link fromSync} for
78
- * throwing transforms.
43
+ * Rebuild the workflow in its current mode after transforming the carried
44
+ * result. Single re-link point for the state/phantom invariant: the mode
45
+ * tag is preserved verbatim, so `M` still describes the new state — TS
46
+ * cannot narrow the phantom `M` from the runtime tag, hence the cast.
79
47
  */
80
- derive<K extends string, T>(key: K, fn: (ctx: C) => T): Railway<C & Record<K, T>, E, M>;
48
+ private step;
81
49
  /**
82
- * Throwing sync transform. Adds `{ [key]: T }` to context; converts any
83
- * exception to `Err<F>` via `onThrow`.
50
+ * Project the carried result into the mode-aware output type. Counterpart
51
+ * of {@link Railway.step} for terminal steps: the runtime branch matches
52
+ * the branch `RailwayOutput` picks for `M` (state/phantom invariant), but
53
+ * TS cannot resolve the conditional on an unresolved `M`, hence the cast.
84
54
  */
55
+ private out;
56
+ /** Lift the carried result into `ResultAsync` for async-upgrading steps. */
57
+ private toAsync;
58
+ /** Pure sync derivation. */
59
+ derive<K extends string, T>(key: K, fn: (ctx: C) => T): Railway<C & Record<K, T>, E, M>;
60
+ /** Throwing sync transform. */
85
61
  fromSync<K extends string, T, F>(key: K, fn: (ctx: C) => T, onThrow: (error: unknown) => F): Railway<C & Record<K, T>, E | F, M>;
86
- /**
87
- * Sync `Result`-returning step. Adds `{ [key]: T }` to context on `Ok`;
88
- * short-circuits the workflow on `Err`. Error union widens to `E | F`.
89
- */
62
+ /** Sync `Result`-returning step. */
90
63
  fromResult<K extends string, T, F>(key: K, fn: (ctx: C) => Result<T, F>): Railway<C & Record<K, T>, E | F, M>;
91
- /**
92
- * Promise-returning step — upgrades the workflow to async mode. Reject
93
- * reasons go through `onReject` to become typed `Err<F>`.
94
- */
64
+ /** Promise-returning step — upgrades the workflow to async mode. */
95
65
  fromPromise<K extends string, T, F>(key: K, fn: (ctx: C) => PromiseLike<T>, onReject: (error: unknown) => F): Railway<C & Record<K, T>, E | F, "async">;
96
- /**
97
- * `ResultAsync`-returning step — upgrades the workflow to async mode.
98
- * Already-typed error: no mapper needed.
99
- */
66
+ /** `ResultAsync`-returning step — upgrades the workflow to async mode. */
100
67
  fromAsync<K extends string, T, F>(key: K, fn: (ctx: C) => ResultAsync<T, F>): Railway<C & Record<K, T>, E | F, "async">;
101
- /**
102
- * Narrow a nullable context field into a required non-null field. If the
103
- * source field is `null` / `undefined`, the workflow short-circuits with
104
- * `onMissing(ctx)`.
105
- *
106
- * @example
107
- * ```ts
108
- * .fromPromise("row", ({ id }) => db.users.findFirst({ where: eq(users.id, id) }))
109
- * .require("user", "row", ({ id }) => ({ kind: "not_found" as const, id }))
110
- * // user is now User (non-null), not User | null
111
- * ```
112
- */
68
+ /** Narrow a nullable context field into a required non-null field. */
113
69
  require<K extends string, S extends keyof C, F>(key: K, source: S, onMissing: (ctx: C) => F): Railway<C & Record<K, NonNullable<C[S]>>, E | F, M>;
114
- /**
115
- * Run independent `ResultAsync` branches concurrently and merge their
116
- * named outputs back into context. Upgrades the workflow to async mode.
117
- * On multiple failures, the first `Err` in record-iteration order wins.
118
- *
119
- * @example
120
- * ```ts
121
- * .parallel({
122
- * recent: ({ userId }) => loadRecent(userId),
123
- * metrics: ({ userId }) => loadMetrics(userId),
124
- * })
125
- * // ctx now has { ..., recent, metrics }
126
- * ```
127
- */
70
+ /** Run independent `ResultAsync` branches concurrently and merge outputs. */
128
71
  parallel<R extends Record<string, BranchFn<C>>>(branches: R): Railway<C & ParallelOutput<R>, E | ParallelError<R>, "async">;
129
- /**
130
- * Project the final context into the workflow's output type. Hides the
131
- * internal context shape from callers.
132
- */
72
+ /** Project the final context into the workflow's output type. */
133
73
  select<T>(fn: (ctx: C) => T): RailwayOutput<T, E, M>;
134
- /**
135
- * Return the accumulated context as-is. Use when downstream code needs
136
- * every named field; prefer {@link select} when you can project to a DTO.
137
- */
74
+ /** Return the accumulated context as-is. */
138
75
  done(): RailwayOutput<C, E, M>;
139
76
  }
140
77
 
141
- type ParserLike<I, T> = {
142
- readonly parse: (input: I) => T;
143
- } | ((input: I) => T);
144
- type UnaryStep<I, O> = (input: I) => O;
145
- /**
146
- * Functional companion to {@link Railway} — point-free composition of
147
- * reusable workflow steps. Starts from `Railway.context({ input })` and
148
- * applies each step in order. Step factories live below: {@link parseWith},
149
- * {@link fromSyncNamed}, {@link fromResultNamed}, {@link fromPromiseNamed},
150
- * {@link fromAsyncNamed}, {@link deriveNamed}, {@link requireNamed},
151
- * {@link parallelNamed}, {@link select}.
152
- *
153
- * @example
154
- * ```ts
155
- * const summary = railway(
156
- * rawId,
157
- * parseWith(IdSchema, toError).as("id"),
158
- * fromPromiseNamed("row", ({ id }) => db.profiles.findFirst({ where: eq(profiles.id, id) }), toError),
159
- * requireNamed("profile", "row", ({ id }) => ({ kind: "not_found" as const, id })),
160
- * deriveNamed("normalized", ({ profile }) => normalizeProfile(profile)),
161
- * select(({ normalized }) => toProfileSummary(normalized)),
162
- * );
163
- * ```
164
- */
165
- declare function railway<I, A>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>): A;
166
- declare function railway<I, A, B>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>): B;
167
- declare function railway<I, A, B, C>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>, step3: UnaryStep<B, C>): C;
168
- declare function railway<I, A, B, C, D>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>, step3: UnaryStep<B, C>, step4: UnaryStep<C, D>): D;
169
- declare function railway<I, A, B, C, D, E>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>, step3: UnaryStep<B, C>, step4: UnaryStep<C, D>, step5: UnaryStep<D, E>): E;
170
- declare function railway<I, A, B, C, D, E, F>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>, step3: UnaryStep<B, C>, step4: UnaryStep<C, D>, step5: UnaryStep<D, E>, step6: UnaryStep<E, F>): F;
171
- declare function railway<I, A, B, C, D, E, F, G>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>, step3: UnaryStep<B, C>, step4: UnaryStep<C, D>, step5: UnaryStep<D, E>, step6: UnaryStep<E, F>, step7: UnaryStep<F, G>): G;
172
- declare function railway<I, A, B, C, D, E, F, G, H>(input: I, step1: UnaryStep<Railway<RailwayInput<I>, never, "sync">, A>, step2: UnaryStep<A, B>, step3: UnaryStep<B, C>, step4: UnaryStep<C, D>, step5: UnaryStep<D, E>, step6: UnaryStep<E, F>, step7: UnaryStep<F, G>, step8: UnaryStep<G, H>): H;
173
- /**
174
- * Step factory: parse the workflow input with a Zod-like schema (or a
175
- * unary parse function). Call `.as(key)` to name the output field.
176
- *
177
- * @example
178
- * ```ts
179
- * parseWith(IdSchema, toError).as("id");
180
- * // Used as the first step in `railway(rawId, parseId, ...)`
181
- * ```
182
- */
183
- declare const parseWith: <I, T, E>(parser: ParserLike<I, T>, onThrow: (error: unknown) => E) => {
184
- as: <K extends string>(key: K) => <F, M extends RailwayMode>(workflow: Railway<RailwayInput<I>, F, M>) => Railway<RailwayInput<I> & Record<K, T>, F | E, M>;
185
- };
186
- /**
187
- * Name-first companion to {@link parseWith}: parse the workflow input with a
188
- * Zod-like schema (or unary parse fn), naming the output field in the same
189
- * `(key, ...)` shape as {@link fromSyncNamed}, {@link deriveNamed}, et al.
190
- * Prefer this in `railway(...)` pipelines so every step's name aligns at the
191
- * left edge.
192
- *
193
- * @example
194
- * ```ts
195
- * railway(
196
- * rawId,
197
- * parseNamed("id", IdSchema, toError),
198
- * fromPromiseNamed("profile", ({ id }) => fetchProfile(id), toError),
199
- * );
200
- * ```
201
- */
202
- declare const parseNamed: <K extends string, I, T, E>(key: K, parser: ParserLike<I, T>, onThrow: (error: unknown) => E) => <F, M extends RailwayMode>(workflow: Railway<RailwayInput<I>, F, M>) => Railway<RailwayInput<I> & Record<K, T>, F | E, M>;
203
- /**
204
- * Reusable wrapper around {@link Railway.fromSync}. Captures key + fn +
205
- * onThrow once; applies to any compatible workflow.
206
- */
207
- declare const fromSyncNamed: <K extends string, C extends object, T, E>(key: K, fn: (ctx: C) => T, onThrow: (error: unknown) => E) => <I extends C, F, M extends RailwayMode>(workflow: Railway<I, F, M>) => Railway<I & Record<K, T>, F | E, M>;
208
- /** Reusable wrapper around {@link Railway.fromResult}. */
209
- declare const fromResultNamed: <K extends string, C extends object, T, E>(key: K, fn: (ctx: C) => Result<T, E>) => <I extends C, F, M extends RailwayMode>(workflow: Railway<I, F, M>) => Railway<I & Record<K, T>, F | E, M>;
210
- /**
211
- * Reusable wrapper around {@link Railway.fromPromise}. Upgrades the
212
- * workflow to async mode when applied.
213
- */
214
- declare const fromPromiseNamed: <K extends string, C extends object, T, E>(key: K, fn: (ctx: C) => PromiseLike<T>, onReject: (error: unknown) => E) => <I extends C, F, M extends RailwayMode>(workflow: Railway<I, F, M>) => Railway<I & Record<K, T>, F | E, "async">;
215
- /** Reusable wrapper around {@link Railway.fromAsync}. */
216
- declare const fromAsyncNamed: <K extends string, C extends object, T, E>(key: K, fn: (ctx: C) => ResultAsync<T, E>) => <I extends C, F, M extends RailwayMode>(workflow: Railway<I, F, M>) => Railway<I & Record<K, T>, F | E, "async">;
217
- /** Reusable wrapper around {@link Railway.derive} — pure sync derivation. */
218
- declare const deriveNamed: <K extends string, C extends object, T>(key: K, fn: (ctx: C) => T) => <I extends C, E, M extends RailwayMode>(workflow: Railway<I, E, M>) => Railway<I & Record<K, T>, E, M>;
219
- /** Reusable wrapper around {@link Railway.require} — narrows a nullable field. */
220
- declare const requireNamed: <K extends string, S extends string, C extends object, E>(key: K, source: S, onMissing: (ctx: C & Record<S, unknown>) => E) => <I extends C & Record<S, unknown>, F, M extends RailwayMode>(workflow: Railway<I, F, M>) => Railway<I & Record<K, NonNullable<I[S]>>, F | E, M>;
221
- /**
222
- * Reusable wrapper around {@link Railway.parallel}. Branches run
223
- * concurrently and merge their named outputs back into context.
224
- */
225
- declare const parallelNamed: <R extends BranchRecord>(branches: R) => <I extends ParallelInput<R>, E, M extends RailwayMode>(workflow: Railway<I, E, M>) => Railway<I & ParallelOutput<R>, E | ParallelError<R>, "async">;
226
- /**
227
- * Reusable wrapper around {@link Railway.select} — projects the final
228
- * context into the workflow's output type.
229
- */
230
- declare const select: <C extends object, T>(fn: (ctx: C) => T) => <E, M extends RailwayMode>(workflow: Railway<C, E, M>) => RailwayOutput<T, E, M>;
231
-
232
- export { Railway, type RailwayInput, type RailwayMode, type RailwayOutput, deriveNamed, fromAsyncNamed, fromPromiseNamed, fromResultNamed, fromSyncNamed, parallelNamed, parseNamed, parseWith, railway, requireNamed, select };
78
+ export { type BranchFn, type BranchRecord, type ParallelError, type ParallelOutput, Railway, type RailwayMode, type RailwayOutput };
package/dist/railway.js CHANGED
@@ -1,3 +1,10 @@
1
+ // src/internal/dual.ts
2
+ var dual = (arity, body) => {
3
+ const run = body;
4
+ const dispatcher = (...args) => args.length >= arity ? run(...args) : (self) => run(self, ...args);
5
+ return dispatcher;
6
+ };
7
+
1
8
  // src/result.ts
2
9
  var ok = (value) => ({
3
10
  _tag: "Ok",
@@ -9,18 +16,18 @@ var err = (error) => ({
9
16
  });
10
17
  var isOk = (result) => result._tag === "Ok";
11
18
  var isErr = (result) => result._tag === "Err";
12
- var mapImpl = (result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error);
13
- function map(...args) {
14
- if (args.length === 2) return mapImpl(args[0], args[1]);
15
- const fn = args[0];
16
- return (result) => mapImpl(result, fn);
17
- }
18
- var mapErrImpl = (result, fn) => isErr(result) ? err(fn(result.error)) : ok(result.value);
19
- function mapErr(...args) {
20
- if (args.length === 2) return mapErrImpl(args[0], args[1]);
21
- const fn = args[0];
22
- return (result) => mapErrImpl(result, fn);
23
- }
19
+ var map = dual(
20
+ 2,
21
+ (result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error)
22
+ );
23
+ var mapErr = dual(
24
+ 2,
25
+ (result, fn) => isErr(result) ? err(fn(result.error)) : ok(result.value)
26
+ );
27
+ var flatMap = dual(
28
+ 2,
29
+ (result, fn) => isOk(result) ? fn(result.value) : err(result.error)
30
+ );
24
31
  function trySync(fn, onThrow) {
25
32
  return (...args) => {
26
33
  try {
@@ -42,17 +49,6 @@ var UnexpectedError = class extends Error {
42
49
  };
43
50
 
44
51
  // src/async.ts
45
- var liftPromiseResult = async (promise, onDefect) => {
46
- try {
47
- const result = await promise;
48
- if (isErr(result)) {
49
- return result;
50
- }
51
- return isResultLike(result.value) ? result.value : result;
52
- } catch (error) {
53
- return err(onDefect(error));
54
- }
55
- };
56
52
  var sequenceSettled = (settled) => {
57
53
  const values = [];
58
54
  for (const result of settled) {
@@ -147,16 +143,23 @@ var ResultAsync = class _ResultAsync {
147
143
  }
148
144
  /**
149
145
  * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
150
- * {@link ResultAsync}, auto-flattening one level of double-wrapping. A thrown
151
- * defect is routed through `onDefect`, defaulting to {@link UnexpectedError}.
146
+ * {@link ResultAsync}. A thrown defect is routed through `onDefect`,
147
+ * defaulting to {@link UnexpectedError}. The `Ok` value passes through
148
+ * verbatim — even when it happens to be Result-shaped.
152
149
  *
153
150
  * @param promise - a promise that already yields a `Result`
154
151
  * @param onDefect - maps an unexpected throw to the `Err` channel
155
- * @see {@link fromAsync} from `@onrails/result/interop`
152
+ * @see {@link fromAsync} from `@onrails/result`
156
153
  */
157
154
  static fromResultPromise(promise, onDefect) {
158
155
  const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
159
- return new _ResultAsync(() => liftPromiseResult(promise, mapDefect));
156
+ return new _ResultAsync(async () => {
157
+ try {
158
+ return await promise;
159
+ } catch (error) {
160
+ return err(mapDefect(error));
161
+ }
162
+ });
160
163
  }
161
164
  /**
162
165
  * Combines a homogeneous array of async results into one. Resolves
@@ -240,7 +243,7 @@ var ResultAsync = class _ResultAsync {
240
243
  *
241
244
  * @example
242
245
  * ```ts
243
- * load(id).mapErr((e): AppError => ({ kind: "load", cause: e }));
246
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
244
247
  * ```
245
248
  */
246
249
  mapErr(fn) {
@@ -264,18 +267,12 @@ var ResultAsync = class _ResultAsync {
264
267
  return first;
265
268
  }
266
269
  const next = fn(first.value);
267
- if (next instanceof _ResultAsync) {
268
- return next.resolve();
269
- }
270
- if (isResultLike(next)) {
271
- return next;
272
- }
273
- return isCompatLike(next) ? next.inner : next;
270
+ return next instanceof _ResultAsync ? next.resolve() : next;
274
271
  });
275
272
  }
276
273
  /**
277
274
  * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
278
- * tier; prefer `flatMap` in new code.
275
+ * tier; prefer {@link flatMap} in new code.
279
276
  *
280
277
  * @example
281
278
  * ```ts
@@ -292,7 +289,7 @@ var ResultAsync = class _ResultAsync {
292
289
  *
293
290
  * @example
294
291
  * ```ts
295
- * loadFromCache(id).recover(() => loadFromOrigin(id));
292
+ * loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
296
293
  * ```
297
294
  */
298
295
  recover(fn) {
@@ -307,7 +304,7 @@ var ResultAsync = class _ResultAsync {
307
304
  }
308
305
  /**
309
306
  * neverthrow-compat alias of {@link recover}. Kept as the documented compat
310
- * tier; prefer `recover` in new code.
307
+ * tier; prefer {@link recover} in new code.
311
308
  *
312
309
  * @example
313
310
  * ```ts
@@ -415,16 +412,9 @@ var ResultAsync = class _ResultAsync {
415
412
  );
416
413
  }
417
414
  };
418
- function isResultLike(v) {
419
- return typeof v === "object" && v !== null && "_tag" in v && (v._tag === "Ok" || v._tag === "Err");
420
- }
421
- function isCompatLike(v) {
422
- return typeof v === "object" && v !== null && "inner" in v && isResultLike(v.inner);
423
- }
424
415
 
425
- // src/railway-core.ts
416
+ // src/railway.ts
426
417
  var addField = (ctx, key, value) => ({ ...ctx, [key]: value });
427
- var liftResult = (result) => ResultAsync.fromResult(result);
428
418
  var Railway = class _Railway {
429
419
  constructor(state) {
430
420
  this.state = state;
@@ -438,10 +428,7 @@ var Railway = class _Railway {
438
428
  static context(context) {
439
429
  return new _Railway({ mode: "sync", result: ok(context) });
440
430
  }
441
- /**
442
- * Start a sync workflow with a throwing function — `onThrow` maps any
443
- * exception to a typed error.
444
- */
431
+ /** Start a sync workflow with a throwing function. */
445
432
  static fromSync(key, fn, onThrow) {
446
433
  return _Railway.empty().fromSync(key, fn, onThrow);
447
434
  }
@@ -458,153 +445,103 @@ var Railway = class _Railway {
458
445
  return _Railway.empty().fromAsync(key, fn);
459
446
  }
460
447
  /**
461
- * Pure sync derivation — `fn` must not throw. Use {@link fromSync} for
462
- * throwing transforms.
448
+ * Rebuild the workflow in its current mode after transforming the carried
449
+ * result. Single re-link point for the state/phantom invariant: the mode
450
+ * tag is preserved verbatim, so `M` still describes the new state — TS
451
+ * cannot narrow the phantom `M` from the runtime tag, hence the cast.
463
452
  */
464
- derive(key, fn) {
465
- return this.fromResult(key, (ctx) => ok(fn(ctx)));
453
+ step(onSync, onAsync) {
454
+ const next = this.state.mode === "sync" ? { mode: "sync", result: onSync(this.state.result) } : { mode: "async", result: onAsync(this.state.result) };
455
+ return new _Railway(next);
466
456
  }
467
457
  /**
468
- * Throwing sync transform. Adds `{ [key]: T }` to context; converts any
469
- * exception to `Err<F>` via `onThrow`.
458
+ * Project the carried result into the mode-aware output type. Counterpart
459
+ * of {@link Railway.step} for terminal steps: the runtime branch matches
460
+ * the branch `RailwayOutput` picks for `M` (state/phantom invariant), but
461
+ * TS cannot resolve the conditional on an unresolved `M`, hence the cast.
470
462
  */
463
+ out(onSync, onAsync) {
464
+ return this.state.mode === "sync" ? onSync(this.state.result) : onAsync(this.state.result);
465
+ }
466
+ /** Lift the carried result into `ResultAsync` for async-upgrading steps. */
467
+ toAsync() {
468
+ return this.state.mode === "sync" ? ResultAsync.fromResult(this.state.result) : this.state.result;
469
+ }
470
+ /** Pure sync derivation. */
471
+ derive(key, fn) {
472
+ return this.fromResult(key, (ctx) => ok(fn(ctx)));
473
+ }
474
+ /** Throwing sync transform. */
471
475
  fromSync(key, fn, onThrow) {
472
476
  return this.fromResult(key, (ctx) => trySync(fn, onThrow)(ctx));
473
477
  }
474
- /**
475
- * Sync `Result`-returning step. Adds `{ [key]: T }` to context on `Ok`;
476
- * short-circuits the workflow on `Err`. Error union widens to `E | F`.
477
- */
478
+ /** Sync `Result`-returning step. */
478
479
  fromResult(key, fn) {
479
- if (this.state.mode === "sync") {
480
- const current = this.state.result;
481
- if (isErr(current)) {
482
- return new _Railway({ mode: "sync", result: err(current.error) });
483
- }
484
- const next = fn(current.value);
485
- return isErr(next) ? new _Railway({ mode: "sync", result: err(next.error) }) : new _Railway({
486
- mode: "sync",
487
- result: ok(addField(current.value, key, next.value))
488
- });
489
- }
490
- return new _Railway({
491
- mode: "async",
492
- result: this.state.result.flatMap(
480
+ return this.step(
481
+ (result) => flatMap(result, (ctx) => map(fn(ctx), (value) => addField(ctx, key, value))),
482
+ (result) => result.flatMap(
493
483
  (ctx) => ResultAsync.fromResult(fn(ctx)).map((value) => addField(ctx, key, value))
494
484
  )
495
- });
485
+ );
496
486
  }
497
- /**
498
- * Promise-returning step — upgrades the workflow to async mode. Reject
499
- * reasons go through `onReject` to become typed `Err<F>`.
500
- */
487
+ /** Promise-returning step — upgrades the workflow to async mode. */
501
488
  fromPromise(key, fn, onReject) {
502
489
  return this.fromAsync(key, (ctx) => ResultAsync.fromPromise(fn(ctx), onReject));
503
490
  }
504
- /**
505
- * `ResultAsync`-returning step — upgrades the workflow to async mode.
506
- * Already-typed error: no mapper needed.
507
- */
491
+ /** `ResultAsync`-returning step — upgrades the workflow to async mode. */
508
492
  fromAsync(key, fn) {
509
- const current = this.state.mode === "sync" ? liftResult(this.state.result) : this.state.result;
510
493
  return new _Railway({
511
494
  mode: "async",
512
- result: current.flatMap((ctx) => fn(ctx).map((value) => addField(ctx, key, value)))
495
+ result: this.toAsync().flatMap((ctx) => fn(ctx).map((value) => addField(ctx, key, value)))
513
496
  });
514
497
  }
515
- /**
516
- * Narrow a nullable context field into a required non-null field. If the
517
- * source field is `null` / `undefined`, the workflow short-circuits with
518
- * `onMissing(ctx)`.
519
- *
520
- * @example
521
- * ```ts
522
- * .fromPromise("row", ({ id }) => db.users.findFirst({ where: eq(users.id, id) }))
523
- * .require("user", "row", ({ id }) => ({ kind: "not_found" as const, id }))
524
- * // user is now User (non-null), not User | null
525
- * ```
526
- */
498
+ /** Narrow a nullable context field into a required non-null field. */
527
499
  require(key, source, onMissing) {
528
500
  return this.fromResult(key, (ctx) => {
529
501
  const value = ctx[source];
530
502
  return value == null ? err(onMissing(ctx)) : ok(value);
531
503
  });
532
504
  }
533
- /**
534
- * Run independent `ResultAsync` branches concurrently and merge their
535
- * named outputs back into context. Upgrades the workflow to async mode.
536
- * On multiple failures, the first `Err` in record-iteration order wins.
537
- *
538
- * @example
539
- * ```ts
540
- * .parallel({
541
- * recent: ({ userId }) => loadRecent(userId),
542
- * metrics: ({ userId }) => loadMetrics(userId),
543
- * })
544
- * // ctx now has { ..., recent, metrics }
545
- * ```
546
- */
505
+ /** Run independent `ResultAsync` branches concurrently and merge outputs. */
547
506
  parallel(branches) {
548
- const current = this.state.mode === "sync" ? liftResult(this.state.result) : this.state.result;
507
+ const merged = this.toAsync().flatMap(
508
+ (ctx) => ResultAsync.combineTupleParallel(
509
+ Object.entries(branches).map(([, branch]) => branch(ctx))
510
+ ).map(
511
+ (values) => (
512
+ // Branch outputs land under exactly R's keys; Object.fromEntries
513
+ // erases that correspondence, so reassert the merged shape.
514
+ {
515
+ ...ctx,
516
+ ...Object.fromEntries(Object.keys(branches).map((key, index) => [key, values[index]]))
517
+ }
518
+ )
519
+ )
520
+ );
549
521
  return new _Railway({
550
522
  mode: "async",
551
- result: current.flatMap(
552
- (ctx) => ResultAsync.combineTupleParallel(
553
- Object.entries(branches).map(([, branch]) => branch(ctx))
554
- ).map((values) => {
555
- const next = { ...ctx };
556
- Object.keys(branches).forEach((key, index) => {
557
- next[key] = values[index];
558
- });
559
- return next;
560
- })
561
- )
523
+ // BranchFn erases per-key types to `ResultAsync<unknown, unknown>`
524
+ // (ParallelOutput/ParallelError recover them from R), so re-assert the
525
+ // precise value/error union the branches actually produce.
526
+ result: merged
562
527
  });
563
528
  }
564
- /**
565
- * Project the final context into the workflow's output type. Hides the
566
- * internal context shape from callers.
567
- */
529
+ /** Project the final context into the workflow's output type. */
568
530
  select(fn) {
569
- if (this.state.mode === "sync") {
570
- const current = this.state.result;
571
- return isErr(current) ? err(current.error) : ok(fn(current.value));
572
- }
573
- return this.state.result.map(fn);
531
+ return this.out(
532
+ (result) => map(result, fn),
533
+ (result) => result.map(fn)
534
+ );
574
535
  }
575
- /**
576
- * Return the accumulated context as-is. Use when downstream code needs
577
- * every named field; prefer {@link select} when you can project to a DTO.
578
- */
536
+ /** Return the accumulated context as-is. */
579
537
  done() {
580
- return this.state.result;
538
+ return this.out(
539
+ (result) => result,
540
+ (result) => result
541
+ );
581
542
  }
582
543
  };
583
544
 
584
- // src/railway-steps.ts
585
- var parseWithParser = (parser, input) => typeof parser === "function" ? parser(input) : parser.parse(input);
586
- function railway(input, ...steps) {
587
- return steps.reduce(
588
- (current, step) => step(current),
589
- Railway.context({ input })
590
- );
591
- }
592
- var parseWith = (parser, onThrow) => ({
593
- as: (key) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow)
594
- });
595
- var parseNamed = (key, parser, onThrow) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow);
596
- var fromSyncNamed = (key, fn, onThrow) => (workflow) => workflow.fromSync(key, fn, onThrow);
597
- var fromResultNamed = (key, fn) => (workflow) => workflow.fromResult(key, fn);
598
- var fromPromiseNamed = (key, fn, onReject) => (workflow) => workflow.fromPromise(key, fn, onReject);
599
- var fromAsyncNamed = (key, fn) => (workflow) => workflow.fromAsync(key, fn);
600
- var deriveNamed = (key, fn) => (workflow) => workflow.derive(key, fn);
601
- var requireNamed = (key, source, onMissing) => (workflow) => workflow.require(key, source, onMissing);
602
- var parallelNamed = (branches) => (workflow) => {
603
- const typedBranches = branches;
604
- return workflow.parallel(typedBranches);
605
- };
606
- var select = (fn) => (workflow) => workflow.select(fn);
607
-
608
- export { Railway, deriveNamed, fromAsyncNamed, fromPromiseNamed, fromResultNamed, fromSyncNamed, parallelNamed, parseNamed, parseWith, railway, requireNamed, select };
545
+ export { Railway };
609
546
  //# sourceMappingURL=railway.js.map
610
547
  //# sourceMappingURL=railway.js.map