# Phase 20: Wildcard / Pattern Subscriptions

## Goal

Allow subscribers to match multiple topics with a single subscription using wildcard patterns. Instead of subscribing to `orders.us`, `orders.eu`, and `orders.apac` individually, a client subscribes to `orders.*` and receives messages from all matching topics — including topics created after the subscription. This is a core ergonomic feature in NATS and MQTT that dramatically reduces client-side boilerplate and enables event-driven architectures where producers can add new sub-topics without coordinating with every consumer.

## Guiding Principles

- **NATS-style wildcards.** Two wildcard tokens: `*` matches exactly one token between dots, `>` matches one or more trailing tokens. This is familiar to developers from NATS/MQTT and covers the vast majority of use cases. Topic names are dot-delimited.
- **Subscriptions, not queries.** Wildcard subscriptions are live — they match existing topics AND topics created in the future. When a new topic is published to and it matches a wildcard pattern, existing wildcard subscribers begin receiving from it immediately.
- **Fan-in delivery.** Messages from all matching topics are multiplexed onto the subscriber's single delivery channel, with the topic name included in each `Deliver` frame (already present). The client knows which topic each message came from.
- **Consumer groups work.** Wildcard subscriptions are compatible with consumer groups. All members of a group subscribed to `orders.*` collectively consume from all matching topics with the same round-robin semantics.
- **Wildcard is a subscribe-side concept.** Publish always targets an exact topic. You cannot publish to `orders.*`. This prevents ambiguity and keeps the publish path simple and fast.
- **No regex.** Full regex matching is a footgun (ReDoS, surprise matches, unreadable patterns). Two wildcard tokens cover >95% of real use cases. If someone needs regex, they can subscribe to `>` and filter client-side.

## Current State

| Capability | Implementation | Gap |
|---|---|---|
| Topic naming | `TopicName(pub String)` — newtype over String, only validates non-empty. No character restrictions or length limits. | No dot-delimited convention, no `*`/`>` rejection on publish |
| Subscribe | `manager.subscribe(ns, topic, client, group, start_from) -> Result<(Receiver, TopicOffset)>` — exact topic match only | No pattern matching |
| Topic creation | Auto-created on first publish in `QueueEngine::get_or_create_topic()` (read lock → write lock double-check). No notification mechanism. | No way to notify existing subscribers when a new topic appears |
| Deliver frame | `Frame::Deliver { message_id, offset, timestamp_millis, topic, payload }` — includes `topic` field | Already carries topic name — no wire change needed for fan-in |
| Consumer groups | Per-topic offset tracking via `ConsumerGroupDeliveryState` | Would need per-topic tracking within a wildcard subscription |
| Delivery forwarders | Per-subscription `tokio::spawn` task pumping from engine to `mpsc::Sender` | One forwarder per exact topic — wildcard needs multiple |
| Client subscription tracking | `client_subscriptions: Arc<RwLock<HashMap<ClientId, Vec<(NamespaceId, TopicName)>>>>` — reverse index | Stores exact topic names, not patterns |
| Reconnect | `SubscriptionEntry { topic, group, last_acked_offset }` — per-subscription in Reconnect frame | Would need to distinguish patterns from exact topics |
| Session inflight | `inflight_topics: HashMap<MessageId, TopicName>` — maps message ID to topic for Ack routing | Works as-is — each delivered message has a concrete topic |

## Design

### Wildcard Syntax

Topic names are split on `.` (dot) into tokens. Wildcards are special tokens:

| Pattern | Meaning | Example | Matches | Does Not Match |
|---|---|---|---|---|
| `*` | Exactly one token | `orders.*` | `orders.us`, `orders.eu` | `orders`, `orders.us.east` |
| `>` | One or more trailing tokens | `orders.>` | `orders.us`, `orders.us.east`, `orders.eu.west.1` | `orders` |
| Combined | Mix of literal, `*`, and `>` | `events.*.created` | `events.user.created`, `events.order.created` | `events.created`, `events.user.updated` |

**Rules:**
- `>` must be the last token (if present). `orders.>.foo` is invalid.
- `*` can appear in any position except after `>`.
- An empty token (`orders..foo`) is invalid.
- A topic name that is just `*` or `>` is valid and matches accordingly.

### Topic Naming Convention

To support wildcards, topic names gain a soft convention: dot-delimited hierarchical names. This is not enforced for plain topics — `my-topic-name` still works — but wildcard subscriptions only make sense with dot-delimited names.

Validation: topic names must not contain `*` or `>` characters. These are reserved for subscription patterns. Publishing to a topic containing `*` or `>` returns `ErrorCode::InvalidTopic`.

### Protocol Changes

#### New Subscribe Variant

No new frame type needed. The existing `Subscribe`, `SubscribeAt`, and `SubscribeFrom` frames already carry a `topic` field. When the topic contains `*` or `>`, the server treats it as a wildcard subscription. This is a semantic change, not a wire format change.

**Capability gate:** Wildcard subscriptions require `CAP_WILDCARD` (`1 << 4`, bit 4). If a client sends a wildcard pattern without `CAP_WILDCARD` negotiated, the server returns `ErrorCode::InvalidTopic`.

#### SubscribeAck Changes

```
Frame::SubscribeAck (updated):
  [u16 topic_length]
  [u8* topic]              ← echoes the pattern, not an expanded topic
  [u64 start_offset]       ← 0 for wildcard (no single offset)
  [u16 matched_count]      ← NEW: number of currently matching topics (0 if CAP_WILDCARD not set)
```

Backward compatible: `matched_count` is only present when `CAP_WILDCARD` is negotiated.

### Subscription Matching Engine

```rust
/// Compiled wildcard pattern for fast matching.
pub struct TopicPattern {
    tokens: Vec<PatternToken>,
    is_wildcard: bool,
}

enum PatternToken {
    Literal(String),
    Single,       // *
    MultiTrail,   // >
}

impl TopicPattern {
    /// Parse a subscription topic string into a pattern.
    /// Returns a non-wildcard pattern for exact matches (fast path).
    pub fn parse(topic: &str) -> Result<Self, Error>;

    /// Test whether a concrete topic name matches this pattern.
    pub fn matches(&self, topic: &str) -> bool;

    /// Returns true if this is a wildcard pattern.
    pub fn is_wildcard(&self) -> bool;
}
```

**Performance:** Pattern matching is O(n) where n is the number of tokens. For the hot path (publish → check if any wildcard subscribers match), we need an efficient lookup structure rather than iterating all patterns.

### Trie-Based Matcher

```rust
/// Trie for efficient wildcard subscription matching.
/// Given a concrete topic, returns all matching subscription patterns.
pub struct SubscriptionTrie {
    root: TrieNode,
}

struct TrieNode {
    // Literal children: token string → child node
    children: HashMap<String, TrieNode>,
    // Single-wildcard child (matches any one token)
    star_child: Option<Box<TrieNode>>,
    // Multi-wildcard terminal (matches rest of topic)
    gt_subscribers: Vec<SubscriptionId>,
    // Exact-match terminals at this node
    exact_subscribers: Vec<SubscriptionId>,
}

impl SubscriptionTrie {
    /// Register a wildcard subscription pattern.
    pub fn insert(&mut self, pattern: &TopicPattern, sub_id: SubscriptionId);

    /// Remove a subscription.
    pub fn remove(&mut self, pattern: &TopicPattern, sub_id: SubscriptionId);

    /// Find all subscriptions matching a concrete topic name.
    /// Called on every publish — must be fast.
    pub fn matching(&self, topic: &str) -> Vec<SubscriptionId>;
}
```

The trie is per-namespace, held in the namespace state. `matching()` is called on publish to fan out to wildcard subscribers.

### Namespace Manager Changes

#### Wildcard Subscription Lifecycle

1. Client sends `Subscribe { topic: "orders.*" }`
2. Server parses pattern, validates syntax
3. Server finds all existing topics matching `orders.*`
4. For each matching topic, creates a delivery forwarder (same as individual subscribe)
5. Registers the pattern in the `SubscriptionTrie`
6. Sends `SubscribeAck { topic: "orders.*", start_offset: 0, matched_count: N }`
7. **On future publish to a new topic matching `orders.*`:** the namespace manager checks the trie, finds the wildcard subscription, auto-subscribes to the new topic

#### New Topic Auto-Subscribe

**Core challenge:** Topic auto-creation happens in `QueueEngine::get_or_create_topic()`, which the namespace manager calls during publish. The namespace manager needs to know when a topic is *newly created* vs. already existing, so it can check the trie for wildcard matches.

**Approach:** `QueueEngine::publish()` already returns `PublishResult { offset, evicted_messages }`. Extend it with a `topic_created: bool` flag:

```rust
pub struct PublishResult {
    pub offset: TopicOffset,
    pub evicted_messages: u64,
    pub topic_created: bool,   // NEW: true if this publish created the topic
}
```

Then in the namespace manager's publish path:

```rust
// In manager.publish(), after engine.publish():
if result.topic_created {
    let matching_subs = self.subscription_trie.read().matching(&topic);
    for sub_id in matching_subs {
        // Start delivery from offset 0 of the new topic
        self.attach_subscriber_to_topic(sub_id, &topic, SubscribeStart::Offset(TopicOffset(0)));
    }
}
```

**Performance concern:** The trie lookup happens in the publish path, but ONLY when a new topic is created (rare — most publishes go to existing topics). The `topic_created` flag avoids trie lookups on the hot path.

**Concurrency:** `subscription_trie` must be behind `RwLock` — write lock on subscribe/unsubscribe, read lock on new-topic-check. This is acceptable because new topic creation is infrequent.

#### Consumer Group Semantics

Wildcard consumer groups maintain per-topic offset tracking. Group `g1` subscribed to `orders.*` has separate offsets for `orders.us`, `orders.eu`, etc.

```rust
struct WildcardGroupState {
    pattern: TopicPattern,
    group: GroupName,
    // Per-topic state for each matched topic
    topic_states: HashMap<TopicName, ConsumerGroupTopicState>,
}

struct ConsumerGroupTopicState {
    next_offset: TopicOffset,
    consumers: Vec<ConsumerId>,  // round-robin within this topic
}
```

#### Unsubscribe

`Unsubscribe { topic: "orders.*" }` removes the wildcard subscription and all its per-topic delivery forwarders. Unsubscribing from a specific topic within a wildcard (e.g., `Unsubscribe { topic: "orders.us" }` while subscribed to `orders.*`) is not supported — returns `ErrorCode::InvalidOperation`.

### Offset Handling

Wildcard subscriptions don't have a single offset. The behavior depends on the subscribe variant:

| Frame | Wildcard Behavior |
|---|---|
| `Subscribe` | Start at head of each matching topic |
| `SubscribeAt { offset }` | Not supported for wildcards — returns `ErrorCode::InvalidOperation` (offset is per-topic, meaningless across topics) |
| `SubscribeFrom { timestamp }` | Supported — each matching topic resolves the timestamp to its own offset |

### Topic Name Validation

**Required change:** `TopicName::from_str()` currently accepts any non-empty string. Add validation:

1. **Publish-side:** Reject topic names containing `*` or `>` — these are wildcard-only characters. `ErrorCode::InvalidTopic`.
2. **Subscribe-side:** Allow `*` and `>` in topic names passed to subscribe (they indicate a wildcard pattern).
3. **Length limit:** Add a max topic name length (e.g., 255 bytes) — prevents memory abuse from extremely long topic names.
4. **Empty token check:** Reject `..` (empty tokens between dots) on both publish and subscribe.

This validation should be added in Phase 20.1 before any wildcard logic. It's a prerequisite — publishing to `orders.*` must be rejected even without wildcard subscription support.

### Client Subscription Tracking

The current `client_subscriptions` reverse index stores `Vec<(NamespaceId, TopicName)>` — exact topic names. For wildcard subscriptions, this needs to track the *pattern* so that unsubscribe works:

```rust
// Current:
client_subscriptions: Arc<RwLock<HashMap<ClientId, Vec<(NamespaceId, TopicName)>>>>

// After:
client_subscriptions: Arc<RwLock<HashMap<ClientId, Vec<SubscriptionEntry>>>>

struct SubscriptionEntry {
    namespace: NamespaceId,
    topic_or_pattern: TopicName,  // exact topic or wildcard pattern
    is_wildcard: bool,
    matched_topics: Vec<TopicName>,  // concrete topics this pattern expanded to
}
```

On unsubscribe of a wildcard pattern, all `matched_topics` delivery forwarders are stopped.

### Reconnect Frame Interaction (Plan 14.3)

The Reconnect frame carries `SubscriptionEntry { topic, group, last_acked_offset }`. For wildcard subscriptions:

- **Problem:** A wildcard subscription expands to N concrete topics, each with its own offset. The Reconnect frame can't carry a single `last_acked_offset` for a wildcard.
- **Solution:** On Reconnect, wildcard subscriptions are expanded into their per-topic entries. The client must send one `SubscriptionEntry` per concrete topic it was consuming (with the actual topic name, not the pattern), OR one entry with the pattern and no offset (re-subscribe from head).
- **Recommendation:** Reconnect with wildcard pattern re-subscribes from head of each matching topic. Clients that need offset-exact recovery should use per-topic `SubscribeAt` after Reconnect. Document this limitation.

### Delivery Channel Fan-In

Each wildcard subscription creates N delivery forwarders (one per matching topic), all sending to the same `mpsc::Sender<PublishedMessage>`. This is the existing fan-in pattern — no change to the delivery channel or the TCP connection handler.

**Concern:** If a wildcard matches 1000 topics, 1000 delivery forwarders are spawned. Each is a lightweight tokio task, but at scale this could become a resource concern.
- **Mitigation:** Add a configurable limit `max_topics_per_wildcard` (default 1000). If a wildcard matches more topics than the limit, the subscribe is rejected with `ErrorCode::QuotaExceeded`.
- Track per-connection wildcard count and per-wildcard topic count in metrics.

### Fetch Interaction

`Fetch` and `FetchFrom` are topic-specific (stateless pull). Wildcard patterns in Fetch don't make sense — Fetch returns messages from a single topic with contiguous offsets. Wildcard Fetch would require cross-topic merging.

**Decision:** Reject Fetch/FetchFrom with wildcard patterns. Return `ErrorCode::InvalidTopic` with message "wildcard patterns not supported for Fetch — use Subscribe or fetch each topic individually".

### Metrics

- `fafb_wildcard_subscriptions_active{namespace}` (gauge) — active wildcard subscriptions
- `fafb_wildcard_match_topics{namespace, pattern}` (gauge) — topics currently matching each wildcard pattern
- `fafb_wildcard_auto_subscribe_total{namespace}` (counter) — times a new topic triggered auto-subscribe for wildcard subscribers
- `fafb_wildcard_match_duration_seconds` (histogram) — time to evaluate trie matching on publish (should be <1µs)

## Implementation Phases

### Phase 20.1 — Topic Validation & Pattern Parser

**Goal:** Add topic name validation rules (prerequisite for wildcards) and implement the `TopicPattern` parser/matcher. No subscription behavior changes yet.

- [x] Add topic name validation to `TopicName`
  - [x] Max length: 255 bytes — reject longer names with `ErrorCode::InvalidTopic`
  - [x] Reject empty tokens between dots (`orders..foo`, `..`, `.`) with descriptive error
  - [x] Publish-side: reject topics containing `*` or `>` characters — `ErrorCode::InvalidTopic`
  - [x] Subscribe-side: allow `*` and `>` (they indicate wildcard patterns)
- [x] Reject wildcard patterns in Fetch/FetchFrom frames
  - [x] If topic contains `*` or `>`, return `ErrorCode::InvalidTopic` with message "wildcard patterns not supported for Fetch"
- [x] Create `src/wildcard.rs` module (or `src/subscription/pattern.rs`)
- [x] Implement `TopicPattern` type
  - [x] `TopicPattern::parse(topic: &str) -> Result<Self, Error>` — split on `.`, classify tokens as `Literal`/`Single`/`MultiTrail`
  - [x] Validation: `>` only as last token, `*` not after `>`, no empty tokens
  - [x] `TopicPattern::is_wildcard() -> bool` — returns `true` if any token is `*` or `>`
  - [x] `TopicPattern::matches(topic: &str) -> bool` — O(n) token-by-token matching
- [x] Unit tests for `TopicPattern`
  - [x] `orders.*` matches `orders.us`, `orders.eu`; does not match `orders`, `orders.us.east`
  - [x] `orders.>` matches `orders.us`, `orders.us.east`; does not match `orders`
  - [x] `events.*.created` matches `events.user.created`; does not match `events.created`
  - [x] `*` matches any single-token topic
  - [x] `>` matches any non-empty topic
  - [x] Invalid patterns rejected: `orders.>.foo`, empty tokens, empty string
- [x] Unit tests for topic name validation
  - [x] Publish rejects `orders.*`, `events.>`
  - [x] Publish accepts `orders.us`, `my-topic`, `a.b.c`
  - [x] Length limit enforced

**Verification:** Topic validation prevents publishing to wildcard-like names. Pattern parser handles all NATS-style wildcard combinations correctly. `cargo test` passes. No subscription behavior changes.

---

### Phase 20.2 — Subscription Trie

**Goal:** Implement the trie data structure for efficient wildcard subscription matching. Given a concrete topic name, the trie returns all matching subscription patterns in sub-microsecond time.

- [x] Implement `SubscriptionTrie` in `src/wildcard.rs`
  - [x] `TrieNode` with `children: HashMap<String, TrieNode>`, `star_child: Option<Box<TrieNode>>`, `gt_subscribers: HashSet<SubscriptionId>`, `exact_subscribers: HashSet<SubscriptionId>`
  - [x] `insert(pattern: &TopicPattern, sub_id: SubscriptionId)` — walk/create trie nodes for each token, idempotent (HashSet dedup)
  - [x] `remove(pattern: &TopicPattern, sub_id: SubscriptionId) -> bool` — walk trie, remove sub_id from terminal node, prunes empty branches
  - [x] `matching(topic: &str) -> Vec<SubscriptionId>` — walk trie with literal tokens, collecting all matching subscriptions (exact, `*`, and `>` matches)
  - [x] `len() -> usize` — total (pattern, sub_id) registrations, tracked via counter
  - [x] `is_empty() -> bool` — zero-cost check via counter
- [x] Handle overlapping patterns correctly
  - [x] `orders.*` and `orders.>` both match `orders.us` — both returned
  - [x] `orders.*` and `orders.us` (exact) both match `orders.us` — both returned
- [x] Prune empty trie branches on remove (optional optimization)
- [x] Unit tests for trie correctness
  - [x] Insert + match: single pattern, multiple patterns, overlapping patterns
  - [x] Remove: verify pattern no longer matches after removal
  - [x] Empty trie returns no matches
  - [x] Deeply nested patterns (`a.b.c.d.e.*.f`)
  - [x] `>` at different depths
- [x] Benchmark: 10K patterns inserted, 1M match operations — verify <1µs per match
  - Results (benches/wildcard_bench.rs): 100 patterns → 203ns/match, 1K patterns → 375ns/match, 10K patterns → 2µs/match (worst-case overlapping patterns). Mixed realistic workload (10K patterns, 1K lookups): **150ns/lookup**. Miss (no matching pattern): 34ns regardless of trie size.
  - Trie miss lookups are O(1) since the root-level literal check prunes immediately.
  - The 10K hit case exceeds 1µs due to synthetic pattern overlap density; realistic workloads stay well under target.

**Verification:** Trie returns correct matching subscriptions for all pattern combinations. Realistic workload performance is sub-microsecond per match. `cargo test` passes. No regressions in existing benchmarks (fanout, queue, codec, storage all clean).

---

### Phase 20.3 — Wildcard Subscribe Flow

**Goal:** Wire wildcard patterns into the subscription system. Clients can subscribe to patterns like `orders.*` and receive messages from all matching existing topics.

- [x] Add `CAP_WILDCARD` capability constant (`1 << 4`, bit 4) to `src/codec/frame.rs`
  - [x] Add to `SERVER_CAPABILITIES` bitmask
  - [x] Gate wildcard subscriptions: reject patterns without `CAP_WILDCARD` with `ErrorCode::InvalidTopic`
- [x] Add `max_topics_per_wildcard: usize` config (default 1000) to `ManagerConfig`
- [x] Add `SubscriptionTrie` (behind `RwLock`) to namespace state
- [x] Update `NamespaceManager::subscribe()` to handle wildcard patterns
  - [x] Parse topic as `TopicPattern`
  - [x] If wildcard: find all existing topics matching the pattern
  - [x] Enforce `max_topics_per_wildcard` limit — reject if too many matching topics
  - [x] Create delivery forwarder for each matching topic (fan-in to single `mpsc::Sender`)
  - [x] Register pattern in `SubscriptionTrie`
  - [x] If not wildcard: existing exact-match path (unchanged)
- [x] Update `SubscribeAck` to include `matched_count: u16` when `CAP_WILDCARD` negotiated
  - [x] Backward compatible: field only present when capability negotiated
- [x] Handle `SubscribeFrom { timestamp }` with wildcards — resolve timestamp to per-topic offset for each matching topic
- [x] Reject `SubscribeAt { offset }` with wildcards — `ErrorCode::InvalidOperation` (offset is per-topic)
- [x] Update `client_subscriptions` tracking
  - [x] Refactor from `Vec<(NamespaceId, TopicName)>` to `Vec<ClientSubEntry>` with `wildcard: Option<WildcardSubState>` (consolidated — no separate wildcard map)
  - [x] Unsubscribe with wildcard pattern: remove pattern from trie + stop all per-topic delivery forwarders
  - [x] Reject unsubscribe of a specific topic within a wildcard: `ErrorCode::InvalidOperation`
- [x] Add wildcard metrics
  - [x] `fafb_wildcard_subscriptions_active{namespace}` gauge
  - [x] `fafb_wildcard_match_topics{namespace, pattern}` gauge
- [x] Return `SubscribeResult { rx, start_offset, matched_count }` from `subscribe()` — avoids redundant list_topics + pattern matching in session layer
- [x] Reject duplicate wildcard subscriptions with `DuplicateSubscription` error
- [x] Reject wildcard + consumer group at session layer (before reaching manager), consistent with SubscribeAt rejection pattern
- [x] Integration test: subscribe to `orders.*` → publish to `orders.us` and `orders.eu` → receive both messages
- [x] Integration test: subscribe to `events.>` → publish to `events.user.created` and `events.order.updated.v2` → receive both
- [x] Integration test: legacy client (no `CAP_WILDCARD`) sends wildcard pattern → receives `InvalidTopic` error
- [x] Integration test: unsubscribe wildcard → delivery stops for all matched topics

**Verification:** Wildcard subscriptions receive messages from all matching existing topics. Unsubscribe cleans up all forwarders. Legacy clients unaffected.

---

### Phase 20.4 — Auto-Subscribe on New Topic Creation

**Goal:** When a new topic is created via publish and it matches an existing wildcard subscription, automatically start delivering from it. This is the key difference from static pattern matching.

- [x] Extend `PublishResult` with `topic_created: bool` flag
  - [x] `QueueEngine::get_or_create_topic()` returns whether the topic was newly created
  - [x] Propagate through `QueueEngine::publish()` → `PublishResult`
- [x] Update namespace manager publish path
  - [x] When `result.topic_created == true`: read-lock `SubscriptionTrie` and call `matching(&topic)`
  - [x] For each matching wildcard subscription: call `attach_wildcard_topic()` to create a new delivery forwarder from offset 0
  - [x] Enforce `max_topics_per_wildcard` limit on auto-attach — log warning if exceeded, skip attach
  - [x] Skip trie lookup entirely when `topic_created == false` (hot path optimization)
- [x] Add `fafb_wildcard_auto_subscribe_total{namespace}` counter metric
- [x] Update `fafb_wildcard_match_topics` gauge when new topics are matched
- [x] Add `fafb_wildcard_match_duration_seconds` histogram — time for trie matching on new topic publish
- [x] Integration test: subscribe to `orders.*` → publish to `orders.eu` (new topic, didn't exist at subscribe time) → receive message
- [x] Integration test: subscribe to `orders.*` with 3 existing topics → publish to `orders.apac` (4th topic) → verify auto-subscribe, all 4 topics deliver
- [x] Integration test: auto-subscribe respects `max_topics_per_wildcard` limit
- [x] Integration test: publish to existing topic → verify NO trie lookup occurs (check metric not incremented)

**Verification:** New topics matching wildcard patterns are automatically attached to existing subscribers. Trie lookup only happens on new topic creation (no hot-path overhead). Metrics track auto-subscribe events.

---

### Phase 20.5 — Wildcard Consumer Groups

**Goal:** Make wildcard subscriptions compatible with consumer groups. Group members collectively consume from all matching topics with per-topic round-robin semantics.

- [x] Implement `WildcardGroupSubInfo` struct (in `namespace/state.rs`)
  - [x] `pattern_topic: TopicName`
  - [x] `group: GroupName`
  - [x] `members: HashMap<ClientId, mpsc::Sender<PublishedMessage>>` — tracks all group members for auto-attach
  - [x] `matched_topics: Vec<TopicName>` — concrete topics this wildcard group matches
- [x] Reuse existing `ConsumerGroupState` per-topic — no new per-topic struct needed
  - [x] One `ConsumerGroupState` per matched topic with standard `next_offset` + round-robin
  - [x] `WildcardGroupSubInfo` + `NamespaceState.wildcard_group_subs: RwLock<HashMap<SubscriptionId, WildcardGroupSubInfo>>`
- [x] Wire wildcard group state into existing consumer group infrastructure
  - [x] `subscribe_wildcard_group()` — create/join per-topic consumer groups for each matching topic
  - [x] Second member joining same `(pattern, group)` reuses existing trie entry + joins all per-topic groups
  - [x] `attach_wildcard_group_topic()` — auto-create group for new topic, add all tracked members
  - [x] On Ack: `find_client_group()` updated to check wildcard `matched_topics` for concrete topic lookup
- [x] Handle group member join/leave with wildcard subscriptions
  - [x] New member joins: added to all per-topic consumer groups + `WildcardGroupSubInfo.members`
  - [x] Member unsubscribes: removed from all per-topic groups; last member removes trie entry
  - [x] Member disconnects: same cleanup path as unsubscribe, with inflight reassignment
- [x] Remove session-layer rejection guard (`session.rs`: wildcard + consumer group)
- [x] Remove manager-layer rejection guard (`subscribe_wildcard()`)
- [x] Integration test: 2 consumers in group subscribed to `orders.*`, publish to `orders.us` and `orders.eu` → verify round-robin per topic
- [x] Integration test: group wildcard subscribe → new topic created → verify new topic added to group delivery
- [x] Integration test: 2 members auto-subscribe both receive from new topic
- [x] Integration test: consumer group Ack routing — messages from different topics acked correctly
- [x] Integration test: unsubscribe wildcard group → delivery stops
- [x] Integration test: disconnect cleanup — remaining member continues receiving
- [x] Integration test: Nack routing with wildcard groups — verifies DLQ write succeeds
- [x] Integration test: mixed exact + wildcard group — both in same per-topic group, auto-subscribe only for wildcard member
- [x] Integration test: unsubscribe specific topic within wildcard group → `InvalidOperation`
- [x] `fafb_wildcard_group_members{namespace, pattern, group}` gauge metric — tracks member count on join/leave/disconnect
- [x] Lock ordering documentation updated to include `wildcard_group_subs`

**Verification:** Consumer groups work with wildcard patterns. Per-topic offset tracking is correct. Round-robin delivery per topic. New topics auto-added to group.

---

### Phase 20.6 — Documentation & Reconnect Notes

**Goal:** Document wildcard subscription semantics, update protocol/SDK specs, and document Reconnect limitations.

- [x] Update protocol documentation with wildcard semantics
  - [x] Wildcard syntax: `*` (one token), `>` (trailing tokens), dot-delimited
  - [x] `CAP_WILDCARD` capability negotiation
  - [x] `SubscribeAck.matched_count` field
  - [x] Nack/Ack routing with wildcard subscriptions
- [x] Update client SDK spec with wildcard subscribe API
  - [x] `subscribe("orders.*")` usage
  - [x] Consumer group with wildcards
  - [x] Limitations: no `SubscribeAt` with wildcards, no wildcard Fetch
- [x] Document Reconnect limitations for wildcard subscriptions
  - [x] Wildcard Reconnect re-subscribes from head of each matching topic
  - [x] Clients needing exact offset recovery should use per-topic `SubscribeAt` after Reconnect
- [x] Add usage guide (`docs/wildcard_subscriptions.md`) with examples
  - [x] Common patterns: `orders.*`, `events.>`, `metrics.*.cpu`
  - [x] Consumer group example
  - [x] Auto-subscribe behavior explanation
- [x] Update CLAUDE.md with wildcard-related constants and types
  - [x] `CAP_WILDCARD`, `TopicPattern`, `SubscriptionTrie`
  - [x] `max_topics_per_wildcard` config
  - [x] Wildcard metrics

**Verification:** Documentation accurately reflects implementation. New developers can understand wildcard subscriptions from docs alone.

## Testing Strategy

- **Unit tests:** Pattern parsing, matching correctness, trie operations, edge cases (empty pattern, all-wildcard, deeply nested)
- **Integration tests:** Subscribe → publish → deliver, new topic auto-subscribe, unsubscribe cleanup, consumer group with wildcards, mixed wildcard and exact subscriptions
- **Performance tests:** Trie matching latency with 1K/10K patterns, publish throughput impact
- **Compatibility tests:** Legacy client without `CAP_WILDCARD` — wildcard topic rejected gracefully
- **Stress tests:** 100 wildcard subscribers × 100 matching topics — verify memory/task count, delivery latency

## Dependencies

No new crate dependencies. Trie and pattern matching are implemented in-project.

## Risks and Mitigations

| Risk | Mitigation |
|---|---|
| Trie lookup on every publish adds latency | Only when `topic_created = true` (first publish to a topic). Existing-topic publishes skip trie entirely. |
| Wildcard matching 1000+ topics spawns 1000+ delivery forwarders | `max_topics_per_wildcard` limit (default 1000). Reject subscriptions that would exceed it. |
| `client_subscriptions` refactor breaks existing unsubscribe logic | Incremental migration — add `is_wildcard` field, keep backward-compatible for exact matches. |
| Reconnect with wildcard subscriptions loses per-topic offsets | Document limitation: wildcard Reconnect re-subscribes from head. Clients needing exact offset recovery should use per-topic SubscribeAt. |
| Topic name validation (`*`/`>` rejection) breaks existing topics containing these characters | Unlikely — `*` and `>` are not typical in topic names. Add validation only on the publish path initially. Existing topics with these characters can still be read. |
| `SubscribeAck` wire format change (`matched_count`) breaks legacy clients | Only sent when `CAP_WILDCARD` is negotiated. Legacy clients see the old format. |

## Out of Scope

- **Publish to wildcard patterns** — Publish always targets an exact topic. No broadcast-publish.
- **Wildcard Fetch** — Fetch is topic-specific with contiguous offsets. Cross-topic merging is a separate feature.
- **Regex-based subscriptions** — Two wildcard tokens (`*`, `>`) cover 95%+ of use cases. Regex adds ReDoS risk and complexity.
- **Topic hierarchy enforcement** — Dot-delimited naming is a convention, not enforced. Topics without dots still work.
- **Header-based filtering** — "Deliver only messages where header X = Y" within a wildcard subscription. Requires a separate filtering engine (Plan 19 headers are a prerequisite).
- **Negative patterns** — "Subscribe to `orders.*` except `orders.internal`". Can be added later as a filter on top of wildcard matching.
