~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-atirc

atirc channels in a browser page: WebRTC to atirc mesh nodes, no server, no token

0
0
main
95 lines · 4.7 KB Raw History
<!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>