once_type_map 0.1.0

Thread-safe, insert-once type map that hands out plain references (no guards), like a OnceLock keyed by type
Documentation
# once_type_map

A thread-safe, insert-once type map that hands out plain `&T` references, not
lock guards.

You can think of it as a `OnceLock` per type, for types you don't know when you
write the owning struct. Library authors can embed one `OnceTypeMap` in a
long-lived object (a client, a session, an app context). Downstream crates can
then attach their own state to that object, such as caches, adapters, or
implementers of extra traits, without the original struct knowing about them.

```rust
use once_type_map::OnceTypeMap;

struct Client {
    extensions: OnceTypeMap,
}

impl Client {
    pub fn extensions(&self) -> &OnceTypeMap {
        &self.extensions
    }
}

// Somewhere in a downstream crate:
struct MyCache { /* ... */ }

fn cache(client: &Client) -> &MyCache {
    client.extensions().get_or_init(|| MyCache { /* expensive setup */ })
}
```

[`examples/extensions.rs`](examples/extensions.rs) shows the full pattern. A
library defines the extensible `Client`, and two independent extension modules
add methods to it through extension traits. The application imports those
traits and uses them, including from several threads:

```sh
cargo run --example extensions
```

[`examples/async_extensions.rs`](examples/async_extensions.rs) shows the
same idea when the extension's state has to be created asynchronously, for
example by a login call. It stores a `tokio::sync::OnceCell` in the map, so
concurrent tasks wait for a single login without blocking the executor:

```sh
cargo run --example async_extensions
```

## Why references instead of guards?

Typical type maps (`http::Extensions`, `anymap`, `RwLock<HashMap<TypeId, Box<dyn Any>>>`)
either need `&mut self` to insert or return a guard that borrows the lock. Both
make it hard for a wrapper type to expose `fn foo(&self) -> &Foo`.

`OnceTypeMap` returns a `&T` that lives as long as the borrow of the map. This
is sound because of three rules:

1. Each type's value lives in its own heap-allocated `OnceLock<T>` slot. When
   the internal hash map grows, it moves the pointer to the slot, not the slot.
2. Through `&self`, a slot can only be created and filled once.
3. Slots are only mutated, removed or dropped through `&mut self` or by
   dropping the map. The borrow checker allows that only when no `&T` handed
   out by the map is still alive.

The same reasoning is why `OnceLock` itself can offer `get_mut` and `take`.

## API

| Method | Receiver | Behavior |
| --- | --- | --- |
| `get::<T>()` | `&self` | `Some(&T)` if set |
| `get_or_init(f)` | `&self` | Returns the value, running `f` first if it is unset |
| `insert(value)` | `&self` | `Ok(&T)` if it was unset, otherwise gives the value back in `Err` |
| `contains::<T>()` | `&self` | Whether a value is set |
| `len()` / `is_empty()` | `&self` | Number of types whose value is set |
| `get_mut::<T>()` | `&mut self` | `Some(&mut T)` if set |
| `remove::<T>()` | `&mut self` | Takes the value out; the type can be set again |
| `clear()` | `&mut self` | Removes all values and frees all slots |

## Initialization semantics

- `get_or_init` runs the initializer outside the map's lock. A slow
  initializer for one type doesn't block other types, and an initializer may
  use the map for other types, as long as initializers don't depend on each
  other in a cycle (see below).
- When many threads race to initialize the same type, only one initializer
  runs. The other threads block until the value is ready. Once an initializer
  returns, no other initializer for that type runs until the value is removed.
- If the initializer panics, the value stays unset and the next call runs its
  own initializer. So an initializer can run more than once over the life of
  the map: after a panic, and after `remove` or `clear`.

### Deadlocks

Neither of these cases panics or returns an error. They hang:

- Calling `get_or_init::<T>` or `insert::<T>` from inside the initializer for
  the same `T`. This is a reentrant `OnceLock` initialization. The standard
  library leaves the outcome unspecified, and today it deadlocks. `insert`
  doesn't return `Err` here.
- Initializers that depend on each other in a cycle. If `A`'s initializer
  uses `B` and `B`'s uses `A`, a single thread hits the reentrant case above.
  With two threads, one starting with `A` and the other with `B`, each blocks
  forever waiting for the other's slot.

### Empty slots

A type's slot is created the first time `get_or_init` or `insert` is called
for it. If the initializer panics, the empty slot stays in the map. Nothing
called through `&self` removes it, though a later successful initialization
reuses it. This costs at most one small allocation per distinct type, but
`len()` and `is_empty()` scan empty slots along with full ones. `remove::<T>()`
and `clear()` free empty slots too.

## Caveats

- One value per type. Use newtypes to store several values of the same
  underlying type.
- Values must be `Send + Sync + 'static`.
- Initializers are synchronous and can't fail. See the next section.
- A lookup takes a shared read lock. Because the returned reference isn't tied
  to a guard, hot paths can look a value up once and keep the `&T`.
- The minimum supported Rust version is 1.86.

## Fallible and async initialization

std's `OnceLock::get_or_try_init` isn't stable yet, so the map has no
`get_or_try_init`. What to store instead depends on what a failure means:

- **The failure is final** (a malformed setting, say). Store the `Result`
  itself with `get_or_init`. Every caller sees the same outcome, and setup
  runs once.
- **The failure is temporary** (a server that isn't up yet). Store a cell
  that supports a fallible initializer, such as `once_cell::sync::OnceCell`:

  ```rust
  map.get_or_init(|| DbCell(OnceCell::new()))
      .0
      .get_or_try_init(|| connect())
  ```

  A failed attempt stores nothing, so the next call tries again, and only one
  attempt runs at a time.
- **Setup is async.** Store an async cell such as `tokio::sync::OnceCell`. The
  map only creates the empty cell, and the cell's async `get_or_init` (or
  `get_or_try_init`, if setup can also fail) does the work without blocking the
  executor. Don't run slow or blocking setup in the
  map's own `get_or_init` from async code, because callers waiting on it block
  their thread.

[`examples/fallible_extensions.rs`](examples/fallible_extensions.rs) shows the
first two cases side by side:

```sh
cargo run --example fallible_extensions
```

Once std stabilizes `OnceLock::get_or_try_init`, the map will gain a matching
method.

## Checking the unsafe code

All `unsafe` code goes through one private `unsafe fn`, `detach`. It extends a
slot reference from the lock guard's lifetime to the borrow of the map. There
are three `unsafe` blocks: the one in the body of `detach`, and one at each of
its two call sites (`slot` and `create_slot`). Each call site has to meet
`detach`'s precondition: the slot must be owned by the map. Any new call to
`detach` needs the same review. The test suite includes a test that keeps a
reference alive while the map grows. Run it under
[Miri](https://github.com/rust-lang/miri) with both aliasing models:

```sh
cargo +nightly miri test
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test
```

## License

Apache-2.0