Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 330f3134069da858cc9a89762b2d6474ab82cbb9
parent 750c5cd9512b4ac65209de21ecdb4274770b4eee
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 01:52:06 -0400

sites: JobLane survives Strict Mode — under next dev a lane dropped every update of its one launch and sat on "Starting…" forever

The launch effect gated its updates on a flag local to it, set by its
cleanup; Strict Mode's mount → cleanup → mount set the flag and the second
run returned early on startedRef, so the job ran and the lane never showed
it. Liveness is now a ref kept by its own effect, declared first. Production
mounts once and is unchanged. No spec had ever clicked a JobLane button
(batch, specific sites, hub), which is how it went unseen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/app/sites/components/JobLane.tsx | 29+++++++++++++++++++++--------
1 file changed, 21 insertions(+), 8 deletions(-)

diff --git a/editor/app/sites/components/JobLane.tsx b/editor/app/sites/components/JobLane.tsx @@ -44,12 +44,26 @@ export function JobLane({ title, subtitle, trigger }: Props) { const [cancelling, setCancelling] = useState(false); const preRef = useRef<HTMLPreElement | null>(null); const startedRef = useRef(false); + // Whether the lane is mounted, kept by its OWN effect. It used to be a flag + // local to the launch effect below, set by that effect's cleanup — and + // Strict Mode (on under `next dev`) runs mount → cleanup → mount: the + // cleanup set the flag, the second run returned early on startedRef, and + // every update of the one launch there is was dropped. The job ran; the lane + // sat on "Starting…" forever. Declared first, so on the remount it is true + // again before anything reads it. Production mounts once and never saw it. + const aliveRef = useRef(false); + useEffect(() => { + aliveRef.current = true; + return () => { + aliveRef.current = false; + }; + }, []); useEffect(() => { // Strict-mode mounts effects twice in dev; guard so the job launches once. if (startedRef.current) return; startedRef.current = true; - let cancelled = false; + const unmounted = () => !aliveRef.current; let stopped = false; let pollTimer: ReturnType<typeof setTimeout> | null = null; @@ -64,7 +78,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { status: string; queuePosition?: number; }; - if (!cancelled) { + if (!unmounted()) { if (data.status === "queued") { setStatus("queued"); setQueuePos( @@ -81,7 +95,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { } catch { /* transient; keep polling */ } - if (!cancelled && !stopped) pollTimer = setTimeout(() => poll(id), 1000); + if (!unmounted() && !stopped) pollTimer = setTimeout(() => poll(id), 1000); } (async () => { @@ -89,13 +103,13 @@ export function JobLane({ title, subtitle, trigger }: Props) { try { result = await trigger(); } catch (e) { - if (!cancelled) { + if (!unmounted()) { setStatus("error"); setError((e as Error).message); } return; } - if (cancelled) return; + if (unmounted()) return; if (!result.ok) { setStatus("error"); setError(result.error); @@ -110,7 +124,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { while (true) { const { value, done } = await reader.read(); if (done) break; - if (value && !cancelled) setLog((prev) => prev + value); + if (value && !unmounted()) setLog((prev) => prev + value); } } catch { /* stream tear-down */ @@ -118,7 +132,7 @@ export function JobLane({ title, subtitle, trigger }: Props) { const term = await result.done; stopped = true; if (pollTimer) clearTimeout(pollTimer); - if (!cancelled) { + if (!unmounted()) { setQueuePos(null); setStatus( term.status === "done" @@ -131,7 +145,6 @@ export function JobLane({ title, subtitle, trigger }: Props) { })(); return () => { - cancelled = true; if (pollTimer) clearTimeout(pollTimer); }; // eslint-disable-next-line react-hooks/exhaustive-deps