159 lines
5.5 KiB
HTML
159 lines
5.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>swactor · ping-pong</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117; --surface: #161b22; --border: #30363d;
|
|
--text: #c9d1d9; --dim: #8b949e; --accent: #58a6ff;
|
|
--green: #3fb950; --yellow: #d29922; --purple: #bc8cff;
|
|
--font: 'SF Mono', 'Cascadia Code', 'Fira Code', ui-monospace, monospace;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0; padding: 32px 20px;
|
|
font-family: var(--font); background: var(--bg); color: var(--text);
|
|
line-height: 1.5;
|
|
}
|
|
h1 { font-size: 1.5em; margin: 0 0 2px; }
|
|
h1 span { color: var(--accent); }
|
|
.subtitle { color: var(--dim); font-size: 0.85em; margin-bottom: 24px; }
|
|
.card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: 8px; padding: 20px; max-width: 640px; margin: 0 auto;
|
|
}
|
|
.controls { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
|
|
label { color: var(--dim); font-size: 0.8em; }
|
|
input {
|
|
background: var(--bg); border: 1px solid var(--border); color: var(--text);
|
|
padding: 6px 10px; border-radius: 4px; font-family: var(--font);
|
|
font-size: 0.85em; width: 72px;
|
|
}
|
|
input:focus { outline: none; border-color: var(--accent); }
|
|
.btn {
|
|
padding: 6px 16px; border: 1px solid var(--border); background: var(--surface);
|
|
color: var(--text); border-radius: 4px; cursor: pointer; font-family: var(--font);
|
|
font-size: 0.85em; transition: 0.15s;
|
|
}
|
|
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
.btn:active:not(:disabled) { transform: scale(0.97); }
|
|
.btn:disabled { opacity: 0.45; cursor: default; }
|
|
#status { color: var(--dim); font-size: 0.8em; margin-left: auto; }
|
|
#status .v { color: var(--green); font-weight: bold; }
|
|
#log {
|
|
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
padding: 12px; font-size: 0.85em; line-height: 1.7; height: 360px;
|
|
overflow-y: auto; white-space: pre;
|
|
}
|
|
.log-line { animation: fadeIn 0.2s ease; }
|
|
.log-line.ping .side { color: var(--accent); }
|
|
.log-line.pong .side { color: var(--purple); }
|
|
.log-line.done .side, .log-line.done { color: var(--yellow); }
|
|
.log-line.stop { color: var(--dim); }
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
#loading { text-align: center; color: var(--dim); padding: 48px; }
|
|
.err { color: #f85149; }
|
|
.dash { color: var(--accent); text-decoration: none; border-bottom: 1px dotted var(--accent); }
|
|
.dash:hover { color: var(--green); border-color: var(--green); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1><span>swactor</span> · ping-pong</h1>
|
|
<p class="subtitle">
|
|
Two actors volley a ball on a WebAssembly actor runtime. The browser drives it
|
|
one <code>tick()</code> at a time; each line below is one actor message.<br>
|
|
<a class="dash" href="http://localhost:9090/" target="_blank" rel="noopener">swactor dashboard ↗</a>
|
|
— live actor stats, served alongside this page.
|
|
</p>
|
|
|
|
<div class="card">
|
|
<div class="controls">
|
|
<label for="volleys">volleys</label>
|
|
<input id="volleys" type="number" min="2" max="999" value="10">
|
|
<button id="serve" class="btn">Serve</button>
|
|
<span id="status"></span>
|
|
</div>
|
|
<div id="log"></div>
|
|
</div>
|
|
|
|
<div id="loading">Loading wasm…</div>
|
|
|
|
<script type="module">
|
|
import init, { App } from "./pkg-web/pingpong.js";
|
|
|
|
let app;
|
|
try {
|
|
await init();
|
|
app = new App();
|
|
} catch (e) {
|
|
document.getElementById("loading").innerHTML =
|
|
'<span class="err">Failed to load wasm.</span><br><br>' +
|
|
'Serve this directory over HTTP (run <code>./serve.sh</code>), ' +
|
|
'don\'t open the file directly.';
|
|
throw e;
|
|
}
|
|
|
|
document.getElementById("loading").style.display = "none";
|
|
|
|
const logEl = document.getElementById("log");
|
|
const serveBtn = document.getElementById("serve");
|
|
const volleysInput = document.getElementById("volleys");
|
|
const statusEl = document.getElementById("status");
|
|
const TICK_MS = 240;
|
|
|
|
function classify(line) {
|
|
if (line.startsWith("ping")) return "ping";
|
|
if (line.startsWith("pong")) return "pong";
|
|
if (line.startsWith("done")) return "done";
|
|
return "stop";
|
|
}
|
|
|
|
function appendLine(line) {
|
|
const cls = classify(line);
|
|
const div = document.createElement("div");
|
|
div.className = "log-line " + cls;
|
|
// Highlight the side token (ping/pong/done) for readability.
|
|
const side = line.slice(0, 4).trimEnd();
|
|
div.innerHTML = '<span class="side">' + side + "</span>" + line.slice(4);
|
|
logEl.appendChild(div);
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
function setStatus(actors, msgs) {
|
|
statusEl.innerHTML = "actors <span class=\"v\">" + actors + "</span> · messages <span class=\"v\">" + msgs + "</span>";
|
|
}
|
|
|
|
function serve() {
|
|
// Reset for a fresh rally.
|
|
logEl.innerHTML = "";
|
|
app = new App();
|
|
const max = Math.max(2, Number(volleysInput.value) || 10);
|
|
|
|
const log = app.new_log();
|
|
const pong = app.spawn_pong(log.addr(), max);
|
|
app.spawn_ping(pong, log.addr(), max);
|
|
|
|
serveBtn.disabled = true;
|
|
setStatus(app.actor_count(), 0);
|
|
|
|
const iv = setInterval(() => {
|
|
app.tick();
|
|
let line;
|
|
while ((line = log.try_recv())) appendLine(line);
|
|
setStatus(app.actor_count(), app.total_messages());
|
|
if (app.actor_count() === 0) {
|
|
clearInterval(iv);
|
|
serveBtn.disabled = false;
|
|
}
|
|
}, TICK_MS);
|
|
}
|
|
|
|
serveBtn.addEventListener("click", serve);
|
|
// Auto-play the first rally on load.
|
|
serve();
|
|
</script>
|
|
</body>
|
|
</html>
|