128 lines
4.9 KiB
JavaScript
128 lines
4.9 KiB
JavaScript
/**
|
|
* Typed fetch wrapper for Clare's JSON API.
|
|
*
|
|
* Every page calls these — never raw `fetch`. Centralizes:
|
|
* - URL construction (relative paths; Vite proxies in dev, same-origin in prod)
|
|
* - error translation: non-2xx → `ApiError(status, detail)`
|
|
* - JSON parsing with typed return
|
|
* - AbortSignal threading so callers can cancel
|
|
*/
|
|
export class ApiError extends Error {
|
|
status;
|
|
detail;
|
|
constructor(status, detail) {
|
|
super(`${status}: ${detail}`);
|
|
this.status = status;
|
|
this.detail = detail;
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
async function request(method, path, options = {}) {
|
|
const url = buildUrl(path, options.params);
|
|
const init = {
|
|
method,
|
|
signal: options.signal,
|
|
headers: options.body !== undefined ? { "content-type": "application/json" } : undefined,
|
|
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
};
|
|
const resp = await fetch(url, init);
|
|
if (!resp.ok) {
|
|
let detail = resp.statusText;
|
|
try {
|
|
const j = await resp.json();
|
|
if (j && typeof j === "object" && "detail" in j) {
|
|
detail = String(j.detail);
|
|
}
|
|
}
|
|
catch {
|
|
// body wasn't json — keep statusText
|
|
}
|
|
throw new ApiError(resp.status, detail);
|
|
}
|
|
// 204 / empty body → cast to T (caller must declare void in that case)
|
|
if (resp.status === 204)
|
|
return undefined;
|
|
return (await resp.json());
|
|
}
|
|
function buildUrl(path, params) {
|
|
if (!params)
|
|
return path;
|
|
const usp = new URLSearchParams();
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v !== undefined && v !== null)
|
|
usp.append(k, String(v));
|
|
}
|
|
const q = usp.toString();
|
|
return q ? `${path}?${q}` : path;
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Projects
|
|
// ---------------------------------------------------------------------------
|
|
export async function listProjects(signal) {
|
|
const r = await request("GET", "/api/v1/projects", { signal });
|
|
return r.projects;
|
|
}
|
|
export async function createProject(body, signal) {
|
|
return request("POST", "/api/v1/projects", { body, signal });
|
|
}
|
|
export async function getProject(nameOrId, signal) {
|
|
return request("GET", `/api/v1/projects/${encodeURIComponent(nameOrId)}`, { signal });
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Tasks
|
|
// ---------------------------------------------------------------------------
|
|
export async function listTasks(params = {}, signal) {
|
|
const r = await request("GET", "/api/v1/tasks", { params, signal });
|
|
return r.tasks;
|
|
}
|
|
export async function createTask(body, signal) {
|
|
return request("POST", "/api/v1/tasks", { body, signal });
|
|
}
|
|
export async function updateTask(taskId, body, signal) {
|
|
return request("POST", `/api/v1/tasks/${encodeURIComponent(taskId)}`, { body, signal });
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Assignments
|
|
// ---------------------------------------------------------------------------
|
|
export async function listAssignments(active = true, signal) {
|
|
const r = await request("GET", "/api/v1/assignments", {
|
|
params: { active: active ? 1 : 0 }, signal,
|
|
});
|
|
return r.assignments;
|
|
}
|
|
export async function createAssignment(body, signal) {
|
|
return request("POST", "/api/v1/assignments", { body, signal });
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Sessions + pull
|
|
// ---------------------------------------------------------------------------
|
|
export async function listSessions(signal) {
|
|
const r = await request("GET", "/api/v1/sessions", { signal });
|
|
return r.sessions;
|
|
}
|
|
export async function pull(signal) {
|
|
return request("POST", "/api/v1/pull", { signal });
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Broadcast
|
|
// ---------------------------------------------------------------------------
|
|
export async function broadcast(body, signal) {
|
|
return request("POST", "/api/v1/broadcast", { body, signal });
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Chat
|
|
// ---------------------------------------------------------------------------
|
|
export async function listChatMessages(params, signal) {
|
|
const r = await request("GET", "/api/v1/chat", { params, signal });
|
|
return r.messages;
|
|
}
|
|
export async function postChatMessage(body, signal) {
|
|
return request("POST", "/api/v1/chat", { body, signal });
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Autocomplete
|
|
// ---------------------------------------------------------------------------
|
|
export async function autocomplete(params, signal) {
|
|
const r = await request("GET", "/api/v1/autocomplete", { params, signal });
|
|
return r.hits;
|
|
}
|