clone

tiny dependency-free nostr libraries for plain html, each under 5kb and named for an element. public domain

main
nobel / test / lithium.test.js
lithium.test.js 2.8K
1// Reconciles lithium's initiator against bindws's Negentropy responder (a port of go-nostr's).2import { assertEquals } from "jsr:@std/assert@1";3import { sha256 } from "npm:@noble/hashes@1.8.0/sha2";4import "../lithium/lithium.js";56const { lithium } = globalThis;7const peer = new URL("../../bindws/src/negentropy.ts", import.meta.url);8const hasPeer = await Deno.stat(peer).then(() => true, () => false);9const { Negentropy, hexToBytes, bytesToHex } = hasPeer ? await import(peer.href) : {};1011const rid = () => [...crypto.getRandomValues(new Uint8Array(32))].map((x) => x.toString(16).padStart(2, "0")).join("");12const item = (t, id = rid()) => ({ t, id, b: hexToBytes(id) });13const sorted = (xs) => [...xs].sort((a, b) => a.t - b.t || (a.id < b.id ? -1 : 1));1415async function reconcile(mine, theirs) {16 const client = lithium.session(sorted(mine));17 const server = new Negentropy(theirs.map((x) => ({ timestamp: x.t, id: x.b })), (b) => sha256(b));18 let msg = await client.start(), rounds = 0;19 const have = new Set(), need = new Set();20 for (;;) {21 const { reply } = server.reconcile(hexToBytes(msg));22 const r = await client.reconcile(bytesToHex(reply));23 r.have.forEach((x) => have.add(x));24 r.need.forEach((x) => need.add(x));25 rounds++;26 if (!r.reply) break;27 msg = r.reply;28 }29 return { have, need, rounds };30}3132function check(mine, theirs, got) {33 const m = new Set(mine.map((x) => x.id)), t = new Set(theirs.map((x) => x.id));34 assertEquals([...got.have].sort(), [...m].filter((x) => !t.has(x)).sort());35 assertEquals([...got.need].sort(), [...t].filter((x) => !m.has(x)).sort());36}3738Deno.test({ name: "lithium reconciles with bindws negentropy", ignore: !hasPeer }, async (t) => {39 const cases = {40 "both empty": [0, 0, 0],41 "only mine": [40, 0, 0],42 "only theirs": [0, 40, 0],43 "around the 32 id list threshold": [31, 33, 16],44 "identical 3000": [0, 0, 3000],45 "5000 each, mostly shared": [300, 200, 4500],46 "small diff in 20000": [3, 5, 20000],47 };48 for (const [name, [a, b, shared]] of Object.entries(cases)) {49 await t.step(name, async () => {50 const common = Array.from({ length: shared }, (_, i) => item(1_700_000_000 + (i % 997)));51 const mine = [...common, ...Array.from({ length: a }, (_, i) => item(1_700_000_000 + i * 7))];52 const theirs = [...common, ...Array.from({ length: b }, (_, i) => item(1_700_000_000 + i * 3))];53 check(mine, theirs, await reconcile(mine, theirs));54 });55 }56 await t.step("shared timestamps force id prefix bounds", async () => {57 const same = Array.from({ length: 2000 }, () => item(1_700_000_000));58 const mine = [...same, ...Array.from({ length: 10 }, () => item(1_700_000_000))];59 const theirs = [...same.slice(50), ...Array.from({ length: 10 }, () => item(1_700_000_000))];60 check(mine, theirs, await reconcile(mine, theirs));61 });62});