@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.
Files changed (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,677 @@
1
+ import React, { useState, useCallback, useRef } from "react";
2
+
3
+ const CAP = 4;
4
+ const MASK = CAP - 1;
5
+
6
+ const COLOR = {
7
+ write: "#1D9E75",
8
+ read: "#378ADD",
9
+ fail: "#E24B4A",
10
+ neutral: "#888780",
11
+ stale: "#C0BDB4",
12
+ };
13
+
14
+ function initState() {
15
+ return {
16
+ buf: Array(CAP).fill(null), // actual slot contents — never cleared by consumer
17
+ live: Array(CAP).fill(false), // is slot logically occupied (cIdx ≤ slot < pIdx)?
18
+ pIdx: 0,
19
+ cIdx: 0,
20
+ };
21
+ }
22
+
23
+ // ── Prose step summary ────────────────────────────────────────────────────────
24
+
25
+ function stepDescription(calc) {
26
+ if (!calc) return null;
27
+
28
+ if (calc.op === "offer") {
29
+ if (calc.size >= CAP) {
30
+ return {
31
+ color: COLOR.fail,
32
+ paragraphs: [
33
+ `The producer reads pIdx = ${calc.pIdx} with a plain load and cIdx = ${calc.cIdx} ` +
34
+ `with an acquire load. The occupancy pIdx − cIdx = ${calc.size} equals the buffer ` +
35
+ `capacity (${CAP}), so every slot holds an unconsumed element.`,
36
+
37
+ `offer() returns false immediately. No slot is written and pIdx stays at ${calc.pIdx}. ` +
38
+ `No CAS is attempted — the single-producer guarantee means no other thread can ` +
39
+ `interfere with pIdx.`,
40
+ ],
41
+ };
42
+ } else {
43
+ return {
44
+ color: COLOR.write,
45
+ paragraphs: [
46
+ `The producer reads pIdx = ${calc.pIdx} with a plain load — no CAS is needed since ` +
47
+ `there is exactly one producer. It reads cIdx = ${calc.cIdx} with an acquire load ` +
48
+ `to check capacity. The occupancy pIdx − cIdx = ${calc.size} is less than ${CAP}, ` +
49
+ `so there is room. The target slot is computed as pIdx & mask = ${calc.pIdx} & ${MASK} = ${calc.slot}.`,
50
+
51
+ `The producer writes the element into buf[${calc.slot}] with a release store, then ` +
52
+ `advances pIdx from ${calc.pIdx} to ${calc.pIdx + 1} with a second release store. ` +
53
+ `Advancing pIdx last is the publication act: the consumer reads pIdx with an acquire ` +
54
+ `load, so any consumer that observes pIdx = ${calc.pIdx + 1} is guaranteed to also ` +
55
+ `see the element already written in buf[${calc.slot}].`,
56
+ ],
57
+ };
58
+ }
59
+ } else {
60
+ if (calc.size === 0) {
61
+ return {
62
+ color: COLOR.fail,
63
+ paragraphs: [
64
+ `The consumer reads cIdx = ${calc.cIdx} with a plain load — no CAS is needed since ` +
65
+ `there is exactly one consumer. It reads pIdx = ${calc.pIdx} with an acquire load. ` +
66
+ `Since pIdx equals cIdx, the buffer contains no elements.`,
67
+
68
+ `take() returns null immediately. cIdx stays at ${calc.cIdx}. ` +
69
+ `No CAS is attempted anywhere in the SPSC algorithm — this is what makes it the ` +
70
+ `fastest of all four ring buffer variants.`,
71
+ ],
72
+ };
73
+ } else {
74
+ return {
75
+ color: COLOR.read,
76
+ paragraphs: [
77
+ `The consumer reads cIdx = ${calc.cIdx} with a plain load — no CAS is needed since ` +
78
+ `there is exactly one consumer. It reads pIdx = ${calc.pIdx} with an acquire load. ` +
79
+ `Since pIdx (${calc.pIdx}) > cIdx (${calc.cIdx}), the buffer is not empty. ` +
80
+ `The slot is computed as cIdx & mask = ${calc.cIdx} & ${MASK} = ${calc.slot}.`,
81
+
82
+ `Unlike SPMC, there is no read-before-CAS concern here. Because only one consumer ` +
83
+ `exists, no other thread can claim this slot, so the consumer reads buf[${calc.slot}] = "${calc.element}" ` +
84
+ `at its leisure. It then advances cIdx from ${calc.cIdx} to ${calc.cIdx + 1} with a ` +
85
+ `release store. This release store pairs with the producer's acquire read of cIdx during ` +
86
+ `capacity checking, ensuring the producer eventually sees the freed slot.`,
87
+
88
+ `buf[${calc.slot}] is NOT cleared. Like SPMC, the slot retains "${calc.element}" until ` +
89
+ `the producer overwrites it on the next lap. Element validity is determined solely by ` +
90
+ `comparing pIdx and cIdx — null-checking slots is never needed.`,
91
+ ],
92
+ };
93
+ }
94
+ }
95
+ }
96
+
97
+ function StepDescription({ calc }) {
98
+ const desc = stepDescription(calc);
99
+
100
+ if (!desc) return (
101
+ <div style={{
102
+ margin: "6px 0 4px", borderRadius: 8,
103
+ border: "1px solid #e8e6df", background: "#f5f4f0",
104
+ padding: "10px 14px", fontSize: 12,
105
+ color: "#ccc", textAlign: "center", fontStyle: "italic",
106
+ }}>
107
+ step summary will appear here after each operation
108
+ </div>
109
+ );
110
+
111
+ return (
112
+ <div style={{
113
+ margin: "6px 0 4px", borderRadius: 8,
114
+ border: `1px solid ${desc.color}22`,
115
+ background: `${desc.color}08`,
116
+ padding: "10px 14px",
117
+ }}>
118
+ {desc.paragraphs.map((p, i) => (
119
+ <p key={i} style={{
120
+ margin: i < desc.paragraphs.length - 1 ? "0 0 6px" : "0",
121
+ fontSize: 12.5, lineHeight: 1.65, color: "#444",
122
+ }}>
123
+ {p}
124
+ </p>
125
+ ))}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // ── Algorithm trace panel ─────────────────────────────────────────────────────
131
+
132
+ function CalcPanel({ calc }) {
133
+ if (!calc) return (
134
+ <div style={{
135
+ margin: "6px 0 0", borderRadius: 8,
136
+ border: "1px solid #e8e6df", background: "#f5f4f0",
137
+ padding: "10px 14px", fontSize: 11,
138
+ fontFamily: "monospace", color: "#ccc", textAlign: "center",
139
+ }}>
140
+ algorithm trace will appear here after each operation
141
+ </div>
142
+ );
143
+
144
+ const isOffer = calc.op === "offer";
145
+ const opColor = isOffer ? COLOR.write : COLOR.read;
146
+
147
+ let rows;
148
+ let decisionColor;
149
+ let decisionText;
150
+
151
+ if (isOffer) {
152
+ const full = calc.size >= CAP;
153
+ rows = [
154
+ {
155
+ label: "pIdx",
156
+ expr: "pIdx",
157
+ val: String(calc.pIdx),
158
+ note: "plain load — single producer, no CAS ever needed",
159
+ hi: false,
160
+ },
161
+ {
162
+ label: "cIdx",
163
+ expr: "cIdx",
164
+ val: String(calc.cIdx),
165
+ note: "acquire load — sees all consumer releases",
166
+ hi: false,
167
+ },
168
+ {
169
+ label: "size",
170
+ expr: "pIdx − cIdx",
171
+ val: String(calc.size),
172
+ note: `${calc.pIdx} − ${calc.cIdx} = ${calc.size} (current occupancy)`,
173
+ hi: true,
174
+ },
175
+ ...(!full ? [{
176
+ label: "slot",
177
+ expr: "pIdx & mask",
178
+ val: String(calc.slot),
179
+ note: `${calc.pIdx} & ${MASK} = ${calc.slot} (bitmask replaces mod)`,
180
+ hi: false,
181
+ }] : []),
182
+ ];
183
+ decisionColor = full ? COLOR.fail : COLOR.write;
184
+ decisionText = full
185
+ ? `✗ size == cap → buffer FULL → return false`
186
+ : `✓ size < cap → write buf[${calc.slot}] (release), then pIdx ${calc.pIdx}→${calc.pIdx + 1} (release)`;
187
+ } else {
188
+ const empty = calc.size === 0;
189
+ rows = [
190
+ {
191
+ label: "cIdx",
192
+ expr: "cIdx",
193
+ val: String(calc.cIdx),
194
+ note: "plain load — single consumer, no CAS ever needed",
195
+ hi: false,
196
+ },
197
+ {
198
+ label: "pIdx",
199
+ expr: "pIdx",
200
+ val: String(calc.pIdx),
201
+ note: "acquire load — sees all producer releases",
202
+ hi: false,
203
+ },
204
+ {
205
+ label: "size",
206
+ expr: "pIdx − cIdx",
207
+ val: String(calc.size),
208
+ note: `${calc.pIdx} − ${calc.cIdx} = ${calc.size} (${empty ? "== 0 → empty" : "> 0 → not empty"})`,
209
+ hi: true,
210
+ },
211
+ ...(!empty ? [
212
+ {
213
+ label: "slot",
214
+ expr: "cIdx & mask",
215
+ val: String(calc.slot),
216
+ note: `${calc.cIdx} & ${MASK} = ${calc.slot} (bitmask replaces mod)`,
217
+ hi: false,
218
+ },
219
+ {
220
+ label: "element",
221
+ expr: "buf[slot]",
222
+ val: `"${calc.element}"`,
223
+ note: `acquire read — no CAS race; single consumer owns this slot exclusively`,
224
+ hi: true,
225
+ },
226
+ ] : []),
227
+ ];
228
+ decisionColor = empty ? COLOR.fail : COLOR.read;
229
+ decisionText = empty
230
+ ? `✗ pIdx == cIdx → buffer EMPTY → return null`
231
+ : `✓ element read → cIdx ${calc.cIdx}→${calc.cIdx + 1} (release) → return "${calc.element}" (buf[${calc.slot}] not cleared)`;
232
+ }
233
+
234
+ return (
235
+ <div style={{
236
+ margin: "6px 0 0", borderRadius: 8,
237
+ border: `1px solid ${opColor}30`,
238
+ overflow: "hidden", fontSize: 12,
239
+ }}>
240
+ <div style={{
241
+ background: `${opColor}14`, padding: "5px 12px",
242
+ display: "flex", alignItems: "center", gap: 8,
243
+ borderBottom: `1px solid ${opColor}20`,
244
+ }}>
245
+ <span style={{ color: opColor, fontFamily: "monospace", fontWeight: 700, fontSize: 12 }}>
246
+ {isOffer ? "offer()" : "take()"}
247
+ </span>
248
+ <span style={{ color: "#bbb", fontSize: 11 }}>— algorithm trace</span>
249
+ <span style={{
250
+ marginLeft: "auto", fontSize: 10,
251
+ fontFamily: "monospace", color: "#bbb",
252
+ }}>
253
+ no CAS — plain loads &amp; release/acquire stores only
254
+ </span>
255
+ </div>
256
+
257
+ {rows.map((r, i) => (
258
+ <div key={i} style={{
259
+ display: "grid",
260
+ gridTemplateColumns: "58px 140px 52px 1fr",
261
+ alignItems: "center",
262
+ padding: "4px 12px",
263
+ borderBottom: i < rows.length - 1 ? "1px solid #f0ede6" : "none",
264
+ background: r.hi ? `${opColor}0c` : "transparent",
265
+ }}>
266
+ <span style={{
267
+ fontFamily: "monospace", fontSize: 11,
268
+ fontWeight: r.hi ? 700 : 400,
269
+ color: r.hi ? opColor : "#aaa",
270
+ }}>
271
+ {r.label}
272
+ </span>
273
+ <span style={{ fontFamily: "monospace", fontSize: 11, color: "#ccc" }}>
274
+ = {r.expr}
275
+ </span>
276
+ <span style={{
277
+ fontFamily: "monospace", fontSize: 14, fontWeight: 700,
278
+ color: r.hi ? decisionColor : "#555",
279
+ textAlign: "right", paddingRight: 10,
280
+ }}>
281
+ {r.val}
282
+ </span>
283
+ <span style={{
284
+ fontFamily: "monospace", fontSize: 10,
285
+ color: r.hi ? decisionColor + "aa" : "#d8d5cc",
286
+ }}>
287
+ {r.note}
288
+ </span>
289
+ </div>
290
+ ))}
291
+
292
+ <div style={{
293
+ padding: "6px 12px",
294
+ background: `${decisionColor}10`,
295
+ borderTop: `1px solid ${decisionColor}28`,
296
+ fontFamily: "monospace", fontSize: 11,
297
+ fontWeight: 600, color: decisionColor,
298
+ }}>
299
+ {decisionText}
300
+ </div>
301
+ </div>
302
+ );
303
+ }
304
+
305
+ // ── SVG ring diagram ──────────────────────────────────────────────────────────
306
+
307
+ function RingDiagram({ buf, live, pIdx, cIdx, hiSlots, hiColor }) {
308
+ const size = pIdx - cIdx;
309
+ const pSlot = pIdx & MASK;
310
+ const cSlot = cIdx & MASK;
311
+ const same = pSlot === cSlot;
312
+
313
+ const SX = [56, 186, 316, 446];
314
+ const SW = 106, SH = 78, SY = 30;
315
+ const bot = SY + SH;
316
+
317
+ const pCX = SX[pSlot] + SW / 2;
318
+ const cCX = SX[cSlot] + SW / 2;
319
+ const pX = same ? pCX - 28 : pCX;
320
+ const cX = same ? cCX + 28 : cCX;
321
+
322
+ const szColor = size === CAP ? COLOR.fail : COLOR.neutral;
323
+
324
+ const statItems = [
325
+ { label: "size", val: `${size} / ${CAP}`, col: szColor },
326
+ { label: "pIdx", val: `${pIdx}`, col: COLOR.write },
327
+ { label: "cIdx", val: `${cIdx}`, col: COLOR.read },
328
+ ];
329
+
330
+ return (
331
+ <svg viewBox="0 0 608 210" style={{ width: "100%", display: "block" }}>
332
+ <defs>
333
+ <marker id="arr-spsc" viewBox="0 0 10 10" refX="8" refY="5"
334
+ markerWidth="5" markerHeight="5" orient="auto-start-reverse">
335
+ <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke"
336
+ strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
337
+ </marker>
338
+ </defs>
339
+
340
+ {SX.map((x, i) => {
341
+ const hi = hiSlots?.includes(i);
342
+ const val = buf[i];
343
+ const isLive = live[i];
344
+ const isStale = !isLive && val !== null;
345
+ const col = hi ? hiColor : undefined;
346
+
347
+ const valColor = hi ? hiColor : isLive ? COLOR.write : isStale ? COLOR.stale : "#ccc";
348
+ const borderColor = hi ? col : isLive ? "#aaa" : "#d4d2c8";
349
+ const fillColor = hi ? col + "18" : isStale ? "#f8f7f4" : "var(--slot-bg, #f5f5f3)";
350
+
351
+ return (
352
+ <g key={i}>
353
+ <rect x={x} y={SY} width={SW} height={SH} rx="8"
354
+ fill={fillColor}
355
+ stroke={borderColor}
356
+ strokeWidth={hi ? 1.5 : 0.5}
357
+ strokeDasharray={isStale && !hi ? "4 3" : undefined} />
358
+
359
+ <text x={x + SW / 2} y={SY + 11} textAnchor="middle"
360
+ dominantBaseline="central"
361
+ fontSize="10" fill="#888" fontFamily="monospace">
362
+ slot {i}
363
+ </text>
364
+
365
+ <text x={x + SW / 2} y={SY + SH / 2 + (isStale ? 0 : 4)} textAnchor="middle"
366
+ dominantBaseline="central"
367
+ fontSize={val ? 20 : 13} fontWeight={isLive ? "600" : "400"}
368
+ fontFamily={val ? "monospace" : "sans-serif"}
369
+ fill={valColor}>
370
+ {val ?? "—"}
371
+ </text>
372
+
373
+ {isStale && !hi && (
374
+ <text x={x + SW / 2} y={SY + SH - 10} textAnchor="middle"
375
+ dominantBaseline="central"
376
+ fontSize="9" fill={COLOR.stale} fontFamily="monospace">
377
+ stale
378
+ </text>
379
+ )}
380
+ </g>
381
+ );
382
+ })}
383
+
384
+ {/* pIdx arrow */}
385
+ <line x1={pX} y1={bot + 2} x2={pX} y2={bot + 12}
386
+ stroke={COLOR.write} strokeWidth="1.5" markerEnd="url(#arr-spsc)" />
387
+ <text x={pX} y={bot + 24} textAnchor="middle" dominantBaseline="central"
388
+ fontSize="11" fontFamily="monospace" fill={COLOR.write}>
389
+ pIdx={pIdx}
390
+ </text>
391
+
392
+ {/* cIdx arrow */}
393
+ <line x1={cX} y1={bot + 2} x2={cX} y2={bot + 12}
394
+ stroke={COLOR.read} strokeWidth="1.5" markerEnd="url(#arr-spsc)" />
395
+ <text x={cX} y={bot + 24} textAnchor="middle" dominantBaseline="central"
396
+ fontSize="11" fontFamily="monospace" fill={COLOR.read}>
397
+ cIdx={cIdx}
398
+ </text>
399
+
400
+ {/* Stats */}
401
+ {statItems.map((s, n) => {
402
+ const sx = 100 + n * 200;
403
+ return (
404
+ <g key={n}>
405
+ <text x={sx} y={166} textAnchor="middle" dominantBaseline="central"
406
+ fontSize="9" fill="#aaa" fontFamily="sans-serif">
407
+ {s.label}
408
+ </text>
409
+ <text x={sx} y={182} textAnchor="middle" dominantBaseline="central"
410
+ fontSize="14" fontWeight="500" fontFamily="monospace" fill={s.col}>
411
+ {s.val}
412
+ </text>
413
+ </g>
414
+ );
415
+ })}
416
+
417
+ {/* Legend */}
418
+ {[
419
+ [COLOR.write, "pIdx — single producer"],
420
+ [COLOR.read, "cIdx — single consumer"],
421
+ [COLOR.stale, "stale — consumed, not yet overwritten"],
422
+ ].map(([c, label], n) => (
423
+ <g key={n}>
424
+ <circle cx={34 + n * 192} cy={203} r="4" fill={c} />
425
+ <text x={44 + n * 192} y={203} dominantBaseline="central"
426
+ fontSize="10" fill="#999" fontFamily="sans-serif">
427
+ {label}
428
+ </text>
429
+ </g>
430
+ ))}
431
+ </svg>
432
+ );
433
+ }
434
+
435
+ // ── History log ───────────────────────────────────────────────────────────────
436
+
437
+ function HistoryLog({ entries, currentIndex }) {
438
+ const bottomRef = useRef(null);
439
+
440
+ return (
441
+ <div style={{
442
+ maxHeight: 110, overflowY: "auto",
443
+ border: "1px solid #e0ded6", borderRadius: 8,
444
+ fontSize: 11, fontFamily: "monospace",
445
+ }}>
446
+ {entries.map((e, i) => {
447
+ const border = { write: COLOR.write, read: COLOR.read, fail: COLOR.fail }[e.kind] ?? "#ccc";
448
+ const isSelected = currentIndex === i;
449
+ return (
450
+ <div key={i} style={{
451
+ display: "flex", gap: 10, padding: "4px 10px",
452
+ borderLeft: `3px solid ${border}`,
453
+ borderBottom: i < entries.length - 1 ? "1px solid #f0ede6" : "none",
454
+ color: border,
455
+ background: isSelected ? `${border}0a` : "transparent",
456
+ fontWeight: isSelected ? 600 : 400,
457
+ }}>
458
+ <span style={{ color: "#bbb", minWidth: 32, fontSize: 10 }}>#{i + 1}</span>
459
+ <span style={{ flex: 1 }}>{e.msg}</span>
460
+ </div>
461
+ );
462
+ })}
463
+ <div ref={bottomRef} />
464
+ </div>
465
+ );
466
+ }
467
+
468
+ // ── Main component ────────────────────────────────────────────────────────────
469
+
470
+ export default function SpscRingBuffer() {
471
+ const [state, setState] = useState(initState);
472
+ const [input, setInput] = useState("A");
473
+ const [history, setHistory] = useState([{
474
+ msg: "initial state", kind: "neutral",
475
+ stateSnapshot: initState(), calcSnapshot: null,
476
+ }]);
477
+ const [hi, setHi] = useState({ slots: null, color: null });
478
+ const [calc, setCalc] = useState(null);
479
+ const [historyIndex, setHistoryIndex] = useState(-1);
480
+
481
+ function addLog(msg, kind, stateSnapshot, calcSnapshot) {
482
+ setHistory(h => [...h, { msg, kind, stateSnapshot, calcSnapshot }]);
483
+ }
484
+
485
+ const displayedState = historyIndex >= 0 ? history[historyIndex].stateSnapshot : state;
486
+
487
+ const goBack = useCallback(() => {
488
+ setHistoryIndex(i => {
489
+ const next = i === -1 ? history.length - 1 : Math.max(0, i - 1);
490
+ setCalc(history[next].calcSnapshot);
491
+ return next;
492
+ });
493
+ }, [history]);
494
+
495
+ const goForward = useCallback(() => {
496
+ setHistoryIndex(i => {
497
+ const next = i === history.length - 1 ? -1 : Math.min(history.length - 1, i + 1);
498
+ if (next >= 0) setCalc(history[next].calcSnapshot);
499
+ else setCalc(null);
500
+ return next;
501
+ });
502
+ }, [history]);
503
+
504
+ const doOffer = useCallback(() => {
505
+ const val = input.trim();
506
+ if (!val) return;
507
+
508
+ setState(prev => {
509
+ const { buf, live, pIdx, cIdx } = prev;
510
+ const size = pIdx - cIdx;
511
+ const slot = pIdx & MASK;
512
+ const snap = { op: "offer", pIdx, cIdx, size, slot };
513
+ setCalc(snap);
514
+
515
+ if (size >= CAP) {
516
+ addLog(`offer("${val}") → false [size=${size} FULL]`, "fail", prev, snap);
517
+ setHi({ slots: [slot], color: COLOR.fail });
518
+ setHistoryIndex(-1);
519
+ return prev;
520
+ }
521
+
522
+ // Write element first (release), then advance pIdx (release) — ordering matters
523
+ const newBuf = [...buf]; newBuf[slot] = val;
524
+ const newLive = [...live]; newLive[slot] = true;
525
+ const newState = { buf: newBuf, live: newLive, pIdx: pIdx + 1, cIdx };
526
+ addLog(
527
+ `offer("${val}") → true [slot ${slot} pIdx: ${pIdx}→${pIdx + 1}]`,
528
+ "write", newState, snap,
529
+ );
530
+ setHi({ slots: [slot], color: COLOR.write });
531
+ if (val.length === 1 && val >= "A" && val < "Z")
532
+ setInput(String.fromCharCode(val.charCodeAt(0) + 1));
533
+ setHistoryIndex(-1);
534
+ return newState;
535
+ });
536
+ }, [input]);
537
+
538
+ const doTake = useCallback(() => {
539
+ setState(prev => {
540
+ const { buf, live, pIdx, cIdx } = prev;
541
+ const size = pIdx - cIdx;
542
+ const slot = cIdx & MASK;
543
+ const element = buf[slot];
544
+ const snap = { op: "take", cIdx, pIdx, size, slot, element };
545
+ setCalc(snap);
546
+
547
+ if (size === 0) {
548
+ addLog(`take() → null [pIdx=${pIdx} cIdx=${cIdx} EMPTY]`, "fail", prev, snap);
549
+ setHi({ slots: [slot], color: COLOR.fail });
550
+ setHistoryIndex(-1);
551
+ return prev;
552
+ }
553
+
554
+ // No CAS — single consumer. buf[slot] NOT cleared; mark stale only.
555
+ const newLive = [...live]; newLive[slot] = false;
556
+ const newState = { buf, live: newLive, pIdx, cIdx: cIdx + 1 }; // buf unchanged!
557
+ addLog(
558
+ `take() → "${element}" [slot ${slot} cIdx: ${cIdx}→${cIdx + 1} buf[${slot}] kept]`,
559
+ "read", newState, snap,
560
+ );
561
+ setHi({ slots: [slot], color: COLOR.read });
562
+ setHistoryIndex(-1);
563
+ return newState;
564
+ });
565
+ }, []);
566
+
567
+ const doReset = useCallback(() => {
568
+ setState(initState());
569
+ setInput("A");
570
+ setHistory([{ msg: "initial state", kind: "neutral", stateSnapshot: initState(), calcSnapshot: null }]);
571
+ setHi({ slots: null, color: null });
572
+ setCalc(null);
573
+ setHistoryIndex(-1);
574
+ }, []);
575
+
576
+ const s = {
577
+ wrap: {
578
+ fontFamily: "sans-serif",
579
+ border: "1px solid #e0ded6", borderRadius: 12,
580
+ padding: "16px 16px 12px", background: "#fafaf8",
581
+ maxWidth: 680, margin: "1.5rem auto",
582
+ },
583
+ heading: {
584
+ fontSize: 13, fontWeight: 600, letterSpacing: "0.04em",
585
+ textTransform: "uppercase", color: "#888",
586
+ marginBottom: 12, textAlign: "center",
587
+ },
588
+ controls: {
589
+ display: "flex", alignItems: "center", gap: 8,
590
+ justifyContent: "center", flexWrap: "wrap", marginBottom: 6,
591
+ },
592
+ input: {
593
+ padding: "6px 10px", borderRadius: 8,
594
+ border: "1px solid #d4d2c8", background: "#fff",
595
+ fontSize: 13, fontFamily: "monospace", width: 110,
596
+ outline: "none", color: "#333",
597
+ },
598
+ btnOffer: {
599
+ padding: "6px 18px", borderRadius: 8, border: "none",
600
+ background: COLOR.write, color: "#fff",
601
+ fontSize: 13, fontWeight: 600, cursor: "pointer",
602
+ },
603
+ btnTake: {
604
+ padding: "6px 18px", borderRadius: 8, border: "none",
605
+ background: COLOR.read, color: "#fff",
606
+ fontSize: 13, fontWeight: 600, cursor: "pointer",
607
+ },
608
+ btnReset: {
609
+ padding: "6px 14px", borderRadius: 8,
610
+ border: "1px solid #d4d2c8", background: "transparent",
611
+ color: "#888", fontSize: 13, cursor: "pointer",
612
+ },
613
+ btnNav: (disabled) => ({
614
+ padding: "6px 12px", borderRadius: 8,
615
+ border: `1px solid ${disabled ? "#e8e6df" : "#d4d2c8"}`, background: "transparent",
616
+ color: disabled ? "#ccc" : "#888", fontSize: 13,
617
+ cursor: disabled ? "default" : "pointer", opacity: disabled ? 0.5 : 1,
618
+ }),
619
+ sectionLabel: { fontSize: 11, color: "#bbb", marginBottom: 4, paddingLeft: 4 },
620
+ hint: {
621
+ fontSize: 11, textAlign: "center", color: "#aaa",
622
+ marginTop: 8, lineHeight: 1.6,
623
+ },
624
+ };
625
+
626
+ return (
627
+ <div style={s.wrap}>
628
+ <div style={s.heading}>SPSC Ring Buffer · Capacity 4</div>
629
+
630
+ <div style={s.controls}>
631
+ <input
632
+ style={s.input} value={input} maxLength={6} placeholder='e.g. "A"'
633
+ onChange={e => setInput(e.target.value)}
634
+ onKeyDown={e => e.key === "Enter" && doOffer()}
635
+ />
636
+ <button style={s.btnOffer} onClick={doOffer}>Offer →</button>
637
+ <button style={s.btnTake} onClick={doTake}>← Take</button>
638
+ <button style={s.btnReset} onClick={doReset}>Reset</button>
639
+ <button style={s.btnNav(historyIndex === 0)} onClick={goBack} disabled={historyIndex === 0}>← Back</button>
640
+ <button style={s.btnNav(historyIndex === -1)} onClick={goForward} disabled={historyIndex === -1}>Forward →</button>
641
+ </div>
642
+
643
+ <RingDiagram
644
+ buf={displayedState.buf}
645
+ live={displayedState.live}
646
+ pIdx={displayedState.pIdx}
647
+ cIdx={displayedState.cIdx}
648
+ hiSlots={hi.slots}
649
+ hiColor={hi.color}
650
+ />
651
+
652
+ <div style={{ marginTop: 8 }}>
653
+ <div style={s.sectionLabel}>Step summary</div>
654
+ <StepDescription calc={calc} />
655
+ </div>
656
+
657
+ <div style={{ marginTop: 8 }}>
658
+ <div style={s.sectionLabel}>Algorithm trace</div>
659
+ <CalcPanel calc={calc} />
660
+ </div>
661
+
662
+ <div style={{ marginTop: 8 }}>
663
+ <div style={s.sectionLabel}>Operation history</div>
664
+ <HistoryLog entries={history} currentIndex={historyIndex} />
665
+ </div>
666
+
667
+ <div style={s.hint}>
668
+ Type any label ·{" "}
669
+ <strong style={{ color: COLOR.write }}>Offer</strong> to enqueue ·{" "}
670
+ <strong style={{ color: COLOR.read }}>Take</strong> to dequeue ·
671
+ no CAS on either side — only release/acquire stores and loads ·{" "}
672
+ <strong style={{ color: COLOR.stale }}>stale</strong> slots show the no-null-clear design ·
673
+ use Back / Forward to replay any step
674
+ </div>
675
+ </div>
676
+ );
677
+ }