fix(viewer): pages that went blank — repeated ids, and Steps on a function full of checks (#2264)

* fix(viewer): a repeated id no longer blanks Entry points or a component's Symbol view

The viewer keys its lists by id and a repeat stops the page drawing. The
routes list was a row per (route, edge), so an inline handler came back once
per call in its body; it is now one row per route — its bound handler, or
the route itself as an inline handler. A Vue/Svelte/Astro component's
members listed its script's symbols twice, plus the component and its file;
each member is now listed once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(viewer): Steps reads a function full of checks in one pass

The in-order reading kept every way forward through an `if` with no
`else` twice — under its condition and without — so eighty checks that
draw nothing (jsoup's `parse`) made 2^80 ways and the page died on a
stack overflow. Ways waiting at the same place are merged, keeping the
conditions they all share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-01 20:23:31 +00:00
committed by GitHub
parent d45edca3fd
commit 81e08b76ed
8 changed files with 233 additions and 12 deletions
+2 -1
View File
@@ -140,7 +140,8 @@ export function routeRow(route: WireEntryRoute): EntryRow {
target: route.handlerId
? { type: 'symbol', id: route.handlerId, name: route.handler, kind: route.handlerKind }
: null,
flowFrom: route.handlerId ? route.handler : null,
// An inline handler has no name of its own to draw a Flow from.
flowFrom: route.handlerId && !route.inline ? route.handler : null,
title: `${route.url} → ${route.handler} (${route.file}:${route.line}), registered at ${route.routeFile}:${route.routeLine}`,
};
}
+23 -3
View File
@@ -174,12 +174,12 @@ export function orderGraph(program: NonNullable<WireStepsPayload['program']>, an
// An arm that answers, returns or throws does not rejoin — nothing
// leaves the last box in it, which is what says so on a canvas.
const armTails = flow(arm.body, entry, []);
if (arm.ends === null) out.push(...armTails);
if (arm.ends === null) for (const t of armTails) out.push(t);
}
// An `if` with no `else` runs on either way; a fork with both sides
// covered runs on only through the arms that did not end.
if (item.arms.length < 2) out.push(...tails.map((t) => ({ ...t, runs: [...t.runs, ...runs] })));
tails = out;
if (item.arms.length < 2) for (const t of tails) out.push({ ...t, runs: [...t.runs, ...runs] });
tails = mergeTails(out);
} else if (item.kind === 'block') {
const label = runWords(item);
const inner = flow(item.body, tails, [...runs, label]);
@@ -206,6 +206,26 @@ export function orderGraph(program: NonNullable<WireStepsPayload['program']>, an
return { edges, depth: rows(anchor, ids, edges), forks };
}
/**
* Ways on that wait at the same place are one place to go on from. Without
* this, an `if` with no `else` — or a fork whose arms draw nothing — doubles
* every way it crosses (once under its condition, once without), and a body
* with eighty such checks (jsoup's `parse`) never finished: the page died
* overflowing the stack. The merged way keeps what every way to it had to
* hold; a condition only some of them needed is dropped, so the line
* under-claims rather than claims a side it does not have.
*/
function mergeTails(tails: readonly Tail[]): Tail[] {
const byPlace = new Map<string, Tail>();
for (const t of tails) {
const key = `${t.id}\0${t.arm ?? ''}\0${t.runs.join('\0')}`;
const found = byPlace.get(key);
if (found) found.when = found.when.filter((w) => t.when.includes(w));
else byPlace.set(key, { ...t, when: [...t.when], runs: [...t.runs] });
}
return [...byPlace.values()];
}
/** Whether anything in this block draws a box — a fork with one drawn arm is a guard clause, not a point. */
function hasStep(block: WireBlock): boolean {
return block.some((item) =>
+4
View File
@@ -377,6 +377,8 @@ export interface WireRoute {
routeFile: string;
routeLine: number;
routeId: string;
/** The handler is written inline at the registration: the route stands in for it. */
inline: boolean;
}
export interface WireRoutes {
@@ -410,6 +412,8 @@ export interface WireEntryRoute {
routeFile: string;
routeLine: number;
routeId: string;
/** The handler is written inline at the registration: the route stands in for it. */
inline: boolean;
}
export interface WireEntryFile extends WireNodeRef {