Files
byre/web/src/api.ts
T
2026-10-14 21:06:30 -07:00

99 lines
3.7 KiB
TypeScript

import type { components, paths } from "./api.gen.js";
export type Schemas = components["schemas"];
export type User = Schemas["User"];
export type Project = Schemas["Project"];
export type Task = Schemas["Task"];
export type Unmerged = Schemas["Unmerged"];
export type AgentSession = Schemas["AgentSession"];
export type InboxItem = Schemas["InboxItem"];
export type InboxResponse = Schemas["InboxResponse"];
export type InboxResponseCreate = Schemas["InboxResponseCreate"];
export type Attachment = Schemas["Attachment"];
export type ByreEvent = Schemas["Event"];
export type Status = Schemas["Status"];
export type TurnRef = Schemas["TurnRef"];
type Method = "get" | "post" | "put";
type PathsWith<M extends Method> = {
[P in keyof paths]: paths[P][M] extends { responses: unknown } ? P : never;
}[keyof paths];
type Op<P extends keyof paths, M extends Method> = NonNullable<paths[P][M]>;
type Params<O> = O extends { parameters: infer X } ? X : never;
type PathParams<O> = Params<O> extends { path: infer X } ? X : never;
type QueryParams<O> = Params<O> extends { query?: infer X } ? X : never;
type Body<O> = O extends { requestBody: { content: { "application/json": infer B } } }
? B
: O extends { requestBody: { content: Record<`image/${string}`, unknown> } }
? Blob
: never;
type Success<O> = O extends { responses: infer R } ? R[Extract<keyof R, 200 | 201 | 202>] : never;
type Result<O> = Success<O> extends { content: { "application/json": infer J } } ? J : never;
type Options<O> = {
path?: PathParams<O>;
query?: QueryParams<O>;
body?: Body<O>;
};
export class ApiError extends Error {
constructor(readonly status: number, message: string) {
super(message);
}
}
export async function request<M extends Method, P extends PathsWith<M>>(
method: M,
path: P,
opts: Options<Op<P, M>> = {},
): Promise<Result<Op<P, M>>> {
const pathParams = (opts.path ?? {}) as Record<string, unknown>;
let url: string = path.replace(/\{(\w+)\}/g, (_, key: string) => {
const value = pathParams[key];
if (value === undefined) {
throw new Error(`missing path parameter ${key} for ${path}`);
}
return encodeURIComponent(String(value));
});
const query = new URLSearchParams();
for (const [key, value] of Object.entries((opts.query ?? {}) as Record<string, unknown>)) {
if (value !== undefined) {
query.set(key, String(value));
}
}
if (query.size > 0) {
url += `?${query}`;
}
const init: RequestInit = { method: method.toUpperCase() };
if (opts.body instanceof Blob) {
init.headers = { "Content-Type": opts.body.type || "application/octet-stream" };
init.body = opts.body;
} else if (opts.body !== undefined) {
init.headers = { "Content-Type": "application/json" };
init.body = JSON.stringify(opts.body);
}
const res = await fetch(url, init);
const data = await res.json();
if (!res.ok) {
throw new ApiError(res.status, (data as Schemas["Error"]).message);
}
return data as Result<Op<P, M>>;
}
export const get = <P extends PathsWith<"get">>(path: P, opts?: Options<Op<P, "get">>) => request("get", path, opts);
export const post = <P extends PathsWith<"post">>(path: P, opts?: Options<Op<P, "post">>) => request("post", path, opts);
export const put = <P extends PathsWith<"put">>(path: P, opts?: Options<Op<P, "put">>) => request("put", path, opts);
export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onState: (connected: boolean) => void): EventSource {
const source = new EventSource(`/api/events?after=${after}`);
source.onopen = () => onState(true);
source.onerror = () => onState(false);
source.onmessage = (msg: MessageEvent<string>) => onEvent(JSON.parse(msg.data) as ByreEvent);
return source;
}