better-effect 0.13.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 +337 -88
- 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 +75 -23
- package/dist/bun.d.mts.map +1 -1
- package/dist/bun.mjs +170 -33
- package/dist/bun.mjs.map +1 -1
- package/dist/{context-BphfFUMO.mjs → context-DSGSqcJj.mjs} +12 -3
- package/dist/context-DSGSqcJj.mjs.map +1 -0
- package/dist/{current-request-DDKNyymv.mjs → current-request-Bc1YHL0w.mjs} +3 -2
- package/dist/{current-request-DDKNyymv.mjs.map → current-request-Bc1YHL0w.mjs.map} +1 -1
- package/dist/{current-request-CIU7AHhF.d.mts → current-request-Dm-QQ3Xr.d.mts} +4 -4
- package/dist/current-request-Dm-QQ3Xr.d.mts.map +1 -0
- package/dist/{effect-CPdV7Yv8.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 +63 -27
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +111 -45
- package/dist/hono.mjs.map +1 -1
- package/dist/{index-CCi07kUw.d.mts → index-3Bkl-sxS.d.mts} +7 -2
- package/dist/{index-CCi07kUw.d.mts.map → index-3Bkl-sxS.d.mts.map} +1 -1
- package/dist/{index-cXgyMa0v.d.mts → index-C2ed9Ilq.d.mts} +32 -7
- package/dist/index-C2ed9Ilq.d.mts.map +1 -0
- package/dist/{index-BodMy6rO.d.mts → index-ClNndn2-.d.mts} +93 -63
- package/dist/index-ClNndn2-.d.mts.map +1 -0
- package/dist/{index-DAKn0qHk.d.mts → index-Cy1VsU-u.d.mts} +2 -4
- package/dist/index-Cy1VsU-u.d.mts.map +1 -0
- package/dist/index.d.mts +6 -8
- package/dist/index.mjs +8 -9
- package/dist/index.mjs.map +1 -1
- 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/layer-C2OaqTmB.d.mts +987 -0
- package/dist/layer-C2OaqTmB.d.mts.map +1 -0
- package/dist/next.d.mts +102 -26
- package/dist/next.d.mts.map +1 -1
- package/dist/next.mjs +157 -40
- package/dist/next.mjs.map +1 -1
- package/dist/{node-context-DczjAcrX.d.mts → node-context-1YEjrU9g.d.mts} +2 -2
- package/dist/{node-context-DczjAcrX.d.mts.map → node-context-1YEjrU9g.d.mts.map} +1 -1
- package/dist/{node-context-Ca18oNSg.mjs → node-context-g3bCcY9t.mjs} +2 -2
- package/dist/{node-context-Ca18oNSg.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 +20 -2
- package/dist/opentelemetry.d.mts.map +1 -1
- package/dist/opentelemetry.mjs +423 -5
- package/dist/opentelemetry.mjs.map +1 -1
- 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-nfgFyNCk.mjs → signal-5Rg9bUUe.mjs} +2 -2
- package/dist/{signal-nfgFyNCk.mjs.map → signal-5Rg9bUUe.mjs.map} +1 -1
- package/dist/{standard-services-vnGIafkS.mjs → standard-services-B-iMM_W7.mjs} +38 -4
- package/dist/standard-services-B-iMM_W7.mjs.map +1 -0
- package/dist/standard-services.d.mts +4 -4
- package/dist/standard-services.mjs +3 -3
- package/dist/testing.d.mts +25 -7
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +51 -7
- package/dist/testing.mjs.map +1 -1
- package/dist/{types-oihI-ESv.d.mts → types-BFTqCCcw.d.mts} +32 -5
- 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 +22 -13
- package/dist/web.d.mts.map +1 -1
- package/dist/web.mjs +1 -1
- package/package.json +10 -16
- package/dist/context-BphfFUMO.mjs.map +0 -1
- package/dist/current-request-CIU7AHhF.d.mts.map +0 -1
- package/dist/disposable-CP-Q2H3l.mjs +0 -57
- package/dist/disposable-CP-Q2H3l.mjs.map +0 -1
- package/dist/effect-CPdV7Yv8.mjs.map +0 -1
- package/dist/errors-BKcsQ0ip.d.mts.map +0 -1
- package/dist/index-AiKM71-u.d.mts +0 -376
- package/dist/index-AiKM71-u.d.mts.map +0 -1
- package/dist/index-BodMy6rO.d.mts.map +0 -1
- package/dist/index-DAKn0qHk.d.mts.map +0 -1
- package/dist/index-cXgyMa0v.d.mts.map +0 -1
- package/dist/layer-D16GKxuX.mjs +0 -269
- package/dist/layer-D16GKxuX.mjs.map +0 -1
- package/dist/observer-9nhbL7Pr.d.mts +0 -68
- package/dist/observer-9nhbL7Pr.d.mts.map +0 -1
- package/dist/outcome-DI8LPIQ4.d.mts +0 -387
- package/dist/outcome-DI8LPIQ4.d.mts.map +0 -1
- package/dist/program-metadata-BxjGkT17.mjs +0 -260
- package/dist/program-metadata-BxjGkT17.mjs.map +0 -1
- package/dist/registration-drnZk0SY.mjs.map +0 -1
- package/dist/runtime-C_55ofnL.mjs +0 -791
- package/dist/runtime-C_55ofnL.mjs.map +0 -1
- package/dist/standard-services-vnGIafkS.mjs.map +0 -1
- package/dist/types-oihI-ESv.d.mts.map +0 -1
- package/dist/web-Bq3MfMrj.mjs +0 -147
- package/dist/web-Bq3MfMrj.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
|
|
@@ -393,11 +448,12 @@ resolves Services, warms the Runtime, creates Scopes, invokes observers or
|
|
|
393
448
|
exposes providers, instances, signals, attributes or backend state. Execution
|
|
394
449
|
entries remain present until their execution Scope cleanup settles. Warmup is
|
|
395
450
|
reported as `idle`, `running`, `completed` or `failed`, and `state` reports
|
|
396
|
-
`active`, `
|
|
451
|
+
`active`, `quiescing`, `draining`, `aborting`, `releasing` or `disposed`.
|
|
397
452
|
|
|
398
453
|
`inspect()` is diagnostic information, not a lock, synchronization primitive or
|
|
399
454
|
readiness guarantee. It cannot cancel or force shutdown of any execution; use
|
|
400
455
|
`dispose()` and cooperative `AbortSignal` handling for lifecycle coordination.
|
|
456
|
+
New executions are rejected as soon as quiescing begins.
|
|
401
457
|
|
|
402
458
|
Missing, circular, and provider-construction failures use the logical Service
|
|
403
459
|
tags in `ServiceNotFoundError`, `CircularDependencyError`, and
|
|
@@ -463,6 +519,8 @@ const observer = OpenTelemetryRuntimeObserver.make({
|
|
|
463
519
|
// An existing provider can be passed instead with `provider` and `tracerName`.
|
|
464
520
|
tracer: trace.getTracer('acme.application'),
|
|
465
521
|
serviceResolution: 'events',
|
|
522
|
+
lifecycle: 'events',
|
|
523
|
+
shutdown: 'events',
|
|
466
524
|
recordFailures: true,
|
|
467
525
|
executionAttributeAllowlist: ['requestId'],
|
|
468
526
|
sanitizeFailure: (cause) =>
|
|
@@ -472,6 +530,11 @@ const observer = OpenTelemetryRuntimeObserver.make({
|
|
|
472
530
|
const runtime = await Runtime.make(AppLive, { observers: [observer] })
|
|
473
531
|
```
|
|
474
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
|
+
|
|
475
538
|
The adapter starts one span for each execution, keyed only by the Runtime's
|
|
476
539
|
`executionId`. The Program name is the span name, with
|
|
477
540
|
`better-effect.execution` as the stable fallback. Executions whose program and
|
|
@@ -501,6 +564,27 @@ isolated from every Runtime result. Call `observer.dispose()` when an observer
|
|
|
501
564
|
may outlive its Runtime to end state left by malformed or missing end events;
|
|
502
565
|
normal execution spans are ended exactly once by their matching `executionId`.
|
|
503
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
|
+
|
|
504
588
|
Telemetry is privacy-preserving by default. The adapter records only bounded
|
|
505
589
|
library, execution ID, Program/outcome, Service-tag and resolution-path data.
|
|
506
590
|
It does not record causes, stacks, requests, arbitrary execution attributes or
|
|
@@ -554,9 +638,9 @@ const cancellableProgram = Effect.fn(async function* () {
|
|
|
554
638
|
```
|
|
555
639
|
|
|
556
640
|
For a Node.js or Bun CLI, use the host-specific `better-effect/node` entrypoint.
|
|
557
|
-
`NodeRuntime.runMain` validates its signal and
|
|
558
|
-
`SIGINT`/`SIGTERM` listeners, links the first signal to
|
|
559
|
-
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:
|
|
560
644
|
|
|
561
645
|
```ts
|
|
562
646
|
import { NodeRuntime } from 'better-effect/node'
|
|
@@ -571,7 +655,11 @@ await NodeRuntime.runMain(AppLive, main, {
|
|
|
571
655
|
console.error(error)
|
|
572
656
|
return 1
|
|
573
657
|
},
|
|
574
|
-
onSuccess: () => 0
|
|
658
|
+
onSuccess: () => 0,
|
|
659
|
+
shutdown: {
|
|
660
|
+
gracePeriod: 10_000,
|
|
661
|
+
abortAfterGracePeriod: true
|
|
662
|
+
}
|
|
575
663
|
})
|
|
576
664
|
```
|
|
577
665
|
|
|
@@ -579,11 +667,21 @@ await NodeRuntime.runMain(AppLive, main, {
|
|
|
579
667
|
remain rejected and may be reported with `onDefect`. Cleanup-only failures use
|
|
580
668
|
`onCleanupFailure`, remain observable, and still set a non-zero
|
|
581
669
|
`process.exitCode` after successful work. The first `SIGINT` or `SIGTERM`
|
|
582
|
-
immediately aborts `CurrentAbortSignal`; Runtime disposal then
|
|
583
|
-
|
|
584
|
-
repeated signals are ignored, and the helper never calls
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
```
|
|
587
685
|
|
|
588
686
|
For request-local context or overrides, add a Layer only to that execution:
|
|
589
687
|
|
|
@@ -609,8 +707,8 @@ import { CurrentRequest } from 'better-effect/standard-services'
|
|
|
609
707
|
import { WebEffect } from 'better-effect/web'
|
|
610
708
|
|
|
611
709
|
const handleRequest = (request: Request) =>
|
|
612
|
-
WebEffect.
|
|
613
|
-
runtime,
|
|
710
|
+
WebEffect.handleWith(
|
|
711
|
+
runtime.executor,
|
|
614
712
|
request,
|
|
615
713
|
Effect.fn(async function* () {
|
|
616
714
|
const currentRequest = yield* CurrentRequest
|
|
@@ -627,7 +725,7 @@ const handleRequest = (request: Request) =>
|
|
|
627
725
|
)
|
|
628
726
|
```
|
|
629
727
|
|
|
630
|
-
`WebEffect.
|
|
728
|
+
`WebEffect.handleWith` supplies `CurrentRequest`, forwards `request.signal`, and
|
|
631
729
|
runs one lazy Program in a child Scope. Request-local resources are released
|
|
632
730
|
before the returned Promise resolves; Runtime-root resources remain owned by
|
|
633
731
|
the Runtime. A `requestLayer` option can add per-request providers or
|
|
@@ -670,6 +768,35 @@ rejected. The Program's Service and failure channels, request-Layer
|
|
|
670
768
|
requirements, and override compatibility are checked at the TypeScript
|
|
671
769
|
boundary.
|
|
672
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
|
+
|
|
673
800
|
### Next.js App Router request boundaries
|
|
674
801
|
|
|
675
802
|
The optional `better-effect/next` entrypoint adapts an application-owned
|
|
@@ -678,7 +805,7 @@ from the core or main entrypoint:
|
|
|
678
805
|
|
|
679
806
|
```ts
|
|
680
807
|
import { Result } from 'better-result'
|
|
681
|
-
import { Effect, Layer,
|
|
808
|
+
import { Effect, Layer, Service } from 'better-effect'
|
|
682
809
|
import { NextEffect } from 'better-effect/next'
|
|
683
810
|
|
|
684
811
|
class UserService extends Service<UserService>()('UserService') {
|
|
@@ -687,8 +814,7 @@ class UserService extends Service<UserService>()('UserService') {
|
|
|
687
814
|
}
|
|
688
815
|
}
|
|
689
816
|
|
|
690
|
-
const
|
|
691
|
-
const http = NextEffect.make(runtime)
|
|
817
|
+
const http = NextEffect.managed(Layer.make(UserService))
|
|
692
818
|
|
|
693
819
|
export const GET = http.gen(async function* (_request, context: RouteContext<'/api/users/[id]'>) {
|
|
694
820
|
const { id } = await context.params
|
|
@@ -698,46 +824,67 @@ export const GET = http.gen(async function* (_request, context: RouteContext<'/a
|
|
|
698
824
|
})
|
|
699
825
|
```
|
|
700
826
|
|
|
701
|
-
`
|
|
827
|
+
Managed builders' `gen` and `handler` methods return the native
|
|
702
828
|
`(request, context) => Promise<Response>` shape expected by App Router route
|
|
703
829
|
files. The context is typed with Next's asynchronous `params`; `handler` is
|
|
704
830
|
useful when the complete `Effect.fn` Program already exists.
|
|
705
831
|
|
|
706
|
-
|
|
707
|
-
|
|
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:
|
|
708
836
|
|
|
709
837
|
```ts
|
|
710
838
|
// app/runtime.ts
|
|
711
|
-
export const
|
|
839
|
+
export const appNext = NextEffect.managed(AppLive, {
|
|
840
|
+
runtime: { warmup: true }
|
|
841
|
+
})
|
|
712
842
|
|
|
713
843
|
// Call this from the actual host/server shutdown hook.
|
|
714
|
-
export const
|
|
844
|
+
export const initializeApp = (): Promise<void> => appNext.initialize()
|
|
845
|
+
export const disposeApp = (): Promise<void> => appNext.dispose()
|
|
715
846
|
```
|
|
716
847
|
|
|
717
|
-
Route modules import
|
|
848
|
+
Route modules import the manager directly:
|
|
718
849
|
|
|
719
850
|
```ts
|
|
720
851
|
// app/api/users/[id]/route.ts
|
|
721
|
-
import {
|
|
852
|
+
import { appNext } from '@/app/runtime'
|
|
722
853
|
|
|
723
|
-
const
|
|
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
|
+
})
|
|
724
863
|
```
|
|
725
864
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
does not create a process-global singleton or mutate hidden adapter state.
|
|
730
|
-
Sharing `appRuntime` is an application ownership choice. If a deployment
|
|
731
|
-
requires per-invocation ownership instead, use the existing `Runtime.use`
|
|
732
|
-
helper explicitly; the adapter never creates a Runtime per request by default:
|
|
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:
|
|
733
868
|
|
|
734
869
|
```ts
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
)
|
|
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
|
+
})
|
|
739
881
|
```
|
|
740
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
|
+
|
|
741
888
|
Each request creates one WebEffect execution and child Scope, installs
|
|
742
889
|
`CurrentRequest` and `CurrentAbortSignal`, and releases request-local Layers
|
|
743
890
|
before the handler Promise resolves. The request's
|
|
@@ -769,14 +916,18 @@ usable without Next.js.
|
|
|
769
916
|
|
|
770
917
|
### Bun.serve fetch adapter
|
|
771
918
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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:
|
|
776
927
|
|
|
777
928
|
```ts
|
|
778
929
|
import { Result } from 'better-result'
|
|
779
|
-
import { CurrentAbortSignal, Effect, Layer, Runtime, Service } from 'better-effect'
|
|
930
|
+
import { CurrentAbortSignal, Effect, Layer, Runtime, Service, ServiceRuntime } from 'better-effect'
|
|
780
931
|
import { CurrentRequest } from 'better-effect/standard-services'
|
|
781
932
|
import { BunEffect } from 'better-effect/bun'
|
|
782
933
|
|
|
@@ -786,50 +937,54 @@ class AppService extends Service<AppService>()('AppService') {
|
|
|
786
937
|
}
|
|
787
938
|
}
|
|
788
939
|
|
|
789
|
-
const
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
port: server.port,
|
|
808
|
-
aborted: signal.aborted
|
|
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
|
+
})
|
|
809
958
|
})
|
|
810
|
-
})
|
|
811
959
|
)
|
|
960
|
+
|
|
961
|
+
return { port: 3000, fetch }
|
|
812
962
|
})
|
|
963
|
+
|
|
964
|
+
const runtime = await Runtime.make(Layer.merge(Layer.make(AppService), ApiServer.layer))
|
|
965
|
+
const server = await runtime.run(() => ServiceRuntime.resolve(ApiServer))
|
|
813
966
|
```
|
|
814
967
|
|
|
815
|
-
`BunEffect.handler` invokes exactly one `WebEffect` request boundary.
|
|
816
|
-
|
|
817
|
-
request-local Layers, applies
|
|
818
|
-
thrown defects rejected, and releases request resources before
|
|
819
|
-
Promise resolves.
|
|
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.
|
|
820
973
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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:
|
|
824
983
|
|
|
825
984
|
```ts
|
|
826
|
-
await server.stop()
|
|
827
985
|
await runtime.dispose()
|
|
828
986
|
```
|
|
829
987
|
|
|
830
|
-
`server.stop()` is the Bun server lifecycle boundary; `BunEffect` does not
|
|
831
|
-
provide a `serve` helper or dispose either resource for you. Keep a single
|
|
832
|
-
application-owned shutdown Promise if shutdown can be requested more than once.
|
|
833
988
|
This Bun-specific entrypoint makes no Node compatibility claim.
|
|
834
989
|
|
|
835
990
|
### Hono request boundaries
|
|
@@ -842,28 +997,63 @@ request Scope cleanup to that shared boundary. You only need to configure one
|
|
|
842
997
|
Hono middleware boundary per request; the adapter prevents duplicate
|
|
843
998
|
registrations from opening another execution.
|
|
844
999
|
|
|
845
|
-
|
|
846
|
-
|
|
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:
|
|
847
1005
|
|
|
848
1006
|
```ts
|
|
849
1007
|
import { Hono } from 'hono'
|
|
1008
|
+
import { Effect, Layer, Runtime } from 'better-effect'
|
|
850
1009
|
import { Result } from 'better-result'
|
|
851
1010
|
import { HonoEffect } from 'better-effect/hono'
|
|
852
1011
|
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
+
)
|
|
857
1029
|
|
|
858
|
-
app
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1030
|
+
return app
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
|
|
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)
|
|
865
1038
|
})
|
|
866
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
|
+
})
|
|
867
1057
|
```
|
|
868
1058
|
|
|
869
1059
|
One or more Hono validators can precede the generator or handler callback, in
|
|
@@ -984,6 +1174,47 @@ low-level provider metadata names.
|
|
|
984
1174
|
`Layer.complete(layer)` is a runtime identity that checks a composition root
|
|
985
1175
|
immediately, so missing Services are reported where the Layer is assembled.
|
|
986
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
|
+
|
|
987
1218
|
---
|
|
988
1219
|
|
|
989
1220
|
## Why better-effect?
|
|
@@ -1230,6 +1461,24 @@ Use `Config.schema(schema)` with `Config.layer(source)` or
|
|
|
1230
1461
|
`Config.layerFromEnv(options)` when several descriptors should share an
|
|
1231
1462
|
explicitly replaceable provider.
|
|
1232
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
|
+
|
|
1233
1482
|
---
|
|
1234
1483
|
|
|
1235
1484
|
## How it compares
|
package/dist/adapters/iti.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import "../index-DAKn0qHk.mjs";
|
|
1
|
+
import { G as LayerRegistration, H as LayerBackendDisposeOptions, Rt as AnyServiceToken, V as LayerBackend } from "../layer-C2OaqTmB.mjs";
|
|
2
|
+
import "../index-Cy1VsU-u.mjs";
|
|
4
3
|
//#region src/adapters/iti.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* ITI-backed Layer backend.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":";;;;;;;;;cAyBa,2BAA2B;UAC9B;mBAES;mBAEA;;;;;;mBAOA;UAET;;EAeR,SAAS,cAAc;;EAwBvB,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;;EAkCnF,WAAW,UAAU,6BAA6B"}
|
package/dist/adapters/iti.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { f as captureServiceTag, n as normalizeLayerRegistration, r as DuplicateServiceError, s as ServiceTagCollisionError, t as captureLayerRegistrationTag, u as ServiceNotFoundError } from "../registration-
|
|
1
|
+
import { f as captureServiceTag, n as normalizeLayerRegistration, r as DuplicateServiceError, s as ServiceTagCollisionError, t as captureLayerRegistrationTag, u as ServiceNotFoundError } from "../registration-Bwe2GbVu.mjs";
|
|
2
2
|
import { t as isPromiseLike } from "../runtime-CDcCF5cb.mjs";
|
|
3
|
-
import { t as assertServiceCompatibility } from "../internal-identity-
|
|
3
|
+
import { t as assertServiceCompatibility } from "../internal-identity-BuHBtP0k.mjs";
|
|
4
4
|
import { createContainer } from "iti";
|
|
5
5
|
//#region src/adapters/iti.ts
|
|
6
6
|
/**
|