better-effect 0.3.0 → 0.4.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 CHANGED
@@ -1,618 +1,272 @@
1
1
  # better-effect
2
2
 
3
- Lightweight Effect-inspired primitives built around [`better-result`](https://www.npmjs.com/package/better-result).
3
+ **Effect-like dependency safety for better-result.**
4
4
 
5
- `better-effect` focuses on a small set of ideas that are useful in application code without bringing in a full effect runtime:
5
+ Type your errors with `better-result`. Typecheck the rest of your application wiring with `better-effect`.
6
6
 
7
- - **Service**contextual dependency access with `yield*`
8
- - **Layer** — declarative composition of live/test environments
9
- - **Scope** — contextual lifetime and finalizer management
10
- - **Resource** — standalone Result-oriented acquire/use/release helper
11
- - **DI adapters** — dependency resolution is delegated to an external container instead of being reimplemented by the library
12
-
13
- The goal is not to recreate Effect. The goal is to provide a small, composable layer on top of `better-result`.
14
-
15
- ## Installation
7
+ Use Services directly inside `Effect.gen`, compose implementations into application environments, and let TypeScript catch missing dependencies before your application starts while keeping Promises, `better-result`, and your DI backend.
16
8
 
17
9
  ```bash
18
10
  bun add better-effect better-result
19
11
  ```
20
12
 
21
- If you want the ITI adapter:
22
-
23
- ```bash
24
- bun add iti
25
- ```
26
-
27
- ## Service
28
-
29
- A service is a class that also acts as its own dependency token.
13
+ ## TypeScript knows what your application needs
30
14
 
31
15
  ```ts
32
16
  import { Result } from 'better-result'
33
- import { Effect, Service } from 'better-effect'
17
+ import { Effect, Layer, Runtime, Service } from 'better-effect'
34
18
 
35
- export class Database extends Service<Database>() {
36
- findUser(email: string) {
37
- return Promise.resolve({
38
- id: '1',
39
- email
40
- })
19
+ class Database extends Service<Database>() {
20
+ findUser(id: string) {
21
+ // ...
41
22
  }
42
23
  }
43
24
 
44
- export class UserRepository extends Service<UserRepository>() {
45
- findByEmail(email: string) {
25
+ class UserRepository extends Service<UserRepository>() {
26
+ findUser(id: string) {
46
27
  return Effect.gen(async function* () {
47
28
  const database = yield* Database
48
29
 
49
- const user = await database.findUser(email)
50
-
51
- return Result.ok(user)
52
- })
53
- }
54
- }
55
- ```
56
-
57
- Use `Effect.gen` when a generator accesses Services. It delegates execution to
58
- `better-result` while carrying the required Service tokens in a type-only
59
- metadata channel:
60
-
61
- ```ts
62
- export class AuthService extends Service<AuthService>() {
63
- login(email: string) {
64
- return Effect.gen(async function* () {
65
- const users = yield* UserRepository
66
- const user = yield* Result.await(users.findByEmail(email))
67
-
68
- return Result.ok(user)
30
+ return Result.ok(await database.findUser(id))
69
31
  })
70
32
  }
71
33
  }
72
- ```
73
-
74
- `Layer.make` derives the requirements from those method return types. A
75
- `Runtime` rejects a merged Layer at compile time when one of its required
76
- Services is missing; `Layer.override` remains the explicit replacement API.
77
34
 
78
- `Runtime.make()` and `buildLayer()` preserve the exact Service-token union
79
- provided by the Layer. Every `run()` boundary checks the final
80
- `EffectRequirements` against that union, so a program that yields an unavailable
81
- Service is rejected at compile time and the diagnostic names
82
- `__betterEffectMissingRuntimeServices`. Programs with no Service requirements
83
- remain valid in any environment:
35
+ const UserRepositoryLive = Layer.make(UserRepository, () => new UserRepository())
84
36
 
85
- ```ts
86
- const runtime = await Runtime.make(AppLive, backend)
87
- // inferred as Runtime<typeof Database | typeof UserRepository>
88
-
89
- type AppRuntime = RuntimeFor<typeof AppLive>
90
- // Runtime<typeof Database | typeof UserRepository>
91
-
92
- const built = await buildLayer(AppLive, backend)
93
- // inferred as BuiltLayer<typeof Database | typeof UserRepository>
94
-
95
- const result = await runtime.run(() =>
96
- Effect.gen(async function* () {
97
- const database = yield* Database
98
-
99
- return Result.ok(database.query())
100
- })
101
- )
37
+ await Runtime.make(UserRepositoryLive, backend)
38
+ // ^^^^^^^^^^^^^^^^^^
39
+ // Type error: Database is required but not provided
102
40
  ```
103
41
 
104
- Use `RuntimeFor<typeof AppLive>` when a Runtime handle inferred from a Layer must
105
- be named in a function signature. This avoids manually repeating
106
- `Runtime<LayerProvided<typeof AppLive>>` while preserving the same checked
107
- Service environment.
42
+ `UserRepository` used `Database`, so `Database` became part of its environment requirements.
108
43
 
109
- Use an unparameterized `Runtime` or `BuiltLayer` annotation only when an
110
- intentionally erased, unchecked environment is needed:
44
+ No dependency list was written manually.
111
45
 
112
- ```ts
113
- const erased: Runtime = runtime
114
- ```
115
-
116
- There are no string tokens:
46
+ Provide it and the environment becomes complete:
117
47
 
118
48
  ```ts
119
- const database = yield * Database
120
- ```
49
+ const DatabaseLive = Layer.make(Database, () => new Database())
121
50
 
122
- The class itself is the identity used by the resolver, and the result is inferred as `Database`.
123
-
124
- ### Why classes are tokens
125
-
126
- Using constructors directly avoids duplicated identifiers such as:
127
-
128
- ```ts
129
- Service<AuthService>()('authService')
130
- ```
51
+ const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
131
52
 
132
- and prevents string-key collisions or typos.
133
-
134
- Conceptually:
135
-
136
- ```text
137
- yield* AuthService
138
-
139
-
140
- ServiceRuntime
141
-
142
-
143
- ServiceResolver
144
-
145
-
146
- AuthService instance
147
- ```
148
-
149
- ## Layer
150
-
151
- A Layer describes which implementations form an application environment.
152
-
153
- It does **not** implement dependency resolution. That remains the responsibility of the configured backend.
154
-
155
- ```ts
156
- import { Layer } from 'better-effect'
157
-
158
- export const DatabaseLive = Layer.scoped(
159
- Database,
160
- async () => {
161
- const database = new Database()
162
-
163
- await database.connect()
164
-
165
- return database
166
- },
167
- (database) => database.close()
168
- )
169
-
170
- export const UserRepositoryLive = Layer.make(UserRepository, () => new UserRepository())
171
-
172
- export const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
173
- ```
174
-
175
- The core Layer API intentionally stays small:
176
-
177
- ```ts
178
- Layer.make(Service, acquire)
179
- Layer.succeed(Service, instance)
180
- Layer.scoped(Service, acquire, release)
181
- Layer.gen(Service, factory)
182
- Layer.scopedGen(Service, factory, release)
183
- Layer.merge(...layers)
184
- Layer.override(base, ...overrides)
53
+ const runtime = await Runtime.make(AppLive, backend)
185
54
  ```
186
55
 
187
- Use `Layer.gen` when constructing a provider requires contextual Services. The
188
- requirements yielded by the factory remain part of the Layer's compile-time
189
- contract:
190
-
191
- ```ts
192
- const UserRepositoryLive = Layer.gen(UserRepository, async function* () {
193
- const database = yield* Database
194
-
195
- return new UserRepository(database)
196
- })
197
- ```
56
+ And the contract does not disappear after startup.
198
57
 
199
- Use `Layer.scopedGen` when acquisition both needs contextual Services and owns a
200
- resource that must be released with the Runtime root Scope:
58
+ A Runtime also knows which Services exist in its environment:
201
59
 
202
60
  ```ts
203
- class DatabaseSession extends Service<DatabaseSession>() {
204
- constructor(readonly database: Database) {
205
- super()
206
- }
207
-
208
- close(outcome: ScopeOutcome) {
209
- return this.database.closeSession(this, outcome)
210
- }
211
- }
212
-
213
- const DatabaseSessionLive = Layer.scopedGen(
214
- DatabaseSession,
215
- async function* () {
61
+ await runtime.run(() =>
62
+ Effect.gen(async function* () {
216
63
  const database = yield* Database
217
64
 
218
- return new DatabaseSession(database)
219
- },
220
- (session, outcome) => session.close(outcome)
65
+ return Result.ok(database)
66
+ })
221
67
  )
222
68
  ```
223
69
 
224
- `Layer.gen` expresses contextual acquisition without registering cleanup;
225
- `Layer.scoped` registers cleanup for a dependency-free factory; `Layer.scopedGen`
226
- combines both. The factory remains lazy, the acquired instance is shared according
227
- to backend caching, and its release runs once when the Runtime root closes. The
228
- release callback receives the root `ScopeOutcome`, including the final outcome of
229
- one-shot `Runtime.run()`.
230
-
231
- ### Test environments
232
-
233
- Layers make implementation replacement explicit:
70
+ If a program asks that Runtime for a Service its environment does not provide, TypeScript rejects the call.
234
71
 
235
- ```ts
236
- const DatabaseTest = Layer.succeed(Database, new InMemoryDatabase())
237
-
238
- const AppTest = Layer.override(AppLive, DatabaseTest)
239
- ```
240
-
241
- ## ITI adapter
242
-
243
- `better-effect` does not depend on ITI in its core. ITI is just one possible backend.
244
-
245
- ```ts
246
- import { buildLayer } from 'better-effect'
247
-
248
- import { ItiLayerBackend } from 'better-effect/adapters/iti'
249
-
250
- const runtime = await buildLayer(AppLive, new ItiLayerBackend())
251
-
252
- try {
253
- await main()
254
- } finally {
255
- await runtime.dispose()
256
- }
72
+ ```text
73
+ yield* Database
74
+
75
+
76
+ program requires Database
77
+
78
+
79
+ Layer provides Database?
80
+
81
+ no ├──────────► TypeScript error
82
+
83
+ yes
84
+
85
+ Runtime can execute it
257
86
  ```
258
87
 
259
- This keeps application code independent from the DI container.
88
+ We call this **typechecked wiring**.
260
89
 
261
- A different backend can implement the same resolver/backend contracts without changing Services or Layers.
90
+ The Services your code uses, the implementations your Layers provide, and the programs your Runtime executes participate in the same type-level contract.
262
91
 
263
- ## Scope
92
+ ---
264
93
 
265
- `Scope` manages a dynamic lifetime containing multiple resources. Resources acquired
266
- inside `runtime.run()` belong to that execution and are released automatically when it
267
- finishes.
94
+ ## Why better-effect?
268
95
 
269
- The contextual `Scope` is a non-owning capability: it can acquire resources, register
270
- finalizers, and fork children, but it cannot close the lifetime that owns it. Owners
271
- receive a `CloseableScope` from `Scope.make()` or `scope.fork()` and remain responsible
272
- for closing it:
96
+ `better-result` already gives TypeScript applications an excellent model for typed failures.
273
97
 
274
- ```ts
275
- const scope = yield * Scope
276
- const child = scope.fork()
98
+ But typed errors are only one part of a growing application.
277
99
 
278
- try {
279
- await Scope.provide(child, () => processBatch())
280
- } finally {
281
- await child.close()
282
- }
283
- ```
100
+ Eventually you also need to answer:
284
101
 
285
- For the common acquire-and-release case, `Effect.acquireRelease()` keeps the same
286
- lifetime semantics without exposing `Scope` in the program. It is an async yieldable
287
- for `Effect.gen`:
102
+ - What does this service depend on?
103
+ - Did the application provide every dependency?
104
+ - Can this program run in this environment?
105
+ - How do I replace implementations in tests?
106
+ - Who owns this database connection?
107
+ - When should this resource be released?
288
108
 
289
- ```ts
290
- const result = await runtime.run(() =>
291
- Effect.gen(async function* () {
292
- const connection = yield* Effect.acquireRelease(
293
- () => database.reserve(),
294
- (connection, outcome) => {
295
- void outcome
296
- return connection.release()
297
- }
298
- )
299
-
300
- return Result.ok(await useConnection(connection))
301
- })
302
- )
303
- ```
109
+ Those problems are often discovered through container errors, startup failures, test setup, or manual composition-root maintenance.
304
110
 
305
- Acquisition failures are returned through the Effect `Result` error channel. The
306
- release callback is registered in the current Scope and remains Scope cleanup, so it
307
- runs when the owning execution closes.
111
+ Effect has powerful ideas for solving them.
308
112
 
309
- When a resource is already acquired and implements `Symbol.dispose` or
310
- `Symbol.asyncDispose`, `Effect.add()` registers it in the current Scope and yields the
311
- same object back to the program:
113
+ `better-effect` explores a smaller path:
312
114
 
313
- ```ts
314
- const result = await runtime.run(() =>
315
- Effect.gen(async function* () {
316
- const file = await createTemporaryFile()
317
- const ownedFile = yield* Effect.add(file)
115
+ **keep `better-result`, Promises and normal TypeScript — borrow the architectural ideas that make dependencies and resource lifetimes easier to reason about.**
318
116
 
319
- return Result.ok(await readFile(ownedFile))
320
- })
321
- )
322
- ```
323
-
324
- `Effect.add()` does not acquire the resource or create a Scope. It must run inside a
325
- managed execution such as `runtime.run()` or `Scope.run()`; without a current Scope,
326
- it preserves the existing missing-Scope failure. The resource is disposed when that
327
- Scope closes, with `Symbol.asyncDispose` preferred when both protocols exist.
117
+ ### Know your dependencies before runtime
328
118
 
329
- Use `Effect.acquireRelease()` when acquisition belongs in the Effect and cleanup needs
330
- an explicit callback or the final `ScopeOutcome`; use `Effect.add()` for an
331
- already-acquired JavaScript disposable.
119
+ Services can be requested directly:
332
120
 
333
121
  ```ts
334
- const result = await runtime.run(() =>
335
- Effect.gen(async function* () {
336
- const scope = yield* Scope
337
-
338
- const connection = await scope.acquire(
339
- () => database.reserve(),
340
- (connection, outcome) => {
341
- void outcome
342
- return connection.release()
343
- }
344
- )
345
-
346
- return Result.ok(await useConnection(connection))
347
- })
348
- )
122
+ const database = yield * Database
349
123
  ```
350
124
 
351
- Disposable objects can be registered directly:
125
+ That access is also captured by the type system.
352
126
 
353
- ```ts
354
- const file = await scope.add(await createTemporaryFile())
355
- ```
127
+ Layers know both what they provide and what their Services require. Incomplete environments can therefore fail during typechecking instead of application startup.
356
128
 
357
- `DisposableResource` requires a callable `Symbol.dispose` or `Symbol.asyncDispose`,
358
- so plain or weakly typed objects must be narrowed before registration. Dynamic values
359
- that cross an unsafe boundary are still checked at runtime and rejected with
360
- `ResourceNotDisposableError` when neither protocol is present.
129
+ Runtime keeps that environment information and checks programs against it when they run.
361
130
 
362
- Finalizers run sequentially in reverse registration order. `Layer.scoped` and
363
- `Layer.scopedGen` resources belong to the Runtime root scope and remain alive between
364
- executions; they are released when the Runtime is disposed. `Resource.acquireUseRelease()`
365
- remains available as a standalone compatibility helper implemented on top of Scope.
131
+ ### Compose application environments
366
132
 
367
- Scopes can also form explicit child lifetimes. `fork()` registers a child with its
368
- parent, while `Scope.provide()` uses an existing scope without taking ownership of its
369
- closure:
133
+ Layers describe implementations without making your application code depend on a specific DI container.
370
134
 
371
135
  ```ts
372
- const parent = Scope.make()
373
- const batch = parent.fork()
374
-
375
- try {
376
- await Scope.provide(batch, () => processBatch())
377
- } finally {
378
- await batch.close()
379
- await parent.close()
380
- }
136
+ const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive, AuthServiceLive)
381
137
  ```
382
138
 
383
- `runtime.run()` creates one child scope per execution. Concurrent executions receive
384
- isolated scopes, and `runtime.dispose()` stops accepting new executions, waits for
385
- active executions to finish, then closes the Runtime root scope and its Layer resources.
386
-
387
- The final result is classified only at the execution boundary. Plain values and
388
- `Result.ok` close the execution with `{ status: 'success' }`; `Result.err` and thrown
389
- exceptions close it with `{ status: 'failure', cause }`. Intermediate Results do not
390
- change the outcome. Release callbacks receive this outcome, which makes commit/rollback
391
- cleanup possible without adding a transaction abstraction.
392
-
393
- For example, a transaction can choose its final action from the outcome:
139
+ Testing can replace implementations explicitly:
394
140
 
395
141
  ```ts
396
- const transaction =
397
- yield *
398
- Effect.acquireRelease(
399
- () => database.begin(),
400
- (transaction, outcome) =>
401
- outcome.status === 'success' ? transaction.commit() : transaction.rollback()
402
- )
403
- ```
404
-
405
- The ownership tree is:
406
-
407
- ```text
408
- Runtime
409
- └── root Scope
410
- ├── Layer resources
411
- └── execution Scope
412
- └── operation resources
142
+ const AppTest = Layer.override(AppLive, DatabaseTest)
413
143
  ```
414
144
 
415
- Graceful disposal follows this order:
145
+ The environment contract remains typed after the override.
416
146
 
417
- ```text
418
- stop accepting executions
419
-
420
- wait for active executions
421
-
422
- close the root Scope
423
-
424
- dispose the backend
425
- ```
147
+ ### Own resource lifetimes
426
148
 
427
- An execution Scope is always closed before its execution Promise settles. The precedence
428
- is `program failure > cleanup failure > program success`: a failed program preserves its
429
- exact `Result.err` or exception, while a successful program rejects with a cleanup error.
430
- Configure `onCleanupFailure` on `buildLayer` or `Runtime.make` to receive one best-effort
431
- diagnostic for suppressed execution or shutdown cleanup failures. Calls to `runtime.run()`
432
- after disposal begins fail with `BuiltLayerDisposedError` without invoking the program.
433
- Shutdown has no cancellation or timeout mechanism and is intended to be initiated outside
434
- the execution being awaited.
149
+ Some dependencies are values.
435
150
 
436
- ## Standalone resource helper
151
+ Others own connections, sessions, files or other resources.
437
152
 
438
- `Resource.acquireUseRelease()` remains useful for local workflows that want a
439
- Result-oriented acquire/use/release API without constructing a Runtime. It is
440
- implemented on top of Scope and remains fully supported; it is not deprecated.
153
+ `Layer.scoped`, `Layer.scopedGen`, `Effect.acquireRelease`, `Effect.add` and `Scope` make their lifetime explicit.
441
154
 
442
155
  ```ts
443
- import { Resource } from 'better-effect'
444
-
445
- const result = await Resource.acquireUseRelease({
446
- name: 'transaction',
447
-
448
- acquire: () => database.begin(),
449
-
450
- use: (transaction) => executeCommand(transaction),
451
-
452
- release: (transaction) => transaction.close()
453
- })
156
+ const DatabaseLive = Layer.scoped(
157
+ Database,
158
+ () => Database.connect(),
159
+ (database) => database.close()
160
+ )
454
161
  ```
455
162
 
456
- When `release` is omitted, Resource prefers the JavaScript explicit resource
457
- management protocol:
163
+ Runtime owns the application lifetime and safely releases scoped resources when that lifetime ends.
458
164
 
459
- ```ts
460
- Symbol.asyncDispose
461
- Symbol.dispose
462
- ```
165
+ Resources acquired during an individual execution belong to that execution instead.
463
166
 
464
- If both `use` and `release` fail, the `use` error is preserved. The precedence is:
167
+ ### Keep your runtime choices
465
168
 
466
- ```text
467
- 1. use failure
468
- 2. release failure
469
- 3. successful use value
470
- ```
169
+ `better-effect` is not a replacement implementation of Effect.
471
170
 
472
- Acquisition exceptions, rejected promises, and unexpected failures are normalized
473
- through `better-result`; release failures use `ResourceReleaseFailure`.
171
+ It does not introduce a fiber runtime, scheduler, streams, queues or a public `Effect<A, E, R>` abstraction.
474
172
 
475
- ## Service vs Layer vs Scope vs Resource
173
+ `Effect.gen` builds on `better-result` generator composition while carrying Service requirements through the TypeScript type system.
476
174
 
477
- | Primitive | Responsibility |
478
- | --------------- | ----------------------------------------------------- |
479
- | `Service` | Request a contextual dependency |
480
- | `Layer` | Describe the implementations that form an environment |
481
- | `Scope` | Manage dynamic lifetimes and finalizers |
482
- | `Resource` | Standalone Result-oriented acquire/use/release helper |
483
- | DI backend | Resolve and cache service instances |
484
- | `better-result` | Typed failures and generator control flow |
175
+ Dependency resolution stays behind a pluggable backend.
485
176
 
486
- ## Complete example
177
+ Your application can keep using ordinary Promises and existing libraries.
487
178
 
488
- The repository contains a TODO API example under:
179
+ ---
489
180
 
490
- ```text
491
- examples/todo-api
492
- ```
181
+ ## How it compares
493
182
 
494
- It demonstrates:
183
+ | | better-result | better-effect | Effect |
184
+ | ----------------------------- | ------------- | ------------------- | --------------- |
185
+ | Typed success/failure | ✓ | ✓ via better-result | ✓ |
186
+ | Generator composition | ✓ | ✓ | ✓ |
187
+ | Contextual Services | — | ✓ | ✓ |
188
+ | Dependency requirements | — | ✓ | ✓ |
189
+ | Checked environments | — | ✓ | ✓ |
190
+ | Scoped resource lifetimes | — | ✓ | ✓ |
191
+ | Pluggable external DI backend | — | ✓ | different model |
192
+ | Fiber runtime | — | — | ✓ |
193
+ | Structured concurrency | — | — | ✓ |
194
+ | Streams / queues / schedules | — | — | ✓ |
195
+ | Full effect ecosystem | — | — | ✓ |
495
196
 
496
- - Bun HTTP server
497
- - SQLite in memory with `Bun.SQL`
498
- - user login
499
- - session authentication
500
- - TODO CRUD
501
- - `Service` dependency access
502
- - Layer composition
503
- - Runtime root and execution scopes
504
- - scoped database lifecycle
505
- - Standalone `Resource` compatibility API
506
- - ITI as the DI backend
197
+ ### Choose `better-result`
507
198
 
508
- Run it from the repository root:
199
+ When typed error handling and Result composition are enough.
509
200
 
510
- ```bash
511
- bun examples/todo-api/index.ts
512
- ```
201
+ ### Add `better-effect`
513
202
 
514
- ## Development
203
+ When your Result-based application also needs contextual Services, typechecked application wiring, composable environments, or resource lifetime management.
515
204
 
516
- Install dependencies:
205
+ ### Choose Effect
517
206
 
518
- ```bash
519
- bun install
520
- ```
207
+ When you want a complete effect system and its runtime, concurrency model, dependency model, resource management and broader ecosystem.
521
208
 
522
- Run tests:
209
+ `better-effect` is inspired by some of those ideas. It is intentionally not a reimplementation of the whole system.
523
210
 
524
- ```bash
525
- bun test
526
- ```
211
+ ---
527
212
 
528
- Typecheck:
213
+ ## Core ideas
529
214
 
530
- ```bash
531
- bun run typecheck
532
- ```
215
+ ### Typechecked wiring
533
216
 
534
- Run the full quality gate:
535
-
536
- ```bash
537
- bun run check
538
- ```
217
+ **Service requirements Layer completeness → Runtime validation**
539
218
 
540
- The project uses:
219
+ Use a Service and its requirement follows the program.
541
220
 
542
- - Bun as package manager and test runner
543
- - TypeScript
544
- - tsdown for library builds
545
- - Oxlint
546
- - Oxfmt
547
- - publint
221
+ Build an incomplete environment and TypeScript tells you what is missing.
548
222
 
549
- ## Design principles
223
+ Run a program against an incompatible Runtime and the mismatch remains visible at compile time.
550
224
 
551
- ### Keep the core small
225
+ ### Composable environments
552
226
 
553
- `better-effect` should not grow into a second Effect runtime.
227
+ **Layer merge override DI backend**
554
228
 
555
- The core intentionally does not implement:
229
+ Describe application implementations independently from the container responsible for resolving them.
556
230
 
557
- - fibers
558
- - schedules
559
- - streams
560
- - queues
561
- - a dependency graph runtime
562
- - a custom DI container
563
- - a custom Context
564
- - `Effect<A, E, R>`
231
+ Compose production environments and replace selected implementations for tests.
565
232
 
566
- ### Delegate instead of rebuilding
233
+ ### Scoped lifetimes
567
234
 
568
- Dependency resolution and caching belong to DI backends. Service release lifecycle is
569
- owned by `Scope`.
235
+ **Scope scoped Layers acquire/release graceful Runtime disposal**
570
236
 
571
- `better-effect` supplies the protocol and composition primitives.
237
+ Make ownership explicit for resources that need cleanup.
572
238
 
573
- ### Preserve inference at public boundaries
239
+ Application resources live with the Runtime. Execution resources live with the execution.
574
240
 
575
- Type safety is part of the API.
241
+ ### better-result underneath
576
242
 
577
- For example:
243
+ **Result → Result.gen → Effect.gen → pipe**
578
244
 
579
- ```ts
580
- const auth = yield * AuthService
581
- // AuthService
582
- ```
245
+ Keep `better-result` as the source of truth for typed successes, failures, short-circuiting
246
+ and generator control flow. `Effect.gen` delegates to `Result.gen`; it adds only the
247
+ phantom Service requirements that TypeScript needs to check the application environment.
248
+ At runtime, an `EffectResult` is still a `better-result` Result; the requirements exist
249
+ only in the type.
583
250
 
584
- and:
251
+ For a linear workflow, `pipe` composes the same kind of program without introducing a
252
+ second Result model or a lazy Effect runtime:
585
253
 
586
254
  ```ts
587
- const database = await ServiceRuntime.resolve(Database)
588
- // Database
589
- ```
590
-
591
- must preserve exact instance types.
255
+ import { Effect, pipe } from 'better-effect'
592
256
 
593
- ### Type erasure stays internal
594
-
595
- Layers may store heterogeneous providers internally using erased types.
596
-
597
- The public constructors (`Layer.make`, `Layer.succeed`, `Layer.scoped`) are responsible for preserving the relationship between a Service token and its instance type.
598
-
599
- ## Current scope
600
-
601
- The project is intentionally small and experimental.
602
-
603
- The initial scope is:
604
-
605
- ```text
606
- Service
607
- Layer
608
- Scope
609
- Resource
610
- DI adapters
611
- better-result integration
257
+ const program = pipe(
258
+ findUser(id),
259
+ Effect.map((user: User) => user.email),
260
+ Effect.andThen(loadPermissions),
261
+ Effect.mapError((cause: LoadUserError | PermissionError) => new ApplicationError({ cause }))
262
+ )
612
263
  ```
613
264
 
614
- New abstractions should only be added when they solve a concrete problem without duplicating responsibilities already handled well by another library.
615
-
616
- ## License
265
+ The combinators keep the `better-result` semantics: `Effect.map` changes the success
266
+ type, `Effect.mapError` changes the error type, and `Effect.andThen` only calls the next
267
+ step after an `Ok`. The pipeline carries the requirements of every step, so Runtime
268
+ still rejects it when its Layer does not provide every required Service.
617
269
 
618
- MIT
270
+ Use `Effect.gen` for larger workflows with several intermediate values, branches or
271
+ procedural logic. Use `pipe` for concise, linear composition; both are ways to compose
272
+ `better-result` programs while keeping dependency checking in the `better-effect` layer.