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

# Python SDK - Caching Behaviour

> When does the Python SDK hit the network, snapshot atomicity, and ETag/304.

The SDK keeps every config / secret / flag in an **in-memory snapshot** that is replaced atomically as a whole on each sync. Reads never block on the network; only the periodic background refresh does.

The same cache semantics apply to both `NexusClient` (sync) and `AsyncNexusClient` (async) - the only difference is whether the background refresh runs in a daemon thread or an `asyncio.Task`.

## When does the SDK hit the network

| Event                                                      | Result                                                                              |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `NexusClient.create()` / `await AsyncNexusClient.create()` | **Blocking** full sync. The function only returns once the cache is populated.      |
| Read within the TTL window                                 | **No network** - served from the in-memory cache                                    |
| Read after TTL has elapsed                                 | Stale cache returned **immediately**; one background refresh fires in parallel      |
| Server returns `304 Not Modified`                          | TTL reset, no payload downloaded                                                    |
| Multiple concurrent expirations                            | Only **one** background sync runs at a time (lock guard)                            |
| SSE event received                                         | Immediate full sync, regardless of TTL                                              |
| SSE stream connected                                       | Effective TTL bumped to **60 s** as a safety net (the stream handles fresh updates) |

## The atomic snapshot guarantee

The snapshot is replaced as one immutable record. Two reads (from any thread or any coroutine) either see the same old state or the same new state - never a mix.

This means you can iterate `get_all_configs()` or `get_all_flags()` without worrying about an entry being added or removed mid-loop. The returned dict is also a defensive copy.

## ETag / 304

Every successful sync stores the server's `ETag`. The next sync sends `If-None-Match: <etag>`. If nothing has changed:

```
HTTP/1.1 304 Not Modified
```

...the SDK resets its `synced_at` timestamp and serves the existing snapshot. **No payload transferred.**

## Background sync errors

Background syncs **never raise** to the caller. If the network call fails:

1. The error is logged via `NexusLogger.error(...)`
2. The existing cache continues to be served
3. The next read after the TTL elapses retries automatically

## Cache lifetime

The cache lives as long as the `NexusClient` / `AsyncNexusClient` instance. Calling `close()` (or `await close()`) tears down the SSE thread/task, the polling timer, and (for async) closes the underlying `httpx.AsyncClient`. Typical use is **one singleton client per process**.

For Django / Flask, the framework adapters call `atexit.register(client.close)` for you. For FastAPI, the lifespan adapter awaits `close()` on shutdown.

## Diagnostics

| Property                 | What it tells you                                                                                   |
| ------------------------ | --------------------------------------------------------------------------------------------------- |
| `nexus.synced_at`        | When was the last successful sync (Unix timestamp) - useful for "last refreshed" UI / health checks |
| `nexus.stream_connected` | Is the SSE stream currently up                                                                      |
| `nexus.key_type`         | `'sk'` or `'pk'` - confirms the SDK saw the right key prefix                                        |

If you wire up a `NexusLogger`, the `debug` level emits one line per sync:

```
nexus: sync OK - configs=12 secrets=4 flags=7 key_type=sk
nexus: sync 304 Not Modified - cache retained
```

## Concurrency model

### Sync client (`NexusClient`)

* **All reads are thread-safe** - internally guarded by a single `threading.Lock` for the snapshot reference.
* **Background sync runs in a daemon thread** - at most one at a time.
* **SSE runs in a separate daemon thread** - started by `connect_stream()`.

### Async client (`AsyncNexusClient`)

* **All reads are non-async** but safe to call from any coroutine.
* **Background sync runs as an `asyncio.Task`** - at most one at a time, guarded by a flag.
* **SSE runs as a separate `asyncio.Task`** - created via `loop.create_task` inside `connect_stream()`.

In both cases, **you don't need to call `close()` explicitly if your process is exiting** - the daemon thread / asyncio loop teardown will reclaim resources. But for clean shutdown (e.g. graceful SIGTERM handling), always call `close()`.
