@zio.dev/zio-blocks 0.0.33 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,108 @@
1
+ ---
2
+ id: spmc
3
+ title: "SPMC RingBuffer"
4
+ ---
5
+
6
+ import SpmcDiagram from './SpmcDiagram.jsx';
7
+
8
+ `SpmcRingBuffer[A]` allows a single producer thread to efficiently feed multiple consumer threads. It uses an **index-based algorithm** where slot validity is determined by comparing producer and consumer indices, allowing multiple consumers to coordinate safely via CAS operations on a shared consumer index.
9
+
10
+ ## Algorithm
11
+
12
+ `SpmcRingBuffer` allows a single producer to feed multiple consumers. The algorithm is **index-based**: slot validity is determined by comparing `producerIndex` and `consumerIndex`, not by null-checking slots.
13
+
14
+ **How it works:**
15
+
16
+ - The **producer** is single-threaded and never uses CAS. It maintains a `producerLimit` derived from reading the volatile `consumerIndex`. On the fast path, it checks its local limit; when exhausted, it refreshes by reading `consumerIndex`. This design avoids the producer ever reading consumer state on the fast path.
17
+ - The **consumers** (any number) coordinate via a CAS loop on `consumerIndex`. Each consumer reads the element at its claimed index, then attempts to CAS `consumerIndex` forward. If CAS fails, it retries with a refreshed index. This ensures each element is claimed by exactly one consumer.
18
+ - **No slot clearing by consumers:** Consumers do not write `null` after reading. Instead, the producer safely overwrites slots once its `producerLimit` check (based on `consumerIndex`) confirms all consumers have advanced past them. This eliminates the race that would occur if a consumer claimed a slot but hadn't yet cleared it.
19
+
20
+ **Trade-off:** Consumer CAS introduces overhead under contention, but the producer remains extremely fast (no synchronization).
21
+
22
+ ### Diagram
23
+
24
+ To see the single-producer multi-consumer algorithm in action, use this interactive stepper. Type any label, click **Offer** to enqueue (single producer) or **Take** to dequeue (any consumer), and watch the trace panel show every intermediate variable — `pIdx`, `cIdx`, `size`, `slot`, `element` — and the exact decision each side makes.
25
+
26
+ <SpmcDiagram />
27
+
28
+ Here is a complete walkthrough of every variable in the trace, in the order the algorithm computes them.
29
+
30
+ ### `pIdx` — the monotonic producer counter
31
+
32
+ The producer has its own index that starts at zero and only ever increases. Because only one thread produces, `pIdx` is read and written with plain loads and stores — no CAS is needed. The producer's job is simple: check capacity (`pIdx - cIdx < capacity`), write to the slot, then advance `pIdx`.
33
+
34
+ ### `cIdx` — the monotonic consumer counter (multi-threaded)
35
+
36
+ All consumers share a single `consumerIndex` counter. Because multiple consumers may be operating concurrently, each consumer reads `cIdx` with volatile semantics to see the most recent updates, then attempts to atomically advance it via CAS. If the CAS fails, another consumer claimed the slot first and the reader discards their read and retries.
37
+
38
+ ### `size = pIdx − cIdx` — the occupancy check
39
+
40
+ Both sides use the same occupancy formula: `pIdx − cIdx` is the number of elements currently in the buffer. The producer checks if `size == capacity` to detect a full buffer. The consumer checks if `size == 0` to detect an empty buffer.
41
+
42
+ ### `slot = pIdx & mask` or `slot = cIdx & mask` — the circular array index
43
+
44
+ Identical to all other ring buffer variants: because capacity is a power of two, the bitwise AND `& mask` replaces modulo division, giving a slot number in `[0, capacity)`. So `pIdx = 7` maps to slot `7 & 3 = 3`, and `pIdx = 8` wraps back to slot `0`.
45
+
46
+ ### `element = buf[slot]` — read-before-CAS
47
+
48
+ This is the critical SPMC distinction. The consumer reads the element *before* attempting the CAS on `cIdx`. This ordering is essential: once the CAS succeeds and `cIdx` advances, the producer is permitted to overwrite that slot on its next lap. By reading the element first, the consumer guarantees it captures the value while the slot is still logically owned.
49
+
50
+ In the trace, when a consumer takes an element, you will see the `element` row highlighted — this captures the read that must happen before the CAS.
51
+
52
+ ### Why SPMC doesn't clear slots
53
+
54
+ Unlike MPSC (which uses FastFlow's null-check pattern), SPMC determines slot validity purely by index comparison: if `cIdx ≤ slot < pIdx`, the slot is live. After a consumer advances `cIdx`, the slot becomes "stale" — no longer live, but still holding the old value until the producer wraps around and overwrites it. This eliminates the race that would occur if a consumer had to null out the slot while holding the CAS; instead, the producer simply overwrites when it knows the consumer has advanced.
55
+
56
+ The diagram marks stale slots with a dashed border and a "stale" label in grey.
57
+
58
+ ## Creating Instances
59
+
60
+ `SpmcRingBuffer[A]` allows a single producer thread to offer elements while multiple consumer threads concurrently take elements via compare-and-swap on the consumer index. Use the companion object to instantiate a buffer:
61
+
62
+ ```scala
63
+ object SpmcRingBuffer {
64
+ def apply[A <: AnyRef](capacity: Int): SpmcRingBuffer[A]
65
+ }
66
+ ```
67
+
68
+ The capacity must be a positive power of two. To create an SPMC buffer:
69
+
70
+ ```scala
71
+ import zio.blocks.ringbuffer.SpmcRingBuffer
72
+
73
+ val rb = SpmcRingBuffer[java.lang.Integer](64)
74
+ ```
75
+
76
+ ## Operations
77
+
78
+ `SpmcRingBuffer` provides three core operations for sending and receiving elements:
79
+
80
+ ### Inserting Elements — `SpmcRingBuffer#offer`
81
+
82
+ Use `SpmcRingBuffer#offer` to insert an element with this signature:
83
+
84
+ ```scala
85
+ final class SpmcRingBuffer[A <: AnyRef](val capacity: Int) {
86
+ def offer(a: A): Boolean
87
+ }
88
+ ```
89
+
90
+ Inserts the element without blocking. Returns `true` on successful insertion, `false` when the buffer is full. Raises `NullPointerException` if the element is `null`. Call only from the single producer thread; concurrent calls from multiple threads cause undefined behavior.
91
+
92
+ ### Removing Elements — `SpmcRingBuffer#take`
93
+
94
+ Use `SpmcRingBuffer#take` to remove an element with this signature:
95
+
96
+ ```scala
97
+ final class SpmcRingBuffer[A <: AnyRef](val capacity: Int) {
98
+ def take(): A
99
+ }
100
+ ```
101
+
102
+ Retrieves and removes an element from the front of the buffer. Returns immediately without blocking. Returns the element, or `null` if the buffer is empty. Thread-safe; multiple consumer threads may call this concurrently.
103
+
104
+ ### Checking State — `size`, `isEmpty`, `isFull`
105
+
106
+ Ring buffers provide three query methods to check their state. Note that under concurrent access, these results are **approximate** — by the time the method returns, other threads may already modify the buffer.
107
+
108
+ All four implementations provide the same method signatures: `size`, `isEmpty`, and `isFull`. See the [`SpscRingBuffer` Operations](./spsc.mdx#operations) section for detailed descriptions and examples of these methods.
@@ -0,0 +1,416 @@
1
+ ---
2
+ id: spsc
3
+ title: "SPSC RingBuffer"
4
+ ---
5
+
6
+ import SpscDiagram from './SpscDiagram.jsx';
7
+
8
+ `SpscRingBuffer[A]` is optimized for the simplest and fastest case: exactly one producer thread and one consumer thread. It uses the **FastFlow** algorithm to eliminate all cross-core cache traffic, achieving nanosecond-scale latencies with no volatile reads on the fast path.
9
+
10
+ ## Why FastFlow?
11
+
12
+ Understanding the FastFlow algorithm helps explain why `SpscRingBuffer` achieves such high performance.
13
+
14
+ Imagine two threads sharing data with a traditional lock-based queue like `ArrayBlockingQueue`. They use a mutex (lock) to coordinate:
15
+
16
+ 1. Producer thread: acquires lock, adds element, releases lock
17
+ 2. Consumer thread: acquires lock, removes element, releases lock
18
+
19
+ This works, but locks have a cost: when threads contend for the same lock, one thread **blocks** (goes to sleep) while waiting. Waking a thread is expensive (thousands of CPU cycles). Even lock-free queues using `synchronized` or `volatile` reads can cause **cache-coherency traffic**: when one CPU core writes to a variable another core reads, the entire cache line must be invalidated and transferred — a costly operation that slows down both cores.
20
+
21
+ The fundamental issue: **if the producer has to read what the consumer wrote (or vice versa), their CPU caches constantly fight**. This is called *false sharing* and can reduce throughput by 10x or more on heavily loaded systems.
22
+
23
+ The problem is that *any* read by the producer of the consumer's state (or vice versa) causes cache line bouncing. So FastFlow's radical idea: **neither side should ever read the other's state**.
24
+
25
+ Instead, the producer simply **writes data into slots** and marks them non-null. The consumer independently **reads slots** and takes any non-null values. The array slot's null/non-null status itself is the only coordination needed — a *happens-before* relationship written once by the producer, read once by the consumer. No locks, no atomic operations on the fast path, no reading the other side's counters.
26
+
27
+ The **note-passing analogy**:
28
+ - You (producer) have a row of empty desks between you and your friend (consumer)
29
+ - Empty desk = `null` (available)
30
+ - Note on desk = non-null value (message ready)
31
+ - **You never look at your friend's side** — you just place notes on empty desks
32
+ - **Your friend never looks at your side** — they just pick up notes they see
33
+ - No shouting "are you ready?", no waiting, no lock contention
34
+
35
+ **This is how FastFlow solves the cache-coherency problem**: since producer and consumer never read each other's counters, there's no cache line bouncing between CPU cores. All coordination happens through the slots themselves, which are written once and read once.
36
+
37
+ The **look-ahead cache** is a further optimization: the producer maintains a local cached limit (`producerLimit`) so they don't have to check every slot individually. It's like glancing ahead at the next N desks to see if they're empty, without actually bending over to look. This keeps the fast path extremely fast — just a local counter check and an array write.
38
+
39
+ **Why FastFlow is fast**:
40
+ - **No locks** — no thread ever blocks another
41
+ - **Minimal cache coordination** — producer and consumer touch separate memory locations; the array slot is written once, read once
42
+ - **Write-once, read-once semantics** — the slot's null/non-null status is the handshake
43
+ - **Cache-line padding** — producer and consumer indices padded to separate cache lines, eliminating false sharing
44
+
45
+ The result: **lock-free** communication that scales linearly with CPU count and achieves nanosecond-scale latencies. This is why FastFlow is the algorithm of choice for SPSC ring buffers in high-performance systems like trading platforms, game engines, and network stacks. (The slow path's retry on full buffers means the algorithm is not wait-free, though contention is rare.)
46
+
47
+ ## Algorithm
48
+
49
+ `SpscRingBuffer` uses the **FastFlow** pattern, originally developed for the C++ FastFlow framework and popularized in Java by JCTools and the LMAX Disruptor. The core insight: the array element's `null`/non-`null` state **is** the synchronization signal. The producer never reads `consumerIndex`; the consumer never reads `producerIndex`. This eliminates all cross-core cache traffic between the two sides.
50
+
51
+ **How it works:**
52
+
53
+ - The producer walks forward through the array, placing elements into slots that are currently `null`. It marks a slot non-`null` after writing. The producer never reads what the consumer has consumed — it only checks its own cached `producerLimit` to know how many slots are available.
54
+ - The consumer walks forward through the array, reading any slot that contains a non-`null` value. After reading, it writes `null` to clear the slot. The consumer never reads the producer's index.
55
+ - The slot's nullness itself is the coordination: written once by the producer, read once by the consumer. No locks, no atomic operations on the fast path, no reading the other side's counters.
56
+
57
+ **Look-ahead cache:** The producer maintains a local `producerLimit` (derived from `consumerIndex` during slow path). This allows the fast path to check a simple local counter instead of reading the volatile `consumerIndex` on every `offer`. The look-ahead step is `max(1, min(capacity/4, 4096))`, balancing between reducing consumer reads and memory usage.
58
+
59
+ ### Diagram
60
+
61
+ To see the single-producer single-consumer FastFlow algorithm in action, use this interactive stepper. Type any label, click **Offer** to enqueue or **Take** to dequeue, and watch how the producer and consumer coordinate using only release/acquire semantics — no CAS on either side.
62
+
63
+ <SpscDiagram />
64
+
65
+ Here is a complete walkthrough of every variable in the trace, in the order the algorithm computes them.
66
+
67
+ ### `pIdx` — the monotonic producer counter (plain load/store)
68
+
69
+ The producer reads `pIdx` with a plain load — no atomic operations at all. Since only one thread produces, no synchronization is needed. The producer maintains its own count, never reads `consumerIndex`, and advances `pIdx` after writing with a release store.
70
+
71
+ ### `cIdx` — the monotonic consumer counter
72
+
73
+ SPSC avoids reading `consumerIndex` entirely. Instead of checking the occupancy formula (`pIdx - cIdx`), the producer reads the array slot at `producerIndex + lookAheadStep` to determine if a slot is free (on the slow path when the look-ahead cache is exhausted). This slot-based approach eliminates all counter reads between producer and consumer.
74
+
75
+ ### `size = pIdx − cIdx` — the occupancy check
76
+
77
+ Both sides use the same occupancy formula, but only during slow paths (rare). On the fast path, neither side reads the other's counter; they rely on slot nullness (FastFlow) for synchronization.
78
+
79
+ ### `slot = pIdx & mask` or `slot = cIdx & mask` — the circular array index
80
+
81
+ Identical bitmask arithmetic: `& mask` replaces modulo, giving a slot in `[0, capacity)`.
82
+
83
+ ### `element = buf[slot]` — no CAS, plain slot reads
84
+
85
+ Unlike SPMC and MPMC, the consumer reads the slot directly with an acquire load — because there's only one consumer, no read-before-CAS is needed. The consumer is the sole owner of the take path. After reading, the consumer nulls the slot with a release store, which serves two purposes: (1) signals to the producer that the slot is free, and (2) releases any bookkeeping updates to other cores via the release semantics.
86
+
87
+ ### Why SPSC doesn't read producer state
88
+
89
+ The consumer never reads `pIdx`. Instead, it reads the slot directly: if the slot is non-null, data is ready. If null, the buffer is empty. The producer wrote a non-null value via a release store, and the consumer reads via an acquire load, forming a happens-before pair. This is the core of FastFlow: **eliminate cross-core reads entirely**.
90
+
91
+ The diagram marks slots as "live" (still owned by the producer-consumer pair) or "stale" (consumed but not yet overwritten). Unlike SPMC where consumers CAS and have ordering concerns, SPSC clears immediately because only one consumer exists — there's no race.
92
+
93
+ ## Creating Instances
94
+
95
+ Ring buffers are instantiated via the companion object's `apply` method. `SpscRingBuffer[A]` uses the FastFlow pattern with a look-ahead cache. On the fast path, the producer checks a cached `producerLimit` to avoid reading `consumerIndex`. When the cached limit is exhausted, the slow path reads the array slot at `producerIndex + lookAheadStep` — never `consumerIndex` directly. This keeps the producer and consumer cache lines fully independent. The consumer uses null/non-null slot reads (FastFlow semantics). Together, these avoid repeated volatile reads and minimize cross-core cache traffic.
96
+
97
+ ```scala
98
+ object SpscRingBuffer {
99
+ def apply[A <: AnyRef](capacity: Int): SpscRingBuffer[A]
100
+ }
101
+ ```
102
+
103
+ Creates a new SPSC ring buffer with the given capacity. The capacity must be a positive power of two.
104
+
105
+ We create an SPSC buffer as follows:
106
+
107
+ ```scala
108
+ import zio.blocks.ringbuffer.SpscRingBuffer
109
+
110
+ val rb = SpscRingBuffer[String](16) // capacity must be power of 2
111
+ ```
112
+
113
+ ## Operations
114
+
115
+ `SpscRingBuffer` provides operations for sending and receiving elements, as well as querying buffer state:
116
+
117
+ ### Inserting Elements — `SpscRingBuffer#offer`
118
+
119
+ Use `SpscRingBuffer#offer` to insert an element with this signature:
120
+
121
+ ```scala
122
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
123
+ def offer(a: A): Boolean
124
+ }
125
+ ```
126
+
127
+ Inserts the element without blocking. Returns `true` on successful insertion, `false` when the buffer is full. Raises `NullPointerException` if the element is `null`. Call only from the producer thread.
128
+
129
+ To handle backpressure by checking if insertion succeeds:
130
+
131
+ ```scala
132
+ import zio.blocks.ringbuffer.SpscRingBuffer
133
+
134
+ val rb = SpscRingBuffer[String](4)
135
+ ```
136
+
137
+ When the buffer becomes full, `offer` returns `false`:
138
+
139
+ ```scala
140
+ val result1 = rb.offer("a")
141
+ // result1: Boolean = true
142
+ val result2 = rb.offer("b")
143
+ // result2: Boolean = true
144
+ val result3 = rb.offer("c")
145
+ // result3: Boolean = true
146
+ val result4 = rb.offer("d")
147
+ // result4: Boolean = true
148
+ val result5 = rb.offer("e")
149
+ // result5: Boolean = false
150
+ ```
151
+
152
+ ### Removing Elements — `SpscRingBuffer#take`
153
+
154
+ Use `SpscRingBuffer#take` to remove an element with this signature:
155
+
156
+ ```scala
157
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
158
+ def take(): A
159
+ }
160
+ ```
161
+
162
+ Retrieves and removes an element from the front of the buffer. Returns immediately without blocking, providing the element or `null` if the buffer is empty. Call only from the consumer thread.
163
+
164
+ To retrieve elements in FIFO order:
165
+
166
+ ```scala
167
+ rb.take()
168
+ // res2: String = "a"
169
+ rb.take()
170
+ // res3: String = "b"
171
+ rb.take()
172
+ // res4: String = "c"
173
+ rb.take()
174
+ // res5: String = "d"
175
+ rb.take()
176
+ // res6: String = null
177
+ ```
178
+
179
+ ### Checking State — `size`, `isEmpty`, `isFull`
180
+
181
+ Ring buffers provide three query methods to check their state. Note that under concurrent access, these results are **approximate** — by the time the method returns, other threads may modify the buffer.
182
+
183
+ Use `SpscRingBuffer#size` to get the approximate element count with this signature:
184
+
185
+ ```scala
186
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
187
+ def size: Int
188
+ }
189
+ ```
190
+
191
+ Returns the approximate number of elements currently in the buffer. Under concurrent access, the result may be stale. O(1).
192
+
193
+ Use `SpscRingBuffer#isEmpty` to check if the buffer is empty with this signature:
194
+
195
+ ```scala
196
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
197
+ def isEmpty: Boolean
198
+ }
199
+ ```
200
+
201
+ Returns `true` if the buffer contains no elements (approximate). O(1).
202
+
203
+ Use `SpscRingBuffer#isFull` to check if the buffer is at capacity with this signature:
204
+
205
+ ```scala
206
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
207
+ def isFull: Boolean
208
+ }
209
+ ```
210
+
211
+ Returns `true` if the buffer is at capacity (approximate). O(1).
212
+
213
+ All four implementations provide the same three methods. To check state after operations:
214
+
215
+ ```scala
216
+ import zio.blocks.ringbuffer.SpscRingBuffer
217
+
218
+ val rb2 = SpscRingBuffer[String](8)
219
+ ```
220
+
221
+ State queries are cheap but approximate under concurrency:
222
+
223
+ ```scala
224
+ rb2.offer("x")
225
+ // res8: Boolean = true
226
+ rb2.offer("y")
227
+ // res9: Boolean = true
228
+
229
+ rb2.size
230
+ // res10: Int = 2
231
+ rb2.isEmpty
232
+ // res11: Boolean = false
233
+ rb2.isFull
234
+ // res12: Boolean = false
235
+ ```
236
+
237
+ ### Batch Operations — `SpscRingBuffer#drain` and `SpscRingBuffer#fill`
238
+
239
+ Use `SpscRingBuffer#drain` to consume up to N elements with this signature:
240
+
241
+ ```scala
242
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
243
+ def drain(consumer: A => Unit, limit: Int): Int
244
+ }
245
+ ```
246
+
247
+ Removes up to `limit` elements from the buffer, passing each to the `consumer` callback. Returns the number of elements actually drained. Raises `IllegalArgumentException` if `limit` is negative. Call only from the consumer thread. O(n) where n is the number of elements drained. If the callback raises an exception, all elements passed to it up to that point remain consumed and the buffer stays in a consistent state.
248
+
249
+ Use `SpscRingBuffer#fill` to produce up to N elements with this signature:
250
+
251
+ ```scala
252
+ final class SpscRingBuffer[A <: AnyRef](val capacity: Int) {
253
+ def fill(supplier: () => A, limit: Int): Int
254
+ }
255
+ ```
256
+
257
+ Inserts up to `limit` elements by calling the `supplier` for each new element. Returns the number of elements actually inserted. Raises `IllegalArgumentException` if `limit` is negative. Raises `NullPointerException` if the supplier returns `null`. Call only from the producer thread. O(n) where n is the number of elements inserted.
258
+
259
+ Batch operations amortize synchronization costs. To drain multiple elements at once:
260
+
261
+ ```scala
262
+ import zio.blocks.ringbuffer.SpscRingBuffer
263
+
264
+ val rb3 = SpscRingBuffer[java.lang.Integer](16)
265
+ (1 to 5).foreach(i => rb3.offer(Integer.valueOf(i)))
266
+
267
+ val collected = scala.collection.mutable.Buffer[java.lang.Integer]()
268
+ val drained = rb3.drain(collected += _, 10) // drained = 5
269
+
270
+ println(s"Drained items: ${collected.mkString(", ")}")
271
+ ```
272
+
273
+ To avoid repeated `offer` calls when producing elements, use fill:
274
+
275
+ ```scala
276
+ import zio.blocks.ringbuffer.SpscRingBuffer
277
+ import java.util.concurrent.atomic.AtomicInteger
278
+
279
+ val rb4 = SpscRingBuffer[String](8)
280
+ val counter = new AtomicInteger(0)
281
+ val filled = rb4.fill(() => { counter.incrementAndGet(); s"item-${counter.get()}" }, 5)
282
+
283
+ println(s"Filled $filled items")
284
+ ```
285
+
286
+ ## Primitive variants
287
+
288
+ The generic `SpscRingBuffer[A]` stores `AnyRef` slots, which boxes Java primitives on the producer side. For SPSC workloads handing off `Int`, `Long`, `Float`, or `Double` values between two threads, four zero-boxing companions are provided:
289
+
290
+ | Type | Class | Slot encoding |
291
+ |---|---|---|
292
+ | `Int` | `IntSpscRingBuffer` | one `Long` per slot: high 32 bits hold an occupancy tag (`0` = empty, `1` = data, `2` = DONE sentinel), low 32 bits hold the `Int` payload. The whole 64-bit word is `0L` when empty, so any tagged value is non-zero regardless of the payload bits. |
293
+ | `Long` | `LongSpscRingBuffer` | one `Long` per slot. `Long.MinValue` is reserved as the empty marker and `Long.MinValue + 1L` as the in-band DONE sentinel; offering either value throws `IllegalArgumentException`. All other `Long` values pass through unchanged. |
294
+ | `Float` | `FloatSpscRingBuffer` | one `Long` per slot: high 32 bits tag (`0` = empty, `1` = data, `2` = DONE), low 32 bits hold `floatToRawIntBits(value)`. The whole word is `0L` when empty. Every `Float` (including `0.0f`, `NaN`, `±Inf`) is representable. |
295
+ | `Double` | `DoubleSpscRingBuffer` | one `Long` per slot. `doubleToRawLongBits(value)` is stored directly; two reserved non-canonical `NaN` bit patterns mark empty (`0xFFF8_0000_0000_0001L`) and DONE (`0xFFF8_0000_0000_0002L`). Any `NaN` payload is canonicalized to `Double.NaN` on offer so it can never collide with the reserved markers. |
296
+
297
+ All four use the same FastFlow algorithm as the generic version — single backing `Array[Long]`, padded indices, look-ahead cache — and expose the same core operations (`offer`, `take`, `peek`, `isEmpty`, `isFull`, `size`, `capacity`). The only API difference is that `take` returns a primitive instead of `AnyRef`, so callers should check `peek()` first to know whether the next read will be a real value or just the empty marker. (The generic `drain`/`fill` batch helpers are not provided on the primitive variants; loop `peek`/`take` directly.)
298
+
299
+ In addition, each primitive variant exposes two methods for **in-band end-of-stream signalling**, used by the concurrent stream operators to propagate "no more data" through a primitive queue without needing a separate side channel:
300
+
301
+ - `offerDone(): Boolean` — write the DONE sentinel into the next free slot, returning `true` on success or `false` if the queue is full. Once `offerDone()` succeeds the producer should not offer any further values.
302
+ - `pollPacked(): Long` — atomically poll the next slot and return its packed encoding: `EMPTY_PACKED`/`EMPTY` / `EMPTY_BITS` when empty (consumer index NOT advanced), `DONE_PACKED`/`DONE` / `DONE_BITS` for the DONE sentinel (index advanced), or a non-sentinel encoded value otherwise (index advanced). The exact constant names live on each companion object (`IntSpscRingBuffer.DONE_PACKED`, `LongSpscRingBuffer.DONE`, `FloatSpscRingBuffer.DONE_PACKED`, `DoubleSpscRingBuffer.DONE_BITS`).
303
+
304
+ `pollPacked` is intended for advanced consumers that want a single atomic operation distinguishing empty / data / DONE. Most application code that simply needs primitive throughput is fine using the higher-level concurrent stream operators below.
305
+
306
+ ```scala
307
+ import zio.blocks.ringbuffer.{IntSpscRingBuffer, LongSpscRingBuffer, DoubleSpscRingBuffer}
308
+
309
+ val ints = new IntSpscRingBuffer(16)
310
+ ints.offer(42)
311
+ if (ints.peek()) {
312
+ val v: Int = ints.take() // 42
313
+ }
314
+
315
+ val longs = new LongSpscRingBuffer(16)
316
+ longs.offer(Long.MaxValue)
317
+ if (longs.peek()) {
318
+ val w: Long = longs.take() // Long.MaxValue
319
+ }
320
+
321
+ val doubles = new DoubleSpscRingBuffer(16)
322
+ doubles.offer(Double.NaN) // canonicalized to Double.NaN's bit pattern on offer
323
+ ```
324
+
325
+ These variants underpin the primitive-specialized concurrent stream operators (`mapPar`, `mergeAll`, `flatMapPar`) so primitive streams do not box during cross-thread handoff.
326
+
327
+ ## Examples
328
+
329
+ ### SPSC: Producer-Consumer Ping-Pong
330
+
331
+ In a single-producer, single-consumer setup, use `SpscRingBuffer` for maximum throughput. This example demonstrates how two threads communicate efficiently using FastFlow signaling.
332
+
333
+ ```scala title="zio-blocks-examples/src/main/scala/ringbuffer/SpscExample.scala"
334
+ package ringbuffer
335
+
336
+ import zio.blocks.ringbuffer.SpscRingBuffer
337
+ import java.util.concurrent.{CountDownLatch, Thread}
338
+
339
+ object SpscExample extends App {
340
+ val buffer = SpscRingBuffer[String](8)
341
+ val latch = new CountDownLatch(1)
342
+
343
+ val producer = new Thread(() => {
344
+ for (i <- 1 to 5) {
345
+ buffer.offer(s"message-$i")
346
+ }
347
+ })
348
+
349
+ val consumer = new Thread(() => {
350
+ for (_ <- 1 to 5) {
351
+ var msg: String = null
352
+ while ({ msg = buffer.take(); msg.eq(null) }) {}
353
+ println(s"Received: $msg")
354
+ }
355
+ latch.countDown()
356
+ })
357
+
358
+ producer.start()
359
+ consumer.start()
360
+ latch.await()
361
+ println("Done")
362
+ }
363
+ ```
364
+
365
+ ([source](https://github.com/zio/zio-blocks/blob/main/zio-blocks-examples/src/main/scala/ringbuffer/SpscExample.scala))
366
+
367
+ ```bash
368
+ sbt "zio-blocks-examples/runMain ringbuffer.SpscExample"
369
+ ```
370
+
371
+ ### Batch Fill and Drain (SPSC)
372
+
373
+ Use `fill` and `drain` for efficient batch operations. This example shows how to amortize synchronization costs by processing multiple elements at once.
374
+
375
+ ```scala title="zio-blocks-examples/src/main/scala/ringbuffer/BatchExample.scala"
376
+ package ringbuffer
377
+
378
+ import zio.blocks.ringbuffer.SpscRingBuffer
379
+ import java.util.concurrent.{CountDownLatch, Thread}
380
+
381
+ object BatchExample extends App {
382
+ val buffer = SpscRingBuffer[java.lang.Integer](64)
383
+ val latch = new CountDownLatch(1)
384
+
385
+ val producer = new Thread(() => {
386
+ var batch = 1
387
+ while (batch <= 3) {
388
+ val currentBatch = batch
389
+ val count = buffer.fill(() => java.lang.Integer.valueOf(currentBatch * 100), 10)
390
+ println(s"Filled $count items in batch $currentBatch")
391
+ batch += 1
392
+ }
393
+ })
394
+
395
+ val consumer = new Thread(() => {
396
+ val items = scala.collection.mutable.Buffer[java.lang.Integer]()
397
+ while (items.size < 30) {
398
+ val drained = buffer.drain(items += _, 10)
399
+ if (drained > 0) println(s"Drained $drained items")
400
+ }
401
+ println(s"Total items consumed: ${items.size}")
402
+ latch.countDown()
403
+ })
404
+
405
+ producer.start()
406
+ consumer.start()
407
+ latch.await()
408
+ println("Done")
409
+ }
410
+ ```
411
+
412
+ ([source](https://github.com/zio/zio-blocks/blob/main/zio-blocks-examples/src/main/scala/ringbuffer/BatchExample.scala))
413
+
414
+ ```bash
415
+ sbt "zio-blocks-examples/runMain ringbuffer.BatchExample"
416
+ ```