Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/lib/pyodide/backend/flask/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 25 additions & 32 deletions src/lib/pyodide/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
export async function resolveBackend(): Promise<void> {
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<void> {
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
Expand Down
6 changes: 2 additions & 4 deletions src/lib/pyodide/backend/pyodide/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ interface StreamState {
export class PyodideBackend extends AbstractBackend {
private worker: Worker | null = null;
private pendingRequests = new Map<string, PendingRequest>();
private isInitializing = false;

private streamState: StreamState = {
id: null,
Expand Down Expand Up @@ -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' });
Expand Down Expand Up @@ -291,7 +289,6 @@ export class PyodideBackend extends AbstractBackend {
loading: false,
progress: STATUS_MESSAGES.READY
}));
this.isInitializing = false;
break;

case 'progress':
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -598,8 +598,7 @@
seedPreloadedToolboxes();
backendReady = (async () => {
try {
await autoDetectBackend();
await initBackendFromUrl();
await resolveBackend();
await initPyodide();
statusText = 'Loading toolboxes...';
await bootstrapToolboxes();
Expand Down
Loading