1// Live NIP-77 sync against real relays. Opt in with NOBEL_RELAYS="wss://a ws://b" (space separated).2// Each relay gets a fresh key: some events only local, some only remote, some on both.3import "npm:fake-indexeddb@6/auto";4import { assertEquals } from "jsr:@std/assert@1";5import "../helium/helium.js";6import "../argon/argon.js";7import "../lithium/lithium.js";89const { helium, argon, lithium } = globalThis;10const relays = (Deno.env.get("NOBEL_RELAYS") ?? "").split(/\s+/).filter(Boolean);11const SIZE = Number(Deno.env.get("NOBEL_SIZE") ?? 60);1213// Publishes in parallel, then retries anything rate limited after the relay's hint.14async function publish(url, events) {15 const ws = new WebSocket(url);16 await new Promise((r, j) => (ws.onopen = r, ws.onerror = j));17 const pending = new Map();18 ws.onmessage = (m) => {19 const [t, id, ok, msg] = JSON.parse(m.data);20 if (t === "OK") pending.get(id)?.([ok, msg]);21 };22 const send = (e) => new Promise((r) => {23 pending.set(e.id, r);24 ws.send(JSON.stringify(["EVENT", e]));25 });26 // bind.ws allows 120 events a minute per connection: burst, then 18 every 10s.27 let todo = events, rest = [];28 while (todo.length) {29 const results = await Promise.all(todo.map(send));30 const limited = todo.filter((_, i) => !results[i][0] && /retry in/.test(results[i][1]));31 const bad = results.find(([ok, msg]) => !ok && !/retry in/.test(msg));32 if (bad) throw new Error(`relay refused: ${bad[1]}`);33 rest = [...limited, ...rest];34 todo = rest.splice(0, 18);35 if (todo.length) await new Promise((r) => setTimeout(r, 10_000));36 }37 ws.close();38}3940for (const url of relays) {41 Deno.test({ name: `lithium syncs with ${url}`, sanitizeResources: false, sanitizeOps: false }, async (t) => {42 await argon.remove();43 const sk = helium.generate(), pk = helium.pub(sk), now = Math.floor(Date.now() / 1000);44 const make = (n, tag) => Promise.all(Array.from({ length: n }, (_, i) =>45 helium.finish({ kind: 1, created_at: now - (i % 50), tags: [["t", "nobel-test"]], content: `${tag} ${i}` }, sk)));46 const shared = await make(SIZE, "shared"), local = await make(Math.ceil(SIZE / 3), "local"), remote = await make(Math.ceil(SIZE / 4), "remote");47 await publish(url, [...shared, ...remote]);48 for (const e of [...shared, ...local]) await argon.put(e);49 const filter = { authors: [pk] };5051 await t.step("first sync pulls what is missing and pushes what the relay lacks", async () => {52 const r = await lithium.sync(filter, url);53 assertEquals([r.have.length, r.need.length, r.pushed, r.pulled], [local.length, remote.length, local.length, remote.length]);54 assertEquals((await argon.query([filter])).length, shared.length + local.length + remote.length);55 });5657 await t.step("second sync finds nothing to do", async () => {58 const r = await lithium.sync(filter, url);59 assertEquals([r.have.length, r.need.length], [0, 0]);60 });6162 await t.step("pull mode never publishes", async () => {63 const extra = await make(3, "pull-only");64 for (const e of extra) await argon.put(e);65 const r = await lithium.sync(filter, url, "pull");66 assertEquals([r.have.length, r.pushed, r.need.length], [3, 0, 0]);67 });68 });69}