better-effect 0.12.0 → 0.14.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/README.md +599 -30
- package/dist/adapters/iti.d.mts +2 -3
- package/dist/adapters/iti.d.mts.map +1 -1
- package/dist/adapters/iti.mjs +2 -2
- package/dist/bun.d.mts +95 -0
- package/dist/bun.d.mts.map +1 -0
- package/dist/bun.mjs +177 -0
- package/dist/bun.mjs.map +1 -0
- package/dist/{context-BUEf1qjL.mjs → context-DSGSqcJj.mjs} +14 -3
- package/dist/context-DSGSqcJj.mjs.map +1 -0
- package/dist/current-request-Bc1YHL0w.mjs +21 -0
- package/dist/current-request-Bc1YHL0w.mjs.map +1 -0
- package/dist/current-request-Dm-QQ3Xr.d.mts +22 -0
- package/dist/current-request-Dm-QQ3Xr.d.mts.map +1 -0
- package/dist/{effect-CK4YZaX0.mjs → effect-W0IQXY8P.mjs} +21 -2
- package/dist/effect-W0IQXY8P.mjs.map +1 -0
- package/dist/{errors-BKcsQ0ip.d.mts → errors-CpHMzPiP.d.mts} +6 -2
- package/dist/errors-CpHMzPiP.d.mts.map +1 -0
- package/dist/hono.d.mts +65 -27
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +177 -53
- package/dist/hono.mjs.map +1 -1
- package/dist/{index-CX6iAoCB.d.mts → index-3Bkl-sxS.d.mts} +7 -2
- package/dist/{index-CX6iAoCB.d.mts.map → index-3Bkl-sxS.d.mts.map} +1 -1
- package/dist/{index-B1nH4luq.d.mts → index-C2ed9Ilq.d.mts} +32 -21
- package/dist/index-C2ed9Ilq.d.mts.map +1 -0
- package/dist/{index-GYJ4qm9K.d.mts → index-ClNndn2-.d.mts} +93 -62
- package/dist/index-ClNndn2-.d.mts.map +1 -0
- package/dist/{index-BMdU_Z72.d.mts → index-Cy1VsU-u.d.mts} +2 -3
- package/dist/index-Cy1VsU-u.d.mts.map +1 -0
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +8 -8
- package/dist/{internal-identity-CbXoZIfw.mjs → internal-identity-BuHBtP0k.mjs} +2 -2
- package/dist/{internal-identity-CbXoZIfw.mjs.map → internal-identity-BuHBtP0k.mjs.map} +1 -1
- package/dist/layer-2kjHVv4N.mjs +343 -0
- package/dist/layer-2kjHVv4N.mjs.map +1 -0
- package/dist/{outcome-CB43bnFW.d.mts → layer-C2OaqTmB.d.mts} +683 -138
- package/dist/layer-C2OaqTmB.d.mts.map +1 -0
- package/dist/next.d.mts +192 -0
- package/dist/next.d.mts.map +1 -0
- package/dist/next.mjs +185 -0
- package/dist/next.mjs.map +1 -0
- package/dist/{node-context-DWsCUBRa.d.mts → node-context-1YEjrU9g.d.mts} +2 -2
- package/dist/{node-context-DWsCUBRa.d.mts.map → node-context-1YEjrU9g.d.mts.map} +1 -1
- package/dist/{node-context-lggRF4bR.mjs → node-context-g3bCcY9t.mjs} +2 -2
- package/dist/{node-context-lggRF4bR.mjs.map → node-context-g3bCcY9t.mjs.map} +1 -1
- package/dist/node.d.mts +11 -6
- package/dist/node.d.mts.map +1 -1
- package/dist/node.mjs +156 -10
- package/dist/node.mjs.map +1 -1
- package/dist/opentelemetry.d.mts +112 -0
- package/dist/opentelemetry.d.mts.map +1 -0
- package/dist/opentelemetry.mjs +871 -0
- package/dist/opentelemetry.mjs.map +1 -0
- package/dist/{registration-drnZk0SY.mjs → registration-Bwe2GbVu.mjs} +8 -2
- package/dist/registration-Bwe2GbVu.mjs.map +1 -0
- package/dist/runtime/explicit.d.mts +2 -2
- package/dist/runtime/explicit.mjs +1 -1
- package/dist/runtime/node.d.mts +3 -3
- package/dist/runtime/node.mjs +2 -2
- package/dist/runtime-qpcVwaz_.mjs +1680 -0
- package/dist/runtime-qpcVwaz_.mjs.map +1 -0
- package/dist/service-CO9VJQYa.mjs +74 -0
- package/dist/service-CO9VJQYa.mjs.map +1 -0
- package/dist/signal-5Rg9bUUe.mjs +52 -0
- package/dist/signal-5Rg9bUUe.mjs.map +1 -0
- package/dist/{standard-services-GBZ32tll.mjs → standard-services-B-iMM_W7.mjs} +39 -19
- package/dist/standard-services-B-iMM_W7.mjs.map +1 -0
- package/dist/standard-services.d.mts +4 -3
- package/dist/standard-services.mjs +3 -2
- package/dist/testing.d.mts +25 -6
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +51 -7
- package/dist/testing.mjs.map +1 -1
- package/dist/types-BFTqCCcw.d.mts +81 -0
- package/dist/types-BFTqCCcw.d.mts.map +1 -0
- package/dist/web-effect-B9hp9jIe.mjs +380 -0
- package/dist/web-effect-B9hp9jIe.mjs.map +1 -0
- package/dist/web.d.mts +56 -0
- package/dist/web.d.mts.map +1 -0
- package/dist/web.mjs +2 -0
- package/package.json +36 -11
- package/dist/context-BUEf1qjL.mjs.map +0 -1
- package/dist/effect-CK4YZaX0.mjs.map +0 -1
- package/dist/errors-BKcsQ0ip.d.mts.map +0 -1
- package/dist/index-B1nH4luq.d.mts.map +0 -1
- package/dist/index-BMdU_Z72.d.mts.map +0 -1
- package/dist/index-GYJ4qm9K.d.mts.map +0 -1
- package/dist/index-heuaRmXR.d.mts +0 -374
- package/dist/index-heuaRmXR.d.mts.map +0 -1
- package/dist/layer-D56fRdLt.mjs +0 -269
- package/dist/layer-D56fRdLt.mjs.map +0 -1
- package/dist/outcome-CB43bnFW.d.mts.map +0 -1
- package/dist/program-metadata-DhQCCirO.mjs +0 -260
- package/dist/program-metadata-DhQCCirO.mjs.map +0 -1
- package/dist/registration-drnZk0SY.mjs.map +0 -1
- package/dist/runtime-CBMrX8Wl.mjs +0 -776
- package/dist/runtime-CBMrX8Wl.mjs.map +0 -1
- package/dist/signal-D2UTcaaG.mjs +0 -105
- package/dist/signal-D2UTcaaG.mjs.map +0 -1
- package/dist/standard-services-GBZ32tll.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ bun add better-effect better-result
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The published Runtime entrypoint is officially supported on Node.js and Bun.
|
|
14
|
-
The
|
|
15
|
-
|
|
14
|
+
The repository uses the latest Bun release by default and the current Node.js
|
|
15
|
+
LTS for interoperability smoke tests. `bun run check` also deletes
|
|
16
16
|
and rebuilds `dist`, packs the result into a temporary consumer, and runs the
|
|
17
17
|
full Node/Bun `NodeRuntime` child-process suite.
|
|
18
18
|
|
|
@@ -149,6 +149,61 @@ const inspectDatabase = Effect.fn(async function* () {
|
|
|
149
149
|
await runtime.run(inspectDatabase)
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
Integration authors that need to trigger work later can capture the Runtime's
|
|
153
|
+
non-owning executor capability from an execution or Layer acquisition:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const captureExecutor = Effect.fn(async function* () {
|
|
157
|
+
const executor = yield* Runtime.executor<Database>()
|
|
158
|
+
return Result.ok(executor)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const executor = (await runtime.run(captureExecutor)).unwrap()
|
|
162
|
+
await executor.run(inspectDatabase)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`Runtime.Executor<R>` exposes `run`, `runWith`, and the opt-in
|
|
166
|
+
`runWithManaged` capability. The first two start a child execution and resolve
|
|
167
|
+
after its Scope closes; `runWithManaged` additionally separates readiness from
|
|
168
|
+
completion for boundaries such as `WebEffect.streamWith`, while keeping the
|
|
169
|
+
same request-local Services and Scope active until completion. The executor
|
|
170
|
+
does not expose `dispose`, `warmup`, `inspect`, backend or Scope ownership.
|
|
171
|
+
Applications normally use `runtime.run`; the contextual executor is mainly for
|
|
172
|
+
framework adapters and long-lived components.
|
|
173
|
+
|
|
174
|
+
For work that must live only as long as the current execution or lifecycle
|
|
175
|
+
Scope, use `Effect.forkScoped`. It returns immediately with a small task handle;
|
|
176
|
+
the task inherits the current Services and resolver, receives its own
|
|
177
|
+
cooperative-cancellation signal, and is interrupted and awaited before the
|
|
178
|
+
parent Scope releases its resources:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const pollOutbox = Effect.fn(async function* () {
|
|
182
|
+
const signal = yield* CurrentAbortSignal
|
|
183
|
+
|
|
184
|
+
while (!signal.aborted) {
|
|
185
|
+
await pollOnce()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Result.ok(undefined)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const OutboxLive = Layer.effectDiscard(
|
|
192
|
+
Effect.fn(async function* () {
|
|
193
|
+
const task = yield* Effect.forkScoped(pollOutbox)
|
|
194
|
+
await task.awaitExit()
|
|
195
|
+
|
|
196
|
+
return Result.ok(undefined)
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Use `task.await()` when the child Result should be returned or its defect should
|
|
202
|
+
be thrown. Use `task.awaitExit()` when interruption and defects must be
|
|
203
|
+
inspected without throwing. `Runtime.inspect()` reports active scoped tasks,
|
|
204
|
+
and Runtime observers receive task start/end events linked to their parent
|
|
205
|
+
execution; scoped tasks do not create independent Runtime executions.
|
|
206
|
+
|
|
152
207
|
Runtime boundaries inspect only nominal `better-result` values: `Result.err`
|
|
153
208
|
becomes a failed execution, while a plain object with `status: 'error'` is
|
|
154
209
|
still a successful value. Intermediate Results do not close the execution
|
|
@@ -344,9 +399,13 @@ await runtime.warmup()
|
|
|
344
399
|
Warmup failures include the Service and resolution path, release resources
|
|
345
400
|
already acquired, dispose the backend and reject the Runtime. Optional
|
|
346
401
|
observers expose Service resolution/acquisition, execution and Layer release
|
|
347
|
-
events without coupling the core to an observability SDK.
|
|
348
|
-
|
|
349
|
-
|
|
402
|
+
events without coupling the core to an observability SDK. Service events carry
|
|
403
|
+
an optional matching `executionId` only when they occur inside an active
|
|
404
|
+
execution; warmup and Runtime-root activity omit it. Execution events carry one
|
|
405
|
+
matching `executionId`, the optional Program name, copied readonly attributes,
|
|
406
|
+
and a monotonic `durationMs` measured through execution cleanup. An end event
|
|
407
|
+
keeps the primary program `outcome` and adds `cleanupFailure` when execution
|
|
408
|
+
Scope cleanup fails:
|
|
350
409
|
|
|
351
410
|
```ts
|
|
352
411
|
const runtime = await Runtime.make(AppLive, {
|
|
@@ -389,11 +448,12 @@ resolves Services, warms the Runtime, creates Scopes, invokes observers or
|
|
|
389
448
|
exposes providers, instances, signals, attributes or backend state. Execution
|
|
390
449
|
entries remain present until their execution Scope cleanup settles. Warmup is
|
|
391
450
|
reported as `idle`, `running`, `completed` or `failed`, and `state` reports
|
|
392
|
-
`active`, `
|
|
451
|
+
`active`, `quiescing`, `draining`, `aborting`, `releasing` or `disposed`.
|
|
393
452
|
|
|
394
453
|
`inspect()` is diagnostic information, not a lock, synchronization primitive or
|
|
395
454
|
readiness guarantee. It cannot cancel or force shutdown of any execution; use
|
|
396
455
|
`dispose()` and cooperative `AbortSignal` handling for lifecycle coordination.
|
|
456
|
+
New executions are rejected as soon as quiescing begins.
|
|
397
457
|
|
|
398
458
|
Missing, circular, and provider-construction failures use the logical Service
|
|
399
459
|
tags in `ServiceNotFoundError`, `CircularDependencyError`, and
|
|
@@ -438,6 +498,108 @@ and its ordered `timeline`; call `clear()` to reuse it. Composition invokes
|
|
|
438
498
|
observers in declaration order and isolates thrown or rejected observer
|
|
439
499
|
failures from the Runtime result.
|
|
440
500
|
|
|
501
|
+
### Optional OpenTelemetry tracing
|
|
502
|
+
|
|
503
|
+
Install the OpenTelemetry API only when this subpath is used:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
bun add better-effect @opentelemetry/api
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`better-effect/opentelemetry` accepts either an existing `Tracer` or an existing
|
|
510
|
+
`TracerProvider`. Its supported `@opentelemetry/api` peer range is
|
|
511
|
+
`>=1.9.0 <1.10.0` (tested with 1.9.1). It never installs a global provider, SDK,
|
|
512
|
+
context manager or exporter. The provider form only calls `getTracer`:
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
import { trace } from '@opentelemetry/api'
|
|
516
|
+
import { OpenTelemetryRuntimeObserver } from 'better-effect/opentelemetry'
|
|
517
|
+
|
|
518
|
+
const observer = OpenTelemetryRuntimeObserver.make({
|
|
519
|
+
// An existing provider can be passed instead with `provider` and `tracerName`.
|
|
520
|
+
tracer: trace.getTracer('acme.application'),
|
|
521
|
+
serviceResolution: 'events',
|
|
522
|
+
lifecycle: 'events',
|
|
523
|
+
shutdown: 'events',
|
|
524
|
+
recordFailures: true,
|
|
525
|
+
executionAttributeAllowlist: ['requestId'],
|
|
526
|
+
sanitizeFailure: (cause) =>
|
|
527
|
+
cause instanceof KnownDomainError ? { message: cause.code } : undefined
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
const runtime = await Runtime.make(AppLive, { observers: [observer] })
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
The same observer can be passed to `NodeRuntime.runMain` or
|
|
534
|
+
`NodeRuntime.launch`. Executions started through `runtime.executor.run` and
|
|
535
|
+
`runtime.executor.runWith` use the same execution pipeline and produce one
|
|
536
|
+
execution span each; capturing an executor never creates a separate span.
|
|
537
|
+
|
|
538
|
+
The adapter starts one span for each execution, keyed only by the Runtime's
|
|
539
|
+
`executionId`. The Program name is the span name, with
|
|
540
|
+
`better-effect.execution` as the stable fallback. Executions whose program and
|
|
541
|
+
execution cleanup succeed use OpenTelemetry `OK`; typed `Result.err` values,
|
|
542
|
+
thrown defects and execution cleanup failures use `ERROR`. The Runtime observer
|
|
543
|
+
keeps cleanup failure separate from the primary program outcome, so the adapter
|
|
544
|
+
can report the final execution status without turning cleanup into a program
|
|
545
|
+
defect or serializing its cause by default.
|
|
546
|
+
Span timing is supplied by OpenTelemetry's start/end clock; `durationMs` is not
|
|
547
|
+
written as a second duration source.
|
|
548
|
+
|
|
549
|
+
Service telemetry is explicit and defaults to `off`:
|
|
550
|
+
|
|
551
|
+
- `off` creates no Service telemetry.
|
|
552
|
+
- `events` adds resolution, acquisition and release events to an execution span
|
|
553
|
+
only when the Runtime event carries an explicit active execution ID. Warmup,
|
|
554
|
+
Runtime-root cleanup and other events without an owner use standalone spans.
|
|
555
|
+
- `spans` creates child Service spans only for events with an explicit active
|
|
556
|
+
execution ID. Warmup and Runtime-root cleanup are represented by standalone
|
|
557
|
+
service spans (or generic event-carrier spans in `events` mode); ambient
|
|
558
|
+
OpenTelemetry context is never used to guess ownership.
|
|
559
|
+
|
|
560
|
+
Service identity is always the logical `serviceTag`; constructor names and
|
|
561
|
+
Service instances are never used. `RuntimeObserver.compose` can combine this
|
|
562
|
+
adapter with recorded, graph and application observers, and a tracer failure is
|
|
563
|
+
isolated from every Runtime result. Call `observer.dispose()` when an observer
|
|
564
|
+
may outlive its Runtime to end state left by malformed or missing end events;
|
|
565
|
+
normal execution spans are ended exactly once by their matching `executionId`.
|
|
566
|
+
|
|
567
|
+
Lifecycle and shutdown telemetry are also explicit and default to `off`:
|
|
568
|
+
|
|
569
|
+
- `lifecycle: 'events'` records activation and release on one
|
|
570
|
+
`better-effect.lifecycle` span; `lifecycle: 'spans'` creates phase spans
|
|
571
|
+
named `better-effect.lifecycle.activate` and
|
|
572
|
+
`better-effect.lifecycle.release`.
|
|
573
|
+
- The public Runtime observer currently exposes lifecycle activation and
|
|
574
|
+
release, but not a per-entry quiesce callback. The adapter does not invent a
|
|
575
|
+
quiesce event; Runtime shutdown quiesce phases remain available through
|
|
576
|
+
`onShutdown`.
|
|
577
|
+
- `shutdown: 'events'` records the public shutdown phases as events on one
|
|
578
|
+
root `better-effect.shutdown` span. `shutdown: 'spans'` keeps that root span
|
|
579
|
+
and adds bounded phase spans for quiesce, drain, abort and release.
|
|
580
|
+
- Shutdown spans use an explicit root parent, never an arbitrary active
|
|
581
|
+
execution span. Shutdown failure marks the root span as `ERROR`; failure
|
|
582
|
+
details are recorded only when `recordFailures: true` and `sanitizeFailure`
|
|
583
|
+
returns them.
|
|
584
|
+
|
|
585
|
+
All lifecycle and shutdown metadata is bounded and excludes resource
|
|
586
|
+
instances, server handles, configuration, payloads and raw causes.
|
|
587
|
+
|
|
588
|
+
Telemetry is privacy-preserving by default. The adapter records only bounded
|
|
589
|
+
library, execution ID, Program/outcome, Service-tag and resolution-path data.
|
|
590
|
+
It does not record causes, stacks, requests, arbitrary execution attributes or
|
|
591
|
+
Service instances, and it never stringifies unknown values. Add attributes only
|
|
592
|
+
through `executionAttributeAllowlist` or the explicit
|
|
593
|
+
`sanitizeExecutionAttributes` callback; add failure details only through
|
|
594
|
+
`sanitizeFailure` together with `recordFailures: true`. Unsupported values are
|
|
595
|
+
dropped, strings are bounded, and default limits are 256 characters, 32
|
|
596
|
+
attributes and 16 resolution-path tags (caller limits remain capped). Sanitizer
|
|
597
|
+
callbacks should return only intentional scalar OpenTelemetry attributes.
|
|
598
|
+
|
|
599
|
+
For distributed tracing, establish the caller's OpenTelemetry context before
|
|
600
|
+
calling `runtime.run`; the adapter does not propagate context or instrument
|
|
601
|
+
HTTP/database libraries automatically.
|
|
602
|
+
|
|
441
603
|
For a startup view of the graph actually observed by a Runtime, compose the
|
|
442
604
|
small graph observer and warm the Layer before accepting work:
|
|
443
605
|
|
|
@@ -476,9 +638,9 @@ const cancellableProgram = Effect.fn(async function* () {
|
|
|
476
638
|
```
|
|
477
639
|
|
|
478
640
|
For a Node.js or Bun CLI, use the host-specific `better-effect/node` entrypoint.
|
|
479
|
-
`NodeRuntime.runMain` validates its signal and
|
|
480
|
-
`SIGINT`/`SIGTERM` listeners, links the first signal to
|
|
481
|
-
and disposes the Runtime exactly once:
|
|
641
|
+
`NodeRuntime.runMain` validates its signal, callback and shutdown options before
|
|
642
|
+
installing `SIGINT`/`SIGTERM` listeners, links the first signal to
|
|
643
|
+
`CurrentAbortSignal`, and disposes the Runtime exactly once:
|
|
482
644
|
|
|
483
645
|
```ts
|
|
484
646
|
import { NodeRuntime } from 'better-effect/node'
|
|
@@ -493,7 +655,11 @@ await NodeRuntime.runMain(AppLive, main, {
|
|
|
493
655
|
console.error(error)
|
|
494
656
|
return 1
|
|
495
657
|
},
|
|
496
|
-
onSuccess: () => 0
|
|
658
|
+
onSuccess: () => 0,
|
|
659
|
+
shutdown: {
|
|
660
|
+
gracePeriod: 10_000,
|
|
661
|
+
abortAfterGracePeriod: true
|
|
662
|
+
}
|
|
497
663
|
})
|
|
498
664
|
```
|
|
499
665
|
|
|
@@ -501,11 +667,21 @@ await NodeRuntime.runMain(AppLive, main, {
|
|
|
501
667
|
remain rejected and may be reported with `onDefect`. Cleanup-only failures use
|
|
502
668
|
`onCleanupFailure`, remain observable, and still set a non-zero
|
|
503
669
|
`process.exitCode` after successful work. The first `SIGINT` or `SIGTERM`
|
|
504
|
-
immediately aborts `CurrentAbortSignal`; Runtime disposal then
|
|
505
|
-
|
|
506
|
-
repeated signals are ignored, and the helper never calls
|
|
507
|
-
|
|
508
|
-
|
|
670
|
+
immediately aborts `CurrentAbortSignal`; Runtime disposal then quiesces root
|
|
671
|
+
resources and applies the configured drain policy. Listeners are removed in
|
|
672
|
+
`finally`, repeated signals are ignored, and the helper never calls
|
|
673
|
+
`process.exit()`.
|
|
674
|
+
|
|
675
|
+
For long-lived applications whose startup and shutdown are completely
|
|
676
|
+
represented by a Layer graph, `NodeRuntime.launch` owns the Runtime and waits
|
|
677
|
+
for a process signal or caller `AbortSignal`:
|
|
678
|
+
|
|
679
|
+
```ts
|
|
680
|
+
await NodeRuntime.launch(AppLive, {
|
|
681
|
+
warmup: true,
|
|
682
|
+
shutdown: { gracePeriod: 10_000, abortAfterGracePeriod: true }
|
|
683
|
+
})
|
|
684
|
+
```
|
|
509
685
|
|
|
510
686
|
For request-local context or overrides, add a Layer only to that execution:
|
|
511
687
|
|
|
@@ -518,32 +694,366 @@ with the execution Scope and never change the shared Runtime environment. Its
|
|
|
518
694
|
external requirements must be provided by the Runtime, and the Hono failure
|
|
519
695
|
handler/request-Layer types are checked at the adapter boundary.
|
|
520
696
|
|
|
697
|
+
### Framework-neutral Web request boundaries
|
|
698
|
+
|
|
699
|
+
The `better-effect/web` entrypoint provides a small Request-to-Response
|
|
700
|
+
boundary without depending on Hono or another framework. Use it anywhere a
|
|
701
|
+
framework gives you standard Web `Request` and `Response` values:
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
import { Result } from 'better-result'
|
|
705
|
+
import { CurrentAbortSignal, Effect } from 'better-effect'
|
|
706
|
+
import { CurrentRequest } from 'better-effect/standard-services'
|
|
707
|
+
import { WebEffect } from 'better-effect/web'
|
|
708
|
+
|
|
709
|
+
const handleRequest = (request: Request) =>
|
|
710
|
+
WebEffect.handleWith(
|
|
711
|
+
runtime.executor,
|
|
712
|
+
request,
|
|
713
|
+
Effect.fn(async function* () {
|
|
714
|
+
const currentRequest = yield* CurrentRequest
|
|
715
|
+
const signal = yield* CurrentAbortSignal
|
|
716
|
+
const user = yield* UserService
|
|
717
|
+
const result = yield* Result.await(user.find())
|
|
718
|
+
|
|
719
|
+
return Result.ok({
|
|
720
|
+
result,
|
|
721
|
+
url: (currentRequest.request as Request).url,
|
|
722
|
+
aborted: signal.aborted
|
|
723
|
+
})
|
|
724
|
+
})
|
|
725
|
+
)
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
`WebEffect.handleWith` supplies `CurrentRequest`, forwards `request.signal`, and
|
|
729
|
+
runs one lazy Program in a child Scope. Request-local resources are released
|
|
730
|
+
before the returned Promise resolves; Runtime-root resources remain owned by
|
|
731
|
+
the Runtime. A `requestLayer` option can add per-request providers or
|
|
732
|
+
intentionally override a compatible request tag.
|
|
733
|
+
|
|
734
|
+
The default success policy passes through a Web `Response`, maps top-level
|
|
735
|
+
`undefined` to 204, and wraps supported values as `{ data: value }` JSON. A
|
|
736
|
+
non-`undefined`, non-`Response` value must be an acyclic graph of `null`,
|
|
737
|
+
booleans, strings, finite numbers, dense arrays, and plain object records.
|
|
738
|
+
Arrays must have only the own string properties `length` and one property for
|
|
739
|
+
each index from `0` through `length - 1`; each index must be an enumerable data
|
|
740
|
+
property. Symbol keys, extra own properties (including non-enumerable ones),
|
|
741
|
+
sparse holes, and accessor elements are rejected. Object records must have
|
|
742
|
+
`Object.prototype` or `null` as their prototype, and every own string-keyed
|
|
743
|
+
property must be an enumerable data property. Own symbol keys,
|
|
744
|
+
non-enumerable properties, accessors, custom prototypes, and other non-plain
|
|
745
|
+
objects are rejected. Shared references in separate branches are serialized as
|
|
746
|
+
repeated values. Nested `undefined`, `bigint`, functions, symbols, and
|
|
747
|
+
non-finite numbers (`NaN` and infinities) are rejected with
|
|
748
|
+
`WebEffectSerializationError` (a `TypeError`) instead of being silently dropped
|
|
749
|
+
or coerced. Use an explicit `onSuccess` policy for other representations. The
|
|
750
|
+
default failure policy passes through a standards-compatible `Response`
|
|
751
|
+
failure and redacts every other typed failure to `{ error: 'Internal Server Error' }`
|
|
752
|
+
with status 500. Custom
|
|
753
|
+
`onSuccess`/`onFailure` policies may be asynchronous but must return a
|
|
754
|
+
standards-compatible `Response`. The boundary checks that protocol structurally:
|
|
755
|
+
`status` is an integer in `0` or `200` through `599`, `ok` matches the 2xx
|
|
756
|
+
status range, `redirected` and `bodyUsed` are booleans, `statusText` and `url`
|
|
757
|
+
are strings, `type` is a standard Response type, and
|
|
758
|
+
`arrayBuffer()`, `blob()`, `bytes()`, `clone()`, `formData()`, `json()`, and
|
|
759
|
+
`text()` are callable. `headers` must provide callable `append()`, `delete()`,
|
|
760
|
+
`get()`, `getSetCookie()`, `has()`, `set()`, and `forEach()` operations. `body`
|
|
761
|
+
must be `null` (including legitimate null-body Responses such as `204`) or a
|
|
762
|
+
ReadableStream-compatible object with a boolean `locked` property and callable
|
|
763
|
+
`cancel()`, `getReader()`, `pipeThrough()`, `pipeTo()`, and `tee()` methods.
|
|
764
|
+
Native cross-realm Responses and other values satisfying this protocol are
|
|
765
|
+
accepted without `instanceof Response`; missing capabilities and forged
|
|
766
|
+
`Response.prototype` values fail with `TypeError`. Thrown defects remain
|
|
767
|
+
rejected. The Program's Service and failure channels, request-Layer
|
|
768
|
+
requirements, and override compatibility are checked at the TypeScript
|
|
769
|
+
boundary.
|
|
770
|
+
|
|
771
|
+
For a response whose body is still produced by request-scoped work, opt in to
|
|
772
|
+
the managed streaming boundary with an explicit descriptor. The descriptor's
|
|
773
|
+
metadata is committed immediately, while its lazy producer is pulled with
|
|
774
|
+
backpressure in the same execution context:
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
const response = await WebEffect.streamWith(
|
|
778
|
+
runtime.executor,
|
|
779
|
+
request,
|
|
780
|
+
Effect.fn(async function* () {
|
|
781
|
+
const file = yield* FileService
|
|
782
|
+
|
|
783
|
+
return Result.ok<WebEffect.Stream>({
|
|
784
|
+
status: 200,
|
|
785
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
786
|
+
producer: async function* ({ signal }) {
|
|
787
|
+
yield* file.bytes({ signal })
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
}),
|
|
791
|
+
{ unconsumedTimeoutMs: 30_000 }
|
|
792
|
+
)
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
`WebEffect.streamWith` retains the request execution until EOF, downstream
|
|
796
|
+
cancel/error, request abort, timeout, or Runtime shutdown. It accepts only the
|
|
797
|
+
explicit descriptor—not an arbitrary `Response`—and never maps failures to a
|
|
798
|
+
new status after headers have been committed.
|
|
799
|
+
|
|
800
|
+
### Next.js App Router request boundaries
|
|
801
|
+
|
|
802
|
+
The optional `better-effect/next` entrypoint adapts an application-owned
|
|
803
|
+
Runtime to native Next.js App Router Route Handlers without importing Next.js
|
|
804
|
+
from the core or main entrypoint:
|
|
805
|
+
|
|
806
|
+
```ts
|
|
807
|
+
import { Result } from 'better-result'
|
|
808
|
+
import { Effect, Layer, Service } from 'better-effect'
|
|
809
|
+
import { NextEffect } from 'better-effect/next'
|
|
810
|
+
|
|
811
|
+
class UserService extends Service<UserService>()('UserService') {
|
|
812
|
+
find(id: string) {
|
|
813
|
+
return { id, name: 'Ada' }
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const http = NextEffect.managed(Layer.make(UserService))
|
|
818
|
+
|
|
819
|
+
export const GET = http.gen(async function* (_request, context: RouteContext<'/api/users/[id]'>) {
|
|
820
|
+
const { id } = await context.params
|
|
821
|
+
const users = yield* UserService
|
|
822
|
+
|
|
823
|
+
return Result.ok(users.find(id))
|
|
824
|
+
})
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Managed builders' `gen` and `handler` methods return the native
|
|
828
|
+
`(request, context) => Promise<Response>` shape expected by App Router route
|
|
829
|
+
files. The context is typed with Next's asynchronous `params`; `handler` is
|
|
830
|
+
useful when the complete `Effect.fn` Program already exists.
|
|
831
|
+
|
|
832
|
+
`managed` is lazy and host-owned: each manager creates at most one Runtime,
|
|
833
|
+
shares concurrent first-request initialization, and keeps root Layer resources
|
|
834
|
+
until explicit disposal. It never installs process signal handlers or creates a
|
|
835
|
+
Runtime per request:
|
|
836
|
+
|
|
837
|
+
```ts
|
|
838
|
+
// app/runtime.ts
|
|
839
|
+
export const appNext = NextEffect.managed(AppLive, {
|
|
840
|
+
runtime: { warmup: true }
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
// Call this from the actual host/server shutdown hook.
|
|
844
|
+
export const initializeApp = (): Promise<void> => appNext.initialize()
|
|
845
|
+
export const disposeApp = (): Promise<void> => appNext.dispose()
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Route modules import the manager directly:
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
// app/api/users/[id]/route.ts
|
|
852
|
+
import { appNext } from '@/app/runtime'
|
|
853
|
+
|
|
854
|
+
export const GET = appNext.gen(async function* (
|
|
855
|
+
_request,
|
|
856
|
+
context: RouteContext<'/api/users/[id]'>
|
|
857
|
+
) {
|
|
858
|
+
const { id } = await context.params
|
|
859
|
+
const users = yield* UserService
|
|
860
|
+
|
|
861
|
+
return Result.ok(users.find(id))
|
|
862
|
+
})
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
For code already being built inside a better-effect Runtime, use the inert
|
|
866
|
+
`fromCurrent` builder. Its `handler` and `gen` operations are yieldable, so
|
|
867
|
+
route requirements become requirements of the surrounding Program:
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
const next = NextEffect.fromCurrent()
|
|
871
|
+
|
|
872
|
+
const makeRoutes = Effect.fn(async function* () {
|
|
873
|
+
const GET = yield* next.gen(async function* (_request, context) {
|
|
874
|
+
const { id } = await context.params
|
|
875
|
+
const users = yield* UserService
|
|
876
|
+
return Result.ok(users.find(id))
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
return Result.ok(GET)
|
|
880
|
+
})
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
`fromCurrent` does not create or own a Runtime and fails explicitly outside an
|
|
884
|
+
active Runtime execution. `managed` is the App Router/module-scope mode. The
|
|
885
|
+
adapter does not promise automatic HMR cleanup, Edge-runtime compatibility, or
|
|
886
|
+
ownership of workers and other ingress resources in the supplied Layer.
|
|
887
|
+
|
|
888
|
+
Each request creates one WebEffect execution and child Scope, installs
|
|
889
|
+
`CurrentRequest` and `CurrentAbortSignal`, and releases request-local Layers
|
|
890
|
+
before the handler Promise resolves. The request's
|
|
891
|
+
`AbortSignal` is linked into the execution signal, and concurrent requests keep
|
|
892
|
+
request Layers and context isolated.
|
|
893
|
+
|
|
894
|
+
The default success policy passes through a returned `Response`; other values
|
|
895
|
+
are encoded as `{ data: value }` JSON using the same strict serialization rules
|
|
896
|
+
as `better-effect/web`. A route may select at most one success policy:
|
|
897
|
+
`respond` handles a complete Response, `serialize` transforms a JSON-safe
|
|
898
|
+
value, and route-level `onSuccess` replaces the shared success policy. Shared
|
|
899
|
+
`onSuccess` and `onFailure` policies receive the native Request and route
|
|
900
|
+
context. Typed
|
|
901
|
+
`Result.err` failures use `onFailure` (or the redacted 500 default), while
|
|
902
|
+
thrown defects remain rejected. Policies must return a standards-compatible
|
|
903
|
+
`Response` and may be asynchronous. This adapter does not add Edge-runtime
|
|
904
|
+
support; use it only in hosts supported by the configured better-effect
|
|
905
|
+
Runtime.
|
|
906
|
+
|
|
907
|
+
Install Next.js only when this subpath is used:
|
|
908
|
+
|
|
909
|
+
```bash
|
|
910
|
+
bun add better-effect better-result next
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
`next` is an optional peer dependency, and `better-effect/next` uses only the
|
|
914
|
+
public native Route Handler shape, so the core and other entrypoints remain
|
|
915
|
+
usable without Next.js.
|
|
916
|
+
|
|
917
|
+
### Bun.serve fetch adapter
|
|
918
|
+
|
|
919
|
+
For Bun applications, `better-effect/bun` is Layer-first. `BunEffect.handler`
|
|
920
|
+
is a yieldable operation: it captures the active `Runtime.Executor` while its
|
|
921
|
+
enclosing Layer is acquired, then uses that executor for each request.
|
|
922
|
+
|
|
923
|
+
`BunEffect.server` owns one native `Bun.serve` instance in its `.layer`. The
|
|
924
|
+
server is quiesced during Runtime shutdown, active executions are drained, and
|
|
925
|
+
the same server stop Promise is awaited during release. There is no parallel
|
|
926
|
+
Runtime or import-time server startup:
|
|
927
|
+
|
|
928
|
+
```ts
|
|
929
|
+
import { Result } from 'better-result'
|
|
930
|
+
import { CurrentAbortSignal, Effect, Layer, Runtime, Service, ServiceRuntime } from 'better-effect'
|
|
931
|
+
import { CurrentRequest } from 'better-effect/standard-services'
|
|
932
|
+
import { BunEffect } from 'better-effect/bun'
|
|
933
|
+
|
|
934
|
+
class AppService extends Service<AppService>()('AppService') {
|
|
935
|
+
handle(url: string) {
|
|
936
|
+
return url
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const ApiServer = BunEffect.server('@app/ApiServer', async function* () {
|
|
941
|
+
const fetch = yield* BunEffect.handler(
|
|
942
|
+
{
|
|
943
|
+
onFailure: (_error, request) =>
|
|
944
|
+
Response.json({ error: 'Request failed', url: request.url }, { status: 500 })
|
|
945
|
+
},
|
|
946
|
+
(request, server) =>
|
|
947
|
+
Effect.fn(async function* () {
|
|
948
|
+
const app = yield* AppService
|
|
949
|
+
const currentRequest = yield* CurrentRequest
|
|
950
|
+
const signal = yield* CurrentAbortSignal
|
|
951
|
+
|
|
952
|
+
return Result.ok({
|
|
953
|
+
value: app.handle(request.url),
|
|
954
|
+
currentUrl: (currentRequest.request as Request).url,
|
|
955
|
+
port: server.port,
|
|
956
|
+
aborted: signal.aborted
|
|
957
|
+
})
|
|
958
|
+
})
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
return { port: 3000, fetch }
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
const runtime = await Runtime.make(Layer.merge(Layer.make(AppService), ApiServer.layer))
|
|
965
|
+
const server = await runtime.run(() => ServiceRuntime.resolve(ApiServer))
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
`BunEffect.handler` invokes exactly one `WebEffect` request boundary. It
|
|
969
|
+
supplies `CurrentRequest`, forwards the request signal to
|
|
970
|
+
`CurrentAbortSignal`, composes request-local Layers, applies success/failure
|
|
971
|
+
policies, keeps thrown defects rejected, and releases request resources before
|
|
972
|
+
the handler Promise resolves.
|
|
973
|
+
|
|
974
|
+
For an application-chosen server Service, use `BunEffect.layer(service,
|
|
975
|
+
factory)`. Its factory returns `{ options, map }`; the adapter maps the one
|
|
976
|
+
native server into your Service while retaining the same Runtime lifecycle.
|
|
977
|
+
`BunEffect.server` is the convenience form whose Service is the raw
|
|
978
|
+
`Bun.Server<Data>`.
|
|
979
|
+
|
|
980
|
+
Dispose the Runtime once during application shutdown. It stops accepting new
|
|
981
|
+
connections, drains active requests, then releases the server and root Layers;
|
|
982
|
+
repeated disposal calls share one Promise:
|
|
983
|
+
|
|
984
|
+
```ts
|
|
985
|
+
await runtime.dispose()
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
This Bun-specific entrypoint makes no Node compatibility claim.
|
|
989
|
+
|
|
521
990
|
### Hono request boundaries
|
|
522
991
|
|
|
523
|
-
The optional `better-effect/hono` entrypoint
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
992
|
+
The optional `better-effect/hono` entrypoint supplies Hono's middleware,
|
|
993
|
+
validator, and Context integration around the same Web request boundary used
|
|
994
|
+
by `better-effect/web`. It adapts Hono's Context to a standard Web `Request`,
|
|
995
|
+
then delegates the single `Runtime` execution, Result/defect handling, and
|
|
996
|
+
request Scope cleanup to that shared boundary. You only need to configure one
|
|
997
|
+
Hono middleware boundary per request; the adapter prevents duplicate
|
|
998
|
+
registrations from opening another execution.
|
|
999
|
+
|
|
1000
|
+
`HonoEffect.app` creates a yieldable application Service and its `.layer`. The
|
|
1001
|
+
factory is acquired once when the application Layer is resolved, so the host
|
|
1002
|
+
owns Runtime composition and shutdown. Handlers can yield Services directly;
|
|
1003
|
+
the adapter provides `CurrentRequest`, forwards `Request.signal`, and converts
|
|
1004
|
+
Results to Responses in one policy:
|
|
527
1005
|
|
|
528
1006
|
```ts
|
|
529
1007
|
import { Hono } from 'hono'
|
|
1008
|
+
import { Effect, Layer, Runtime } from 'better-effect'
|
|
530
1009
|
import { Result } from 'better-result'
|
|
531
1010
|
import { HonoEffect } from 'better-effect/hono'
|
|
532
1011
|
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1012
|
+
const App = HonoEffect.app(
|
|
1013
|
+
'@app/HonoApp',
|
|
1014
|
+
{
|
|
1015
|
+
onFailure: (_error, c) => c.json({ error: 'Request failed' }, 400)
|
|
1016
|
+
},
|
|
1017
|
+
async function* (http) {
|
|
1018
|
+
const app = new Hono()
|
|
1019
|
+
|
|
1020
|
+
app.use('*', yield* http.middleware())
|
|
1021
|
+
app.get(
|
|
1022
|
+
'/work-orders',
|
|
1023
|
+
yield* http.gen(async function* () {
|
|
1024
|
+
const workOrders = yield* WorkOrderService
|
|
1025
|
+
const items = yield* Result.await(workOrders.list())
|
|
1026
|
+
return Result.ok(items)
|
|
1027
|
+
})
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
return app
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
537
1033
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const workOrders = yield* WorkOrderService
|
|
543
|
-
const items = yield* Result.await(workOrders.list())
|
|
544
|
-
return Result.ok(items)
|
|
1034
|
+
const runtime = await Runtime.make(Layer.merge(AppLive, App.layer))
|
|
1035
|
+
const appResult = await runtime.run(
|
|
1036
|
+
Effect.fn(async function* () {
|
|
1037
|
+
return Result.ok(yield* App)
|
|
545
1038
|
})
|
|
546
1039
|
)
|
|
1040
|
+
if (Result.isError(appResult)) throw new Error(String(appResult.error))
|
|
1041
|
+
const app = appResult.value
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
Route examples below belong inside the `HonoEffect.app` factory. For an
|
|
1045
|
+
existing application Service, use `HonoEffect.layer(ServiceToken, options,
|
|
1046
|
+
factory)` and compose the returned Layer with the Runtime's other Layers.
|
|
1047
|
+
|
|
1048
|
+
Use `HonoEffect.layer` when the application should be provided through an
|
|
1049
|
+
existing Service token instead of a generated one:
|
|
1050
|
+
|
|
1051
|
+
```ts
|
|
1052
|
+
const AppLive = HonoEffect.layer(MyApplication, {}, async function* (http) {
|
|
1053
|
+
const app = new Hono()
|
|
1054
|
+
app.use('*', yield* http.middleware())
|
|
1055
|
+
return MyApplication.of({ app })
|
|
1056
|
+
})
|
|
547
1057
|
```
|
|
548
1058
|
|
|
549
1059
|
One or more Hono validators can precede the generator or handler callback, in
|
|
@@ -664,6 +1174,47 @@ low-level provider metadata names.
|
|
|
664
1174
|
`Layer.complete(layer)` is a runtime identity that checks a composition root
|
|
665
1175
|
immediately, so missing Services are reported where the Layer is assembled.
|
|
666
1176
|
|
|
1177
|
+
### Lifecycle-only Layers
|
|
1178
|
+
|
|
1179
|
+
Use `Layer.scopedDiscard` for application components that need startup and
|
|
1180
|
+
shutdown ownership but are not themselves a Service:
|
|
1181
|
+
|
|
1182
|
+
```ts
|
|
1183
|
+
const PollerLive = Layer.scopedDiscard(
|
|
1184
|
+
() => startPoller(),
|
|
1185
|
+
(poller, outcome) => poller.stop(outcome)
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
const ServerLive = Layer.scopedDiscard(() => startServer(), {
|
|
1189
|
+
quiesce: (server) => server.stopAccepting(),
|
|
1190
|
+
release: (server, outcome) => server.close(outcome)
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
const AppLive = Layer.complete(Layer.merge(ConfigLive, PollerLive))
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
The acquisition runs once when `Runtime.make` activates the Layer, after
|
|
1197
|
+
provider registration and optional warmup. The release belongs to the Runtime
|
|
1198
|
+
root Scope, runs in reverse activation order, and receives the final
|
|
1199
|
+
`ScopeOutcome`. A contextual acquisition can use `yield*` with
|
|
1200
|
+
`Layer.scopedDiscard` (or the explicit `Layer.scopedDiscardGen`) and retains
|
|
1201
|
+
those Service requirements even though the Layer provides `never`:
|
|
1202
|
+
|
|
1203
|
+
```ts
|
|
1204
|
+
const PollerLive = Layer.scopedDiscard(
|
|
1205
|
+
async function* () {
|
|
1206
|
+
const config = yield* Config
|
|
1207
|
+
return startPoller(config)
|
|
1208
|
+
},
|
|
1209
|
+
(poller, outcome) => poller.stop(outcome)
|
|
1210
|
+
)
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
For startup programs, `Layer.effectDiscard` accepts only
|
|
1214
|
+
`Effect.Program<void, never, R>`-compatible programs. A typed failure must be
|
|
1215
|
+
handled by the program before it is made a lifecycle entry; it is not silently
|
|
1216
|
+
converted into a Runtime defect.
|
|
1217
|
+
|
|
667
1218
|
---
|
|
668
1219
|
|
|
669
1220
|
## Why better-effect?
|
|
@@ -910,6 +1461,24 @@ Use `Config.schema(schema)` with `Config.layer(source)` or
|
|
|
910
1461
|
`Config.layerFromEnv(options)` when several descriptors should share an
|
|
911
1462
|
explicitly replaceable provider.
|
|
912
1463
|
|
|
1464
|
+
When configuration should be consumed through a Service token, bind the schema
|
|
1465
|
+
to that token so `get` only accepts keys from the decoded schema output:
|
|
1466
|
+
|
|
1467
|
+
```ts
|
|
1468
|
+
const AppConfig = Config.withSchema(EnvSchema)
|
|
1469
|
+
const AppConfigLive = AppConfig.layerFromEnv({ dotEnvPath: '.env' })
|
|
1470
|
+
|
|
1471
|
+
const program = Effect.fn(async function* () {
|
|
1472
|
+
const config = yield* AppConfig
|
|
1473
|
+
return Result.ok(config.get('DATABASE_URL'))
|
|
1474
|
+
// ^ autocomplete is restricted to EnvSchema output keys
|
|
1475
|
+
})
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
`config.get(...)` reads the validated schema output, with the value type inferred
|
|
1479
|
+
from the selected key. Use `Config.schema(schema)` or
|
|
1480
|
+
`Config.fromEnv({ schema })` when the complete transformed output is needed.
|
|
1481
|
+
|
|
913
1482
|
---
|
|
914
1483
|
|
|
915
1484
|
## How it compares
|