resultar 3.5.2 → 3.7.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 CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2019 Giorgio Delgado
4
+ Copyright (c) 2026 Inaiat Moraes
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -8,10 +8,12 @@ Resultar makes expected failures visible in function signatures instead of hidin
8
8
  ```ts
9
9
  Result<T, E>
10
10
  ResultAsync<T, E>
11
+ ResultTask<T, E, R>
11
12
  ```
12
13
 
13
14
  - `T` is the success value.
14
15
  - `E` is the expected failure.
16
+ - `R` is the typed service environment still required by a `ResultTask`.
15
17
  - Callers must handle the result before they can use the value.
16
18
 
17
19
  Resultar stays focused on explicit values. It does not require a framework, dependency injection
@@ -22,12 +24,13 @@ container, scheduler, or application runtime.
22
24
  | You need | Resultar gives you |
23
25
  | --- | --- |
24
26
  | Expected failures in the type system | `Result<T, E>` and `ResultAsync<T, E>` |
27
+ | Reusable lazy workflows | `ResultTask<T, E, R>` with explicit execution boundaries and typed services |
25
28
  | Production errors with useful metadata | Real `Error` subclasses from `createTaggedError` |
26
29
  | Composable sync and async workflows | `map`, `mapErr`, `andThen`, `asyncAndThen`, and `orElse` |
27
30
  | Linear code without scattered `try/catch` | `safeTry` with `yield*` short-circuiting |
28
31
  | Explicit production async policy | Typed timeout, retry, race, concurrency, and cleanup helpers |
29
32
  | Exhaustive application boundaries | `matchTags` for tagged error unions |
30
- | Guardrails for ignored results | The optional `resultar-check` CLI and language-server plugin |
33
+ | Guardrails for ignored results | The optional native `resultar-check` CLI |
31
34
 
32
35
  Resultar began as a fork of `neverthrow`. The current API keeps the familiar explicit Result model
33
36
  and adds Resultar-specific tagged errors, production async helpers, exhaustive tagged boundaries,
@@ -41,8 +44,9 @@ and optional TypeScript-backed diagnostics.
41
44
  - [Compose Results](#compose-results)
42
45
  - [Tagged Errors](#tagged-errors)
43
46
  - [Async Work](#async-work)
47
+ - [Lazy Workflows With ResultTask](#lazy-workflows-with-resulttask)
44
48
  - [Production Async Policies](#production-async-policies)
45
- - [Linear Workflows With safeTry](#linear-workflows-with-safetry)
49
+ - [Linear Workflows With Result.gen](#linear-workflows-with-resultgen)
46
50
  - [Recover Inside A Workflow](#recover-inside-a-workflow)
47
51
  - [Handle Results At Boundaries](#handle-results-at-boundaries)
48
52
  - [Strict Results At Production Boundaries](#strict-results-at-production-boundaries)
@@ -50,6 +54,7 @@ and optional TypeScript-backed diagnostics.
50
54
  - [HTTP Request Packages](#http-request-packages)
51
55
  - [API Decision Guide](#api-decision-guide)
52
56
  - [More Documentation](#more-documentation)
57
+ - [Limitations](#limitations)
53
58
 
54
59
  ## Install
55
60
 
@@ -400,6 +405,233 @@ Choose the wrapper that matches the external boundary:
400
405
 
401
406
  Prefer a factory with `tryResultAsync` when creating the promise can also throw synchronously.
402
407
 
408
+ ## Lazy Workflows With ResultTask
409
+
410
+ `ResultTask<T, E, R>` is the lazy workflow primitive in Resultar. Creating one does not start the
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.
414
+
415
+ Unlike `ResultAsync`, a `ResultTask` is a reusable description of work rather than an already-started
416
+ operation. Mapping, chaining, recovery, service provision, and generator composition all remain lazy.
417
+
418
+ | Need | API |
419
+ | --- | --- |
420
+ | Create an immediate success or failure | `succeed`, `fail`, `fromResult` |
421
+ | Defer synchronous work | `sync`, `try` |
422
+ | Defer promise-producing work | `tryPromise` |
423
+ | Transform, observe, or chain | `map`, `mapError`, `flatMap`, `andThen`, `tap`, `tapError`, `as`, `match` |
424
+ | Recover typed failures | `catchAll` |
425
+ | Write a linear lazy workflow | `gen` with `yield*` |
426
+ | Declare and provide dependencies | `service`, `provideService`, `provideServices`, `provideServiceResolver` |
427
+ | Own resources and await cleanup | `acquireRelease`, `scoped` |
428
+ | Execute at the application boundary | `runExit`, `runResult`, `runPromise` |
429
+ | Bridge to started promises | `fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
430
+
431
+ ```ts
432
+ import { ResultTask } from 'resultar'
433
+
434
+ const loadUser = (id: string) =>
435
+ ResultTask.tryPromise({
436
+ try: (signal) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),
437
+ catch: (cause) => new Error(`Could not load user: ${String(cause)}`),
438
+ })
439
+
440
+ const task = loadUser('user_123')
441
+ const result = await ResultTask.runResult(task)
442
+ ```
443
+
444
+ `sync` treats a thrown value as an unexpected defect. Use `try` or `tryPromise` when the boundary is
445
+ expected to throw or reject and should map that cause into `E`. `tryPromise` receives the execution
446
+ `AbortSignal`, so callers can cancel cooperative work without starting it early.
447
+
448
+ Choose the execution boundary based on how much information the application needs:
449
+
450
+ | Boundary | Result |
451
+ | --- | --- |
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.
459
+
460
+ ```ts
461
+ const controller = new AbortController()
462
+ const exit = await ResultTask.runExit(loadUser('user_123'), {
463
+ signal: controller.signal,
464
+ })
465
+
466
+ if (exit._tag === 'Failure' && exit.cause._tag === 'Die') {
467
+ console.error('Unexpected defect', exit.cause.defect)
468
+ }
469
+ ```
470
+
471
+ Instance methods and their static functional forms preserve laziness and infer combined error and
472
+ environment types:
473
+
474
+ ```ts
475
+ const userName = loadUser('user_123')
476
+ .map((user) => String(user.name))
477
+ .andThen((name) => ResultTask.succeed(name.trim()))
478
+ .catchAll((error) => ResultTask.succeed(`unavailable: ${error.message}`))
479
+ ```
480
+
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.
495
+
496
+ Workflows can request typed services with `yield*` and receive them at the boundary. Pass the service
497
+ type and its literal identifier so the named environment remains checked:
498
+
499
+ ```ts
500
+ const Database = ResultTask.service<
501
+ { findUser: (id: string) => Promise<string> },
502
+ 'Database'
503
+ >('Database')
504
+
505
+ const taskWithDatabase = ResultTask.gen(function* () {
506
+ const database = yield* Database
507
+ return yield* ResultTask.tryPromise({
508
+ try: () => database.findUser('user_123'),
509
+ catch: () => 'database-error' as const,
510
+ })
511
+ })
512
+
513
+ const resultWithDatabase = await ResultTask.runResult(taskWithDatabase, {
514
+ services: { Database: { findUser: async () => 'Ada' } },
515
+ })
516
+ ```
517
+
518
+ The environment requirement is part of the task type. A missing or misspelled `Database` property is
519
+ a compile-time error at `runResult`, `runExit`, or `runPromise`. Dependencies can also be bound before
520
+ the final boundary:
521
+
522
+ ```ts
523
+ const database = { findUser: async () => 'Ada' }
524
+
525
+ const readyWithOne = ResultTask.provideService(taskWithDatabase, Database, database)
526
+ const readyWithAll = ResultTask.provideServices(taskWithDatabase, { Database: database })
527
+
528
+ await ResultTask.runResult(readyWithOne)
529
+ await ResultTask.runResult(readyWithAll)
530
+ ```
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
+
546
+ `ResultTask.gen` composes tasks and services linearly. On short-circuit, generator `finally` blocks
547
+ are closed and any yielded cleanup tasks or services are interpreted before execution completes:
548
+
549
+ ```ts
550
+ const program = ResultTask.gen(function* () {
551
+ try {
552
+ return yield* loadUser('user_123')
553
+ } finally {
554
+ yield* ResultTask.sync(() => logger.info('load-user finished'))
555
+ }
556
+ })
557
+ ```
558
+
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`.
634
+
403
635
  ## Production Async Policies
404
636
 
405
637
  Resultar models common resilience policy in the expected error channel. These helpers use lazy tasks
@@ -507,19 +739,19 @@ See the full guides for
507
739
  [concurrency](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#async-concurrency-mapping),
508
740
  and [resource cleanup](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#resourceful-async-iterables).
509
741
 
510
- ## Linear Workflows With `safeTry`
742
+ ## Linear Workflows With `Result.gen`
511
743
 
512
- Use `safeTry` when a longer chain reads better as linear code. `yield*` extracts the `Ok` value and
513
- short-circuits on the first `Err`.
744
+ Use `Result.gen` when a longer chain reads better as linear code. `yield*` extracts the `Ok` value and
745
+ short-circuits on the first `Err`. `safeTry` remains an exact compatibility alias.
514
746
 
515
747
  ```ts
516
- import { safeTry } from 'resultar'
748
+ import { Result } from 'resultar'
517
749
  import type { ResultAsync } from 'resultar'
518
750
 
519
751
  const createAccountLinear = (
520
752
  input: string,
521
753
  ): ResultAsync<Account, CreateAccountAsyncError> =>
522
- safeTry(async function* () {
754
+ Result.gen(async function* () {
523
755
  const email = yield* normalizeEmail(input)
524
756
  yield* ensureAccountIsAvailable(email)
525
757
 
@@ -531,7 +763,7 @@ The object form can map unexpected throws from inside the generator while leavin
531
763
  values unchanged:
532
764
 
533
765
  ```ts
534
- const account = safeTry({
766
+ const account = Result.gen({
535
767
  async *try() {
536
768
  const email = yield* normalizeEmail(input)
537
769
  return persistAccount(email)
@@ -639,11 +871,10 @@ messages, causes, stack traces, and structured metadata.
639
871
 
640
872
  ## Prevent Ignored Results
641
873
 
642
- Result values are useful only when callers handle them. Install `resultar-check` with a project-local
643
- TypeScript 7+:
874
+ Result values are useful only when callers handle them. Install the native `resultar-check` CLI:
644
875
 
645
876
  ```sh
646
- pnpm add -D resultar-check "typescript@>=7"
877
+ pnpm add -D resultar-check
647
878
  ```
648
879
 
649
880
  Use the CLI as the authoritative local and CI check:
@@ -656,8 +887,8 @@ Use the CLI as the authoritative local and CI check:
656
887
  }
657
888
  ```
658
889
 
659
- The CLI runs TypeScript first and then Resultar diagnostics over the same `tsconfig.json`. Configure
660
- the TypeScript language-server plugin for equivalent feedback while editing:
890
+ The CLI uses TypeScript-Go to run compiler and Resultar diagnostics over the same `tsconfig.json`.
891
+ Configure its rules in the project file:
661
892
 
662
893
  ```json
663
894
  {
@@ -673,13 +904,13 @@ the TypeScript language-server plugin for equivalent feedback while editing:
673
904
  }
674
905
  ```
675
906
 
676
- After the CLI and language server are working, add the Oxlint, ESLint, or Deno Lint adapter only if
677
- you want optional low-latency syntax feedback. The adapters intentionally expose fewer rules because
678
- they do not have TypeScript type information.
907
+ The `plugins` entry is configuration consumed by the native CLI and stdio LSP server; it does not
908
+ install an editor extension. Run `resultar-check lsp` from an editor language-server configuration.
909
+ A separate TypeScript installation is not required.
679
910
 
680
911
  See the
681
912
  [`resultar-check` guide](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
682
- for all rules, editor setup, ignore patterns, and lint adapter configuration.
913
+ for all diagnostics, severities, modes, and ignore patterns.
683
914
 
684
915
  ## HTTP Request Packages
685
916
 
@@ -708,7 +939,12 @@ service needs.
708
939
  | Continue fallible work | `andThen`, `asyncAndThen` |
709
940
  | Recover from failure | `orElse`, `catchTag`, `catchTags` |
710
941
  | Wrap throwing or rejecting code | `tryResult`, `tryResultAsync`, `fromPromise` |
711
- | Write linear Result code | `safeTry` |
942
+ | Write linear Result code | `Result.gen` (`safeTry` compatibility alias) |
943
+ | Describe reusable lazy work | `ResultTask.succeed`, `sync`, `try`, `tryPromise` |
944
+ | Compose or recover lazy work | `ResultTask.map`, `flatMap`, `andThen`, `catchAll`, `gen` |
945
+ | Require or bind typed services | `ResultTask.service`, `provideService`, `provideServices`, `provideServiceResolver` |
946
+ | Execute lazy work explicitly | `ResultTask.runExit`, `runResult`, `runPromise` |
947
+ | Bridge to started promises | `ResultTask.fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
712
948
  | Handle a final boundary | `match`, `matchTags`, `matchTagsPartial` |
713
949
  | Combine independent results | `zip`, `combine`, `combineWithAllErrors` |
714
950
  | Try ordered fallback candidates | `firstSuccessOf` |
@@ -729,6 +965,7 @@ reasons, disposable results, and compatibility APIs.
729
965
  ## More Documentation
730
966
 
731
967
  - [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
968
+ - [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/docs/rfcs/rfc-0001-result-task-core.md)
732
969
  - [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
733
970
  - [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
734
971
  - [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)
@@ -740,6 +977,26 @@ reasons, disposable results, and compatibility APIs.
740
977
 
741
978
  Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
742
979
 
980
+ ## Limitations
981
+
982
+ ResultTask implements the RFC 0001 lazy core, generator with services, and resource scopes. The
983
+ following are explicitly out of scope for this slice:
984
+
985
+ - No `Stream`, STM, caching, clustering, RPC, or platform layer; this is not a full Effect API port.
986
+ - No implicit global runtime: every `run*` call creates its own root scope and signal, and simple
987
+ tasks need no services or configuration.
988
+ - No fibers or structured concurrency yet: `forkChild`/`join`/`interrupt`, task `race`/`timeout`,
989
+ and uniform `all`/`forEach` over the task runtime are still Phase 3. The `Cause` model covers
990
+ `Fail`, `Die`, `Interrupt`, and `Sequential`; `Parallel` is reserved for that future work. Use the
991
+ `ResultAsync` race, timeout, retry, and concurrency helpers in the meantime — they keep their own
992
+ eager, per-helper protocols.
993
+ - No `Schedule`, schedule-based retry, or injectable clock yet (Phase 4).
994
+ - No `Layer` system in the core: service graphs with their own lifecycle belong to the optional
995
+ `resultar-di` package, and the core does not depend on it.
996
+ - `ResultAsync` stays eager and awaitable. `fromResultAsync` on an existing instance captures an
997
+ already-started execution; only the factory form defers creation.
998
+ - Validated on Node.js 24+ only; other runtimes such as Bun are unvalidated.
999
+
743
1000
  ## License
744
1001
 
745
1002
  MIT