@onrails/result 0.1.4 → 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 (72) hide show
  1. package/DESIGN.md +11 -19
  2. package/README.md +14 -62
  3. package/RECIPES.md +12 -25
  4. package/dist/async-BR3AX0ck.d.cts +326 -0
  5. package/dist/async-SZ7BEl0g.d.ts +326 -0
  6. package/dist/compat/neverthrow.cjs +266 -94
  7. package/dist/compat/neverthrow.cjs.map +1 -1
  8. package/dist/compat/neverthrow.d.cts +4 -4
  9. package/dist/compat/neverthrow.d.ts +4 -4
  10. package/dist/compat/neverthrow.js +263 -91
  11. package/dist/compat/neverthrow.js.map +1 -1
  12. package/dist/extra.cjs +2 -25
  13. package/dist/extra.cjs.map +1 -1
  14. package/dist/extra.d.cts +7 -19
  15. package/dist/extra.d.ts +7 -19
  16. package/dist/extra.js +3 -25
  17. package/dist/extra.js.map +1 -1
  18. package/dist/fluent.cjs +55 -29
  19. package/dist/fluent.cjs.map +1 -1
  20. package/dist/fluent.d.cts +38 -11
  21. package/dist/fluent.d.ts +38 -11
  22. package/dist/fluent.js +55 -29
  23. package/dist/fluent.js.map +1 -1
  24. package/dist/index.cjs +290 -131
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +294 -69
  27. package/dist/index.d.ts +294 -69
  28. package/dist/index.js +287 -127
  29. package/dist/index.js.map +1 -1
  30. package/dist/pipe.cjs.map +1 -1
  31. package/dist/pipe.d.cts +7 -5
  32. package/dist/pipe.d.ts +7 -5
  33. package/dist/pipe.js.map +1 -1
  34. package/dist/railway.cjs +293 -188
  35. package/dist/railway.cjs.map +1 -1
  36. package/dist/railway.d.cts +26 -180
  37. package/dist/railway.d.ts +26 -180
  38. package/dist/railway.js +294 -178
  39. package/dist/railway.js.map +1 -1
  40. package/dist/try-gen.cjs +1 -1
  41. package/dist/try-gen.cjs.map +1 -1
  42. package/dist/try-gen.d.cts +29 -3
  43. package/dist/try-gen.d.ts +29 -3
  44. package/dist/try-gen.js +1 -1
  45. package/dist/try-gen.js.map +1 -1
  46. package/dist/types-BQ9vv0nD.d.cts +55 -0
  47. package/dist/types-BQ9vv0nD.d.ts +55 -0
  48. package/package.json +5 -18
  49. package/skills/railway-do-notation/SKILL.md +397 -0
  50. package/skills/result-composition/SKILL.md +323 -0
  51. package/dist/$.cjs +0 -40
  52. package/dist/$.cjs.map +0 -1
  53. package/dist/$.js +0 -36
  54. package/dist/$.js.map +0 -1
  55. package/dist/_.d.cts +0 -2
  56. package/dist/_.d.ts +0 -2
  57. package/dist/async-CH8Cs8KE.d.cts +0 -161
  58. package/dist/async-DKISqIBc.d.ts +0 -161
  59. package/dist/interop.cjs +0 -253
  60. package/dist/interop.cjs.map +0 -1
  61. package/dist/interop.d.cts +0 -35
  62. package/dist/interop.d.ts +0 -35
  63. package/dist/interop.js +0 -249
  64. package/dist/interop.js.map +0 -1
  65. package/dist/types-C2Dp1d5J.d.cts +0 -21
  66. package/dist/types-C2Dp1d5J.d.ts +0 -21
  67. package/dist/validation.cjs +0 -53
  68. package/dist/validation.cjs.map +0 -1
  69. package/dist/validation.d.cts +0 -72
  70. package/dist/validation.d.ts +0 -72
  71. package/dist/validation.js +0 -48
  72. package/dist/validation.js.map +0 -1
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) {
@@ -69,9 +65,32 @@ var ResultAsync = class _ResultAsync {
69
65
  }
70
66
  run;
71
67
  promise = null;
68
+ /**
69
+ * Lifts an already-settled sync {@link Result} into a {@link ResultAsync}.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const ra = ResultAsync.fromResult(ok(42)); // ResultAsync<number, never>
74
+ * ```
75
+ */
72
76
  static fromResult(result) {
73
77
  return new _ResultAsync(async () => result);
74
78
  }
79
+ /**
80
+ * Wraps a `PromiseLike<T>` that may reject. Rejections pass through `onReject`
81
+ * to become a typed `Err`; resolution becomes `Ok<T>`.
82
+ *
83
+ * @param promise - the promise to wrap
84
+ * @param onReject - maps a rejection reason to the `Err` channel
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const body = ResultAsync.fromPromise(
89
+ * fetch(url).then((r) => r.text()),
90
+ * (e): NetError => ({ kind: "net", cause: String(e) }),
91
+ * );
92
+ * ```
93
+ */
75
94
  static fromPromise(promise, onReject) {
76
95
  return new _ResultAsync(async () => {
77
96
  try {
@@ -81,12 +100,29 @@ var ResultAsync = class _ResultAsync {
81
100
  }
82
101
  });
83
102
  }
103
+ /**
104
+ * Wraps a `PromiseLike<T>` that is **guaranteed not to reject**, skipping the
105
+ * `onReject` mapper. Use only when rejection is provably impossible.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const now = ResultAsync.fromSafePromise(Promise.resolve(Date.now()));
110
+ * ```
111
+ */
84
112
  static fromSafePromise(promise) {
85
113
  return new _ResultAsync(async () => ok(await promise));
86
114
  }
87
115
  /**
88
- * Defers work until {@link resolve}. Unlike {@link fromPromise}, nothing runs
89
- * until the `ResultAsync` is resolved (e.g. by `combineTuple` / `combineTupleParallel`).
116
+ * Defers work until {@link resolve}. Unlike {@link fromPromise}, the factory
117
+ * does not run until the `ResultAsync` is resolved (e.g. by `combineTuple` /
118
+ * `combineTupleParallel`). The factory runs at most once — `resolve` memoizes.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const insert = ResultAsync.defer(() => db.orders.insert(row));
123
+ * // nothing has run yet
124
+ * const r = await insert; // factory runs exactly once here
125
+ * ```
90
126
  */
91
127
  static defer(fn) {
92
128
  return new _ResultAsync(fn);
@@ -94,14 +130,49 @@ var ResultAsync = class _ResultAsync {
94
130
  static ok(value) {
95
131
  return new _ResultAsync(async () => ok(value));
96
132
  }
133
+ /**
134
+ * Lifts an error into an `Err` async result.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const r = ResultAsync.err({ kind: "not_found" as const });
139
+ * ```
140
+ */
97
141
  static err(error) {
98
142
  return new _ResultAsync(async () => err(error));
99
143
  }
100
- /** @see {@link fromAsync} from `@onrails/result/interop` */
144
+ /**
145
+ * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
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.
149
+ *
150
+ * @param promise - a promise that already yields a `Result`
151
+ * @param onDefect - maps an unexpected throw to the `Err` channel
152
+ * @see {@link fromAsync} from `@onrails/result`
153
+ */
101
154
  static fromResultPromise(promise, onDefect) {
102
155
  const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
103
- 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
+ });
104
163
  }
164
+ /**
165
+ * Combines a homogeneous array of async results into one. Resolves
166
+ * **sequentially** in input order, short-circuiting on the first `Err`.
167
+ * For heterogeneous tuples that preserve per-index types, use
168
+ * {@link combineTuple}; for wall-clock overlap, {@link combineTupleParallel}.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const all = ResultAsync.combine([loadA(), loadB(), loadC()]);
173
+ * // ResultAsync<Item[], LoadError>
174
+ * ```
175
+ */
105
176
  static combine(results) {
106
177
  return new _ResultAsync(async () => {
107
178
  const values = [];
@@ -115,6 +186,23 @@ var ResultAsync = class _ResultAsync {
115
186
  return ok(values);
116
187
  });
117
188
  }
189
+ /**
190
+ * Heterogeneous async tuple combine — resolves branches **sequentially**
191
+ * (left-to-right), returning the first `Err` in input order. Preserves each
192
+ * branch's `Ok` type by position, so the result destructures type-safely.
193
+ * This is the canonical sequential async combine (replaces the former
194
+ * `sequenceTupleAsync`).
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const combined = ResultAsync.combineTuple([loadCfg(), loadCatalog()] as const);
199
+ * // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
200
+ * const r = await combined;
201
+ * if (isOk(r)) {
202
+ * const [cfg, catalog] = r.value; // typed per position
203
+ * }
204
+ * ```
205
+ */
118
206
  static combineTuple(results) {
119
207
  return _ResultAsync.combine(
120
208
  results
@@ -123,18 +211,55 @@ var ResultAsync = class _ResultAsync {
123
211
  /**
124
212
  * Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
125
213
  * parallel for independent IO). On failure, returns the first `Err` in input order.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * // independent IO — overlap them
218
+ * const combined = ResultAsync.combineTupleParallel([
219
+ * loadProfile(id),
220
+ * loadMetrics(id),
221
+ * ] as const);
222
+ * ```
126
223
  */
127
224
  static combineTupleParallel(results) {
128
225
  return new _ResultAsync(
129
226
  async () => sequenceSettled(await Promise.all(results.map((ra) => ra.resolve())))
130
227
  );
131
228
  }
229
+ /**
230
+ * Transforms the `Ok` value, passing `Err` through unchanged.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * ResultAsync.ok(2).map((n) => n * 3); // ResultAsync<number> → Ok 6
235
+ * ```
236
+ */
132
237
  map(fn) {
133
238
  return new _ResultAsync(async () => map(await this.resolve(), fn));
134
239
  }
240
+ /**
241
+ * Transforms the `Err` value, passing `Ok` through unchanged — useful for
242
+ * unifying heterogeneous failures into one app-level union.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
247
+ * ```
248
+ */
135
249
  mapErr(fn) {
136
250
  return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
137
251
  }
252
+ /**
253
+ * Canonical bind — chains a step that itself returns a `ResultAsync` or sync
254
+ * `Result`, short-circuiting on `Err`. Error types accumulate (`E | F`).
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * authenticate(req)
259
+ * .flatMap((user) => loadProfile(user.id)) // ResultAsync
260
+ * .flatMap((p) => validate(p)); // sync Result also accepted
261
+ * ```
262
+ */
138
263
  flatMap(fn) {
139
264
  return new _ResultAsync(async () => {
140
265
  const first = await this.resolve();
@@ -142,25 +267,31 @@ var ResultAsync = class _ResultAsync {
142
267
  return first;
143
268
  }
144
269
  const next = fn(first.value);
145
- if (next instanceof _ResultAsync) {
146
- return next.resolve();
147
- }
148
- if (isResultLike(next)) {
149
- return next;
150
- }
151
- return isCompatLike(next) ? next.inner : next;
270
+ return next instanceof _ResultAsync ? next.resolve() : next;
152
271
  });
153
272
  }
273
+ /**
274
+ * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
275
+ * tier; prefer {@link flatMap} in new code.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * authenticate(req).andThen((user) => loadProfile(user.id));
280
+ * ```
281
+ */
154
282
  andThen(fn) {
155
283
  return this.flatMap(fn);
156
284
  }
157
285
  /**
158
- * Alias for {@link flatMap}.
159
- * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
286
+ * Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
287
+ * `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
288
+ * {@link flatMap} on the error track.
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
293
+ * ```
160
294
  */
161
- chain(fn) {
162
- return this.flatMap(fn);
163
- }
164
295
  recover(fn) {
165
296
  return new _ResultAsync(async () => {
166
297
  const first = await this.resolve();
@@ -171,9 +302,27 @@ var ResultAsync = class _ResultAsync {
171
302
  return next instanceof _ResultAsync ? next.resolve() : next;
172
303
  });
173
304
  }
305
+ /**
306
+ * neverthrow-compat alias of {@link recover}. Kept as the documented compat
307
+ * tier; prefer {@link recover} in new code.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * loadFromCache(id).orElse(() => loadFromOrigin(id));
312
+ * ```
313
+ */
174
314
  orElse(fn) {
175
315
  return this.recover(fn);
176
316
  }
317
+ /**
318
+ * Runs a side effect on the `Ok` value and passes the result through
319
+ * unchanged; a no-op on `Err`. Mirrors {@link tapErr}.
320
+ *
321
+ * @example
322
+ * ```ts
323
+ * saveUser(u).tap((saved) => analytics.track("user_saved", saved.id));
324
+ * ```
325
+ */
177
326
  tap(fn) {
178
327
  return new _ResultAsync(async () => {
179
328
  const result = await this.resolve();
@@ -183,6 +332,15 @@ var ResultAsync = class _ResultAsync {
183
332
  return result;
184
333
  });
185
334
  }
335
+ /**
336
+ * Runs a side effect on the `Err` value and passes the result through
337
+ * unchanged; a no-op on `Ok`. The error-track mirror of {@link tap}.
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * saveUser(u).tapErr((e) => logger.warn("save failed", e));
342
+ * ```
343
+ */
186
344
  tapErr(fn) {
187
345
  return new _ResultAsync(async () => {
188
346
  const result = await this.resolve();
@@ -192,28 +350,49 @@ var ResultAsync = class _ResultAsync {
192
350
  return result;
193
351
  });
194
352
  }
195
- unwrapOr(defaultValue) {
196
- return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
197
- }
198
353
  /**
199
- * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
200
- * re-executes deferred factories and cannot narrow.
354
+ * Settles to the `Ok` value, or `defaultValue` if `Err`. Terminal — returns a
355
+ * plain `Promise`, not a `ResultAsync`.
356
+ *
357
+ * @example
358
+ * ```ts
359
+ * const profile = await loadProfile(id).unwrapOr(guestProfile);
360
+ * ```
201
361
  */
202
- isOk() {
203
- return this.resolve().then((result) => !isErr(result));
362
+ unwrapOr(defaultValue) {
363
+ return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
204
364
  }
205
365
  /**
206
- * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
207
- * re-executes deferred factories and cannot narrow.
366
+ * Terminal collapse — folds both tracks into a single awaited value. Returns
367
+ * a plain `Promise`, settling the carrier exactly once.
368
+ *
369
+ * @param onOk - handles the `Ok` value
370
+ * @param onErr - handles the `Err` value
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * const status = await save(row).match(
375
+ * () => 200,
376
+ * (e) => (e.kind === "conflict" ? 409 : 500),
377
+ * );
378
+ * ```
208
379
  */
209
- isErr() {
210
- return this.resolve().then((result) => isErr(result));
211
- }
212
380
  match(onOk, onErr) {
213
381
  return this.resolve().then(
214
382
  (result) => isErr(result) ? onErr(result.error) : onOk(result.value)
215
383
  );
216
384
  }
385
+ /**
386
+ * Settles the carrier to a bare sync {@link Result}, memoizing so the
387
+ * underlying factory runs at most once. Prefer `await ra` (the thenable) or
388
+ * {@link match} in app code; use `resolve` when you need the tagged union back.
389
+ *
390
+ * @example
391
+ * ```ts
392
+ * const r = await load(id).resolve(); // Result<Data, LoadError>
393
+ * if (isOk(r)) use(r.value);
394
+ * ```
395
+ */
217
396
  resolve() {
218
397
  if (!this.promise) {
219
398
  this.promise = this.run();
@@ -233,16 +412,9 @@ var ResultAsync = class _ResultAsync {
233
412
  );
234
413
  }
235
414
  };
236
- function isResultLike(v) {
237
- return typeof v === "object" && v !== null && "_tag" in v && (v._tag === "Ok" || v._tag === "Err");
238
- }
239
- function isCompatLike(v) {
240
- return typeof v === "object" && v !== null && "inner" in v && isResultLike(v.inner);
241
- }
242
415
 
243
- // src/railway-core.ts
416
+ // src/railway.ts
244
417
  var addField = (ctx, key, value) => ({ ...ctx, [key]: value });
245
- var liftResult = (result) => ResultAsync.fromResult(result);
246
418
  var Railway = class _Railway {
247
419
  constructor(state) {
248
420
  this.state = state;
@@ -256,10 +428,7 @@ var Railway = class _Railway {
256
428
  static context(context) {
257
429
  return new _Railway({ mode: "sync", result: ok(context) });
258
430
  }
259
- /**
260
- * Start a sync workflow with a throwing function — `onThrow` maps any
261
- * exception to a typed error.
262
- */
431
+ /** Start a sync workflow with a throwing function. */
263
432
  static fromSync(key, fn, onThrow) {
264
433
  return _Railway.empty().fromSync(key, fn, onThrow);
265
434
  }
@@ -276,156 +445,103 @@ var Railway = class _Railway {
276
445
  return _Railway.empty().fromAsync(key, fn);
277
446
  }
278
447
  /**
279
- * Pure sync derivation — `fn` must not throw. Use {@link fromSync} for
280
- * 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.
281
452
  */
282
- derive(key, fn) {
283
- 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);
284
456
  }
285
457
  /**
286
- * Throwing sync transform. Adds `{ [key]: T }` to context; converts any
287
- * 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.
288
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. */
289
475
  fromSync(key, fn, onThrow) {
290
476
  return this.fromResult(key, (ctx) => trySync(fn, onThrow)(ctx));
291
477
  }
292
- /**
293
- * Sync `Result`-returning step. Adds `{ [key]: T }` to context on `Ok`;
294
- * short-circuits the workflow on `Err`. Error union widens to `E | F`.
295
- */
478
+ /** Sync `Result`-returning step. */
296
479
  fromResult(key, fn) {
297
- if (this.state.mode === "sync") {
298
- const current = this.state.result;
299
- if (isErr(current)) {
300
- return new _Railway({ mode: "sync", result: err(current.error) });
301
- }
302
- const next = fn(current.value);
303
- if (isErr(next)) {
304
- return new _Railway({ mode: "sync", result: err(next.error) });
305
- }
306
- return new _Railway({
307
- mode: "sync",
308
- result: ok(addField(current.value, key, next.value))
309
- });
310
- }
311
- return new _Railway({
312
- mode: "async",
313
- 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(
314
483
  (ctx) => ResultAsync.fromResult(fn(ctx)).map((value) => addField(ctx, key, value))
315
484
  )
316
- });
485
+ );
317
486
  }
318
- /**
319
- * Promise-returning step — upgrades the workflow to async mode. Reject
320
- * reasons go through `onReject` to become typed `Err<F>`.
321
- */
487
+ /** Promise-returning step — upgrades the workflow to async mode. */
322
488
  fromPromise(key, fn, onReject) {
323
489
  return this.fromAsync(key, (ctx) => ResultAsync.fromPromise(fn(ctx), onReject));
324
490
  }
325
- /**
326
- * `ResultAsync`-returning step — upgrades the workflow to async mode.
327
- * Already-typed error: no mapper needed.
328
- */
491
+ /** `ResultAsync`-returning step — upgrades the workflow to async mode. */
329
492
  fromAsync(key, fn) {
330
- const current = this.state.mode === "sync" ? liftResult(this.state.result) : this.state.result;
331
493
  return new _Railway({
332
494
  mode: "async",
333
- 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)))
334
496
  });
335
497
  }
336
- /**
337
- * Narrow a nullable context field into a required non-null field. If the
338
- * source field is `null` / `undefined`, the workflow short-circuits with
339
- * `onMissing(ctx)`.
340
- *
341
- * @example
342
- * ```ts
343
- * .fromPromise("row", ({ id }) => db.users.findFirst({ where: eq(users.id, id) }))
344
- * .require("user", "row", ({ id }) => ({ kind: "not_found" as const, id }))
345
- * // user is now User (non-null), not User | null
346
- * ```
347
- */
498
+ /** Narrow a nullable context field into a required non-null field. */
348
499
  require(key, source, onMissing) {
349
500
  return this.fromResult(key, (ctx) => {
350
501
  const value = ctx[source];
351
502
  return value == null ? err(onMissing(ctx)) : ok(value);
352
503
  });
353
504
  }
354
- /**
355
- * Run independent `ResultAsync` branches concurrently and merge their
356
- * named outputs back into context. Upgrades the workflow to async mode.
357
- * On multiple failures, the first `Err` in record-iteration order wins.
358
- *
359
- * @example
360
- * ```ts
361
- * .parallel({
362
- * recent: ({ userId }) => loadRecent(userId),
363
- * metrics: ({ userId }) => loadMetrics(userId),
364
- * })
365
- * // ctx now has { ..., recent, metrics }
366
- * ```
367
- */
505
+ /** Run independent `ResultAsync` branches concurrently and merge outputs. */
368
506
  parallel(branches) {
369
- 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
+ );
370
521
  return new _Railway({
371
522
  mode: "async",
372
- result: current.flatMap(
373
- (ctx) => ResultAsync.combineTupleParallel(
374
- Object.entries(branches).map(([, branch]) => branch(ctx))
375
- ).map((values) => {
376
- const next = { ...ctx };
377
- Object.keys(branches).forEach((key, index) => {
378
- next[key] = values[index];
379
- });
380
- return next;
381
- })
382
- )
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
383
527
  });
384
528
  }
385
- /**
386
- * Project the final context into the workflow's output type. Hides the
387
- * internal context shape from callers.
388
- */
529
+ /** Project the final context into the workflow's output type. */
389
530
  select(fn) {
390
- if (this.state.mode === "sync") {
391
- const current = this.state.result;
392
- return isErr(current) ? err(current.error) : ok(fn(current.value));
393
- }
394
- return this.state.result.map(fn);
531
+ return this.out(
532
+ (result) => map(result, fn),
533
+ (result) => result.map(fn)
534
+ );
395
535
  }
396
- /**
397
- * Return the accumulated context as-is. Use when downstream code needs
398
- * every named field; prefer {@link select} when you can project to a DTO.
399
- */
536
+ /** Return the accumulated context as-is. */
400
537
  done() {
401
- return this.state.result;
538
+ return this.out(
539
+ (result) => result,
540
+ (result) => result
541
+ );
402
542
  }
403
543
  };
404
544
 
405
- // src/railway-steps.ts
406
- var parseWithParser = (parser, input) => typeof parser === "function" ? parser(input) : parser.parse(input);
407
- function railway(input, ...steps) {
408
- return steps.reduce(
409
- (current, step) => step(current),
410
- Railway.context({ input })
411
- );
412
- }
413
- var parseWith = (parser, onThrow) => ({
414
- as: (key) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow)
415
- });
416
- var parseNamed = (key, parser, onThrow) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow);
417
- var fromSyncNamed = (key, fn, onThrow) => (workflow) => workflow.fromSync(key, fn, onThrow);
418
- var fromResultNamed = (key, fn) => (workflow) => workflow.fromResult(key, fn);
419
- var fromPromiseNamed = (key, fn, onReject) => (workflow) => workflow.fromPromise(key, fn, onReject);
420
- var fromAsyncNamed = (key, fn) => (workflow) => workflow.fromAsync(key, fn);
421
- var deriveNamed = (key, fn) => (workflow) => workflow.derive(key, fn);
422
- var requireNamed = (key, source, onMissing) => (workflow) => workflow.require(key, source, onMissing);
423
- var parallelNamed = (branches) => (workflow) => {
424
- const typedBranches = branches;
425
- return workflow.parallel(typedBranches);
426
- };
427
- var select = (fn) => (workflow) => workflow.select(fn);
428
-
429
- export { Railway, deriveNamed, fromAsyncNamed, fromPromiseNamed, fromResultNamed, fromSyncNamed, parallelNamed, parseNamed, parseWith, railway, requireNamed, select };
545
+ export { Railway };
430
546
  //# sourceMappingURL=railway.js.map
431
547
  //# sourceMappingURL=railway.js.map