johnandersen777.bsky.social/atlibs-atirc
atirc channels in a browser page: WebRTC to atirc mesh nodes, no server, no token
0
0
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>atlibs-atirc loopback</title>
<!-- Two clients of alice.test against local `atirc mesh run` nodes (test/loopback.ts): one through the
owner's bridge (to vm.alice.test, a node without WebRTC), one direct to bot.agent.test. -->
<script type="importmap">
{
"imports": {
"age-encryption": "https://esm.sh/age-encryption@0.3.1",
"@publicdomainrelay/atlibs-webrtc": "/node_modules/@publicdomainrelay/atlibs-webrtc/js/index.js",
"@publicdomainrelay/atlibs-atirc": "/js/index.js"
}
}
</script>
<script type="module">
import * as age from "age-encryption";
import { atirc } from "@publicdomainrelay/atlibs-atirc";
import { localSignals } from "@publicdomainrelay/atlibs-webrtc";
const q = new URLSearchParams(location.search);
const did = q.get("did");
const post = (method, body) =>
fetch(`/xrpc/com.atproto.repo.${method}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ repo: did, ...body }) });
const repo = {
did,
async getRecord({ repo, collection, rkey }) {
const response = await fetch(`/xrpc/com.atproto.repo.getRecord?${new URLSearchParams({ repo, collection, rkey })}`);
return response.ok ? response.json() : null;
},
putRecord: (input) => post("putRecord", input),
deleteRecord: (input) => post("deleteRecord", input),
};
const log = (text) => fetch("/log", { method: "POST", body: text });
const step = async (name) => {
const response = await fetch(`/step/${name}`, { method: "POST" });
if (!response.ok) throw new Error(`step ${name}: ${await response.text()}`);
};
const until = async (check, what, ms = 60000) => {
for (const end = Date.now() + ms; Date.now() < end; await new Promise((r) => setTimeout(r, 200))) if (await check()) return;
throw new Error(`timed out waiting for ${what}`);
};
const heard = (messages, channel, subhandle, text) => () => messages.some((m) => m.channel === channel && m.identity === subhandle && m.text === text);
try {
const a = atirc({ repo, handle: "alice.test", age });
const got = [];
a.onMessage((m) => (got.push(m), log(`A ${m.channel} ${m.identity}: ${m.text}`)));
a.onPresence((p) => log(`A presence ${p.event} ${p.channel} ${p.identity}`));
await a.join("#alice_test");
await until(heard(got, "#alice_test", "vm.alice.test", "vm online"), "the vm's replayed line via the bridge");
await a.post("#alice_test", "hi from the page");
await step("vm-heard");
await until(async () => { const { peers } = await a.peers("#alice_test"); return ["vm.alice.test", "bot.agent.test"].every((s) => peers.some((p) => p.subhandle === s)); }, "members via the bridge");
await a.join("#vm_alice_test");
await step("vm-own");
await until(heard(got, "#vm_alice_test", "vm.alice.test", "vm own channel"), "the vm's own channel");
await a.post("#vm_alice_test", "page on the vm channel");
await step("vm-own-heard");
const b = atirc({ repo, handle: "alice.test", subhandle: "web2.alice.test", age, bridge: false, peers: [{ did: q.get("agent"), subhandle: "bot.agent.test" }] });
const gotB = [];
b.onMessage((m) => (gotB.push(m), log(`B ${m.channel} ${m.identity}: ${m.text}`)));
await b.join("#alice_test");
await step("bot-say");
await until(heard(gotB, "#alice_test", "bot.agent.test", "bot says hi"), "bot's line, direct");
await until(heard(got, "#alice_test", "bot.agent.test", "bot says hi"), "bot's line, via the bridge");
await b.post("#alice_test", "page direct to bot");
await step("bot-heard");
// A guest: the vm node's local listener through webrtc-peer, signalled like a contract's tunnel.
const receipt = { uri: "at://did:plc:loopback/com.publicdomainrelay.temp.market.event/receipt", cid: "bafyreiloopback" };
const c = atirc({ guests: [{ receipt, ...localSignals(q.get("signal")) }], age });
const gotC = [];
c.onMessage((m) => (gotC.push(m), log(`C ${m.channel} ${m.identity}: ${m.text}`)));
await c.join("#alice_test");
await step("bot-to-guest");
await until(heard(gotC, "#alice_test", "bot.agent.test", "bot to the guest"), "bot's line via the guest node");
await c.post("#alice_test", "from the guest page");
await step("guest-heard");
if ((await c.peers()).me !== "vm.alice.test") throw new Error("the guest page posts as the vm node");
c.close();
const peersA = (await a.peers("#alice_test")).peers.map((p) => p.subhandle).sort();
a.close();
b.close();
await fetch("/result", { method: "POST", body: JSON.stringify({ ok: true, peersA, a: got.length, b: gotB.length }) });
} catch (error) {
await fetch("/result", { method: "POST", body: JSON.stringify({ ok: false, error: String(error?.stack ?? error) }) });
}
</script>
</head>
<body></body>
</html>