# Singleflight — Design

## Goal

Add a Go-style `singleflight` primitive to the `go` crate: deduplicate concurrent
calls keyed by an arbitrary key so that only one underlying future executes at a
time, with all duplicate callers sharing the single result.

Equivalent to `golang.org/x/sync/singleflight`, adapted to async Rust and the
crate's runtime-agnostic style.

## Module & types

New `pub mod singleflight` (mirrors `pub mod chan`). Depends only on
`event_listener` (already a dependency) for `do_`/`forget`; `do_chan` uses the
crate's own `go::spawn`, so the whole module stays runtime-agnostic.

```rust
pub struct Group<K, V> { inner: Arc<GroupInner<K, V>> }   // Clone, like WaitGroup

struct GroupInner<K, V> {
    calls: Mutex<HashMap<K, Arc<Call<V>>>>,
}

struct Call<V> {
    event: Event,                  // notified on completion or abandonment
    state: Mutex<CallState<V>>,
}

struct CallState<V> {
    dups: usize,
    result: Option<V>,             // Some once the leader produces a value
}
```

Bounds: `K: Hash + Eq + Clone`, `V: Clone`.

## Public API

```rust
impl<K: Hash + Eq + Clone, V: Clone> Group<K, V> {
    pub fn new() -> Self;

    /// Deduplicate concurrent calls for `key`. The first caller (the leader)
    /// runs `fut`; concurrent callers for the same key wait and receive a clone
    /// of the leader's result. Returns `(value, shared)` where `shared` is true
    /// if the value was shared with at least one other caller.
    pub async fn do_<F: Future<Output = V>>(&self, key: K, fut: F) -> (V, bool);

    /// Evict an in-flight key so the next `do_` starts a fresh execution.
    pub fn forget(&self, key: &K);

    /// Channel variant: spawns the work and returns a receiver that yields
    /// `(value, shared)` exactly once. Requires `Send + 'static` because it
    /// spawns onto the active runtime.
    pub fn do_chan<F>(&self, key: K, fut: F) -> Receiver<(V, bool)>
    where
        F: Future<Output = V> + Send + 'static,
        K: Send + 'static,
        V: Send + 'static;
}
```

`Group` also implements `Clone` (cheap `Arc` clone) and `Default`.

The method is named `do_` because `do` is a reserved Rust keyword.
`Receiver` is `async_channel::Receiver`, consistent with `go::chan`.

## Semantics & flow

### Leader / follower

`do_` runs a loop that, under the map lock, classifies the caller:

- **Leader** — no entry for `key`: insert a fresh `Call`, release the lock, and
  await the caller's own future.
- **Follower** — entry exists: increment `dups`, release the lock, and wait on
  the `Call`'s event. The follower's own future is held unpolled and discarded
  once a result arrives.

On successful completion the leader, under the map lock, removes the key, sets
`result = Some(value.clone())`, computes `shared = dups > 0`, then notifies all
waiters (`event.notify(usize::MAX)`). Followers wake, clone the result, and
return `(value, true)` — a follower's own presence guarantees the value was
shared, matching Go's behavior where every participant observes the same
`shared` value.

Followers use the listen-before-check pattern already used by `WaitGroup::wait`
to avoid the lost-wakeup race.

### Cancellation / panic safety

If the leader's future is dropped (cancelled) or panics **before** producing a
value, a drop-guard removes the key from the map and notifies waiters while
`result` is still `None`. Woken followers observe no result and loop: they
re-acquire the map, and one of them becomes the new leader by awaiting *its own*
held future.

This is why each caller retains its future until it is certain it is a follower
with a result in hand — it guarantees no waiter can hang on a cancelled or
panicked leader.

This is a deliberate divergence from Go, which re-panics into every caller.
Documented behavior: **an abandoned in-flight call causes waiting callers to
retry (one becomes the new leader) rather than hang or share a panic.**

### `forget`

`forget(&key)` removes the entry from the map. Callers already waiting (they
hold the `Arc<Call>`) still receive their result when the original leader
finishes. Callers arriving after `forget` create a fresh leader. Forgetting an
absent key is a no-op.

### `do_chan`

`do_chan` creates an `async_channel::bounded(1)`, clones the `Group`, and uses
`go::spawn` to run `do_(key, fut)` and send the `(value, shared)` pair into the
channel. It returns the `Receiver`. The `Send + 'static` bounds are inherent to
spawning onto the runtime.

## Wiring

- `src/singleflight.rs` — the implementation.
- `src/lib.rs` — add `pub mod singleflight;` and a bullet under `## Primitives`:
  `- [`singleflight::Group`] — deduplicate concurrent calls by key`.

## Testing

Integration tests in `tests/integration.rs`, all driven through the existing
`run_async!` macro so they run on tokio, async-std, and smol:

1. **Single call** — `do_` returns the value with `shared == false`.
2. **Dedup under concurrency** — N tasks (`go::spawn`) call `do_` on one key; the
   future increments an `AtomicUsize` and `go::sleep`s so the calls overlap.
   Assert the counter is `1`, all results are equal, and the followers see
   `shared == true`.
3. **Distinct keys** — two different keys run independently; counter is `2`.
4. **`forget` re-executes** — start a leader, `forget` the key, start a second
   call; the work runs twice (counter `2`).
5. **`do_chan`** — `recv().await` yields the `(value, shared)` pair.
6. **Abandon safety** — drop/cancel a leader future (e.g. via a `go::timeout`
   that elapses), then a fresh `do_` on the same key still completes — proving no
   waiter hangs.

A doc example on `Group::do_` is added and exercised by `cargo test --doc`.

## Out of scope

- Sharing panics across callers (Go-style re-panic) — replaced by the retry
  behavior above.
- A `shared`/result-caching layer beyond the in-flight window (singleflight only
  deduplicates *concurrent* calls, not a cache).
