rust-elm 0.9.0

Elm Architecture for Rust: composable reducers, pure effects, async runtime.
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
# Store

The `Store` is the **handle** to a running app. It does not own state — it shares the
runtime's `Arc<Mutex<S>>` and the action bus. You dispatch actions through it, read
snapshots from it, scope it to a child, and subscribe to state changes.

All examples below use the shop from [`examples/ecommerce.rs`](../examples/ecommerce.rs):
`ShopState` (root), `CartState` / `CheckoutState` / `WishlistState` (children), and the
`ShopAction` enum tree.

---

## 1. Anatomy

A `Store<S, M>` is a thin, cloneable wrapper over a shared `StoreBackend<S, M>`:

```62:102:rust-elm/src/runtime/store.rs
pub struct Store<S, M> {
    pub(crate) backend: StoreBackend<S, M>,
}

impl<S, M> Clone for Store<S, M>
where
    S: Send + 'static,
    M: Send + 'static,
{
    fn clone(&self) -> Self {
        Self {
            backend: self.backend.clone(),
        }
    }
}

pub(crate) struct StoreBackend<S, M> {
    pub state: Arc<Mutex<S>>,
    pub sender: BusSender<M>,
    state_listeners: Arc<Mutex<Vec<Sender<()>>>>,
    effect_done: Arc<Mutex<VecDeque<Sender<()>>>>,
    in_flight: Arc<AtomicUsize>,
    pub interpreter: Arc<InterpreterState<M>>,
}
```

Every field is an `Arc`, so `store.clone()` is cheap — all clones point at the same
state, bus, and listener list. You can hand a clone to every worker thread / UI surface.

```
            ┌─────────────── one StoreBackend (all Arc) ───────────────┐
 Store ───► │ state: Arc<Mutex<S>>   sender: Bus   listeners: Vec<tx>   │
 Store ───► │           ▲                  │              ▲             │
 Store ───► │           │ reducer thread   │ bus          │ notify      │
            └───────────┼──────────────────┼──────────────┼────────────┘
                        │                  ▼              │
                    locks state      reducer applies   sends () to
                    & mutates        action → state     each listener
```

**Key rule:** the reducer runs on the runtime's dedicated thread, not on your caller's
thread. `send` / `dispatch` only enqueue — they never run `update` inline. This is why
reducers can't re-enter themselves from your call stack (UDF parity).

---

## 2. How do you bind a view with state?

Two halves: **read** state out, **dispatch** actions in.

> **Zero-copy path (recommended):** see [binding.md]./binding.md for `StateBinding`,
> keypath projection, and `subscribe_changes()` — no full-state clone on the hot path.

### Read: subscribe to snapshots (legacy)

```189:200:rust-elm/src/runtime/store.rs
    pub fn subscribe_state(&self) -> StateSubscriber<S>
    where
        S: Clone + PartialEq,
    {
        let (tx, rx) = crossbeam_channel::unbounded();
        self.backend.state_listeners.lock().push(tx);
        StateSubscriber {
            rx,
            state: self.backend.state.clone(),
            last: Some(Arc::new(self.backend.state.lock().clone())),
        }
    }
```

`last` is the subscriber's **last emitted snapshot**. It is touched in three places,
all inside `StateSubscriber` (`rust-elm/src/runtime/store.rs`):

| Where | What `last` does |
|-------|------------------|
| `subscribe_state` (above) | Seed with the current state so the first `next()` does **not** fire for state that already existed at subscribe time. |
| `next()` | Dedup: after coalescing channel ticks, compare the fresh snapshot against `last` with `PartialEq`; skip if equal (notify without real change). On a real change, `last = Some(snapshot.clone())` — an `Arc` refcount bump, not a deep clone of `S`. |
| `latest()` | Return `self.last.clone()` — the most recent snapshot the subscriber yielded, without waiting on the channel. |

The same subscriber pattern exists on [`Shared`](rust-elm/src/infra/shared.rs) as
`SharedSubscriber.last: Option<T>` (plain `T`, not `Arc`). Initialization,
dedup-in-`next()`, and `latest()` work the same way; only the snapshot type differs.
See [§5](#5-how-does-subscribe_state-work-and-why-clone-last) for the `next()` loop in
full.

A view loop binds like this (from the ecommerce demo):

```rust
let store: Store<ShopState, ShopAction> = runtime.store();
let mut sub = store.subscribe_state();

// user interaction → dispatch
store.dispatch(ShopAction::Catalog(CatalogAction::SetQuery("hoodie".into())));

// state change → re-render
if let Some(snapshot) = sub.wait_next(Duration::from_secs(2)) {
    // snapshot: Arc<ShopState> — render it
    if let Some(detail) = snapshot.catalog.detail.as_ref() {
        render(detail);
    }
}
```

- `next()` — non-blocking poll; returns `Some(Arc<S>)` only if state changed.
- `wait_next(timeout)` — blocks up to timeout (polls every 5 ms internally).
- `latest()` — last snapshot you saw, no waiting.

The cycle is: **dispatch → reducer mutates state → backend notifies listeners →
subscriber yields a new `Arc<S>` → view renders.**

### Write: dispatch / send

| Call | Behaviour |
|------|-----------|
| `store.dispatch(a)` | fire-and-forget |
| `store.send(a)` | returns `StoreTask`; `.finish()` waits until that action's effects complete |

---

## 3. How do you bind a view with state — no clone / no copy?

Be aware of what actually clones today:

- `store.state()` clones the **whole** `S` (`self.backend.state.lock().clone()`).
- `subscribe_state()` clones once at creation, and `next()` clones once per change.

`Store` does **not** hand out a `&S` reference, because the canonical state lives behind
a `Mutex` owned by the reducer thread; lending a borrow across that boundary isn't sound.
So "zero clone" needs one of these strategies:

**a) Lock the runtime's state directly (true zero-clone read).**
`Runtime.state` is public, so a reader on the same process can take the guard:

```rust
let guard = runtime.state.lock();   // parking_lot::MutexGuard<ShopState>
render(&guard.cart);                 // &CartState — no clone
// drop(guard) ASAP — you are blocking the reducer thread while held
```

This borrows `&S` with no clone/copy, but it **contends with the reducer**. Hold it for
the shortest possible time (read the fields you need, drop the guard).

**b) Make `Clone` shallow by putting big payloads behind `Arc`.**
If `ShopState` fields are `Arc<…>`, then the `clone()` inside `state()` /
`subscribe_state()` only bumps refcounts — no deep copy. The snapshot you get is
`Arc<ShopState>`, and sharing it to a view is another refcount bump:

```rust
let snap: Arc<ShopState> = sub.next().unwrap(); // cloning Arc<S> = refcount++
view.bind(snap);                                 // share, don't deep-copy
```

This is the recommended pattern for big state: the subscriber already yields `Arc<S>`, so
**downstream** binding is clone-free; only the inner `S` construction matters, and `Arc`
fields keep that shallow.

**c) Scope so you observe only a small child.** A `ScopedStateSubscriber` clones the
parent to diff, then focuses the child — see §6. The child you bind is small.

> Not available today: a `&S` / `Arc<S>`-returning accessor on `Store` itself, and a
> subscriber that yields without any clone. The dedup logic (§5) requires owning a
> snapshot to compare against. If you need fully borrow-based binding, read via (a) or
> structure state with `Arc`/`Shared` (see `infra::shared::Shared`).

---

## 4. Why are state listeners a `Vec`, and why behind a `Mutex`?

```81:81:rust-elm/src/runtime/store.rs
    state_listeners: Arc<Mutex<Vec<Sender<()>>>>,
```

**Why a `Vec` (not a single entry):** a store can have *many* simultaneous observers —
the main view, a debug panel, a logger, several `ScopedStore` subscribers, etc. Each
`subscribe_state()` pushes its **own** channel sender, so every subscriber gets its own
independent notification stream. One entry could only ever feed one observer.

**Why a `Mutex`:** the listener list is touched from different threads at once —
subscribers are added from arbitrary caller threads (`push(tx)` in `subscribe_state`),
while the **reducer thread** iterates the list to notify after each reduce:

```142:146:rust-elm/src/runtime/store.rs
    pub fn notify_state(&self) {
        self.state_listeners
            .lock()
            .retain(|tx| tx.send(()).is_ok());
    }
```

The `Mutex` makes add-vs-notify races safe. `retain(... send().is_ok())` doubles as
cleanup: when a subscriber is dropped its receiver disconnects, `send` fails, and the dead
sender is pruned automatically. The notification payload is just `()` — a "state changed"
tick; the subscriber then reads the real snapshot from the shared `Arc<Mutex<S>>`.

---

## 5. How does `subscribe_state` work, and why clone `last`?

The subscriber holds the receiver, a handle to shared state, and the **last snapshot it
emitted**:

```264:302:rust-elm/src/runtime/store.rs
impl<S: PartialEq + Clone> StateSubscriber<S> {
    #[allow(clippy::should_implement_trait)]
    pub fn next(&mut self) -> Option<Arc<S>> {
        loop {
            match self.rx.try_recv() {
                Ok(()) => {
                    while self.rx.try_recv().is_ok() {}
                    let snapshot = Arc::new(self.state.lock().clone());
                    if self
                        .last
                        .as_ref()
                        .is_some_and(|prev| prev.as_ref() == snapshot.as_ref())
                    {
                        continue;
                    }
                    self.last = Some(snapshot.clone());
                    return Some(snapshot);
                }
                Err(crossbeam_channel::TryRecvError::Empty) => return None,
                Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
            }
        }
    }
```

Step by step:

1. **Coalesce**`while self.rx.try_recv().is_ok() {}` drains all pending ticks, so a
   burst of N reduces becomes at most **one** render.
2. **Snapshot** — clone the current state into an `Arc<S>` once.
3. **Dedup** — compare against `last` with `PartialEq`. If equal, skip (`continue`) — the
   state was notified but is identical (e.g. an action that didn't change anything).
4. **Remember** — store `last = Some(snapshot.clone())` and return the snapshot.

**Why clone `last`:** dedup needs to compare the *next* snapshot against the *previous*
one, so the subscriber must **own** the previous value. Since the emitted value is an
`Arc<S>`, `snapshot.clone()` is just a refcount bump — one `Arc` goes back to the caller,
an identical `Arc` is retained in `last`. It's not a deep copy of `S`.

**Why `last` starts at the current state** (in `subscribe_state`): so a brand-new
subscriber doesn't immediately fire for the state that already existed when it subscribed —
`next()` only yields on the *first real change* after subscription.

The same three touch points exist on `SharedSubscriber` in
`rust-elm/src/infra/shared.rs` (`last: Option<T>` instead of `Option<Arc<S>>`); see the
table in [§2](#2-how-do-you-bind-a-view-with-state).

---

## 6. How does `scope` work, and in what order?

There are **two** different "scopes". Don't confuse them.

### 6a. `Store::scope` — a child *handle* (runtime side)

```203:223:rust-elm/src/runtime/store.rs
    pub fn scope<CS, CM, AK, SK>(
        &self,
        state_kp: SK,
        action_kp: AK,
    ) -> ScopedStore<S, M, CS, CM, AK, SK>
    where
        CS: Clone + PartialEq + Send + Sync + 'static,
        CM: Clone + Send + 'static,
        S: 'static,
        M: 'static,
        AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
        SK: RefKpTrait<S, CS> + Clone,
    {
        ScopedStore {
            store: self.clone(),
            state_kp,
            action_kp,
            _marker: PhantomData,
        }
    }
```

A `ScopedStore` **shares the same backend** (it holds `self.clone()`) plus two optics:

- `state_kp: RefKpTrait<S, CS>` — focus parent state → child state (read side).
- `action_kp: Casepath<M, CM>` — wrap child action → parent action (write side).

From the shop:

```rust
let cart_store = store.scope(cart_lens(), ShopAction::cart_cp());
cart_store.dispatch(CartAction::Line(0, CartLineAction::Inc));
```

When you dispatch on the child, it **wraps** the child action into a parent action and
sends it on the *same* bus:

```343:349:rust-elm/src/runtime/store.rs
    pub fn send(&self, action: CM) -> StoreTask {
        self.store.send(self.action_kp.wrap(action))
    }

    pub fn dispatch(&self, action: CM) {
        self.store.dispatch(self.action_kp.wrap(action));
    }
```

So `cart_store.dispatch(CartAction::Line(0, Inc))` is exactly
`store.dispatch(ShopAction::Cart(CartAction::Line(0, Inc)))` — both lines in the example do
the same thing. Reading focuses the child out of a parent snapshot:

```351:353:rust-elm/src/runtime/store.rs
    pub fn child_state(&self) -> Option<CS> {
        self.state_kp.focus(&self.store.state()).cloned()
    }
```

**Order for `Store::scope`:**
1. `action_kp.wrap(child_action)` → parent action
2. parent action → bus → reducer thread
3. root reducer runs (see 6b for *its* order), state mutates under the `Mutex`
4. listeners notified; a `ScopedStateSubscriber` focuses child state back out

### 6b. `ScopeReducer` — a child *reducer* (reduce side, ordering matters)

`ScopeReducer` is the composition piece used inside the root reducer:

```52:62:rust-elm/src/compose/scope.rs
    fn reduce(&self, state: &mut PS, action: PA) -> Cmd<PA> {
        let Some(child_action) = self.action_kp.extract(&action) else {
            return Cmd::none();
        };
        let Some(child) = self.state_kp.focus_mut(state) else {
            return Cmd::none();
        };
        let cmd = self.child.reduce(child, child_action);
        lift_cmd(cmd, self.action_kp.clone(), self.cancel_id)
    }
```

Order inside one reduce:
1. **Extract** — does this parent action belong to my case? If not, `Cmd::none()` (no-op).
2. **Focus** — get `&mut child` out of parent state. If absent, `Cmd::none()`.
3. **Delegate** — run the child reducer on child state.
4. **Lift** — map the child's `Cmd` back into the parent action space and tag effects with
   `cancel_id` (so the child's in-flight effects can be cancelled on dismiss/removal).

In the shop, the root `shop_reducer` is a `CombineReducers` tuple; its **slot order** is
the execution order for a single action:

```431:455:rust-elm/examples/ecommerce.rs
fn shop_reducer() -> impl Reducer<State = ShopState, Action = ShopAction> {
    CatchReducer::new(
        CombineReducers((
            Reduce::new(subscription_metrics_reducer),
            Reduce::new(detail_cart_bridge_reducer),
            Reduce::new(global_reducer),
            ScopeReducer::new(
                ShopState::catalog(),
                ShopAction::catalog_cp(),
                CATALOG_CANCEL,
                Reduce::new(catalog_reducer),
            ),
            ScopeReducer::new(
                ShopState::cart(),
                ShopAction::cart_cp(),
                CART_CANCEL,
                Reduce::new(cart_reducer),
            ),
            checkout_and_wishlist_reducer(),
        )),
        ...
```

`CombineReducers` runs each slot **in tuple order**, cloning the action for each and
batching the resulting commands:

```71:74:rust-elm/src/compose/reducer.rs
            fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
                let ($($R,)+) = &self.0;
                Cmd::batch([$( $R.reduce(state, action.clone()) ),+])
            }
```

So a `ShopAction::Cart(...)` flows: metrics (no-op) → bridge (no-op) → global (no-op) →
catalog scope (extract fails, no-op) → **cart scope (matches → runs `cart_reducer`)** →
checkout/wishlist (no-op). Only the matching scope mutates; the rest extract-fail cheaply.

---

## 7. Does sending an action through the store update the state in the mutex?

**Yes — asynchronously.** `send` only enqueues:

```165:176:rust-elm/src/runtime/store.rs
    pub fn send(&self, action: M) -> StoreTask {
        let (tx, rx) = crossbeam_channel::bounded(1);
        self.backend.effect_done.lock().push_back(tx);
        self.backend.begin_store_work();
        let _ = self.backend.sender.send_blocking(action);
        StoreTask { rx }
    }

    /// Fire-and-forget dispatch.
    pub fn dispatch(&self, action: M) {
        let _ = self.send(action);
    }
```

The action goes onto the bus; the **reducer thread** picks it up, locks
`Arc<Mutex<S>>`, runs the reducer (mutating the state in place), then unlocks and notifies
listeners. So after `dispatch` returns, the mutation has been *scheduled*, not necessarily
*applied* yet. To observe it deterministically:

- `store.send(a).finish()` — wait for that action's effects to complete, or
- `sub.wait_next(timeout)` — wait for the next state change, or
- (tests) sleep / `ExhaustiveTestStore`.

This is why the ecommerce demo sleeps between dispatch and `store.state()`.

---

## 8. Does sending through `let cs = store.scope(...)` update the state in the mutex?

**Yes — same mechanism, same mutex.** A `ScopedStore` holds a clone of the *same*
backend, so `cs.dispatch(child)` just wraps and forwards onto the *same* bus
(`self.store.dispatch(self.action_kp.wrap(action))`). There is no separate state — the
root `Arc<Mutex<ShopState>>` is the only state. These two are identical in effect:

```rust
let cart_store = store.scope(cart_lens(), ShopAction::cart_cp());
cart_store.dispatch(CartAction::Line(0, CartLineAction::Inc)); // wraps → ShopAction::Cart(..)
store.dispatch(ShopAction::Cart(CartAction::Line(0, CartLineAction::Inc))); // same thing
```

Both reach the reducer thread, both mutate `cart` inside the one root mutex.

---

## 9. How to subscribe without clone?

Today there is **no fully clone-free subscriber** — dedup needs an owned snapshot to
compare (`prev.as_ref() == snapshot.as_ref()`). Practical ways to minimize/avoid copies:

1. **Yield `Arc<S>`, bind by `Arc`.** `next()` already returns `Arc<S>`; passing it to a
   view is a refcount bump, not a copy. Keep heavy fields in the state behind `Arc` so the
   one internal `clone()` is shallow.
2. **Scope to a small child** so the value you actually bind is tiny:

```355:365:rust-elm/src/runtime/store.rs
    pub fn subscribe_state(&self) -> ScopedStateSubscriber<S, CS, SK>
    where
        S: Clone + PartialEq,
        SK: Clone,
    {
        ScopedStateSubscriber {
            inner: self.store.subscribe_state(),
            state_kp: self.state_kp.clone(),
            _marker: PhantomData,
        }
    }
```

   (The parent is still cloned to diff, but you only hand the focused `CS` to the view.)
3. **Read via lock for a true `&S`** when you don't need change-notifications:
   `let g = runtime.state.lock(); render(&g.cart);` — borrow, no clone, but hold briefly.
4. **Use `infra::shared::Shared<T>`** for values you want to observe by shared handle
   rather than by snapshot.

> If you want a genuinely zero-clone *push* subscription, that would be a new API (e.g. a
> listener that receives `Arc<S>` the reducer already built, skipping per-subscriber
> dedup). Not present today — call it out if you need it.

---

## 10. `Reducer` vs `ScopeReducer` vs `CatchReducer`

All three implement the same trait — they differ in *what they wrap*:

```5:10:rust-elm/src/compose/reducer.rs
pub trait Reducer {
    type State;
    type Action;

    fn reduce(&self, state: &mut Self::State, action: Self::Action) -> Cmd<Self::Action>;
}
```

### `Reducer` (and `Reduce` / fn pointers / `CombineReducers`)

The base abstraction: given `&mut state` and an `action`, mutate state and return a `Cmd`
(effects to run). Plain `fn(&mut S, A) -> Cmd<A>` is a `Reducer` via a blanket impl;
`Reduce::new(f)` wraps one; `CombineReducers((..))` runs several **siblings of the same
`State`/`Action`** in order and batches their commands. Use it for your actual update
logic — e.g. `global_reducer`, `cart_reducer` in the shop.

### `ScopeReducer` — a *structural* adapter (different state/action)

`ScopeReducer<…>` lets a child reducer with `State = CS, Action = CA` participate in a
parent whose `State = PS, Action = PA`. It **extracts** the child action (casepath),
**focuses** the child state (keypath), runs the child, and **lifts** the result back —
including cancel-id tagging for the child's effects (see §6b). It changes *which slice of
state/action* a reducer sees; it does **not** change semantics or catch anything.
Relatives: `IfLetReducer` (optional child + dismiss/cancel), `ForEachReducer`
(per-`IdentifiedVec`-element), `OptionalReducer`, `IfCaseLetReducer`.

### `CatchReducer` — a *safety* adapter (same state/action)

`CatchReducer<R, F>` wraps any reducer and runs it inside [`safe_reduce_update`](./safe_reducer.md). On panic
it calls your `recover` with a [`SafeReduceError`](./safe_reducer.md) to produce a fallback `Cmd` instead of unwinding the reducer
thread:

```100:114:rust-elm/src/compose/reducer.rs
impl<R, F, S, A> Reducer for CatchReducer<R, F>
where
    R: Reducer<State = S, Action = A>,
    F: Fn(SafeReduceError) -> Cmd<A>,
{
    type State = S;
    type Action = A;

    fn reduce(&self, state: &mut S, action: A) -> Cmd<A> {
        match safe_reduce_update(state, |s, a| self.inner.reduce(s, a), action) {
            Ok(cmd) => cmd,
            Err(err) => (self.recover)(err),
        }
    }
}
```

It keeps the **same** `State`/`Action` (it's a transparent wrapper), and by default does
**not** revert partial mutations made before the panic (use [`RollbackCatchReducer`](./safe_reducer.md) /
[`safe_reduce_rollback`](./safe_reducer.md) for checkpoint rollback). In the shop it wraps the entire `CombineReducers` tuple so any
reducer panic is contained and the app keeps running.

### Side-by-side

| Aspect | `Reducer` / `CombineReducers` | `ScopeReducer` | `CatchReducer` |
|--------|-------------------------------|----------------|----------------|
| Purpose | the actual update logic | focus a child slice | contain panics |
| Changes State/Action type? | no | **yes** (`PS/PA``CS/CA`) | no |
| Uses optics? | no | yes (keypath + casepath) | no |
| Wraps another reducer? | combines siblings | wraps a child reducer | wraps one reducer |
| On panic | propagates (unwinds) | propagates | **caught**`recover` cmd |
| Effects | as returned | child effects lifted + cancel-tagged | inner's, or recover's |
| Shop usage | `global_reducer`, `cart_reducer` | `catalog`/`cart` scopes, `ForEach` wishlists | root wrapper |

**Typical composition** (outermost → innermost): `CatchReducer` → `CombineReducers` →
`ScopeReducer`/`ForEachReducer`/`IfLetReducer` → your leaf `Reduce`/fn.

---

## Quick reference

| Task | `Store` (mutex) | `RwStore` (RwLock) | `SwapStore` (`arc-swap`) | `TeaStore` (channel) |
|------|-----------------|---------------------|---------------------------|------------------------|
| Get handle | `runtime.store()` | `runtime.rw_store()` | `runtime.swap_store()` | `runtime.tea_store()` |
| Fire-and-forget | `store.dispatch(action)` | same | same | same |
| Dispatch + await effects | `store.send(action).finish()` | same | same | same |
| Read latest model | `store.state()` (lock) | `store.state()` | `snapshot_store().load()` | `subscribe_state().latest()` or `view_store().load()` |
| Live view / readers | `subscribe_state()` ping+lock | same | push via ArcSwap load | **`subscribe_state()` push `Arc<S>`** |
| Concurrent reader threads | mutex serializes | `read_store()` | `snapshot_store()` | `view_store().subscribe_state()` |
| Child handle | `store.scope(kp, cp)` | `ScopedRwStore` | `ScopedSwapStore` | `ScopedTeaStore` |
| State bound | `S: Send` | `S: Send + Sync` | `S: Send + Sync + Clone` | `S: Send + Sync + Clone` |

RwLock guide: [rw_ecommerce.md](./rw_ecommerce.md). Arc-swap: [swap_ecommerce.md](./swap_ecommerce.md). Channel TEA: [tea_ecommerce.md](./tea_ecommerce.md). Safe reduce: [safe_reducer.md](./safe_reducer.md).