OXYGENHUB
◆ v0.9.15 oxygenhub / assets / wasm-tab.js
3.4 KiB · JavaScript · last changed Run a pipeline's WASM jobs in the browser tab (refs andymccall/oxygenflow#80); runner builds shared with the swarm (refs andymccall/oxygenflow#84); 0.9.15 7 days ago source blame history raw
c26267b3 Andy McCall ↶ 1
// "Run in this tab" (oxygenflow#80): each WASM job of the pipeline in a
2
// worker of its own (wasi-worker.js), over the bookmark's files fetched once
3
// as a zip. A job shows its output as it comes, its exit code and its time.
4
"use strict";
5
(() => {
6
  const root = document.getElementById("tab-runner");
7
  if (!root) return;
8
  const jobs = JSON.parse(root.dataset.jobs);
9
  const status = document.getElementById("tab-status");
10
  const list = document.getElementById("tab-jobs");
11
  let archive = null;
12
  const fetchArchive = () => {
13
    archive ??= fetch(root.dataset.archive, { credentials: "same-origin" }).then((r) => {
14
      if (!r.ok) throw new Error(`the files did not come (HTTP ${r.status})`);
15
      return r.arrayBuffer();
16
    });
17
    return archive;
18
  };
19
  const el = (tag, props, ...kids) => {
20
    const e = Object.assign(document.createElement(tag), props);
21
    e.append(...kids);
22
    return e;
23
  };
24
  const rows = jobs.map((job) => {
25
    const state = el("span", { className: "small mute", textContent: "not run" });
26
    const out = el("pre", { className: "snippet", hidden: true });
27
    const run = el("button", { type: "button", textContent: "Run" });
28
    const stop = el("button", { type: "button", textContent: "Stop", hidden: true });
29
    const box = el(
30
      "div",
31
      { className: "tab-job" },
32
      el("h3", {}, job.name, " ", state),
33
      el("p", { className: "small mute" }, el("code", { textContent: job.exec.join(" ") })),
34
      el("p", {}, run, " ", stop),
35
      out,
36
    );
37
    list.append(box);
38
    let worker = null;
39
    const line = (cls, text) => {
40
      out.hidden = false;
41
      out.append(el("span", { className: cls, textContent: text + "\n" }));
42
      out.scrollTop = out.scrollHeight;
43
    };
44
    const go = () =>
45
      new Promise((resolve) => {
46
        out.replaceChildren();
47
        state.textContent = "fetching the files…";
48
        state.className = "small mute";
49
        run.disabled = true;
50
        stop.hidden = false;
51
        const finish = (text, cls, ok) => {
52
          state.textContent = text;
53
          state.className = "small " + cls;
54
          run.disabled = false;
55
          stop.hidden = true;
56
          worker?.terminate();
57
          worker = null;
58
          resolve(ok);
59
        };
60
        stop.onclick = () => finish("stopped", "bad", false);
61
        fetchArchive()
62
          .then((buf) => {
63
            state.textContent = "running…";
64
            worker = new Worker(root.dataset.worker);
65
            worker.onmessage = ({ data: m }) => {
66
              if (m.kind === "done") finish(m.code === 0 ? `passed in ${m.ms} ms` : `failed, exit ${m.code}, in ${m.ms} ms`, m.code === 0 ? "ok" : "bad", m.code === 0);
67
              else if (m.kind === "failed") finish(`could not run: ${m.text}`, "bad", false);
68
              else line(m.kind === "out" ? "" : m.kind === "err" ? "bad" : "mute", m.text);
69
            };
70
            worker.onerror = (e) => finish(`the worker stopped: ${e.message}`, "bad", false);
71
            worker.postMessage({ archive: buf.slice(0), exec: job.exec, env: job.env });
72
          })
73
          .catch((e) => finish(e.message, "bad", false));
74
      });
75
    run.onclick = () => go();
76
    return go;
77
  });
78
  document.getElementById("tab-run-all").onclick = async (e) => {
79
    e.target.disabled = true;
80
    let passed = 0;
81
    for (const go of rows) if (await go()) passed += 1;
82
    status.textContent = `${passed} of ${rows.length} passed`;
83
    e.target.disabled = false;
84
  };
85
})();