mod-events 1.0.0

A high-performance, zero-overhead event dispatcher library for Rust
Documentation
# mod-events — Stability and SemVer Policy

> This document is the formal stability contract for the `1.x` line.
> It is binding: any release that violates it is a bug.

## Headline guarantee

**Every public symbol in `1.0.0` is locked for the entire `1.x`
line.** Signatures, observable behaviour, and panic-vs-error
semantics cannot change without a major version bump to `2.0.0`.

This applies to:

- Every symbol re-exported by `mod_events::*` and the `prelude`
  module: `EventDispatcher`, `Event`, `EventListener`,
  `AsyncEventListener`, `Priority`, `ListenerId`, `ListenerError`,
  `DispatchResult`, `EventMetadata`, `AsyncEventResult`.
- The trait implementations on those types (`Debug`, `Default`,
  `Clone`, etc.).
- The behavioural contracts documented in each method's rustdoc
  (panic safety, FIFO ordering within equal priority, sequential
  async dispatch, `errors()` returning `&[ListenerError]`, etc.).
- The `async` cargo feature semantics. The default feature set is
  `["async"]`; removing async from the defaults is a major bump.

## Version policy

The `1.x` line follows [Semantic Versioning 2.0.0](https://semver.org/).
The specific rules for this crate:

### Patch releases (`1.x.Y`)

May contain:

- Bug fixes that do not change observable behaviour for any
  documented input.
- Documentation improvements.
- Internal performance work that does not move any public surface.
- Test additions.
- Internal dependency updates (within their compatible range).

May NOT contain:

- New public items.
- Behavioural changes (e.g. changing the panic message format,
  changing which lock primitive is used in a way callers can
  observe through profiling tools).
- MSRV bumps.

### Minor releases (`1.Y.0`)

May contain everything a patch release may contain, plus:

- New public items (functions, methods, types, modules) so long as
  they are pure additions — no signature change to any existing
  symbol.
- New optional cargo features that default to off, or default-on
  features that activate purely additive code paths.
- MSRV bumps. Each MSRV bump is a minor-version bump minimum and
  is called out in the CHANGELOG.
- New variants on `#[non_exhaustive]` types (`Priority`,
  `EventMetadata`).
- New fields on `#[non_exhaustive]` structs (`EventMetadata`).

### Major releases (`2.0.0`)

Required for any change that violates the above. Specifically, any
of the following requires `2.0.0`:

- Removal or rename of any public symbol.
- A signature change to any existing public symbol (including
  making panicking conditions stricter or looser in a
  user-observable way).
- A change to the `async` default-feature status (e.g. moving
  async to a non-default feature).
- A change that breaks 0.x → 1.x → 2.x migration documented in
  `docs/migration.md`.
- Adding a runtime dependency to the `[dependencies]` table beyond
  the two already there (`parking_lot`, optional `futures-util`).
- Removing the `parking_lot` dep in favour of `std::sync::RwLock`
  (which would re-introduce lock poisoning — observable).

## Determinism

`mod-events` is **not deterministic** by design — listener
execution order is fully deterministic (priority then FIFO), but
the dispatcher does not record reproducible internal state. There
is no seed, no replay capability, and no commitment to identical
output across runs.

Determinism guarantees that hold:

- Listener ordering: within a single dispatch, listeners run in
  descending-priority order. Within equal priority, they run in
  FIFO registration order. Both are part of the contract.
- Sequential async dispatch: `dispatch_async` awaits listeners one
  at a time in priority order. Concurrent execution would lose
  ordering; if you need it, spawn from inside each listener.

Non-determinism that is acceptable:

- The `EventMetadata::last_dispatch: Instant` field. Time is not
  reproducible.
- The exact contents of a panic-payload-derived `ListenerError`
  message ("listener panicked: ..."). The prefix is part of the
  contract; the suffix is whatever the panic carried.

## Panic safety

Both `dispatch` and `dispatch_async` wrap each listener in
`catch_unwind`. A panicking listener becomes a
`ListenerError` with the message prefix `"listener panicked: "`
and subsequent listeners still run. This is part of the public
contract and cannot weaken in the `1.x` line.

`emit` shares the same `catch_unwind` wrapping (panics from
fire-and-forget listeners are caught and dropped, never propagated
to the caller). This is also locked.

The dispatcher itself never panics through normal use. It does not
panic on:

- Unknown `ListenerId` passed to `unsubscribe` (returns `false`).
- Empty event-type registry (returns immediately).
- Middleware returning `false` (returns `DispatchResult::is_blocked()`).
- Lock acquisition (the `parking_lot` primitive does not poison).

## Deprecation policy

A symbol marked `#[deprecated]` in a `1.x.Y` release:

- Remains callable for the entire `1.x` line.
- Continues to behave per its documented contract.
- May only be removed in a `2.0.0` release.

Deprecation is for guidance, not removal. Code that compiles
against `1.0.0` must continue to compile against `1.x.LATEST`
(modulo the deprecation warning), without source changes.

## What is NOT covered

The stability contract does NOT cover:

- **Internal performance characteristics.** A `1.x.Y` may make any
  operation faster or slower than `1.0.0`, so long as it stays
  within the spirit of the documented performance properties
  ("sub-microsecond dispatch", "zero-allocation success path",
  etc.). Documented benchmark numbers in the README and
  `docs/performance.md` are illustrative, not contractual.
- **Internal types and modules.** Any item not re-exported through
  `lib.rs` (`type_id_map::*`, `middleware::*`, the `pub(crate)`
  guts of `metrics.rs` and `listener.rs`) is internal and may
  move or change between minor releases.
- **Exact wording of error messages and panic-payload messages.**
  The error kind, the `Display` chain via `Error::source`, and
  the `"listener panicked: "` prefix are part of the contract;
  the human-readable text following them is not.
- **The `TypeId` hashing implementation.** `mod-events` uses an
  internal no-op hasher specialised for `TypeId`. This is an
  implementation detail and may change.

## Dependencies

Two runtime dependencies are sealed for the `1.x` line:

- `parking_lot` (exact-pinned to `=0.12.4`) — hot-path lock
  primitive. The exact pin per REPS §Dependency Management for
  critical crates.
- `futures-util` (optional, gated on the `async` feature) — used
  only for `FutureExt::catch_unwind` on the async dispatch path.

Adding any third runtime dependency requires a `2.0.0` bump.
Removing either requires a `2.0.0` bump.

## MSRV

`1.0.0` ships with MSRV `1.81.0`. Any change to the MSRV is a
minor-version bump minimum and is called out in the CHANGELOG.

The `1.81` floor is deliberate: Cargo lockfile format v4 (default
since Rust 1.78) cannot be parsed by older toolchains, and the
`0.2.x` MSRV CI job on `1.75` failed at lockfile parse before any
code compiled. `1.81` is the conservative choice — stable since
2024-09-05, well over a year of release window.

## Feature flags

The default feature set is `["async"]`. The `async` feature pulls
in `futures-util` and enables the async dispatch surface
(`AsyncEventListener`, `dispatch_async`, `subscribe_async`,
`subscribe_async_with_priority`, `AsyncEventResult`).

Removing `async` from defaults, or adding a new default feature,
requires a major version bump.

Adding a non-default feature is a minor bump. Renaming a feature
is a major bump.

## Reporting a stability break

If you encounter what looks like a `1.x` stability break:

1. Run the failing case against the latest patch release of `1.x`
   to confirm reproducibility.
2. Capture the exact API call sequence and observed-vs-expected
   behaviour.
3. Open an issue at
   <https://github.com/jamesgober/mod-events/issues> with the
   repro.

Stability breaks are bugs and are fixed in the next patch
release.