Files
BridgeSwarm/examples/hrpc-demo.html
T
2026-02-12 06:01:02 -05:00

410 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BridgeSwarm HRPC Demo</title>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 640px;
margin: 0 auto;
padding: 1rem;
background: #1a1b26;
color: #c0caf5;
min-height: 100vh;
}
h1 { font-size: 1.25rem; margin: 0 0 0.5rem; color: #7aa2f7; }
h2 { font-size: 0.95rem; margin: 1rem 0 0.5rem; color: #bb9af7; font-weight: 600; }
p { margin: 0 0 0.5rem; font-size: 0.9rem; color: #a9b1d6; }
.row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; flex-wrap: wrap; }
input[type="text"] {
flex: 1;
min-width: 120px;
padding: 0.5rem 0.75rem;
border: 1px solid #3b4261;
border-radius: 6px;
background: #24283b;
color: #c0caf5;
font-size: 0.9rem;
}
input::placeholder { color: #565f89; }
button {
padding: 0.5rem 0.75rem;
border: none;
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
font-weight: 500;
}
button.primary { background: #7aa2f7; color: #1a1b26; }
button.primary:hover { background: #89b4fa; }
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
button.secondary { background: #3b4261; color: #c0caf5; }
button.secondary:hover { background: #414868; }
button.danger { background: #f7768e; color: #1a1b26; }
button.danger:hover { background: #ff9db5; }
.status { font-size: 0.85rem; margin-bottom: 0.5rem; }
.status.ok { color: #9ece6a; }
.status.error { color: #f7768e; }
.status.warn { color: #e0af68; }
.log {
background: #24283b;
border: 1px solid #3b4261;
border-radius: 6px;
padding: 0.6rem;
height: 220px;
overflow-y: auto;
font-family: ui-monospace, monospace;
font-size: 0.75rem;
line-height: 1.35;
margin-top: 0.5rem;
}
.log .peer { color: #bb9af7; }
.log .hrpc { color: #7dcfff; }
.log .sys { color: #7aa2f7; }
.log .err { color: #f7768e; }
.section { margin-bottom: 1rem; }
.badge { font-size: 0.7rem; padding: 0.15rem 0.4rem; border-radius: 4px; background: #3b4261; color: #a9b1d6; }
</style>
</head>
<body>
<h1>BridgeSwarm HRPC Demo</h1>
<p>HRPC is <strong>auto-enabled</strong> on every connection. Open this page in two tabs, join the same topic in both; once connected, you can &quot;Ping peer&quot; in either tab (no manual enable step).</p>
<div class="section">
<h2>1. Swarm <span class="badge">join / leave</span></h2>
<p id="status" class="status">Waiting for extension…</p>
<div class="row">
<input type="text" id="topic" placeholder="Topic" value="bridge-swarm-hrpc-demo">
<button class="primary" id="btnJoin">Join topic</button>
<button class="danger" id="btnLeave" disabled>Leave &amp; destroy</button>
</div>
</div>
<div class="section">
<h2>2. Connection &amp; HRPC <span class="badge">attachHrpc, hrpcInvoke</span></h2>
<p id="peers">Peers: 0</p>
<div class="row" id="connSelectRow" style="display:none;">
<label for="connSelect" style="font-size:0.85rem; color:#a9b1d6;">Use connection:</label>
<select id="connSelect" style="min-width:180px; padding:0.35rem 0.5rem; border-radius:6px; border:1px solid #3b4261; background:#24283b; color:#c0caf5; font-size:0.85rem;"></select>
</div>
<div class="row">
<button class="secondary" id="btnEnableHrpc" disabled>Retry HRPC on selected connection</button>
</div>
<p id="hrpcStatus" class="status" style="display:none;"></p>
<div class="row" id="pingRow" style="display:none;">
<input type="text" id="pingValue" placeholder="Ping value (optional)">
<button class="primary" id="btnPing">Ping peer</button>
</div>
<p id="pongResult" class="status" style="display:none;"></p>
</div>
<div class="section">
<h2>Log</h2>
<div class="log" id="log"></div>
</div>
<script>
(function () {
const statusEl = document.getElementById('status');
const topicEl = document.getElementById('topic');
const btnJoin = document.getElementById('btnJoin');
const btnLeave = document.getElementById('btnLeave');
const peersEl = document.getElementById('peers');
const connSelectRow = document.getElementById('connSelectRow');
const connSelect = document.getElementById('connSelect');
const btnEnableHrpc = document.getElementById('btnEnableHrpc');
const hrpcStatusEl = document.getElementById('hrpcStatus');
const pingRow = document.getElementById('pingRow');
const pingValueEl = document.getElementById('pingValue');
const btnPing = document.getElementById('btnPing');
const pongResultEl = document.getElementById('pongResult');
const logEl = document.getElementById('log');
function setStatus(msg, className) {
statusEl.textContent = msg;
statusEl.className = 'status ' + (className || 'ok');
}
function log(msg, type) {
const line = document.createElement('div');
line.className = type || 'sys';
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
logEl.appendChild(line);
logEl.scrollTop = logEl.scrollHeight;
}
function waitForExtension(cb) {
if (typeof window.BridgeSwarm !== 'undefined') {
cb();
return;
}
setStatus('Waiting for extension…', 'warn');
let attempts = 0;
const t = setInterval(function () {
attempts++;
if (typeof window.BridgeSwarm !== 'undefined') {
clearInterval(t);
cb();
return;
}
if (attempts >= 50) {
clearInterval(t);
setStatus('BridgeSwarm extension not detected. Install it and reload.', 'error');
}
}, 100);
}
connSelect.addEventListener('change', function () {
currentConnId = connSelect.value || null;
});
waitForExtension(function () {
setStatus('Ready. Join a topic, then open another tab and join the same topic.');
});
let swarm = null;
let currentConnId = null;
let hrpcEnabled = false;
const connectionEntries = [];
function updatePeers() {
peersEl.textContent = 'Peers: ' + connectionEntries.length;
btnEnableHrpc.disabled = !swarm || connectionEntries.length === 0;
if (connectionEntries.length === 0) {
connSelectRow.style.display = 'none';
connSelect.innerHTML = '';
currentConnId = null;
hrpcStatusEl.style.display = 'none';
pingRow.style.display = 'none';
pongResultEl.style.display = 'none';
return;
}
connSelectRow.style.display = 'flex';
const sel = connSelect.value;
connSelect.innerHTML = '';
const hasAnyHrpc = connectionEntries.some(function (e) { return e.hrpcEnabled; });
hrpcStatusEl.style.display = '';
if (hasAnyHrpc) {
pingRow.style.display = 'flex';
hrpcStatusEl.textContent = 'HRPC enabled on ' + connectionEntries.filter(function (e) { return e.hrpcEnabled; }).length + ' connection(s). You can ping.';
hrpcStatusEl.className = 'status ok';
} else {
pingRow.style.display = 'none';
hrpcStatusEl.textContent = 'Enabling HRPC on connection(s)… (wait up to 30s for other tab)';
hrpcStatusEl.className = 'status';
}
connectionEntries.forEach(function (e, i) {
const keyShort = (e.peerInfo.publicKey || '').slice(0, 16) + '\u2026';
const opt = document.createElement('option');
opt.value = e.connId;
opt.textContent = 'Peer ' + (i + 1) + ' (' + keyShort + ')' + (e.hrpcEnabled ? ' \u2713' : '');
if (e.connId === currentConnId || (!currentConnId && i === 0)) opt.selected = true;
connSelect.appendChild(opt);
});
currentConnId = connSelect.value || (connectionEntries[0] && connectionEntries[0].connId) || null;
}
function enableHrpcForConn(connId) {
if (typeof window.BridgeSwarm.request !== 'function') return;
window.BridgeSwarm.request('attachHrpc', { connId }).then(function (res) {
const entry = connectionEntries.find(function (e) { return e.connId === connId; });
if (!entry) return;
if (res && res.ok) {
entry.hrpcEnabled = true;
log('HRPC enabled on connection ' + (connectionEntries.indexOf(entry) + 1), 'hrpc');
updatePeers();
} else {
log('HRPC failed for connection: ' + (res && res.error ? res.error : 'unknown'), 'err');
if (res && res.error && res.error.indexOf('Connection not found') !== -1) {
const idx = connectionEntries.findIndex(function (e) { return e.connId === connId; });
if (idx !== -1) connectionEntries.splice(idx, 1);
if (currentConnId === connId) currentConnId = connectionEntries[0] ? connectionEntries[0].connId : null;
updatePeers();
}
}
}).catch(function (err) {
log('HRPC error: ' + err.message, 'err');
});
}
btnJoin.addEventListener('click', async function () {
if (typeof window.BridgeSwarm === 'undefined') {
setStatus('Extension not ready.', 'error');
return;
}
const topic = topicEl.value.trim() || 'hrpc-demo';
if (swarm) return;
try {
setStatus('Joining "' + topic + '"…');
swarm = new window.BridgeSwarm({ appName: 'bridge-swarm-hrpc-demo' });
swarm.on('connection', function (conn, peerInfo) {
const peerKey = peerInfo.publicKey || '';
const keyShort = peerKey.slice(0, 16) + '…';
const existingIdx = connectionEntries.findIndex(function (e) { return (e.peerInfo.publicKey || '') === peerKey; });
if (existingIdx !== -1) {
const old = connectionEntries[existingIdx];
connectionEntries.splice(existingIdx, 1);
try { old.conn.destroy(); } catch (_) {}
log('Replaced connection for peer ' + keyShort, 'sys');
}
log('Connection: peer ' + keyShort, 'peer');
connectionEntries.push({ conn, connId: conn.connId, peerInfo, hrpcEnabled: false });
updatePeers();
if (!currentConnId) currentConnId = conn.connId;
enableHrpcForConn(conn.connId);
conn.on('end', function () {
const i = connectionEntries.findIndex(function (e) { return e.conn === conn; });
if (i !== -1) connectionEntries.splice(i, 1);
if (currentConnId === conn.connId) currentConnId = connectionEntries[0] ? connectionEntries[0].connId : null;
updatePeers();
log('Peer left', 'sys');
});
conn.on('error', function (err) { log('Peer error: ' + err.message, 'err'); });
});
await swarm.join(topic);
setStatus('Joined "' + topic + '". Open another tab and join the same topic; HRPC enables automatically.', 'ok');
btnJoin.disabled = true;
btnLeave.disabled = false;
log('Joined topic: ' + topic, 'sys');
} catch (err) {
setStatus('Error: ' + err.message, 'error');
log('Error: ' + err.message, 'err');
}
});
btnLeave.addEventListener('click', async function () {
if (!swarm) return;
try {
const topic = topicEl.value.trim() || 'hrpc-demo';
await swarm.leave(topic);
await swarm.destroy();
connectionEntries.length = 0;
currentConnId = null;
hrpcEnabled = false;
pingRow.style.display = 'none';
hrpcStatusEl.style.display = 'none';
pongResultEl.style.display = 'none';
updatePeers();
btnEnableHrpc.disabled = true;
swarm = null;
setStatus('Left topic. You can join again.');
btnJoin.disabled = false;
btnLeave.disabled = true;
log('Left topic and destroyed swarm', 'sys');
} catch (err) {
log('Error: ' + err.message, 'err');
}
});
btnEnableHrpc.addEventListener('click', async function () {
const connId = currentConnId || (connectionEntries[0] && connectionEntries[0].connId);
if (!connId || typeof window.BridgeSwarm.request !== 'function') return;
const entry = connectionEntries.find(function (e) { return e.connId === connId; });
try {
btnEnableHrpc.disabled = true;
hrpcStatusEl.style.display = '';
hrpcStatusEl.textContent = 'Enabling HRPC… (wait up to 30s for other tab)';
hrpcStatusEl.className = 'status';
log('Retrying HRPC on connection…', 'hrpc');
const res = await window.BridgeSwarm.request('attachHrpc', { connId });
if (res && res.ok) {
if (entry) entry.hrpcEnabled = true;
hrpcEnabled = true;
hrpcStatusEl.textContent = 'HRPC enabled. You can ping the peer.';
hrpcStatusEl.className = 'status ok';
pingRow.style.display = 'flex';
log('HRPC enabled', 'hrpc');
updatePeers();
} else {
const errMsg = res && res.error ? res.error : 'Failed to enable HRPC';
if (errMsg.indexOf('Connection not found') !== -1) {
const idx = connectionEntries.findIndex(function (e) { return e.connId === connId; });
if (idx !== -1) connectionEntries.splice(idx, 1);
if (currentConnId === connId) currentConnId = connectionEntries[0] ? connectionEntries[0].connId : null;
updatePeers();
hrpcStatusEl.style.display = '';
hrpcStatusEl.textContent = 'Connection not found (removed). Select another or refresh.';
hrpcStatusEl.className = 'status error';
log('attachHrpc: ' + errMsg, 'err');
} else {
hrpcStatusEl.style.display = '';
hrpcStatusEl.textContent = errMsg;
hrpcStatusEl.className = 'status error';
log('attachHrpc failed: ' + errMsg, 'err');
}
}
} catch (err) {
hrpcStatusEl.style.display = '';
hrpcStatusEl.textContent = 'Error: ' + err.message;
hrpcStatusEl.className = 'status error';
log('Error: ' + err.message, 'err');
}
btnEnableHrpc.disabled = !swarm || connectionEntries.length === 0;
});
btnPing.addEventListener('click', async function () {
const hrpcEntry = connectionEntries.find(function (e) { return e.hrpcEnabled && (e.connId === currentConnId || !currentConnId); }) || connectionEntries.find(function (e) { return e.hrpcEnabled; });
const connId = hrpcEntry ? hrpcEntry.connId : (currentConnId || (connectionEntries[0] && connectionEntries[0].connId));
if (!connId || typeof window.BridgeSwarm.request !== 'function') return;
const value = pingValueEl.value.trim();
const args = value ? { value: value } : {};
btnPing.disabled = true;
pongResultEl.style.display = 'none';
log('Ping peer… (wait up to 15s)', 'hrpc');
if (!hrpcEntry) log('No HRPC-enabled connection selected; trying anyway.', 'sys');
const requestPromise = window.BridgeSwarm.request('hrpcInvoke', {
connId: connId,
method: 'ping',
args: args
});
const timeoutMs = 16000;
const timeoutPromise = new Promise(function (_, reject) {
setTimeout(function () {
reject(new Error('Request timed out. Ensure the other tab enabled HRPC and only two tabs are in the topic.'));
}, timeoutMs);
});
try {
const res = await Promise.race([requestPromise, timeoutPromise]);
if (res && res.ok && res.result) {
const pong = res.result.pong != null ? res.result.pong : JSON.stringify(res.result);
pongResultEl.style.display = '';
pongResultEl.textContent = 'Pong: ' + pong;
pongResultEl.className = 'status ok';
log('Pong: ' + pong, 'hrpc');
} else {
const errMsg = res && res.error ? res.error : 'No response';
pongResultEl.style.display = '';
pongResultEl.textContent = errMsg;
pongResultEl.className = 'status error';
log('Ping failed: ' + errMsg, 'err');
if (errMsg.indexOf('Connection not found') !== -1 || errMsg.indexOf('HRPC not attached') !== -1) {
const idx = connectionEntries.findIndex(function (e) { return e.connId === connId; });
if (idx !== -1) connectionEntries.splice(idx, 1);
if (currentConnId === connId) currentConnId = connectionEntries[0] ? connectionEntries[0].connId : null;
updatePeers();
}
}
} catch (err) {
pongResultEl.style.display = '';
pongResultEl.textContent = 'Error: ' + err.message;
pongResultEl.className = 'status error';
log('Error: ' + err.message, 'err');
if (err.message.indexOf('Connection not found') !== -1 || err.message.indexOf('HRPC not attached') !== -1) {
const idx = connectionEntries.findIndex(function (e) { return e.connId === connId; });
if (idx !== -1) connectionEntries.splice(idx, 1);
if (currentConnId === connId) currentConnId = connectionEntries[0] ? connectionEntries[0].connId : null;
updatePeers();
}
}
btnPing.disabled = false;
});
})();
</script>
</body>
</html>