gwchq-textjam 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -133,6 +133,8 @@ const PyodideWorker = () => {
133
133
  let useSyncXhrStdin = false;
134
134
  /** True when current input request was cancelled from outside the worker. */
135
135
  let stdinCancelled = false;
136
+ /** True when the sync-XHR stdin transport failed (SW unreachable / request escaped). */
137
+ let stdinTransportFailed = false;
136
138
  /** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
137
139
  let pendingStdinResolve = null;
138
140
  // Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
@@ -185,6 +187,7 @@ const PyodideWorker = () => {
185
187
  case "runPython":
186
188
  currentRunId = data.runId || null;
187
189
  stdinCancelled = false;
190
+ stdinTransportFailed = false;
188
191
  runPython(data.python, data.userModuleNames);
189
192
  break;
190
193
  case "stopPython": {
@@ -238,10 +241,7 @@ const PyodideWorker = () => {
238
241
  await runUserCode();
239
242
  });
240
243
  } catch (error) {
241
- const isStdinControlError =
242
- error?.message === "PYODIDE_STDIN_CANCELLED" ||
243
- error?.message === "PYODIDE_STDIN_TIMEOUT" ||
244
- error?.message === "PYODIDE_STDIN_ABORTED";
244
+ const isStdinControlError = error?.message === "PYODIDE_STDIN_CANCELLED";
245
245
 
246
246
  if (stdinCancelled || isStdinControlError) {
247
247
  postMessage({
@@ -256,6 +256,19 @@ const PyodideWorker = () => {
256
256
  return;
257
257
  }
258
258
 
259
+ if (stdinTransportFailed) {
260
+ postMessage({
261
+ method: "handleError",
262
+ file: "main.py",
263
+ line: "",
264
+ mistake: "",
265
+ type: "InputUnavailableError",
266
+ info: "Input timed out. Rerun your code and try again.",
267
+ });
268
+ await clearPyodideData(userModuleNames);
269
+ return;
270
+ }
271
+
259
272
  if (!(error instanceof pyodide.ffi.PythonError)) {
260
273
  postMessage({
261
274
  method: "handleError",
@@ -691,59 +704,64 @@ const PyodideWorker = () => {
691
704
  };
692
705
 
693
706
  const readInputViaSyncXhr = (runId) => {
694
- const requestId = crypto.randomUUID();
695
- const requestUrl = new URL(
696
- stdinFallbackConfig.endpointPath,
697
- // eslint-disable-next-line no-restricted-globals
698
- self.location.origin,
699
- );
700
- requestUrl.searchParams.set("runId", runId || "");
701
- requestUrl.searchParams.set("requestId", requestId);
702
- requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
703
-
704
- const xhr = new XMLHttpRequest();
705
- xhr.open("GET", requestUrl.toString(), false);
706
- xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
707
- xhr.setRequestHeader("Cache-Control", "no-store");
708
-
707
+ // Mark any failure with XHR setup as stdinTransportFailed for a clean error
709
708
  try {
709
+ const requestId = crypto.randomUUID();
710
+ const requestUrl = new URL(
711
+ stdinFallbackConfig.endpointPath,
712
+ // eslint-disable-next-line no-restricted-globals
713
+ self.location.origin,
714
+ );
715
+ requestUrl.searchParams.set("runId", runId || "");
716
+ requestUrl.searchParams.set("requestId", requestId);
717
+ requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
718
+
719
+ const xhr = new XMLHttpRequest();
720
+ xhr.open("GET", requestUrl.toString(), false);
721
+ xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
722
+ xhr.setRequestHeader("Cache-Control", "no-store");
710
723
  xhr.send(null);
711
- } catch (_) {
712
- throw new Error("Failed to read Python input via stdin fallback");
713
- }
714
724
 
715
- if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
716
- stdinCancelled = true;
717
- throw new Error("PYODIDE_STDIN_CANCELLED");
718
- }
725
+ if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
726
+ stdinCancelled = true;
727
+ throw new Error("PYODIDE_STDIN_CANCELLED");
728
+ }
719
729
 
720
- if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
721
- throw new Error("PYODIDE_STDIN_TIMEOUT");
722
- }
730
+ if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
731
+ stdinTransportFailed = true;
732
+ throw new Error("PYODIDE_STDIN_TIMEOUT");
733
+ }
723
734
 
724
- if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
725
- throw new Error("PYODIDE_STDIN_ABORTED");
726
- }
735
+ if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
736
+ stdinTransportFailed = true;
737
+ throw new Error("PYODIDE_STDIN_ABORTED");
738
+ }
727
739
 
728
- // EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
729
- // Pyodide's line-based stdin handler to raise EOFError at the input() call
730
- // site, rather than escalating to a KeyboardInterrupt that tears down the run.
731
- if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
732
- return null;
733
- }
740
+ // EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
741
+ // Pyodide's line-based stdin handler to raise EOFError at the input() call
742
+ // site, rather than escalating to a KeyboardInterrupt that tears down the run.
743
+ if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
744
+ return null;
745
+ }
734
746
 
735
- if (xhr.status !== 200) {
736
- throw new Error(
737
- `Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
738
- );
739
- }
747
+ if (xhr.status !== 200) {
748
+ throw new Error(
749
+ `Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
750
+ );
751
+ }
740
752
 
741
- let content = xhr.responseText ?? "";
742
- if (!content.endsWith("\n")) {
743
- content += "\n";
744
- }
753
+ let content = xhr.responseText ?? "";
754
+ if (!content.endsWith("\n")) {
755
+ content += "\n";
756
+ }
745
757
 
746
- return content;
758
+ return content;
759
+ } catch (err) {
760
+ if (!stdinCancelled) {
761
+ stdinTransportFailed = true;
762
+ }
763
+ throw err;
764
+ }
747
765
  };
748
766
 
749
767
  const clearPyodideData = async (userModuleNames) => {
package/dist/index.js CHANGED
@@ -370252,18 +370252,18 @@ const useHtmlRunner = () => {
370252
370252
  };
370253
370253
  }, [shouldUseBrowserHistory]);
370254
370254
  (0, react_1.useEffect)(() => {
370255
- if (isPreviewMode) {
370256
- const handleMessage = (event) => {
370257
- if (event.data.type === BroadcastMessageType.RELOAD_PROJECT) {
370258
- // Trigger a reload of a preview page
370259
- window.location.reload();
370260
- }
370261
- };
370262
- broadcastChannel.current?.addEventListener("message", handleMessage);
370263
- return () => {
370264
- broadcastChannel.current?.removeEventListener("message", handleMessage);
370265
- };
370266
- }
370255
+ if (!isPreviewMode)
370256
+ return;
370257
+ const handleMessage = (event) => {
370258
+ if (event.data.type === BroadcastMessageType.RELOAD_PROJECT) {
370259
+ // Trigger a reload of a preview page
370260
+ window.location.reload();
370261
+ }
370262
+ };
370263
+ broadcastChannel.current?.addEventListener("message", handleMessage);
370264
+ return () => {
370265
+ broadcastChannel.current?.removeEventListener("message", handleMessage);
370266
+ };
370267
370267
  }, [isPreviewMode]);
370268
370268
  (0, react_1.useEffect)(() => {
370269
370269
  const handleReloadMessage = (event) => {
@@ -370315,15 +370315,15 @@ const useHtmlRunner = () => {
370315
370315
  dispatch((0, EditorSlice_1.setLoadedRunner)(EditorTypes_1.RunnerType.HTML));
370316
370316
  }, []);
370317
370317
  (0, react_1.useEffect)(() => {
370318
- if (codeRunTriggered) {
370319
- // Fresh iframe for each run (clean slate); it persists across view switches
370320
- createIframe();
370321
- runCode();
370322
- if (!isPreviewMode) {
370323
- broadcastChannel.current?.postMessage({
370324
- type: BroadcastMessageType.RELOAD_PROJECT,
370325
- });
370326
- }
370318
+ if (!codeRunTriggered)
370319
+ return;
370320
+ // Fresh iframe for each run (clean slate); it persists across view switches
370321
+ createIframe();
370322
+ runCode();
370323
+ if (!isPreviewMode) {
370324
+ broadcastChannel.current?.postMessage({
370325
+ type: BroadcastMessageType.RELOAD_PROJECT,
370326
+ });
370327
370327
  }
370328
370328
  }, [codeRunTriggered, page, isPreviewMode]);
370329
370329
  (0, react_1.useEffect)(() => {
@@ -370537,7 +370537,7 @@ const useIframeHost = ({ codeHasBeenRun, error, page, isPreviewMode, onIframeLoa
370537
370537
  slotRef.current = node;
370538
370538
  setSlotVersion((v) => v + 1);
370539
370539
  }, []);
370540
- // Create the persistent host + iframe once; tear it down on unmount
370540
+ // Create the persistent host + iframe once; tear it down on unmount.
370541
370541
  (0, react_1.useEffect)(() => {
370542
370542
  if (typeof document === "undefined")
370543
370543
  return;
@@ -370555,6 +370555,7 @@ const useIframeHost = ({ codeHasBeenRun, error, page, isPreviewMode, onIframeLoa
370555
370555
  document.body.removeChild(host);
370556
370556
  hostRef.current = null;
370557
370557
  output.current = null;
370558
+ setHostReady(false);
370558
370559
  };
370559
370560
  }, [createIframe]);
370560
370561
  // Keep the host aligned with the active slot across resizes/scrolls/switches
@@ -370902,7 +370903,7 @@ const styles_module_scss_1 = __importDefault(__webpack_require__(12914));
370902
370903
  * The React component itself stays free of any worker plumbing — it just
370903
370904
  * consumes the values returned here to drive its JSX.
370904
370905
  */
370905
- const usePyodideRunner = ({ active, packageApiUrl, }) => {
370906
+ const usePyodideRunner = ({ packageApiUrl, }) => {
370906
370907
  const dispatch = (0, react_redux_1.useDispatch)();
370907
370908
  // -------------------------------------------------------------------------
370908
370909
  // Redux selectors
@@ -371067,7 +371068,11 @@ const usePyodideRunner = ({ active, packageApiUrl, }) => {
371067
371068
  const handleError = (0, react_1.useCallback)((file, line, mistake, type, info) => {
371068
371069
  let errorMessage;
371069
371070
  if (type === "KeyboardInterrupt") {
371070
- errorMessage = "Execution interrupted";
371071
+ errorMessage = info || "Execution interrupted";
371072
+ }
371073
+ else if (type === "InputUnavailableError") {
371074
+ errorMessage =
371075
+ info || "Input timed out. Rerun your code and try again.";
371071
371076
  }
371072
371077
  else {
371073
371078
  const message = [type, info].filter((s) => s).join(": ");
@@ -371142,7 +371147,6 @@ const usePyodideRunner = ({ active, packageApiUrl, }) => {
371142
371147
  handleVisual,
371143
371148
  ]);
371144
371149
  const { worker: pyodideWorker, recreateWorker } = (0, usePyodideWorker_1.usePyodideWorker)({
371145
- active,
371146
371150
  projectIdentifier: project.identifier,
371147
371151
  onMessage: handleMessage,
371148
371152
  onBeforeRecreate: handleBeforeRecreate,
@@ -371170,7 +371174,6 @@ const usePyodideRunner = ({ active, packageApiUrl, }) => {
371170
371174
  recreateWorker();
371171
371175
  }, [recreateWorker]);
371172
371176
  const { ensureInputServiceWorker, registerStdinClient } = (0, useStdinServiceWorker_1.useStdinServiceWorker)({
371173
- active,
371174
371177
  tabIdRef,
371175
371178
  onStdinRequest: handleStdinRequest,
371176
371179
  onControllerChange: handleControllerChange,
@@ -371334,13 +371337,13 @@ const usePyodideRunner = ({ active, packageApiUrl, }) => {
371334
371337
  }, [pyodideWorker, ensureInputServiceWorker, packageApiUrl]);
371335
371338
  // Kick off a run when the Redux flag flips on.
371336
371339
  (0, react_1.useEffect)(() => {
371337
- if (codeRunTriggered && active && output.current) {
371340
+ if (codeRunTriggered && output.current) {
371338
371341
  handleRun();
371339
371342
  }
371340
371343
  }, [codeRunTriggered, output.current]);
371341
371344
  // Stop a run when the Redux flag flips on.
371342
371345
  (0, react_1.useEffect)(() => {
371343
- if (codeRunStopped && active) {
371346
+ if (codeRunStopped) {
371344
371347
  handleStop();
371345
371348
  }
371346
371349
  }, [codeRunStopped]);
@@ -371369,7 +371372,7 @@ const assetUrls_1 = __webpack_require__(85799);
371369
371372
  * Owns the lifecycle of the Pyodide Web Worker.
371370
371373
  *
371371
371374
  * Responsibilities:
371372
- * - Create the worker once the runner becomes active.
371375
+ * - Create the worker on mount.
371373
371376
  * - Re-create the worker whenever the project identifier changes (each
371374
371377
  * project starts from a clean Python environment).
371375
371378
  * - Expose an imperative `recreateWorker` for callers that need to force a
@@ -371379,7 +371382,7 @@ const assetUrls_1 = __webpack_require__(85799);
371379
371382
  * Posting messages and decoding incoming messages is the orchestrator's job —
371380
371383
  * this hook just hands the worker over and forwards messages.
371381
371384
  */
371382
- const usePyodideWorker = ({ active, projectIdentifier, onMessage, onBeforeRecreate, }) => {
371385
+ const usePyodideWorker = ({ projectIdentifier, onMessage, onBeforeRecreate, }) => {
371383
371386
  const [worker, setWorker] = (0, react_1.useState)(null);
371384
371387
  // The onMessage callback closes over orchestrator state, so we route through
371385
371388
  // a ref to avoid rebuilding the worker every render.
@@ -371407,22 +371410,14 @@ const usePyodideWorker = ({ active, projectIdentifier, onMessage, onBeforeRecrea
371407
371410
  return createWorker();
371408
371411
  });
371409
371412
  }, [createWorker]);
371410
- // First-mount creation: only when active and no worker exists yet.
371413
+ // First-mount creation: only when no worker exists yet.
371411
371414
  (0, react_1.useEffect)(() => {
371412
371415
  if (worker)
371413
371416
  return;
371414
- if (!active)
371415
- return;
371416
371417
  setWorker(createWorker());
371417
- }, [active, worker, createWorker]);
371418
- // Project changes always reset the worker — but only when this runner is
371419
- // the active one. Without this gate the hook would spin up a Pyodide
371420
- // worker for HTML projects too, since the runner factory now calls
371421
- // usePythonRunner unconditionally and only skips its side effects via
371422
- // `active`.
371418
+ }, [worker, createWorker]);
371419
+ // Project changes always reset the worker.
371423
371420
  (0, react_1.useEffect)(() => {
371424
- if (!active)
371425
- return;
371426
371421
  recreateWorker();
371427
371422
  // eslint-disable-next-line react-hooks/exhaustive-deps
371428
371423
  }, [projectIdentifier]);
@@ -371462,7 +371457,7 @@ const serviceWorker_1 = __webpack_require__(58158);
371462
371457
  * Returns helpers (`ensureInputServiceWorker`, `registerStdinClient`) the
371463
371458
  * orchestrator calls from `run` and from `init`.
371464
371459
  */
371465
- const useStdinServiceWorker = ({ active, tabIdRef, onStdinRequest, onControllerChange, }) => {
371460
+ const useStdinServiceWorker = ({ tabIdRef, onStdinRequest, onControllerChange, }) => {
371466
371461
  // Keep latest callbacks in refs so the event listener effects don't need to
371467
371462
  // re-subscribe whenever the parent re-renders with a new closure.
371468
371463
  const onStdinRequestRef = (0, react_1.useRef)(onStdinRequest);
@@ -371566,9 +371561,6 @@ const useStdinServiceWorker = ({ active, tabIdRef, onStdinRequest, onControllerC
371566
371561
  // Re-register stdin client and notify the orchestrator whenever the page
371567
371562
  // gains a new SW controller (e.g. after first registration or a SW update).
371568
371563
  (0, react_1.useEffect)(() => {
371569
- if (!active) {
371570
- return;
371571
- }
371572
371564
  if (!("serviceWorker" in navigator)) {
371573
371565
  return;
371574
371566
  }
@@ -371583,7 +371575,7 @@ const useStdinServiceWorker = ({ active, tabIdRef, onStdinRequest, onControllerC
371583
371575
  return () => {
371584
371576
  navigator.serviceWorker.removeEventListener("controllerchange", handleControllerChange);
371585
371577
  };
371586
- }, [active, registerStdinClient]);
371578
+ }, [registerStdinClient]);
371587
371579
  return { ensureInputServiceWorker, registerStdinClient };
371588
371580
  };
371589
371581
  exports.useStdinServiceWorker = useStdinServiceWorker;
@@ -372045,18 +372037,17 @@ const styles_module_scss_1 = __importDefault(__webpack_require__(12914));
372045
372037
  const VisualOutputPane_1 = __importDefault(__webpack_require__(39626));
372046
372038
  const usePyodideRunner_1 = __webpack_require__(65587);
372047
372039
  const consoleInput_1 = __webpack_require__(35660);
372048
- const usePythonRunner = ({ active, packageApiUrl, }) => {
372040
+ const usePythonRunner = ({ packageApiUrl, }) => {
372049
372041
  const dispatch = (0, react_redux_1.useDispatch)();
372050
372042
  const activeRunner = (0, stores_1.useAppSelector)((state) => state.editor.activeRunner);
372051
372043
  const { mountConsole, visuals, setVisuals } = (0, usePyodideRunner_1.usePyodideRunner)({
372052
- active,
372053
372044
  packageApiUrl,
372054
372045
  });
372055
372046
  (0, react_1.useEffect)(() => {
372056
- if (active && activeRunner !== EditorTypes_1.RunnerType.PYODIDE) {
372047
+ if (activeRunner !== EditorTypes_1.RunnerType.PYODIDE) {
372057
372048
  dispatch((0, EditorSlice_1.loadingRunner)(EditorTypes_1.RunnerType.PYODIDE));
372058
372049
  }
372059
- }, [active, activeRunner, dispatch]);
372050
+ }, [activeRunner, dispatch]);
372060
372051
  const renderConsoleOutput = (0, react_1.useCallback)(() => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ErrorMessage_1.default, {}), (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_1.default.consoleMount, onClick: consoleInput_1.shiftFocusToInput, ref: mountConsole })] })), [mountConsole]);
372061
372052
  const renderVisualOutput = (0, react_1.useCallback)(() => ((0, jsx_runtime_1.jsx)(VisualOutputPane_1.default, { visuals: visuals, setVisuals: setVisuals })), [visuals, setVisuals]);
372062
372053
  const outputMeta = {
@@ -372112,9 +372103,9 @@ const ProjectTypes_1 = __webpack_require__(27130);
372112
372103
  const EditorTypes_1 = __webpack_require__(42108);
372113
372104
  const useRunnerFactory = ({ projectType, packageApiUrl, }) => {
372114
372105
  const isWeb = projectType === ProjectTypes_1.ProjectType.WEB;
372115
- const htmlRunner = (0, useHtmlRunner_1.default)();
372116
- const pythonRunner = (0, usePythonRunner_1.default)({ active: !isWeb, packageApiUrl });
372117
- const { renderVisualOutput, renderConsoleOutput, outputMeta, panelOrder } = isWeb ? htmlRunner : pythonRunner;
372106
+ const { renderVisualOutput, renderConsoleOutput, outputMeta, panelOrder } =
372107
+ // eslint-disable-next-line react-hooks/rules-of-hooks
372108
+ isWeb ? (0, useHtmlRunner_1.default)() : (0, usePythonRunner_1.default)({ packageApiUrl });
372118
372109
  // The output-type → renderer mapping is identical across runners, so compose
372119
372110
  // the public renderOutput here rather than duplicating it in each hook.
372120
372111
  const renderOutput = (0, react_1.useCallback)((outputType) => outputType === EditorTypes_1.OutputType.VISUAL
@@ -385732,7 +385723,7 @@ module.exports = webpackAsyncContext;
385732
385723
  /***/ 24427:
385733
385724
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
385734
385725
 
385735
- module.exports = __webpack_require__.p + "assets/PyodideWorkerb1409749c37aedc47dc8.js";
385726
+ module.exports = __webpack_require__.p + "assets/PyodideWorkereba24d5259c22fbe422a.js";
385736
385727
 
385737
385728
  /***/ }),
385738
385729
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gwchq-textjam",
3
3
  "description": "Embeddable React editor used in Raspberry Pi text-based projects.",
4
- "version": "0.3.6",
4
+ "version": "0.3.7",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/GirlsFirst/gwchq-textjam",
7
7
  "author": "Girls Who Code HQ",