> ## 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.

# Node.js SDK - Stream Observer

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

Pass a `StreamObserver` via `NexusConfig.streamObserver` 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 `console` output by substring match.

The observer is **opt-in**. When `NexusConfig.streamObserver` is `undefined` the SDK uses `NOOP_STREAM_OBSERVER` at zero overhead.

## The interface

```ts theme={null}
import type { StreamObserver } from '@westyx-nexus/sdk-nodejs';

interface StreamObserver {
  onConnected?(): void;
  onDisconnected?(cause: unknown | null): void;
  onEvent?(name: string): void;
  onReconnectAttempt?(attempt: number): void;
  onFallback?(reason: string): void;
  onQuarantined?(reason: string, expiresAt: Date): void;  // v0.4.0+
}
```

Every method is **optional** - the SDK calls only the ones you set. You can write `{ onEvent: (n) => count(n) }` and ignore the rest.

## 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 disconnect, transport error, or server-initiated close | `null` for clean shutdown, the underlying error 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 gives up and switches to TTL polling                                                    | `FALLBACK_REASON_MAX_ERRORS` or `FALLBACK_REASON_RATE_LIMITED` |
| `onQuarantined(reason, expiresAt)` *(v0.4.0)* | The stream received a quarantine `429` - the SDK will sleep until `expiresAt` and reconnect     | server-supplied reason string + expiry `Date`                  |

### What counts as a "whitelisted event"

`onEvent` fires only for events on the SDK's documented event-type list:

* `resync`
* `config.created` / `config.updated` / `config.deleted`
* `flag.created` / `flag.updated` / `flag.toggled` / `flag.deleted`
* `secret.created` / `secret.updated` / `secret.deleted`

Unknown events (or the WIF `auth_expiring` control event) do NOT trigger `onEvent`.

### `onFallback` reasons

```ts theme={null}
import {
  FALLBACK_REASON_MAX_ERRORS,
  FALLBACK_REASON_RATE_LIMITED,
} from '@westyx-nexus/sdk-nodejs';
```

| Constant         | When                                                                                                |
| ---------------- | --------------------------------------------------------------------------------------------------- |
| `'max-errors'`   | 3 consecutive transport errors - backoff exhausted, the SDK gives up                                |
| `'rate-limited'` | server returned `429 Too Many Requests` (tier connection-limit hit) - see dual-semantics note below |

#### `FALLBACK_REASON_RATE_LIMITED` - dual semantics (v0.3.1+)

As of v0.3.1, `'rate-limited'` fires in **both** of the following situations:

1. **Terminate case (no `Retry-After`)** - the server returned `429` without a parseable `Retry-After` header. The SDK closes the stream, sets `client.streamStatus` to `'fallback'`, and does NOT reconnect. This is the legacy behaviour, preserved as-is.
2. **Cooperative-reconnect case (with `Retry-After`)** - the server returned `429` with a parseable `Retry-After` header (clamped to `[5 s, 5 min]`). The SDK fires `onFallback('rate-limited')` for the metric, then schedules an automatic reconnect after the indicated delay. The Retry-After delay does NOT count toward `MAX_FAILURES`.

<Note>
  Quarantine `429` (v0.4.0+, body `{"error":"quarantined",...}`) does NOT fire `onFallback` - it fires `onQuarantined` instead. The stream reconnects automatically at `expiresAt`.
</Note>

In other words, `onFallback('rate-limited')` is **per rate-limit episode** - not a one-shot "the SDK has given up" signal. If you need to distinguish the two cases, inspect `client.streamStatus` shortly after the callback returns: it will be `'fallback'` in the terminate case and either `'connected'` or transitioning back to `'connected'` in the cooperative-reconnect case.

```ts theme={null}
const observer: StreamObserver = {
  onFallback(reason) {
    if (reason === FALLBACK_REASON_RATE_LIMITED) {
      metrics.increment('nexus.sse.rate_limited');
      // gauge update - defer one tick so the SDK has finished its state
      // transition before we read streamStatus
      setImmediate(() => {
        gauge.set('nexus.sse.connected', client.streamStatus === 'connected' ? 1 : 0);
      });
    }
  },
};
```

## Threading contract

<Warning>
  Observer methods are called **synchronously from the SDK's stream-reader event loop**. They MUST return promptly.
</Warning>

If your observer does anything substantial (network I/O, mutex contention, disk writes), copy the argument and dispatch the work via `setImmediate`, `queueMicrotask`, or your own queue before returning:

```ts theme={null}
const queue: string[] = [];
const observer: StreamObserver = {
  onEvent(name) {
    queue.push(name);
    setImmediate(processQueue);
  },
};
```

## Counter pattern

```ts theme={null}
import { NexusClient, type StreamObserver } from '@westyx-nexus/sdk-nodejs';

const counters = { events: 0, reconnects: 0, fallbacks: 0 };
const observer: StreamObserver = {
  onEvent: () => counters.events++,
  onReconnectAttempt: () => counters.reconnects++,
  onFallback: () => counters.fallbacks++,
};

const client = await NexusClient.create({
  endpoint: 'https://...',
  apiKey:   'wxs_...',
  streamObserver: observer,
});

// later ...
console.log('SSE:', counters);
```

## Active-connection gauge pattern

```ts theme={null}
let connected = false;
const observer: StreamObserver = {
  onConnected:    () => { connected = true; },
  onDisconnected: () => { connected = false; },
  onFallback:     () => { connected = false; },
};
```

`onFallback` is included because in the terminate cases the SDK will not reconnect on its own - the gauge should stay `false` until you call `connectStream()` again. In the v0.3.1+ cooperative `rate-limited` path (server sent a `Retry-After`), the SDK will reconnect automatically and `onConnected` will fire again when the stream comes back.

## Independence from the logger

The observer is independent of any logging output. The two are complementary:

* The SDK's `console.*` output carries human-readable text for operators.
* `StreamObserver` carries structured metrics for tooling.
