Add HRPC demo
This commit is contained in:
@@ -204,6 +204,7 @@ You can attach Hypercore replication to a BridgeSwarm connection so peers can sy
|
||||
| **examples/chat.html** | Minimal P2P chat: join a topic, send messages between tabs or machines. |
|
||||
| **examples/data-demo.html** | Data API demo: Hypercore, Hyperbee, Hyperdrive, Autobase, and Hyperdb (Get/Put/Delete/Find/Flush) with a log. |
|
||||
| **examples/sdk-demo.html** | Demonstrates BridgeSwarm + Protomux usage. |
|
||||
| **examples/hrpc-demo.html** | HRPC: auto-enable and ping (open in two tabs, same topic). |
|
||||
|
||||
Open any example via File → Open (or drag the file into the browser). See [examples/README.md](examples/README.md) for details.
|
||||
|
||||
@@ -261,6 +262,7 @@ bridge-swarm/
|
||||
│ ├── chat.html # P2P chat demo
|
||||
│ ├── data-demo.html # Data API + Hyperdb demo
|
||||
│ ├── sdk-demo.html # Protomux / SDK demo
|
||||
│ ├── hrpc-demo.html # HRPC (attachHrpc, ping) demo
|
||||
│ └── README.md
|
||||
├── extension/ # Browser extension (MV3)
|
||||
│ ├── manifest.json
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"path": "ABSOLUTE_PATH_TO_NATIVE_HOST",
|
||||
"type": "stdio",
|
||||
"allowed_origins": [
|
||||
"chrome-extension://ahihpjhaledlngogjiplpcjigebfigoo/"
|
||||
"chrome-extension://peddgdekaggpighcndlcigkkiaobgcpe/"
|
||||
],
|
||||
"allowed_extensions": [
|
||||
"bridgeswarm-395cda49a8e4c8d[email protected]"
|
||||
"bridgeswarm-dff25bbc3f27032[email protected]"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ After a connection is established, the browser can ask the host to “take over
|
||||
| Type | Payload | Response |
|
||||
|------|---------|----------|
|
||||
| `attachReplication` | `{ connId, coreKeyHex? }` — if coreKeyHex omitted, host uses default core | `{ ok }` or `{ ok: false, error }` |
|
||||
| `attachHrpc` | `{ connId }` | `{ ok }` or `{ ok: false, error }` |
|
||||
| `attachHrpc` | `{ connId }` | `{ ok }` or `{ ok: false, error }` — returns immediately; channel pairs when both sides have enabled; first RPC may wait for channel (up to 12s). |
|
||||
| `hrpcInvoke` | `{ connId, method, args? }` — only `method: 'ping'` supported from browser (unary) | `{ ok, result }` (e.g. `result: { pong }`) or `{ ok: false, error }` |
|
||||
|
||||
See [DATA-API.md](DATA-API.md) and [HRPC.md](HRPC.md) for details.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ The host emits events (e.g. `connection`, `data`, `end`, `error`) when swarms or
|
||||
1. **Init swarm**: Page creates a BridgeSwarm and calls `join(topic)`. That triggers `init` (if not already inited) then `join`. The host creates a Hyperswarm and joins the topic.
|
||||
2. **Connection**: When a peer connects, the host creates a `connId`, stores the socket, and emits a `connection` event with `connId`, `swarmId`, and `peerInfo`. The page receives it and gets a BridgeSwarmConnection with that `connId`.
|
||||
3. **Data**: The page can `conn.write(data)` (sent as `write` with base64 data) and receives `data` events (host forwards socket data as base64 in `data` events). Or the page can **attach** the connection:
|
||||
4. **Attachment**: The page sends `attachReplication({ connId, coreKeyHex? })` or `attachHrpc({ connId })`. The host stops forwarding that connection’s data to the browser, creates a Protomux on the socket, and either runs Hypercore replication or opens the HRPC channel. That connection is then used only by the host for that protocol.
|
||||
4. **Attachment**: The page sends `attachReplication({ connId, coreKeyHex? })` or `attachHrpc({ connId })`. The host stops forwarding that connection’s data to the browser, creates a Protomux on the socket, and either runs Hypercore replication or opens the HRPC channel. That connection is then used only by the host for that protocol. The host keeps at most one connection per peer per swarm (duplicate connections from the same peer are dropped) so both sides use the same connection pair for HRPC.
|
||||
|
||||
## See also
|
||||
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ These commands attach host-side behavior to a connection (identified by `connId`
|
||||
| Type | Payload | Response |
|
||||
|------|---------|----------|
|
||||
| `attachReplication` | `{ connId, coreKeyHex? }` | `{ ok }` — Attach Hypercore replication to the connection. If `coreKeyHex` is provided, the host uses that core; otherwise the default core. |
|
||||
| `attachHrpc` | `{ connId }` | `{ ok }` — Enable HRPC on the connection (Protomux channel `bridgeswarm-hrpc`). See [HRPC.md](HRPC.md). |
|
||||
| `attachHrpc` | `{ connId }` | `{ ok }` — Enable HRPC on the connection (Protomux channel `bridgeswarm-hrpc`). Returns immediately; channel pairs when both sides have enabled; first ping may wait up to 12s for channel. See [HRPC.md](HRPC.md). |
|
||||
| `hrpcInvoke` | `{ connId, method, args? }` — Call an HRPC method from the browser; only `method: 'ping'` is supported (unary). | `{ ok, result }` (e.g. `result: { pong }`) or `{ ok: false, error }` |
|
||||
|
||||
## Example: Hyperbee
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ if (res.ok) {
|
||||
|
||||
- **Payload**: `{ connId }` — the connection ID from the `connection` event payload.
|
||||
- **Response**: `{ ok: true }` or `{ ok: false, error: '...' }`. On success, that connection is no longer forwarded to the browser; the host uses it for the hrpc channel.
|
||||
- **Handshake**: `attachHrpc` returns immediately (no blocking wait). The Protomux channel pairs when **both** sides have called `attachHrpc` on the same connection pair. When you call `hrpcInvoke` (e.g. ping), the host waits up to **12 seconds** for the channel to open if it is not yet open; if the other tab has also enabled HRPC, the channel opens and the RPC runs. Enable HRPC in both tabs (or on both ends of the connection); the first ping may briefly wait for the channel.
|
||||
- **One connection per peer**: The host keeps only the first connection per peer per swarm; later connections from the same peer are dropped so both sides use the same connection pair for HRPC.
|
||||
|
||||
## Protocol
|
||||
|
||||
@@ -51,6 +53,21 @@ To change the API (add commands or message types), edit [scripts/build-hrpc.js](
|
||||
|
||||
Handlers are implemented in [native-host/host.js](../native-host/host.js): ping returns pong, streamSum accumulates request stream and returns sum/count, fetchStream writes chunks to the response stream, duplex reads from the request stream and writes to the response stream, notify logs to stderr. The host loads the generated HRPC class from `spec/hrpc/` and registers these handlers when `attachHrpc` runs.
|
||||
|
||||
## Invoking ping from the browser
|
||||
|
||||
The host supports `BridgeSwarm.request('hrpcInvoke', { connId, method: 'ping', args })` so the browser can call the unary `ping` RPC. The host uses the same HRPC instance (server) as a client to send the request to the peer; the peer’s host must have HRPC attached so its handler replies. Example:
|
||||
|
||||
```javascript
|
||||
const res = await BridgeSwarm.request('hrpcInvoke', {
|
||||
connId: conn.connId,
|
||||
method: 'ping',
|
||||
args: { value: 'hello' }
|
||||
});
|
||||
if (res.ok && res.result) console.log('Pong:', res.result.pong);
|
||||
```
|
||||
|
||||
Only `ping` is supported via `hrpcInvoke` (streaming commands would require a different bridge). See **examples/hrpc-demo.html** for a full demo: open in two tabs, join the same topic; HRPC is auto-enabled on each connection, then either tab can ping.
|
||||
|
||||
## If the spec is missing
|
||||
|
||||
If `spec/hrpc/` was not built or fails to load (e.g. missing dependencies), the host still starts. In that case `attachHrpc` returns an error such as `"hrpc not available: ..."` or `"hrpc spec not built (run: npm run build:hrpc)"`. Run `npm run build:hrpc` from the repo root (or re-run the installer), then restart the native host.
|
||||
|
||||
@@ -22,6 +22,10 @@ A complete example of all BridgeSwarm SDK features:
|
||||
|
||||
Open `sdk-demo.html` in two tabs (or two devices), join the same topic, then try sending raw messages and Protomux messages. The log shows which path each message used.
|
||||
|
||||
## HRPC Demo (`hrpc-demo.html`)
|
||||
|
||||
Demonstrates **HRPC** on a connection: HRPC is **auto-enabled** on each connection. Open the page in two tabs, join the same topic; when a peer connects, the host enables HRPC and either tab can use “Ping peer” to get a pong. No manual enable step. See [../docs/HRPC.md](../docs/HRPC.md).
|
||||
|
||||
## Data API Demo (`data-demo.html`)
|
||||
|
||||
Uses the native host’s **Hypercore**, **Hyperbee**, **Hyperdrive**, and **Autobase** via `BridgeSwarm.request(type, payload)`.
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
<!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 "Ping peer" 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 & destroy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Connection & 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>
|
||||
+1
-1
@@ -239,7 +239,7 @@
|
||||
/**
|
||||
* Send a request to the native host (Hypercore, Hyperbee, Hyperdrive, Autobase RPC).
|
||||
* Returns a Promise that resolves with the host response payload.
|
||||
* @param {string} type - Command: coreInfo, coreAppend, coreGet, beeGet, beePut, beeDel, driveGet, drivePut, driveList, driveDel, autobaseAppend, autobaseViewGet, autobaseInfo, attachReplication, etc.
|
||||
* @param {string} type - Command: coreInfo, coreAppend, coreGet, beeGet, beePut, beeDel, driveGet, drivePut, driveList, driveDel, autobaseAppend, autobaseViewGet, autobaseInfo, attachReplication, attachHrpc, hrpcInvoke, etc.
|
||||
* @param {object} [payload] - Command payload (e.g. { key: 'foo' } for beeGet)
|
||||
* @returns {Promise<object>} Response (e.g. { ok: true, value: '...' } or { ok: false, error: '...' })
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "BridgeSwarm",
|
||||
"version": "1.0.0",
|
||||
"description": "P2P Hyperswarm in the browser via native messaging host",
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkikOI5ZYZ170eU5L6YOjI5TP78Et2bCSxB4TD5n/K6zfCTZCuVeyX3B4jbtiJL3RHkJpeqXKpdveEn4Wkt/uYENin7bPMAVcUvBBCIidFXaEqiup0fzeRo+bNFbNwOaO+mG1qFQ77xgK0iHR6JCW/VHeAl8+pa1UaqWNTeRX/9j+kuLGAzQa+bVI3UhfBRlUFi4W4BO33y/O/5bRTWdg48c9CSJT09nIulMUCsI2TGkxvVK8lcvBWutfABpixyGVqadtUZ3Xpap0/K5iRm9WWdPM9BxgN1cAZq8reVXltpKePC64ZI2kzQQUQzJ2zmqXq98I2ikUbclD/n5NRDN9PQIDAQAB",
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0OrADq6zKa85rDFCL6S7/K7OsuFxwCgFbn5RooAs8qLwVYBCge/RivZwfqt5J5Egp98JeTPH8jcmblOQHPhunW5SGMKMd0NezbwcKGRXU8PESCUT25TAfD3tDNGSVIJ/Dq70emZJqqc2afCaoYmTRLBxhsqFYJQ4FuRp43IU0GiyXI9OeDnudofDPvupPBXt63MGXJZJatc9WyYLIk5buWoajJlmudXoMOCewyvgAx8cUaVG4QijuCWFMWECWYKgZlSg3mTNwVlLwxZmmbx7G+RbR5hAWxt7FyVOG35wbZFw+/2AnLJRsPiPIZo3zF6pDHhgAYtnSH6FkyzX5fV7rQIDAQAB",
|
||||
"permissions": [
|
||||
"nativeMessaging",
|
||||
"storage",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "bridgeswarm-395cda49a8e4c8d[email protected]",
|
||||
"id": "bridgeswarm-dff25bbc3f27032[email protected]",
|
||||
"strict_min_version": "79.0"
|
||||
}
|
||||
}
|
||||
|
||||
+100
-17
@@ -30,9 +30,11 @@ try {
|
||||
const HRPC_PROTOCOL = 'bridgeswarm-hrpc';
|
||||
|
||||
const swarms = new Map();
|
||||
/** @type {Map<string, { socket: import('stream').Duplex, protomux?: import('protomux'), hrpc?: InstanceType<typeof HRPC>, hrpcDuplex?: import('stream').Duplex, forwarding: boolean, _listeners: object }>} */
|
||||
/** @type {Map<string, { socket: import('stream').Duplex, protomux?: import('protomux'), hrpc?: InstanceType<typeof HRPC>, hrpcDuplex?: import('stream').Duplex, hrpcChannel?: object, forwarding: boolean, _listeners: object, peerKeyHex: string, _closed: boolean }>} */
|
||||
const connections = new Map();
|
||||
const connToSwarm = new Map(); // connId -> swarmId
|
||||
/** swarmId -> (peerKeyHex -> connId) for one connection per peer per swarm */
|
||||
const peerConnections = new Map();
|
||||
let nextConnId = 0;
|
||||
|
||||
// Lazy corestore and default data structures
|
||||
@@ -147,32 +149,54 @@ async function handleMessageAsync(send, msg) {
|
||||
}
|
||||
const swarm = new Hyperswarm(options);
|
||||
swarm.on('connection', (socket, peerInfo) => {
|
||||
const peerKeyHex = b4a.toString(peerInfo.publicKey, 'hex');
|
||||
if (!peerConnections.has(swarmId)) peerConnections.set(swarmId, new Map());
|
||||
const peerMap = peerConnections.get(swarmId);
|
||||
const existingConnId = peerMap.get(peerKeyHex);
|
||||
if (existingConnId) {
|
||||
const oldEntry = connections.get(existingConnId);
|
||||
if (oldEntry && !oldEntry._closed && oldEntry.socket === socket) {
|
||||
return;
|
||||
}
|
||||
try { socket.destroy(); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
|
||||
const connId = generateConnId();
|
||||
const entry = {
|
||||
socket,
|
||||
forwarding: true,
|
||||
_listeners: {},
|
||||
peerKeyHex,
|
||||
_closed: false,
|
||||
};
|
||||
function onData(data) {
|
||||
if (!entry.forwarding) return;
|
||||
emit('data', { connId, swarmId, data: b4a.toString(data, 'base64') });
|
||||
}
|
||||
function onEnd() {
|
||||
function closeConnection() {
|
||||
if (entry._closed) return;
|
||||
entry._closed = true;
|
||||
entry.socket.removeAllListeners();
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
if (peerMap.get(peerKeyHex) === connId) peerMap.delete(peerKeyHex);
|
||||
emit('end', { connId, swarmId });
|
||||
}
|
||||
function onEnd() {
|
||||
closeConnection();
|
||||
}
|
||||
function onError(err) {
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
emit('error', { connId, swarmId, message: err.message });
|
||||
if (!entry._closed) emit('error', { connId, swarmId, message: err.message });
|
||||
closeConnection();
|
||||
}
|
||||
function onClose() {
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
closeConnection();
|
||||
}
|
||||
const entry = {
|
||||
socket,
|
||||
forwarding: true,
|
||||
_listeners: { onData, onEnd, onError, onClose },
|
||||
};
|
||||
entry._listeners = { onData, onEnd, onError, onClose };
|
||||
connections.set(connId, entry);
|
||||
connToSwarm.set(connId, swarmId);
|
||||
peerMap.set(peerKeyHex, connId);
|
||||
socket.on('data', onData);
|
||||
socket.on('end', onEnd);
|
||||
socket.on('error', onError);
|
||||
@@ -222,7 +246,7 @@ async function handleMessageAsync(send, msg) {
|
||||
const { connId, data: base64Data } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (!entry) {
|
||||
reply({ ok: false, error: 'Connection not found' });
|
||||
reply({ ok: false, error: 'Connection not found. Refresh the page if the host was restarted.' });
|
||||
return;
|
||||
}
|
||||
const data = Buffer.from(base64Data, 'base64');
|
||||
@@ -235,12 +259,17 @@ async function handleMessageAsync(send, msg) {
|
||||
const { connId } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (entry) {
|
||||
entry._closed = true;
|
||||
entry.socket.removeAllListeners();
|
||||
if (entry.hrpcDuplex && !entry.hrpcDuplex.destroyed) {
|
||||
entry.hrpcDuplex.destroy();
|
||||
}
|
||||
entry.socket.destroy();
|
||||
const swarmIdForConn = connToSwarm.get(connId);
|
||||
const pm = swarmIdForConn ? peerConnections.get(swarmIdForConn) : null;
|
||||
if (pm && pm.get(entry.peerKeyHex) === connId) pm.delete(entry.peerKeyHex);
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
try { entry.socket.destroy(); } catch (_) {}
|
||||
}
|
||||
reply({ ok: true });
|
||||
break;
|
||||
@@ -250,7 +279,7 @@ async function handleMessageAsync(send, msg) {
|
||||
const { connId, coreKeyHex } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (!entry) {
|
||||
reply({ ok: false, error: 'Connection not found' });
|
||||
reply({ ok: false, error: 'Connection not found. Refresh the page if the host was restarted.' });
|
||||
return;
|
||||
}
|
||||
if (entry.protomux) {
|
||||
@@ -264,6 +293,9 @@ async function handleMessageAsync(send, msg) {
|
||||
entry.socket.removeListener('error', entry._listeners.onError);
|
||||
entry.socket.removeListener('close', entry._listeners.onClose);
|
||||
entry.socket.once('close', () => {
|
||||
const sid = connToSwarm.get(connId);
|
||||
const pm = sid ? peerConnections.get(sid) : null;
|
||||
if (pm && pm.get(entry.peerKeyHex) === connId) pm.delete(entry.peerKeyHex);
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
});
|
||||
@@ -294,7 +326,7 @@ async function handleMessageAsync(send, msg) {
|
||||
const { connId } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (!entry) {
|
||||
reply({ ok: false, error: 'Connection not found' });
|
||||
reply({ ok: false, error: 'Connection not found. Refresh the page if the host was restarted.' });
|
||||
return;
|
||||
}
|
||||
if (entry.hrpc) {
|
||||
@@ -308,6 +340,9 @@ async function handleMessageAsync(send, msg) {
|
||||
entry.socket.removeListener('error', entry._listeners.onError);
|
||||
entry.socket.removeListener('close', entry._listeners.onClose);
|
||||
entry.socket.once('close', () => {
|
||||
const sid = connToSwarm.get(connId);
|
||||
const pm = sid ? peerConnections.get(sid) : null;
|
||||
if (pm && pm.get(entry.peerKeyHex) === connId) pm.delete(entry.peerKeyHex);
|
||||
connections.delete(connId);
|
||||
connToSwarm.delete(connId);
|
||||
});
|
||||
@@ -337,6 +372,7 @@ async function handleMessageAsync(send, msg) {
|
||||
}
|
||||
});
|
||||
channel.open();
|
||||
entry.hrpcChannel = channel;
|
||||
const rpc = new HRPC(hrpcDuplex);
|
||||
rpc.onPing((data) => {
|
||||
return { pong: (data && data.value) || 'pong' };
|
||||
@@ -391,6 +427,44 @@ async function handleMessageAsync(send, msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'hrpcInvoke': {
|
||||
const { connId, method, args = {} } = payload;
|
||||
const entry = connections.get(connId);
|
||||
if (!entry || !entry.hrpc) {
|
||||
reply({ ok: false, error: 'Connection not found or HRPC not attached. Refresh the page if the host was restarted.' });
|
||||
return;
|
||||
}
|
||||
if (method !== 'ping') {
|
||||
reply({ ok: false, error: 'Only ping is supported from the browser (unary only)' });
|
||||
return;
|
||||
}
|
||||
if (entry.hrpcChannel) {
|
||||
try {
|
||||
await Promise.race([
|
||||
entry.hrpcChannel.fullyOpened(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Channel not ready yet. Ensure the other tab has joined the same topic.')), 12000))
|
||||
]);
|
||||
} catch (chErr) {
|
||||
reply({ ok: false, error: chErr.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const timeoutMs = 15000;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Ping timed out. Ensure the other tab also enabled HRPC on its connection.')), timeoutMs);
|
||||
});
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
entry.hrpc.ping(args),
|
||||
timeoutPromise
|
||||
]);
|
||||
reply({ ok: true, result });
|
||||
} catch (err) {
|
||||
reply({ ok: false, error: err.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'destroy': {
|
||||
const { swarmId } = payload;
|
||||
const swarm = swarms.get(swarmId);
|
||||
@@ -398,10 +472,16 @@ async function handleMessageAsync(send, msg) {
|
||||
const toDestroy = [...connToSwarm.entries()].filter(([, sid]) => sid === swarmId);
|
||||
for (const [cid] of toDestroy) {
|
||||
const entry = connections.get(cid);
|
||||
if (entry) entry.socket.destroy();
|
||||
if (entry) {
|
||||
entry._closed = true;
|
||||
entry.socket.removeAllListeners();
|
||||
peerConnections.get(swarmId)?.delete(entry.peerKeyHex);
|
||||
try { entry.socket.destroy(); } catch (_) {}
|
||||
}
|
||||
connections.delete(cid);
|
||||
connToSwarm.delete(cid);
|
||||
}
|
||||
peerConnections.delete(swarmId);
|
||||
await swarm.destroy();
|
||||
swarms.delete(swarmId);
|
||||
}
|
||||
@@ -648,14 +728,17 @@ async function handleMessageAsync(send, msg) {
|
||||
function cleanup() {
|
||||
for (const entry of connections.values()) {
|
||||
try {
|
||||
entry._closed = true;
|
||||
if (entry.hrpcDuplex && !entry.hrpcDuplex.destroyed) {
|
||||
entry.hrpcDuplex.destroy();
|
||||
}
|
||||
entry.socket.removeAllListeners();
|
||||
entry.socket.destroy();
|
||||
} catch (_) {}
|
||||
}
|
||||
connections.clear();
|
||||
connToSwarm.clear();
|
||||
peerConnections.clear();
|
||||
for (const swarm of swarms.values()) {
|
||||
swarm.destroy().catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user