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
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ and optional TypeScript-backed diagnostics.
|
|
|
54
54
|
- [HTTP Request Packages](#http-request-packages)
|
|
55
55
|
- [API Decision Guide](#api-decision-guide)
|
|
56
56
|
- [More Documentation](#more-documentation)
|
|
57
|
+
- [Limitations](#limitations)
|
|
57
58
|
|
|
58
59
|
## Install
|
|
59
60
|
|
|
@@ -407,8 +408,9 @@ Prefer a factory with `tryResultAsync` when creating the promise can also throw
|
|
|
407
408
|
## Lazy Workflows With ResultTask
|
|
408
409
|
|
|
409
410
|
`ResultTask<T, E, R>` is the lazy workflow primitive in Resultar. Creating one does not start the
|
|
410
|
-
work; `runResult`, `runExit`, or `runPromise` executes it explicitly. `R` records
|
|
411
|
-
|
|
411
|
+
work; `runResult`, `runExit`, or `runPromise` executes it explicitly. `R` records required service
|
|
412
|
+
tags and resource scope requirements. Run boundaries supply the root scope automatically and
|
|
413
|
+
require an explicit environment for any remaining service tags.
|
|
412
414
|
|
|
413
415
|
Unlike `ResultAsync`, a `ResultTask` is a reusable description of work rather than an already-started
|
|
414
416
|
operation. Mapping, chaining, recovery, service provision, and generator composition all remain lazy.
|
|
@@ -418,11 +420,13 @@ operation. Mapping, chaining, recovery, service provision, and generator composi
|
|
|
418
420
|
| Create an immediate success or failure | `succeed`, `fail`, `fromResult` |
|
|
419
421
|
| Defer synchronous work | `sync`, `try` |
|
|
420
422
|
| Defer promise-producing work | `tryPromise` |
|
|
421
|
-
| Transform or chain | `map`, `flatMap`, `andThen` |
|
|
423
|
+
| Transform, observe, or chain | `map`, `mapError`, `flatMap`, `andThen`, `tap`, `tapError`, `as`, `match` |
|
|
422
424
|
| Recover typed failures | `catchAll` |
|
|
423
425
|
| Write a linear lazy workflow | `gen` with `yield*` |
|
|
424
|
-
| Declare and provide dependencies | `service`, `provideService`, `provideServices` |
|
|
426
|
+
| Declare and provide dependencies | `service`, `provideService`, `provideServices`, `provideServiceResolver` |
|
|
427
|
+
| Own resources and await cleanup | `acquireRelease`, `scoped` |
|
|
425
428
|
| Execute at the application boundary | `runExit`, `runResult`, `runPromise` |
|
|
429
|
+
| Bridge to started promises | `fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
|
|
426
430
|
|
|
427
431
|
```ts
|
|
428
432
|
import { ResultTask } from 'resultar'
|
|
@@ -445,9 +449,13 @@ Choose the execution boundary based on how much information the application need
|
|
|
445
449
|
|
|
446
450
|
| Boundary | Result |
|
|
447
451
|
| --- | --- |
|
|
448
|
-
| `runExit(task)` |
|
|
449
|
-
| `runResult(task)` |
|
|
450
|
-
| `runPromise(task)` | `T`;
|
|
452
|
+
| `runExit(task)` | Preserves `Success`, typed `Fail`, `Die`, `Interrupt`, and `Sequential` causes |
|
|
453
|
+
| `runResult(task)` | Returns a single typed failure as `Err`; rejects defects, interruption, and composite causes |
|
|
454
|
+
| `runPromise(task)` | Returns `T`; rejects every failure for integration with Promise-only APIs |
|
|
455
|
+
|
|
456
|
+
The error type of `runExit` and `runResult` includes deferred release failures from the root scope.
|
|
457
|
+
Interruption rejects with `AbortError`. A composite cause rejects with `ResultTaskCauseError`, whose
|
|
458
|
+
`cause` preserves the complete tree; use `runExit` to inspect it without rejection.
|
|
451
459
|
|
|
452
460
|
```ts
|
|
453
461
|
const controller = new AbortController()
|
|
@@ -470,9 +478,20 @@ const userName = loadUser('user_123')
|
|
|
470
478
|
.catchAll((error) => ResultTask.succeed(`unavailable: ${error.message}`))
|
|
471
479
|
```
|
|
472
480
|
|
|
473
|
-
`catchAll` recovers
|
|
474
|
-
`
|
|
475
|
-
|
|
481
|
+
`catchAll` recovers a single typed failure. Defects and interruption bypass recovery. Composite
|
|
482
|
+
causes bypass recovery as `Die(ResultTaskCauseError)` with the original tree on the error's `cause`;
|
|
483
|
+
this prevents removed error types from escaping as typed failures. The equivalent functional forms
|
|
484
|
+
are `ResultTask.map`, `ResultTask.flatMap`, and `ResultTask.catchAll`.
|
|
485
|
+
|
|
486
|
+
For promise adapters that do not need an error mapper, pass the lazy callback directly:
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
const response = ResultTask.tryPromise((signal) => fetch(url, { signal }))
|
|
490
|
+
// ResultTask<Response, unknown>: synchronous throws and rejections remain typed failures.
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
The callback runs only when the task executes. Use `tryPromise({ try, catch })` when you need a
|
|
494
|
+
specific error type; both forms receive the runtime signal and preserve interruption semantics.
|
|
476
495
|
|
|
477
496
|
Workflows can request typed services with `yield*` and receive them at the boundary. Pass the service
|
|
478
497
|
type and its literal identifier so the named environment remains checked:
|
|
@@ -510,6 +529,20 @@ await ResultTask.runResult(readyWithOne)
|
|
|
510
529
|
await ResultTask.runResult(readyWithAll)
|
|
511
530
|
```
|
|
512
531
|
|
|
532
|
+
Infrastructure such as a dependency-injection adapter can resolve service tags lazily with
|
|
533
|
+
`provideServiceResolver(task, { Logger: () => loggerTask })`. Each required identifier has a
|
|
534
|
+
typed lazy provider. Provider errors and external requirements remain in the resulting task type;
|
|
535
|
+
resource-scope requirements also remain visible. All three providers (`provideService`,
|
|
536
|
+
`provideServices`, `provideServiceResolver`) support data-first and curried (`.pipe`) forms.
|
|
537
|
+
A provider whose value is incompatible with the required service contract is rejected at compile
|
|
538
|
+
time, including through a reused curried provider; partial providers keep the remaining
|
|
539
|
+
requirements until they are satisfied at the boundary.
|
|
540
|
+
|
|
541
|
+
Adapters can use `ResultTask.makeScope()` to own resources across executions. Run tasks with
|
|
542
|
+
`owner.use(task)` and execute `owner.close()` after consumers finish. Failed uses roll back their
|
|
543
|
+
partial acquisitions. `ResultTask.memoize(task)` shares pending and successful executions and
|
|
544
|
+
allows retry after failure; its first execution supplies the environment and resource scope.
|
|
545
|
+
|
|
513
546
|
`ResultTask.gen` composes tasks and services linearly. On short-circuit, generator `finally` blocks
|
|
514
547
|
are closed and any yielded cleanup tasks or services are interpreted before execution completes:
|
|
515
548
|
|
|
@@ -523,9 +556,81 @@ const program = ResultTask.gen(function* () {
|
|
|
523
556
|
})
|
|
524
557
|
```
|
|
525
558
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
559
|
+
Generator `finally` retains its existing replacement policy if its cleanup fails. Use resource
|
|
560
|
+
scopes below when both the body and cleanup failure must be preserved.
|
|
561
|
+
|
|
562
|
+
Generators also accept `yield* result` for plain `Result` values: `Ok` unwraps its value and
|
|
563
|
+
`Err` short-circuits into `E`.
|
|
564
|
+
|
|
565
|
+
### Resource scopes and application lifecycle
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
const connection = ResultTask.acquireRelease({
|
|
569
|
+
acquire: ResultTask.tryPromise({
|
|
570
|
+
try: (signal) => connectDatabase(signal),
|
|
571
|
+
catch: (cause) => new ConnectionError({ cause }),
|
|
572
|
+
}),
|
|
573
|
+
release: (database, exit) => ResultTask.tryPromise({
|
|
574
|
+
try: () => database.close(exit),
|
|
575
|
+
catch: (cause) => new CloseError({ cause }),
|
|
576
|
+
}),
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
const program = ResultTask.scoped(ResultTask.gen(function* () {
|
|
580
|
+
const database = yield* connection
|
|
581
|
+
return yield* ResultTask.tryPromise({
|
|
582
|
+
try: (signal) => database.query('SELECT 1', signal),
|
|
583
|
+
catch: (cause) => new QueryError({ cause }),
|
|
584
|
+
})
|
|
585
|
+
}))
|
|
586
|
+
|
|
587
|
+
const exit = await ResultTask.runExit(program)
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Every run owns a root scope. `scoped` closes a child scope before the next task continues. Successful
|
|
591
|
+
acquisitions register finalizers in LIFO order; every finalizer is awaited once, even after a body
|
|
592
|
+
failure, defect, or interruption. Failed acquisition does not register its release. Earlier resources
|
|
593
|
+
are still released if a later acquisition fails. Each finalizer receives the same region exit,
|
|
594
|
+
before release failures are appended. Services provided around acquisition remain available during
|
|
595
|
+
release, including services used only by the release callback.
|
|
596
|
+
|
|
597
|
+
`acquireRelease` tracks deferred errors with `ResultTaskScope<ReleaseError>` in `R`. `scoped` removes
|
|
598
|
+
that requirement and adds its errors to `E`; run boundaries do the same for the root scope. Service
|
|
599
|
+
provision and `catchAll` before scope closure cannot erase pending release failures. To recover a
|
|
600
|
+
single release failure, put `catchAll` after `scoped`.
|
|
601
|
+
|
|
602
|
+
If use succeeds but release fails, the failure is returned as `Err`. If both fail, `runExit` retains
|
|
603
|
+
them as `Sequential(useCause, releaseCause)`; subsequent release failures are appended in order.
|
|
604
|
+
Release callbacks that throw produce defects. Resources acquired during a finalizer belong to a
|
|
605
|
+
private cleanup scope, which is closed before the next finalizer starts.
|
|
606
|
+
|
|
607
|
+
Cancellation is cooperative: tasks check the signal before starting, and promise operations receive
|
|
608
|
+
it. Rejection after abort is classified as interruption. The runtime waits for in-flight operations
|
|
609
|
+
to settle so a resource acquired during cancellation can still be registered and released. Cleanup
|
|
610
|
+
uses a fresh, non-aborted signal. An operation or finalizer that never settles can therefore keep
|
|
611
|
+
the run pending; there is no preemptive cancellation, scheduler, or `Fiber` runtime in this slice.
|
|
612
|
+
|
|
613
|
+
For application composition, keep ordinary factories and existing `StrictResultAsync` use cases.
|
|
614
|
+
Use service tags only when a program needs requirements supplied at execution. Put the **whole server
|
|
615
|
+
lifetime** inside the resource scope: acquire database, session, then HTTP; wait for shutdown; drain
|
|
616
|
+
HTTP, close session, then close database. Returning a server from a completed scope would return
|
|
617
|
+
already-released resources. TypeScript does not enforce resource reference lifetimes.
|
|
618
|
+
|
|
619
|
+
See the runnable [application lifecycle example](../../examples/resultar/src/application-lifecycle.ts)
|
|
620
|
+
and its [smoke test](../../examples/resultar/scripts/lifecycle-smoke.ts). Run `pnpm run example:resultar`
|
|
621
|
+
from the workspace root. Its factory and resource contracts return `ResultTask` with database,
|
|
622
|
+
session, and HTTP error types. Drivers expose Promises only inside `ResultTask.tryPromise` adapters,
|
|
623
|
+
which receive the runtime signal without threading it through every application interface.
|
|
624
|
+
Concrete adapters must clean up partial acquisitions before returning a
|
|
625
|
+
failure and implement request draining, SSE termination, and suitable deadlines.
|
|
626
|
+
|
|
627
|
+
`ResultAsync.withResource` keeps its existing best-effort release behavior. Existing `ResultAsync`
|
|
628
|
+
instances have already started; wrapping one does not make it lazy or automatically cancelable.
|
|
629
|
+
Use `ResultTask.fromResultAsync` to run a `ResultAsync` inside a task boundary: pass a
|
|
630
|
+
`(signal) => ResultAsync` factory to defer creation until execution, or an existing instance to
|
|
631
|
+
capture an already-started execution. Use `ResultTask.toResultAsync(task)` — or the
|
|
632
|
+
`ResultAsync.fromTask(task)` alias — to start a requirement-free task immediately on the default
|
|
633
|
+
runtime and consume it as `ResultAsync`.
|
|
529
634
|
|
|
530
635
|
## Production Async Policies
|
|
531
636
|
|
|
@@ -837,8 +942,9 @@ service needs.
|
|
|
837
942
|
| Write linear Result code | `Result.gen` (`safeTry` compatibility alias) |
|
|
838
943
|
| Describe reusable lazy work | `ResultTask.succeed`, `sync`, `try`, `tryPromise` |
|
|
839
944
|
| Compose or recover lazy work | `ResultTask.map`, `flatMap`, `andThen`, `catchAll`, `gen` |
|
|
840
|
-
| Require or bind typed services | `ResultTask.service`, `provideService`, `provideServices` |
|
|
945
|
+
| Require or bind typed services | `ResultTask.service`, `provideService`, `provideServices`, `provideServiceResolver` |
|
|
841
946
|
| Execute lazy work explicitly | `ResultTask.runExit`, `runResult`, `runPromise` |
|
|
947
|
+
| Bridge to started promises | `ResultTask.fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
|
|
842
948
|
| Handle a final boundary | `match`, `matchTags`, `matchTagsPartial` |
|
|
843
949
|
| Combine independent results | `zip`, `combine`, `combineWithAllErrors` |
|
|
844
950
|
| Try ordered fallback candidates | `firstSuccessOf` |
|
|
@@ -856,10 +962,20 @@ The
|
|
|
856
962
|
covers overloads, aliases, callback semantics, collection helpers, conditional helpers, tagged
|
|
857
963
|
reasons, disposable results, and compatibility APIs.
|
|
858
964
|
|
|
965
|
+
## Coding agents
|
|
966
|
+
|
|
967
|
+
Each npm build includes `dist/llms.txt`, `dist/agent/SKILL.md`, focused references, a compiled
|
|
968
|
+
core workflow example, and `dist/agent/versions.json`. Start from these installed files so the
|
|
969
|
+
guide matches the package version. Companion package READMEs take precedence when their installed
|
|
970
|
+
versions differ from the recorded companion versions. The online `main` guide tracks development.
|
|
971
|
+
|
|
972
|
+
The guide distinguishes `Result.gen` from `ResultTask.gen`, immediate from lazy execution,
|
|
973
|
+
observation from effectful `tap`, and `runResult` from the non-rejecting `runExit` boundary.
|
|
974
|
+
|
|
859
975
|
## More Documentation
|
|
860
976
|
|
|
861
977
|
- [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
|
|
862
|
-
- [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/
|
|
978
|
+
- [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/docs/rfcs/rfc-0001-result-task-core.md)
|
|
863
979
|
- [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
|
|
864
980
|
- [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
|
|
865
981
|
- [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)
|
|
@@ -871,6 +987,26 @@ reasons, disposable results, and compatibility APIs.
|
|
|
871
987
|
|
|
872
988
|
Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
|
|
873
989
|
|
|
990
|
+
## Limitations
|
|
991
|
+
|
|
992
|
+
ResultTask implements the RFC 0001 lazy core, generator with services, and resource scopes. The
|
|
993
|
+
following are explicitly out of scope for this slice:
|
|
994
|
+
|
|
995
|
+
- No `Stream`, STM, caching, clustering, RPC, or platform layer; this is not a full Effect API port.
|
|
996
|
+
- No implicit global runtime: every `run*` call creates its own root scope and signal, and simple
|
|
997
|
+
tasks need no services or configuration.
|
|
998
|
+
- No fibers or structured concurrency yet: `forkChild`/`join`/`interrupt`, task `race`/`timeout`,
|
|
999
|
+
and uniform `all`/`forEach` over the task runtime are still Phase 3. The `Cause` model covers
|
|
1000
|
+
`Fail`, `Die`, `Interrupt`, and `Sequential`; `Parallel` is reserved for that future work. Use the
|
|
1001
|
+
`ResultAsync` race, timeout, retry, and concurrency helpers in the meantime — they keep their own
|
|
1002
|
+
eager, per-helper protocols.
|
|
1003
|
+
- No `Schedule`, schedule-based retry, or injectable clock yet (Phase 4).
|
|
1004
|
+
- No `Layer` system in the core: service graphs with their own lifecycle belong to the optional
|
|
1005
|
+
`resultar-di` package, and the core does not depend on it.
|
|
1006
|
+
- `ResultAsync` stays eager and awaitable. `fromResultAsync` on an existing instance captures an
|
|
1007
|
+
already-started execution; only the factory form defers creation.
|
|
1008
|
+
- Validated on Node.js 24+ only; other runtimes such as Bun are unvalidated.
|
|
1009
|
+
|
|
874
1010
|
## License
|
|
875
1011
|
|
|
876
1012
|
MIT
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resultar
|
|
3
|
+
description: Build, review, migrate, test, and document TypeScript code using Resultar v3.7+, including Result, ResultAsync, ResultTask, StrictResult, createTaggedError, Result.gen/safeTry, request adapters, and resultar-check. Use when expected failures should become typed values, thrown or rejected work must be wrapped, Resultar pipelines or async policies need design, HTTP JSON needs TypeBox or Zod validation, Resultar diagnostics need configuration, or an existing Resultar codebase needs review or migration.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: [typescript, error-handling]
|
|
6
|
+
domains: [foundation]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Resultar Engineering
|
|
10
|
+
|
|
11
|
+
Use Resultar to keep expected failures explicit in TypeScript without introducing an application
|
|
12
|
+
runtime. Produce code that preserves concrete success and error types from the domain boundary to
|
|
13
|
+
the final transport, job, CLI, or integration boundary.
|
|
14
|
+
|
|
15
|
+
## Start From Repository Truth
|
|
16
|
+
|
|
17
|
+
1. Inspect the consuming project's `package.json`, lockfile, Resultar version, module format, and
|
|
18
|
+
TypeScript version before proposing code.
|
|
19
|
+
2. Prefer the installed package types and local source over remembered APIs. The core package
|
|
20
|
+
ships this guide at `node_modules/resultar/dist/agent/SKILL.md`; check its `versions.json` before
|
|
21
|
+
using companion-package recipes. Online `main` documentation may describe a newer release.
|
|
22
|
+
3. In the Resultar repository, use these sources in order:
|
|
23
|
+
- `packages/resultar/src/index.ts` for public exports.
|
|
24
|
+
- `packages/resultar/README.md` and `DOCUMENTATION.md` for current semantics and examples.
|
|
25
|
+
- `packages/check/README.md` for diagnostics.
|
|
26
|
+
- `packages/request*/README.md` and `examples/request/` for request integrations.
|
|
27
|
+
4. Adapt examples when the consumer uses an older Resultar version; do not silently emit APIs
|
|
28
|
+
that are absent locally.
|
|
29
|
+
|
|
30
|
+
## Choose The Execution Model
|
|
31
|
+
|
|
32
|
+
| Contract | `Result` / `Result.gen` | `ResultAsync` | `ResultTask` |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| Execution | Sync values; `Result.gen` also accepts async generators | Starts immediately | Building/composing starts nothing; each `run*` executes again |
|
|
35
|
+
| Generator return | `return ok(value)`; `yield*` short-circuits failures | Compose through `Result.gen` with `yield*` | `return value`; compose tasks, services and Results with `yield*` |
|
|
36
|
+
| Error transform / recovery | `mapErr` / `orElse` | `mapErr` / `orElse` | `mapError` / `catchAll` |
|
|
37
|
+
| `tap` / `tapError` failure | Ignored observation failure | Ignored observation failure | Returned task failures join `E`; throws/rejected Promises become `Die` |
|
|
38
|
+
| Cleanup | Disposal callbacks are best-effort | `withResource` release is best-effort | `acquireRelease` + `scoped` await release and preserve failures |
|
|
39
|
+
|
|
40
|
+
Use `ResultAsync` for existing Promise-shaped use cases. Choose `ResultTask` for reusable lazy
|
|
41
|
+
work, typed service requirements, or owned resource lifetimes. Preserve existing eager/lazy
|
|
42
|
+
contracts; converting an already-started Promise or ResultAsync cannot undo its execution.
|
|
43
|
+
`ResultTask.runResult` can reject on defects, interruption, and composite causes. Use `runExit`
|
|
44
|
+
when the boundary must inspect every exit without rejection. See [API details](references/api.md).
|
|
45
|
+
For DI, Hono and Fastify, read [services and HTTP scopes](references/services.md).
|
|
46
|
+
|
|
47
|
+
## Apply Opinionated Defaults
|
|
48
|
+
|
|
49
|
+
- Return `Result<T, E>` for synchronous fallible work and `ResultAsync<T, E>` for asynchronous
|
|
50
|
+
fallible work.
|
|
51
|
+
- Prefer `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` at
|
|
52
|
+
application, service, HTTP, job, queue, CLI, and integration boundaries.
|
|
53
|
+
- Return `ResultAsync` / `StrictResultAsync` directly from async services and repositories instead
|
|
54
|
+
of `Promise<T>`, `Promise<Result>` or `Promise<StrictResult>`. Map external driver failures to
|
|
55
|
+
typed errors in the repository adapter; services compose that result directly. Remove the
|
|
56
|
+
function's `async` wrapper; changing only its return annotation cannot change the runtime value.
|
|
57
|
+
- Use `preferResultAsync: "error"` with `preferResultAsyncMode: "all"` to enforce these application
|
|
58
|
+
contracts. Document local rule suppressions for native framework/driver callbacks that require
|
|
59
|
+
Promise interoperability.
|
|
60
|
+
- Prefer `createTaggedError` for expected domain and application failures that need stable identity,
|
|
61
|
+
metadata, causes, serialization, or exhaustive handling.
|
|
62
|
+
- Keep error unions specific. Let composition widen them naturally; do not collapse them to `Error`
|
|
63
|
+
before the boundary.
|
|
64
|
+
- Return `Err` for expected failures. Reserve `throw` for programmer defects, invalid library
|
|
65
|
+
arguments, or deliberate terminal/process boundaries.
|
|
66
|
+
- Wrap third-party throws and promise rejections at the edge, then keep the internal flow Resultar-
|
|
67
|
+
native.
|
|
68
|
+
- Avoid `T | Error`, sentinel `null`, broad `try/catch`, rejected expected failures, and unsafe casts
|
|
69
|
+
as substitutes for a Resultar error channel.
|
|
70
|
+
- Keep `_unsafeUnwrap()` and `_unsafeUnwrapErr()` in tests. Use `unwrapOrThrow()` only when throwing
|
|
71
|
+
is explicitly the intended boundary behavior.
|
|
72
|
+
|
|
73
|
+
## Choose The Composition Operator
|
|
74
|
+
|
|
75
|
+
| Need | Prefer |
|
|
76
|
+
| ----------------------------------------- | ------------------------ |
|
|
77
|
+
| Transform an `Ok` without failure | `map` |
|
|
78
|
+
| Transform an `Err` | `mapErr` |
|
|
79
|
+
| Validate an `Ok` with a predicate | `filterOrElse` |
|
|
80
|
+
| Continue with a sync fallible step | `andThen` |
|
|
81
|
+
| Continue from `Result` into `ResultAsync` | `asyncAndThen` |
|
|
82
|
+
| Recover the whole error channel | `orElse` |
|
|
83
|
+
| Recover selected tagged errors | `catchTag` / `catchTags` |
|
|
84
|
+
| Branch directly | `isOk()` / `isErr()` |
|
|
85
|
+
| Convert a result at a boundary | `match` / `matchTags` |
|
|
86
|
+
| Map selected tagged errors with fallback | `matchTagsPartial` |
|
|
87
|
+
| Reuse a named Result combinator | `pipe` |
|
|
88
|
+
| Write a readable linear pipeline | `safeTry` with `yield*` |
|
|
89
|
+
|
|
90
|
+
Use `catchTags` for partial pipeline recovery; unhandled tags stay in the error channel. Do not
|
|
91
|
+
invent `partialCatchTags`. Use `matchTagsPartial` only for boundary matching with a deliberate
|
|
92
|
+
fallback.
|
|
93
|
+
|
|
94
|
+
## Model Tagged Errors
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { createTaggedError, ok } from "resultar";
|
|
98
|
+
import type { StrictResult } from "resultar";
|
|
99
|
+
|
|
100
|
+
class InvalidEmailError extends createTaggedError({
|
|
101
|
+
name: "InvalidEmailError",
|
|
102
|
+
message: "Invalid email $email",
|
|
103
|
+
}) {}
|
|
104
|
+
|
|
105
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
106
|
+
email.includes("@") ? ok(email) : InvalidEmailError.err({ email });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- Let message-template variables define required constructor props.
|
|
110
|
+
- Preserve `cause` when converting an external failure.
|
|
111
|
+
- Treat `ErrorClass.is(value)` as nominal; do not rely on spoofed `_tag` objects.
|
|
112
|
+
- Use `TaggedEnum` only for lightweight tagged domain variants that do not need to be real errors.
|
|
113
|
+
- Include an `Error` handler when a matched union can contain untagged errors.
|
|
114
|
+
|
|
115
|
+
## Compose Async Work
|
|
116
|
+
|
|
117
|
+
Wrap uncontrolled async work with the helper that matches how it is created:
|
|
118
|
+
|
|
119
|
+
- Use `fromPromise(promise, toError)` for an already-created promise.
|
|
120
|
+
- Use `tryResultAsync({ try, catch })` or `tryResultAsync(factory, toError)` when creating the
|
|
121
|
+
promise can throw synchronously.
|
|
122
|
+
- Use `fromSafePromise` only when rejection is impossible by contract.
|
|
123
|
+
- Use `fromCallback` for callback or subscription APIs with explicit cleanup.
|
|
124
|
+
- Use `runPromise` only at a terminal boundary that intentionally resolves or throws.
|
|
125
|
+
|
|
126
|
+
Prefer lazy Resultar policies over ad hoc timers or `Promise.race`:
|
|
127
|
+
|
|
128
|
+
- Use `ResultAsync.retry` / `retryOrElse` for typed retry and fallback policy.
|
|
129
|
+
- Use `timeout`, `race`, `raceAll`, or `raceFirst` for signal-aware concurrency.
|
|
130
|
+
- Use mapped `forEach` for first-error batch work and mapped `validateAll` for collecting every
|
|
131
|
+
independent validation error.
|
|
132
|
+
- Use `withResource` when successful acquisition must always trigger best-effort release.
|
|
133
|
+
- Pass the provided abort signal into the underlying client; cancellation is cooperative.
|
|
134
|
+
|
|
135
|
+
On `Result` and `ResultAsync`, use `tap`, `tapError`, and `log` for best-effort observation.
|
|
136
|
+
`ResultTask.tap` and `tapError` participate in failure handling; they do not swallow failures. Use `andThen`, `orElse`, or a
|
|
137
|
+
Resultar wrapper when callback failure must change control flow.
|
|
138
|
+
|
|
139
|
+
## Use `Result.gen` For Linear Result Flows
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
|
|
143
|
+
Result.gen(async function* () {
|
|
144
|
+
const email = yield* validateEmail(input.email);
|
|
145
|
+
const available = yield* ensureEmailAvailable(email);
|
|
146
|
+
const user = yield* insertUser({ ...input, email: available });
|
|
147
|
+
|
|
148
|
+
return ok(user);
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `safeTry` is the top-level alias of `Result.gen`; both have the same rules.
|
|
153
|
+
- Use `yield*` for both `Result` and `ResultAsync` values.
|
|
154
|
+
- Wrap raw promises before yielding them.
|
|
155
|
+
- Do not use raw `await` or broad `try/catch` inside a Resultar `safeTry` workflow.
|
|
156
|
+
- Prefer `yield* result` over legacy `safeUnwrap()`.
|
|
157
|
+
|
|
158
|
+
## Keep Boundaries Concrete
|
|
159
|
+
|
|
160
|
+
- Define request, command, and event payloads with concrete types; do not widen them to generic JSON
|
|
161
|
+
records before the use case.
|
|
162
|
+
- Keep transport concerns out of domain code. Convert tagged errors to HTTP responses, CLI exit
|
|
163
|
+
codes, job outcomes, or logs at the outer boundary.
|
|
164
|
+
- Use exhaustive `matchTags` when every tagged error needs a distinct mapping.
|
|
165
|
+
- Use local `catchTag` / `catchTags` only when recovery belongs to domain policy.
|
|
166
|
+
|
|
167
|
+
## Handle HTTP JSON Requests
|
|
168
|
+
|
|
169
|
+
Use the Resultar request family instead of repeating fetch, parsing, validation, retry, and error
|
|
170
|
+
mapping logic:
|
|
171
|
+
|
|
172
|
+
- Use `resultar-request` with `validator` or `decode` for schema-library-agnostic boundaries.
|
|
173
|
+
- Use `resultar-request-typebox` when contracts use TypeBox and preserve `Static<typeof schema>`.
|
|
174
|
+
- Use `resultar-request-zod` when contracts use Zod and preserve transformed
|
|
175
|
+
`z.output<typeof schema>`.
|
|
176
|
+
- Map request, HTTP, invalid-JSON, and validation failures to concrete tagged errors when they cross
|
|
177
|
+
an application boundary.
|
|
178
|
+
|
|
179
|
+
Read `references/integrations.md` before implementing request adapters or diagnostics.
|
|
180
|
+
|
|
181
|
+
## Validate In The Preferred Order
|
|
182
|
+
|
|
183
|
+
1. Run the `resultar-check` CLI as the authoritative project and CI gate. It runs TypeScript and the
|
|
184
|
+
full Resultar rule set against the project.
|
|
185
|
+
2. Configure `resultar-check lsp` as the editor's stdio language server for inline diagnostics and
|
|
186
|
+
quick fixes. It reads the same project configuration as the CLI.
|
|
187
|
+
3. Use JSONL, SARIF, or JUnit output when another CI or review system needs structured diagnostics;
|
|
188
|
+
do not present those consumers as replacements for the native checker.
|
|
189
|
+
4. Run the repository's native formatting, build, tests, analysis, package smoke, and example
|
|
190
|
+
workflows when available.
|
|
191
|
+
|
|
192
|
+
JSONL fixes distinguish `correction` from `intentional-discard`; LSP actions carry the same
|
|
193
|
+
classification in `data.kind`. Do not apply a discard just to silence the checker: `void task`
|
|
194
|
+
does not execute a lazy task or handle its errors. Corrections still need compilation and tests.
|
|
195
|
+
|
|
196
|
+
Treat stable `resultar/*` rule IDs as architecture feedback. Fix the typed boundary or composition
|
|
197
|
+
problem instead of suppressing diagnostics broadly. Use narrow ignore patterns only for deliberate
|
|
198
|
+
test, script, generated, or process-boundary exceptions.
|
|
199
|
+
|
|
200
|
+
## Test And Document Behavior
|
|
201
|
+
|
|
202
|
+
- Cover `Ok` and every expected `Err` path with runtime tests.
|
|
203
|
+
- Cover inferred error unions, tagged-error props, narrowing, and exhaustive handlers with type
|
|
204
|
+
tests. Use `expectTypeOf` and `@ts-expect-error` where supported.
|
|
205
|
+
- Use the consuming project's test runner. Use `vite-plus/test` inside this repository.
|
|
206
|
+
- Test retry, timeout, cancellation, concurrency, and cleanup policies with deterministic fakes.
|
|
207
|
+
- Derive documentation examples from checked source or runnable examples and keep package links
|
|
208
|
+
reciprocal when packages are alternatives or adapters.
|
|
209
|
+
|
|
210
|
+
## Review Before Finishing
|
|
211
|
+
|
|
212
|
+
- Run the project's Resultar CLI check after edits.
|
|
213
|
+
- Confirm no Result, ResultAsync, or ResultTask value is silently discarded.
|
|
214
|
+
- Confirm mapped errors preserve `cause` and useful metadata.
|
|
215
|
+
- Confirm boundary handlers cover the actual error union.
|
|
216
|
+
- Confirm request validation derives the downstream type instead of widening it.
|
|
217
|
+
- Confirm validation claims match commands that actually passed.
|
|
218
|
+
|
|
219
|
+
## Load References Selectively
|
|
220
|
+
|
|
221
|
+
- Read `references/api.md` for current exports, collection helpers, and async policy semantics.
|
|
222
|
+
- Read `references/patterns.md` for copyable implementation and migration patterns.
|
|
223
|
+
- Read `references/integrations.md` for request adapters and the CLI/editor/lint workflow.
|
|
224
|
+
- Read `references/review-checklist.md` for reviews, migrations, and final validation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createTaggedError, ok, Result, ResultTask } from 'resultar';
|
|
2
|
+
export class InvalidEmailError extends createTaggedError({
|
|
3
|
+
name: 'InvalidEmailError',
|
|
4
|
+
message: 'Invalid email $email',
|
|
5
|
+
}) {
|
|
6
|
+
}
|
|
7
|
+
export const validateEmail = (email) => {
|
|
8
|
+
const normalized = email.trim().toLowerCase();
|
|
9
|
+
return normalized.includes('@') ? ok(normalized) : InvalidEmailError.err({ email });
|
|
10
|
+
};
|
|
11
|
+
export const account = (email) => Result.gen(function* () {
|
|
12
|
+
const normalized = yield* validateEmail(email);
|
|
13
|
+
return ok({ email: normalized });
|
|
14
|
+
});
|
|
15
|
+
export const Greeting = ResultTask.service('greeting');
|
|
16
|
+
// Creating this task runs no work. The boundary supplies its typed dependency.
|
|
17
|
+
export const welcome = (email) => ResultTask.gen(function* () {
|
|
18
|
+
const normalized = yield* validateEmail(email);
|
|
19
|
+
const greeting = yield* Greeting;
|
|
20
|
+
return `${greeting}, ${normalized}`;
|
|
21
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createTaggedError, ok, Result, ResultTask } from 'resultar'
|
|
2
|
+
import type { StrictResult } from 'resultar'
|
|
3
|
+
|
|
4
|
+
export class InvalidEmailError extends createTaggedError({
|
|
5
|
+
name: 'InvalidEmailError',
|
|
6
|
+
message: 'Invalid email $email',
|
|
7
|
+
}) {}
|
|
8
|
+
|
|
9
|
+
export const validateEmail = (email: string): StrictResult<string, InvalidEmailError> => {
|
|
10
|
+
const normalized = email.trim().toLowerCase()
|
|
11
|
+
return normalized.includes('@') ? ok(normalized) : InvalidEmailError.err({ email })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const account = (email: string): StrictResult<{ email: string }, InvalidEmailError> =>
|
|
15
|
+
Result.gen(function* () {
|
|
16
|
+
const normalized = yield* validateEmail(email)
|
|
17
|
+
return ok({ email: normalized })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const Greeting = ResultTask.service<string, 'greeting'>('greeting')
|
|
21
|
+
|
|
22
|
+
// Creating this task runs no work. The boundary supplies its typed dependency.
|
|
23
|
+
export const welcome = (email: string) => ResultTask.gen(function* () {
|
|
24
|
+
const normalized = yield* validateEmail(email)
|
|
25
|
+
const greeting = yield* Greeting
|
|
26
|
+
return `${greeting}, ${normalized}`
|
|
27
|
+
})
|