opencode-effect-enforcer 0.2.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/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-layer-design
|
|
3
|
+
description: Design and compose Effect layers for clean dependency management
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Layer Design Skill
|
|
7
|
+
|
|
8
|
+
Create layers that construct services while managing their dependencies cleanly.
|
|
9
|
+
|
|
10
|
+
## Effect Source Reference
|
|
11
|
+
|
|
12
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
13
|
+
Browse and read files there directly to look up APIs, types, and implementations.
|
|
14
|
+
|
|
15
|
+
Reference this for:
|
|
16
|
+
|
|
17
|
+
- Layer source: `packages/effect/src/Layer.ts`
|
|
18
|
+
- Context source: `packages/effect/src/Context.ts`
|
|
19
|
+
- Migration guide: `MIGRATION.md`
|
|
20
|
+
- Effect source: `packages/effect/src/`
|
|
21
|
+
|
|
22
|
+
## Layer Structure
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Layer } from 'effect';
|
|
26
|
+
|
|
27
|
+
// Layer<RequirementsOut, Error, RequirementsIn>
|
|
28
|
+
// ▲ ▲ ▲
|
|
29
|
+
// │ │ └─ What this layer needs
|
|
30
|
+
// │ └─ Errors during construction
|
|
31
|
+
// └─ What this layer produces
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Choose the Constructor by Output
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
Layer.succeed(Service, implementation); // already built
|
|
38
|
+
Layer.sync(Service, () => implementation); // lazy synchronous construction
|
|
39
|
+
Layer.effect(Service, acquisition); // effectful single-service acquisition
|
|
40
|
+
Layer.effectContext(acquisition); // effectful Context with multiple services
|
|
41
|
+
Layer.effectDiscard(initialization); // acquisition that provides no service
|
|
42
|
+
Layer.unwrap(effectProducingLayer); // config or discovery chooses a layer
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Default production services with dependencies or resources to `Layer.effect`. Use `Layer.effectContext` when one acquisition intentionally provides multiple tags, especially when the same controllable test implementation backs both a production service and a test-control service.
|
|
46
|
+
|
|
47
|
+
## Pattern: Simple Layer (No Dependencies)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Context, Effect, Layer } from 'effect';
|
|
51
|
+
|
|
52
|
+
interface ConfigData {
|
|
53
|
+
readonly logLevel: string;
|
|
54
|
+
readonly connection: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class Config extends Context.Service<
|
|
58
|
+
Config,
|
|
59
|
+
{
|
|
60
|
+
readonly getConfig: Effect.Effect<ConfigData>;
|
|
61
|
+
}
|
|
62
|
+
>()('Config') {}
|
|
63
|
+
|
|
64
|
+
// Layer<Config, never, never>
|
|
65
|
+
// ▲ ▲ ▲
|
|
66
|
+
// │ │ └─ No dependencies
|
|
67
|
+
// │ └─ Cannot fail
|
|
68
|
+
// └─ Produces Config
|
|
69
|
+
export const ConfigLive = Layer.succeed(
|
|
70
|
+
Config,
|
|
71
|
+
Config.of({
|
|
72
|
+
getConfig: Effect.succeed({
|
|
73
|
+
logLevel: 'INFO',
|
|
74
|
+
connection: 'mysql://localhost/db'
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Pattern: Layer with Dependencies
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Context, Effect, Layer, Console } from 'effect';
|
|
84
|
+
|
|
85
|
+
interface ConfigData {
|
|
86
|
+
readonly logLevel: string;
|
|
87
|
+
readonly connection: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class Config extends Context.Service<
|
|
91
|
+
Config,
|
|
92
|
+
{
|
|
93
|
+
readonly getConfig: Effect.Effect<ConfigData>;
|
|
94
|
+
}
|
|
95
|
+
>()('Config') {}
|
|
96
|
+
|
|
97
|
+
export class Logger extends Context.Service<
|
|
98
|
+
Logger,
|
|
99
|
+
{
|
|
100
|
+
readonly log: (message: string) => Effect.Effect<void>;
|
|
101
|
+
}
|
|
102
|
+
>()('Logger') {}
|
|
103
|
+
|
|
104
|
+
// Layer<Logger, never, Config>
|
|
105
|
+
// ▲ ▲ ▲
|
|
106
|
+
// │ │ └─ Needs Config
|
|
107
|
+
// │ └─ Cannot fail
|
|
108
|
+
// └─ Produces Logger
|
|
109
|
+
export const LoggerLive = Layer.effect(
|
|
110
|
+
Logger,
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
const config = yield* Config; // Access dependency
|
|
113
|
+
return Logger.of({
|
|
114
|
+
log: (message) =>
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
const { logLevel } = yield* config.getConfig;
|
|
117
|
+
yield* Console.log(`[${logLevel}] ${message}`);
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Pattern: Layer with Resource Management
|
|
125
|
+
|
|
126
|
+
Use `Layer.effect` for effectfully acquired services, including resources that need cleanup. In Effect v4, `Layer.effect` automatically handles `Scope` lifecycle — `Layer.scoped` is no longer needed.
|
|
127
|
+
|
|
128
|
+
Resources are acquired and released using `Effect.acquireRelease` or `Effect.addFinalizer` inside the `Layer.effect` constructor:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Context, Effect, Layer } from 'effect';
|
|
132
|
+
|
|
133
|
+
interface ConfigData {
|
|
134
|
+
readonly logLevel: string;
|
|
135
|
+
readonly connection: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface Connection {
|
|
139
|
+
readonly close: () => void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface DatabaseError {
|
|
143
|
+
readonly _tag: 'DatabaseError';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class Config extends Context.Service<
|
|
147
|
+
Config,
|
|
148
|
+
{
|
|
149
|
+
readonly getConfig: Effect.Effect<ConfigData>;
|
|
150
|
+
}
|
|
151
|
+
>()('Config') {}
|
|
152
|
+
|
|
153
|
+
export class Database extends Context.Service<
|
|
154
|
+
Database,
|
|
155
|
+
{
|
|
156
|
+
readonly query: (sql: string) => Effect.Effect<unknown, DatabaseError>;
|
|
157
|
+
}
|
|
158
|
+
>()('Database') {}
|
|
159
|
+
|
|
160
|
+
declare const connectToDatabase: (
|
|
161
|
+
config: ConfigData
|
|
162
|
+
) => Effect.Effect<Connection, DatabaseError>;
|
|
163
|
+
declare const executeQuery: (
|
|
164
|
+
connection: Connection,
|
|
165
|
+
sql: string
|
|
166
|
+
) => Effect.Effect<unknown, DatabaseError>;
|
|
167
|
+
|
|
168
|
+
// Layer<Database, DatabaseError, Config>
|
|
169
|
+
export const DatabaseLive = Layer.effect(
|
|
170
|
+
Database,
|
|
171
|
+
Effect.gen(function* () {
|
|
172
|
+
const config = yield* Config;
|
|
173
|
+
const configData = yield* config.getConfig;
|
|
174
|
+
|
|
175
|
+
// Acquire resource with automatic release — Layer.effect handles Scope
|
|
176
|
+
const connection = yield* Effect.acquireRelease(
|
|
177
|
+
connectToDatabase(configData),
|
|
178
|
+
(conn) => Effect.sync(() => conn.close()) // Cleanup
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return Database.of({
|
|
182
|
+
query: (sql) => executeQuery(connection, sql)
|
|
183
|
+
});
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Layer construction must complete. If acquisition starts a listener, stream, subscription, worker, or forever loop, fork it into the layer scope rather than running it inline:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
export const WorkerLive = Layer.effectDiscard(
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
const events = yield* Events.Service;
|
|
194
|
+
yield* events.stream.pipe(
|
|
195
|
+
Stream.runForEach(handleEvent),
|
|
196
|
+
Effect.forkScoped
|
|
197
|
+
);
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Use `Effect.forkScoped`, `FiberSet`, or `FiberMap` so closing the layer scope interrupts the background work. `forkScoped` alone is appropriate only for best-effort work that reports its own failures; monitor or supervise failure-significant consumers so their failures remain observable. Never block layer acquisition on a long-lived loop.
|
|
203
|
+
|
|
204
|
+
## Composing Layers: Merge vs Provide
|
|
205
|
+
|
|
206
|
+
Start from the service graph, not from whichever combinator makes the types compile:
|
|
207
|
+
|
|
208
|
+
- `Layer.merge` / `Layer.mergeAll` expose independent outputs; they do not satisfy dependencies between the merged layers.
|
|
209
|
+
- `Layer.provide` satisfies and hides an implementation dependency.
|
|
210
|
+
- `Layer.provideMerge` satisfies a dependency and deliberately keeps that dependency in the output.
|
|
211
|
+
- Name and reuse shared dependency layer values when acquisition must be shared.
|
|
212
|
+
- Do not blindly merge every layer or use `provideMerge` as a make-it-compile tool; both can expose authority and lifecycle services that should remain private.
|
|
213
|
+
|
|
214
|
+
### Merge (Parallel Composition)
|
|
215
|
+
|
|
216
|
+
Merge layers when both outputs should remain exposed. This does not wire one output into another layer's requirements:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { Context, Layer } from 'effect';
|
|
220
|
+
|
|
221
|
+
declare class Config extends Context.Service<Config, {}>()('Config') {}
|
|
222
|
+
declare class Logger extends Context.Service<Logger, {}>()('Logger') {}
|
|
223
|
+
|
|
224
|
+
declare const ConfigLive: Layer.Layer<Config, never, never>;
|
|
225
|
+
declare const LoggerLive: Layer.Layer<Logger, never, Config>;
|
|
226
|
+
|
|
227
|
+
// Layer<Config | Logger, never, Config>
|
|
228
|
+
// ▲ ▲ ▲
|
|
229
|
+
// │ │ └─ LoggerLive needs Config
|
|
230
|
+
// │ └─ No errors
|
|
231
|
+
// └─ Produces both Config and Logger
|
|
232
|
+
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Result combines:
|
|
236
|
+
|
|
237
|
+
- **Requirements**: Union (`never | Config = Config`)
|
|
238
|
+
- **Outputs**: Union (`Config | Logger`)
|
|
239
|
+
|
|
240
|
+
### Provide (Sequential Composition)
|
|
241
|
+
|
|
242
|
+
Chain dependent layers:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { Context, Layer } from 'effect';
|
|
246
|
+
|
|
247
|
+
declare class Config extends Context.Service<Config, {}>()('Config') {}
|
|
248
|
+
declare class Logger extends Context.Service<Logger, {}>()('Logger') {}
|
|
249
|
+
|
|
250
|
+
declare const ConfigLive: Layer.Layer<Config, never, never>;
|
|
251
|
+
declare const LoggerLive: Layer.Layer<Logger, never, Config>;
|
|
252
|
+
|
|
253
|
+
// Layer<Logger, never, never>
|
|
254
|
+
// ▲ ▲ ▲
|
|
255
|
+
// │ │ └─ ConfigLive satisfies LoggerLive's requirement
|
|
256
|
+
// │ └─ No errors
|
|
257
|
+
// └─ Only Logger in output
|
|
258
|
+
const FullLoggerLive = Layer.provide(LoggerLive, ConfigLive);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Result:
|
|
262
|
+
|
|
263
|
+
- **Requirements**: Outer layer's requirements (`never`)
|
|
264
|
+
- **Output**: Inner layer's output (`Logger`)
|
|
265
|
+
|
|
266
|
+
## Pattern: Direct Default Composition
|
|
267
|
+
|
|
268
|
+
Compose `defaultLayer` directly unless you have a real module-evaluation or circular-import problem. Most services do not need deferred composition.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { Layer } from 'effect';
|
|
272
|
+
|
|
273
|
+
// Raw layer — declares its dependencies in the type
|
|
274
|
+
export const layer: Layer.Layer<MyService, never, DepA | DepB> = Layer.effect(
|
|
275
|
+
MyService,
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
const depA = yield* DepA;
|
|
278
|
+
const depB = yield* DepB;
|
|
279
|
+
return MyService.of({
|
|
280
|
+
/* ... */
|
|
281
|
+
});
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Fully-wired layer — compose directly in the normal case
|
|
286
|
+
export const defaultLayer = layer.pipe(
|
|
287
|
+
Layer.provide(DepA.defaultLayer),
|
|
288
|
+
Layer.provide(DepB.defaultLayer)
|
|
289
|
+
);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Deferred Composition with Layer.suspend
|
|
293
|
+
|
|
294
|
+
Use `Layer.suspend(() => ...)` when import evaluation order genuinely requires deferral:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Layer } from 'effect';
|
|
298
|
+
|
|
299
|
+
export const defaultLayer = Layer.suspend(() =>
|
|
300
|
+
layer.pipe(Layer.provide(Dep.defaultLayer))
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`Layer.unwrap(Effect.sync(...))` still works, but it is not the universal default. Reach for deferred composition only when the dependency graph actually needs it.
|
|
305
|
+
|
|
306
|
+
**Naming convention:**
|
|
307
|
+
|
|
308
|
+
- **`layer`** — exposes the service's true dependency graph in its type signature. Tests compose against `layer` directly, providing mock layers.
|
|
309
|
+
- **`defaultLayer`** — the fully-wired production composition with all dependencies satisfied. Only define `defaultLayer` when `layer` has unsatisfied requirements. Self-contained layers (no external dependencies) export just `layer`.
|
|
310
|
+
|
|
311
|
+
### When to defer
|
|
312
|
+
|
|
313
|
+
Use deferred composition only for:
|
|
314
|
+
|
|
315
|
+
- real circular-import or module-evaluation hazards
|
|
316
|
+
- runtime-selected layer variants that should not be built eagerly
|
|
317
|
+
- recursive layer graphs that must be tied lazily
|
|
318
|
+
|
|
319
|
+
If none of those apply, compose directly.
|
|
320
|
+
|
|
321
|
+
## Pattern: Layered Architecture
|
|
322
|
+
|
|
323
|
+
Build applications in layers:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { Context, Layer } from 'effect';
|
|
327
|
+
|
|
328
|
+
declare class Config extends Context.Service<Config, {}>()('Config') {}
|
|
329
|
+
declare class Database extends Context.Service<Database, {}>()('Database') {}
|
|
330
|
+
declare class Cache extends Context.Service<Cache, {}>()('Cache') {}
|
|
331
|
+
declare class PaymentDomain extends Context.Service<PaymentDomain, {}>()(
|
|
332
|
+
'PaymentDomain'
|
|
333
|
+
) {}
|
|
334
|
+
declare class OrderDomain extends Context.Service<OrderDomain, {}>()(
|
|
335
|
+
'OrderDomain'
|
|
336
|
+
) {}
|
|
337
|
+
declare class PaymentGateway extends Context.Service<PaymentGateway, {}>()(
|
|
338
|
+
'PaymentGateway'
|
|
339
|
+
) {}
|
|
340
|
+
declare class NotificationService extends Context.Service<
|
|
341
|
+
NotificationService,
|
|
342
|
+
{}
|
|
343
|
+
>()('NotificationService') {}
|
|
344
|
+
|
|
345
|
+
declare const ConfigLive: Layer.Layer<Config, never, never>;
|
|
346
|
+
declare const DatabaseLive: Layer.Layer<Database, never, Config>;
|
|
347
|
+
declare const CacheLive: Layer.Layer<Cache, never, Config>;
|
|
348
|
+
declare const PaymentDomainLive: Layer.Layer<PaymentDomain, never, Database>;
|
|
349
|
+
declare const OrderDomainLive: Layer.Layer<OrderDomain, never, Database>;
|
|
350
|
+
declare const PaymentGatewayLive: Layer.Layer<
|
|
351
|
+
PaymentGateway,
|
|
352
|
+
never,
|
|
353
|
+
PaymentDomain
|
|
354
|
+
>;
|
|
355
|
+
declare const NotificationServiceLive: Layer.Layer<
|
|
356
|
+
NotificationService,
|
|
357
|
+
never,
|
|
358
|
+
OrderDomain
|
|
359
|
+
>;
|
|
360
|
+
|
|
361
|
+
// Infrastructure: No dependencies
|
|
362
|
+
const InfrastructureLive = Layer.mergeAll(
|
|
363
|
+
ConfigLive, // Layer<Config, never, never>
|
|
364
|
+
DatabaseLive, // Layer<Database, never, Config>
|
|
365
|
+
CacheLive // Layer<Cache, never, Config>
|
|
366
|
+
).pipe(
|
|
367
|
+
Layer.provide(ConfigLive) // Satisfy Config requirement
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Domain: Depends on infrastructure
|
|
371
|
+
const DomainLive = Layer.mergeAll(
|
|
372
|
+
PaymentDomainLive, // Layer<PaymentDomain, never, Database>
|
|
373
|
+
OrderDomainLive // Layer<OrderDomain, never, Database>
|
|
374
|
+
).pipe(Layer.provide(InfrastructureLive));
|
|
375
|
+
|
|
376
|
+
// Application: Depends on domain
|
|
377
|
+
const ApplicationLive = Layer.mergeAll(
|
|
378
|
+
PaymentGatewayLive,
|
|
379
|
+
NotificationServiceLive
|
|
380
|
+
).pipe(Layer.provide(DomainLive));
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Pattern: Multiple Implementations
|
|
384
|
+
|
|
385
|
+
Switch implementations for different environments:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { Context, Effect, Layer } from 'effect';
|
|
389
|
+
|
|
390
|
+
interface Connection {
|
|
391
|
+
readonly close: () => void;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export class Database extends Context.Service<
|
|
395
|
+
Database,
|
|
396
|
+
{
|
|
397
|
+
readonly query: (sql: string) => Effect.Effect<{ rows: unknown[] }>;
|
|
398
|
+
}
|
|
399
|
+
>()('Database') {}
|
|
400
|
+
|
|
401
|
+
declare const connectToProduction: () => Effect.Effect<Connection>;
|
|
402
|
+
declare const createDatabaseService: (connection: Connection) => {
|
|
403
|
+
readonly query: (sql: string) => Effect.Effect<{ rows: unknown[] }>;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
declare const myProgram: Effect.Effect<void, never, Database>;
|
|
407
|
+
|
|
408
|
+
// Production
|
|
409
|
+
export const DatabaseLive = Layer.effect(
|
|
410
|
+
Database,
|
|
411
|
+
Effect.gen(function* () {
|
|
412
|
+
const connection = yield* connectToProduction();
|
|
413
|
+
return createDatabaseService(connection);
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Test
|
|
418
|
+
export const DatabaseTest = Layer.succeed(
|
|
419
|
+
Database,
|
|
420
|
+
Database.of({
|
|
421
|
+
query: () => Effect.succeed({ rows: [] })
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Use in application
|
|
426
|
+
const program = Effect.gen(function* () {
|
|
427
|
+
const nodeEnv = yield* Config.string('NODE_ENV').pipe(
|
|
428
|
+
Config.withDefault('production')
|
|
429
|
+
);
|
|
430
|
+
yield* myProgram.pipe(
|
|
431
|
+
Effect.provide(nodeEnv === 'test' ? DatabaseTest : DatabaseLive)
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## Pattern: Layer Sharing
|
|
437
|
+
|
|
438
|
+
Layers are memoized - same instance shared across program:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import { Context, Effect, Layer } from 'effect';
|
|
442
|
+
|
|
443
|
+
declare class Config extends Context.Service<
|
|
444
|
+
Config,
|
|
445
|
+
{ readonly value: string }
|
|
446
|
+
>()('Config') {}
|
|
447
|
+
declare const ConfigLive: Layer.Layer<Config, never, never>;
|
|
448
|
+
|
|
449
|
+
// Config is constructed once and shared
|
|
450
|
+
const program = Effect.all([
|
|
451
|
+
Effect.gen(function* () {
|
|
452
|
+
const config = yield* Config;
|
|
453
|
+
// Uses shared instance
|
|
454
|
+
}),
|
|
455
|
+
Effect.gen(function* () {
|
|
456
|
+
const config = yield* Config;
|
|
457
|
+
// Same instance
|
|
458
|
+
})
|
|
459
|
+
]).pipe(Effect.provide(ConfigLive));
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
> **Memo-map fork nuance:** Sharing is mediated by a `MemoMap`. A root memo map (`Layer.makeMemoMapUnsafe()`, used implicitly by `Effect.provide`) shares every layer allocation it builds. A _forked_ memo map (`Layer.forkMemoMap` / `Layer.forkMemoMapUnsafe`) can still see allocations its parent already built, but new allocations it builds stay isolated and are not written back to the parent. This is the mechanism `@effect/vitest` uses to reuse parent layers while isolating nested `it.layer` suites.
|
|
463
|
+
|
|
464
|
+
## Error Handling in Layers
|
|
465
|
+
|
|
466
|
+
Handle construction errors:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { Context, Effect, Layer, Schema } from 'effect';
|
|
470
|
+
|
|
471
|
+
interface Connection {
|
|
472
|
+
readonly close: () => void;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
class ConnectionError extends Schema.TaggedError<ConnectionError>()(
|
|
476
|
+
'ConnectionError',
|
|
477
|
+
{
|
|
478
|
+
message: Schema.String
|
|
479
|
+
}
|
|
480
|
+
) {}
|
|
481
|
+
|
|
482
|
+
class DatabaseConstructionError extends Schema.TaggedError<DatabaseConstructionError>()(
|
|
483
|
+
'DatabaseConstructionError',
|
|
484
|
+
{ cause: ConnectionError }
|
|
485
|
+
) {}
|
|
486
|
+
|
|
487
|
+
export class Database extends Context.Service<
|
|
488
|
+
Database,
|
|
489
|
+
{
|
|
490
|
+
readonly query: (sql: string) => Effect.Effect<unknown>;
|
|
491
|
+
}
|
|
492
|
+
>()('Database') {}
|
|
493
|
+
|
|
494
|
+
declare const connectToDatabase: () => Effect.Effect<
|
|
495
|
+
Connection,
|
|
496
|
+
ConnectionError
|
|
497
|
+
>;
|
|
498
|
+
declare const createDatabaseService: (connection: Connection) => {
|
|
499
|
+
readonly query: (sql: string) => Effect.Effect<unknown>;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export const DatabaseLive = Layer.effect(
|
|
503
|
+
Database,
|
|
504
|
+
Effect.gen(function* () {
|
|
505
|
+
const connection = yield* connectToDatabase().pipe(
|
|
506
|
+
Effect.catchTag('ConnectionError', (error) =>
|
|
507
|
+
Effect.fail(new DatabaseConstructionError({ cause: error }))
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
return createDatabaseService(connection);
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Composing Layers: Deliberate ProvideMerge
|
|
516
|
+
|
|
517
|
+
`Layer.provideMerge` satisfies dependencies AND passes them through to the output. Use it only when downstream consumers intentionally need both outputs, including carefully designed test stacks.
|
|
518
|
+
|
|
519
|
+
### Provide vs ProvideMerge
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { Layer } from 'effect';
|
|
523
|
+
|
|
524
|
+
declare const ConfigLayer: Layer.Layer<Config>;
|
|
525
|
+
declare const DatabaseLayer: Layer.Layer<Database, never, Config>;
|
|
526
|
+
declare const UserServiceLayer: Layer.Layer<UserService, never, Database>;
|
|
527
|
+
|
|
528
|
+
// Layer.provide — satisfies requirement, REMOVES it from output
|
|
529
|
+
const db = DatabaseLayer.pipe(Layer.provide(ConfigLayer));
|
|
530
|
+
// db: Layer<Database> — Config is NOT in the output
|
|
531
|
+
|
|
532
|
+
// Layer.provideMerge — satisfies requirement, KEEPS it in output
|
|
533
|
+
const dbWithConfig = DatabaseLayer.pipe(Layer.provideMerge(ConfigLayer));
|
|
534
|
+
// dbWithConfig: Layer<Database | Config> — Config remains available
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### When to use ProvideMerge
|
|
538
|
+
|
|
539
|
+
Use `Layer.provideMerge` when downstream test layers intentionally need the upstream services as outputs as well as dependencies:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { Layer } from 'effect';
|
|
543
|
+
|
|
544
|
+
// Test layer composition — downstream layers need Config AND Database
|
|
545
|
+
const infra = Layer.mergeAll(ConfigLayer, DatabaseLayer).pipe(
|
|
546
|
+
Layer.provideMerge(ConfigLayer) // Config stays visible for downstream
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Both UserService and OrderService can access Config and Database
|
|
550
|
+
const services = Layer.mergeAll(UserServiceLayer, OrderServiceLayer).pipe(
|
|
551
|
+
Layer.provideMerge(infra)
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
If downstream code does not need the dependency, use `Layer.provide` and keep it hidden. Do not preserve every intermediate service by default.
|
|
556
|
+
|
|
557
|
+
## Pattern: SynchronizedRef + Deferred State Machine
|
|
558
|
+
|
|
559
|
+
For services that need atomic state transitions with concurrent callers, use `SynchronizedRef.modifyEffect` combined with `Deferred` for result sharing:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { Deferred, Effect, Fiber, Scope, SynchronizedRef } from 'effect';
|
|
563
|
+
|
|
564
|
+
type State<A, E> =
|
|
565
|
+
| { readonly _tag: 'Idle' }
|
|
566
|
+
| {
|
|
567
|
+
readonly _tag: 'Running';
|
|
568
|
+
readonly done: Deferred.Deferred<A, E>;
|
|
569
|
+
readonly fiber: Fiber.Fiber<A, E>;
|
|
570
|
+
}
|
|
571
|
+
| { readonly _tag: 'Pending'; readonly done: Deferred.Deferred<A, E> };
|
|
572
|
+
|
|
573
|
+
const make = <A, E>(scope: Scope.Scope) => {
|
|
574
|
+
const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: 'Idle' });
|
|
575
|
+
|
|
576
|
+
const run = (work: Effect.Effect<A, E>) =>
|
|
577
|
+
SynchronizedRef.modifyEffect(
|
|
578
|
+
ref,
|
|
579
|
+
Effect.fnUntraced(function* (state) {
|
|
580
|
+
switch (state._tag) {
|
|
581
|
+
case 'Running':
|
|
582
|
+
// Already running — share the existing result
|
|
583
|
+
return [Deferred.await(state.done), state];
|
|
584
|
+
case 'Idle': {
|
|
585
|
+
// Start new work
|
|
586
|
+
const done = yield* Deferred.make<A, E>();
|
|
587
|
+
const fiber = yield* Effect.forkIn(
|
|
588
|
+
work.pipe(Effect.intoDeferred(done)),
|
|
589
|
+
scope
|
|
590
|
+
);
|
|
591
|
+
return [
|
|
592
|
+
Deferred.await(done),
|
|
593
|
+
{ _tag: 'Running' as const, done, fiber }
|
|
594
|
+
];
|
|
595
|
+
}
|
|
596
|
+
case 'Pending': {
|
|
597
|
+
// Queued — share the pending result
|
|
598
|
+
return [Deferred.await(state.done), state];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
).pipe(Effect.flatten);
|
|
603
|
+
|
|
604
|
+
return { run };
|
|
605
|
+
};
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Key properties:
|
|
609
|
+
|
|
610
|
+
- **`SynchronizedRef.modifyEffect`** — atomically reads state, runs an effect, and updates state in one operation. No other caller can interleave.
|
|
611
|
+
- **`Deferred`** — shares the result of in-flight work with concurrent callers who arrive while it's running.
|
|
612
|
+
- **`Effect.forkIn(work, scope)`** — ties the worker fiber to the service scope, not the calling fiber.
|
|
613
|
+
- The state machine pattern ensures at most one concurrent execution of `work`, with all callers sharing the same result.
|
|
614
|
+
|
|
615
|
+
Use this pattern when:
|
|
616
|
+
|
|
617
|
+
- Multiple concurrent callers may trigger the same expensive operation
|
|
618
|
+
- Only one execution should run at a time
|
|
619
|
+
- All callers should receive the same result
|
|
620
|
+
|
|
621
|
+
## Naming Convention
|
|
622
|
+
|
|
623
|
+
- `*Live` - Production implementation
|
|
624
|
+
- `*Test` - Test implementation
|
|
625
|
+
- `*Mock` - Mock for testing
|
|
626
|
+
- Descriptive names for specialized implementations
|
|
627
|
+
|
|
628
|
+
## Quality Checklist
|
|
629
|
+
|
|
630
|
+
- [ ] Layer type accurately reflects dependencies
|
|
631
|
+
- [ ] `Service.of({...})` used when returning from `Layer.effect`, never a plain object
|
|
632
|
+
- [ ] Resource cleanup using `acquireRelease` or `addFinalizer` if needed
|
|
633
|
+
- [ ] Layer can be tested with mock dependencies
|
|
634
|
+
- [ ] No dependency leakage into service interface
|
|
635
|
+
- [ ] Merge/provide/provideMerge follows the intended exposed service graph, not only type errors
|
|
636
|
+
- [ ] Long-lived acquisition completes and forks background work into the layer scope
|
|
637
|
+
- [ ] `defaultLayer` only present when `layer` has unsatisfied requirements
|
|
638
|
+
- [ ] `defaultLayer` composes directly unless deferred evaluation is truly required
|
|
639
|
+
- [ ] Error handling for construction failures
|
|
640
|
+
- [ ] JSDoc with example usage
|
|
641
|
+
|
|
642
|
+
Layers should make dependency management explicit while keeping service interfaces clean and focused.
|