134 lines
3.3 KiB
JavaScript
134 lines
3.3 KiB
JavaScript
/**
|
|
* API Client — all fetch calls + EventSource wrapper
|
|
*/
|
|
|
|
const BASE = '/api/v1';
|
|
|
|
class ApiClient {
|
|
constructor() {
|
|
this._eventSources = new Map();
|
|
this._bus = new EventTarget();
|
|
}
|
|
|
|
// --- Event bus ---
|
|
on(event, fn) { this._bus.addEventListener(event, fn); }
|
|
off(event, fn) { this._bus.removeEventListener(event, fn); }
|
|
_emit(event, detail) {
|
|
this._bus.dispatchEvent(new CustomEvent(event, { detail }));
|
|
}
|
|
|
|
// --- HTTP helpers ---
|
|
async _fetch(path, opts = {}) {
|
|
try {
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
headers: { 'Content-Type': 'application/json', ...opts.headers },
|
|
...opts,
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
throw new Error(err.detail || `HTTP ${res.status}`);
|
|
}
|
|
return res.json();
|
|
} catch (e) {
|
|
this._emit('error', { message: e.message, path });
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// --- Health ---
|
|
async checkHealth() {
|
|
try {
|
|
const data = await fetch('/health').then(r => r.json());
|
|
this._emit('health', data);
|
|
return data;
|
|
} catch {
|
|
this._emit('health', { status: 'disconnected' });
|
|
return { status: 'disconnected' };
|
|
}
|
|
}
|
|
|
|
// --- Sessions ---
|
|
async createSession(body) {
|
|
const data = await this._fetch('/sessions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
this._emit('session:created', data);
|
|
return data;
|
|
}
|
|
|
|
async getSession(sessionId) {
|
|
const data = await this._fetch(`/sessions/${sessionId}`);
|
|
this._emit('session:state', data);
|
|
return data;
|
|
}
|
|
|
|
async deleteSession(sessionId) {
|
|
const data = await this._fetch(`/sessions/${sessionId}`, { method: 'DELETE' });
|
|
this._emit('session:deleted', { session_id: sessionId });
|
|
return data;
|
|
}
|
|
|
|
async getEvents(sessionId) {
|
|
return this._fetch(`/sessions/${sessionId}/events`);
|
|
}
|
|
|
|
// --- Messages ---
|
|
async sendMessage(sessionId, message, stream = false) {
|
|
return this._fetch(`/sessions/${sessionId}/messages`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ message, stream }),
|
|
});
|
|
}
|
|
|
|
// --- SSE ---
|
|
subscribeSSE(sessionId) {
|
|
this.unsubscribeSSE(sessionId);
|
|
|
|
const url = `${BASE}/sessions/${sessionId}/stream`;
|
|
const es = new EventSource(url);
|
|
|
|
const eventTypes = [
|
|
'session.created', 'execution.started', 'agent.delta',
|
|
'tool.started', 'tool.completed', 'subagent.assigned',
|
|
'execution.completed', 'error', 'keepalive',
|
|
];
|
|
|
|
for (const type of eventTypes) {
|
|
es.addEventListener(type, (e) => {
|
|
try {
|
|
const payload = JSON.parse(e.data);
|
|
this._emit('sse', { type, ...payload });
|
|
this._emit(`sse:${type}`, payload);
|
|
} catch {
|
|
this._emit('sse', { type, raw: e.data });
|
|
}
|
|
});
|
|
}
|
|
|
|
es.onerror = () => {
|
|
this._emit('sse:error', { sessionId });
|
|
};
|
|
|
|
this._eventSources.set(sessionId, es);
|
|
return es;
|
|
}
|
|
|
|
unsubscribeSSE(sessionId) {
|
|
const es = this._eventSources.get(sessionId);
|
|
if (es) {
|
|
es.close();
|
|
this._eventSources.delete(sessionId);
|
|
}
|
|
}
|
|
|
|
unsubscribeAll() {
|
|
for (const [id, es] of this._eventSources) {
|
|
es.close();
|
|
}
|
|
this._eventSources.clear();
|
|
}
|
|
}
|
|
|
|
export const api = new ApiClient();
|