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 +148 -494
- package/dist/adapters/iti.d.mts +1 -1
- package/dist/adapters/iti.mjs +1 -1
- package/dist/{errors-CnvKqBpb.mjs → errors-DlHCwICc.mjs} +5 -1
- package/dist/{errors-CnvKqBpb.mjs.map → errors-DlHCwICc.mjs.map} +1 -1
- package/dist/{index-Bg6ofRE1.d.mts → index-BYQKfyeJ.d.mts} +13 -10
- package/dist/index-BYQKfyeJ.d.mts.map +1 -0
- package/dist/index.d.mts +55 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +65 -3
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +6 -9
- package/dist/index-Bg6ofRE1.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -1,618 +1,272 @@
|
|
|
1
1
|
# better-effect
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Effect-like dependency safety for better-result.**
|
|
4
4
|
|
|
5
|
-
`better-
|
|
5
|
+
Type your errors with `better-result`. Typecheck the rest of your application wiring with `better-effect`.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
findUser(
|
|
37
|
-
|
|
38
|
-
id: '1',
|
|
39
|
-
email
|
|
40
|
-
})
|
|
19
|
+
class Database extends Service<Database>() {
|
|
20
|
+
findUser(id: string) {
|
|
21
|
+
// ...
|
|
41
22
|
}
|
|
42
23
|
}
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
intentionally erased, unchecked environment is needed:
|
|
44
|
+
No dependency list was written manually.
|
|
111
45
|
|
|
112
|
-
|
|
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
|
|
120
|
-
```
|
|
49
|
+
const DatabaseLive = Layer.make(Database, () => new Database())
|
|
121
50
|
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|
219
|
-
}
|
|
220
|
-
(session, outcome) => session.close(outcome)
|
|
65
|
+
return Result.ok(database)
|
|
66
|
+
})
|
|
221
67
|
)
|
|
222
68
|
```
|
|
223
69
|
|
|
224
|
-
|
|
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
|
-
```
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
88
|
+
We call this **typechecked wiring**.
|
|
260
89
|
|
|
261
|
-
|
|
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
|
-
|
|
92
|
+
---
|
|
264
93
|
|
|
265
|
-
|
|
266
|
-
inside `runtime.run()` belong to that execution and are released automatically when it
|
|
267
|
-
finishes.
|
|
94
|
+
## Why better-effect?
|
|
268
95
|
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
await Scope.provide(child, () => processBatch())
|
|
280
|
-
} finally {
|
|
281
|
-
await child.close()
|
|
282
|
-
}
|
|
283
|
-
```
|
|
100
|
+
Eventually you also need to answer:
|
|
284
101
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
125
|
+
That access is also captured by the type system.
|
|
352
126
|
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
145
|
+
The environment contract remains typed after the override.
|
|
416
146
|
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
Others own connections, sessions, files or other resources.
|
|
437
152
|
|
|
438
|
-
`
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
457
|
-
management protocol:
|
|
163
|
+
Runtime owns the application lifetime and safely releases scoped resources when that lifetime ends.
|
|
458
164
|
|
|
459
|
-
|
|
460
|
-
Symbol.asyncDispose
|
|
461
|
-
Symbol.dispose
|
|
462
|
-
```
|
|
165
|
+
Resources acquired during an individual execution belong to that execution instead.
|
|
463
166
|
|
|
464
|
-
|
|
167
|
+
### Keep your runtime choices
|
|
465
168
|
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
`Effect.gen` builds on `better-result` generator composition while carrying Service requirements through the TypeScript type system.
|
|
476
174
|
|
|
477
|
-
|
|
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
|
-
|
|
177
|
+
Your application can keep using ordinary Promises and existing libraries.
|
|
487
178
|
|
|
488
|
-
|
|
179
|
+
---
|
|
489
180
|
|
|
490
|
-
|
|
491
|
-
examples/todo-api
|
|
492
|
-
```
|
|
181
|
+
## How it compares
|
|
493
182
|
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
+
When typed error handling and Result composition are enough.
|
|
509
200
|
|
|
510
|
-
|
|
511
|
-
bun examples/todo-api/index.ts
|
|
512
|
-
```
|
|
201
|
+
### Add `better-effect`
|
|
513
202
|
|
|
514
|
-
|
|
203
|
+
When your Result-based application also needs contextual Services, typechecked application wiring, composable environments, or resource lifetime management.
|
|
515
204
|
|
|
516
|
-
|
|
205
|
+
### Choose Effect
|
|
517
206
|
|
|
518
|
-
|
|
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
|
-
|
|
209
|
+
`better-effect` is inspired by some of those ideas. It is intentionally not a reimplementation of the whole system.
|
|
523
210
|
|
|
524
|
-
|
|
525
|
-
bun test
|
|
526
|
-
```
|
|
211
|
+
---
|
|
527
212
|
|
|
528
|
-
|
|
213
|
+
## Core ideas
|
|
529
214
|
|
|
530
|
-
|
|
531
|
-
bun run typecheck
|
|
532
|
-
```
|
|
215
|
+
### Typechecked wiring
|
|
533
216
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
```bash
|
|
537
|
-
bun run check
|
|
538
|
-
```
|
|
217
|
+
**Service requirements → Layer completeness → Runtime validation**
|
|
539
218
|
|
|
540
|
-
|
|
219
|
+
Use a Service and its requirement follows the program.
|
|
541
220
|
|
|
542
|
-
|
|
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
|
-
|
|
223
|
+
Run a program against an incompatible Runtime and the mismatch remains visible at compile time.
|
|
550
224
|
|
|
551
|
-
###
|
|
225
|
+
### Composable environments
|
|
552
226
|
|
|
553
|
-
|
|
227
|
+
**Layer → merge → override → DI backend**
|
|
554
228
|
|
|
555
|
-
|
|
229
|
+
Describe application implementations independently from the container responsible for resolving them.
|
|
556
230
|
|
|
557
|
-
|
|
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
|
-
###
|
|
233
|
+
### Scoped lifetimes
|
|
567
234
|
|
|
568
|
-
|
|
569
|
-
owned by `Scope`.
|
|
235
|
+
**Scope → scoped Layers → acquire/release → graceful Runtime disposal**
|
|
570
236
|
|
|
571
|
-
|
|
237
|
+
Make ownership explicit for resources that need cleanup.
|
|
572
238
|
|
|
573
|
-
|
|
239
|
+
Application resources live with the Runtime. Execution resources live with the execution.
|
|
574
240
|
|
|
575
|
-
|
|
241
|
+
### better-result underneath
|
|
576
242
|
|
|
577
|
-
|
|
243
|
+
**Result → Result.gen → Effect.gen → pipe**
|
|
578
244
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
588
|
-
// Database
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
must preserve exact instance types.
|
|
255
|
+
import { Effect, pipe } from 'better-effect'
|
|
592
256
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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.
|