v1.3 - added an sqlite db and built data structure
This commit is contained in:
parent
b0fba52c0e
commit
5efdafbb97
37 changed files with 6414 additions and 1988 deletions
102
src/lib/api.js
Normal file
102
src/lib/api.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
API CLIENT
|
||||
|
||||
One place that knows how to talk to the server, so components
|
||||
never call fetch directly and swapping the transport later is a
|
||||
single-file change.
|
||||
|
||||
Two things worth knowing about the design:
|
||||
|
||||
The cache is a module-level Map of in-flight and settled
|
||||
promises. Two components asking for /events during the same
|
||||
render pass share one request, and a remount inside the TTL
|
||||
costs nothing. It resets on page load, which is the right
|
||||
lifetime for content that changes weekly.
|
||||
|
||||
Every reader can pass a `fallback`. If the request fails, that
|
||||
value is used instead. This is what keeps the Figma preview
|
||||
working: pass the old static module as the fallback and the
|
||||
preview renders real content with no server in sight.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
const BASE = import.meta.env?.VITE_API_BASE ?? "/api";
|
||||
const DEFAULT_TTL = 60_000;
|
||||
|
||||
const cache = new Map(); // path → { at, promise }
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, { status, fields } = {}) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.fields = fields;
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${BASE}${path}`, {
|
||||
headers: { Accept: "application/json", ...options.headers },
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.status === 204) return null;
|
||||
|
||||
const type = response.headers.get("content-type") ?? "";
|
||||
if (!type.includes("application/json")) {
|
||||
// Usually the SPA fallback returning index.html for a URL the
|
||||
// API doesn't serve. Parsing it would throw something useless.
|
||||
throw new ApiError("Server did not return JSON.", {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(body.error ?? "Request failed.", {
|
||||
status: response.status,
|
||||
fields: body.fields,
|
||||
});
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/* ── Reads ─────────────────────────────────────────────────────
|
||||
get("/events") → cached for 60s
|
||||
get("/events", { ttl: 0 }) → always fresh
|
||||
get("/events", { fallback }) → fallback on any failure
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function get(path, { ttl = DEFAULT_TTL, fallback } = {}) {
|
||||
const hit = cache.get(path);
|
||||
|
||||
if (hit && Date.now() - hit.at < ttl) return hit.promise;
|
||||
|
||||
const promise = request(path).catch((err) => {
|
||||
cache.delete(path); // a failure shouldn't be cached
|
||||
if (fallback !== undefined) {
|
||||
console.warn(`api: ${path} failed, using fallback`, err);
|
||||
return fallback;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
cache.set(path, { at: Date.now(), promise });
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function invalidate(path) {
|
||||
if (path) cache.delete(path);
|
||||
else cache.clear();
|
||||
}
|
||||
|
||||
/* ── Writes ──────────────────────────────────────────────────── */
|
||||
|
||||
export function post(path, data) {
|
||||
return request(path, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue