diff --git a/src/lib/pyodide/backend/flask/backend.ts b/src/lib/pyodide/backend/flask/backend.ts index 384fe2ab..c337ddea 100644 --- a/src/lib/pyodide/backend/flask/backend.ts +++ b/src/lib/pyodide/backend/flask/backend.ts @@ -387,7 +387,8 @@ export class FlaskBackend extends AbstractBackend { try { this.streamState.onData(JSON.parse(msg.value as string)); } catch { - // Ignore parse errors + // Surface (don't silently drop) a corrupt stream frame. + this.stderrCallback?.('[stream] dropped an unparseable data frame\n'); } } break; diff --git a/src/lib/pyodide/backend/index.ts b/src/lib/pyodide/backend/index.ts index ce4f4522..1450e7cc 100644 --- a/src/lib/pyodide/backend/index.ts +++ b/src/lib/pyodide/backend/index.ts @@ -38,54 +38,47 @@ import { backendState } from './state'; import { consoleStore } from '$lib/stores/console'; /** - * Initialize backend from URL parameters. - * Reads `?backend=flask` and `?host=...` from the current URL. - * Call this early in page mount, before any backend usage. + * Resolve and initialize the execution backend at startup. + * + * Single entry point replacing the old `initBackendFromUrl` + `autoDetectBackend` + * pair (which were both awaited and could each switch+init independently). + * Precedence: + * 1. `?backend=flask[&host=...]` URL override — explicit wins. + * 2. Same-origin Flask server (pip-package mode), detected via `/api/health`. + * 3. Default: Pyodide (created lazily by `getBackend()` on first use). + * Only switches/initializes a Flask backend; the Pyodide default needs no work + * here (it initializes on first `init()`/`exec()`). */ -export async function initBackendFromUrl(): Promise { +export async function resolveBackend(): Promise { if (typeof window === 'undefined') return; const params = new URLSearchParams(window.location.search); - const backendParam = params.get('backend'); - const hostParam = params.get('host'); - if (backendParam === 'flask') { - if (hostParam) { - setFlaskHost(hostParam); - } + // 1. Explicit URL override. + if (params.get('backend') === 'flask') { + const host = params.get('host'); + if (host) setFlaskHost(host); switchBackend('flask'); await init(); + return; } -} - -/** - * Auto-detect if a Flask backend is available at the same origin. - * Used when the frontend is served by the Flask server (pip package mode). - * URL parameters take precedence — if `?backend=` is set, auto-detection is skipped. - */ -export async function autoDetectBackend(): Promise { - if (typeof window === 'undefined') return; - - // URL params override auto-detection - const params = new URLSearchParams(window.location.search); - if (params.has('backend')) return; + // 2. Auto-detect a same-origin Flask server. try { const response = await fetch('/api/health', { method: 'GET', signal: AbortSignal.timeout(2000) }); - if (response.ok) { - const data = await response.json(); - if (data.status === 'ok') { - setFlaskHost(window.location.origin); - switchBackend('flask'); - // Run full init — sets up callbacks, logs progress, initializes worker - await init(); - } + if (response.ok && (await response.json())?.status === 'ok') { + setFlaskHost(window.location.origin); + switchBackend('flask'); + await init(); + return; } } catch { - // No Flask backend at same origin — will use Pyodide + // No Flask backend at same origin — fall through to the Pyodide default. } + + // 3. Default: Pyodide (lazy). } // Alias for backward compatibility diff --git a/src/lib/pyodide/backend/pyodide/backend.ts b/src/lib/pyodide/backend/pyodide/backend.ts index b06d1247..fd765ec0 100644 --- a/src/lib/pyodide/backend/pyodide/backend.ts +++ b/src/lib/pyodide/backend/pyodide/backend.ts @@ -33,7 +33,6 @@ interface StreamState { export class PyodideBackend extends AbstractBackend { private worker: Worker | null = null; private pendingRequests = new Map(); - private isInitializing = false; private streamState: StreamState = { id: null, @@ -66,7 +65,6 @@ export class PyodideBackend extends AbstractBackend { error: null, progress: PROGRESS_MESSAGES.STARTING_WORKER })); - this.isInitializing = true; try { this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); @@ -291,7 +289,6 @@ export class PyodideBackend extends AbstractBackend { loading: false, progress: STATUS_MESSAGES.READY })); - this.isInitializing = false; break; case 'progress': @@ -341,7 +338,8 @@ export class PyodideBackend extends AbstractBackend { try { this.streamState.onData(JSON.parse(response.value)); } catch { - // Ignore parse errors + // Surface (don't silently drop) a corrupt stream frame. + this.stderrCallback?.('[stream] dropped an unparseable data frame\n'); } } break; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 33a1f4c8..5436e217 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -47,7 +47,7 @@ import type { MenuItemType } from '$lib/components/ContextMenu.svelte'; import { pyodideState, simulationState, initPyodide, stopSimulation, continueStreamingSimulation, stageMutations, resetSimulation } from '$lib/pyodide/bridge'; import { pendingMutationCount } from '$lib/pyodide/mutationQueue'; - import { initBackendFromUrl, autoDetectBackend } from '$lib/pyodide/backend'; + import { resolveBackend } from '$lib/pyodide/backend'; import { runGraphStreamingSimulation, validateGraphSimulation, exportToPython } from '$lib/pyodide/pathsimRunner'; import { consoleStore } from '$lib/stores/console'; import { newGraph, saveFile, saveAsFile, setupAutoSave, clearAutoSave, debouncedAutoSave, openImportDialog, importFromUrl, currentFileName, loadGraphFile, listRecentFiles, openRecentFile, removeRecentFile } from '$lib/schema/fileOps'; @@ -598,8 +598,7 @@ seedPreloadedToolboxes(); backendReady = (async () => { try { - await autoDetectBackend(); - await initBackendFromUrl(); + await resolveBackend(); await initPyodide(); statusText = 'Loading toolboxes...'; await bootstrapToolboxes();