resultar 3.6.0 → 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
@@ -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 the service tags
411
- still required by the workflow, so the execution boundary can require an explicit environment.
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)` | `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 |
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 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`.
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
- 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.
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` |
@@ -859,7 +965,7 @@ reasons, disposable results, and compatibility APIs.
859
965
  ## More Documentation
860
966
 
861
967
  - [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)
968
+ - [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/docs/rfcs/rfc-0001-result-task-core.md)
863
969
  - [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
864
970
  - [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
865
971
  - [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)
@@ -871,6 +977,26 @@ reasons, disposable results, and compatibility APIs.
871
977
 
872
978
  Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
873
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
+
874
1000
  ## License
875
1001
 
876
1002
  MIT