-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpy-list-snippet-worker.js
More file actions
124 lines (103 loc) · 3.44 KB
/
py-list-snippet-worker.js
File metadata and controls
124 lines (103 loc) · 3.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Web worker that runs a user-provided Python snippet against a single input
// using Pyodide. The caller supplies the source code, a JSON-serializable
// input value, and the name of the function to invoke.
//
// Note: variables set via `pyodide.globals.set(name, value)` land directly in
// Python's top-level globals namespace. They are NOT accessible as
// `from js import name` — attempting that raises ImportError. We therefore
// reference them by name inside Python.
const PYODIDE_VERSION = "0.29.3";
const PYODIDE_BASE = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
let pyodidePromise = null;
let pyodide = null;
let runnerInstalled = false;
const loadedPackages = new Set();
async function ensurePyodide() {
if (pyodide) return pyodide;
if (!pyodidePromise) {
pyodidePromise = (async () => {
importScripts(`${PYODIDE_BASE}pyodide.js`);
pyodide = await loadPyodide({ indexURL: PYODIDE_BASE });
return pyodide;
})();
}
return pyodidePromise;
}
async function ensurePackages(packages) {
const api = await ensurePyodide();
const missing = (packages || []).filter((name) => !loadedPackages.has(name));
if (missing.length > 0) {
await api.loadPackage(missing);
missing.forEach((name) => loadedPackages.add(name));
}
return api;
}
// Installed once per worker. Uses only names that are set via
// `pyodide.globals.set` — no `from js import` — which is the bug the old
// version tripped on.
const RUNNER_PY = `
import json
def __run_snippet(user_code, user_input_json, function_name):
user_input = json.loads(user_input_json)
ns = {}
exec(user_code, ns)
if function_name not in ns:
raise NameError(
f"Expected a function named {function_name!r} to be defined in the snippet."
)
fn = ns[function_name]
def normalize(value):
if value is None or isinstance(value, (str, int, float, bool)):
return value
if hasattr(value, "tolist"):
value = value.tolist()
if isinstance(value, dict):
return {str(k): normalize(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [normalize(v) for v in value]
return repr(value)
try:
got = fn(user_input)
return json.dumps({"ok": True, "got": normalize(got)})
except Exception as e:
return json.dumps({
"ok": False,
"error": f"{type(e).__name__}: {e}",
})
`;
async function installRunner(api) {
if (runnerInstalled) return;
await api.runPythonAsync(RUNNER_PY);
runnerInstalled = true;
}
self.onmessage = async (event) => {
const {
id,
code,
input,
functionName = "solve",
packages = [],
} = event.data || {};
try {
const api = await ensurePackages(packages);
await installRunner(api);
// Serialize the input as JSON so Python sees native dict/list/int/etc.,
// sidestepping Pyodide's JsProxy wrappers entirely.
api.globals.set("__USER_CODE__", code);
api.globals.set("__USER_INPUT_JSON__", JSON.stringify(input));
api.globals.set("__FUNCTION_NAME__", functionName);
const payloadJson = await api.runPythonAsync(
"__run_snippet(__USER_CODE__, __USER_INPUT_JSON__, __FUNCTION_NAME__)"
);
self.postMessage({
id,
...JSON.parse(payloadJson),
});
} catch (error) {
self.postMessage({
id,
ok: false,
error: String(error?.message || error),
});
}
};