@zio.dev/zio-blocks 0.0.33 → 0.0.55
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/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +1499 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +1032 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: async
|
|
3
|
+
title: "Async"
|
|
4
|
+
description: "Reference for the ZIO Blocks async module: Async[A], Pollable, Completer, Async.Running, and Cancelable."
|
|
5
|
+
keywords:
|
|
6
|
+
- "Asynchronous Effects"
|
|
7
|
+
- "Eager Evaluation"
|
|
8
|
+
- "Callback Bridge"
|
|
9
|
+
- "Structured Cancellation"
|
|
10
|
+
- "Async"
|
|
11
|
+
- "Pollable"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
import Tabs from '@theme/Tabs';
|
|
15
|
+
import TabItem from '@theme/TabItem';
|
|
16
|
+
|
|
17
|
+
The `async` module provides `Async[A]` — a small asynchronous effect type for Scala 2.13 and Scala 3, targeting both JVM and Scala.js.
|
|
18
|
+
|
|
19
|
+
An `Async[A]` value is a computation that either yields an `A` or fails with a `Throwable`. Unlike a lazy effect type, building one *runs* it: the synchronous work happens as you construct it, and only a computation that genuinely has to wait for something is left pending. [Evaluation model](#evaluation-model) explains where that line falls.
|
|
20
|
+
|
|
21
|
+
Conceptually, an `Async[A]` is one of three things — a value that is already available, a failure that has already happened, or a computation that will complete later:
|
|
22
|
+
|
|
23
|
+
```scala
|
|
24
|
+
// The mental model, not the real encoding.
|
|
25
|
+
enum Async[+A]:
|
|
26
|
+
case Ready(value: A) // already available
|
|
27
|
+
case Failed(cause: Throwable) // already failed
|
|
28
|
+
case Suspended(source: Pollable[A]) // completes later, via a callback
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The real definition is a type alias whose runtime representation is `Any`. That is a performance decision, not a modelling one: a ready value *is* its own `Async`, so the common path allocates nothing and boxes nothing. Reach for the three cases above when reasoning about behaviour, and let the encoding stay invisible — no combinator in this module requires you to know it.
|
|
32
|
+
|
|
33
|
+
Suspension is where the other types enter. A `Pollable[A]` is the extension point that produces a not-yet-available result, [`Completer`](#completer) is the ready-made `Pollable` for bridging callbacks, [`Async.Running`](#asyncrunning) is a `Pollable` you can also cancel, and [`Cancelable`](#cancelable) is that cancellation interface on its own.
|
|
34
|
+
|
|
35
|
+
The type is aimed at infrastructure-level code: places that need a first-class asynchronous value, on both the JVM and Scala.js, without taking on an ecosystem to get one. Where a fuller effect system offers an environment type, a typed error channel and fibers, `Async` offers a computation, a `Throwable`, and a handle you can cancel — and in exchange stays cheap enough to use on paths that are usually synchronous.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Add the module to your build:
|
|
40
|
+
|
|
41
|
+
```scala
|
|
42
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-async" % "0.0.55"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
In a cross-built project, use `%%%` so the same line resolves for both JVM and Scala.js:
|
|
46
|
+
|
|
47
|
+
```scala
|
|
48
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-async" % "0.0.55"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The module publishes for JVM and Scala.js, on Scala 2.13 and Scala 3. That single coordinate is all you add: it brings `zio-blocks-combinators` with it, along with `dotty-cps-async` on Scala 3 or `scala-reflect` on Scala 2, both of which power the direct-style rewrite.
|
|
52
|
+
|
|
53
|
+
## Overview
|
|
54
|
+
|
|
55
|
+
`Async[A]` is the type you will spend nearly all your time with. Every way of creating a value, every way of transforming one, and every way of running one produces or consumes an `Async[A]`. If you only learn this type, you can already write complete programs — the rest of the module exists to feed values into it or to control one that is already running.
|
|
56
|
+
|
|
57
|
+
`Completer[A]` is the type you reach for next, and the reason is a problem you have almost certainly hit: some library hands you a result through a callback rather than returning it. `Async.promise` gives you a `Completer`, you pass that to the callback, and you get back an `Async[A]` that completes when the callback fires. From that point on it behaves like any other `Async[A]`, so the callback-based API disappears into ordinary code.
|
|
58
|
+
|
|
59
|
+
`Async.Running[A]` appears when you start work without waiting for it. Calling `.start` on an `Async[A]` begins the computation immediately and hands you a `Running` as a receipt. Keep it, and you can wait for the result later, run several pieces of work at once and collect them all, or stop the work early.
|
|
60
|
+
|
|
61
|
+
`Cancelable` is that last ability on its own — a single way to say "stop this." `Async.Running` provides it, and so can anything else you write that needs to be stoppable.
|
|
62
|
+
|
|
63
|
+
`Pollable[A]` is the one type most programs never touch. It is the extension point for teaching the module about a brand-new source of delayed results — a timer, a socket read, a platform-specific callback. Implementing one makes your source usable anywhere an `Async[A]` is expected. Reach for it only when you are wiring up something genuinely new; for ordinary callback bridging, `Async.promise` and a `Completer` are the right tools.
|
|
64
|
+
|
|
65
|
+
## Evaluation Model
|
|
66
|
+
|
|
67
|
+
`Async[A]` is **eager**, not a lazy `IO`. Building one performs its synchronous work straight away — constructing the value *runs* it, up to the first point where it genuinely has to wait:
|
|
68
|
+
|
|
69
|
+
```scala
|
|
70
|
+
import zio.blocks.async._
|
|
71
|
+
|
|
72
|
+
// "computing" is printed by this line, not by the one below it.
|
|
73
|
+
val fa: Async[Int] = Async.attempt { println("computing"); 42 }
|
|
74
|
+
val n: Int = fa.block // the value was already there; nothing more runs
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The same is true across the module. `Async.promise` runs its setup block when you call it, and `Async.succeed(x).map(f)` applies `f` immediately, because `x` is right there. Only a combinator applied to a value that is *already suspended* defers: then the function is kept and runs when a driver settles the value.
|
|
78
|
+
|
|
79
|
+
A direct-style block splits the same way, at its first genuine wait. Everything above that line runs as you build the value; everything below it is kept for later:
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
Async.async {
|
|
83
|
+
val cfg = loadConfig() // ┐
|
|
84
|
+
logger.info("starting") // ├ runs now, as this value is built
|
|
85
|
+
val base = compute(cfg) // ┘
|
|
86
|
+
val row = fetchRow(base).await // the first genuine wait
|
|
87
|
+
transform(row) // runs later, when a driver settles it
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
An `await` whose value is ready before it is asked does not count as a wait. It hands the value over on the spot and the block carries straight on, so the dividing line is not the first `await` you wrote — it is the first one that has nothing to give yet:
|
|
92
|
+
|
|
93
|
+
```scala
|
|
94
|
+
Async.async {
|
|
95
|
+
val a = Async.succeed(1).await // already a value: no wait, keep going
|
|
96
|
+
val b = compute(a).await // also ready: no wait, keep going
|
|
97
|
+
val c = fetchFromNetwork().await // nothing yet — the block stops here
|
|
98
|
+
a + b + c // deferred, along with everything below
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The practical consequence is that you cannot find the pause by counting `await`s. A block whose values are all ready pauses nowhere and finishes as you construct it; a block whose *first* `await` is a network call has run none of the lines below it by the time `Async.async { … }` hands you a value.
|
|
103
|
+
|
|
104
|
+
So where does waiting come from at all? From exactly one place: a [`Pollable`](#pollable) that was asked for its value and answered *not yet*. That is the only thing in the module that can make a computation pending. Everything else already has its answer — `succeed` has a value, `fail` has a cause, `attempt` has run, `map` merely applies a function.
|
|
105
|
+
|
|
106
|
+
Waiting then spreads in one direction only: to the combinators stacked on top of that pending value, which cannot produce a result until it does.
|
|
107
|
+
|
|
108
|
+
```scala
|
|
109
|
+
val c = new Completer[Int] // a Pollable; not completed yet
|
|
110
|
+
val a = c.peek.map(_ + 1) // above a pending value → deferred
|
|
111
|
+
val b = a.flatMap(n => Async.succeed(n * 2)) // still above it → deferred
|
|
112
|
+
|
|
113
|
+
val d = Async.succeed(1).map(_ + 1) // no pending value anywhere → already ran
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`a` and `b` wait only because they sit on top of a `Completer` nobody has completed. `d` shares none of that history, so it is simply the number `2` — the `+ 1` happened as the line was evaluated.
|
|
117
|
+
|
|
118
|
+
One consequence catches people out. If a function you pass to `map`, `flatMap` or `tap` throws, and the value it is applied to is *ready*, that function runs as you build the value — so the exception escapes at that line rather than becoming a failed `Async` you can `catchAll`. Only `Async.attempt` turns a throw into a `Failure`; the combinators do not:
|
|
119
|
+
|
|
120
|
+
```scala
|
|
121
|
+
Async.succeed(text).map(_.toInt) // throws here if text is not a number
|
|
122
|
+
Async.succeed(text).flatMap(s => Async.attempt(s.toInt)) // fails as an Async instead
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The "only one place" part is what distinguishes `Async` from a lazy effect type. `map`, `flatMap` and `zipWith` do not themselves defer anything, and building a chain does not create a plan to be executed later. If you cannot point at a value still waiting to be completed, nothing in your chain is pending: it has all already run.
|
|
126
|
+
|
|
127
|
+
Two things follow. Any pending computation can be traced to the thing it is waiting on — a `Completer` for a callback bridge, an [`Async.Running`](#asyncrunning) for started work, or your own `Pollable`. And the waiting is temporary and forward-only: when that value is completed, everything above it becomes ready, and nothing below it was ever held up, because that code had already run.
|
|
128
|
+
|
|
129
|
+
This is a deliberate trade. Eager evaluation is what keeps the ready path allocation-free — no effect tree, no per-step thunk, none of the wrapper objects most effect types build — and that is where the throughput comes from. It is also why `Async[A]` costs close to nothing on a mostly-synchronous path: when there is nothing to wait for, chaining operations onto a value just runs them.
|
|
130
|
+
|
|
131
|
+
The costs are real too. Building a value has effects, so `Async` is not referentially transparent: you cannot move a construction around, or replace a value with the expression that produced it, and be sure the program still means the same thing.
|
|
132
|
+
|
|
133
|
+
And letting go of a value does not stop it. With a lazy effect type, discarding an unused effect discards the work with it, because none of it had happened yet. Here the work is already under way:
|
|
134
|
+
|
|
135
|
+
```scala
|
|
136
|
+
val running = Async.start(uploadHugeFile())
|
|
137
|
+
|
|
138
|
+
// Reassigning or forgetting `running` does not stop the upload. The worker
|
|
139
|
+
// keeps going and the bytes keep moving; you have only thrown away your
|
|
140
|
+
// ability to watch it or stop it.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Stopping requires asking, through [`Cancelable#cancel`](#cancelable), and that reaches less far than you might expect.
|
|
144
|
+
|
|
145
|
+
The handle is your only route to that request. There is no registry of running computations to consult and no supervisor to ask, so a handle you have dropped cannot be recovered: that work becomes unstoppable for the rest of the process, and it finishes on its own schedule, holding its thread and its socket until it does. Nothing counts how many observers are left, so nothing notices when the last one goes away.
|
|
146
|
+
|
|
147
|
+
The rule that falls out is to decide at `start` time whether this work might ever need stopping. If it might, give the handle somewhere to live:
|
|
148
|
+
|
|
149
|
+
```scala
|
|
150
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
151
|
+
|
|
152
|
+
class Uploader {
|
|
153
|
+
private val current = new AtomicReference[Async.Running[Unit]]()
|
|
154
|
+
|
|
155
|
+
def begin(): Unit = {
|
|
156
|
+
val previous = current.getAndSet(Async.start(uploadHugeFile()))
|
|
157
|
+
if (previous ne null) previous.cancel() // stop watching the one displaced
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def abort(): Unit = {
|
|
161
|
+
val running = current.getAndSet(null)
|
|
162
|
+
if (running ne null) running.cancel() // possible only because it was kept
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Note what `begin` has to do on the way past: replacing a handle means cancelling the one it displaces, or that upload becomes unstoppable while still running. The `AtomicReference` is there because the field is reachable from more than one thread — `abort` may well be called while `begin` is assigning.
|
|
168
|
+
|
|
169
|
+
A [`Using`](#integration-points) block does the same job when the work is confined to a scope. And if the answer is that it never needs stopping, dropping the handle is fine — that is fire-and-forget, chosen deliberately rather than by accident.
|
|
170
|
+
|
|
171
|
+
In all of this `Async` sits beside `scala.concurrent.Future`, which is also eager, rather than beside cats-effect `IO` or ZIO.
|
|
172
|
+
|
|
173
|
+
### Depth Limits on Waiting Values
|
|
174
|
+
|
|
175
|
+
Call `map` on a value that is still waiting and you get back a wrapper: it remembers the original value and the function you gave it. Ask that wrapper for a result and it has to ask the original first, because until the original produces something there is nothing to apply the function to. That question is an ordinary method call, so the wrapper is left part-way through its own work — holding its place on the call stack — while the value underneath answers.
|
|
176
|
+
|
|
177
|
+
Stack several and each repeats the pattern. Take `c.peek.map(f).flatMap(g).map(h)`, where `c` is a `Completer` nobody has completed yet. Asking the outermost wrapper sets off a chain of questions inward, and every one of them waits where it stands:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
map h asks … still waiting
|
|
181
|
+
└─ flatMap g asks … still waiting
|
|
182
|
+
└─ map f asks … still waiting
|
|
183
|
+
└─ c answers "not yet" — and that travels back out through all three
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
None of them can finish until the innermost one answers, so all of them are held open at the same time. A chain written N deep costs N held-open calls, every single time it is asked.
|
|
187
|
+
|
|
188
|
+
Most effect systems avoid that with a trampoline: rather than calling its child directly, each step returns a small object meaning *"do this next"* to a loop that keeps running steps until one produces a value. One loop frame serves any depth. The price is paid on every step of every poll — an object allocated to describe the step, and a dispatch through the loop instead of a direct call.
|
|
189
|
+
|
|
190
|
+
There is no third option, so the choice was between the two:
|
|
191
|
+
|
|
192
|
+
| Approach | Depth | Cost per step |
|
|
193
|
+
|--------------|---------------------------|----------------------------------------------------|
|
|
194
|
+
| Trampoline | Unlimited | An object allocated, and a dispatch, on every poll |
|
|
195
|
+
| Direct calls | Limited by the call stack | Nothing |
|
|
196
|
+
|
|
197
|
+
`Async` takes the second. That is why nothing is allocated while polling, and it is also why a long enough chain over a waiting value ends in a `StackOverflowError` rather than a slowdown — the ceiling is the bill for the speed, not an oversight.
|
|
198
|
+
|
|
199
|
+
You are unlikely to meet it by hand. A handful of `map` and `flatMap` calls around a network request is nowhere near the limit. It becomes a real risk when the length of the chain is decided by *data* — one `flatMap` per row, per file, per retry — because then the depth is however large the input happens to be, and code that is comfortable in a test can overflow in production on a bigger batch.
|
|
200
|
+
|
|
201
|
+
Whether you are anywhere near the limit comes down to a single question: is the chain being built on top of a value that is still waiting?
|
|
202
|
+
|
|
203
|
+
- **On a value that is already there, chains are safe at any length.** Each step runs as you write it and gives back a plain value again, so nothing is left holding anything open. A loop like `var fa = …; while (…) fa = fa.flatMap(g)` stays flat no matter how many times it goes round — the suite takes it to a million.
|
|
204
|
+
- **On a value that is still waiting, a long chain is not safe.** Every `fa.flatMap(g)`, `fa.map(g)` or `fa.zipWith(…)` wraps the one before it, so asking for the result opens one call per wrapper before anything can answer, and the program runs out of stack somewhere around 50,000–100,000 on a default JVM. How you wrote the loop makes no difference — a recursive `def loop(n) = src.flatMap(_ => loop(n - 1))` and an iterative `fa = fa.flatMap(_ => src.flatMap(…))` build the same stack of wrappers.
|
|
205
|
+
- **`Async.collectAll` and a `while` loop inside `Async.async` stay safe even when the values are still waiting** — both are tested at 50,000 such steps. `collectAll` is a single step that walks the collection itself, stacking no wrappers at all, and the direct-style loop runs one turn each time it is asked rather than building the whole chain in advance.
|
|
206
|
+
|
|
207
|
+
So for long, wait-heavy work, reach for `collectAll` or an `Async.async` `while` loop instead of a hand-built tower of `flatMap`. `Future` never runs into this, but only because it sends every `flatMap` through an `ExecutionContext`; `Async` skips that hop to stay fast and takes the depth limit instead.
|
|
208
|
+
|
|
209
|
+
### Pending Suspensions Differ by Platform
|
|
210
|
+
|
|
211
|
+
Say a computation has hit a real wait. When the thing it waits for finally arrives, what makes the rest of the block run? Each platform answers with whatever its own runtime does fastest, so the answers differ:
|
|
212
|
+
|
|
213
|
+
- **JVM, and Scala.js on Scala 2 or Scala 3 before 3.8** — nothing runs it for you. The value sits there until something asks it for a result: `.block`, `.start`, or an interop converter. Build a value and never drive it, and the code after the wait does not run late — it never runs at all.
|
|
214
|
+
- **Scala.js on Scala 3.8 and later** — the block compiles into a real JavaScript async function, and JavaScript already has something whose job is resuming those: the event loop. It is always running, you did not start it, and you cannot opt out of it. So once the awaited value arrives, your code carries on by itself, whether or not anyone is watching.
|
|
215
|
+
|
|
216
|
+
```scala
|
|
217
|
+
val fa = Async.async { record(fetchRow().await) }
|
|
218
|
+
|
|
219
|
+
// JVM: fetchRow may well finish, but `record` has not run — nothing drove fa.
|
|
220
|
+
// Scala.js 3.8+: once fetchRow finishes, `record` runs anyway.
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
You are unlikely ever to see this. Values get built in order to be used, and the moment you `block` on one, `start` it, or hand it to a `Future`, both platforms behave identically and produce the same result. Noticing the difference takes a peculiar shape: build a block, let the thing it waits for arrive, then never drive it — a program that has already gone wrong, since it constructed work and then discarded it.
|
|
224
|
+
|
|
225
|
+
It is worth documenting because it changes *when side effects happen*. If the code after a wait prints, writes a file, or bumps a counter, Scala.js may do that without you driving anything, while the JVM will not. Cross-platform code that leans on "this has not run yet" is leaning on something true in only one of the two.
|
|
226
|
+
|
|
227
|
+
## How They Work Together
|
|
228
|
+
|
|
229
|
+
A computation moves through four phases: construct leaf values, compose them, drive the result, then observe or cancel, as shown in the following flow diagram:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
┌─ 1. CONSTRUCT — the synchronous part runs now ─────────────────┐
|
|
233
|
+
│ Async.succeed(a) a value you already have │
|
|
234
|
+
│ Async.fail(t) a failure you already have │
|
|
235
|
+
│ Async.attempt { … } runs the block, here, on this thread │
|
|
236
|
+
│ Async.promise { … } runs its setup block now │
|
|
237
|
+
│ new Pollable[A] { … } the one genuinely deferred leaf │
|
|
238
|
+
└────────────────────────────────────────────────────────────────┘
|
|
239
|
+
│
|
|
240
|
+
▼
|
|
241
|
+
Async[A] ── ready, failed, or waiting on something
|
|
242
|
+
│
|
|
243
|
+
▼
|
|
244
|
+
┌─ 2. COMPOSE — runs now if ready, defers if not ────────────────┐
|
|
245
|
+
│ map flatMap zipWith tap │
|
|
246
|
+
│ catchAll ensuring collectAll │
|
|
247
|
+
│ │
|
|
248
|
+
│ on a ready value the function runs immediately; │
|
|
249
|
+
│ on a pending one it is kept for the driver to run │
|
|
250
|
+
└────────────────────────────────────────────────────────────────┘
|
|
251
|
+
│
|
|
252
|
+
▼
|
|
253
|
+
┌─ 3. DRIVE — settle whatever is still pending ──────────────────┐
|
|
254
|
+
│ .block .start .toFuture │
|
|
255
|
+
│ wait right here run in the .toJsPromise │
|
|
256
|
+
│ background hand it to the │
|
|
257
|
+
│ │ │ platform │
|
|
258
|
+
│ ▼ ▼ │
|
|
259
|
+
│ the A, or Async.Running[A] │
|
|
260
|
+
│ the Throwable — your receipt │
|
|
261
|
+
└────────────────────────────────────────────────────────────────┘
|
|
262
|
+
│
|
|
263
|
+
▼
|
|
264
|
+
┌─ 4. OBSERVE or CANCEL — using the receipt ─────────────────────┐
|
|
265
|
+
│ .block .flatMap .zipWith wait for it, or compose more │
|
|
266
|
+
│ .cancel() stop it (via Cancelable) │
|
|
267
|
+
└────────────────────────────────────────────────────────────────┘
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Three of these types are closely related, and seeing why makes the module much smaller than it first looks. `Pollable[A]` answers one question — *is the result ready yet?* — and anything that can answer it is a `Pollable`:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
Pollable[A] — "a result that is not here yet"
|
|
274
|
+
│
|
|
275
|
+
├── Completer[A] you complete it yourself, once, from a callback
|
|
276
|
+
├── Async.Running[A] work that is already running; can also be stopped
|
|
277
|
+
└── Failure a computation that has already failed
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
That shared parent is what lets all three be used interchangeably. Wherever an `Async[A]` is expected, you can supply any of them, and every combinator — `map`, `flatMap`, `zipWith`, and the rest — works on the result without knowing or caring which one it is.
|
|
281
|
+
|
|
282
|
+
`Failure` is on the list because failing is just another way of being finished — a computation that has failed is not waiting for anything. [`Failure`](#failure) describes what that means for the combinators downstream of it.
|
|
283
|
+
|
|
284
|
+
You will use `Completer` and `Async.Running` constantly, and `Failure` mostly without naming it. Writing your own `Pollable` is the rare case, reserved for teaching the module about a new source of delayed results.
|
|
285
|
+
|
|
286
|
+
Two details the diagram leaves out. Composing over a ready value is allocation-free, while composing over a waiting one allocates a `Pollable` that the driver walks poll by poll. And the handle from phase 3 is itself an `Async`, which is what makes phase 4 ordinary composition rather than a separate API.
|
|
287
|
+
|
|
288
|
+
The following snippet grounds all four phases in an example: two off-thread `Completer` completions are composed with `zipWith` and driven by `block`:
|
|
289
|
+
|
|
290
|
+
```scala
|
|
291
|
+
import zio.blocks.async._
|
|
292
|
+
|
|
293
|
+
def delayed[A](value: A, ms: Long): Async[A] = {
|
|
294
|
+
val c = new Completer[A]
|
|
295
|
+
val t = new Thread(new Runnable {
|
|
296
|
+
def run(): Unit = { Thread.sleep(ms); c.succeed(value) }
|
|
297
|
+
})
|
|
298
|
+
t.setDaemon(true)
|
|
299
|
+
t.start()
|
|
300
|
+
c.peek // the Completer is itself an Async[A]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Phase 1 and 2: construct two off-thread leaves and compose with zipWith
|
|
304
|
+
val r: Async[Int] = delayed(3, 30).zipWith(delayed(4, 5))(_ + _)
|
|
305
|
+
|
|
306
|
+
// Phase 3: drive — parks the calling thread until both off-thread wakers fire
|
|
307
|
+
val result: Int = r.block // => 7
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
A second example shows what `Async.collectAll` guarantees: the results come back in the order you listed the computations, not the order they happened to finish. To make that visible, the delays below are deliberately reversed — the first element takes the longest, the last finishes almost immediately:
|
|
311
|
+
|
|
312
|
+
```scala
|
|
313
|
+
import zio.blocks.async._
|
|
314
|
+
|
|
315
|
+
// Completes with `value` after `ms`, on another thread.
|
|
316
|
+
def delayed[A](value: A, ms: Long): Async[A] = {
|
|
317
|
+
val c = new Completer[A]
|
|
318
|
+
val t = new Thread(new Runnable {
|
|
319
|
+
def run(): Unit = { Thread.sleep(ms); c.succeed(value) }
|
|
320
|
+
})
|
|
321
|
+
t.setDaemon(true)
|
|
322
|
+
t.start()
|
|
323
|
+
c.peek
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
val ordered: Async[List[Int]] = Async.collectAll(List[Async[Int]](
|
|
327
|
+
delayed(1, 90), // finishes third
|
|
328
|
+
delayed(2, 45), // finishes second
|
|
329
|
+
delayed(3, 5) // finishes first
|
|
330
|
+
))
|
|
331
|
+
|
|
332
|
+
// Completion order is 3, 2, 1 — the list is still 1, 2, 3.
|
|
333
|
+
val results: List[Int] = ordered.block // => List(1, 2, 3)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Without that guarantee you would have to tag each computation and re-sort the results yourself. Because `collectAll` keeps the positions, you can zip the output against the input list — or pattern-match on it positionally — and trust that element *n* belongs to computation *n*.
|
|
337
|
+
|
|
338
|
+
## Operations
|
|
339
|
+
|
|
340
|
+
Everything you can do with an `Async[A]`: make one, transform it, combine it with another, recover from a failure, and eventually run it.
|
|
341
|
+
|
|
342
|
+
One property is worth carrying into the signatures below. `Async[A]` is declared `Async[+A]`, which makes it *covariant*: whenever `B` is a subtype of `A`, an `Async[B]` counts as an `Async[A]`. That matters because `Async.fail` and `Async.never` have no value to offer, so their type is `Async[Nothing]` — and `Nothing` is a subtype of every type in Scala. An `Async[Nothing]` is therefore an `Async[String]`, an `Async[User]`, an `Async` of anything at all:
|
|
343
|
+
|
|
344
|
+
```scala
|
|
345
|
+
import zio.blocks.async._
|
|
346
|
+
|
|
347
|
+
case class User(id: Int, name: String)
|
|
348
|
+
|
|
349
|
+
def fetchUser(id: Int): Async[User] =
|
|
350
|
+
if (id < 0) Async.fail(new IllegalArgumentException("bad id")) // Async[Nothing]
|
|
351
|
+
else Async.succeed(User(id, "sam")) // Async[User]
|
|
352
|
+
|
|
353
|
+
val forever: Async[User] = Async.never // Async[Nothing] fits too
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Without covariance neither of those would compile against the declared `Async[User]`, and you would be writing `Async.fail[User](…)` or a cast at every failure. This is a property you notice only through the errors it saves you from.
|
|
357
|
+
|
|
358
|
+
### Creating Values
|
|
359
|
+
|
|
360
|
+
The companion object provides factories for constructing leaf `Async[A]` values:
|
|
361
|
+
|
|
362
|
+
```scala
|
|
363
|
+
object Async {
|
|
364
|
+
def succeed[A](a: A): Async[A]
|
|
365
|
+
def fail(cause: Throwable): Async[Nothing]
|
|
366
|
+
def attempt[A](body: => A): Async[A]
|
|
367
|
+
def promise[A](body: Completer[A] => Unit): Async[A] // shape differs on Scala 3; see Completer
|
|
368
|
+
def start[A](body: => A): Async.Running[A]
|
|
369
|
+
val never: Async[Nothing]
|
|
370
|
+
def collectAll[A](as: IterableOnce[Async[A]]): Async[List[A]]
|
|
371
|
+
def async[A](body: A): Async[A] // rewritten in place: a macro on Scala 2,
|
|
372
|
+
// a transparent inline def on Scala 3.
|
|
373
|
+
// See the Direct Style pattern
|
|
374
|
+
|
|
375
|
+
// JVM only
|
|
376
|
+
def fromFuture[A](future: scala.concurrent.Future[A]): Async[A]
|
|
377
|
+
def fromCompletionStage[A](cs: java.util.concurrent.CompletionStage[A]): Async[A]
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`Async.succeed` lifts a pure, immediately-available value into an `Async[A]`:
|
|
382
|
+
|
|
383
|
+
```scala
|
|
384
|
+
import zio.blocks.async._
|
|
385
|
+
|
|
386
|
+
val ready: Async[Int] = Async.succeed(42)
|
|
387
|
+
val result: Int = ready.block // => 42
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`Async.fail` creates a terminal failure; [`Failure`](#failure) covers how it short-circuits the rest of a chain:
|
|
391
|
+
|
|
392
|
+
```scala
|
|
393
|
+
import zio.blocks.async._
|
|
394
|
+
|
|
395
|
+
val boom: Async[Int] = Async.fail(new RuntimeException("boom"))
|
|
396
|
+
val result: Int = boom.catchAll(_ => Async.succeed(-1)).block // => -1
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`Async.attempt` captures a by-name expression and converts any thrown `Throwable` into a failure:
|
|
400
|
+
|
|
401
|
+
```scala
|
|
402
|
+
import zio.blocks.async._
|
|
403
|
+
|
|
404
|
+
val parsed: Async[Int] = Async.attempt("42".toInt)
|
|
405
|
+
val bad: Async[Int] = Async.attempt("nope".toInt) // => Async.fail(NumberFormatException)
|
|
406
|
+
val result: Int = bad.catchAll(_ => Async.succeed(0)).block // => 0
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`Async.never` is a permanently-suspended `Async[Nothing]` — a placeholder where an `Async[A]` is required but no value should ever arrive, and the usual way to test cancellation, as shown under [`Async.Running`](#asyncrunning).
|
|
410
|
+
|
|
411
|
+
`Async.start(body: => A)` runs a body on a background worker and hands back an `Async.Running[A]`; [Concurrent Fan-Out](#concurrent-fan-out-via-running) covers when to reach for it and the trap to avoid:
|
|
412
|
+
|
|
413
|
+
```scala
|
|
414
|
+
import zio.blocks.async._
|
|
415
|
+
|
|
416
|
+
val running: Async.Running[Int] = Async.start { 42 }
|
|
417
|
+
val result: Int = running.block // => 42
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Transformation
|
|
421
|
+
|
|
422
|
+
Pure transformations apply a function to the success value and return a new `Async`:
|
|
423
|
+
|
|
424
|
+
```scala
|
|
425
|
+
implicit class AsyncOps[A](fa: Async[A]) {
|
|
426
|
+
def map[B](f: A => B): Async[B]
|
|
427
|
+
def flatMap[B](f: A => Async[B]): Async[B]
|
|
428
|
+
def as[B](b: B): Async[B]
|
|
429
|
+
def unit: Async[Unit]
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// flatten is a separate extension, on a nested Async:
|
|
433
|
+
implicit class AsyncNestedOps[A](ffa: Async[Async[A]]) {
|
|
434
|
+
def flatten: Async[A] // collapses one nesting level
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`map` applies a pure function and `flatMap` sequences a dependent second computation:
|
|
439
|
+
|
|
440
|
+
```scala
|
|
441
|
+
import zio.blocks.async._
|
|
442
|
+
|
|
443
|
+
val result: Async[String] =
|
|
444
|
+
Async.succeed(21)
|
|
445
|
+
.map(_ * 2)
|
|
446
|
+
.flatMap(n => Async.succeed(s"value: $n"))
|
|
447
|
+
val out: String = result.block // => "value: 42"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Composition
|
|
451
|
+
|
|
452
|
+
Compositional operators combine independent or dependent `Async` values:
|
|
453
|
+
|
|
454
|
+
```scala
|
|
455
|
+
implicit class AsyncOps[A](fa: Async[A]) {
|
|
456
|
+
def zipWith[B, C](that: Async[B])(f: (A, B) => C): Async[C]
|
|
457
|
+
def zip[B](that: Async[B])(implicit t: Tuples[A, B]): Async[t.Out] // flattens; see below
|
|
458
|
+
def tap(f: A => Async[Any]): Async[A]
|
|
459
|
+
def ensuring(finalizer: Async[Any]): Async[A]
|
|
460
|
+
def *>[B](that: Async[B]): Async[B]
|
|
461
|
+
def <*[B](that: Async[B]): Async[A]
|
|
462
|
+
def orElse[B](that: => Async[B]): Async[_] // result type merges A and B via Concat typeclass
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`zipWith` waits for both sides and combines their results; `tap` runs a side-effecting action while passing the original value through. `zip` pairs the two results, and chains of it stay flat rather than nesting — `a zip b zip c` yields `Async[(A, B, C)]`, not `Async[((A, B), C)]` — because it combines through the `Tuples` instances described in the [combinators reference](combinators.md):
|
|
467
|
+
|
|
468
|
+
```scala
|
|
469
|
+
import zio.blocks.async._
|
|
470
|
+
|
|
471
|
+
val combined: Async[Int] =
|
|
472
|
+
Async.succeed(3).zipWith(Async.succeed(4))(_ + _)
|
|
473
|
+
val tapped: Async[Int] =
|
|
474
|
+
combined.tap(v => Async.attempt(println(s"sum is $v")))
|
|
475
|
+
val result: Int = tapped.block // => 7; "sum is 7" was printed by the line above
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`*>` and `<*` sequence two effects and discard the left or right result respectively:
|
|
479
|
+
|
|
480
|
+
```scala
|
|
481
|
+
import zio.blocks.async._
|
|
482
|
+
|
|
483
|
+
val logged: Async[Int] =
|
|
484
|
+
Async.attempt(println("starting")).*>(Async.succeed(42))
|
|
485
|
+
val result: Int = logged.block // => 42
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Error Handling
|
|
489
|
+
|
|
490
|
+
`Async` represents failure as a `Throwable` and provides dedicated recovery operators:
|
|
491
|
+
|
|
492
|
+
```scala
|
|
493
|
+
implicit class AsyncOps[A](fa: Async[A]) {
|
|
494
|
+
def catchAll[A1 >: A](f: Throwable => Async[A1]): Async[A1]
|
|
495
|
+
def mapError(f: Throwable => Throwable): Async[A]
|
|
496
|
+
def foldCause[B](onFailure: Throwable => B)(onSuccess: A => B): Async[B]
|
|
497
|
+
def either: Async[Either[Throwable, A]]
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`catchAll` recovers from any failure by supplying a replacement `Async[A]`; `either` converts the outcome to an `Either` so the failure surface is visible in the return type:
|
|
502
|
+
|
|
503
|
+
```scala
|
|
504
|
+
import zio.blocks.async._
|
|
505
|
+
|
|
506
|
+
val safe: Async[Either[Throwable, Int]] =
|
|
507
|
+
Async.fail(new Exception("oops")).either
|
|
508
|
+
val result: Either[Throwable, Int] = safe.block // => Left(Exception("oops"))
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`foldCause` handles both the success and failure branches in a single call without allocating a recovery `Async`:
|
|
512
|
+
|
|
513
|
+
```scala
|
|
514
|
+
import zio.blocks.async._
|
|
515
|
+
|
|
516
|
+
val message: Async[String] =
|
|
517
|
+
Async.attempt("42".toInt).foldCause(
|
|
518
|
+
(err: Throwable) => s"failed: ${err.getMessage}"
|
|
519
|
+
)(
|
|
520
|
+
(n: Int) => s"parsed: $n"
|
|
521
|
+
)
|
|
522
|
+
val result: String = message.block // => "parsed: 42"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Driving
|
|
526
|
+
|
|
527
|
+
Driving settles whatever part of an `Async[A]` is still waiting, and delivers the result through one of three mechanisms:
|
|
528
|
+
|
|
529
|
+
```scala
|
|
530
|
+
implicit class AsyncOps[A](fa: Async[A]) {
|
|
531
|
+
def block: A // parks calling thread; re-throws on failure
|
|
532
|
+
def await: A // inside Async.async { } only; the rewrite
|
|
533
|
+
// removes it. Using it elsewhere does not compile
|
|
534
|
+
def start: Async.Running[A]
|
|
535
|
+
def toFuture(implicit ec: scala.concurrent.ExecutionContext): scala.concurrent.Future[A]
|
|
536
|
+
def toCompletableFuture(implicit ec: scala.concurrent.ExecutionContext)
|
|
537
|
+
: java.util.concurrent.CompletableFuture[A] // JVM only
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`block` parks the calling thread until the computation settles, then returns the value or re-throws the underlying `Throwable`:
|
|
542
|
+
|
|
543
|
+
```scala
|
|
544
|
+
import zio.blocks.async._
|
|
545
|
+
|
|
546
|
+
val result: Int = Async.succeed(42).map(_ + 1).block // => 43
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Two limits apply. On the JVM, never call `block` from inside a `poll` — you would be putting to sleep the very thread that has to deliver your result, which deadlocks the loop. Keep it at the edge of your program: `main`, a test, the boundary with synchronous code.
|
|
550
|
+
|
|
551
|
+
On Scala.js there is no thread to park at all. A ready value returns as usual, but a *pending* one gets a single chance to complete synchronously, and if it has not, `block` throws `IllegalStateException`. Scala.js code should reach for `toFuture` or `toJsPromise` and let the event loop deliver the result, or stay inside `Async.async { … }` and use `await`.
|
|
552
|
+
|
|
553
|
+
`start` hands whatever is still waiting to a *driver* and returns an `Async.Running[A]` immediately, without blocking. The driver is the thing that keeps asking a pending value for its result. On the JVM it is a chain of serialized tasks on `ForkJoinPool.commonPool()`; on Scala.js it is the microtask queue. Neither holds a thread for the duration: a task exits as soon as its current pollable is still pending, and the waker that `poll` registered submits the next task when there is something new to see.
|
|
554
|
+
|
|
555
|
+
`Async.start(body)` is the other entry point and it is not the same mechanism. It takes a block of ordinary synchronous code rather than an `Async`, so it needs somewhere to run that block: on the JVM a dedicated daemon thread named `zio-blocks-async-eval`, and on Scala.js the next microtask.
|
|
556
|
+
|
|
557
|
+
```scala
|
|
558
|
+
import zio.blocks.async._
|
|
559
|
+
|
|
560
|
+
def compute(): Int = 42
|
|
561
|
+
|
|
562
|
+
val running: Async.Running[Int] = Async.start { compute() }
|
|
563
|
+
val result: Int = running.block // wait here for the result
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
`toFuture` hands off to a `scala.concurrent.Future`, bridging into any code that already expects the standard-library async type:
|
|
567
|
+
|
|
568
|
+
```scala
|
|
569
|
+
import zio.blocks.async._
|
|
570
|
+
import scala.concurrent.ExecutionContext.Implicits.global
|
|
571
|
+
|
|
572
|
+
val future: scala.concurrent.Future[Int] =
|
|
573
|
+
Async.succeed(99).toFuture
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Conditional Execution
|
|
577
|
+
|
|
578
|
+
`when` and `unless` are package-level functions (brought in by `import zio.blocks.async._`) that conditionally evaluate an `Async[Any]` based on a `Boolean` condition. The unevaluated branch is passed by name so no `Async` is constructed when the condition is false:
|
|
579
|
+
|
|
580
|
+
```scala
|
|
581
|
+
import zio.blocks.async._
|
|
582
|
+
|
|
583
|
+
val flag = true
|
|
584
|
+
val logged: Async[Unit] = when(flag)(Async.attempt(println("running")))
|
|
585
|
+
val skipped: Async[Unit] = unless(flag)(Async.attempt(println("skipped")))
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Common Patterns
|
|
589
|
+
|
|
590
|
+
The five patterns below address the most frequent tasks: bridging callbacks, writing sequential-looking code, guaranteeing cleanup, sharing in-flight computations, and collecting parallel results.
|
|
591
|
+
|
|
592
|
+
### Callback Bridge
|
|
593
|
+
|
|
594
|
+
Callback-based APIs all share one shape. Instead of returning the result, they return `Unit` immediately and call one of two functions you hand them once the work is done:
|
|
595
|
+
|
|
596
|
+
```scala
|
|
597
|
+
// The API you are stuck with. A real one calls back later, from
|
|
598
|
+
// another thread; the shape is what matters here.
|
|
599
|
+
def legacyApi(onSuccess: String => Unit, onError: Throwable => Unit): Unit =
|
|
600
|
+
onSuccess("done")
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
That signature is the problem. Because `legacyApi` returns `Unit`, there is no value to return from your own function, nothing to pass to another function, and no way to say "do this, then that" — the result only ever appears inside a callback body, so the rest of your program has to be written in there too.
|
|
604
|
+
|
|
605
|
+
`Async.promise` inverts it. It gives you a `Completer[A]`, which is a value that can be completed later, and hands you back an `Async[A]` representing the eventual result. You pass the completer's two methods where `legacyApi` expects its two functions. The body is written slightly differently on each Scala version — `c =>` on Scala 2, `c ?=>` on Scala 3, for the reason explained under [`Completer`](#completer):
|
|
606
|
+
|
|
607
|
+
<Tabs groupId="scala-version" defaultValue="scala2">
|
|
608
|
+
<TabItem value="scala2" label="Scala 2">
|
|
609
|
+
|
|
610
|
+
```scala
|
|
611
|
+
import zio.blocks.async._
|
|
612
|
+
|
|
613
|
+
def legacyApi(onSuccess: String => Unit, onError: Throwable => Unit): Unit =
|
|
614
|
+
onSuccess("done")
|
|
615
|
+
|
|
616
|
+
val async: Async[String] = Async.promise[String] { c =>
|
|
617
|
+
legacyApi(
|
|
618
|
+
result => c.succeed(result),
|
|
619
|
+
err => c.fail(err)
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
val result: String = async.block // waits for the callback; this stub already fired
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
</TabItem>
|
|
626
|
+
<TabItem value="scala3" label="Scala 3">
|
|
627
|
+
|
|
628
|
+
```scala
|
|
629
|
+
import zio.blocks.async._
|
|
630
|
+
|
|
631
|
+
def legacyApi(onSuccess: String => Unit, onError: Throwable => Unit): Unit =
|
|
632
|
+
onSuccess("done")
|
|
633
|
+
|
|
634
|
+
val async: Async[String] = Async.promise[String] { c ?=>
|
|
635
|
+
legacyApi(
|
|
636
|
+
result => c.succeed(result),
|
|
637
|
+
err => c.fail(err)
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
val result: String = async.block // waits for the callback; this stub already fired
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
</TabItem>
|
|
644
|
+
</Tabs>
|
|
645
|
+
|
|
646
|
+
The two lines inside `legacyApi` are the whole bridge: whichever callback fires, it completes `c`, and completing `c` completes the `Async[String]`. Note what has been gained — `async` is an ordinary value. You can return it, store it, or chain `map` and `flatMap` onto it, and the callback API is no longer visible to anything downstream.
|
|
647
|
+
|
|
648
|
+
The bridge is also safe against a callback that fires more than once — see [`Completer`](#completer).
|
|
649
|
+
|
|
650
|
+
### Direct Style
|
|
651
|
+
|
|
652
|
+
Inside `Async.async { ... }`, use `await` to extract values from `Async` computations in sequential-looking code without explicit `flatMap` chains:
|
|
653
|
+
|
|
654
|
+
```scala
|
|
655
|
+
import zio.blocks.async._
|
|
656
|
+
|
|
657
|
+
case class Order(id: Int, userId: Int)
|
|
658
|
+
case class User(id: Int, name: String, tier: String)
|
|
659
|
+
case class Shipment(orderId: Int, carrier: String)
|
|
660
|
+
|
|
661
|
+
def fetchOrder(id: Int): Async[Order] = Async.succeed(Order(id, 1))
|
|
662
|
+
def fetchUser(id: Int): Async[User] = Async.succeed(User(id, "sam", "gold"))
|
|
663
|
+
def fulfill(orderId: Int): Async[Shipment] = Async.succeed(Shipment(orderId, "express"))
|
|
664
|
+
|
|
665
|
+
def fulfillOrGuest(orderId: Int): Async[String] = Async.async {
|
|
666
|
+
val order = fetchOrder(orderId).catchAll(_ => fetchOrder(9001)).await
|
|
667
|
+
val user = fetchUser(order.userId)
|
|
668
|
+
.catchAll(_ => Async.succeed(User(0, "guest", "bronze"))).await
|
|
669
|
+
val shipment = fulfill(order.id).await
|
|
670
|
+
s"shipped ${shipment.orderId} for ${user.name} via ${shipment.carrier}"
|
|
671
|
+
}
|
|
672
|
+
val result: String = fulfillOrGuest(9001).block
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Awaits run in source order; a failed `Async[A]` under `await` propagates as `Async.fail`.
|
|
676
|
+
|
|
677
|
+
Nothing new happens at runtime here. `Async.async` rewrites its body at compile time: the block is split at each `await` and reassembled into the `flatMap` chain you would have written by hand. The example above compiles to roughly this:
|
|
678
|
+
|
|
679
|
+
```scala
|
|
680
|
+
fetchOrder(orderId).catchAll(_ => fetchOrder(9001)).flatMap { order =>
|
|
681
|
+
fetchUser(order.userId)
|
|
682
|
+
.catchAll(_ => Async.succeed(User(0, "guest", "bronze")))
|
|
683
|
+
.flatMap { user =>
|
|
684
|
+
fulfill(order.id).map { shipment =>
|
|
685
|
+
s"shipped ${shipment.orderId} for ${user.name} via ${shipment.carrier}"
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
So `await` is not a method that blocks or waits. It is a marker the rewrite removes, and everything after it becomes the continuation that runs once the value arrives. Direct style therefore costs nothing over writing the chain yourself — by the time the code runs, it *is* that chain. Choose whichever reads better.
|
|
692
|
+
|
|
693
|
+
One consequence is worth remembering: `await` only means something inside an `Async.async` block. Elsewhere there is no rewrite to remove it, and both Scala versions reject it at compile time — Scala 2 through `@compileTimeOnly`, Scala 3 by aborting the macro expansion. You will not ship this mistake.
|
|
694
|
+
|
|
695
|
+
Within the block, `await` is not restricted to statement position. It also works inside the closures you pass to the strict collections — `List`, `Option`, `Vector`, `Set`, `Map`, `Array`, `Queue`, `ArraySeq` — for `map`, `foreach`, `flatMap`, `filter`, `filterNot`, `collect`, `find`, `exists`, `forall`, `foldLeft`, `foldRight`, `reduce` and `reduceLeft`, and in for-comprehensions over them:
|
|
696
|
+
|
|
697
|
+
```scala
|
|
698
|
+
import zio.blocks.async._
|
|
699
|
+
|
|
700
|
+
def fetchName(id: Int): Async[String] = Async.succeed(s"user-$id")
|
|
701
|
+
|
|
702
|
+
val names: Async[List[String]] = Async.async {
|
|
703
|
+
List(1, 2, 3).map(id => fetchName(id).await)
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Read that list literally; the near neighbours are not all included. `reduceRight` and `reduceOption` are not, and neither is the two-argument `fold`. `takeWhile` and `dropWhile` are, but only over an ordered receiver — `List`, `Vector`, `Queue`, `ArraySeq` or `Array` — because the prefix they compute is meaningless on a `Set` or a `Map`. And two cases surprise people: `Map.filter` with an `await` inside works on Scala 2 only, and a `Map.collect` whose closure yields a pair is unsupported everywhere. Lazy collections are outside the set entirely — force them to a strict collection first.
|
|
708
|
+
|
|
709
|
+
The semantics are uniform across all of them. Each is lazy and sequential: the closure for element *n+1* runs only after element *n*'s `await` has completed, so a `List(a, b, c).map(fetch(_).await)` performs three fetches one after another rather than at once — use `Async.collectAll` when you want them overlapped. A failed `await` short-circuits the remainder, and the result keeps the receiver's collection type.
|
|
710
|
+
|
|
711
|
+
Where a collection method would throw on its own, it still does: `reduce` over an empty receiver fails with `UnsupportedOperationException`, which arrives as an ordinary `Async` failure you can `catchAll`.
|
|
712
|
+
|
|
713
|
+
The rewrite is performed by `dotty-cps-async` on Scala 3 and by a built-in `scala-reflect` macro on Scala 2.13. Both Scala versions support direct style, and neither asks you to add anything to your build.
|
|
714
|
+
|
|
715
|
+
Scala.js 3.8 and later takes a hybrid route, decided per call site. An `await` in direct position compiles to JavaScript's own `async`/`await`, which is the fastest path available; an `await` sitting under a lambda, a by-name argument, or a nested method falls back to the `dotty-cps-async` transform, because the native primitive is not legal in those positions. Nothing about this is yours to configure — the wider `Async.async` surface works either way.
|
|
716
|
+
|
|
717
|
+
### Bracket and Ensuring
|
|
718
|
+
|
|
719
|
+
Some work has to happen no matter what: closing a file, releasing a connection, deleting a temporary directory. In ordinary code you write that in a `finally` block. `ensuring` is the same idea for `Async`: you attach a cleanup value, and its *outcome* is applied once the computation settles, whether that produced a value or a failure. Note "value", not "thunk" — `ensuring` takes an `Async`, and under the [evaluation model](#evaluation-model) building one runs its synchronous part immediately:
|
|
720
|
+
|
|
721
|
+
```scala
|
|
722
|
+
import zio.blocks.async._
|
|
723
|
+
|
|
724
|
+
val result: Async[String] =
|
|
725
|
+
Async.attempt(openResource()).flatMap { res =>
|
|
726
|
+
Async.attempt(res.read()).ensuring(Async.attempt(res.close()))
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
That example is safe only because `Async.attempt(res.read())` is already finished by the time `ensuring` is reached. Written over something that genuinely waits, the same shape closes the resource while the read is still in flight:
|
|
731
|
+
|
|
732
|
+
```scala
|
|
733
|
+
// WRONG: res.close() runs here, as the argument is built —
|
|
734
|
+
// not after the read completes.
|
|
735
|
+
readAsync(res).ensuring(Async.attempt(res.close()))
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
To defer the effect itself, put it somewhere that is only run when driven — a `flatMap` or `tap` closure, or a finalizer that is genuinely suspended:
|
|
739
|
+
|
|
740
|
+
```scala
|
|
741
|
+
readAsync(res).flatMap(v => Async.attempt(res.close()).as(v))
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
What `ensuring` does guarantee is the rule worth remembering: **the cleanup never changes the answer.** It cannot turn a failure into a success, and it cannot turn a success into a failure.
|
|
745
|
+
|
|
746
|
+
That last part raises an obvious question — what if the cleanup itself fails? Closing a file can throw too. The answer depends on how the main computation ended, so it is worth seeing both cases:
|
|
747
|
+
|
|
748
|
+
```scala
|
|
749
|
+
import zio.blocks.async._
|
|
750
|
+
|
|
751
|
+
// Both fail: reading the resource, and then closing it.
|
|
752
|
+
val bothFail: Async[String] =
|
|
753
|
+
Async
|
|
754
|
+
.attempt[String](throw new RuntimeException("read failed"))
|
|
755
|
+
.ensuring(Async.attempt(throw new IllegalStateException("close failed")))
|
|
756
|
+
|
|
757
|
+
bothFail.either.block match {
|
|
758
|
+
case Left(e) =>
|
|
759
|
+
println(e.getMessage) // read failed <- the original failure
|
|
760
|
+
println(e.getSuppressed()(0).getMessage) // close failed <- attached to it
|
|
761
|
+
case Right(_) => ()
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Only the cleanup fails.
|
|
765
|
+
val readOk: Async[String] =
|
|
766
|
+
Async
|
|
767
|
+
.succeed("contents")
|
|
768
|
+
.ensuring(Async.attempt(throw new IllegalStateException("close failed")))
|
|
769
|
+
|
|
770
|
+
val value: String = readOk.block // "contents" — the close failure is gone
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
In the first case the read had already failed, so you get the read's exception — the one that explains what actually went wrong. The close error is not thrown away, though: it is carried along inside that exception, in a list the JVM keeps for exactly this purpose. `getSuppressed` returns that list. Your logging framework almost certainly prints it, usually under a line beginning `Suppressed:`, so both problems end up on the page.
|
|
774
|
+
|
|
775
|
+
The second case is the one to watch. The read succeeded, so there is no exception to carry the close error, and it is simply dropped — `readOk.block` returns `"contents"` and you never hear that closing failed. If a cleanup error matters to you on the success path, catch it inside the cleanup step itself and log it there:
|
|
776
|
+
|
|
777
|
+
```scala
|
|
778
|
+
Async
|
|
779
|
+
.attempt(res.read())
|
|
780
|
+
.ensuring(Async.attempt(res.close()).catchAll { t =>
|
|
781
|
+
Async.succeed(logger.warn("close failed", t))
|
|
782
|
+
})
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Concurrent Fan-Out via Running
|
|
786
|
+
|
|
787
|
+
Suppose one expensive computation feeds several parts of your program — a report that is both summarised and emailed, say. The obvious approach is to build the work once and use that value in both places — but driving it is what produces the result, so each place that drives it does the work again. You want the work to happen once, in the background, with everyone reading the same outcome.
|
|
788
|
+
|
|
789
|
+
`Async.start` does that. It hands the body to a background worker and returns immediately with an `Async.Running[A]` — a handle to work already in flight:
|
|
790
|
+
|
|
791
|
+
```scala
|
|
792
|
+
import zio.blocks.async._
|
|
793
|
+
|
|
794
|
+
def heavyComputation(): Int = { Thread.sleep(50); 42 }
|
|
795
|
+
|
|
796
|
+
// Returns straight away; the work proceeds on a background worker.
|
|
797
|
+
val running: Async.Running[Int] = Async.start(heavyComputation())
|
|
798
|
+
|
|
799
|
+
// The handle is itself an Async[Int], so it composes like anything else.
|
|
800
|
+
val doubled: Async[Int] = running.map(_ * 2)
|
|
801
|
+
val labelled: Async[String] = running.map(n => s"got $n")
|
|
802
|
+
|
|
803
|
+
val a: Int = doubled.block // 84
|
|
804
|
+
val b: String = labelled.block // "got 42" — heavyComputation ran once, not twice
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
Both consumers see the same settled outcome, because they share one running computation rather than one recipe. `Async.Running[A]` is a subtype of `Async[A]`, so it works with `map`, `flatMap`, and `zipWith` without conversion, and `running.cancel()` stops the driver, which is [less than it sounds](#cancelable).
|
|
808
|
+
|
|
809
|
+
Sharing the handle is not merely tidier than the alternative — the alternative is unsafe. Driving the same raw `Async` from two places at once is **undefined behaviour**: two `fa.start` calls on the same `fa`, or an `fa.start` racing an `fa.block`. On the JVM that polls the same combinator concurrently, so a function you passed to `map`, `flatMap`, or `tap` may run more than once, and a `collectAll` may read its drain buffer mid-update. Start once, share the `Running`.
|
|
810
|
+
|
|
811
|
+
Sequential re-use is fine — polling or composing a value again after an earlier drive has settled is well defined. Only *concurrent* driving of the same raw value is not, and single-threaded Scala.js cannot hit it at all.
|
|
812
|
+
|
|
813
|
+
Take care to start the work the right way round, because the wrong version looks almost identical:
|
|
814
|
+
|
|
815
|
+
```scala
|
|
816
|
+
Async.start(heavyComputation()) // ✅ the worker evaluates it
|
|
817
|
+
Async.attempt(heavyComputation()).start // ❌ already evaluated, on this thread
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
Both lines compile, and both hand you an `Async.Running[Int]`. Only the first one runs anything in the background.
|
|
821
|
+
|
|
822
|
+
The difference is *when the argument gets evaluated*. Scala normally evaluates an argument before passing it, so in `Async.attempt(heavyComputation())` the computation runs first — on your own thread, right at that line — and `attempt` merely wraps the answer it produced. Tacking `.start` on afterwards cannot un-run it; there is nothing left to move to a worker.
|
|
823
|
+
|
|
824
|
+
`Async.start` is declared differently. Its parameter is `body: => A`, and that `=>` means "don't evaluate this yet — hand me the code and I will run it when I am ready." It passes the code to a worker thread, which is why the call returns immediately.
|
|
825
|
+
|
|
826
|
+
The clock shows it plainly:
|
|
827
|
+
|
|
828
|
+
```scala
|
|
829
|
+
Async.start(heavyComputation()) // returns in about 0 ms
|
|
830
|
+
Async.attempt(heavyComputation()).start // returns in about 50 ms — you waited for it
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
So: use `Async.start` for work you want moved off the calling thread. Use `fa.start` when `fa` is an `Async` you have already built and composed and now want driven.
|
|
834
|
+
|
|
835
|
+
### Batch Collection
|
|
836
|
+
|
|
837
|
+
Use `Async.collectAll` to sequence a list of `Async` values and gather results into a `List[A]` in input order:
|
|
838
|
+
|
|
839
|
+
```scala
|
|
840
|
+
import zio.blocks.async._
|
|
841
|
+
|
|
842
|
+
val batch: Async[List[Int]] = Async.collectAll(List(
|
|
843
|
+
Async.attempt(compute(1)),
|
|
844
|
+
Async.attempt(compute(2)),
|
|
845
|
+
Async.attempt(compute(3))
|
|
846
|
+
))
|
|
847
|
+
val results: List[Int] = batch.block // => List(r1, r2, r3) in input order
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
The first failure short-circuits and remaining elements are not driven. Already-ready lists take an optimized path that skips allocating a sequencing continuation.
|
|
851
|
+
|
|
852
|
+
## Integration Points
|
|
853
|
+
|
|
854
|
+
You are unlikely to be starting from scratch. Your codebase probably already returns `Future`s, calls a Java library that returns a `CompletionStage`, or talks to a JavaScript API that returns a `Promise`. `Async` is built to sit next to those, so you can adopt it in one part of a program without rewriting everything around it.
|
|
855
|
+
|
|
856
|
+
Conversions go in both directions, and none of them blocks a thread:
|
|
857
|
+
|
|
858
|
+
| You have | Bring it in with | You need | Hand it out with |
|
|
859
|
+
|----------------------------|---------------------------------|------------------------------|--------------------------|
|
|
860
|
+
| `Future[A]` | `Async.fromFuture(f)` | `Future[A]` | `fa.toFuture` |
|
|
861
|
+
| `CompletionStage[A]` (JVM) | `Async.fromCompletionStage(cs)` | `CompletableFuture[A]` (JVM) | `fa.toCompletableFuture` |
|
|
862
|
+
| `js.Promise[A]` (Scala.js) | `Async.fromJsPromise(p)` | `js.Promise[A]` (Scala.js) | `fa.toJsPromise` |
|
|
863
|
+
|
|
864
|
+
A round trip through `Future` looks like this — take what an existing service hands you, work with it as an `Async`, and give a `Future` back to a caller who still expects one:
|
|
865
|
+
|
|
866
|
+
```scala
|
|
867
|
+
import zio.blocks.async._
|
|
868
|
+
import scala.concurrent.{ExecutionContext, Future}
|
|
869
|
+
import scala.concurrent.ExecutionContext.Implicits.global
|
|
870
|
+
|
|
871
|
+
// The service you already have.
|
|
872
|
+
def loadUserName(id: Int): Future[String] = Future.successful("sam")
|
|
873
|
+
|
|
874
|
+
// Bring it in, work with it as an Async, hand a Future back out.
|
|
875
|
+
def greet(id: Int): Future[String] =
|
|
876
|
+
Async
|
|
877
|
+
.fromFuture(loadUserName(id))
|
|
878
|
+
.map(name => s"hello, $name")
|
|
879
|
+
.catchAll(_ => Async.succeed("hello, guest"))
|
|
880
|
+
.toFuture
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
Java's `CompletionStage` works the same way:
|
|
884
|
+
|
|
885
|
+
```scala
|
|
886
|
+
import zio.blocks.async._
|
|
887
|
+
import java.util.concurrent.{CompletableFuture, CompletionStage}
|
|
888
|
+
import scala.concurrent.ExecutionContext.Implicits.global
|
|
889
|
+
|
|
890
|
+
def fetchToken(): CompletionStage[String] = CompletableFuture.completedFuture("t-123")
|
|
891
|
+
|
|
892
|
+
val token: Async[String] = Async.fromCompletionStage(fetchToken())
|
|
893
|
+
val backToJava: CompletableFuture[String] = token.map(_.toUpperCase).toCompletableFuture
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
On Scala.js the pair is `fromJsPromise` and `toJsPromise`:
|
|
897
|
+
|
|
898
|
+
```scala
|
|
899
|
+
import zio.blocks.async._
|
|
900
|
+
import scala.scalajs.js
|
|
901
|
+
|
|
902
|
+
def fetchJson(url: String): js.Promise[String] = js.native
|
|
903
|
+
|
|
904
|
+
val parsed: Async[String] = Async.fromJsPromise(fetchJson("/api/config"))
|
|
905
|
+
val handedBack: js.Promise[String] = parsed.map(_.trim).toJsPromise
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
Two details are worth knowing before you use them.
|
|
909
|
+
|
|
910
|
+
Handing a value out to `Future` or `CompletableFuture` needs an `ExecutionContext` in scope, exactly as ordinary `Future` code does — usually `import scala.concurrent.ExecutionContext.Implicits.global`, as above, or whichever one your application already provides. `toJsPromise` needs nothing, because JavaScript has a single built-in event loop to run the callback on.
|
|
911
|
+
|
|
912
|
+
Failures survive the trip. That takes some care on the Java side: when a `CompletionStage` fails, Java wraps your exception in a `CompletionException` before handing it over. `fromCompletionStage` unwraps it, so the `Async` fails with the exception you actually threw rather than with Java's wrapper — which means `catchAll` sees what you expect.
|
|
913
|
+
|
|
914
|
+
Two further integration points are worth knowing about:
|
|
915
|
+
|
|
916
|
+
**Cancelling with `Using`.** `Async.Running` is an `AutoCloseable` — `Cancelable` extends it — so `scala.util.Using` (or Java's try-with-resources) cancels the work automatically when the block ends, the same way it closes a file handle:
|
|
917
|
+
|
|
918
|
+
```scala
|
|
919
|
+
import zio.blocks.async._
|
|
920
|
+
import scala.util.Using
|
|
921
|
+
|
|
922
|
+
def pollForUpdates(): Nothing = { while (true) Thread.sleep(100); ??? }
|
|
923
|
+
|
|
924
|
+
Using(Async.start(pollForUpdates())) { running =>
|
|
925
|
+
// Do other work while the poller runs.
|
|
926
|
+
Thread.sleep(500)
|
|
927
|
+
} // leaving the block cancels the driver, whether or not the body threw —
|
|
928
|
+
// the loop itself keeps running; see Cancelable
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
The [Scope reference](resource-management/scope.md) covers the wider resource-management model.
|
|
932
|
+
|
|
933
|
+
**Feeding [streams](streams/core/stream.md).** A callback-based source can be turned into a stream with `Async.promise` and a `Completer`. Because a stream pulls values as it is ready for them, a source that produces faster than the consumer can handle will not overwhelm it.
|
|
934
|
+
|
|
935
|
+
## Custom Suspension
|
|
936
|
+
|
|
937
|
+
A result that is not here yet arrives in one of two ways: either something tells you when it is ready, or you have to keep asking. This module has a type for each. `Completer[A]` covers being told, and it is the one you will almost always want. `Pollable[A]` covers having to ask, and exists for the sources that leave you no choice.
|
|
938
|
+
|
|
939
|
+
### Pollable
|
|
940
|
+
|
|
941
|
+
Imagine waiting on something that never calls you back — a non-blocking socket that answers "no data yet" when you read it, a hardware timer you have to check, a native handle that reports progress only when asked. There is no callback to hand a `Completer` to. The only way to learn whether the result has arrived is to *ask*, and to keep asking. The module cannot know how to ask your particular source; only your code knows that.
|
|
942
|
+
|
|
943
|
+
`Pollable[A]` is where you supply that knowledge. It is a single method:
|
|
944
|
+
|
|
945
|
+
```scala
|
|
946
|
+
abstract class Pollable[+A] {
|
|
947
|
+
def poll(onComplete: Runnable): Async[A]
|
|
948
|
+
}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
The driver calls `poll` whenever it gets the chance, and what you return tells it what to do next:
|
|
952
|
+
|
|
953
|
+
- **Ready?** Return `Async.succeed(a)`. The driver takes the value and stops asking.
|
|
954
|
+
- **Not yet?** Return `this` — *and make sure `onComplete` will be run*. The driver does not come back on its own.
|
|
955
|
+
- **Failed?** Return `Async.fail(t)`. The driver takes the failure and stops asking, and it travels downstream like any other failure.
|
|
956
|
+
|
|
957
|
+
`onComplete` is not an optimisation — it is the only thing that gets you polled again. After a `poll` returns `this`, the driver parks and waits for that callback; if nothing ever runs it, a `block` waits forever on the JVM and throws `IllegalStateException` on Scala.js. `Async.never` is precisely a `poll` that returns `this` and never arms it. Either run `onComplete` before returning, or hand it to whatever will know when to check again.
|
|
958
|
+
|
|
959
|
+
Most programs never need any of this. For a callback-based API, `Async.promise` with a `Completer` is simpler and already correct; for blocking I/O, `Async.attempt` on a worker via `Async.start` fits better. Reach for `Pollable` only when the result genuinely has to be checked rather than delivered.
|
|
960
|
+
|
|
961
|
+
Here is the smallest thing that behaves like a real suspension — a value that refuses to be ready for its first two visits:
|
|
962
|
+
|
|
963
|
+
```scala
|
|
964
|
+
import zio.blocks.async._
|
|
965
|
+
|
|
966
|
+
class Delayed[A](v: A, var ticks: Int) extends Pollable[A] {
|
|
967
|
+
def poll(onComplete: Runnable): Async[A] =
|
|
968
|
+
if (ticks <= 0) Async.succeed(v)
|
|
969
|
+
else { ticks -= 1; onComplete.run(); this }
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
val result: String = new Delayed("done", ticks = 2).block // => "done"
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Follow it one visit at a time:
|
|
976
|
+
|
|
977
|
+
| Visit | `ticks` | What `poll` returns | What the driver does |
|
|
978
|
+
|-------|--------:|-------------------------|-------------------------------|
|
|
979
|
+
| 1st | 2 | `this` | Not ready — asks again |
|
|
980
|
+
| 2nd | 1 | `this` | Not ready — asks again |
|
|
981
|
+
| 3rd | 0 | `Async.succeed("done")` | Takes the value, stops asking |
|
|
982
|
+
|
|
983
|
+
Returning `this` means "still me, still waiting." Returning `Async.succeed(v)` means "here it is." And `onComplete.run()` is the nudge that tells the driver to come back soon rather than in its own time. The toy above runs it immediately, which just asks for another visit right away; real code instead hands `onComplete` to whatever it is waiting on — a socket selector, a timer callback — and lets that source run it when something actually happens. The driver can then stay asleep in between, rather than burning a thread asking a question whose answer has not changed. Meanwhile `.block` waits through all three visits and hands you `"done"` at the end.
|
|
984
|
+
|
|
985
|
+
A real implementation replaces the counter with the actual question — has the socket got bytes, has the timer expired — but the shape does not change.
|
|
986
|
+
|
|
987
|
+
Here is a case you are likely to meet. A service starts a long job — rendering a report, transcoding a video, restoring an archive — and gives you back a job id. There is no webhook and no callback: the only way to find out whether it has finished is to call `GET /jobs/{id}` and look at the status. That is a `Pollable`:
|
|
988
|
+
|
|
989
|
+
```scala
|
|
990
|
+
import zio.blocks.async._
|
|
991
|
+
|
|
992
|
+
sealed trait JobStatus
|
|
993
|
+
case object Pending extends JobStatus
|
|
994
|
+
case class Done(url: String) extends JobStatus
|
|
995
|
+
case class Failed(reason: String) extends JobStatus
|
|
996
|
+
|
|
997
|
+
// The API you are given: you can ask it, it will never tell you.
|
|
998
|
+
def checkJob(id: String): JobStatus = Done("https://example.invalid/report.pdf")
|
|
999
|
+
|
|
1000
|
+
def download(url: String): Unit = ()
|
|
1001
|
+
|
|
1002
|
+
// Run `task` once, later, without holding on to a thread in the meantime.
|
|
1003
|
+
def scheduleIn(ms: Long, task: Runnable): Unit = {
|
|
1004
|
+
val t = new Thread(() => { Thread.sleep(ms); task.run() })
|
|
1005
|
+
t.setDaemon(true)
|
|
1006
|
+
t.start()
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
final class JobPollable(id: String) extends Pollable[String] {
|
|
1010
|
+
def poll(onComplete: Runnable): Async[String] =
|
|
1011
|
+
checkJob(id) match {
|
|
1012
|
+
case Done(url) => Async.succeed(url)
|
|
1013
|
+
case Failed(reason) => Async.fail(new RuntimeException(s"job $id failed: $reason"))
|
|
1014
|
+
case Pending =>
|
|
1015
|
+
// Nothing will announce the change, so arrange our own next look.
|
|
1016
|
+
scheduleIn(2000, onComplete)
|
|
1017
|
+
this
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// From here on it is an ordinary Async: compose it, start it, cancel it.
|
|
1022
|
+
val reportUrl: Async[String] = new JobPollable("job-42")
|
|
1023
|
+
val saved: Async[Unit] = reportUrl.map(url => download(url))
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Three things to notice. The status check happens inside `poll`, so it runs only when the driver visits — you are not running a loop of your own. The `Pending` branch schedules the next visit two seconds out, which is what stops this from hammering the service. And a failed job becomes `Async.fail`, so the error travels the same path as every other failure and `catchAll` can recover it.
|
|
1027
|
+
|
|
1028
|
+
Note also what is *not* in the example: no blocking wait, no lock, no shared mutable state. Polling puts you in charge of when the check happens, which is the reason to choose it. Because a `Pollable[A]` can be used wherever an `Async[A]` is expected, the value drops straight into any composition and works with every combinator.
|
|
1029
|
+
|
|
1030
|
+
### The Pollable Protocol
|
|
1031
|
+
|
|
1032
|
+
Everything above is what a `Pollable` looks like from the outside. This is the contract a driver and an implementation hold each other to, and it matters only if you are on one side of it — writing a `poll`, or writing a driver of your own. If you are composing `Async` values and running them with `block`, `start`, or an interop converter, the built-in drivers already honour every rule below and you can skip the section.
|
|
1033
|
+
|
|
1034
|
+
**Polling is one-shot.** A driver keeps polling only while `poll` returns a still-pending `Pollable`, and must stop as soon as it returns a terminal result — a raw value, or a failed `Async`. Re-polling a `Pollable` after that is outside the contract: it is not guaranteed to be a pure re-observation, a continuation may run a second time, and diagnostic state may be repeated.
|
|
1035
|
+
|
|
1036
|
+
**Identity carries meaning.** A still-pending result is either this pollable itself — the common case — or a *replacement* pollable standing for the rest of the computation, and the caller directs its next poll at whatever came back. Which of the two it is decides what the caller may do next:
|
|
1037
|
+
|
|
1038
|
+
| What `poll` returns | What it means | What the driver does next |
|
|
1039
|
+
|------------------------------|-------------------------------------|----------------------------------------|
|
|
1040
|
+
| `this` — the same identity | Pending; `onComplete` is registered | May wait for that callback |
|
|
1041
|
+
| A different `Pollable` | Synchronous progress, not readiness | Polls the replacement, without waiting |
|
|
1042
|
+
| A value, or a failed `Async` | Terminal | Stops polling |
|
|
1043
|
+
|
|
1044
|
+
The middle row is the one that trips people up. An identity change is *not* a readiness event, so a caller or a wrapper must never synthesize an `onComplete` call for it — it means "there is more to do right now", and waiting on a callback that nobody will run is how such a wrapper hangs.
|
|
1045
|
+
|
|
1046
|
+
**Wakes are permits, not proofs.** Real callbacks may be stale, reentrant, or duplicated. `onComplete` is therefore a coalescible wake permit — "look again" — rather than proof that any particular generation has completed. A driver re-polls and lets `poll` decide; an implementation is free to run `onComplete` more times than strictly necessary without breaking anything, though never fewer.
|
|
1047
|
+
|
|
1048
|
+
**Cancellation reaches the leaf.** Alongside `poll`, a `Pollable` may override `cancel()`, which the driver signals on the active pending operation when cancellation wins the race against completion. The default is a no-op, so a source with abortable work has to supply it — and it must be idempotent and non-blocking, because cancellation never waits:
|
|
1049
|
+
|
|
1050
|
+
```scala
|
|
1051
|
+
import zio.blocks.async._
|
|
1052
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
1053
|
+
|
|
1054
|
+
// `register` stands in for handing the waker to the real source: a socket
|
|
1055
|
+
// selector, a timer, a third-party library's callback slot.
|
|
1056
|
+
final class AbortableRead(register: Runnable => Unit) extends Pollable[Array[Byte]] {
|
|
1057
|
+
private val aborted = new AtomicBoolean(false)
|
|
1058
|
+
|
|
1059
|
+
def poll(onComplete: Runnable): Async[Array[Byte]] =
|
|
1060
|
+
if (aborted.get()) Async.fail(new java.io.IOException("read aborted"))
|
|
1061
|
+
else { register(onComplete); this }
|
|
1062
|
+
|
|
1063
|
+
// Idempotent and non-blocking: it records the intent and returns. The
|
|
1064
|
+
// driver has already stopped listening by the time this runs.
|
|
1065
|
+
override def cancel(): Unit = aborted.set(true)
|
|
1066
|
+
}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
The full contract lives in the scaladoc of `Pollable#poll`; [`Cancelable`](#cancelable) covers what cancelling does and does not stop.
|
|
1070
|
+
|
|
1071
|
+
### Completer
|
|
1072
|
+
|
|
1073
|
+
Polling is the awkward case. Far more often the source does call you back — that is what `Completer[A]` is for, and why you will reach for it and not `Pollable`.
|
|
1074
|
+
|
|
1075
|
+
`Completer[A]` is a `Pollable[A]` that is already written: instead of implementing "is it ready?", you hold a value someone else completes exactly once. It is thread-safe, and the first call to `Completer#succeed` or `Completer#fail` wins while every later call does nothing — so a callback that fires twice cannot corrupt the result.
|
|
1076
|
+
|
|
1077
|
+
The structural declaration is:
|
|
1078
|
+
|
|
1079
|
+
```scala
|
|
1080
|
+
final class Completer[A] extends Pollable[A] {
|
|
1081
|
+
def succeed(a: A): Unit
|
|
1082
|
+
def fail(cause: Throwable): Unit
|
|
1083
|
+
def peek: Async[A]
|
|
1084
|
+
def poll(onComplete: Runnable): Async[A]
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
`Async.promise` creates a new `Completer[A]`, passes it to the body, and returns the `Completer` as an `Async[A]` that the driver polls until the callback fires. If the body happens to complete it before returning — a cache hit, a callback that fires inline — the result collapses to a plain ready value and no `Pollable` is allocated at all. It is the only place in the module where the *code you write* differs between Scala versions — elsewhere the signatures differ but the call sites are identical:
|
|
1089
|
+
|
|
1090
|
+
```scala
|
|
1091
|
+
// Scala 2 — the completer is an ordinary function parameter
|
|
1092
|
+
def promise[A](body: Completer[A] => Unit): Async[A]
|
|
1093
|
+
|
|
1094
|
+
// Scala 3 — the completer is a context parameter of the body
|
|
1095
|
+
inline def promise[A](inline body: Completer[A] ?=> Unit): Async[A]
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
The `?=>` on Scala 3 makes the `Completer` a given inside the body rather than a plain argument, which is why the body is written `{ c ?=> ... }` there and `{ c => ... }` on Scala 2. (The two `inline` keywords are what splice the body into the call site instead of allocating a function object — the same technique behind the allocation-free ready path described under [Evaluation Model](#evaluation-model).)
|
|
1099
|
+
|
|
1100
|
+
Being a given is not just bookkeeping: it buys you top-level `succeed` and `fail` helpers that find the completer themselves, so on Scala 3 the bridge need not name it at all.
|
|
1101
|
+
|
|
1102
|
+
```scala
|
|
1103
|
+
// Scala 3 only — `succeed` and `fail` take the Completer as a given.
|
|
1104
|
+
val fetched: Async[Int] = Async.promise[Int] { c ?=>
|
|
1105
|
+
legacyLookup(onOk = value => succeed(value), onErr = cause => fail(cause))
|
|
1106
|
+
}
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
Naming it, as the examples below do, is equally valid and reads better when the callback is registered several lines away from where it fires.
|
|
1110
|
+
|
|
1111
|
+
<Tabs groupId="scala-version" defaultValue="scala2">
|
|
1112
|
+
<TabItem value="scala2" label="Scala 2">
|
|
1113
|
+
|
|
1114
|
+
```scala
|
|
1115
|
+
import zio.blocks.async._
|
|
1116
|
+
|
|
1117
|
+
val async: Async[Int] = Async.promise[Int] { c =>
|
|
1118
|
+
new Thread(() => { Thread.sleep(20); c.succeed(42) }).start()
|
|
1119
|
+
}
|
|
1120
|
+
val result: Int = async.block // => 42
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
</TabItem>
|
|
1124
|
+
<TabItem value="scala3" label="Scala 3">
|
|
1125
|
+
|
|
1126
|
+
```scala
|
|
1127
|
+
import zio.blocks.async._
|
|
1128
|
+
|
|
1129
|
+
val async: Async[Int] = Async.promise[Int] { c ?=>
|
|
1130
|
+
new Thread(() => { Thread.sleep(20); c.succeed(42) }).start()
|
|
1131
|
+
}
|
|
1132
|
+
val result: Int = async.block // => 42
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
</TabItem>
|
|
1136
|
+
</Tabs>
|
|
1137
|
+
|
|
1138
|
+
Now a case from the JDK rather than a sleeping thread. `AsynchronousFileChannel` reads a file without blocking, and reports the outcome through a `CompletionHandler` with two methods: `completed` when the bytes arrive, `failed` when the read goes wrong. Those two are exactly `succeed` and `fail`, so the bridge is almost mechanical:
|
|
1139
|
+
|
|
1140
|
+
```scala
|
|
1141
|
+
import zio.blocks.async._
|
|
1142
|
+
import java.nio.ByteBuffer
|
|
1143
|
+
import java.nio.channels.{AsynchronousFileChannel, CompletionHandler}
|
|
1144
|
+
import java.nio.file.{Path, StandardOpenOption}
|
|
1145
|
+
|
|
1146
|
+
def readChunk(path: Path, size: Int): Async[ByteBuffer] = {
|
|
1147
|
+
val completer = new Completer[ByteBuffer]
|
|
1148
|
+
val channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)
|
|
1149
|
+
val buffer = ByteBuffer.allocate(size)
|
|
1150
|
+
|
|
1151
|
+
channel.read(buffer, 0L, buffer, new CompletionHandler[Integer, ByteBuffer] {
|
|
1152
|
+
def completed(bytesRead: Integer, buf: ByteBuffer): Unit = {
|
|
1153
|
+
buf.flip()
|
|
1154
|
+
completer.succeed(buf) // the read finished
|
|
1155
|
+
}
|
|
1156
|
+
def failed(cause: Throwable, buf: ByteBuffer): Unit =
|
|
1157
|
+
completer.fail(cause) // the read went wrong
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
completer.peek // hand the pending result to the caller
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// An ordinary Async from here on.
|
|
1164
|
+
val firstBytes: Async[Int] = readChunk(Path.of("data.bin"), 1024).map(_.remaining)
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
This is the same bridge as `Async.promise`, written out by hand: create the `Completer`, give its two methods to the callback, and return `completer.peek` as the `Async[ByteBuffer]` the caller waits on. `Async.promise` packages exactly those three steps, so the same function written with it is shorter:
|
|
1168
|
+
|
|
1169
|
+
```scala
|
|
1170
|
+
import zio.blocks.async._
|
|
1171
|
+
import java.nio.ByteBuffer
|
|
1172
|
+
import java.nio.channels.{AsynchronousFileChannel, CompletionHandler}
|
|
1173
|
+
import java.nio.file.{Path, StandardOpenOption}
|
|
1174
|
+
|
|
1175
|
+
def readChunk(path: Path, size: Int): Async[ByteBuffer] =
|
|
1176
|
+
Async.promise[ByteBuffer] { c ?=>
|
|
1177
|
+
val channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)
|
|
1178
|
+
val buffer = ByteBuffer.allocate(size)
|
|
1179
|
+
|
|
1180
|
+
channel.read(buffer, 0L, buffer, new CompletionHandler[Integer, ByteBuffer] {
|
|
1181
|
+
def completed(bytesRead: Integer, buf: ByteBuffer): Unit = {
|
|
1182
|
+
buf.flip()
|
|
1183
|
+
c.succeed(buf)
|
|
1184
|
+
}
|
|
1185
|
+
def failed(cause: Throwable, buf: ByteBuffer): Unit =
|
|
1186
|
+
c.fail(cause)
|
|
1187
|
+
})
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
The completer is created for you and named `c`, and there is no `peek` at the end — `promise` returns the `Async` itself.
|
|
1192
|
+
|
|
1193
|
+
Prefer this version. Write the completer out by hand when the registration does not fit neatly in a single block, when you need to keep the completer around to complete it from elsewhere, or when you want identical source on Scala 2 and Scala 3 — `new Completer[A]` has no context-function syntax to differ over.
|
|
1194
|
+
|
|
1195
|
+
`readChunk` returns before a single byte has been read. Nothing blocks, no thread waits, and the caller receives an `Async[ByteBuffer]` that behaves like any other — `map` it, `zipWith` another read, recover it with `catchAll`, or `block` on it at the edge of the program.
|
|
1196
|
+
|
|
1197
|
+
The once-only guarantee earns its keep in code like this. You are trusting a third-party library to call your handler correctly; if a buggy or retrying implementation calls `completed` twice, or calls both `completed` and `failed`, the first call still decides the outcome and the rest are ignored. You do not have to defend against it yourself.
|
|
1198
|
+
|
|
1199
|
+
`Completer#peek` returns the `Completer` itself as an `Async[A]`, bypassing the `Async.promise` body — useful when managing scheduling manually, as shown in the `delayed` helper under [How They Work Together](#how-they-work-together).
|
|
1200
|
+
|
|
1201
|
+
## Controlling In-Flight Work
|
|
1202
|
+
|
|
1203
|
+
Once `start` has handed you an `Async.Running[A]`, the computation is being driven for you — by the platform driver if it still has waiting to do, and already settled if it does not. These two types are how you keep a grip on it: `Async.Running` is the handle, and `Cancelable` is the ability to stop what it refers to.
|
|
1204
|
+
|
|
1205
|
+
### Async.Running
|
|
1206
|
+
|
|
1207
|
+
`Async.Running[A]` is the handle returned by `start`. It extends `Pollable[A]`, which makes it an `Async[A]` in its own right, and [`Cancelable`](#cancelable), which is what lets you stop it.
|
|
1208
|
+
|
|
1209
|
+
The structural declaration is:
|
|
1210
|
+
|
|
1211
|
+
```scala
|
|
1212
|
+
abstract class Running[+A] extends Pollable[A] with Cancelable
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
Because a `Running` is an `Async[A]`, you can wait for its result with `block`, compose it further with `map` or `flatMap`, or pass it anywhere an `Async[A]` is expected. [Concurrent Fan-Out via Running](#concurrent-fan-out-via-running) walks through that pattern, and why several consumers should share one handle rather than each starting the work themselves.
|
|
1216
|
+
|
|
1217
|
+
What you should not do is call `poll` yourself. It is there for drivers, and its contract — stop at a terminal value, never re-poll a settled one — is easy to violate by hand. Drive a `Running` the same way you drive any other `Async`: `block`, `toFuture`, or composition. There is no `isCompleted`; if you want to know whether it has finished without waiting, keep that flag yourself where you complete the work.
|
|
1218
|
+
|
|
1219
|
+
Calling `cancel` stops the *driver*, and does nothing if the run has already settled. [`Cancelable`](#cancelable) covers how far that reaches; two consequences belong here:
|
|
1220
|
+
|
|
1221
|
+
```scala
|
|
1222
|
+
import zio.blocks.async._
|
|
1223
|
+
|
|
1224
|
+
val running: Async.Running[Nothing] = Async.never.start
|
|
1225
|
+
running.cancel() // the driver stops polling; no value is ever published
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
A cancelled run never settles at all — it does not fail, it simply stops. So anything still holding that handle and calling `block` on it waits forever on the JVM, and gets an `IllegalStateException` on Scala.js. Cancel only when you own every consumer of the handle.
|
|
1229
|
+
|
|
1230
|
+
And with `Async.start(body)`, `cancel` stops the driver, not the evaluation of `body`. That block runs to completion regardless — on its own daemon thread on the JVM, on the next microtask on Scala.js — so cancelling that particular `Running` means you have stopped waiting for the result, not that the work behind it has stopped. A suspended `fa.start`, by contrast, does have its active leaf signalled; [`Cancelable`](#cancelable) draws the line.
|
|
1231
|
+
|
|
1232
|
+
A `Running` is also an `AutoCloseable`, so [`scala.util.Using`](#integration-points) cancels it on leaving a block.
|
|
1233
|
+
|
|
1234
|
+
`block` on a pending handle is available on the JVM only — see [Platform Support](#platform-support).
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
Attach what you want to observe *before* calling `start`, not after. `start` hands the still-waiting part of the value to a driver, and whatever you composed onto it beforehand is part of what that driver runs:
|
|
1238
|
+
|
|
1239
|
+
```scala
|
|
1240
|
+
import zio.blocks.async._
|
|
1241
|
+
|
|
1242
|
+
// A row that arrives a moment from now, on another thread.
|
|
1243
|
+
def fetchRow(): Async[String] = Async.promise[String] { c ?=>
|
|
1244
|
+
val t = new Thread(() => { Thread.sleep(250); c.succeed("row-1") })
|
|
1245
|
+
t.setDaemon(true)
|
|
1246
|
+
t.start()
|
|
1247
|
+
}
|
|
1248
|
+
def log(msg: String): Unit = ()
|
|
1249
|
+
|
|
1250
|
+
// Before start: the tap is part of what the driver runs, and fires when the
|
|
1251
|
+
// row arrives.
|
|
1252
|
+
val watched: Async.Running[String] =
|
|
1253
|
+
fetchRow().tap(row => Async.attempt(log(s"got $row"))).start
|
|
1254
|
+
|
|
1255
|
+
// After start: this builds a *new* Async that nobody is driving. The tap runs
|
|
1256
|
+
// only if you drive this one too — by blocking on it, or starting it.
|
|
1257
|
+
val bolted: Async[String] =
|
|
1258
|
+
fetchRow().start.tap(row => Async.attempt(log(s"got $row")))
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
The second version is not a compile error and not a lost value, which is what makes it easy to write by mistake: `bolted` is simply a value nobody has driven, so its `tap` has not run and will not until something asks `bolted` for a result. So if you want to time the fetch, log its progress, or react the moment it fails, the observer has to be inside the value you hand to `start`.
|
|
1262
|
+
|
|
1263
|
+
`either` and `foldCause` matter more, because they decide whether the run counts as failed. Written `fa.either.start`, where `fa` is the value you are about to start, the run always succeeds, carrying a `Left` or a `Right`. Written `fa.start.either`, the run has already failed; you get your `Either`, but everyone else holding that handle still gets the exception.
|
|
1264
|
+
|
|
1265
|
+
All of that assumes there was something to wait for. If the value already holds its answer there is nothing to hand to a driver: `start` wraps it and returns, spawning no worker, and anything you attached ran while you were building the value — see [Evaluation Model](#evaluation-model).
|
|
1266
|
+
|
|
1267
|
+
### Running#cancel
|
|
1268
|
+
|
|
1269
|
+
A `Running` carries two `cancel` methods. One it inherits from [`Cancelable`](#cancelable) and takes nothing; the other takes a reporter:
|
|
1270
|
+
|
|
1271
|
+
```scala
|
|
1272
|
+
abstract class Running[+A] extends Pollable[A] with Cancelable {
|
|
1273
|
+
def cancel(): Unit // inherited from Cancelable
|
|
1274
|
+
def cancel(onCleanupFailure: Throwable => Unit): Unit
|
|
1275
|
+
}
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
They cancel identically. What differs is where a failure thrown by *cleanup* ends up — and cleanup is the one thing cancellation can still fail at. Cancelling signals `cancel()` on the active leaf, and the teardown that follows may be asynchronous and may throw. That failure has nowhere natural to go: the run has been cancelled, so it will never deliver a result, and there is no channel left to carry an exception to whoever was waiting.
|
|
1279
|
+
|
|
1280
|
+
So it is reported instead. `cancel()` sends it to the ambient handler for the platform — the calling thread's `UncaughtExceptionHandler` on the JVM, the queue's failure reporter on Scala.js. `cancel(onCleanupFailure)` sends it to your function, which is what you want whenever "a socket refused to close" should reach a log or a metric rather than stderr:
|
|
1281
|
+
|
|
1282
|
+
```scala
|
|
1283
|
+
import zio.blocks.async._
|
|
1284
|
+
|
|
1285
|
+
val running: Async.Running[Nothing] = Async.never.start
|
|
1286
|
+
|
|
1287
|
+
running.cancel { cause =>
|
|
1288
|
+
System.err.println(s"cleanup after cancellation failed: ${cause.getMessage}")
|
|
1289
|
+
}
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
Three properties are worth knowing before you rely on it:
|
|
1293
|
+
|
|
1294
|
+
- **The reporter is kept only if this cancellation wins.** If the run had already settled, or another `cancel` call claimed it first, your function is dropped and never invoked. A dropped reporter is not an error; it means there was no cancellation cleanup of yours to report on.
|
|
1295
|
+
- **It is invoked at most once.** When cleanup fails in more than one place, the later causes are attached to the first as suppressed exceptions and the first is what you are handed — so check `getSuppressed` if you are logging the whole picture.
|
|
1296
|
+
- **It may run on any thread.** Cleanup is driven by whichever party claims it, which is not necessarily the thread that called `cancel`. Keep the reporter short and make sure it cannot throw.
|
|
1297
|
+
|
|
1298
|
+
### Cancelable
|
|
1299
|
+
|
|
1300
|
+
`Cancelable` is the minimal cancellation interface: one `cancel()` method, safe to call from any thread and safe to call twice.
|
|
1301
|
+
|
|
1302
|
+
Be precise about what it stops. Cancellation does two things, and they reach different distances:
|
|
1303
|
+
|
|
1304
|
+
- **It stops the driver.** The poll loop halts and publication of a terminal value is suppressed. A cancelled run therefore never delivers at all — it does not fail, it simply stays pending forever, which is why anything still calling `block` on that handle waits indefinitely.
|
|
1305
|
+
- **It signals the active pending operation.** When cancellation wins the race against completion, the driver calls [`Pollable.cancel()`](#the-pollable-protocol) on whatever leaf the run is currently suspended on. A leaf that owns abortable work — an in-flight socket read, a timer, a registration with a third-party library — implements that hook and gets the chance to tear it down.
|
|
1306
|
+
|
|
1307
|
+
The second point is the part to get right, because the default hook is a **no-op**. A leaf that does not override `cancel()` is not aborted by cancelling the run: its socket read stays outstanding, its timer still fires, its `js.Promise` still settles. Whether cancellation reaches the work is a property of the leaf, not of the handle you called `cancel()` on.
|
|
1308
|
+
|
|
1309
|
+
Cancellation is also **cooperative**, never preemptive. It does not wait for an already-running `poll` invocation to return, and it interrupts no thread. Calling `cancel()` publishes the signal and returns; whatever teardown that triggers is driven afterwards, on whichever party wins the claim.
|
|
1310
|
+
|
|
1311
|
+
That is why a leaf holding a resource still deserves an explicit finalizer. If a cancelled computation would otherwise leave a socket or a file handle open and you do not control its `Pollable`, close it yourself — pair the cancellation with [`ensuring`](#bracket-and-ensuring), or hold the resource in a `Using` block.
|
|
1312
|
+
|
|
1313
|
+
The structural declaration is:
|
|
1314
|
+
|
|
1315
|
+
```scala
|
|
1316
|
+
trait Cancelable extends AutoCloseable {
|
|
1317
|
+
def cancel(): Unit
|
|
1318
|
+
final def close(): Unit = cancel()
|
|
1319
|
+
}
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
`Cancelable.noop` is the predefined no-op instance, useful as a placeholder when no real cancellation is needed:
|
|
1323
|
+
|
|
1324
|
+
```scala
|
|
1325
|
+
import zio.blocks.async._
|
|
1326
|
+
|
|
1327
|
+
val c: Cancelable = Cancelable.noop
|
|
1328
|
+
c.cancel() // no-op
|
|
1329
|
+
c.close() // no-op; delegates to cancel()
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
## AsyncSelector
|
|
1333
|
+
|
|
1334
|
+
`AsyncSelector[A]` is a low-level building block, and that is worth saying before anything else: most code should not reach for it. If what you want is "run N of these at a time and give me results as they land", the streams module already provides it — [`mapPar`](streams/core/stream.md#streammappar), [`mergeAll`](streams/core/stream.md#streammergeall), and `mapParAsync` — with backpressure, ordering rules, and resource cleanup you would otherwise write yourself. The selector exists because those operators needed a primitive underneath them, and the streams concurrency engine is its only production consumer.
|
|
1335
|
+
|
|
1336
|
+
What it gives you is a **repeatable** multi-way wait. You hand it a fixed number of slots, each holding an independently-pending computation. `select` waits until one of them completes, hands you the winning slot index along with its value, and disarms that slot. `replace` arms the same slot with its next computation, and you select again. That loop is the whole point: a fan-in that runs for the life of a connection, a worker pool that keeps N requests in flight, anything where the same slot is re-used thousands of times.
|
|
1337
|
+
|
|
1338
|
+
A hand-rolled race loop can do the first round of that, but not the thousandth. Racing N computations registers a callback on every loser, and the losers survive into the next round, so a fresh callback piles up on each of them every time round. The selector instead keeps **one stable waker per armed slot** for as long as that slot stays armed, no matter how many selections pass over it.
|
|
1339
|
+
|
|
1340
|
+
Selection is round-robin rather than first-past-the-post. The scan starts at the slot *after* the previous winner, so a slot that is continuously eligible — armed, and with a completed computation — wins within at most `armedCount` *successful* selections. No slot can be starved by a faster neighbour.
|
|
1341
|
+
|
|
1342
|
+
The public surface is small:
|
|
1343
|
+
|
|
1344
|
+
```scala
|
|
1345
|
+
final class AsyncSelector[A] private (slotCount: Int, initial: IndexedSeq[(Int, Async[A])]) extends Cancelable {
|
|
1346
|
+
def size: Int
|
|
1347
|
+
def replace(index: Int, value: => Async[A]): Unit
|
|
1348
|
+
def select: Async[(Int, A)]
|
|
1349
|
+
def shutdown: Async[Unit]
|
|
1350
|
+
override def cancel(): Unit
|
|
1351
|
+
}
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
You never construct one directly; `Async.selector` does it:
|
|
1355
|
+
|
|
1356
|
+
```scala
|
|
1357
|
+
def selector[A](inputs: IndexedSeq[Async[A]]): AsyncSelector[A]
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
The `inputs` sequence fixes the slot count for the life of the selector — `size` reports it, and it never changes, disarmed slots included. Here is the full loop:
|
|
1361
|
+
|
|
1362
|
+
```scala
|
|
1363
|
+
import zio.blocks.async._
|
|
1364
|
+
|
|
1365
|
+
val selector: AsyncSelector[Int] =
|
|
1366
|
+
Async.selector(Vector(Async.succeed(10), Async.succeed(20), Async.succeed(30)))
|
|
1367
|
+
|
|
1368
|
+
// Whichever slot is eligible, scanning from just after the previous winner.
|
|
1369
|
+
val (firstSlot, firstValue) = selector.select.block
|
|
1370
|
+
|
|
1371
|
+
// Re-arm the slot that just won. Until this runs, that slot sits disarmed
|
|
1372
|
+
// and is skipped by every selection.
|
|
1373
|
+
selector.replace(firstSlot, Async.succeed(firstValue + 1))
|
|
1374
|
+
|
|
1375
|
+
val (secondSlot, secondValue) = selector.select.block
|
|
1376
|
+
|
|
1377
|
+
// Cancel every still-armed loser and join all of their cleanup.
|
|
1378
|
+
selector.shutdown.block
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
Four rules keep that loop honest:
|
|
1382
|
+
|
|
1383
|
+
- **A slot can only be armed when it is disarmed.** `replace` on a slot that is still armed throws `IllegalStateException("selector slot N is already armed")`, and `replace` after shutdown throws `IllegalStateException("selector is closed")`. The safe pattern is the one above: re-arm the index you were just handed by `select`, and nothing else.
|
|
1384
|
+
- **`value` is by-name for a reason.** The thunk is evaluated lazily and outside the selector's own lock, so constructing the next computation cannot deadlock against a concurrent `select`. A thunk that returns `null` is reified as a failed `Async` carrying `NullPointerException("selector input returned null Async")` rather than corrupting the slot.
|
|
1385
|
+
- **A winner is removed exactly once.** Disarming happens atomically with the win, so the same completion cannot be handed to two selections, and a failure from an input surfaces as a failed `select` rather than poisoning the selector.
|
|
1386
|
+
- **`shutdown` is where cleanup is joined.** It cancels every armed loser, is idempotent, and — importantly — does not complete until cleanup for every still-armed loser has finished. `cancel()` is the `Cancelable` spelling of the same thing: it starts `shutdown` and returns immediately without waiting. Use `cancel()` when the selector is a resource being closed on the way out of a scope, and `shutdown` when you need to know the cleanup actually finished.
|
|
1387
|
+
|
|
1388
|
+
:::warning[`.block` per selection is for the example only]
|
|
1389
|
+
The snippet above blocks once per round so it reads top to bottom. Real code composes `select` like any other `Async` — `flatMap` it, or `await` it inside `Async.async` — and blocks once at the edge, if at all. Blocking inside a driver deadlocks it; see [Driving](#driving).
|
|
1390
|
+
:::
|
|
1391
|
+
|
|
1392
|
+
## Failure
|
|
1393
|
+
|
|
1394
|
+
`Failure` is how a failed `Async` is represented. You never construct one — `Async.fail` and `Completer#fail` produce it, `catchAll` and `either` recover from it — but it explains why a failure travels through a chain untouched: it extends `Pollable[Nothing]`, and `map` and `flatMap` return it unchanged instead of running their functions.
|
|
1395
|
+
|
|
1396
|
+
```scala
|
|
1397
|
+
final class Failure private (val cause: Throwable, private[async] val trusted: Boolean) extends Pollable[Nothing] {
|
|
1398
|
+
|
|
1399
|
+
/** Public construction is deliberately an ordinary, untrusted failure. */
|
|
1400
|
+
def this(cause: Throwable) = this(cause, false)
|
|
1401
|
+
|
|
1402
|
+
def poll(onComplete: Runnable): Async[Nothing] = this
|
|
1403
|
+
}
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
The one-argument constructor is the only one you can call, and it produces an ordinary, untrusted failure. The `trusted` flag is `private[async]`: it marks a failure that entered through one of the library's own internal boundaries — the typed error channel that `zio-blocks-streams` carries in its `Either` — rather than through user code. It changes nothing in the public API: `cause` is the same `Throwable` either way, and every recovery combinator treats both kinds identically. The distinction is visible only to the module's internal fold, which routes a trusted cause down the typed-error path instead of the defect path.
|
|
1407
|
+
|
|
1408
|
+
[`block`](#driving) re-throws `cause`; `catchAll` hands your recovery function the original `Throwable`, unwrapped; [`either`](#error-handling) turns it into a `Left` instead.
|
|
1409
|
+
|
|
1410
|
+
## Platform Support
|
|
1411
|
+
|
|
1412
|
+
The core API behaves identically everywhere by design, and the cross-platform test suite fails if any user-visible core behaviour diverges. What varies is the interop surface, which is deliberately platform-specific, and the two operations that depend on having a thread:
|
|
1413
|
+
|
|
1414
|
+
| Feature | JVM | Scala.js | How it differs |
|
|
1415
|
+
|------------------------------|:---:|:--------:|--------------------------------------------------------------------|
|
|
1416
|
+
| Constructors and combinators | yes | yes | Identical on both |
|
|
1417
|
+
| `Async.async` / `await` | yes | yes | Different backend per platform — see [Direct Style](#direct-style) |
|
|
1418
|
+
| `block` on a pending value | yes | no | Throws on Scala.js: no thread to park |
|
|
1419
|
+
| `fa.start` / `Async.Running` | yes | yes | Serialized `ForkJoinPool` tasks on the JVM, microtasks on Scala.js |
|
|
1420
|
+
| `Async.start(body)` | yes | yes | Daemon thread on the JVM, the next microtask on Scala.js |
|
|
1421
|
+
| `Future` interop | yes | yes | Same API on both |
|
|
1422
|
+
| `CompletionStage` interop | yes | no | JVM only |
|
|
1423
|
+
| `js.Promise` interop | no | yes | Scala.js only |
|
|
1424
|
+
|
|
1425
|
+
All of it works on Scala 2.13 and Scala 3.
|
|
1426
|
+
|
|
1427
|
+
### Where Suspended Work Runs
|
|
1428
|
+
|
|
1429
|
+
On the **JVM**, a suspended run is a chain of serialized tasks on `ForkJoinPool.commonPool()`. There is no dedicated worker thread per run and no pool of the module's own: a task exits the moment its current pollable is still pending, and the waker that `poll` registered submits the next task. A run that spends most of its life waiting therefore occupies no thread at all while it waits.
|
|
1430
|
+
|
|
1431
|
+
The one exception is `Async.start(body)`, which evaluates an arbitrary synchronous block rather than driving an `Async`. That block gets a thread of its own — a daemon named `zio-blocks-async-eval`.
|
|
1432
|
+
|
|
1433
|
+
`block` parks with `LockSupport.park` / `unpark` rather than a monitor, which is a deliberate choice for **virtual threads**: a parked virtual thread (JDK 21+) unmounts its carrier instead of pinning it. The module is Loom-*friendly* in this sense, but it is not Loom-based — it configures no virtual-thread executor, and running on a virtual thread is entirely the caller's decision.
|
|
1434
|
+
|
|
1435
|
+
On **Scala.js** there is no thread to hand work to, so scheduling goes through `JSExecutionContext.queue` as microtasks, with a `setTimeout(0)` macrotask escape for work that must yield back to the host event loop. A cap of 1024 consecutive ready resumptions bounds how long a run of already-ready steps can hold the queue before yielding, so a long synchronous chain cannot starve rendering or I/O.
|
|
1436
|
+
|
|
1437
|
+
That is also why `block` cannot work there. A pending value gets one chance to complete synchronously inside `poll`; if it has not, `block` throws:
|
|
1438
|
+
|
|
1439
|
+
```
|
|
1440
|
+
java.lang.IllegalStateException: Async.block: suspension did not complete synchronously
|
|
1441
|
+
and JavaScript cannot block. Drive the Pollable from a non-blocking entry point instead.
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1444
|
+
:::note[There is no blocking-operations API]
|
|
1445
|
+
The async module has no `attemptBlocking`, no blocking thread pool, and no way to mark an operation as blocking. `block` is the only "blocking" thing in it, and it is a terminal — the point where you leave `Async` and go back to synchronous code, not a place to put a blocking call. Run genuinely blocking work on a thread you control, for example with `Async.start(body)`, and bridge the result back through the resulting `Running`.
|
|
1446
|
+
:::
|
|
1447
|
+
|
|
1448
|
+
## Using Async with Streams
|
|
1449
|
+
|
|
1450
|
+
`zio-blocks-streams` depends on this module, so `Async` is not a neighbouring library to streams — it is part of the streams vocabulary. Every cross-platform stream terminal hands you an `Async`, and everything on this page applies to the value you get back.
|
|
1451
|
+
|
|
1452
|
+
One convention travels with it. Stream terminals return `Async[Either[E, Z]]`, never `Async[Z]`, because a stream has a typed error channel and `Async` does not. The two are deliberately kept apart:
|
|
1453
|
+
|
|
1454
|
+
- The stream's **typed** error `E` stays inside the `Either`. A stream that fails with a typed error still completes its `Async` *successfully*, carrying a `Left`.
|
|
1455
|
+
- `Async`'s own untyped `Throwable` channel is reserved for **defects** — a callback that threw, a finalizer that failed, a cleanup failure. Those fail the outer `Async` and never appear as a `Left`.
|
|
1456
|
+
|
|
1457
|
+
So `catchAll` on a stream terminal recovers bugs, not the errors the stream declares. Those you match on:
|
|
1458
|
+
|
|
1459
|
+
```scala
|
|
1460
|
+
import zio.blocks.async._
|
|
1461
|
+
import zio.blocks.streams._
|
|
1462
|
+
|
|
1463
|
+
val readings: Stream[String, Int] = Stream(12, 7, 30)
|
|
1464
|
+
|
|
1465
|
+
// A terminal is an ordinary Async, so every combinator on this page applies.
|
|
1466
|
+
val summary: Async[String] =
|
|
1467
|
+
readings.runCollectAsync.map(result =>
|
|
1468
|
+
result match {
|
|
1469
|
+
case Right(values) => s"collected ${values.length} readings"
|
|
1470
|
+
case Left(error) => s"typed error: $error"
|
|
1471
|
+
}
|
|
1472
|
+
)
|
|
1473
|
+
|
|
1474
|
+
// At the edge of a JVM `main`, and nowhere else:
|
|
1475
|
+
val text: String = summary.block
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
`.block` is the edge-of-the-world move described under [Driving](#driving), and the same two limits hold: never inside a stream callback or a `poll`, and never on a value that may still be pending when you are on Scala.js, where it throws. Cross-platform stream code keeps the `Async` and hands it to the host — `toFuture`, `toJsPromise` — or stays inside `Async.async { … }` and uses `await`.
|
|
1479
|
+
|
|
1480
|
+
Cancellation carries across too: cancelling a running stream is the [`Cancelable`](#cancelable) contract on this page, applied to a reader rather than a single leaf.
|
|
1481
|
+
|
|
1482
|
+
[Asynchronous Stream Execution](streams/execution-and-compatibility/async-execution.md) is the reference for all of it — the terminal family, the async source constructors and operators, manual pull and reader ownership, and what cancelling a stream cleans up.
|
|
1483
|
+
|
|
1484
|
+
## Running the Examples
|
|
1485
|
+
|
|
1486
|
+
The `async-examples` module ships `AsyncShowcaseExample`, a single runnable pipeline exercising the whole module: `Completer`-backed callback bridges, the `Async.async` direct-style DSL, `catchAll` recovery, and a final `block`. Its `fulfillOrGuest` function is the one shown under [Direct Style](#direct-style); the file also carries the helpers it calls, which is what makes it runnable as it stands.
|
|
1487
|
+
|
|
1488
|
+
To run the full example, clone the repository and execute:
|
|
1489
|
+
|
|
1490
|
+
```
|
|
1491
|
+
sbt "async-examples/run"
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
## See Also
|
|
1495
|
+
|
|
1496
|
+
- [Asynchronous Stream Execution](streams/execution-and-compatibility/async-execution.md) — the `Async` terminal family, async source constructors and operators, reader ownership, and stream cancellation
|
|
1497
|
+
- [Stream Reference](streams/core/stream.md) — pull-based streaming with resource safety; use `Async.promise` and `Completer` to bridge callback-based push sources into the pull-based stream model
|
|
1498
|
+
- [Scope Reference](resource-management/scope.md) — compile-time resource safety; `Async.Running` extends `AutoCloseable` and can be used inside `scala.util.Using` or any Scope-managed context for structured cancellation
|
|
1499
|
+
- [Compile-Time Resource Safety with Scope](../guides/compile-time-resource-safety-with-scope.md) — step-by-step tutorial on resource ownership that applies equally to `Async.Running` handles
|