-
-
Notifications
You must be signed in to change notification settings - Fork 222
fix(UI): prevent startup spinner stall when external model resolution hangs #860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,21 @@ const LearnNow = lazy(() => import("./pages/LearnNow")); | |
| const MainContainer = lazy(() => import("./pages/MainContainer")); | ||
|
|
||
| const { Content } = Layout; | ||
| const APP_INIT_TIMEOUT_MS = 12000; | ||
|
|
||
| const withTimeout = async (promise: Promise<void>, ms: number, message: string): Promise<void> => { | ||
| let timeoutHandle: ReturnType<typeof setTimeout> | undefined; | ||
| const timeoutPromise = new Promise<void>((_, reject) => { | ||
| timeoutHandle = setTimeout(() => reject(new Error(message)), ms); | ||
| }); | ||
| try { | ||
| await Promise.race([promise, timeoutPromise]); | ||
| } finally { | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const App = () => { | ||
| const navigate = useNavigate(); | ||
|
|
@@ -42,12 +57,20 @@ const App = () => { | |
| compressedData = searchParams.get("data"); | ||
| } | ||
| if (compressedData) { | ||
| await loadFromLink(compressedData); | ||
| await withTimeout( | ||
| loadFromLink(compressedData), | ||
| APP_INIT_TIMEOUT_MS, | ||
| `Initialization timed out after ${APP_INIT_TIMEOUT_MS}ms while loading shared data.` | ||
| ); | ||
| if (window.location.pathname !== "/") { | ||
| navigate("/", { replace: true }); | ||
| } | ||
| } else { | ||
| await init(); | ||
| await withTimeout( | ||
| init(), | ||
| APP_INIT_TIMEOUT_MS, | ||
| `Initialization timed out after ${APP_INIT_TIMEOUT_MS}ms.` | ||
| ); | ||
| } | ||
|
Comment on lines
+60
to
74
|
||
| } catch (error) { | ||
| console.error("Initialization error:", error); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,15 +79,43 @@ export interface DecompressedData { | |
|
|
||
| const rebuildDeBounce = debounce(rebuild, 500); | ||
|
|
||
| const EXTERNAL_MODEL_RESOLUTION_TIMEOUT_MS = 10000; | ||
|
|
||
| function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> { | ||
| return new Promise<T>((resolve, reject) => { | ||
| const timer = setTimeout(() => reject(new Error(message)), ms); | ||
| promise | ||
| .then((value) => { | ||
| clearTimeout(timer); | ||
| resolve(value); | ||
| }) | ||
| .catch((error: unknown) => { | ||
| clearTimeout(timer); | ||
| reject(error); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| function hasExternalImports(model: string): boolean { | ||
| return /^\s*import\s+/m.test(model); | ||
| } | ||
|
|
||
| async function rebuild(template: string, model: string, dataString: string): Promise<string> { | ||
| // Validate inputs before expensive operations | ||
| // This fails fast on invalid JSON or CTO syntax without running network calls | ||
| await validateBeforeRebuild(template, model, dataString); | ||
|
|
||
| const modelManager = new ModelManager({ strict: true }); | ||
| modelManager.addCTOModel(model, undefined, true); | ||
| await modelManager.updateExternalModels(); | ||
| const engine = new TemplateMarkInterpreter(modelManager, {}); | ||
| if (hasExternalImports(model)) { | ||
| await withTimeout( | ||
| modelManager.updateExternalModels(), | ||
| EXTERNAL_MODEL_RESOLUTION_TIMEOUT_MS, | ||
| `External model resolution timed out after ${EXTERNAL_MODEL_RESOLUTION_TIMEOUT_MS}ms. Check connectivity or remove remote imports.` | ||
| ); | ||
| } | ||
|
Comment on lines
+82
to
+116
|
||
| // template-engine currently resolves concerto-core v3 types; cast to bridge v4 app types at this boundary. | ||
| const engine = new TemplateMarkInterpreter(modelManager as unknown as never, {}); | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call | ||
| const templateMarkTransformer = new TemplateMarkTransformer(); | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withTimeoutclears the timer only whentimeoutHandleis truthy. In browsers (and especially in some test environments),setTimeoutcan return0, which would skip the cleanup and leave the timer running. Use an explicittimeoutHandle !== undefinedcheck (or initialize tonull) so the timeout is always cleared when set.