@onrails/result 0.1.3 → 0.2.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 (68) hide show
  1. package/DESIGN.md +2 -2
  2. package/README.md +0 -3
  3. package/RECIPES.md +16 -15
  4. package/dist/$.cjs +40 -0
  5. package/dist/$.cjs.map +1 -0
  6. package/dist/$.js +36 -0
  7. package/dist/$.js.map +1 -0
  8. package/dist/_.d.cts +2 -0
  9. package/dist/_.d.ts +2 -0
  10. package/dist/async-1fIhfzFu.d.ts +290 -0
  11. package/dist/async-u8U5P2EH.d.cts +290 -0
  12. package/dist/compat/neverthrow.cjs +234 -50
  13. package/dist/compat/neverthrow.cjs.map +1 -1
  14. package/dist/compat/neverthrow.d.cts +2 -2
  15. package/dist/compat/neverthrow.d.ts +2 -2
  16. package/dist/compat/neverthrow.js +231 -47
  17. package/dist/compat/neverthrow.js.map +1 -1
  18. package/dist/extra.cjs +1 -24
  19. package/dist/extra.cjs.map +1 -1
  20. package/dist/extra.d.cts +3 -9
  21. package/dist/extra.d.ts +3 -9
  22. package/dist/extra.js +2 -24
  23. package/dist/extra.js.map +1 -1
  24. package/dist/fluent.cjs +0 -2
  25. package/dist/fluent.cjs.map +1 -1
  26. package/dist/fluent.d.cts +2 -6
  27. package/dist/fluent.d.ts +2 -6
  28. package/dist/fluent.js +0 -2
  29. package/dist/fluent.js.map +1 -1
  30. package/dist/index.cjs +226 -47
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +174 -51
  33. package/dist/index.d.ts +174 -51
  34. package/dist/index.js +227 -44
  35. package/dist/index.js.map +1 -1
  36. package/dist/interop.cjs +209 -23
  37. package/dist/interop.cjs.map +1 -1
  38. package/dist/interop.d.cts +60 -10
  39. package/dist/interop.d.ts +60 -10
  40. package/dist/interop.js +209 -23
  41. package/dist/interop.js.map +1 -1
  42. package/dist/pipe.cjs.map +1 -1
  43. package/dist/pipe.d.cts +7 -5
  44. package/dist/pipe.d.ts +7 -5
  45. package/dist/pipe.js.map +1 -1
  46. package/dist/railway.cjs +205 -24
  47. package/dist/railway.cjs.map +1 -1
  48. package/dist/railway.d.cts +20 -3
  49. package/dist/railway.d.ts +20 -3
  50. package/dist/railway.js +205 -25
  51. package/dist/railway.js.map +1 -1
  52. package/dist/try-gen.cjs.map +1 -1
  53. package/dist/try-gen.d.cts +29 -3
  54. package/dist/try-gen.d.ts +29 -3
  55. package/dist/try-gen.js.map +1 -1
  56. package/dist/types-BQ9vv0nD.d.cts +55 -0
  57. package/dist/types-BQ9vv0nD.d.ts +55 -0
  58. package/dist/validation.cjs.map +1 -1
  59. package/dist/validation.d.cts +1 -1
  60. package/dist/validation.d.ts +1 -1
  61. package/dist/validation.js.map +1 -1
  62. package/package.json +10 -3
  63. package/skills/railway-do-notation/SKILL.md +436 -0
  64. package/skills/result-composition/SKILL.md +325 -0
  65. package/dist/async-CH8Cs8KE.d.cts +0 -161
  66. package/dist/async-DKISqIBc.d.ts +0 -161
  67. package/dist/types-C2Dp1d5J.d.cts +0 -21
  68. package/dist/types-C2Dp1d5J.d.ts +0 -21
package/dist/index.cjs CHANGED
@@ -11,7 +11,6 @@ var err = (error) => ({
11
11
  });
12
12
  var isOk = (result) => result._tag === "Ok";
13
13
  var isErr = (result) => result._tag === "Err";
14
- var of = ok;
15
14
  var mapImpl = (result, fn) => isOk(result) ? ok(fn(result.value)) : err(result.error);
16
15
  function map(...args) {
17
16
  if (args.length === 2) return mapImpl(args[0], args[1]);
@@ -66,8 +65,6 @@ function match(...args) {
66
65
  const [onOk, onErr] = args;
67
66
  return (result) => matchImpl(result, onOk, onErr);
68
67
  }
69
- var matchResult = match;
70
- var fold = (handlers) => (result) => matchImpl(result, handlers.ok, handlers.err);
71
68
  var unwrapOr = (result, defaultValue) => isOk(result) ? result.value : defaultValue;
72
69
  function unwrapOk(result) {
73
70
  if (isErr(result)) throw result.error;
@@ -86,18 +83,6 @@ function trySync(fn, onThrow) {
86
83
  }
87
84
  };
88
85
  }
89
- var combine = (results) => {
90
- const values = [];
91
- for (const result of results) {
92
- if (isErr(result)) return err(result.error);
93
- values.push(result.value);
94
- }
95
- return ok(values);
96
- };
97
- var combineTuple = (results) => (
98
- // Runtime identical to combine; the cast restores per-index tuple types.
99
- combine(results)
100
- );
101
86
  function pipe(value, ...fns) {
102
87
  let acc = value;
103
88
  for (const fn of fns) {
@@ -144,9 +129,32 @@ var ResultAsync = class _ResultAsync {
144
129
  }
145
130
  run;
146
131
  promise = null;
132
+ /**
133
+ * Lifts an already-settled sync {@link Result} into a {@link ResultAsync}.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const ra = ResultAsync.fromResult(ok(42)); // ResultAsync<number, never>
138
+ * ```
139
+ */
147
140
  static fromResult(result) {
148
141
  return new _ResultAsync(async () => result);
149
142
  }
143
+ /**
144
+ * Wraps a `PromiseLike<T>` that may reject. Rejections pass through `onReject`
145
+ * to become a typed `Err`; resolution becomes `Ok<T>`.
146
+ *
147
+ * @param promise - the promise to wrap
148
+ * @param onReject - maps a rejection reason to the `Err` channel
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * const body = ResultAsync.fromPromise(
153
+ * fetch(url).then((r) => r.text()),
154
+ * (e): NetError => ({ kind: "net", cause: String(e) }),
155
+ * );
156
+ * ```
157
+ */
150
158
  static fromPromise(promise, onReject) {
151
159
  return new _ResultAsync(async () => {
152
160
  try {
@@ -156,12 +164,29 @@ var ResultAsync = class _ResultAsync {
156
164
  }
157
165
  });
158
166
  }
167
+ /**
168
+ * Wraps a `PromiseLike<T>` that is **guaranteed not to reject**, skipping the
169
+ * `onReject` mapper. Use only when rejection is provably impossible.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const now = ResultAsync.fromSafePromise(Promise.resolve(Date.now()));
174
+ * ```
175
+ */
159
176
  static fromSafePromise(promise) {
160
177
  return new _ResultAsync(async () => ok(await promise));
161
178
  }
162
179
  /**
163
- * Defers work until {@link resolve}. Unlike {@link fromPromise}, nothing runs
164
- * until the `ResultAsync` is resolved (e.g. by `combineTuple` / `combineTupleParallel`).
180
+ * Defers work until {@link resolve}. Unlike {@link fromPromise}, the factory
181
+ * does not run until the `ResultAsync` is resolved (e.g. by `combineTuple` /
182
+ * `combineTupleParallel`). The factory runs at most once — `resolve` memoizes.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * const insert = ResultAsync.defer(() => db.orders.insert(row));
187
+ * // nothing has run yet
188
+ * const r = await insert; // factory runs exactly once here
189
+ * ```
165
190
  */
166
191
  static defer(fn) {
167
192
  return new _ResultAsync(fn);
@@ -169,14 +194,42 @@ var ResultAsync = class _ResultAsync {
169
194
  static ok(value) {
170
195
  return new _ResultAsync(async () => ok(value));
171
196
  }
197
+ /**
198
+ * Lifts an error into an `Err` async result.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const r = ResultAsync.err({ kind: "not_found" as const });
203
+ * ```
204
+ */
172
205
  static err(error) {
173
206
  return new _ResultAsync(async () => err(error));
174
207
  }
175
- /** @see {@link fromAsync} from `@onrails/result/interop` */
208
+ /**
209
+ * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
210
+ * {@link ResultAsync}, auto-flattening one level of double-wrapping. A thrown
211
+ * defect is routed through `onDefect`, defaulting to {@link UnexpectedError}.
212
+ *
213
+ * @param promise - a promise that already yields a `Result`
214
+ * @param onDefect - maps an unexpected throw to the `Err` channel
215
+ * @see {@link fromAsync} from `@onrails/result/interop`
216
+ */
176
217
  static fromResultPromise(promise, onDefect) {
177
218
  const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
178
219
  return new _ResultAsync(() => liftPromiseResult(promise, mapDefect));
179
220
  }
221
+ /**
222
+ * Combines a homogeneous array of async results into one. Resolves
223
+ * **sequentially** in input order, short-circuiting on the first `Err`.
224
+ * For heterogeneous tuples that preserve per-index types, use
225
+ * {@link combineTuple}; for wall-clock overlap, {@link combineTupleParallel}.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * const all = ResultAsync.combine([loadA(), loadB(), loadC()]);
230
+ * // ResultAsync<Item[], LoadError>
231
+ * ```
232
+ */
180
233
  static combine(results) {
181
234
  return new _ResultAsync(async () => {
182
235
  const values = [];
@@ -190,6 +243,23 @@ var ResultAsync = class _ResultAsync {
190
243
  return ok(values);
191
244
  });
192
245
  }
246
+ /**
247
+ * Heterogeneous async tuple combine — resolves branches **sequentially**
248
+ * (left-to-right), returning the first `Err` in input order. Preserves each
249
+ * branch's `Ok` type by position, so the result destructures type-safely.
250
+ * This is the canonical sequential async combine (replaces the former
251
+ * `sequenceTupleAsync`).
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * const combined = ResultAsync.combineTuple([loadCfg(), loadCatalog()] as const);
256
+ * // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
257
+ * const r = await combined;
258
+ * if (isOk(r)) {
259
+ * const [cfg, catalog] = r.value; // typed per position
260
+ * }
261
+ * ```
262
+ */
193
263
  static combineTuple(results) {
194
264
  return _ResultAsync.combine(
195
265
  results
@@ -198,18 +268,55 @@ var ResultAsync = class _ResultAsync {
198
268
  /**
199
269
  * Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
200
270
  * parallel for independent IO). On failure, returns the first `Err` in input order.
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * // independent IO — overlap them
275
+ * const combined = ResultAsync.combineTupleParallel([
276
+ * loadProfile(id),
277
+ * loadMetrics(id),
278
+ * ] as const);
279
+ * ```
201
280
  */
202
281
  static combineTupleParallel(results) {
203
282
  return new _ResultAsync(
204
283
  async () => sequenceSettled(await Promise.all(results.map((ra) => ra.resolve())))
205
284
  );
206
285
  }
286
+ /**
287
+ * Transforms the `Ok` value, passing `Err` through unchanged.
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * ResultAsync.ok(2).map((n) => n * 3); // ResultAsync<number> → Ok 6
292
+ * ```
293
+ */
207
294
  map(fn) {
208
295
  return new _ResultAsync(async () => map(await this.resolve(), fn));
209
296
  }
297
+ /**
298
+ * Transforms the `Err` value, passing `Ok` through unchanged — useful for
299
+ * unifying heterogeneous failures into one app-level union.
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e }));
304
+ * ```
305
+ */
210
306
  mapErr(fn) {
211
307
  return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
212
308
  }
309
+ /**
310
+ * Canonical bind — chains a step that itself returns a `ResultAsync` or sync
311
+ * `Result`, short-circuiting on `Err`. Error types accumulate (`E | F`).
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * authenticate(req)
316
+ * .flatMap((user) => loadProfile(user.id)) // ResultAsync
317
+ * .flatMap((p) => validate(p)); // sync Result also accepted
318
+ * ```
319
+ */
213
320
  flatMap(fn) {
214
321
  return new _ResultAsync(async () => {
215
322
  const first = await this.resolve();
@@ -226,16 +333,28 @@ var ResultAsync = class _ResultAsync {
226
333
  return isCompatLike(next) ? next.inner : next;
227
334
  });
228
335
  }
336
+ /**
337
+ * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
338
+ * tier; prefer `flatMap` in new code.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * authenticate(req).andThen((user) => loadProfile(user.id));
343
+ * ```
344
+ */
229
345
  andThen(fn) {
230
346
  return this.flatMap(fn);
231
347
  }
232
348
  /**
233
- * Alias for {@link flatMap}.
234
- * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
349
+ * Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
350
+ * `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
351
+ * {@link flatMap} on the error track.
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * loadFromCache(id).recover(() => loadFromOrigin(id));
356
+ * ```
235
357
  */
236
- chain(fn) {
237
- return this.flatMap(fn);
238
- }
239
358
  recover(fn) {
240
359
  return new _ResultAsync(async () => {
241
360
  const first = await this.resolve();
@@ -246,9 +365,27 @@ var ResultAsync = class _ResultAsync {
246
365
  return next instanceof _ResultAsync ? next.resolve() : next;
247
366
  });
248
367
  }
368
+ /**
369
+ * neverthrow-compat alias of {@link recover}. Kept as the documented compat
370
+ * tier; prefer `recover` in new code.
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * loadFromCache(id).orElse(() => loadFromOrigin(id));
375
+ * ```
376
+ */
249
377
  orElse(fn) {
250
378
  return this.recover(fn);
251
379
  }
380
+ /**
381
+ * Runs a side effect on the `Ok` value and passes the result through
382
+ * unchanged; a no-op on `Err`. Mirrors {@link tapErr}.
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * saveUser(u).tap((saved) => analytics.track("user_saved", saved.id));
387
+ * ```
388
+ */
252
389
  tap(fn) {
253
390
  return new _ResultAsync(async () => {
254
391
  const result = await this.resolve();
@@ -258,6 +395,15 @@ var ResultAsync = class _ResultAsync {
258
395
  return result;
259
396
  });
260
397
  }
398
+ /**
399
+ * Runs a side effect on the `Err` value and passes the result through
400
+ * unchanged; a no-op on `Ok`. The error-track mirror of {@link tap}.
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * saveUser(u).tapErr((e) => logger.warn("save failed", e));
405
+ * ```
406
+ */
261
407
  tapErr(fn) {
262
408
  return new _ResultAsync(async () => {
263
409
  const result = await this.resolve();
@@ -267,28 +413,49 @@ var ResultAsync = class _ResultAsync {
267
413
  return result;
268
414
  });
269
415
  }
270
- unwrapOr(defaultValue) {
271
- return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
272
- }
273
416
  /**
274
- * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
275
- * re-executes deferred factories and cannot narrow.
417
+ * Settles to the `Ok` value, or `defaultValue` if `Err`. Terminal — returns a
418
+ * plain `Promise`, not a `ResultAsync`.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * const profile = await loadProfile(id).unwrapOr(guestProfile);
423
+ * ```
276
424
  */
277
- isOk() {
278
- return this.resolve().then((result) => !isErr(result));
425
+ unwrapOr(defaultValue) {
426
+ return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
279
427
  }
280
428
  /**
281
- * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
282
- * re-executes deferred factories and cannot narrow.
429
+ * Terminal collapse — folds both tracks into a single awaited value. Returns
430
+ * a plain `Promise`, settling the carrier exactly once.
431
+ *
432
+ * @param onOk - handles the `Ok` value
433
+ * @param onErr - handles the `Err` value
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * const status = await save(row).match(
438
+ * () => 200,
439
+ * (e) => (e.kind === "conflict" ? 409 : 500),
440
+ * );
441
+ * ```
283
442
  */
284
- isErr() {
285
- return this.resolve().then((result) => isErr(result));
286
- }
287
443
  match(onOk, onErr) {
288
444
  return this.resolve().then(
289
445
  (result) => isErr(result) ? onErr(result.error) : onOk(result.value)
290
446
  );
291
447
  }
448
+ /**
449
+ * Settles the carrier to a bare sync {@link Result}, memoizing so the
450
+ * underlying factory runs at most once. Prefer `await ra` (the thenable) or
451
+ * {@link match} in app code; use `resolve` when you need the tagged union back.
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * const r = await load(id).resolve(); // Result<Data, LoadError>
456
+ * if (isOk(r)) use(r.value);
457
+ * ```
458
+ */
292
459
  resolve() {
293
460
  if (!this.promise) {
294
461
  this.promise = this.run();
@@ -314,26 +481,42 @@ function isResultLike(v) {
314
481
  function isCompatLike(v) {
315
482
  return typeof v === "object" && v !== null && "inner" in v && isResultLike(v.inner);
316
483
  }
484
+
485
+ // src/async-lift.ts
317
486
  var okAsync = ResultAsync.ok;
318
487
  var errAsync = ResultAsync.err;
319
488
  var fromPromise = ResultAsync.fromPromise;
320
489
  var fromSafePromise = ResultAsync.fromSafePromise;
321
- var sequenceTupleAsync = ResultAsync.combineTuple;
322
490
  var parallelTupleAsync = ResultAsync.combineTupleParallel;
323
491
  var toError = (error) => error instanceof Error ? error : new Error(String(error));
324
492
  function tryAsync(promise, onReject) {
325
- if (onReject) {
326
- return ResultAsync.fromPromise(promise, onReject);
327
- }
328
- return ResultAsync.fromPromise(promise, toError);
493
+ return onReject ? ResultAsync.fromPromise(promise, onReject) : ResultAsync.fromPromise(promise, toError);
329
494
  }
330
495
 
496
+ // src/collections.ts
497
+ var combine = (results) => {
498
+ const values = [];
499
+ for (const result of results) {
500
+ if (isErr(result)) return err(result.error);
501
+ values.push(result.value);
502
+ }
503
+ return ok(values);
504
+ };
505
+ var combineTuple = (results) => (
506
+ // Runtime identical to combine; the cast restores per-index tuple types.
507
+ combine(results)
508
+ );
509
+
331
510
  // src/interop.ts
332
511
  var fromResult = (result) => ResultAsync.fromResult(result);
333
- var asyncAfter = (result, fn) => fromResult(result).flatMap(fn);
334
- function fromAsync(fn, onDefect) {
335
- return (...args) => ResultAsync.fromResultPromise(fn(...args), onDefect);
512
+ function asyncAfter(resultOrFn, fn) {
513
+ if (fn === void 0) {
514
+ const f = resultOrFn;
515
+ return (result) => fromResult(result).flatMap(f);
516
+ }
517
+ return fromResult(resultOrFn).flatMap(fn);
336
518
  }
519
+ var fromAsync = (fn, onDefect) => (...args) => ResultAsync.fromResultPromise(fn(...args), onDefect);
337
520
 
338
521
  // src/pipe.ts
339
522
  function flow(first, ...rest) {
@@ -382,7 +565,6 @@ exports.err = err;
382
565
  exports.errAsync = errAsync;
383
566
  exports.flatMap = flatMap;
384
567
  exports.flow = flow;
385
- exports.fold = fold;
386
568
  exports.fromAsync = fromAsync;
387
569
  exports.fromPromise = fromPromise;
388
570
  exports.fromResult = fromResult;
@@ -392,14 +574,11 @@ exports.isOk = isOk;
392
574
  exports.map = map;
393
575
  exports.mapErr = mapErr;
394
576
  exports.match = match;
395
- exports.matchResult = matchResult;
396
- exports.of = of;
397
577
  exports.ok = ok;
398
578
  exports.okAsync = okAsync;
399
579
  exports.parallelTupleAsync = parallelTupleAsync;
400
580
  exports.pipe = pipe;
401
581
  exports.recover = recover;
402
- exports.sequenceTupleAsync = sequenceTupleAsync;
403
582
  exports.tap = tap;
404
583
  exports.tapErr = tapErr;
405
584
  exports.tryAsync = tryAsync;