@danieljvdm/dev-kit 0.11.3 → 0.13.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 +111 -78
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/build-effect-clis/SKILL.md +61 -0
- package/skills/build-effect-clis/agents/openai.yaml +4 -0
- package/skills/build-effect-clis/references/command-design.md +93 -0
- package/skills/build-effect-clis/references/entrypoints-and-testing.md +59 -0
- package/skills/build-effect-clis/references/processes-and-platform.md +73 -0
- package/skills/dev-kit/SKILL.md +77 -48
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +86 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +3 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +18 -16
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +181 -131
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
|
@@ -1,989 +0,0 @@
|
|
|
1
|
-
# Layers Guide
|
|
2
|
-
|
|
3
|
-
This guide covers service ownership, service design, Layer construction,
|
|
4
|
-
dependency visibility, composition, and provisioning.
|
|
5
|
-
|
|
6
|
-
Key source files:
|
|
7
|
-
|
|
8
|
-
- `packages/effect/src/Context.ts`
|
|
9
|
-
- `packages/effect/src/Layer.ts`
|
|
10
|
-
- `packages/effect/src/Effect.ts`
|
|
11
|
-
- `packages/effect/src/ManagedRuntime.ts`
|
|
12
|
-
|
|
13
|
-
## Mental Model
|
|
14
|
-
|
|
15
|
-
A service is a typed dependency.
|
|
16
|
-
|
|
17
|
-
A layer is a recipe for building one or more services, possibly using other services as dependencies.
|
|
18
|
-
|
|
19
|
-
Effect's model is:
|
|
20
|
-
|
|
21
|
-
- define service identifiers with `Context.Service` or `Context.Service(...)`
|
|
22
|
-
- require services from effects with `Effect.service` or by yielding the service key directly
|
|
23
|
-
- build implementations with `Layer`
|
|
24
|
-
- provide layers at the program boundary or subsystem boundary
|
|
25
|
-
|
|
26
|
-
`Layer<ROut, E, RIn>` means:
|
|
27
|
-
|
|
28
|
-
- `ROut`: services produced by the layer
|
|
29
|
-
- `E`: possible failures while constructing the layer
|
|
30
|
-
- `RIn`: dependencies required to build it
|
|
31
|
-
|
|
32
|
-
Repo references:
|
|
33
|
-
|
|
34
|
-
- `packages/effect/src/Context.ts`
|
|
35
|
-
- `packages/effect/src/Layer.ts`
|
|
36
|
-
|
|
37
|
-
## Service Or Value Decision
|
|
38
|
-
|
|
39
|
-
Treat a service as an authority seam: a cohesive capability whose requirements
|
|
40
|
-
should propagate through the Effect environment.
|
|
41
|
-
|
|
42
|
-
Create or retain a service when it owns persistence, credentials, external I/O,
|
|
43
|
-
runtime resources, configuration, time, randomness, lifecycle, reusable
|
|
44
|
-
effectful policy, or state with meaningful production and test variation.
|
|
45
|
-
Prefer an existing Effect capability such as `Clock`, `Config`, `Random`,
|
|
46
|
-
`HttpClient`, `FileSystem`, or `Path` when it already owns that authority.
|
|
47
|
-
|
|
48
|
-
Keep parsed inputs, per-call request data, deterministic calculations,
|
|
49
|
-
adapter-confined framework values, and forwarding wrappers as values or pure
|
|
50
|
-
modules.
|
|
51
|
-
|
|
52
|
-
Use the deletion test: if removing the service merely eliminates indirection,
|
|
53
|
-
it is probably not an authority seam; if removal spreads resource ownership,
|
|
54
|
-
policy, or implementation complexity into callers, it probably is. Test
|
|
55
|
-
convenience by itself does not justify a service.
|
|
56
|
-
|
|
57
|
-
## Services
|
|
58
|
-
|
|
59
|
-
## What A Service Is
|
|
60
|
-
|
|
61
|
-
A service is a typed key plus its implementation shape.
|
|
62
|
-
|
|
63
|
-
In Effect, services are values in `Context`, not global singletons.
|
|
64
|
-
|
|
65
|
-
This gives you:
|
|
66
|
-
|
|
67
|
-
- explicit dependencies
|
|
68
|
-
- easy substitution in tests
|
|
69
|
-
- layer-based composition
|
|
70
|
-
- multiple implementations for the same interface
|
|
71
|
-
|
|
72
|
-
## Preferred Service Definition Style
|
|
73
|
-
|
|
74
|
-
Prefer the class syntax with `Context.Service`.
|
|
75
|
-
|
|
76
|
-
This matches the canonical source's current style.
|
|
77
|
-
|
|
78
|
-
There are two good definition styles:
|
|
79
|
-
|
|
80
|
-
- explicit service shape in the `Context.Service<...>` generic
|
|
81
|
-
- inferred service shape from the `make` argument
|
|
82
|
-
|
|
83
|
-
Example:
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { Context, Effect, Schema } from "effect";
|
|
87
|
-
|
|
88
|
-
class UserRepoError extends Schema.TaggedErrorClass<UserRepoError>()("UserRepoError", {
|
|
89
|
-
message: Schema.String,
|
|
90
|
-
}) {}
|
|
91
|
-
|
|
92
|
-
class UserRepo extends Context.Service<
|
|
93
|
-
UserRepo,
|
|
94
|
-
{
|
|
95
|
-
readonly getById: (id: string) => Effect.Effect<{ id: string; name: string }, UserRepoError>;
|
|
96
|
-
}
|
|
97
|
-
>()("UserRepo") {}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Why this style is preferred:
|
|
101
|
-
|
|
102
|
-
- the service identifier and shape live in one place
|
|
103
|
-
- it works naturally with `yield* UserRepo`
|
|
104
|
-
- it matches the patterns used across canonical `Effect-TS/effect` source
|
|
105
|
-
|
|
106
|
-
Keep the tag and implementation layer together when one module owns both. If
|
|
107
|
-
the application owns a port and a technology adapter owns its implementation,
|
|
108
|
-
keep the contract beside the application operation and concrete construction
|
|
109
|
-
in the adapter module.
|
|
110
|
-
|
|
111
|
-
Prefer a static lowercase `layer` for the default implementation unless the
|
|
112
|
-
repository already has a consistent `Live` convention. Use `layerNoDeps` when
|
|
113
|
-
calling out dependency-free construction is useful. Avoid splitting
|
|
114
|
-
`foo-service.ts` and `foo-service-live.ts` before there is a real ownership
|
|
115
|
-
boundary.
|
|
116
|
-
|
|
117
|
-
Repo reference:
|
|
118
|
-
|
|
119
|
-
- `packages/effect/src/Context.ts`
|
|
120
|
-
|
|
121
|
-
## Service Shape Inference With `make`
|
|
122
|
-
|
|
123
|
-
When the implementation shape is clearer than the interface declaration, prefer using the `make` argument so the service shape is inferred from the implementation.
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
import { Context, Effect, Schema } from "effect";
|
|
127
|
-
|
|
128
|
-
class UserRepoError extends Schema.TaggedErrorClass<UserRepoError>()("UserRepoError", {
|
|
129
|
-
message: Schema.String,
|
|
130
|
-
}) {}
|
|
131
|
-
|
|
132
|
-
class UserRepo extends Context.Service<UserRepo>()("UserRepo", {
|
|
133
|
-
make: Effect.succeed({
|
|
134
|
-
getById: Effect.fn("UserRepo.getById")(function* (id: string) {
|
|
135
|
-
return yield* Effect.fail(UserRepoError.make({ message: `User ${id} not found` }));
|
|
136
|
-
}),
|
|
137
|
-
}),
|
|
138
|
-
}) {}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Why this style is useful:
|
|
142
|
-
|
|
143
|
-
- the implementation and inferred API stay together
|
|
144
|
-
- TypeScript derives the service shape automatically
|
|
145
|
-
- it avoids repeating the same method signatures twice
|
|
146
|
-
|
|
147
|
-
Prefer this style when:
|
|
148
|
-
|
|
149
|
-
- the implementation is small and obvious
|
|
150
|
-
- the explicit service shape would only duplicate the implementation
|
|
151
|
-
|
|
152
|
-
Prefer the explicit generic shape when:
|
|
153
|
-
|
|
154
|
-
- you want the contract stated before the implementation
|
|
155
|
-
- the API surface should be emphasized separately from the implementation
|
|
156
|
-
|
|
157
|
-
## Service Example
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
import { Context, Effect, Schema } from "effect";
|
|
161
|
-
|
|
162
|
-
class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()("UserNotFound", {
|
|
163
|
-
userId: Schema.String,
|
|
164
|
-
}) {}
|
|
165
|
-
|
|
166
|
-
class UserRepo extends Context.Service<
|
|
167
|
-
UserRepo,
|
|
168
|
-
{
|
|
169
|
-
readonly getById: (id: string) => Effect.Effect<{ id: string; name: string }, UserNotFound>;
|
|
170
|
-
}
|
|
171
|
-
>()("UserRepo") {}
|
|
172
|
-
|
|
173
|
-
const loadUser = (userId: string) =>
|
|
174
|
-
Effect.gen(function* () {
|
|
175
|
-
const repo = yield* UserRepo;
|
|
176
|
-
return yield* repo.getById(userId);
|
|
177
|
-
});
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Key points:
|
|
181
|
-
|
|
182
|
-
- `UserRepo` is both the identifier and a value you can yield from `Effect.gen`
|
|
183
|
-
- the implementation shape is explicit in the service definition
|
|
184
|
-
- the effect that uses it stays abstract over the implementation
|
|
185
|
-
|
|
186
|
-
## When To Use `Context.Reference`
|
|
187
|
-
|
|
188
|
-
Use `Context.Reference` for contextual values with defaults, not for full service APIs.
|
|
189
|
-
|
|
190
|
-
Good use cases:
|
|
191
|
-
|
|
192
|
-
- current configuration knobs
|
|
193
|
-
- current request metadata
|
|
194
|
-
- feature flags or tracing flags with defaults
|
|
195
|
-
|
|
196
|
-
Repo reference:
|
|
197
|
-
|
|
198
|
-
- `packages/effect/src/Context.ts`
|
|
199
|
-
|
|
200
|
-
Use a full service instead when:
|
|
201
|
-
|
|
202
|
-
- behavior matters more than data
|
|
203
|
-
- you need multiple methods
|
|
204
|
-
- you want a concrete test double or alternate implementation
|
|
205
|
-
|
|
206
|
-
## Accessing Services
|
|
207
|
-
|
|
208
|
-
Common patterns:
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
const program = Effect.gen(function* () {
|
|
212
|
-
const repo = yield* UserRepo;
|
|
213
|
-
return yield* repo.getById("u_123");
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
or:
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
const program = Effect.service(UserRepo).pipe(Effect.flatMap((repo) => repo.getById("u_123")));
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Best practice:
|
|
224
|
-
|
|
225
|
-
- use `yield* Service` or `Effect.service(Service)` inside business logic
|
|
226
|
-
- do not manually thread service implementations through function arguments when they are real application dependencies
|
|
227
|
-
|
|
228
|
-
## Service Encapsulation
|
|
229
|
-
|
|
230
|
-
Prefer keeping service access inside the business operation that needs it rather than exporting thin accessor wrappers for every method.
|
|
231
|
-
|
|
232
|
-
Avoid this pattern:
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
export const createTodo = Effect.fn(function* (title: string) {
|
|
236
|
-
const todos = yield* TodoService;
|
|
237
|
-
return yield* todos.create(title);
|
|
238
|
-
});
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Why this is usually a bad pattern:
|
|
242
|
-
|
|
243
|
-
- it leaks the service dependency into a second public API layer
|
|
244
|
-
- it encourages a redundant accessor function per service method
|
|
245
|
-
- it spreads dependency access patterns across the codebase
|
|
246
|
-
- it weakens service encapsulation instead of improving it
|
|
247
|
-
|
|
248
|
-
Prefer one of these patterns instead:
|
|
249
|
-
|
|
250
|
-
1. Put the real business logic in a function that uses the service internally because it adds behavior beyond simple forwarding.
|
|
251
|
-
2. Expose the service itself and call its methods from the module that owns the workflow.
|
|
252
|
-
3. If you need a public operation, make it a real business operation, not a trivial alias of one service method.
|
|
253
|
-
|
|
254
|
-
Good:
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
export const completeTodo = Effect.fn("completeTodo")(function* (id: number) {
|
|
258
|
-
const todos = yield* TodoService;
|
|
259
|
-
const todo = yield* todos.getById(id);
|
|
260
|
-
if (todo.completed) {
|
|
261
|
-
return todo;
|
|
262
|
-
}
|
|
263
|
-
return yield* todos.setCompleted(id, true);
|
|
264
|
-
});
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
This is good because:
|
|
268
|
-
|
|
269
|
-
- the exported function represents a business operation
|
|
270
|
-
- the service remains an internal dependency of that operation
|
|
271
|
-
- the function adds behavior rather than just forwarding one method call
|
|
272
|
-
|
|
273
|
-
## Domain-Specific Service Abstractions
|
|
274
|
-
|
|
275
|
-
When a repository provides a stronger domain-specific `Tag` helper, prefer it
|
|
276
|
-
over plain `Context.Service`. The helper should wrap `Context.Service`, attach
|
|
277
|
-
the canonical construction effect, and expose the default Layer so a named
|
|
278
|
-
domain definition is declared once.
|
|
279
|
-
|
|
280
|
-
Keep intrinsic definitions and canonical options inline in that declaration.
|
|
281
|
-
Extract them only when they are reused, composed, tested independently, or
|
|
282
|
-
selected dynamically.
|
|
283
|
-
|
|
284
|
-
Avoid factories that return nested service namespaces and force callers to
|
|
285
|
-
yield an inner tag or supply the canonical definition repeatedly. A reusable
|
|
286
|
-
domain abstraction should expose its canonical tag, value constructor, and
|
|
287
|
-
Layer directly. Use domain verbs such as `send`, `dispatch`, `snapshot`,
|
|
288
|
-
`execute`, or `close`; reserve `request` for real request/response protocols.
|
|
289
|
-
|
|
290
|
-
## Layers
|
|
291
|
-
|
|
292
|
-
## What A Layer Is
|
|
293
|
-
|
|
294
|
-
A layer constructs services from dependencies.
|
|
295
|
-
|
|
296
|
-
Use a layer when:
|
|
297
|
-
|
|
298
|
-
- service construction is effectful
|
|
299
|
-
- the service depends on other services
|
|
300
|
-
- the service owns resources that must be acquired and released safely
|
|
301
|
-
- you want composition and reuse across modules
|
|
302
|
-
|
|
303
|
-
## Preferred Layer Constructors
|
|
304
|
-
|
|
305
|
-
### `Layer.succeed`
|
|
306
|
-
|
|
307
|
-
Use for pure, already-constructed implementations.
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
const UserRepoTest = Layer.succeed(UserRepo)({
|
|
311
|
-
getById: (id) => Effect.succeed({ id, name: "Test User" }),
|
|
312
|
-
});
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Use this when:
|
|
316
|
-
|
|
317
|
-
- construction is pure
|
|
318
|
-
- no dependencies are needed
|
|
319
|
-
- no scoped resources are involved
|
|
320
|
-
|
|
321
|
-
### `Layer.effect`
|
|
322
|
-
|
|
323
|
-
Use when constructing a service requires effects, other services, or scoped resource acquisition.
|
|
324
|
-
|
|
325
|
-
```ts
|
|
326
|
-
class Config extends Context.Service<
|
|
327
|
-
Config,
|
|
328
|
-
{
|
|
329
|
-
readonly apiBaseUrl: string;
|
|
330
|
-
}
|
|
331
|
-
>()("Config") {}
|
|
332
|
-
|
|
333
|
-
const UserRepoLayer = Layer.effect(UserRepo)(
|
|
334
|
-
Effect.gen(function* () {
|
|
335
|
-
const config = yield* Config;
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
getById: (id) =>
|
|
339
|
-
Effect.succeed({
|
|
340
|
-
id,
|
|
341
|
-
name: `Fetched from ${config.apiBaseUrl}`,
|
|
342
|
-
}),
|
|
343
|
-
};
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Use this when:
|
|
349
|
-
|
|
350
|
-
- construction is effectful
|
|
351
|
-
- construction depends on other services
|
|
352
|
-
- construction needs `Scope` and finalization
|
|
353
|
-
- you want typed construction failure
|
|
354
|
-
|
|
355
|
-
This is also the correct constructor for services that own resources with acquisition and release semantics. In this repo, `Layer.effect` replaces the old `Layer.scoped` API.
|
|
356
|
-
|
|
357
|
-
Typical examples:
|
|
358
|
-
|
|
359
|
-
- database pools
|
|
360
|
-
- sockets
|
|
361
|
-
- background worker processes
|
|
362
|
-
- long-lived subscriptions
|
|
363
|
-
|
|
364
|
-
### `Layer.effectDiscard`
|
|
365
|
-
|
|
366
|
-
Use `Layer.effectDiscard` for scoped startup effects that do not provide a service.
|
|
367
|
-
|
|
368
|
-
Good use cases:
|
|
369
|
-
|
|
370
|
-
- starting background fibers in a layer
|
|
371
|
-
- one-time scoped initialization side effects
|
|
372
|
-
- subsystem startup hooks
|
|
373
|
-
|
|
374
|
-
### `Layer.effectContext`
|
|
375
|
-
|
|
376
|
-
Use when one effect constructs a full `Context` containing multiple services.
|
|
377
|
-
|
|
378
|
-
This is useful for subsystem builders that provide several related services together.
|
|
379
|
-
|
|
380
|
-
## Layer Composition
|
|
381
|
-
|
|
382
|
-
These operators do different things. Do not treat them as interchangeable.
|
|
383
|
-
|
|
384
|
-
### Composition Cheat Sheet
|
|
385
|
-
|
|
386
|
-
- `Layer.mergeAll(a, b, ...)`: combine outputs of multiple layers
|
|
387
|
-
- `Layer.provide(target, dependencies)`: feed dependency outputs into `target` and keep only `target` outputs
|
|
388
|
-
- `Layer.provideMerge(target, dependencies)`: feed dependency outputs into `target` and keep both dependency outputs and target outputs
|
|
389
|
-
- `Layer.flatMap(layer, f)`: choose the next layer based on the built service value
|
|
390
|
-
|
|
391
|
-
### Example Services
|
|
392
|
-
|
|
393
|
-
```ts
|
|
394
|
-
import { Context, Effect, Layer } from "effect";
|
|
395
|
-
|
|
396
|
-
class Config extends Context.Service<
|
|
397
|
-
Config,
|
|
398
|
-
{
|
|
399
|
-
readonly apiBaseUrl: string;
|
|
400
|
-
}
|
|
401
|
-
>()("Config") {}
|
|
402
|
-
|
|
403
|
-
class Logger extends Context.Service<
|
|
404
|
-
Logger,
|
|
405
|
-
{
|
|
406
|
-
readonly log: (message: string) => Effect.Effect<void>;
|
|
407
|
-
}
|
|
408
|
-
>()("Logger") {}
|
|
409
|
-
|
|
410
|
-
class UserRepo extends Context.Service<
|
|
411
|
-
UserRepo,
|
|
412
|
-
{
|
|
413
|
-
readonly getById: (id: string) => Effect.Effect<{ id: string; name: string }>;
|
|
414
|
-
}
|
|
415
|
-
>()("UserRepo") {}
|
|
416
|
-
|
|
417
|
-
const ConfigLayer = Layer.succeed(Config)({
|
|
418
|
-
apiBaseUrl: "https://api.example.com",
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
const LoggerLayer = Layer.succeed(Logger)({
|
|
422
|
-
log: (message) => Effect.sync(() => console.log(message)),
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
const UserRepoLayer = Layer.effect(UserRepo)(
|
|
426
|
-
Effect.gen(function* () {
|
|
427
|
-
const config = yield* Config;
|
|
428
|
-
const logger = yield* Logger;
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
getById: (id) =>
|
|
432
|
-
Effect.gen(function* () {
|
|
433
|
-
yield* logger.log(`loading ${id} from ${config.apiBaseUrl}`);
|
|
434
|
-
return { id, name: "Ada" };
|
|
435
|
-
}),
|
|
436
|
-
};
|
|
437
|
-
}),
|
|
438
|
-
);
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### `Layer.mergeAll`
|
|
442
|
-
|
|
443
|
-
Use `Layer.mergeAll` to combine outputs of independent layers.
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
const Dependencies = Layer.mergeAll(ConfigLayer, LoggerLayer);
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
Use this when:
|
|
450
|
-
|
|
451
|
-
- the layers provide different services
|
|
452
|
-
- neither needs to transform the other directly
|
|
453
|
-
|
|
454
|
-
Semantics:
|
|
455
|
-
|
|
456
|
-
- inputs are combined
|
|
457
|
-
- outputs are combined
|
|
458
|
-
- no dependency feeding happens automatically
|
|
459
|
-
|
|
460
|
-
Important:
|
|
461
|
-
|
|
462
|
-
- `Layer.mergeAll(ConfigLayer, UserRepoLayer)` is wrong if `UserRepoLayer` requires `Config` and `Logger`
|
|
463
|
-
- `mergeAll` does not satisfy `UserRepoLayer`'s requirements
|
|
464
|
-
- it only places both layers side by side in the output graph
|
|
465
|
-
|
|
466
|
-
Correct pattern:
|
|
467
|
-
|
|
468
|
-
```ts
|
|
469
|
-
const Dependencies = Layer.mergeAll(ConfigLayer, LoggerLayer);
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### `Layer.provide`
|
|
473
|
-
|
|
474
|
-
Use `Layer.provide` to satisfy a target layer's dependencies with another layer, while keeping only the target layer's outputs.
|
|
475
|
-
|
|
476
|
-
```ts
|
|
477
|
-
const Dependencies = Layer.mergeAll(ConfigLayer, LoggerLayer);
|
|
478
|
-
|
|
479
|
-
const UserRepoLayerReady = Layer.provide(UserRepoLayer, Dependencies);
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
Interpretation:
|
|
483
|
-
|
|
484
|
-
- `UserRepoLayer` requires `Config` and `Logger`
|
|
485
|
-
- `Dependencies` provides those dependencies
|
|
486
|
-
- the resulting layer provides only `UserRepo`
|
|
487
|
-
- `Config` and `Logger` are used for construction but are not kept in the final output
|
|
488
|
-
|
|
489
|
-
This is the operator to use when you want to hide construction dependencies behind a narrower public layer.
|
|
490
|
-
|
|
491
|
-
Example program:
|
|
492
|
-
|
|
493
|
-
```ts
|
|
494
|
-
const program = Effect.gen(function* () {
|
|
495
|
-
const repo = yield* UserRepo;
|
|
496
|
-
return yield* repo.getById("u_123");
|
|
497
|
-
}).pipe(Effect.provide(UserRepoLayerReady));
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### `Layer.provideMerge`
|
|
501
|
-
|
|
502
|
-
Use `provideMerge` when you want to satisfy dependencies and retain both the dependency outputs and the target outputs.
|
|
503
|
-
|
|
504
|
-
```ts
|
|
505
|
-
const Dependencies = Layer.mergeAll(ConfigLayer, LoggerLayer);
|
|
506
|
-
|
|
507
|
-
const AppLayer = Layer.provideMerge(UserRepoLayer, Dependencies);
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Interpretation:
|
|
511
|
-
|
|
512
|
-
- `UserRepoLayer` still gets `Config` and `Logger`
|
|
513
|
-
- the resulting layer provides `UserRepo`, `Config`, and `Logger`
|
|
514
|
-
|
|
515
|
-
This is useful for assembling larger application layers incrementally, especially when downstream code still needs access to the dependencies.
|
|
516
|
-
|
|
517
|
-
Example program:
|
|
518
|
-
|
|
519
|
-
```ts
|
|
520
|
-
const program = Effect.gen(function* () {
|
|
521
|
-
const repo = yield* UserRepo;
|
|
522
|
-
const logger = yield* Logger;
|
|
523
|
-
|
|
524
|
-
const user = yield* repo.getById("u_123");
|
|
525
|
-
yield* logger.log(user.name);
|
|
526
|
-
return user;
|
|
527
|
-
}).pipe(Effect.provide(AppLayer));
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
Preferred rule:
|
|
531
|
-
|
|
532
|
-
- use `provide` when you want to hide dependency details
|
|
533
|
-
- use `provideMerge` when you want to keep dependency services available downstream
|
|
534
|
-
|
|
535
|
-
### `Layer.mergeAll` vs `Layer.provide` vs `Layer.provideMerge`
|
|
536
|
-
|
|
537
|
-
Think of them like this:
|
|
538
|
-
|
|
539
|
-
- `mergeAll`: put layers next to each other
|
|
540
|
-
- `provide`: plug one layer into another, expose only the target
|
|
541
|
-
- `provideMerge`: plug one layer into another, expose both sides
|
|
542
|
-
|
|
543
|
-
### Composition Style Best Practice
|
|
544
|
-
|
|
545
|
-
Layers should almost always be fully composed locally before they are assembled into the final application layer.
|
|
546
|
-
|
|
547
|
-
Preferred style:
|
|
548
|
-
|
|
549
|
-
- define each service layer separately
|
|
550
|
-
- define local subsystem dependency bundles separately
|
|
551
|
-
- fully compose each subsystem locally with `Layer.provide` or `Layer.provideMerge`
|
|
552
|
-
- assemble the final application layer with `Layer.mergeAll(...)`
|
|
553
|
-
- apply top-level cross-cutting provisioning in a small number of explicit trailing steps
|
|
554
|
-
|
|
555
|
-
Good pattern:
|
|
556
|
-
|
|
557
|
-
```ts
|
|
558
|
-
const UserDependencies = Layer.mergeAll(ConfigLayer, LoggerLayer);
|
|
559
|
-
|
|
560
|
-
const UserLayer = Layer.provide(UserRepoLayer, UserDependencies);
|
|
561
|
-
|
|
562
|
-
const BillingDependencies = Layer.mergeAll(ConfigLayer, LoggerLayer, DatabaseLayer);
|
|
563
|
-
|
|
564
|
-
const BillingLayer = Layer.provide(BillingServiceLayer, BillingDependencies);
|
|
565
|
-
|
|
566
|
-
const AppLayer = Layer.mergeAll(UserLayer, BillingLayer, HttpLayer).pipe(
|
|
567
|
-
Layer.provide(Telemetry),
|
|
568
|
-
Layer.provide(NodeSdk),
|
|
569
|
-
);
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
Why this style is preferred:
|
|
573
|
-
|
|
574
|
-
- subsystem wiring stays local to the subsystem
|
|
575
|
-
- the final application layer reads as a high-level composition map
|
|
576
|
-
- cross-cutting concerns such as telemetry stay visible at the top level
|
|
577
|
-
- it avoids deeply nested inline layer expressions
|
|
578
|
-
|
|
579
|
-
Avoid this style when a clearer local name would help:
|
|
580
|
-
|
|
581
|
-
```ts
|
|
582
|
-
const AppLayer = Layer.provide(
|
|
583
|
-
Layer.mergeAll(
|
|
584
|
-
Layer.provide(UserRepoLayer, Layer.mergeAll(ConfigLayer, LoggerLayer)),
|
|
585
|
-
Layer.provide(BillingServiceLayer, Layer.mergeAll(ConfigLayer, LoggerLayer, DatabaseLayer)),
|
|
586
|
-
HttpLayer,
|
|
587
|
-
),
|
|
588
|
-
Telemetry,
|
|
589
|
-
).pipe(Layer.provide(NodeSdk));
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
That style is harder to read because:
|
|
593
|
-
|
|
594
|
-
- subsystem composition is hidden inside the final assembly
|
|
595
|
-
- shared dependencies are harder to spot
|
|
596
|
-
- it is harder to refactor or reuse subsystem layers
|
|
597
|
-
|
|
598
|
-
### `Layer.flatMap`
|
|
599
|
-
|
|
600
|
-
Use `flatMap` when the next layer depends on the actual constructed service value, not just its type-level requirement.
|
|
601
|
-
|
|
602
|
-
Example:
|
|
603
|
-
|
|
604
|
-
```ts
|
|
605
|
-
const UserRepoLayerFromConfig = Layer.flatMap(ConfigLayer, (config) =>
|
|
606
|
-
Layer.succeed(UserRepo)({
|
|
607
|
-
getById: (id) => Effect.succeed({ id, name: config.apiBaseUrl }),
|
|
608
|
-
}),
|
|
609
|
-
);
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
This is more specialized than `merge` or `provide`.
|
|
613
|
-
|
|
614
|
-
Prefer simpler composition first:
|
|
615
|
-
|
|
616
|
-
- `merge` for combining
|
|
617
|
-
- `provide` for dependency satisfaction
|
|
618
|
-
- `flatMap` only when construction logic truly depends on the built value
|
|
619
|
-
|
|
620
|
-
## Providing Layers To Effects
|
|
621
|
-
|
|
622
|
-
## Preferred Rule
|
|
623
|
-
|
|
624
|
-
Provide layers at boundaries.
|
|
625
|
-
|
|
626
|
-
Usually that means:
|
|
627
|
-
|
|
628
|
-
- the application entrypoint
|
|
629
|
-
- a subsystem entrypoint
|
|
630
|
-
- a test boundary
|
|
631
|
-
|
|
632
|
-
Avoid repeatedly providing layers deep inside business logic unless you are deliberately isolating a subsystem.
|
|
633
|
-
|
|
634
|
-
### Anti-Pattern: Local `Effect.provide`
|
|
635
|
-
|
|
636
|
-
`Effect.provide` should be used only once at the entry of your program in normal application code.
|
|
637
|
-
|
|
638
|
-
Bad pattern:
|
|
639
|
-
|
|
640
|
-
```ts
|
|
641
|
-
const loadUser = (userId: string) =>
|
|
642
|
-
Effect.gen(function* () {
|
|
643
|
-
const repo = yield* UserRepo;
|
|
644
|
-
return yield* repo.getById(userId);
|
|
645
|
-
}).pipe(Effect.provide(UserRepoLayer));
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
Why this is an anti-pattern:
|
|
649
|
-
|
|
650
|
-
- it hides dependency wiring inside business logic
|
|
651
|
-
- it makes implementations harder to swap in tests
|
|
652
|
-
- it prevents clean top-level composition
|
|
653
|
-
- it encourages many small local runtimes instead of one coherent application graph
|
|
654
|
-
- it makes shared cross-cutting services harder to reason about
|
|
655
|
-
|
|
656
|
-
Preferred pattern:
|
|
657
|
-
|
|
658
|
-
```ts
|
|
659
|
-
const loadUser = (userId: string) =>
|
|
660
|
-
Effect.gen(function* () {
|
|
661
|
-
const repo = yield* UserRepo;
|
|
662
|
-
return yield* repo.getById(userId);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
const program = loadUser("u_123").pipe(Effect.provide(AppLayer));
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
Rule of thumb:
|
|
669
|
-
|
|
670
|
-
- business logic should require services
|
|
671
|
-
- composition should happen in layers
|
|
672
|
-
- `Effect.provide` should happen at the outermost entry boundary
|
|
673
|
-
|
|
674
|
-
### Multiple Entry Points
|
|
675
|
-
|
|
676
|
-
If your code integrates with a framework and has multiple entry points, prefer `ManagedRuntime` instead of repeatedly calling `Effect.provide` at many call sites.
|
|
677
|
-
|
|
678
|
-
Typical examples:
|
|
679
|
-
|
|
680
|
-
- HTTP handlers registered separately
|
|
681
|
-
- queue consumers
|
|
682
|
-
- cron jobs
|
|
683
|
-
- framework lifecycle hooks
|
|
684
|
-
- RPC handlers or worker callbacks
|
|
685
|
-
|
|
686
|
-
Preferred pattern:
|
|
687
|
-
|
|
688
|
-
```ts
|
|
689
|
-
const runtime = ManagedRuntime.make(AppLayer);
|
|
690
|
-
|
|
691
|
-
const handleRequest = (id: string) => runtime.runPromise(loadUser(id));
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
Why:
|
|
695
|
-
|
|
696
|
-
- the layer graph is still composed once
|
|
697
|
-
- services remain shared according to layer semantics
|
|
698
|
-
- the framework integration gets a stable runtime boundary
|
|
699
|
-
- resource lifecycle is explicit through `ManagedRuntime`
|
|
700
|
-
|
|
701
|
-
Repo reference:
|
|
702
|
-
|
|
703
|
-
- `packages/effect/src/ManagedRuntime.ts`
|
|
704
|
-
|
|
705
|
-
## `Effect.provide`
|
|
706
|
-
|
|
707
|
-
Use `Effect.provide` to satisfy an effect's dependencies with a layer or context.
|
|
708
|
-
|
|
709
|
-
```ts
|
|
710
|
-
const program = loadUser("u_123").pipe(Effect.provide(UserRepoLayerReady));
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
This is the main boundary provisioning operator.
|
|
714
|
-
|
|
715
|
-
## `Effect.provideService`
|
|
716
|
-
|
|
717
|
-
Use `provideService` for a single ad hoc implementation.
|
|
718
|
-
|
|
719
|
-
```ts
|
|
720
|
-
const program = loadUser("u_123").pipe(
|
|
721
|
-
Effect.provideService(UserRepo, {
|
|
722
|
-
getById: (id) => Effect.succeed({ id, name: "Inline User" }),
|
|
723
|
-
}),
|
|
724
|
-
);
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
Good use cases:
|
|
728
|
-
|
|
729
|
-
- small tests
|
|
730
|
-
- one-off overrides
|
|
731
|
-
- local customization
|
|
732
|
-
|
|
733
|
-
Do not use this as the default replacement for real application layers.
|
|
734
|
-
|
|
735
|
-
## `Effect.provideServiceEffect`
|
|
736
|
-
|
|
737
|
-
Use `provideServiceEffect` when one service instance must be built effectfully without creating a reusable layer.
|
|
738
|
-
|
|
739
|
-
This is useful for targeted overrides, but if the construction is reusable or part of application wiring, prefer a named `Layer.effect`.
|
|
740
|
-
|
|
741
|
-
## Best Practices
|
|
742
|
-
|
|
743
|
-
## 1. Keep service interfaces small and focused
|
|
744
|
-
|
|
745
|
-
Prefer cohesive services over giant "everything" services.
|
|
746
|
-
|
|
747
|
-
Good:
|
|
748
|
-
|
|
749
|
-
- `UserRepo`
|
|
750
|
-
- `Mailer`
|
|
751
|
-
- `Clock`-like configuration or time abstractions
|
|
752
|
-
|
|
753
|
-
Avoid:
|
|
754
|
-
|
|
755
|
-
- large service shapes that mix unrelated responsibilities
|
|
756
|
-
|
|
757
|
-
## 2. Prefer layers over manual wiring
|
|
758
|
-
|
|
759
|
-
If construction has dependencies or effects, represent it as a layer.
|
|
760
|
-
|
|
761
|
-
Avoid manually grabbing dependencies and assembling concrete objects all over the codebase.
|
|
762
|
-
|
|
763
|
-
## 2.5 Prefer Effect-native integrations over raw runtime clients
|
|
764
|
-
|
|
765
|
-
When Effect already provides a module for a capability, prefer the Effect-native integration over directly embedding a raw runtime client in service code.
|
|
766
|
-
|
|
767
|
-
Examples:
|
|
768
|
-
|
|
769
|
-
- prefer `effect/unstable/sql` modules over directly coupling business services to native SQL driver APIs
|
|
770
|
-
- prefer Effect HTTP modules over direct ad hoc request clients when the project is already using Effect HTTP abstractions
|
|
771
|
-
|
|
772
|
-
Why:
|
|
773
|
-
|
|
774
|
-
- resource handling, tracing, and errors stay inside the Effect model
|
|
775
|
-
- integrations compose better with layers and services
|
|
776
|
-
- observability and transactions are easier to keep consistent
|
|
777
|
-
|
|
778
|
-
## 3. Keep business logic abstract over implementations
|
|
779
|
-
|
|
780
|
-
Business functions should require services, not construct them.
|
|
781
|
-
|
|
782
|
-
Good:
|
|
783
|
-
|
|
784
|
-
```ts
|
|
785
|
-
const sendWelcomeEmail = (userId: string) =>
|
|
786
|
-
Effect.gen(function* () {
|
|
787
|
-
const repo = yield* UserRepo;
|
|
788
|
-
const user = yield* repo.getById(userId);
|
|
789
|
-
return user;
|
|
790
|
-
});
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
Avoid constructing `UserRepo` inside `sendWelcomeEmail`.
|
|
794
|
-
|
|
795
|
-
Service methods take domain inputs only. Yield configuration, clients,
|
|
796
|
-
databases, clocks, loggers, request context, platform bindings, and other
|
|
797
|
-
runtime dependencies from the Effect environment. Do not recreate dependency
|
|
798
|
-
injection through method, constructor, options-bag, or implementation-helper
|
|
799
|
-
parameters; yield a dependency while constructing the service and close over
|
|
800
|
-
it in the method implementation.
|
|
801
|
-
|
|
802
|
-
## 4. Use `Layer.succeed` only for pure values
|
|
803
|
-
|
|
804
|
-
Do not hide effectful initialization inside supposedly pure service objects.
|
|
805
|
-
|
|
806
|
-
If initialization can fail, depends on effects, or needs scoped acquisition, use `Layer.effect`.
|
|
807
|
-
|
|
808
|
-
## 5. Use `Layer.effect` for owned resources
|
|
809
|
-
|
|
810
|
-
If the service opens something that must later close, model that lifecycle explicitly.
|
|
811
|
-
|
|
812
|
-
This is one of the main reasons layers exist.
|
|
813
|
-
|
|
814
|
-
## 6. Prefer top-level composition
|
|
815
|
-
|
|
816
|
-
Compose major application layers once near the boundary.
|
|
817
|
-
|
|
818
|
-
Good pattern:
|
|
819
|
-
|
|
820
|
-
- define `ConfigLayer`
|
|
821
|
-
- define `UserRepoLayer`
|
|
822
|
-
- define `Dependencies = Layer.mergeAll(...)`
|
|
823
|
-
- define `AppLayer` separately with `Layer.provide(...)` or `Layer.provideMerge(...)`
|
|
824
|
-
- provide `AppLayer` to the top-level program
|
|
825
|
-
|
|
826
|
-
Let the composition root choose concrete backends and own transport lifecycles.
|
|
827
|
-
Do not hide request, websocket, worker, RPC, serialization, or server adapter
|
|
828
|
-
Layers inside an exported domain service Layer merely because one entrypoint
|
|
829
|
-
needs them.
|
|
830
|
-
|
|
831
|
-
## 7. Use `Layer.fresh` only when you really need a new instance
|
|
832
|
-
|
|
833
|
-
Layers are shared by default.
|
|
834
|
-
|
|
835
|
-
That is usually what you want.
|
|
836
|
-
|
|
837
|
-
Use `Layer.fresh` only when you intentionally need to bypass sharing and rebuild the layer.
|
|
838
|
-
|
|
839
|
-
## 7.5 Understand Layer Memoization
|
|
840
|
-
|
|
841
|
-
Layers are memoized by reference.
|
|
842
|
-
|
|
843
|
-
That means:
|
|
844
|
-
|
|
845
|
-
- reusing the same layer value preserves memoization and sharing
|
|
846
|
-
- creating a new layer value creates a new memoization identity
|
|
847
|
-
|
|
848
|
-
Prefer named Layer values. Layer constructors must not accept runtime
|
|
849
|
-
dependencies or configuration:
|
|
850
|
-
|
|
851
|
-
```ts
|
|
852
|
-
// Avoid: requirements disappear from the Layer input channel.
|
|
853
|
-
const makeUserRepoLayer = (config: Config, client: HttpClient) =>
|
|
854
|
-
Layer.effect(UserRepo)(makeUserRepo(config, client));
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
Model configuration, clients, databases, clocks, platform bindings, and
|
|
858
|
-
backend selection as focused services. Yield them inside `Layer.effect` so
|
|
859
|
-
requirements remain visible, and provide their concrete Layers once at the
|
|
860
|
-
outer composition boundary:
|
|
861
|
-
|
|
862
|
-
```ts
|
|
863
|
-
const UserRepoLayer = Layer.effect(UserRepo)(
|
|
864
|
-
Effect.gen(function* () {
|
|
865
|
-
const config = yield* Config;
|
|
866
|
-
const client = yield* HttpClient;
|
|
867
|
-
return UserRepo.of(makeUserRepo(config, client));
|
|
868
|
-
}),
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
const AppLayer = UserRepoLayer.pipe(Layer.provide(ConfigLayer), Layer.provide(HttpClientLayer));
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
Arguments are acceptable only when they are intrinsic definitions that create
|
|
875
|
-
a distinct named service instance, not runtime dependencies or configuration.
|
|
876
|
-
If a Layer-producing function is genuinely needed, call it once during
|
|
877
|
-
construction and reuse the resulting Layer value. Use `Layer.fresh` when a
|
|
878
|
-
distinct instance is intentional.
|
|
879
|
-
|
|
880
|
-
## 8. Treat `Layer.orDie` carefully
|
|
881
|
-
|
|
882
|
-
`Layer.orDie` converts layer construction failures into defects.
|
|
883
|
-
|
|
884
|
-
Only use it when failure is truly unrecoverable at that boundary.
|
|
885
|
-
|
|
886
|
-
Do not use it to hide legitimate configuration or infrastructure failures.
|
|
887
|
-
|
|
888
|
-
## 9. Use `ManagedRuntime.make` at true runtime boundaries
|
|
889
|
-
|
|
890
|
-
If you need a reusable runtime built from a layer, `ManagedRuntime.make` is the edge tool for that.
|
|
891
|
-
|
|
892
|
-
Good use cases:
|
|
893
|
-
|
|
894
|
-
- embedding Effect into external frameworks
|
|
895
|
-
- scripts or hosts that repeatedly run Effect programs
|
|
896
|
-
|
|
897
|
-
Repo reference:
|
|
898
|
-
|
|
899
|
-
- `packages/effect/src/ManagedRuntime.ts`
|
|
900
|
-
|
|
901
|
-
## 10. Prefer explicit test layers
|
|
902
|
-
|
|
903
|
-
For tests, prefer:
|
|
904
|
-
|
|
905
|
-
- `Layer.succeed` for simple fakes
|
|
906
|
-
- `Layer.mock` for partial mocks when appropriate
|
|
907
|
-
|
|
908
|
-
This keeps test wiring explicit and close to production composition style.
|
|
909
|
-
|
|
910
|
-
## Recommended Patterns
|
|
911
|
-
|
|
912
|
-
## Pattern: service definition plus live layer
|
|
913
|
-
|
|
914
|
-
```ts
|
|
915
|
-
import { Context, Effect, Layer } from "effect";
|
|
916
|
-
|
|
917
|
-
class Config extends Context.Service<
|
|
918
|
-
Config,
|
|
919
|
-
{
|
|
920
|
-
readonly apiBaseUrl: string;
|
|
921
|
-
}
|
|
922
|
-
>()("Config") {}
|
|
923
|
-
|
|
924
|
-
class UserRepo extends Context.Service<
|
|
925
|
-
UserRepo,
|
|
926
|
-
{
|
|
927
|
-
readonly getById: (id: string) => Effect.Effect<{ id: string; name: string }>;
|
|
928
|
-
}
|
|
929
|
-
>()("UserRepo") {}
|
|
930
|
-
|
|
931
|
-
const ConfigLayer = Layer.succeed(Config)({
|
|
932
|
-
apiBaseUrl: "https://api.example.com",
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
const UserRepoLayer = Layer.effect(UserRepo)(
|
|
936
|
-
Effect.gen(function* () {
|
|
937
|
-
const config = yield* Config;
|
|
938
|
-
|
|
939
|
-
return {
|
|
940
|
-
getById: (id) =>
|
|
941
|
-
Effect.succeed({
|
|
942
|
-
id,
|
|
943
|
-
name: `Loaded via ${config.apiBaseUrl}`,
|
|
944
|
-
}),
|
|
945
|
-
};
|
|
946
|
-
}),
|
|
947
|
-
);
|
|
948
|
-
|
|
949
|
-
const Dependencies = Layer.mergeAll(ConfigLayer);
|
|
950
|
-
|
|
951
|
-
const AppLayer = Layer.provide(UserRepoLayer, Dependencies);
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
## Pattern: provide at the top level
|
|
955
|
-
|
|
956
|
-
```ts
|
|
957
|
-
const program = Effect.gen(function* () {
|
|
958
|
-
const repo = yield* UserRepo;
|
|
959
|
-
return yield* repo.getById("u_123");
|
|
960
|
-
}).pipe(Effect.provide(AppLayer));
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
## Pattern: single-service override in tests
|
|
964
|
-
|
|
965
|
-
```ts
|
|
966
|
-
const TestRepo = Layer.succeed(UserRepo)({
|
|
967
|
-
getById: (id) => Effect.succeed({ id, name: "Test" }),
|
|
968
|
-
});
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
## Anti-Patterns
|
|
972
|
-
|
|
973
|
-
- constructing live services directly inside business logic
|
|
974
|
-
- using `Layer.succeed` for values that actually require effectful initialization
|
|
975
|
-
- providing the same large layer repeatedly throughout the call graph
|
|
976
|
-
- collapsing unrelated responsibilities into one service
|
|
977
|
-
- using `Layer.orDie` to hide normal initialization failures
|
|
978
|
-
- bypassing layers entirely for resource-owning services
|
|
979
|
-
|
|
980
|
-
## Good Repo Examples To Study
|
|
981
|
-
|
|
982
|
-
- `packages/effect/src/Context.ts`
|
|
983
|
-
- `packages/effect/src/Layer.ts`
|
|
984
|
-
- `packages/effect/src/ManagedRuntime.ts`
|
|
985
|
-
- `packages/effect/src/Stream.ts`
|
|
986
|
-
- `packages/effect/src/unstable/sql/SqlClient.ts`
|
|
987
|
-
- `packages/effect/src/unstable/persistence/Persistence.ts`
|
|
988
|
-
- `packages/effect/src/unstable/rpc/RpcSerialization.ts`
|
|
989
|
-
- `packages/effect/src/unstable/reactivity/Reactivity.ts`
|