fabro/apps/fabro-web/app/lib/run-events.ts
Fabro d2c6667c87 fabro(01KQT9MH7PZ2T0694NH0YFQ6Q9): implement (succeeded)
Fabro-Run: 01KQT9MH7PZ2T0694NH0YFQ6Q9
Fabro-Completed: 5
Fabro-Checkpoint: 68e64e4f5f

⚒️ Generated with [Fabro](https://fabro.sh)
2026-05-04 20:40:29 +00:00

187 lines
No EOL
4.7 KiB
TypeScript

import { useEffect } from "react";
import { useSWRConfig } from "swr";
import {
subscribeToCrossTabSse,
type CrossTabSseCoordinator,
} from "./cross-tab-sse";
import { queryKeys } from "./query-keys";
import {
createBrowserEventSource,
subscribeToSharedEventSource,
type EventPayload,
type EventSourceLike,
type MutateFn,
type SharedEventSubscription,
} from "./sse";
interface RunEventPayload extends EventPayload {
event?: string;
run_id?: string;
node_id?: string;
properties?: Record<string, unknown>;
}
interface RunEventOptions {
debounceMs?: number;
coordinator?: CrossTabSseCoordinator;
}
const subscriptions = new Map<string, SharedEventSubscription>();
const TERMINAL_EVENTS = new Set(["run.completed", "run.failed"]);
const RUN_SUMMARY_EVENTS = new Set([
"run.submitted",
"run.queued",
"run.starting",
"run.running",
"run.paused",
"run.unpaused",
"run.blocked",
"run.unblocked",
"run.archived",
"run.unarchived",
]);
const STAGE_EVENTS = new Set([
"stage.started",
"stage.completed",
"stage.failed",
"stage.retrying",
]);
const COMMAND_EVENTS = new Set(["command.started", "command.completed"]);
const INTERVIEW_EVENTS = new Set([
"interview.started",
"interview.completed",
"interview.timeout",
"interview.interrupted",
]);
export function queryKeysForRunEvent(
runId: string,
event: string,
stageId?: string,
): string[] {
if (event === "checkpoint.completed") {
return [queryKeys.runs.files(runId)];
}
if (TERMINAL_EVENTS.has(event)) {
return [
queryKeys.runs.detail(runId),
queryKeys.runs.files(runId),
queryKeys.runs.billing(runId),
queryKeys.runs.stages(runId),
queryKeys.runs.graph(runId, "LR"),
queryKeys.runs.graph(runId, "TB"),
];
}
if (RUN_SUMMARY_EVENTS.has(event)) {
return [queryKeys.runs.detail(runId)];
}
if (INTERVIEW_EVENTS.has(event)) {
return [
queryKeys.runs.questions(runId, 25, 0),
queryKeys.runs.detail(runId),
];
}
if (STAGE_EVENTS.has(event)) {
const keys = [
queryKeys.runs.stages(runId),
queryKeys.runs.billing(runId),
queryKeys.runs.events(runId, 1000),
queryKeys.runs.graph(runId, "LR"),
queryKeys.runs.graph(runId, "TB"),
queryKeys.runs.detail(runId),
];
if (stageId) {
keys.push(queryKeys.runs.stageTurns(runId, stageId));
}
return keys;
}
if (COMMAND_EVENTS.has(event)) {
const keys = [
queryKeys.runs.stages(runId),
queryKeys.runs.events(runId, 1000),
];
if (stageId) {
keys.push(queryKeys.runs.stageTurns(runId, stageId));
}
return keys;
}
return [];
}
export function subscribeToRunEvents(
runId: string,
mutate: MutateFn,
eventSourceFactory: (url: string) => EventSourceLike = createBrowserEventSource,
{ debounceMs = 300, coordinator }: RunEventOptions = {},
): () => void {
return subscribeToCrossTabSse<RunEventPayload>({
coordinator,
subscriptionKey: `run:${runId}`,
mutate,
debounceMs,
resyncKeys: () => resyncKeysForRun(runId),
resolveInvalidation: (payload) => {
if (payload.run_id !== runId) return { keys: [] };
return runInvalidation(runId, payload);
},
fallbackSubscribe: () =>
subscribeToSharedEventSource<RunEventPayload>({
subscriptions,
subscriptionKey: runId,
url: queryKeys.runs.attach(runId),
mutate,
eventSourceFactory,
debounceMs,
resolveInvalidation: (payload) => {
const result = runInvalidation(runId, payload);
return { ...result, close: result.immediate };
},
}),
});
}
function runInvalidation(runId: string, payload: RunEventPayload) {
const event = payload.event;
if (!event) return { keys: [], immediate: false };
const stageId = stageIdFromPayload(payload);
const keys = queryKeysForRunEvent(runId, event, stageId);
const terminal = TERMINAL_EVENTS.has(event);
return { keys, immediate: terminal };
}
function resyncKeysForRun(runId: string) {
return [
queryKeys.runs.detail(runId),
queryKeys.runs.files(runId),
queryKeys.runs.billing(runId),
queryKeys.runs.stages(runId),
queryKeys.runs.events(runId, 1000),
queryKeys.runs.graph(runId, "LR"),
queryKeys.runs.graph(runId, "TB"),
queryKeys.runs.questions(runId, 25, 0),
];
}
function stageIdFromPayload(payload: RunEventPayload): string | undefined {
if (typeof payload.node_id === "string") return payload.node_id;
const nodeId = payload.properties?.node_id;
return typeof nodeId === "string" ? nodeId : undefined;
}
export function useRunEvents(runId: string | undefined) {
const { mutate } = useSWRConfig();
useEffect(() => {
if (!runId) return;
return subscribeToRunEvents(runId, mutate as MutateFn);
}, [mutate, runId]);
}