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