@zio.dev/zio-blocks 0.0.51 → 0.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +6 -0
- package/guides/getting-started-with-mux.md +0 -112
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +395 -1
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +131 -70
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +200 -559
- package/package.json +1 -1
- package/reference/async.md +1379 -531
- package/reference/chunk.md +3 -3
- package/reference/codegen/index.md +1 -1
- package/reference/combinators.md +4 -4
- 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 +6 -49
- 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 +2 -2
- package/reference/docs.md +2 -2
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +1 -0
- package/reference/endpoint/index.md +9 -89
- package/reference/endpoint/path-codec.md +12 -24
- package/reference/endpoint/route-pattern.md +4 -6
- package/reference/endpoint/segment-codec.md +19 -32
- package/reference/html.md +313 -9
- package/reference/htmx/index.md +4 -52
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +3 -1
- package/reference/http-model/model.md +107 -71
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +6 -3
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +128 -11
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +7 -2
- package/reference/openapi.md +3 -3
- package/reference/projection.md +654 -0
- package/reference/resource-management/index.md +1 -1
- package/reference/resource-management/resource.md +2 -98
- package/reference/resource-management/scope.md +1 -209
- package/reference/resource-management/wire.md +4 -50
- package/reference/ringbuffer/advanced.mdx +1 -1
- package/reference/ringbuffer/index.mdx +3 -3
- package/reference/ringbuffer/mpmc.mdx +38 -4
- package/reference/ringbuffer/mpsc.mdx +36 -4
- package/reference/ringbuffer/spmc.mdx +1 -1
- package/reference/ringbuffer/spsc.mdx +87 -15
- package/reference/schema/allows.md +0 -96
- package/reference/schema/binding.md +2 -2
- package/reference/schema/built-in-codecs/avro.md +2 -2
- package/reference/schema/built-in-codecs/bson.md +50 -20
- package/reference/schema/built-in-codecs/csv.md +2 -2
- package/reference/schema/built-in-codecs/index.md +3 -3
- package/reference/schema/built-in-codecs/json/index.md +2 -2
- package/reference/schema/built-in-codecs/json/json.md +1 -0
- package/reference/schema/built-in-codecs/messagepack.md +3 -3
- package/reference/schema/built-in-codecs/thrift.md +2 -2
- package/reference/schema/built-in-codecs/toon.md +3 -3
- package/reference/schema/built-in-codecs/yaml.md +2 -2
- package/reference/schema/codec.md +11 -11
- package/reference/schema/dynamic-optic.md +48 -3
- package/reference/schema/dynamic-schema.md +3 -3
- package/reference/schema/index.md +2 -0
- package/reference/schema/path-interpolator.md +2 -0
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/schema/schema-evolution/as.md +4 -4
- package/reference/schema/schema-evolution/into.md +2 -2
- package/reference/schema/schema-expr.md +2 -2
- package/reference/schema/schema-search.md +263 -0
- package/reference/schema/schema.md +10 -2
- package/reference/schema/type-class-derivation.md +1 -1
- package/reference/smithy.md +502 -3
- package/reference/sql/db-codec-deriver.md +3 -3
- package/reference/sql/db-codec.md +22 -22
- package/reference/sql/db-con.md +4 -4
- package/reference/sql/db-connection.md +1 -1
- package/reference/sql/db-param.md +1 -1
- package/reference/sql/db-result-reader.md +4 -2
- package/reference/sql/db-tx.md +46 -14
- package/reference/sql/ddl.md +1 -1
- package/reference/sql/frag.md +44 -10
- package/reference/sql/index.md +7 -7
- package/reference/sql/repo.md +15 -15
- package/reference/sql/sql-dialect.md +1 -1
- package/reference/sql/sql-logger.md +1 -1
- package/reference/sql/sql-name-mapper.md +3 -3
- package/reference/sql/table-metadata.md +3 -3
- package/reference/sql/table.md +10 -10
- package/reference/sql/transactor-zio.md +1 -1
- package/reference/sql/transactor.md +21 -11
- package/reference/sql-zio.md +2 -2
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
- package/reference/streams/{sink.md → core/sink.md} +331 -353
- package/reference/streams/{stream.md → core/stream.md} +919 -209
- 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 +140 -67
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/{writer.md → primitives/writer.md} +254 -98
- 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 +0 -64
- package/sidebars.js +365 -185
- package/undocumented-report.md +528 -270
- package/reference/config.md +0 -158
- package/reference/streams/concurrent-operators.md +0 -106
- package/reference/streams/reader.md +0 -1284
- package/reference/streams/scala-2-compatibility.md +0 -55
- package/reference/streams/zero-boxing.md +0 -275
- package/reference/telemetry.md +0 -693
package/reference/async.md
CHANGED
|
@@ -1,651 +1,1499 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: async
|
|
3
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"
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
effect
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.56"
|
|
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.56"
|
|
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.
|
|
13
52
|
|
|
14
53
|
## Overview
|
|
15
54
|
|
|
16
|
-
`Async[A]` is an
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
The heart of the demo is straight-line code over suspending steps — no
|
|
61
|
-
callback nesting, no manual `flatMap` chains:
|
|
62
|
-
|
|
63
|
-
```scala
|
|
64
|
-
def fulfill(orderId: Int): Async[Shipment] = Async.async {
|
|
65
|
-
val order = fetchOrder(orderId).await
|
|
66
|
-
val lines = order.items.map { item =>
|
|
67
|
-
val stock = stockFor(item.sku).await
|
|
68
|
-
if (stock.onHand < item.qty)
|
|
69
|
-
throw new IllegalStateException(s"short ${item.sku}")
|
|
70
|
-
(item.sku, item.qty)
|
|
71
|
-
}
|
|
72
|
-
Shipment(orderId, lines, carrier = "zio-blocks-express")
|
|
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
|
|
73
99
|
}
|
|
74
100
|
```
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
78
117
|
|
|
79
|
-
|
|
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:
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
83
148
|
|
|
84
149
|
```scala
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
90
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
|
+
}
|
|
91
165
|
```
|
|
92
166
|
|
|
93
|
-
|
|
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.
|
|
94
168
|
|
|
95
|
-
|
|
96
|
-
foreign runtime — implement [[Pollable]] and return it from `flatMap` to
|
|
97
|
-
**sequence** it, or store it via `Async.succeed` / `map` to keep it as a
|
|
98
|
-
**value** (the runtime wraps pollable success values so combinators never
|
|
99
|
-
mistake them for suspended computations; note that the top-level drivers —
|
|
100
|
-
`.block`, `Async.start`, and the interop converters — do drive a directly
|
|
101
|
-
stored pollable for its effects at delivery, settling to the pollable itself).
|
|
102
|
-
The showcase includes a `Delayed` pollable that becomes ready after a few
|
|
103
|
-
scheduler ticks.
|
|
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.
|
|
104
170
|
|
|
105
|
-
|
|
106
|
-
[`AsyncShowcaseExample.scala`](https://github.com/zio/zio-blocks/blob/main/async-examples/src/main/scala/async/AsyncShowcaseExample.scala)
|
|
107
|
-
for the full program.
|
|
171
|
+
In all of this `Async` sits beside `scala.concurrent.Future`, which is also eager, rather than beside cats-effect `IO` or ZIO.
|
|
108
172
|
|
|
109
|
-
|
|
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
|
+
```
|
|
110
269
|
|
|
111
|
-
|
|
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`:
|
|
112
271
|
|
|
113
|
-
```sbt
|
|
114
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-async" % "0.0.51"
|
|
115
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
|
+
}
|
|
116
325
|
|
|
117
|
-
|
|
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
|
+
))
|
|
118
331
|
|
|
119
|
-
|
|
120
|
-
|
|
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)
|
|
121
334
|
```
|
|
122
335
|
|
|
123
|
-
|
|
124
|
-
direct-style `Async.async` block is rewritten by dotty-cps-async on Scala 3
|
|
125
|
-
(JVM and older Scala 3 JS), by a hybrid backend on Scala 3.8+ JS (native
|
|
126
|
-
`js.async` / `js.await` for direct-position awaits, with the dotty-cps-async
|
|
127
|
-
transform as fallback for awaits inside closures, by-name arguments, or nested
|
|
128
|
-
methods), and by a hand-written macro on Scala 2.
|
|
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*.
|
|
129
337
|
|
|
130
|
-
##
|
|
338
|
+
## Operations
|
|
131
339
|
|
|
132
|
-
|
|
133
|
-
|
|
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:
|
|
134
343
|
|
|
135
344
|
```scala
|
|
136
345
|
import zio.blocks.async._
|
|
137
346
|
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
140
357
|
|
|
141
|
-
|
|
142
|
-
// failed: Async[Nothing] = zio.blocks.async.Failure@12260721
|
|
358
|
+
### Creating Values
|
|
143
359
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
146
379
|
```
|
|
147
380
|
|
|
148
|
-
`Async.
|
|
149
|
-
the first failure:
|
|
381
|
+
`Async.succeed` lifts a pure, immediately-available value into an `Async[A]`:
|
|
150
382
|
|
|
151
383
|
```scala
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
384
|
+
import zio.blocks.async._
|
|
385
|
+
|
|
386
|
+
val ready: Async[Int] = Async.succeed(42)
|
|
387
|
+
val result: Int = ready.block // => 42
|
|
155
388
|
```
|
|
156
389
|
|
|
157
|
-
|
|
390
|
+
`Async.fail` creates a terminal failure; [`Failure`](#failure) covers how it short-circuits the rest of a chain:
|
|
158
391
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
```
|
|
162
398
|
|
|
163
|
-
|
|
164
|
-
runs its setup block now; `Async.async { ... }` runs its synchronous prefix
|
|
165
|
-
(and any **ready** `.await`s) now; `succeed(x).map(f)` runs `f` now. Only a
|
|
166
|
-
combinator applied to an **already-suspended** value defers — its function
|
|
167
|
-
runs when the value is later driven.
|
|
168
|
-
- The single genuinely-lazy primitive is a custom [`Pollable`](#low-level-building-blocks-pollable):
|
|
169
|
-
its `poll` runs only when a driver asks for the value. Suspension exists only
|
|
170
|
-
*downstream of* an unresolved `poll`.
|
|
399
|
+
`Async.attempt` captures a by-name expression and converts any thrown `Throwable` into a failure:
|
|
171
400
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
```
|
|
177
408
|
|
|
178
|
-
|
|
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).
|
|
179
410
|
|
|
180
|
-
|
|
181
|
-
not-yet-complete value), what advances it follows each platform's *fastest*
|
|
182
|
-
suspension mechanism, so the two platforms diverge:
|
|
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:
|
|
183
412
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
pending suspension runs only when an external driver polls it — `.block`,
|
|
187
|
-
`fa.start`, or an interop runner (`toFuture` / `unsafeRunAsync`). A built-but-
|
|
188
|
-
never-driven block leaves that continuation un-run.
|
|
189
|
-
- **Scala.js on Scala 3.8+:** `Async.async`/`.await` compile to native
|
|
190
|
-
`js.async` / `js.await` — a real JavaScript async function whose driver *is*
|
|
191
|
-
the event loop. Once the awaited value settles, the continuation self-resumes
|
|
192
|
-
off the microtask queue even if nothing polls the `Async`. This is the same
|
|
193
|
-
event-loop driving that makes await-heavy direct-style blocks substantially
|
|
194
|
-
faster than the dotty-cps-async backend, so the behavior is intentional, not a
|
|
195
|
-
defect: it is the zero-cost default of the fastest JS suspension primitive.
|
|
413
|
+
```scala
|
|
414
|
+
import zio.blocks.async._
|
|
196
415
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
416
|
+
val running: Async.Running[Int] = Async.start { 42 }
|
|
417
|
+
val result: Int = running.block // => 42
|
|
418
|
+
```
|
|
200
419
|
|
|
201
|
-
|
|
420
|
+
### Transformation
|
|
202
421
|
|
|
203
|
-
|
|
204
|
-
underlying value; only a genuinely pending `Async` takes the suspended slow
|
|
205
|
-
path.
|
|
422
|
+
Pure transformations apply a function to the success value and return a new `Async`:
|
|
206
423
|
|
|
207
424
|
```scala
|
|
208
|
-
|
|
209
|
-
|
|
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:
|
|
210
439
|
|
|
211
|
-
|
|
212
|
-
|
|
440
|
+
```scala
|
|
441
|
+
import zio.blocks.async._
|
|
213
442
|
|
|
214
|
-
val
|
|
215
|
-
Async.
|
|
216
|
-
|
|
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"
|
|
217
448
|
```
|
|
218
449
|
|
|
219
|
-
###
|
|
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
|
+
```
|
|
220
465
|
|
|
221
|
-
`zip`
|
|
222
|
-
`Tuples` combiner, so chained zips flatten automatically (`a zip b zip c`
|
|
223
|
-
yields `Async[(A, B, C)]`, not `Async[((A, B), C)]`). Use `zipWith` to combine
|
|
224
|
-
with an explicit function:
|
|
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):
|
|
225
467
|
|
|
226
468
|
```scala
|
|
227
|
-
|
|
228
|
-
Async.succeed(1).zip(Async.succeed("two"))
|
|
229
|
-
// zipped: Async[Tuple2[Int, String]] = (1, "two")
|
|
469
|
+
import zio.blocks.async._
|
|
230
470
|
|
|
231
|
-
val
|
|
471
|
+
val combined: Async[Int] =
|
|
232
472
|
Async.succeed(3).zipWith(Async.succeed(4))(_ + _)
|
|
233
|
-
|
|
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
|
+
}
|
|
234
499
|
```
|
|
235
500
|
|
|
236
|
-
|
|
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
|
+
```
|
|
237
510
|
|
|
238
|
-
`
|
|
239
|
-
falls back to another `Async`, and `either` reifies the outcome:
|
|
511
|
+
`foldCause` handles both the success and failure branches in a single call without allocating a recovery `Async`:
|
|
240
512
|
|
|
241
513
|
```scala
|
|
242
|
-
|
|
243
|
-
Async.fail(new RuntimeException("x")).either
|
|
244
|
-
// asEither: Async[Either[Throwable, Int]] = Left(
|
|
245
|
-
// java.lang.RuntimeException: x
|
|
246
|
-
// )
|
|
514
|
+
import zio.blocks.async._
|
|
247
515
|
|
|
248
|
-
val
|
|
249
|
-
Async.
|
|
250
|
-
|
|
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"
|
|
251
523
|
```
|
|
252
524
|
|
|
253
|
-
###
|
|
525
|
+
### Driving
|
|
254
526
|
|
|
255
|
-
|
|
256
|
-
value. `Async.never` is an `Async` that never completes — useful as a sentinel:
|
|
527
|
+
Driving settles whatever part of an `Async[A]` is still waiting, and delivers the result through one of three mechanisms:
|
|
257
528
|
|
|
258
529
|
```scala
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
```
|
|
261
540
|
|
|
262
|
-
|
|
263
|
-
// skipped: Async[Unit] = ()
|
|
541
|
+
`block` parks the calling thread until the computation settles, then returns the value or re-throws the underlying `Throwable`:
|
|
264
542
|
|
|
265
|
-
|
|
266
|
-
|
|
543
|
+
```scala
|
|
544
|
+
import zio.blocks.async._
|
|
545
|
+
|
|
546
|
+
val result: Int = Async.succeed(42).map(_ + 1).block // => 43
|
|
267
547
|
```
|
|
268
548
|
|
|
269
|
-
|
|
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`.
|
|
270
552
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
on the happy path. `.await` is **lexically restricted** to `Async.async`
|
|
275
|
-
blocks; using it elsewhere is a compile error.
|
|
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.
|
|
276
556
|
|
|
277
557
|
```scala
|
|
278
|
-
|
|
279
|
-
def loadOrders(user: String): Async[List[String]] = Async.succeed(List(s"$user-order"))
|
|
558
|
+
import zio.blocks.async._
|
|
280
559
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
orders.size
|
|
286
|
-
}
|
|
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
|
|
287
564
|
```
|
|
288
565
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
- **`List.foreach`** is **lazy / sequential**: the closure for element `n+1` runs
|
|
310
|
-
only after element `n`'s `.await` completes successfully, and a failed await
|
|
311
|
-
short-circuits the remaining elements. The result is `Unit`.
|
|
312
|
-
- **`List.flatMap`** is **lazy / sequential** like `foreach`, but accumulates each
|
|
313
|
-
closure's `IterableOnce` into the result `List`.
|
|
314
|
-
- **`Option.map` / `Option.flatMap` / `Option.foreach`**: an `Option` holds at
|
|
315
|
-
most one element, so the eager/lazy distinction collapses to a single
|
|
316
|
-
`Some`/`None` branch — `None` short-circuits (the closure never runs), `Some(x)`
|
|
317
|
-
runs the closure and (for `map`/`flatMap`) rewraps the result; a failed await
|
|
318
|
-
propagates.
|
|
319
|
-
- **`Vector` / immutable `Set` / immutable `Queue` / immutable `ArraySeq`**
|
|
320
|
-
(`map` / `flatMap` / `foreach`, plus the builder-backed methods below): **lazy
|
|
321
|
-
/ sequential** like `List.foreach` (the closure for element `n+1` runs only
|
|
322
|
-
after element `n`'s await completes; a failure short-circuits the rest). Note
|
|
323
|
-
that `Vector.map` / `Queue.map` / `ArraySeq.map` are lazy — only `List.map` is
|
|
324
|
-
eager (it is the special case backed by dotty-cps-async's `ListAsyncShift`).
|
|
325
|
-
The result **collection type is preserved** (`Vector.map` → `Vector`,
|
|
326
|
-
`Queue.map` → `Queue`, `ArraySeq.map` → `ArraySeq`, `Set.map` → `Set`); for
|
|
327
|
-
`Set`, the *awaited* values are deduplicated.
|
|
328
|
-
- **`Array`** (`map` / `flatMap` / `foreach` / `filter` / `takeWhile` /
|
|
329
|
-
`dropWhile` / `foldLeft` / `collect` / `find` / `exists` / `forall`): the
|
|
330
|
-
result is always an `Array[B]` with the **element type preserved, including
|
|
331
|
-
primitives** (e.g. `Array[Int].map(_.toLong)` → a primitive `Array[Long]`).
|
|
332
|
-
`Array.map` is **eager** like `List.map` (a failing await still runs every
|
|
333
|
-
preceding closure); `Array.flatMap` and the rest are **lazy / sequential**.
|
|
334
|
-
The result-building HOFs (`map` / `flatMap` / `filter` / `takeWhile` /
|
|
335
|
-
`collect`) rebuild via `Array.newBuilder`, which needs a `ClassTag[B]` — the
|
|
336
|
-
same one the user's own `Array` HOF already required, so it resolves for any
|
|
337
|
-
concrete element type (an abstract/path-dependent `B` without a `ClassTag` in
|
|
338
|
-
scope is not supported, exactly as the standard-library call would not be).
|
|
339
|
-
- **immutable `Map`** (`map` / `flatMap` / `foreach`): **lazy / sequential** over
|
|
340
|
-
the map's `(K, V)` entries. A pair-returning `map`/`flatMap` rebuilds a
|
|
341
|
-
`Map[K2, V2]` (later entries with the same key win); a non-pair `map`/`flatMap`
|
|
342
|
-
widens the result to an `Iterable`, matching the standard library's overload
|
|
343
|
-
choice. `foreach` runs the closure for each entry, returning `Unit`.
|
|
344
|
-
- **Short-circuiting predicate scans** (`find` / `exists` / `forall`, predicate
|
|
345
|
-
`A => Boolean`): **lazy / sequential** over any whitelisted receiver — the
|
|
346
|
-
predicate for element `n+1` runs only after element `n`'s await completes, and
|
|
347
|
-
the scan stops at the first decisive element (`exists` → first `true`; `forall`
|
|
348
|
-
→ first `false`; `find` → first matching element as `Some`, else `None`).
|
|
349
|
-
`Option.find` is covered on every cell too — on Scala 2 it resolves via the
|
|
350
|
-
`Option`→`Iterable` implicit conversion, which the macro recognizes specifically
|
|
351
|
-
for `find`.
|
|
352
|
-
- **`foldLeft`** (op `(B, A) => B`): **lazy / sequential** over any whitelisted
|
|
353
|
-
receiver via `.iterator` — a left fold is inherently sequential (element
|
|
354
|
-
`n+1`'s op needs `n`'s accumulator), so the op for element `n+1` runs only
|
|
355
|
-
after element `n`'s await completes, and a failed await short-circuits the
|
|
356
|
-
rest. The accumulator is threaded through and `foldLeft[B]` returns `B`
|
|
357
|
-
directly (it may differ from the element type), so awaits in the initial
|
|
358
|
-
accumulator are sequenced before the fold.
|
|
359
|
-
- **`reduce` / `reduceLeft`** (op `(B, A) => B`): **lazy / sequential** over any
|
|
360
|
-
whitelisted receiver via `.iterator` — `foldLeft` seeded by the FIRST element
|
|
361
|
-
instead of an initial value, so the op for element `n+1` runs only after
|
|
362
|
-
element `n`'s await completes and a failed await short-circuits the rest. A
|
|
363
|
-
single-element receiver returns that element without running the op; an EMPTY
|
|
364
|
-
receiver fails with `UnsupportedOperationException` (catchable via `catchAll`,
|
|
365
|
-
rethrown by `.block`).
|
|
366
|
-
- **`foldRight`** (op `(A, B) => B`): **lazy / sequential** but
|
|
367
|
-
**right-associative** — `op(x1, op(x2, ..., op(xn, z)))` — so the op for the
|
|
368
|
-
RIGHTMOST element runs first (the receiver is materialized and drained in
|
|
369
|
-
reverse to keep the await-ordering correct). An empty receiver yields the
|
|
370
|
-
initial accumulator (the op never runs); a failed await short-circuits the
|
|
371
|
-
remaining (right-to-left) elements.
|
|
372
|
-
- **`filter` / `filterNot`** (predicate `A => Boolean`): **lazy / sequential**
|
|
373
|
-
over a `List` / `Vector` / `Array` / immutable `Set` / immutable `Queue` /
|
|
374
|
-
immutable `ArraySeq` / `Option` — the predicate for element `n+1` runs only
|
|
375
|
-
after element `n`'s await completes, and a failed await short-circuits the
|
|
376
|
-
rest. The result **collection type is preserved** (`filter` keeps elements
|
|
377
|
-
whose predicate is `true`, `filterNot` those whose predicate is `false`).
|
|
378
|
-
**Divergence:** `Map.filter` / `Map.filterNot` with `.await` is a
|
|
379
|
-
**Scala-2-only superset** — dotty-cps-async has no working `MapOpsAsyncShift.filter`
|
|
380
|
-
and rejects it on Scala 3.
|
|
381
|
-
- **`takeWhile` / `dropWhile`** (predicate `A => Boolean`): **lazy / sequential**
|
|
382
|
-
over an ordered receiver (`List` / `Vector` / immutable `Queue` / immutable
|
|
383
|
-
`ArraySeq` / `Array`) — these are **prefix-ordered**, so the predicate for
|
|
384
|
-
element `n+1` runs only after element `n`'s await completes, and the FIRST
|
|
385
|
-
element whose predicate is `false` decides the boundary (`takeWhile` keeps the
|
|
386
|
-
leading run and discards it and the rest; `dropWhile` drops the leading run and
|
|
387
|
-
keeps it and the rest **unconditionally**, never re-evaluating the predicate).
|
|
388
|
-
A failed await short-circuits the rest. The result **collection type is
|
|
389
|
-
preserved**. They are restricted to ordered receivers because a leading-prefix
|
|
390
|
-
predicate is ill-defined on an unordered `Set` / `Map` (and `Option` does not
|
|
391
|
-
provide them); the Scala 2 macro rejects those with an actionable compile
|
|
392
|
-
error.
|
|
393
|
-
- **`collect`** (partial function `{ case ... }`): **lazy / sequential** over a
|
|
394
|
-
`List` / `Vector` / `Array` / immutable `Set` / immutable `Queue` / immutable
|
|
395
|
-
`ArraySeq` — keeps the elements the partial function is defined at, mapping
|
|
396
|
-
each through its (awaiting) case body; the case for element `n+1` runs only
|
|
397
|
-
after element `n`'s await completes, and a failed await short-circuits the
|
|
398
|
-
rest. The result **collection type is preserved**. An `Option` receiver is
|
|
399
|
-
supported too: `None` short-circuits without evaluating the partial function,
|
|
400
|
-
`Some(a)` yields `Some(b)` if a case matches, else `None`. A **non-pair
|
|
401
|
-
`Map.collect`** (whose case bodies yield a `B`, so the result is an
|
|
402
|
-
`Iterable[B]`) is supported on every cell. The case guard runs exactly once per
|
|
403
|
-
element (Scala 2). A `.await` in a case GUARD is rejected.
|
|
404
|
-
**Divergence:** a **pair-yielding `Map.collect`** (whose case bodies yield
|
|
405
|
-
`(K2, V2)` pairs, so the result is a `Map[K2, V2]`) is **unsupported on every
|
|
406
|
-
cell** — dotty-cps-async has only an `IterableOpsAsyncShift.collect[F, B]`
|
|
407
|
-
shift (no Map-specific one), so the `Map`-returning overload is a compile error
|
|
408
|
-
on Scala 3, and the Scala 2 macro rejects it to stay at parity. Rewrite it as
|
|
409
|
-
`m.toVector.collect { case ... => k -> v.await }.toMap`.
|
|
410
|
-
|
|
411
|
-
These behave identically across Scala 2/3 and JVM/JS **except** for the handful of
|
|
412
|
-
positions flagged **Divergence** above (`Map.filter` / `filterNot` is a
|
|
413
|
-
Scala-2-only superset; a pair-yielding `Map.collect` is unsupported everywhere).
|
|
414
|
-
Because Scala desugars
|
|
415
|
-
for-comprehensions over a `List` / `Option` / `Vector` / `Set` / `Map` into these
|
|
416
|
-
methods,
|
|
417
|
-
single- and multi-generator `for` comprehensions with `.await` work too
|
|
418
|
-
(`for ... yield` → `map`; nested generators → `flatMap`/`map`; `for { ... }`
|
|
419
|
-
without `yield` → `foreach`; a guard `if` → `withFilter`):
|
|
420
|
-
|
|
421
|
-
```scala
|
|
422
|
-
val pairs: Async[List[Int]] = Async.async {
|
|
423
|
-
for {
|
|
424
|
-
i <- List(1, 2)
|
|
425
|
-
j <- List(10, 20)
|
|
426
|
-
} yield Async.succeed(i + j).await
|
|
427
|
-
} // List(11, 21, 12, 22)
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
> **Scala 2 limitation (current):** the Scala 2 macro supports `.await` in
|
|
431
|
-
> sequential statements, `if` / `match` / `while` / `try`-`catch`-`finally`,
|
|
432
|
-
> `throw`, assignments, `List` / `Option` / `Vector` / `Array` / immutable `Set` /
|
|
433
|
-
> immutable `Queue` / immutable `ArraySeq` / immutable
|
|
434
|
-
> `Map` `map` / `foreach` / `flatMap` closures, the short-circuiting predicate
|
|
435
|
-
> scans `find` / `exists` / `forall`, `filter` / `filterNot`, `foldLeft`,
|
|
436
|
-
> `foldRight`, and `reduce` / `reduceLeft` over
|
|
437
|
-
> those receivers, the prefix-ordered `takeWhile` / `dropWhile` over ordered
|
|
438
|
-
> receivers (`List` / `Vector` / immutable `Queue` / immutable `ArraySeq` /
|
|
439
|
-
> `Array`), `collect` over builder-backed receivers (`List` / `Vector` / `Array`
|
|
440
|
-
> / immutable `Set` / immutable `Queue` / immutable `ArraySeq`), and the
|
|
441
|
-
> for-comprehensions that desugar to the former (including guards), but **rejects**
|
|
442
|
-
> `.await` inside other function
|
|
443
|
-
> literals / higher-order-function arguments (and HOFs over collections other than
|
|
444
|
-
> those whitelisted families), with an actionable compile error. Those positions
|
|
445
|
-
> are supported on Scala 3. The whitelisted set above is the **final, stable**
|
|
446
|
-
> Scala 2 contract for the standard strict collections; positions outside it are
|
|
447
|
-
> intentionally unsupported on Scala 2 (a custom collection, or `.await` inside an
|
|
448
|
-
> arbitrary user lambda passed to a third-party HOF, cannot be rewritten without a
|
|
449
|
-
> shift typeclass the Scala 2 macro deliberately does not depend on).
|
|
450
|
-
>
|
|
451
|
-
> Conversely, the Scala 2 macro is a strict superset for some guard shapes that
|
|
452
|
-
> dotty-cps-async on Scala 3 currently rejects: *multiple* `List`
|
|
453
|
-
> for-comprehension guards (chained `withFilter`), and *any* `Option`
|
|
454
|
-
> for-comprehension guard (DCA has no `AsyncShift[Option#WithFilter]`). Single
|
|
455
|
-
> `List` guards behave identically on every cell.
|
|
456
|
-
|
|
457
|
-
## The callback bridge: `Async.promise`
|
|
458
|
-
|
|
459
|
-
`Async.promise` builds an `Async` from a callback-style API. You receive a
|
|
460
|
-
`Completer` and call `succeed` / `fail` when the result arrives. Completion is
|
|
461
|
-
one-shot — the first `succeed` or `fail` wins; later calls are silent no-ops. If
|
|
462
|
-
the body completes the completer synchronously, the result collapses to a bare
|
|
463
|
-
value with no `Pollable` allocation.
|
|
464
|
-
|
|
465
|
-
On Scala 3 the completer is supplied via a context function, so you can call the
|
|
466
|
-
top-level `succeed` / `fail` helpers directly:
|
|
467
|
-
|
|
468
|
-
```scala
|
|
469
|
-
import zio.blocks.async._
|
|
470
|
-
|
|
471
|
-
val fromCallback: Async[Int] =
|
|
472
|
-
Async.promise[Int] {
|
|
473
|
-
// register a callback with some external system, then:
|
|
474
|
-
succeed(42)
|
|
475
|
-
}
|
|
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")))
|
|
476
586
|
```
|
|
477
587
|
|
|
478
|
-
|
|
479
|
-
|
|
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:
|
|
480
595
|
|
|
481
596
|
```scala
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
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")
|
|
485
601
|
```
|
|
486
602
|
|
|
487
|
-
|
|
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
|
+
```
|
|
488
624
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
on JS, where the platform cannot block. Use `.block` only at the edge of your
|
|
492
|
-
program, never on a scheduler/reactor thread.
|
|
625
|
+
</TabItem>
|
|
626
|
+
<TabItem value="scala3" label="Scala 3">
|
|
493
627
|
|
|
494
628
|
```scala
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
497
641
|
```
|
|
498
642
|
|
|
499
|
-
|
|
643
|
+
</TabItem>
|
|
644
|
+
</Tabs>
|
|
500
645
|
|
|
501
|
-
The `
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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:
|
|
505
653
|
|
|
506
654
|
```scala
|
|
507
655
|
import zio.blocks.async._
|
|
508
656
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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"))
|
|
514
664
|
|
|
515
|
-
|
|
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
|
|
516
673
|
```
|
|
517
674
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
```
|
|
525
690
|
|
|
526
|
-
|
|
527
|
-
on the calling thread. For a suspended `Async`, driving proceeds on a daemon
|
|
528
|
-
worker thread on the JVM, or via microtasks on Scala.js. `cancel()` is
|
|
529
|
-
driver-level only: it stops the poll loop and suppresses publishing a terminal
|
|
530
|
-
value, but does not abort an in-flight leaf (socket, timer, JS promise).
|
|
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.
|
|
531
692
|
|
|
532
|
-
|
|
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.
|
|
533
694
|
|
|
534
|
-
|
|
535
|
-
the `Running` handle** — `Running` publishes its outcome through an atomic, so
|
|
536
|
-
the underlying `Async` (and any side effects in `map`/`flatMap`/`tap`) is driven
|
|
537
|
-
exactly once no matter how many consumers poll, block, or compose on the handle:
|
|
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:
|
|
538
696
|
|
|
539
697
|
```scala
|
|
540
698
|
import zio.blocks.async._
|
|
541
699
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
val
|
|
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
|
+
}
|
|
545
705
|
```
|
|
546
706
|
|
|
547
|
-
|
|
548
|
-
separate `fa.start`s on the same `fa`, or `fa.start` racing `fa.block`). On the
|
|
549
|
-
JVM that polls the same combinator concurrently, which is **undefined**: a
|
|
550
|
-
`map`/`flatMap`/`tap` function may run more than once, and a `collectAll` batch
|
|
551
|
-
may observe its drain buffer mid-update. This matches `Pollable`'s contract that
|
|
552
|
-
re-polling a settled value is undefined and platform-specific — it cannot arise
|
|
553
|
-
on single-threaded Scala.js. Sequential re-use (re-polling or composing after an
|
|
554
|
-
earlier drive has settled) is fine; only *concurrent* re-driving of the raw
|
|
555
|
-
value is not.
|
|
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.
|
|
556
708
|
|
|
557
|
-
|
|
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.
|
|
558
710
|
|
|
559
|
-
`
|
|
560
|
-
both success and failure. Ingress lives on the `Async` companion
|
|
561
|
-
(`Async.fromFuture`, `Async.fromCompletionStage`, `Async.fromJsPromise`) and
|
|
562
|
-
egress lives on extension methods (`fa.toFuture`, `fa.toCompletableFuture`,
|
|
563
|
-
`fa.toJsPromise`). `scala.concurrent.Future` conversion is available on both
|
|
564
|
-
platforms; the JVM additionally offers Java `CompletionStage` /
|
|
565
|
-
`CompletableFuture`, and Scala.js offers `js.Promise`.
|
|
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`.
|
|
566
712
|
|
|
567
|
-
|
|
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:
|
|
568
720
|
|
|
569
721
|
```scala
|
|
570
722
|
import zio.blocks.async._
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
+
```
|
|
646
1493
|
|
|
647
1494
|
## See Also
|
|
648
1495
|
|
|
649
|
-
- [
|
|
650
|
-
- [
|
|
651
|
-
|
|
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
|