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);
|
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 {
|
export function getProExamplesVersion(): number {
|
||||||
return proExamplesVersion;
|
return proExamplesVersion;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import './index.css';
|
||||||
// useTranslation() always resolves against a live instance. Must come
|
// useTranslation() always resolves against a live instance. Must come
|
||||||
// before App.
|
// before App.
|
||||||
import './i18n';
|
import './i18n';
|
||||||
|
import { markProExamplesSettled } from './data/examples';
|
||||||
import './components/velxio-components/IC74HC595';
|
import './components/velxio-components/IC74HC595';
|
||||||
import './components/velxio-components/LogicGateElements';
|
import './components/velxio-components/LogicGateElements';
|
||||||
import './components/velxio-components/TransistorElements';
|
import './components/velxio-components/TransistorElements';
|
||||||
|
|
@ -59,12 +60,17 @@ if (import.meta.env.VITE_PRO_BUILD) {
|
||||||
if (import.meta.env.VITE_DESKTOP) {
|
if (import.meta.env.VITE_DESKTOP) {
|
||||||
import('@pro/desktop_index')
|
import('@pro/desktop_index')
|
||||||
.then((m) => m.mountProDesktop?.())
|
.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 {
|
} else {
|
||||||
import('@pro/index')
|
import('@pro/index')
|
||||||
.then((m) => m.mountPro?.())
|
.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).
|
// Desktop-only hooks (ESP32 QEMU prompt now, welcome screen in Phase 3).
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
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 { loadExample, type LibraryInstallProgress } from '../utils/loadExample';
|
||||||
import { EditorPage } from './EditorPage';
|
import { EditorPage } from './EditorPage';
|
||||||
import { AppHeader } from '../components/layout/AppHeader';
|
import { AppHeader } from '../components/layout/AppHeader';
|
||||||
|
|
@ -59,13 +60,25 @@ export const ExampleEditorPage: React.FC = () => {
|
||||||
: `${DOMAIN}/examples`,
|
: `${DOMAIN}/examples`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const settled = useSyncExternalStore(
|
||||||
|
subscribeProExamples,
|
||||||
|
areProExamplesSettled,
|
||||||
|
areProExamplesSettled,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!exampleId) {
|
if (!exampleId) {
|
||||||
setError(true);
|
setError(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!example) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (loadedIdRef.current === exampleId) return;
|
if (loadedIdRef.current === exampleId) return;
|
||||||
|
|
@ -91,7 +104,7 @@ export const ExampleEditorPage: React.FC = () => {
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [exampleId, example]);
|
}, [exampleId, example, settled]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue