@zio.dev/zio-blocks 0.0.51 → 0.0.56

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 (166) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -559
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/endpoint.md +1 -0
  37. package/reference/endpoint/index.md +9 -89
  38. package/reference/endpoint/path-codec.md +12 -24
  39. package/reference/endpoint/route-pattern.md +4 -6
  40. package/reference/endpoint/segment-codec.md +19 -32
  41. package/reference/html.md +313 -9
  42. package/reference/htmx/index.md +4 -52
  43. package/reference/htmx/response-headers.md +240 -0
  44. package/reference/http-model/headers.md +735 -0
  45. package/reference/http-model/index.md +3 -1
  46. package/reference/http-model/model.md +107 -71
  47. package/reference/http-model/schema-codecs.md +522 -0
  48. package/reference/http-model/schema.md +6 -3
  49. package/reference/http-model/server-sent-event.md +341 -0
  50. package/reference/jwt.md +195 -0
  51. package/reference/maybe.md +128 -11
  52. package/reference/media-type.md +2 -2
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/index.md +1 -1
  57. package/reference/resource-management/resource.md +2 -98
  58. package/reference/resource-management/scope.md +1 -209
  59. package/reference/resource-management/wire.md +4 -50
  60. package/reference/ringbuffer/advanced.mdx +1 -1
  61. package/reference/ringbuffer/index.mdx +3 -3
  62. package/reference/ringbuffer/mpmc.mdx +38 -4
  63. package/reference/ringbuffer/mpsc.mdx +36 -4
  64. package/reference/ringbuffer/spmc.mdx +1 -1
  65. package/reference/ringbuffer/spsc.mdx +87 -15
  66. package/reference/schema/allows.md +0 -96
  67. package/reference/schema/binding.md +2 -2
  68. package/reference/schema/built-in-codecs/avro.md +2 -2
  69. package/reference/schema/built-in-codecs/bson.md +50 -20
  70. package/reference/schema/built-in-codecs/csv.md +2 -2
  71. package/reference/schema/built-in-codecs/index.md +3 -3
  72. package/reference/schema/built-in-codecs/json/index.md +2 -2
  73. package/reference/schema/built-in-codecs/json/json.md +1 -0
  74. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  75. package/reference/schema/built-in-codecs/thrift.md +2 -2
  76. package/reference/schema/built-in-codecs/toon.md +3 -3
  77. package/reference/schema/built-in-codecs/yaml.md +2 -2
  78. package/reference/schema/codec.md +11 -11
  79. package/reference/schema/dynamic-optic.md +48 -3
  80. package/reference/schema/dynamic-schema.md +3 -3
  81. package/reference/schema/index.md +2 -0
  82. package/reference/schema/path-interpolator.md +2 -0
  83. package/reference/schema/reflect-transformer.md +140 -0
  84. package/reference/schema/schema-evolution/as.md +4 -4
  85. package/reference/schema/schema-evolution/into.md +2 -2
  86. package/reference/schema/schema-expr.md +2 -2
  87. package/reference/schema/schema-search.md +263 -0
  88. package/reference/schema/schema.md +10 -2
  89. package/reference/schema/type-class-derivation.md +1 -1
  90. package/reference/smithy.md +502 -3
  91. package/reference/sql/db-codec-deriver.md +3 -3
  92. package/reference/sql/db-codec.md +22 -22
  93. package/reference/sql/db-con.md +4 -4
  94. package/reference/sql/db-connection.md +1 -1
  95. package/reference/sql/db-param.md +1 -1
  96. package/reference/sql/db-result-reader.md +4 -2
  97. package/reference/sql/db-tx.md +46 -14
  98. package/reference/sql/ddl.md +1 -1
  99. package/reference/sql/frag.md +44 -10
  100. package/reference/sql/index.md +7 -7
  101. package/reference/sql/repo.md +15 -15
  102. package/reference/sql/sql-dialect.md +1 -1
  103. package/reference/sql/sql-logger.md +1 -1
  104. package/reference/sql/sql-name-mapper.md +3 -3
  105. package/reference/sql/table-metadata.md +3 -3
  106. package/reference/sql/table.md +10 -10
  107. package/reference/sql/transactor-zio.md +1 -1
  108. package/reference/sql/transactor.md +21 -11
  109. package/reference/sql-zio.md +2 -2
  110. package/reference/streams/core/index.md +32 -0
  111. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  112. package/reference/streams/{sink.md → core/sink.md} +331 -353
  113. package/reference/streams/{stream.md → core/stream.md} +919 -209
  114. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  115. package/reference/streams/execution-and-compatibility/index.md +35 -0
  116. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  117. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  118. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  119. package/reference/streams/index.md +140 -67
  120. package/reference/streams/primitives/index.md +30 -0
  121. package/reference/streams/primitives/reader.md +1992 -0
  122. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  123. package/reference/telemetry/common/any-value.md +90 -0
  124. package/reference/telemetry/common/attribute-key.md +87 -0
  125. package/reference/telemetry/common/attributes.md +118 -0
  126. package/reference/telemetry/common/index.md +39 -0
  127. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  128. package/reference/telemetry/common/resource.md +34 -0
  129. package/reference/telemetry/index.md +311 -0
  130. package/reference/telemetry/logging/index.md +197 -0
  131. package/reference/telemetry/logging/log-enrichment.md +72 -0
  132. package/reference/telemetry/logging/log-formatter.md +100 -0
  133. package/reference/telemetry/logging/log-record-processor.md +56 -0
  134. package/reference/telemetry/logging/log-record.md +44 -0
  135. package/reference/telemetry/logging/log-writer.md +64 -0
  136. package/reference/telemetry/logging/logger-provider.md +142 -0
  137. package/reference/telemetry/logging/logger.md +83 -0
  138. package/reference/telemetry/logging/severity.md +62 -0
  139. package/reference/telemetry/metrics/index.md +150 -0
  140. package/reference/telemetry/metrics/instruments.md +183 -0
  141. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  142. package/reference/telemetry/metrics/meter-provider.md +76 -0
  143. package/reference/telemetry/metrics/meter.md +98 -0
  144. package/reference/telemetry/metrics/metric-data.md +57 -0
  145. package/reference/telemetry/otel/custom-exporter.md +216 -0
  146. package/reference/telemetry/otel/index.md +212 -0
  147. package/reference/telemetry/tracing/index.md +155 -0
  148. package/reference/telemetry/tracing/sampler.md +89 -0
  149. package/reference/telemetry/tracing/span-builder.md +57 -0
  150. package/reference/telemetry/tracing/span-context.md +39 -0
  151. package/reference/telemetry/tracing/span-data.md +32 -0
  152. package/reference/telemetry/tracing/span-kind.md +55 -0
  153. package/reference/telemetry/tracing/span-processor.md +53 -0
  154. package/reference/telemetry/tracing/span-status.md +47 -0
  155. package/reference/telemetry/tracing/span.md +117 -0
  156. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  157. package/reference/telemetry/tracing/tracer.md +52 -0
  158. package/reference/typeid.md +0 -64
  159. package/sidebars.js +365 -185
  160. package/undocumented-report.md +528 -270
  161. package/reference/config.md +0 -158
  162. package/reference/streams/concurrent-operators.md +0 -106
  163. package/reference/streams/reader.md +0 -1284
  164. package/reference/streams/scala-2-compatibility.md +0 -55
  165. package/reference/streams/zero-boxing.md +0 -275
  166. package/reference/telemetry.md +0 -693
@@ -38,10 +38,12 @@ This separation keeps your domain logic portable and testable while maintaining
38
38
 
39
39
  ## Getting Started
40
40
 
41
- Start with the [HTTP Model](./model.md) to understand the core data types and how they compose. Then explore [Schema-Based Typed Access](./schema.md) to learn how to safely extract and validate HTTP parameters and headers.
41
+ Start with the [HTTP Model](./model.md) to understand the core data types and how they compose. [Header](./headers.md) covers the typed header model — the 75 built-in headers, the codec type class behind them, and the parse cache — and [ServerSentEvent](./server-sent-event.md) covers the SSE envelope and its wire format. Then explore [Schema-Based Typed Access](./schema.md) to learn how to safely extract and validate HTTP parameters and headers.
42
42
 
43
43
  ---
44
44
 
45
45
  **Modules:**
46
46
  - [`zio-http-model`](./model.md) — Core immutable data types for HTTP
47
+ - [`zio-http-model`](./headers.md) — Typed header model and the `Header.Codec` type class
48
+ - [`zio-http-model`](./server-sent-event.md) — Server-Sent Event envelope and payload encoders
47
49
  - [`zio-http-model-schema`](./schema.md) — Schema-based typed extraction for query parameters and headers
@@ -3,7 +3,7 @@ id: model
3
3
  title: "HTTP Model"
4
4
  ---
5
5
 
6
- `zio-http-model` is a **pure, zero-dependency HTTP data model** for building HTTP clients and servers. It provides immutable types representing all HTTP concepts: requests, responses, headers, URLs, paths, query parameters, methods, status codes, versions, cookies, and forms.
6
+ `zio-http-model` is a **runtime-independent HTTP data model** for building HTTP clients and servers. It provides immutable types representing all HTTP concepts: requests, responses, headers, URLs, paths, query parameters, methods, status codes, versions, cookies, and forms.
7
7
 
8
8
  Core types: `Request`, `Response`, `URL`, `Headers`, `Body`, `Method`, `Status`, `Version`, `Scheme`, `Path`, `QueryParams`, `ContentType`, `RequestCookie`, `ResponseCookie`, `Form`.
9
9
 
@@ -42,19 +42,19 @@ Imagine building a distributed system where you need an HTTP client to call exte
42
42
  This creates a coupling problem:
43
43
 
44
44
  **Scenario 1: Sharing Types Across Layers**
45
- You want your client request logic (building a request to send) to use the same types as your server request handling (receiving and parsing a request). But your HTTP library makes this difficult — the `Request` type is tied to async effects, file streams, or a specific Scala version's IO model. Sharing becomes messy.
45
+ You want your client request logic (building a request to send) to use the same types as your server request handling (receiving and parsing a request). But your HTTP library makes this difficult — the `Request` type is tied to a particular runtime, file-stream implementation, or async framework. Sharing becomes messy.
46
46
 
47
- **Scenario 2: Testing Without Effects**
48
- You're writing unit tests for your request-building logic. You want to serialize a request to JSON for snapshots, or cache requests for debugging. But your `Request` type requires pulling in async runtimes, streaming libraries, or other baggage you don't need in tests. A simple unit test becomes a production-grade effect setup.
47
+ **Scenario 2: Testing Without an HTTP Runtime**
48
+ You're writing unit tests for your request-building logic. You want to serialize a request to JSON for snapshots, or cache requests for debugging. But your `Request` type requires pulling in an HTTP runtime or other baggage you don't need in tests. A simple unit test becomes a production-grade setup.
49
49
 
50
50
  **Scenario 3: Lock-In**
51
51
  You've built your entire API client around ZIO's HTTP library, but your team decides to use Akka for one microservice. Now your request/response types aren't portable — they're coupled to ZIO. Refactoring is painful.
52
52
 
53
- ### The Solution: Pure HTTP Data
53
+ ### The Solution: Runtime-Independent HTTP Data
54
54
 
55
55
  `zio-http-model` separates **protocol concerns** (representing HTTP messages) from **effect concerns** (actually sending/receiving them). It provides:
56
56
 
57
- - **Pure immutable data types** — `Request`, `Response`, `URL`, `Headers`, and chunk-backed `Body` values are just data. Stream-backed bodies are still effect-free, but collecting them may consume the underlying stream.
57
+ - **Immutable message values** — `Request`, `Response`, `URL`, and `Headers` are immutable data. `Body` wraps a pull-based byte stream; materializing the complete body is represented by the lightweight `Async` abstraction.
58
58
 
59
59
  - **No dependency on an HTTP runtime** — Not coupled to ZIO HTTP, Akka, or a server/client implementation. The module depends on ZIO Blocks primitives such as Chunk, MediaType, and Stream.
60
60
 
@@ -70,13 +70,13 @@ This separation is powerful: you can build, manipulate, serialize, and test HTTP
70
70
  Add the following to your `build.sbt`:
71
71
 
72
72
  ```scala
73
- libraryDependencies += "dev.zio" %% "zio-http-model" % "0.0.51"
73
+ libraryDependencies += "dev.zio" %% "zio-http-model" % "0.0.56"
74
74
  ```
75
75
 
76
76
  For cross-platform projects (Scala.js):
77
77
 
78
78
  ```scala
79
- libraryDependencies += "dev.zio" %%% "zio-http-model" % "0.0.51"
79
+ libraryDependencies += "dev.zio" %%% "zio-http-model" % "0.0.56"
80
80
  ```
81
81
 
82
82
  Supported Scala versions: Scala 2.13 and Scala 3.x. The artifacts are cross-platform for JVM and Scala.js.
@@ -635,37 +635,79 @@ val allIds = params.get("id") // Some(Chunk("1", "2", "3"))
635
635
 
636
636
  ## Headers
637
637
 
638
- `Headers` is an immutable, case-insensitive collection of HTTP headers with lazy parsing for typed header access:
638
+ `Headers` is an immutable, case-insensitive collection of HTTP header fields. It stores names pre-lowercased and raw values as strings, allows several entries with the same name, and parses a value into a typed `Header` only when a typed read asks for it:
639
639
 
640
640
  ```scala
641
- final class Headers private[http] (...)
641
+ final class Headers private[http] (...) {
642
+ def size: Int
643
+ def get[A](headerCodec: Header.Codec[A]): Option[A]
644
+ def rawGet(name: String): Option[String]
645
+ def add(header: Header): Headers
646
+ def set(header: Header): Headers
647
+ def remove(name: String): Headers
648
+ }
642
649
  ```
643
650
 
651
+ This section covers the collection itself. The 75 built-in typed headers, the codec type class behind them, the parse cache, and the validation rules all live on the [Header](./headers.md) page.
652
+
644
653
  ### Creating Headers
645
654
 
646
- Create header collections:
655
+ `Headers.apply` takes name-value pairs, and `Headers.empty` is the starting point for a builder chain:
647
656
 
648
657
  ```scala
649
658
  import zio.http.Headers
650
659
 
651
- val headers1 = Headers("content-type" -> "application/json", "authorization" -> "Bearer token123")
652
- val headers2 = Headers.empty
660
+ val headers = Headers("content-type" -> "application/json", "cache-control" -> "no-cache")
653
661
  ```
654
662
 
655
- ### Headers Operations
663
+ Names are lowercased as they are stored, so the collection reports them in canonical form regardless of how they were written:
664
+
665
+ ```scala
666
+ Headers("Content-Type" -> "application/json").toList
667
+ // res27: List[Tuple2[String, String]] = List(
668
+ // ("content-type", "application/json")
669
+ // )
670
+ ```
656
671
 
657
- Access and modify headers:
672
+ ### Reading Raw Values
673
+
674
+ Three methods read the stored string without parsing, differing in which entry they pick when a name repeats:
658
675
 
659
676
  ```scala
660
- import zio.http.Headers
677
+ headers.rawGet("Content-Type")
678
+ // res28: Option[String] = Some("application/json")
679
+ headers.rawGetAll("cache-control")
680
+ // res29: Chunk[String] = IndexedSeq("no-cache")
681
+ headers.has("content-type")
682
+ // res30: Boolean = true
683
+ ```
661
684
 
662
- val headers = Headers("content-type" -> "application/json", "cache-control" -> "no-cache")
685
+ Lookups are case-insensitive, and `Headers#contains` is an alias for `Headers#has`. For typed reads — `Headers#get`, `Headers#getAll`, and `Headers#getLast`, which decode into a `Header` — see [Header](./headers.md).
686
+
687
+ ### Modifying Headers
663
688
 
664
- // Headers can be queried and modified using methods
665
- val withAuth = headers.add("authorization", "Bearer token") // Add header
666
- val asList = headers.toList // All headers as List
689
+ `Headers#add` appends and `Headers#set` replaces every entry with the same name, which is the distinction that matters for multi-value headers:
690
+
691
+ ```scala
692
+ val twice = headers.add("set-cookie", "a=1").add("set-cookie", "b=2")
693
+ val replaced = twice.set("set-cookie", "c=3")
694
+ ```
695
+
696
+ Appending keeps both cookies while setting collapses them to one:
697
+
698
+ ```scala
699
+ twice.rawGetAll("set-cookie")
700
+ // res31: Chunk[String] = IndexedSeq("a=1", "b=2")
701
+ replaced.rawGetAll("set-cookie")
702
+ // res32: Chunk[String] = IndexedSeq("c=3")
667
703
  ```
668
704
 
705
+ `Headers#remove` drops every entry with a name, `Headers#++` concatenates two collections without deduplicating, and `Headers#toList` and `Headers#toChunk` expose the pairs for iteration. Every operation returns a new `Headers`.
706
+
707
+ :::warning[Names and values are validated by throwing]
708
+ A header name must be an HTTP token and a value must not contain CR or LF — the latter is what prevents response splitting. The mutating methods enforce both with `IllegalArgumentException`, so check untrusted input with `Headers.validateName` or `Headers.validateValue` first. See [Header](./headers.md) for the rules and the safe pre-checks.
709
+ :::
710
+
669
711
  ---
670
712
 
671
713
  ## Body
@@ -748,14 +790,37 @@ val body = Body.fromString("Hello!", Charset.UTF8)
748
790
  body.length // Some(6)
749
791
  body.isEmpty // false
750
792
  body.nonEmpty // true
751
- body.asString() // "Hello!" (UTF-8 default)
752
- body.asString(Charset.ASCII) // "Hello!" (explicit charset)
753
- body.toChunk // Chunk[Byte](72, 101, 108, 108, 111, 33)
754
793
  body.toStream // Stream[Nothing, Byte]
755
- body.toArray // Array[Byte](72, 101, 108, 108, 111, 33)
756
794
  body.contentType // ContentType(text/plain; charset=UTF-8)
795
+
796
+ val utf8: zio.blocks.async.Async[String] = body.asStringAsync()
797
+ val ascii: zio.blocks.async.Async[String] = body.asStringAsync(Charset.ASCII)
798
+ val bytes: zio.blocks.async.Async[Array[Byte]] = body.toArrayAsync
799
+ val chunk: zio.blocks.async.Async[zio.blocks.chunk.Chunk[Byte]] = body.toChunkAsync
800
+ val fromContentType: zio.blocks.async.Async[String] = body.asStringFromContentTypeAsync
801
+ val text: zio.blocks.async.Async[String] = body.textAsync
802
+ ```
803
+
804
+ The five asynchronous materializers—`toChunkAsync`, `toArrayAsync`, `asStringAsync`, `asStringFromContentTypeAsync`, and the `textAsync` alias—are defined in shared sources and are available on both JVM and Scala.js. `asStringFromContentTypeAsync` and `textAsync` use the charset declared by the content type, falling back to UTF-8.
805
+
806
+ `Body` retains synchronous twins with the corresponding result types:
807
+
808
+ ```scala
809
+ import zio.http.{Body, Charset}
810
+ import zio.blocks.chunk.Chunk
811
+
812
+ val body = Body.fromString("Hello!", Charset.UTF8)
813
+
814
+ val chunk: Chunk[Byte] = body.toChunk
815
+ val bytes: Array[Byte] = body.toArray
816
+ val utf8: String = body.asString()
817
+ val ascii: String = body.asString(Charset.ASCII)
818
+ val fromContentType: String = body.asStringFromContentType
819
+ val text: String = body.text
757
820
  ```
758
821
 
822
+ The synchronous methods remain available on both platforms for compatibility and for streams that complete synchronously. Prefer the asynchronous methods whenever a body can suspend; Scala.js cannot block while waiting for pending asynchronous work.
823
+
759
824
  ---
760
825
 
761
826
  ## ContentType
@@ -1328,69 +1393,36 @@ This design shines in three ways:
1328
1393
 
1329
1394
  **Simplicity**: Your code is clean — you just ask for headers by name, and http-model handles parsing transparently. No manual string manipulation or error handling on your end.
1330
1395
 
1331
- ### No Streaming
1396
+ ### Stream-Backed Bodies
1332
1397
 
1333
1398
  Let's say you're downloading a 500MB video file over HTTP. Should your `Body` object represent that as:
1334
1399
 
1335
1400
  **Option A: A single `Chunk[Byte]` with all 500MB in memory?**
1336
1401
 
1337
1402
  ```scala
1338
- val body = Body(data = Chunk[Byte](/* 500MB of bytes */))
1403
+ val body = Body.fromChunk(Chunk[Byte](/* 500MB of bytes */))
1339
1404
  // Everything loaded into RAM at once
1340
1405
  ```
1341
1406
 
1342
1407
  **Option B: A Stream that yields bytes incrementally as they arrive?**
1343
1408
 
1344
1409
  ```scala
1345
- val body = Body(data = Stream[Byte]) // Yields chunks as they download
1410
+ val byteStream: Stream[Nothing, Byte] = /* yields bytes as they download */
1411
+ val body = Body.fromStream(byteStream)
1346
1412
  // Only a small buffer in RAM; the rest comes from the network
1347
1413
  ```
1348
1414
 
1349
1415
  Most HTTP libraries choose Option B for large files — streaming makes sense when you want to process data *as it arrives* without loading everything into memory first.
1350
1416
 
1351
- **http-model chooses an effect-free stream-backed body.** Here's why.
1417
+ **http-model chooses a stream-backed body with runtime-independent asynchronous materialization.**
1352
1418
 
1353
1419
  #### The Streaming Trade-off
1354
1420
 
1355
- Streaming sounds great on paper — save memory, start processing immediately — but it brings complexity:
1356
-
1357
- **Streaming requires effects:**
1358
-
1359
- ```scala
1360
- // With streaming, reading a body becomes an effect:
1361
- val body: Body = request.body
1362
- val bytes: IO[Chunk[Byte]] = body.stream.runCollect()
1363
- // Reading the body is now an IO operation, not a pure value!
1364
- ```
1365
-
1366
- This couples `Body` to a specific effect system (ZIO, Cats Effect, Scala Futures, etc.). Different effect systems have different streaming abstractions, and your `Body` type would need to know about all of them — or you'd lock users into one.
1367
-
1368
- **Streaming requires error handling:**
1369
- ```scala
1370
- // With streaming, errors can happen mid-stream:
1371
- body.stream.fold(
1372
- error => handleNetworkFailure(error), // Network cut out!
1373
- chunk => processChunk(chunk),
1374
- () => done()
1375
- )
1376
- // You must handle errors at every chunk boundary
1377
- ```
1378
-
1379
- **Streaming complicates testing:**
1380
-
1381
- ```scala
1382
- // Testing code that consumes streams is verbose:
1383
- val testStream = Stream(
1384
- Chunk(1, 2, 3),
1385
- Chunk(4, 5, 6),
1386
- Chunk(7, 8, 9)
1387
- ).flatMap(_.stream)
1388
- // vs. just: Chunk(1, 2, 3, 4, 5, 6, 7, 8, 9)
1389
- ```
1421
+ Streaming saves memory and allows processing to begin before the complete payload arrives, but collecting the complete body may need to wait for asynchronous input. `Body` exposes that operation as `Async`, rather than coupling the HTTP model to ZIO, Cats Effect, Scala Futures, or a particular HTTP runtime.
1390
1422
 
1391
1423
  #### http-model's Choice: Stream-Backed Bodies
1392
1424
 
1393
- http-model wraps a `Stream[Nothing, Byte]` — a synchronous, pull-based stream with no effect system:
1425
+ http-model wraps a `Stream[Nothing, Byte]` — a pull-based stream with cross-platform asynchronous materialization and no required effect runtime:
1394
1426
 
1395
1427
  ```scala
1396
1428
  final class Body private (
@@ -1407,13 +1439,17 @@ val body = Body.fromChunk(
1407
1439
  ContentType.`application/json`
1408
1440
  )
1409
1441
 
1410
- // Accessing the data:
1411
- val bytes: Chunk[Byte] = body.toChunk // Materializes the stream (O(1) for chunk-backed bodies)
1412
- val len: Option[Long] = body.length // Known length without materializing, if available
1413
- val raw: Stream[Nothing, Byte] = body.toStream // Access the underlying stream directly
1442
+ // Accessing the data on JVM and Scala.js:
1443
+ val chunk: Async[Chunk[Byte]] = body.toChunkAsync
1444
+ val bytes: Async[Array[Byte]] = body.toArrayAsync
1445
+ val decoded: Async[String] = body.asStringAsync(Charset.UTF8)
1446
+ val decodedFromContentType: Async[String] = body.asStringFromContentTypeAsync
1447
+ val text: Async[String] = body.textAsync
1448
+ val len: Option[Long] = body.length
1449
+ val raw: Stream[Nothing, Byte] = body.toStream
1414
1450
  ```
1415
1451
 
1416
- For chunk-backed bodies, `toChunk` is O(1) and `length` returns `Some(n)`. For stream-backed bodies, `toChunk` runs the stream to collect all bytes and `length` returns `None`.
1452
+ For chunk-backed bodies, materialization reuses the known chunk and `length` returns `Some(n)`. For a stream without known metadata, materialization consumes the stream and `length` returns `None`. The shared `Body.scala` defines both the five `Async` methods and their synchronous compatibility twins.
1417
1453
 
1418
1454
  ```
1419
1455
  Your Application Code
@@ -1428,12 +1464,12 @@ Your Application Code
1428
1464
  (wraps/unwraps)
1429
1465
  ↓
1430
1466
  ┌─────────────────────┐
1431
- │ http-model Body │ (stream-backed, synchronous, no effects)
1467
+ │ http-model Body │ (stream-backed, async materialization)
1432
1468
  │ (pull-based I/O) │
1433
1469
  └─────────────────────┘
1434
1470
  ```
1435
1471
 
1436
- Body is synchronous and effect-free — it uses ZIO Blocks' pull-based `Stream`, not an effectful stream type. When the body wraps a known `Chunk`, access is pure and immediate. When it wraps an opaque stream, `toChunk` pulls all bytes on demand.
1472
+ Body remains independent of any HTTP or effect runtime, but materializing an opaque stream can be asynchronous. Prefer `toChunkAsync`, `toArrayAsync`, `asStringAsync`, `asStringFromContentTypeAsync`, or `textAsync` for streams that can suspend. The synchronous twins remain available on both platforms for compatibility and synchronously completing streams.
1437
1473
 
1438
1474
  ## Running the Examples
1439
1475