Compare commits
No commits in common. "216545b83812ff14783330fddeba7d70323fcda9" and "a66f1fdd57d31cbd4c4101155a4ccc947377598f" have entirely different histories.
216545b838
...
a66f1fdd57
7 changed files with 0 additions and 315 deletions
|
|
@ -17,7 +17,6 @@ import {
|
|||
createDashboardRequestAborter,
|
||||
runViewportAwareDashboardHydrationQueue,
|
||||
shouldPauseDashboardRefresh,
|
||||
subscribeToDashboardTileEvents,
|
||||
waitForDashboardHydrationIdle,
|
||||
type DashboardIntersectionObserverFactory,
|
||||
type DashboardTileReference,
|
||||
|
|
@ -178,7 +177,6 @@ export default function App() {
|
|||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let refreshTimer: number | undefined;
|
||||
let unsubscribeTileEvents: (() => void) | undefined;
|
||||
let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
|
||||
let hydrationRun = 0;
|
||||
const requestAborter = createDashboardRequestAborter();
|
||||
|
|
@ -215,8 +213,6 @@ export default function App() {
|
|||
|
||||
function pauseRefreshes() {
|
||||
clearRefreshTimer();
|
||||
unsubscribeTileEvents?.();
|
||||
unsubscribeTileEvents = undefined;
|
||||
requestAborter.abortActiveRequests();
|
||||
setHydratingItemIds(new Set());
|
||||
}
|
||||
|
|
@ -270,14 +266,6 @@ export default function App() {
|
|||
schemaVersion: restored.dashboard.schemaVersion,
|
||||
},
|
||||
);
|
||||
subscribeDashboardTileEvents(
|
||||
currentRun,
|
||||
tileSignal,
|
||||
{
|
||||
currentRevisionId: restored.dashboard.currentRevisionId,
|
||||
schemaVersion: restored.dashboard.schemaVersion,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setHydratingItemIds(new Set());
|
||||
}
|
||||
|
|
@ -370,30 +358,6 @@ export default function App() {
|
|||
});
|
||||
}
|
||||
|
||||
function subscribeDashboardTileEvents(
|
||||
run: number,
|
||||
signal: AbortSignal,
|
||||
snapshotContext: DashboardTileSnapshotStoreContext,
|
||||
) {
|
||||
unsubscribeTileEvents?.();
|
||||
unsubscribeTileEvents = subscribeToDashboardTileEvents({
|
||||
onTile: (event) => {
|
||||
const tileResponse = event as DashboardTileResponse;
|
||||
if (!isDashboardTileResponse(tileResponse)) return;
|
||||
applyDashboardTileHydrationResponse(
|
||||
tileResponse.tile,
|
||||
tileResponse,
|
||||
run,
|
||||
signal,
|
||||
snapshotContext,
|
||||
);
|
||||
},
|
||||
onUnavailable: () => {
|
||||
unsubscribeTileEvents = undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function hydrateDashboardTileBatch(
|
||||
tiles: DashboardTileReference[],
|
||||
run: number,
|
||||
|
|
@ -562,7 +526,6 @@ export default function App() {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
clearRefreshTimer();
|
||||
unsubscribeTileEvents?.();
|
||||
requestAborter.abortActiveRequests();
|
||||
detachRefreshLifecycle();
|
||||
};
|
||||
|
|
@ -662,20 +625,6 @@ function isAbortError(error: unknown): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function isDashboardTileResponse(value: unknown): value is DashboardTileResponse {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"state" in value &&
|
||||
(value.state === "ready" ||
|
||||
value.state === "not_found" ||
|
||||
value.state === "disabled") &&
|
||||
"tile" in value &&
|
||||
typeof value.tile === "object" &&
|
||||
value.tile !== null
|
||||
);
|
||||
}
|
||||
|
||||
export function restoreDashboardTileSnapshots(
|
||||
dashboard: DashboardRuntimeState,
|
||||
snapshots: RestoredDashboardTileSnapshot[],
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
runViewportAwareDashboardHydrationQueue,
|
||||
shouldPauseDashboardRefresh,
|
||||
splitDashboardHydrationItemsByVisibility,
|
||||
subscribeToDashboardTileEvents,
|
||||
waitForDashboardHydrationIdle,
|
||||
type DashboardIntersectionEntry,
|
||||
} from "./dashboard-refresh";
|
||||
|
|
@ -303,69 +302,6 @@ describe("dashboard refresh lifecycle", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test("subscribes to dashboard tile events", () => {
|
||||
const received: unknown[] = [];
|
||||
let listener: ((event: MessageEvent<string>) => void) | undefined;
|
||||
let closed = false;
|
||||
|
||||
const unsubscribe = subscribeToDashboardTileEvents({
|
||||
createEventSource: (url) => {
|
||||
expect(url).toBe("/api/dashboard/events");
|
||||
return {
|
||||
addEventListener(_type, eventListener) {
|
||||
listener = eventListener;
|
||||
},
|
||||
close() {
|
||||
closed = true;
|
||||
},
|
||||
onerror: null,
|
||||
};
|
||||
},
|
||||
onTile: (tile) => received.push(tile),
|
||||
});
|
||||
|
||||
listener?.({ data: JSON.stringify({ state: "ready" }) } as MessageEvent<string>);
|
||||
expect(received).toEqual([{ state: "ready" }]);
|
||||
unsubscribe();
|
||||
expect(closed).toBe(true);
|
||||
});
|
||||
|
||||
test("falls back when dashboard tile events are unavailable or fail", () => {
|
||||
let unavailableCount = 0;
|
||||
|
||||
subscribeToDashboardTileEvents({
|
||||
createEventSource: undefined,
|
||||
onTile: () => undefined,
|
||||
onUnavailable: () => {
|
||||
unavailableCount += 1;
|
||||
},
|
||||
});
|
||||
|
||||
let errorHandler: (() => void) | null = null;
|
||||
const unsubscribe = subscribeToDashboardTileEvents({
|
||||
createEventSource: () => ({
|
||||
addEventListener: () => undefined,
|
||||
close: () => undefined,
|
||||
get onerror() {
|
||||
return errorHandler;
|
||||
},
|
||||
set onerror(handler) {
|
||||
errorHandler = handler;
|
||||
},
|
||||
}),
|
||||
onTile: () => undefined,
|
||||
onUnavailable: () => {
|
||||
unavailableCount += 1;
|
||||
},
|
||||
});
|
||||
|
||||
if (!errorHandler) throw new Error("expected error handler");
|
||||
const triggerError = errorHandler as unknown as () => void;
|
||||
triggerError();
|
||||
unsubscribe();
|
||||
expect(unavailableCount).toBe(2);
|
||||
});
|
||||
|
||||
test("backs off failed tile keys and resets after success", () => {
|
||||
const backoff = createDashboardTileBackoff();
|
||||
|
||||
|
|
|
|||
|
|
@ -489,51 +489,6 @@ export function createDashboardPerformanceMarks(
|
|||
};
|
||||
}
|
||||
|
||||
export interface DashboardTileEventSource {
|
||||
addEventListener(
|
||||
type: "dashboard-tile",
|
||||
listener: (event: MessageEvent<string>) => void,
|
||||
): void;
|
||||
close(): void;
|
||||
onerror: (() => void) | null;
|
||||
}
|
||||
|
||||
export interface DashboardTileEventSubscriptionOptions {
|
||||
createEventSource?: (url: string) => DashboardTileEventSource;
|
||||
onTile: (data: unknown) => void;
|
||||
onUnavailable?: () => void;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export function subscribeToDashboardTileEvents(
|
||||
options: DashboardTileEventSubscriptionOptions,
|
||||
): () => void {
|
||||
const createEventSource = options.createEventSource ||
|
||||
(typeof globalThis.EventSource !== "undefined"
|
||||
? (url: string) => new globalThis.EventSource(url)
|
||||
: undefined);
|
||||
|
||||
if (!createEventSource) {
|
||||
options.onUnavailable?.();
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const source = createEventSource(options.url || "/api/dashboard/events");
|
||||
source.addEventListener("dashboard-tile", (event) => {
|
||||
try {
|
||||
options.onTile(JSON.parse(event.data));
|
||||
} catch {
|
||||
// Ignore malformed diagnostics from an optional live transport.
|
||||
}
|
||||
});
|
||||
source.onerror = () => {
|
||||
source.close();
|
||||
options.onUnavailable?.();
|
||||
};
|
||||
|
||||
return () => source.close();
|
||||
}
|
||||
|
||||
const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];
|
||||
|
||||
export function createDashboardTileBackoff() {
|
||||
|
|
|
|||
|
|
@ -50,28 +50,4 @@ describe("server request routing", () => {
|
|||
expect(response.status).toBe(405);
|
||||
expect(response.headers.get("allow")).toBe("POST");
|
||||
});
|
||||
|
||||
test("routes dashboard event stream requests", async () => {
|
||||
const response = await handleRequest(
|
||||
new Request("https://example.test/api/dashboard/events", {
|
||||
method: "GET",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe(
|
||||
"text/event-stream; charset=utf-8",
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects non-get dashboard event stream requests", async () => {
|
||||
const response = await handleRequest(
|
||||
new Request("https://example.test/api/dashboard/events", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
expect(response.headers.get("allow")).toBe("GET");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { extname, normalize } from "node:path";
|
||||
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
|
||||
import {
|
||||
handleDashboardEventsRoute,
|
||||
handleDashboardRoute,
|
||||
handleDashboardTileRoute,
|
||||
handleDashboardTilesRoute,
|
||||
|
|
@ -33,11 +32,6 @@ export async function handleRequest(request: Request): Promise<Response> {
|
|||
return handleDashboardTilesRoute(request);
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/dashboard/events") {
|
||||
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
||||
return handleDashboardEventsRoute({ signal: request.signal });
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/dashboard/tile/")) {
|
||||
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
||||
return handleDashboardTileRoute(url.pathname);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
|
|||
import {
|
||||
createDashboardTileCache,
|
||||
dashboardTileCacheKey,
|
||||
handleDashboardEventsRoute,
|
||||
handleDashboardTilesRoute,
|
||||
handleDashboardTileRoute,
|
||||
loadDashboardResponse,
|
||||
|
|
@ -606,45 +605,6 @@ describe("dashboard API route", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("streams ready dashboard tile events", async () => {
|
||||
const controller = new AbortController();
|
||||
const response = await handleDashboardEventsRoute({
|
||||
refreshSeedDocument: true,
|
||||
seedIfEmpty: true,
|
||||
signal: controller.signal,
|
||||
tileCache: {
|
||||
async resolve(key) {
|
||||
controller.abort();
|
||||
const tile = JSON.parse(key);
|
||||
|
||||
return {
|
||||
cache: "miss",
|
||||
coalesced: false,
|
||||
response: {
|
||||
state: "ready",
|
||||
tile,
|
||||
item: {
|
||||
id: tile.id,
|
||||
label: "Status",
|
||||
severity: "ok",
|
||||
value: "ok",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.headers.get("content-type")).toBe(
|
||||
"text/event-stream; charset=utf-8",
|
||||
);
|
||||
expect(response.headers.get("cache-control")).toBe("no-cache");
|
||||
const body = await response.text();
|
||||
expect(body).toContain("event: dashboard-tile");
|
||||
expect(body).toContain('"state":"ready"');
|
||||
expect(body).toContain('"tile"');
|
||||
});
|
||||
|
||||
test("rejects invalid batch tile requests", async () => {
|
||||
const response = await handleDashboardTilesRoute(
|
||||
new Request("https://example.test/api/dashboard/tiles", {
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ export interface LoadDashboardResponseOptions
|
|||
tileCache?: DashboardTileCache;
|
||||
}
|
||||
|
||||
export interface DashboardEventsRouteOptions extends LoadDashboardResponseOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface DashboardTileCacheEntry {
|
||||
expiresAt: number;
|
||||
response: DashboardTileResolution;
|
||||
|
|
@ -297,48 +293,6 @@ export async function handleDashboardTilesRoute(
|
|||
});
|
||||
}
|
||||
|
||||
export async function handleDashboardEventsRoute(
|
||||
options: DashboardEventsRouteOptions = {},
|
||||
): Promise<Response> {
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
const dashboard = await loadDashboardResponse({
|
||||
...options,
|
||||
hydrateLiveDatasources: false,
|
||||
});
|
||||
|
||||
if (dashboard.state !== "ready" || options.signal?.aborted) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const tile of dashboardEventTiles(dashboard.document)) {
|
||||
if (options.signal?.aborted) break;
|
||||
const resolution = await loadDashboardTileResponse(tile, options);
|
||||
if (resolution.state === "ready") {
|
||||
controller.enqueue(
|
||||
encoder.encode(dashboardTileEventChunk(resolution)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function dashboardTileCacheKey(tile: DashboardTileReference): string {
|
||||
return JSON.stringify(tile);
|
||||
}
|
||||
|
|
@ -468,45 +422,6 @@ function parseDashboardTileReference(value: unknown): DashboardTileReference | n
|
|||
return null;
|
||||
}
|
||||
|
||||
function dashboardEventTiles(document: DashboardDocument): DashboardTileReference[] {
|
||||
const status = document.statusStrips.flatMap((strip) =>
|
||||
strip.items.map((item): DashboardTileReference => ({
|
||||
kind: "status",
|
||||
stripId: strip.id,
|
||||
id: item.id,
|
||||
})),
|
||||
);
|
||||
const telemetry = document.telemetry
|
||||
.filter((card) => card.datasource?.type === "external")
|
||||
.map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id }));
|
||||
const modules = (document.modules || [])
|
||||
.filter((module) =>
|
||||
module.datasource?.type === "external" ||
|
||||
module.id === "runtime-health-summary"
|
||||
)
|
||||
.map((module): DashboardTileReference => ({ kind: "module", id: module.id }));
|
||||
const services = document.serviceGroups.flatMap((group) =>
|
||||
group.services
|
||||
.filter((service) => service.datasource?.type === "external")
|
||||
.map((service): DashboardTileReference => ({
|
||||
kind: "service",
|
||||
groupId: group.id,
|
||||
id: service.id,
|
||||
})),
|
||||
);
|
||||
|
||||
return [...status, ...telemetry, ...modules, ...services];
|
||||
}
|
||||
|
||||
function dashboardTileEventChunk(resolution: DashboardTileResolution): string {
|
||||
return [
|
||||
"event: dashboard-tile",
|
||||
`data: ${JSON.stringify(resolution)}`,
|
||||
"",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
class DashboardTileCacheResolutionError extends Error {
|
||||
readonly cache: "hit" | "miss";
|
||||
readonly coalesced: boolean;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue