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.
- package/README.md +667 -815
- package/dist/agent/SKILL.md +243 -76
- package/dist/agent/examples/workflow.mjs +5 -3
- package/dist/agent/examples/workflow.mts +13 -8
- package/dist/agent/references/api.md +339 -94
- package/dist/agent/references/integrations.md +49 -9
- package/dist/agent/references/patterns.md +232 -74
- package/dist/agent/references/review-checklist.md +60 -19
- package/dist/agent/references/services.md +25 -5
- package/dist/agent/versions.json +8 -8
- package/dist/index.d.ts +3183 -976
- package/dist/index.js +5583 -2258
- package/dist/llms.txt +1 -1
- package/package.json +8 -8
|
@@ -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
|
|
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
|
|
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>`
|
|
249
|
-
`StrictResultAsync` and return composed operations directly
|
|
250
|
-
-
|
|
251
|
-
|
|
252
|
-
and
|
|
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
|
|
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*`
|
|
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):
|
|
43
|
-
validateEmail(input.email)
|
|
44
|
-
.
|
|
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
|
|
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 `
|
|
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):
|
|
58
|
-
|
|
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
|
|
68
|
+
return user;
|
|
64
69
|
});
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
Use `yield*` for every fallible
|
|
68
|
-
|
|
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
|
-
):
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
109
|
+
Deprecated `fromCallback` returns eager `ResultAsync`. Invoke it inside a Task factory and return
|
|
110
|
+
cleanup from `subscribe`:
|
|
98
111
|
|
|
99
112
|
```ts
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
117
|
-
(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
`
|
|
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
|
|
142
|
-
- `raceFirst`: first
|
|
143
|
-
|
|
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 =
|
|
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 =
|
|
228
|
+
const validated = ResultTask.validateAll(rows, validateRow, { concurrency: 8 });
|
|
157
229
|
```
|
|
158
230
|
|
|
159
|
-
|
|
160
|
-
|
|
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 =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 =
|
|
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
|
|
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
|
-
.
|
|
205
|
-
.
|
|
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
|
-
|
|
210
|
-
`
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
219
|
-
4.
|
|
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
|