# Usage Tracker

Buffers feature deltas in memory (or behind a custom store), flushes them to the billing API on a Tokio interval, and feeds the refreshed snapshot back to the caller. Pairs with `Client::license_sync_usage` for the `offline_snapshot` licensing mode.

```rust
use akira_billing::{UsageBuffer, MemoryBuffer, UsageTracker, TrackerOptions};
```

For `online_realtime` mode, skip the tracker and call `Client::track_usage` directly with `action = "check"` then `action = "increment"`.

## UsageBuffer trait

```rust
#[async_trait]
pub trait UsageBuffer: Send + Sync {
    async fn add(&self, feature: &str, delta: u64) -> Result<(), Error>;
    async fn drain(&self) -> Result<HashMap<String, u64>, Error>;
    async fn restore(&self, deltas: HashMap<String, u64>) -> Result<(), Error>;
}
```

Contract:

- `add` — accumulate `delta` for the feature.
- `drain` — return all pending deltas and **clear** the buffer atomically.
- `restore` — merge deltas back when a flush fails. Add to existing pending count, do not overwrite.

The trait uses `async_trait` so it's object-safe; `Arc<dyn UsageBuffer>` is the typical hand-off into `TrackerOptions`.

Implement your own buffer for durability (e.g. SQLite, sled). The in-process `MemoryBuffer` is fine for short-lived processes.

## MemoryBuffer

```rust
pub struct MemoryBuffer { /* … */ }

impl MemoryBuffer {
    pub fn new() -> Self;
}

impl Default for MemoryBuffer { /* identical */ }
```

In-process implementation backed by `tokio::sync::Mutex<HashMap<String, u64>>`. Lost on process exit — wrap with a durable store if the app may crash with pending deltas.

## TrackerOptions

```rust
pub struct TrackerOptions {
    pub buffer: Arc<dyn UsageBuffer>,
    pub sync: SyncFn,
    pub serial: Option<SerialFn>,
    pub on_refresh: Option<RefreshFn>,
    pub flush_interval: Option<Duration>,    // default 300s when None or zero
}
```

Closure types:

```rust
pub type SyncFn = Arc<
    dyn Fn(HashMap<String, u64>, u64)
        -> BoxFuture<'static, Result<LicenseSyncUsageResponse, Error>>
        + Send + Sync,
>;

pub type SerialFn = Arc<dyn Fn() -> BoxFuture<'static, Result<u64, Error>> + Send + Sync>;

pub type RefreshFn = Arc<
    dyn Fn(LicenseSyncUsageResponse) -> BoxFuture<'static, Result<(), Error>>
        + Send + Sync,
>;
```

| Field | Purpose |
|-------|---------|
| `buffer` | Required. Persists pending deltas. |
| `sync` | Required. Calls `Client::license_sync_usage`. |
| `serial` | Reads the local snapshot's `serial`. Omit to send `0`. |
| `on_refresh` | Persist the refreshed snapshot returned by the server. |
| `flush_interval` | Auto-flush cadence; `None` / zero collapses to 300s. |

## UsageTracker

```rust
pub struct UsageTracker { /* … */ }

impl UsageTracker {
    pub fn new(opts: TrackerOptions) -> Self;

    pub async fn track(&self, feature: &str, delta: u64) -> Result<(), Error>;
    pub async fn flush(&self) -> Result<(), Error>;

    pub async fn start(self: &Arc<Self>);
    pub async fn stop(&self) -> Result<(), Error>;
}
```

- `track` — append a delta to the buffer. No-op when `delta == 0`.
- `flush` — drain the buffer, fetch the current serial, call `sync`, fire `on_refresh`. Empty buffer is a no-op. On `serial`/`sync` failure, the buffer is restored (`restore`); the error is propagated.
- `start` — spawn a Tokio task running `tokio::time::interval(flush_interval)`. Idempotent — calling twice is a no-op.
- `stop` — abort the spawned task and perform one final flush.

The auto-flush task swallows errors (`let _ = me.flush().await`) so the loop keeps running. Surface failures via your own observability hook inside `sync`.

`start` takes `self: &Arc<Self>` so the spawned task can hold an `Arc` clone of the tracker without leaking the lifetime.

## Rollback semantics

```
flush()
  ├── buffer.drain()         ── empty? return Ok(())
  ├── serial_fn?().await     ── err? buffer.restore(deltas); return Err
  ├── sync(deltas, serial)   ── err? buffer.restore(deltas); return Err
  └── on_refresh?(resp).await
```

Restore merges deltas back; any `track` calls that arrived during the in-flight `sync` are preserved (they were added to the buffer, not the drained map).

## Worked example — agent runtime

```rust
use std::sync::Arc;
use std::time::Duration;
use akira_billing::{Client, MemoryBuffer, UsageTracker, TrackerOptions};

let client = Arc::new(Client::new(base_url, "unified-dev", secret));

let sync_client = Arc::clone(&client);
let tracker = Arc::new(UsageTracker::new(TrackerOptions {
    buffer: Arc::new(MemoryBuffer::new()),
    sync: Arc::new(move |deltas, serial| {
        let client = Arc::clone(&sync_client);
        Box::pin(async move {
            client
                .license_sync_usage(LicenseSyncUsagePayload {
                    product: "unified-dev",
                    fingerprint: "<fp>",
                    serial,
                    deltas,
                })
                .await
        })
    }),
    serial: Some(Arc::new(move || {
        let store = Arc::clone(&store);
        Box::pin(async move { store.read_serial().await })
    })),
    on_refresh: Some(Arc::new(move |resp| {
        let store = Arc::clone(&store);
        Box::pin(async move {
            store.write_license(resp.license).await?;
            store.write_serial(resp.serial).await
        })
    })),
    flush_interval: Some(Duration::from_secs(300)),
}));

tracker.start().await;

// Application code
tracker.track("agent_run", 1).await?;

// On shutdown
tracker.stop().await?;
```

## Concurrency

`UsageTracker` is `Send + Sync` (every closure is `Send + Sync`). The buffer is the synchronization primitive — wrap your custom store with a `Mutex` or use a lock-free structure. Concurrent `track` calls are safe.

`flush` is **not** re-entrant in the strict sense (two concurrent `flush()` calls could both drain a partial buffer). The auto-flush task is the only invoker in normal use; explicit calls during the auto-flush window are rare but tolerated — the second `drain` returns `{}` and the second `flush` is a no-op.

---

Navigation: [← Gate](31-gate.md) · **Usage** · [Lifecycle →](33-lifecycle.md)
