hyphae 1.3.1

Reactive cells and runtime primitives for rship
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//! Wide-parallel-wave correctness for hyphae's time-based and structural
//! operators under the opt-in `scheduler` feature.
//!
//! # What a "wave-parallel hazard" actually is here
//!
//! The scheduler defers every `notify` inside [`batch`] into a height-ordered
//! queue and drains it one height-wave at a time. Two cells at the *same*
//! height can never depend on each other, so once a wave crosses
//! `PARALLEL_WAVE_THRESHOLD` (8) the scheduler dispatches the whole wave across
//! rayon and its ops run genuinely concurrently. The canonical torn-value
//! hazard that motivates this file's siblings
//! (`scheduler_completeness::join_vec_wide_wave...`) needs a **multi-input
//! combine** whose sink peeks at a *sibling's* state while both sides run in the
//! same wide wave — that's the shape `join`/`join_vec` had to be hardened for.
//!
//! **None of the twelve operators covered here is that shape.** Every one is
//! either single-input (debounce/throttle/delay/timeout/buffer_time/
//! backpressure/cold/finalize/retry/audit/parallel) or a *sequential* two-input
//! composition that never combines its inputs (concat: one input is live at a
//! time). So none is a genuine torn-value / Template-A candidate — there is no
//! sibling state for a concurrent wave to tear. What is still worth pinning
//! down is **robustness under a wide wave**: N independent instances of an
//! operator, driven through one genuinely parallel wave, must each settle on
//! exactly the value their own input dictates, with no cross-instance leak,
//! drop, or corruption of the operator's per-instance state (generation
//! counters, first-skip flags, ArrayQueue/SegQueue buffers, once-callbacks).
//! That is the Template-B correctness harness, and it is what this file writes.
//!
//! # Where the wave does and doesn't reach (per operator)
//!
//! An operator's derived cell only rides the wave if its subscribe callback
//! calls `notify` *synchronously*. Operators that instead arm a platform timer
//! (`spawn_delayed`/`spawn_interval`) settle their output from the shared timer
//! reactor thread, **outside any batch wave** — so a wave-parallel test of their
//! *output* is genuinely N/A. For those we still drive the operator's
//! **input side** through a wide parallel wave (N independent sources set in one
//! batch → a wide height-0 wave running N operator callbacks concurrently) and
//! then observe the timer-driven output settle. Each such test is marked with a
//! `// NOTE:` spelling out what the wave does and does not cover.
//!
//! Classification (see the per-test NOTEs for detail):
//!   - Synchronous output, fully wave-tested:   throttle (leading edge),
//!     timeout (value pass-through), drop_oldest, drop_newest, sample_latest,
//!     concat, cold, finalize, retry, parallel.
//!   - Timer-driven output, input-side wave only: debounce, delay, buffer_time,
//!     audit.
//!   - Genuine torn-value (Template-A) candidates: NONE — all are single-input
//!     or sequential-two-input, documented above.
#![cfg(feature = "scheduler")]

use std::{
    sync::{
        Arc, Mutex,
        atomic::{AtomicI64, Ordering},
    },
    thread,
    time::Duration,
};

use hyphae::{
    AuditExt, BackpressureExt, BufferTimeExt, Cell, ColdExt, ConcatExt, DebounceExt, DelayExt,
    FinalizeExt, Gettable, MaterializeDefinite, MaterializeEmpty, Mutable, ParallelExt, RetryExt,
    Signal, ThrottleExt, TimeoutExt, Watchable, batch,
};

/// The scheduler's tick queue is one process-wide structure, so `#[test]` fns
/// (run as concurrent threads by the test harness) would otherwise interleave
/// their batches. Serialize every test in this file through one lock. Copied
/// verbatim from `scheduler_completeness.rs`.
fn scheduler_test_serial() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}

/// Width of every parallel wave below. 16 > PARALLEL_WAVE_THRESHOLD (8), so
/// each wave genuinely dispatches across rayon rather than taking the
/// small-wave sequential fallback.
const WIDE: usize = 16;

// ---------------------------------------------------------------------------
// Time-based operators
// ---------------------------------------------------------------------------

// NOTE: debounce's OUTPUT is timer-driven (`spawn_delayed`), so the derived
// cell never rides the batch wave — a wave-parallel test of the output is N/A.
// What IS wave-parallel here is the INPUT side: N independent sources set in one
// batch form a wide height-0 wave that runs N debounce callbacks concurrently,
// each arming its own generation-stamped timer. This pins that concurrent
// arming doesn't cross-wire generations between independent instances. Distinct
// per-instance values catch any such leak.
#[test]
fn debounce_wide_parallel_input_wave_all_fire() {
    let _serial = scheduler_test_serial();
    let dur = Duration::from_millis(30);

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.debounce(dur)).collect();

    // One wide batch: all WIDE source ops land at height 0 → parallel wave.
    batch(|| {
        for (i, s) in sources.iter().enumerate() {
            s.set(1000 + i as i64);
        }
    });

    // Output settles from the timer reactor after the debounce quiet period.
    thread::sleep(Duration::from_millis(200));
    for (i, out) in outs.iter().enumerate() {
        assert_eq!(
            out.get(),
            1000 + i as i64,
            "debounce instance {i} settled on the wrong value under a wide input wave"
        );
    }
}

// NOTE: throttle's leading emit is SYNCHRONOUS (`c.notify` runs inside the
// subscribe callback; the timer only re-opens the gate later), so the derived
// cell DOES ride the wave — this is a genuine wave-parallel output test. The
// construction-time replay consumes each instance's `can_emit` token, so we let
// the per-instance reset timers fire before each wide-wave set.
#[test]
fn throttle_wide_parallel_wave_leading_emit_correct() {
    let _serial = scheduler_test_serial();
    let dur = Duration::from_millis(20);

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.throttle(dur)).collect();

    for round in 0..3i64 {
        // Let every instance's `can_emit` reset before the next leading edge.
        thread::sleep(Duration::from_millis(60));
        let base = (round + 1) * 1000;
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(base + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                base + i as i64,
                "throttle instance {i} leading-edge emit wrong under a wide wave (round {round})"
            );
        }
    }
}

// NOTE: delay's output is timer-driven (`spawn_delayed` on every signal), so
// its output is out of the wave path — N/A. Wide-wave coverage is on the input
// side: N independent sources set in one batch run N delay callbacks
// concurrently, each scheduling its own delivery. Assert the delayed values all
// arrive intact and uncrossed.
#[test]
fn delay_wide_parallel_input_wave_all_fire() {
    let _serial = scheduler_test_serial();
    let dur = Duration::from_millis(30);

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.delay(dur)).collect();

    batch(|| {
        for (i, s) in sources.iter().enumerate() {
            s.set(2000 + i as i64);
        }
    });

    thread::sleep(Duration::from_millis(200));
    for (i, out) in outs.iter().enumerate() {
        assert_eq!(
            out.get(),
            2000 + i as i64,
            "delay instance {i} delivered the wrong value under a wide input wave"
        );
    }
}

// NOTE: timeout's value pass-through is SYNCHRONOUS (`d.notify(Value)` inside
// the callback), so the derived cell rides the wave — this is a genuine
// wave-parallel output test of the value path. The timeout-ERROR branch is the
// only timer-driven part and is orthogonal to the wave; a generous window keeps
// it from firing while we drive values. The loop stays well under that window.
#[test]
fn timeout_wide_parallel_wave_value_passthrough_correct() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 100;
    let dur = Duration::from_millis(500);

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.timeout(dur)).collect();

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                it * 1000 + i as i64,
                "timeout instance {i} passed a torn/stale value under a wide wave (iter {it})"
            );
        }
    }
}

// NOTE: buffer_time's output is timer-driven (`spawn_interval`), so the output
// is out of the wave path — N/A. Wide-wave coverage is on the input side: N
// independent sources push into N independent SegQueues concurrently in one wide
// height-0 wave. Each instance pushes exactly one value, so the flattened union
// of that instance's window emissions must be exactly its one value — no push
// lost, none leaked to a sibling buffer.
#[test]
fn buffer_time_wide_parallel_input_wave_all_collect() {
    let _serial = scheduler_test_serial();
    let dur = Duration::from_millis(30);

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.buffer_time(dur)).collect();

    // Accumulate every non-empty flattened emission per instance.
    let seen: Vec<Arc<Mutex<Vec<i64>>>> = (0..WIDE)
        .map(|_| Arc::new(Mutex::new(Vec::new())))
        .collect();
    let mut guards = Vec::new();
    for (i, out) in outs.iter().enumerate() {
        let sink = seen[i].clone();
        guards.push(out.subscribe(move |sig| {
            if let Signal::Value(v) = sig {
                sink.lock().unwrap().extend(v.iter().copied());
            }
        }));
    }

    batch(|| {
        for (i, s) in sources.iter().enumerate() {
            s.set(3000 + i as i64);
        }
    });

    // Let a couple of windows elapse so the pushed value is flushed.
    thread::sleep(Duration::from_millis(200));
    for (i, s) in seen.iter().enumerate() {
        assert_eq!(
            *s.lock().unwrap(),
            vec![3000 + i as i64],
            "buffer_time instance {i} lost/leaked its buffered value under a wide input wave"
        );
    }
    drop(guards);
}

// NOTE: audit's output is timer-driven (`spawn_delayed` at window open) and it
// samples the LAST value seen in the window via a per-instance Mutex — but that
// Mutex is never shared across instances, so there is no cross-instance combine
// to tear. Output is out of the wave path — N/A. Wide-wave coverage is on the
// input side: N independent sources each open a window concurrently in one wide
// height-0 wave; each must emit its own last value.
#[test]
fn audit_wide_parallel_input_wave_all_fire() {
    let _serial = scheduler_test_serial();
    let dur = Duration::from_millis(30);

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.audit(dur)).collect();

    batch(|| {
        for (i, s) in sources.iter().enumerate() {
            s.set(4000 + i as i64);
        }
    });

    thread::sleep(Duration::from_millis(200));
    for (i, out) in outs.iter().enumerate() {
        assert_eq!(
            out.get(),
            4000 + i as i64,
            "audit instance {i} sampled the wrong last value under a wide input wave"
        );
    }
}

// ---------------------------------------------------------------------------
// Backpressure operators (synchronous pass-through — full wave-parallel output)
// ---------------------------------------------------------------------------

// NOTE: drop_oldest emits synchronously on every value, so its derived cell
// rides the wave. Capacity is kept above the single per-batch push so no drop
// occurs and the settled value is exactly deterministic. Genuine wave-parallel
// output test across WIDE independent instances.
#[test]
fn backpressure_drop_oldest_wide_parallel_wave_correct() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.drop_oldest(8)).collect();

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                it * 1000 + i as i64,
                "drop_oldest instance {i} wrong under a wide wave (iter {it})"
            );
        }
    }
}

// NOTE: drop_newest emits synchronously when the buffer has room. Capacity is
// kept above the single per-batch push so the value is never dropped and the
// settled value is deterministic. Genuine wave-parallel output test.
#[test]
fn backpressure_drop_newest_wide_parallel_wave_correct() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    const CAP: i64 = 8;
    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources
        .iter()
        .map(|s| s.drop_newest(CAP as usize))
        .collect();

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            // drop_newest's ArrayQueue(CAP) is never drained (see the operator's
            // own `test_drop_newest`): after CAP accepted values it is full and
            // every later value is dropped with no emission. So each instance
            // passes values through for its first CAP iterations, then freezes at
            // its CAP-th value. The wide-wave property under test is that no
            // instance is corrupted by a concurrent sibling — each output only
            // ever holds ITS OWN source's values, frozen at exactly the right one.
            let expected = it.min(CAP) * 1000 + i as i64;
            assert_eq!(
                out.get(),
                expected,
                "drop_newest instance {i} wrong under a wide wave (iter {it})"
            );
        }
    }
}

// NOTE: sample_latest is a synchronous latest-value pass-through, so its derived
// cell rides the wave. Genuine wave-parallel output test.
#[test]
fn backpressure_sample_latest_wide_parallel_wave_correct() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.sample_latest()).collect();

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                it * 1000 + i as i64,
                "sample_latest instance {i} wrong under a wide wave (iter {it})"
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Structural operators
// ---------------------------------------------------------------------------

// NOTE: concat is a SEQUENTIAL two-input composition — only one input is live at
// a time, so it never *combines* the two and has no sibling state for a wave to
// tear (not a Template-A candidate). Its output forwards synchronously, so it
// rides the wave. This drives BOTH input sides under a wide parallel wave with
// the completion-driven hand-off in between, checking the per-instance
// first_skip/first_done/second_skip state survives concurrent settling.
#[test]
fn concat_wide_parallel_wave_both_sides_correct() {
    let _serial = scheduler_test_serial();

    let firsts: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let seconds: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = firsts
        .iter()
        .zip(seconds.iter())
        .map(|(f, s)| f.concat(s))
        .collect();

    // Phase 1: drive the FIRST input side under a wide wave.
    batch(|| {
        for (i, f) in firsts.iter().enumerate() {
            f.set(5000 + i as i64);
        }
    });
    for (i, out) in outs.iter().enumerate() {
        assert_eq!(
            out.get(),
            5000 + i as i64,
            "concat instance {i} wrong on the first-input side under a wide wave"
        );
    }

    // Hand off to the second input by completing every first source. Done
    // outside a batch so each second subscription is established synchronously.
    for f in &firsts {
        f.complete();
    }

    // Phase 2: drive the SECOND input side under a wide wave.
    batch(|| {
        for (i, s) in seconds.iter().enumerate() {
            s.set(6000 + i as i64);
        }
    });
    for (i, out) in outs.iter().enumerate() {
        assert_eq!(
            out.get(),
            6000 + i as i64,
            "concat instance {i} wrong on the second-input side after the completion hand-off"
        );
    }
}

// NOTE: cold is a single-input pipeline operator that swallows the first
// (retained) emission and lifts subsequent ones to Some(Arc<_>). Output forwards
// synchronously → rides the wave. Genuine wide-wave output test: each cold cell
// starts None and settles Some(value) after the first post-subscribe emission.
#[test]
fn cold_wide_parallel_wave_settles_some() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources
        .iter()
        .map(|s| s.clone().cold().materialize())
        .collect();

    // First-skip is consumed at construction (synchronous replay), so each
    // cold cell reads None before any batch.
    for (i, out) in outs.iter().enumerate() {
        assert_eq!(out.get(), None, "cold instance {i} should start None");
    }

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                Some(Arc::new(it * 1000 + i as i64)),
                "cold instance {i} settled wrong under a wide wave (iter {it})"
            );
        }
    }
}

// NOTE: finalize is a single-input pass-through with a once-only terminal
// callback. Values forward synchronously → the value path rides the wave. This
// checks value pass-through under a wide wave AND that each instance's
// OnceCallback fires exactly once on completion (the terminal path is not itself
// wave-parallel, but its once-guard is exercised across all instances).
#[test]
fn finalize_wide_parallel_wave_passthrough_and_terminal() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    let flags: Vec<Arc<AtomicI64>> = (0..WIDE).map(|_| Arc::new(AtomicI64::new(0))).collect();
    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources
        .iter()
        .zip(flags.iter())
        .map(|(s, flag)| {
            let flag = flag.clone();
            s.clone()
                .finalize(move || {
                    flag.fetch_add(1, Ordering::SeqCst);
                })
                .materialize()
        })
        .collect();

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                it * 1000 + i as i64,
                "finalize instance {i} passed a wrong value under a wide wave (iter {it})"
            );
        }
    }

    // Terminal: complete every source; each finalize callback fires exactly once.
    for s in &sources {
        s.complete();
    }
    for (i, flag) in flags.iter().enumerate() {
        assert_eq!(
            flag.load(Ordering::SeqCst),
            1,
            "finalize instance {i} terminal callback did not fire exactly once"
        );
    }
}

// NOTE: retry is single-input; on a value it passes through synchronously (the
// retry/resubscribe machinery only engages on Error, which is orthogonal to the
// wave). Value path rides the wave → genuine wide-wave output test of
// pass-through with a high attempt budget so no error path is taken.
#[test]
fn retry_wide_parallel_wave_value_passthrough() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let outs: Vec<_> = sources.iter().map(|s| s.retry(1_000)).collect();

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        for (i, out) in outs.iter().enumerate() {
            assert_eq!(
                out.get(),
                it * 1000 + i as i64,
                "retry instance {i} passed a wrong value under a wide wave (iter {it})"
            );
        }
    }
}

// NOTE: `parallel()` fans OUT to its own subscribers via rayon directly, inside
// the source op — it bypasses the scheduler queue for its fan-out. Driving WIDE
// independent sources in one batch makes a wide height-0 wave (itself on rayon)
// whose ops each run parallel's nested rayon fan-out: rayon-within-rayon. This
// checks that nested parallel dispatch delivers each source's value to its own
// subscriber intact, with no cross-instance leak. Not a torn-value candidate
// (single input, per-instance subscriber set).
#[test]
fn parallel_wide_parallel_input_wave_correct() {
    let _serial = scheduler_test_serial();
    const ITERS: i64 = 200;

    let sources: Vec<Cell<i64, _>> = (0..WIDE).map(|_| Cell::new(0i64)).collect();
    let cells: Vec<_> = sources.iter().map(|s| s.parallel()).collect();

    let slots: Vec<Arc<AtomicI64>> = (0..WIDE)
        .map(|_| Arc::new(AtomicI64::new(i64::MIN)))
        .collect();
    let mut guards = Vec::new();
    for (i, cell) in cells.iter().enumerate() {
        let slot = slots[i].clone();
        guards.push(cell.subscribe(move |sig| {
            if let Signal::Value(v) = sig {
                slot.store(**v, Ordering::SeqCst);
            }
        }));
    }

    for it in 1..=ITERS {
        batch(|| {
            for (i, s) in sources.iter().enumerate() {
                s.set(it * 1000 + i as i64);
            }
        });
        // parallel's fan-out is synchronous within the wave op, so all slots
        // are settled by the time the outermost same-thread batch returns.
        for (i, slot) in slots.iter().enumerate() {
            assert_eq!(
                slot.load(Ordering::SeqCst),
                it * 1000 + i as i64,
                "parallel instance {i} delivered a wrong value under a wide wave (iter {it})"
            );
        }
    }
    drop(guards);
}