resultar 3.5.1 → 3.6.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.
Files changed (4) hide show
  1. package/README.md +149 -18
  2. package/dist/index.d.ts +900 -760
  3. package/dist/index.js +286 -7
  4. package/package.json +10 -10
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)
@@ -400,6 +404,129 @@ Choose the wrapper that matches the external boundary:
400
404
 
401
405
  Prefer a factory with `tryResultAsync` when creating the promise can also throw synchronously.
402
406
 
407
+ ## Lazy Workflows With ResultTask
408
+
409
+ `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 the service tags
411
+ still required by the workflow, so the execution boundary can require an explicit environment.
412
+
413
+ Unlike `ResultAsync`, a `ResultTask` is a reusable description of work rather than an already-started
414
+ operation. Mapping, chaining, recovery, service provision, and generator composition all remain lazy.
415
+
416
+ | Need | API |
417
+ | --- | --- |
418
+ | Create an immediate success or failure | `succeed`, `fail`, `fromResult` |
419
+ | Defer synchronous work | `sync`, `try` |
420
+ | Defer promise-producing work | `tryPromise` |
421
+ | Transform or chain | `map`, `flatMap`, `andThen` |
422
+ | Recover typed failures | `catchAll` |
423
+ | Write a linear lazy workflow | `gen` with `yield*` |
424
+ | Declare and provide dependencies | `service`, `provideService`, `provideServices` |
425
+ | Execute at the application boundary | `runExit`, `runResult`, `runPromise` |
426
+
427
+ ```ts
428
+ import { ResultTask } from 'resultar'
429
+
430
+ const loadUser = (id: string) =>
431
+ ResultTask.tryPromise({
432
+ try: (signal) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),
433
+ catch: (cause) => new Error(`Could not load user: ${String(cause)}`),
434
+ })
435
+
436
+ const task = loadUser('user_123')
437
+ const result = await ResultTask.runResult(task)
438
+ ```
439
+
440
+ `sync` treats a thrown value as an unexpected defect. Use `try` or `tryPromise` when the boundary is
441
+ expected to throw or reject and should map that cause into `E`. `tryPromise` receives the execution
442
+ `AbortSignal`, so callers can cancel cooperative work without starting it early.
443
+
444
+ Choose the execution boundary based on how much information the application needs:
445
+
446
+ | Boundary | Result |
447
+ | --- | --- |
448
+ | `runExit(task)` | `Exit<T, E>` preserving `Success`, typed `Fail`, and unexpected `Die` causes |
449
+ | `runResult(task)` | `Result<T, E>`; a `Die` rejects instead of entering the typed error channel |
450
+ | `runPromise(task)` | `T`; typed failures and defects reject for integration with Promise-only APIs |
451
+
452
+ ```ts
453
+ const controller = new AbortController()
454
+ const exit = await ResultTask.runExit(loadUser('user_123'), {
455
+ signal: controller.signal,
456
+ })
457
+
458
+ if (exit._tag === 'Failure' && exit.cause._tag === 'Die') {
459
+ console.error('Unexpected defect', exit.cause.defect)
460
+ }
461
+ ```
462
+
463
+ Instance methods and their static functional forms preserve laziness and infer combined error and
464
+ environment types:
465
+
466
+ ```ts
467
+ const userName = loadUser('user_123')
468
+ .map((user) => String(user.name))
469
+ .andThen((name) => ResultTask.succeed(name.trim()))
470
+ .catchAll((error) => ResultTask.succeed(`unavailable: ${error.message}`))
471
+ ```
472
+
473
+ `catchAll` recovers only typed failures. Runtime defects remain defects and are visible through
474
+ `runExit`. The equivalent functional forms are `ResultTask.map`, `ResultTask.flatMap`, and
475
+ `ResultTask.catchAll`.
476
+
477
+ Workflows can request typed services with `yield*` and receive them at the boundary. Pass the service
478
+ type and its literal identifier so the named environment remains checked:
479
+
480
+ ```ts
481
+ const Database = ResultTask.service<
482
+ { findUser: (id: string) => Promise<string> },
483
+ 'Database'
484
+ >('Database')
485
+
486
+ const taskWithDatabase = ResultTask.gen(function* () {
487
+ const database = yield* Database
488
+ return yield* ResultTask.tryPromise({
489
+ try: () => database.findUser('user_123'),
490
+ catch: () => 'database-error' as const,
491
+ })
492
+ })
493
+
494
+ const resultWithDatabase = await ResultTask.runResult(taskWithDatabase, {
495
+ services: { Database: { findUser: async () => 'Ada' } },
496
+ })
497
+ ```
498
+
499
+ The environment requirement is part of the task type. A missing or misspelled `Database` property is
500
+ a compile-time error at `runResult`, `runExit`, or `runPromise`. Dependencies can also be bound before
501
+ the final boundary:
502
+
503
+ ```ts
504
+ const database = { findUser: async () => 'Ada' }
505
+
506
+ const readyWithOne = ResultTask.provideService(taskWithDatabase, Database, database)
507
+ const readyWithAll = ResultTask.provideServices(taskWithDatabase, { Database: database })
508
+
509
+ await ResultTask.runResult(readyWithOne)
510
+ await ResultTask.runResult(readyWithAll)
511
+ ```
512
+
513
+ `ResultTask.gen` composes tasks and services linearly. On short-circuit, generator `finally` blocks
514
+ are closed and any yielded cleanup tasks or services are interpreted before execution completes:
515
+
516
+ ```ts
517
+ const program = ResultTask.gen(function* () {
518
+ try {
519
+ return yield* loadUser('user_123')
520
+ } finally {
521
+ yield* ResultTask.sync(() => logger.info('load-user finished'))
522
+ }
523
+ })
524
+ ```
525
+
526
+ If cleanup itself fails or defects, that cleanup exit becomes the final exit. ResultTask 3.6 keeps
527
+ execution deliberately small: it provides laziness, typed services, cooperative cancellation, and
528
+ explicit exits, but does not yet include a scheduler, scopes, or `Fiber` runtime.
529
+
403
530
  ## Production Async Policies
404
531
 
405
532
  Resultar models common resilience policy in the expected error channel. These helpers use lazy tasks
@@ -507,19 +634,19 @@ See the full guides for
507
634
  [concurrency](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#async-concurrency-mapping),
508
635
  and [resource cleanup](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#resourceful-async-iterables).
509
636
 
510
- ## Linear Workflows With `safeTry`
637
+ ## Linear Workflows With `Result.gen`
511
638
 
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`.
639
+ Use `Result.gen` when a longer chain reads better as linear code. `yield*` extracts the `Ok` value and
640
+ short-circuits on the first `Err`. `safeTry` remains an exact compatibility alias.
514
641
 
515
642
  ```ts
516
- import { safeTry } from 'resultar'
643
+ import { Result } from 'resultar'
517
644
  import type { ResultAsync } from 'resultar'
518
645
 
519
646
  const createAccountLinear = (
520
647
  input: string,
521
648
  ): ResultAsync<Account, CreateAccountAsyncError> =>
522
- safeTry(async function* () {
649
+ Result.gen(async function* () {
523
650
  const email = yield* normalizeEmail(input)
524
651
  yield* ensureAccountIsAvailable(email)
525
652
 
@@ -531,7 +658,7 @@ The object form can map unexpected throws from inside the generator while leavin
531
658
  values unchanged:
532
659
 
533
660
  ```ts
534
- const account = safeTry({
661
+ const account = Result.gen({
535
662
  async *try() {
536
663
  const email = yield* normalizeEmail(input)
537
664
  return persistAccount(email)
@@ -639,11 +766,10 @@ messages, causes, stack traces, and structured metadata.
639
766
 
640
767
  ## Prevent Ignored Results
641
768
 
642
- Result values are useful only when callers handle them. Install `resultar-check` with a project-local
643
- TypeScript 7+:
769
+ Result values are useful only when callers handle them. Install the native `resultar-check` CLI:
644
770
 
645
771
  ```sh
646
- pnpm add -D resultar-check "typescript@>=7"
772
+ pnpm add -D resultar-check
647
773
  ```
648
774
 
649
775
  Use the CLI as the authoritative local and CI check:
@@ -656,8 +782,8 @@ Use the CLI as the authoritative local and CI check:
656
782
  }
657
783
  ```
658
784
 
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:
785
+ The CLI uses TypeScript-Go to run compiler and Resultar diagnostics over the same `tsconfig.json`.
786
+ Configure its rules in the project file:
661
787
 
662
788
  ```json
663
789
  {
@@ -673,13 +799,13 @@ the TypeScript language-server plugin for equivalent feedback while editing:
673
799
  }
674
800
  ```
675
801
 
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.
802
+ The `plugins` entry is configuration consumed by the native CLI and stdio LSP server; it does not
803
+ install an editor extension. Run `resultar-check lsp` from an editor language-server configuration.
804
+ A separate TypeScript installation is not required.
679
805
 
680
806
  See the
681
807
  [`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.
808
+ for all diagnostics, severities, modes, and ignore patterns.
683
809
 
684
810
  ## HTTP Request Packages
685
811
 
@@ -708,7 +834,11 @@ service needs.
708
834
  | Continue fallible work | `andThen`, `asyncAndThen` |
709
835
  | Recover from failure | `orElse`, `catchTag`, `catchTags` |
710
836
  | Wrap throwing or rejecting code | `tryResult`, `tryResultAsync`, `fromPromise` |
711
- | Write linear Result code | `safeTry` |
837
+ | Write linear Result code | `Result.gen` (`safeTry` compatibility alias) |
838
+ | Describe reusable lazy work | `ResultTask.succeed`, `sync`, `try`, `tryPromise` |
839
+ | Compose or recover lazy work | `ResultTask.map`, `flatMap`, `andThen`, `catchAll`, `gen` |
840
+ | Require or bind typed services | `ResultTask.service`, `provideService`, `provideServices` |
841
+ | Execute lazy work explicitly | `ResultTask.runExit`, `runResult`, `runPromise` |
712
842
  | Handle a final boundary | `match`, `matchTags`, `matchTagsPartial` |
713
843
  | Combine independent results | `zip`, `combine`, `combineWithAllErrors` |
714
844
  | Try ordered fallback candidates | `firstSuccessOf` |
@@ -729,6 +859,7 @@ reasons, disposable results, and compatibility APIs.
729
859
  ## More Documentation
730
860
 
731
861
  - [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
862
+ - [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/packages/resultar/RESULT-TASK-CORE-RFC.md)
732
863
  - [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
733
864
  - [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
734
865
  - [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)