Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.env
.cache

# Local TLS certs/keys (do not commit)
project-331.local.pem
project-331.local-key.pem

*.secret.yml

# Generated by Cargo
Expand Down
8 changes: 6 additions & 2 deletions services/tmc/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const config = {
},
output: "standalone",
outputFileTracingRoot: ".",
webpack(config) {
webpack(config, { isServer }) {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
Expand All @@ -33,7 +33,11 @@ const config = {
svgProps: { role: "presentation" },
},
})

// Pyodide is loaded from CDN at runtime (main thread: script tag; worker: importScripts).
// Do not resolve/bundle the "pyodide" package.
if (!isServer) {
config.resolve.fallback = { ...config.resolve.fallback, pyodide: false }
}
return config
},
turbopack: {
Expand Down
7 changes: 4 additions & 3 deletions services/tmc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "1.0.0",
"packageManager": "pnpm@10.26.2",
"scripts": {
"build": "NODE_ENV=production next build",
"dev": "next dev --port 3005 --turbopack",
"build": "node scripts/inject-pyodide-version.cjs && NODE_ENV=production next build",
"dev": "node scripts/inject-pyodide-version.cjs && next dev --port 3005 --turbopack",
"export": "next export",
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest"
},
Expand Down Expand Up @@ -59,7 +59,8 @@
"use-debounce": "^10.0.6",
"uuid": "^13.0.0",
"workerpool": "^9.3.4",
"zstddec": "^0.1.0"
"zstddec": "^0.1.0",
"pyodide": "^0.29.3"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
Expand Down
20 changes: 20 additions & 0 deletions services/tmc/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions services/tmc/public/browserTestWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Classic Web Worker: runs the inlined Python test script in Pyodide.
* Loads Pyodide from CDN via importScripts, captures stdout, parses JSON RunResult, posts back.
* PYODIDE_INDEX_URL is injected at build time from src/util/pyodide-version.json.
*/
/* global importScripts, loadPyodide */
var PYODIDE_INDEX_URL = "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/"

importScripts(PYODIDE_INDEX_URL + "pyodide.js")

var pyodidePromise = null

function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL })
return pyodidePromise
}
Comment on lines +13 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

pyodidePromise is permanently cached on rejection — worker cannot recover.

Unlike runWorker.js (which resets pyodidePromise = null in a .catch), this getPyodide() permanently holds a rejected promise. Any CDN or load failure makes all subsequent test runs fail immediately with no retry.

🐛 Proposed fix
 function getPyodide() {
   if (pyodidePromise !== null) {
     return pyodidePromise
   }
-  pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL })
-  return pyodidePromise
+  pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL }).catch(function (err) {
+    pyodidePromise = null
+    throw err
+  })
+  return pyodidePromise
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL })
return pyodidePromise
}
function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL }).catch(function (err) {
pyodidePromise = null
throw err
})
return pyodidePromise
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/browserTestWorker.js` around lines 13 - 19, getPyodide()
currently caches a rejected promise in the module-level variable pyodidePromise
so any load failure blocks retries; change the load logic so that when calling
loadPyodide({ indexURL: PYODIDE_INDEX_URL }) you attach a .catch handler that
sets pyodidePromise = null (so subsequent calls retry) and then rethrows the
error; update getPyodide (and the pyodidePromise assignment) to use this
fault-tolerant promise handling while keeping the cached-success behavior.


self.onmessage = function (e) {
var script = e.data.script
getPyodide()
.then(function (pyodide) {
var stdout = ""
var stderr = ""
pyodide.setStdout({
batched: function (msg) {
stdout += msg + "\n"
},
})
pyodide.setStderr({
batched: function (msg) {
stderr += msg + "\n"
},
})
return pyodide.runPythonAsync(script).then(function () {
var lines = stdout.split("\n").filter(function (s) {
return s.trim().length > 0
})
var lastLine = lines.length > 0 ? lines[lines.length - 1] : ""
var runResult = JSON.parse(lastLine)
self.postMessage({ runResult: runResult })
})
})
.catch(function (err) {
var message = err && err.message ? err.message : String(err)
self.postMessage({ error: message })
})
}
156 changes: 156 additions & 0 deletions services/tmc/public/runWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Run worker: executes user Python code in Pyodide with interactive stdin.
* When the program calls input(), the worker posts "stdin_request", waits for
* the main thread to send "stdin_line" via postMessage, then returns that line
* to Python. No SharedArrayBuffer required (works without cross-origin isolation).
* PYODIDE_INDEX_URL is injected at build time from src/util/pyodide-version.json.
*/
/* global importScripts, loadPyodide */
var PYODIDE_INDEX_URL = "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/"

importScripts(PYODIDE_INDEX_URL + "pyodide.js")

var pyodidePromise = null
var pendingStdinResolve = null
var templatePromise = null

function base64EncodeUtf8(str) {
var bytes = new TextEncoder().encode(str)
var binary = ""
for (var i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}

function getTemplate() {
if (templatePromise !== null) {
return templatePromise
}
var templateUrl = new URL("runWorkerTemplate.py", self.location.href).href
templatePromise = fetch(templateUrl)
.then(function (r) {
if (!r.ok) {
templatePromise = null
throw new Error("Template fetch failed: " + r.status + " " + r.statusText)
}
return r.text()
})
.catch(function (err) {
templatePromise = null
throw err
})
return templatePromise
}

function getPyodide() {
if (pyodidePromise !== null) {
return pyodidePromise
}
pyodidePromise = loadPyodide({ indexURL: PYODIDE_INDEX_URL }).catch(function (err) {
pyodidePromise = null
throw err
})
return pyodidePromise
}

self.inputPromise = function (prompt) {
self.postMessage({
type: "stdin_request",
prompt: prompt != null ? String(prompt) : "",
})
return new Promise(function (resolve) {
pendingStdinResolve = resolve
})
}

self.printError = function (message, kind, line, tb) {
var msg = kind + " on line " + line + ": " + message
self.postMessage({ type: "run_error", message: msg, output: stdout })
runHadError = true
}
Comment on lines +67 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pipeline warning: unused tb parameter in printError.

tb (traceback frames) is accepted but never used. Prefix it with _ to silence the linter and signal intent.

💡 Proposed fix
-self.printError = function (message, kind, line, tb) {
+self.printError = function (message, kind, line, _tb) {
🧰 Tools
🪛 GitHub Actions: Code Style

[warning] 67-67: eslint: warning: 'tb' is defined but never used. (no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 67 - 71, The parameter `tb` in
the `self.printError` function is unused; rename it to `_tb` (or prefix it with
an underscore) in the `self.printError = function (message, kind, line, tb)`
signature to silence the linter and signal intent, and ensure no other code
relies on the original `tb` name (update any callers if necessary); do not
otherwise change the function body that constructs `msg`, posts the `run_error`
message, and sets `runHadError`.


var runHadError = false
var stdout = ""
var stderr = ""
Comment on lines +73 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pipeline warning: stderr accumulation buffer is never read.

stderr is collected byte-by-byte (line 125) but the accumulated string is never consumed — neither in self.exit() nor in error messages. The individual chunks are already forwarded in real-time via postMessage({ type: "stderr" }), so the buffer is redundant. Remove the variable and its reset assignments to fix the linter warning and reduce confusion.

💡 Proposed fix
 var runHadError = false
 var stdout = ""
-var stderr = ""
       stdout = ""
-      stderr = ""
       var stdoutDecoder = new TextDecoder("utf-8", { fatal: false })
       var stderrDecoder = new TextDecoder("utf-8", { fatal: false })
           if (chunk.length > 0) {
-            stderr += chunk
             self.postMessage({ type: "stderr", chunk: chunk })
           }
🧰 Tools
🪛 GitHub Actions: Code Style

[warning] 75-75: eslint: warning: 'stderr' is assigned a value but never used. (no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/tmc/public/runWorker.js` around lines 73 - 75, The local
accumulation buffer stderr is declared and appended to but never read; remove
the unused variable declaration "var stderr = \"\"" and any assignments/reset of
stderr (and the code that concatenates into it) so only real-time postMessage({
type: "stderr", ... }) is relied upon; search for the symbol stderr in
runWorker.js and delete its declaration and any writes (but do not change the
postMessage calls or self.exit() behavior).


self.exit = function () {
if (!runHadError) {
self.postMessage({ type: "run_done", output: stdout })
}
}

self.onmessage = function (e) {
var data = e.data

if (data.type === "stdin_line") {
if (pendingStdinResolve) {
var line = data.line != null ? String(data.line) : ""
if (line.length > 0 && line.charAt(line.length - 1) !== "\n") {
line += "\n"
}
pendingStdinResolve(line)
pendingStdinResolve = null
}
return
}

if (data.type !== "run") {
return
}

var script = data.script
runHadError = false
stdout = ""

getPyodide()
.then(function (pyodide) {
stdout = ""
stderr = ""
var stdoutDecoder = new TextDecoder("utf-8", { fatal: false })
var stderrDecoder = new TextDecoder("utf-8", { fatal: false })
pyodide.setStdout({
raw: function (byte) {
var chunk = stdoutDecoder.decode(new Uint8Array([byte]), { stream: true })
if (chunk.length > 0) {
stdout += chunk
self.postMessage({ type: "stdout", chunk: chunk })
}
},
})
pyodide.setStderr({
raw: function (byte) {
var chunk = stderrDecoder.decode(new Uint8Array([byte]), { stream: true })
if (chunk.length > 0) {
stderr += chunk
self.postMessage({ type: "stderr", chunk: chunk })
}
},
})

self.userScriptB64 = base64EncodeUtf8(script)
return getTemplate()
.then(function (template) {
return pyodide.runPythonAsync(template)
})
.then(function () {
var flushOut = stdoutDecoder.decode(new Uint8Array(0), { stream: false })
var flushErr = stderrDecoder.decode(new Uint8Array(0), { stream: false })
if (flushOut.length > 0) {
stdout += flushOut
self.postMessage({ type: "stdout", chunk: flushOut })
}
if (flushErr.length > 0) {
stderr += flushErr
self.postMessage({ type: "stderr", chunk: flushErr })
}
})
})
.then(function () {
/* run_done is posted by exit() when user code finishes */
})
.catch(function (err) {
var message = err && err.message ? err.message : String(err)
self.postMessage({ type: "run_error", message: message })
})
}
Loading
Loading