better-effect 0.3.0 → 0.5.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 +167 -486
- package/dist/adapters/iti.d.mts +11 -3
- package/dist/adapters/iti.d.mts.map +1 -1
- package/dist/adapters/iti.mjs +34 -14
- package/dist/adapters/iti.mjs.map +1 -1
- package/dist/errors-GR3K_nRu.mjs +74 -0
- package/dist/errors-GR3K_nRu.mjs.map +1 -0
- package/dist/index-D77AvuBl.d.mts +510 -0
- package/dist/index-D77AvuBl.d.mts.map +1 -0
- package/dist/index.d.mts +263 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +444 -106
- package/dist/index.mjs.map +1 -1
- package/dist/internal-identity-BnZC3Au-.mjs +26 -0
- package/dist/internal-identity-BnZC3Au-.mjs.map +1 -0
- package/dist/testing.d.mts +11 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +33 -12
- package/dist/testing.mjs.map +1 -1
- package/package.json +6 -9
- package/dist/errors-CnvKqBpb.mjs +0 -63
- package/dist/errors-CnvKqBpb.mjs.map +0 -1
- package/dist/index-Bg6ofRE1.d.mts +0 -232
- package/dist/index-Bg6ofRE1.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -1,618 +1,299 @@
|
|
|
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>()('Database') {
|
|
20
|
+
findUser(id: string) {
|
|
21
|
+
// ...
|
|
41
22
|
}
|
|
42
23
|
}
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
class UserRepository extends Service<UserRepository>()('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)
|
|
30
|
+
return Result.ok(await database.findUser(id))
|
|
52
31
|
})
|
|
53
32
|
}
|
|
54
33
|
}
|
|
55
|
-
```
|
|
56
34
|
|
|
57
|
-
|
|
58
|
-
`better-result` while carrying the required Service tokens in a type-only
|
|
59
|
-
metadata channel:
|
|
35
|
+
const UserRepositoryLive = Layer.make(UserRepository)
|
|
60
36
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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)
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
}
|
|
37
|
+
await Runtime.make(UserRepositoryLive, backend)
|
|
38
|
+
// ^^^^^^^^^^^^^^^^^^
|
|
39
|
+
// Type error: Database is required but not provided
|
|
72
40
|
```
|
|
73
41
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
84
|
-
|
|
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
|
-
)
|
|
102
|
-
```
|
|
103
|
-
|
|
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.
|
|
108
|
-
|
|
109
|
-
Use an unparameterized `Runtime` or `BuiltLayer` annotation only when an
|
|
110
|
-
intentionally erased, unchecked environment is needed:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
const erased: Runtime = runtime
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
There are no string tokens:
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
const database = yield * Database
|
|
120
|
-
```
|
|
42
|
+
The explicit self type keeps `yield*` inference exact, while the non-empty
|
|
43
|
+
literal is the Service's stable logical identity. Services with identical
|
|
44
|
+
methods but different tags are different dependencies; use a namespaced tag
|
|
45
|
+
such as `@acme/Database` when identities must be shared across packages.
|
|
121
46
|
|
|
122
|
-
|
|
47
|
+
`UserRepository` used `Database`, so `Database` became part of its environment requirements.
|
|
123
48
|
|
|
124
|
-
|
|
49
|
+
No dependency list was written manually.
|
|
125
50
|
|
|
126
|
-
|
|
51
|
+
Services can also describe a contract without requiring a class instance. Use the
|
|
52
|
+
static `of` helper to type-check a structural implementation; it returns the same
|
|
53
|
+
object unchanged at runtime:
|
|
127
54
|
|
|
128
55
|
```ts
|
|
129
|
-
Service<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
and prevents string-key collisions or typos.
|
|
56
|
+
class Authorization extends Service<Authorization>()('Authorization') {
|
|
57
|
+
declare readonly authorize: (token: string) => Promise<boolean>
|
|
58
|
+
}
|
|
133
59
|
|
|
134
|
-
|
|
60
|
+
const authorization = Authorization.of({
|
|
61
|
+
authorize: async (token) => token.length > 0
|
|
62
|
+
})
|
|
135
63
|
|
|
136
|
-
|
|
137
|
-
yield* AuthService
|
|
138
|
-
│
|
|
139
|
-
▼
|
|
140
|
-
ServiceRuntime
|
|
141
|
-
│
|
|
142
|
-
▼
|
|
143
|
-
ServiceResolver
|
|
144
|
-
│
|
|
145
|
-
▼
|
|
146
|
-
AuthService instance
|
|
64
|
+
const AuthorizationLive = Layer.succeed(Authorization, authorization)
|
|
147
65
|
```
|
|
148
66
|
|
|
149
|
-
|
|
67
|
+
`Authorization.of(...)` does not call a constructor or make the result an
|
|
68
|
+
`instanceof Authorization`. For services with constructors, private fields or
|
|
69
|
+
other runtime invariants, use `new Authorization(...)` instead.
|
|
150
70
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
It does **not** implement dependency resolution. That remains the responsibility of the configured backend.
|
|
71
|
+
Provide it and the environment becomes complete:
|
|
154
72
|
|
|
155
73
|
```ts
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
export const DatabaseLive = Layer.scoped(
|
|
159
|
-
Database,
|
|
160
|
-
async () => {
|
|
161
|
-
const database = new Database()
|
|
74
|
+
const DatabaseLive = Layer.make(Database)
|
|
162
75
|
|
|
163
|
-
|
|
76
|
+
const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
|
|
164
77
|
|
|
165
|
-
|
|
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)
|
|
78
|
+
const runtime = await Runtime.make(AppLive, backend)
|
|
185
79
|
```
|
|
186
80
|
|
|
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
|
-
```
|
|
81
|
+
And the contract does not disappear after startup.
|
|
198
82
|
|
|
199
|
-
|
|
200
|
-
resource that must be released with the Runtime root Scope:
|
|
83
|
+
A Runtime also knows which Services exist in its environment:
|
|
201
84
|
|
|
202
85
|
```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* () {
|
|
86
|
+
await runtime.run(() =>
|
|
87
|
+
Effect.gen(async function* () {
|
|
216
88
|
const database = yield* Database
|
|
217
89
|
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
(session, outcome) => session.close(outcome)
|
|
90
|
+
return Result.ok(database)
|
|
91
|
+
})
|
|
221
92
|
)
|
|
222
93
|
```
|
|
223
94
|
|
|
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:
|
|
234
|
-
|
|
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.
|
|
95
|
+
If a program asks that Runtime for a Service its environment does not provide, TypeScript rejects the call.
|
|
244
96
|
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
97
|
+
```text
|
|
98
|
+
yield* Database
|
|
99
|
+
│
|
|
100
|
+
▼
|
|
101
|
+
program requires Database
|
|
102
|
+
│
|
|
103
|
+
▼
|
|
104
|
+
Layer provides Database?
|
|
105
|
+
│
|
|
106
|
+
no ├──────────► TypeScript error
|
|
107
|
+
│
|
|
108
|
+
yes
|
|
109
|
+
▼
|
|
110
|
+
Runtime can execute it
|
|
257
111
|
```
|
|
258
112
|
|
|
259
|
-
|
|
113
|
+
We call this **typechecked wiring**.
|
|
260
114
|
|
|
261
|
-
|
|
115
|
+
The Services your code uses, the implementations your Layers provide, and the programs your Runtime executes participate in the same type-level contract.
|
|
262
116
|
|
|
263
|
-
|
|
117
|
+
---
|
|
264
118
|
|
|
265
|
-
|
|
266
|
-
inside `runtime.run()` belong to that execution and are released automatically when it
|
|
267
|
-
finishes.
|
|
119
|
+
## Why better-effect?
|
|
268
120
|
|
|
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:
|
|
121
|
+
`better-result` already gives TypeScript applications an excellent model for typed failures.
|
|
273
122
|
|
|
274
|
-
|
|
275
|
-
const scope = yield * Scope
|
|
276
|
-
const child = scope.fork()
|
|
123
|
+
But typed errors are only one part of a growing application.
|
|
277
124
|
|
|
278
|
-
|
|
279
|
-
await Scope.provide(child, () => processBatch())
|
|
280
|
-
} finally {
|
|
281
|
-
await child.close()
|
|
282
|
-
}
|
|
283
|
-
```
|
|
125
|
+
Eventually you also need to answer:
|
|
284
126
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
127
|
+
- What does this service depend on?
|
|
128
|
+
- Did the application provide every dependency?
|
|
129
|
+
- Can this program run in this environment?
|
|
130
|
+
- How do I replace implementations in tests?
|
|
131
|
+
- Who owns this database connection?
|
|
132
|
+
- When should this resource be released?
|
|
288
133
|
|
|
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
|
-
```
|
|
134
|
+
Those problems are often discovered through container errors, startup failures, test setup, or manual composition-root maintenance.
|
|
304
135
|
|
|
305
|
-
|
|
306
|
-
release callback is registered in the current Scope and remains Scope cleanup, so it
|
|
307
|
-
runs when the owning execution closes.
|
|
136
|
+
Effect has powerful ideas for solving them.
|
|
308
137
|
|
|
309
|
-
|
|
310
|
-
`Symbol.asyncDispose`, `Effect.add()` registers it in the current Scope and yields the
|
|
311
|
-
same object back to the program:
|
|
138
|
+
`better-effect` explores a smaller path:
|
|
312
139
|
|
|
313
|
-
|
|
314
|
-
const result = await runtime.run(() =>
|
|
315
|
-
Effect.gen(async function* () {
|
|
316
|
-
const file = await createTemporaryFile()
|
|
317
|
-
const ownedFile = yield* Effect.add(file)
|
|
318
|
-
|
|
319
|
-
return Result.ok(await readFile(ownedFile))
|
|
320
|
-
})
|
|
321
|
-
)
|
|
322
|
-
```
|
|
140
|
+
**keep `better-result`, Promises and normal TypeScript — borrow the architectural ideas that make dependencies and resource lifetimes easier to reason about.**
|
|
323
141
|
|
|
324
|
-
|
|
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.
|
|
142
|
+
### Know your dependencies before runtime
|
|
328
143
|
|
|
329
|
-
|
|
330
|
-
an explicit callback or the final `ScopeOutcome`; use `Effect.add()` for an
|
|
331
|
-
already-acquired JavaScript disposable.
|
|
144
|
+
Services can be requested directly:
|
|
332
145
|
|
|
333
146
|
```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
|
-
)
|
|
147
|
+
const database = yield * Database
|
|
349
148
|
```
|
|
350
149
|
|
|
351
|
-
|
|
150
|
+
That access is also captured by the type system.
|
|
352
151
|
|
|
353
|
-
|
|
354
|
-
const file = await scope.add(await createTemporaryFile())
|
|
355
|
-
```
|
|
152
|
+
Layers know both what they provide and what their Services require. Incomplete environments can therefore fail during typechecking instead of application startup.
|
|
356
153
|
|
|
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.
|
|
154
|
+
Runtime keeps that environment information and checks programs against it when they run.
|
|
361
155
|
|
|
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.
|
|
156
|
+
### Compose application environments
|
|
366
157
|
|
|
367
|
-
|
|
368
|
-
parent, while `Scope.provide()` uses an existing scope without taking ownership of its
|
|
369
|
-
closure:
|
|
158
|
+
Layers describe implementations without making your application code depend on a specific DI container.
|
|
370
159
|
|
|
371
160
|
```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
|
-
}
|
|
161
|
+
const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive, AuthServiceLive)
|
|
381
162
|
```
|
|
382
163
|
|
|
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:
|
|
164
|
+
Testing can replace implementations explicitly:
|
|
394
165
|
|
|
395
166
|
```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
|
|
167
|
+
const AppTest = Layer.override(AppLive, DatabaseTest)
|
|
413
168
|
```
|
|
414
169
|
|
|
415
|
-
|
|
170
|
+
The environment contract remains typed after the override.
|
|
416
171
|
|
|
417
|
-
|
|
418
|
-
stop accepting executions
|
|
419
|
-
↓
|
|
420
|
-
wait for active executions
|
|
421
|
-
↓
|
|
422
|
-
close the root Scope
|
|
423
|
-
↓
|
|
424
|
-
dispose the backend
|
|
425
|
-
```
|
|
172
|
+
### Own resource lifetimes
|
|
426
173
|
|
|
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.
|
|
174
|
+
Some dependencies are values.
|
|
435
175
|
|
|
436
|
-
|
|
176
|
+
Others own connections, sessions, files or other resources.
|
|
437
177
|
|
|
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.
|
|
178
|
+
`Layer.scoped`, `Layer.scopedGen`, `Effect.acquireRelease`, `Effect.add` and `Scope` make their lifetime explicit.
|
|
441
179
|
|
|
442
180
|
```ts
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
acquire: () => database.begin(),
|
|
449
|
-
|
|
450
|
-
use: (transaction) => executeCommand(transaction),
|
|
451
|
-
|
|
452
|
-
release: (transaction) => transaction.close()
|
|
453
|
-
})
|
|
181
|
+
const DatabaseLive = Layer.scoped(
|
|
182
|
+
Database,
|
|
183
|
+
() => Database.connect(),
|
|
184
|
+
(database) => database.close()
|
|
185
|
+
)
|
|
454
186
|
```
|
|
455
187
|
|
|
456
|
-
|
|
457
|
-
management protocol:
|
|
188
|
+
Runtime owns the application lifetime and safely releases scoped resources when that lifetime ends.
|
|
458
189
|
|
|
459
|
-
|
|
460
|
-
Symbol.asyncDispose
|
|
461
|
-
Symbol.dispose
|
|
462
|
-
```
|
|
190
|
+
Resources acquired during an individual execution belong to that execution instead.
|
|
463
191
|
|
|
464
|
-
|
|
192
|
+
### Keep your runtime choices
|
|
465
193
|
|
|
466
|
-
|
|
467
|
-
1. use failure
|
|
468
|
-
2. release failure
|
|
469
|
-
3. successful use value
|
|
470
|
-
```
|
|
194
|
+
`better-effect` is not a replacement implementation of Effect.
|
|
471
195
|
|
|
472
|
-
|
|
473
|
-
through `better-result`; release failures use `ResourceReleaseFailure`.
|
|
196
|
+
It does not introduce a fiber runtime, scheduler, streams, queues or a public `Effect<A, E, R>` abstraction.
|
|
474
197
|
|
|
475
|
-
|
|
198
|
+
`Effect.gen` builds on `better-result` generator composition while carrying Service requirements through the TypeScript type system.
|
|
476
199
|
|
|
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 |
|
|
200
|
+
Dependency resolution stays behind a pluggable backend.
|
|
485
201
|
|
|
486
|
-
|
|
202
|
+
Your application can keep using ordinary Promises and existing libraries.
|
|
487
203
|
|
|
488
|
-
|
|
204
|
+
---
|
|
489
205
|
|
|
490
|
-
|
|
491
|
-
examples/todo-api
|
|
492
|
-
```
|
|
206
|
+
## How it compares
|
|
493
207
|
|
|
494
|
-
|
|
208
|
+
| | better-result | better-effect | Effect |
|
|
209
|
+
| ----------------------------- | ------------- | ------------------- | --------------- |
|
|
210
|
+
| Typed success/failure | ✓ | ✓ via better-result | ✓ |
|
|
211
|
+
| Generator composition | ✓ | ✓ | ✓ |
|
|
212
|
+
| Contextual Services | — | ✓ | ✓ |
|
|
213
|
+
| Dependency requirements | — | ✓ | ✓ |
|
|
214
|
+
| Checked environments | — | ✓ | ✓ |
|
|
215
|
+
| Scoped resource lifetimes | — | ✓ | ✓ |
|
|
216
|
+
| Pluggable external DI backend | — | ✓ | different model |
|
|
217
|
+
| Fiber runtime | — | — | ✓ |
|
|
218
|
+
| Structured concurrency | — | — | ✓ |
|
|
219
|
+
| Streams / queues / schedules | — | — | ✓ |
|
|
220
|
+
| Full effect ecosystem | — | — | ✓ |
|
|
495
221
|
|
|
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
|
|
222
|
+
### Choose `better-result`
|
|
507
223
|
|
|
508
|
-
|
|
224
|
+
When typed error handling and Result composition are enough.
|
|
509
225
|
|
|
510
|
-
|
|
511
|
-
bun examples/todo-api/index.ts
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
## Development
|
|
226
|
+
### Add `better-effect`
|
|
515
227
|
|
|
516
|
-
|
|
228
|
+
When your Result-based application also needs contextual Services, typechecked application wiring, composable environments, or resource lifetime management.
|
|
517
229
|
|
|
518
|
-
|
|
519
|
-
bun install
|
|
520
|
-
```
|
|
230
|
+
### Choose Effect
|
|
521
231
|
|
|
522
|
-
|
|
232
|
+
When you want a complete effect system and its runtime, concurrency model, dependency model, resource management and broader ecosystem.
|
|
523
233
|
|
|
524
|
-
|
|
525
|
-
bun test
|
|
526
|
-
```
|
|
234
|
+
`better-effect` is inspired by some of those ideas. It is intentionally not a reimplementation of the whole system.
|
|
527
235
|
|
|
528
|
-
|
|
236
|
+
---
|
|
529
237
|
|
|
530
|
-
|
|
531
|
-
bun run typecheck
|
|
532
|
-
```
|
|
238
|
+
## Core ideas
|
|
533
239
|
|
|
534
|
-
|
|
240
|
+
### Typechecked wiring
|
|
535
241
|
|
|
536
|
-
|
|
537
|
-
bun run check
|
|
538
|
-
```
|
|
242
|
+
**Service requirements → Layer completeness → Runtime validation**
|
|
539
243
|
|
|
540
|
-
|
|
244
|
+
Use a Service and its requirement follows the program.
|
|
541
245
|
|
|
542
|
-
|
|
543
|
-
- TypeScript
|
|
544
|
-
- tsdown for library builds
|
|
545
|
-
- Oxlint
|
|
546
|
-
- Oxfmt
|
|
547
|
-
- publint
|
|
246
|
+
Build an incomplete environment and TypeScript tells you what is missing.
|
|
548
247
|
|
|
549
|
-
|
|
248
|
+
Run a program against an incompatible Runtime and the mismatch remains visible at compile time.
|
|
550
249
|
|
|
551
|
-
###
|
|
250
|
+
### Composable environments
|
|
552
251
|
|
|
553
|
-
|
|
252
|
+
**Layer → merge → override → DI backend**
|
|
554
253
|
|
|
555
|
-
|
|
254
|
+
Describe application implementations independently from the container responsible for resolving them.
|
|
556
255
|
|
|
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>`
|
|
256
|
+
Compose production environments and replace selected implementations for tests.
|
|
565
257
|
|
|
566
|
-
###
|
|
258
|
+
### Scoped lifetimes
|
|
567
259
|
|
|
568
|
-
|
|
569
|
-
owned by `Scope`.
|
|
260
|
+
**Scope → scoped Layers → acquire/release → graceful Runtime disposal**
|
|
570
261
|
|
|
571
|
-
|
|
262
|
+
Make ownership explicit for resources that need cleanup.
|
|
572
263
|
|
|
573
|
-
|
|
264
|
+
Application resources live with the Runtime. Execution resources live with the execution.
|
|
574
265
|
|
|
575
|
-
|
|
266
|
+
### better-result underneath
|
|
576
267
|
|
|
577
|
-
|
|
268
|
+
**Result → Result.gen → Effect.gen → pipe**
|
|
578
269
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
270
|
+
Keep `better-result` as the source of truth for typed successes, failures, short-circuiting
|
|
271
|
+
and generator control flow. `Effect.gen` delegates to `Result.gen`; it adds only the
|
|
272
|
+
phantom Service requirements that TypeScript needs to check the application environment.
|
|
273
|
+
At runtime, an `EffectResult` is still a `better-result` Result; the requirements exist
|
|
274
|
+
only in the type.
|
|
583
275
|
|
|
584
|
-
|
|
276
|
+
For a linear workflow, `pipe` composes the same kind of program without introducing a
|
|
277
|
+
second Result model or a lazy Effect runtime:
|
|
585
278
|
|
|
586
279
|
```ts
|
|
587
|
-
|
|
588
|
-
// Database
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
must preserve exact instance types.
|
|
280
|
+
import { Effect, pipe } from 'better-effect'
|
|
592
281
|
|
|
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
|
|
282
|
+
const program = pipe(
|
|
283
|
+
findUser(id),
|
|
284
|
+
Effect.map((user: User) => user.email),
|
|
285
|
+
Effect.andThen(loadPermissions),
|
|
286
|
+
Effect.mapError((cause: LoadUserError | PermissionError) => new ApplicationError({ cause }))
|
|
287
|
+
)
|
|
612
288
|
```
|
|
613
289
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
290
|
+
The combinators keep the `better-result` semantics: `Effect.map` changes the success
|
|
291
|
+
type, `Effect.mapError` changes the error type, and `Effect.andThen` only calls the next
|
|
292
|
+
step after an `Ok`. Use `Effect.andThenAsync` when the next operation returns a
|
|
293
|
+
`Promise<Result>`; it always returns a Promise, including when the source is synchronous
|
|
294
|
+
or already an `Err`. The pipeline carries the requirements of every step, so Runtime
|
|
295
|
+
still rejects it when its Layer does not provide every required Service.
|
|
617
296
|
|
|
618
|
-
|
|
297
|
+
Use `Effect.gen` for larger workflows with several intermediate values, branches or
|
|
298
|
+
procedural logic. Use `pipe` for concise, linear composition; both are ways to compose
|
|
299
|
+
`better-result` programs while keeping dependency checking in the `better-effect` layer.
|