Skip to content

Commit fb3cbec

Browse files
committed
fix: update scoped service layer pattern to use Effect.Service's scoped property
Update pattern to use Effect.Service's scoped property instead of Layer.scoped Add proper resource cleanup with Effect.addFinalizer Improve example with better type definitions and comments Fix program execution to use Effect.scoped properly Regenerate published version through pipeline This fixes the pattern to show the recommended way of creating service layers around managed resources in Effect.
1 parent a0e535d commit fb3cbec

8 files changed

Lines changed: 187 additions & 75 deletions

File tree

content/published/scoped-service-layer.mdx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ title: "Create a Service Layer from a Managed Resource"
33
id: "scoped-service-layer"
44
skillLevel: "intermediate"
55
useCase: ["Resource Management", "Dependency Injection", "Application Architecture"]
6-
summary: "Use the `scoped` property of Effect.Service to transform a managed resource into a shareable, application-wide service."
6+
summary: "Use `Layer.scoped` with `Effect.Service` to transform a managed resource into a shareable, application-wide service."
77
tags: ["resource", "layer", "scope", "service", "dependency-injection", "context", "acquire-release"]
88
rule:
9-
description: "Provide a managed resource to the application context using the `scoped` property of Effect.Service."
9+
description: "Provide a managed resource to the application context using `Layer.scoped`."
1010
related: ["acquire-release-bracket"]
1111
author: "PaulJPhilp"
1212
---
@@ -15,7 +15,7 @@ author: "PaulJPhilp"
1515

1616
## Guideline
1717

18-
Define a service using `class MyService extends Effect.Service(...)`. Implement the service using the `scoped` property of the service class. This property should be a scoped `Effect` (typically from `Effect.acquireRelease`) that builds and releases the underlying resource.
18+
Define a service using `class MyService extends Effect.Service(...)` and provide a `scoped` property in the implementation object. This property should be an `Effect` (typically from `Effect.acquireRelease`) that builds and releases the underlying resource.
1919

2020
## Rationale
2121

@@ -24,36 +24,52 @@ This pattern is the key to building robust, testable, and leak-proof application
2424
## Good Example
2525

2626
```typescript
27-
import { Effect, Layer, Console } from "effect";
27+
import { Effect, Console } from "effect";
2828

29-
interface DbOps {
30-
query: (sql: string) => Effect.Effect<string[], never, never>;
29+
// 1. Define the service interface
30+
interface DatabaseService {
31+
readonly query: (sql: string) => Effect.Effect<string[], never, never>
3132
}
3233

33-
// 1. Define the service using Effect.Service
34-
class Database extends Effect.Service<DbOps>()(
34+
// 2. Define the service implementation with scoped resource management
35+
class Database extends Effect.Service<DatabaseService>()(
3536
"Database",
3637
{
38+
// The scoped property manages the resource lifecycle
3739
scoped: Effect.gen(function* () {
3840
const id = Math.floor(Math.random() * 1000);
41+
42+
// Acquire the connection
3943
yield* Console.log(`[Pool ${id}] Acquired`);
44+
45+
// Setup cleanup to run when scope closes
46+
yield* Effect.addFinalizer(() =>
47+
Console.log(`[Pool ${id}] Released`)
48+
);
49+
50+
// Return the service implementation
4051
return {
41-
query: (sql: string): Effect.Effect<string[], never, never> =>
42-
Effect.sync(() => [`Result for '${sql}' from pool ${id}`])
52+
query: (sql: string) => Effect.sync(() =>
53+
[`Result for '${sql}' from pool ${id}`]
54+
)
4355
};
4456
})
4557
}
4658
) {}
4759

48-
// This program depends on the abstract Database service
60+
// 3. Use the service in your program
4961
const program = Effect.gen(function* () {
5062
const db = yield* Database;
5163
const users = yield* db.query("SELECT * FROM users");
5264
yield* Console.log(`Query successful: ${users[0]}`);
5365
});
5466

55-
// Provide the live implementation to run the program
56-
Effect.runPromise(Effect.provide(program, Database.Default));
67+
// 4. Run the program with scoped resource management
68+
Effect.runPromise(
69+
Effect.scoped(program).pipe(
70+
Effect.provide(Database.Default)
71+
)
72+
);
5773

5874
/*
5975
Output:

content/raw/scoped-service-layer.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ author: "PaulJPhilp"
1515

1616
## Guideline
1717

18-
Define a service using `class MyService extends Effect.Service(...)`. Implement the service using the `scoped` property of the service class. This property should be a scoped `Effect` (typically from `Effect.acquireRelease`) that builds and releases the underlying resource.
18+
Define a service using `class MyService extends Effect.Service(...)` and provide a `scoped` property in the implementation object. This property should be an `Effect` (typically from `Effect.acquireRelease`) that builds and releases the underlying resource.
1919

2020
## Rationale
2121

content/src/scoped-service-layer.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,49 @@
1-
import { Effect, Layer, Console } from "effect";
1+
import { Effect, Console } from "effect";
22

3-
interface DbOps {
4-
query: (sql: string) => Effect.Effect<string[], never, never>;
3+
// 1. Define the service interface
4+
interface DatabaseService {
5+
readonly query: (sql: string) => Effect.Effect<string[], never, never>
56
}
67

7-
// 1. Define the service using Effect.Service
8-
class Database extends Effect.Service<DbOps>()(
8+
// 2. Define the service implementation with scoped resource management
9+
class Database extends Effect.Service<DatabaseService>()(
910
"Database",
1011
{
12+
// The scoped property manages the resource lifecycle
1113
scoped: Effect.gen(function* () {
1214
const id = Math.floor(Math.random() * 1000);
15+
16+
// Acquire the connection
1317
yield* Console.log(`[Pool ${id}] Acquired`);
18+
19+
// Setup cleanup to run when scope closes
20+
yield* Effect.addFinalizer(() =>
21+
Console.log(`[Pool ${id}] Released`)
22+
);
23+
24+
// Return the service implementation
1425
return {
15-
query: (sql: string): Effect.Effect<string[], never, never> =>
16-
Effect.sync(() => [`Result for '${sql}' from pool ${id}`])
26+
query: (sql: string) => Effect.sync(() =>
27+
[`Result for '${sql}' from pool ${id}`]
28+
)
1729
};
1830
})
1931
}
2032
) {}
2133

22-
// This program depends on the abstract Database service
34+
// 3. Use the service in your program
2335
const program = Effect.gen(function* () {
2436
const db = yield* Database;
2537
const users = yield* db.query("SELECT * FROM users");
2638
yield* Console.log(`Query successful: ${users[0]}`);
2739
});
2840

29-
// Provide the live implementation to run the program
30-
Effect.runPromise(Effect.provide(program, Database.Default));
41+
// 4. Run the program with scoped resource management
42+
Effect.runPromise(
43+
Effect.scoped(program).pipe(
44+
Effect.provide(Database.Default)
45+
)
46+
);
3147

3248
/*
3349
Output:

rules/by-use-case/application-architecture.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,36 +75,52 @@ We define two completely independent services, `Database` and `ApiClient`, each
7575

7676
### Example
7777
```typescript
78-
import { Effect, Layer, Console } from "effect";
78+
import { Effect, Console } from "effect";
7979

80-
interface DbOps {
81-
query: (sql: string) => Effect.Effect<string[], never, never>;
80+
// 1. Define the service interface
81+
interface DatabaseService {
82+
readonly query: (sql: string) => Effect.Effect<string[], never, never>
8283
}
8384

84-
// 1. Define the service using Effect.Service
85-
class Database extends Effect.Service<DbOps>()(
85+
// 2. Define the service implementation with scoped resource management
86+
class Database extends Effect.Service<DatabaseService>()(
8687
"Database",
8788
{
89+
// The scoped property manages the resource lifecycle
8890
scoped: Effect.gen(function* () {
8991
const id = Math.floor(Math.random() * 1000);
92+
93+
// Acquire the connection
9094
yield* Console.log(`[Pool ${id}] Acquired`);
95+
96+
// Setup cleanup to run when scope closes
97+
yield* Effect.addFinalizer(() =>
98+
Console.log(`[Pool ${id}] Released`)
99+
);
100+
101+
// Return the service implementation
91102
return {
92-
query: (sql: string): Effect.Effect<string[], never, never> =>
93-
Effect.sync(() => [`Result for '${sql}' from pool ${id}`])
103+
query: (sql: string) => Effect.sync(() =>
104+
[`Result for '${sql}' from pool ${id}`]
105+
)
94106
};
95107
})
96108
}
97109
) {}
98110

99-
// This program depends on the abstract Database service
111+
// 3. Use the service in your program
100112
const program = Effect.gen(function* () {
101113
const db = yield* Database;
102114
const users = yield* db.query("SELECT * FROM users");
103115
yield* Console.log(`Query successful: ${users[0]}`);
104116
});
105117

106-
// Provide the live implementation to run the program
107-
Effect.runPromise(Effect.provide(program, Database.Default));
118+
// 4. Run the program with scoped resource management
119+
Effect.runPromise(
120+
Effect.scoped(program).pipe(
121+
Effect.provide(Database.Default)
122+
)
123+
);
108124

109125
/*
110126
Output:

rules/by-use-case/dependency-injection.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,36 +75,52 @@ We define two completely independent services, `Database` and `ApiClient`, each
7575

7676
### Example
7777
```typescript
78-
import { Effect, Layer, Console } from "effect";
78+
import { Effect, Console } from "effect";
7979

80-
interface DbOps {
81-
query: (sql: string) => Effect.Effect<string[], never, never>;
80+
// 1. Define the service interface
81+
interface DatabaseService {
82+
readonly query: (sql: string) => Effect.Effect<string[], never, never>
8283
}
8384

84-
// 1. Define the service using Effect.Service
85-
class Database extends Effect.Service<DbOps>()(
85+
// 2. Define the service implementation with scoped resource management
86+
class Database extends Effect.Service<DatabaseService>()(
8687
"Database",
8788
{
89+
// The scoped property manages the resource lifecycle
8890
scoped: Effect.gen(function* () {
8991
const id = Math.floor(Math.random() * 1000);
92+
93+
// Acquire the connection
9094
yield* Console.log(`[Pool ${id}] Acquired`);
95+
96+
// Setup cleanup to run when scope closes
97+
yield* Effect.addFinalizer(() =>
98+
Console.log(`[Pool ${id}] Released`)
99+
);
100+
101+
// Return the service implementation
91102
return {
92-
query: (sql: string): Effect.Effect<string[], never, never> =>
93-
Effect.sync(() => [`Result for '${sql}' from pool ${id}`])
103+
query: (sql: string) => Effect.sync(() =>
104+
[`Result for '${sql}' from pool ${id}`]
105+
)
94106
};
95107
})
96108
}
97109
) {}
98110

99-
// This program depends on the abstract Database service
111+
// 3. Use the service in your program
100112
const program = Effect.gen(function* () {
101113
const db = yield* Database;
102114
const users = yield* db.query("SELECT * FROM users");
103115
yield* Console.log(`Query successful: ${users[0]}`);
104116
});
105117

106-
// Provide the live implementation to run the program
107-
Effect.runPromise(Effect.provide(program, Database.Default));
118+
// 4. Run the program with scoped resource management
119+
Effect.runPromise(
120+
Effect.scoped(program).pipe(
121+
Effect.provide(Database.Default)
122+
)
123+
);
108124

109125
/*
110126
Output:

rules/by-use-case/resource-management.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,36 +114,52 @@ in the event of errors or interruptions.
114114

115115
### Example
116116
```typescript
117-
import { Effect, Layer, Console } from "effect";
117+
import { Effect, Console } from "effect";
118118

119-
interface DbOps {
120-
query: (sql: string) => Effect.Effect<string[], never, never>;
119+
// 1. Define the service interface
120+
interface DatabaseService {
121+
readonly query: (sql: string) => Effect.Effect<string[], never, never>
121122
}
122123

123-
// 1. Define the service using Effect.Service
124-
class Database extends Effect.Service<DbOps>()(
124+
// 2. Define the service implementation with scoped resource management
125+
class Database extends Effect.Service<DatabaseService>()(
125126
"Database",
126127
{
128+
// The scoped property manages the resource lifecycle
127129
scoped: Effect.gen(function* () {
128130
const id = Math.floor(Math.random() * 1000);
131+
132+
// Acquire the connection
129133
yield* Console.log(`[Pool ${id}] Acquired`);
134+
135+
// Setup cleanup to run when scope closes
136+
yield* Effect.addFinalizer(() =>
137+
Console.log(`[Pool ${id}] Released`)
138+
);
139+
140+
// Return the service implementation
130141
return {
131-
query: (sql: string): Effect.Effect<string[], never, never> =>
132-
Effect.sync(() => [`Result for '${sql}' from pool ${id}`])
142+
query: (sql: string) => Effect.sync(() =>
143+
[`Result for '${sql}' from pool ${id}`]
144+
)
133145
};
134146
})
135147
}
136148
) {}
137149

138-
// This program depends on the abstract Database service
150+
// 3. Use the service in your program
139151
const program = Effect.gen(function* () {
140152
const db = yield* Database;
141153
const users = yield* db.query("SELECT * FROM users");
142154
yield* Console.log(`Query successful: ${users[0]}`);
143155
});
144156

145-
// Provide the live implementation to run the program
146-
Effect.runPromise(Effect.provide(program, Database.Default));
157+
// 4. Run the program with scoped resource management
158+
Effect.runPromise(
159+
Effect.scoped(program).pipe(
160+
Effect.provide(Database.Default)
161+
)
162+
);
147163

148164
/*
149165
Output:

0 commit comments

Comments
 (0)