@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.
@@ -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
+ ```