@zio.dev/zio-blocks 0.0.29 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/compile-time-resource-safety-with-scope.md +997 -0
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +203 -203
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +20 -14
- package/package.json +1 -1
- package/reference/codec.md +7 -7
- package/reference/context.md +1 -1
- package/reference/docs.md +1 -1
- package/reference/dynamic-schema.md +39 -42
- package/reference/media-type.md +2 -2
- package/reference/resource-management/defer-handle.md +246 -0
- package/reference/resource-management/finalization.md +286 -0
- package/reference/resource-management/finalizer.md +167 -0
- package/reference/{resource-management-di → resource-management}/index.md +3 -8
- package/reference/{resource-management-di → resource-management}/resource.md +532 -50
- package/reference/resource-management/scope.md +3026 -0
- package/reference/resource-management/unscoped.md +125 -0
- package/reference/{resource-management-di → resource-management}/wire.md +74 -28
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/streams.md +989 -0
- package/reference/type-class-derivation.md +1 -1
- package/ringbuffer.md +1 -1
- package/sidebars.js +9 -4
- package/reference/resource-management-di/scope.md +0 -1423
|
@@ -36,24 +36,282 @@ Without Resources, managing complex initialization and cleanup is tedious and er
|
|
|
36
36
|
Add the ZIO Blocks Scope module to your `build.sbt`:
|
|
37
37
|
|
|
38
38
|
```scala
|
|
39
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "0.0.
|
|
39
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "0.0.30"
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
For cross-platform (Scala.js):
|
|
43
43
|
|
|
44
44
|
```scala
|
|
45
|
-
libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "0.0.
|
|
45
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "0.0.30"
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Supported Scala versions: 2.13.x and 3.x.
|
|
49
49
|
|
|
50
|
+
## Shared and Unique Resources
|
|
51
|
+
|
|
52
|
+
Resource offers two distinct types for managing instance lifecycles: **Shared** for singleton-like resources that are allocated once and reused across multiple scopes, and **Unique** for fresh resources created on each allocation. Understanding when to use each type is critical for building efficient and correct resource-management architectures.
|
|
53
|
+
|
|
54
|
+
### Comparison
|
|
55
|
+
|
|
56
|
+
| Aspect | Shared | Unique |
|
|
57
|
+
|--------------------|----------------------------------------------------|-------------------------------------------|
|
|
58
|
+
| **Creation** | `Resource.shared(f)` | `Resource.unique(f)` or `Resource(value)` |
|
|
59
|
+
| **Memoization** | Yes, with reference counting | No, fresh per allocation |
|
|
60
|
+
| **When to use** | Expensive resources (DB connections, thread pools) | Per-request state, isolated handlers |
|
|
61
|
+
| **Instance reuse** | Same instance across all allocations | New instance per allocation |
|
|
62
|
+
| **Finalization** | Runs when last reference released | Runs immediately when scope closes |
|
|
63
|
+
| **Thread safety** | Lock-free atomic reference counting | Per-scope, no global coordination needed |
|
|
64
|
+
|
|
65
|
+
### Shared Resources
|
|
66
|
+
|
|
67
|
+
**Shared resources are memoized**: the first allocation initializes the instance; subsequent allocations return the same reference with automatic reference counting. Finalizers run only when the last reference is released. Use shared resources for **expensive, globally singular components** — database connection pools, thread pools, logging systems, caches, and other heavyweight infrastructure that should exist exactly once for the lifetime of the application.
|
|
68
|
+
|
|
69
|
+
The canonical example is a **database connection pool**. Building a fresh pool for each service layer is wasteful and defeats pooling's purpose. Instead, wrap the pool in a shared resource: both the user service and order service allocate the same pool instance, with the system automatically tracking references and closing the pool only when the last service releases it.
|
|
70
|
+
|
|
71
|
+
Here's what shared acquisition looks like:
|
|
72
|
+
|
|
73
|
+
```scala
|
|
74
|
+
object Resource {
|
|
75
|
+
def shared[A](f: Scope => A): Resource[A]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When you allocate a shared resource, reference counting ensures cleanup happens exactly once:
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
import zio.blocks.scope._
|
|
83
|
+
|
|
84
|
+
class DatabasePool extends AutoCloseable {
|
|
85
|
+
def close(): Unit = println("Pool closed (after all services released)")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
val poolResource = Resource.shared { scope =>
|
|
89
|
+
println("Creating database pool (first allocation only)")
|
|
90
|
+
new DatabasePool
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Allocate in ServiceA
|
|
94
|
+
Scope.global.scoped { scopeA =>
|
|
95
|
+
import scopeA._
|
|
96
|
+
val poolA: $[DatabasePool] = poolResource.allocate
|
|
97
|
+
println("ServiceA allocated pool")
|
|
98
|
+
|
|
99
|
+
// Allocate in ServiceB within a nested scope
|
|
100
|
+
scopeA.scoped { scopeB =>
|
|
101
|
+
import scopeB._
|
|
102
|
+
val poolB: $[DatabasePool] = poolResource.allocate
|
|
103
|
+
println("ServiceB allocated same pool instance (reference count incremented)")
|
|
104
|
+
|
|
105
|
+
// Both services share the same underlying pool instance
|
|
106
|
+
println("Both ServiceA and ServiceB are using the same pool instance")
|
|
107
|
+
}
|
|
108
|
+
println("ServiceB released, but pool stays open for ServiceA (ref count -= 1)")
|
|
109
|
+
}
|
|
110
|
+
println("All services released, pool closed (ref count == 0)")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Unique Resources
|
|
114
|
+
|
|
115
|
+
**Unique resources create fresh instances each time**: every allocation produces a new value. Finalizers run when their owning scope closes, independent of other allocations. Use unique resources for **per-request state, isolated services, and stateful handlers** — anything that should never be shared because it encapsulates request-specific or scope-specific data.
|
|
116
|
+
|
|
117
|
+
A typical scenario is **per-request caches**: each API request gets its own cache instance to avoid one request polluting another's cached data. Similarly, stateful handlers (parser state machines, transaction contexts, event buffers) need isolation to prevent cross-contamination.
|
|
118
|
+
|
|
119
|
+
Here's what unique acquisition looks like:
|
|
120
|
+
|
|
121
|
+
```scala
|
|
122
|
+
object Resource {
|
|
123
|
+
def unique[A](f: Scope => A): Resource[A]
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When you allocate a unique resource, each allocation is independent:
|
|
128
|
+
|
|
129
|
+
```scala
|
|
130
|
+
import zio.blocks.scope._
|
|
131
|
+
|
|
132
|
+
class RequestCache extends AutoCloseable {
|
|
133
|
+
def close(): Unit = println("Request cache closed")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
val cacheResource = Resource.unique { scope =>
|
|
137
|
+
println("Creating new request cache")
|
|
138
|
+
new RequestCache
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Two allocations in the same scope produce different instances
|
|
142
|
+
Scope.global.scoped { scope =>
|
|
143
|
+
import scope._
|
|
144
|
+
println("Creating first cache...")
|
|
145
|
+
val cache1: $[RequestCache] = cacheResource.allocate
|
|
146
|
+
|
|
147
|
+
println("Creating second cache...")
|
|
148
|
+
val cache2: $[RequestCache] = cacheResource.allocate
|
|
149
|
+
|
|
150
|
+
println("Both caches are independent instances")
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Diamond Dependency Pattern
|
|
155
|
+
|
|
156
|
+
A classic architecture uses shared resources to solve **diamond dependencies**, where multiple services depend on the same expensive component. Consider an e-commerce application where both `ProductService` and `OrderService` depend on `Logger`:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
CachingApp
|
|
160
|
+
/ \
|
|
161
|
+
/ \
|
|
162
|
+
ProductService OrderService
|
|
163
|
+
\ /
|
|
164
|
+
\ /
|
|
165
|
+
Logger
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Without shared resources, each service would receive a different `Logger` instance. With `Resource.shared`, both services automatically receive the same singleton instance:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
import zio.blocks.scope._
|
|
172
|
+
|
|
173
|
+
class Logger extends AutoCloseable {
|
|
174
|
+
def log(msg: String): Unit = println(s"LOG: $msg")
|
|
175
|
+
def close(): Unit = println("Logger closed")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class ProductService(val logger: Logger) {
|
|
179
|
+
def findProduct(id: String): String = {
|
|
180
|
+
logger.log(s"Finding product $id")
|
|
181
|
+
s"Product-$id"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class OrderService(val logger: Logger) {
|
|
186
|
+
def createOrder(productId: String): String = {
|
|
187
|
+
logger.log(s"Creating order for $productId")
|
|
188
|
+
s"ORD-123"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class CachingApp(val productService: ProductService, val orderService: OrderService) extends AutoCloseable {
|
|
193
|
+
def close(): Unit = println("App closed")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Scope.global.scoped { scope =>
|
|
197
|
+
import scope._
|
|
198
|
+
|
|
199
|
+
// Use Wire.shared[Logger] to ensure only one instance is created
|
|
200
|
+
val app: $[CachingApp] = allocate(
|
|
201
|
+
Resource.from[CachingApp](
|
|
202
|
+
Wire.shared[Logger] // Both services get the same Logger instance
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
$(app) { a =>
|
|
207
|
+
println(s"ProductService and OrderService share Logger? ${a.productService.logger eq a.orderService.logger}")
|
|
208
|
+
a.productService.findProduct("P001")
|
|
209
|
+
a.orderService.createOrder("P001")
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
In this pattern, `Resource.shared` (via `Wire.shared`) guarantees a single `Logger` instance across all services, eliminating duplication while maintaining proper cleanup semantics.
|
|
215
|
+
|
|
216
|
+
### Multiple Shared Dependencies
|
|
217
|
+
|
|
218
|
+
Most applications need more than one shared resource. Here's a realistic example where `CachingApp` depends on both a shared `Logger` and a shared `MetricsCollector`. Both resources are singletons that all services share:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
CachingApp
|
|
222
|
+
/ \
|
|
223
|
+
/ \
|
|
224
|
+
/ \
|
|
225
|
+
ProductService OrderService
|
|
226
|
+
/ \ / \
|
|
227
|
+
/ \ / \
|
|
228
|
+
Logger Metrics Logger Metrics
|
|
229
|
+
\ / \ /
|
|
230
|
+
\ / \ /
|
|
231
|
+
\/ \/
|
|
232
|
+
(shared singleton instances)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Here's the implementation:
|
|
236
|
+
|
|
237
|
+
```scala
|
|
238
|
+
import zio.blocks.scope._
|
|
239
|
+
|
|
240
|
+
class Logger extends AutoCloseable {
|
|
241
|
+
def log(msg: String): Unit = println(s"[LOG] $msg")
|
|
242
|
+
def close(): Unit = println("[Logger] Closed")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
class MetricsCollector extends AutoCloseable {
|
|
246
|
+
private var eventCount = 0
|
|
247
|
+
def recordEvent(name: String): Unit = {
|
|
248
|
+
eventCount += 1
|
|
249
|
+
println(s"[METRICS] Event: $name (total: $eventCount)")
|
|
250
|
+
}
|
|
251
|
+
def close(): Unit = println(s"[MetricsCollector] Closed after $eventCount events")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
class ProductService(val logger: Logger, val metrics: MetricsCollector) {
|
|
255
|
+
def findProduct(id: String): String = {
|
|
256
|
+
logger.log(s"Finding product $id")
|
|
257
|
+
metrics.recordEvent("product.find")
|
|
258
|
+
s"Product-$id"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
class OrderService(val logger: Logger, val metrics: MetricsCollector) {
|
|
263
|
+
def createOrder(productId: String): String = {
|
|
264
|
+
logger.log(s"Creating order for $productId")
|
|
265
|
+
metrics.recordEvent("order.create")
|
|
266
|
+
s"ORD-123"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
class CachingApp(
|
|
271
|
+
val productService: ProductService,
|
|
272
|
+
val orderService: OrderService
|
|
273
|
+
) extends AutoCloseable {
|
|
274
|
+
def close(): Unit = println("[CachingApp] Closed")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
Scope.global.scoped { scope =>
|
|
278
|
+
import scope._
|
|
279
|
+
|
|
280
|
+
// Both Logger and MetricsCollector are shared (singleton instances)
|
|
281
|
+
val app: $[CachingApp] = allocate(
|
|
282
|
+
Resource.from[CachingApp](
|
|
283
|
+
Wire.shared[Logger], // One Logger for all services
|
|
284
|
+
Wire.shared[MetricsCollector] // One MetricsCollector for all services
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
$(app) { a =>
|
|
289
|
+
println(s"ProductService and OrderService share Logger? ${a.productService.logger eq a.orderService.logger}")
|
|
290
|
+
println(s"ProductService and OrderService share Metrics? ${a.productService.metrics eq a.orderService.metrics}")
|
|
291
|
+
|
|
292
|
+
a.productService.findProduct("P001")
|
|
293
|
+
a.orderService.createOrder("P001")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
When this scope closes, both the `Logger` and `MetricsCollector` are finalized exactly once, regardless of how many services reference them. The macro automatically builds a dependency graph, verifies all wires, and ensures LIFO cleanup order.
|
|
299
|
+
|
|
50
300
|
## Construction
|
|
51
301
|
|
|
52
|
-
Resources can be created in several ways: from values, from explicit acquire/release pairs, from `AutoCloseable` types, or from
|
|
302
|
+
Resources can be created in several ways: from values, from explicit acquire/release pairs, from `AutoCloseable` types, from custom functions, or derived automatically from a type's constructor using the `Resource.from` macros.
|
|
53
303
|
|
|
54
|
-
### `Resource.apply` —
|
|
304
|
+
### `Resource.apply` — Wrap a Value
|
|
55
305
|
|
|
56
|
-
Wraps a by-name value as a resource. If the value implements `AutoCloseable`, its `close()` method is automatically registered as a finalizer
|
|
306
|
+
Wraps a by-name value as a resource. If the value implements `AutoCloseable`, its `close()` method is automatically registered as a finalizer:
|
|
307
|
+
|
|
308
|
+
```scala
|
|
309
|
+
object Resource {
|
|
310
|
+
def apply[A](value: => A): Resource[A]
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Here's how to use it:
|
|
57
315
|
|
|
58
316
|
```scala
|
|
59
317
|
import zio.blocks.scope._
|
|
@@ -68,10 +326,18 @@ val configResource = Resource(Config(debug = true))
|
|
|
68
326
|
val dbResource = Resource(new Database("mydb"))
|
|
69
327
|
```
|
|
70
328
|
|
|
71
|
-
### `Resource.acquireRelease` —
|
|
329
|
+
### `Resource.acquireRelease` — Explicit Lifecycle
|
|
72
330
|
|
|
73
331
|
Creates a resource with separate acquire and release functions. The acquire thunk runs during allocation; the release function is registered as a finalizer:
|
|
74
332
|
|
|
333
|
+
```scala
|
|
334
|
+
object Resource {
|
|
335
|
+
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Here's an example with file handling:
|
|
340
|
+
|
|
75
341
|
```scala
|
|
76
342
|
import zio.blocks.scope._
|
|
77
343
|
import java.io.FileInputStream
|
|
@@ -83,10 +349,18 @@ val fileResource = Resource.acquireRelease {
|
|
|
83
349
|
}
|
|
84
350
|
```
|
|
85
351
|
|
|
86
|
-
### `Resource.fromAutoCloseable` —
|
|
352
|
+
### `Resource.fromAutoCloseable` — Type-Safe Wrapping
|
|
87
353
|
|
|
88
354
|
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
355
|
|
|
356
|
+
```scala
|
|
357
|
+
object Resource {
|
|
358
|
+
def fromAutoCloseable[A <: AutoCloseable](value: => A): Resource[A]
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Here's an example with a stream:
|
|
363
|
+
|
|
90
364
|
```scala
|
|
91
365
|
import zio.blocks.scope._
|
|
92
366
|
import java.io.BufferedInputStream
|
|
@@ -97,25 +371,70 @@ val streamResource = Resource.fromAutoCloseable {
|
|
|
97
371
|
}
|
|
98
372
|
```
|
|
99
373
|
|
|
100
|
-
### `Resource.shared` —
|
|
374
|
+
### `Resource.shared` — Memoized with Reference Counting
|
|
375
|
+
|
|
376
|
+
Creates a shared resource that memoizes its value across multiple allocations using reference counting. The first allocation initializes the value using an `OpenScope` parented to `Scope.global`; subsequent allocations increment the reference count and return the same instance. Each scope that receives the shared value registers a finalizer that decrements the count. When the count reaches zero, the shared scope closes automatically. This mechanism is **thread-safe and lock-free**, implemented using `AtomicReference` with atomic compare-and-swap operations to avoid contention.
|
|
377
|
+
|
|
378
|
+
Under the hood, a shared resource progresses through four states: (1) **Uninitialized** — the resource has never been allocated and the first call triggers initialization; (2) **Pending** — initialization is in progress and other threads wait via spin-yield for the value to become available; (3) **Created** — the value is fully initialized and ready, each allocation increments the reference count, and each scope registers a finalizer to decrement it; (4) **Destroyed** — the reference count reached zero, the resource was cleaned up, and further allocations will fail. This state machine ensures that no matter how many threads try to allocate simultaneously, the underlying value initializes exactly once, and cleanup happens exactly once when the last reference is released.
|
|
101
379
|
|
|
102
|
-
|
|
380
|
+
Use shared resources for **expensive, singleton-like components** (database connection pools, thread pools, logging systems, caches) that should exist exactly once for the lifetime of the application, even when multiple services depend on them.
|
|
381
|
+
|
|
382
|
+
Here's the signature:
|
|
383
|
+
|
|
384
|
+
```scala
|
|
385
|
+
object Resource {
|
|
386
|
+
def shared[A](f: Scope => A): Resource[A]
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Here's a realistic example showing reference counting and automatic cleanup:
|
|
103
391
|
|
|
104
392
|
```scala
|
|
105
393
|
import zio.blocks.scope._
|
|
106
394
|
|
|
107
|
-
|
|
395
|
+
class ExpensiveComponent extends AutoCloseable {
|
|
396
|
+
println("ExpensiveComponent initialized (expensive operation)")
|
|
397
|
+
def close(): Unit = println("ExpensiveComponent cleaned up (last reference released)")
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
val sharedResource = Resource.shared { scope =>
|
|
401
|
+
new ExpensiveComponent()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Multiple services allocating the same shared resource
|
|
405
|
+
Scope.global.scoped { scope =>
|
|
406
|
+
import scope._
|
|
407
|
+
|
|
408
|
+
// First allocation initializes the component
|
|
409
|
+
println("ServiceA allocating...")
|
|
410
|
+
val componentA: $[ExpensiveComponent] = sharedResource.allocate
|
|
108
411
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
412
|
+
// ServiceB in a nested scope receives the same instance
|
|
413
|
+
scope.scoped { innerScope =>
|
|
414
|
+
import innerScope._
|
|
415
|
+
println("ServiceB allocating (same instance, ref count += 1)...")
|
|
416
|
+
val componentB: $[ExpensiveComponent] = sharedResource.allocate
|
|
417
|
+
println("Both services have the same instance")
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
println("ServiceB released, but component stays alive (ref count -= 1)")
|
|
112
421
|
}
|
|
422
|
+
|
|
423
|
+
println("ServiceA released, component cleaned up (ref count == 0)")
|
|
113
424
|
```
|
|
114
425
|
|
|
115
|
-
### `Resource.unique` —
|
|
426
|
+
### `Resource.unique` — Fresh Instances
|
|
116
427
|
|
|
117
428
|
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
429
|
|
|
430
|
+
```scala
|
|
431
|
+
object Resource {
|
|
432
|
+
def unique[A](f: Scope => A): Resource[A]
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Here's an example showing fresh instances:
|
|
437
|
+
|
|
119
438
|
```scala
|
|
120
439
|
import zio.blocks.scope._
|
|
121
440
|
|
|
@@ -127,13 +446,92 @@ val uniqueResource = Resource.unique[Int] { _ =>
|
|
|
127
446
|
}
|
|
128
447
|
```
|
|
129
448
|
|
|
449
|
+
### `Resource.from[T]` — Derive from Constructor
|
|
450
|
+
|
|
451
|
+
Derives a `Resource[T]` from `T`'s primary constructor, requiring no external dependencies.
|
|
452
|
+
If `T` extends `AutoCloseable`, `Resource.from` automatically registers its `close()` method
|
|
453
|
+
as a finalizer:
|
|
454
|
+
|
|
455
|
+
```scala
|
|
456
|
+
object Resource {
|
|
457
|
+
def from[T]: Resource[T]
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
To derive a resource for a type that has no constructor dependencies:
|
|
462
|
+
|
|
463
|
+
```scala
|
|
464
|
+
import zio.blocks.scope._
|
|
465
|
+
|
|
466
|
+
class MetricsCollector extends AutoCloseable {
|
|
467
|
+
def record(event: String): Unit = println(s"Recording: $event")
|
|
468
|
+
def close(): Unit = println("MetricsCollector closed")
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
val metricsResource = Resource.from[MetricsCollector]
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Internally, the macro inspects `T`'s constructor at compile time, verifies that no external
|
|
475
|
+
dependencies are needed, and synthesizes a `Resource.shared` that instantiates `T` and —
|
|
476
|
+
when `T` extends `AutoCloseable` — registers `close()` as a scope finalizer. Any attempt
|
|
477
|
+
to use `Resource.from[T]` on a type with unsatisfied constructor parameters results in a
|
|
478
|
+
compile-time error.
|
|
479
|
+
|
|
480
|
+
### `Resource.from[T](wires: Wire[?, ?]*)` — Derive with Dependency Overrides
|
|
481
|
+
|
|
482
|
+
Derives a `Resource[T]` from `T`'s constructor, with `Wire` values provided as dependency
|
|
483
|
+
overrides. Any dependency not covered by an explicit wire is auto-derived if the macro can
|
|
484
|
+
construct it; otherwise a compile-time error is produced:
|
|
485
|
+
|
|
486
|
+
```scala
|
|
487
|
+
object Resource {
|
|
488
|
+
def from[T](wires: Wire[?, ?]*): Resource[T]
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
To derive a resource for a type whose dependencies are provided via wires:
|
|
493
|
+
|
|
494
|
+
```scala
|
|
495
|
+
import zio.blocks.scope._
|
|
496
|
+
|
|
497
|
+
case class Config(host: String, port: Int)
|
|
498
|
+
|
|
499
|
+
class Logger(config: Config) extends AutoCloseable {
|
|
500
|
+
def log(msg: String): Unit = println(s"[$msg] ${config.host}:${config.port}")
|
|
501
|
+
def close(): Unit = log("Logger shutting down")
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
class Service(logger: Logger) extends AutoCloseable {
|
|
505
|
+
def run(): Unit = logger.log("Service running")
|
|
506
|
+
def close(): Unit = logger.log("Service shutting down")
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
val serviceResource = Resource.from[Service](
|
|
510
|
+
Wire(Config("localhost", 8080))
|
|
511
|
+
)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Internally, the macro builds a complete dependency graph at compile time: it inspects each
|
|
515
|
+
provided wire's input and output types, identifies all constructor parameters of `T`, and
|
|
516
|
+
auto-derives wires for any remaining dependencies. It then topologically sorts all wires to
|
|
517
|
+
determine acquisition order and composes them into a single `Resource` whose finalizers run
|
|
518
|
+
in LIFO order — inner dependencies close before outer ones.
|
|
519
|
+
|
|
130
520
|
## Core Operations
|
|
131
521
|
|
|
132
522
|
Resources support transformation and composition through `map`, `flatMap`, and `zip`.
|
|
133
523
|
|
|
134
|
-
### `Resource#map` —
|
|
524
|
+
### `Resource#map` — Transform the Value
|
|
135
525
|
|
|
136
|
-
Transforms the value produced by a resource without affecting finalization. The transformation function is applied after the resource is acquired
|
|
526
|
+
Transforms the value produced by a resource without affecting finalization. The transformation function is applied after the resource is acquired:
|
|
527
|
+
|
|
528
|
+
```scala
|
|
529
|
+
trait Resource[+A] {
|
|
530
|
+
def map[B](f: A => B): Resource[B]
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Here's a usage example:
|
|
137
535
|
|
|
138
536
|
```scala
|
|
139
537
|
import zio.blocks.scope._
|
|
@@ -142,10 +540,18 @@ val portResource = Resource(8080)
|
|
|
142
540
|
val urlResource = portResource.map(port => s"http://localhost:$port")
|
|
143
541
|
```
|
|
144
542
|
|
|
145
|
-
### `Resource#flatMap` —
|
|
543
|
+
### `Resource#flatMap` — Sequence Resources
|
|
146
544
|
|
|
147
545
|
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
546
|
|
|
547
|
+
```scala
|
|
548
|
+
trait Resource[+A] {
|
|
549
|
+
def flatMap[B](f: A => Resource[B]): Resource[B]
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Here's an example with dependent resources:
|
|
554
|
+
|
|
149
555
|
```scala
|
|
150
556
|
import zio.blocks.scope._
|
|
151
557
|
|
|
@@ -162,10 +568,18 @@ val dbResource = configResource.flatMap { config =>
|
|
|
162
568
|
}
|
|
163
569
|
```
|
|
164
570
|
|
|
165
|
-
### `Resource#zip` —
|
|
571
|
+
### `Resource#zip` — Combine Resources
|
|
166
572
|
|
|
167
573
|
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
574
|
|
|
575
|
+
```scala
|
|
576
|
+
trait Resource[+A] {
|
|
577
|
+
def zip[B](that: Resource[B]): Resource[(A, B)]
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Here's an example combining multiple resources:
|
|
582
|
+
|
|
169
583
|
```scala
|
|
170
584
|
import zio.blocks.scope._
|
|
171
585
|
|
|
@@ -184,44 +598,90 @@ val cacheResource = Resource.fromAutoCloseable(new Cache())
|
|
|
184
598
|
val combined = dbResource.zip(cacheResource)
|
|
185
599
|
```
|
|
186
600
|
|
|
187
|
-
|
|
601
|
+
### `Resource#allocate` — Acquire Within a Scope
|
|
602
|
+
|
|
603
|
+
Allocates a `Resource[A]` within the current `Scope`, returning a scoped `$[A]`. This is syntax sugar for `scope.allocate(resource)`, available via `import scope._` inside a `scoped` block:
|
|
604
|
+
|
|
605
|
+
```scala
|
|
606
|
+
implicit class ResourceOps[A](private val r: Resource[A]) {
|
|
607
|
+
def allocate: $[A]
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
To allocate a resource and use its value inside a scope:
|
|
612
|
+
|
|
613
|
+
```scala
|
|
614
|
+
import zio.blocks.scope._
|
|
615
|
+
|
|
616
|
+
class Database extends AutoCloseable {
|
|
617
|
+
def query(sql: String): String = s"Result: $sql"
|
|
618
|
+
def close(): Unit = println("Database closed")
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
Scope.global.scoped { implicit scope =>
|
|
622
|
+
import scope._
|
|
623
|
+
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
624
|
+
$(db)(_.query("SELECT 1"))
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### `$[Resource[A]]#allocate` — Allocate a Scoped Resource
|
|
188
629
|
|
|
189
|
-
|
|
630
|
+
Allocates a `$[Resource[A]]` — a Resource that is itself a scoped value — returning `$[A]`. Use this when a method on a scoped object returns a Resource and you need to immediately acquire it while keeping the result scoped.
|
|
190
631
|
|
|
191
|
-
|
|
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 |
|
|
632
|
+
**What is a scoped resource?** A scoped value `$[A]` represents an `A` that is valid only while a scope is alive. A **scoped resource** `$[Resource[A]]` is a Resource object that exists *inside* that scope. When you call a method like `$(pool)(_.lease())` that returns a Resource, the result is typed as `$[Resource[Connection]]` — the Resource itself is scoped. The `.allocate` extension method unwraps this scoped resource and acquires it, returning the acquired value as a new scoped value `$[A]`.
|
|
198
633
|
|
|
199
|
-
|
|
634
|
+
**Motivation:** This pattern appears frequently in resource factories. A scoped object (like a database pool) has methods that produce new Resources. Without this extension, you'd need to unwrap the `$[Resource[A]]` from the `$` context, allocate it separately, and re-wrap the result. The `.allocate` method chains naturally, letting you write `$(pool)(_.lease()).allocate` instead of dealing with intermediate unwrapping.
|
|
200
635
|
|
|
201
|
-
|
|
636
|
+
The implicit class:
|
|
637
|
+
|
|
638
|
+
```scala
|
|
639
|
+
implicit class ScopedResourceOps[A](private val sr: $[Resource[A]]) {
|
|
640
|
+
def allocate: $[A]
|
|
641
|
+
}
|
|
642
|
+
```
|
|
202
643
|
|
|
203
|
-
|
|
644
|
+
Here's a realistic example where a database pool (a scoped object) has a method that returns a Resource for individual connections:
|
|
204
645
|
|
|
205
646
|
```scala
|
|
206
647
|
import zio.blocks.scope._
|
|
207
648
|
|
|
208
|
-
|
|
649
|
+
class Connection extends AutoCloseable {
|
|
650
|
+
def query(sql: String): String = s"Result: $sql"
|
|
651
|
+
def close(): Unit = println("Connection closed")
|
|
652
|
+
}
|
|
209
653
|
|
|
210
|
-
class
|
|
211
|
-
def
|
|
654
|
+
class Pool extends AutoCloseable {
|
|
655
|
+
def lease(): Resource[Connection] = Resource.fromAutoCloseable(new Connection)
|
|
656
|
+
def close(): Unit = println("Pool closed")
|
|
212
657
|
}
|
|
213
658
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
659
|
+
Scope.global.scoped { implicit scope =>
|
|
660
|
+
import scope._
|
|
661
|
+
val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
|
|
662
|
+
val conn: $[Connection] = $(pool)(_.lease()).allocate
|
|
663
|
+
$(conn)(_.query("SELECT 1"))
|
|
217
664
|
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
## Integration
|
|
669
|
+
|
|
670
|
+
Resource is a core abstraction in ZIO Blocks' resource management ecosystem:
|
|
218
671
|
|
|
672
|
+
- **[`Scope`](./scope.md)** — Resources require a `Scope` for allocation. The `Scope` manages the lifetime of acquired resources and automatically runs finalizers when the scope closes.
|
|
673
|
+
- **[`Wire`](./wire.md)** — The `Resource.from[T](wires)` macro builds dependency graphs using `Wire` values, enabling constructor-based dependency injection with automatic resource management.
|
|
674
|
+
- **[`Finalizer`](./finalizer.md)** — Resources register their cleanup logic with `Finalizer` objects, which execute in LIFO order when scopes close.
|
|
675
|
+
|
|
676
|
+
For example, `Resource.from[T]` uses `Wire` to construct instances with their dependencies, automatically registering any `AutoCloseable` cleanup:
|
|
677
|
+
|
|
678
|
+
```scala
|
|
219
679
|
val serviceResource = Resource.from[Service](
|
|
220
|
-
Wire(Config(
|
|
680
|
+
Wire(Config("localhost", 8080))
|
|
221
681
|
)
|
|
222
682
|
```
|
|
223
683
|
|
|
224
|
-
|
|
684
|
+
This builds a complete dependency graph: `Config` → `Logger` → `Service`, with all finalizers managed by the containing `Scope`.
|
|
225
685
|
|
|
226
686
|
## Running the Examples
|
|
227
687
|
|
|
@@ -236,7 +696,9 @@ cd zio-blocks
|
|
|
236
696
|
|
|
237
697
|
**2. Run individual examples with sbt:**
|
|
238
698
|
|
|
239
|
-
|
|
699
|
+
### Basic Lifecycle Management with Temporary Files
|
|
700
|
+
|
|
701
|
+
This example demonstrates creating and automatically cleaning up temporary files using Resource's lifecycle management. It shows how Resource ensures files are closed even if exceptions occur:
|
|
240
702
|
|
|
241
703
|
```scala title="scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala"
|
|
242
704
|
/*
|
|
@@ -341,11 +803,15 @@ private def createTempFile(s: Scope, path: String, content: String): TempFile =
|
|
|
341
803
|
|
|
342
804
|
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala))
|
|
343
805
|
|
|
806
|
+
Run this example with:
|
|
807
|
+
|
|
344
808
|
```bash
|
|
345
|
-
sbt "scope-examples/runMain scope.examples.
|
|
809
|
+
sbt "scope-examples/runMain scope.examples.tempFileHandlingExample"
|
|
346
810
|
```
|
|
347
811
|
|
|
348
|
-
|
|
812
|
+
### Acquiring and Releasing Database Connections
|
|
813
|
+
|
|
814
|
+
This example demonstrates the acquire-release pattern using Resource to manage database connections. It shows proper connection initialization and guaranteed cleanup:
|
|
349
815
|
|
|
350
816
|
```scala title="scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala"
|
|
351
817
|
/*
|
|
@@ -490,11 +956,15 @@ final class Database(config: DbConfig) extends AutoCloseable {
|
|
|
490
956
|
|
|
491
957
|
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala))
|
|
492
958
|
|
|
959
|
+
Run this example with:
|
|
960
|
+
|
|
493
961
|
```bash
|
|
494
|
-
sbt "scope-examples/runMain scope.examples.
|
|
962
|
+
sbt "scope-examples/runMain scope.examples.runDatabaseExample"
|
|
495
963
|
```
|
|
496
964
|
|
|
497
|
-
|
|
965
|
+
### Shared Resources with Memoization and Reference Counting
|
|
966
|
+
|
|
967
|
+
This example demonstrates Resource.shared to create a singleton logger instance that is automatically cleaned up only when the last service releases it. Shows reference counting in action:
|
|
498
968
|
|
|
499
969
|
```scala title="scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala"
|
|
500
970
|
/*
|
|
@@ -653,11 +1123,15 @@ object CachingSharedLoggerExample {
|
|
|
653
1123
|
|
|
654
1124
|
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala))
|
|
655
1125
|
|
|
1126
|
+
Run this example with:
|
|
1127
|
+
|
|
656
1128
|
```bash
|
|
657
|
-
sbt "scope-examples/runMain scope.examples.
|
|
1129
|
+
sbt "scope-examples/runMain scope.examples.runCachingExample"
|
|
658
1130
|
```
|
|
659
1131
|
|
|
660
|
-
|
|
1132
|
+
### Managing Shared Expensive Resources
|
|
1133
|
+
|
|
1134
|
+
This example demonstrates using Resource.shared for a database connection pool—an expensive resource that should exist exactly once. Shows how multiple services safely share the same pool instance with automatic cleanup:
|
|
661
1135
|
|
|
662
1136
|
```scala title="scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala"
|
|
663
1137
|
/*
|
|
@@ -822,11 +1296,15 @@ final class ConnectionPool(config: PoolConfig) extends AutoCloseable {
|
|
|
822
1296
|
|
|
823
1297
|
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala))
|
|
824
1298
|
|
|
1299
|
+
Run this example with:
|
|
1300
|
+
|
|
825
1301
|
```bash
|
|
826
|
-
sbt "scope-examples/runMain scope.examples.
|
|
1302
|
+
sbt "scope-examples/runMain scope.examples.connectionPoolExample"
|
|
827
1303
|
```
|
|
828
1304
|
|
|
829
|
-
|
|
1305
|
+
### Transactional Resource Management
|
|
1306
|
+
|
|
1307
|
+
This example demonstrates combining Resource with transaction boundaries. Shows how to manage resources (connections, transactions) that must be coordinated across scopes with proper rollback on failure:
|
|
830
1308
|
|
|
831
1309
|
```scala title="scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala"
|
|
832
1310
|
/*
|
|
@@ -986,11 +1464,15 @@ object TransactionBoundaryExample {
|
|
|
986
1464
|
|
|
987
1465
|
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala))
|
|
988
1466
|
|
|
1467
|
+
Run this example with:
|
|
1468
|
+
|
|
989
1469
|
```bash
|
|
990
|
-
sbt "scope-examples/runMain scope.examples.
|
|
1470
|
+
sbt "scope-examples/runMain scope.examples.runTransactionBoundaryExample"
|
|
991
1471
|
```
|
|
992
1472
|
|
|
993
|
-
|
|
1473
|
+
### Multi-Layer Service Construction
|
|
1474
|
+
|
|
1475
|
+
This example demonstrates Resource.from macro to automatically build a complex dependency graph with multiple services. Shows automatic wiring of constructor dependencies and cleanup in correct LIFO order:
|
|
994
1476
|
|
|
995
1477
|
```scala title="scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala"
|
|
996
1478
|
/*
|
|
@@ -1121,5 +1603,5 @@ class UserController(repo: UserRepository) extends AutoCloseable {
|
|
|
1121
1603
|
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala))
|
|
1122
1604
|
|
|
1123
1605
|
```bash
|
|
1124
|
-
sbt "scope-examples/runMain scope.examples.
|
|
1606
|
+
sbt "scope-examples/runMain scope.examples.layeredWebServiceExample"
|
|
1125
1607
|
```
|