# Changelog

All notable changes to `ankusa_rabbitmq` are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project
follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.7.0] - 2026-10-28

### Added

- `:max_message_bytes` (default 128 MiB, RabbitMQ's `max_message_size`; DEL-5):
  the size preflight of `Ankusa.Sink.Messaging`. Requires an `ankusa` release
  with the `max_message_bytes` sink callback; publish core first.
- `Sink.RabbitMQ`: a hook of an `ordered: true` sink carries the lane's key as the
  `ankusa_ordering_key` AMQP header (consumer-side ordering, ADR 0004).

### Changed

- Requires `ankusa ~> 0.7` (`Ankusa.Sink.Messaging`, `Ankusa.Sink.Connections`);
  publish core first.
- `Ankusa.Sink.RabbitMQ` is built on core's `Ankusa.Sink.Messaging` and
  `Ankusa.Sink.Connections`. What it publishes is unchanged.

## [0.6.0] - 2026-10-14

### Added

- `:max_inflight` (default 256): publishes awaiting a confirm per connection;
  past it a publish answers `{:error, :busy}` without being sent.
- `:idle_timeout_ms` (default `600_000`, `0` never, at least 16 s): a
  connection with no delivery for that long is closed (`Ankusa.Sink.Reaper`,
  requires core's) and the next delivery opens a new one, so a deleted or
  re-pointed source no longer leaves its connection open for the life of the
  node.
- Every publish carries the `ankusa_idempotency_key` AMQP header: the same
  tenant-scoped key as the message's `idempotency_key`
  (`Ankusa.Envelope.idempotency_key/1`), always present. **Requires `ankusa`
  0.5 or newer**: the key comes from `Ankusa.Envelope.idempotency_key/1`,
  which 0.4.x does not have, so the Hex requirement is now `~> 0.5`.
- AMQP `message_id` is the hook id; the `ankusa_dedupe_key` and
  `ankusa_replay_id` AMQP headers are set when present.

### Changed

- Requires `ankusa ~> 0.6` (`Ankusa.Sink.Reaper`); publish core first.
- Stopping a connection process closes its AMQP connection: `amqp_client`
  owns that connection, so it used to outlive the process. Publishes waiting
  on it answer `{:error, :not_connected}`.
- Confirms are asynchronous: many publishes are outstanding on the channel at
  once and each caller is answered on its own confirm, so one slow confirm no
  longer serializes every delivery on the connection.
- A publish still queued in the connection when its caller's deadline passed
  answers `{:error, :expired}` and is never sent.
- The connection dials after it starts (`handle_continue`), so a dead broker
  never blocks the supervisor; it is registered under a digest of the URL,
  and its `format_status/1` redacts the URL, so a password never appears in a
  process name, `:sys.get_state/1` or a crash report.
- The connection's child spec carries only the options the connection reads,
  with the URL inside a closure, so `:sys.get_status/1` of
  `Ankusa.Sink.RabbitMQ.Supervisor`, or a supervisor report, never prints
  the password.
- **Breaking: `Ankusa.Sink.RabbitMQ.Connection.publish/4`'s fourth argument is
  a keyword list of properties** (`message_id:`, `headers:`), not a header
  list.
- **Breaking: every publish is mandatory.** A publish no queue is bound to
  receive is no longer reported as delivered: `deliver/3` answers
  `{:error, {:unroutable, routing_key}}` instead of `:ok`, so the hook is
  retried and then dead-lettered (a `503` under `wal.type: none`) rather than
  settled while the broker kept nothing. There is no option to turn this off.

### Fixed

- amqp_client's processes hold the connection password encrypted: the
  application sets a random `credentials_obfuscation` secret at start unless
  the host already set one, so `:sys.get_status/1` or a crash report of an
  amqp_client process no longer prints `{:plaintext, password}`.
- A channel the broker closes (a `404` publishing to a deleted exchange, a `406`)
  is reopened at once on the same connection, re-declaring the exchange. The
  publish it interrupted answers `{:error, {:channel_closed, reason}}`.
  Previously every later publish failed with `{:publish_failed, _}` until the
  node restarted.
- `:confirm_timeout_ms` is milliseconds. The confirm wait read it as seconds,
  so an unconfirmed publish blocked the connection for up to 70 s while its
  caller gave up at 15 s. Each publish now waits for its own confirm only,
  so one stale publish no longer delays later ones.
- A channel that fails setup (for example a `406` on `exchange.declare`) no
  longer leaks an AMQP connection on every retry; the connection is kept and
  only the channel is retried.

## [0.4.0] - 2026-10-02

### Added

- `Ankusa.Sink.RabbitMQ` implements `c:Ankusa.Sink.describe/2`, advertising its
  broker, vhost, exchange, and routing key to the AsyncAPI document the admin
  API serves. Credentials in `:url` are never part of it.
- `Ankusa.Sink.RabbitMQ.Connection.default_url/0`.

### Changed

- The Hex requirement on `ankusa` is `~> 0.4` (was `~> 0.2`): `describe/2`
  needs `Ankusa.Sink.Description`, which core has from 0.4.0.

### Removed

- The sink no longer implements the removed Ankusa.Sink.ordering_key/2 callback:
  deliveries are unordered. The routing key still decides the queue, so
  RabbitMQ's per-queue ordering is unchanged. Requires the matching `ankusa`
  core.

## [0.3.0] - 2026-10-01

No code changes. Released as 0.3.0 in step with `ankusa` 0.3.0 and the other packages in this release.

## [0.2.1] - 2026-09-28

### Added

- A `sha256` field on a claim-checked message: the lowercase hex digest of the
  payload, for the reader to verify the bytes it redeems against. It used to
  live inside the claim ref.

### Changed

- The message `claim` field is `urn:ankusa:claim:v1:<tenant>:<claim_id>`: a
  tenant and a canonical (uppercase) ULID, where it used to carry the pack id,
  byte offset/length, and digest. The message `v` stays `1`. Both come from
  `Ankusa.Sink.Message` in `ankusa` 0.2.1.

## [0.2.0] - 2026-09-27

### Added

- Ankusa.Sink.ordering_key/2, implemented by this sink as the routing key:
  RabbitMQ orders per queue, and the routing key decides the queue. Dispatch
  now serializes deliveries sharing a key and runs different keys concurrently.

### Changed

- The message `claim` field is now a claim-check ref URN string instead of a
  nested ticket object, breaking for consumers; the message `v` stays `1`.
- `:inline_max_bytes` defaults to 64 KiB (was 8 KiB), read from
  `Ankusa.Sink.Message.inline_max_bytes/1`.
- Depends on `ankusa` `~> 0.2`: core 0.1 has neither
  `Ankusa.Sink.Message.inline_max_bytes/1` nor Ankusa.Sink.ordering_key/2.

## [0.1.0] - 2026-09-23

### Added

- `Ankusa.Sink.RabbitMQ`: publishes delivered hooks to a RabbitMQ exchange
  (never a queue: consumers own their own queue/binding). Bodies under
  `:inline_max_bytes` ride along base64-encoded; larger bodies are checked
  in through `Ankusa.ClaimCheck`, with the message carrying a ticket.
  Publisher-confirmed delivery; supervised, auto-reconnecting connection per
  `(instance, exchange)`.
- Messages carry a format version, `"v": 1`. Additive: consumers that ignore
  unknown keys need no change.

### Changed

- The message is built by `Ankusa.Sink.Message` (in `ankusa`), shared
  with `ankusa_kafka`, so both sinks publish byte-identical messages.

[Unreleased]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.7.0...HEAD
[0.7.0]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.6.0...ankusa_rabbitmq-v0.7.0
[0.6.0]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.4.0...ankusa_rabbitmq-v0.6.0
[0.4.0]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.3.0...ankusa_rabbitmq-v0.4.0
[0.3.0]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.2.1...ankusa_rabbitmq-v0.3.0
[0.2.1]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.2.0...ankusa_rabbitmq-v0.2.1
[0.2.0]: https://github.com/jamescarr/ankusa/compare/ankusa_rabbitmq-v0.1.0...ankusa_rabbitmq-v0.2.0
[0.1.0]: https://github.com/jamescarr/ankusa/releases/tag/ankusa_rabbitmq-v0.1.0
