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.
@@ -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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "resultar-check": "3.2.0",
3
+ "resultar-di": "0.2.0",
4
+ "resultar-fastify": "0.2.0",
5
+ "resultar-hono": "0.2.0",
6
+ "resultar-request": "0.1.2",
7
+ "resultar-request-typebox": "0.1.2",
8
+ "resultar-request-zod": "0.1.2",
9
+ "resultar": "3.8.0"
10
+ }