# @mounaji_npm/api-client

HTTP client, service base class, and WebSocket connector for Mounaji-powered applications. Designed to pair with `@mounaji_npm/auth` for automatic token injection and 401 retry.

---

## Install

```bash
npm install @mounaji_npm/api-client
```

No peer dependencies. Works in any browser or Node.js environment with `fetch` and `WebSocket`.

---

## Quick Start

```js
// src/api/client.js
import { ApiClient } from '@mounaji_npm/api-client';

export const apiClient = new ApiClient({
  baseUrl: import.meta.env.VITE_API_URL || process.env.NEXT_PUBLIC_API_URL,
});
```

```js
// src/api/services/AssistantService.js
import { ServiceBase } from '@mounaji_npm/api-client';
import { apiClient } from '../client.js';

class AssistantService extends ServiceBase {
  constructor() { super(apiClient, '/assistants'); }
  list()          { return this.get('/'); }
  getById(id)     { return this.get(`/${id}`); }
  create(data)    { return this.post('/', data); }
  update(id, d)   { return this.put(`/${id}`, d); }
  remove(id)      { return this.delete(`/${id}`); }
}

export const assistantService = new AssistantService();
```

```jsx
// In a component
import { assistantService } from './api/services/AssistantService.js';

useEffect(() => {
  assistantService.list().then(setAssistants);
}, []);
```

---

## ApiClient

Core HTTP client with token injection, 401 retry, and interceptors.

### Setup

```js
import { ApiClient } from '@mounaji_npm/api-client';

const apiClient = new ApiClient({
  baseUrl:        'https://api.example.com',
  timeout:        30_000,     // ms (default: 30s)
  defaultHeaders: {},         // added to every request
});
```

### Auth wiring

```js
// Call after login / on token change
apiClient.setAuthToken(token);

// Provide a refresh function — called automatically on 401 TOKEN_EXPIRED
apiClient.setTokenRefresher(() => authAdapter.getToken(true));

// Listen for session expiry (when refresh fails)
window.addEventListener('auth:session-expired', () => router.push('/login'));
```

### HTTP methods

```js
// GET
const list = await apiClient.get('/assistants');
const one  = await apiClient.get('/assistants/123');
const page = await apiClient.get('/assistants', { params: { page: 1, limit: 20 } });

// POST / PUT / PATCH
const created = await apiClient.post('/assistants', { name: 'My Bot' });
await apiClient.put('/assistants/123', { name: 'Updated' });
await apiClient.patch('/assistants/123', { active: false });

// DELETE
await apiClient.delete('/assistants/123');

// Raw Response (bypass JSON parsing)
const res  = await apiClient.get('/export', { raw: true });
const blob = await res.blob();
```

### Error handling

On non-2xx responses, `ApiClient` throws an enriched `Error`:

```js
try {
  await apiClient.get('/protected');
} catch (err) {
  err.message  // human-readable message
  err.status   // HTTP status code (e.g. 403)
  err.code     // server error code string (e.g. 'FORBIDDEN')
  err.details  // full parsed error body
}
```

### Interceptors

```js
// Log every request
apiClient.addRequestInterceptor((url, config) => {
  console.log('→', config.method, url);
  return config;
});

// Log every response
apiClient.addResponseInterceptor(async (res, url) => {
  console.log('←', res.status, url);
  return res;
});
```

---

## ServiceBase

Base class for typed domain service objects.

```js
import { ServiceBase } from '@mounaji_npm/api-client';

class DocumentService extends ServiceBase {
  constructor() { super(apiClient, '/documents'); }

  listByKb(kbId)       { return this.get('/', { params: { knowledgeBaseId: kbId } }); }
  upload(formData)     { return this.post('/', formData); }
  getById(id)          { return this.get(`/${id}`); }
  delete(id)           { return this.delete(`/${id}`); }
  getStatus(id)        { return this.get(`/${id}/status`); }
}

export const documentService = new DocumentService();
```

**Available methods:** `this.get` `this.post` `this.put` `this.patch` `this.delete` — all prefixed with the `basePath` passed to `super()`.

---

## WsConnector

WebSocket client with typed message routing, exponential backoff reconnect, and ping/pong keepalive.

```js
import { WsConnector } from '@mounaji_npm/api-client';

const ws = new WsConnector({
  url:               'wss://api.example.com/ws',
  getToken:          () => apiClient._token,
  tokenMode:         'query',     // 'query' (URL param) | 'message' (first message)
  maxReconnects:     10,
  reconnectBaseDelay:1000,        // ms — doubles each attempt, caps at 30s
  pingInterval:      30_000,      // ms — 0 = disable keepalive
});

// Subscribe to typed messages
ws.on('chat:message',   (data) => addMessage(data));
ws.on('assistant:done', (data) => setLoading(false));
ws.on('error',          (err)  => showToast(err.message));
ws.on('open',           ()     => setConnected(true));
ws.on('close',          ({ code, reason }) => setConnected(false));
ws.on('*',              (data) => console.log('raw', data));  // wildcard

// Send
ws.connect();
ws.send('chat:message', { content: 'Hello', sessionId: '...' });

// Clean shutdown
ws.disconnect();

// Unsubscribe
ws.off('chat:message');
```

**Expected incoming message shape:**

```json
{ "type": "chat:message", "content": "Hello", "timestamp": 1234567890 }
```

**Reconnect delays (baseDelay=1000ms):**
1s → 2s → 4s → 8s → 16s → 30s (capped)

---

## Wiring with @mounaji_npm/auth

```jsx
import { useEffect } from 'react';
import { AuthProvider } from '@mounaji_npm/auth';
import { createFirebaseAdapter } from '@mounaji_npm/auth/firebase';
import { apiClient } from './api/client.js';

const adapter = createFirebaseAdapter({ auth, googleProvider });

function ApiAuthBridge() {
  useEffect(() => {
    const unsub = adapter.onAuthChange((user, token) => {
      apiClient.setAuthToken(token);
    });
    apiClient.setTokenRefresher(() => adapter.getToken(true));
    return () => unsub?.();
  }, []);
  return null;
}

export default function Root() {
  return (
    <AuthProvider adapter={adapter}>
      <ApiAuthBridge />
      <App />
    </AuthProvider>
  );
}
```

---

## Recommended Project Layout

```
src/
└── api/
    ├── client.js                  ← shared ApiClient instance
    ├── services/
    │   ├── AssistantService.js
    │   ├── DocumentService.js
    │   └── WorkflowService.js
    └── connectors/
        └── chatSocket.js          ← WsConnector instance
```

---

> Full API reference with all options: [DOCS.md](./DOCS.md)
