synapse / frontend /src /lib /api.js
adwitiyashukla's picture
pushed Synapse, an agentic AI assistant with hybrid RAG
b2c1c67
Raw
History Blame Contribute Delete
3.35 kB
// API client: token handling, automatic refresh, JSON requests and SSE streaming.
const TOKEN_KEY = "synapse_tokens";
export function getTokens() {
try {
return JSON.parse(localStorage.getItem(TOKEN_KEY)) || null;
} catch {
return null;
}
}
export function setTokens(tokens) {
if (tokens) localStorage.setItem(TOKEN_KEY, JSON.stringify(tokens));
else localStorage.removeItem(TOKEN_KEY);
}
let onUnauthorized = () => {};
export function setUnauthorizedHandler(handler) {
onUnauthorized = handler;
}
async function tryRefresh() {
const tokens = getTokens();
if (!tokens?.refresh_token) return false;
const response = await fetch("/api/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: tokens.refresh_token }),
});
if (!response.ok) return false;
setTokens(await response.json());
return true;
}
async function rawRequest(path, options = {}) {
const tokens = getTokens();
const headers = { ...(options.headers || {}) };
if (tokens?.access_token) headers.Authorization = `Bearer ${tokens.access_token}`;
if (options.body && !(options.body instanceof FormData)) {
headers["Content-Type"] = "application/json";
options = { ...options, body: JSON.stringify(options.body) };
}
return fetch(path, { ...options, headers });
}
export async function api(path, options = {}) {
let response = await rawRequest(path, options);
if (response.status === 401 && (await tryRefresh())) {
response = await rawRequest(path, options);
}
if (response.status === 401) {
setTokens(null);
onUnauthorized();
throw new Error("Session expired. Please sign in again.");
}
if (!response.ok) {
let detail = `Request failed (${response.status})`;
try {
const body = await response.json();
if (body.detail) detail = typeof body.detail === "string" ? body.detail : detail;
} catch {
/* keep default detail */
}
throw new Error(detail);
}
if (response.status === 204) return null;
return response.json();
}
// Stream a chat reply. handlers: { onEvent(event), onError(err) }
export async function streamChat(sessionId, body, handlers, signal) {
let response = await rawRequest(`/api/chat/${sessionId}`, {
method: "POST",
body,
signal,
});
if (response.status === 401 && (await tryRefresh())) {
response = await rawRequest(`/api/chat/${sessionId}`, { method: "POST", body, signal });
}
if (!response.ok || !response.body) {
let detail = `Chat request failed (${response.status})`;
try {
const errBody = await response.json();
if (typeof errBody.detail === "string") detail = errBody.detail;
} catch {
/* ignore */
}
throw new Error(detail);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const blocks = buffer.split("\n\n");
buffer = blocks.pop() ?? "";
for (const block of blocks) {
const line = block.trim();
if (!line.startsWith("data: ")) continue;
try {
handlers.onEvent(JSON.parse(line.slice(6)));
} catch {
/* skip malformed frames */
}
}
}
}