HearthNet-Nemotron / webagent /src /mesh /browsermesh.js
GitHub Actions
feat: WebLLM browser agent with PeerJS mesh, HybridRAG, news signals, and easter-egg ticker
78cc96f
Raw
History Blame
6.92 kB
// src/mesh/browsermesh.js
// Over-the-internet browser mesh using PeerJS (WebRTC). JavaScript only.
//
// Design (BitTorrent/rendezvous style, no custom server):
// - DISCOVERY: a well-known "anchor" peer id is derived from the room name.
// The first browser in a room claims that id and becomes the anchor. The
// anchor only gossips a roster of member ids β€” it is a rendezvous point.
// - DATA: every browser also runs its own random-id "self" peer. Members use
// the roster to open direct WebRTC data channels to each other => full mesh.
// - FAILOVER: if the anchor leaves, its id frees up and remaining members
// periodically try to re-claim it, so the mesh self-heals.
//
// Requires global `Peer` from https://unpkg.com/peerjs (loaded in index.html).
function slug(s) {
return String(s || "default").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
}
export function createMesh({ room = "hearthnet", name = "anon", onStatus, onPeers, onMessage } = {}) {
const Peer = globalThis.Peer;
if (!Peer) throw new Error("PeerJS not loaded (expected global `Peer`).");
const anchorId = `hn-anchor-${slug(room)}`;
const peers = new Map(); // selfId -> { conn, name }
let self = null; // my mesh identity (random id)
let selfId = null;
let anchor = null; // Peer bound to anchorId if I'm the anchor
let anchorConn = null; // my connection to the anchor (if I'm a member)
let isAnchor = false;
let reclaimTimer = null;
let alive = false;
const status = (extra = {}) =>
onStatus?.({ room, anchorId, selfId, isAnchor, peers: [...peers.keys()], ...extra });
const roster = () => [selfId, ...peers.keys()].filter(Boolean);
// ── data mesh (self <-> self direct channels) ──────────────────────────────
function wireDataConn(conn) {
conn.on("open", () => {
peers.set(conn.peer, { conn, name: conn.metadata?.name || conn.peer.slice(0, 6) });
conn.send({ type: "presence", payload: { name } });
onPeers?.(listPeers());
status();
});
conn.on("data", (msg) => {
if (!msg || typeof msg !== "object") return;
if (msg.type === "presence" && peers.has(conn.peer)) {
peers.get(conn.peer).name = msg.payload?.name || peers.get(conn.peer).name;
onPeers?.(listPeers());
}
onMessage?.({ from: conn.peer, fromName: peers.get(conn.peer)?.name, type: msg.type, payload: msg.payload });
});
const drop = () => {
if (peers.delete(conn.peer)) {
onPeers?.(listPeers());
status();
}
};
conn.on("close", drop);
conn.on("error", drop);
}
function connectTo(peerId) {
if (!peerId || peerId === selfId || peers.has(peerId)) return;
const conn = self.connect(peerId, { reliable: true, metadata: { name } });
wireDataConn(conn);
}
function applyRoster(ids) {
for (const id of ids) connectTo(id);
}
// ── anchor role: gossip the roster to all anchor clients ───────────────────
function becomeAnchor() {
isAnchor = true;
const known = new Set([selfId]);
const clients = new Set();
const broadcast = () => {
const members = [...known];
for (const c of clients) {
try {
c.send({ type: "roster", payload: { members } });
} catch { /* client gone */ }
}
};
anchor.on("connection", (c) => {
c.on("open", () => {
clients.add(c);
c.on("data", (m) => {
if (m?.type === "hello" && m.payload?.selfId) {
known.add(m.payload.selfId);
broadcast();
}
});
// tell the newcomer the current roster (incl. the anchor's own self id)
c.send({ type: "roster", payload: { members: [...known] } });
broadcast();
});
const gone = () => {
clients.delete(c);
};
c.on("close", gone);
c.on("error", gone);
});
status({ note: "became anchor" });
}
// ── member role: connect to the anchor, learn the roster ───────────────────
function joinViaAnchor() {
anchorConn = self.connect(anchorId, { reliable: true, metadata: { name } });
anchorConn.on("open", () => {
anchorConn.send({ type: "hello", payload: { selfId } });
clearReclaim();
});
anchorConn.on("data", (m) => {
if (m?.type === "roster") applyRoster(m.payload?.members || []);
});
const lost = () => {
anchorConn = null;
scheduleReclaim(); // anchor may have left β€” try to take over discovery
};
anchorConn.on("close", lost);
anchorConn.on("error", lost);
}
function tryClaimAnchor() {
if (!alive || isAnchor) return;
const a = new Peer(anchorId);
a.on("open", () => {
anchor = a;
becomeAnchor();
});
a.on("error", (err) => {
a.destroy?.();
if (err?.type === "unavailable-id") {
// someone else is the anchor β€” connect to it
if (!anchorConn) joinViaAnchor();
}
});
}
function scheduleReclaim() {
clearReclaim();
reclaimTimer = setInterval(tryClaimAnchor, 4000 + Math.random() * 3000);
}
function clearReclaim() {
if (reclaimTimer) clearInterval(reclaimTimer);
reclaimTimer = null;
}
function listPeers() {
return [...peers.entries()].map(([id, v]) => ({ id, name: v.name }));
}
// ── public API ─────────────────────────────────────────────────────────────
function join() {
if (alive) return;
alive = true;
self = new Peer(); // random id
self.on("open", (id) => {
selfId = id;
status({ note: "self online" });
self.on("connection", (conn) => wireDataConn(conn)); // accept inbound mesh links
tryClaimAnchor();
scheduleReclaim();
});
self.on("error", (err) => status({ error: err?.type || String(err) }));
}
function leave() {
alive = false;
clearReclaim();
for (const { conn } of peers.values()) conn.close?.();
peers.clear();
anchorConn?.close?.();
anchor?.destroy?.();
self?.destroy?.();
self = anchor = anchorConn = null;
selfId = null;
isAnchor = false;
onPeers?.([]);
status({ note: "left" });
}
function broadcast(type, payload) {
for (const { conn } of peers.values()) {
try {
conn.send({ type, payload });
} catch { /* peer gone */ }
}
}
function send(peerId, type, payload) {
peers.get(peerId)?.conn?.send({ type, payload });
}
return {
join,
leave,
broadcast,
send,
get id() { return selfId; },
get peers() { return listPeers(); },
get isAnchor() { return isAnchor; },
get room() { return room; },
};
}