100 lines
2.7 KiB
TypeScript
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();
|