> ## Documentation Index
> Fetch the complete documentation index at: https://docs.westyx.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# .NET SDK - Stream Observer

> Opt-in structured callbacks for SSE lifecycle events in the .NET SDK.

Pass an `IStreamObserver` via `NexusConfig.Observer` to receive structured callbacks for every SSE lifecycle transition. The observer is the **canonical** way to wire SDK state into ops dashboards, custom telemetry, or load-test tooling - far more reliable than scraping `INexusLogger` output by substring match.

The observer is **opt-in**. When `NexusConfig.Observer` is `null` the SDK uses the internal no-op observer at zero overhead.

## The interface

```csharp theme={null}
public interface IStreamObserver
{
    void OnConnected();
    void OnDisconnected(Exception? cause);
    void OnEvent(string name);
    void OnReconnectAttempt(int attempt);
    void OnFallback(string reason);
}
```

Inherit `NoopStreamObserver` to override only the hooks you care about:

```csharp theme={null}
public sealed class MyObserver : NoopStreamObserver
{
    private long _events;
    public long Events => Interlocked.Read(ref _events);
    public override void OnEvent(string name) => Interlocked.Increment(ref _events);
}
```

## Hook semantics

| Hook                    | Fires when                                                                                    | Argument                                                      |
| ----------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `OnConnected()`         | `GET /v1/stream` returns `200 OK` and the SDK is ready to read events                         | none                                                          |
| `OnDisconnected(cause)` | An active stream connection ends - clean cancel, transport error, or server-initiated close   | `null` for clean shutdown, the underlying exception otherwise |
| `OnEvent(name)`         | A whitelisted SSE event arrives, **before** the SDK runs the event-triggered sync             | event name string (e.g. `"config.updated"`)                   |
| `OnReconnectAttempt(n)` | The SDK is about to back off and retry after a transport error                                | 1-based attempt counter (max 3 with current threshold)        |
| `OnFallback(reason)`    | The SDK switches to TTL polling **or** enters a `Retry-After`-driven reconnect wait (v0.3.1+) | `FallbackReason.MaxErrors` or `FallbackReason.RateLimited`    |

### `OnFallback` reasons

```csharp theme={null}
public static class FallbackReason
{
    public const string MaxErrors   = "max-errors";
    public const string RateLimited = "rate-limited";
}
```

| Constant                     | When                                                                                                                   |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `FallbackReason.MaxErrors`   | 3 consecutive transport errors - backoff exhausted, the SDK gives up and switches to TTL polling for good              |
| `FallbackReason.RateLimited` | server returned `429 Too Many Requests` (tier connection-limit hit) - see active-recovery vs terminate semantics below |

#### `RateLimited` - active recovery vs terminate (v0.3.1+)

As of v0.3.1, `FallbackReason.RateLimited` no longer implies the stream is dead. It fires in **both** of these cases:

1. **Active recovery** - `429` with a parseable `Retry-After` header. The SDK fires `OnFallback(FallbackReason.RateLimited)`, `await Task.Delay`-s the indicated duration (clamped `[5 s, 5 min]`), then **reconnects automatically** on the next loop iteration in `RunStreamAsync`. The episode does NOT increment the transport-failure counter. No action is required from the observer; a subsequent `OnConnected()` confirms the stream has resumed.
2. **Terminate (legacy)** - `429` with no parseable `Retry-After`. The SDK fires `OnFallback(FallbackReason.RateLimited)`, closes the stream, and returns from `RunStreamAsync`. TTL polling takes over.

## Threading contract

<Warning>
  Observer methods are called **synchronously from the SDK's stream-reader Task**. They MUST return promptly. The SDK does not protect against blocking observers and does not wrap the calls in `Task.Run`.
</Warning>

If your observer does anything substantial (network I/O, lock contention, disk writes), copy the argument and dispatch the work to your own pipeline before returning:

```csharp theme={null}
public sealed class SlowObserver : NoopStreamObserver
{
    private readonly Channel<string> _queue;
    public override void OnEvent(string name)
    {
        // Drop on overflow - applying backpressure to the SDK Task would
        // block sync() and stall reads.
        _queue.Writer.TryWrite(name);
    }
}
```

## Counter pattern

```csharp theme={null}
public sealed class CounterObserver : NoopStreamObserver
{
    private long _events, _reconnects, _fallbacks;
    public long Events     => Interlocked.Read(ref _events);
    public long Reconnects => Interlocked.Read(ref _reconnects);
    public long Fallbacks  => Interlocked.Read(ref _fallbacks);

    public override void OnEvent(string _)            => Interlocked.Increment(ref _events);
    public override void OnReconnectAttempt(int _)    => Interlocked.Increment(ref _reconnects);
    public override void OnFallback(string _)         => Interlocked.Increment(ref _fallbacks);
}

var obs = new CounterObserver();
await using var client = await NexusClient.CreateAsync(new NexusConfig
{
    BaseUrl  = "...",
    ApiKey   = "...",
    Observer = obs,
});
```

## Active-connection gauge pattern

```csharp theme={null}
public sealed class GaugeObserver : NoopStreamObserver
{
    public bool Connected { get; private set; }
    public override void OnConnected()                  => Connected = true;
    public override void OnDisconnected(Exception? _)   => Connected = false;
    public override void OnFallback(string _)           => Connected = false;
}
```

`OnFallback` is included because after a `MaxErrors` fallback the SDK will not reconnect - the gauge should stay `false` until you call `ConnectStream()` again on a new client. For `RateLimited` with `Retry-After` (v0.3.1+) the SDK reconnects on its own; the subsequent `OnConnected()` re-arms the gauge to `true` automatically.

## Per-event-type counter

```csharp theme={null}
public sealed class PerEventObserver : NoopStreamObserver
{
    private readonly ConcurrentDictionary<string, long> _counts = new();
    public IReadOnlyDictionary<string, long> Counts => _counts;
    public override void OnEvent(string name) =>
        _counts.AddOrUpdate(name, 1, (_, n) => n + 1);
}
```
