UNPKG

nostr-relaypool

Version:

A Nostr RelayPool implementation in TypeScript using only nostr-tools library as a dependency.

925 lines (847 loc) 22.7 kB
/* eslint-env jest */ import { getSignature, generatePrivateKey, getEventHash, getPublicKey, type Event, } from "nostr-tools"; import {RelayPool} from "./relay-pool"; import {InMemoryRelayServer} from "./in-memory-relay-server"; let relaypool: RelayPool; // const relayurls = ['wss://nostr-dev.wellorder.net/'] // const relayurls2 = ['wss://nostr.v0l.io/'] const relayurls = ["ws://localhost:8083/"]; const relayurls2 = ["ws://localhost:8084/"]; let _relayServer: InMemoryRelayServer; let _relayServer2: InMemoryRelayServer; beforeAll(() => { _relayServer = new InMemoryRelayServer(8083); _relayServer2 = new InMemoryRelayServer(8084); }); beforeEach(() => { relaypool = new RelayPool([], { subscriptionCache: true, useEventCache: true, }); _relayServer.clear(); _relayServer2.clear(); }); afterEach(async () => { await relaypool.close(); }); afterAll(async () => { await _relayServer.close(); await _relayServer2.close(); }); function createSignedEvent( kind = 27572, content = "nostr-tools test suite" ): Event & {id: string} { const sk = generatePrivateKey(); const pk = getPublicKey(sk); const unsignedEvent = { kind, pubkey: pk, created_at: Math.floor(Date.now() / 1000), tags: [], content, }; const eventId = getEventHash(unsignedEvent); return {id: eventId, ...unsignedEvent, sig: getSignature(unsignedEvent, sk)}; } async function publishAndGetEvent( relays: string[], kind = 27572, content = "nostr-tools test suite" ): Promise<Event & {id: string}> { const event = createSignedEvent(kind, content); relaypool.publish(event, relays); const a = relaypool.getEventById(event.id, relays, Infinity); relaypool.sendSubscriptions(); await a; return event; } test("external geteventbyid", async () => { const event = await publishAndGetEvent(relayurls); var resolve1: (success: boolean) => void; var resolve2: (success: boolean) => void; const promise1 = new Promise((resolve) => { resolve1 = resolve; }); const promise2 = new Promise((resolve) => { resolve2 = resolve; }); const promiseAll = Promise.all([promise1, promise2]); relaypool.close(); relaypool = new RelayPool(relayurls, { externalGetEventById: (id) => { if (id === event.id) { resolve2(true); return event; } }, }); expect(event.kind).toEqual(27572); relaypool.subscribe( [ { kinds: [27572], // Force no caching }, ], relayurls, (event, afterEose, url) => { expect(event).toHaveProperty("id", event.id); expect(afterEose).toBe(false); // expect(url).toBe(relayurls[0]) resolve1(true); }, undefined, undefined ); return expect(promiseAll).resolves.toEqual([true, true]); }); test("empty", async () => { var resolve2: (success: boolean) => void; const promiseAll = Promise.all([ new Promise((resolve) => { resolve2 = resolve; }), ]); relaypool.subscribe( [ { kinds: [27572], // Force no caching }, ], relayurls, (event, afterEose, url) => {}, undefined, (url, minCreatedAt) => { expect(minCreatedAt).toBe(Infinity); expect(url).toBe(relayurls[0]); resolve2(true); } ); return expect(promiseAll).resolves.toEqual([true]); }); test("querying relaypool", async () => { const event = await publishAndGetEvent(relayurls); expect(event.kind).toEqual(27572); var resolve1: (success: boolean) => void; var resolve2: (success: boolean) => void; const promiseAll = Promise.all([ new Promise((resolve) => { resolve1 = resolve; }), new Promise((resolve) => { resolve2 = resolve; }), ]); relaypool.subscribe( [ { kinds: [27572], // Force no caching }, ], relayurls, (event, afterEose, url) => { expect(event).toHaveProperty("id", event.id); expect(afterEose).toBe(false); // expect(url).toBe(relayurls[0]) resolve1(true); }, undefined, (url, minCreatedAt) => { expect(minCreatedAt).toBe(event.created_at); expect(url).toBe(relayurls[0]); resolve2(true); } ); return expect(promiseAll).resolves.toEqual([true, true]); }); test("listening and publishing", async () => { const event = createSignedEvent(); let resolve2: (success: boolean) => void; relaypool.subscribe( [ { kinds: [27572], authors: [event.pubkey], }, ], relayurls, (event) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve2(true); } ); relaypool.publish(event, relayurls); return expect( new Promise((resolve) => { resolve2 = resolve; }) ).resolves.toEqual(true); }); test("relay option in filter", async () => { const event = await publishAndGetEvent(relayurls); var resolve1: (success: boolean) => void; var resolve2: (success: boolean) => void; const promiseAll = Promise.all([ new Promise((resolve) => { resolve1 = resolve; }), new Promise((resolve) => { resolve2 = resolve; }), ]); relaypool.subscribe( [ { kinds: [event.kind], relay: relayurls[0], }, ], [], (event, afterEose, url) => { expect(event).toHaveProperty("id", event.id); expect(afterEose).toBe(false); resolve1(true); }, undefined, (url, minCreatedAt) => { expect(minCreatedAt).toBe(event.created_at); expect(url).toBe(relayurls[0]); resolve2(true); } ); return expect(promiseAll).resolves.toEqual([true, true]); }); test("cached result", async () => { const event = createSignedEvent(); relaypool.publish(event, relayurls); await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [27572], authors: [event.pubkey], }, ], relayurls, (event) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }) ).resolves.toEqual(true); const secondOnEvent = new Promise((resolve) => { relaypool.subscribe( [ { ids: [event.id], }, ], [], (event) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }); return expect(secondOnEvent).resolves.toEqual(true); }); test("remove duplicates", async () => { const event = await publishAndGetEvent(relayurls); await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [27572], authors: [event.pubkey], }, ], relayurls, (event, afterEose, url) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }) ).resolves.toEqual(true); await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [27572], authors: [event.pubkey], }, ], relayurls, (event, afterEose, url) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); relaypool.publish(event, relayurls); }) ).resolves.toEqual(true); let counter = 0; await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [27572], authors: [event.pubkey], noCache: true, }, ], [...relayurls, ...relayurls2], (event, afterEose, url) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); counter += 1; if (counter === 2) { resolve(true); } }, undefined, undefined, {allowDuplicateEvents: true} ); relaypool.publish(event, relayurls); relaypool.publish(event, relayurls2); }) ).resolves.toEqual(true); let counter2 = 0; const thirdOnEvent = new Promise((resolve) => { relaypool.subscribe( [ { authors: [event.pubkey], }, ], [...relayurls, ...relayurls2], (event, afterEose, url) => { expect(event).toHaveProperty("pubkey", event.pubkey); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); counter2 += 1; if (counter2 === 2) { resolve(true); } } ); relaypool.publish(event, relayurls); relaypool.publish(event, relayurls2); }); await expect( Promise.race([ thirdOnEvent, new Promise((resolve) => setTimeout(() => resolve(-1), 50)), ]) ).resolves.toEqual(-1); }); test("cache authors", async () => { let event = createSignedEvent(); let pk = event.pubkey; await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [27572], authors: [pk], }, ], relayurls2, (event, afterEose, url) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); relaypool.publish(event, relayurls2); }) ).resolves.toEqual(true); return expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [27572], authors: [pk], }, ], relayurls2, (event, afterEose, url) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 27572); expect(event).toHaveProperty("content", "nostr-tools test suite"); expect(url).toEqual(undefined); resolve(true); } ); }) ).resolves.toEqual(true); }); test("kind3", async () => { let event = createSignedEvent(3); relaypool.publish(event, relayurls); let pk = event.pubkey; await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [3], authors: [pk], }, ], relayurls, (event) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 3); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }) ).resolves.toEqual(true); const secondOnEvent = new Promise((resolve) => { relaypool.subscribe( [ { ids: [event.id], }, ], [], (event) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 3); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }); return expect(secondOnEvent).resolves.toEqual(true); }); test("kind0", async () => { let event = createSignedEvent(0); relaypool.publish(event, relayurls); let pk = event.pubkey; await expect( new Promise((resolve) => { relaypool.subscribe( [ { kinds: [0], authors: [pk], }, ], relayurls, (event) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 0); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); }, undefined, undefined, {logAllEvents: true} ); }) ).resolves.toEqual(true); console.log("first on event done"); const secondOnEvent = new Promise((resolve) => { relaypool.subscribe( [ { ids: [event.id], }, ], [], (event) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 0); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }); await expect(secondOnEvent).resolves.toEqual(true); const thirdOnEvent = new Promise((resolve) => { relaypool.subscribe( [ { kinds: [0], authors: [pk], }, ], [], (event) => { expect(event).toHaveProperty("pubkey", pk); expect(event).toHaveProperty("kind", 0); expect(event).toHaveProperty("content", "nostr-tools test suite"); resolve(true); } ); }); return expect(thirdOnEvent).resolves.toEqual(true); }); test("getRelayStatuses", async () => { let event = createSignedEvent(0); relaypool.publish(event, relayurls); expect(relaypool.getRelayStatuses()).toEqual([[relayurls[0], 0]]); }); test("nounsub", async () => { let event = createSignedEvent(0); relaypool.publish(event, relayurls); let p2 = new Promise((resolve2) => { new Promise((resolve1) => { let counter = 0; let _sub1 = relaypool.subscribe( filtersByKind(event), relayurls, (event) => { expect(event).toHaveProperty("kind", 0); counter++; if (counter === 1) { let event2 = createSignedEvent(0); relaypool.publish(event2, relayurls); } else if (counter === 2) { resolve2(true); } } ); }); }); await expect(p2).resolves.toEqual(true); }); test("unsub", async () => { let event = createSignedEvent(0); relaypool.publish(event, relayurls); let p2 = new Promise((resolve2) => { new Promise((resolve1) => { let counter = 0; let sub1 = relaypool.subscribe( filtersByKind(event), relayurls, (event) => { expect(event).toHaveProperty("kind", 0); counter++; if (counter === 1) { sub1(); let event2 = createSignedEvent(0); relaypool.publish(event2, relayurls); } else if (counter === 2) { resolve2(true); } } ); }); }); await expect( Promise.race([ p2, new Promise((resolve) => { setTimeout(() => resolve(false), 50); }), ]) ).resolves.toEqual(false); }); test("delay_nounsub", async () => { let event = createSignedEvent(0); relaypool.publish(event, relayurls); let p2 = new Promise((resolve2) => { new Promise((resolve1) => { let counter = 0; let _sub1 = relaypool.subscribe( filtersByKind(event), relayurls, (event) => { expect(event).toHaveProperty("kind", 0); counter++; if (counter === 1) { let event2 = createSignedEvent(0); relaypool.publish(event2, relayurls); } else if (counter === 2) { resolve2(true); } }, 0 ); }); }); await expect( Promise.race([ p2, new Promise((resolve) => { setTimeout(() => resolve(false), 50); }), ]) ).resolves.toEqual(true); }); test("delay_unsub", async () => { let event = createSignedEvent(0); relaypool.publish(event, relayurls); let p2 = new Promise((resolve2) => { new Promise((resolve1) => { let counter = 0; let sub1 = relaypool.subscribe( filtersByKind(event), relayurls, (event) => { expect(event).toHaveProperty("kind", 0); counter++; if (counter === 1) { sub1(); let event2 = createSignedEvent(0); relaypool.publish(event2, relayurls); } else if (counter === 2) { resolve2(true); } }, 0 ); }); }); await expect( Promise.race([ p2, new Promise((resolve) => { setTimeout(() => resolve(false), 50); }), ]) ).resolves.toEqual(false); }); test("unsubscribeOnEose", async () => { let relayServer = new InMemoryRelayServer(8099); let event = createSignedEvent(); relaypool.close(); relaypool = new RelayPool([]); relaypool.publish(event, ["ws://localhost:8099/"]); expect(relayServer.subs.size).toEqual(0); await new Promise((resolve) => { let sub = relaypool.subscribe( filtersByKind(event), ["ws://localhost:8099/"], (event) => { expect(event).toHaveProperty("kind", event.kind); sub(); setTimeout(() => resolve(true), 50); } ); }); expect(_relayServer.subs.size).toEqual(0); let found = false; let p2; let p = new Promise((resolve) => { p2 = new Promise((resolve2) => { let _sub = relaypool.subscribe( filtersByKind(event), ["ws://localhost:8099/"], (event) => { expect(event).toHaveProperty("kind", event.kind); found = true; resolve(true); }, undefined, () => resolve2(true), {unsubscribeOnEose: true} ); }); }); await expect(p).resolves.toEqual(true); await expect(p2).resolves.toEqual(true); expect(found).toEqual(true); await sleepms(50); expect(relayServer.subs.size).toEqual(0); relayServer.close(); }); const filtersByAuthor = (event: Event) => [{authors: [event.pubkey]}]; const filtersByKind = (event: Event) => [{kinds: [event.kind]}]; const _filtersById = (event: Event & {id: string}) => [{ids: [event.id]}]; const sleepms = (timeoutMs: number) => new Promise((resolve) => setTimeout(() => resolve(true), timeoutMs)); // const subscribePromise = ( // relaypool: RelayPool, // filters: Filter[], // relays: string[], // onEventPromise: (resolve: (value: any) => void) => OnEvent, // maxDelayms?: number | undefined, // onEosePromise?: (resolve: (value: any) => void) => OnEose, // options?: SubscriptionOptions // ) => // new Promise((resolve) => // relaypool.subscribe( // filters, // relays, // onEventPromise(resolve), // maxDelayms, // onEosePromise?.(resolve), // options // ) // ); test("subscriptionCache", async () => { let event = createSignedEvent(); relaypool.publish(event, relayurls); expect(_relayServer.totalSubscriptions).toEqual(0); await new Promise((resolve) => { relaypool.subscribe(filtersByKind(event), relayurls, (event) => { resolve(true); }); }); await new Promise((resolve) => { relaypool.subscribe( filtersByAuthor(event), relayurls, (event) => { resolve(true); }, undefined, undefined, {unsubscribeOnEose: true} ); }); await new Promise((resolve) => { relaypool.subscribe( filtersByAuthor(event), relayurls, (event) => { resolve(true); }, undefined, undefined, {unsubscribeOnEose: true} ); }); await sleepms(10); expect(_relayServer.totalSubscriptions).toEqual(2); }); // jest -t 'pool memory' --testTimeout 1000000 --logHeapUsage // PASS ./relay-pool.test.ts (92.9 s, 348 MB heap size) test.skip("pool memory usage", async () => { console.log("creating new relaypool"); relaypool.close(); relaypool = new RelayPool(relayurls); relaypool.relayByUrl.forEach((relay) => { // @ts-ignore relay.relay.logging = false; }); await publishAndGetEvent(relayurls, 100, "x".repeat(20 * 1024 * 1024)); for (let i = 0; i < 300; i++) { await new Promise((resolve) => { const unsub = relaypool.subscribe([{}], relayurls, (event) => { unsub(); resolve(true); }); }); } }); test("delayfiltering", async () => { let event = createSignedEvent(); relaypool.publish(event, relayurls); expect(_relayServer.totalSubscriptions).toEqual(0); await new Promise((resolve) => { relaypool.subscribe(filtersByKind(event), relayurls, (event) => { resolve(true); }); }); expect(_relayServer.totalSubscriptions).toEqual(1); relaypool.close(); relaypool = new RelayPool([], {}); const p2 = new Promise((resolve) => { relaypool.subscribe( [{ids: ["notfound"], authors: [event.pubkey]}], relayurls, (event) => { resolve(false); }, 1, undefined, {unsubscribeOnEose: true} ); }); const p1 = new Promise((resolve) => { relaypool.subscribe( [{ids: [event.id], authors: [event.pubkey]}], relayurls, (event) => { resolve(true); }, 1, undefined, {unsubscribeOnEose: true} ); }); await p1; expect(await p1).toEqual(true); expect(await Promise.race([p2, sleepms(10).then(() => true)])).toEqual(true); await sleepms(10); expect(_relayServer.totalSubscriptions).toEqual(2); }); test("auth", async () => { _relayServer.auth = "123"; relaypool.close(); const relaypool2 = new RelayPool(relayurls, { subscriptionCache: true, useEventCache: true, }); await expect( new Promise((resolve) => { relaypool2.onauth(() => resolve(true)); }) ).resolves.toBe(true); await relaypool2.close(); }); test("dontSendOtherFilters", async () => { const event = createSignedEvent(); relaypool.publish(event, relayurls); const never = new Promise((resolve) => { relaypool.subscribe( filtersByKind(event), relayurls, (_event) => { resolve(true); }, Infinity, undefined, {unsubscribeOnEose: true} ); }); await new Promise((resolve) => { relaypool.subscribe( filtersByKind(event), relayurls, (_event) => { resolve(true); }, undefined, undefined, {unsubscribeOnEose: false, dontSendOtherFilters: true} ); }); const neverResult = await Promise.race([ never, sleepms(10).then(() => false), ]); expect(neverResult).toEqual(false); });