foundation_core 0.0.6

Central crate for all foundation crates for the ewe-platform project
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
# Valtron
An experimental async runtime built on iterators that do not specifically rely on the async future paradigm nor follow the current `async/wait` paradigm most know for a more elaborate iterator and trait syntax that both provide a unified set of principle and control.

## Troubleshooting

### Multi-threaded Executor: Tests Hanging or Not Receiving Values

**Symptom:** Integration tests using `WebSocketClient`, `DrivenStreamIterator`, or other multi-threaded valtron-based iterators hang indefinitely or return `Stream::Ignore` immediately without processing any values.

**Cause:** The `PoolGuard` returned by `initialize_pool()` must be kept alive for the duration of test execution. Dropping it (e.g., `let _ = initialize_pool(...)`) immediately shuts down all worker threads, preventing any task execution.

In multi-threaded mode (`feature = "multi"`), tasks run on worker threads managed by the pool. When `PoolGuard` is dropped, the pool shuts down and all worker threads exit. This means queued tasks never execute, so the concurrent queue never receives any values. The iterator then yields `Stream::Ignore` or loops waiting for values that will never arrive.

**Fix:** Keep `PoolGuard` alive for the test duration:

```rust
// WRONG - guard dropped immediately, worker threads shut down
let _ = foundation_core::valtron::initialize_pool(42, None);

// CORRECT - guard kept alive for test duration
let _pool_guard: PoolGuard = foundation_core::valtron::initialize_pool(42, None);
```

**Note:** An initial fix attempt added a `thread::sleep` wait loop in `run_until_stream_has_value` for multi-threaded mode, but this was unnecessary. The real issue was the dropped `PoolGuard` - once the guard is kept alive, worker threads remain active and tasks execute normally.

**Files affected:**
- `tests/backends/foundation_core/websocket/echo_tests.rs` - Test fix
- `tests/backends/foundation_core/websocket/subprotocol_tests.rs` - Test fix
- `tests/backends/foundation_core/websocket/reconnection_tests.rs` - Test fix
- `tests/backends/foundation_core/event_source/consumer_integration_tests.rs` - Test fix
- `backends/foundation_core/src/valtron/executors/drivers.rs` - Removed unnecessary multi-threaded wait code

---

### Multi-threaded Executor: Parallel Test Interference

**Symptom:** Integration tests using `initialize_pool()` exhibit intermittent hangs or timeouts when running tests in parallel. Different tests fail on each run, with symptoms showing worker threads starting but never completing, or WebSocket connections established but messages never received.

**Root Cause:** The valtron executor uses global static registries (`REGISTRY` and `BG_REGISTRY`) to store the `ThreadRegistry` and `BackgroundJobRegistry`. When tests run in parallel:

1. Test A starts, calls `initialize_pool()` → sets `REGISTRY = Arc_A`
2. Test B starts, calls `initialize_pool()` → **overwrites** `REGISTRY = Arc_B`
3. Test A's worker threads now reference the wrong registry
4. When Test A's `PoolGuard` drops, it shuts down workers that may be serving Test B

This race condition causes intermittent failures that are non-deterministic and vary between test runs.

**Fix:** Tests that use `initialize_pool()` must be serialized using the `#[serial]` attribute with a named lock group. All tests within the same file share the same lock name:

```rust
use serial_test::serial;

// All tests in this file use the same serial lock to prevent PoolGuard interference
#[test]
#[traced_test]
#[ntest::timeout(60000)]
#[serial(websocket_pool)]  // Named lock groups tests by feature area
fn test_text_message_echo() {
    let _pool_guard: PoolGuard = foundation_core::valtron::initialize_pool(42, None);
    // ... test code
}
```

**Named lock groups by test file:**
- `echo_tests.rs` → `#[serial(websocket_pool)]`
- `subprotocol_tests.rs` → `#[serial(subprotocol_tests)]`
- `reconnection_tests.rs` → `#[serial(reconnection_tests)]`
- `http_redirect_integration.rs` → `#[serial(http_redirect)]`
- `consumer_integration_tests.rs` → `#[serial(sse_consumer_tests)]`

**Why not fix the global state?** The global registry design allows for a simple single-instance executor model. Adding per-test isolation would require significant architectural changes. The `#[serial]` attribute provides a pragmatic solution that:
- Keeps tests isolated with their own clean pool
- Allows parallel execution across different test groups (websocket vs http vs event_source)
- Maintains test independence without shared state

**Additional safeguards:**
- `ntest::timeout` attribute on all integration tests prevents indefinite hangs
- `PoolGuard::drop()` now clears global registries to prevent state leakage between sequential tests

**Files updated:**
- `backends/foundation_core/src/valtron/executors/threads.rs` - Added cleanup callback to PoolGuard
- `backends/foundation_core/src/valtron/executors/multi/mod.rs` - PoolGuard::with_cleanup() clears registries on drop
- All integration test files in `tests/backends/foundation_core/` - Added `#[serial(...)]` attributes

---

## Tasks
Tasks in Valtron are a custom iterator and in another sense can also become regular interators, they specifically provide a way to represent a asynchronous task as a series of values that can be iterated over and also provide a clear distinction when they are busy or when they are ready to give you a value.

```rust
use foundation_core::{
    valtron::{FnReady, TaskIterator},
};

struct Counter(usize, usize);

impl Counter {
    pub fn new(val: usize) -> Self {
        Self(0, val)
    }
}

impl TaskIterator for Counter {
    /// Pending means how we wish to communicate that
    /// the task is not yet ready to send its next `Done`
    /// value
    type Pending = ();

    /// Done means the actual value we really care about
    /// it can be returend as many times as possible.
    type Ready = usize;

    fn next(
        &mut self,
    ) -> Option<crate::valtron::TaskStatus<Self::Ready, Self::Pending>> {
        let current = self.0;
        let next = current + 1;

        if next == self.0 {
            return None;
        }

        self.0 = next;
        Some(crate::valtron::TaskStatus::Ready(next))
    }
}
```

## Executors
The crate provides two main means of driving execution:

### Mulit-Threaded Executor

Provided in the [multi](./executors/multi) module which allows you start a multi-threaded executor that utilizes as much threads as you allow it where work is distributed across these threads but one unique behaviour of the executor is that they never steal work, once a job has being allocated to a thread executor that job is forever owned by it till completion hence removing concerns on runtime cost to move work across threads and memory boundaries.

#### Example 1

```rust
use core::time;
use std::thread;

use rand::RngCore;
use foundation_core::valtron::FnReady;
use foundation_core::valtron::multi::{block_on, get_pool};

fn main() {
    let seed = rand::thread_rng().next_u64();

    let handler_kill = thread::spawn(move || {
        thread::sleep(time::Duration::from_millis(3000));
        get_pool().kill();
    });

    let _guard = initialize_pool(seed, None);
    block_on(seed, None, |pool| {
        pool.spawn()
            .with_task(Counter::new(5))
            .with_resolver(Box::new(FnReady::new(|item, _executor| {
                // We can hook into the value returned by the task
                // for us to perform something with it or spawn some other task
                // from that value.
                println!("Received next: {}", item);
            })))
            .schedule()
            .expect("should deliver task");
    });

    handler_kill.join().expect("should finish");
}
```

#### Example 2


```rust
use core::time;
use std::thread;

use rand::RngCore;
use foundation_core::valtron::FnReady;
use foundation_core::valtron::multi::{block_on, get_pool};

fn main() {
    let seed = rand::thread_rng().next_u64();

    let handler_kill = thread::spawn(move || {
        thread::sleep(time::Duration::from_millis(3000));
        get_pool().kill();
    });

    let _guard = initialize_pool(seed, None);
    // spawn a task and get back a iterator that returns the state 
    // this means it return immediately with Pending or Done, so it never 
    // blocks but lets you spin things or control polling of iterator.
    for status in pool.spawn().with_task(Counter::new(5)).iter() {
        // do something with the status
    }

    handler_kill.join().expect("should finish");
}
```

#### Example 3


```rust
use core::time;
use std::thread;

use rand::RngCore;
use foundation_core::valtron::FnReady;
use foundation_core::valtron::multi::{block_on, get_pool};

fn main() {
    let seed = rand::thread_rng().next_u64();

    let handler_kill = thread::spawn(move || {
        thread::sleep(time::Duration::from_millis(3000));
        get_pool().kill();
    });

    let _guard = initialize_pool(seed, None);
    // spawn a task but block the thread till we get a Done 
    // item, so it consumes the pending and blocks till 
    // a `Done` is received.
    //
    // Its iterable until all the the None value is received that 
    // closes the iterator.
    for item in pool.spawn().with_task(Counter::new(5)).blocking_iter() {
        // do something with the item
    }

    handler_kill.join().expect("should finish");
}
```

### Single-Threaded Executor
Valtron also provides a single-threaded module that provides a more finegrained and controllable executor useful in environments like WebAssembly or embedded systems. We get great control to determine when a executor actually processes all queued up tasks to completion.

A benefit of single-threaded executor is the freedom to not be forced to think in multi-threaded contexts and require use of Send-safe smart pointers and wrappers but work with normal smart pointers like RefCell and Rc.

#### SingleExecutorSingleton (wasm32/wasm64 only)

On wasm32/wasm64 targets (when `multi` feature is off), valtron provides `SingleExecutorSingleton` — a convenience wrapper that combines initialization + guard ownership:

```rust
use foundation_core::valtron::SingleExecutorSingleton;

// Initialize executor and get guard in one call
let _guard = SingleExecutorSingleton::get_or_init(42, |_| {});

// Later calls return the same guard (zero-sized, cheap clone)
let guard = SingleExecutorSingleton::guard();

// Schedule and run
spawn()
    .with_task(Counter::new(5, Rc::new(RefCell::new(Vec::new()))))
    .with_resolver(Box::new(FnReady::new(|item, _| {
        println!("Got: {:?}", item);
    })))
    .schedule()
    .expect("should deliver task");

run_until_complete();
```

The existing free functions (`initialize_pool`, `spawn`, `run_until_complete`) continue to work independently — `SingleExecutorSingleton` is optional.

#### Free function API (always available)

```rust
use std::{cell::RefCell, rc::Rc};

use rand::RngCore;
use tracing_test::traced_test;

use foundation_core::valtron::{
    single::{initialize_pool, run_until_complete, spawn},
    FnReady, TaskIterator,
};

struct Counter(usize, Rc<RefCell<Vec<usize>>>);

impl Counter {
    pub fn new(val: usize, list: Rc<RefCell<Vec<usize>>>) -> Self {
        Self(val, list)
    }
}

impl TaskIterator for Counter {
    type Pending = ();

    type Ready = usize;

    fn next_status(
        &mut self,
        _ctx: crate::valtron::Ctx,
    ) -> Option<crate::valtron::TaskStatus<Self::Ready, Self::Pending>> {
        let item_size = self.1.borrow().len();

        if item_size == self.0 {
            return None;
        }

        self.1.borrow_mut().push(item_size);

        Some(crate::valtron::TaskStatus::Ready(self.1.borrow().len()))
    }
}

```

#### Example 1

```rust

fn main() {
    let seed = rand::thread_rng().next_u64();

    let shared_list = Rc::new(RefCell::new(Vec::new()));
    let counter = Counter::new(5, shared_list.clone());

    // initialize executor with a predictable seed
    // your task can get a random number generator that
    // can provide predictable random numbers every single
    // time if the seed provided is the same.
    let _guard = initialize_pool(seed, None);

    // spawn a task.
    spawn()
        .with_task(counter)
        .with_resolver(Box::new(FnReady::new(|item, _| {
            tracing::info!("Received next: {:?}", item)
        })))
        .schedule()
        .expect("should deliver task");

    // run tasks queued to completion
    run_until_complete();

    // check output is what we expect
    let items = shared_list.borrow().clone(); // should be: vec![0, 1, 2, 3, 4]);
}
```

#### Example 2

```rust

fn main() {
    let seed = rand::thread_rng().next_u64();

    let shared_list = Rc::new(RefCell::new(Vec::new()));
    let counter = Counter::new(5, shared_list.clone());

    // initialize executor with a predictable seed
    let _guard = initialize_pool(seed, None);

    // spawn a task and get back a iterator that returns the state
    // this means it return immediately with Pending or Done, so it never
    // blocks but lets you spin things or control polling of iterator.
    for status in spawn().with_task(counter).schedule_iter(std::time::Duration::from_nanos(50))
        .expect("should deliver task")
    {
        // do something with status
    }

    // check output is what we expect
    let items = shared_list.borrow().clone(); // should be: vec![0, 1, 2, 3, 4]);
}
```

#### Example 3

```rust

fn main() {
    let seed = rand::thread_rng().next_u64();

    let shared_list = Rc::new(RefCell::new(Vec::new()));
    let counter = Counter::new(5, shared_list.clone());

    // initialize executor with a predictable seed
    let _guard = initialize_pool(seed, None);

    // spawn a task but block the thread till we get a Done
    // item, so it consumes the pending and blocks till
    // a `Done` is received.
    //
    // Its iterable until all the the None value is received that
    // closes the iterator.
    for status in spawn().with_task(counter).schedule_iter(std::time::Duration::from_nanos(50))
        .expect("should deliver task")
    {
        // do something with status
    }

    // check output is what we expect
    let items = shared_list.borrow().clone(); // should be: vec![0, 1, 2, 3, 4]);
}
```

---

## StreamIterator — Send vs Non-Send Variants

Valtron provides two parallel implementations of `StreamIteratorExt` (the combinator methods like `map_done`, `map_pending`, `map_circuit`):

| Feature | `StreamIterator` blanket impl | `StreamIteratorExt` source | Send bound |
|---------|------------------------------|---------------------------|------------|
| `multi` | `D: Send + 'static, P: Send + 'static` | `extensions/streams/sendable.rs` | Required |
| `not(multi)` | `D: 'static, P: 'static` | `extensions/streams/non_sendable.rs` | Not required |

The blanket `StreamIterator` impl is cfg-gated to match: on `multi` it requires `Send` bounds, on non-`multi` (wasm32, single-threaded native) it only requires `'static`. This means `DrivenStreamIterator<FutureTask<F>>` — whose `Ready` is `Result<T, E>` (not `Send` when `T`/`E` aren't) and whose `Pending` is `()` — correctly implements `StreamIterator` on the non-`multi` path and gets access to `StreamIteratorExt` combinators.

### What this means for downstream code

You use the same API regardless of target:

```rust
use foundation_core::valtron::{from_future, execute, StreamIteratorExt};

let task = from_future(my_async_fn());
let stream = execute(task, None)?;

// Works on both multi (Send) and non-multi (non-Send) targets.
// The correct StreamIteratorExt implementation is selected at compile time.
let mapped = stream
    .map_done(|result| result.map_err(MyError::from))
    .map_pending(|_| ());
```

No `#[cfg(target_arch = "wasm32")]` or feature-gated imports needed in downstream code. The executor (`execute`, `from_future`) and the combinator trait (`StreamIteratorExt`) are always exported from `foundation_core::valtron` — the cfg selection happens entirely within valtron itself.

### Why two implementations

The `multi` executor runs tasks on background worker threads, so all task outputs (`Ready`, `Pending`) must be `Send` to cross thread boundaries. The single-threaded executor (used on wasm32 and native without `multi`) runs everything on the current thread, so no `Send` bound is needed. The `StreamIteratorExt` combinator types (`MapDone`, `MapPending`, etc.) store boxed closures — on the `multi` path those closures must be `Send`, on the non-`multi` path they don't need to be.

---

## ⚠️ CRITICAL: TaskIterator/Iterator Recursion Trap

**Location:** `task.rs:311-401`

**Problem:** The `TaskIterator` trait has a blanket implementation for any type that implements `Iterator<Item = TaskStatus<...>>`. This creates bidirectional coherence:

```rust
// Any Iterator<TaskStatus<...>> automatically implements TaskIterator
impl<I, D, P> TaskIterator for I
where
    I: Iterator<Item = TaskStatus<D, P>>,
    ...
```

**The Trap:** Wrapper types that implement BOTH `Iterator` and `TaskIterator` will infinitely recurse on `next_status()`:

```rust
// WRONG - implements both, causes infinite recursion!
pub struct BadWrapper<I: TaskIterator> {
    inner: I,
}

impl<I: TaskIterator> Iterator for BadWrapper<I> {
    type Item = TaskStatus<I::Ready, I::Pending>;
    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next_status() // Calls Iterator::next() due to blanket impl
        // Which calls next_status() again... infinite loop!
    }
}

impl<I: TaskIterator> TaskIterator for BadWrapper<I> {
    // Blanket impl provides this, causing the recursion
}
```

**The Solution:** Wrapper types should:
1. Implement `TaskIterator` with explicit `next_status()`
2. Delegate to `inner.next_status()` (not `Iterator::next()`)
3. Use `Box<dyn TaskIterator>` to break the cycle via `as_mut().next_status()`

```rust
// CORRECT - explicit TaskIterator impl
pub struct GoodWrapper<I: TaskIterator> {
    inner: I,
}

impl<I: TaskIterator> TaskIterator for GoodWrapper<I> {
    type Ready = I::Ready;
    type Pending = I::Pending;

    fn next_status(&mut self, ctx: Ctx) -> Option<TaskStatus<Self::Ready, Self::Pending>> {
        self.inner.next_status(ctx) // Direct delegation, no recursion
    }
}
```

**Bottom Line:** Never implement both `Iterator<Item = TaskStatus<...>>` and `TaskIterator` on the same type. The blanket impl makes them mutually exclusive. Use `Box<dyn TaskIterator>` or explicit delegation to break the cycle.