~ 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
199 lines · 8.8 KB Raw History
import { test } from "node:test";
import assert from "node:assert/strict";
import { atirc, channelOf, normalizeChannel, sessionRepo } from "./index.js";

const ME = "did:plc:alice";
const PEER = "computer.socialweb.peer";
const until = async (check) => {
	for (let i = 0; i < 200; i++, await new Promise((r) => setTimeout(r, 5))) if (check()) return;
	throw new Error("timed out");
};

// A repo of peer records and a WebrtcHost stand-in: each peer is a handler of the channel's lines.
function world(records, handlers) {
	const repo = {
		did: ME,
		getRecord: async ({ repo: did, collection, rkey }) => {
			const value = records[`${did}/${collection}/${rkey}`];
			return value ? { value, cid: "bafy" } : null;
		},
		putRecord: async () => ({}),
		deleteRecord: async () => ({}),
	};
	const host = () => ({
		close() {},
		async call({ params: { receipt } }) {
			const subhandle = receipt.uri.split("/").pop();
			const handler = handlers[subhandle];
			if (!handler) throw new Error(`${subhandle} does not answer`);
			const queue = [];
			let wake;
			let closed = false;
			const conn = {
				sent: [],
				push: (line) => (queue.push(`${JSON.stringify(line)}\n`), wake?.()),
				end: () => ((closed = true), wake?.()),
			};
			return {
				close: conn.end,
				send: async (data) => {
					const line = JSON.parse(data);
					conn.sent.push(line);
					handler(line, conn);
				},
				async *[Symbol.asyncIterator]() {
					for (;;) {
						while (!queue.length && !closed) await new Promise((r) => (wake = r));
						if (queue.length) yield queue.shift();
						else return;
					}
				},
			};
		},
	});
	return { repo, host };
}

const record = (channels) => ({ webrtc: ME, iroh: "x", channels });

test("channel names follow atirc", () => {
	assert.equal(channelOf("alice.test"), "#alice_test");
	assert.equal(channelOf("Bot.Agent.test"), "#bot_agent_test");
	assert.equal(channelOf(""), "#atirc");
	assert.equal(normalizeChannel(" Alice_Test "), "#alice_test");
});

test("through the owner's bridge: attributed lines, members, post, leave", async () => {
	let conn;
	const { repo, host } = world({ [`${ME}/${PEER}/alice.test`]: record(["#alice_test"]) }, {
		"alice.test": (line, c) => {
			conn = c;
			if (line.hello) c.push({ event: "hello", did: ME, subhandle: "alice.test", bridge: true, epoch: 9, since: 0 });
		},
	});
	const chat = atirc({ repo, handle: "alice.test", host });
	const messages = [];
	const presence = [];
	chat.onMessage((m) => messages.push(m));
	chat.onPresence((p) => presence.push(`${p.event} ${p.identity}`));
	await chat.join("#alice_test");
	assert.equal(conn.sent[0].channel, "#alice_test");
	conn.push({ members: [{ did: "did:plc:agent", subhandle: "bot.agent.test" }] });
	conn.push({ seq: 1, text: "hi", ts: "t", did: "did:plc:agent", subhandle: "bot.agent.test" });
	conn.push({ seq: 1, text: "replayed twice", ts: "t" });
	await until(() => messages.length === 1);
	assert.deepEqual(messages[0], { channel: "#alice_test", identity: "bot.agent.test", nick: "bot", did: "did:plc:agent", text: "hi", ts: "t", seq: 1, via: "alice.test" });
	const { me, channels, peers } = await chat.peers();
	assert.deepEqual([me, channels, peers.map((p) => p.subhandle)], ["alice.test", ["#alice_test"], ["bot.agent.test"]]);
	await chat.post("alice_test", "hello agents");
	assert.deepEqual([conn.sent[1].seq, conn.sent[1].text], [1, "hello agents"]);
	assert.deepEqual([messages.at(-1).self, messages.at(-1).identity], [true, "alice.test"], "posted as the bridge node");
	chat.leave("#alice_test");
	assert.deepEqual(presence, ["join alice.test", "join bot.agent.test", "leave bot.agent.test", "leave alice.test"]);
	assert.deepEqual((await chat.peers()).channels, []);
	await assert.rejects(chat.post("#alice_test", "gone"), /not in #alice_test/);
});

test("without a bridge: direct peers that carry the channel speak only for themselves", async () => {
	const conns = {};
	const reply = (subhandle) => (line, c) => {
		conns[subhandle] = c;
		if (line.hello) c.push({ event: "hello", did: "did:plc:agent", subhandle, epoch: 1, peers: [{ did: "did:plc:agent", subhandle: "two.agent.test" }] });
	};
	const { repo, host } = world({
		[`did:plc:agent/${PEER}/one.agent.test`]: record(["#alice_test"]),
		[`did:plc:agent/${PEER}/two.agent.test`]: record(["#alice_test"]),
		[`did:plc:agent/${PEER}/elsewhere.agent.test`]: record(["#other"]),
	}, { "one.agent.test": reply("one.agent.test"), "two.agent.test": reply("two.agent.test") });
	const chat = atirc({
		repo,
		handle: "alice.test",
		host,
		peers: [{ did: "did:plc:agent", subhandle: "one.agent.test" }, { did: "did:plc:agent", subhandle: "elsewhere.agent.test" }],
	});
	const messages = [];
	chat.onMessage((m) => messages.push(m));
	await chat.join("#alice_test");
	// two.agent.test came from one's peer list.
	await until(() => conns["two.agent.test"]);
	conns["one.agent.test"].push({ seq: 4, text: "mine", did: "did:plc:alice", subhandle: "alice.test" });
	await until(() => messages.length === 1);
	assert.equal(messages[0].identity, "one.agent.test", "a direct peer cannot speak as another");
	await chat.post("#alice_test", "both");
	assert.equal(conns["one.agent.test"].sent.at(-1).text, "both");
	assert.equal(conns["two.agent.test"].sent.at(-1).text, "both");
	chat.close();
});

test("a dropped link redials with since, and nobody to dial fails the join", async () => {
	const hellos = [];
	let conn;
	const { repo, host } = world({ [`${ME}/${PEER}/alice.test`]: record(["#alice_test"]) }, {
		"alice.test": (line, c) => {
			conn = c;
			if (line.hello) hellos.push(line), c.push({ event: "hello", bridge: true, epoch: 9 });
		},
	});
	const chat = atirc({ repo, handle: "alice.test", host, retryMs: 10 });
	await chat.join("#alice_test");
	conn.push({ seq: 3, text: "x" });
	await new Promise((r) => setTimeout(r, 20));
	conn.end();
	await until(() => hellos.length === 2);
	assert.deepEqual([hellos[1].since, hellos[1].seen_epoch], [3, 9]);
	chat.close();
	await assert.rejects(atirc({ repo, handle: "bob.test", host, bridge: false }).join("#nobody"), /no peer of #nobody takes WebRTC/);
});

test("guests: the first VM node whose tunnel opens, as a bridge", async () => {
	let conn;
	const opened = [];
	const host = ({ events, signal }) => ({
		close() {},
		async call({ params: { receipt, tag } }) {
			opened.push(`${receipt.uri} ${tag} ${events.name} ${signal.name}`);
			if (receipt.uri.endsWith("gone")) throw new Error("the guest is gone");
			return world({}, {
				vm: (line, c) => {
					conn = c;
					if (line.hello) c.push({ event: "hello", did: ME, subhandle: "you-bot.alice.test", bridge: true, epoch: 2 });
				},
			}).host().call({ params: { receipt: { uri: "at://x/y/vm" } } });
		},
	});
	const events = async function events() {}, signal = async function signal() {};
	const chat = atirc({ guests: [{ receipt: { uri: "at://b/accept/gone" }, events, signal }, { receipt: { uri: "at://b/accept/new" }, events, signal }], host });
	const messages = [];
	chat.onMessage((m) => messages.push(m));
	await chat.join("#bot_alice_test");
	assert.deepEqual(opened, ["at://b/accept/gone atirc events signal", "at://b/accept/new atirc events signal"]);
	conn.push({ seq: 7, text: "hello", ts: "t", did: "did:plc:alice", subhandle: "bot.alice.test" });
	await until(() => messages.length === 1);
	assert.equal(messages[0].nick, "bot");
	await chat.post("#bot_alice_test", "hi bot");
	assert.equal(conn.sent.at(-1).text, "hi bot");
	assert.equal(messages.at(-1).identity, "you-bot.alice.test");
	assert.equal((await chat.peers()).me, "you-bot.alice.test");
	chat.close();
	assert.throws(() => atirc({}), /needs guests, or repo/);
});

test("sessionRepo: public reads at the record's PDS, writes through the session", async () => {
	const calls = [];
	const fetch = async (url) => {
		calls.push(String(url));
		if (String(url) === "https://plc.directory/did:plc:agent") return Response.json({ service: [{ id: "#atproto_pds", serviceEndpoint: "https://pds.example" }] });
		if (String(url).includes("rkey=missing")) return Response.json({ error: "RecordNotFound" }, { status: 400 });
		return Response.json({ value: { webrtc: "did:plc:agent" }, cid: "c" });
	};
	const written = [];
	const session = { did: ME, fetchHandler: async (path, init) => (written.push([path, JSON.parse(init.body)]), Response.json({})) };
	const repo = sessionRepo(session, { fetch });
	assert.equal(repo.did, ME);
	assert.equal((await repo.getRecord({ repo: "did:plc:agent", collection: PEER, rkey: "bot" })).cid, "c");
	assert.equal(await repo.getRecord({ repo: "did:plc:agent", collection: PEER, rkey: "missing" }), null);
	assert.equal(calls.filter((u) => u.startsWith("https://plc.directory")).length, 1, "the PDS is cached");
	assert.ok(calls[1].startsWith("https://pds.example/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aagent"));
	await repo.putRecord({ collection: "c", rkey: "k", record: {} });
	assert.deepEqual(written[0], ["/xrpc/com.atproto.repo.putRecord", { repo: ME, collection: "c", rkey: "k", record: {} }]);
});