resultar 3.6.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/LICENSE +1 -0
- package/README.md +151 -15
- 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 +343 -172
- package/dist/index.js +902 -279
- package/dist/llms.txt +11 -0
- package/package.json +7 -6
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Resultar v3.7+ API Guide
|
|
2
|
+
|
|
3
|
+
Use this reference after checking the version installed by the consuming project. The local package
|
|
4
|
+
types and source are authoritative when they differ from this guide.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Package Baseline](#package-baseline)
|
|
9
|
+
- [Core Types And Constructors](#core-types-and-constructors)
|
|
10
|
+
- [Composition And Recovery](#composition-and-recovery)
|
|
11
|
+
- [Tagged Errors](#tagged-errors)
|
|
12
|
+
- [Boundary Matching](#boundary-matching)
|
|
13
|
+
- [Async Boundaries](#async-boundaries)
|
|
14
|
+
- [Async Policies](#async-policies)
|
|
15
|
+
- [Collections And Control Flow](#collections-and-control-flow)
|
|
16
|
+
- [`safeTry`](#safetry)
|
|
17
|
+
- [ResultTask (Lazy Workflows)](#resulttask-lazy-workflows)
|
|
18
|
+
- [Observation And Cleanup](#observation-and-cleanup)
|
|
19
|
+
|
|
20
|
+
## Package Baseline
|
|
21
|
+
|
|
22
|
+
Resultar v3.7 is an ESM package targeting Node.js 24+. The repository currently uses TypeScript 7.
|
|
23
|
+
Do not infer consumer requirements from memory: inspect `package.json`, the lockfile, and exported
|
|
24
|
+
types first.
|
|
25
|
+
|
|
26
|
+
Prefer named imports and type-only imports:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { ResultAsync, createTaggedError, ok, safeTry, tryResult, tryResultAsync } from "resultar";
|
|
30
|
+
import type { StrictResult, StrictResultAsync } from "resultar";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`tryCatch` and `tryCatchAsync` remain compatibility aliases. Prefer `tryResult` and
|
|
34
|
+
`tryResultAsync` in new v3.7+ code.
|
|
35
|
+
|
|
36
|
+
## Core Types And Constructors
|
|
37
|
+
|
|
38
|
+
| Need | API |
|
|
39
|
+
| --------------------------------------------------- | ---------------------------------------- |
|
|
40
|
+
| Synchronous fallible value | `Result<T, E>` |
|
|
41
|
+
| Asynchronous fallible value | `ResultAsync<T, E>` |
|
|
42
|
+
| Error-only sync channel | `StrictResult<T, E extends Error>` |
|
|
43
|
+
| Error-only async channel | `StrictResultAsync<T, E extends Error>` |
|
|
44
|
+
| Successful value | `ok(value)` / `okAsync(value)` |
|
|
45
|
+
| Successful `undefined` | `unit()` / `unitAsync()` |
|
|
46
|
+
| Expected failure | `err(error)` / `errAsync(error)` |
|
|
47
|
+
| Catch synchronous throws now | `tryResult(fn, toError)` |
|
|
48
|
+
| Wrap a synchronous throwing function | `fromThrowable(fn, toError)` |
|
|
49
|
+
| Catch promise rejection or a throwing async factory | `tryResultAsync(input, toError)` |
|
|
50
|
+
| Wrap an async throwing function | `fromThrowableAsync(fn, toError)` |
|
|
51
|
+
| Deliberately leave Resultar at a terminal boundary | `runSync(result)` / `runPromise(result)` |
|
|
52
|
+
|
|
53
|
+
Use the named options form when it improves readability:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const parsed = tryResult({
|
|
57
|
+
try: () => JSON.parse(input) as unknown,
|
|
58
|
+
catch: (cause) => new ParsePayloadError({ cause }),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`runSync` returns the `Ok` value or throws the `Err`; `runPromise` resolves the `Ok` value or
|
|
63
|
+
rejects with the `Err`. Keep both at framework, bootstrap, CLI, or test boundaries.
|
|
64
|
+
|
|
65
|
+
## Composition And Recovery
|
|
66
|
+
|
|
67
|
+
- `map` transforms an `Ok` value with an infallible function.
|
|
68
|
+
- `mapErr` transforms an `Err` value.
|
|
69
|
+
- `filterOrElse` validates or narrows an `Ok` value and introduces a typed error on failure.
|
|
70
|
+
- `andThen` chains fallible work. On `ResultAsync`, it accepts sync or async Resultar work.
|
|
71
|
+
- `asyncAndThen` bridges a synchronous `Result` into `ResultAsync`.
|
|
72
|
+
- `orElse` recovers the complete error channel.
|
|
73
|
+
- `catchTag` recovers one tagged error.
|
|
74
|
+
- `catchTags` recovers selected tagged errors; unhandled variants stay in the error union.
|
|
75
|
+
- `pipe` applies named combinators without changing Result semantics.
|
|
76
|
+
|
|
77
|
+
Let each step expose its narrow error type and let composition infer the union:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
type CreateUserError = InvalidEmailError | UserExistsError | SaveUserError;
|
|
81
|
+
|
|
82
|
+
const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
|
|
83
|
+
validateEmail(input.email)
|
|
84
|
+
.asyncAndThen(ensureEmailAvailable)
|
|
85
|
+
.andThen((email) => saveUser({ ...input, email }));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Do not manually cast the final error union. A surprising inferred union is usually evidence that a
|
|
89
|
+
step has a broad or inaccurate signature.
|
|
90
|
+
|
|
91
|
+
## Tagged Errors
|
|
92
|
+
|
|
93
|
+
`createTaggedError` creates nominal `Error` subclasses with stable `_tag`, message-template props,
|
|
94
|
+
`cause`, `fingerprint`, `toJSON()`, and `.err()` support.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
class UserNotFoundError extends createTaggedError({
|
|
98
|
+
name: "UserNotFoundError",
|
|
99
|
+
message: "User $userId was not found in $source",
|
|
100
|
+
}) {}
|
|
101
|
+
|
|
102
|
+
const missing = UserNotFoundError.err({ userId, source: "database" });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Let `$variables` in the message template define required constructor props.
|
|
106
|
+
- Preserve the original failure with `cause` when adapting external code.
|
|
107
|
+
- Keep the configured `name` equal to the class name.
|
|
108
|
+
- Do not override the generated constructor.
|
|
109
|
+
- Treat `ErrorClass.is(value)` as a nominal `instanceof`-based guard; a matching `_tag` is not
|
|
110
|
+
sufficient.
|
|
111
|
+
- Avoid reserved template fields such as `_tag`, `name`, `message`, `messageTemplate`,
|
|
112
|
+
`fingerprint`, `stack`, and `cause`.
|
|
113
|
+
- Use `TaggedEnum`/`taggedEnum` for lightweight tagged data that should not be an `Error`.
|
|
114
|
+
- Use `redact`, `isRedacted`, and `revealRedacted` when sensitive error metadata must not serialize
|
|
115
|
+
or log by default.
|
|
116
|
+
- Use `findCause` or an instance's `findCause` to inspect a typed cause chain.
|
|
117
|
+
|
|
118
|
+
## Boundary Matching
|
|
119
|
+
|
|
120
|
+
Use branch methods inside an algorithm and matching when converting the completed result:
|
|
121
|
+
|
|
122
|
+
- `isOk()` / `isErr()` narrow for direct control flow.
|
|
123
|
+
- `match(onOk, onErr)` handles a simple boundary.
|
|
124
|
+
- `matchTags(onOk, handlers)` exhaustively maps tagged errors.
|
|
125
|
+
- `matchTagsPartial(onOk, handlers, fallback)` handles selected tags with a deliberate fallback.
|
|
126
|
+
- `matchError(error, handlers)` handles an error value directly.
|
|
127
|
+
- `matchErrorPartial(error, handlers, fallback)` partially handles an error value.
|
|
128
|
+
|
|
129
|
+
Include an `Error` handler whenever an error union can contain an untagged `Error`. Do not invent
|
|
130
|
+
`partialCatchTags`; `catchTags` already supports partial recovery.
|
|
131
|
+
|
|
132
|
+
## Async Boundaries
|
|
133
|
+
|
|
134
|
+
Choose the wrapper based on ownership of the promise:
|
|
135
|
+
|
|
136
|
+
- `fromPromise(existingPromise, toError)` wraps a promise that has already been created.
|
|
137
|
+
- `tryResultAsync(() => createPromise(), toError)` also catches synchronous factory throws and is
|
|
138
|
+
preferred when the agent controls creation.
|
|
139
|
+
- `fromSafePromise(promise)` is only for promises guaranteed not to reject by contract.
|
|
140
|
+
- `fromCallback(options)` adapts callback or subscription APIs and returns
|
|
141
|
+
`ResultAsync<T, E | AbortError>`. Supply cleanup and forward cancellation where supported.
|
|
142
|
+
- `AbortError` and `isAbortError` identify Resultar cancellation.
|
|
143
|
+
|
|
144
|
+
Always map `unknown` causes into a specific error near the external boundary:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const loadUser = (id: string): StrictResultAsync<User, LoadUserError> =>
|
|
148
|
+
tryResultAsync(
|
|
149
|
+
() => client.loadUser(id),
|
|
150
|
+
(cause) => new LoadUserError({ cause, id }),
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Async Policies
|
|
155
|
+
|
|
156
|
+
Use the built-in lazy policies instead of hand-written `Promise.race`, timers, or retry loops:
|
|
157
|
+
|
|
158
|
+
- `ResultAsync.retry(task, options)` retries typed failures and includes `AbortError` in the error
|
|
159
|
+
channel. Use `times`, `delayMs`, `jittered`, `while`, and the provided signal deliberately.
|
|
160
|
+
- `ResultAsync.retryOrElse(task, options)` applies a typed fallback after retry exhaustion. The
|
|
161
|
+
exhausted task error is replaced by the fallback error channel.
|
|
162
|
+
- `ResultAsync.timeout(task, options)` returns the task error or the typed `onTimeout` error.
|
|
163
|
+
- `ResultAsync.race(left, right)` and `raceAll(tasks)` return the first settled result.
|
|
164
|
+
- `ResultAsync.raceFirst(left, right)` returns the first `Ok`; if both fail, it returns the last
|
|
165
|
+
observed `Err`.
|
|
166
|
+
- `ResultAsync.raceWith(left, right, handlers)` exposes the winner and a handle to the still-running
|
|
167
|
+
task for custom cooperative policy.
|
|
168
|
+
- `ResultAsync.withResource({ acquire, use, release })` releases after every acquire-success path.
|
|
169
|
+
Release errors are best-effort and do not enter the result channel; model cleanup in `use` when it
|
|
170
|
+
must affect control flow.
|
|
171
|
+
|
|
172
|
+
Race, timeout, retry, callback, and resource helpers rely on cooperative cancellation. Pass their
|
|
173
|
+
`AbortSignal` into fetch, database, SDK, timer, or subscription work; aborting a wrapper cannot stop
|
|
174
|
+
an underlying operation that ignores the signal.
|
|
175
|
+
|
|
176
|
+
## Collections And Control Flow
|
|
177
|
+
|
|
178
|
+
Both `Result` and `ResultAsync` provide collection helpers:
|
|
179
|
+
|
|
180
|
+
- `combine` preserves array/tuple/record shape and returns the first error.
|
|
181
|
+
- `combineWithAllErrors` collects every error.
|
|
182
|
+
- `validateAll` collects all validation errors; async mapped use supports bounded `concurrency`.
|
|
183
|
+
- `zip` combines exactly two values.
|
|
184
|
+
- `firstSuccessOf` tries candidates until one succeeds and otherwise returns the final error.
|
|
185
|
+
- `forEach` stops at the first error; use `{ discard: true }` when successful values are irrelevant.
|
|
186
|
+
- `when`, `unless`, `loop`, and `iterate` model Resultar-native control flow where they improve
|
|
187
|
+
clarity.
|
|
188
|
+
|
|
189
|
+
Prefer `ResultAsync.forEach(items, mapper, { concurrency })` for bounded first-error processing and
|
|
190
|
+
`ResultAsync.validateAll(items, mapper, { concurrency })` for independent validation where every
|
|
191
|
+
error matters.
|
|
192
|
+
|
|
193
|
+
## `safeTry`
|
|
194
|
+
|
|
195
|
+
Use `safeTry` when a linear generator is clearer than a chain:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const workflow = safeTry(async function* () {
|
|
199
|
+
const input = yield* validateInput(raw);
|
|
200
|
+
const remote = yield* loadRemote(input);
|
|
201
|
+
const saved = yield* saveRemote(remote);
|
|
202
|
+
|
|
203
|
+
return ok(saved);
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Use `yield*` for `Result` and `ResultAsync`. Wrap raw promises before yielding. Avoid raw `await`,
|
|
208
|
+
`try/catch`, and legacy `safeUnwrap()` inside the generator.
|
|
209
|
+
|
|
210
|
+
## ResultTask (Lazy Workflows)
|
|
211
|
+
|
|
212
|
+
`ResultTask<A, E, R>` is a lazy, reusable description of work: building one starts nothing, and
|
|
213
|
+
each `run*` call executes it again. Never substitute `ResultAsync` (already-started) where
|
|
214
|
+
laziness matters.
|
|
215
|
+
|
|
216
|
+
- Build: `succeed`, `fail`, `fromResult`, `sync`, `try`, `tryPromise` (`try`/`tryPromise` map
|
|
217
|
+
external throws into `E`; `sync` treats them as defects).
|
|
218
|
+
- Compose lazily: `map`, `flatMap`, `catchAll`, `tap`, `gen` with `yield*` (also accepts
|
|
219
|
+
`yield* result`: `Ok` unwraps, `Err` short-circuits into `E`).
|
|
220
|
+
- Provide services: `yield* Tag`, then `provideService` / `provideServices` /
|
|
221
|
+
`provideServiceResolver` (data-first and curried); missing or incompatible providers are
|
|
222
|
+
compile-time errors, never runtime surprises.
|
|
223
|
+
- Resources: `acquireRelease` + `scoped`; LIFO awaited finalizers run on every exit; deferred
|
|
224
|
+
release errors live in `R` until `scoped` folds them into `E`; `catchAll` recovers a release
|
|
225
|
+
failure only after `scoped`.
|
|
226
|
+
- Boundaries: `runExit` preserves `Success`/`Fail`/`Die`/`Interrupt`/`Sequential`; `runResult`
|
|
227
|
+
returns one `Err` and rejects the rest (`AbortError` for interrupts, `ResultTaskCauseError`
|
|
228
|
+
for composites); `runPromise` unwraps or rejects. Cancellation is cooperative: no fibers,
|
|
229
|
+
`race`, `timeout`, or scheduler exist on `ResultTask` yet — use the `ResultAsync` helpers.
|
|
230
|
+
|
|
231
|
+
`Result.gen` is the canonical name for linear Result workflows; `safeTry` remains as its alias.
|
|
232
|
+
For lazy `ResultTask` workflows with typed services and scopes, see `packages/resultar/README.md`
|
|
233
|
+
(Lazy Workflows With ResultTask) and `docs/rfcs/rfc-0001-result-task-core.md`.
|
|
234
|
+
|
|
235
|
+
## Observation And Cleanup
|
|
236
|
+
|
|
237
|
+
On `Result` and `ResultAsync`, `tap`, `tapError`, and `log` preserve the original result. Callback throws and rejected callback
|
|
238
|
+
promises are intentionally ignored, so use them only for best-effort logging, metrics, tracing, and
|
|
239
|
+
observation. Use `andThen`, `orElse`, or a Resultar boundary when side-effect failure must alter the
|
|
240
|
+
workflow.
|
|
241
|
+
|
|
242
|
+
`toDisposable` and `toAsyncDisposable` integrate with Node.js `using` / `await using`; their
|
|
243
|
+
cleanup callbacks are also best-effort. Prefer `withResource` for acquire/use/release workflows.
|
|
244
|
+
|
|
245
|
+
On `ResultTask`, `tap` and `tapError` are lazy effectful composition. Returned task errors and
|
|
246
|
+
requirements join the workflow type. A thrown callback or rejected Promise becomes `Die`;
|
|
247
|
+
a returned `ResultTask.fail(error)` becomes a typed failure. Use an explicitly recovered task
|
|
248
|
+
when observation must be best-effort. `runExit` preserves these exits; `runResult` can reject.
|
|
249
|
+
Do not substitute `ResultAsync.withResource` for awaited ResultTask resource ownership.
|
|
@@ -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.
|