From 3cc1510caee4492c15cce08c115048ebd9e7def0 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 30 Jul 2026 17:06:05 +0200 Subject: [PATCH] fix(examples): el enlace directo a un ejemplo pro ya no parpadea un 404 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reportado en /example/pi5-opencv-vision: al recargar aparecia un instante el 404 con el header completo (menus incluidos) y despues cargaba el editor. La carrera: los ejemplos pro se registran cuando aterriza el import dinamico del overlay, y la pagina resolvia la galeria ANTES, concluia "no existe", pintaba el 404 y al llegar el registro re-renderizaba al editor. "No esta en la galeria" y "no esta TODAVIA" son respuestas distintas, asi que el registro gana una senal de asentado: main.tsx la activa cuando el import del overlay resuelve (o falla — finally), e inmediatamente en la build OSS, donde no viene overlay y un 404 debe ser instantaneo. La pagina se queda en "Loading example..." hasta que el registro asienta y solo entonces un id ausente es de verdad un 404. markProExamplesSettled es idempotente y solo notifica una vez, con test — el sintoma de romper eso seria una tormenta de re-renders por hot-reload del overlay. --- .../__tests__/pro-examples-settled.test.ts | 31 +++++++++++++++++++ frontend/src/data/examples.ts | 24 ++++++++++++++ frontend/src/main.tsx | 10 ++++-- frontend/src/pages/ExampleEditorPage.tsx | 19 ++++++++++-- 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 frontend/src/__tests__/pro-examples-settled.test.ts diff --git a/frontend/src/__tests__/pro-examples-settled.test.ts b/frontend/src/__tests__/pro-examples-settled.test.ts new file mode 100644 index 00000000..bcfb7831 --- /dev/null +++ b/frontend/src/__tests__/pro-examples-settled.test.ts @@ -0,0 +1,31 @@ +/** + * The "settled" signal for late-registered pro examples. + * + * A direct link to a pro example races the overlay's dynamic import; the + * page must be able to tell "not in the gallery" from "not YET" or it + * flashes a 404 before the editor loads (as reported on + * /example/pi5-opencv-vision). main.tsx flips the flag once the overlay + * import settles — or immediately when no overlay is configured. + */ +import { describe, it, expect } from 'vitest'; +import { + areProExamplesSettled, + markProExamplesSettled, + subscribeProExamples, +} from '../data/examples'; + +describe('pro examples settled flag', () => { + it('starts unsettled, settles once, notifies subscribers exactly once', () => { + expect(areProExamplesSettled()).toBe(false); + let ticks = 0; + const unsub = subscribeProExamples(() => ticks++); + markProExamplesSettled(); + expect(areProExamplesSettled()).toBe(true); + expect(ticks).toBe(1); + // Idempotent: settling again must not re-notify (a re-render storm + // on every overlay hot-reload would be the symptom). + markProExamplesSettled(); + expect(ticks).toBe(1); + unsub(); + }); +}); diff --git a/frontend/src/data/examples.ts b/frontend/src/data/examples.ts index b3b827c4..2ed10293 100644 --- a/frontend/src/data/examples.ts +++ b/frontend/src/data/examples.ts @@ -11395,6 +11395,30 @@ export function subscribeProExamples(cb: () => void): () => void { return () => proExamplesListeners.delete(cb); } +/** + * Has the pro overlay had its chance to register examples? + * + * A direct link to a PRO example (/example/pi5-opencv-vision) races the + * overlay's dynamic import: the page resolves the gallery before + * registerProExamples has run, concluded "not found", and flashed a 404 — + * full marketing header included — before re-rendering into the editor. + * The page needs to distinguish "not in the gallery" from "not in the + * gallery YET". main.tsx flips this: immediately when no overlay is + * configured, otherwise when the overlay's import settles (either way). + */ +let proExamplesSettled = false; + +export function markProExamplesSettled(): void { + if (proExamplesSettled) return; + proExamplesSettled = true; + proExamplesVersion++; + for (const l of proExamplesListeners) l(); +} + +export function areProExamplesSettled(): boolean { + return proExamplesSettled; +} + export function getProExamplesVersion(): number { return proExamplesVersion; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 890d42cf..833c8422 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,6 +5,7 @@ import './index.css'; // useTranslation() always resolves against a live instance. Must come // before App. import './i18n'; +import { markProExamplesSettled } from './data/examples'; import './components/velxio-components/IC74HC595'; import './components/velxio-components/LogicGateElements'; import './components/velxio-components/TransistorElements'; @@ -59,12 +60,17 @@ if (import.meta.env.VITE_PRO_BUILD) { if (import.meta.env.VITE_DESKTOP) { import('@pro/desktop_index') .then((m) => m.mountProDesktop?.()) - .catch((err) => console.warn('[pro-desktop] failed to load slim overlay:', err)); + .catch((err) => console.warn('[pro-desktop] failed to load slim overlay:', err)) + .finally(markProExamplesSettled); } else { import('@pro/index') .then((m) => m.mountPro?.()) - .catch((err) => console.warn('[pro] failed to load overlay:', err)); + .catch((err) => console.warn('[pro] failed to load overlay:', err)) + .finally(markProExamplesSettled); } +} else { + // No overlay is coming: what the gallery has now is all there will be. + markProExamplesSettled(); } // Desktop-only hooks (ESP32 QEMU prompt now, welcome screen in Phase 3). diff --git a/frontend/src/pages/ExampleEditorPage.tsx b/frontend/src/pages/ExampleEditorPage.tsx index 0fb6f510..db010f86 100644 --- a/frontend/src/pages/ExampleEditorPage.tsx +++ b/frontend/src/pages/ExampleEditorPage.tsx @@ -23,7 +23,8 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import { useParams } from 'react-router-dom'; -import { exampleProjects, subscribeProExamples, getProExamplesVersion } from '../data/examples'; +import { exampleProjects, subscribeProExamples, + areProExamplesSettled, getProExamplesVersion } from '../data/examples'; import { loadExample, type LibraryInstallProgress } from '../utils/loadExample'; import { EditorPage } from './EditorPage'; import { AppHeader } from '../components/layout/AppHeader'; @@ -59,13 +60,25 @@ export const ExampleEditorPage: React.FC = () => { : `${DOMAIN}/examples`, }); + const settled = useSyncExternalStore( + subscribeProExamples, + areProExamplesSettled, + areProExamplesSettled, + ); + useEffect(() => { if (!exampleId) { setError(true); return; } if (!example) { - setError(true); + // "Not in the gallery" and "not in the gallery YET" are different + // answers while the pro overlay's dynamic import is still in flight: + // a direct link to a pro example used to flash a 404 (marketing + // header and all) before the overlay registered it and the editor + // took over. Stay on the loading screen until the registry settles; + // only then is a missing id really a 404. + if (settled) setError(true); return; } if (loadedIdRef.current === exampleId) return; @@ -91,7 +104,7 @@ export const ExampleEditorPage: React.FC = () => { return () => { cancelled = true; }; - }, [exampleId, example]); + }, [exampleId, example, settled]); if (error) { return (