diff --git a/gui/src/app/prototypes/AnalysisPyPrototype/AnalysisPyFileEditor.tsx b/gui/src/app/prototypes/AnalysisPyPrototype/AnalysisPyFileEditor.tsx index 7219bbc8..2f36d53a 100644 --- a/gui/src/app/prototypes/AnalysisPyPrototype/AnalysisPyFileEditor.tsx +++ b/gui/src/app/prototypes/AnalysisPyPrototype/AnalysisPyFileEditor.tsx @@ -7,9 +7,8 @@ import { } from "react"; import { PlayArrow } from "@mui/icons-material"; import TextEditor, { ToolbarItem } from "../../FileEditor/TextEditor"; -// https://vitejs.dev/guide/assets#importing-script-as-a-worker -// https://vitejs.dev/guide/assets#importing-asset-as-url -import analysisPyWorkerURL from "./analysisPyWorker?worker&url"; +import { PydodideWorkerStatus } from "../pyodideWorker/pyodideWorkerTypes"; +import PyodideWorkerInterface from "../pyodideWorker/pyodideWorkerInterface"; type Props = { fileName: string; @@ -34,64 +33,53 @@ const AnalysisPyFileEditor: FunctionComponent = ({ height, outputDiv, }) => { - const [status, setStatus] = useState< - "idle" | "loading" | "running" | "completed" | "failed" - >("idle"); + const [status, setStatus] = useState("idle"); - const [analysisPyWorker, setAnalysisPyWorker] = useState( - undefined, - ); + const [analysisPyWorker, setAnalysisPyWorker] = useState< + PyodideWorkerInterface | undefined + >(undefined); // worker creation useEffect(() => { - const worker = new Worker(analysisPyWorkerURL, { - name: "dataPyWorker", - type: "module", - }); - setAnalysisPyWorker(worker); - return () => { - console.log("terminating dataPy worker"); - worker.terminate(); - }; - }, []); - - // message handling - useEffect(() => { - if (!analysisPyWorker) return; - - analysisPyWorker.onmessage = (e: MessageEvent) => { - const dd = e.data; - if (dd.type === "setStatus") { - setStatus(dd.status); - } else if (dd.type === "addImage") { - const b64 = dd.image; - const imageUrl = `data:image/png;base64,${b64}`; - - const img = document.createElement("img"); - img.src = imageUrl; - - const divElement = document.createElement("div"); - divElement.appendChild(img); - outputDiv?.appendChild(divElement); - } else if (dd.type === "stdout") { - console.log(dd.data); + const worker = PyodideWorkerInterface.create("analysis.py", { + onStdout: (x) => { + console.log(x); const divElement = document.createElement("div"); divElement.style.color = "blue"; const preElement = document.createElement("pre"); divElement.appendChild(preElement); - preElement.textContent = dd.data; + preElement.textContent = x; outputDiv?.appendChild(divElement); - } else if (dd.type === "stderr") { - console.error(dd.data); + }, + onStderr: (x) => { + console.error(x); const divElement = document.createElement("div"); divElement.style.color = "red"; const preElement = document.createElement("pre"); divElement.appendChild(preElement); - preElement.textContent = dd.data; + preElement.textContent = x; + outputDiv?.appendChild(divElement); + }, + onStatus: (status) => { + setStatus(status); + }, + onImage: (image) => { + const b64 = image; + const imageUrl = `data:image/png;base64,${b64}`; + + const img = document.createElement("img"); + img.src = imageUrl; + + const divElement = document.createElement("div"); + divElement.appendChild(img); outputDiv?.appendChild(divElement); - } + }, + }); + setAnalysisPyWorker(worker); + return () => { + worker.destroy(); }; - }, [analysisPyWorker, outputDiv]); + }, [outputDiv]); const handleRun = useCallback(async () => { if (status === "running") { @@ -100,12 +88,11 @@ const AnalysisPyFileEditor: FunctionComponent = ({ if (editedFileContent !== fileContent) { throw new Error("Cannot run edited code"); } - if (outputDiv) outputDiv.innerHTML = ""; - analysisPyWorker?.postMessage({ - type: "run", - code: fileContent, - }); - }, [editedFileContent, fileContent, status, analysisPyWorker, outputDiv]); + if (!analysisPyWorker) { + throw new Error("analysisPyWorker is not defined"); + } + analysisPyWorker.run(fileContent); + }, [editedFileContent, fileContent, status, analysisPyWorker]); const toolbarItems: ToolbarItem[] = useMemo(() => { const ret: ToolbarItem[] = []; const runnable = fileContent === editedFileContent && outputDiv; diff --git a/gui/src/app/prototypes/AnalysisPyPrototype/analysisPyWorker.ts b/gui/src/app/prototypes/AnalysisPyPrototype/analysisPyWorker.ts deleted file mode 100644 index fa283a9a..00000000 --- a/gui/src/app/prototypes/AnalysisPyPrototype/analysisPyWorker.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { PyodideInterface, loadPyodide } from "pyodide"; - -let pyodide: PyodideInterface | null = null; -const loadPyodideInstance = async () => { - if (pyodide === null) { - const p = await loadPyodide({ - indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.1/full", - stdout: (x: string) => { - sendStdout(x); - }, - stderr: (x: string) => { - sendStderr(x); - }, - packages: ["numpy", "matplotlib"], - }); - pyodide = p; - return pyodide; - } else { - return pyodide; - } -}; - -self.onmessage = (e) => { - const dd = e.data; - if (dd.type === "run") { - run(dd.code); - } -}; - -const sendStdout = (data: string) => { - self.postMessage({ type: "stdout", data }); -}; - -const sendStderr = (data: string) => { - self.postMessage({ type: "stderr", data }); -}; - -const setStatus = (status: string) => { - self.postMessage({ type: "setStatus", status }); -}; - -const addImage = (image: any) => { - self.postMessage({ type: "addImage", image }); -}; - -// see https://github.com/pyodide/matplotlib-pyodide/issues/6#issuecomment-1242747625 -// replace show() with a function that base64 encodes the image and then stashes it for us -const MPLPreamble = ` -SP_IMAGES = [] -def patch_matplotlib(SP_IMAGES): - import os - os.environ['MPLBACKEND'] = 'AGG' - import base64 - from io import BytesIO - import matplotlib.pyplot - _old_show = matplotlib.pyplot.show - def show(): - buf = BytesIO() - matplotlib.pyplot.savefig(buf, format='png') - buf.seek(0) - # encode to a base64 str - SP_IMAGES.append(base64.b64encode(buf.read()).decode('utf-8')) - matplotlib.pyplot.clf() - matplotlib.pyplot.show = show -patch_matplotlib(SP_IMAGES) -`; - -const run = async (code: string) => { - setStatus("loading"); - try { - const pyodide = await loadPyodideInstance(); - setStatus("running"); - // here's where we can pass in globals - const globals = pyodide.toPy({ _sp_example_global: 5 }); - const script = MPLPreamble + "\n" + code; - let succeeded = false; - try { - if (script.includes("arviz")) { - // If the script has arviz, we need to install it - setStatus("loading"); - try { - await pyodide.loadPackage("micropip"); - const microPip = pyodide.pyimport("micropip"); - await microPip.install("arviz<0.18"); - } - finally { - setStatus("running"); - } - } - pyodide.runPython(script, { globals }); - succeeded = true; - } catch (e: any) { - console.error(e); - sendStderr(e.toString()); - } - - const images = globals.get("SP_IMAGES").toJs(); - if (!isListOfStrings(images)) { - throw new Error("Expected SP_IMAGES to be a list of strings"); - } - - for (const image of images) { - addImage(image); - } - setStatus(succeeded ? "completed" : "failed"); - } catch (e: any) { - console.error(e); - self.postMessage({ - type: "stderr", - data: "UNEXPECTED ERROR: " + e.toString(), - }); - setStatus("failed"); - } -}; - -const isListOfStrings = (x: any): x is string[] => { - if (!x) return false; - return Array.isArray(x) && x.every((y) => typeof y === "string"); -}; diff --git a/gui/src/app/prototypes/DataPyPrototype/DataPyFileEditor.tsx b/gui/src/app/prototypes/DataPyPrototype/DataPyFileEditor.tsx index bfd6b5f3..970972ae 100644 --- a/gui/src/app/prototypes/DataPyPrototype/DataPyFileEditor.tsx +++ b/gui/src/app/prototypes/DataPyPrototype/DataPyFileEditor.tsx @@ -7,9 +7,8 @@ import { } from "react"; import { PlayArrow } from "@mui/icons-material"; import TextEditor, { ToolbarItem } from "../../FileEditor/TextEditor"; -// https://vitejs.dev/guide/assets#importing-script-as-a-worker -// https://vitejs.dev/guide/assets#importing-asset-as-url -import dataPyWorkerURL from "./dataPyWorker?worker&url"; +import PyodideWorkerInterface from "../pyodideWorker/pyodideWorkerInterface"; +import { PydodideWorkerStatus } from "../pyodideWorker/pyodideWorkerTypes"; type Props = { fileName: string; @@ -34,44 +33,31 @@ const DataPyFileEditor: FunctionComponent = ({ width, height, }) => { - const [status, setStatus] = useState< - "idle" | "loading" | "running" | "completed" | "failed" - >("idle"); + const [status, setStatus] = useState("idle"); - const [dataPyWorker, setDataPyWorker] = useState( - undefined, - ); + const [dataPyWorker, setDataPyWorker] = useState< + PyodideWorkerInterface | undefined + >(undefined); // worker creation useEffect(() => { - const worker = new Worker(dataPyWorkerURL, { - name: "dataPyWorker", - type: "module", + const worker = PyodideWorkerInterface.create("data.py", { + onStdout: (x) => { + console.log(x); + }, + onStderr: (x) => { + console.error(x); + }, + onStatus: (status) => { + setStatus(status); + }, + onData: setData, }); setDataPyWorker(worker); return () => { - console.log("terminating dataPy worker"); - worker.terminate(); + worker.destroy(); }; - }, []); - - // message handling - useEffect(() => { - if (!dataPyWorker) return; - - dataPyWorker.onmessage = (e: MessageEvent) => { - const dd = e.data; - if (dd.type === "setStatus") { - setStatus(dd.status); - } else if (dd.type === "setData") { - setData && setData(dd.data); - } else if (dd.type === "stdout") { - console.log(dd.data); - } else if (dd.type === "stderr") { - console.error(dd.data); - } - }; - }, [dataPyWorker, setData]); + }, [setData]); const handleRun = useCallback(async () => { if (status === "running") { @@ -80,10 +66,10 @@ const DataPyFileEditor: FunctionComponent = ({ if (editedFileContent !== fileContent) { throw new Error("Cannot run edited code"); } - dataPyWorker?.postMessage({ - type: "run", - code: fileContent, - }); + if (!dataPyWorker) { + throw new Error("dataPyWorker is not defined"); + } + dataPyWorker.run(fileContent); }, [editedFileContent, fileContent, status, dataPyWorker]); const toolbarItems: ToolbarItem[] = useMemo(() => { const ret: ToolbarItem[] = []; diff --git a/gui/src/app/prototypes/DataPyPrototype/dataPyWorker.ts b/gui/src/app/prototypes/DataPyPrototype/dataPyWorker.ts deleted file mode 100644 index aba8d609..00000000 --- a/gui/src/app/prototypes/DataPyPrototype/dataPyWorker.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { PyodideInterface, loadPyodide } from "pyodide"; - -let pyodide: PyodideInterface | null = null; -const loadPyodideInstance = async () => { - if (pyodide === null) { - const p = await loadPyodide({ - indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.1/full", - stdout: (x: string) => { - self.postMessage({ type: "stdout", data: x }); - }, - stderr: (x: string) => { - self.postMessage({ type: "stderr", data: x }); - }, - packages: ["numpy", "micropip"] - }); - pyodide = p; - const micropip = pyodide.pyimport("micropip"); - await micropip.install("stanio"); - return pyodide; - } else { - return pyodide; - } -}; - -self.onmessage = (e) => { - const dd = e.data; - if (dd.type === "run") { - run(dd.code); - } -} - -const setStatus = (status: string) => { - self.postMessage({ type: "setStatus", status }); -} - -const setData = (data: any) => { - self.postMessage({ type: "setData", data }); -} - -const run = async (code: string) => { - setStatus("loading"); - try { - const pyodide = await loadPyodideInstance(); - setStatus("running"); - // the runPython call is going to be blocking, so we want to give - // react a chance to update the status in the UI. - await new Promise((resolve) => setTimeout(resolve, 100)); - - // here's where we can pass in globals - const globals = pyodide.toPy({ _sp_example_global: 5 }); - let script = code; - - // We serialize the data object to json string in the python script - script += "\n"; - script += "import stanio\n"; - script += "import json\n"; - script += "data = stanio.dump_stan_json(data)\n"; - pyodide.runPython(script, { globals }); - - // get the data object from the python script - const data = JSON.parse(globals.get("data")); - setData(resultToData(data)); - setStatus("completed"); - } catch (e) { - console.error(e); - setStatus("failed"); - } -}; - -const resultToData = (result: any): any => { - if (result === null || result === undefined) { - return result; - } - if (typeof result !== "object") { - return result; - } - if (result instanceof Map) { - const ret: { [key: string]: any } = {}; - for (const k of result.keys()) { - ret[k] = resultToData(result.get(k)); - } - return ret; - } else if ( - result instanceof Int16Array || - result instanceof Int32Array || - result instanceof Int8Array || - result instanceof Uint16Array || - result instanceof Uint32Array || - result instanceof Uint8Array || - result instanceof Uint8ClampedArray || - result instanceof Float32Array || - result instanceof Float64Array - ) { - return Array.from(result); - } else if (result instanceof Array) { - return result.map(resultToData); - } else { - const ret: { [key: string]: any } = {}; - for (const k of Object.keys(result)) { - ret[k] = resultToData(result[k]); - } - return ret; - } - }; \ No newline at end of file diff --git a/gui/src/app/prototypes/pyodideWorker/pyodideWorker.ts b/gui/src/app/prototypes/pyodideWorker/pyodideWorker.ts new file mode 100644 index 00000000..b596ff1a --- /dev/null +++ b/gui/src/app/prototypes/pyodideWorker/pyodideWorker.ts @@ -0,0 +1,232 @@ +import { PyodideInterface, loadPyodide } from "pyodide"; +import { + MessageFromPyodideWorker, + isMessageToPyodideWorker, + PyodideWorkerMode, + PydodideWorkerStatus, +} from "./pyodideWorkerTypes"; + +let pyodideWorkerMode: PyodideWorkerMode | undefined = undefined; + +let pyodide: PyodideInterface | null = null; +const loadPyodideInstance = async () => { + if (pyodide === null) { + if (!pyodideWorkerMode) { + throw Error("pyodideWorkerMode is not defined"); + } + const packages = + pyodideWorkerMode === "data.py" + ? ["numpy", "micropip"] + : pyodideWorkerMode === "analysis.py" + ? ["numpy", "matplotlib"] + : []; + pyodide = await loadPyodide({ + indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.1/full", + stdout: (x: string) => { + sendStdout(x); + }, + stderr: (x: string) => { + sendStderr(x); + }, + packages, + }); + if (pyodideWorkerMode === "data.py") { + const micropip = pyodide.pyimport("micropip"); + await micropip.install("stanio"); + } + return pyodide; + } else { + return pyodide; + } +}; + +self.onmessage = (e) => { + const message = e.data; + if (!isMessageToPyodideWorker(message)) { + console.error("Invalid message from main", message); + return; + } + if (message.type === "setPyodideWorkerMode") { + if (pyodideWorkerMode !== undefined) { + throw Error("pyodideWorkerMode is already defined"); + } + if (!["data.py", "analysis.py"].includes(message.mode)) { + throw Error("Invalid pyodideWorkerMode"); + } + console.log("---- setting pyodideWorkerMode", message.mode); + pyodideWorkerMode = message.mode; + } else if (message.type === "run") { + run(message.code); + } +}; + +const sendMessageToMain = (message: MessageFromPyodideWorker) => { + self.postMessage(message); +}; + +const sendStdout = (data: string) => { + sendMessageToMain({ type: "stdout", data }); +}; + +const sendStderr = (data: string) => { + sendMessageToMain({ type: "stderr", data }); +}; + +const setStatus = (status: PydodideWorkerStatus) => { + sendMessageToMain({ type: "setStatus", status }); +}; + +const setData = (data: any) => { + if (pyodideWorkerMode !== "data.py") { + throw Error("setData is only supported in data.py mode"); + } + sendMessageToMain({ type: "setData", data }); +}; + +const addImage = (image: any) => { + if (pyodideWorkerMode !== "analysis.py") { + throw Error("addImage is only supported in analysis.py mode"); + } + sendMessageToMain({ type: "addImage", image }); +}; + +const run = async (code: string) => { + if (!pyodideWorkerMode) { + throw Error("pyodideWorkerMode is not defined"); + } + setStatus("loading"); + try { + const pyodide = await loadPyodideInstance(); + setStatus("running"); + + const scriptPreamble = getScriptPreable(pyodideWorkerMode); + + // here's where we can pass in globals + const globals = pyodide.toPy({ _stan_playground: true }); + let script = scriptPreamble + "\n" + code; + + if (pyodideWorkerMode === "data.py") { + // We serialize the data object to json string in the python script + script += "\n"; + script += "import stanio\n"; + script += "import json\n"; + script += "data = stanio.dump_stan_json(data)\n"; + } + + let succeeded = false; + try { + if (script.includes("arviz")) { + // If the script has arviz, we need to install it + setStatus("loading"); + try { + await pyodide.loadPackage("micropip"); + const microPip = pyodide.pyimport("micropip"); + await microPip.install("arviz<0.18"); + } finally { + setStatus("running"); + } + } + console.log("--- running script", script); + pyodide.runPython(script, { globals }); + succeeded = true; + } catch (e: any) { + console.error(e); + sendStderr(e.toString()); + } + + if (pyodideWorkerMode === "analysis.py") { + const images = globals.get("SP_IMAGES").toJs(); + if (!isListOfStrings(images)) { + throw new Error("Expected SP_IMAGES to be a list of strings"); + } + for (const image of images) { + addImage(image); + } + } else if (pyodideWorkerMode === "data.py") { + // get the data object from the python script + const data = JSON.parse(globals.get("data")); + setData(resultToData(data)); + } + setStatus(succeeded ? "completed" : "failed"); + } catch (e: any) { + console.error(e); + sendStderr("UNEXPECTED ERROR: " + e.toString()); + setStatus("failed"); + } +}; + +const resultToData = (result: any): any => { + if (pyodideWorkerMode !== "data.py") { + throw Error("resultToData is only supported in data.py mode"); + } + if (result === null || result === undefined) { + return result; + } + if (typeof result !== "object") { + return result; + } + if (result instanceof Map) { + const ret: { [key: string]: any } = {}; + for (const k of result.keys()) { + ret[k] = resultToData(result.get(k)); + } + return ret; + } else if ( + result instanceof Int16Array || + result instanceof Int32Array || + result instanceof Int8Array || + result instanceof Uint16Array || + result instanceof Uint32Array || + result instanceof Uint8Array || + result instanceof Uint8ClampedArray || + result instanceof Float32Array || + result instanceof Float64Array + ) { + return Array.from(result); + } else if (result instanceof Array) { + return result.map(resultToData); + } else { + const ret: { [key: string]: any } = {}; + for (const k of Object.keys(result)) { + ret[k] = resultToData(result[k]); + } + return ret; + } +}; + +const getScriptPreable = (mode: PyodideWorkerMode): string => { + if (mode === "analysis.py") { + // see https://github.com/pyodide/matplotlib-pyodide/issues/6#issuecomment-1242747625 + // replace show() with a function that base64 encodes the image and then stashes it for us + + return `SP_IMAGES = [] +print('---- 0') +def patch_matplotlib(SP_IMAGES): + print('--- 1') + import os + os.environ['MPLBACKEND'] = 'AGG' + import base64 + from io import BytesIO + import matplotlib.pyplot + _old_show = matplotlib.pyplot.show + def show(): + buf = BytesIO() + matplotlib.pyplot.savefig(buf, format='png') + buf.seek(0) + # encode to a base64 str + SP_IMAGES.append(base64.b64encode(buf.read()).decode('utf-8')) + matplotlib.pyplot.clf() + matplotlib.pyplot.show = show +print('--- a') +patch_matplotlib(SP_IMAGES) +print('--- b') +`; + } else { + return ""; + } +}; + +const isListOfStrings = (x: any): x is string[] => { + if (!x) return false; + return Array.isArray(x) && x.every((y) => typeof y === "string"); +}; diff --git a/gui/src/app/prototypes/pyodideWorker/pyodideWorkerInterface.ts b/gui/src/app/prototypes/pyodideWorker/pyodideWorkerInterface.ts new file mode 100644 index 00000000..693c97b7 --- /dev/null +++ b/gui/src/app/prototypes/pyodideWorker/pyodideWorkerInterface.ts @@ -0,0 +1,86 @@ +// https://vitejs.dev/guide/assets#importing-script-as-a-worker +// https://vitejs.dev/guide/assets#importing-asset-as-url +import pyodideWorkerURL from "./pyodideWorker?worker&url"; +import { + MessageToPyodideWorker, + PydodideWorkerStatus, + PyodideWorkerMode, + isMessageFromPyodideWorker, +} from "./pyodideWorkerTypes"; + +class PyodideWorkerInterface { + constructor( + private _worker: Worker, + private _mode: PyodideWorkerMode, + ) { + // do not call this directly, use create() instead + } + static create( + mode: PyodideWorkerMode, + callbacks: { + onStdout: (data: string) => void; + onStderr: (data: string) => void; + onStatus: (status: PydodideWorkerStatus) => void; + onData?: (data: any) => void; + onImage?: (image: string) => void; + }, + ) { + const worker = new Worker(pyodideWorkerURL, { + name: "pyodideWorker_" + mode, + type: "module", + }); + const msg: MessageToPyodideWorker = { + type: "setPyodideWorkerMode", + mode, + }; + worker.onmessage = (e: MessageEvent) => { + const msg = e.data; + if (!isMessageFromPyodideWorker(msg)) { + console.error("invalid message from worker", msg); + return; + } else if (msg.type === "setStatus") { + callbacks.onStatus(msg.status); + } else if (msg.type === "stdout") { + callbacks.onStdout(msg.data); + } else if (msg.type === "stderr") { + callbacks.onStderr(msg.data); + } else if (msg.type === "setData") { + if (mode !== "data.py") { + console.error("setData is only supported in data.py mode"); + return; + } + if (!callbacks.onData) { + console.error("onData callback is required for data.py mode"); + return; + } + callbacks.onData(msg.data); + } else if (msg.type === "addImage") { + if (mode !== "analysis.py") { + console.error("addImage is only supported in analysis.py mode"); + return; + } + if (!callbacks.onImage) { + console.error("onImage callback is required for analysis.py mode"); + return; + } + callbacks.onImage(msg.image); + } + }; + worker.postMessage(msg); + return new PyodideWorkerInterface(worker, mode); + } + run(code: string) { + const msg: MessageToPyodideWorker = { + type: "run", + code, + }; + this._worker.postMessage(msg); + } + + destroy() { + console.log(`terminating ${this._mode} worker`); + this._worker.terminate(); + } +} + +export default PyodideWorkerInterface; diff --git a/gui/src/app/prototypes/pyodideWorker/pyodideWorkerTypes.ts b/gui/src/app/prototypes/pyodideWorker/pyodideWorkerTypes.ts new file mode 100644 index 00000000..07ba0054 --- /dev/null +++ b/gui/src/app/prototypes/pyodideWorker/pyodideWorkerTypes.ts @@ -0,0 +1,64 @@ +export type PyodideWorkerMode = "data.py" | "analysis.py"; + +export type MessageToPyodideWorker = + | { + type: "setPyodideWorkerMode"; + mode: PyodideWorkerMode; + } + | { + type: "run"; + code: string; + }; + +export const isMessageToPyodideWorker = ( + x: any, +): x is MessageToPyodideWorker => { + if (!x) return false; + if (typeof x !== "object") return false; + if (x.type === "setPyodideWorkerMode") + return ["data.py", "analysis.py"].includes(x.mode); + if (x.type === "run") return x.code !== undefined; + return false; +}; + +export type MessageFromPyodideWorker = + | { + type: "stdout" | "stderr"; + data: string; + } + | { + type: "setStatus"; + status: PydodideWorkerStatus; + } + | { + type: "setData"; // for data.py mode + data: any; + } + | { + type: "addImage"; // for analysis.py mode + image: any; + }; + +export const isMessageFromPyodideWorker = ( + x: any, +): x is MessageFromPyodideWorker => { + if (!x) return false; + if (typeof x !== "object") return false; + if (x.type === "stdout") return x.data !== undefined; + if (x.type === "stderr") return x.data !== undefined; + if (x.type === "setStatus") return isPydodideWorkerStatus(x.status); + if (x.type === "setData") return x.data !== undefined; + if (x.type === "addImage") return x.image !== undefined; + return false; +}; + +export type PydodideWorkerStatus = + | "idle" + | "loading" + | "running" + | "completed" + | "failed"; + +export const isPydodideWorkerStatus = (x: any): x is PydodideWorkerStatus => { + return ["idle", "loading", "running", "completed", "failed"].includes(x); +};