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,
|
createDashboardRequestAborter,
|
||||||
runViewportAwareDashboardHydrationQueue,
|
runViewportAwareDashboardHydrationQueue,
|
||||||
shouldPauseDashboardRefresh,
|
shouldPauseDashboardRefresh,
|
||||||
subscribeToDashboardTileEvents,
|
|
||||||
waitForDashboardHydrationIdle,
|
waitForDashboardHydrationIdle,
|
||||||
type DashboardIntersectionObserverFactory,
|
type DashboardIntersectionObserverFactory,
|
||||||
type DashboardTileReference,
|
type DashboardTileReference,
|
||||||
|
|
@ -178,7 +177,6 @@ export default function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let refreshTimer: number | undefined;
|
let refreshTimer: number | undefined;
|
||||||
let unsubscribeTileEvents: (() => void) | undefined;
|
|
||||||
let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
|
let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
|
||||||
let hydrationRun = 0;
|
let hydrationRun = 0;
|
||||||
const requestAborter = createDashboardRequestAborter();
|
const requestAborter = createDashboardRequestAborter();
|
||||||
|
|
@ -215,8 +213,6 @@ export default function App() {
|
||||||
|
|
||||||
function pauseRefreshes() {
|
function pauseRefreshes() {
|
||||||
clearRefreshTimer();
|
clearRefreshTimer();
|
||||||
unsubscribeTileEvents?.();
|
|
||||||
unsubscribeTileEvents = undefined;
|
|
||||||
requestAborter.abortActiveRequests();
|
requestAborter.abortActiveRequests();
|
||||||
setHydratingItemIds(new Set());
|
setHydratingItemIds(new Set());
|
||||||
}
|
}
|
||||||
|
|
@ -270,14 +266,6 @@ export default function App() {
|
||||||
schemaVersion: restored.dashboard.schemaVersion,
|
schemaVersion: restored.dashboard.schemaVersion,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
subscribeDashboardTileEvents(
|
|
||||||
currentRun,
|
|
||||||
tileSignal,
|
|
||||||
{
|
|
||||||
currentRevisionId: restored.dashboard.currentRevisionId,
|
|
||||||
schemaVersion: restored.dashboard.schemaVersion,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setHydratingItemIds(new Set());
|
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(
|
async function hydrateDashboardTileBatch(
|
||||||
tiles: DashboardTileReference[],
|
tiles: DashboardTileReference[],
|
||||||
run: number,
|
run: number,
|
||||||
|
|
@ -562,7 +526,6 @@ export default function App() {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearRefreshTimer();
|
clearRefreshTimer();
|
||||||
unsubscribeTileEvents?.();
|
|
||||||
requestAborter.abortActiveRequests();
|
requestAborter.abortActiveRequests();
|
||||||
detachRefreshLifecycle();
|
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(
|
export function restoreDashboardTileSnapshots(
|
||||||
dashboard: DashboardRuntimeState,
|
dashboard: DashboardRuntimeState,
|
||||||
snapshots: RestoredDashboardTileSnapshot[],
|
snapshots: RestoredDashboardTileSnapshot[],
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
runViewportAwareDashboardHydrationQueue,
|
runViewportAwareDashboardHydrationQueue,
|
||||||
shouldPauseDashboardRefresh,
|
shouldPauseDashboardRefresh,
|
||||||
splitDashboardHydrationItemsByVisibility,
|
splitDashboardHydrationItemsByVisibility,
|
||||||
subscribeToDashboardTileEvents,
|
|
||||||
waitForDashboardHydrationIdle,
|
waitForDashboardHydrationIdle,
|
||||||
type DashboardIntersectionEntry,
|
type DashboardIntersectionEntry,
|
||||||
} from "./dashboard-refresh";
|
} 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", () => {
|
test("backs off failed tile keys and resets after success", () => {
|
||||||
const backoff = createDashboardTileBackoff();
|
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];
|
const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];
|
||||||
|
|
||||||
export function createDashboardTileBackoff() {
|
export function createDashboardTileBackoff() {
|
||||||
|
|
|
||||||
|
|
@ -50,28 +50,4 @@ describe("server request routing", () => {
|
||||||
expect(response.status).toBe(405);
|
expect(response.status).toBe(405);
|
||||||
expect(response.headers.get("allow")).toBe("POST");
|
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 { extname, normalize } from "node:path";
|
||||||
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
|
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
|
||||||
import {
|
import {
|
||||||
handleDashboardEventsRoute,
|
|
||||||
handleDashboardRoute,
|
handleDashboardRoute,
|
||||||
handleDashboardTileRoute,
|
handleDashboardTileRoute,
|
||||||
handleDashboardTilesRoute,
|
handleDashboardTilesRoute,
|
||||||
|
|
@ -33,11 +32,6 @@ export async function handleRequest(request: Request): Promise<Response> {
|
||||||
return handleDashboardTilesRoute(request);
|
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 (url.pathname.startsWith("/api/dashboard/tile/")) {
|
||||||
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
if (request.method !== "GET") return methodNotAllowed(["GET"]);
|
||||||
return handleDashboardTileRoute(url.pathname);
|
return handleDashboardTileRoute(url.pathname);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
|
||||||
import {
|
import {
|
||||||
createDashboardTileCache,
|
createDashboardTileCache,
|
||||||
dashboardTileCacheKey,
|
dashboardTileCacheKey,
|
||||||
handleDashboardEventsRoute,
|
|
||||||
handleDashboardTilesRoute,
|
handleDashboardTilesRoute,
|
||||||
handleDashboardTileRoute,
|
handleDashboardTileRoute,
|
||||||
loadDashboardResponse,
|
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 () => {
|
test("rejects invalid batch tile requests", async () => {
|
||||||
const response = await handleDashboardTilesRoute(
|
const response = await handleDashboardTilesRoute(
|
||||||
new Request("https://example.test/api/dashboard/tiles", {
|
new Request("https://example.test/api/dashboard/tiles", {
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,6 @@ export interface LoadDashboardResponseOptions
|
||||||
tileCache?: DashboardTileCache;
|
tileCache?: DashboardTileCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardEventsRouteOptions extends LoadDashboardResponseOptions {
|
|
||||||
signal?: AbortSignal;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DashboardTileCacheEntry {
|
interface DashboardTileCacheEntry {
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
response: DashboardTileResolution;
|
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 {
|
export function dashboardTileCacheKey(tile: DashboardTileReference): string {
|
||||||
return JSON.stringify(tile);
|
return JSON.stringify(tile);
|
||||||
}
|
}
|
||||||
|
|
@ -468,45 +422,6 @@ function parseDashboardTileReference(value: unknown): DashboardTileReference | n
|
||||||
return null;
|
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 {
|
class DashboardTileCacheResolutionError extends Error {
|
||||||
readonly cache: "hit" | "miss";
|
readonly cache: "hit" | "miss";
|
||||||
readonly coalesced: boolean;
|
readonly coalesced: boolean;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue