Files
beaver-gateway/ui/src/lib/gateway.svelte.ts
T

100 lines
2.7 KiB
TypeScript

import type {
BusEvent,
JobsResponse,
LimitsResponse,
LimitWindow,
} from "./api/types";
import { panelCache } from "./panel/cache";
import { browserHost } from "./panel/host";
import { ConversationIndex } from "./panel/index.svelte";
import { session } from "./session.svelte";
const TAPE_SIZE = 120;
const QUIET = new Set(["stream", "hello"]);
const JOBS_KEY = "jobs";
const cache = panelCache("beaver.admin");
// Gateway-wide live state for the admin: the conversation index, the last
// non-chatty events as a tape, and the quota windows updated the moment
// the SDK reports them.
class Gateway extends ConversationIndex {
tape = $state<BusEvent[]>([]);
limits = $state<LimitsResponse | null>(null);
// The scheduler snapshot: what was last seen, shown at once, then refreshed.
jobs = $state<JobsResponse | null>(cache.get<JobsResponse>(JOBS_KEY) ?? null);
constructor() {
super(() => session.client, browserHost().cache ?? null);
}
async refreshJobs(): Promise<void> {
const client = this.client();
if (!client) {
return;
}
this.jobs = await client.jobs();
cache.set(JOBS_KEY, this.jobs);
}
override async load(): Promise<void> {
const client = this.client();
if (!client) {
return;
}
const [, limits] = await Promise.all([
super.load(),
client.limits().catch(() => null),
]);
if (limits) {
this.limits = limits;
}
this.refreshJobs().catch(() => undefined);
}
async refreshLimits(): Promise<void> {
const client = this.client();
if (client) {
this.limits = await client.limits();
}
}
override apply(event: BusEvent): void {
if (!QUIET.has(event.type)) {
this.tape.unshift(event);
if (this.tape.length > TAPE_SIZE) {
this.tape.length = TAPE_SIZE;
}
}
if (event.type === "rate_limit") {
this.applyLimit(event);
return;
}
if (event.type.startsWith("job.") || event.type.startsWith("schedule.")) {
this.refreshJobs().catch(() => undefined);
}
super.apply(event);
}
private applyLimit(event: BusEvent): void {
if (!this.limits || typeof event.window !== "string") {
this.refreshLimits().catch(() => undefined);
return;
}
const current = this.limits.windows.find((w) => w.window === event.window);
const patch = {
overage_status: event.overage_status ?? null,
resets_at: event.resets_at,
status: event.status,
ts: event.ts,
utilization: event.utilization,
} as Partial<LimitWindow>;
if (current) {
Object.assign(current, patch);
} else {
this.refreshLimits().catch(() => undefined);
}
}
}
export const gateway = new Gateway();