@onrails/result 0.1.1 → 0.1.3

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 (52) hide show
  1. package/DESIGN.md +0 -1
  2. package/README.md +10 -17
  3. package/RECIPES.md +143 -21
  4. package/dist/{async-CCA1yK8q.d.cts → async-CH8Cs8KE.d.cts} +14 -0
  5. package/dist/{async-DH_-dNIo.d.ts → async-DKISqIBc.d.ts} +14 -0
  6. package/dist/compat/neverthrow.cjs +78 -63
  7. package/dist/compat/neverthrow.cjs.map +1 -1
  8. package/dist/compat/neverthrow.d.cts +1 -1
  9. package/dist/compat/neverthrow.d.ts +1 -1
  10. package/dist/compat/neverthrow.js +78 -63
  11. package/dist/compat/neverthrow.js.map +1 -1
  12. package/dist/extra.cjs +17 -10
  13. package/dist/extra.cjs.map +1 -1
  14. package/dist/extra.d.cts +10 -2
  15. package/dist/extra.d.ts +10 -2
  16. package/dist/extra.js +17 -10
  17. package/dist/extra.js.map +1 -1
  18. package/dist/fluent.cjs.map +1 -1
  19. package/dist/fluent.d.cts +3 -1
  20. package/dist/fluent.d.ts +3 -1
  21. package/dist/fluent.js.map +1 -1
  22. package/dist/index.cjs +53 -58
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +21 -5
  25. package/dist/index.d.ts +21 -5
  26. package/dist/index.js +53 -58
  27. package/dist/index.js.map +1 -1
  28. package/dist/interop.cjs +49 -50
  29. package/dist/interop.cjs.map +1 -1
  30. package/dist/interop.d.cts +1 -1
  31. package/dist/interop.d.ts +1 -1
  32. package/dist/interop.js +49 -50
  33. package/dist/interop.js.map +1 -1
  34. package/dist/railway.cjs +58 -59
  35. package/dist/railway.cjs.map +1 -1
  36. package/dist/railway.d.cts +7 -6
  37. package/dist/railway.d.ts +7 -6
  38. package/dist/railway.js +58 -59
  39. package/dist/railway.js.map +1 -1
  40. package/dist/try-gen.cjs.map +1 -1
  41. package/dist/try-gen.js.map +1 -1
  42. package/dist/validation.cjs +8 -25
  43. package/dist/validation.cjs.map +1 -1
  44. package/dist/validation.js +8 -25
  45. package/dist/validation.js.map +1 -1
  46. package/package.json +1 -6
  47. package/dist/mcp.cjs +0 -292
  48. package/dist/mcp.cjs.map +0 -1
  49. package/dist/mcp.d.cts +0 -62
  50. package/dist/mcp.d.ts +0 -62
  51. package/dist/mcp.js +0 -284
  52. package/dist/mcp.js.map +0 -1
package/DESIGN.md CHANGED
@@ -96,7 +96,6 @@ Use `flatMap` for dependent checks where later checks need earlier successful va
96
96
  | `@onrails/result/fluent` | Dot-style chains |
97
97
  | `@onrails/result/extra` | Error-type utilities |
98
98
  | `@onrails/result/interop` | Promise/ResultAsync boundary helpers |
99
- | `@onrails/result/mcp` | MCP / HTTP boundary helpers |
100
99
  | `@onrails/result/pipe` | Variadic `flow` for point-free composition |
101
100
  | `@onrails/result/railway` | Named workflow builder and reusable step factories |
102
101
  | `@onrails/result/try-gen` | Sync generator-style `Result` sugar |
package/README.md CHANGED
@@ -132,13 +132,18 @@ else console.error(r.error);
132
132
 
133
133
  ## Match and unwrap helpers
134
134
 
135
- `matchResult` is an alias for `match` for files that also import `match` from `ts-pattern`:
135
+ `match` is the canonical positional fold. If a file imports `match` from `@onrails/pattern` or `ts-pattern`, resolve the collision by using namespace imports:
136
136
 
137
137
  ```ts
138
- import { matchResult } from "@onrails/result";
138
+ import * as R from "@onrails/result";
139
139
  import { match } from "ts-pattern";
140
+
141
+ R.match(result, onOk, onErr);
140
142
  ```
141
143
 
144
+ > [!NOTE]
145
+ > `matchResult` is deprecated and will be removed in the next major version.
146
+
142
147
  `unwrapOk` and `unwrapErr` are test/assertion helpers. Prefer `match`, `isOk`, or `isErr` in production control flow.
143
148
 
144
149
  ```ts
@@ -147,17 +152,6 @@ import { unwrapOk } from "@onrails/result";
147
152
  expect(unwrapOk(parseConfig(raw))).toEqual(expected);
148
153
  ```
149
154
 
150
- ## MCP / HTTP boundaries
151
-
152
- ```ts
153
- import { toToolResponseAsync, unwrapFetchResultAsync } from "@onrails/result/mcp";
154
-
155
- const ra = unwrapFetchResultAsync(
156
- client.GET("/tokens/{id}"),
157
- ({ error, response }) => new PrintrApiError(response.status, detail),
158
- );
159
- return toToolResponseAsync(ra);
160
- ```
161
155
 
162
156
  ## `tryGen` — sync `?`
163
157
 
@@ -173,12 +167,12 @@ const out = tryGen(() => {
173
167
  });
174
168
  ```
175
169
 
176
- Use `sequenceTupleAsync` (or `parallelTupleAsync` when branches should overlap) when combining heterogeneous async results and destructuring the result:
170
+ Use `ResultAsync.combineTuple` (or `parallelTupleAsync` when branches should overlap) when combining heterogeneous async results and destructuring the result:
177
171
 
178
172
  ```ts
179
- import { sequenceTupleAsync } from "@onrails/result";
173
+ import { ResultAsync } from "@onrails/result";
180
174
 
181
- const combined = sequenceTupleAsync([
175
+ const combined = ResultAsync.combineTuple([
182
176
  loadSettings(),
183
177
  loadModelCatalog(),
184
178
  ] as const);
@@ -314,7 +308,6 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
314
308
  | `@onrails/result/fluent` | `fluent()`, `fluentAsync()` |
315
309
  | `@onrails/result/extra` | Error-type utilities |
316
310
  | `@onrails/result/interop` | `fromAsync`, `fromResult`, `asyncAfter` |
317
- | `@onrails/result/mcp` | MCP / openapi-fetch helpers |
318
311
  | `@onrails/result/pipe` | `flow` (variadic point-free composition) |
319
312
  | `@onrails/result/railway` | `Railway`, `railway`, named workflow helpers |
320
313
  | `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
package/RECIPES.md CHANGED
@@ -11,6 +11,68 @@ map(fn)(result); // curried, data-last — feeds `pipe(...)` and `flow(...)`
11
11
 
12
12
  See [README.md](./README.md) for the API reference and decision tree.
13
13
 
14
+ ## Composition Guidelines (The Four Tiers)
15
+
16
+ To keep codebases readable and consistent, follow the four-tier decision guideline when choosing how to compose operations. Use the simplest pattern that fits your workflow:
17
+
18
+ | Tier | Use Case | Recommended Pattern |
19
+ | ---- | -------- | ------------------- |
20
+ | **1** | 1–2 steps, linear | direct data-first calls or method chains |
21
+ | **2** | 3+ steps, linear | `pipe` or `flow` |
22
+ | **3** | branchy, value reused | `tryGen` escape hatch |
23
+ | **4** | 4+ named steps, mixed IO | `Railway` or `railway()` steps |
24
+
25
+ `/fluent` is documented as app-edge sugar only — never in library or service internals.
26
+
27
+ ### Tier 1: 1–2 steps, linear (Direct data-first call or method chain)
28
+
29
+ ```ts
30
+ import { map } from "@onrails/result";
31
+
32
+ const normalized = map(rawResult, normalize);
33
+ ```
34
+
35
+ ### Tier 2: 3+ steps, linear (pipe / flow)
36
+
37
+ ```ts
38
+ import { flow, map, flatMap, recover } from "@onrails/result";
39
+
40
+ const process = flow(
41
+ map(normalize),
42
+ flatMap(validate),
43
+ recover(fallback)
44
+ );
45
+ ```
46
+
47
+ ### Tier 3: Branchy, value reused (tryGen)
48
+
49
+ ```ts
50
+ import { tryGen, $ } from "@onrails/result/try-gen";
51
+ import { ok, err } from "@onrails/result";
52
+
53
+ const stepResult = tryGen(() => {
54
+ const user = $(authenticate(req));
55
+ const post = $(fetchPost(postId));
56
+ if (post.authorId !== user.id && !user.isAdmin) {
57
+ return err({ kind: "unauthorized" as const });
58
+ }
59
+ return ok(post);
60
+ });
61
+ ```
62
+
63
+ ### Tier 4: 4+ named steps, mixed IO (Railway context builder)
64
+
65
+ ```ts
66
+ import { Railway } from "@onrails/result/railway";
67
+
68
+ const workflow = Railway
69
+ .fromSync("id", () => IdSchema.parse(raw), toError)
70
+ .fromPromise("row", ({ id }) => db.profiles.find(id), toError)
71
+ .require("profile", "row", ({ id }) => ({ kind: "not_found" as const, id }))
72
+ .derive("normalized", ({ profile }) => normalizeProfile(profile))
73
+ .select(({ normalized }) => normalized);
74
+ ```
75
+
14
76
  ---
15
77
 
16
78
  ## 1. Reusable parser builder via `flow`
@@ -26,16 +88,8 @@ type SchemaError = { kind: "schema"; field: string };
26
88
 
27
89
  const parseJsonWith = <T>(schema: { parse: (x: unknown) => T }) =>
28
90
  flow(
29
- trySync(
30
- (raw: string) => JSON.parse(raw),
31
- (e): ParseError => ({ kind: "parse", message: String(e) }),
32
- ),
33
- flatMap((data: unknown) =>
34
- trySync(
35
- () => schema.parse(data),
36
- (e): SchemaError => ({ kind: "schema", field: String(e) }),
37
- )(),
38
- ),
91
+ trySync(JSON.parse, (e): ParseError => ({ kind: "parse", message: String(e) })),
92
+ flatMap(trySync(schema.parse, (e): SchemaError => ({ kind: "schema", field: String(e) }))),
39
93
  );
40
94
 
41
95
  const parseUser = parseJsonWith(UserSchema);
@@ -167,7 +221,7 @@ const buildSummary = (userId: string) =>
167
221
  // ResultAsync<Summary, ProfileError | MetricsError>
168
222
  ```
169
223
 
170
- `parallelTupleAsync` preserves tuple positions, so destructuring stays type-safe. Use `sequenceTupleAsync` if branches must run left-to-right.
224
+ `parallelTupleAsync` preserves tuple positions, so destructuring stays type-safe. Use static `ResultAsync.combineTuple` if branches must run left-to-right.
171
225
 
172
226
  ---
173
227
 
@@ -181,24 +235,24 @@ import { flatMap, recover, ok, err } from "@onrails/result";
181
235
 
182
236
  type LengthError = { kind: "len"; min: number };
183
237
  type CharsError = { kind: "chars"; bad: string };
238
+ type TooShortError = { kind: "too_short"; min: number };
184
239
 
185
- const requireMin = (min: number) =>
186
- flow((s: string) => (s.length >= min ? ok(s) : err({ kind: "len" as const, min })));
240
+ const requireMin = (min: number) => (s: string) =>
241
+ s.length >= min ? ok(s) : err({ kind: "len" as const, min });
187
242
 
188
- const requireAscii = flow(
189
- (s: string) =>
190
- /^[\x20-\x7e]*$/.test(s) ? ok(s) : err({ kind: "chars" as const, bad: s }),
191
- );
243
+ const requireAscii = (s: string) =>
244
+ /^[\x20-\x7e]*$/.test(s) ? ok(s) : err({ kind: "chars" as const, bad: s });
192
245
 
193
246
  const validateUsername = flow(
194
247
  (raw: string) => ok(raw.trim()),
195
248
  flatMap(requireMin(3)),
196
249
  flatMap(requireAscii),
197
- recover((e: LengthError | CharsError) =>
198
- e.kind === "len" ? err({ kind: "too_short" as const, min: e.min }) : err(e),
250
+ recover(
251
+ (e: LengthError | CharsError): Result<string, TooShortError | CharsError> =>
252
+ e.kind === "len" ? err({ kind: "too_short" as const, min: e.min }) : err(e),
199
253
  ),
200
254
  );
201
- // (raw: string) => Result<string, { kind: "too_short"; min: number } | CharsError>
255
+ // (raw: string) => Result<string, TooShortError | CharsError>
202
256
  ```
203
257
 
204
258
  `requireMin(3)` is a curried factory — `flow` strings it into the pipeline alongside `requireAscii`. None of the inner steps mention the value.
@@ -301,7 +355,12 @@ import { map, recover, ok, err, type Result } from "@onrails/result";
301
355
 
302
356
  type FetchError = { kind: "network" } | { kind: "fatal"; message: string };
303
357
 
304
- const fetchWith = (cfg: { fallback?: Body; rethrow: (e: FetchError) => boolean }) =>
358
+ type FetchConfig = {
359
+ readonly fallback?: Body;
360
+ readonly rethrow: (e: FetchError) => boolean;
361
+ };
362
+
363
+ const fetchWith = (cfg: FetchConfig) =>
305
364
  flow(
306
365
  fetchSync,
307
366
  recover((e: FetchError) =>
@@ -350,6 +409,69 @@ Each sub-flow has a clear purpose; the top-level `ingest` reads as a sentence. E
350
409
 
351
410
  ---
352
411
 
412
+ ## 12. Async pipelines via `ResultAsync` composition
413
+
414
+ When composing functions that return `ResultAsync` values, `flow` chains them by wrapping the async transforms in arrow functions. The resulting pipeline remains point-free over the input.
415
+
416
+ ```ts
417
+ import { flow } from "@onrails/result/pipe";
418
+ import { type ResultAsync } from "@onrails/result";
419
+
420
+ type Profile = { id: string; name: string };
421
+ type Metrics = { score: number };
422
+ type Summary = { name: string; score: number };
423
+
424
+ declare const fetchProfile: (id: string) => ResultAsync<Profile, Error>;
425
+ declare const fetchMetrics: (p: Profile) => ResultAsync<Metrics, Error>;
426
+ declare const formatSummary: (p: Profile, m: Metrics) => Summary;
427
+
428
+ const loadSummary = flow(
429
+ fetchProfile,
430
+ (ra) => ra.flatMap((profile) =>
431
+ fetchMetrics(profile).map((metrics) => formatSummary(profile, metrics))
432
+ ),
433
+ );
434
+ // (id: string) => ResultAsync<Summary, Error>
435
+ ```
436
+
437
+ ---
438
+
439
+ ## 13. Functional Railway pipelines (`railway` + named steps)
440
+
441
+ Use `railway(input, ...steps)` from `@onrails/result/railway` to build a multi-step async pipeline using point-free reusable step wrappers. If any step is async, the entire pipeline resolves to a `ResultAsync`.
442
+
443
+ ```ts
444
+ import { railway, parseWith, fromPromiseNamed, deriveNamed, select } from "@onrails/result/railway";
445
+ import { type ResultAsync } from "@onrails/result";
446
+
447
+ type Dashboard = { title: string };
448
+
449
+ type IdContext = { readonly id: string };
450
+ type ProfileContext = { readonly profile: Profile };
451
+ type TitleContext = { readonly title: string };
452
+
453
+ declare const IdSchema: { parse: (x: unknown) => string };
454
+ declare const fetchProfile: (id: string) => Promise<Profile>;
455
+ declare const toError: (e: unknown) => Error;
456
+
457
+ const loadDashboard = (rawId: unknown): ResultAsync<Dashboard, Error> =>
458
+ railway(
459
+ rawId,
460
+ parseWith(IdSchema, toError).as("id"),
461
+ fromPromiseNamed(
462
+ "profile",
463
+ ({ id }: IdContext) => fetchProfile(id),
464
+ toError,
465
+ ),
466
+ deriveNamed("title", ({ profile }: ProfileContext) =>
467
+ profile.name.toUpperCase(),
468
+ ),
469
+ select(({ title }: TitleContext) => ({ title })),
470
+ );
471
+ ```
472
+
473
+ ---
474
+
353
475
  ## When NOT to go point-free
354
476
 
355
477
  Pipelines should read top-to-bottom and each step should do one obvious thing. Reach for `pipe` when:
@@ -14,6 +14,7 @@ type CombineTupleAsync<R extends readonly ResultAsync<unknown, unknown>[]> = Res
14
14
  */
15
15
  declare class ResultAsync<T, E> {
16
16
  protected readonly run: PromiseFactory<T, E>;
17
+ private promise;
17
18
  protected constructor(run: PromiseFactory<T, E>);
18
19
  static fromResult<T, E>(result: Result<T, E>): ResultAsync<T, E>;
19
20
  static fromPromise<T, E>(promise: PromiseLike<T>, onReject: (error: unknown) => E): ResultAsync<T, E>;
@@ -43,6 +44,10 @@ declare class ResultAsync<T, E> {
43
44
  andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
44
45
  inner: Result<U, F>;
45
46
  }): ResultAsync<U, E | F>;
47
+ /**
48
+ * Alias for {@link flatMap}.
49
+ * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
50
+ */
46
51
  chain<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
47
52
  inner: Result<U, F>;
48
53
  }): ResultAsync<U, E | F>;
@@ -51,7 +56,15 @@ declare class ResultAsync<T, E> {
51
56
  tap(fn: (value: T) => void): ResultAsync<T, E>;
52
57
  tapErr(fn: (error: E) => void): ResultAsync<T, E>;
53
58
  unwrapOr<U>(defaultValue: U): Promise<T | U>;
59
+ /**
60
+ * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
61
+ * re-executes deferred factories and cannot narrow.
62
+ */
54
63
  isOk(): Promise<boolean>;
64
+ /**
65
+ * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
66
+ * re-executes deferred factories and cannot narrow.
67
+ */
55
68
  isErr(): Promise<boolean>;
56
69
  match<U1, U2 = U1>(onOk: (value: T) => U1, onErr: (error: E) => U2): Promise<U1 | U2>;
57
70
  resolve(): Promise<Result<T, E>>;
@@ -107,6 +120,7 @@ declare const fromSafePromise: typeof ResultAsync.fromSafePromise;
107
120
  * const combined = sequenceTupleAsync([loadCfg(), loadCatalog()] as const);
108
121
  * // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
109
122
  * ```
123
+ * @deprecated Use static {@link ResultAsync.combineTuple} instead.
110
124
  */
111
125
  declare const sequenceTupleAsync: typeof ResultAsync.combineTuple;
112
126
  /**
@@ -14,6 +14,7 @@ type CombineTupleAsync<R extends readonly ResultAsync<unknown, unknown>[]> = Res
14
14
  */
15
15
  declare class ResultAsync<T, E> {
16
16
  protected readonly run: PromiseFactory<T, E>;
17
+ private promise;
17
18
  protected constructor(run: PromiseFactory<T, E>);
18
19
  static fromResult<T, E>(result: Result<T, E>): ResultAsync<T, E>;
19
20
  static fromPromise<T, E>(promise: PromiseLike<T>, onReject: (error: unknown) => E): ResultAsync<T, E>;
@@ -43,6 +44,10 @@ declare class ResultAsync<T, E> {
43
44
  andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
44
45
  inner: Result<U, F>;
45
46
  }): ResultAsync<U, E | F>;
47
+ /**
48
+ * Alias for {@link flatMap}.
49
+ * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
50
+ */
46
51
  chain<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
47
52
  inner: Result<U, F>;
48
53
  }): ResultAsync<U, E | F>;
@@ -51,7 +56,15 @@ declare class ResultAsync<T, E> {
51
56
  tap(fn: (value: T) => void): ResultAsync<T, E>;
52
57
  tapErr(fn: (error: E) => void): ResultAsync<T, E>;
53
58
  unwrapOr<U>(defaultValue: U): Promise<T | U>;
59
+ /**
60
+ * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
61
+ * re-executes deferred factories and cannot narrow.
62
+ */
54
63
  isOk(): Promise<boolean>;
64
+ /**
65
+ * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
66
+ * re-executes deferred factories and cannot narrow.
67
+ */
55
68
  isErr(): Promise<boolean>;
56
69
  match<U1, U2 = U1>(onOk: (value: T) => U1, onErr: (error: E) => U2): Promise<U1 | U2>;
57
70
  resolve(): Promise<Result<T, E>>;
@@ -107,6 +120,7 @@ declare const fromSafePromise: typeof ResultAsync.fromSafePromise;
107
120
  * const combined = sequenceTupleAsync([loadCfg(), loadCatalog()] as const);
108
121
  * // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
109
122
  * ```
123
+ * @deprecated Use static {@link ResultAsync.combineTuple} instead.
110
124
  */
111
125
  declare const sequenceTupleAsync: typeof ResultAsync.combineTuple;
112
126
  /**
@@ -39,7 +39,7 @@ function trySync(fn, onThrow) {
39
39
  }
40
40
  };
41
41
  }
42
- var combineTuple = (results) => {
42
+ var combine = (results) => {
43
43
  const values = [];
44
44
  for (const result of results) {
45
45
  if (isErr(result)) return err(result.error);
@@ -47,6 +47,10 @@ var combineTuple = (results) => {
47
47
  }
48
48
  return ok(values);
49
49
  };
50
+ var combineTuple = (results) => (
51
+ // Runtime identical to combine; the cast restores per-index tuple types.
52
+ combine(results)
53
+ );
50
54
 
51
55
  // src/types.ts
52
56
  var UnexpectedError = class extends Error {
@@ -65,22 +69,27 @@ var liftPromiseResult = async (promise, onDefect) => {
65
69
  if (isErr(result)) {
66
70
  return result;
67
71
  }
68
- const inner = result.value;
69
- if (inner && typeof inner === "object" && "_tag" in inner) {
70
- if (inner._tag === "Ok" || inner._tag === "Err") {
71
- return inner;
72
- }
73
- }
74
- return result;
72
+ return isResultLike(result.value) ? result.value : result;
75
73
  } catch (error) {
76
74
  return err(onDefect(error));
77
75
  }
78
76
  };
77
+ var sequenceSettled = (settled) => {
78
+ const values = [];
79
+ for (const result of settled) {
80
+ if (isErr(result)) {
81
+ return err(result.error);
82
+ }
83
+ values.push(result.value);
84
+ }
85
+ return ok(values);
86
+ };
79
87
  var ResultAsync = class _ResultAsync {
80
88
  constructor(run) {
81
89
  this.run = run;
82
90
  }
83
91
  run;
92
+ promise = null;
84
93
  static fromResult(result) {
85
94
  return new _ResultAsync(async () => result);
86
95
  }
@@ -128,46 +137,30 @@ var ResultAsync = class _ResultAsync {
128
137
  });
129
138
  }
130
139
  static combineTuple(results) {
131
- return new _ResultAsync(async () => {
132
- const values = [];
133
- for (const ra of results) {
134
- const result = await ra.resolve();
135
- if (isErr(result)) {
136
- return err(result.error);
137
- }
138
- values.push(result.value);
139
- }
140
- return ok(values);
141
- });
140
+ return _ResultAsync.combine(
141
+ results
142
+ );
142
143
  }
143
144
  /**
144
145
  * Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
145
146
  * parallel for independent IO). On failure, returns the first `Err` in input order.
146
147
  */
147
148
  static combineTupleParallel(results) {
148
- return new _ResultAsync(async () => {
149
- const settled = await Promise.all(results.map((ra) => ra.resolve()));
150
- const values = [];
151
- for (const result of settled) {
152
- if (isErr(result)) {
153
- return err(result.error);
154
- }
155
- values.push(result.value);
156
- }
157
- return ok(values);
158
- });
149
+ return new _ResultAsync(
150
+ async () => sequenceSettled(await Promise.all(results.map((ra) => ra.resolve())))
151
+ );
159
152
  }
160
153
  map(fn) {
161
- return new _ResultAsync(async () => map(await this.run(), fn));
154
+ return new _ResultAsync(async () => map(await this.resolve(), fn));
162
155
  }
163
156
  mapErr(fn) {
164
- return new _ResultAsync(async () => mapErr(await this.run(), fn));
157
+ return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
165
158
  }
166
159
  flatMap(fn) {
167
160
  return new _ResultAsync(async () => {
168
- const first = await this.run();
161
+ const first = await this.resolve();
169
162
  if (isErr(first)) {
170
- return err(first.error);
163
+ return first;
171
164
  }
172
165
  const next = fn(first.value);
173
166
  if (next instanceof _ResultAsync) {
@@ -176,29 +169,27 @@ var ResultAsync = class _ResultAsync {
176
169
  if (isResultLike(next)) {
177
170
  return next;
178
171
  }
179
- if (isCompatLike(next)) {
180
- return next.inner;
181
- }
182
- return next;
172
+ return isCompatLike(next) ? next.inner : next;
183
173
  });
184
174
  }
185
175
  andThen(fn) {
186
176
  return this.flatMap(fn);
187
177
  }
178
+ /**
179
+ * Alias for {@link flatMap}.
180
+ * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
181
+ */
188
182
  chain(fn) {
189
183
  return this.flatMap(fn);
190
184
  }
191
185
  recover(fn) {
192
186
  return new _ResultAsync(async () => {
193
- const first = await this.run();
187
+ const first = await this.resolve();
194
188
  if (!isErr(first)) {
195
189
  return first;
196
190
  }
197
191
  const next = fn(first.error);
198
- if (next instanceof _ResultAsync) {
199
- return next.resolve();
200
- }
201
- return next;
192
+ return next instanceof _ResultAsync ? next.resolve() : next;
202
193
  });
203
194
  }
204
195
  orElse(fn) {
@@ -206,7 +197,7 @@ var ResultAsync = class _ResultAsync {
206
197
  }
207
198
  tap(fn) {
208
199
  return new _ResultAsync(async () => {
209
- const result = await this.run();
200
+ const result = await this.resolve();
210
201
  if (!isErr(result)) {
211
202
  fn(result.value);
212
203
  }
@@ -215,7 +206,7 @@ var ResultAsync = class _ResultAsync {
215
206
  }
216
207
  tapErr(fn) {
217
208
  return new _ResultAsync(async () => {
218
- const result = await this.run();
209
+ const result = await this.resolve();
219
210
  if (isErr(result)) {
220
211
  fn(result.error);
221
212
  }
@@ -223,21 +214,32 @@ var ResultAsync = class _ResultAsync {
223
214
  });
224
215
  }
225
216
  unwrapOr(defaultValue) {
226
- return this.run().then((result) => isErr(result) ? defaultValue : result.value);
217
+ return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
227
218
  }
219
+ /**
220
+ * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
221
+ * re-executes deferred factories and cannot narrow.
222
+ */
228
223
  isOk() {
229
- return this.run().then((result) => !isErr(result));
224
+ return this.resolve().then((result) => !isErr(result));
230
225
  }
226
+ /**
227
+ * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
228
+ * re-executes deferred factories and cannot narrow.
229
+ */
231
230
  isErr() {
232
- return this.run().then((result) => isErr(result));
231
+ return this.resolve().then((result) => isErr(result));
233
232
  }
234
233
  match(onOk, onErr) {
235
- return this.run().then(
234
+ return this.resolve().then(
236
235
  (result) => isErr(result) ? onErr(result.error) : onOk(result.value)
237
236
  );
238
237
  }
239
238
  resolve() {
240
- return this.run();
239
+ if (!this.promise) {
240
+ this.promise = this.run();
241
+ }
242
+ return this.promise;
241
243
  }
242
244
  /**
243
245
  * Thenable shim — `await ra` resolves to a bare tagged-union `Result<T, E>`.
@@ -245,7 +247,8 @@ var ResultAsync = class _ResultAsync {
245
247
  */
246
248
  // biome-ignore lint/suspicious/noThenProperty: makes ResultAsync awaitable
247
249
  then(onfulfilled, onrejected) {
248
- return this.run().then(
250
+ return this.resolve().then(
251
+ // Safe: without onfulfilled, R1 stays at its Result<T, E> default.
249
252
  (r) => onfulfilled ? onfulfilled(r) : r,
250
253
  onrejected ?? void 0
251
254
  );
@@ -298,7 +301,7 @@ var CompatResult = class _CompatResult {
298
301
  if (isErr(this.inner)) {
299
302
  return CompatResultAsync.err(this.inner.error);
300
303
  }
301
- return CompatResultAsync.fromInner(coerceToCore(fn(this.inner.value)));
304
+ return CompatResultAsync.fromInner(coerceToCoreAsync(fn(this.inner.value)));
302
305
  }
303
306
  orElse(fn) {
304
307
  if (isOk(this.inner)) {
@@ -376,9 +379,7 @@ var CompatResultAsync = class _CompatResultAsync {
376
379
  return new _CompatResultAsync(this.inner.mapErr(fn));
377
380
  }
378
381
  andThen(fn) {
379
- return new _CompatResultAsync(
380
- this.inner.andThen((value) => coerceToCore(fn(value)))
381
- );
382
+ return new _CompatResultAsync(this.inner.andThen((value) => coerceToCore(fn(value))));
382
383
  }
383
384
  chain(fn) {
384
385
  return this.andThen(fn);
@@ -406,18 +407,31 @@ var CompatResultAsync = class _CompatResultAsync {
406
407
  return this.inner.match(onOk, onErr);
407
408
  }
408
409
  andTee(fn) {
409
- return this.andThen(
410
- (value) => _CompatResultAsync.fromSafePromise(Promise.resolve(fn(value)).then(() => value))
410
+ return new _CompatResultAsync(
411
+ ResultAsync.defer(async () => {
412
+ const r = await this.inner.resolve();
413
+ if (!isErr(r)) {
414
+ try {
415
+ await fn(r.value);
416
+ } catch {
417
+ }
418
+ }
419
+ return r;
420
+ })
411
421
  );
412
422
  }
413
423
  orTee(fn) {
414
424
  return new _CompatResultAsync(
415
- ResultAsync.fromResultPromise(
416
- this.inner.resolve().then(async (r) => {
417
- if (isErr(r)) await fn(r.error);
418
- return r;
419
- })
420
- )
425
+ ResultAsync.defer(async () => {
426
+ const r = await this.inner.resolve();
427
+ if (isErr(r)) {
428
+ try {
429
+ await fn(r.error);
430
+ } catch {
431
+ }
432
+ }
433
+ return r;
434
+ })
421
435
  );
422
436
  }
423
437
  };
@@ -427,6 +441,7 @@ function coerceToCore(next) {
427
441
  if (next instanceof CompatResult) return next.inner;
428
442
  return next;
429
443
  }
444
+ var coerceToCoreAsync = (next) => next instanceof CompatResultAsync ? next.toCore() : next;
430
445
  var okAsync2 = CompatResultAsync.ok;
431
446
  var errAsync2 = CompatResultAsync.err;
432
447
  var fromPromise2 = CompatResultAsync.fromPromise;