resultar 3.10.0 → 4.0.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.
@@ -7,6 +7,7 @@ versions and local README files before copying an example.
7
7
 
8
8
  - [Choose A Request Package](#choose-a-request-package)
9
9
  - [Use The Schema-Agnostic Core](#use-the-schema-agnostic-core)
10
+ - [Compose Requests In Tasks](#compose-requests-in-tasks)
10
11
  - [Use The TypeBox Adapter](#use-the-typebox-adapter)
11
12
  - [Use The Zod Adapter](#use-the-zod-adapter)
12
13
  - [Map Request Errors](#map-request-errors)
@@ -27,6 +28,10 @@ versions and local README files before copying an example.
27
28
  All three share the same transport implementation. The adapters add schema validation and re-export
28
29
  the public `resultar-request` types. Install only the core and schema adapter the service needs.
29
30
 
31
+ Their `requestJson` helpers return eager `ResultAsync` from `resultar`. Import in-flight constructors, helpers, or types from `resultar` in application code.
32
+ Calling a helper starts its request; the examples below document that real API. New workflows can
33
+ compose these adapters lazily through the Task factory bridge without changing the request contract.
34
+
30
35
  Keep request payloads concrete. Do not widen a known response contract to `JsonRecord`,
31
36
  `Record<string, unknown>`, or `unknown` after validation.
32
37
 
@@ -56,6 +61,36 @@ const user = requestJson({
56
61
  Use `decode` when validation also normalizes or transforms the response. The decoder must make
57
62
  success and validation failure explicit; check the installed package types for the exact shape.
58
63
 
64
+ ## Compose Requests In Tasks
65
+
66
+ Create the eager helper inside `fromResultAsync`, forwarding the Task signal to fetch:
67
+
68
+ ```ts
69
+ import { ResultTask } from "resultar";
70
+ import { requestJson } from "resultar-request";
71
+
72
+ const loadUser = (userId: string) =>
73
+ ResultTask.fromResultAsync((signal) =>
74
+ requestJson({
75
+ request: () => fetch(`https://example.com/users/${userId}`, { signal }),
76
+ validator: isUser,
77
+ }),
78
+ );
79
+
80
+ const userId = await loadUser("user-1")
81
+ .map((user) => user.id)
82
+ .runPromise();
83
+ ```
84
+
85
+ This reuses the `isUser` guard above and infers `ResultTask<User, RequestError>`. Each run creates
86
+ a fresh eager request. The same bridge works with the TypeBox and Zod helpers. Keep `map`
87
+ synchronous, use `andThen` for another async Task, and compose `match` / `matchTags` before the
88
+ runner when turning the response into a transport value. Use `runExit` when the boundary must
89
+ inspect interruption, defects, and composite cleanup causes as well as typed errors.
90
+
91
+ Passing `requestJson(...)` directly to `fromResultAsync` captures an already-started request;
92
+ it cannot make that execution lazy or repeatable.
93
+
59
94
  ## Use The TypeBox Adapter
60
95
 
61
96
  ```ts
@@ -174,9 +209,9 @@ const user = requestJsonZod({
174
209
  The default policy retries request failures and HTTP `5xx`, but not `4xx`, invalid JSON, or
175
210
  validation failures. Retry requires a request factory; an already-created promise cannot be
176
211
  restarted. Ensure each attempt receives a fresh request. The current request retry configuration
177
- does not expose ResultAsync's internal retry signal, so do not claim that `requestJson`
212
+ does not expose the eager Async policy's internal retry signal, so do not claim that `requestJson`
178
213
  automatically forwards policy cancellation. Supply an external fetch signal through the request
179
- closure when the caller owns separate cancellation.
214
+ closure when the caller owns cancellation, including the signal supplied by a Task bridge.
180
215
 
181
216
  ## Validate In The Preferred Order
182
217
 
@@ -245,15 +280,20 @@ When an agent encounters a diagnostic, fix the architecture first:
245
280
  - handle, compose, return, or intentionally discard a returned Resultar value; a discard does not
246
281
  run a lazy task and must not be applied just to silence diagnostics;
247
282
  - replace nested `Result` from `map` with `andThen`;
248
- - replace `Promise<Result>` / `Promise<StrictResult>` contracts with `ResultAsync` /
249
- `StrictResultAsync` and return composed operations directly (`prefer-result-async`);
250
- - enable `preferResultAsyncMode: "all"` with `preferResultAsync: "error"` to also catch raw
251
- `Promise<T>` repository/service contracts and inferred returns; map driver errors in adapters
252
- and document local suppressions for required native framework or conversion callbacks;
283
+ - for existing eager contracts, replace `Promise<Result>` / `Promise<StrictResult>` with
284
+ `ResultAsync` / `StrictResultAsync` and return composed operations directly
285
+ (`prefer-result-async`); the rule name remains stable for that contract;
286
+ - scope `preferResultAsync` to an intentional eager-boundary migration. Its `"all"` mode also
287
+ catches raw `Promise<T>` contracts and inferred returns. It does not make Task and Async
288
+ interchangeable or prescribe Async for new services. A new Task contract needs a factory,
289
+ Task composition, and explicit callers/runners; document native framework conversion boundaries;
253
290
  - replace error-only recovery with `mapErr`;
254
- - replace project-wide `try/catch` with `tryResult` or `tryResultAsync` when `noTryCatch` is enabled;
291
+ - replace project-wide `try/catch` with a narrow adapter when `noTryCatch` is enabled: `tryResult`
292
+ for sync work, `ResultTask.tryPromise` for new lazy work, or `tryResultAsync` for eager compatibility;
255
293
  - wrap raw promise awaits in Resultar contexts;
256
- - use `yield*` and avoid `try/catch` inside `Result.gen` / `safeTry`;
294
+ - use `yield*` in generators. Modern `Result.gen` / `Result.fn` return plain success values; deprecated
295
+ `safeTry` returns a final Result. Both support sync and async bodies (with async bodies returning eager `ResultAsync`). Use `ResultTask.gen`
296
+ for new lazy async work and bridge eager helpers inside it explicitly;
257
297
  - preserve inferred error and task requirements channels instead of asserting them away;
258
298
  - remove obsolete suppression entries without suppressing their diagnostic broadly.
259
299
 
@@ -1,6 +1,8 @@
1
1
  # Resultar Implementation Patterns
2
2
 
3
- Use these patterns as starting points, then align names and types with the consuming domain.
3
+ Use these v4 patterns as starting points, then align names and types with the consuming domain.
4
+ New async recipes use lazy, nonthenable `ResultTask` from `resultar`. Use `ResultAsync` for
5
+ in-flight Results and preserve eager `ResultAsync` boundaries (and compatibility helpers exported from `resultar`) where callers require eager execution.
4
6
 
5
7
  ## Contents
6
8
 
@@ -14,6 +16,7 @@ Use these patterns as starting points, then align names and types with the consu
14
16
  - [Recover Locally And Match At Boundaries](#recover-locally-and-match-at-boundaries)
15
17
  - [Add Observability](#add-observability)
16
18
  - [Migrate Incrementally](#migrate-incrementally)
19
+ - [Preserve An Eager Compatibility Contract](#preserve-an-eager-compatibility-contract)
17
20
  - [Test Runtime And Type Behavior](#test-runtime-and-type-behavior)
18
21
 
19
22
  ## Model A Domain Failure
@@ -39,33 +42,43 @@ boundary. Keep strings, enums, or `TaggedEnum` variants for intentionally local
39
42
  ```ts
40
43
  type CreateUserError = InvalidEmailError | UserAlreadyExistsError | SaveUserError;
41
44
 
42
- const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
43
- validateEmail(input.email)
44
- .asyncAndThen(ensureEmailAvailable)
45
+ const createUser = (input: CreateUserInput): ResultTask<User, CreateUserError> =>
46
+ ResultTask.fromResult(validateEmail(input.email))
47
+ .andThen(ensureEmailAvailable)
45
48
  .andThen((email) => saveUser({ ...input, email }));
46
49
  ```
47
50
 
48
- - Use `map` only for an infallible value transform.
51
+ - `ensureEmailAvailable` and `saveUser` return Tasks. Use `map` only for a synchronous value transform;
52
+ `ResultTask.map(asyncCallback)` stores a Promise as the success value.
49
53
  - Use `filterOrElse` for predicate validation.
50
54
  - Use `andThen` when the next operation can fail.
51
- - Use `asyncAndThen` only for the sync-to-async bridge.
55
+ - Use `ResultTask.fromResult` to enter a lazy workflow from a synchronous Result.
56
+ `Result.asyncAndThen(result, callback)` (or `.asyncAndThen(callback)`) bridges into the eager `ResultAsync` compatibility model.
52
57
  - Keep each function's error type narrow and allow the final union to be inferred.
53
58
 
54
59
  ## Write A Linear Workflow
55
60
 
56
61
  ```ts
57
- const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
58
- Result.gen(async function* () {
62
+ const createUser = (input: CreateUserInput): ResultTask<User, CreateUserError> =>
63
+ ResultTask.gen(function* () {
59
64
  const email = yield* validateEmail(input.email);
60
65
  yield* ensureEmailAvailable(email);
61
66
  const user = yield* saveUser({ ...input, email });
62
67
 
63
- return ok(user);
68
+ return user;
64
69
  });
65
70
  ```
66
71
 
67
- Use `yield*` for every fallible Resultar step. Wrap an uncontrolled promise with
68
- `tryResultAsync` before yielding it. Do not mix raw `await` or broad `try/catch` into the generator.
72
+ Use `yield*` for every fallible step and return the plain success value. This generator can compose
73
+ Tasks, synchronous Results, and service tags. Wrap uncontrolled Promise factories with
74
+ `ResultTask.tryPromise`; adapt an eager adapter inside `ResultTask.fromResultAsync(factory)`.
75
+ `return yield* finalStep()` executes the last step; `return finalStep()` leaves its Task as a value.
76
+ Execute the workflow explicitly at its boundary.
77
+
78
+ For a wholly synchronous linear flow, use `Result.gen(function* () { ... })` with the same
79
+ plain-value return convention. `Result.gen` / `Result.fn` also support eager async bodies;
80
+ `safeTry` supports both forms with final-Result returns. The legacy `safeTry` contract returns a
81
+ final Result; it is not an alias of modern plain-value `Result.gen`.
69
82
 
70
83
  ## Wrap External Work
71
84
 
@@ -77,35 +90,39 @@ class ProfileApiError extends createTaggedError({
77
90
 
78
91
  type LoadProfileOptions = {
79
92
  readonly attempt?: number;
80
- readonly signal?: AbortSignal;
81
93
  };
82
94
 
83
95
  const loadProfile = (
84
96
  userId: string,
85
97
  options: LoadProfileOptions = {},
86
- ): StrictResultAsync<Profile, ProfileApiError> =>
87
- tryResultAsync(
88
- () => profileClient.load(userId, options),
89
- (cause) => new ProfileApiError({ cause, userId }),
90
- );
98
+ ): ResultTask<Profile, ProfileApiError> =>
99
+ ResultTask.tryPromise({
100
+ try: (signal) => profileClient.load(userId, { ...options, signal }),
101
+ catch: (cause) => new ProfileApiError({ cause, userId }),
102
+ });
91
103
  ```
92
104
 
93
- Prefer a promise factory because creating the promise may throw synchronously. Use
94
- `fromPromise(existingPromise, mapper)` only when the promise already exists, and use
95
- `fromSafePromise` only when non-rejection is guaranteed by contract.
105
+ The Promise factory runs on each Task execution, catching both synchronous throws and rejections.
106
+ Pass its signal into the client. Building a Task around an already-created Promise cannot defer or
107
+ repeat that Promise's work.
96
108
 
97
- For callback or subscription APIs, use `fromCallback` and return cleanup from `subscribe`:
109
+ Deprecated `fromCallback` returns eager `ResultAsync`. Invoke it inside a Task factory and return
110
+ cleanup from `subscribe`:
98
111
 
99
112
  ```ts
100
- const message = fromCallback<Message, SubscriptionError>({
101
- catch: (cause) => new SubscriptionError({ cause, topic }),
102
- signal,
103
- subscribe: ({ resolve, reject }) => {
104
- const subscription = client.subscribe(topic, resolve, reject);
105
-
106
- return () => subscription.close();
107
- },
108
- });
113
+ import { fromCallback } from "resultar";
114
+
115
+ const message = ResultTask.fromResultAsync((signal) =>
116
+ fromCallback<Message, SubscriptionError>({
117
+ catch: (cause) => new SubscriptionError({ cause, topic }),
118
+ signal,
119
+ subscribe: ({ resolve, reject }) => {
120
+ const subscription = client.subscribe(topic, resolve, reject);
121
+
122
+ return () => subscription.close();
123
+ },
124
+ }),
125
+ );
109
126
  ```
110
127
 
111
128
  Check the installed `ResultAsyncFromCallbackOptions` type for exact client integration details.
@@ -113,72 +130,135 @@ Check the installed `ResultAsyncFromCallbackOptions` type for exact client integ
113
130
  ## Apply Retry And Timeout Policy
114
131
 
115
132
  ```ts
116
- const profile = ResultAsync.timeout(
117
- (signal) =>
118
- ResultAsync.retry(
119
- (attempt, retrySignal) => loadProfile(userId, { attempt, signal: retrySignal }),
120
- {
121
- times: 2,
122
- delayMs: ({ nextAttempt }) => nextAttempt * 100,
123
- jittered: 0.2,
124
- while: (error) => error._tag === "ProfileApiError",
125
- signal,
126
- },
127
- ),
128
- {
129
- timeoutMs: 1_500,
130
- onTimeout: () => new ProfileTimeoutError({ timeoutMs: 1_500, userId }),
131
- },
132
- );
133
+ const profile = loadProfile(userId)
134
+ .retry(
135
+ Retry.policy<ProfileApiError>({
136
+ times: 2,
137
+ delayMs: ({ nextAttempt }) => nextAttempt * 100,
138
+ jittered: 0.2,
139
+ while: (error) => error._tag === "ProfileApiError",
140
+ }),
141
+ )
142
+ .timeout(1_500, () => new ProfileTimeoutError({ timeoutMs: 1_500, userId }));
133
143
  ```
134
144
 
135
145
  Forward the supplied signal into the underlying operation. Confirm whether the desired timeout
136
146
  covers each attempt or the complete retry budget, then nest the policies accordingly. Use
137
- `retryOrElse` only when fallback after exhaustion is domain policy.
147
+ `orElse` after retry only when fallback after exhaustion is domain policy. `retryOrElse` remains an
148
+ Async compatibility helper; it is not a Task method.
149
+
150
+ For a policy reused across Task workflows and an existing eager contract, construct the portable
151
+ contract explicitly:
152
+
153
+ ```ts
154
+ import { ResultAsync, Retry } from "resultar";
155
+
156
+ const retryPolicy = Retry.policy({
157
+ times: 2,
158
+ delayMs: 100,
159
+ backoff: 2,
160
+ jittered: 0.2,
161
+ });
162
+
163
+ const lazy = loadProfile(userId).retry(retryPolicy);
164
+ // Existing eager adapter; loadProfileAsync still returns an eager ResultAsync.
165
+ const eager = ResultAsync.retry(
166
+ (attempt, signal) => loadProfileAsync(userId, { attempt, signal }),
167
+ { ...retryPolicy, signal: requestSignal },
168
+ );
169
+ ```
170
+
171
+ Both use zero-based context in `delayMs`, `while`, and `onRetry` callbacks; `times` counts retries
172
+ after the initial attempt. With `times: 2`, the first failure has two retries remaining. Canonical
173
+ `while` is called only when another retry can run, and `onRetry` is awaited but best-effort. Reuse
174
+ shares configuration, not progress. Task's older options remain supported: its callback retry index
175
+ is one-based and `jittered: true` samples between zero and the delay. Eager Async options have their
176
+ own callback arguments and jitter behavior; use the portable policy when sharing configuration.
177
+
178
+ For a task adapter, choose how interruption handles an uncooperative promise:
179
+
180
+ ```ts
181
+ const read = ResultTask.tryPromise({
182
+ try: (signal) => client.read(signal),
183
+ catch: (cause) => new ReadError({ cause }),
184
+ interruption: "detach",
185
+ });
186
+ ```
187
+
188
+ The default is `"wait"`. `"detach"` allows interruption to finish without waiting for the promise,
189
+ while registered scope finalizers are still awaited. It does not stop the client. Keep resource
190
+ acquisition on `"wait"` when its completion must precede release; a detached adapter owns anything
191
+ it acquires after the task has stopped waiting.
192
+
193
+ At execution, configure cooperative yields when a long instruction chain must share the event loop:
194
+
195
+ ```ts
196
+ const exit = await lazy.runExit({ yieldEvery: 256 });
197
+ ```
198
+
199
+ The default is 2048 instructions with `Scheduler.live`. A custom `TaskScheduler` receives the
200
+ execution signal in `yield(signal)`. `Infinity` disables automatic yields. This does not interrupt
201
+ a synchronous client callback midway; it is separate from `clock` and retry timing.
138
202
 
139
203
  Choose races deliberately:
140
204
 
141
- - `race` / `raceAll`: first settled `Ok` or `Err` wins.
142
- - `raceFirst`: first `Ok` wins; both must fail before an `Err` is returned.
143
- - `raceWith`: winner-specific logic needs access to the still-running task.
205
+ - `ResultTask.race(left, right)`: first success wins; if both fail, preserve their causes in `Parallel`.
206
+ - `ResultTask.raceFirst(left, right)`: first completed outcome wins.
207
+
208
+ Task races and timeouts own their children and await loser cleanup. Existing Async `race` /
209
+ `raceAll` return the first `Ok` or the last completed `Err` if all fail; `raceFirst` returns the first
210
+ Result, and `raceWith` exposes a handle for custom policy. Those eager races and timeouts abort
211
+ pending losers without waiting for their settlement or cleanup.
212
+
213
+ For a scope-owned child, use `Fiber.await(child)` to observe its full Exit without requesting
214
+ interruption. Use `Fiber.join(child)` to compose its result, and `Fiber.interrupt(child)` to request
215
+ cancellation and await cleanup. Keep the parent scope open until child consumers finish.
144
216
 
145
217
  ## Process Batches
146
218
 
147
219
  Stop at the first error with bounded concurrency:
148
220
 
149
221
  ```ts
150
- const saved = ResultAsync.forEach(rows, saveRow, { concurrency: 8 });
222
+ const saved = ResultTask.forEach(rows, saveRow, { concurrency: 8 });
151
223
  ```
152
224
 
153
225
  Collect all independent errors instead:
154
226
 
155
227
  ```ts
156
- const validated = ResultAsync.validateAll(rows, validateRow, { concurrency: 8 });
228
+ const validated = ResultTask.validateAll(rows, validateRow, { concurrency: 8 });
157
229
  ```
158
230
 
159
- Use `{ discard: true }` with `forEach` when successful values are intentionally unused. Do not
160
- silently discard the returned `ResultAsync` itself.
231
+ The mappers return Tasks; lift a synchronous validation with `ResultTask.fromResult` when needed.
232
+ Use `.asVoid()` on the returned Task when its successful array is intentionally unused; the
233
+ `{ discard: true }` option belongs to Result and Async collections. The Task still needs composition
234
+ or an explicit runner; discarding it does not run the batch.
161
235
 
162
236
  ## Acquire And Release Resources
163
237
 
164
238
  ```ts
165
- const imported = ResultAsync.withResource({
166
- acquire: (signal) => openImportSession({ signal }),
167
- use: (session, signal) => importRows(session, rows, { signal }),
168
- release: (session) => session.close(),
169
- });
239
+ const imported = ResultTask.scoped(
240
+ ResultTask.gen(function* () {
241
+ const session = yield* ResultTask.acquireRelease({
242
+ acquire: openImportSession(),
243
+ release: (resource) => resource.close(),
244
+ });
245
+
246
+ return yield* importRows(session, rows);
247
+ }),
248
+ );
170
249
  ```
171
250
 
172
- Release runs after every successful acquire path, but release failures are best-effort. If cleanup
173
- failure must affect the returned result, include that operation in `use` or otherwise model it as a
174
- typed Resultar step.
251
+ Here `openImportSession`, `resource.close`, and `importRows` return Tasks. Release runs once after
252
+ successful acquisition, is awaited on every exit, and preserves failures. The scope folds deferred
253
+ release errors into the workflow's error channel. Use `acquireDisposable` for native disposables,
254
+ or `ensuring(finalizer)` for unconditional Task cleanup that does not acquire a resource.
175
255
 
176
256
  ## Recover Locally And Match At Boundaries
177
257
 
178
258
  Recover only where the fallback is part of application policy:
179
259
 
180
260
  ```ts
181
- const profile = loadProfile(userId).catchTags({
261
+ const profile = findProfile(userId).catchTags({
182
262
  ProfileNotFoundError: () => ok(defaultProfile),
183
263
  ProfileDisabledError: () => ok(disabledProfile),
184
264
  });
@@ -187,36 +267,40 @@ const profile = loadProfile(userId).catchTags({
187
267
  Unhandled tags remain in the error channel. At the HTTP boundary, map the complete union:
188
268
 
189
269
  ```ts
190
- const response = await createUser(input).matchTags((user) => ({ body: user, statusCode: 201 }), {
270
+ const responseTask = createUser(input).matchTags((user) => ({ body: user, statusCode: 201 }), {
191
271
  InvalidEmailError: (error) => ({ body: error.toJSON(), statusCode: 400 }),
192
272
  UserAlreadyExistsError: (error) => ({ body: error.toJSON(), statusCode: 409 }),
193
273
  SaveUserError: (error) => ({ body: error.toJSON(), statusCode: 500 }),
194
274
  });
275
+
276
+ const response = await responseTask.runPromise();
195
277
  ```
196
278
 
197
279
  Use `matchTagsPartial` only when a shared fallback is intentional. Keep HTTP status codes out of
198
- domain functions.
280
+ domain functions. Task matching is a lazy fold, so it remains composable until the runner.
281
+ `runExit` is the boundary to choose when defects, interruption, or composite causes also need inspection.
199
282
 
200
283
  ## Add Observability
201
284
 
202
285
  ```ts
203
286
  const observed = loadProfile(userId)
204
- .tap((profile) => metrics.increment("profile.loaded", { userId: profile.userId }))
205
- .tapError((error) => metrics.increment("profile.failed", { tag: error._tag }))
287
+ .inspect((profile) => metrics.increment("profile.loaded", { userId: profile.userId }))
288
+ .inspectError((error) => metrics.increment("profile.failed", { tag: error._tag }))
206
289
  .log((profile, error) => logger.info({ error, profile }, "profile result"));
207
290
  ```
208
291
 
209
- These `ResultAsync` callbacks are best-effort and cannot change the result.
210
- `ResultTask.tap` and `tapError` instead propagate returned task failures and turn callback
211
- throws or rejected Promises into defects. Use `andThen` or `orElse` when a
212
- logging, auditing, publishing, or cleanup failure must affect control flow.
292
+ `inspect`, `inspectError`, `inspectBoth`, and `log` are best-effort across `Result`, `ResultAsync`, and `ResultTask`: observer failures are ignored, returned tasks are not executed, and the original result is preserved.
293
+ Deprecated `Result` and `ResultAsync` `tap` / `tapError` retain their best-effort behavior.
294
+ Only `ResultTask` `tap`, `tapError`, `tapCause`, and `tapBoth` compose effectful side operations:
295
+ they propagate returned Task failures and turn callback throws or rejected Promises into defects.
296
+ Use Task `tap`, `andThen`, or `orElse` when auditing, publishing, or cleanup must affect control flow.
213
297
 
214
298
  ## Migrate Incrementally
215
299
 
216
300
  1. Identify the outermost throwing or rejecting dependency.
217
301
  2. Create concrete tagged errors for expected domain and infrastructure failures.
218
- 3. Wrap throws and rejections at that edge with `tryResult` or `tryResultAsync`.
219
- 4. Change callers to compose with `map`, `andThen`, `asyncAndThen`, and `mapErr`.
302
+ 3. Wrap synchronous throws with `tryResult` and new async work with `ResultTask.tryPromise`.
303
+ 4. Return Tasks directly from lazy services and compose with `map`, `andThen`, and `mapErr`.
220
304
  5. Move transport conversion to one boundary matcher.
221
305
  6. Replace broad error types and unsafe casts with inferred unions.
222
306
  7. Enable `resultar-check` rules and remove narrow temporary exceptions.
@@ -224,6 +308,80 @@ logging, auditing, publishing, or cleanup failure must affect control flow.
224
308
  Avoid converting the whole codebase in one unsafe cast. A wrapper at an integration seam lets the
225
309
  typed error channel spread in reviewable steps.
226
310
 
311
+ ## Preserve An Eager Compatibility Contract
312
+
313
+ `ResultAsync<T, E>` and `StrictResultAsync<T, E>` are the official in-flight `Result` types
314
+ (`PromiseLike<Result<T, E>>`), returned by `task.runResult()` / `ResultTask.runResult(task)`.
315
+ Import eager compatibility constructors or static orchestration helpers from `resultar` when
316
+ existing callers depend on starting once at construction or awaiting the same execution:
317
+
318
+ ```ts
319
+ import { ResultAsync, tryResultAsync } from "resultar";
320
+ import type { StrictResultAsync } from "resultar";
321
+
322
+ const loadProfileAsync = (userId: string): StrictResultAsync<Profile, ProfileApiError> =>
323
+ tryResultAsync({
324
+ try: () => profileClient.load(userId),
325
+ catch: (cause) => new ProfileApiError({ cause, userId }),
326
+ });
327
+
328
+ const loadProfileTask = (userId: string): ResultTask<Profile, ProfileApiError> =>
329
+ ResultTask.fromResultAsync(() => loadProfileAsync(userId));
330
+
331
+ // A caller receives one eagerly started ResultAsync execution via runResult().
332
+ const existingCaller = loadProfileTask(userId).runResult();
333
+ ```
334
+
335
+ Use `fromPromise` for an already-created Promise and `fromSafePromise` only when rejection is
336
+ impossible by contract. Async `map` / `mapErr` still await Promise-returning callbacks; `andThen`
337
+ accepts Result or Async. `Result.asyncMap` and `Result.asyncAndThen` (and their instance methods) bridge synchronous Results into eager `ResultAsync`:
338
+
339
+ ```ts
340
+ import { Result, ok, okAsync } from "resultar";
341
+
342
+ const eagerMapped = Result.asyncMap(ok(20), async (value) => value + 1);
343
+ const eagerChained = Result.asyncAndThen(ok(20), (value) => okAsync(value + 1));
344
+ ```
345
+
346
+ Both static helpers also accept curried calls for `pipe`. All eager constructors and helpers, including
347
+ `errAsync`, are available from `resultar`. Prefer
348
+ `task.runResult()` (or `ResultTask.runResult(task)`) when bridging a requirement-free Task into an
349
+ in-flight `ResultAsync`, and `errAsync(new DomainError(props))` in eager recipes;
350
+ `DomainError.errAsync(props)`, `ResultAsync.fromTask(task)`, and static/instance `ResultTask.toResultAsync` remain callable as compatibility aliases.
351
+ Async instance `match` / `matchTags` / `matchTagsPartial` return native Promises; static/curried
352
+ forms return composable Async values. Task matching returns a Task.
353
+
354
+ The eager compatibility surface keeps deprecated `retry` / `retryOrElse`, `timeout`, races, collections, and control flow.
355
+ Use Async factories for retries, and retain `withResource({ acquire, use, release })` where its
356
+ best-effort release is the existing contract. A cleanup failure that must affect that eager result
357
+ needs an explicit fallible step. These policies and deprecated observer methods are documented in
358
+ [the compatibility API reference](api.md#eager-async-compatibility-policies).
359
+
360
+ `Result.gen` / `Result.fn` support eager async bodies, and `safeTry` supports legacy
361
+ async final-Result bodies:
362
+
363
+ ```ts
364
+ import { Result, ok, safeTry } from "resultar";
365
+
366
+ const eagerWorkflow = Result.gen(async function* () {
367
+ const profile = yield* loadProfileAsync(userId);
368
+ return profile;
369
+ });
370
+
371
+ const reusableEager = Result.fn(async function* (id: string) {
372
+ return yield* loadProfileAsync(id);
373
+ });
374
+
375
+ const legacyEager = safeTry(async function* () {
376
+ const profile = yield* loadProfileAsync(userId);
377
+ return ok(profile);
378
+ });
379
+ ```
380
+
381
+ Do not change legacy `safeTry` generators to return plain values: their final value is a Result.
382
+ Modern `Result.gen` retains plain-value returns. New synchronous generators use `Result.gen`;
383
+ new lazy workflows use `ResultTask.gen`. Migrate a service to Task by changing creation, composition, runners, and callers together.
384
+
227
385
  ## Test Runtime And Type Behavior
228
386
 
229
387
  ```ts