@onrails/result 0.1.2 → 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 (50) 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 +30 -12
  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 +30 -12
  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 +30 -12
  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 +30 -12
  27. package/dist/index.js.map +1 -1
  28. package/dist/interop.cjs +30 -12
  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 +30 -12
  33. package/dist/interop.js.map +1 -1
  34. package/dist/railway.cjs +30 -12
  35. package/dist/railway.cjs.map +1 -1
  36. package/dist/railway.d.cts +1 -1
  37. package/dist/railway.d.ts +1 -1
  38. package/dist/railway.js +30 -12
  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.map +1 -1
  43. package/dist/validation.js.map +1 -1
  44. package/package.json +1 -6
  45. package/dist/mcp.cjs +0 -273
  46. package/dist/mcp.cjs.map +0 -1
  47. package/dist/mcp.d.cts +0 -62
  48. package/dist/mcp.d.ts +0 -62
  49. package/dist/mcp.js +0 -265
  50. 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
  /**
@@ -89,6 +89,7 @@ var ResultAsync = class _ResultAsync {
89
89
  this.run = run;
90
90
  }
91
91
  run;
92
+ promise = null;
92
93
  static fromResult(result) {
93
94
  return new _ResultAsync(async () => result);
94
95
  }
@@ -150,14 +151,14 @@ var ResultAsync = class _ResultAsync {
150
151
  );
151
152
  }
152
153
  map(fn) {
153
- return new _ResultAsync(async () => map(await this.run(), fn));
154
+ return new _ResultAsync(async () => map(await this.resolve(), fn));
154
155
  }
155
156
  mapErr(fn) {
156
- return new _ResultAsync(async () => mapErr(await this.run(), fn));
157
+ return new _ResultAsync(async () => mapErr(await this.resolve(), fn));
157
158
  }
158
159
  flatMap(fn) {
159
160
  return new _ResultAsync(async () => {
160
- const first = await this.run();
161
+ const first = await this.resolve();
161
162
  if (isErr(first)) {
162
163
  return first;
163
164
  }
@@ -174,12 +175,16 @@ var ResultAsync = class _ResultAsync {
174
175
  andThen(fn) {
175
176
  return this.flatMap(fn);
176
177
  }
178
+ /**
179
+ * Alias for {@link flatMap}.
180
+ * @deprecated Use {@link flatMap} or keep/use compat {@link andThen} instead.
181
+ */
177
182
  chain(fn) {
178
183
  return this.flatMap(fn);
179
184
  }
180
185
  recover(fn) {
181
186
  return new _ResultAsync(async () => {
182
- const first = await this.run();
187
+ const first = await this.resolve();
183
188
  if (!isErr(first)) {
184
189
  return first;
185
190
  }
@@ -192,7 +197,7 @@ var ResultAsync = class _ResultAsync {
192
197
  }
193
198
  tap(fn) {
194
199
  return new _ResultAsync(async () => {
195
- const result = await this.run();
200
+ const result = await this.resolve();
196
201
  if (!isErr(result)) {
197
202
  fn(result.value);
198
203
  }
@@ -201,7 +206,7 @@ var ResultAsync = class _ResultAsync {
201
206
  }
202
207
  tapErr(fn) {
203
208
  return new _ResultAsync(async () => {
204
- const result = await this.run();
209
+ const result = await this.resolve();
205
210
  if (isErr(result)) {
206
211
  fn(result.error);
207
212
  }
@@ -209,19 +214,32 @@ var ResultAsync = class _ResultAsync {
209
214
  });
210
215
  }
211
216
  unwrapOr(defaultValue) {
212
- return this.run().then((result) => isErr(result) ? defaultValue : result.value);
217
+ return this.resolve().then((result) => isErr(result) ? defaultValue : result.value);
213
218
  }
219
+ /**
220
+ * @deprecated Await the ResultAsync and narrow with {@link isOk} — this method
221
+ * re-executes deferred factories and cannot narrow.
222
+ */
214
223
  isOk() {
215
- return this.run().then((result) => !isErr(result));
224
+ return this.resolve().then((result) => !isErr(result));
216
225
  }
226
+ /**
227
+ * @deprecated Await the ResultAsync and narrow with {@link isErr} — this method
228
+ * re-executes deferred factories and cannot narrow.
229
+ */
217
230
  isErr() {
218
- return this.run().then((result) => isErr(result));
231
+ return this.resolve().then((result) => isErr(result));
219
232
  }
220
233
  match(onOk, onErr) {
221
- return this.run().then((result) => isErr(result) ? onErr(result.error) : onOk(result.value));
234
+ return this.resolve().then(
235
+ (result) => isErr(result) ? onErr(result.error) : onOk(result.value)
236
+ );
222
237
  }
223
238
  resolve() {
224
- return this.run();
239
+ if (!this.promise) {
240
+ this.promise = this.run();
241
+ }
242
+ return this.promise;
225
243
  }
226
244
  /**
227
245
  * Thenable shim — `await ra` resolves to a bare tagged-union `Result<T, E>`.
@@ -229,7 +247,7 @@ var ResultAsync = class _ResultAsync {
229
247
  */
230
248
  // biome-ignore lint/suspicious/noThenProperty: makes ResultAsync awaitable
231
249
  then(onfulfilled, onrejected) {
232
- return this.run().then(
250
+ return this.resolve().then(
233
251
  // Safe: without onfulfilled, R1 stays at its Result<T, E> default.
234
252
  (r) => onfulfilled ? onfulfilled(r) : r,
235
253
  onrejected ?? void 0