@zio.dev/zio-blocks 0.0.29 → 0.0.30
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/guides/compile-time-resource-safety-with-scope.md +997 -0
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +203 -203
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +20 -14
- package/package.json +1 -1
- package/reference/codec.md +7 -7
- package/reference/context.md +1 -1
- package/reference/docs.md +1 -1
- package/reference/dynamic-schema.md +39 -42
- package/reference/media-type.md +2 -2
- package/reference/resource-management/defer-handle.md +246 -0
- package/reference/resource-management/finalization.md +286 -0
- package/reference/resource-management/finalizer.md +167 -0
- package/reference/{resource-management-di → resource-management}/index.md +3 -8
- package/reference/{resource-management-di → resource-management}/resource.md +532 -50
- package/reference/resource-management/scope.md +3026 -0
- package/reference/resource-management/unscoped.md +125 -0
- package/reference/{resource-management-di → resource-management}/wire.md +74 -28
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/streams.md +989 -0
- package/reference/type-class-derivation.md +1 -1
- package/ringbuffer.md +1 -1
- package/sidebars.js +9 -4
- package/reference/resource-management-di/scope.md +0 -1423
|
@@ -0,0 +1,3026 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: scope
|
|
3
|
+
title: "Scope"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Scope` is a **compile-time safe resource lifecycle manager** that tags allocated values with a scope-specific type, preventing use-after-close at compile time. Each scope instance has a distinct `$[A]` type that is unique to that scope, making values from different scopes structurally incompatible. The `$` operator macro and `Unscoped` typeclass create multiple layers of compile-time protection, eliminating an entire class of lifetime bugs without runtime overhead.
|
|
7
|
+
|
|
8
|
+
`Scope`:
|
|
9
|
+
- Prevents resource leaks and use-after-close via compile-time type checking
|
|
10
|
+
- Allocates resources eagerly and runs finalizers deterministically in LIFO order
|
|
11
|
+
- Is purely synchronous with zero runtime overhead (scoped values erase to underlying types)
|
|
12
|
+
|
|
13
|
+
Here's the interface definition:
|
|
14
|
+
|
|
15
|
+
```scala
|
|
16
|
+
trait Scope {
|
|
17
|
+
type $[+A]
|
|
18
|
+
|
|
19
|
+
def scoped[A](f: Scope => A): A
|
|
20
|
+
def allocate[A](resource: Resource[A]): $[A]
|
|
21
|
+
def allocate(value: => AutoCloseable): $[AutoCloseable]
|
|
22
|
+
def open(): $[OpenScope]
|
|
23
|
+
def defer(f: => Unit): DeferHandle
|
|
24
|
+
def lower[A](value: parent.$[A]): $[A]
|
|
25
|
+
def isClosed: Boolean
|
|
26
|
+
def isOwner: Boolean
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Motivation
|
|
31
|
+
|
|
32
|
+
Most resource bugs in Scala are "escape" bugs—scenarios where a resource is used outside of its intended lifetime, leading to undefined behavior, crashes, or data corruption:
|
|
33
|
+
|
|
34
|
+
- **Storing in fields:** You open a database connection and store it in a field, intending to close it in a finalizer. But if the finalizer runs before you're truly done with the connection, or if you forget to close it, the connection is silently used after closure.
|
|
35
|
+
- **Capturing in closures:** You create a file handle and pass it to an async framework via a callback. The callback might be invoked long after your scope has closed and the file has been released, causing the program to crash or silently read/write corrupted data.
|
|
36
|
+
- **Passing to untrusted code:** You pass a resource to a library function that might store a reference and use it later, outside your scope. You have no way to know when it's safe to close.
|
|
37
|
+
- **Mixing lifetimes:** In large codebases, it becomes unclear which scope owns which resource. A developer might use a resource in the wrong scope, or two scopes might try to close the same resource.
|
|
38
|
+
|
|
39
|
+
Scope addresses these with a *tight* design. Each design choice solves a specific problem and works together with the others:
|
|
40
|
+
|
|
41
|
+
1. **Compile-time leak prevention via type tagging** — Every scope has its own `$[A]` type, combined with the `$` macro that restricts how you can use values and the `Unscoped` typeclass that marks safe return types. Together, these prevent returning resources from their scope at compile time. No runtime wrapper objects needed.
|
|
42
|
+
|
|
43
|
+
2. **Zero runtime overhead** — Scoped values erase to the underlying type `A` at runtime (via casts). There's no boxing, no extra objects, no GC pressure. The compile-time safety is "free."
|
|
44
|
+
|
|
45
|
+
3. **Eager allocation** — Resources are acquired immediately when you call `allocate`, not deferred to some later point. This makes lifetimes predictable and your code matches your mental model.
|
|
46
|
+
|
|
47
|
+
4. **Deterministic, LIFO finalization** — Finalizers are guaranteed to run in reverse order of allocation when a scope closes. If acquisition order implies dependencies (common in resource hierarchies), cleanup order is automatically correct. Exceptions in finalizers are collected rather than stopping cleanup.
|
|
48
|
+
|
|
49
|
+
5. **Structured scopes with parent-child relationships** — Scopes form a hierarchy; children always close before parents. The `lower` operator lets you safely use parent-scoped values in children, since parent will outlive child.
|
|
50
|
+
|
|
51
|
+
If you've used `try/finally`, `Using`, or ZIO's `Scope`, this is the same problem space—but optimized for **synchronous code** with **compile-time boundaries**.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Add the following dependency to your `build.sbt`:
|
|
56
|
+
|
|
57
|
+
```scala
|
|
58
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "0.0.30"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Supported Scala versions: **2.13.x** and **3.x**.
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
Here's a minimal example showing resource allocation, usage, and cleanup. This example introduces a canonical `Database` stub that we'll reuse throughout this guide:
|
|
66
|
+
|
|
67
|
+
```scala
|
|
68
|
+
import zio.blocks.scope._
|
|
69
|
+
|
|
70
|
+
final class Database extends AutoCloseable {
|
|
71
|
+
def query(sql: String): String = s"result: $sql"
|
|
72
|
+
def close(): Unit = println("db closed")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
val out: String =
|
|
76
|
+
Scope.global.scoped { scope =>
|
|
77
|
+
import scope._
|
|
78
|
+
|
|
79
|
+
val db: $[Database] =
|
|
80
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
81
|
+
|
|
82
|
+
// Safe access: the lambda parameter can only be used as a receiver
|
|
83
|
+
$(db)(_.query("SELECT 1"))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
println(out)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
What's happening in this code:
|
|
90
|
+
|
|
91
|
+
**Allocating resources in a scope.** When you call `Resource.fromAutoCloseable(new Database).allocate`, you're acquiring a database connection. The `allocate` method returns a **scoped value** of type `scope.$[Database]`—notice the `$` wrapper. This type is unique to the `scope` instance. You can import the scope to use the short form `$[Database]`.
|
|
92
|
+
|
|
93
|
+
**The `$` operator restricts access.** You cannot call `db.query(...)` directly on `$[Database]` because the methods are hidden at the type level. Instead, you use the `$` access operator: `$(db)(f)`, which takes a lambda. The lambda's parameter must be used only as a receiver (for method/field access), preventing accidental capture or escape.
|
|
94
|
+
|
|
95
|
+
**Safe return from scoped.** The `scoped` block returns a plain `String` (the result of `_.query("SELECT 1")`). This is safe because `String` is marked as `Unscoped`—a typeclass that says "this type is pure data, safe to leave a scope." If you tried to return `db` instead, the compiler would error.
|
|
96
|
+
|
|
97
|
+
**LIFO cleanup.** When the `scoped` block exits (normally or via exception), all finalizers run in reverse order. The database's `close()` method is registered automatically because `Database` extends `AutoCloseable`. So cleanup happens at the right time, in the right order, even if an exception occurred.
|
|
98
|
+
|
|
99
|
+
## Safety Model
|
|
100
|
+
|
|
101
|
+
Scope's compile-time safety comes from *three reinforcing layers* that work together to prevent resource leaks.
|
|
102
|
+
|
|
103
|
+
1. **Type identity per scope.** Every scope has a distinct `$[A]` type. This makes values from different scopes **structurally incompatible** at compile time, so you cannot accidentally use a resource in the wrong scope without an explicit conversion (`lower` for parent → child). For example, `scope1.$[Database]` and `scope2.$[Database]` are different types—the compiler refuses to mix them:
|
|
104
|
+
|
|
105
|
+
```scala
|
|
106
|
+
// does not compile:
|
|
107
|
+
Scope.global.scoped { scope1 =>
|
|
108
|
+
import scope1._
|
|
109
|
+
val db1 = allocate(new Database)
|
|
110
|
+
|
|
111
|
+
scope1.scoped { scope2 =>
|
|
112
|
+
import scope2._
|
|
113
|
+
val x: scope2.$[Database] = db1 // Error: type mismatch
|
|
114
|
+
// scope2.$[Database] is not compatible with scope1.$[Database]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To safely use a parent scope's resource in a child scope, use `lower`:
|
|
120
|
+
|
|
121
|
+
```scala
|
|
122
|
+
Scope.global.scoped { outer =>
|
|
123
|
+
import outer._
|
|
124
|
+
val db = allocate(new Database)
|
|
125
|
+
|
|
126
|
+
outer.scoped { inner =>
|
|
127
|
+
import inner._
|
|
128
|
+
val dbInChild = inner.lower(db) // ✓ Correct: retags for child scope
|
|
129
|
+
$(dbInChild)(_.query("SELECT 1"))
|
|
130
|
+
}
|
|
131
|
+
// db is still alive here after child closes
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
2. **Controlled access via the `$` macro.** The `$` operator only allows using an unwrapped value as a **method/field receiver**. This prevents returning the resource, storing it in a local val/var, passing it as an argument to a function, or capturing it in a closure. The `$` macro also requires a **lambda literal** (not a method reference or variable):
|
|
136
|
+
|
|
137
|
+
```scala
|
|
138
|
+
// does not compile:
|
|
139
|
+
val f: Database => String = _.query("x")
|
|
140
|
+
(scope $ db)(f) // Error: "$ requires a lambda literal ..."
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A lambda literal is an anonymous function written directly in code (e.g., `_.query("x")` or `x => x + 1`). The macro inspects the actual code you pass, so you must pass the lambda directly: `$(db)(_.query("x"))` compiles, but storing it in a variable first defeats this check. Without this restriction, you could smuggle the resource out indirectly via a stored function:
|
|
144
|
+
|
|
145
|
+
```scala
|
|
146
|
+
// hypothetical: if the macro didn't require a lambda literal
|
|
147
|
+
var leaked: Database = null
|
|
148
|
+
|
|
149
|
+
val f: Database => String = { db =>
|
|
150
|
+
leaked = db // Store the database somewhere the macro can't see
|
|
151
|
+
db.query("x")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
$(db)(f) // Macro sees the call but can't detect the smuggling above
|
|
155
|
+
|
|
156
|
+
// After the scope closes, the resource is still accessible:
|
|
157
|
+
leaked.query("SELECT *") // Use-after-close bug!
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
By requiring a lambda literal, the macro can analyze the actual code syntax. It rejects any attempt to store or capture the parameter, making smuggling impossible.
|
|
161
|
+
|
|
162
|
+
3. **Scope boundary enforcement via `Unscoped`.** A `scoped { ... }` block can only return values with an `Unscoped` instance (pure data). Resources and closures cannot escape the scope boundary at compile time. For example, trying to return a resource directly fails:
|
|
163
|
+
|
|
164
|
+
```scala
|
|
165
|
+
// does not compile:
|
|
166
|
+
Scope.global.scoped { scope =>
|
|
167
|
+
import scope._
|
|
168
|
+
val db = allocate(new Database)
|
|
169
|
+
db // Error: No given instance of Unscoped[$[Database]]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Closures over resources are also rejected:
|
|
174
|
+
|
|
175
|
+
```scala
|
|
176
|
+
// does not compile:
|
|
177
|
+
Scope.global.scoped { scope =>
|
|
178
|
+
import scope._
|
|
179
|
+
val db = allocate(new Database)
|
|
180
|
+
() => db.query("SELECT 1") // Error: No given instance of Unscoped[() => String]
|
|
181
|
+
// (the closure captures db)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Only types with an `Unscoped` instance can cross the scope boundary—typically pure data:
|
|
186
|
+
|
|
187
|
+
```scala
|
|
188
|
+
Scope.global.scoped { scope =>
|
|
189
|
+
import scope._
|
|
190
|
+
val db = allocate(new Database)
|
|
191
|
+
$(db)(_.query("SELECT 1")) // ✓ Correct: returns String, which is Unscoped
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Construction
|
|
196
|
+
|
|
197
|
+
### `Scope.global` — The Root Scope
|
|
198
|
+
|
|
199
|
+
`Scope.global` is the predefined root scope instance. It exists for the lifetime of your application and is the entry point for all scope-based resource management.
|
|
200
|
+
|
|
201
|
+
In `Scope.global`, the `$[A]` type is an identity type (i.e., `$[A] = A`). Finalizers registered in the global scope run on JVM shutdown via a shutdown hook. On Scala.js, global finalizers are not automatically invoked.
|
|
202
|
+
|
|
203
|
+
Use `Scope.global` to access the root scope:
|
|
204
|
+
|
|
205
|
+
```scala
|
|
206
|
+
import zio.blocks.scope._
|
|
207
|
+
|
|
208
|
+
val result: String = Scope.global.scoped { scope =>
|
|
209
|
+
import scope._
|
|
210
|
+
"no resources allocated"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `Scope#scoped` — Create and Enter a Child Scope
|
|
215
|
+
|
|
216
|
+
`scoped` creates a new child scope with lexical lifetime. All resources allocated within the lambda are automatically cleaned up (LIFO) when the lambda exits, whether normally or via exception:
|
|
217
|
+
|
|
218
|
+
The lambda receives the child scope as a parameter. You can import its members to use the short form `$[A]` instead of `scope.$[A]`:
|
|
219
|
+
|
|
220
|
+
```scala
|
|
221
|
+
import zio.blocks.scope._
|
|
222
|
+
|
|
223
|
+
final class Database extends AutoCloseable {
|
|
224
|
+
def query(sql: String): String = s"result: $sql"
|
|
225
|
+
def close(): Unit = println("database closed")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Scope.global.scoped { scope =>
|
|
229
|
+
import scope._
|
|
230
|
+
|
|
231
|
+
val db: $[Database] =
|
|
232
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
233
|
+
|
|
234
|
+
// Use the database within the scope
|
|
235
|
+
val result = $(db)(_.query("SELECT 1"))
|
|
236
|
+
result
|
|
237
|
+
// db is automatically closed here (scope exits)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `Scope#open` — Create an Unowned Child Scope
|
|
242
|
+
|
|
243
|
+
`open()` creates a child scope you explicitly close, returning an `OpenScope` handle. Unlike `scoped { }`, this allows non-lexical lifetime management:
|
|
244
|
+
|
|
245
|
+
The child scope is **unowned** (usable from any thread) but remains **linked to the parent** (if the parent closes, the child's finalizers also run). You must call `close()` to detach and finalize immediately.
|
|
246
|
+
|
|
247
|
+
This is useful for resource pools, lazy initialization, or service factories where you need to decouple resource acquisition from cleanup. Unlike `scoped { }`, which ties lifetime to a lexical block, `open()` lets you keep resources alive across function boundaries and explicit time boundaries.
|
|
248
|
+
|
|
249
|
+
Here's a practical application initialization pattern:
|
|
250
|
+
|
|
251
|
+
```scala
|
|
252
|
+
import zio.blocks.scope._
|
|
253
|
+
|
|
254
|
+
final class Database extends AutoCloseable {
|
|
255
|
+
def query(sql: String): String = s"result: $sql"
|
|
256
|
+
def close(): Unit = println("db closed")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Application initialization: open resources early, return handle for later cleanup
|
|
260
|
+
val appResources = Scope.global.open()
|
|
261
|
+
val db = appResources.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Use database from anywhere in the application
|
|
265
|
+
val result = appResources.scope.scoped { scope =>
|
|
266
|
+
import scope._
|
|
267
|
+
// Can create child scopes and use parent resources with lower()
|
|
268
|
+
val dbInChild = scope.lower(db)
|
|
269
|
+
$(dbInChild)(_.query("SELECT 1"))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
println(s"Query result: $result")
|
|
273
|
+
|
|
274
|
+
// ... rest of application code ...
|
|
275
|
+
|
|
276
|
+
} finally {
|
|
277
|
+
// Application shutdown: explicit cleanup (decoupled from creation)
|
|
278
|
+
appResources.close().orThrow()
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Core Operations
|
|
283
|
+
|
|
284
|
+
### `Scope#allocate` — Acquire a Resource
|
|
285
|
+
|
|
286
|
+
Allocates a `Resource[A]` in this scope, acquiring the underlying value immediately and registering its finalizer:
|
|
287
|
+
|
|
288
|
+
```scala
|
|
289
|
+
trait Scope {
|
|
290
|
+
def allocate[A](resource: Resource[A]): $[A]
|
|
291
|
+
def allocate[A <: AutoCloseable](value: => A): $[A]
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The first overload accepts any `Resource`. The second is a convenience for `AutoCloseable` values—their `close()` method is automatically registered as a finalizer.
|
|
296
|
+
|
|
297
|
+
If the scope is already closed, `allocate` throws `IllegalStateException`. Otherwise, the resource is acquired eagerly and its finalizer is registered to run LIFO when the scope closes:
|
|
298
|
+
|
|
299
|
+
```scala
|
|
300
|
+
import zio.blocks.scope._
|
|
301
|
+
|
|
302
|
+
final class Database extends AutoCloseable {
|
|
303
|
+
def query(sql: String): String = s"result: $sql"
|
|
304
|
+
def close(): Unit = println("db closed")
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
Scope.global.scoped { scope =>
|
|
308
|
+
import scope._
|
|
309
|
+
|
|
310
|
+
// Using Resource factory
|
|
311
|
+
val db1: $[Database] =
|
|
312
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
313
|
+
|
|
314
|
+
// Using AutoCloseable overload (convenience)
|
|
315
|
+
val db2: $[Database] = allocate(new Database)
|
|
316
|
+
|
|
317
|
+
// Both are equivalent; use whichever is more readable
|
|
318
|
+
()
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `$` — Access a Scoped Value
|
|
323
|
+
|
|
324
|
+
The `$` operator safely accesses a scoped value by enforcing it is only used as a method/field receiver, preventing accidental capture or escape:
|
|
325
|
+
|
|
326
|
+
**Single value:** Use infix or unqualified syntax:
|
|
327
|
+
|
|
328
|
+
```scala
|
|
329
|
+
import zio.blocks.scope._
|
|
330
|
+
|
|
331
|
+
final class Database extends AutoCloseable {
|
|
332
|
+
def query(sql: String): String = s"result: $sql"
|
|
333
|
+
def close(): Unit = println("db closed")
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
Scope.global.scoped { scope =>
|
|
337
|
+
import scope._
|
|
338
|
+
|
|
339
|
+
val db: $[Database] = allocate(new Database)
|
|
340
|
+
|
|
341
|
+
// Infix syntax
|
|
342
|
+
val result1 = (scope $ db)(_.query("SELECT 1"))
|
|
343
|
+
|
|
344
|
+
// Unqualified after `import scope._`
|
|
345
|
+
val result2 = $(db)(_.query("SELECT 2"))
|
|
346
|
+
|
|
347
|
+
result1 + result2
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Multiple values:** Use unqualified syntax only:
|
|
352
|
+
|
|
353
|
+
```scala
|
|
354
|
+
import zio.blocks.scope._
|
|
355
|
+
|
|
356
|
+
final class Database extends AutoCloseable {
|
|
357
|
+
def query(sql: String): String = s"result: $sql"
|
|
358
|
+
def close(): Unit = println("db closed")
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
final class Cache extends AutoCloseable {
|
|
362
|
+
def key(): String = "cache_key"
|
|
363
|
+
def close(): Unit = ()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
Scope.global.scoped { scope =>
|
|
367
|
+
import scope._
|
|
368
|
+
|
|
369
|
+
val db: $[Database] = allocate(new Database)
|
|
370
|
+
val cache: $[Cache] = allocate(new Cache)
|
|
371
|
+
|
|
372
|
+
// Multiple values: each parameter may only be a receiver
|
|
373
|
+
val result = $(db, cache)((d, c) => d.query(c.key()))
|
|
374
|
+
result
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The `$` macro enforces receiver-only rules at compile time:
|
|
379
|
+
- ✓ Allowed: `d.method()`, `d.method(c.key())` (method calls, field access)
|
|
380
|
+
- ✗ Rejected: `store(d)`, `() => d.method()`, `d` (returned), `{ val x = d; 1 }` (binding)
|
|
381
|
+
|
|
382
|
+
If a result type is `Unscoped[B]` (pure data), `$` auto-unwraps it to `B`. Otherwise, it returns `scope.$[B]`.
|
|
383
|
+
|
|
384
|
+
### `Scope#lower` — Use a Parent Value in a Child Scope
|
|
385
|
+
|
|
386
|
+
`lower` retagges a parent-scoped value into a child scope. This is safe because a parent scope always outlives its children:
|
|
387
|
+
|
|
388
|
+
This is useful when a child scope needs access to resources allocated in its parent:
|
|
389
|
+
|
|
390
|
+
```scala
|
|
391
|
+
import zio.blocks.scope._
|
|
392
|
+
|
|
393
|
+
final class Database extends AutoCloseable {
|
|
394
|
+
def query(sql: String): String = s"result: $sql"
|
|
395
|
+
def close(): Unit = println("db closed")
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Scope.global.scoped { outer =>
|
|
399
|
+
import outer._
|
|
400
|
+
|
|
401
|
+
val db: $[Database] = allocate(new Database)
|
|
402
|
+
|
|
403
|
+
// Create an inner scope that needs the database
|
|
404
|
+
outer.scoped { inner =>
|
|
405
|
+
import inner._
|
|
406
|
+
|
|
407
|
+
// Retag the parent's database into the child
|
|
408
|
+
val dbInChild: inner.$[Database] = inner.lower(db)
|
|
409
|
+
|
|
410
|
+
// Now use it in the child
|
|
411
|
+
$(dbInChild)(_.query("child query"))
|
|
412
|
+
}
|
|
413
|
+
// When inner exits, its finalizers run
|
|
414
|
+
// When outer exits, db's finalizers run (still alive for the outer scope)
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### `Finalizer#defer` — Register a Manual Finalizer
|
|
419
|
+
|
|
420
|
+
`defer` registers a cleanup action to run when the scope closes. It returns a `DeferHandle` that can cancel the registration:
|
|
421
|
+
|
|
422
|
+
```scala
|
|
423
|
+
trait Finalizer {
|
|
424
|
+
def defer(f: => Unit): DeferHandle
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`defer` is useful for resources that are not wrapped in `Resource`, or when you need explicit control over finalization. Here's a practical example—managing a temporary file and a logger that don't implement `AutoCloseable`:
|
|
429
|
+
|
|
430
|
+
```scala
|
|
431
|
+
import zio.blocks.scope._
|
|
432
|
+
import java.nio.file._
|
|
433
|
+
|
|
434
|
+
// A logger that needs manual cleanup but doesn't implement AutoCloseable
|
|
435
|
+
class Logger {
|
|
436
|
+
def log(msg: String): Unit = println(s"[LOG] $msg")
|
|
437
|
+
def close(): Unit = println("Logger closed")
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
Scope.global.scoped { scope =>
|
|
441
|
+
import scope._
|
|
442
|
+
|
|
443
|
+
// Create a temporary file (not AutoCloseable from standard library)
|
|
444
|
+
val tempFile = Files.createTempFile("app", ".tmp")
|
|
445
|
+
defer {
|
|
446
|
+
Files.deleteIfExists(tempFile)
|
|
447
|
+
println(s"Temp file deleted: $tempFile")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Create a logger (not AutoCloseable)
|
|
451
|
+
val logger = new Logger
|
|
452
|
+
val loggerHandle = defer(logger.close())
|
|
453
|
+
|
|
454
|
+
// Use both resources
|
|
455
|
+
logger.log("Processing file: " + tempFile)
|
|
456
|
+
Files.write(tempFile, "data".getBytes())
|
|
457
|
+
|
|
458
|
+
// If needed, cancel the finalizer and clean up manually
|
|
459
|
+
val data = Files.readAllBytes(tempFile)
|
|
460
|
+
logger.log(s"Read ${data.length} bytes")
|
|
461
|
+
|
|
462
|
+
// loggerHandle.cancel() // Would prevent auto-cleanup
|
|
463
|
+
}
|
|
464
|
+
// When the scope exits: logger closes, then temp file is deleted (LIFO order)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
If the scope is already closed, `defer` is silently ignored (no-op). The finalizer is guaranteed to run in LIFO order with other finalizers when the scope closes.
|
|
468
|
+
|
|
469
|
+
### `Scope#isClosed` — Check If Closed
|
|
470
|
+
|
|
471
|
+
Returns whether this scope's finalizers have already run:
|
|
472
|
+
|
|
473
|
+
```scala
|
|
474
|
+
trait Scope {
|
|
475
|
+
def isClosed: Boolean
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Once `isClosed` returns `true`, subsequent calls to `allocate`, `open`, or `$` throw `IllegalStateException`. This is checked to prevent use-after-close bugs.
|
|
480
|
+
|
|
481
|
+
Here's a practical example—a resource manager that guards against using a closed scope:
|
|
482
|
+
|
|
483
|
+
```scala
|
|
484
|
+
import zio.blocks.scope._
|
|
485
|
+
|
|
486
|
+
final class Database extends AutoCloseable {
|
|
487
|
+
def query(sql: String): String = s"result: $sql"
|
|
488
|
+
def close(): Unit = println("db closed")
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// A service that holds and manages a scope
|
|
492
|
+
class DatabaseService {
|
|
493
|
+
private val serviceScope = Scope.global.open()
|
|
494
|
+
|
|
495
|
+
// Initialize database once at startup
|
|
496
|
+
private val db = {
|
|
497
|
+
try {
|
|
498
|
+
serviceScope.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
499
|
+
} catch {
|
|
500
|
+
case e: IllegalStateException =>
|
|
501
|
+
serviceScope.close().orThrow()
|
|
502
|
+
throw e
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
def isAvailable: Boolean = !serviceScope.scope.isClosed
|
|
507
|
+
|
|
508
|
+
def execute(query: String): Either[String, String] = {
|
|
509
|
+
if (serviceScope.scope.isClosed) {
|
|
510
|
+
Left("Database service has been shut down")
|
|
511
|
+
} else {
|
|
512
|
+
try {
|
|
513
|
+
Right(serviceScope.scope.scoped { scope =>
|
|
514
|
+
import scope._
|
|
515
|
+
val dbInChild = scope.lower(db)
|
|
516
|
+
$(dbInChild)(_.query(query))
|
|
517
|
+
})
|
|
518
|
+
} catch {
|
|
519
|
+
case e: IllegalStateException => Left(s"Service error: ${e.getMessage}")
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
def shutdown(): Unit = {
|
|
525
|
+
if (!serviceScope.scope.isClosed) {
|
|
526
|
+
serviceScope.close().orThrow()
|
|
527
|
+
println("Service shutdown complete")
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Usage
|
|
533
|
+
val service = new DatabaseService
|
|
534
|
+
println(s"Service available: ${service.isAvailable}")
|
|
535
|
+
|
|
536
|
+
val result1 = service.execute("SELECT 1")
|
|
537
|
+
println(s"Query result: $result1")
|
|
538
|
+
|
|
539
|
+
service.shutdown()
|
|
540
|
+
|
|
541
|
+
// Attempting to use after shutdown is now safe
|
|
542
|
+
val result2 = service.execute("SELECT 2")
|
|
543
|
+
println(s"Query after shutdown: $result2")
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
`Scope.global` returns `false` until JVM shutdown. Child scopes created with `scoped { }` are closed when the block exits, while those created with `open()` remain open until you call `close()`.
|
|
547
|
+
|
|
548
|
+
### `Scope#isOwner` — Check Thread Ownership
|
|
549
|
+
|
|
550
|
+
Returns whether the calling thread is the owner of this scope:
|
|
551
|
+
|
|
552
|
+
```scala
|
|
553
|
+
trait Scope {
|
|
554
|
+
def isOwner: Boolean
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Ownership is used to detect cross-thread scope misuse. Thread ownership rules:
|
|
559
|
+
- `Scope.global`: always returns `true` (any thread may use it)
|
|
560
|
+
- Child scopes created via `scoped { }`: returns `true` only on the thread that entered the block
|
|
561
|
+
- Child scopes created via `open()`: always returns `true` (unowned, usable cross-thread)
|
|
562
|
+
|
|
563
|
+
Calling `scoped { }` on a scope you don't own throws `IllegalStateException` at runtime:
|
|
564
|
+
|
|
565
|
+
```scala
|
|
566
|
+
import zio.blocks.scope._
|
|
567
|
+
|
|
568
|
+
Scope.global.scoped { scope =>
|
|
569
|
+
// On the thread that entered scoped, isOwner is true
|
|
570
|
+
assert(scope.isOwner)
|
|
571
|
+
|
|
572
|
+
// On a different thread, isOwner returns false
|
|
573
|
+
val thread = new Thread {
|
|
574
|
+
override def run(): Unit = {
|
|
575
|
+
assert(!scope.isOwner)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
thread.start()
|
|
579
|
+
thread.join()
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Example 1: Thread-owned scope (scoped) — fails on worker thread**
|
|
584
|
+
|
|
585
|
+
Thread-owned scopes cannot be used to create child scopes from a different thread:
|
|
586
|
+
|
|
587
|
+
```scala
|
|
588
|
+
import zio.blocks.scope._
|
|
589
|
+
import java.util.concurrent._
|
|
590
|
+
|
|
591
|
+
final class Database extends AutoCloseable {
|
|
592
|
+
def query(sql: String): String = s"result: $sql"
|
|
593
|
+
def close(): Unit = println("db closed")
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
val executor = Executors.newFixedThreadPool(1)
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
Scope.global.scoped { scope =>
|
|
600
|
+
import scope._
|
|
601
|
+
val db = allocate(new Database)
|
|
602
|
+
|
|
603
|
+
// Try to create a child scope from a different thread
|
|
604
|
+
val future = executor.submit { () =>
|
|
605
|
+
try {
|
|
606
|
+
scope.scoped { childScope =>
|
|
607
|
+
import childScope._
|
|
608
|
+
val dbInChild = childScope.lower(db)
|
|
609
|
+
$(dbInChild)(_.query("SELECT 1"))
|
|
610
|
+
}
|
|
611
|
+
} catch {
|
|
612
|
+
case e: IllegalStateException => s"Error: ${e.getMessage}"
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
println(future.get())
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
executor.shutdown()
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Example Output:
|
|
622
|
+
// Error: Cannot create child scope: current thread 'pool-1-thread-1' does not own this scope (owner: 'main')
|
|
623
|
+
// db closed
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Example 2: Unowned scope (open) — works across threads**
|
|
627
|
+
|
|
628
|
+
Open scopes are unowned and usable from any thread:
|
|
629
|
+
|
|
630
|
+
```scala
|
|
631
|
+
import zio.blocks.scope._
|
|
632
|
+
import java.util.concurrent._
|
|
633
|
+
|
|
634
|
+
final class Database extends AutoCloseable {
|
|
635
|
+
def query(sql: String): String = s"result: $sql"
|
|
636
|
+
def close(): Unit = println("db closed")
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
val executor = Executors.newFixedThreadPool(1)
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
val poolScope = Scope.global.open()
|
|
643
|
+
val db = poolScope.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
644
|
+
|
|
645
|
+
// Use the resource from a worker thread
|
|
646
|
+
val future = executor.submit { () =>
|
|
647
|
+
poolScope.scope.scoped { scope =>
|
|
648
|
+
import scope._
|
|
649
|
+
val dbInChild = scope.lower(db)
|
|
650
|
+
$(dbInChild)(_.query("SELECT 1"))
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
println(future.get())
|
|
654
|
+
|
|
655
|
+
poolScope.close().orThrow()
|
|
656
|
+
} finally {
|
|
657
|
+
executor.shutdown()
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Output:
|
|
661
|
+
// result: SELECT 1
|
|
662
|
+
// db closed
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
The key difference: `scoped { }` creates **owned** scopes (tied to the entering thread), while `open()` creates **unowned** scopes (usable from any thread). Choose based on whether your resources need to cross thread boundaries.
|
|
666
|
+
|
|
667
|
+
## Returning Unscoped Data from a Scope
|
|
668
|
+
|
|
669
|
+
A `scoped { }` block can only return values that have an `Unscoped` instance—that is, pure data types with no embedded resources or cleanup logic. This restriction prevents resource leaks: you cannot accidentally return a resource that would be cleaned up before you could use it.
|
|
670
|
+
|
|
671
|
+
For built-in types like `String`, `Int`, or `List[String]`, `Unscoped` instances exist automatically. However, when your custom type contains a field whose type has no predefined `Unscoped` instance (such as `java.util.Date`, a legacy Java type that is pure data but not automatically recognized), automatic derivation won't work. In such cases, you must provide an `Unscoped` instance explicitly, asserting that your type holds only pure data:
|
|
672
|
+
|
|
673
|
+
```scala
|
|
674
|
+
import java.util.Date
|
|
675
|
+
import zio.blocks.scope._
|
|
676
|
+
import zio.blocks.scope.Unscoped
|
|
677
|
+
|
|
678
|
+
// java.util.Date has no predefined Unscoped instance, so Unscoped.derived
|
|
679
|
+
// won't work here — we must provide the instance explicitly
|
|
680
|
+
case class QueryResult(rows: List[String], count: Int, executedAt: Date)
|
|
681
|
+
|
|
682
|
+
object QueryResult {
|
|
683
|
+
implicit val unscoped: Unscoped[QueryResult] = new Unscoped[QueryResult] {}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
Scope.global.scoped { scope =>
|
|
687
|
+
import scope._
|
|
688
|
+
// ... acquire database ...
|
|
689
|
+
QueryResult(List("a", "b"), 2, new Date()) // Returns safely
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Only add `Unscoped` for pure data types.** Never add it for types that hold resources (connections, streams, file handles). If you encounter the compile error [`No given instance of Unscoped[MyType]`](#no-given-instance-of-unscopedmytype--escaping-a-scope), see the compile errors section for how to fix it. For the complete API and examples, see the [Unscoped reference](./unscoped.md).
|
|
694
|
+
|
|
695
|
+
## Lexical vs Explicit Scopes
|
|
696
|
+
|
|
697
|
+
The Scope API provides two primary patterns for managing resource lifetimes. Choose `Scope#scoped` if you can write both the code that acquires and the code that releases the resource in the same expression; choose `Scope#open()` if the resource lifetime must outlive the function that creates it. Most user code should prefer `scoped` for automatic cleanup and thread safety—use `open()` only when you need manual lifetime control, such as in connection pools or DI containers.
|
|
698
|
+
|
|
699
|
+
### Lexical Scopes with `Scope#scoped`
|
|
700
|
+
|
|
701
|
+
Use `Scope#scoped` when the resource lifetime is lexically bounded. Lexical scopes are thread-owned by default, preventing accidental cross-thread access and providing automatic cleanup even on exception. This makes them safe and composable: you can nest `scoped` blocks to express hierarchical resource dependencies, and the code structure naturally matches the resource lifetime.
|
|
702
|
+
|
|
703
|
+
Here's a basic pattern showing how to acquire and use a resource within a single scope:
|
|
704
|
+
|
|
705
|
+
```scala
|
|
706
|
+
import zio.blocks.scope._
|
|
707
|
+
|
|
708
|
+
final class Database extends AutoCloseable {
|
|
709
|
+
def query(sql: String): String = s"result: $sql"
|
|
710
|
+
def close(): Unit = println("db closed")
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
Scope.global.scoped { scope =>
|
|
714
|
+
import scope._
|
|
715
|
+
|
|
716
|
+
val db = allocate(new Database)
|
|
717
|
+
$(db)(_.query("SELECT * FROM users"))
|
|
718
|
+
// db closes when scope exits
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
The only trade-off is that you must know the scope's lifetime upfront and cannot easily extend resource lifetime across function boundaries without returning resources themselves. For details on different allocation approaches (with `Resource.fromAutoCloseable()` or directly with `AutoCloseable`), see [Core Operations — allocate](#scopeallocate--acquire-a-resource).
|
|
723
|
+
|
|
724
|
+
#### Nesting for hierarchical resources
|
|
725
|
+
|
|
726
|
+
When resources depend on each other, nest `scoped` blocks to express the hierarchy. Parent scopes always outlive their children, so you can safely use parent resources in child scopes:
|
|
727
|
+
|
|
728
|
+
```scala
|
|
729
|
+
import zio.blocks.scope._
|
|
730
|
+
|
|
731
|
+
final class Database extends AutoCloseable {
|
|
732
|
+
def query(sql: String): String = s"result: $sql"
|
|
733
|
+
def close(): Unit = println("db closed")
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
final class Connection extends AutoCloseable {
|
|
737
|
+
def close(): Unit = println("connection closed")
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
Scope.global.scoped { outerScope =>
|
|
741
|
+
import outerScope._
|
|
742
|
+
|
|
743
|
+
val db = allocate(new Database)
|
|
744
|
+
|
|
745
|
+
// Child scope for connection
|
|
746
|
+
outerScope.scoped { innerScope =>
|
|
747
|
+
import innerScope._
|
|
748
|
+
|
|
749
|
+
val conn = allocate(new Connection)
|
|
750
|
+
// Use conn and db here
|
|
751
|
+
// conn closes first (LIFO)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Can still use db here
|
|
755
|
+
// db closes when outerScope exits
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Explicit Scopes with `Scope#open`
|
|
760
|
+
|
|
761
|
+
Use `Scope#open()` when the resource lifetime is not lexically bounded. Open scopes are unowned (usable from any thread), which makes them suitable for patterns like connection pools, resource caches, and DI containers where resources must outlive the function that creates them. This pattern gives you full control over resource acquisition and release timing.
|
|
762
|
+
|
|
763
|
+
The trade-off is that you accept full responsibility for cleanup: forgetting to call `close()` leaves resources open, and any exception during cleanup must be explicitly handled. Here's the key pattern—returning an `OpenScope` handle from a function:
|
|
764
|
+
|
|
765
|
+
```scala
|
|
766
|
+
import zio.blocks.scope._
|
|
767
|
+
|
|
768
|
+
final class Database extends AutoCloseable {
|
|
769
|
+
def query(sql: String): String = s"result: $sql"
|
|
770
|
+
def close(): Unit = println("db closed")
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
def acquireDatabase(): Scope.OpenScope = {
|
|
774
|
+
val os = Scope.global.open()
|
|
775
|
+
val _ = os.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
776
|
+
os
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
val handle = acquireDatabase()
|
|
780
|
+
try {
|
|
781
|
+
// Use handle.scope as needed
|
|
782
|
+
()
|
|
783
|
+
} finally {
|
|
784
|
+
handle.close().orThrow()
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
## Dependency Injection
|
|
789
|
+
|
|
790
|
+
`Scope` integrates seamlessly with `Wire` and `Resource.from` for automatic dependency injection. `Wire` describes a recipe for constructing a service and its dependencies, while `Scope` manages the resource lifetime. Together they eliminate manual dependency passing and ensure proper cleanup in LIFO order.
|
|
791
|
+
|
|
792
|
+
Here's an example using `Wire` and `Resource.from` within a scope:
|
|
793
|
+
|
|
794
|
+
```scala
|
|
795
|
+
import zio.blocks.scope._
|
|
796
|
+
|
|
797
|
+
final class Config(val dbUrl: String)
|
|
798
|
+
|
|
799
|
+
final class Database(config: Config) extends AutoCloseable {
|
|
800
|
+
def query(sql: String): String = s"result: $sql"
|
|
801
|
+
def close(): Unit = println(s"db closed (${config.dbUrl})")
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
final class UserService(db: Database) {
|
|
805
|
+
def getUser(id: Int): String = s"user $id from ${db.query("SELECT * FROM users")}"
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
final class App(service: UserService) {
|
|
809
|
+
def run(): Unit = println(service.getUser(1))
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Wire describes the dependency graph: App -> UserService -> Database -> Config
|
|
813
|
+
// Resource.from uses the Wire to automatically construct the entire graph
|
|
814
|
+
Scope.global.scoped { scope =>
|
|
815
|
+
import scope._
|
|
816
|
+
val config = Config("jdbc:postgres://localhost/db")
|
|
817
|
+
val app = allocate(Resource.from[App](
|
|
818
|
+
Wire(config)
|
|
819
|
+
))
|
|
820
|
+
$(app)(_.run())
|
|
821
|
+
// All resources (Database, App) clean up automatically in reverse order
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
For more details on `Wire` sharing strategies, resource composition, and advanced DI patterns, see the [Wire reference](./wire.md) and [Resource reference](./resource.md).
|
|
826
|
+
|
|
827
|
+
## Best Practices
|
|
828
|
+
|
|
829
|
+
### Entry point pattern — use `Scope.global.scoped` at the top level
|
|
830
|
+
|
|
831
|
+
Wrap your entire application's resource acquisition in a single lexical scope:
|
|
832
|
+
|
|
833
|
+
```scala
|
|
834
|
+
import zio.blocks.scope._
|
|
835
|
+
|
|
836
|
+
object MyApp {
|
|
837
|
+
def main(args: Array[String]): Unit = {
|
|
838
|
+
Scope.global.scoped { scope =>
|
|
839
|
+
import scope._
|
|
840
|
+
// All resources acquired here
|
|
841
|
+
// Automatic cleanup when main exits
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
This is your "outer boundary" for resource safety. Everything inside is protected.
|
|
848
|
+
|
|
849
|
+
### Composition — use `Resource` builders before allocation
|
|
850
|
+
|
|
851
|
+
Build resource acquisition/release logic outside the scope, then `Scope#allocate` once inside. This separates *construction* (how) from *allocation* (when), making code testable and reusable.
|
|
852
|
+
|
|
853
|
+
**Key combinators:**
|
|
854
|
+
|
|
855
|
+
- **`.map(f)`** — Transform a resource's value
|
|
856
|
+
- **`.flatMap(f)`** — Chain resources where the second depends on the first
|
|
857
|
+
- **`.zip(other)`** — Combine two independent resources
|
|
858
|
+
|
|
859
|
+
**Example: Using `.zip()` to combine independent resources:**
|
|
860
|
+
|
|
861
|
+
```scala
|
|
862
|
+
import zio.blocks.scope._
|
|
863
|
+
|
|
864
|
+
final class Database extends AutoCloseable {
|
|
865
|
+
def query(sql: String): String = s"result: $sql"
|
|
866
|
+
def close(): Unit = println("db closed")
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
final class Cache extends AutoCloseable {
|
|
870
|
+
def get(key: String): Option[String] = None
|
|
871
|
+
def close(): Unit = println("cache closed")
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Compose outside scope — reusable across multiple applications
|
|
875
|
+
val dbResource = Resource.fromAutoCloseable(new Database)
|
|
876
|
+
val cacheResource = Resource.fromAutoCloseable(new Cache)
|
|
877
|
+
val appResources = dbResource.zip(cacheResource)
|
|
878
|
+
|
|
879
|
+
// Allocate once inside scope
|
|
880
|
+
Scope.global.scoped { scope =>
|
|
881
|
+
import scope._
|
|
882
|
+
val (db, cache) = allocate(appResources)
|
|
883
|
+
// Use both — cleanup happens in LIFO order (cache first, then db)
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
**Example: Using `.flatMap()` for dependent resources:**
|
|
888
|
+
|
|
889
|
+
```scala
|
|
890
|
+
import zio.blocks.scope._
|
|
891
|
+
|
|
892
|
+
final class Config(val host: String, val port: Int)
|
|
893
|
+
|
|
894
|
+
final class Database(val config: Config) extends AutoCloseable {
|
|
895
|
+
def query(sql: String): String = s"result: $sql"
|
|
896
|
+
def close(): Unit = println("db closed")
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Config resource must be acquired first, then database
|
|
900
|
+
val configResource = Resource(new Config("localhost", 5432))
|
|
901
|
+
val dbResource = configResource.flatMap { cfg =>
|
|
902
|
+
Resource.fromAutoCloseable(new Database(cfg))
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Allocate the dependent chain
|
|
906
|
+
Scope.global.scoped { scope =>
|
|
907
|
+
import scope._
|
|
908
|
+
val db = allocate(dbResource)
|
|
909
|
+
// db was initialized with config; cleanup happens in reverse order
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
To learn more about building and composing resources, see the [Resource reference](./resource.md).
|
|
914
|
+
|
|
915
|
+
## Runtime Errors
|
|
916
|
+
|
|
917
|
+
Runtime errors occur when you violate scope rules at runtime—typically by accessing resources after the scope has already cleaned them up, or by mixing scopes across threads.
|
|
918
|
+
|
|
919
|
+
### `IllegalStateException` — accessing a closed scope
|
|
920
|
+
|
|
921
|
+
This error occurs when you attempt to acquire resources (via `allocate`, `open`, or the `$` operator) on a scope that has already closed. Every `scoped { }` block cleans up its resources as soon as the block exits, so any attempt to use the scope after that point fails.
|
|
922
|
+
|
|
923
|
+
The error message is typically:
|
|
924
|
+
```
|
|
925
|
+
Cannot acquire resource: scope has already been closed. ...
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
This usually happens when you:
|
|
929
|
+
- Store a scope in a field or closure and try to use it later after the enclosing `scoped` block has exited
|
|
930
|
+
- Accidentally pass a scope to an async operation that runs after cleanup
|
|
931
|
+
|
|
932
|
+
To avoid this, keep the scope's lifetime clear: allocate resources, use them, then let them clean up when the scope exits. If you need resources to survive longer, use `Scope.global.open()` to get a handle you can manage manually. You can also check `scope.isClosed` before attempting operations as a defensive check.
|
|
933
|
+
|
|
934
|
+
### `IllegalStateException` — cross-thread scope usage
|
|
935
|
+
|
|
936
|
+
Scopes are thread-owned by default. When you create a child scope using `scoped { }`, it's owned by the thread that created it. If you try to access that scope (allocate resources, create child scopes) from a different thread, the scope will reject it.
|
|
937
|
+
|
|
938
|
+
The error message is typically:
|
|
939
|
+
```
|
|
940
|
+
Cannot create child scope: current thread '...' does not own this scope (owner: '...')
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
This happens when you try to:
|
|
944
|
+
- Pass a scope to another thread and use it there
|
|
945
|
+
- Share a scope across multiple threads that call `scoped { }` on it
|
|
946
|
+
|
|
947
|
+
To fix this, use thread-unowned scopes when you need to share across threads. Instead of `scope.scoped { }` (which creates a thread-owned child), use `Scope.global.open()` or the scope's `open()` method directly to get an `OpenScope` handle. These unowned scopes can be safely passed and used from any thread, though you're responsible for manual cleanup via the returned handle.
|
|
948
|
+
|
|
949
|
+
## Compile Errors
|
|
950
|
+
|
|
951
|
+
The following compile errors occur when `Scope` type rules are violated. All examples below use this scoping pattern (see [Quickstart](#quickstart) for full context):
|
|
952
|
+
|
|
953
|
+
```scala
|
|
954
|
+
Scope.global.scoped { scope =>
|
|
955
|
+
import scope._
|
|
956
|
+
val db: $[Database] = allocate(new Database)
|
|
957
|
+
// ... usage or error ...
|
|
958
|
+
}
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
### `No given instance of Unscoped[MyType]` — escaping a scope
|
|
962
|
+
|
|
963
|
+
This error occurs when the type you return from a `scoped { }` block has no `Unscoped` instance. See [Scope boundary enforcement via `Unscoped`](#safety-model) for the full explanation of why this restriction exists.
|
|
964
|
+
|
|
965
|
+
If you write:
|
|
966
|
+
```scala
|
|
967
|
+
db // ERROR: No given instance of Unscoped[$[Database]]
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
The compiler rejects this because `db` is a scoped resource (type `$[Database]`), not safe data. Even though you're inside the `scoped` block, the type system prevents you from returning it because it would be useless outside the scope (the resource would already be cleaned up).
|
|
971
|
+
|
|
972
|
+
To fix this, you have two options:
|
|
973
|
+
|
|
974
|
+
**Option 1: Extract data from the resource before returning**
|
|
975
|
+
|
|
976
|
+
Call a method on the resource to get pure data (strings, numbers, etc.) that are naturally `Unscoped`:
|
|
977
|
+
|
|
978
|
+
```scala
|
|
979
|
+
$(db)(_.query("data")) // ✓ Correct: Returns String, which is Unscoped
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
The `String` returned by `query()` is pure data with no cleanup logic, so it can safely escape the scope.
|
|
983
|
+
|
|
984
|
+
**Option 2: Implement `Unscoped` for your custom types**
|
|
985
|
+
|
|
986
|
+
If you create custom types that hold only pure data, add an `Unscoped` instance so they can escape scopes. See the [Unscoped reference](./unscoped.md) for details and examples.
|
|
987
|
+
|
|
988
|
+
### `Scoped values may only be used as a method receiver` — macro violation
|
|
989
|
+
|
|
990
|
+
**What this means:** A scoped value (the parameter inside a `$(value)` lambda) can only be used as the receiver of a method call—the object you call `.method()` on. It cannot be passed to other functions, stored in variables, or captured in nested lambdas. This restriction prevents the resource from leaking out of its scope and being used after cleanup.
|
|
991
|
+
|
|
992
|
+
**When you hit this error:**
|
|
993
|
+
|
|
994
|
+
The macro detects several violations:
|
|
995
|
+
|
|
996
|
+
- **Passing as an argument:**
|
|
997
|
+
```scala
|
|
998
|
+
$(db)(d => store(d)) // ERROR: cannot pass scoped value to a function
|
|
999
|
+
$(db)(d => println(d)) // ERROR: cannot pass to println
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
- **Storing in a variable:**
|
|
1003
|
+
```scala
|
|
1004
|
+
$(db)(d => {
|
|
1005
|
+
val conn = d // ERROR: cannot bind to val/var
|
|
1006
|
+
conn.query()
|
|
1007
|
+
})
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
- **Returning the value itself:**
|
|
1011
|
+
```scala
|
|
1012
|
+
$(db)(d => d) // ERROR: must call a method, not return bare reference
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
- **Capturing in a nested lambda or closure:**
|
|
1016
|
+
```scala
|
|
1017
|
+
$(db)(d =>
|
|
1018
|
+
() => d.query() // ERROR: cannot capture in nested lambda
|
|
1019
|
+
)
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
**What works — calling methods on the parameter:**
|
|
1023
|
+
|
|
1024
|
+
```scala
|
|
1025
|
+
$(db)(d => d.query("SELECT * FROM users")) // ✓ Method call on receiver
|
|
1026
|
+
$(db)(_.query("data")) // ✓ Using underscore shorthand
|
|
1027
|
+
$(db)(d => d.execute(statement).rows) // ✓ Chain method calls
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
If you need to transform or extract data from a resource before using it elsewhere, call a method to extract what you need:
|
|
1031
|
+
|
|
1032
|
+
```scala
|
|
1033
|
+
$(db)(d => d.query("SELECT COUNT(*)")) // ✓ Returns String (pure data)
|
|
1034
|
+
// The returned String can now be passed to other functions
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
**Why this restriction exists:** Scoped values are bound to a specific cleanup phase. Allowing them to escape (via arguments or closures) would let them be used after cleanup, causing crashes or data corruption. By restricting usage to method calls only, the macro ensures the resource never leaves its scope.
|
|
1038
|
+
|
|
1039
|
+
## Integration
|
|
1040
|
+
|
|
1041
|
+
Scope integrates seamlessly with ZIO Blocks' other data types for building complex resource management systems.
|
|
1042
|
+
|
|
1043
|
+
### Resource
|
|
1044
|
+
|
|
1045
|
+
`Scope` manages the lifecycle of `Resource[A]` values through the `allocate` method. A `Resource` describes how to acquire and clean up a value; `Scope` executes that plan and tracks finalizers. For comprehensive information on constructing, composing, and sharing resources, see the [Resource reference](./resource.md).
|
|
1046
|
+
|
|
1047
|
+
Key integration points:
|
|
1048
|
+
- Use `Resource[A].allocate` to acquire within a scope
|
|
1049
|
+
- Compose resources with `flatMap`, `andThen`, and other combinators before allocating
|
|
1050
|
+
- Use `Resource.shared` for multiple-use resources within a scope
|
|
1051
|
+
|
|
1052
|
+
### Finalizer
|
|
1053
|
+
|
|
1054
|
+
`Scope` extends `Finalizer`, the interface for registering cleanup actions. The `defer` method registers a finalizer that runs when the scope closes. For information on the `DeferHandle` and cancellation, see the [Finalizer reference](./finalizer.md).
|
|
1055
|
+
|
|
1056
|
+
Key integration points:
|
|
1057
|
+
- `scope.defer(f)` registers a cleanup action
|
|
1058
|
+
- `DeferHandle.cancel()` prevents a finalizer from running
|
|
1059
|
+
- Finalizers run in LIFO order regardless of whether registered via `allocate` or `defer`
|
|
1060
|
+
|
|
1061
|
+
### Wire + Resource.from
|
|
1062
|
+
|
|
1063
|
+
For dependency injection patterns, Scope works naturally with [`Wire`](./wire.md) and [`Resource.from`](./resource.md) to build layered service architectures. Allocate resources in a parent scope, then use `lower` to pass them to child scopes as needed.
|
|
1064
|
+
|
|
1065
|
+
## Running the Examples
|
|
1066
|
+
|
|
1067
|
+
All code from this guide is available as runnable examples in the `scope-examples` module.
|
|
1068
|
+
|
|
1069
|
+
**1. Clone the repository and navigate to the project:**
|
|
1070
|
+
|
|
1071
|
+
```bash
|
|
1072
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
1073
|
+
cd zio-blocks
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**2. Run individual examples with sbt:**
|
|
1077
|
+
|
|
1078
|
+
### Basic Database Connection Lifecycle Management
|
|
1079
|
+
|
|
1080
|
+
This example demonstrates how to allocate a database connection within a scope, ensure proper cleanup, and handle the connection's lifecycle safely.
|
|
1081
|
+
|
|
1082
|
+
```scala title="scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala"
|
|
1083
|
+
/*
|
|
1084
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1085
|
+
*
|
|
1086
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1087
|
+
* you may not use this file except in compliance with the License.
|
|
1088
|
+
* You may obtain a copy of the License at
|
|
1089
|
+
*
|
|
1090
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1091
|
+
*
|
|
1092
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1093
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1094
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1095
|
+
* See the License for the specific language governing permissions and
|
|
1096
|
+
* limitations under the License.
|
|
1097
|
+
*/
|
|
1098
|
+
|
|
1099
|
+
package scope.examples
|
|
1100
|
+
|
|
1101
|
+
import zio.blocks.scope._
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Configuration for database connection.
|
|
1105
|
+
*
|
|
1106
|
+
* @param host
|
|
1107
|
+
* the database server hostname
|
|
1108
|
+
* @param port
|
|
1109
|
+
* the database server port
|
|
1110
|
+
* @param database
|
|
1111
|
+
* the database name to connect to
|
|
1112
|
+
*/
|
|
1113
|
+
final case class DbConfig(host: String, port: Int, database: String) {
|
|
1114
|
+
def connectionUrl: String = s"jdbc:postgresql://$host:$port/$database"
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Represents the result of a database query.
|
|
1119
|
+
*
|
|
1120
|
+
* @param rows
|
|
1121
|
+
* the result set as a list of row maps
|
|
1122
|
+
*/
|
|
1123
|
+
final case class QueryResult(rows: List[Map[String, String]]) {
|
|
1124
|
+
def isEmpty: Boolean = rows.isEmpty
|
|
1125
|
+
def size: Int = rows.size
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Simulates a database connection with lifecycle management.
|
|
1130
|
+
*
|
|
1131
|
+
* This class demonstrates how AutoCloseable resources integrate with ZIO Blocks
|
|
1132
|
+
* Scope. When allocated via `allocate(Resource(...))`, the `close()` method is
|
|
1133
|
+
* automatically registered as a finalizer.
|
|
1134
|
+
*
|
|
1135
|
+
* @param config
|
|
1136
|
+
* the database configuration
|
|
1137
|
+
*/
|
|
1138
|
+
final class Database(config: DbConfig) extends AutoCloseable {
|
|
1139
|
+
private var connected = false
|
|
1140
|
+
|
|
1141
|
+
def connect(): Unit = {
|
|
1142
|
+
println(s"[Database] Connecting to ${config.connectionUrl}...")
|
|
1143
|
+
connected = true
|
|
1144
|
+
println(s"[Database] Connected successfully")
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
def query(sql: String): QueryResult = {
|
|
1148
|
+
require(connected, "Database not connected")
|
|
1149
|
+
println(s"[Database] Executing: $sql")
|
|
1150
|
+
sql match {
|
|
1151
|
+
case s if s.contains("users") =>
|
|
1152
|
+
QueryResult(
|
|
1153
|
+
List(
|
|
1154
|
+
Map("id" -> "1", "name" -> "Alice"),
|
|
1155
|
+
Map("id" -> "2", "name" -> "Bob")
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
case s if s.contains("orders") =>
|
|
1159
|
+
QueryResult(
|
|
1160
|
+
List(
|
|
1161
|
+
Map("order_id" -> "101", "user_id" -> "1", "total" -> "99.99"),
|
|
1162
|
+
Map("order_id" -> "102", "user_id" -> "2", "total" -> "149.50")
|
|
1163
|
+
)
|
|
1164
|
+
)
|
|
1165
|
+
case _ =>
|
|
1166
|
+
QueryResult(List(Map("result" -> "OK")))
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
override def close(): Unit = {
|
|
1171
|
+
println(s"[Database] Closing connection to ${config.connectionUrl}")
|
|
1172
|
+
connected = false
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Demonstrates basic resource lifecycle management with ZIO Blocks Scope.
|
|
1178
|
+
*
|
|
1179
|
+
* This example shows:
|
|
1180
|
+
* - Allocating an AutoCloseable resource with automatic cleanup
|
|
1181
|
+
* - Using `$(value)(f)` to access scoped values and execute queries
|
|
1182
|
+
* - LIFO finalizer ordering (last allocated = first closed)
|
|
1183
|
+
*
|
|
1184
|
+
* When the scope exits, all registered finalizers run in reverse order,
|
|
1185
|
+
* ensuring proper cleanup even if exceptions occur.
|
|
1186
|
+
*/
|
|
1187
|
+
@main def runDatabaseExample(): Unit = {
|
|
1188
|
+
println("=== Database Connection Example ===\n")
|
|
1189
|
+
|
|
1190
|
+
val config = DbConfig("localhost", 5432, "myapp")
|
|
1191
|
+
|
|
1192
|
+
Scope.global.scoped { scope =>
|
|
1193
|
+
import scope._
|
|
1194
|
+
println("[Scope] Entering scoped region\n")
|
|
1195
|
+
|
|
1196
|
+
// Allocate the database resource. Because Database extends AutoCloseable,
|
|
1197
|
+
// its close() method is automatically registered as a finalizer.
|
|
1198
|
+
val db: $[Database] = allocate(Resource {
|
|
1199
|
+
val database = new Database(config)
|
|
1200
|
+
database.connect()
|
|
1201
|
+
database
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
// Use $(value)(f) to access the scoped value and execute queries.
|
|
1205
|
+
$(db) { database =>
|
|
1206
|
+
val users = database.query("SELECT * FROM users")
|
|
1207
|
+
println(s"[Result] Found ${users.size} users: ${users.rows.map(_("name")).mkString(", ")}\n")
|
|
1208
|
+
|
|
1209
|
+
val orders = database.query("SELECT * FROM orders WHERE status = 'pending'")
|
|
1210
|
+
println(s"[Result] Found ${orders.size} orders\n")
|
|
1211
|
+
|
|
1212
|
+
val health = database.query("SELECT 1 AS health_check")
|
|
1213
|
+
println(s"[Result] Health check: ${health.rows.head("result")}\n")
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
println("[Scope] Exiting scoped region - finalizers will run in LIFO order")
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
println("\n=== Example Complete ===")
|
|
1220
|
+
}
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala))
|
|
1224
|
+
|
|
1225
|
+
```bash
|
|
1226
|
+
sbt "scope-examples/runMain runDatabaseExample"
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### Managing a Connection Pool with Multiple Allocations
|
|
1230
|
+
|
|
1231
|
+
This example demonstrates allocating multiple connections from a pool within the same scope and ensuring all are cleaned up correctly.
|
|
1232
|
+
|
|
1233
|
+
```scala title="scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala"
|
|
1234
|
+
/*
|
|
1235
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1236
|
+
*
|
|
1237
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1238
|
+
* you may not use this file except in compliance with the License.
|
|
1239
|
+
* You may obtain a copy of the License at
|
|
1240
|
+
*
|
|
1241
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1242
|
+
*
|
|
1243
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1244
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1245
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1246
|
+
* See the License for the specific language governing permissions and
|
|
1247
|
+
* limitations under the License.
|
|
1248
|
+
*/
|
|
1249
|
+
|
|
1250
|
+
package scope.examples
|
|
1251
|
+
|
|
1252
|
+
import zio.blocks.scope._
|
|
1253
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Demonstrates `Resource.Shared` with reference counting and nested resource
|
|
1257
|
+
* acquisition.
|
|
1258
|
+
*
|
|
1259
|
+
* This example shows a realistic connection pool pattern where:
|
|
1260
|
+
* - The pool itself is a shared resource (created once, ref-counted)
|
|
1261
|
+
* - Individual connections are resources that must be allocated in a scope
|
|
1262
|
+
* - `pool.acquire` returns `Resource[PooledConnection]`, forcing proper
|
|
1263
|
+
* scoping
|
|
1264
|
+
*
|
|
1265
|
+
* This pattern is common for database pools, HTTP client pools, and thread
|
|
1266
|
+
* pools.
|
|
1267
|
+
*/
|
|
1268
|
+
|
|
1269
|
+
/** Configuration for the connection pool. */
|
|
1270
|
+
final case class PoolConfig(maxConnections: Int, timeout: Long)
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* A connection retrieved from the pool.
|
|
1274
|
+
*
|
|
1275
|
+
* Connections are resources - they must be released back to the pool when done.
|
|
1276
|
+
* This is enforced by making `acquire` return a `Resource[PooledConnection]`.
|
|
1277
|
+
*/
|
|
1278
|
+
final class PooledConnection(val id: Int, pool: ConnectionPool) extends AutoCloseable {
|
|
1279
|
+
println(s" [Conn#$id] Acquired from pool")
|
|
1280
|
+
|
|
1281
|
+
def execute(sql: String): String = {
|
|
1282
|
+
println(s" [Conn#$id] Executing: $sql")
|
|
1283
|
+
s"Result from connection $id"
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
override def close(): Unit =
|
|
1287
|
+
pool.release(this)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* A connection pool that manages pooled connections.
|
|
1292
|
+
*
|
|
1293
|
+
* Key design: `acquire` returns `Resource[PooledConnection]`, not a raw
|
|
1294
|
+
* connection. This forces callers to allocate the connection in a scope,
|
|
1295
|
+
* ensuring proper release even if exceptions occur.
|
|
1296
|
+
*/
|
|
1297
|
+
final class ConnectionPool(config: PoolConfig) extends AutoCloseable {
|
|
1298
|
+
private val nextId = new AtomicInteger(0)
|
|
1299
|
+
private val active = new AtomicInteger(0)
|
|
1300
|
+
private val _closed = new AtomicInteger(0)
|
|
1301
|
+
|
|
1302
|
+
println(s" [Pool] Created with max ${config.maxConnections} connections")
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Acquires a connection from the pool.
|
|
1306
|
+
*
|
|
1307
|
+
* Returns a `Resource[PooledConnection]` that must be allocated in a scope.
|
|
1308
|
+
* The connection is automatically released when the scope exits.
|
|
1309
|
+
*/
|
|
1310
|
+
def acquire: Resource[PooledConnection] = Resource.acquireRelease {
|
|
1311
|
+
if (_closed.get() > 0) throw new IllegalStateException("Pool is closed")
|
|
1312
|
+
if (active.get() >= config.maxConnections)
|
|
1313
|
+
throw new IllegalStateException(s"Pool exhausted (max: ${config.maxConnections})")
|
|
1314
|
+
|
|
1315
|
+
val id = nextId.incrementAndGet()
|
|
1316
|
+
val conn = new PooledConnection(id, this)
|
|
1317
|
+
active.incrementAndGet()
|
|
1318
|
+
println(s" [Pool] Active connections: ${active.get()}/${config.maxConnections}")
|
|
1319
|
+
conn
|
|
1320
|
+
} { conn =>
|
|
1321
|
+
conn.close()
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
private[examples] def release(conn: PooledConnection): Unit = {
|
|
1325
|
+
val count = active.decrementAndGet()
|
|
1326
|
+
println(s" [Conn#${conn.id}] Released back to pool (active: $count)")
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
def activeConnections: Int = active.get()
|
|
1330
|
+
|
|
1331
|
+
override def close(): Unit =
|
|
1332
|
+
if (_closed.compareAndSet(0, 1)) {
|
|
1333
|
+
println(s" [Pool] *** POOL CLOSED *** (served ${nextId.get()} total connections)")
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
@main def connectionPoolExample(): Unit = {
|
|
1338
|
+
println("=== Connection Pool with Resource-based Acquire ===\n")
|
|
1339
|
+
|
|
1340
|
+
val poolConfig = PoolConfig(maxConnections = 3, timeout = 5000L)
|
|
1341
|
+
|
|
1342
|
+
val poolResource: Resource[ConnectionPool] =
|
|
1343
|
+
Resource.fromAutoCloseable(new ConnectionPool(poolConfig))
|
|
1344
|
+
|
|
1345
|
+
Scope.global.scoped { appScope =>
|
|
1346
|
+
import appScope._
|
|
1347
|
+
println("[App] Allocating pool\n")
|
|
1348
|
+
val pool: $[ConnectionPool] = poolResource.allocate
|
|
1349
|
+
|
|
1350
|
+
println("--- ServiceA doing work (connection scoped to this block) ---")
|
|
1351
|
+
appScope.scoped { workScope =>
|
|
1352
|
+
import workScope._
|
|
1353
|
+
val p: $[ConnectionPool] = lower(pool)
|
|
1354
|
+
val c: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
1355
|
+
val result = $(c)(_.execute("SELECT * FROM service_a_table"))
|
|
1356
|
+
println(s" [ServiceA] Got: $result")
|
|
1357
|
+
}
|
|
1358
|
+
println()
|
|
1359
|
+
|
|
1360
|
+
println("--- ServiceB doing work ---")
|
|
1361
|
+
appScope.scoped { workScope =>
|
|
1362
|
+
import workScope._
|
|
1363
|
+
val p: $[ConnectionPool] = lower(pool)
|
|
1364
|
+
val c: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
1365
|
+
val result = $(c)(_.execute("SELECT * FROM service_b_table"))
|
|
1366
|
+
println(s" [ServiceB] Got: $result")
|
|
1367
|
+
}
|
|
1368
|
+
println()
|
|
1369
|
+
|
|
1370
|
+
println("--- Multiple connections in same scope ---")
|
|
1371
|
+
appScope.scoped { workScope =>
|
|
1372
|
+
import workScope._
|
|
1373
|
+
val p: $[ConnectionPool] = lower(pool)
|
|
1374
|
+
val a: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
1375
|
+
val b: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
1376
|
+
val aId = $(a)(_.id)
|
|
1377
|
+
val bId = $(b)(_.id)
|
|
1378
|
+
println(s" [Parallel] Using connections $aId and $bId")
|
|
1379
|
+
$(a)(_.execute("UPDATE table_a SET x = 1"))
|
|
1380
|
+
$(b)(_.execute("UPDATE table_b SET y = 2"))
|
|
1381
|
+
()
|
|
1382
|
+
}
|
|
1383
|
+
println()
|
|
1384
|
+
|
|
1385
|
+
println("[App] All work complete, exiting app scope...")
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
println("\n=== Example Complete ===")
|
|
1389
|
+
println("\nKey insight: pool.acquire returns Resource[PooledConnection],")
|
|
1390
|
+
println("forcing proper scoped allocation and automatic release.")
|
|
1391
|
+
}
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala))
|
|
1395
|
+
|
|
1396
|
+
```bash
|
|
1397
|
+
sbt "scope-examples/runMain scope.examples.connectionPoolExample"
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
### Handling Temporary File Resources with Automatic Cleanup
|
|
1401
|
+
|
|
1402
|
+
This example shows how to allocate temporary file resources and ensure they are automatically cleaned up when the scope closes, even if errors occur.
|
|
1403
|
+
|
|
1404
|
+
```scala title="scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala"
|
|
1405
|
+
/*
|
|
1406
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1407
|
+
*
|
|
1408
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1409
|
+
* you may not use this file except in compliance with the License.
|
|
1410
|
+
* You may obtain a copy of the License at
|
|
1411
|
+
*
|
|
1412
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1413
|
+
*
|
|
1414
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1415
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1416
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1417
|
+
* See the License for the specific language governing permissions and
|
|
1418
|
+
* limitations under the License.
|
|
1419
|
+
*/
|
|
1420
|
+
|
|
1421
|
+
package scope.examples
|
|
1422
|
+
|
|
1423
|
+
import zio.blocks.scope._
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Demonstrates `scope.defer(...)` for registering manual cleanup actions.
|
|
1427
|
+
*
|
|
1428
|
+
* This example shows how to create temporary files during processing and ensure
|
|
1429
|
+
* they are deleted when the scope exits—even if processing fails. Deferred
|
|
1430
|
+
* cleanup actions run in LIFO (last-in-first-out) order.
|
|
1431
|
+
*/
|
|
1432
|
+
|
|
1433
|
+
/** Represents a temporary file with basic read/write operations. */
|
|
1434
|
+
case class TempFile(path: String) {
|
|
1435
|
+
private var content: String = ""
|
|
1436
|
+
|
|
1437
|
+
def write(data: String): Unit = content = data
|
|
1438
|
+
def read(): String = content
|
|
1439
|
+
def delete(): Boolean = { println(s" Deleting: $path"); true }
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/** Result of processing temporary files. */
|
|
1443
|
+
case class ProcessingResult(processedCount: Int, totalBytes: Long, errors: List[String])
|
|
1444
|
+
|
|
1445
|
+
/** Processes a list of temporary files and aggregates results. */
|
|
1446
|
+
object FileProcessor {
|
|
1447
|
+
def process(files: List[TempFile]): ProcessingResult = {
|
|
1448
|
+
val totalBytes = files.map(_.read().length.toLong).sum
|
|
1449
|
+
ProcessingResult(processedCount = files.size, totalBytes = totalBytes, errors = Nil)
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
@main def tempFileHandlingExample(): Unit = {
|
|
1454
|
+
println("=== Temp File Handling Example ===\n")
|
|
1455
|
+
println("Demonstrating scope.defer() for manual cleanup registration.\n")
|
|
1456
|
+
|
|
1457
|
+
val result = Scope.global.scoped { scope =>
|
|
1458
|
+
// Create temp files and register cleanup via defer.
|
|
1459
|
+
// Cleanup runs in LIFO order: file3, file2, file1.
|
|
1460
|
+
|
|
1461
|
+
val file1 = createTempFile(scope, "/tmp/data-001.tmp", "First file content")
|
|
1462
|
+
val file2 = createTempFile(scope, "/tmp/data-002.tmp", "Second file - more data here")
|
|
1463
|
+
val file3 = createTempFile(scope, "/tmp/data-003.tmp", "Third file with the most content of all")
|
|
1464
|
+
|
|
1465
|
+
println("\nProcessing files...")
|
|
1466
|
+
val processingResult = FileProcessor.process(List(file1, file2, file3))
|
|
1467
|
+
println(s"Processed ${processingResult.processedCount} files, ${processingResult.totalBytes} bytes\n")
|
|
1468
|
+
|
|
1469
|
+
println("Exiting scope - cleanup runs in LIFO order:")
|
|
1470
|
+
processingResult
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
println(s"\nFinal result: $result")
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Creates a temporary file and registers its cleanup with the scope.
|
|
1478
|
+
*
|
|
1479
|
+
* The cleanup action is registered via `defer(...)`, ensuring the file is
|
|
1480
|
+
* deleted when the scope closes—regardless of whether processing succeeds.
|
|
1481
|
+
*
|
|
1482
|
+
* @param s
|
|
1483
|
+
* the scope to register cleanup with
|
|
1484
|
+
* @param path
|
|
1485
|
+
* the file path
|
|
1486
|
+
* @param content
|
|
1487
|
+
* initial content to write
|
|
1488
|
+
* @return
|
|
1489
|
+
* the created TempFile
|
|
1490
|
+
*/
|
|
1491
|
+
private def createTempFile(s: Scope, path: String, content: String): TempFile = {
|
|
1492
|
+
val file = TempFile(path)
|
|
1493
|
+
file.write(content)
|
|
1494
|
+
println(s"Created: $path (${content.length} bytes)")
|
|
1495
|
+
|
|
1496
|
+
// Register cleanup - will run when scope exits, in LIFO order
|
|
1497
|
+
s.defer {
|
|
1498
|
+
file.delete()
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
file
|
|
1502
|
+
}
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala))
|
|
1506
|
+
|
|
1507
|
+
```bash
|
|
1508
|
+
sbt "scope-examples/runMain scope.examples.tempFileHandlingExample"
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
### Managing Database Transactions with Commit/Rollback Semantics
|
|
1512
|
+
|
|
1513
|
+
This example demonstrates managing database transactions within a scope, showing how to handle commit and rollback operations correctly.
|
|
1514
|
+
|
|
1515
|
+
```scala title="scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala"
|
|
1516
|
+
/*
|
|
1517
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1518
|
+
*
|
|
1519
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1520
|
+
* you may not use this file except in compliance with the License.
|
|
1521
|
+
* You may obtain a copy of the License at
|
|
1522
|
+
*
|
|
1523
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1524
|
+
*
|
|
1525
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1526
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1527
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1528
|
+
* See the License for the specific language governing permissions and
|
|
1529
|
+
* limitations under the License.
|
|
1530
|
+
*/
|
|
1531
|
+
|
|
1532
|
+
package scope.examples
|
|
1533
|
+
|
|
1534
|
+
import zio.blocks.scope._
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Transaction Boundary Example
|
|
1538
|
+
*
|
|
1539
|
+
* Demonstrates nested scopes and resource-returning methods for database
|
|
1540
|
+
* transaction management.
|
|
1541
|
+
*
|
|
1542
|
+
* Key patterns shown:
|
|
1543
|
+
* - '''Resource-returning methods''': `beginTransaction` returns
|
|
1544
|
+
* `Resource[DbTransaction]`
|
|
1545
|
+
* - '''Nested scopes''': Transactions live in child scopes of the connection
|
|
1546
|
+
* - '''Automatic cleanup''': Uncommitted transactions auto-rollback on scope
|
|
1547
|
+
* exit
|
|
1548
|
+
* - '''LIFO ordering''': Transaction closes before connection
|
|
1549
|
+
*/
|
|
1550
|
+
object TransactionBoundaryExample {
|
|
1551
|
+
|
|
1552
|
+
/** Simulates a database connection that can create transactions. */
|
|
1553
|
+
class DbConnection(val id: String) extends AutoCloseable {
|
|
1554
|
+
println(s" [DbConnection $id] Opened")
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Begins a new transaction.
|
|
1558
|
+
*
|
|
1559
|
+
* Returns a `Resource[DbTransaction]` that must be allocated in a scope.
|
|
1560
|
+
* This ensures the transaction is always properly closed (with rollback if
|
|
1561
|
+
* not committed) when the scope exits.
|
|
1562
|
+
*/
|
|
1563
|
+
def beginTransaction(txId: String): Resource[DbTransaction] =
|
|
1564
|
+
Resource.acquireRelease {
|
|
1565
|
+
new DbTransaction(this, txId)
|
|
1566
|
+
} { tx =>
|
|
1567
|
+
tx.close()
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
def close(): Unit =
|
|
1571
|
+
println(s" [DbConnection $id] Closed")
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Simulates an active database transaction. */
|
|
1575
|
+
class DbTransaction(val conn: DbConnection, val id: String) extends AutoCloseable {
|
|
1576
|
+
private var committed = false
|
|
1577
|
+
private var rolledBack = false
|
|
1578
|
+
println(s" [Tx $id] Started on connection ${conn.id}")
|
|
1579
|
+
|
|
1580
|
+
def execute(sql: String): Int = {
|
|
1581
|
+
require(!committed && !rolledBack, s"Transaction $id already completed")
|
|
1582
|
+
println(s" [Tx $id] Execute: $sql")
|
|
1583
|
+
sql.hashCode.abs % 100 + 1
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
def commit(): Unit = {
|
|
1587
|
+
require(!committed && !rolledBack, s"Transaction $id already completed")
|
|
1588
|
+
committed = true
|
|
1589
|
+
println(s" [Tx $id] Committed")
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
def rollback(): Unit =
|
|
1593
|
+
if (!committed && !rolledBack) {
|
|
1594
|
+
rolledBack = true
|
|
1595
|
+
println(s" [Tx $id] Rolled back")
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
def close(): Unit = {
|
|
1599
|
+
if (!committed && !rolledBack) {
|
|
1600
|
+
println(s" [Tx $id] Auto-rollback (not committed)")
|
|
1601
|
+
rollback()
|
|
1602
|
+
}
|
|
1603
|
+
println(s" [Tx $id] Closed")
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** Result of transaction operations. */
|
|
1608
|
+
case class TxResult(success: Boolean, affectedRows: Int) derives Unscoped
|
|
1609
|
+
|
|
1610
|
+
@main def runTransactionBoundaryExample(): Unit = {
|
|
1611
|
+
println("=== Transaction Boundary Example ===\n")
|
|
1612
|
+
println("Demonstrating Resource-returning beginTransaction method\n")
|
|
1613
|
+
|
|
1614
|
+
Scope.global.scoped { connScope =>
|
|
1615
|
+
import connScope._
|
|
1616
|
+
// Allocate the connection in the outer scope
|
|
1617
|
+
val conn: $[DbConnection] = Resource.fromAutoCloseable(new DbConnection("db-001")).allocate
|
|
1618
|
+
println()
|
|
1619
|
+
|
|
1620
|
+
// Transaction 1: Successful insert
|
|
1621
|
+
println("--- Transaction 1: Insert user ---")
|
|
1622
|
+
val result1: TxResult =
|
|
1623
|
+
connScope.scoped { txScope =>
|
|
1624
|
+
import txScope._
|
|
1625
|
+
val c: $[DbConnection] = lower(conn)
|
|
1626
|
+
val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-001")).allocate
|
|
1627
|
+
val rows = $(tx)(_.execute("INSERT INTO users VALUES (1, 'Alice')"))
|
|
1628
|
+
$(tx)(_.commit())
|
|
1629
|
+
TxResult(success = true, affectedRows = rows)
|
|
1630
|
+
}
|
|
1631
|
+
println(s" Result: $result1\n")
|
|
1632
|
+
|
|
1633
|
+
// Transaction 2: Transfer funds (multiple operations)
|
|
1634
|
+
println("--- Transaction 2: Transfer funds ---")
|
|
1635
|
+
val result2: TxResult =
|
|
1636
|
+
connScope.scoped { txScope =>
|
|
1637
|
+
import txScope._
|
|
1638
|
+
val c: $[DbConnection] = lower(conn)
|
|
1639
|
+
val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-002")).allocate
|
|
1640
|
+
val rows1 = $(tx)(_.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1"))
|
|
1641
|
+
val rows2 = $(tx)(_.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2"))
|
|
1642
|
+
$(tx)(_.commit())
|
|
1643
|
+
TxResult(success = true, affectedRows = rows1 + rows2)
|
|
1644
|
+
}
|
|
1645
|
+
println(s" Result: $result2\n")
|
|
1646
|
+
|
|
1647
|
+
// Transaction 3: Demonstrates auto-rollback on scope exit without commit
|
|
1648
|
+
println("--- Transaction 3: Auto-rollback (no explicit commit) ---")
|
|
1649
|
+
val result3: TxResult =
|
|
1650
|
+
connScope.scoped { txScope =>
|
|
1651
|
+
import txScope._
|
|
1652
|
+
val c: $[DbConnection] = lower(conn)
|
|
1653
|
+
val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-003")).allocate
|
|
1654
|
+
$(tx)(_.execute("DELETE FROM audit_log"))
|
|
1655
|
+
println(" [App] Not committing - scope exit will trigger auto-rollback...")
|
|
1656
|
+
TxResult(success = false, affectedRows = 0)
|
|
1657
|
+
}
|
|
1658
|
+
println(s" Result: $result3\n")
|
|
1659
|
+
|
|
1660
|
+
println("--- All transactions complete, connection still open ---")
|
|
1661
|
+
println("--- Exiting connection scope ---")
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
println("\n=== Example complete ===")
|
|
1665
|
+
println("\nKey insight: beginTransaction() returns Resource[DbTransaction],")
|
|
1666
|
+
println("forcing proper scoped allocation and automatic cleanup.")
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala))
|
|
1672
|
+
|
|
1673
|
+
```bash
|
|
1674
|
+
sbt "scope-examples/runMain scope.examples.runTransactionBoundaryExample"
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
### Implementing an HTTP Client Pipeline with Request/Response Interceptors
|
|
1678
|
+
|
|
1679
|
+
This example shows how to build an HTTP client pipeline with interceptors for logging, authentication, and error handling, all managed within a scope.
|
|
1680
|
+
|
|
1681
|
+
```scala title="scope-examples/src/main/scala/scope/examples/HttpClientPipelineExample.scala"
|
|
1682
|
+
/*
|
|
1683
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1684
|
+
*
|
|
1685
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1686
|
+
* you may not use this file except in compliance with the License.
|
|
1687
|
+
* You may obtain a copy of the License at
|
|
1688
|
+
*
|
|
1689
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1690
|
+
*
|
|
1691
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1692
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1693
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1694
|
+
* See the License for the specific language governing permissions and
|
|
1695
|
+
* limitations under the License.
|
|
1696
|
+
*/
|
|
1697
|
+
|
|
1698
|
+
package scope.examples
|
|
1699
|
+
|
|
1700
|
+
import zio.blocks.scope._
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* HTTP Client Pipeline Example
|
|
1704
|
+
*
|
|
1705
|
+
* Demonstrates using scoped values with the `$` operator for safe resource
|
|
1706
|
+
* access. Operations are eager with the new opaque type API.
|
|
1707
|
+
*/
|
|
1708
|
+
|
|
1709
|
+
/** API configuration containing base URL and authentication credentials. */
|
|
1710
|
+
final case class ApiConfig(baseUrl: String, apiKey: String)
|
|
1711
|
+
|
|
1712
|
+
/** Parsed JSON data as a simple key-value store. */
|
|
1713
|
+
final case class ParsedData(values: Map[String, String]) derives Unscoped
|
|
1714
|
+
|
|
1715
|
+
/** HTTP response containing status, body, and headers. */
|
|
1716
|
+
final case class HttpResponse(statusCode: Int, body: String, headers: Map[String, String])
|
|
1717
|
+
|
|
1718
|
+
/** Stateless JSON parser that converts raw JSON strings to structured data. */
|
|
1719
|
+
object JsonParser {
|
|
1720
|
+
def parse(json: String): ParsedData = {
|
|
1721
|
+
println(s" [JsonParser] Parsing ${json.take(50)}...")
|
|
1722
|
+
val entries = json.stripPrefix("{").stripSuffix("}").split(",").map(_.trim).filter(_.nonEmpty)
|
|
1723
|
+
val values = entries.flatMap { entry =>
|
|
1724
|
+
entry.split(":").map(_.trim.stripPrefix("\"").stripSuffix("\"")) match {
|
|
1725
|
+
case Array(k, v) => Some(k -> v)
|
|
1726
|
+
case _ => None
|
|
1727
|
+
}
|
|
1728
|
+
}.toMap
|
|
1729
|
+
ParsedData(values)
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* HTTP client that manages a connection to an API server.
|
|
1735
|
+
*
|
|
1736
|
+
* Implements `AutoCloseable` so the scope automatically registers cleanup.
|
|
1737
|
+
*/
|
|
1738
|
+
final class HttpClient(config: ApiConfig) extends AutoCloseable {
|
|
1739
|
+
println(s" [HttpClient] Opening connection to ${config.baseUrl}")
|
|
1740
|
+
|
|
1741
|
+
def get(path: String): HttpResponse = {
|
|
1742
|
+
println(s" [HttpClient] GET $path")
|
|
1743
|
+
HttpResponse(200, s"""{"path":"$path","data":"sample"}""", Map("X-Api-Key" -> config.apiKey))
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
def post(path: String, body: String): HttpResponse = {
|
|
1747
|
+
println(s" [HttpClient] POST $path with body: $body")
|
|
1748
|
+
HttpResponse(201, s"""{"created":true,"echo":"$body"}""", Map("Content-Type" -> "application/json"))
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
override def close(): Unit =
|
|
1752
|
+
println(s" [HttpClient] Closing connection to ${config.baseUrl}")
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Demonstrates using scoped values with the `$` operator.
|
|
1757
|
+
*
|
|
1758
|
+
* Key concepts:
|
|
1759
|
+
* - `allocate` returns `$[A]` (scoped value)
|
|
1760
|
+
* - `$(scopedValue)(f)` applies a function to the underlying value
|
|
1761
|
+
* - `$` auto-unwraps to pure data when the return type is `Unscoped`
|
|
1762
|
+
* - Operations are eager (zero-cost wrapper)
|
|
1763
|
+
*/
|
|
1764
|
+
@main def httpClientPipelineExample(): Unit = {
|
|
1765
|
+
println("=== HTTP Client Pipeline Example ===\n")
|
|
1766
|
+
val config = ApiConfig("https://api.example.com", "secret-key-123")
|
|
1767
|
+
|
|
1768
|
+
Scope.global.scoped { scope =>
|
|
1769
|
+
import scope._
|
|
1770
|
+
// Step 1: Allocate the HTTP client (automatically cleaned up when scope closes)
|
|
1771
|
+
val client: $[HttpClient] = allocate(Resource[HttpClient](new HttpClient(config)))
|
|
1772
|
+
|
|
1773
|
+
// Step 2: Use the client to fetch and parse data
|
|
1774
|
+
println("Executing requests...\n")
|
|
1775
|
+
|
|
1776
|
+
// Fetch and parse users
|
|
1777
|
+
println("--- Fetching: users ---")
|
|
1778
|
+
val users: ParsedData = $(client) { c =>
|
|
1779
|
+
val response = c.get("/users")
|
|
1780
|
+
JsonParser.parse(response.body)
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Fetch and parse orders
|
|
1784
|
+
println("\n--- Fetching: orders ---")
|
|
1785
|
+
val orders: ParsedData = $(client) { c =>
|
|
1786
|
+
val response = c.get("/orders")
|
|
1787
|
+
JsonParser.parse(response.body)
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Post analytics event
|
|
1791
|
+
println("\n--- Posting: analytics ---")
|
|
1792
|
+
val analytics: ParsedData = $(client) { c =>
|
|
1793
|
+
val response = c.post("/analytics", """{"event":"fetch_complete"}""")
|
|
1794
|
+
JsonParser.parse(response.body)
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Step 3: Access all results
|
|
1798
|
+
println(s"\n=== Users Result ===")
|
|
1799
|
+
println(s"Users data: ${users.values}")
|
|
1800
|
+
println(s"\n=== Orders Result ===")
|
|
1801
|
+
println(s"Orders data: ${orders.values}")
|
|
1802
|
+
println(s"\n=== Analytics Result ===")
|
|
1803
|
+
println(s"Analytics: ${analytics.values}")
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
println("\n[Scope closed - HttpClient was automatically cleaned up]")
|
|
1807
|
+
}
|
|
1808
|
+
```
|
|
1809
|
+
|
|
1810
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/HttpClientPipelineExample.scala))
|
|
1811
|
+
|
|
1812
|
+
```bash
|
|
1813
|
+
sbt "scope-examples/runMain scope.examples.httpClientPipelineExample"
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
### Managing a Shared, Cached Logger Across Multiple Services
|
|
1817
|
+
|
|
1818
|
+
This example demonstrates allocating a logger once at the top level and sharing it across multiple services, ensuring it is properly closed when the application shuts down.
|
|
1819
|
+
|
|
1820
|
+
```scala title="scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala"
|
|
1821
|
+
/*
|
|
1822
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1823
|
+
*
|
|
1824
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1825
|
+
* you may not use this file except in compliance with the License.
|
|
1826
|
+
* You may obtain a copy of the License at
|
|
1827
|
+
*
|
|
1828
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1829
|
+
*
|
|
1830
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1831
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1832
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1833
|
+
* See the License for the specific language governing permissions and
|
|
1834
|
+
* limitations under the License.
|
|
1835
|
+
*/
|
|
1836
|
+
|
|
1837
|
+
package scope.examples
|
|
1838
|
+
|
|
1839
|
+
import zio.blocks.scope._
|
|
1840
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Demonstrates `Wire.shared` vs `Wire.unique` and diamond dependency patterns.
|
|
1844
|
+
*
|
|
1845
|
+
* Two services (ProductService, OrderService) share one Logger instance
|
|
1846
|
+
* (diamond pattern), but each gets its own unique Cache instance. This shows
|
|
1847
|
+
* how shared wires provide singleton behavior while unique wires create fresh
|
|
1848
|
+
* instances per injection site.
|
|
1849
|
+
*
|
|
1850
|
+
* Key concepts:
|
|
1851
|
+
* - `Wire.shared[T]`: Single instance shared across all dependents (memoized)
|
|
1852
|
+
* - `Wire.unique[T]`: Fresh instance created for each dependent
|
|
1853
|
+
* - Diamond dependency: Multiple services depend on the same shared resource
|
|
1854
|
+
* - Reference counting: Shared resources track usage and clean up when last
|
|
1855
|
+
* user closes
|
|
1856
|
+
*/
|
|
1857
|
+
object CachingSharedLoggerExample {
|
|
1858
|
+
|
|
1859
|
+
/** Tracks instantiation counts for demonstration purposes. */
|
|
1860
|
+
val loggerInstances = new AtomicInteger(0)
|
|
1861
|
+
val cacheInstances = new AtomicInteger(0)
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* A shared logger that tracks instantiations and provides logging methods.
|
|
1865
|
+
* Implements AutoCloseable for proper resource cleanup.
|
|
1866
|
+
*/
|
|
1867
|
+
class Logger extends AutoCloseable {
|
|
1868
|
+
val instanceId: Int = loggerInstances.incrementAndGet()
|
|
1869
|
+
println(s" [Logger#$instanceId] Created")
|
|
1870
|
+
|
|
1871
|
+
def info(msg: String): Unit = println(s" [Logger#$instanceId] INFO: $msg")
|
|
1872
|
+
def debug(msg: String): Unit = println(s" [Logger#$instanceId] DEBUG: $msg")
|
|
1873
|
+
def close(): Unit = println(s" [Logger#$instanceId] Closed")
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* A unique cache per service. Each service gets its own isolated cache
|
|
1878
|
+
* instance. Implements AutoCloseable for proper resource cleanup. Note: No
|
|
1879
|
+
* constructor params so it can be auto-wired with Wire.unique.
|
|
1880
|
+
*/
|
|
1881
|
+
class Cache extends AutoCloseable {
|
|
1882
|
+
val instanceId: Int = cacheInstances.incrementAndGet()
|
|
1883
|
+
private var store: Map[String, String] = Map.empty
|
|
1884
|
+
println(s" [Cache#$instanceId] Created")
|
|
1885
|
+
|
|
1886
|
+
def get(key: String): Option[String] = store.get(key)
|
|
1887
|
+
def put(key: String, value: String): Unit = store = store.updated(key, value)
|
|
1888
|
+
def close(): Unit = println(s" [Cache#$instanceId] Closed")
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/** Product service with its own cache but sharing the logger. */
|
|
1892
|
+
class ProductService(val logger: Logger, val cache: Cache) {
|
|
1893
|
+
println(s" [ProductService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
|
|
1894
|
+
|
|
1895
|
+
def findProduct(id: String): String =
|
|
1896
|
+
cache.get(id) match {
|
|
1897
|
+
case Some(product) =>
|
|
1898
|
+
logger.debug(s"Cache hit for product $id")
|
|
1899
|
+
product
|
|
1900
|
+
case None =>
|
|
1901
|
+
logger.info(s"Loading product $id from database")
|
|
1902
|
+
val product = s"Product-$id"
|
|
1903
|
+
cache.put(id, product)
|
|
1904
|
+
product
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Order service with its own cache but sharing the same logger as
|
|
1910
|
+
* ProductService.
|
|
1911
|
+
*/
|
|
1912
|
+
class OrderService(val logger: Logger, val cache: Cache) {
|
|
1913
|
+
println(s" [OrderService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
|
|
1914
|
+
|
|
1915
|
+
def createOrder(productId: String): String = {
|
|
1916
|
+
val orderId = s"ORD-${System.currentTimeMillis() % 10000}"
|
|
1917
|
+
cache.put(orderId, productId)
|
|
1918
|
+
logger.info(s"Created order $orderId for product $productId")
|
|
1919
|
+
orderId
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/** Top-level application combining both services. */
|
|
1924
|
+
class CachingApp(val productService: ProductService, val orderService: OrderService) extends AutoCloseable {
|
|
1925
|
+
def run(): Unit = {
|
|
1926
|
+
productService.logger.info("=== Application Started ===")
|
|
1927
|
+
val product = productService.findProduct("P001")
|
|
1928
|
+
orderService.createOrder(product)
|
|
1929
|
+
productService.findProduct("P001") // cache hit
|
|
1930
|
+
}
|
|
1931
|
+
def close(): Unit = println(" [CachingApp] Closed")
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
@main def runCachingExample(): Unit = {
|
|
1935
|
+
println("\n╔════════════════════════════════════════════════════════════════╗")
|
|
1936
|
+
println("║ Wire.shared vs Wire.unique - Diamond Dependency Example ║")
|
|
1937
|
+
println("╚════════════════════════════════════════════════════════════════╝\n")
|
|
1938
|
+
|
|
1939
|
+
println("Creating wires...")
|
|
1940
|
+
println(" - Logger: Wire.shared (singleton across all services)")
|
|
1941
|
+
println(" - Cache: Wire.unique (fresh instance per service)\n")
|
|
1942
|
+
|
|
1943
|
+
println("─── Resource Acquisition ───")
|
|
1944
|
+
Scope.global.scoped { scope =>
|
|
1945
|
+
import scope._
|
|
1946
|
+
val app: $[CachingApp] = allocate(
|
|
1947
|
+
Resource.from[CachingApp](
|
|
1948
|
+
Wire.shared[Logger],
|
|
1949
|
+
Wire.unique[Cache]
|
|
1950
|
+
)
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
println("\n─── Verification ───")
|
|
1954
|
+
println(s" Logger instances created: ${loggerInstances.get()} (expected: 1)")
|
|
1955
|
+
println(s" Cache instances created: ${cacheInstances.get()} (expected: 2)")
|
|
1956
|
+
$(app) { a =>
|
|
1957
|
+
println(s" ProductService.logger eq OrderService.logger: ${a.productService.logger eq a.orderService.logger}")
|
|
1958
|
+
println(s" ProductService.cache eq OrderService.cache: ${a.productService.cache eq a.orderService.cache}")
|
|
1959
|
+
|
|
1960
|
+
println("\n─── Running Application ───")
|
|
1961
|
+
a.run()
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
println("\n─── Scope Closing (LIFO cleanup) ───")
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
println("\n─── Summary ───")
|
|
1968
|
+
println(s" Final Logger count: ${loggerInstances.get()} (shared = 1 instance)")
|
|
1969
|
+
println(s" Final Cache count: ${cacheInstances.get()} (unique = 2 instances)")
|
|
1970
|
+
println("\nDiamond pattern verified: both services received the same Logger instance.")
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
```
|
|
1974
|
+
|
|
1975
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala))
|
|
1976
|
+
|
|
1977
|
+
```bash
|
|
1978
|
+
sbt "scope-examples/runMain scope.examples.runCachingExample"
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
### Building a Layered Web Service with Dependency Injection
|
|
1982
|
+
|
|
1983
|
+
This example shows how to build a multi-layered web service using Scope for dependency injection, allocating services at different layers and passing them down through child scopes.
|
|
1984
|
+
|
|
1985
|
+
```scala title="scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala"
|
|
1986
|
+
/*
|
|
1987
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1988
|
+
*
|
|
1989
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1990
|
+
* you may not use this file except in compliance with the License.
|
|
1991
|
+
* You may obtain a copy of the License at
|
|
1992
|
+
*
|
|
1993
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1994
|
+
*
|
|
1995
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1996
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1997
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1998
|
+
* See the License for the specific language governing permissions and
|
|
1999
|
+
* limitations under the License.
|
|
2000
|
+
*/
|
|
2001
|
+
|
|
2002
|
+
package scope.examples
|
|
2003
|
+
|
|
2004
|
+
import zio.blocks.scope._
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* Demonstrates auto-wiring a layered web service using
|
|
2008
|
+
* `Resource.from[T](wires*)`.
|
|
2009
|
+
*
|
|
2010
|
+
* The macro automatically derives wires for concrete classes (Database,
|
|
2011
|
+
* UserRepository, UserController) while requiring only the leaf config value to
|
|
2012
|
+
* be provided explicitly. Resources are cleaned up in LIFO order when the scope
|
|
2013
|
+
* closes.
|
|
2014
|
+
*
|
|
2015
|
+
* Layer hierarchy:
|
|
2016
|
+
* {{{
|
|
2017
|
+
* AppConfig (leaf value via Wire)
|
|
2018
|
+
* ↓
|
|
2019
|
+
* Database (auto-wired, AutoCloseable)
|
|
2020
|
+
* ↓
|
|
2021
|
+
* UserRepository (auto-wired)
|
|
2022
|
+
* ↓
|
|
2023
|
+
* UserController (auto-wired, AutoCloseable)
|
|
2024
|
+
* }}}
|
|
2025
|
+
*/
|
|
2026
|
+
|
|
2027
|
+
/** Application configuration - the leaf dependency provided via Wire(value). */
|
|
2028
|
+
case class WebAppConfig(dbUrl: String, serverPort: Int)
|
|
2029
|
+
|
|
2030
|
+
/** Domain model for users. */
|
|
2031
|
+
case class User(id: Long, name: String, email: String)
|
|
2032
|
+
|
|
2033
|
+
/** Database layer - acquires a connection and releases it on close. */
|
|
2034
|
+
class WebDatabase(config: WebAppConfig) extends AutoCloseable {
|
|
2035
|
+
println(s" [WebDatabase] Connecting to ${config.dbUrl}")
|
|
2036
|
+
|
|
2037
|
+
def execute(sql: String): Int = {
|
|
2038
|
+
println(s" [WebDatabase] Executing: $sql")
|
|
2039
|
+
1
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
def close(): Unit = println(" [WebDatabase] Connection closed")
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/** Repository layer - provides data access using the database. */
|
|
2046
|
+
class UserRepository(db: WebDatabase) {
|
|
2047
|
+
println(" [UserRepository] Initialized")
|
|
2048
|
+
|
|
2049
|
+
private var nextId = 1L
|
|
2050
|
+
|
|
2051
|
+
def findById(id: Long): Option[User] = {
|
|
2052
|
+
db.execute(s"SELECT * FROM users WHERE id = $id")
|
|
2053
|
+
if (id > 0) Some(User(id, "Alice", "alice@example.com")) else None
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
def save(user: User): Long = {
|
|
2057
|
+
db.execute(s"INSERT INTO users VALUES (${user.id}, '${user.name}', '${user.email}')")
|
|
2058
|
+
val id = nextId
|
|
2059
|
+
nextId += 1
|
|
2060
|
+
id
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/** Controller layer - handles HTTP requests using the repository. */
|
|
2065
|
+
class UserController(repo: UserRepository) extends AutoCloseable {
|
|
2066
|
+
println(" [UserController] Ready to serve requests")
|
|
2067
|
+
|
|
2068
|
+
def getUser(id: Long): String =
|
|
2069
|
+
repo.findById(id).map(u => s"User(${u.id}, ${u.name})").getOrElse("Not found")
|
|
2070
|
+
|
|
2071
|
+
def createUser(name: String, email: String): String = {
|
|
2072
|
+
val id = repo.save(User(0, name, email))
|
|
2073
|
+
s"Created user with id=$id"
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
def close(): Unit = println(" [UserController] Shutting down")
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* Entry point demonstrating the auto-wiring feature.
|
|
2081
|
+
*
|
|
2082
|
+
* Only `Wire(config)` is provided; the macro derives wires for Database,
|
|
2083
|
+
* UserRepository, and UserController from their constructors.
|
|
2084
|
+
*/
|
|
2085
|
+
@main def layeredWebServiceExample(): Unit = {
|
|
2086
|
+
val config = WebAppConfig(dbUrl = "jdbc:postgresql://localhost:5432/mydb", serverPort = 8080)
|
|
2087
|
+
|
|
2088
|
+
println("=== Constructing layers (order: config → database → repository → controller) ===")
|
|
2089
|
+
|
|
2090
|
+
// Resource.from auto-wires the entire dependency graph
|
|
2091
|
+
val controllerResource: Resource[UserController] = Resource.from[UserController](
|
|
2092
|
+
Wire(config)
|
|
2093
|
+
)
|
|
2094
|
+
|
|
2095
|
+
// Allocate within a scoped block; cleanup runs on scope exit
|
|
2096
|
+
Scope.global.scoped { scope =>
|
|
2097
|
+
import scope._
|
|
2098
|
+
val controller: $[UserController] = allocate(controllerResource)
|
|
2099
|
+
|
|
2100
|
+
println("\n=== Handling requests ===")
|
|
2101
|
+
println(s" GET /users/1 → ${$(controller)(_.getUser(1))}")
|
|
2102
|
+
println(s" POST /users → ${$(controller)(_.createUser("Bob", "bob@example.com"))}")
|
|
2103
|
+
|
|
2104
|
+
println("\n=== Scope closing (LIFO cleanup: controller → database) ===")
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
println("=== Done ===")
|
|
2108
|
+
}
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala))
|
|
2112
|
+
|
|
2113
|
+
```bash
|
|
2114
|
+
sbt "scope-examples/runMain scope.examples.layeredWebServiceExample"
|
|
2115
|
+
```
|
|
2116
|
+
|
|
2117
|
+
### Reading Configuration from a File with Scope Management
|
|
2118
|
+
|
|
2119
|
+
This example demonstrates loading configuration from a file within a scope, ensuring the file handle is properly closed when no longer needed.
|
|
2120
|
+
|
|
2121
|
+
```scala title="scope-examples/src/main/scala/scope/examples/ConfigReaderExample.scala"
|
|
2122
|
+
/*
|
|
2123
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
2124
|
+
*
|
|
2125
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2126
|
+
* you may not use this file except in compliance with the License.
|
|
2127
|
+
* You may obtain a copy of the License at
|
|
2128
|
+
*
|
|
2129
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2130
|
+
*
|
|
2131
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2132
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2133
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2134
|
+
* See the License for the specific language governing permissions and
|
|
2135
|
+
* limitations under the License.
|
|
2136
|
+
*/
|
|
2137
|
+
|
|
2138
|
+
package scope.examples
|
|
2139
|
+
|
|
2140
|
+
import zio.blocks.scope._
|
|
2141
|
+
|
|
2142
|
+
/**
|
|
2143
|
+
* Demonstrates the `Unscoped` marker trait behavior.
|
|
2144
|
+
*
|
|
2145
|
+
* ==Key Concepts==
|
|
2146
|
+
*
|
|
2147
|
+
* - '''`Unscoped`''' marks pure data types that can safely escape a scope
|
|
2148
|
+
* - Pure data escapes freely; resources remain scope-bound
|
|
2149
|
+
*
|
|
2150
|
+
* ==Example Scenario==
|
|
2151
|
+
*
|
|
2152
|
+
* A configuration reader produces `ConfigData` (pure, escapable data), while a
|
|
2153
|
+
* secret store holds resources that must remain scoped to prevent leakage.
|
|
2154
|
+
*/
|
|
2155
|
+
|
|
2156
|
+
// ---------------------------------------------------------------------------
|
|
2157
|
+
// Domain Types
|
|
2158
|
+
// ---------------------------------------------------------------------------
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* Pure configuration data that can safely escape any scope.
|
|
2162
|
+
*
|
|
2163
|
+
* By deriving `Unscoped`, we declare this type contains no resources. When
|
|
2164
|
+
* returned from `scoped { ... }`, the raw `ConfigData` is returned.
|
|
2165
|
+
*/
|
|
2166
|
+
case class ConfigData(
|
|
2167
|
+
appName: String,
|
|
2168
|
+
version: String,
|
|
2169
|
+
settings: Map[String, String]
|
|
2170
|
+
) derives Unscoped
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Reads configuration files from disk.
|
|
2174
|
+
*
|
|
2175
|
+
* This is a resource (holds file handles, caches) and must be closed.
|
|
2176
|
+
*/
|
|
2177
|
+
class ConfigReader extends AutoCloseable {
|
|
2178
|
+
private var closed = false
|
|
2179
|
+
|
|
2180
|
+
def readConfig(@annotation.unused path: String): ConfigData = {
|
|
2181
|
+
require(!closed, "ConfigReader is closed")
|
|
2182
|
+
ConfigData(
|
|
2183
|
+
appName = "MyApplication",
|
|
2184
|
+
version = "1.0.0",
|
|
2185
|
+
settings = Map(
|
|
2186
|
+
"database.host" -> "localhost",
|
|
2187
|
+
"database.port" -> "5432",
|
|
2188
|
+
"log.level" -> "INFO"
|
|
2189
|
+
)
|
|
2190
|
+
)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
override def close(): Unit = {
|
|
2194
|
+
closed = true
|
|
2195
|
+
println(" [ConfigReader] Closed.")
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Manages access to application secrets.
|
|
2201
|
+
*
|
|
2202
|
+
* This resource maintains connections and caches; it should NOT have an
|
|
2203
|
+
* `Unscoped` instance. It cannot escape the scope.
|
|
2204
|
+
*/
|
|
2205
|
+
class SecretStore extends AutoCloseable {
|
|
2206
|
+
private var closed = false
|
|
2207
|
+
|
|
2208
|
+
def getSecret(key: String): String = {
|
|
2209
|
+
require(!closed, "SecretStore is closed")
|
|
2210
|
+
s"secret-value-for-$key"
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
override def close(): Unit = {
|
|
2214
|
+
closed = true
|
|
2215
|
+
println(" [SecretStore] Closed.")
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// ---------------------------------------------------------------------------
|
|
2220
|
+
// Main Example
|
|
2221
|
+
// ---------------------------------------------------------------------------
|
|
2222
|
+
|
|
2223
|
+
@main def runConfigReaderExample(): Unit = {
|
|
2224
|
+
println("=== Unscoped Example ===\n")
|
|
2225
|
+
|
|
2226
|
+
// ConfigData is Unscoped, so it escapes the scope as raw ConfigData
|
|
2227
|
+
val escapedConfig: ConfigData = Scope.global.scoped { scope =>
|
|
2228
|
+
import scope._
|
|
2229
|
+
val reader: $[ConfigReader] = allocate(Resource(new ConfigReader))
|
|
2230
|
+
|
|
2231
|
+
// $(reader)(f) auto-unwraps to ConfigData (Unscoped)
|
|
2232
|
+
$(reader)(_.readConfig("/etc/app/config.json"))
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
println("Escaped config (used outside scope):")
|
|
2236
|
+
println(s" App: ${escapedConfig.appName} v${escapedConfig.version}")
|
|
2237
|
+
escapedConfig.settings.foreach { case (k, v) => println(s" $k = $v") }
|
|
2238
|
+
println()
|
|
2239
|
+
|
|
2240
|
+
println("SecretStore stays scoped:")
|
|
2241
|
+
Scope.global.scoped { scope =>
|
|
2242
|
+
import scope._
|
|
2243
|
+
val secrets: $[SecretStore] = allocate(Resource(new SecretStore))
|
|
2244
|
+
|
|
2245
|
+
$(secrets) { s =>
|
|
2246
|
+
val dbPassword = s.getSecret("database.password")
|
|
2247
|
+
println(s" Retrieved secret: $dbPassword")
|
|
2248
|
+
}
|
|
2249
|
+
()
|
|
2250
|
+
}
|
|
2251
|
+
println("\n=== Example Complete ===")
|
|
2252
|
+
}
|
|
2253
|
+
```
|
|
2254
|
+
|
|
2255
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ConfigReaderExample.scala))
|
|
2256
|
+
|
|
2257
|
+
```bash
|
|
2258
|
+
sbt "scope-examples/runMain scope.examples.runConfigReaderExample"
|
|
2259
|
+
```
|
|
2260
|
+
|
|
2261
|
+
### Implementing a Plugin Architecture with Automatic Resource Discovery
|
|
2262
|
+
|
|
2263
|
+
This example shows how to build a plugin system that discovers and loads plugins dynamically, managing their lifecycle with scopes.
|
|
2264
|
+
|
|
2265
|
+
```scala title="scope-examples/src/main/scala/scope/examples/PluginArchitectureExample.scala"
|
|
2266
|
+
/*
|
|
2267
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
2268
|
+
*
|
|
2269
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2270
|
+
* you may not use this file except in compliance with the License.
|
|
2271
|
+
* You may obtain a copy of the License at
|
|
2272
|
+
*
|
|
2273
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2274
|
+
*
|
|
2275
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2276
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2277
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2278
|
+
* See the License for the specific language governing permissions and
|
|
2279
|
+
* limitations under the License.
|
|
2280
|
+
*/
|
|
2281
|
+
|
|
2282
|
+
package scope.examples
|
|
2283
|
+
|
|
2284
|
+
import zio.blocks.scope._
|
|
2285
|
+
|
|
2286
|
+
/**
|
|
2287
|
+
* Plugin Architecture Example — Trait Injection via Subtype Wires
|
|
2288
|
+
*
|
|
2289
|
+
* Demonstrates how abstract trait dependencies are resolved via concrete
|
|
2290
|
+
* implementation wires using subtype resolution. This pattern enables:
|
|
2291
|
+
* - Clean interface/implementation separation
|
|
2292
|
+
* - Easy swapping of implementations (e.g., Stripe vs PayPal)
|
|
2293
|
+
* - Compile-time verified dependency graphs
|
|
2294
|
+
*/
|
|
2295
|
+
|
|
2296
|
+
/** Configuration for payment gateway connections. */
|
|
2297
|
+
final case class GatewayConfig(apiKey: String, merchantId: String)
|
|
2298
|
+
|
|
2299
|
+
/** Result of a payment operation. */
|
|
2300
|
+
final case class PaymentResult(transactionId: String, success: Boolean, message: String)
|
|
2301
|
+
|
|
2302
|
+
/**
|
|
2303
|
+
* Abstract payment gateway interface.
|
|
2304
|
+
*
|
|
2305
|
+
* Services depend on this trait, not concrete implementations.
|
|
2306
|
+
*/
|
|
2307
|
+
trait PaymentGateway {
|
|
2308
|
+
def charge(amount: BigDecimal, currency: String): PaymentResult
|
|
2309
|
+
def refund(transactionId: String): PaymentResult
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/**
|
|
2313
|
+
* Stripe implementation of [[PaymentGateway]].
|
|
2314
|
+
*
|
|
2315
|
+
* When wired via `Wire.shared[StripeGateway]`, this satisfies any
|
|
2316
|
+
* `PaymentGateway` dependency through subtype resolution.
|
|
2317
|
+
*/
|
|
2318
|
+
final class StripeGateway(config: GatewayConfig) extends PaymentGateway with AutoCloseable {
|
|
2319
|
+
println(s"[Stripe] Connected with merchant ${config.merchantId}")
|
|
2320
|
+
|
|
2321
|
+
def charge(amount: BigDecimal, currency: String): PaymentResult = {
|
|
2322
|
+
val txId = s"stripe_${System.nanoTime()}"
|
|
2323
|
+
PaymentResult(txId, success = true, s"Charged $currency $amount via Stripe")
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
def refund(transactionId: String): PaymentResult =
|
|
2327
|
+
PaymentResult(transactionId, success = true, s"Refunded $transactionId via Stripe")
|
|
2328
|
+
|
|
2329
|
+
def close(): Unit = println("[Stripe] Connection closed")
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/** PayPal implementation — demonstrates swappability. */
|
|
2333
|
+
final class PayPalGateway(config: GatewayConfig) extends PaymentGateway with AutoCloseable {
|
|
2334
|
+
println(s"[PayPal] Connected with merchant ${config.merchantId}")
|
|
2335
|
+
|
|
2336
|
+
def charge(amount: BigDecimal, currency: String): PaymentResult = {
|
|
2337
|
+
val txId = s"paypal_${System.nanoTime()}"
|
|
2338
|
+
PaymentResult(txId, success = true, s"Charged $currency $amount via PayPal")
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
def refund(transactionId: String): PaymentResult =
|
|
2342
|
+
PaymentResult(transactionId, success = true, s"Refunded $transactionId via PayPal")
|
|
2343
|
+
|
|
2344
|
+
def close(): Unit = println("[PayPal] Connection closed")
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* Checkout service that depends on the abstract [[PaymentGateway]] trait.
|
|
2349
|
+
*
|
|
2350
|
+
* This service is unaware of which gateway implementation is injected.
|
|
2351
|
+
*/
|
|
2352
|
+
final class CheckoutService(gateway: PaymentGateway) extends AutoCloseable {
|
|
2353
|
+
def processOrder(orderId: String, amount: BigDecimal): PaymentResult = {
|
|
2354
|
+
println(s"[Checkout] Processing order $orderId")
|
|
2355
|
+
gateway.charge(amount, "USD")
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
def close(): Unit = println("[Checkout] Service shutdown")
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
@main def pluginArchitectureExample(): Unit = {
|
|
2362
|
+
val config = GatewayConfig(apiKey = "sk_test_xxx", merchantId = "acme_corp")
|
|
2363
|
+
|
|
2364
|
+
println("=== Using Stripe Gateway ===")
|
|
2365
|
+
val stripeResource: Resource[CheckoutService] = Resource.from[CheckoutService](
|
|
2366
|
+
Wire(config),
|
|
2367
|
+
Wire.shared[StripeGateway] // Satisfies PaymentGateway via subtyping
|
|
2368
|
+
)
|
|
2369
|
+
|
|
2370
|
+
Scope.global.scoped { scope =>
|
|
2371
|
+
import scope._
|
|
2372
|
+
val checkout: $[CheckoutService] = allocate(stripeResource)
|
|
2373
|
+
$(checkout) { c =>
|
|
2374
|
+
val result = c.processOrder("ORD-001", BigDecimal("99.99"))
|
|
2375
|
+
println(s"Result: ${result.message}")
|
|
2376
|
+
}
|
|
2377
|
+
()
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
println("\n=== Using PayPal Gateway ===")
|
|
2381
|
+
val paypalResource: Resource[CheckoutService] = Resource.from[CheckoutService](
|
|
2382
|
+
Wire(config),
|
|
2383
|
+
Wire.shared[PayPalGateway] // Swap to PayPal — no other changes needed
|
|
2384
|
+
)
|
|
2385
|
+
|
|
2386
|
+
Scope.global.scoped { scope =>
|
|
2387
|
+
import scope._
|
|
2388
|
+
val checkout: $[CheckoutService] = allocate(paypalResource)
|
|
2389
|
+
$(checkout) { c =>
|
|
2390
|
+
val result = c.processOrder("ORD-002", BigDecimal("149.99"))
|
|
2391
|
+
println(s"Result: ${result.message}")
|
|
2392
|
+
}
|
|
2393
|
+
()
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
```
|
|
2397
|
+
|
|
2398
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/PluginArchitectureExample.scala))
|
|
2399
|
+
|
|
2400
|
+
```bash
|
|
2401
|
+
sbt "scope-examples/runMain scope.examples.pluginArchitectureExample"
|
|
2402
|
+
```
|
|
2403
|
+
|
|
2404
|
+
### Demonstrating Thread Ownership Enforcement in Scope Hierarchies
|
|
2405
|
+
|
|
2406
|
+
This example demonstrates how Scope enforces thread ownership, preventing cross-thread scope misuse and illustrating the difference between owned and unowned scopes.
|
|
2407
|
+
|
|
2408
|
+
```scala title="scope-examples/src/main/scala/scope/examples/ThreadOwnershipExample.scala"
|
|
2409
|
+
/*
|
|
2410
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
2411
|
+
*
|
|
2412
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2413
|
+
* you may not use this file except in compliance with the License.
|
|
2414
|
+
* You may obtain a copy of the License at
|
|
2415
|
+
*
|
|
2416
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2417
|
+
*
|
|
2418
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2419
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2420
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2421
|
+
* See the License for the specific language governing permissions and
|
|
2422
|
+
* limitations under the License.
|
|
2423
|
+
*/
|
|
2424
|
+
|
|
2425
|
+
package scope.examples
|
|
2426
|
+
|
|
2427
|
+
import zio.blocks.scope._
|
|
2428
|
+
import java.util.concurrent.{Executors, CountDownLatch}
|
|
2429
|
+
|
|
2430
|
+
/**
|
|
2431
|
+
* Simulates a stateful resource that tracks which thread owns it.
|
|
2432
|
+
*
|
|
2433
|
+
* @param name
|
|
2434
|
+
* the resource name
|
|
2435
|
+
*/
|
|
2436
|
+
final class ThreadAwareResource(val name: String) extends AutoCloseable {
|
|
2437
|
+
private val createdThread = Thread.currentThread()
|
|
2438
|
+
|
|
2439
|
+
def getInfo: String = {
|
|
2440
|
+
val currentThread = Thread.currentThread()
|
|
2441
|
+
val owner = createdThread.getName
|
|
2442
|
+
val current = currentThread.getName
|
|
2443
|
+
if (createdThread eq currentThread) {
|
|
2444
|
+
s"[$name] Safe: owned by '$owner', accessed by '$current' (same thread)"
|
|
2445
|
+
} else {
|
|
2446
|
+
s"[$name] WARNING: owned by '$owner', accessed by '$current' (different thread!)"
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
override def close(): Unit =
|
|
2451
|
+
println(s"[$name] Closing resource (was created by ${createdThread.getName})")
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Demonstrates thread ownership enforcement in ZIO Blocks Scope.
|
|
2456
|
+
*
|
|
2457
|
+
* This example shows:
|
|
2458
|
+
* - Scope.global: `isOwner` always true; any thread can create children from
|
|
2459
|
+
* it
|
|
2460
|
+
* - Scope.Child: captures the creating thread; `isOwner` checks
|
|
2461
|
+
* `Thread.currentThread() eq owner`
|
|
2462
|
+
* - Scope.open(): creates an unowned child scope; `isOwner` always true from
|
|
2463
|
+
* any thread
|
|
2464
|
+
* - Calling `scoped` on a Scope.Child from a different thread throws
|
|
2465
|
+
* IllegalStateException
|
|
2466
|
+
*
|
|
2467
|
+
* Thread ownership prevents accidentally passing a scope to another thread and
|
|
2468
|
+
* using it there, which would violate structured concurrency guarantees.
|
|
2469
|
+
*/
|
|
2470
|
+
@main def runThreadOwnershipExample(): Unit = {
|
|
2471
|
+
println("=== Thread Ownership Example ===\n")
|
|
2472
|
+
|
|
2473
|
+
// === Part 1: Single-thread usage (CORRECT) ===
|
|
2474
|
+
println("--- Part 1: Single-thread usage (correct) ---\n")
|
|
2475
|
+
|
|
2476
|
+
Scope.global.scoped { scope =>
|
|
2477
|
+
val currentThread = Thread.currentThread().getName
|
|
2478
|
+
println(s"[Main] Entered scope on thread: $currentThread\n")
|
|
2479
|
+
|
|
2480
|
+
// Scope.Child is owned by the current thread (main)
|
|
2481
|
+
scope.scoped { child =>
|
|
2482
|
+
import child._
|
|
2483
|
+
println(s"[Main] Created child scope on thread: $currentThread")
|
|
2484
|
+
println(s"[Main] Child scope isOwner: ${child.isOwner} (true only for the creating thread)")
|
|
2485
|
+
|
|
2486
|
+
val res: $[ThreadAwareResource] =
|
|
2487
|
+
allocate(Resource(new ThreadAwareResource("SingleThreadResource")))
|
|
2488
|
+
|
|
2489
|
+
$(res) { r =>
|
|
2490
|
+
println(s"[Main] ${r.getInfo}\n")
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
println(s"[Main] Child scope closed, finalizers ran\n")
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// === Part 2: Demonstrating Scope.open() for cross-thread usage ===
|
|
2498
|
+
println("--- Part 2: Unowned scope via open() (for cross-thread) ---\n")
|
|
2499
|
+
|
|
2500
|
+
Scope.global.scoped { scope =>
|
|
2501
|
+
import scope._
|
|
2502
|
+
val mainThread = Thread.currentThread().getName
|
|
2503
|
+
println(s"[Main] On thread: $mainThread\n")
|
|
2504
|
+
|
|
2505
|
+
// open() creates an unowned scope that any thread can use
|
|
2506
|
+
$(open()) { handle =>
|
|
2507
|
+
val childScope = handle.scope
|
|
2508
|
+
println(s"[Main] Created unowned scope via open()")
|
|
2509
|
+
println(s"[Main] Unowned scope isOwner: ${childScope.isOwner} (true from any thread)\n")
|
|
2510
|
+
|
|
2511
|
+
// Now we can use this scope from a different thread
|
|
2512
|
+
val executor = Executors.newSingleThreadExecutor { r =>
|
|
2513
|
+
val t = new Thread(r)
|
|
2514
|
+
t.setName("worker-thread")
|
|
2515
|
+
t
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
try {
|
|
2519
|
+
val latch = new CountDownLatch(1)
|
|
2520
|
+
|
|
2521
|
+
executor.execute { () =>
|
|
2522
|
+
try {
|
|
2523
|
+
val workerThread = Thread.currentThread().getName
|
|
2524
|
+
println(s"[Worker] On thread: $workerThread\n")
|
|
2525
|
+
|
|
2526
|
+
// Using the unowned scope from a different thread - this works!
|
|
2527
|
+
childScope.scoped { workerChild =>
|
|
2528
|
+
import workerChild._
|
|
2529
|
+
println(s"[Worker] Created child of unowned scope")
|
|
2530
|
+
|
|
2531
|
+
val res: $[ThreadAwareResource] =
|
|
2532
|
+
allocate(Resource(new ThreadAwareResource("CrossThreadResource")))
|
|
2533
|
+
|
|
2534
|
+
$(res) { r =>
|
|
2535
|
+
println(s"[Worker] ${r.getInfo}\n")
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
println("[Worker] Worker scope closed")
|
|
2539
|
+
}
|
|
2540
|
+
} finally {
|
|
2541
|
+
latch.countDown()
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// Wait for worker thread to finish
|
|
2546
|
+
latch.await()
|
|
2547
|
+
println()
|
|
2548
|
+
} finally {
|
|
2549
|
+
executor.shutdown()
|
|
2550
|
+
// Clean up the open scope and propagate any finalizer failures
|
|
2551
|
+
handle.close().orThrow()
|
|
2552
|
+
println("[Main] Unowned scope closed\n")
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// === Part 3: Explanation of ownership violation (what would fail) ===
|
|
2558
|
+
println("--- Part 3: Thread ownership violation (explanation) ---\n")
|
|
2559
|
+
println("""
|
|
2560
|
+
If you tried to pass a Scope.Child to another thread and call scoped on it,
|
|
2561
|
+
you would get an IllegalStateException:
|
|
2562
|
+
|
|
2563
|
+
Scope.global.scoped { scope =>
|
|
2564
|
+
import scope._
|
|
2565
|
+
|
|
2566
|
+
// This scope is owned by the main thread
|
|
2567
|
+
val executor = Executors.newSingleThreadExecutor()
|
|
2568
|
+
|
|
2569
|
+
executor.execute { () =>
|
|
2570
|
+
// This would throw: Cannot create child scope: current thread does not own this scope.
|
|
2571
|
+
scope.scoped { child => ... } // WRONG: scope is owned by main thread
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
Solution: Use scope.open() instead, which creates an unowned scope that
|
|
2576
|
+
any thread can use. See Part 2 above for the correct pattern.
|
|
2577
|
+
""")
|
|
2578
|
+
|
|
2579
|
+
println("=== Example Complete ===")
|
|
2580
|
+
}
|
|
2581
|
+
```
|
|
2582
|
+
|
|
2583
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ThreadOwnershipExample.scala))
|
|
2584
|
+
|
|
2585
|
+
```bash
|
|
2586
|
+
sbt "scope-examples/runMain runThreadOwnershipExample"
|
|
2587
|
+
```
|
|
2588
|
+
|
|
2589
|
+
### Detecting and Demonstrating Circular Dependency Scenarios
|
|
2590
|
+
|
|
2591
|
+
This example shows how to detect and handle circular dependencies in resource management, illustrating how scopes help prevent subtle bugs in complex dependency graphs.
|
|
2592
|
+
|
|
2593
|
+
```scala title="scope-examples/src/main/scala/scope/examples/CircularDependencyDemoExample.scala"
|
|
2594
|
+
/*
|
|
2595
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
2596
|
+
*
|
|
2597
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2598
|
+
* you may not use this file except in compliance with the License.
|
|
2599
|
+
* You may obtain a copy of the License at
|
|
2600
|
+
*
|
|
2601
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2602
|
+
*
|
|
2603
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2604
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2605
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2606
|
+
* See the License for the specific language governing permissions and
|
|
2607
|
+
* limitations under the License.
|
|
2608
|
+
*/
|
|
2609
|
+
|
|
2610
|
+
package scope.examples
|
|
2611
|
+
|
|
2612
|
+
import zio.blocks.scope._
|
|
2613
|
+
|
|
2614
|
+
/**
|
|
2615
|
+
* Demonstrates compile-time cycle detection in ZIO Blocks Scope.
|
|
2616
|
+
*
|
|
2617
|
+
* The `Resource.from[T]` macro analyzes the dependency graph at compile time
|
|
2618
|
+
* and rejects circular dependencies with a descriptive error message showing
|
|
2619
|
+
* the exact cycle path.
|
|
2620
|
+
*
|
|
2621
|
+
* ==The Problem==
|
|
2622
|
+
* Circular dependencies (A → B → A) cannot be resolved by constructor injection
|
|
2623
|
+
* because neither service can be instantiated without the other already
|
|
2624
|
+
* existing.
|
|
2625
|
+
*
|
|
2626
|
+
* ==The Solution==
|
|
2627
|
+
* Break the cycle by introducing an interface (trait) that one service depends
|
|
2628
|
+
* on, allowing the implementation to be provided separately. This is a standard
|
|
2629
|
+
* Dependency Inversion Principle pattern.
|
|
2630
|
+
*/
|
|
2631
|
+
|
|
2632
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2633
|
+
// PROBLEMATIC: Circular Dependency (would not compile)
|
|
2634
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2635
|
+
|
|
2636
|
+
// These classes form a cycle: ServiceA → ServiceB → ServiceA
|
|
2637
|
+
// Uncommenting the Resource.from call below would produce a compile error.
|
|
2638
|
+
|
|
2639
|
+
// class ServiceA(b: ServiceB) {
|
|
2640
|
+
// def greet(): String = s"A says hello, B says: ${b.respond()}"
|
|
2641
|
+
// }
|
|
2642
|
+
//
|
|
2643
|
+
// class ServiceB(a: ServiceA) {
|
|
2644
|
+
// def respond(): String = s"B responds, A type: ${a.getClass.getSimpleName}"
|
|
2645
|
+
// }
|
|
2646
|
+
//
|
|
2647
|
+
// Attempting to wire this would fail at compile time:
|
|
2648
|
+
// val circularResource = Resource.from[ServiceA]()
|
|
2649
|
+
//
|
|
2650
|
+
// Expected compile error:
|
|
2651
|
+
// ┌────────────────────────────┐
|
|
2652
|
+
// │ ▼
|
|
2653
|
+
// ServiceA ──► ServiceB
|
|
2654
|
+
// ▲ │
|
|
2655
|
+
// └────────────────────────────┘
|
|
2656
|
+
//
|
|
2657
|
+
// Break the cycle by:
|
|
2658
|
+
// • Introducing an interface/trait
|
|
2659
|
+
// • Using lazy initialization
|
|
2660
|
+
// • Restructuring dependencies
|
|
2661
|
+
|
|
2662
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2663
|
+
// SOLUTION: Break the cycle with an interface
|
|
2664
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2665
|
+
|
|
2666
|
+
/** Interface that ServiceA depends on, breaking the compile-time cycle. */
|
|
2667
|
+
trait ServiceBApi {
|
|
2668
|
+
def respond(): String
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
/** Concrete implementation of ServiceA that depends only on the interface. */
|
|
2672
|
+
class ServiceAImpl(b: ServiceBApi) {
|
|
2673
|
+
def greet(): String = s"A says hello, B says: ${b.respond()}"
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
/** Concrete implementation of ServiceB without any dependency on A. */
|
|
2677
|
+
class ServiceBImpl extends ServiceBApi {
|
|
2678
|
+
override def respond(): String = "B responds successfully"
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
/** Application that composes both services. */
|
|
2682
|
+
class Application(a: ServiceAImpl, @annotation.unused b: ServiceBApi) {
|
|
2683
|
+
def run(): String = a.greet()
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* Demonstrates the working (non-circular) pattern.
|
|
2688
|
+
*
|
|
2689
|
+
* The dependency graph is now: Application → ServiceAImpl → ServiceBApi ↘
|
|
2690
|
+
* ServiceBApi
|
|
2691
|
+
*
|
|
2692
|
+
* ServiceBImpl provides ServiceBApi, and there is no cycle.
|
|
2693
|
+
*/
|
|
2694
|
+
@main def circularDependencyDemoExample(): Unit = {
|
|
2695
|
+
println("=== Circular Dependency Demo ===\n")
|
|
2696
|
+
println("Demonstrating compile-time cycle detection and how to break cycles.\n")
|
|
2697
|
+
|
|
2698
|
+
Scope.global.scoped { scope =>
|
|
2699
|
+
import scope._
|
|
2700
|
+
println("Creating application with proper dependency structure...")
|
|
2701
|
+
|
|
2702
|
+
// Wire.shared[ServiceBImpl] provides both ServiceBImpl and ServiceBApi (via subtyping)
|
|
2703
|
+
val app: $[Application] = allocate(
|
|
2704
|
+
Resource.from[Application](
|
|
2705
|
+
Wire.shared[ServiceBImpl]
|
|
2706
|
+
)
|
|
2707
|
+
)
|
|
2708
|
+
|
|
2709
|
+
println(s"Result: ${$(app)(_.run())}")
|
|
2710
|
+
println("\nThe dependency graph was validated at compile time.")
|
|
2711
|
+
println("No cycles detected - application wired successfully.")
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
println("\n─── Key Takeaways ───")
|
|
2715
|
+
println("• Resource.from[T] detects cycles at compile time")
|
|
2716
|
+
println("• Cycles produce clear ASCII diagrams showing the path")
|
|
2717
|
+
println("• Break cycles by introducing interfaces/traits")
|
|
2718
|
+
println("• The Dependency Inversion Principle resolves most cycles")
|
|
2719
|
+
}
|
|
2720
|
+
```
|
|
2721
|
+
|
|
2722
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/CircularDependencyDemoExample.scala))
|
|
2723
|
+
|
|
2724
|
+
```bash
|
|
2725
|
+
sbt "scope-examples/runMain scope.examples.circularDependencyDemoExample"
|
|
2726
|
+
```
|
|
2727
|
+
|
|
2728
|
+
### Using Scope with Legacy Libraries that Don't Support Managed Resources
|
|
2729
|
+
|
|
2730
|
+
This example demonstrates how to integrate Scope with legacy libraries that don't natively support resource management, using wrapper resources and the `leak` escape hatch when necessary.
|
|
2731
|
+
|
|
2732
|
+
```scala title="scope-examples/src/main/scala/scope/examples/LegacyLibraryInteropExample.scala"
|
|
2733
|
+
/*
|
|
2734
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
2735
|
+
*
|
|
2736
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2737
|
+
* you may not use this file except in compliance with the License.
|
|
2738
|
+
* You may obtain a copy of the License at
|
|
2739
|
+
*
|
|
2740
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2741
|
+
*
|
|
2742
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2743
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2744
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2745
|
+
* See the License for the specific language governing permissions and
|
|
2746
|
+
* limitations under the License.
|
|
2747
|
+
*/
|
|
2748
|
+
|
|
2749
|
+
package scope.examples
|
|
2750
|
+
|
|
2751
|
+
import zio.blocks.scope._
|
|
2752
|
+
|
|
2753
|
+
import scala.annotation.nowarn
|
|
2754
|
+
|
|
2755
|
+
/**
|
|
2756
|
+
* Demonstrates `leak(scopedValue)` for third-party library interop.
|
|
2757
|
+
*
|
|
2758
|
+
* Sometimes you must pass scoped resources to legacy or third-party libraries
|
|
2759
|
+
* that require raw types. The `leak` escape hatch extracts the underlying
|
|
2760
|
+
* value, bypassing compile-time safety. Use sparingly—you assume responsibility
|
|
2761
|
+
* for ensuring the resource outlives its usage.
|
|
2762
|
+
*/
|
|
2763
|
+
|
|
2764
|
+
// ---------------------------------------------------------------------------
|
|
2765
|
+
// Fake classes simulating a legacy networking library
|
|
2766
|
+
// ---------------------------------------------------------------------------
|
|
2767
|
+
|
|
2768
|
+
/** Configuration for establishing a socket connection. */
|
|
2769
|
+
case class SocketConfig(host: String, port: Int)
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* A managed socket that implements AutoCloseable.
|
|
2773
|
+
*
|
|
2774
|
+
* In a real scenario, this would wrap an actual network socket.
|
|
2775
|
+
*/
|
|
2776
|
+
class ManagedSocket(val config: SocketConfig) extends AutoCloseable {
|
|
2777
|
+
private var closed = false
|
|
2778
|
+
|
|
2779
|
+
def send(data: Array[Byte]): Unit =
|
|
2780
|
+
if (closed) throw new IllegalStateException("Socket closed")
|
|
2781
|
+
else println(s" [Socket] Sent ${data.length} bytes to ${config.host}:${config.port}")
|
|
2782
|
+
|
|
2783
|
+
def receive(): Array[Byte] =
|
|
2784
|
+
if (closed) throw new IllegalStateException("Socket closed")
|
|
2785
|
+
else {
|
|
2786
|
+
println(s" [Socket] Received response from ${config.host}:${config.port}")
|
|
2787
|
+
"ACK".getBytes
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
override def close(): Unit = {
|
|
2791
|
+
closed = true
|
|
2792
|
+
println(s" [Socket] Connection to ${config.host}:${config.port} closed")
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
/**
|
|
2797
|
+
* Simulates a third-party protocol handler that cannot be modified.
|
|
2798
|
+
*
|
|
2799
|
+
* This legacy library requires a raw `ManagedSocket` and does not understand
|
|
2800
|
+
* scoped types. This is the typical scenario where `leak` becomes necessary.
|
|
2801
|
+
*/
|
|
2802
|
+
object LegacyProtocolHandler {
|
|
2803
|
+
|
|
2804
|
+
/**
|
|
2805
|
+
* Handles a connection using a proprietary protocol.
|
|
2806
|
+
*
|
|
2807
|
+
* @param socket
|
|
2808
|
+
* the raw socket—must remain open for the duration of this call
|
|
2809
|
+
*/
|
|
2810
|
+
def handleConnection(socket: ManagedSocket): Unit = {
|
|
2811
|
+
println(" [Legacy] Starting proprietary protocol handshake...")
|
|
2812
|
+
socket.send("HELLO".getBytes)
|
|
2813
|
+
val response = socket.receive()
|
|
2814
|
+
println(s" [Legacy] Handshake complete: ${new String(response)}")
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// ---------------------------------------------------------------------------
|
|
2819
|
+
// Example entry point
|
|
2820
|
+
// ---------------------------------------------------------------------------
|
|
2821
|
+
|
|
2822
|
+
@main def legacyLibraryInteropExample(): Unit = {
|
|
2823
|
+
println("=== Legacy Library Interop Example ===\n")
|
|
2824
|
+
println("Demonstrating leak() for passing scoped resources to third-party code.\n")
|
|
2825
|
+
|
|
2826
|
+
Scope.global.scoped { scope =>
|
|
2827
|
+
import scope._
|
|
2828
|
+
// Allocate the socket as a scoped resource.
|
|
2829
|
+
// The socket is tagged with the scope's type, preventing accidental escape.
|
|
2830
|
+
val scopedSocket: $[ManagedSocket] = allocate(
|
|
2831
|
+
Resource.fromAutoCloseable(new ManagedSocket(SocketConfig("api.example.com", 443)))
|
|
2832
|
+
)
|
|
2833
|
+
println("Allocated scoped socket.\n")
|
|
2834
|
+
|
|
2835
|
+
// -------------------------------------------------------------------------
|
|
2836
|
+
// WARNING: leak() bypasses compile-time safety guarantees!
|
|
2837
|
+
//
|
|
2838
|
+
// After calling leak(), the compiler cannot prevent you from:
|
|
2839
|
+
// - Storing the socket in a field that outlives the scope
|
|
2840
|
+
// - Passing it to code that might cache or close it unexpectedly
|
|
2841
|
+
// - Using it after the scope has closed
|
|
2842
|
+
//
|
|
2843
|
+
// Only use leak() when:
|
|
2844
|
+
// 1. The third-party API genuinely cannot accept scoped types
|
|
2845
|
+
// 2. You can guarantee the scope outlives all usage of the leaked value
|
|
2846
|
+
// 3. The third-party code won't cache or transfer ownership
|
|
2847
|
+
// -------------------------------------------------------------------------
|
|
2848
|
+
|
|
2849
|
+
// WARNING: leak() bypasses compile-time safety — use only for third-party interop.
|
|
2850
|
+
// This intentionally escapes the scoped type and will emit a compiler warning.
|
|
2851
|
+
@nowarn("msg=.*leaked.*|.*leak.*")
|
|
2852
|
+
val rawSocket: ManagedSocket = leak(scopedSocket)
|
|
2853
|
+
|
|
2854
|
+
println("Passing raw socket to legacy protocol handler:")
|
|
2855
|
+
LegacyProtocolHandler.handleConnection(rawSocket)
|
|
2856
|
+
|
|
2857
|
+
println("\nScope exiting - socket will be closed automatically:")
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
println("\nExample complete. The socket was safely closed when the scope exited.")
|
|
2861
|
+
}
|
|
2862
|
+
```
|
|
2863
|
+
|
|
2864
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/LegacyLibraryInteropExample.scala))
|
|
2865
|
+
|
|
2866
|
+
```bash
|
|
2867
|
+
sbt "scope-examples/runMain scope.examples.legacyLibraryInteropExample"
|
|
2868
|
+
```
|
|
2869
|
+
|
|
2870
|
+
### Integration Testing with Automatic Setup and Teardown
|
|
2871
|
+
|
|
2872
|
+
This example shows how to use Scope to manage test fixtures and resources in integration tests, ensuring automatic cleanup between test runs and proper resource finalization.
|
|
2873
|
+
|
|
2874
|
+
```scala title="scope-examples/src/main/scala/scope/examples/IntegrationTestHarnessExample.scala"
|
|
2875
|
+
/*
|
|
2876
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
2877
|
+
*
|
|
2878
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2879
|
+
* you may not use this file except in compliance with the License.
|
|
2880
|
+
* You may obtain a copy of the License at
|
|
2881
|
+
*
|
|
2882
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2883
|
+
*
|
|
2884
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2885
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2886
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2887
|
+
* See the License for the specific language governing permissions and
|
|
2888
|
+
* limitations under the License.
|
|
2889
|
+
*/
|
|
2890
|
+
|
|
2891
|
+
package scope.examples
|
|
2892
|
+
|
|
2893
|
+
import zio.blocks.scope._
|
|
2894
|
+
|
|
2895
|
+
/**
|
|
2896
|
+
* Demonstrates combining DI-wired services with manually-managed test fixtures.
|
|
2897
|
+
*
|
|
2898
|
+
* This example shows a realistic integration test setup where:
|
|
2899
|
+
* - Application services are wired via [[Resource.from]] (DI approach)
|
|
2900
|
+
* - Test fixtures are managed with [[Resource.acquireRelease]] (manual
|
|
2901
|
+
* approach)
|
|
2902
|
+
* - Both resource types are properly cleaned up in LIFO order
|
|
2903
|
+
*/
|
|
2904
|
+
object IntegrationTestHarnessExample {
|
|
2905
|
+
|
|
2906
|
+
// --- Test Infrastructure ---
|
|
2907
|
+
|
|
2908
|
+
/** Configuration for the test environment. */
|
|
2909
|
+
case class TestConfig(dbUrl: String, serverPort: Int)
|
|
2910
|
+
|
|
2911
|
+
/** Test database with lifecycle hooks for setup/teardown and data seeding. */
|
|
2912
|
+
class TestDatabase(val config: TestConfig) extends AutoCloseable {
|
|
2913
|
+
private var data = Map.empty[String, Any]
|
|
2914
|
+
|
|
2915
|
+
def setup(): Unit = println(s" [DB] Initialized at ${config.dbUrl}")
|
|
2916
|
+
def teardown(): Unit = { data = Map.empty; println(" [DB] Data cleared") }
|
|
2917
|
+
def seed(newData: Map[String, Any]): Unit = {
|
|
2918
|
+
data = newData; println(s" [DB] Seeded with ${newData.size} entries")
|
|
2919
|
+
}
|
|
2920
|
+
def query(key: String): Option[Any] = data.get(key)
|
|
2921
|
+
def close(): Unit = println(" [DB] Connection closed")
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
/** Test HTTP server that can be started and stopped. */
|
|
2925
|
+
class TestServer(val config: TestConfig) extends AutoCloseable {
|
|
2926
|
+
val baseUrl: String = s"http://localhost:${config.serverPort}"
|
|
2927
|
+
|
|
2928
|
+
def start(): Unit = println(s" [Server] Started at $baseUrl")
|
|
2929
|
+
def stop(): Unit = println(" [Server] Stopped")
|
|
2930
|
+
def close(): Unit = println(" [Server] Resources released")
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
/** Aggregates test fixtures for convenient access during tests. */
|
|
2934
|
+
case class TestFixture(db: TestDatabase, server: TestServer)
|
|
2935
|
+
|
|
2936
|
+
// --- Application Under Test ---
|
|
2937
|
+
|
|
2938
|
+
/** The application being tested; requires a database connection. */
|
|
2939
|
+
class AppUnderTest(val db: TestDatabase) extends AutoCloseable {
|
|
2940
|
+
def handleRequest(req: String): String = db.query(req).map(_.toString).getOrElse("Not found")
|
|
2941
|
+
def close(): Unit = println(" [App] Shutdown complete")
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// --- Resource Definitions ---
|
|
2945
|
+
|
|
2946
|
+
/**
|
|
2947
|
+
* Creates a manually-managed test fixture using [[Resource.acquireRelease]].
|
|
2948
|
+
*
|
|
2949
|
+
* This approach gives explicit control over setup and teardown phases, which
|
|
2950
|
+
* is typical for test fixtures that need initialization beyond construction.
|
|
2951
|
+
*/
|
|
2952
|
+
def testFixtureResource(config: TestConfig): Resource[TestFixture] =
|
|
2953
|
+
Resource.acquireRelease {
|
|
2954
|
+
println(" [Fixture] Acquiring test fixture...")
|
|
2955
|
+
val db = new TestDatabase(config)
|
|
2956
|
+
val server = new TestServer(config)
|
|
2957
|
+
db.setup()
|
|
2958
|
+
server.start()
|
|
2959
|
+
TestFixture(db, server)
|
|
2960
|
+
} { fixture =>
|
|
2961
|
+
println(" [Fixture] Releasing test fixture...")
|
|
2962
|
+
fixture.server.stop()
|
|
2963
|
+
fixture.db.teardown()
|
|
2964
|
+
fixture.db.close()
|
|
2965
|
+
fixture.server.close()
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/**
|
|
2969
|
+
* Runs the integration test harness example.
|
|
2970
|
+
*
|
|
2971
|
+
* Demonstrates:
|
|
2972
|
+
* 1. Manual fixture via [[Resource.acquireRelease]] for test infrastructure
|
|
2973
|
+
* 2. DI-wired application via [[Resource.from]] consuming the fixture
|
|
2974
|
+
* 3. Proper cleanup ordering: app closes before fixtures
|
|
2975
|
+
*/
|
|
2976
|
+
def run(): Unit = {
|
|
2977
|
+
println("=== Integration Test Harness Example ===\n")
|
|
2978
|
+
|
|
2979
|
+
val config = TestConfig("jdbc:h2:mem:test", 8080)
|
|
2980
|
+
|
|
2981
|
+
// Combine manual fixtures with DI-wired application
|
|
2982
|
+
val testHarnessResource: Resource[(TestFixture, AppUnderTest)] =
|
|
2983
|
+
testFixtureResource(config).flatMap { fixture =>
|
|
2984
|
+
// Seed test data
|
|
2985
|
+
fixture.db.seed(Map("user:1" -> "Alice", "user:2" -> "Bob"))
|
|
2986
|
+
|
|
2987
|
+
// Wire the app using DI, injecting the fixture's database
|
|
2988
|
+
val appWire = Wire.shared[AppUnderTest]
|
|
2989
|
+
val dbWire = Wire(fixture.db)
|
|
2990
|
+
val appResource = Resource.from[AppUnderTest](appWire, dbWire)
|
|
2991
|
+
|
|
2992
|
+
appResource.map(app => (fixture, app))
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// Run in a scoped block - all resources cleaned up on exit
|
|
2996
|
+
Scope.global.scoped { scope =>
|
|
2997
|
+
import scope._
|
|
2998
|
+
println("Allocating resources...")
|
|
2999
|
+
val harness: $[(TestFixture, AppUnderTest)] = allocate(testHarnessResource)
|
|
3000
|
+
println()
|
|
3001
|
+
|
|
3002
|
+
// Run test scenarios - access the tuple via $
|
|
3003
|
+
println("Running test scenarios:")
|
|
3004
|
+
$(harness) { h =>
|
|
3005
|
+
println(s" GET user:1 -> ${h._2.handleRequest("user:1")}")
|
|
3006
|
+
println(s" GET user:2 -> ${h._2.handleRequest("user:2")}")
|
|
3007
|
+
println(s" GET user:3 -> ${h._2.handleRequest("user:3")}")
|
|
3008
|
+
println(s" Server URL: ${h._1.server.baseUrl}")
|
|
3009
|
+
}
|
|
3010
|
+
println()
|
|
3011
|
+
|
|
3012
|
+
println("Scope closing, releasing resources in LIFO order...")
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
println("\n=== Example Complete ===")
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
@main def runIntegrationTestHarness(): Unit = IntegrationTestHarnessExample.run()
|
|
3020
|
+
```
|
|
3021
|
+
|
|
3022
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/IntegrationTestHarnessExample.scala))
|
|
3023
|
+
|
|
3024
|
+
```bash
|
|
3025
|
+
sbt "scope-examples/runMain scope.examples.IntegrationTestHarnessExample"
|
|
3026
|
+
```
|