fix(examples): el enlace directo a un ejemplo pro ya no parpadea un 404
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.
This commit is contained in:
parent
03652c0748
commit
3cc1510cae
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Reference in New Issue