103 lines
4.8 KiB
HTML
103 lines
4.8 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>MoQ shared world client</title>
|
||
<link rel="icon" href="data:,">
|
||
<style>
|
||
:root { --bg: #f6f7f9; --fg: #15171a; --muted: #5d6470; --panel: #ffffff; --line: #d9dde3; }
|
||
@media (prefers-color-scheme: dark) { :root { --bg: #111317; --fg: #e8eaee; --muted: #9aa2ae; --panel: #1a1d23; --line: #2c313a; } }
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; padding: 16px; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
|
||
h1 { font-size: 16px; margin: 0 0 12px; }
|
||
section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 10px; margin-bottom: 12px; min-width: 0; }
|
||
.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||
dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 12px; margin: 0; font-size: 13px; }
|
||
dt { color: var(--muted); }
|
||
dd { margin: 0; font-family: ui-monospace, monospace; overflow-wrap: anywhere; }
|
||
#status { color: var(--muted); }
|
||
#log { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); max-height: 260px; overflow: auto; margin: 0; overflow-wrap: anywhere; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!--
|
||
The reference client of a room's shared world (WAI extensions/live-worlds.md), over a room
|
||
service: ?service=<the service's path or URL>[&role=viewer][&ws=<the relay's WebSocket URL>].
|
||
Enter takes an actor's grant for a key this page makes; each arrow moves this actor by one,
|
||
as a signed action published on its world track. Everything a test reads is on
|
||
window.worldTest.
|
||
-->
|
||
<h1>Shared world</h1>
|
||
<section class="controls">
|
||
<button id="enter" type="button">Enter</button>
|
||
<button type="button" data-move="-1,0" disabled>←</button>
|
||
<button type="button" data-move="1,0" disabled>→</button>
|
||
<button type="button" data-move="0,-1" disabled>↑</button>
|
||
<button type="button" data-move="0,1" disabled>↓</button>
|
||
<button id="leave" type="button" disabled>Leave</button>
|
||
<span id="status" role="status">Not in the room.</span>
|
||
</section>
|
||
<section>
|
||
<dl>
|
||
<dt>World</dt><dd id="kind">—</dd>
|
||
<dt>You</dt><dd id="me">—</dd>
|
||
<dt>Ops</dt><dd id="ops">0</dd>
|
||
<dt>Session hash</dt><dd id="hash">—</dd>
|
||
<dt>Receipts verified</dt><dd id="verified">0</dd>
|
||
<dt>Actors</dt><dd id="actors">—</dd>
|
||
</dl>
|
||
</section>
|
||
<section>
|
||
<pre id="log"></pre>
|
||
</section>
|
||
<script type="module">
|
||
import { WorldClient } from "./world.js";
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const q = new URLSearchParams(location.search);
|
||
const service = q.get("service") || "/room-api";
|
||
const ws = q.get("ws") || `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/moq`;
|
||
const lines = [];
|
||
const log = (m) => {
|
||
const l = `${(performance.now() / 1000).toFixed(3)} ${m}`;
|
||
lines.push(l);
|
||
if (lines.length > 2000) lines.shift();
|
||
$("log").textContent += l + "\n";
|
||
console.log(l);
|
||
};
|
||
const render = () => {
|
||
const s = client.snapshot();
|
||
$("kind").textContent = s.room ? `${s.private ? "private" : "public"}, ${s.statement.capability} ${s.statement.width}×${s.statement.height}` : "—";
|
||
$("me").textContent = s.me ? s.me.slice(0, 16) + "…" : "—";
|
||
$("ops").textContent = String(s.ops);
|
||
$("hash").textContent = s.session_hash || "—";
|
||
$("verified").textContent = String(s.stats.verified);
|
||
$("actors").textContent = Object.entries(s.by_actor).map(([k, n]) => `${k.slice(0, 8)}: ${n}`).join(", ") || "—";
|
||
};
|
||
const client = new WorldClient({ service, ws, log, role: q.get("role") || "publisher", onChange: () => render() });
|
||
const fail = (e) => { client.fail(e); $("status").textContent = `Something went wrong: ${e.message || e}`; };
|
||
$("enter").addEventListener("click", async () => {
|
||
$("enter").disabled = true;
|
||
try {
|
||
const phase = await client.enter();
|
||
$("status").textContent = phase === "full" ? "The room is full." : phase === "acting" ? "In the room: each arrow is a signed action." : "Watching the world.";
|
||
for (const b of document.querySelectorAll("[data-move]")) b.disabled = phase !== "acting";
|
||
$("leave").disabled = false;
|
||
} catch (e) { fail(e); }
|
||
});
|
||
for (const b of document.querySelectorAll("[data-move]")) {
|
||
b.addEventListener("click", () => { const [dx, dy] = b.dataset.move.split(",").map(Number); client.act(dx, dy); });
|
||
}
|
||
$("leave").addEventListener("click", () => { client.leave(); $("status").textContent = "Left the room."; });
|
||
window.worldTest = {
|
||
client, lines,
|
||
enter: () => client.enter(),
|
||
act: (dx, dy) => client.act(dx, dy),
|
||
flushed: () => client.sending,
|
||
leave: () => client.leave(),
|
||
snapshot: () => client.snapshot(),
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|