Frontend Labs
⋯

public/api.js

// A tiny API client: JSON in/out, the CSRF header on every state-changing
// request, and errors that carry the HTTP status (§67).

let csrfToken = "";
export const setCsrf = (t) => { csrfToken = t; };

export class ApiError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

export async function api(path, { method = "GET", body, signal } = {}) {
  const res = await fetch(path, {
    method,
    signal,
    credentials: "same-origin",
    headers: {
      ...(body !== undefined && { "Content-Type": "application/json" }),
      ...(method !== "GET" && { "X-CSRF-Token": csrfToken }),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) {
    const { error = res.statusText } = await res.json().catch(() => ({}));
    throw new ApiError(res.status, error);
  }
  return res;
}

// Server-Sent Events over fetch (EventSource can't POST or send headers).
// Yields one parsed event object per `data:` frame (§59).
export async function* readEvents(res) {
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buf = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) return;
    buf += value;
    let end;
    while ((end = buf.indexOf("\n\n")) !== -1) {
      const frame = buf.slice(0, end);
      buf = buf.slice(end + 2);
      const data = frame
        .split("\n")
        .filter((line) => line.startsWith("data:"))
        .map((line) => line.slice(5).trimStart())
        .join("\n");
      if (data) yield JSON.parse(data);
    }
  }
}

Generated from SOURCE.md · 8 Oct 2026 16:36