Skip to content

Commit eec26ff

Browse files
authored
Merge pull request #9 from pathsim/fix/pyodide-worker-crash-on-example-change
fix pyodide worker crash when switching examples (#8)
2 parents 5768d4e + 47c4ed0 commit eec26ff

2 files changed

Lines changed: 31 additions & 20 deletions

File tree

src/lib/pyodide/index.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,19 @@ export async function initPyodide(packages?: PyodidePackageInfo[], packageVersio
188188
}
189189

190190
// Already initialized with same versions
191-
let state: { status: string } = { status: 'idle' };
192-
pyodideState.subscribe((s) => (state = s))();
193-
if (state.status === 'ready') {
191+
if (worker) {
194192
return;
195193
}
196194

197195
initPromise = new Promise<void>((resolve, reject) => {
196+
let settled = false;
197+
198198
// Create worker
199199
worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
200200
worker.onmessage = handleWorkerMessage;
201201
worker.onerror = (error) => {
202202
setError(error.message);
203-
reject(new Error(error.message));
203+
if (!settled) { settled = true; reject(new Error(error.message)); }
204204
};
205205

206206
// Wait for ready message
@@ -211,10 +211,10 @@ export async function initPyodide(packages?: PyodidePackageInfo[], packageVersio
211211
worker!.onmessage = originalHandler;
212212
initializedPackages = packages ?? null;
213213
initializedVersions = packageVersions ?? null;
214-
resolve();
214+
if (!settled) { settled = true; resolve(); }
215215
} else if (event.data.type === 'error' && !event.data.id) {
216216
worker!.onmessage = originalHandler;
217-
reject(new Error(event.data.error));
217+
if (!settled) { settled = true; reject(new Error(event.data.error)); }
218218
}
219219
};
220220

@@ -224,7 +224,8 @@ export async function initPyodide(packages?: PyodidePackageInfo[], packageVersio
224224

225225
// Set timeout
226226
setTimeout(() => {
227-
if (state.status === 'loading') {
227+
if (!settled) {
228+
settled = true;
228229
const error = ERROR_MESSAGES.EXECUTION_TIMEOUT;
229230
setError(error);
230231
reject(new Error(error));
@@ -293,10 +294,15 @@ export async function execute(
293294
/**
294295
* Reset the Python namespace
295296
* Clears all user-defined variables but keeps common imports (np, plt)
297+
* Only sends reset if the worker is alive and initialized
296298
*/
297-
export async function reset(): Promise<void> {
298-
await initPyodide();
299-
send({ type: 'reset' });
299+
export function reset(): void {
300+
if (!worker || !initPromise) return;
301+
try {
302+
send({ type: 'reset' });
303+
} catch {
304+
// Worker may have been terminated between our check and the send
305+
}
300306
}
301307

302308
/**
@@ -318,7 +324,19 @@ export function terminate(): void {
318324
initPromise = null;
319325
initializedPackages = null;
320326
initializedVersions = null;
327+
328+
// Resolve all pending executions with an error so callers don't hang forever
329+
for (const [id, pending] of pendingExecutions) {
330+
pending.resolve({
331+
stdout: pending.stdout.join('\n'),
332+
stderr: pending.stderr.join('\n'),
333+
plots: pending.plots,
334+
error: { message: 'Worker terminated' },
335+
duration: Date.now() - pending.startTime
336+
});
337+
}
321338
pendingExecutions.clear();
339+
322340
updateState({ status: 'idle', progress: '', error: null });
323341
}
324342

src/routes/[package]/[version]/examples/[slug]/+page.svelte

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { packageVersionsStore } from '$lib/stores/packageVersionsStore';
88
import { exampleGroupsStore } from '$lib/stores/examplesContext';
99
import { notebookStore } from '$lib/stores/notebookStore';
10+
import { reset as resetPyodide } from '$lib/pyodide';
1011
import { groupByCategory } from '$lib/notebook/manifest';
1112
import { packages } from '$lib/config/packages';
1213
import type { PageData } from './$types';
@@ -43,17 +44,9 @@
4344
$effect(() => {
4445
const slug = data.meta.slug;
4546
46-
return async () => {
47-
// Cell IDs are prefixed with the slug, so old cells won't collide
48-
// with new ones. But we still need to clean up stale store entries
49-
// and reset the Python namespace.
47+
return () => {
5048
notebookStore.reset();
51-
try {
52-
const { reset } = await import('$lib/pyodide');
53-
await reset();
54-
} catch {
55-
// Ignore if Pyodide not loaded
56-
}
49+
resetPyodide();
5750
};
5851
});
5952

0 commit comments

Comments
 (0)