@zio.dev/zio-blocks 0.0.28 → 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.
Files changed (35) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +997 -0
  2. package/guides/query-dsl-extending.md +1 -1
  3. package/guides/query-dsl-fluent-builder.md +203 -203
  4. package/guides/query-dsl-reified-optics.md +1 -1
  5. package/guides/query-dsl-sql.md +1 -1
  6. package/guides/zio-schema-migration.md +6 -6
  7. package/index.md +68 -16
  8. package/package.json +1 -1
  9. package/path-interpolator.md +70 -9
  10. package/reference/allows.md +96 -0
  11. package/reference/codec.md +8 -8
  12. package/reference/combinators.md +345 -0
  13. package/reference/context.md +639 -67
  14. package/reference/docs.md +1 -1
  15. package/reference/dynamic-schema.md +39 -42
  16. package/reference/http-model.md +1716 -0
  17. package/reference/json-differ.md +320 -0
  18. package/reference/json-patch.md +1 -1
  19. package/reference/media-type.md +2 -2
  20. package/reference/resource-management/defer-handle.md +246 -0
  21. package/reference/resource-management/finalization.md +286 -0
  22. package/reference/resource-management/finalizer.md +167 -0
  23. package/reference/resource-management/index.md +44 -0
  24. package/reference/resource-management/resource.md +1607 -0
  25. package/reference/resource-management/scope.md +3026 -0
  26. package/reference/resource-management/unscoped.md +125 -0
  27. package/reference/resource-management/wire.md +878 -0
  28. package/reference/schema-evolution/as.md +4 -4
  29. package/reference/schema-evolution/into.md +2 -2
  30. package/reference/schema-expr.md +2 -2
  31. package/reference/streams.md +989 -0
  32. package/reference/type-class-derivation.md +31 -31
  33. package/ringbuffer.md +249 -0
  34. package/sidebars.js +19 -2
  35. package/scope.md +0 -1423
@@ -0,0 +1,878 @@
1
+ ---
2
+ id: wire
3
+ title: "Wire"
4
+ ---
5
+
6
+ `Wire[-In, +Out]` is a **compile-time safe recipe for constructing a service and its dependencies**. Wires describe how to construct an `Out` value given access to its dependencies via a `Context[In]` and a `Scope` for finalization:
7
+
8
+ ```scala
9
+ sealed trait Wire[-In, +Out] {
10
+ def isShared: Boolean
11
+ def isUnique: Boolean = !isShared
12
+
13
+ def shared: Wire.Shared[In, Out]
14
+ def unique: Wire.Unique[In, Out]
15
+
16
+ def toResource(deps: Context[In]): Resource[Out]
17
+ }
18
+ ```
19
+
20
+ The type parameters are:
21
+ - **`In` (contravariant)**: the dependencies required to construct the service
22
+ - **`Out` (covariant)**: the service produced
23
+
24
+ Wires are the building blocks of dependency injection in Scope. They form the foundation for constructor-based dependency injection via `Resource.from[T](wires*)`.
25
+
26
+ ## Overview
27
+
28
+ A `Wire` is a **lazy recipe**, not an execution. It holds a construction function `(Scope, Context[In]) => Out` and a **sharing strategy** — either `Shared` (reference-counted via `Resource.shared`) or `Unique` (fresh instance per allocation). The Wire itself does nothing until you convert it to a `Resource` and allocate it within a scope.
29
+
30
+ Here's the typical flow:
31
+
32
+ ```
33
+ Wire (recipe)
34
+ ↓
35
+ Resource (lazy, composable)
36
+ ↓
37
+ scope.allocate(...)
38
+ ↓
39
+ $[T] (scoped value in scope)
40
+ ```
41
+
42
+ ## Motivation
43
+
44
+ Without Wires, building a multi-layer application requires manual dependency passing:
45
+
46
+ ```scala
47
+ final case class Config(dbUrl: String)
48
+
49
+ final class Database(config: Config) extends AutoCloseable {
50
+ def close(): Unit = println(s"closing connection to ${config.dbUrl}")
51
+ }
52
+
53
+ final class UserService(db: Database) {
54
+ def getUser(id: Int): String = s"user $id"
55
+ }
56
+
57
+ final class App(service: UserService) {
58
+ def run(): Unit = println(service.getUser(1))
59
+ }
60
+
61
+ // Manual wiring:
62
+ Scope.global.scoped { scope =>
63
+ import scope._
64
+ val config = Config("jdbc:postgres://localhost/db")
65
+ val db = Resource.fromAutoCloseable(new Database(config)).allocate
66
+ val service = new UserService($(db)(identity))
67
+ val app = new App(service)
68
+ app.run()
69
+ }
70
+ ```
71
+
72
+ With `Wire` + `Resource.from`, the macro handles the dependency graph:
73
+
74
+ ```scala
75
+ Scope.global.scoped { scope =>
76
+ import scope._
77
+ val app = Resource.from[App](
78
+ Wire(Config("jdbc:postgres://localhost/db"))
79
+ ).allocate
80
+ $(app)(_.run())
81
+ }
82
+ ```
83
+
84
+ **Benefits:**
85
+ - **Compile-time graph validation** — cycle detection, duplicate providers, missing dependencies
86
+ - **Automatic finalization** — `AutoCloseable` resources are finalized in LIFO order
87
+ - **Sharing control** — choose which services are singletons (shared) vs fresh per allocation (unique)
88
+ - **Type-safe construction** — no stringly-typed dependency resolution
89
+
90
+ ## Installation
91
+
92
+ Add the following dependency to your `build.sbt`:
93
+
94
+ ```scala
95
+ libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "0.0.30"
96
+ ```
97
+
98
+ For cross-platform (Scala.js):
99
+
100
+ ```scala
101
+ libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "0.0.30"
102
+ ```
103
+
104
+ Supported Scala versions: 2.13.x and 3.x.
105
+
106
+ ## Construction
107
+
108
+ `Wire` provides multiple ways to create instances, ranging from automatic macro-based derivation to manual construction for custom logic. Here are the main construction methods:
109
+
110
+ ### Wire.shared[T] — derive a shared wire
111
+
112
+ The `Wire.shared[T]` macro inspects `T`'s primary constructor and generates a shared wire that reuses the same instance across dependents:
113
+
114
+ ```scala
115
+ import zio.blocks.scope._
116
+ import zio.blocks.context.Context
117
+
118
+ final case class Config(debug: Boolean)
119
+
120
+ final class Database(config: Config) extends AutoCloseable {
121
+ def query(sql: String): String = s"[db] $sql"
122
+ def close(): Unit = println("database closed")
123
+ }
124
+
125
+ val wire: Wire.Shared[Config, Database] = Wire.shared[Database]
126
+
127
+ Scope.global.scoped { scope =>
128
+ import scope._
129
+ val config = Config(debug = true)
130
+ val deps = Context[Config](config)
131
+ val db = allocate(wire.toResource(deps))
132
+ $(db)(_.query("SELECT 1"))
133
+ }
134
+ ```
135
+
136
+ ### Wire.unique[T] — derive a unique wire
137
+
138
+ Like `Wire.shared[T]`, but creates a fresh instance each time the wire is used. Use for request-scoped or per-call services:
139
+
140
+ ```scala
141
+ import zio.blocks.scope._
142
+ import zio.blocks.context.Context
143
+
144
+ final class RequestHandler {
145
+ val id = scala.util.Random.nextInt()
146
+ }
147
+
148
+ val wire: Wire.Unique[Any, RequestHandler] = Wire.unique[RequestHandler]
149
+
150
+ Scope.global.scoped { scope =>
151
+ import scope._
152
+ val deps = Context.empty[Any]
153
+ val resource = wire.toResource(deps)
154
+
155
+ val h1 = allocate(resource)
156
+ val h2 = allocate(resource)
157
+
158
+ val ids: (Int, Int) = (
159
+ $(h1)(_.id),
160
+ $(h2)(_.id)
161
+ )
162
+ // ids._1 != ids._2 (different instances)
163
+ }
164
+ ```
165
+
166
+ ### Wire.apply[T] — lift a pre-existing value
167
+
168
+ Creates a shared wire that injects a value you already have. If the value is `AutoCloseable`, its `close()` method is automatically registered as a finalizer:
169
+
170
+ ```scala
171
+ import zio.blocks.scope._
172
+
173
+ final case class Config(dbUrl: String)
174
+
175
+ val config = Config("jdbc:postgres://localhost/db")
176
+ val wire: Wire.Shared[Any, Config] = Wire(config)
177
+
178
+ Scope.global.scoped { scope =>
179
+ import scope._
180
+ val cfg = allocate(wire.toResource(Context.empty[Any]))
181
+ $(cfg)(_.dbUrl)
182
+ }
183
+ ```
184
+
185
+ ### Wire.Shared.fromFunction — manual shared wire construction
186
+
187
+ Use this for custom construction logic when macro derivation doesn't fit:
188
+
189
+ ```scala
190
+ import zio.blocks.scope._
191
+ import zio.blocks.context.Context
192
+
193
+ final case class Config(timeout: Int)
194
+
195
+ final class Client(config: Config) {
196
+ def call(): String = s"calling with timeout=${config.timeout}"
197
+ }
198
+
199
+ val wire: Wire.Shared[Config, Client] =
200
+ Wire.Shared.fromFunction { (scope, ctx) =>
201
+ val config = ctx.get[Config]
202
+ new Client(config)
203
+ }
204
+
205
+ Scope.global.scoped { scope =>
206
+ import scope._
207
+ val config = Config(30)
208
+ val deps = Context[Config](config)
209
+ val client = allocate(wire.toResource(deps))
210
+ $(client)(_.call())
211
+ }
212
+ ```
213
+
214
+ ### Wire.Unique.fromFunction — manual unique wire construction
215
+
216
+ Like `Wire.Shared.fromFunction`, but for unique wires:
217
+
218
+ ```scala
219
+ import zio.blocks.scope._
220
+ import zio.blocks.context.Context
221
+
222
+ final class RequestContext {
223
+ val id = scala.util.Random.nextInt()
224
+ }
225
+
226
+ val wire: Wire.Unique[Any, RequestContext] =
227
+ Wire.Unique.fromFunction { (_, _) =>
228
+ new RequestContext
229
+ }
230
+
231
+ Scope.global.scoped { scope =>
232
+ import scope._
233
+ val deps = Context.empty[Any]
234
+ val resource = wire.toResource(deps)
235
+
236
+ val r1 = allocate(resource)
237
+ val r2 = allocate(resource)
238
+
239
+ val different: Boolean = $(r1)(_.id) != $(r2)(_.id)
240
+ // different == true
241
+ }
242
+ ```
243
+
244
+ ## Shared vs Unique
245
+
246
+ The fundamental difference is **reuse semantics**:
247
+
248
+ | Aspect | Shared | Unique |
249
+ |--------|--------|--------|
250
+ | **Construction** | `Wire.shared[T]` macro or `Wire.Shared.fromFunction` | `Wire.unique[T]` macro or `Wire.Unique.fromFunction` |
251
+ | **Resource type** | `Resource.shared[T]` (reference-counted) | `Resource.unique[T]` (fresh per call) |
252
+ | **When to use** | Singletons, expensive resources (connections, thread pools) | Request scoped, stateful per-call (request handlers) |
253
+ | **Instance reuse** | Same instance across entire dependency graph | New instance per allocation |
254
+ | **Finalization** | Runs when last referencing scope closes | Runs when each scope closes |
255
+
256
+ In the diamond pattern (where `App` depends on both `UserService` and `OrderService`, both of which depend on `Database`), a shared wire ensures `Database` is constructed once and both services receive the same instance:
257
+
258
+ ```scala
259
+ import zio.blocks.scope._
260
+
261
+ final class Database {
262
+ val id = scala.util.Random.nextInt()
263
+ }
264
+
265
+ final class UserService(db: Database) {
266
+ def getDbId(): Int = db.id
267
+ }
268
+
269
+ final class OrderService(db: Database) {
270
+ def getDbId(): Int = db.id
271
+ }
272
+
273
+ final class App(userService: UserService, orderService: OrderService) {
274
+ def check(): Boolean = {
275
+ // With shared Database, these should be equal
276
+ userService.getDbId() == orderService.getDbId()
277
+ }
278
+ }
279
+
280
+ // Using shared wires for all dependencies
281
+ val resource = Resource.from[App](
282
+ Wire.shared[Database],
283
+ Wire.shared[UserService],
284
+ Wire.shared[OrderService]
285
+ )
286
+
287
+ Scope.global.scoped { scope =>
288
+ import scope._
289
+ val app = resource.allocate
290
+ $(app)(_.check()) // true: Database is shared
291
+ }
292
+ ```
293
+
294
+ ## Core Operations
295
+
296
+ The `Wire` interface provides a set of operations for inspecting and transforming wires:
297
+
298
+ ### `Wire#isShared` and `Wire#isUnique`
299
+
300
+ Check the sharing strategy of a wire:
301
+
302
+ ```scala
303
+ trait Wire[-In, +Out] {
304
+ def isShared: Boolean
305
+ def isUnique: Boolean = !isShared
306
+ }
307
+ ```
308
+
309
+ Here's how to use these methods:
310
+
311
+ ```scala
312
+ import zio.blocks.scope._
313
+
314
+ val sharedWire = Wire.shared[String]
315
+ val uniqueWire = Wire.unique[String]
316
+
317
+ println(s"sharedWire.isShared: ${sharedWire.isShared}") // true
318
+ println(s"sharedWire.isUnique: ${sharedWire.isUnique}") // false
319
+ println(s"uniqueWire.isShared: ${uniqueWire.isShared}") // false
320
+ println(s"uniqueWire.isUnique: ${uniqueWire.isUnique}") // true
321
+ ```
322
+
323
+ ### `Wire#shared` and `Wire#unique` — convert between strategies
324
+
325
+ Convert a wire to the opposite strategy:
326
+
327
+ ```scala
328
+ trait Wire[-In, +Out] {
329
+ def shared: Wire.Shared[In, Out]
330
+ def unique: Wire.Unique[In, Out]
331
+ }
332
+ ```
333
+
334
+ Here's how to convert between sharing strategies:
335
+
336
+ ```scala
337
+ import zio.blocks.scope._
338
+
339
+ val original = Wire.shared[String]
340
+
341
+ val converted: Wire.Unique[Any, String] = original.unique
342
+ println(s"original.isShared: ${original.isShared}") // true
343
+ println(s"converted.isUnique: ${converted.isUnique}") // true
344
+
345
+ // Converting back returns a new shared wire
346
+ val backToShared: Wire.Shared[Any, String] = converted.shared
347
+ println(s"backToShared.isShared: ${backToShared.isShared}") // true
348
+ ```
349
+
350
+ Calling `shared` on an already-shared wire returns `this` (identity); likewise `unique` on a unique wire returns `this`.
351
+
352
+ ### `Wire#toResource` — convert a wire to a Resource
353
+
354
+ Converts the wire to a lazy `Resource` by providing the dependency context:
355
+
356
+ ```scala
357
+ trait Wire[-In, +Out] {
358
+ def toResource(deps: Context[In]): Resource[Out]
359
+ }
360
+ ```
361
+
362
+ Here's how to use this method:
363
+
364
+ ```scala
365
+ import zio.blocks.scope._
366
+ import zio.blocks.context.Context
367
+
368
+ final case class Config(value: String)
369
+
370
+ val wire = Wire.shared[Config]
371
+ val deps = Context[Config](Config("hello"))
372
+
373
+ val resource: Resource[Config] = wire.toResource(deps)
374
+
375
+ Scope.global.scoped { scope =>
376
+ import scope._
377
+ val cfg = allocate(resource)
378
+ $(cfg)(_.value)
379
+ }
380
+ ```
381
+
382
+ ### `Wire#make` — construct directly from a wire
383
+
384
+ Directly construct a value without going through `Resource.toResource`. This is a low-level operation; prefer `allocate(wire.toResource(...))` for safety:
385
+
386
+ ```scala
387
+ trait Wire.Shared[-In, +Out] {
388
+ def make(scope: Scope, context: Context[In]): Out
389
+ }
390
+ ```
391
+
392
+ Here's how to use this method:
393
+
394
+ ```scala
395
+ import zio.blocks.scope._
396
+ import zio.blocks.context.Context
397
+
398
+ final class Service {
399
+ def getName: String = "service"
400
+ }
401
+
402
+ val wire = Wire.shared[Service]
403
+
404
+ Scope.global.scoped { scope =>
405
+ import scope._
406
+ val service = wire.asInstanceOf[Wire.Shared[Any, Service]].make(scope, Context.empty[Any])
407
+ println(service.getName)
408
+ }
409
+ ```
410
+
411
+ ## Macro Derivation
412
+
413
+ When you call `Wire.shared[T]` or `Wire.unique[T]`, the macro performs these checks:
414
+
415
+ 1. **Is `T` a class?** — traits and abstract classes are rejected; only concrete classes can be auto-wired.
416
+ 2. **Does `T` have a primary constructor?** — the macro inspects constructor parameters to determine dependencies.
417
+ 3. **Is each parameter either a dependency type or a special injected type?** — parameters of type `Scope` or `Finalizer` are recognized and injected automatically; others are looked up in the context.
418
+ 4. **Does `T` extend `AutoCloseable`?** — if yes, `close()` is automatically registered as a finalizer in the scope.
419
+
420
+ Example with all three features:
421
+
422
+ ```scala
423
+ import zio.blocks.scope._
424
+
425
+ final case class Config(dbUrl: String)
426
+
427
+ final class Logger(implicit finalizer: Finalizer) {
428
+ def log(msg: String): Unit = println(msg)
429
+ }
430
+
431
+ final class Database(config: Config)(implicit scope: Scope) extends AutoCloseable {
432
+ def connect(): Unit = {
433
+ scope.defer(println("database connection closed"))
434
+ println(s"connecting to ${config.dbUrl}")
435
+ }
436
+
437
+ def query(sql: String): String = s"result: $sql"
438
+
439
+ def close(): Unit = println("database closed")
440
+ }
441
+
442
+ final class Service(db: Database, logger: Logger) {
443
+ def run(): Unit = {
444
+ logger.log(db.query("SELECT 1"))
445
+ }
446
+ }
447
+
448
+ // Macro handles Finalizer injection, Scope injection, and AutoCloseable registration
449
+ val wire = Wire.shared[Service]
450
+ ```
451
+
452
+ ### What happens with subtype conflicts
453
+
454
+ If a constructor has dependencies of related types (e.g., both `FileInputStream` and `InputStream`), the macro rejects the wire because `Context` is type-indexed and cannot reliably disambiguate.
455
+
456
+ ```scala
457
+ // This will NOT compile
458
+ final class App(input: InputStream, fileInput: FileInputStream)
459
+ val wire = Wire.shared[App] // error: subtype conflict
460
+
461
+ // Fix: wrap one type to make it distinct
462
+ final case class FileInputWrapper(value: FileInputStream)
463
+ final class App(input: InputStream, fileInput: FileInputWrapper)
464
+ val wire = Wire.shared[App] // ok
465
+ ```
466
+
467
+ ## Integration with Resource.from
468
+
469
+ `Wire` is designed for use with `Resource.from[T](wires*)`, which performs whole-graph dependency injection:
470
+
471
+ ```scala
472
+ import zio.blocks.scope._
473
+
474
+ final case class AppConfig(dbUrl: String)
475
+
476
+ final class Database(config: AppConfig) extends AutoCloseable {
477
+ def query(sql: String): String = s"result: $sql"
478
+ def close(): Unit = ()
479
+ }
480
+
481
+ final class Repository(db: Database) {
482
+ def query(): String = db.query("SELECT *")
483
+ }
484
+
485
+ final class Service(repo: Repository) extends AutoCloseable {
486
+ def run(): String = repo.query()
487
+ def close(): Unit = ()
488
+ }
489
+
490
+ final class App(service: Service) {
491
+ def run(): String = service.run()
492
+ }
493
+
494
+ // Provide only the leaf dependency; Resource.from derives the rest
495
+ val appResource: Resource[App] = Resource.from[App](
496
+ Wire(AppConfig("jdbc:postgres://localhost/db"))
497
+ )
498
+
499
+ Scope.global.scoped { scope =>
500
+ import scope._
501
+ val app = allocate(appResource)
502
+ $(app)(_.run())
503
+ }
504
+ ```
505
+
506
+ When `Resource.from` composes wires, it respects the sharing strategy:
507
+ - **Shared wires** → reference-counted (single instance in the graph)
508
+ - **Unique wires** → fresh per allocation
509
+
510
+ The macro detects cycles, duplicate providers, and missing dependencies at compile time.
511
+
512
+ ## Comparison with Alternatives
513
+
514
+ | Feature | Wire | Manual Passing | Service Locator |
515
+ |---------|------|----------------|-----------------|
516
+ | **Type safety** | ✓ (compile-time validation) | ✓ (implicit) | ✗ (string keys) |
517
+ | **Cycle detection** | ✓ (compile time) | ✗ | ✗ (runtime) |
518
+ | **Sharing semantics** | ✓ (configurable) | Manual | ✓ (singleton pattern) |
519
+ | **Finalization** | ✓ (LIFO, automatic) | Manual | Manual |
520
+ | **Performance** | ~0 overhead (macro-generated) | ~0 overhead | ~1 allocation overhead |
521
+
522
+ ## Running the Examples
523
+
524
+ All code from this guide is available as runnable examples in the `scope-examples` module.
525
+
526
+ **1. Clone the repository and navigate to the project:**
527
+
528
+ ```bash
529
+ git clone https://github.com/zio/zio-blocks.git
530
+ cd zio-blocks
531
+ ```
532
+
533
+ **2. Run individual examples with sbt:**
534
+
535
+ Basic wire construction demonstrates how to create and use `Wire` for dependency injection. View the example source code:
536
+
537
+ ```scala title="scope-examples/src/main/scala/wire/WireBasicExample.scala"
538
+ package wire
539
+
540
+ import zio.blocks.scope._
541
+ import zio.blocks.context.Context
542
+
543
+ /**
544
+ * Demonstrates basic Wire construction patterns:
545
+ * - Wire.shared[T] macro derivation
546
+ * - Wire.apply(value) to lift a value
547
+ * - Converting wires to Resources
548
+ * - Using wires in a dependency graph
549
+ */
550
+
551
+ final case class DbConfig(host: String, port: Int) {
552
+ def url: String = s"jdbc:postgresql://$host:$port/db"
553
+ }
554
+
555
+ final class Database(config: DbConfig) extends AutoCloseable {
556
+ println(s"[Database] Connecting to ${config.url}")
557
+
558
+ def query(sql: String): String = {
559
+ println(s"[Database] Executing: $sql")
560
+ "result"
561
+ }
562
+
563
+ def close(): Unit =
564
+ println("[Database] Connection closed")
565
+ }
566
+
567
+ final class UserService(db: Database) {
568
+ println("[UserService] Initialized")
569
+
570
+ def getUser(id: Int): String = {
571
+ db.query(s"SELECT * FROM users WHERE id = $id")
572
+ s"User(id=$id, name=Alice)"
573
+ }
574
+ }
575
+
576
+ final class BasicApp(service: UserService) {
577
+ def run(): Unit = {
578
+ val user = service.getUser(1)
579
+ println(s"[App] Got: $user")
580
+ }
581
+ }
582
+
583
+ @main def wireBasicExample(): Unit = {
584
+ println("=== Wire Basic Construction Example ===\n")
585
+
586
+ // Create the dependency leaf (config) using Wire.apply
587
+ val configWire: Wire.Shared[Any, DbConfig] =
588
+ Wire(DbConfig("localhost", 5432))
589
+
590
+ // Derive wires for Database and UserService using the macro
591
+ val dbWire: Wire.Shared[DbConfig, Database] =
592
+ Wire.shared[Database]
593
+
594
+ val serviceWire: Wire.Shared[Database, UserService] =
595
+ Wire.shared[UserService]
596
+
597
+ val appWire: Wire.Shared[UserService, BasicApp] =
598
+ Wire.shared[BasicApp]
599
+
600
+ println("[Setup] Created all wires\n")
601
+
602
+ // Use Resource.from to automatically compose the dependency graph
603
+ val appResource: Resource[BasicApp] = Resource.from[BasicApp](
604
+ configWire,
605
+ dbWire,
606
+ serviceWire,
607
+ appWire
608
+ )
609
+
610
+ println("[Setup] Composed resource graph\n")
611
+
612
+ // Allocate within a scope
613
+ Scope.global.scoped { scope =>
614
+ import scope._
615
+
616
+ println("[Scope] Entering scoped region\n")
617
+
618
+ val app: $[BasicApp] = allocate(appResource)
619
+
620
+ println("\n[App] Running application")
621
+ $(app)(_.run())
622
+
623
+ println("\n[Scope] Exiting scoped region - finalizers will run")
624
+ }
625
+
626
+ println("\n=== Example Complete ===")
627
+ }
628
+ ```
629
+
630
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/wire/WireBasicExample.scala))
631
+
632
+ Run this example with:
633
+
634
+ ```bash
635
+ sbt "scope-examples/runMain wire.wireBasicExample"
636
+ ```
637
+
638
+ Comparing shared vs unique semantics shows how shared wires reuse the same instance across dependents, while unique wires create fresh instances. View the example:
639
+
640
+ ```scala title="scope-examples/src/main/scala/wire/WireSharedUniqueExample.scala"
641
+ package wire
642
+
643
+ import zio.blocks.scope._
644
+ import java.util.concurrent.atomic.AtomicInteger
645
+
646
+ /**
647
+ * Demonstrates the semantic difference between shared and unique wires:
648
+ * - Shared wires: same instance across dependents (reference-counted)
649
+ * - Unique wires: fresh instance per allocation
650
+ *
651
+ * Uses a counter to track how many times each service is instantiated.
652
+ */
653
+
654
+ final class Counter {
655
+ private val count = new AtomicInteger(0)
656
+
657
+ def next(): Int = count.incrementAndGet()
658
+
659
+ def value: Int = count.get()
660
+ }
661
+
662
+ final class ServiceA(counter: Counter) {
663
+ val id = counter.next()
664
+ println(s"[ServiceA] Initialized with counter id=$id")
665
+ }
666
+
667
+ final class ServiceB(counter: Counter) {
668
+ val id = counter.next()
669
+ println(s"[ServiceB] Initialized with counter id=$id")
670
+ }
671
+
672
+ final class SharedDependencyApp(a: ServiceA, b: ServiceB) {
673
+ def checkSharing(): Boolean = {
674
+ // If Counter is shared, both services got the same instance
675
+ val aCountId = a.id
676
+ val bCountId = b.id
677
+ println(s"[SharedDependencyApp] ServiceA counter id=$aCountId, ServiceB counter id=$bCountId")
678
+ aCountId != bCountId && a.id < b.id // Sequential IDs from same Counter
679
+ }
680
+ }
681
+
682
+ final class UniqueDependencyApp(a: ServiceA, b: ServiceB) {
683
+ def checkUniqueness(): Boolean = {
684
+ // If Counter is unique, services got different instances (different starting IDs)
685
+ val aCountId = a.id
686
+ val bCountId = b.id
687
+ println(s"[UniqueDependencyApp] ServiceA counter id=$aCountId, ServiceB counter id=$bCountId")
688
+ aCountId != bCountId // Different Counter instances
689
+ }
690
+ }
691
+
692
+ @main def wireSharedUniqueExample(): Unit = {
693
+ println("=== Wire Shared vs Unique Example ===\n")
694
+
695
+ println("--- Test 1: Shared Counter (diamond pattern) ---\n")
696
+
697
+ // Using a SHARED Counter: both ServiceA and ServiceB share the same Counter instance
698
+ val sharedResource: Resource[SharedDependencyApp] = Resource.from[SharedDependencyApp](
699
+ Wire.shared[Counter] // Shared: one instance across the graph
700
+ )
701
+
702
+ Scope.global.scoped { scope =>
703
+ import scope._
704
+
705
+ println("[Scope] Entering scoped region\n")
706
+
707
+ val app = allocate(sharedResource)
708
+ val isShared = $(app)(_.checkSharing())
709
+
710
+ println(s"\n[Result] Counter was shared: $isShared\n")
711
+ }
712
+
713
+ println("\n--- Test 2: Unique Counter ---\n")
714
+
715
+ // Using a UNIQUE Counter: each dependency gets a fresh Counter instance
716
+ val uniqueResource: Resource[UniqueDependencyApp] = Resource.from[UniqueDependencyApp](
717
+ Wire.unique[Counter] // Unique: fresh instance per dependency
718
+ )
719
+
720
+ Scope.global.scoped { scope =>
721
+ import scope._
722
+
723
+ println("[Scope] Entering scoped region\n")
724
+
725
+ val app = allocate(uniqueResource)
726
+ val isUnique = $(app)(_.checkUniqueness())
727
+
728
+ println(s"\n[Result] Counters were unique: $isUnique\n")
729
+ }
730
+
731
+ println("\n=== Example Complete ===")
732
+ }
733
+ ```
734
+
735
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/wire/WireSharedUniqueExample.scala))
736
+
737
+ Run this example with:
738
+
739
+ ```bash
740
+ sbt "scope-examples/runMain wire.wireSharedUniqueExample"
741
+ ```
742
+
743
+ Manual wire construction demonstrates how to use `fromFunction` for custom construction logic. View the example:
744
+
745
+ ```scala title="scope-examples/src/main/scala/wire/WireFromFunctionExample.scala"
746
+ package wire
747
+
748
+ import zio.blocks.scope._
749
+ import zio.blocks.context.Context
750
+
751
+ /**
752
+ * Demonstrates manual wire construction using fromFunction:
753
+ * - Wire.Shared.fromFunction for custom shared wire logic
754
+ * - Wire.Unique.fromFunction for custom unique wire logic
755
+ * - Using manual wires when macro derivation doesn't fit
756
+ *
757
+ * This is useful for complex initialization, conditional logic, or when you
758
+ * need control over which dependencies to use.
759
+ */
760
+
761
+ final case class ApiKey(value: String)
762
+
763
+ final case class HttpConfig(baseUrl: String, timeout: Int)
764
+
765
+ /**
766
+ * A custom HTTP client that uses an API key and timeout from config.
767
+ * Demonstrates a scenario where we want custom construction logic rather than
768
+ * simple parameter passing.
769
+ */
770
+ final class HttpClient(config: HttpConfig, apiKey: ApiKey) extends AutoCloseable {
771
+ println(
772
+ s"[HttpClient] Created with baseUrl=${config.baseUrl}, timeout=${config.timeout}ms, apiKey=${apiKey.value}"
773
+ )
774
+
775
+ def get(path: String): String = {
776
+ println(s"[HttpClient] GET $path with api key")
777
+ "response"
778
+ }
779
+
780
+ def close(): Unit =
781
+ println("[HttpClient] Connection pool closed")
782
+ }
783
+
784
+ /**
785
+ * A custom authenticator that needs the API key. Demonstrates context
786
+ * extraction in a manual wire.
787
+ */
788
+ final class Authenticator(apiKey: ApiKey) {
789
+ println(s"[Authenticator] Using API key: ${apiKey.value}")
790
+
791
+ def authenticate(): Boolean = {
792
+ println("[Authenticator] Validating API key...")
793
+ true
794
+ }
795
+ }
796
+
797
+ final class ManualWireApp(client: HttpClient, auth: Authenticator) {
798
+ def run(): Unit =
799
+ if (auth.authenticate()) {
800
+ val response = client.get("/api/users")
801
+ println(s"[App] Got response: $response")
802
+ }
803
+ }
804
+
805
+ @main def wireFromFunctionExample(): Unit = {
806
+ println("=== Wire fromFunction (Manual Construction) Example ===\n")
807
+
808
+ // Manually create wires using fromFunction for custom logic
809
+ // This gives full control when macro derivation isn't suitable
810
+
811
+ val httpClientWire: Wire.Shared[HttpConfig & ApiKey, HttpClient] =
812
+ Wire.Shared.fromFunction { (scope, ctx) =>
813
+ // Extract both dependencies from context
814
+ val config = ctx.get[HttpConfig]
815
+ val apiKey = ctx.get[ApiKey]
816
+
817
+ // Custom initialization logic
818
+ println("[Manual] Custom HttpClient construction")
819
+ val client = new HttpClient(config, apiKey)
820
+
821
+ // Register custom cleanup if needed (in addition to AutoCloseable)
822
+ scope.defer(println("[Manual] HttpClient cleanup deferred"))
823
+
824
+ client
825
+ }
826
+
827
+ val authenticatorWire: Wire.Shared[ApiKey, Authenticator] =
828
+ Wire.Shared.fromFunction { (scope, ctx) =>
829
+ val apiKey = ctx.get[ApiKey]
830
+
831
+ // Custom initialization logic
832
+ println("[Manual] Custom Authenticator construction")
833
+ val auth = new Authenticator(apiKey)
834
+
835
+ scope.defer(println("[Manual] Authenticator cleanup deferred"))
836
+
837
+ auth
838
+ }
839
+
840
+ // Provide leaf dependencies
841
+ val configWire = Wire(HttpConfig("https://api.example.com", 30000))
842
+ val apiKeyWire = Wire(ApiKey("secret-key-12345"))
843
+
844
+ // Compose into the app resource
845
+ val appResource: Resource[ManualWireApp] = Resource.from[ManualWireApp](
846
+ configWire,
847
+ apiKeyWire,
848
+ httpClientWire,
849
+ authenticatorWire
850
+ )
851
+
852
+ println("[Setup] Created manual wires\n")
853
+
854
+ // Allocate and run
855
+ Scope.global.scoped { scope =>
856
+ import scope._
857
+
858
+ println("[Scope] Entering scoped region\n")
859
+
860
+ val app = allocate(appResource)
861
+
862
+ println("\n[App] Running application")
863
+ $(app)(_.run())
864
+
865
+ println("\n[Scope] Exiting scoped region - finalizers will run")
866
+ }
867
+
868
+ println("\n=== Example Complete ===")
869
+ }
870
+ ```
871
+
872
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/wire/WireFromFunctionExample.scala))
873
+
874
+ Run this example with:
875
+
876
+ ```bash
877
+ sbt "scope-examples/runMain wire.wireFromFunctionExample"
878
+ ```