# Wildcard Subscriptions

Wildcard subscriptions allow a client to match multiple topics with a single subscribe request. Instead of subscribing to `orders.us`, `orders.eu`, and `orders.apac` individually, subscribe to `orders.*` and receive messages from all matching topics — including topics created after the subscription.

## Capability Requirement

Wildcard subscriptions require the `CAP_WILDCARD` capability (bit 4, `0x10`) to be negotiated during the Connect handshake. If a client sends a wildcard pattern without `CAP_WILDCARD`, the server returns `ErrorCode::InvalidTopic`.

Include `CAP_WILDCARD` in the capabilities bitmask when connecting:

```
capabilities = CAP_GOING_AWAY | CAP_RECONNECT | CAP_NACK | CAP_HEADERS | CAP_WILDCARD
             = 0x1F
```

## Wildcard Syntax

Topic names are dot-delimited segments. Two wildcard tokens are supported:

### `*` — Single Segment

Matches exactly **one** segment at the position it appears.

| Pattern | Matches | Does Not Match |
|---------|---------|----------------|
| `orders.*` | `orders.us`, `orders.eu` | `orders`, `orders.us.east` |
| `*.created` | `user.created`, `order.created` | `created`, `user.profile.created` |
| `events.*.created` | `events.user.created`, `events.order.created` | `events.created`, `events.user.profile.created` |
| `*` | `orders`, `events`, `metrics` | `orders.us` (two segments) |

### `>` — Trailing Segments

Matches **one or more** trailing segments. Must be the last token in the pattern.

| Pattern | Matches | Does Not Match |
|---------|---------|----------------|
| `orders.>` | `orders.us`, `orders.us.east`, `orders.eu.west.1` | `orders` (zero trailing) |
| `>` | Any topic with one or more segments (i.e., every topic) | *(matches everything)* |

### Combined Patterns

You can mix literal segments, `*`, and `>`:

| Pattern | Matches |
|---------|---------|
| `events.*.>` | `events.user.created`, `events.order.updated.v2` |
| `*.*.metrics` | `us.east.metrics`, `eu.west.metrics` |
| `dc1.>` | `dc1.cpu`, `dc1.mem.used`, `dc1.disk.ssd.free` |

### Invalid Patterns

| Pattern | Error |
|---------|-------|
| `orders.>.foo` | `>` must be the last segment |
| `orders..foo` | Empty segment between dots |
| *(empty string)* | Topic must be non-empty |
| `..` | Empty segments |

## Topic Name Validation

Topic names (for publishing) are validated:

- Maximum 255 bytes
- Must not contain `*` or `>` characters — these are reserved for subscription patterns
- Must not have empty segments (no `..`, leading `.`, or trailing `.`)
- Publishing to `orders.*` returns `ErrorCode::InvalidTopic`

Topic names without dots still work (`my-queue-name`). Dot-delimited naming is a convention that enables wildcards, not a requirement.

## Subscribe Examples

### Basic Wildcard Subscribe

```
// Subscribe to all order topics
client.subscribe("orders.*")
// SubscribeAck: { topic: "orders.*", start_offset: 0, matched_count: 3 }
// Now receiving from orders.us, orders.eu, orders.apac
```

### Subscribe From Timestamp

```
// Subscribe from a specific point in time across all matching topics
client.subscribeFrom("events.>", timestamp_millis)
// Each matching topic resolves the timestamp to its own offset
```

### Wildcard with Consumer Group

```
// Consumer A
client.subscribe("orders.*", group: "order-processors")

// Consumer B (same group)
client.subscribe("orders.*", group: "order-processors")

// Both consumers collectively process from all orders.* topics
// Per-topic round-robin: orders.us messages alternate between A and B,
// orders.eu messages alternate between A and B, etc.
```

## Auto-Subscribe on New Topics

When a new topic is created that matches an existing wildcard subscription, the subscriber automatically starts receiving from it — no re-subscribe needed.

**Example timeline:**

1. Client subscribes to `orders.*` — matches `orders.us` and `orders.eu` (2 topics)
2. A producer publishes to `orders.apac` for the first time (topic created)
3. The server detects the match, creates a delivery forwarder from offset 0
4. Client begins receiving `orders.apac` messages on the same delivery channel

This works for both individual subscribers and consumer groups. For groups, all members are automatically added to the new topic's consumer group.

### `max_topics_per_wildcard` Limit

A configurable limit (default 1000) caps how many topics a single wildcard subscription can match. This applies at subscribe time and during auto-subscribe:

- **At subscribe time:** If the pattern matches more topics than the limit, the subscribe is rejected with `ErrorCode::ResourceExhausted`.
- **During auto-subscribe:** If adding a new topic would exceed the limit, the new topic is skipped with a server-side warning log. Existing deliveries are unaffected.

## Unsubscribe

Unsubscribe using the original pattern:

```
client.unsubscribe("orders.*")
// All per-topic delivery forwarders are stopped
```

You **cannot** unsubscribe from a specific topic within a wildcard. Sending `unsubscribe("orders.us")` while subscribed to `orders.*` returns `ErrorCode::InvalidOperation`. To stop receiving from a specific topic, unsubscribe the entire pattern and re-subscribe with individual topic subscriptions.

## Ack and Nack

Ack and Nack work the same as with exact subscriptions. Each delivered message has a concrete `topic` and `message_id` — ack/nack routes correctly regardless of whether the subscription was wildcard or exact.

## Limitations

| Feature | Supported | Notes |
|---------|-----------|-------|
| `Subscribe` (head) | Yes | Starts at head of each matching topic |
| `SubscribeFrom` (timestamp) | Yes | Per-topic timestamp resolution |
| `SubscribeAt` (offset) | **No** | Returns `InvalidOperation` — offsets are per-topic |
| `Fetch` / `FetchFrom` | **No** | Returns `InvalidTopic` — use Subscribe or fetch individually |
| Consumer groups | Yes | Per-topic round-robin across group members |
| Nack / DLQ | Yes | Routes correctly per topic |
| Reconnect | **Partial** | Re-subscribes from head, not per-topic offsets (see below) |

### Reconnect Limitation

The Reconnect frame carries per-subscription `last_acked_offset`, which is per-topic. A wildcard subscription expands to N topics, each with its own offset.

**Recommended approach:** After reconnection, either:

1. **Re-subscribe with the wildcard pattern from head** — simplest, but may re-deliver messages that were already processed. Use idempotent processing.
2. **Send per-topic `SubscribeAt` requests** — tracks exact offsets per topic, but requires N round-trips instead of 1. Track the `offset` from every acked `Deliver` frame to enable this.

## Metrics

| Metric | Type | Description |
|--------|------|-------------|
| `fafb_wildcard_subscriptions_active{namespace}` | gauge | Active wildcard subscriptions per namespace |
| `fafb_wildcard_auto_subscribe_total{namespace}` | counter | New topics triggering auto-subscribe for wildcard subscribers |
| `fafb_wildcard_match_duration_seconds` | histogram | Time for trie matching on new topic publish (should be <1us) |
| `fafb_wildcard_group_members{namespace, pattern, group}` | gauge | Current member count of a wildcard consumer group |

## Configuration

| Config Key | Default | Description |
|------------|---------|-------------|
| `manager.max_topics_per_wildcard` | 1000 | Maximum topics a single wildcard subscription can match |
