# ClientManager

## Overview

`ClientManager` is the **central orchestrator** that unifies three essential concerns of IC development into a single, coherent system:

**HttpAgent**
Manages the IC HTTP agent for all canister communication, handling network
    detection and initialization

**Identity**
Holds the active identity and swaps it into the agent when it changes.
    Internet Identity sign-in itself lives in `AuthenticationManager`
    (`@ic-reactor/react`)

**QueryClient**
Integrates TanStack Query for automatic caching, deduplication, and
    background data updates

### The Problem It Solves

Without ClientManager, connecting to the IC typically involves:

- Creating and configuring an `HttpAgent` manually
- Passing agents to actors and keeping them in sync after login/logout
- Manually invalidating cached data when identity changes

**ClientManager solves this by providing a unified interface** where all these concerns are managed together. When the identity changes, the agent is updated, the affected queries invalidate, and your reactors seamlessly use the new identity.

```typescript
// One ClientManager, shared by all your reactors
const clientManager = new ClientManager({ queryClient })

// Create multiple reactors - they all share the same agent and identity
const backend = new Reactor<BackendService>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId,
})
const ledger = new Reactor<LedgerService>({
  clientManager,
  idlFactory: ledgerIdl,
  name: "ledger",
  canisterId: ledgerId,
})

// Sign in once - all reactors automatically use the new identity
const authentication = new AuthenticationManager({ clientManager })
await authentication.login()
```

## Import

```typescript
import { ClientManager } from "@ic-reactor/core"
```

## Usage

```typescript
import { ClientManager } from "@ic-reactor/core"
import { QueryClient } from "@tanstack/query-core"

const queryClient = new QueryClient()

const clientManager = new ClientManager({
  queryClient,
})
```

## Constructor Options

| Option            | Type               | Default        | Description                                                                                                                                                                                                                                                                                                           |
| ----------------- | ------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queryClient`     | `QueryClient`      | Required       | TanStack Query client instance                                                                                                                                                                                                                                                                                        |
| `agentOptions`    | `HttpAgentOptions` | `{}`           | Options forwarded to the `HttpAgent` (`host`, `identity`, `rootKey`, `retryTimes`, `verifyQuerySignatures`, …)                                                                                                                                                                                                        |
| `allowEnvConfig`  | `boolean`          | host-dependent | Whether to trust the `ic_env` cookie: its root key, its Internet Identity provider, and the canister IDs a reactor resolves by name. Defaults to `true` only for hosts that are unambiguously a local replica (loopback, `localhost` and its subdomains, dev-container tunnel domains); every other host must opt in. |
| `allowEnvRootKey` | `boolean`          | —              | **Deprecated.** The former name of `allowEnvConfig`. Still honoured, and still scoped to what it always granted: the root key alone, not the identity provider or the canister ID.                                                                                                                                    |

Network detection needs no flag — see [Network Configuration](#network-configuration).

## Properties

| Property          | Type                          | Description                                                   |
| ----------------- | ----------------------------- | ------------------------------------------------------------- |
| `agent`           | `HttpAgent`                   | The IC HTTP agent (getter)                                    |
| `queryClient`     | `QueryClient`                 | TanStack Query client                                         |
| `agentState`      | `AgentState`                  | Current agent initialization state                            |
| `agentHost`       | `URL \| undefined`            | The host URL of the agent                                     |
| `network`         | `"ic" \| "local" \| "remote"` | Current network type                                          |
| `isLocal`         | `boolean`                     | Whether connected to local replica                            |
| `trustsEnvConfig` | `boolean`                     | Whether the `ic_env` cookie is trusted for this host (getter) |

### AgentState Type

```typescript
interface AgentState {
  isInitialized: boolean
  isInitializing: boolean
  isLocalhost: boolean
  network: string | undefined // "ic" | "local" | "remote"
  error: Error | undefined
}
```

## Methods

### initialize

Initialize the agent:

```typescript
await clientManager.initialize()
```

This method:

1. Initializes the HttpAgent (fetches root key for local networks)
2. Returns the ClientManager instance

Session restoration is **not** part of this call — it belongs to `AuthenticationManager.authenticate()` (or the `useAuth` hook, which calls it for you).

---

### initializeAgent

Initialize the HttpAgent. `initialize()` delegates to this method, so the two are interchangeable:

```typescript
await clientManager.initializeAgent()
```

---

### updateAgent

Swap the agent's identity and invalidate the queries of every registered canister:

```typescript
clientManager.updateAgent(newIdentity)
```

In-flight queries for those canisters are cancelled first, so results signed by the previous identity cannot land in the cache. Queries that do not belong to a registered canister — the rest of your app's REST/GraphQL data — are left untouched.

---

### getUserPrincipal

Get the current user's Principal. This forwards `agent.getPrincipal()`, so it returns a **promise**:

```typescript
const principal = await clientManager.getUserPrincipal()
```

---

### subscribeAgentState

Subscribe to agent state changes:

```typescript
const unsubscribe = clientManager.subscribeAgentState((state) => {
  console.log("Agent state:", state)
  // { isInitialized, isInitializing, isLocalhost, network, error }
})

// Later
unsubscribe()
```

---

### subscribe

Subscribe to identity changes:

```typescript
const unsubscribe = clientManager.subscribe((identity) => {
  console.log("New identity:", identity.getPrincipal().toText())
})
```

---

### registerCanisterId

Register a canister ID for tracking:

```typescript
clientManager.registerCanisterId(canisterId, "backend")
```

This is called automatically when creating a Reactor.

---

### connectedCanisterIds

Get all registered canister IDs:

```typescript
const canisterIds = clientManager.connectedCanisterIds()
```

## Examples

### Complete Setup

```typescript
// src/reactor/index.ts
import { ClientManager, Reactor } from "@ic-reactor/core"
import { QueryClient } from "@tanstack/query-core"
import { idlFactory, type _SERVICE } from "../declarations/backend"

// Create QueryClient with defaults
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000, // 1 minute
      gcTime: 5 * 60_000, // 5 minutes
      retry: 3,
    },
  },
})

// Create ClientManager
export const clientManager = new ClientManager({
  queryClient,
})

// Create Reactor
export const backend = new Reactor<_SERVICE>({
  clientManager,
  idlFactory,
  name: "backend",
  canisterId: import.meta.env.VITE_BACKEND_CANISTER_ID,
})
```

### Network Configuration

```typescript
// Browser: the host is inferred from window.location.origin when the page is
// served from localhost/127.0.0.1 or from an IC boundary domain
const clientManager = new ClientManager({ queryClient })

// Explicit local host — non-browser environments, or a replica on a custom port
const clientManager = new ClientManager({
  queryClient,
  agentOptions: { host: "http://127.0.0.1:8080" },
})

// Custom agent options
const clientManager = new ClientManager({
  queryClient,
  agentOptions: {
    host: "https://icp-api.io",
    verifyQuerySignatures: true,
  },
})
```

### Canister Environment

When your app is served by the `@icp-sdk` toolchain (e.g., `icp-cli`) or by [`@ic-reactor/vite-plugin`](https://ic-reactor.b3pay.net/v3/packages/vite-plugin), the dev server injects an `ic_env` cookie carrying the local canister IDs and root key. `ClientManager` reads that cookie **automatically** in the browser. Host detection needs no option to enable; everything the cookie _carries_ is gated on one decision, exposed as the `trustsEnvConfig` getter and controlled by `allowEnvConfig`:

- Agent host taken from `window.location.origin` when the page is served from a local or IC boundary host
- Query signature verification disabled in development for performance
- Root key from the cookie, accepted only on hosts that are unambiguously a local replica — loopback (the whole of 127.0.0.0/8, plus `::1`), `localhost` and its subdomains, and the dev-container domains that tunnel a local replica. Cookies are not origin-isolated and the root key is what certificate verification is checked against, so any other host — including a custom domain — must opt in with `allowEnvConfig: true`.
- Canister IDs from the cookie (`PUBLIC_CANISTER_ID:<name>`), used when a reactor is constructed without an explicit `canisterId`, and accepted on exactly the same hosts. On any other host the reactor throws instead, naming the option to set. A substituted canister ID is not something certificate verification can catch — the attacker names a real canister, and its responses verify against the real mainnet root key — so it fails closed rather than silently talking to someone else's canister.

### Subscribing to State Changes

```typescript
// In React with useEffect
useEffect(() => {
  const unsubIdentity = clientManager.subscribe((identity) => {
    console.log("New identity:", identity.getPrincipal().toText())
  })

  const unsubAgent = clientManager.subscribeAgentState((state) => {
    if (state.isInitialized) {
      console.log("Agent ready, network:", state.network)
    }
  })

  return () => {
    unsubIdentity()
    unsubAgent()
  }
}, [])
```

### Authentication Flow

Sign-in lives on `AuthenticationManager` from `@ic-reactor/react`; `ClientManager` only receives the resulting identity.

```typescript
import { AuthenticationManager } from "@ic-reactor/react"

const authentication = new AuthenticationManager({ clientManager })

// Initialize the agent on app start
await clientManager.initialize()

// Restore a previous session, if any
await authentication.authenticate()

if (authentication.authState.isAuthenticated) {
  console.log(
    "Welcome back!",
    (await clientManager.getUserPrincipal()).toText()
  )
}

// Login handler
async function handleLogin() {
  await authentication.login({
    onSuccess: () => {
      console.log("Logged in!")
      // Queries for connected canisters are automatically invalidated
    },
    onError: (error) => {
      console.error("Login failed:", error)
    },
  })
}

// Logout handler
async function handleLogout() {
  await authentication.logout()
  // Queries for connected canisters are automatically invalidated
}
```

### Network Detection

```typescript
const clientManager = new ClientManager({ queryClient })

// Check current network
console.log(clientManager.isLocal) // true if local
console.log(clientManager.network) // "local", "remote", or "ic"
console.log(clientManager.agentHost?.toString()) // The host URL
```

## Automatic Query Invalidation

When the identity changes, `updateAgent()` sweeps the cached queries of every canister registered with this ClientManager:

- **In-flight queries are cancelled**, so a response signed by the previous identity cannot land after the switch
- **Inactive entries are removed** — not just invalidated. Query keys carry no principal, so a merely-stale entry (a balance whose component has unmounted, a cached profile) would stay readable through `getQueryData` or `fetchQuery` under the new identity
- **Active entries are invalidated**, so their mounted observers refetch as the new identity. They keep showing the previous identity's data only for the length of that refetch

```typescript
// This happens automatically on identity change, for each connected canister:
queryClient.cancelQueries({ queryKey: [canisterId] })
queryClient.removeQueries({ queryKey: [canisterId], type: "inactive" })
queryClient.invalidateQueries({ queryKey: [canisterId] })
```

Because the scope is the canister ID — the first segment of every reactor query key — non-reactor queries in a shared `QueryClient` are not refetched or removed on sign-in or sign-out.

## Pay-as-you-go Authentication

The `@icp-sdk/auth` package is an **optional peer dependency**. `ClientManager` never imports it. `AuthenticationManager` loads it through a dynamic `import("@icp-sdk/auth/client")`, so bundlers put it in a separate chunk and drop it entirely from apps that never construct one.

```bash
# Only install if you need authentication
npm install @icp-sdk/auth
```

## Notes

**Note:** All Reactors sharing the same ClientManager will share the same agent and
  identity. This is the recommended pattern.

**Tip:** Network detection needs no flag. In the browser the host is inferred from
  `window.location.origin` when the page is served from `localhost`/`127.0.0.1`
  (or an IC boundary domain). In Node/SSR, `ICP_NETWORK`/`DFX_NETWORK` is read
  from `process.env`. Pass `agentOptions.host` to override either.

**Caution:** Always call `initialize()` before making canister calls while using the local
  development setup. The React auth hooks (`useAuth`) do this automatically.

## External Authentication

While `AuthenticationManager` covers Internet Identity, ClientManager itself is provider-agnostic and works with **any identity provider**. You can integrate alternative authentication methods by updating the agent's identity.

### Using External Auth Providers

When using external authentication (wallets, SIWE, NFID, etc.), update the agent after authenticating:

```typescript
// Authenticate with your external provider
const identity = await externalAuthProvider.getIdentity()

// Update ClientManager - all reactors automatically use the new identity
clientManager.updateAgent(identity)
```

This pattern works with any provider that produces a valid `Identity` object.

### Planned Integrations (Roadmap)

We're exploring first-class support for popular authentication standards:

| Provider | Description                                               | Status     |
| -------- | --------------------------------------------------------- | ---------- |
| **SIWE** | Sign-In With Ethereum — use MetaMask, WalletConnect, etc. | 🔮 Planned |
| **SIWS** | Sign-In With Solana — use Phantom, Solflare, etc.         | 🔮 Planned |

**Tip:** Want to contribute an auth adapter? Check out the [GitHub
  repository](https://github.com/b3pay/ic-reactor) and open an issue to discuss
  your integration idea!

### Example: SIWE Integration

Here's how SIWE integration works with [ic-siwe-js](https://github.com/kristoferlund/ic-siwe):

```tsx
// 1. Wrap your app with SiweIdentityProvider
import { SiweIdentityProvider } from "ic-siwe-js/react"

function App() {
  return (
    <SiweIdentityProvider canisterId={siweProviderCanisterId}>
      <YourApp />
    </SiweIdentityProvider>
  )
}

// 2. Use the useSiwe hook to login and get identity
import { useSiwe } from "ic-siwe-js/react"

function LoginButton() {
  const { login, identity, isLoggingIn } = useSiwe()

  // After login, update ClientManager with the SIWE identity
  useEffect(() => {
    if (identity) {
      clientManager.updateAgent(identity)
    }
  }, [identity])

  return (
    <button onClick={login} disabled={isLoggingIn}>
      {isLoggingIn ? "Signing in..." : "Sign in with Ethereum"}
    </button>
  )
}
```

## See Also

- [Reactor](https://ic-reactor.b3pay.net/v3/reference/reactor) — Canister reactor class
- [createAuthHooks](https://ic-reactor.b3pay.net/v3/reference/createauthhooks/overview) — Authentication hooks
- [Authentication](https://ic-reactor.b3pay.net/v3/guides/authentication) — Auth patterns guide