# Changelog
## v0.3.0 (2026-06-17)
### Enhancements
- **Transactional callback variants** — `handle_cast_tx/3`, `handle_call_tx/4`, and
`handle_info_tx/3` are optional callback variants that receive a `tx_ctx()` map as
their first argument (`#{td := tenant(), tuid := tuid()}`). When exported, the `_tx`
variant is preferred over the plain variant. The `td` value is the open backend
transaction+directory pair for the current consume cycle, allowing callbacks to read
or write arbitrary keys atomically alongside the module-state commit. The new
`tx_ctx/0` type is exported from `dgen_server`.
- **`lock_timeout` option for `dgen_server`** — Sets the maximum milliseconds a
distributed lock may be held before other consumers treat it as stale and
clear it. Previously a consumer killed (SIGKILL / VM abort) while holding
the lock would block all other consumers permanently if no new messages
arrived to trigger a re-check. With `lock_timeout` set, a backstop timer is
scheduled whenever a live lock is observed: after the remaining timeout the
consumer re-evaluates staleness and clears the lock if the holder has not
done so. `infinity` (the default) preserves the previous behaviour.
`dgen_registry` uses `lock_timeout: 6_000`.
- **`dgen_registry`** — Experimental. An OTP-compatible process registry backed by the
configured storage backend. Implements the four-function
`{via, dgen_registry, {Name, LogicalName}}` contract so standard OTP
processes (`gen_server`, `gen_statem`, etc.) can be started and addressed by
name across an Erlang cluster. Writes and consistent reads go through an
elected leader; `whereis_name/1` (used by OTP via-tuple routing) is a
snapshot read from the local member's in-memory map with no backend
round-trip. The leader monitors registered pids and propagates
`{name_unregistered}` to followers on process exit. Start with
`dgen_registry:start_link(Name, Tenant)` and supply a supervisor name as the
first argument to `start_link/3` to embed it in an existing supervision tree.
Partition recovery is reliable: each join carries a unique token so stale
`member_down` messages from before a reconnect are discarded rather than
undoing the rejoin. Leader transitions during a partition no longer
trigger automatic distribution reconnect.
### Breaking changes
- **`DGenServer` renamed to `DGen.Server`** — `use DGenServer` becomes `use DGen.Server`;
all `DGenServer.*` call sites become `DGen.Server.*`. The module now lives at
`lib/dgen/server.ex`.
- **`handle_locked/3` → `handle_locked/4`** — a `db_ctx()` map is now prepended as
the first argument, matching the convention of `handle_call_tx/4` and friends.
`db_ctx()` carries `#{db := tenant(), tuid := tuid()}` where `db` is the DB-level
tenant (not a transaction); use `dgen_backend:transactional/2` inside the callback
to open explicit transactions. Update all `handle_locked` implementations to accept
the new first argument.
## v0.2.0 (2026-04-05)
### Enhancements
- **Dead-letter queue** — opt-in poison-message handling via the new
`dead_letter_threshold` start option (default `infinity`, i.e. disabled).
When set to a positive integer, messages that crash the consumer that many
times are moved to a dead-letter queue (DLQ) stored in FoundationDB instead
of being retried indefinitely. For `call` messages the blocked caller raises
`{dead_letter, N}`. The optional `handle_dead_letter/2` callback is invoked
when a message is dead-lettered.
- `dgen_server:outbox_cast/1,2` — returns a `Cast = fun((Tx, Message) -> ok)`
closure for enqueuing a cast message atomically within the caller's own FDB
transaction. Call it before opening the transaction as a preparatory step;
the closure captures the queue directory and identifier internally. Intended
for callers already operating directly with a backend transaction who need
to compose the enqueue with other writes atomically.
## v0.1.0 (2026-02-22)
Initial release.