@zio.dev/zio-blocks 0.0.28 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +62 -16
- package/package.json +1 -1
- package/path-interpolator.md +70 -9
- package/reference/allows.md +96 -0
- package/reference/codec.md +8 -8
- package/reference/combinators.md +345 -0
- package/reference/context.md +639 -67
- package/reference/docs.md +1 -1
- package/reference/http-model.md +1716 -0
- package/reference/json-differ.md +320 -0
- package/reference/json-patch.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/resource-management-di/index.md +49 -0
- package/reference/resource-management-di/resource.md +1125 -0
- package/{scope.md → reference/resource-management-di/scope.md} +1 -1
- package/reference/resource-management-di/wire.md +832 -0
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/type-class-derivation.md +31 -31
- package/ringbuffer.md +249 -0
- package/sidebars.js +14 -2
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: resource
|
|
3
|
+
title: "Resource"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Resource[A]` is a **lazy recipe for managing resource lifecycles**, encapsulating both acquisition and finalization tied to a `Scope`. Resources describe *what* to do, not *when* — creation only happens when a resource is passed to `scope.allocate()`. They compose naturally with `map`, `flatMap`, and `zip` to build complex dependency graphs with automatic cleanup in LIFO order.
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
sealed trait Resource[+A] {
|
|
10
|
+
def map[B](f: A => B): Resource[B]
|
|
11
|
+
def flatMap[B](f: A => Resource[B]): Resource[B]
|
|
12
|
+
def zip[B](that: Resource[B]): Resource[(A, B)]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Key properties:
|
|
17
|
+
- **Lazy**: Resources don't acquire anything until allocated via `scope.allocate()`
|
|
18
|
+
- **Covariant**: `Resource[Dog]` is a subtype of `Resource[Animal]` when `Dog <: Animal`
|
|
19
|
+
- **Composable**: `map`, `flatMap`, and `zip` combine resources into larger structures
|
|
20
|
+
- **Two strategies**: Shared (memoized with reference counting) and Unique (fresh per allocation)
|
|
21
|
+
- **Auto-cleanup**: Finalizers run automatically in LIFO order when scopes close
|
|
22
|
+
|
|
23
|
+
## Motivation
|
|
24
|
+
|
|
25
|
+
Without Resources, managing complex initialization and cleanup is tedious and error-prone. Resources eliminate manual bookkeeping by tying value lifecycles to Scopes and registering finalizers automatically.
|
|
26
|
+
|
|
27
|
+
**Benefits:**
|
|
28
|
+
- Automatic cleanup even on exceptions
|
|
29
|
+
- LIFO finalization order (inner resources close before outer ones)
|
|
30
|
+
- Compositional: build complex dependency graphs declaratively
|
|
31
|
+
- Type-safe: compiler ensures you have dependencies available
|
|
32
|
+
- Works seamlessly with `Wire` for constructor-based dependency injection
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Add the ZIO Blocks Scope module to your `build.sbt`:
|
|
37
|
+
|
|
38
|
+
```scala
|
|
39
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "0.0.29"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For cross-platform (Scala.js):
|
|
43
|
+
|
|
44
|
+
```scala
|
|
45
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "0.0.29"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Supported Scala versions: 2.13.x and 3.x.
|
|
49
|
+
|
|
50
|
+
## Construction
|
|
51
|
+
|
|
52
|
+
Resources can be created in several ways: from values, from explicit acquire/release pairs, from `AutoCloseable` types, or from custom functions.
|
|
53
|
+
|
|
54
|
+
### `Resource.apply` — wrap a value
|
|
55
|
+
|
|
56
|
+
Wraps a by-name value as a resource. If the value implements `AutoCloseable`, its `close()` method is automatically registered as a finalizer.
|
|
57
|
+
|
|
58
|
+
```scala
|
|
59
|
+
import zio.blocks.scope._
|
|
60
|
+
|
|
61
|
+
case class Config(debug: Boolean)
|
|
62
|
+
|
|
63
|
+
class Database(val name: String) extends AutoCloseable {
|
|
64
|
+
def close(): Unit = println(s"Closing database $name")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
val configResource = Resource(Config(debug = true))
|
|
68
|
+
val dbResource = Resource(new Database("mydb"))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `Resource.acquireRelease` — explicit lifecycle
|
|
72
|
+
|
|
73
|
+
Creates a resource with separate acquire and release functions. The acquire thunk runs during allocation; the release function is registered as a finalizer:
|
|
74
|
+
|
|
75
|
+
```scala
|
|
76
|
+
import zio.blocks.scope._
|
|
77
|
+
import java.io.FileInputStream
|
|
78
|
+
|
|
79
|
+
val fileResource = Resource.acquireRelease {
|
|
80
|
+
new FileInputStream("data.txt")
|
|
81
|
+
} { stream =>
|
|
82
|
+
stream.close()
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `Resource.fromAutoCloseable` — type-safe wrapping
|
|
87
|
+
|
|
88
|
+
Creates a resource specifically for `AutoCloseable` subtypes. This is a compile-time verified alternative to `Resource(value)` when you know the value is closeable:
|
|
89
|
+
|
|
90
|
+
```scala
|
|
91
|
+
import zio.blocks.scope._
|
|
92
|
+
import java.io.BufferedInputStream
|
|
93
|
+
import java.io.FileInputStream
|
|
94
|
+
|
|
95
|
+
val streamResource = Resource.fromAutoCloseable {
|
|
96
|
+
new BufferedInputStream(new FileInputStream("data.bin"))
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `Resource.shared` — memoized with reference counting
|
|
101
|
+
|
|
102
|
+
Creates a shared resource that memoizes its value across multiple allocations. The first call initializes the value; subsequent calls return the same instance with reference counting. Finalizers run only when the last reference is released:
|
|
103
|
+
|
|
104
|
+
```scala
|
|
105
|
+
import zio.blocks.scope._
|
|
106
|
+
|
|
107
|
+
var initCount = 0
|
|
108
|
+
|
|
109
|
+
val sharedResource = Resource.shared[Int] { _ =>
|
|
110
|
+
initCount += 1
|
|
111
|
+
initCount
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `Resource.unique` — fresh instances
|
|
116
|
+
|
|
117
|
+
Creates a unique resource that produces a fresh instance each time it's allocated. Use for per-request state or resources that should never be shared:
|
|
118
|
+
|
|
119
|
+
```scala
|
|
120
|
+
import zio.blocks.scope._
|
|
121
|
+
|
|
122
|
+
var counter = 0
|
|
123
|
+
|
|
124
|
+
val uniqueResource = Resource.unique[Int] { _ =>
|
|
125
|
+
counter += 1
|
|
126
|
+
counter
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Core Operations
|
|
131
|
+
|
|
132
|
+
Resources support transformation and composition through `map`, `flatMap`, and `zip`.
|
|
133
|
+
|
|
134
|
+
### `Resource#map` — transform the value
|
|
135
|
+
|
|
136
|
+
Transforms the value produced by a resource without affecting finalization. The transformation function is applied after the resource is acquired.
|
|
137
|
+
|
|
138
|
+
```scala
|
|
139
|
+
import zio.blocks.scope._
|
|
140
|
+
|
|
141
|
+
val portResource = Resource(8080)
|
|
142
|
+
val urlResource = portResource.map(port => s"http://localhost:$port")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `Resource#flatMap` — sequence resources
|
|
146
|
+
|
|
147
|
+
Sequences two resources, using the result of the first to create the second. Both sets of finalizers are registered and run in LIFO order (inner before outer):
|
|
148
|
+
|
|
149
|
+
```scala
|
|
150
|
+
import zio.blocks.scope._
|
|
151
|
+
|
|
152
|
+
case class DbConfig(url: String)
|
|
153
|
+
|
|
154
|
+
class Database(config: DbConfig) extends AutoCloseable {
|
|
155
|
+
def query(sql: String): String = s"Result from ${config.url}: $sql"
|
|
156
|
+
def close(): Unit = println("Database closed")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
val configResource = Resource(DbConfig("jdbc:postgres://localhost"))
|
|
160
|
+
val dbResource = configResource.flatMap { config =>
|
|
161
|
+
Resource.fromAutoCloseable(new Database(config))
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `Resource#zip` — combine resources
|
|
166
|
+
|
|
167
|
+
Combines two resources into a single resource that produces a tuple of both values. Both resources are acquired and both sets of finalizers are registered:
|
|
168
|
+
|
|
169
|
+
```scala
|
|
170
|
+
import zio.blocks.scope._
|
|
171
|
+
|
|
172
|
+
case class DbConfig(url: String)
|
|
173
|
+
|
|
174
|
+
class Database(config: DbConfig) extends AutoCloseable {
|
|
175
|
+
def close(): Unit = println("Database closed")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class Cache extends AutoCloseable {
|
|
179
|
+
def close(): Unit = println("Cache closed")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
val dbResource = Resource.fromAutoCloseable(new Database(DbConfig("jdbc:postgres://localhost")))
|
|
183
|
+
val cacheResource = Resource.fromAutoCloseable(new Cache())
|
|
184
|
+
val combined = dbResource.zip(cacheResource)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Shared vs. Unique
|
|
188
|
+
|
|
189
|
+
The fundamental difference is **reuse semantics**:
|
|
190
|
+
|
|
191
|
+
| Aspect | Shared | Unique |
|
|
192
|
+
|-------------------|-----------------------------------|----------------------------------------------|
|
|
193
|
+
| **Creation** | `Resource.shared(f)` | `Resource.unique(f)` or `Resource(value)` |
|
|
194
|
+
| **Memoization** | Yes, with reference counting | No, fresh per allocation |
|
|
195
|
+
| **When to use** | Expensive resources (DB connections, thread pools) | Per-request state, stateful handlers |
|
|
196
|
+
| **Instance reuse** | Same instance across nested scopes | New instance per allocation |
|
|
197
|
+
| **Finalization** | Runs when last reference released | Runs when scope closes |
|
|
198
|
+
|
|
199
|
+
In a diamond dependency pattern (where `AppService` depends on both `UserService` and `OrderService`, both depending on `Database`), using `Resource.shared[Database]` ensures both services receive the same instance.
|
|
200
|
+
|
|
201
|
+
## Integration with Wire and Scope
|
|
202
|
+
|
|
203
|
+
`Resource` is the foundation of ZIO Blocks' dependency injection. `Wire` describes how to build a service; `Resource` describes how to manage its lifecycle. When used together with the `Resource.from[T]` macro, they enable compile-safe automatic dependency injection:
|
|
204
|
+
|
|
205
|
+
```scala
|
|
206
|
+
import zio.blocks.scope._
|
|
207
|
+
|
|
208
|
+
case class Config(debug: Boolean)
|
|
209
|
+
|
|
210
|
+
class Logger(config: Config) {
|
|
211
|
+
def log(msg: String): Unit = println(s"[${config.debug}] $msg")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class Service(logger: Logger) extends AutoCloseable {
|
|
215
|
+
def run(): Unit = logger.log("Running")
|
|
216
|
+
def close(): Unit = logger.log("Shutting down")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
val serviceResource = Resource.from[Service](
|
|
220
|
+
Wire(Config(debug = true))
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
See [`Wire`](./wire.md) for how to declare dependency recipes and [`Scope`](./scope.md) for scope-based resource management.
|
|
225
|
+
|
|
226
|
+
## Running the Examples
|
|
227
|
+
|
|
228
|
+
All code from this guide is available as runnable examples in the `scope-examples` module.
|
|
229
|
+
|
|
230
|
+
**1. Clone the repository and navigate to the project:**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
234
|
+
cd zio-blocks
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**2. Run individual examples with sbt:**
|
|
238
|
+
|
|
239
|
+
**Basic lifecycle management with temporary files**
|
|
240
|
+
|
|
241
|
+
```scala title="scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala"
|
|
242
|
+
/*
|
|
243
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
244
|
+
*
|
|
245
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
246
|
+
* you may not use this file except in compliance with the License.
|
|
247
|
+
* You may obtain a copy of the License at
|
|
248
|
+
*
|
|
249
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
250
|
+
*
|
|
251
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
252
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
253
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
254
|
+
* See the License for the specific language governing permissions and
|
|
255
|
+
* limitations under the License.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
package scope.examples
|
|
259
|
+
|
|
260
|
+
import zio.blocks.scope._
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Demonstrates `scope.defer(...)` for registering manual cleanup actions.
|
|
264
|
+
*
|
|
265
|
+
* This example shows how to create temporary files during processing and ensure
|
|
266
|
+
* they are deleted when the scope exits—even if processing fails. Deferred
|
|
267
|
+
* cleanup actions run in LIFO (last-in-first-out) order.
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
/** Represents a temporary file with basic read/write operations. */
|
|
271
|
+
case class TempFile(path: String) {
|
|
272
|
+
private var content: String = ""
|
|
273
|
+
|
|
274
|
+
def write(data: String): Unit = content = data
|
|
275
|
+
def read(): String = content
|
|
276
|
+
def delete(): Boolean = { println(s" Deleting: $path"); true }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Result of processing temporary files. */
|
|
280
|
+
case class ProcessingResult(processedCount: Int, totalBytes: Long, errors: List[String])
|
|
281
|
+
|
|
282
|
+
/** Processes a list of temporary files and aggregates results. */
|
|
283
|
+
object FileProcessor {
|
|
284
|
+
def process(files: List[TempFile]): ProcessingResult = {
|
|
285
|
+
val totalBytes = files.map(_.read().length.toLong).sum
|
|
286
|
+
ProcessingResult(processedCount = files.size, totalBytes = totalBytes, errors = Nil)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@main def tempFileHandlingExample(): Unit = {
|
|
291
|
+
println("=== Temp File Handling Example ===\n")
|
|
292
|
+
println("Demonstrating scope.defer() for manual cleanup registration.\n")
|
|
293
|
+
|
|
294
|
+
val result = Scope.global.scoped { scope =>
|
|
295
|
+
// Create temp files and register cleanup via defer.
|
|
296
|
+
// Cleanup runs in LIFO order: file3, file2, file1.
|
|
297
|
+
|
|
298
|
+
val file1 = createTempFile(scope, "/tmp/data-001.tmp", "First file content")
|
|
299
|
+
val file2 = createTempFile(scope, "/tmp/data-002.tmp", "Second file - more data here")
|
|
300
|
+
val file3 = createTempFile(scope, "/tmp/data-003.tmp", "Third file with the most content of all")
|
|
301
|
+
|
|
302
|
+
println("\nProcessing files...")
|
|
303
|
+
val processingResult = FileProcessor.process(List(file1, file2, file3))
|
|
304
|
+
println(s"Processed ${processingResult.processedCount} files, ${processingResult.totalBytes} bytes\n")
|
|
305
|
+
|
|
306
|
+
println("Exiting scope - cleanup runs in LIFO order:")
|
|
307
|
+
processingResult
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
println(s"\nFinal result: $result")
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Creates a temporary file and registers its cleanup with the scope.
|
|
315
|
+
*
|
|
316
|
+
* The cleanup action is registered via `defer(...)`, ensuring the file is
|
|
317
|
+
* deleted when the scope closes—regardless of whether processing succeeds.
|
|
318
|
+
*
|
|
319
|
+
* @param s
|
|
320
|
+
* the scope to register cleanup with
|
|
321
|
+
* @param path
|
|
322
|
+
* the file path
|
|
323
|
+
* @param content
|
|
324
|
+
* initial content to write
|
|
325
|
+
* @return
|
|
326
|
+
* the created TempFile
|
|
327
|
+
*/
|
|
328
|
+
private def createTempFile(s: Scope, path: String, content: String): TempFile = {
|
|
329
|
+
val file = TempFile(path)
|
|
330
|
+
file.write(content)
|
|
331
|
+
println(s"Created: $path (${content.length} bytes)")
|
|
332
|
+
|
|
333
|
+
// Register cleanup - will run when scope exits, in LIFO order
|
|
334
|
+
s.defer {
|
|
335
|
+
file.delete()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
file
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala))
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
sbt "scope-examples/runMain scope.examples.TempFileHandlingExample"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Acquiring and releasing database connections**
|
|
349
|
+
|
|
350
|
+
```scala title="scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala"
|
|
351
|
+
/*
|
|
352
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
353
|
+
*
|
|
354
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
355
|
+
* you may not use this file except in compliance with the License.
|
|
356
|
+
* You may obtain a copy of the License at
|
|
357
|
+
*
|
|
358
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
359
|
+
*
|
|
360
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
361
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
362
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
363
|
+
* See the License for the specific language governing permissions and
|
|
364
|
+
* limitations under the License.
|
|
365
|
+
*/
|
|
366
|
+
|
|
367
|
+
package scope.examples
|
|
368
|
+
|
|
369
|
+
import zio.blocks.scope._
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Configuration for database connection.
|
|
373
|
+
*
|
|
374
|
+
* @param host
|
|
375
|
+
* the database server hostname
|
|
376
|
+
* @param port
|
|
377
|
+
* the database server port
|
|
378
|
+
* @param database
|
|
379
|
+
* the database name to connect to
|
|
380
|
+
*/
|
|
381
|
+
final case class DbConfig(host: String, port: Int, database: String) {
|
|
382
|
+
def connectionUrl: String = s"jdbc:postgresql://$host:$port/$database"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Represents the result of a database query.
|
|
387
|
+
*
|
|
388
|
+
* @param rows
|
|
389
|
+
* the result set as a list of row maps
|
|
390
|
+
*/
|
|
391
|
+
final case class QueryResult(rows: List[Map[String, String]]) {
|
|
392
|
+
def isEmpty: Boolean = rows.isEmpty
|
|
393
|
+
def size: Int = rows.size
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Simulates a database connection with lifecycle management.
|
|
398
|
+
*
|
|
399
|
+
* This class demonstrates how AutoCloseable resources integrate with ZIO Blocks
|
|
400
|
+
* Scope. When allocated via `allocate(Resource(...))`, the `close()` method is
|
|
401
|
+
* automatically registered as a finalizer.
|
|
402
|
+
*
|
|
403
|
+
* @param config
|
|
404
|
+
* the database configuration
|
|
405
|
+
*/
|
|
406
|
+
final class Database(config: DbConfig) extends AutoCloseable {
|
|
407
|
+
private var connected = false
|
|
408
|
+
|
|
409
|
+
def connect(): Unit = {
|
|
410
|
+
println(s"[Database] Connecting to ${config.connectionUrl}...")
|
|
411
|
+
connected = true
|
|
412
|
+
println(s"[Database] Connected successfully")
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
def query(sql: String): QueryResult = {
|
|
416
|
+
require(connected, "Database not connected")
|
|
417
|
+
println(s"[Database] Executing: $sql")
|
|
418
|
+
sql match {
|
|
419
|
+
case s if s.contains("users") =>
|
|
420
|
+
QueryResult(
|
|
421
|
+
List(
|
|
422
|
+
Map("id" -> "1", "name" -> "Alice"),
|
|
423
|
+
Map("id" -> "2", "name" -> "Bob")
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
case s if s.contains("orders") =>
|
|
427
|
+
QueryResult(
|
|
428
|
+
List(
|
|
429
|
+
Map("order_id" -> "101", "user_id" -> "1", "total" -> "99.99"),
|
|
430
|
+
Map("order_id" -> "102", "user_id" -> "2", "total" -> "149.50")
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
case _ =>
|
|
434
|
+
QueryResult(List(Map("result" -> "OK")))
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
override def close(): Unit = {
|
|
439
|
+
println(s"[Database] Closing connection to ${config.connectionUrl}")
|
|
440
|
+
connected = false
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Demonstrates basic resource lifecycle management with ZIO Blocks Scope.
|
|
446
|
+
*
|
|
447
|
+
* This example shows:
|
|
448
|
+
* - Allocating an AutoCloseable resource with automatic cleanup
|
|
449
|
+
* - Using `$(value)(f)` to access scoped values and execute queries
|
|
450
|
+
* - LIFO finalizer ordering (last allocated = first closed)
|
|
451
|
+
*
|
|
452
|
+
* When the scope exits, all registered finalizers run in reverse order,
|
|
453
|
+
* ensuring proper cleanup even if exceptions occur.
|
|
454
|
+
*/
|
|
455
|
+
@main def runDatabaseExample(): Unit = {
|
|
456
|
+
println("=== Database Connection Example ===\n")
|
|
457
|
+
|
|
458
|
+
val config = DbConfig("localhost", 5432, "myapp")
|
|
459
|
+
|
|
460
|
+
Scope.global.scoped { scope =>
|
|
461
|
+
import scope._
|
|
462
|
+
println("[Scope] Entering scoped region\n")
|
|
463
|
+
|
|
464
|
+
// Allocate the database resource. Because Database extends AutoCloseable,
|
|
465
|
+
// its close() method is automatically registered as a finalizer.
|
|
466
|
+
val db: $[Database] = allocate(Resource {
|
|
467
|
+
val database = new Database(config)
|
|
468
|
+
database.connect()
|
|
469
|
+
database
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// Use $(value)(f) to access the scoped value and execute queries.
|
|
473
|
+
$(db) { database =>
|
|
474
|
+
val users = database.query("SELECT * FROM users")
|
|
475
|
+
println(s"[Result] Found ${users.size} users: ${users.rows.map(_("name")).mkString(", ")}\n")
|
|
476
|
+
|
|
477
|
+
val orders = database.query("SELECT * FROM orders WHERE status = 'pending'")
|
|
478
|
+
println(s"[Result] Found ${orders.size} orders\n")
|
|
479
|
+
|
|
480
|
+
val health = database.query("SELECT 1 AS health_check")
|
|
481
|
+
println(s"[Result] Health check: ${health.rows.head("result")}\n")
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
println("[Scope] Exiting scoped region - finalizers will run in LIFO order")
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
println("\n=== Example Complete ===")
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala))
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
sbt "scope-examples/runMain scope.examples.DatabaseConnectionExample"
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Shared resources with memoization and reference counting**
|
|
498
|
+
|
|
499
|
+
```scala title="scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala"
|
|
500
|
+
/*
|
|
501
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
502
|
+
*
|
|
503
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
504
|
+
* you may not use this file except in compliance with the License.
|
|
505
|
+
* You may obtain a copy of the License at
|
|
506
|
+
*
|
|
507
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
508
|
+
*
|
|
509
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
510
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
511
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
512
|
+
* See the License for the specific language governing permissions and
|
|
513
|
+
* limitations under the License.
|
|
514
|
+
*/
|
|
515
|
+
|
|
516
|
+
package scope.examples
|
|
517
|
+
|
|
518
|
+
import zio.blocks.scope._
|
|
519
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Demonstrates `Wire.shared` vs `Wire.unique` and diamond dependency patterns.
|
|
523
|
+
*
|
|
524
|
+
* Two services (ProductService, OrderService) share one Logger instance
|
|
525
|
+
* (diamond pattern), but each gets its own unique Cache instance. This shows
|
|
526
|
+
* how shared wires provide singleton behavior while unique wires create fresh
|
|
527
|
+
* instances per injection site.
|
|
528
|
+
*
|
|
529
|
+
* Key concepts:
|
|
530
|
+
* - `Wire.shared[T]`: Single instance shared across all dependents (memoized)
|
|
531
|
+
* - `Wire.unique[T]`: Fresh instance created for each dependent
|
|
532
|
+
* - Diamond dependency: Multiple services depend on the same shared resource
|
|
533
|
+
* - Reference counting: Shared resources track usage and clean up when last
|
|
534
|
+
* user closes
|
|
535
|
+
*/
|
|
536
|
+
object CachingSharedLoggerExample {
|
|
537
|
+
|
|
538
|
+
/** Tracks instantiation counts for demonstration purposes. */
|
|
539
|
+
val loggerInstances = new AtomicInteger(0)
|
|
540
|
+
val cacheInstances = new AtomicInteger(0)
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* A shared logger that tracks instantiations and provides logging methods.
|
|
544
|
+
* Implements AutoCloseable for proper resource cleanup.
|
|
545
|
+
*/
|
|
546
|
+
class Logger extends AutoCloseable {
|
|
547
|
+
val instanceId: Int = loggerInstances.incrementAndGet()
|
|
548
|
+
println(s" [Logger#$instanceId] Created")
|
|
549
|
+
|
|
550
|
+
def info(msg: String): Unit = println(s" [Logger#$instanceId] INFO: $msg")
|
|
551
|
+
def debug(msg: String): Unit = println(s" [Logger#$instanceId] DEBUG: $msg")
|
|
552
|
+
def close(): Unit = println(s" [Logger#$instanceId] Closed")
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* A unique cache per service. Each service gets its own isolated cache
|
|
557
|
+
* instance. Implements AutoCloseable for proper resource cleanup. Note: No
|
|
558
|
+
* constructor params so it can be auto-wired with Wire.unique.
|
|
559
|
+
*/
|
|
560
|
+
class Cache extends AutoCloseable {
|
|
561
|
+
val instanceId: Int = cacheInstances.incrementAndGet()
|
|
562
|
+
private var store: Map[String, String] = Map.empty
|
|
563
|
+
println(s" [Cache#$instanceId] Created")
|
|
564
|
+
|
|
565
|
+
def get(key: String): Option[String] = store.get(key)
|
|
566
|
+
def put(key: String, value: String): Unit = store = store.updated(key, value)
|
|
567
|
+
def close(): Unit = println(s" [Cache#$instanceId] Closed")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Product service with its own cache but sharing the logger. */
|
|
571
|
+
class ProductService(val logger: Logger, val cache: Cache) {
|
|
572
|
+
println(s" [ProductService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
|
|
573
|
+
|
|
574
|
+
def findProduct(id: String): String =
|
|
575
|
+
cache.get(id) match {
|
|
576
|
+
case Some(product) =>
|
|
577
|
+
logger.debug(s"Cache hit for product $id")
|
|
578
|
+
product
|
|
579
|
+
case None =>
|
|
580
|
+
logger.info(s"Loading product $id from database")
|
|
581
|
+
val product = s"Product-$id"
|
|
582
|
+
cache.put(id, product)
|
|
583
|
+
product
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Order service with its own cache but sharing the same logger as
|
|
589
|
+
* ProductService.
|
|
590
|
+
*/
|
|
591
|
+
class OrderService(val logger: Logger, val cache: Cache) {
|
|
592
|
+
println(s" [OrderService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
|
|
593
|
+
|
|
594
|
+
def createOrder(productId: String): String = {
|
|
595
|
+
val orderId = s"ORD-${System.currentTimeMillis() % 10000}"
|
|
596
|
+
cache.put(orderId, productId)
|
|
597
|
+
logger.info(s"Created order $orderId for product $productId")
|
|
598
|
+
orderId
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Top-level application combining both services. */
|
|
603
|
+
class CachingApp(val productService: ProductService, val orderService: OrderService) extends AutoCloseable {
|
|
604
|
+
def run(): Unit = {
|
|
605
|
+
productService.logger.info("=== Application Started ===")
|
|
606
|
+
val product = productService.findProduct("P001")
|
|
607
|
+
orderService.createOrder(product)
|
|
608
|
+
productService.findProduct("P001") // cache hit
|
|
609
|
+
}
|
|
610
|
+
def close(): Unit = println(" [CachingApp] Closed")
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@main def runCachingExample(): Unit = {
|
|
614
|
+
println("\n╔════════════════════════════════════════════════════════════════╗")
|
|
615
|
+
println("║ Wire.shared vs Wire.unique - Diamond Dependency Example ║")
|
|
616
|
+
println("╚════════════════════════════════════════════════════════════════╝\n")
|
|
617
|
+
|
|
618
|
+
println("Creating wires...")
|
|
619
|
+
println(" - Logger: Wire.shared (singleton across all services)")
|
|
620
|
+
println(" - Cache: Wire.unique (fresh instance per service)\n")
|
|
621
|
+
|
|
622
|
+
println("─── Resource Acquisition ───")
|
|
623
|
+
Scope.global.scoped { scope =>
|
|
624
|
+
import scope._
|
|
625
|
+
val app: $[CachingApp] = allocate(
|
|
626
|
+
Resource.from[CachingApp](
|
|
627
|
+
Wire.shared[Logger],
|
|
628
|
+
Wire.unique[Cache]
|
|
629
|
+
)
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
println("\n─── Verification ───")
|
|
633
|
+
println(s" Logger instances created: ${loggerInstances.get()} (expected: 1)")
|
|
634
|
+
println(s" Cache instances created: ${cacheInstances.get()} (expected: 2)")
|
|
635
|
+
$(app) { a =>
|
|
636
|
+
println(s" ProductService.logger eq OrderService.logger: ${a.productService.logger eq a.orderService.logger}")
|
|
637
|
+
println(s" ProductService.cache eq OrderService.cache: ${a.productService.cache eq a.orderService.cache}")
|
|
638
|
+
|
|
639
|
+
println("\n─── Running Application ───")
|
|
640
|
+
a.run()
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
println("\n─── Scope Closing (LIFO cleanup) ───")
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
println("\n─── Summary ───")
|
|
647
|
+
println(s" Final Logger count: ${loggerInstances.get()} (shared = 1 instance)")
|
|
648
|
+
println(s" Final Cache count: ${cacheInstances.get()} (unique = 2 instances)")
|
|
649
|
+
println("\nDiamond pattern verified: both services received the same Logger instance.")
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala))
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
sbt "scope-examples/runMain scope.examples.CachingSharedLoggerExample"
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Managing shared expensive resources**
|
|
661
|
+
|
|
662
|
+
```scala title="scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala"
|
|
663
|
+
/*
|
|
664
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
665
|
+
*
|
|
666
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
667
|
+
* you may not use this file except in compliance with the License.
|
|
668
|
+
* You may obtain a copy of the License at
|
|
669
|
+
*
|
|
670
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
671
|
+
*
|
|
672
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
673
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
674
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
675
|
+
* See the License for the specific language governing permissions and
|
|
676
|
+
* limitations under the License.
|
|
677
|
+
*/
|
|
678
|
+
|
|
679
|
+
package scope.examples
|
|
680
|
+
|
|
681
|
+
import zio.blocks.scope._
|
|
682
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Demonstrates `Resource.Shared` with reference counting and nested resource
|
|
686
|
+
* acquisition.
|
|
687
|
+
*
|
|
688
|
+
* This example shows a realistic connection pool pattern where:
|
|
689
|
+
* - The pool itself is a shared resource (created once, ref-counted)
|
|
690
|
+
* - Individual connections are resources that must be allocated in a scope
|
|
691
|
+
* - `pool.acquire` returns `Resource[PooledConnection]`, forcing proper
|
|
692
|
+
* scoping
|
|
693
|
+
*
|
|
694
|
+
* This pattern is common for database pools, HTTP client pools, and thread
|
|
695
|
+
* pools.
|
|
696
|
+
*/
|
|
697
|
+
|
|
698
|
+
/** Configuration for the connection pool. */
|
|
699
|
+
final case class PoolConfig(maxConnections: Int, timeout: Long)
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* A connection retrieved from the pool.
|
|
703
|
+
*
|
|
704
|
+
* Connections are resources - they must be released back to the pool when done.
|
|
705
|
+
* This is enforced by making `acquire` return a `Resource[PooledConnection]`.
|
|
706
|
+
*/
|
|
707
|
+
final class PooledConnection(val id: Int, pool: ConnectionPool) extends AutoCloseable {
|
|
708
|
+
println(s" [Conn#$id] Acquired from pool")
|
|
709
|
+
|
|
710
|
+
def execute(sql: String): String = {
|
|
711
|
+
println(s" [Conn#$id] Executing: $sql")
|
|
712
|
+
s"Result from connection $id"
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
override def close(): Unit =
|
|
716
|
+
pool.release(this)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* A connection pool that manages pooled connections.
|
|
721
|
+
*
|
|
722
|
+
* Key design: `acquire` returns `Resource[PooledConnection]`, not a raw
|
|
723
|
+
* connection. This forces callers to allocate the connection in a scope,
|
|
724
|
+
* ensuring proper release even if exceptions occur.
|
|
725
|
+
*/
|
|
726
|
+
final class ConnectionPool(config: PoolConfig) extends AutoCloseable {
|
|
727
|
+
private val nextId = new AtomicInteger(0)
|
|
728
|
+
private val active = new AtomicInteger(0)
|
|
729
|
+
private val _closed = new AtomicInteger(0)
|
|
730
|
+
|
|
731
|
+
println(s" [Pool] Created with max ${config.maxConnections} connections")
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Acquires a connection from the pool.
|
|
735
|
+
*
|
|
736
|
+
* Returns a `Resource[PooledConnection]` that must be allocated in a scope.
|
|
737
|
+
* The connection is automatically released when the scope exits.
|
|
738
|
+
*/
|
|
739
|
+
def acquire: Resource[PooledConnection] = Resource.acquireRelease {
|
|
740
|
+
if (_closed.get() > 0) throw new IllegalStateException("Pool is closed")
|
|
741
|
+
if (active.get() >= config.maxConnections)
|
|
742
|
+
throw new IllegalStateException(s"Pool exhausted (max: ${config.maxConnections})")
|
|
743
|
+
|
|
744
|
+
val id = nextId.incrementAndGet()
|
|
745
|
+
val conn = new PooledConnection(id, this)
|
|
746
|
+
active.incrementAndGet()
|
|
747
|
+
println(s" [Pool] Active connections: ${active.get()}/${config.maxConnections}")
|
|
748
|
+
conn
|
|
749
|
+
} { conn =>
|
|
750
|
+
conn.close()
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private[examples] def release(conn: PooledConnection): Unit = {
|
|
754
|
+
val count = active.decrementAndGet()
|
|
755
|
+
println(s" [Conn#${conn.id}] Released back to pool (active: $count)")
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
def activeConnections: Int = active.get()
|
|
759
|
+
|
|
760
|
+
override def close(): Unit =
|
|
761
|
+
if (_closed.compareAndSet(0, 1)) {
|
|
762
|
+
println(s" [Pool] *** POOL CLOSED *** (served ${nextId.get()} total connections)")
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@main def connectionPoolExample(): Unit = {
|
|
767
|
+
println("=== Connection Pool with Resource-based Acquire ===\n")
|
|
768
|
+
|
|
769
|
+
val poolConfig = PoolConfig(maxConnections = 3, timeout = 5000L)
|
|
770
|
+
|
|
771
|
+
val poolResource: Resource[ConnectionPool] =
|
|
772
|
+
Resource.fromAutoCloseable(new ConnectionPool(poolConfig))
|
|
773
|
+
|
|
774
|
+
Scope.global.scoped { appScope =>
|
|
775
|
+
import appScope._
|
|
776
|
+
println("[App] Allocating pool\n")
|
|
777
|
+
val pool: $[ConnectionPool] = poolResource.allocate
|
|
778
|
+
|
|
779
|
+
println("--- ServiceA doing work (connection scoped to this block) ---")
|
|
780
|
+
appScope.scoped { workScope =>
|
|
781
|
+
import workScope._
|
|
782
|
+
val p: $[ConnectionPool] = lower(pool)
|
|
783
|
+
val c: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
784
|
+
val result = $(c)(_.execute("SELECT * FROM service_a_table"))
|
|
785
|
+
println(s" [ServiceA] Got: $result")
|
|
786
|
+
}
|
|
787
|
+
println()
|
|
788
|
+
|
|
789
|
+
println("--- ServiceB doing work ---")
|
|
790
|
+
appScope.scoped { workScope =>
|
|
791
|
+
import workScope._
|
|
792
|
+
val p: $[ConnectionPool] = lower(pool)
|
|
793
|
+
val c: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
794
|
+
val result = $(c)(_.execute("SELECT * FROM service_b_table"))
|
|
795
|
+
println(s" [ServiceB] Got: $result")
|
|
796
|
+
}
|
|
797
|
+
println()
|
|
798
|
+
|
|
799
|
+
println("--- Multiple connections in same scope ---")
|
|
800
|
+
appScope.scoped { workScope =>
|
|
801
|
+
import workScope._
|
|
802
|
+
val p: $[ConnectionPool] = lower(pool)
|
|
803
|
+
val a: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
804
|
+
val b: $[PooledConnection] = $(p)(_.acquire).allocate
|
|
805
|
+
val aId = $(a)(_.id)
|
|
806
|
+
val bId = $(b)(_.id)
|
|
807
|
+
println(s" [Parallel] Using connections $aId and $bId")
|
|
808
|
+
$(a)(_.execute("UPDATE table_a SET x = 1"))
|
|
809
|
+
$(b)(_.execute("UPDATE table_b SET y = 2"))
|
|
810
|
+
()
|
|
811
|
+
}
|
|
812
|
+
println()
|
|
813
|
+
|
|
814
|
+
println("[App] All work complete, exiting app scope...")
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
println("\n=== Example Complete ===")
|
|
818
|
+
println("\nKey insight: pool.acquire returns Resource[PooledConnection],")
|
|
819
|
+
println("forcing proper scoped allocation and automatic release.")
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala))
|
|
824
|
+
|
|
825
|
+
```bash
|
|
826
|
+
sbt "scope-examples/runMain scope.examples.ConnectionPoolExample"
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
**Transactional resource management**
|
|
830
|
+
|
|
831
|
+
```scala title="scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala"
|
|
832
|
+
/*
|
|
833
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
834
|
+
*
|
|
835
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
836
|
+
* you may not use this file except in compliance with the License.
|
|
837
|
+
* You may obtain a copy of the License at
|
|
838
|
+
*
|
|
839
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
840
|
+
*
|
|
841
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
842
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
843
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
844
|
+
* See the License for the specific language governing permissions and
|
|
845
|
+
* limitations under the License.
|
|
846
|
+
*/
|
|
847
|
+
|
|
848
|
+
package scope.examples
|
|
849
|
+
|
|
850
|
+
import zio.blocks.scope._
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Transaction Boundary Example
|
|
854
|
+
*
|
|
855
|
+
* Demonstrates nested scopes and resource-returning methods for database
|
|
856
|
+
* transaction management.
|
|
857
|
+
*
|
|
858
|
+
* Key patterns shown:
|
|
859
|
+
* - '''Resource-returning methods''': `beginTransaction` returns
|
|
860
|
+
* `Resource[DbTransaction]`
|
|
861
|
+
* - '''Nested scopes''': Transactions live in child scopes of the connection
|
|
862
|
+
* - '''Automatic cleanup''': Uncommitted transactions auto-rollback on scope
|
|
863
|
+
* exit
|
|
864
|
+
* - '''LIFO ordering''': Transaction closes before connection
|
|
865
|
+
*/
|
|
866
|
+
object TransactionBoundaryExample {
|
|
867
|
+
|
|
868
|
+
/** Simulates a database connection that can create transactions. */
|
|
869
|
+
class DbConnection(val id: String) extends AutoCloseable {
|
|
870
|
+
println(s" [DbConnection $id] Opened")
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Begins a new transaction.
|
|
874
|
+
*
|
|
875
|
+
* Returns a `Resource[DbTransaction]` that must be allocated in a scope.
|
|
876
|
+
* This ensures the transaction is always properly closed (with rollback if
|
|
877
|
+
* not committed) when the scope exits.
|
|
878
|
+
*/
|
|
879
|
+
def beginTransaction(txId: String): Resource[DbTransaction] =
|
|
880
|
+
Resource.acquireRelease {
|
|
881
|
+
new DbTransaction(this, txId)
|
|
882
|
+
} { tx =>
|
|
883
|
+
tx.close()
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
def close(): Unit =
|
|
887
|
+
println(s" [DbConnection $id] Closed")
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** Simulates an active database transaction. */
|
|
891
|
+
class DbTransaction(val conn: DbConnection, val id: String) extends AutoCloseable {
|
|
892
|
+
private var committed = false
|
|
893
|
+
private var rolledBack = false
|
|
894
|
+
println(s" [Tx $id] Started on connection ${conn.id}")
|
|
895
|
+
|
|
896
|
+
def execute(sql: String): Int = {
|
|
897
|
+
require(!committed && !rolledBack, s"Transaction $id already completed")
|
|
898
|
+
println(s" [Tx $id] Execute: $sql")
|
|
899
|
+
sql.hashCode.abs % 100 + 1
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
def commit(): Unit = {
|
|
903
|
+
require(!committed && !rolledBack, s"Transaction $id already completed")
|
|
904
|
+
committed = true
|
|
905
|
+
println(s" [Tx $id] Committed")
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
def rollback(): Unit =
|
|
909
|
+
if (!committed && !rolledBack) {
|
|
910
|
+
rolledBack = true
|
|
911
|
+
println(s" [Tx $id] Rolled back")
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
def close(): Unit = {
|
|
915
|
+
if (!committed && !rolledBack) {
|
|
916
|
+
println(s" [Tx $id] Auto-rollback (not committed)")
|
|
917
|
+
rollback()
|
|
918
|
+
}
|
|
919
|
+
println(s" [Tx $id] Closed")
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Result of transaction operations. */
|
|
924
|
+
case class TxResult(success: Boolean, affectedRows: Int) derives Unscoped
|
|
925
|
+
|
|
926
|
+
@main def runTransactionBoundaryExample(): Unit = {
|
|
927
|
+
println("=== Transaction Boundary Example ===\n")
|
|
928
|
+
println("Demonstrating Resource-returning beginTransaction method\n")
|
|
929
|
+
|
|
930
|
+
Scope.global.scoped { connScope =>
|
|
931
|
+
import connScope._
|
|
932
|
+
// Allocate the connection in the outer scope
|
|
933
|
+
val conn: $[DbConnection] = Resource.fromAutoCloseable(new DbConnection("db-001")).allocate
|
|
934
|
+
println()
|
|
935
|
+
|
|
936
|
+
// Transaction 1: Successful insert
|
|
937
|
+
println("--- Transaction 1: Insert user ---")
|
|
938
|
+
val result1: TxResult =
|
|
939
|
+
connScope.scoped { txScope =>
|
|
940
|
+
import txScope._
|
|
941
|
+
val c: $[DbConnection] = lower(conn)
|
|
942
|
+
val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-001")).allocate
|
|
943
|
+
val rows = $(tx)(_.execute("INSERT INTO users VALUES (1, 'Alice')"))
|
|
944
|
+
$(tx)(_.commit())
|
|
945
|
+
TxResult(success = true, affectedRows = rows)
|
|
946
|
+
}
|
|
947
|
+
println(s" Result: $result1\n")
|
|
948
|
+
|
|
949
|
+
// Transaction 2: Transfer funds (multiple operations)
|
|
950
|
+
println("--- Transaction 2: Transfer funds ---")
|
|
951
|
+
val result2: TxResult =
|
|
952
|
+
connScope.scoped { txScope =>
|
|
953
|
+
import txScope._
|
|
954
|
+
val c: $[DbConnection] = lower(conn)
|
|
955
|
+
val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-002")).allocate
|
|
956
|
+
val rows1 = $(tx)(_.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1"))
|
|
957
|
+
val rows2 = $(tx)(_.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2"))
|
|
958
|
+
$(tx)(_.commit())
|
|
959
|
+
TxResult(success = true, affectedRows = rows1 + rows2)
|
|
960
|
+
}
|
|
961
|
+
println(s" Result: $result2\n")
|
|
962
|
+
|
|
963
|
+
// Transaction 3: Demonstrates auto-rollback on scope exit without commit
|
|
964
|
+
println("--- Transaction 3: Auto-rollback (no explicit commit) ---")
|
|
965
|
+
val result3: TxResult =
|
|
966
|
+
connScope.scoped { txScope =>
|
|
967
|
+
import txScope._
|
|
968
|
+
val c: $[DbConnection] = lower(conn)
|
|
969
|
+
val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-003")).allocate
|
|
970
|
+
$(tx)(_.execute("DELETE FROM audit_log"))
|
|
971
|
+
println(" [App] Not committing - scope exit will trigger auto-rollback...")
|
|
972
|
+
TxResult(success = false, affectedRows = 0)
|
|
973
|
+
}
|
|
974
|
+
println(s" Result: $result3\n")
|
|
975
|
+
|
|
976
|
+
println("--- All transactions complete, connection still open ---")
|
|
977
|
+
println("--- Exiting connection scope ---")
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
println("\n=== Example complete ===")
|
|
981
|
+
println("\nKey insight: beginTransaction() returns Resource[DbTransaction],")
|
|
982
|
+
println("forcing proper scoped allocation and automatic cleanup.")
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala))
|
|
988
|
+
|
|
989
|
+
```bash
|
|
990
|
+
sbt "scope-examples/runMain scope.examples.TransactionBoundaryExample"
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
**Multi-layer service construction**
|
|
994
|
+
|
|
995
|
+
```scala title="scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala"
|
|
996
|
+
/*
|
|
997
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
998
|
+
*
|
|
999
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1000
|
+
* you may not use this file except in compliance with the License.
|
|
1001
|
+
* You may obtain a copy of the License at
|
|
1002
|
+
*
|
|
1003
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1004
|
+
*
|
|
1005
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1006
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1007
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1008
|
+
* See the License for the specific language governing permissions and
|
|
1009
|
+
* limitations under the License.
|
|
1010
|
+
*/
|
|
1011
|
+
|
|
1012
|
+
package scope.examples
|
|
1013
|
+
|
|
1014
|
+
import zio.blocks.scope._
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Demonstrates auto-wiring a layered web service using
|
|
1018
|
+
* `Resource.from[T](wires*)`.
|
|
1019
|
+
*
|
|
1020
|
+
* The macro automatically derives wires for concrete classes (Database,
|
|
1021
|
+
* UserRepository, UserController) while requiring only the leaf config value to
|
|
1022
|
+
* be provided explicitly. Resources are cleaned up in LIFO order when the scope
|
|
1023
|
+
* closes.
|
|
1024
|
+
*
|
|
1025
|
+
* Layer hierarchy:
|
|
1026
|
+
* {{{
|
|
1027
|
+
* AppConfig (leaf value via Wire)
|
|
1028
|
+
* ↓
|
|
1029
|
+
* Database (auto-wired, AutoCloseable)
|
|
1030
|
+
* ↓
|
|
1031
|
+
* UserRepository (auto-wired)
|
|
1032
|
+
* ↓
|
|
1033
|
+
* UserController (auto-wired, AutoCloseable)
|
|
1034
|
+
* }}}
|
|
1035
|
+
*/
|
|
1036
|
+
|
|
1037
|
+
/** Application configuration - the leaf dependency provided via Wire(value). */
|
|
1038
|
+
case class WebAppConfig(dbUrl: String, serverPort: Int)
|
|
1039
|
+
|
|
1040
|
+
/** Domain model for users. */
|
|
1041
|
+
case class User(id: Long, name: String, email: String)
|
|
1042
|
+
|
|
1043
|
+
/** Database layer - acquires a connection and releases it on close. */
|
|
1044
|
+
class WebDatabase(config: WebAppConfig) extends AutoCloseable {
|
|
1045
|
+
println(s" [WebDatabase] Connecting to ${config.dbUrl}")
|
|
1046
|
+
|
|
1047
|
+
def execute(sql: String): Int = {
|
|
1048
|
+
println(s" [WebDatabase] Executing: $sql")
|
|
1049
|
+
1
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
def close(): Unit = println(" [WebDatabase] Connection closed")
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** Repository layer - provides data access using the database. */
|
|
1056
|
+
class UserRepository(db: WebDatabase) {
|
|
1057
|
+
println(" [UserRepository] Initialized")
|
|
1058
|
+
|
|
1059
|
+
private var nextId = 1L
|
|
1060
|
+
|
|
1061
|
+
def findById(id: Long): Option[User] = {
|
|
1062
|
+
db.execute(s"SELECT * FROM users WHERE id = $id")
|
|
1063
|
+
if (id > 0) Some(User(id, "Alice", "alice@example.com")) else None
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
def save(user: User): Long = {
|
|
1067
|
+
db.execute(s"INSERT INTO users VALUES (${user.id}, '${user.name}', '${user.email}')")
|
|
1068
|
+
val id = nextId
|
|
1069
|
+
nextId += 1
|
|
1070
|
+
id
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Controller layer - handles HTTP requests using the repository. */
|
|
1075
|
+
class UserController(repo: UserRepository) extends AutoCloseable {
|
|
1076
|
+
println(" [UserController] Ready to serve requests")
|
|
1077
|
+
|
|
1078
|
+
def getUser(id: Long): String =
|
|
1079
|
+
repo.findById(id).map(u => s"User(${u.id}, ${u.name})").getOrElse("Not found")
|
|
1080
|
+
|
|
1081
|
+
def createUser(name: String, email: String): String = {
|
|
1082
|
+
val id = repo.save(User(0, name, email))
|
|
1083
|
+
s"Created user with id=$id"
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
def close(): Unit = println(" [UserController] Shutting down")
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Entry point demonstrating the auto-wiring feature.
|
|
1091
|
+
*
|
|
1092
|
+
* Only `Wire(config)` is provided; the macro derives wires for Database,
|
|
1093
|
+
* UserRepository, and UserController from their constructors.
|
|
1094
|
+
*/
|
|
1095
|
+
@main def layeredWebServiceExample(): Unit = {
|
|
1096
|
+
val config = WebAppConfig(dbUrl = "jdbc:postgresql://localhost:5432/mydb", serverPort = 8080)
|
|
1097
|
+
|
|
1098
|
+
println("=== Constructing layers (order: config → database → repository → controller) ===")
|
|
1099
|
+
|
|
1100
|
+
// Resource.from auto-wires the entire dependency graph
|
|
1101
|
+
val controllerResource: Resource[UserController] = Resource.from[UserController](
|
|
1102
|
+
Wire(config)
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
// Allocate within a scoped block; cleanup runs on scope exit
|
|
1106
|
+
Scope.global.scoped { scope =>
|
|
1107
|
+
import scope._
|
|
1108
|
+
val controller: $[UserController] = allocate(controllerResource)
|
|
1109
|
+
|
|
1110
|
+
println("\n=== Handling requests ===")
|
|
1111
|
+
println(s" GET /users/1 → ${$(controller)(_.getUser(1))}")
|
|
1112
|
+
println(s" POST /users → ${$(controller)(_.createUser("Bob", "bob@example.com"))}")
|
|
1113
|
+
|
|
1114
|
+
println("\n=== Scope closing (LIFO cleanup: controller → database) ===")
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
println("=== Done ===")
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala))
|
|
1122
|
+
|
|
1123
|
+
```bash
|
|
1124
|
+
sbt "scope-examples/runMain scope.examples.LayeredWebServiceExample"
|
|
1125
|
+
```
|