resultar 3.7.0 → 3.8.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 +10 -0
- package/dist/agent/SKILL.md +224 -0
- package/dist/agent/agents/openai.yaml +4 -0
- package/dist/agent/examples/workflow.mjs +21 -0
- package/dist/agent/examples/workflow.mts +27 -0
- package/dist/agent/references/api.md +249 -0
- package/dist/agent/references/integrations.md +264 -0
- package/dist/agent/references/patterns.md +246 -0
- package/dist/agent/references/review-checklist.md +122 -0
- package/dist/agent/references/services.md +39 -0
- package/dist/agent/versions.json +10 -0
- package/dist/index.d.ts +63 -66
- package/dist/index.js +130 -126
- package/dist/llms.txt +11 -0
- package/package.json +5 -5
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Resultar Requests And Validation Tooling
|
|
2
|
+
|
|
3
|
+
Use this reference for HTTP JSON integrations and Resultar-specific diagnostics. Verify package
|
|
4
|
+
versions and local README files before copying an example.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Choose A Request Package](#choose-a-request-package)
|
|
9
|
+
- [Use The Schema-Agnostic Core](#use-the-schema-agnostic-core)
|
|
10
|
+
- [Use The TypeBox Adapter](#use-the-typebox-adapter)
|
|
11
|
+
- [Use The Zod Adapter](#use-the-zod-adapter)
|
|
12
|
+
- [Map Request Errors](#map-request-errors)
|
|
13
|
+
- [Apply Retry Correctly](#apply-retry-correctly)
|
|
14
|
+
- [Validate In The Preferred Order](#validate-in-the-preferred-order)
|
|
15
|
+
- [Configure The Native Checker](#configure-the-native-checker)
|
|
16
|
+
- [Act On Diagnostics](#act-on-diagnostics)
|
|
17
|
+
- [Discover Repository Commands](#discover-repository-commands)
|
|
18
|
+
|
|
19
|
+
## Choose A Request Package
|
|
20
|
+
|
|
21
|
+
| Contract style | Package | Success type |
|
|
22
|
+
| -------------------------------- | -------------------------- | ---------------------------- |
|
|
23
|
+
| Custom type guard or decoder | `resultar-request` | Type guard or decoder output |
|
|
24
|
+
| TypeBox schema | `resultar-request-typebox` | `Static<typeof schema>` |
|
|
25
|
+
| Zod schema, including transforms | `resultar-request-zod` | `z.output<typeof schema>` |
|
|
26
|
+
|
|
27
|
+
All three share the same transport implementation. The adapters add schema validation and re-export
|
|
28
|
+
the public `resultar-request` types. Install only the core and schema adapter the service needs.
|
|
29
|
+
|
|
30
|
+
Keep request payloads concrete. Do not widen a known response contract to `JsonRecord`,
|
|
31
|
+
`Record<string, unknown>`, or `unknown` after validation.
|
|
32
|
+
|
|
33
|
+
## Use The Schema-Agnostic Core
|
|
34
|
+
|
|
35
|
+
Use a type guard when validation does not transform the payload:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { requestJson } from "resultar-request";
|
|
39
|
+
|
|
40
|
+
type User = { readonly email: string; readonly id: string };
|
|
41
|
+
|
|
42
|
+
const isUser = (value: unknown): value is User =>
|
|
43
|
+
typeof value === "object" &&
|
|
44
|
+
value !== null &&
|
|
45
|
+
"email" in value &&
|
|
46
|
+
typeof value.email === "string" &&
|
|
47
|
+
"id" in value &&
|
|
48
|
+
typeof value.id === "string";
|
|
49
|
+
|
|
50
|
+
const user = requestJson({
|
|
51
|
+
request: () => fetch(`https://example.com/users/${userId}`),
|
|
52
|
+
validator: isUser,
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `decode` when validation also normalizes or transforms the response. The decoder must make
|
|
57
|
+
success and validation failure explicit; check the installed package types for the exact shape.
|
|
58
|
+
|
|
59
|
+
## Use The TypeBox Adapter
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createTaggedError } from "resultar";
|
|
63
|
+
import {
|
|
64
|
+
requestJson as requestJsonTypeBox,
|
|
65
|
+
type RequestJsonMapError,
|
|
66
|
+
} from "resultar-request-typebox";
|
|
67
|
+
import { Type, type Static } from "typebox";
|
|
68
|
+
|
|
69
|
+
const userSchema = Type.Object(
|
|
70
|
+
{
|
|
71
|
+
email: Type.String(),
|
|
72
|
+
id: Type.String(),
|
|
73
|
+
plan: Type.Union([Type.Literal("free"), Type.Literal("team")]),
|
|
74
|
+
},
|
|
75
|
+
{ additionalProperties: false },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
type User = Static<typeof userSchema>;
|
|
79
|
+
|
|
80
|
+
class UserApiError extends createTaggedError({
|
|
81
|
+
name: "UserApiError",
|
|
82
|
+
message: "User API $reason: $detail",
|
|
83
|
+
}) {}
|
|
84
|
+
|
|
85
|
+
const mapUserError = {
|
|
86
|
+
404: ({ statusCode }) => new UserApiError({ detail: `HTTP ${statusCode}`, reason: "not-found" }),
|
|
87
|
+
http: ({ statusCode }) => new UserApiError({ detail: `HTTP ${statusCode}`, reason: "http" }),
|
|
88
|
+
invalidJson: ({ cause }) =>
|
|
89
|
+
new UserApiError({ cause, detail: cause.message, reason: "invalid-json" }),
|
|
90
|
+
request: ({ cause }) => new UserApiError({ cause, detail: cause.message, reason: "request" }),
|
|
91
|
+
validation: ({ errors }) =>
|
|
92
|
+
new UserApiError({ detail: `${errors.length} validation issue(s)`, reason: "validation" }),
|
|
93
|
+
} satisfies RequestJsonMapError;
|
|
94
|
+
|
|
95
|
+
const user = requestJsonTypeBox({
|
|
96
|
+
request: () => fetch(`https://example.com/users/${userId}`),
|
|
97
|
+
schema: userSchema,
|
|
98
|
+
mapError: mapUserError,
|
|
99
|
+
retry: { times: 2, delayMs: ({ nextAttempt }) => nextAttempt * 100, jittered: 0.2 },
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The success channel is `User`, derived from `Static<typeof userSchema>`. Keep the named adapter
|
|
104
|
+
alias when a file uses more than one request helper or when the schema library should be obvious at
|
|
105
|
+
the call site.
|
|
106
|
+
|
|
107
|
+
## Use The Zod Adapter
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createTaggedError } from "resultar";
|
|
111
|
+
import { requestJson as requestJsonZod, type RequestJsonMapError } from "resultar-request-zod";
|
|
112
|
+
import { z } from "zod";
|
|
113
|
+
|
|
114
|
+
const userSchema = z
|
|
115
|
+
.object({ email: z.string().email(), id: z.string(), seats: z.number().int().nonnegative() })
|
|
116
|
+
.transform((user) => ({
|
|
117
|
+
...user,
|
|
118
|
+
email: user.email.toLowerCase(),
|
|
119
|
+
seatLabel: `${user.seats} seats`,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
type User = z.output<typeof userSchema>;
|
|
123
|
+
|
|
124
|
+
class UserApiError extends createTaggedError({
|
|
125
|
+
name: "UserApiError",
|
|
126
|
+
message: "User API $reason: $detail",
|
|
127
|
+
}) {}
|
|
128
|
+
|
|
129
|
+
const mapUserError = {
|
|
130
|
+
404: ({ statusCode }) => new UserApiError({ detail: `HTTP ${statusCode}`, reason: "not-found" }),
|
|
131
|
+
http: ({ statusCode }) => new UserApiError({ detail: `HTTP ${statusCode}`, reason: "http" }),
|
|
132
|
+
invalidJson: ({ cause }) =>
|
|
133
|
+
new UserApiError({ cause, detail: cause.message, reason: "invalid-json" }),
|
|
134
|
+
request: ({ cause }) => new UserApiError({ cause, detail: cause.message, reason: "request" }),
|
|
135
|
+
validation: ({ errors }) =>
|
|
136
|
+
new UserApiError({ detail: `${errors.length} validation issue(s)`, reason: "validation" }),
|
|
137
|
+
} satisfies RequestJsonMapError;
|
|
138
|
+
|
|
139
|
+
const user = requestJsonZod({
|
|
140
|
+
request: () => fetch(`https://example.com/users/${userId}`),
|
|
141
|
+
schema: userSchema,
|
|
142
|
+
mapError: mapUserError,
|
|
143
|
+
retry: { times: 2, delayMs: ({ nextAttempt }) => nextAttempt * 100, jittered: 0.2 },
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use `z.output`, not `z.input` or an independently maintained interface, because Zod transforms can
|
|
148
|
+
change the success type.
|
|
149
|
+
|
|
150
|
+
## Map Request Errors
|
|
151
|
+
|
|
152
|
+
Without `mapError`, request helpers return the package's `RequestError` union. Use that union for
|
|
153
|
+
infrastructure-local code. At an application boundary, use `mapError` to convert these categories
|
|
154
|
+
to concrete tagged errors:
|
|
155
|
+
|
|
156
|
+
- request creation, network, or fetch failure;
|
|
157
|
+
- status-specific HTTP failure, with numeric handlers taking precedence over `http`;
|
|
158
|
+
- invalid JSON;
|
|
159
|
+
- response validation or decode failure.
|
|
160
|
+
|
|
161
|
+
Preserve `cause` for request and JSON failures and preserve status, URL, validation details, and
|
|
162
|
+
other safe context needed for diagnosis. Do not map every category to a message-only `Error`.
|
|
163
|
+
|
|
164
|
+
## Apply Retry Correctly
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const user = requestJsonZod({
|
|
168
|
+
request: () => fetch(`https://example.com/users/${userId}`),
|
|
169
|
+
schema: userSchema,
|
|
170
|
+
retry: { times: 2, delayMs: ({ nextAttempt }) => nextAttempt * 100, jittered: 0.2 },
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The default policy retries request failures and HTTP `5xx`, but not `4xx`, invalid JSON, or
|
|
175
|
+
validation failures. Retry requires a request factory; an already-created promise cannot be
|
|
176
|
+
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`
|
|
178
|
+
automatically forwards policy cancellation. Supply an external fetch signal through the request
|
|
179
|
+
closure when the caller owns separate cancellation.
|
|
180
|
+
|
|
181
|
+
## Validate In The Preferred Order
|
|
182
|
+
|
|
183
|
+
1. Run the native `resultar-check` CLI as the authoritative local and CI gate. It uses TypeScript-Go
|
|
184
|
+
to run compiler diagnostics and all Resultar rules over the same project.
|
|
185
|
+
2. Run the repository's formatter, build, tests, static analysis, package smoke tests, and runnable
|
|
186
|
+
examples.
|
|
187
|
+
|
|
188
|
+
The Go analyzer is the single implementation of Resultar diagnostics.
|
|
189
|
+
|
|
190
|
+
## Configure The Native Checker
|
|
191
|
+
|
|
192
|
+
Install Resultar Check. A separate TypeScript installation is not required:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
pnpm add -D resultar-check
|
|
196
|
+
pnpm exec resultar-check
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Add the checker configuration marker to `tsconfig.json` and tune rules only when project policy
|
|
200
|
+
requires it:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"$schema": "./node_modules/resultar-check/schema.json",
|
|
205
|
+
"compilerOptions": {
|
|
206
|
+
"plugins": [
|
|
207
|
+
{
|
|
208
|
+
"name": "resultar-check",
|
|
209
|
+
"noDiscard": "error",
|
|
210
|
+
"noUnsafeAwait": "error",
|
|
211
|
+
"preferAndThen": "error",
|
|
212
|
+
"preferMapErr": "error",
|
|
213
|
+
"unsafeResultTypeAssertion": "error"
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The `plugins` entry is consumed by the native CLI and stdio LSP server; it does not install an editor
|
|
221
|
+
extension. Start `resultar-check lsp` from the editor's language-server configuration. Findings use
|
|
222
|
+
rule IDs such as `resultar/no-discard`; pass `--format json` (or `--json`) when machine-readable JSON
|
|
223
|
+
Lines output is needed.
|
|
224
|
+
|
|
225
|
+
Read `packages/check/README.md` for all rules, severities, modes, and command-line flags. Use narrow
|
|
226
|
+
`ignoreFilePatterns` only for deliberate tests, scripts, generated files, or terminal process boundaries.
|
|
227
|
+
|
|
228
|
+
## Act On Diagnostics
|
|
229
|
+
|
|
230
|
+
When an agent encounters a diagnostic, fix the architecture first:
|
|
231
|
+
|
|
232
|
+
- handle, compose, return, or intentionally discard a returned Resultar value; a discard does not
|
|
233
|
+
run a lazy task and must not be applied just to silence diagnostics;
|
|
234
|
+
- replace nested `Result` from `map` with `andThen`;
|
|
235
|
+
- replace `Promise<Result>` / `Promise<StrictResult>` contracts with `ResultAsync` /
|
|
236
|
+
`StrictResultAsync` and return composed operations directly (`prefer-result-async`);
|
|
237
|
+
- enable `preferResultAsyncMode: "all"` with `preferResultAsync: "error"` to also catch raw
|
|
238
|
+
`Promise<T>` repository/service contracts and inferred returns; map driver errors in adapters
|
|
239
|
+
and document local suppressions for required native framework or conversion callbacks;
|
|
240
|
+
- replace error-only recovery with `mapErr`;
|
|
241
|
+
- replace project-wide `try/catch` with `tryResult` or `tryResultAsync` when `noTryCatch` is enabled;
|
|
242
|
+
- wrap raw promise awaits in Resultar contexts;
|
|
243
|
+
- use `yield*` and avoid `try/catch` inside `Result.gen` / `safeTry`;
|
|
244
|
+
- preserve the inferred error channel instead of asserting it away.
|
|
245
|
+
|
|
246
|
+
## Discover Repository Commands
|
|
247
|
+
|
|
248
|
+
Inspect `package.json` scripts instead of assuming a package manager or command. In the Resultar
|
|
249
|
+
repository, the relevant root commands currently include:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
pnpm fmt:check
|
|
253
|
+
pnpm build
|
|
254
|
+
pnpm check
|
|
255
|
+
pnpm test
|
|
256
|
+
pnpm analyze
|
|
257
|
+
pnpm smoke:package
|
|
258
|
+
pnpm test:examples
|
|
259
|
+
pnpm test:agents
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Run the narrowest useful command while iterating, then the complete relevant validation set before
|
|
263
|
+
claiming completion. Report pre-existing dependency, lockfile, or environment blockers separately
|
|
264
|
+
from failures introduced by the skill work.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Resultar Implementation Patterns
|
|
2
|
+
|
|
3
|
+
Use these patterns as starting points, then align names and types with the consuming domain.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Model A Domain Failure](#model-a-domain-failure)
|
|
8
|
+
- [Compose Sync And Async Steps](#compose-sync-and-async-steps)
|
|
9
|
+
- [Write A Linear Workflow](#write-a-linear-workflow)
|
|
10
|
+
- [Wrap External Work](#wrap-external-work)
|
|
11
|
+
- [Apply Retry And Timeout Policy](#apply-retry-and-timeout-policy)
|
|
12
|
+
- [Process Batches](#process-batches)
|
|
13
|
+
- [Acquire And Release Resources](#acquire-and-release-resources)
|
|
14
|
+
- [Recover Locally And Match At Boundaries](#recover-locally-and-match-at-boundaries)
|
|
15
|
+
- [Add Observability](#add-observability)
|
|
16
|
+
- [Migrate Incrementally](#migrate-incrementally)
|
|
17
|
+
- [Test Runtime And Type Behavior](#test-runtime-and-type-behavior)
|
|
18
|
+
|
|
19
|
+
## Model A Domain Failure
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createTaggedError, ok } from "resultar";
|
|
23
|
+
import type { StrictResult } from "resultar";
|
|
24
|
+
|
|
25
|
+
class InvalidEmailError extends createTaggedError({
|
|
26
|
+
name: "InvalidEmailError",
|
|
27
|
+
message: "Invalid email $email",
|
|
28
|
+
}) {}
|
|
29
|
+
|
|
30
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
31
|
+
email.includes("@") ? ok(email.trim().toLowerCase()) : InvalidEmailError.err({ email });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use real tagged errors when the failure crosses a service, HTTP, job, queue, CLI, or integration
|
|
35
|
+
boundary. Keep strings, enums, or `TaggedEnum` variants for intentionally local flows.
|
|
36
|
+
|
|
37
|
+
## Compose Sync And Async Steps
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
type CreateUserError = InvalidEmailError | UserAlreadyExistsError | SaveUserError;
|
|
41
|
+
|
|
42
|
+
const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
|
|
43
|
+
validateEmail(input.email)
|
|
44
|
+
.asyncAndThen(ensureEmailAvailable)
|
|
45
|
+
.andThen((email) => saveUser({ ...input, email }));
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- Use `map` only for an infallible value transform.
|
|
49
|
+
- Use `filterOrElse` for predicate validation.
|
|
50
|
+
- Use `andThen` when the next operation can fail.
|
|
51
|
+
- Use `asyncAndThen` only for the sync-to-async bridge.
|
|
52
|
+
- Keep each function's error type narrow and allow the final union to be inferred.
|
|
53
|
+
|
|
54
|
+
## Write A Linear Workflow
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
|
|
58
|
+
Result.gen(async function* () {
|
|
59
|
+
const email = yield* validateEmail(input.email);
|
|
60
|
+
yield* ensureEmailAvailable(email);
|
|
61
|
+
const user = yield* saveUser({ ...input, email });
|
|
62
|
+
|
|
63
|
+
return ok(user);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
## Wrap External Work
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
class ProfileApiError extends createTaggedError({
|
|
74
|
+
name: "ProfileApiError",
|
|
75
|
+
message: "Profile API failed for $userId",
|
|
76
|
+
}) {}
|
|
77
|
+
|
|
78
|
+
type LoadProfileOptions = {
|
|
79
|
+
readonly attempt?: number;
|
|
80
|
+
readonly signal?: AbortSignal;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const loadProfile = (
|
|
84
|
+
userId: string,
|
|
85
|
+
options: LoadProfileOptions = {},
|
|
86
|
+
): StrictResultAsync<Profile, ProfileApiError> =>
|
|
87
|
+
tryResultAsync(
|
|
88
|
+
() => profileClient.load(userId, options),
|
|
89
|
+
(cause) => new ProfileApiError({ cause, userId }),
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
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.
|
|
96
|
+
|
|
97
|
+
For callback or subscription APIs, use `fromCallback` and return cleanup from `subscribe`:
|
|
98
|
+
|
|
99
|
+
```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
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Check the installed `ResultAsyncFromCallbackOptions` type for exact client integration details.
|
|
112
|
+
|
|
113
|
+
## Apply Retry And Timeout Policy
|
|
114
|
+
|
|
115
|
+
```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
|
+
```
|
|
134
|
+
|
|
135
|
+
Forward the supplied signal into the underlying operation. Confirm whether the desired timeout
|
|
136
|
+
covers each attempt or the complete retry budget, then nest the policies accordingly. Use
|
|
137
|
+
`retryOrElse` only when fallback after exhaustion is domain policy.
|
|
138
|
+
|
|
139
|
+
Choose races deliberately:
|
|
140
|
+
|
|
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.
|
|
144
|
+
|
|
145
|
+
## Process Batches
|
|
146
|
+
|
|
147
|
+
Stop at the first error with bounded concurrency:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const saved = ResultAsync.forEach(rows, saveRow, { concurrency: 8 });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Collect all independent errors instead:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const validated = ResultAsync.validateAll(rows, validateRow, { concurrency: 8 });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `{ discard: true }` with `forEach` when successful values are intentionally unused. Do not
|
|
160
|
+
silently discard the returned `ResultAsync` itself.
|
|
161
|
+
|
|
162
|
+
## Acquire And Release Resources
|
|
163
|
+
|
|
164
|
+
```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
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
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.
|
|
175
|
+
|
|
176
|
+
## Recover Locally And Match At Boundaries
|
|
177
|
+
|
|
178
|
+
Recover only where the fallback is part of application policy:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const profile = loadProfile(userId).catchTags({
|
|
182
|
+
ProfileNotFoundError: () => ok(defaultProfile),
|
|
183
|
+
ProfileDisabledError: () => ok(disabledProfile),
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Unhandled tags remain in the error channel. At the HTTP boundary, map the complete union:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const response = await createUser(input).matchTags((user) => ({ body: user, statusCode: 201 }), {
|
|
191
|
+
InvalidEmailError: (error) => ({ body: error.toJSON(), statusCode: 400 }),
|
|
192
|
+
UserAlreadyExistsError: (error) => ({ body: error.toJSON(), statusCode: 409 }),
|
|
193
|
+
SaveUserError: (error) => ({ body: error.toJSON(), statusCode: 500 }),
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use `matchTagsPartial` only when a shared fallback is intentional. Keep HTTP status codes out of
|
|
198
|
+
domain functions.
|
|
199
|
+
|
|
200
|
+
## Add Observability
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const observed = loadProfile(userId)
|
|
204
|
+
.tap((profile) => metrics.increment("profile.loaded", { userId: profile.userId }))
|
|
205
|
+
.tapError((error) => metrics.increment("profile.failed", { tag: error._tag }))
|
|
206
|
+
.log((profile, error) => logger.info({ error, profile }, "profile result"));
|
|
207
|
+
```
|
|
208
|
+
|
|
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.
|
|
213
|
+
|
|
214
|
+
## Migrate Incrementally
|
|
215
|
+
|
|
216
|
+
1. Identify the outermost throwing or rejecting dependency.
|
|
217
|
+
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`.
|
|
220
|
+
5. Move transport conversion to one boundary matcher.
|
|
221
|
+
6. Replace broad error types and unsafe casts with inferred unions.
|
|
222
|
+
7. Enable `resultar-check` rules and remove narrow temporary exceptions.
|
|
223
|
+
|
|
224
|
+
Avoid converting the whole codebase in one unsafe cast. A wrapper at an integration seam lets the
|
|
225
|
+
typed error channel spread in reviewable steps.
|
|
226
|
+
|
|
227
|
+
## Test Runtime And Type Behavior
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { equal, ok as assert } from "node:assert";
|
|
231
|
+
import { describe, expectTypeOf, it } from "vite-plus/test";
|
|
232
|
+
|
|
233
|
+
describe("validateEmail", () => {
|
|
234
|
+
it("returns a typed tagged error", () => {
|
|
235
|
+
const result = validateEmail("invalid");
|
|
236
|
+
|
|
237
|
+
assert(result.isErr());
|
|
238
|
+
equal(result.error._tag, "InvalidEmailError");
|
|
239
|
+
expectTypeOf(result.error).toEqualTypeOf<InvalidEmailError>();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Test every expected error path, not just `Ok`. Add type tests for inferred unions, required tagged
|
|
245
|
+
error props, narrowing, and exhaustive handlers. Use deterministic fake clocks, signals, and
|
|
246
|
+
clients for retry, timeout, cancellation, race, concurrency, and cleanup tests.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Resultar Review And Migration Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist for implementation reviews, migrations, documentation changes, and final
|
|
4
|
+
validation. Report only issues supported by the installed Resultar version and repository evidence.
|
|
5
|
+
|
|
6
|
+
## Source And Scope
|
|
7
|
+
|
|
8
|
+
- Confirm the installed `resultar`, request adapter, and `resultar-check` versions.
|
|
9
|
+
- Inspect local exports, types, package README files, and runnable examples before suggesting an API.
|
|
10
|
+
- Separate issues introduced by the change from pre-existing workspace or dependency problems.
|
|
11
|
+
- Preserve unrelated changes in a dirty worktree.
|
|
12
|
+
|
|
13
|
+
## Failure Model
|
|
14
|
+
|
|
15
|
+
- Expected failures return `Result`, `ResultAsync`, or `ResultTask`; programmer defects may still throw.
|
|
16
|
+
- Application and integration boundaries prefer `StrictResult` / `StrictResultAsync`.
|
|
17
|
+
- Tagged errors have stable names, useful metadata, inferred template props, and preserved causes.
|
|
18
|
+
- Error unions remain concrete instead of widening early to `Error`, `unknown`, or a string.
|
|
19
|
+
- `TaggedEnum` is limited to lightweight non-Error domain variants.
|
|
20
|
+
- Sensitive metadata is redacted when serialization or logging could expose it.
|
|
21
|
+
|
|
22
|
+
## Composition
|
|
23
|
+
|
|
24
|
+
- `map` performs only infallible success transforms.
|
|
25
|
+
- `mapErr` performs error-only transforms.
|
|
26
|
+
- `filterOrElse` represents predicate validation.
|
|
27
|
+
- `andThen` composes fallible steps; `asyncAndThen` is used for the sync-to-async bridge.
|
|
28
|
+
- `orElse` recovers the whole error channel; `catchTag` / `catchTags` recover selected domain cases.
|
|
29
|
+
- `safeTry` uses `yield*` for Resultar values and contains no raw `await` or broad `try/catch`.
|
|
30
|
+
- Returned `Result`, `ResultAsync`, and `ResultTask` values are handled or explicitly discarded by supported policy.
|
|
31
|
+
- Type assertions do not narrow away possible errors.
|
|
32
|
+
|
|
33
|
+
## Async Boundaries And Policy
|
|
34
|
+
|
|
35
|
+
- External throws and rejections are wrapped once, close to the uncontrolled dependency.
|
|
36
|
+
- Error mappers accept `unknown` and produce concrete errors with `cause`.
|
|
37
|
+
- Promise factories use `tryResultAsync`; already-created promises use `fromPromise`.
|
|
38
|
+
- `fromSafePromise` is used only when rejection is impossible by contract.
|
|
39
|
+
- Callback integrations provide cleanup and model `AbortError` where applicable.
|
|
40
|
+
- Retry policy defines bounded attempts, delay/jitter, retryable errors, and cancellation.
|
|
41
|
+
- Timeout scope is explicit: per attempt or across the complete retry budget.
|
|
42
|
+
- `race`/`raceAll` are used for first settlement; `raceFirst` for first success.
|
|
43
|
+
- Signals reach the underlying client so cancellation is cooperative rather than cosmetic.
|
|
44
|
+
- Batch concurrency is bounded when workload size or dependency capacity requires it.
|
|
45
|
+
- `validateAll` is used only when collecting all independent failures is desired.
|
|
46
|
+
- `withResource` cleanup is treated as best-effort; important cleanup failures are modeled in the
|
|
47
|
+
result channel.
|
|
48
|
+
|
|
49
|
+
## Boundaries And Recovery
|
|
50
|
+
|
|
51
|
+
- Domain code does not contain transport status codes, framework responses, or CLI exit behavior.
|
|
52
|
+
- `matchTags` handles the complete tagged error union at the boundary.
|
|
53
|
+
- `matchTagsPartial` and `matchErrorPartial` have intentional fallbacks.
|
|
54
|
+
- An `Error` handler exists when untagged errors can reach tagged matching.
|
|
55
|
+
- Recovery is local only when the fallback is genuine domain/application policy.
|
|
56
|
+
- `_unsafeUnwrap*` remains in tests; `runSync`, `runPromise`, and `unwrapOrThrow` appear only at
|
|
57
|
+
deliberate terminal boundaries.
|
|
58
|
+
|
|
59
|
+
## HTTP Requests
|
|
60
|
+
|
|
61
|
+
- `resultar-request` is used for a custom guard/decoder; TypeBox and Zod use their dedicated
|
|
62
|
+
adapters.
|
|
63
|
+
- TypeBox success types derive from `Static<typeof schema>`.
|
|
64
|
+
- Zod success types derive from `z.output<typeof schema>` so transforms are preserved.
|
|
65
|
+
- Validated values are not widened back to `JsonRecord`, `Record<string, unknown>`, or `unknown`.
|
|
66
|
+
- Error mapping distinguishes request, HTTP, invalid-JSON, and validation failures.
|
|
67
|
+
- Numeric HTTP handlers take precedence over generic HTTP mapping where needed.
|
|
68
|
+
- Retry uses a request factory and does not retry non-retryable `4xx`, JSON, or validation failures
|
|
69
|
+
without explicit policy.
|
|
70
|
+
|
|
71
|
+
## Observability And Cleanup
|
|
72
|
+
|
|
73
|
+
- `Result` / `ResultAsync` observation failures are best-effort; `ResultTask.tap` / `tapError`
|
|
74
|
+
can introduce typed failures or defects.
|
|
75
|
+
- Resource cleanup and observation use the semantics of the concrete execution model.
|
|
76
|
+
- Audits, publishes, or cleanup that must succeed use `andThen`, `orElse`, or another Resultar
|
|
77
|
+
boundary.
|
|
78
|
+
- Logs expose stable tags and safe context without leaking redacted values.
|
|
79
|
+
|
|
80
|
+
## Tests And Documentation
|
|
81
|
+
|
|
82
|
+
- Runtime tests cover `Ok` and every expected `Err` branch.
|
|
83
|
+
- Type tests cover inferred error unions, required tagged-error props, narrowing, and exhaustive
|
|
84
|
+
handlers.
|
|
85
|
+
- Retry, timeout, cancellation, races, concurrency, and cleanup use deterministic fakes.
|
|
86
|
+
- Tests use the repository's actual runner; this repository uses `vite-plus/test`.
|
|
87
|
+
- Documentation examples compile or have a matching runnable example.
|
|
88
|
+
- Core request, TypeBox, and Zod docs link to one another as alternatives/adapters.
|
|
89
|
+
- API names, package links, and version claims match current source.
|
|
90
|
+
- `Result.gen` returns a Result; `ResultTask.gen` returns a plain success value.
|
|
91
|
+
- `runResult` rejection paths and deferred release errors are handled at the owning boundary.
|
|
92
|
+
- An intentional-discard fix is not mistaken for executing or handling a lazy task.
|
|
93
|
+
|
|
94
|
+
## Validation Order
|
|
95
|
+
|
|
96
|
+
1. Run the `resultar-check` CLI as the authoritative project check.
|
|
97
|
+
2. Confirm the `resultar-check lsp` stdio server emits Resultar diagnostics and quick fixes in the
|
|
98
|
+
editor.
|
|
99
|
+
3. Use JSONL, SARIF, or JUnit output when a CI/review system needs structured diagnostics; keep the
|
|
100
|
+
native CLI as the authoritative check.
|
|
101
|
+
4. Run formatting, build, tests, analysis, package smoke tests, and example workflows that apply.
|
|
102
|
+
5. Report exactly which commands passed, failed, or were blocked.
|
|
103
|
+
|
|
104
|
+
## Review Red Flags
|
|
105
|
+
|
|
106
|
+
- `T | Error`, sentinel `null`, message strings, or thrown expected failures replace a Resultar
|
|
107
|
+
channel.
|
|
108
|
+
- A broad `try/catch` surrounds an entire workflow instead of wrapping one external boundary.
|
|
109
|
+
- Project-wide `try/catch` remains after the strict `noTryCatch` migration rule is enabled.
|
|
110
|
+
- `map` returns another `Result` or `ResultAsync`.
|
|
111
|
+
- `orElse` only replaces one error with another and should be `mapErr`.
|
|
112
|
+
- A raw promise is awaited inside a Resultar workflow without a typed mapper.
|
|
113
|
+
- `safeTry` contains raw `await`, plain `yield`, `try/catch`, or legacy `safeUnwrap()`.
|
|
114
|
+
- A Resultar value is created and ignored.
|
|
115
|
+
- `_unsafeUnwrap()` appears in production code.
|
|
116
|
+
- An assertion makes an error union narrower than the implementation can guarantee.
|
|
117
|
+
- Tagged errors override generated constructors, mismatch class names, or omit useful causes.
|
|
118
|
+
- A schema adapter returns a broad JSON type or duplicates the schema with a drifting interface.
|
|
119
|
+
- A promise rather than a request factory is configured for retry.
|
|
120
|
+
- Hand-written retry, timeout, or race logic duplicates a built-in ResultAsync policy.
|
|
121
|
+
- A lint-only pass is described as full Resultar validation.
|
|
122
|
+
- Documentation recommends an API absent from the installed package.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Typed services and HTTP scopes
|
|
2
|
+
|
|
3
|
+
Read the installed `resultar-di/README.md`, `resultar-hono/README.md` and `resultar-fastify/README.md` for the exact companion
|
|
4
|
+
versions. The primary DI exports are `createModule`, `service`, and `resource`; class tokens and
|
|
5
|
+
named factory overloads belong to `resultar-di/advanced`.
|
|
6
|
+
|
|
7
|
+
- Register a service token with `.singleton(Token)`, `.scoped(Token)`, or `.transient(Token)`.
|
|
8
|
+
Dependency objects are inferred from tokens. Use `service(name, task)` for asynchronous
|
|
9
|
+
initialization; an async function passed as an ordinary factory is a defect in the contract.
|
|
10
|
+
- Keep a server's serving-and-waiting task inside its owning `use` / ResultTask scope. Returning a
|
|
11
|
+
live resource from a completed scope returns a resource whose finalizer has already run.
|
|
12
|
+
- Use `createHonoApp` for Hono routes. Ordinary `await` and `result.match` inside a route are valid;
|
|
13
|
+
generator-specific restrictions do not apply to a route body.
|
|
14
|
+
- Use `createHonoServices(module).middleware(keys, { locals })` to add services to an existing Hono
|
|
15
|
+
router. Inline route middleware infers `c.var.services` while preserving `c.env` and native RPC
|
|
16
|
+
responses. Install one services middleware per request; consume/cancel responses before `close()`.
|
|
17
|
+
- Use `createFastifyPlugin({ services, bindings, appBindings?, locals? })` for native Fastify routes.
|
|
18
|
+
`app.services` contains the selected startup singletons; `request.services` is available from
|
|
19
|
+
`preHandler` through response completion. Register authentication providing locals earlier.
|
|
20
|
+
Infer decorators with `InferAppServices` / `InferRequestServices`; augment Fastify in the application
|
|
21
|
+
or use its typed `getDecorator`. Annotate module/locals callback parameters with native Fastify
|
|
22
|
+
types when their inferred return values participate in selection checking.
|
|
23
|
+
- Keep business logic in plain factories with async methods when sufficient. Register them using
|
|
24
|
+
`service(name, dependencies, factory)`. Map results explicitly through `matchTags` and native
|
|
25
|
+
response APIs; the framework adapters install no status table or `reply.result` helper.
|
|
26
|
+
- `scope.useSingletons(keys, callback)` restricts selected services and callback tokens to the root
|
|
27
|
+
lifetime. Keep its callback active while serving; it is not a free-standing resolver. Factory
|
|
28
|
+
initialization stays lazy until selection; resource release remains explicit.
|
|
29
|
+
- `scope.fetch`, `services.http`, and `createHonoApp` keep the request scope open until the Response
|
|
30
|
+
body ends, fails, or is canceled. Tests must consume or cancel response bodies before shutdown.
|
|
31
|
+
- Stop and drain the server, finish/cancel persistent streams, then await application close.
|
|
32
|
+
Handle typed release errors; defects and composite causes may reject `close()`.
|
|
33
|
+
- Supply authenticated request locals through the appropriate per-request integration. A singleton
|
|
34
|
+
cannot capture scoped services or request-local data. Extracting authentication remains the
|
|
35
|
+
framework adapter's responsibility.
|
|
36
|
+
|
|
37
|
+
For a complete runnable pattern, see the workspace `examples/hono/` and `examples/resultar/`
|
|
38
|
+
application lifecycle examples. The distributed guide includes the checked core recipe at
|
|
39
|
+
`examples/workflow.mjs`; use the published declarations to adapt companion integrations.
|