@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/railway.cjs CHANGED
@@ -71,9 +71,32 @@ var ResultAsync = class _ResultAsync {
71
71
  }
72
72
  run;
73
73
  promise = null;
74
+ /**
75
+ * Lifts an already-settled sync {@link Result} into a {@link ResultAsync}.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const ra = ResultAsync.fromResult(ok(42)); // ResultAsync<number, never>
80
+ * ```
81
+ */
74
82
  static fromResult(result) {
75
83
  return new _ResultAsync(async () => result);
76
84
  }
85
+ /**
86
+ * Wraps a `PromiseLike<T>` that may reject. Rejections pass through `onReject`
87
+ * to become a typed `Err`; resolution becomes `Ok<T>`.
88
+ *
89
+ * @param promise - the promise to wrap
90
+ * @param onReject - maps a rejection reason to the `Err` channel
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const body = ResultAsync.fromPromise(
95
+ * fetch(url).then((r) => r.text()),
96
+ * (e): NetError => ({ kind: "net", cause: String(e) }),
97
+ * );
98
+ * ```
99
+ */
77
100
  static fromPromise(promise, onReject) {
78
101
  return new _ResultAsync(async () => {
79
102
  try {
@@ -83,12 +106,29 @@ var ResultAsync = class _ResultAsync {
83
106
  }
84
107
  });
85
108
  }
109
+ /**
110
+ * Wraps a `PromiseLike<T>` that is **guaranteed not to reject**, skipping the
111
+ * `onReject` mapper. Use only when rejection is provably impossible.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const now = ResultAsync.fromSafePromise(Promise.resolve(Date.now()));
116
+ * ```
117
+ */
86
118
  static fromSafePromise(promise) {
87
119
  return new _ResultAsync(async () => ok(await promise));
88
120
  }
89
121
  /**
90
- * Defers work until {@link resolve}. Unlike {@link fromPromise}, nothing runs
91
- * until the `ResultAsync` is resolved (e.g. by `combineTuple` / `combineTupleParallel`).
122
+ * Defers work until {@link resolve}. Unlike {@link fromPromise}, the factory
123
+ * does not run until the `ResultAsync` is resolved (e.g. by `combineTuple` /
124
+ * `combineTupleParallel`). The factory runs at most once — `resolve` memoizes.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * const insert = ResultAsync.defer(() => db.orders.insert(row));
129
+ * // nothing has run yet
130
+ * const r = await insert; // factory runs exactly once here
131
+ * ```
92
132
  */
93
133
  static defer(fn) {
94
134
  return new _ResultAsync(fn);
@@ -96,14 +136,42 @@ var ResultAsync = class _ResultAsync {
96
136
  static ok(value) {
97
137
  return new _ResultAsync(async () => ok(value));
98
138
  }
139
+ /**
140
+ * Lifts an error into an `Err` async result.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * const r = ResultAsync.err({ kind: "not_found" as const });
145
+ * ```
146
+ */
99
147
  static err(error) {
100
148
  return new _ResultAsync(async () => err(error));
101
149
  }
102
- /** @see {@link fromAsync} from `@onrails/result/interop` */
150
+ /**
151
+ * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
152
+ * {@link ResultAsync}, auto-flattening one level of double-wrapping. A thrown
153
+ * defect is routed through `onDefect`, defaulting to {@link UnexpectedError}.
154
+ *
155
+ * @param promise - a promise that already yields a `Result`
156
+ * @param onDefect - maps an unexpected throw to the `Err` channel
157
+ * @see {@link fromAsync} from `@onrails/result/interop`
158
+ */
103
159
  static fromResultPromise(promise, onDefect) {
104
160
  const mapDefect = onDefect ?? ((error) => new UnexpectedError("Unexpected async defect", error));
105
161
  return new _ResultAsync(() => liftPromiseResult(promise, mapDefect));
106
162
  }
163
+ /**
164
+ * Combines a homogeneous array of async results into one. Resolves
165
+ * **sequentially** in input order, short-circuiting on the first `Err`.
166
+ * For heterogeneous tuples that preserve per-index types, use
167
+ * {@link combineTuple}; for wall-clock overlap, {@link combineTupleParallel}.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const all = ResultAsync.combine([loadA(), loadB(), loadC()]);
172
+ * // ResultAsync<Item[], LoadError>
173
+ * ```
174
+ */
107
175
  static combine(results) {
108
176
  return new _ResultAsync(async () => {
109
177
  const values = [];
@@ -117,6 +185,23 @@ var ResultAsync = class _ResultAsync {
117
185
  return ok(values);
118
186
  });
119
187
  }
188
+ /**
189
+ * Heterogeneous async tuple combine — resolves branches **sequentially**
190
+ * (left-to-right), returning the first `Err` in input order. Preserves each
191
+ * branch's `Ok` type by position, so the result destructures type-safely.
192
+ * This is the canonical sequential async combine (replaces the former
193
+ * `sequenceTupleAsync`).
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * const combined = ResultAsync.combineTuple([loadCfg(), loadCatalog()] as const);
198
+ * // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
199
+ * const r = await combined;
200
+ * if (isOk(r)) {
201
+ * const [cfg, catalog] = r.value; // typed per position
202
+ * }
203
+ * ```
204
+ */
120
205
  static combineTuple(results) {
121
206
  return _ResultAsync.combine(
122
207
  results
@@ -125,18 +210,55 @@ var ResultAsync = class _ResultAsync {
125
210
  /**
126
211
  * Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
127
212
  * parallel for independent IO). On failure, returns the first `Err` in input order.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * // independent IO — overlap them
217
+ * const combined = ResultAsync.combineTupleParallel([
218
+ * loadProfile(id),
219
+ * loadMetrics(id),
220
+ * ] as const);
221
+ * ```
128
222
  */
129
223
  static combineTupleParallel(results) {
130
224
  return new _ResultAsync(
131
225
  async () => sequenceSettled(await Promise.all(results.map((ra) => ra.resolve())))
132
226
  );
133
227
  }
228
+ /**
229
+ * Transforms the `Ok` value, passing `Err` through unchanged.
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * ResultAsync.ok(2).map((n) => n * 3); // ResultAsync<number> → Ok 6
234
+ * ```
235
+ */
134
236
  map(fn) {
135
237
  return new _ResultAsync(async () => map(await this.resolve(), fn));
136
238
  }
239
+ /**
240
+ * Transforms the `Err` value, passing `Ok` through unchanged — useful for
241
+ * unifying heterogeneous failures into one app-level union.
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e }));
246
+ * ```
247
+ */
137
248
  mapErr(fn) {
138
249
  return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
139
250
  }
251
+ /**
252
+ * Canonical bind — chains a step that itself returns a `ResultAsync` or sync
253
+ * `Result`, short-circuiting on `Err`. Error types accumulate (`E | F`).
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * authenticate(req)
258
+ * .flatMap((user) => loadProfile(user.id)) // ResultAsync
259
+ * .flatMap((p) => validate(p)); // sync Result also accepted
260
+ * ```
261
+ */
140
262
  flatMap(fn) {
141
263
  return new _ResultAsync(async () => {
142
264
  const first = await this.resolve();
@@ -153,16 +275,28 @@ var ResultAsync = class _ResultAsync {
153
275
  return isCompatLike(next) ? next.inner : next;
154
276
  });
155
277
  }
278
+ /**
279
+ * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
280
+ * tier; prefer `flatMap` in new code.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * authenticate(req).andThen((user) => loadProfile(user.id));
285
+ * ```
286
+ */
156
287
  andThen(fn) {
157
288
  return this.flatMap(fn);
158
289
  }
159
290
  /**
160
- * Alias for {@link flatMap}.
161
- * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
291
+ * Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
292
+ * `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
293
+ * {@link flatMap} on the error track.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * loadFromCache(id).recover(() => loadFromOrigin(id));
298
+ * ```
162
299
  */
163
- chain(fn) {
164
- return this.flatMap(fn);
165
- }
166
300
  recover(fn) {
167
301
  return new _ResultAsync(async () => {
168
302
  const first = await this.resolve();
@@ -173,9 +307,27 @@ var ResultAsync = class _ResultAsync {
173
307
  return next instanceof _ResultAsync ? next.resolve() : next;
174
308
  });
175
309
  }
310
+ /**
311
+ * neverthrow-compat alias of {@link recover}. Kept as the documented compat
312
+ * tier; prefer `recover` in new code.
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * loadFromCache(id).orElse(() => loadFromOrigin(id));
317
+ * ```
318
+ */
176
319
  orElse(fn) {
177
320
  return this.recover(fn);
178
321
  }
322
+ /**
323
+ * Runs a side effect on the `Ok` value and passes the result through
324
+ * unchanged; a no-op on `Err`. Mirrors {@link tapErr}.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * saveUser(u).tap((saved) => analytics.track("user_saved", saved.id));
329
+ * ```
330
+ */
179
331
  tap(fn) {
180
332
  return new _ResultAsync(async () => {
181
333
  const result = await this.resolve();
@@ -185,6 +337,15 @@ var ResultAsync = class _ResultAsync {
185
337
  return result;
186
338
  });
187
339
  }
340
+ /**
341
+ * Runs a side effect on the `Err` value and passes the result through
342
+ * unchanged; a no-op on `Ok`. The error-track mirror of {@link tap}.
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * saveUser(u).tapErr((e) => logger.warn("save failed", e));
347
+ * ```
348
+ */
188
349
  tapErr(fn) {
189
350
  return new _ResultAsync(async () => {
190
351
  const result = await this.resolve();
@@ -194,28 +355,49 @@ var ResultAsync = class _ResultAsync {
194
355
  return result;
195
356
  });
196
357
  }
197
- unwrapOr(defaultValue) {
198
- return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
199
- }
200
358
  /**
201
- * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
202
- * re-executes deferred factories and cannot narrow.
359
+ * Settles to the `Ok` value, or `defaultValue` if `Err`. Terminal — returns a
360
+ * plain `Promise`, not a `ResultAsync`.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * const profile = await loadProfile(id).unwrapOr(guestProfile);
365
+ * ```
203
366
  */
204
- isOk() {
205
- return this.resolve().then((result) => !isErr(result));
367
+ unwrapOr(defaultValue) {
368
+ return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
206
369
  }
207
370
  /**
208
- * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
209
- * re-executes deferred factories and cannot narrow.
371
+ * Terminal collapse — folds both tracks into a single awaited value. Returns
372
+ * a plain `Promise`, settling the carrier exactly once.
373
+ *
374
+ * @param onOk - handles the `Ok` value
375
+ * @param onErr - handles the `Err` value
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * const status = await save(row).match(
380
+ * () => 200,
381
+ * (e) => (e.kind === "conflict" ? 409 : 500),
382
+ * );
383
+ * ```
210
384
  */
211
- isErr() {
212
- return this.resolve().then((result) => isErr(result));
213
- }
214
385
  match(onOk, onErr) {
215
386
  return this.resolve().then(
216
387
  (result) => isErr(result) ? onErr(result.error) : onOk(result.value)
217
388
  );
218
389
  }
390
+ /**
391
+ * Settles the carrier to a bare sync {@link Result}, memoizing so the
392
+ * underlying factory runs at most once. Prefer `await ra` (the thenable) or
393
+ * {@link match} in app code; use `resolve` when you need the tagged union back.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * const r = await load(id).resolve(); // Result<Data, LoadError>
398
+ * if (isOk(r)) use(r.value);
399
+ * ```
400
+ */
219
401
  resolve() {
220
402
  if (!this.promise) {
221
403
  this.promise = this.run();
@@ -302,10 +484,7 @@ var Railway = class _Railway {
302
484
  return new _Railway({ mode: "sync", result: err(current.error) });
303
485
  }
304
486
  const next = fn(current.value);
305
- if (isErr(next)) {
306
- return new _Railway({ mode: "sync", result: err(next.error) });
307
- }
308
- return new _Railway({
487
+ return isErr(next) ? new _Railway({ mode: "sync", result: err(next.error) }) : new _Railway({
309
488
  mode: "sync",
310
489
  result: ok(addField(current.value, key, next.value))
311
490
  });
@@ -415,6 +594,7 @@ function railway(input, ...steps) {
415
594
  var parseWith = (parser, onThrow) => ({
416
595
  as: (key) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow)
417
596
  });
597
+ var parseNamed = (key, parser, onThrow) => (workflow) => workflow.fromSync(key, ({ input }) => parseWithParser(parser, input), onThrow);
418
598
  var fromSyncNamed = (key, fn, onThrow) => (workflow) => workflow.fromSync(key, fn, onThrow);
419
599
  var fromResultNamed = (key, fn) => (workflow) => workflow.fromResult(key, fn);
420
600
  var fromPromiseNamed = (key, fn, onReject) => (workflow) => workflow.fromPromise(key, fn, onReject);
@@ -434,6 +614,7 @@ exports.fromPromiseNamed = fromPromiseNamed;
434
614
  exports.fromResultNamed = fromResultNamed;
435
615
  exports.fromSyncNamed = fromSyncNamed;
436
616
  exports.parallelNamed = parallelNamed;
617
+ exports.parseNamed = parseNamed;
437
618
  exports.parseWith = parseWith;
438
619
  exports.railway = railway;
439
620
  exports.requireNamed = requireNamed;