@zio.dev/zio-blocks 0.0.33 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: async
|
|
3
|
+
title: "Async"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The `async` module provides `Async[A]`, a lightweight, zero-dependency
|
|
7
|
+
asynchronous effect type for modern Scala. It is designed around a single idea:
|
|
8
|
+
**a ready `Async[A]` is just an `A`**. The happy path allocates nothing — no
|
|
9
|
+
effect tree, no wrapper, no boxing beyond what a generic JVM method already
|
|
10
|
+
requires — so synchronous code composed with `map` / `flatMap` runs at
|
|
11
|
+
hand-written speed while still being able to suspend on genuinely asynchronous
|
|
12
|
+
work.
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
`Async[A]` is an opaque type whose ready representation is the value itself and
|
|
17
|
+
whose pending representation is a `Pollable[A]`. You never construct it
|
|
18
|
+
directly; you enter the type through constructors and transform it through
|
|
19
|
+
extension methods:
|
|
20
|
+
|
|
21
|
+
- **Constructors** — `Async.succeed`, `Async.fail`, `Async.attempt`,
|
|
22
|
+
`Async.never`, `Async.collectAll`, and the callback bridge `Async.promise`.
|
|
23
|
+
- **Transformers** — `map`, `flatMap`, `zip`, `zipWith`, `catchAll`,
|
|
24
|
+
`mapError`, `orElse`, `foldCause`, `either`, `tap`, `ensuring`, `as`, `unit`,
|
|
25
|
+
`*>`, `<*`, `flatten`, and the conditional helpers `when` / `unless`.
|
|
26
|
+
- **Direct style** — `Async.async { ... .await ... }` lets you write
|
|
27
|
+
straight-line code with `.await`, rewritten at compile time into a
|
|
28
|
+
non-blocking `flatMap` chain.
|
|
29
|
+
- **Running** — `.block` drives an `Async` to its value (blocking on the JVM,
|
|
30
|
+
throwing on a genuinely pending value on JS).
|
|
31
|
+
- **Interop** — conversions to and from `scala.concurrent.Future` on every
|
|
32
|
+
platform, Java's `CompletionStage` / `CompletableFuture` on the JVM, and
|
|
33
|
+
`js.Promise` on Scala.js.
|
|
34
|
+
|
|
35
|
+
On Scala 3 the transformers are zero-cost `inline` extension methods (the ready
|
|
36
|
+
path applies your function directly to the underlying value with no `Function1`
|
|
37
|
+
allocation). On Scala 2 they are methods on an implicit `AsyncOps` class. The
|
|
38
|
+
raw-value representation — a ready `Async[A]` *is* an `A` — holds identically on
|
|
39
|
+
both.
|
|
40
|
+
|
|
41
|
+
## Runnable example
|
|
42
|
+
|
|
43
|
+
The [`async-examples`](https://github.com/zio/zio-blocks/tree/main/async-examples)
|
|
44
|
+
module contains a single self-contained program that walks through the major
|
|
45
|
+
features in one file — ready-path composition, direct-style `Async.async` /
|
|
46
|
+
`.await`, `zip` / `collectAll`, error handling, `Async.promise`, a custom
|
|
47
|
+
[[Pollable]] leaf, `tap` / `ensuring`, cancellable `Async.start` /
|
|
48
|
+
`Async.Running`, and JVM `Future` interop.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sbt "++3.8.3; async-examples/run"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The program models a small order-fulfillment pipeline: fetch a user and order,
|
|
55
|
+
check warehouse stock, pack a shipment, and audit the steps. The structure is
|
|
56
|
+
intentionally linear so you can read it top-to-bottom as a tutorial.
|
|
57
|
+
|
|
58
|
+
### Direct-style fulfillment
|
|
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")
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A failure from any `.await` short-circuits the block as a failed `Async`, the
|
|
77
|
+
same as throwing inside synchronous code.
|
|
78
|
+
|
|
79
|
+
### Callback bridge
|
|
80
|
+
|
|
81
|
+
Legacy APIs that take success/error callbacks lift cleanly through
|
|
82
|
+
`Async.promise` (Scala 3 context-function style):
|
|
83
|
+
|
|
84
|
+
```scala
|
|
85
|
+
val json: Async[String] =
|
|
86
|
+
Async.promise[String] {
|
|
87
|
+
// Capture the completer — nested callbacks do not inherit the `?=>` context.
|
|
88
|
+
val completer = summon[Completer[String]]
|
|
89
|
+
legacyHttpGet("/users/42", completer)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Custom asynchronous leaves
|
|
94
|
+
|
|
95
|
+
When you need a bespoke source of suspension — a socket read, a timer, a
|
|
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.
|
|
104
|
+
|
|
105
|
+
See
|
|
106
|
+
[`AsyncShowcaseExample.scala`](https://github.com/zio/zio-blocks/blob/main/async-examples/src/main/scala/async/AsyncShowcaseExample.scala)
|
|
107
|
+
for the full program.
|
|
108
|
+
|
|
109
|
+
## Installation
|
|
110
|
+
|
|
111
|
+
Add the following to your `build.sbt`:
|
|
112
|
+
|
|
113
|
+
```sbt
|
|
114
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-async" % "0.0.51"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For cross-platform projects (Scala.js):
|
|
118
|
+
|
|
119
|
+
```sbt
|
|
120
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-async" % "0.0.51"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Supported platforms: JVM and Scala.js, Scala 2.13 and Scala 3.x. The
|
|
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.
|
|
129
|
+
|
|
130
|
+
## Constructors
|
|
131
|
+
|
|
132
|
+
`Async.succeed` lifts a pure value; `Async.fail` lifts an error; `Async.attempt`
|
|
133
|
+
catches a thrown exception and turns it into a failure.
|
|
134
|
+
|
|
135
|
+
```scala
|
|
136
|
+
import zio.blocks.async._
|
|
137
|
+
|
|
138
|
+
val ready: Async[Int] = Async.succeed(42)
|
|
139
|
+
// ready: Async[Int] = 42
|
|
140
|
+
|
|
141
|
+
val failed: Async[Nothing] = Async.fail(new RuntimeException("boom"))
|
|
142
|
+
// failed: Async[Nothing] = zio.blocks.async.Failure@12260721
|
|
143
|
+
|
|
144
|
+
val caught: Async[Int] = Async.attempt(Integer.parseInt("123"))
|
|
145
|
+
// caught: Async[Int] = 123
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`Async.collectAll` sequences a collection of `Async` values, short-circuiting on
|
|
149
|
+
the first failure:
|
|
150
|
+
|
|
151
|
+
```scala
|
|
152
|
+
val all: Async[List[Int]] =
|
|
153
|
+
Async.collectAll(List(Async.succeed(1), Async.succeed(2), Async.succeed(3)))
|
|
154
|
+
// all: Async[List[Int]] = List(1, 2, 3)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Evaluation model: eager up to suspension
|
|
158
|
+
|
|
159
|
+
`Async` is **eager**, not a lazy `IO`. Constructing an `Async` performs all of
|
|
160
|
+
its synchronous work immediately — building the value *runs* it, up to the first
|
|
161
|
+
point where it genuinely has to wait:
|
|
162
|
+
|
|
163
|
+
- `Async.attempt(body)` runs `body` now (on the calling thread); `Async.promise`
|
|
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`.
|
|
171
|
+
|
|
172
|
+
This makes the success/ready path allocation-free (no effect tree, no per-step
|
|
173
|
+
thunk) — the source of its throughput — at the cost of referential transparency
|
|
174
|
+
(building has effects) and cancel-by-drop (use [`Cancelable.cancel`](#eager-cancellable-running-asyncstart-and-asyncrunning)
|
|
175
|
+
instead). It sits next to `scala.concurrent.Future` (also eager) rather than
|
|
176
|
+
cats-effect `IO` / ZIO (lazy).
|
|
177
|
+
|
|
178
|
+
### What happens at a pending suspension differs by platform — by design
|
|
179
|
+
|
|
180
|
+
Once an `Async` hits a genuinely **pending** suspension (an await of a
|
|
181
|
+
not-yet-complete value), what advances it follows each platform's *fastest*
|
|
182
|
+
suspension mechanism, so the two platforms diverge:
|
|
183
|
+
|
|
184
|
+
- **JVM (and Scala.js on Scala 3 < 3.8, and Scala 2):** the value is a
|
|
185
|
+
poll-driven `Pollable` with no ambient driver. The continuation after the
|
|
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.
|
|
196
|
+
|
|
197
|
+
In practice this is invisible — you always drive an `Async` you build — and the
|
|
198
|
+
*value* is identical on every cell. The divergence is observable only by a block
|
|
199
|
+
that is constructed, has its awaited value settle, and is then never driven.
|
|
200
|
+
|
|
201
|
+
## Transforming values
|
|
202
|
+
|
|
203
|
+
On the ready path the transformers apply your function directly to the
|
|
204
|
+
underlying value; only a genuinely pending `Async` takes the suspended slow
|
|
205
|
+
path.
|
|
206
|
+
|
|
207
|
+
```scala
|
|
208
|
+
val mapped: Async[Int] = Async.succeed(20).map(_ + 1)
|
|
209
|
+
// mapped: Async[Int] = 21
|
|
210
|
+
|
|
211
|
+
val chained: Async[Int] = Async.succeed(20).flatMap(n => Async.succeed(n * 2))
|
|
212
|
+
// chained: Async[Int] = 40
|
|
213
|
+
|
|
214
|
+
val recovered: Async[Int] =
|
|
215
|
+
Async.fail(new RuntimeException("nope")).catchAll(_ => Async.succeed(-1))
|
|
216
|
+
// recovered: Async[Int] = -1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Combining with `zip`
|
|
220
|
+
|
|
221
|
+
`zip` fuses two `Async` values into a tuple using the `combinators` module's
|
|
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:
|
|
225
|
+
|
|
226
|
+
```scala
|
|
227
|
+
val zipped: Async[(Int, String)] =
|
|
228
|
+
Async.succeed(1).zip(Async.succeed("two"))
|
|
229
|
+
// zipped: Async[Tuple2[Int, String]] = (1, "two")
|
|
230
|
+
|
|
231
|
+
val summed: Async[Int] =
|
|
232
|
+
Async.succeed(3).zipWith(Async.succeed(4))(_ + _)
|
|
233
|
+
// summed: Async[Int] = 7
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Error handling
|
|
237
|
+
|
|
238
|
+
`catchAll` recovers a failure, `mapError` transforms the cause, `orElse`
|
|
239
|
+
falls back to another `Async`, and `either` reifies the outcome:
|
|
240
|
+
|
|
241
|
+
```scala
|
|
242
|
+
val asEither: Async[Either[Throwable, Int]] =
|
|
243
|
+
Async.fail(new RuntimeException("x")).either
|
|
244
|
+
// asEither: Async[Either[Throwable, Int]] = Left(
|
|
245
|
+
// java.lang.RuntimeException: x
|
|
246
|
+
// )
|
|
247
|
+
|
|
248
|
+
val fallback: Async[Int] =
|
|
249
|
+
Async.fail(new RuntimeException("x")).orElse(Async.succeed(0))
|
|
250
|
+
// fallback: Async[Int] = 0
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Conditional effects
|
|
254
|
+
|
|
255
|
+
`when` / `unless` run an `Async` only when a condition holds, discarding its
|
|
256
|
+
value. `Async.never` is an `Async` that never completes — useful as a sentinel:
|
|
257
|
+
|
|
258
|
+
```scala
|
|
259
|
+
val maybe: Async[Unit] = when(1 < 2)(Async.succeed(()))
|
|
260
|
+
// maybe: Async[Unit] = ()
|
|
261
|
+
|
|
262
|
+
val skipped: Async[Unit] = unless(1 < 2)(Async.succeed(()))
|
|
263
|
+
// skipped: Async[Unit] = ()
|
|
264
|
+
|
|
265
|
+
val forever: Async[Nothing] = Async.never
|
|
266
|
+
// forever: Async[Nothing] = zio.blocks.async.Async$$anon$1@65db325f
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Direct style: `Async.async` and `.await`
|
|
270
|
+
|
|
271
|
+
Inside an `Async.async { ... }` block you can write straight-line code and use
|
|
272
|
+
`.await` to extract the value of any `Async`. The block is rewritten at compile
|
|
273
|
+
time into a non-blocking `flatMap` / `map` chain — there is no thread blocking
|
|
274
|
+
on the happy path. `.await` is **lexically restricted** to `Async.async`
|
|
275
|
+
blocks; using it elsewhere is a compile error.
|
|
276
|
+
|
|
277
|
+
```scala
|
|
278
|
+
def loadUser(id: Int): Async[String] = Async.succeed(s"user-$id")
|
|
279
|
+
def loadOrders(user: String): Async[List[String]] = Async.succeed(List(s"$user-order"))
|
|
280
|
+
|
|
281
|
+
val program: Async[Int] =
|
|
282
|
+
Async.async {
|
|
283
|
+
val user = loadUser(1).await
|
|
284
|
+
val orders = loadOrders(user).await
|
|
285
|
+
orders.size
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
A failure encountered by `.await` short-circuits the block and surfaces as a
|
|
290
|
+
failed `Async`, exactly as if you had thrown — `Async.async { Async.fail(t).await }`
|
|
291
|
+
is equivalent to `Async.fail(t)`.
|
|
292
|
+
|
|
293
|
+
`.await` is also supported inside the higher-order-function closures of the
|
|
294
|
+
standard strict collections — `List`, `Option`, `Vector`, immutable `Set`,
|
|
295
|
+
immutable `Map`, `Array`, immutable `Queue`, and immutable `ArraySeq` — across a
|
|
296
|
+
broad set of methods (`map` / `foreach` / `flatMap`, the predicate scans
|
|
297
|
+
`find` / `exists` / `forall` / `filter` / `filterNot`, the folds
|
|
298
|
+
`foldLeft` / `foldRight` / `reduce` / `reduceLeft`, the prefix scans
|
|
299
|
+
`takeWhile` / `dropWhile`, and `collect`). Each is detailed below, with semantics
|
|
300
|
+
that match the method's natural meaning (and the Scala 3 backends exactly). A few
|
|
301
|
+
positions diverge between Scala 2 and Scala 3 — those are called out explicitly as
|
|
302
|
+
**Divergence** notes:
|
|
303
|
+
|
|
304
|
+
- **`List.map`** is **eager**: strict `map` applies the closure to every element
|
|
305
|
+
first — running all construction-time side effects — producing a
|
|
306
|
+
`List[Async[B]]`, and the awaits are then sequenced left-to-right via
|
|
307
|
+
`Async.collectAll` (fail-fast on the first failure). This mirrors how
|
|
308
|
+
`Array.map(async ...)` composes in JavaScript.
|
|
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
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
On Scala 2 the body receives the `Completer` explicitly; mark it `implicit` to
|
|
479
|
+
use the same top-level `succeed` / `fail` helpers, or call its methods directly:
|
|
480
|
+
|
|
481
|
+
```scala
|
|
482
|
+
// Scala 2
|
|
483
|
+
Async.promise[Int] { implicit c => succeed(42) }
|
|
484
|
+
Async.promise[Int] { c => c.succeed(42) }
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Running an `Async`
|
|
488
|
+
|
|
489
|
+
`.block` drives an `Async` to its value. A ready value returns immediately. A
|
|
490
|
+
pending value blocks the calling thread on the JVM (Loom-friendly) and throws
|
|
491
|
+
on JS, where the platform cannot block. Use `.block` only at the edge of your
|
|
492
|
+
program, never on a scheduler/reactor thread.
|
|
493
|
+
|
|
494
|
+
```scala
|
|
495
|
+
val result: Int = Async.succeed(20).map(_ + 1).block
|
|
496
|
+
// result: Int = 21
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Eager, cancellable running: `Async.start` and `Async.Running`
|
|
500
|
+
|
|
501
|
+
The `fa.start` extension eagerly drives an already-built `Async` without
|
|
502
|
+
blocking and returns a `Running[A]` handle — itself an `Async[A]` you can poll,
|
|
503
|
+
compose, or cancel. Compose with `either`, `tap`, `foldCause`, and the other
|
|
504
|
+
operators **before** `start` to observe or transform the outcome:
|
|
505
|
+
|
|
506
|
+
```scala
|
|
507
|
+
import zio.blocks.async._
|
|
508
|
+
|
|
509
|
+
val running: Async.Running[Either[Throwable, Int]] =
|
|
510
|
+
Async.succeed(1).map(_ + 1).either.tap {
|
|
511
|
+
case Right(value) => Async.succeed(println(s"done: $value"))
|
|
512
|
+
case Left(cause) => Async.succeed(println(s"failed: $cause"))
|
|
513
|
+
}.start
|
|
514
|
+
|
|
515
|
+
running.cancel() // idempotent; no-op once the run has completed
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The companion `Async.start(body)` is a single by-name method that evaluates
|
|
519
|
+
`body` on a background worker (JVM) or microtask (JS) and returns a `Running`
|
|
520
|
+
for the result — the `Async` analogue of `Future.apply`. It captures a throwing
|
|
521
|
+
body (even a statically `Nothing`-typed one such as `Async.start(sys.error(...))`)
|
|
522
|
+
as a failed run rather than letting it escape at the call site. (Driving an
|
|
523
|
+
existing `Async` value is the `fa.start` extension above, not `Async.start(fa)`,
|
|
524
|
+
which would treat `fa` as a by-name body to evaluate.)
|
|
525
|
+
|
|
526
|
+
For an already-ready `Async`, observers composed before `start` run synchronously
|
|
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).
|
|
531
|
+
|
|
532
|
+
#### Fanning one `Async` out to several consumers
|
|
533
|
+
|
|
534
|
+
To deliver one `Async`'s result to multiple consumers, **start it once and share
|
|
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:
|
|
538
|
+
|
|
539
|
+
```scala
|
|
540
|
+
import zio.blocks.async._
|
|
541
|
+
|
|
542
|
+
val shared: Async.Running[Int] = Async.succeed(1).map(_ + 1).start
|
|
543
|
+
val a: Int = shared.block // both observe the one result;
|
|
544
|
+
val b: Int = shared.block // the `+ 1` ran once, on the worker
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Do **not** instead drive the same raw `Async` from two places at once (two
|
|
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.
|
|
556
|
+
|
|
557
|
+
## Interop
|
|
558
|
+
|
|
559
|
+
`Async` converts to and from the platform's standard async types, preserving
|
|
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`.
|
|
566
|
+
|
|
567
|
+
On the JVM:
|
|
568
|
+
|
|
569
|
+
```scala
|
|
570
|
+
import zio.blocks.async._
|
|
571
|
+
import scala.concurrent.{ExecutionContext, Future}
|
|
572
|
+
import java.util.concurrent.CompletableFuture
|
|
573
|
+
|
|
574
|
+
implicit val ec: ExecutionContext = ExecutionContext.global
|
|
575
|
+
|
|
576
|
+
val fromFut: Async[Int] = Async.fromFuture(Future.successful(1))
|
|
577
|
+
val toFut: Future[Int] = Async.succeed(1).toFuture
|
|
578
|
+
val fromStage: Async[Int] = Async.fromCompletionStage(CompletableFuture.completedFuture(1))
|
|
579
|
+
val toStage: CompletableFuture[Int] = Async.succeed(1).toCompletableFuture
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
On Scala.js, the companion provides `Async.fromFuture` / `Async.fromJsPromise`
|
|
583
|
+
and the egress extensions provide `fa.toFuture` / `fa.toJsPromise` for native
|
|
584
|
+
`scala.scalajs.js.Promise` interop.
|
|
585
|
+
|
|
586
|
+
## Low-level building blocks: `Pollable`
|
|
587
|
+
|
|
588
|
+
Most code should use the constructors and `Async.promise`. For custom
|
|
589
|
+
asynchronous leaves you can implement a `Pollable[A]` directly. `poll(onComplete)`
|
|
590
|
+
returns the ready value (or a `Failure`) when available, or a `Pollable`
|
|
591
|
+
(commonly `this`, optionally a replacement representing the rest of the
|
|
592
|
+
computation — drivers and combinators direct their next poll at whichever
|
|
593
|
+
pollable was returned) when still pending; a pending pollable must arrange to
|
|
594
|
+
call `onComplete.run()` once progress can be made, prompting the scheduler to
|
|
595
|
+
re-poll.
|
|
596
|
+
|
|
597
|
+
### Combinator chain depth
|
|
598
|
+
|
|
599
|
+
Combinator continuations poll their children recursively without a trampoline
|
|
600
|
+
(a deliberate trade: the poll path stays allocation- and indirection-free). The
|
|
601
|
+
determinant of depth-safety is whether driving has to unwind a deep chain of
|
|
602
|
+
combinators in **receiver position** over a value that is still **pending** —
|
|
603
|
+
not whether the chain was written iteratively or recursively:
|
|
604
|
+
|
|
605
|
+
- **Over a ready source, `flatMap` / `map` / `zipWith` chains are depth-safe to
|
|
606
|
+
any length.** When the receiver is already a value, each step resolves
|
|
607
|
+
eagerly and collapses — no `Pollable` is retained — so `var fa = …;
|
|
608
|
+
while (…) fa = fa.flatMap(g)` (and the `.map` form) consume constant stack
|
|
609
|
+
regardless of length (verified into the millions).
|
|
610
|
+
- **Over a pending source, a deep receiver-position chain is NOT depth-safe.**
|
|
611
|
+
When the receiver stays pending, each `fa.flatMap(g)` / `fa.map(g)` /
|
|
612
|
+
`fa.zipWith(...)` wraps the previous pending value, so driving descends one
|
|
613
|
+
stack frame per level before anything settles and overflows around
|
|
614
|
+
default-JVM-stack depths of a few tens of thousands (~50–100k). This is true
|
|
615
|
+
for **both** a recursive shape (`def loop(n) = src.flatMap(_ => loop(n-1))`)
|
|
616
|
+
**and** an iterative accumulation (`fa = fa.flatMap(_ => src.flatMap(...))`) —
|
|
617
|
+
the syntax doesn't matter, the pending receiver spine does.
|
|
618
|
+
- **`Async.collectAll` and direct-style `Async.async` `while` loops are
|
|
619
|
+
depth-safe even over pending sources.** `collectAll` is a single `Pollable`
|
|
620
|
+
that iterates its elements internally (no receiver spine), and the
|
|
621
|
+
`Async.async` loop rewrite advances one iteration per driver poll rather than
|
|
622
|
+
pre-building a deep chain — both verified at 200k+ pending steps.
|
|
623
|
+
|
|
624
|
+
So: to sequence a large, *pending-heavy* workload, reach for `collectAll` or an
|
|
625
|
+
`Async.async` `while` loop, not a hand-built `flatMap`/`map`/`zipWith` tower over
|
|
626
|
+
a pending value. (`Future` avoids this overflow for any shape only because it
|
|
627
|
+
bounces every `flatMap` through its `ExecutionContext`; `Async` skips that hop
|
|
628
|
+
for speed and accepts the depth bound instead.)
|
|
629
|
+
|
|
630
|
+
## Cross-platform and cross-version notes
|
|
631
|
+
|
|
632
|
+
| Feature | JVM | JS | Scala 2.13 | Scala 3.x | Notes |
|
|
633
|
+
|----------------------------------|-----|----|------------|-----------|---------------------------------------------------------|
|
|
634
|
+
| Constructors & transformers | ✅ | ✅ | ✅ | ✅ | Identical behavior everywhere |
|
|
635
|
+
| `Async.async` / `.await` | ✅ | ✅ | ✅ | ✅ | DCA (Scala 3), native `js.async`/`js.await` for direct-position awaits with DCA fallback for closure/by-name awaits (3.8+ JS), macro (Scala 2); `.await` in the standard strict-collection HOF closures (`List` / `Option` / `Vector` / `Set` / `Map` / `Array` / `Queue` / `ArraySeq`: `map`/`foreach`/`flatMap`/`filter`/`collect`/`fold*`/`reduce*`/`takeWhile`/`dropWhile`/`find`/`exists`/`forall`) and their for-comprehensions is supported on every cell, except a few explicitly-noted divergences (`Map.filter` Scala-2-only; a pair-yielding `Map.collect` unsupported everywhere) — see the HOF section above |
|
|
636
|
+
| `.block` on a pending value | ✅ | ❌ | ✅ | ✅ | Blocks on JVM; throws on JS (cannot block) |
|
|
637
|
+
| `Async.start` / `Async.Running` | ✅ | ✅ | ✅ | ✅ | Eager non-blocking runner; worker thread (JVM) / microtask (JS) |
|
|
638
|
+
| `Future` interop | ✅ | ✅ | ✅ | ✅ | `Async.fromFuture` / `fa.toFuture` on both platforms |
|
|
639
|
+
| `CompletionStage` interop | ✅ | ❌ | ✅ | ✅ | JVM-only (`fromCompletionStage` / `toCompletableFuture`) |
|
|
640
|
+
| `js.Promise` interop | ❌ | ✅ | ✅ | ✅ | JS-only (`fromJsPromise` / `toJsPromise`) |
|
|
641
|
+
|
|
642
|
+
The core `Async` API is identical across all platforms and Scala versions by
|
|
643
|
+
design; platform interop APIs are intentionally platform-specific as shown
|
|
644
|
+
above. The cross-platform test suite fails if any user-visible core behavior
|
|
645
|
+
diverges.
|
|
646
|
+
|
|
647
|
+
## See Also
|
|
648
|
+
|
|
649
|
+
- [Runnable example](#runnable-example) — `async-examples` single-file showcase
|
|
650
|
+
- [Combinators](./combinators.md) — `Async#zip` uses the `Tuples` combiner for
|
|
651
|
+
automatic tuple flattening.
|