- NodeAddr abstraction replacing SocketAddr across distribution crate (~30 files) - Core wasm32 compatibility: web-time, cfg-gated threads, wire encoding extraction - WebSocket transport (browser) with GatewayControl protocol - WebSocket gateway (native) with session routing and control protocol - BrowserRuntime API: JS actor support, connect/spawn/send/tick - Dashboard: extracted HTML to static files, gossip protocol panel with membership event log, visual fixes, click-to-filter, worker-colored rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
5.8 KiB
HTML
153 lines
5.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Swactor Browser Runtime</title>
|
|
<style>
|
|
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 2rem; }
|
|
h1 { color: #00d4ff; }
|
|
.panel { background: #16213e; border: 1px solid #0f3460; border-radius: 4px; padding: 1rem; margin: 1rem 0; }
|
|
button { background: #0f3460; color: #e0e0e0; border: 1px solid #00d4ff; padding: 0.5rem 1rem; cursor: pointer; border-radius: 3px; margin: 0.25rem; }
|
|
button:hover { background: #00d4ff; color: #1a1a2e; }
|
|
input { background: #16213e; color: #e0e0e0; border: 1px solid #0f3460; padding: 0.5rem; border-radius: 3px; width: 300px; }
|
|
#log { max-height: 400px; overflow-y: auto; white-space: pre-wrap; font-size: 0.85rem; line-height: 1.4; }
|
|
.info { color: #00d4ff; }
|
|
.sent { color: #e94560; }
|
|
.recv { color: #0cca4a; }
|
|
.error { color: #ff6b6b; }
|
|
.status { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; }
|
|
.status.connected { background: #0cca4a; }
|
|
.status.disconnected { background: #ff6b6b; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Swactor Browser Runtime</h1>
|
|
|
|
<div class="panel">
|
|
<h3>Connection</h3>
|
|
<span id="status-dot" class="status disconnected"></span>
|
|
<span id="status-text">Disconnected</span>
|
|
<br><br>
|
|
<input id="gateway-url" value="ws://localhost:9000" placeholder="Gateway URL">
|
|
<button id="btn-connect">Connect</button>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h3>Actors</h3>
|
|
<button id="btn-spawn">Spawn JS Actor</button>
|
|
<span id="actor-count">Actors: 0</span>
|
|
<div id="actors-list" style="margin-top: 0.5rem; font-size: 0.85rem;"></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h3>Send Message</h3>
|
|
<input id="dest-addr" placeholder="Destination hex address (64 chars)">
|
|
<input id="msg-payload" placeholder="JSON payload" value='{"hello": "world"}'>
|
|
<button id="btn-send">Send</button>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h3>Log</h3>
|
|
<div id="log"></div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
// The wasm-pack output will be at ./pkg/swactor_wasm.js
|
|
// Build with: cd crates/wasm && wasm-pack build --target web
|
|
import init, { BrowserRuntime } from './pkg/swactor_wasm.js';
|
|
|
|
let rt = null;
|
|
const logEl = document.getElementById('log');
|
|
|
|
function log(msg, cls = 'info') {
|
|
const line = document.createElement('div');
|
|
line.className = cls;
|
|
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
|
logEl.appendChild(line);
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
await init();
|
|
log('WASM module loaded');
|
|
|
|
rt = new BrowserRuntime();
|
|
log('BrowserRuntime created');
|
|
|
|
// Tick loop
|
|
setInterval(() => {
|
|
if (rt) {
|
|
rt.tick();
|
|
document.getElementById('actor-count').textContent = `Actors: ${rt.actor_count()}`;
|
|
|
|
const connected = rt.is_connected();
|
|
const dot = document.getElementById('status-dot');
|
|
const text = document.getElementById('status-text');
|
|
dot.className = `status ${connected ? 'connected' : 'disconnected'}`;
|
|
text.textContent = connected ? 'Connected' : 'Disconnected';
|
|
}
|
|
}, 16); // ~60fps
|
|
|
|
} catch (e) {
|
|
log(`Init error: ${e}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Connect button
|
|
document.getElementById('btn-connect').addEventListener('click', () => {
|
|
if (!rt) { log('Runtime not ready', 'error'); return; }
|
|
const url = document.getElementById('gateway-url').value;
|
|
try {
|
|
rt.connect(url);
|
|
log(`Connecting to ${url}...`);
|
|
} catch (e) {
|
|
log(`Connect error: ${e}`, 'error');
|
|
}
|
|
});
|
|
|
|
// Spawn JS actor
|
|
document.getElementById('btn-spawn').addEventListener('click', () => {
|
|
if (!rt) { log('Runtime not ready', 'error'); return; }
|
|
|
|
const addr = rt.spawn_js_actor((ctx, typeTag, msgJson) => {
|
|
log(`Actor ${ctx.self_addr().substring(0, 16)}... received [${typeTag}]: ${msgJson}`, 'recv');
|
|
});
|
|
|
|
log(`Spawned JS actor: ${addr.substring(0, 16)}...`, 'info');
|
|
|
|
const list = document.getElementById('actors-list');
|
|
const item = document.createElement('div');
|
|
item.textContent = addr;
|
|
item.style.cursor = 'pointer';
|
|
item.title = 'Click to copy';
|
|
item.addEventListener('click', () => {
|
|
navigator.clipboard.writeText(addr);
|
|
log(`Copied address to clipboard`);
|
|
});
|
|
list.appendChild(item);
|
|
});
|
|
|
|
// Send message
|
|
document.getElementById('btn-send').addEventListener('click', () => {
|
|
if (!rt) { log('Runtime not ready', 'error'); return; }
|
|
const dest = document.getElementById('dest-addr').value;
|
|
const payload = document.getElementById('msg-payload').value;
|
|
|
|
if (dest.length !== 64) {
|
|
log('Destination must be a 64-character hex address', 'error');
|
|
return;
|
|
}
|
|
|
|
const ok = rt.send_json(dest, 'js', payload);
|
|
if (ok) {
|
|
log(`Sent to ${dest.substring(0, 16)}...: ${payload}`, 'sent');
|
|
} else {
|
|
log('Send failed', 'error');
|
|
}
|
|
});
|
|
|
|
main();
|
|
</script>
|
|
</body>
|
|
</html>
|