import type { NostrEvent } from "@nostr-fetch/kernel/nostr"; import { collectAsyncIter } from "@nostr-fetch/testutil/asyncIter"; import { pubkeyFromAuthorName } from "@nostr-fetch/testutil/fakeEvent"; import { compareNostrEvents } from "./fetcherHelper"; import { FakedFetcherBuilder } from "./testutil/fakedFetcher"; import { verifyEventSig } from "@nostr-fetch/kernel/crypto"; import { assert, describe, expect, test, vi } from "vitest"; /* Tests */ describe.concurrent("NostrFetcher", () => { const fetcher = new FakedFetcherBuilder() .addRelay("wss://relay1/", { eventsSpec: [ { content: "test1 early", createdAt: { since: 0, until: 999 }, n: 10, }, { content: "test1 within range", createdAt: { since: 1000, until: 2000 }, n: 10, }, { content: "test1 late", createdAt: { since: 2001, until: 3000 }, n: 10, }, ], }) .addRelay("wss://relay2/", { eventsSpec: [ { content: "test2 early", createdAt: { since: 0, until: 999 }, n: 10, }, { content: "test2 within range", createdAt: { since: 1000, until: 2000 }, n: 10, }, { content: "test2 late", createdAt: { since: 2001, until: 3000 }, n: 10, }, ], }) .addRelay("wss://relay3/", { eventsSpec: [ { content: "test3 early", createdAt: { since: 0, until: 999 }, n: 10, }, { content: "test3 within range", createdAt: { since: 1000, until: 2000 }, n: 10, }, { content: "test3 late", createdAt: { since: 2001, until: 3000 }, n: 10, }, ], }) .addRelay("wss://dup1/", { eventsSpec: [{ content: "dup" }], }) .addRelay("wss://dup2/", { eventsSpec: [{ content: "dup" }], }) .addRelay("wss://healthy/", { eventsSpec: [{ content: "healthy", n: 10 }], }) .addRelay("wss://invalid-sig/", { eventsSpec: [{ content: "invalid sig", invalidSig: true }], }) .addRelay("wss://unreachable/", { eventsSpec: [{ content: "unreachable" }], connectable: false, }) .addRelay("wss://slow-to-connect/", { eventsSpec: [{ content: "slow to connect" }], connectDurMs: 2000, }) .addRelay("wss://slow-to-return-events/", { eventsSpec: [{ content: "slow to return events" }], sendEventInterval: 1000, }) .addRelay("wss://delayed/", { eventsSpec: [{ content: "delayed", n: 10 }], sendEventInterval: 100, }) .addRelay("wss://search/", { eventsSpec: [{ content: "search", n: 10 }], supportedNips: [50], }) .addRelay("wss://latest1/", { eventsSpec: [ { content: "test1 old", createdAt: { since: 0, until: 500 }, n: 10 }, { content: "test1 middle", createdAt: { since: 600, until: 800 }, n: 10 }, { content: "test1 latest", createdAt: { since: 1000, until: 2000 }, n: 10 }, ], sendEventInterval: 5, }) .addRelay("wss://latest2/", { eventsSpec: [ { content: "test2 old", createdAt: { since: 0, until: 500 }, n: 10 }, { content: "test2 middle", createdAt: { since: 600, until: 800 }, n: 10 }, { content: "test2 latest", createdAt: { since: 1000, until: 2000 }, n: 10 }, ], sendEventInterval: 10, }) .addRelay("wss://latest3-with-invalid-sig/", { eventsSpec: [ { content: "test3 old", createdAt: { since: 0, until: 500 }, n: 10 }, { content: "test3 near-latest", createdAt: 999, n: 1 }, { content: "test3 latest", createdAt: { since: 1000, until: 2000 }, n: 9 }, { content: "test3 invalid", createdAt: { since: 1000, until: 2000 }, invalidSig: true, n: 1, }, ], sendEventInterval: 10, }) .addRelay("wss://last-has-invalid-sig/", { eventsSpec: [{ content: "invalid", createdAt: 2001, invalidSig: true }], }) .addRelay("wss://per-author1/", { eventsSpec: [ { content: "test1", authorName: "alice", createdAt: { since: 0, until: 999 }, n: 10 }, { content: "test1", authorName: "bob", createdAt: { since: 1000, until: 1999 }, n: 10 }, { content: "test1", authorName: "cat", createdAt: { since: 2000, until: 2999 }, n: 10 }, { content: "test1 bob last", authorName: "bob", createdAt: 5000 }, { content: "test1 alice 2nd", authorName: "alice", createdAt: 4999 }, ], sendEventInterval: 5, }) .addRelay("wss://per-author2/", { eventsSpec: [ { content: "test2", authorName: "alice", createdAt: { since: 0, until: 999 }, n: 10 }, { content: "test2", authorName: "bob", createdAt: { since: 1000, until: 1999 }, n: 10 }, { content: "test2", authorName: "cat", createdAt: { since: 2000, until: 2999 }, n: 10 }, { content: "test2 cat last", authorName: "cat", createdAt: 5000 }, { content: "test2 bob 2nd", authorName: "bob", createdAt: 4999 }, ], sendEventInterval: 5, }) .addRelay("wss://per-author3/", { eventsSpec: [ { content: "test3", authorName: "alice", createdAt: { since: 0, until: 999 }, n: 10 }, { content: "test3", authorName: "bob", createdAt: { since: 1000, until: 1999 }, n: 10 }, { content: "test3", authorName: "cat", createdAt: { since: 2000, until: 2999 }, n: 10 }, { content: "test3 alice last", authorName: "alice", createdAt: 5000 }, { content: "test3 cat 2nd", authorName: "cat", createdAt: 4999 }, ], sendEventInterval: 5, }) .addRelay("wss://kinds-1/", { eventsSpec: [ { content: "text", kind: 1, createdAt: { since: 0, until: 999 }, n: 10 }, { content: "+", kind: 7, createdAt: { since: 1000, until: 1999 }, n: 5 }, ], }) .addRelay("wss://kinds-2/", { eventsSpec: [ { content: "+", kind: 7, createdAt: { since: 1000, until: 1999 }, n: 5 }, { content: "chan msg", kind: 42, createdAt: { since: 2000, until: 2999 }, n: 10 }, ], }) // 5 events for each p-tag in total .addRelay("wss://single-ptag/", { eventsSpec: [ { content: "to alice", tags: [["p", "alice"]], n: 2 }, { content: "to bob", tags: [["p", "bob"]], n: 2 }, { content: "to cat", tags: [["p", "cat"]], n: 2 }, ], }) .addRelay("wss://per-author-as-of1/", { eventsSpec: [ { content: "A old", authorName: "alice", createdAt: { since: 0, until: 100 }, n: 10 }, { content: "A middle", authorName: "alice", createdAt: { since: 200, until: 400 }, n: 10 }, { content: "B old", authorName: "bob", createdAt: { since: 0, until: 100 }, n: 10 }, { content: "B latest", authorName: "bob", createdAt: { since: 600, until: 700 }, n: 10 }, { content: "C middle", authorName: "cat", createdAt: { since: 200, until: 400 }, n: 10 }, { content: "C latest", authorName: "cat", createdAt: { since: 600, until: 700 }, n: 10 }, ], }) .addRelay("wss://per-author-as-of2/", { eventsSpec: [ { content: "A latest", authorName: "alice", createdAt: { since: 600, until: 700 }, n: 10 }, { content: "B middle", authorName: "bob", createdAt: { since: 200, until: 400 }, n: 10 }, { content: "C old", authorName: "cat", createdAt: { since: 0, until: 100 }, n: 10 }, ], }) .addRelay("wss://multi-ptag/", { eventsSpec: [ { content: "to alice and bob", tags: [ ["p", "alice"], ["p", "bob"], ], }, { content: "to alice and cat", tags: [ ["p", "alice"], ["p", "cat"], ], }, { content: "to bob and cat", tags: [ ["p", "bob"], ["p", "cat"], ], }, { content: "to all", tags: [ ["p", "alice"], ["p", "bob"], ["p", "cat"], ], }, ], }) .buildFetcher(); describe.concurrent("allEventsIterator", () => { test("fetches all events (single relay)", async () => { const evIter = fetcher.allEventsIterator(["wss://relay1/"], {}, {}, { limitPerReq: 5 }); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(30); }); test("fetches all events (multiple relays)", async () => { const evIter = fetcher.allEventsIterator( ["wss://relay1/", "wss://relay2/", "wss://relay3/"], {}, {}, { limitPerReq: 5 }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(90); }); test("fetches all events within time range (single relay)", async () => { const evIter = fetcher.allEventsIterator(["wss://relay1/"], {}, { since: 1000, until: 2000 }); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(10); assert(evs.every(({ content }) => content.includes("within range"))); }); test("fetches all events within time range (multiple relays)", async () => { const evIter = fetcher.allEventsIterator( ["wss://relay1/", "wss://relay2/", "wss://relay3/"], {}, { since: 1000, until: 2000 }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(30); assert(evs.every(({ content }) => content.includes("within range"))); }); test("throws error if filter has invalid tag queries", () => { expect(() => { fetcher.allEventsIterator( ["wss://healthy/"], { "#invalid": ["invalid"], "#malformed": ["malformed"], "#e": ["foo"] }, {}, ); }).toThrow("Filter has invalid tag queries: #invalid, #malformed"); }); test("throws error if time range is invalid", () => { expect(() => { fetcher.allEventsIterator(["wss://healthy/"], {}, { since: 1, until: 0 }); }).toThrow("Invalid time range (since > until)"); }); test("dedups events based on event id", async () => { const evIter = fetcher.allEventsIterator(["wss://dup1/", "wss://dup2/"], {}, {}); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(1); }); test("sends the same event multiple times with updated seenOn", async () => { const evIter = fetcher.allEventsIterator( ["wss://dup1/", "wss://dup2/"], {}, {}, { withSeenOn: true }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(2); const [fst, snd] = evs.map((ev) => ev.seenOn) as [string[], string[]]; assert(fst.length === 1 && (fst.includes("wss://dup1/") || fst.includes("wss://dup2/"))); expect(snd).toEqual(expect.arrayContaining(["wss://dup1/", "wss://dup2/"])); }); test("verifies signature by default, with specified verifier", async () => { const spyEventVerifier = vi.fn((ev: NostrEvent) => Promise.resolve(verifyEventSig(ev))); const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://invalid-sig/"], {}, {}, { eventVerifier: spyEventVerifier }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(10); assert(evs.every(({ content }) => content.includes("healthy"))); expect(spyEventVerifier).toBeCalled(); }); test("skips signature verification if skipVerification is true", async () => { const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://invalid-sig/"], {}, {}, { skipVerification: true }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(11); assert( evs.every(({ content }) => content.includes("healthy") || content.includes("invalid sig")), ); }); test("ignores unreachable relays", async () => { const evIter = fetcher.allEventsIterator(["wss://healthy/", "wss://unreachable/"], {}, {}); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(10); assert(evs.every(({ content }) => content.includes("healthy"))); }); test("skips slow-to-connect relays if timeout exceeds", async () => { const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://slow-to-connect/"], {}, {}, { connectTimeoutMs: 1000 }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(10); assert(evs.every(({ content }) => content.includes("healthy"))); }); test("waits slow-to-connect relays until timeout", async () => { const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://slow-to-connect/"], {}, {}, { connectTimeoutMs: 3000 }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(11); assert( evs.every( ({ content }) => content.includes("healthy") || content.includes("slow to connect"), ), ); }); test("cut off slow-to-return-events relays if timeout exceeds", async () => { const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://slow-to-return-events/"], {}, {}, { abortSubBeforeEoseTimeoutMs: 100 }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(10); assert(evs.every(({ content }) => content.includes("healthy"))); }); test("waits slow-to-return-events relays until timeout", async () => { const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://slow-to-return-events/"], {}, {}, { abortSubBeforeEoseTimeoutMs: 2000 }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(11); assert( evs.every( ({ content }) => content.includes("healthy") || content.includes("slow to return event"), ), ); }); test("can be aborted by AbortController", async () => { const ac = new AbortController(); setTimeout(() => { ac.abort(); }, 500); const evIter = fetcher.allEventsIterator( ["wss://delayed/"], {}, {}, { abortSignal: ac.signal }, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBeLessThan(10); }); test("uses only searchable relays (supports NIP-50) if the filter contains search field", async () => { const evIter = fetcher.allEventsIterator( ["wss://healthy/", "wss://search/"], { search: "search" }, {}, ); const evs = await collectAsyncIter(evIter); expect(evs.length).toBe(10); assert(evs.every(({ content }) => content.includes("search"))); }); }); describe.concurrent("fetchAllEvents", () => { test("throws error if filter has invalid tag queries", async () => { await expect(() => fetcher.fetchAllEvents( ["wss://healthy/"], { "#invalid": ["invalid"], "#malformed": ["malformed"], "#e": ["foo"] }, {}, ), ).rejects.toThrow("Filter has invalid tag queries: #invalid, #malformed"); }); test("throws error if time range is invalid", async () => { await expect(() => fetcher.fetchAllEvents(["wss://healthy/"], {}, { since: 1, until: 0 }), ).rejects.toThrow("Invalid time range (since > until)"); }); test("sorts result if sort: true", async () => { const evs = await fetcher.fetchAllEvents( ["wss://relay1/", "wss://relay2/", "wss://relay3/"], {}, {}, { sort: true }, ); const sorted = evs.slice().sort(compareNostrEvents); expect(evs).toStrictEqual(sorted); }); test("withSeenOn: true works correctly", async () => { const evs = await fetcher.fetchAllEvents( ["wss://dup1/", "wss://dup2/"], {}, {}, { withSeenOn: true }, ); expect(evs.length).toBe(1); expect(evs[0]?.seenOn).toEqual(expect.arrayContaining(["wss://dup1/", "wss://dup2/"])); }); }); describe.concurrent("fetchLatestEvents", () => { test("throws error if filter has invalid tag queries", async () => { await expect(() => fetcher.fetchLatestEvents( ["wss://healthy/"], { "#invalid": ["invalid"], "#malformed": ["malformed"], "#e": ["foo"] }, 100, ), ).rejects.toThrow("Filter has invalid tag queries: #invalid, #malformed"); }); test("throws error if limit <= 0", async () => { await expect(() => fetcher.fetchLatestEvents(["wss://healthy/"], {}, 0)).rejects.toThrow( '"limit" should be positive number', ); }); test("fetches latest N events", async () => { const evs = await fetcher.fetchLatestEvents(["wss://latest1/", "wss://latest2/"], {}, 20); expect(evs.length).toBe(20); assert(evs.every(({ content }) => content.includes("latest"))); const sorted = evs.slice().sort(compareNostrEvents); expect(evs).toStrictEqual(sorted); }); test("fetches latest N events as of specified time", async () => { // "middle" events have timestamp 600-800 const evs = await fetcher.fetchLatestEvents(["wss://latest1/", "wss://latest2/"], {}, 20, { asOf: 900, }); expect(evs.length).toBe(20); assert(evs.every(({ content }) => content.includes("middle"))); const sorted = evs.slice().sort(compareNostrEvents); expect(evs).toStrictEqual(sorted); }); test("moves up near-latest events in reduced verification mode", async () => { const evs = await fetcher.fetchLatestEvents( ["wss://latest1/", "wss://latest3-with-invalid-sig/"], {}, 20, { reduceVerification: true, }, ); expect(evs.length).toBe(20); assert(evs.every(({ content }) => content.includes("latest"))); assert(evs.some(({ content }) => content.includes("near-latest"))); }); test("withSeenOn: true works correctly", async () => { const evs = await fetcher.fetchLatestEvents(["wss://dup1/", "wss://dup2/"], {}, 1, { withSeenOn: true, }); expect(evs.length).toBe(1); expect(evs[0]?.seenOn).toEqual(expect.arrayContaining(["wss://dup1/", "wss://dup2/"])); }); }); describe.concurrent("fetchLastEvent", () => { test("moves up second-last event in reduced verification mode", async () => { const ev = await fetcher.fetchLastEvent( ["wss://latest1/", "wss://last-has-invalid-sig/"], {}, { reduceVerification: true, }, ); expect(ev).not.toBeUndefined(); assert(ev?.content?.includes("latest")); }); test("returns undefined if no events match the filter", async () => { const ev = await fetcher.fetchLastEvent(["wss://healthy/"], { authors: [pubkeyFromAuthorName("nobody")], // in "healthy" relay, the author of all events is "test" }); expect(ev).toBeUndefined(); }); }); describe.concurrent("fetchLatestEventsPerKey", () => { test("throws error if keyName is an invalid tag query key", () => { expect(() => { fetcher.fetchLatestEventsPerKey("#invalid", [], { "#e": ["foo"] }, 100); }).toThrow("Specified key '#invalid' is invalid tag query key"); }); test("throws error if otherFilter has invalid tag queries", () => { expect(() => { fetcher.fetchLatestEventsPerKey( "authors", [], { "#invalid": ["invalid"], "#malformed": ["malformed"], "#e": ["foo"] }, 100, ); }).toThrow("Filter has invalid tag queries: #invalid, #malformed"); }); const pkA = pubkeyFromAuthorName("alice"); const pkB = pubkeyFromAuthorName("bob"); const pkC = pubkeyFromAuthorName("cat"); test("relay set for all authors", async () => { const iter = fetcher.fetchLatestEventsPerKey( "authors", { keys: [pkA, pkB, pkC], relayUrls: ["wss://per-author1/", "wss://per-author2/", "wss://per-author3/"], }, {}, 5, ); const authors: string[] = []; for await (const { key: author, events } of iter) { authors.push(author); expect(events.length).toBe(5); assert(events.every((ev) => ev.pubkey === author)); // check if events are sorted const sorted = events.slice().sort(compareNostrEvents); expect(events).toStrictEqual(sorted); } // all events of pkC > all events of pkB > all events of pkA, where `>` is "after than" // so events of pkC should be returned first, then events of pkB, then events of pkA. expect(authors).toStrictEqual([pkC, pkB, pkA]); }); test("relay set for all authors (w/ duplicated key)", async () => { const iter = fetcher.fetchLatestEventsPerKey( "authors", { keys: [pkA, pkB, pkC, pkC, pkB, pkA], relayUrls: ["wss://per-author1/", "wss://per-author2/", "wss://per-author3/"], }, {}, 5, ); const authors: string[] = []; for await (const { key: author, events } of iter) { authors.push(author); expect(events.length).toBe(5); assert(events.every((ev) => ev.pubkey === author)); // check if events are sorted const sorted = events.slice().sort(compareNostrEvents); expect(events).toStrictEqual(sorted); } // all events of pkC > all events of pkB > all events of pkA, where `>` is "after than" // so events of pkC should be returned first, then events of pkB, then events of pkA. expect(authors).toStrictEqual([pkC, pkB, pkA]); }); test("relay set per author", async () => { const relaySetPerAuthor = new Map([ [pkA, ["wss://per-author1/", "wss://per-author2/"]], [pkB, ["wss://per-author2/", "wss://per-author3/"]], [pkC, ["wss://per-author3/", "wss://per-author1/"]], ]); const eventsPerAuthor = new Map(); const iter = fetcher.fetchLatestEventsPerKey("authors", relaySetPerAuthor, {}, 5); for await (const { key: author, events } of iter) { eventsPerAuthor.set(author, events); expect(events.length).toBe(5); assert(events.every((ev) => ev.pubkey === author)); // check if events are sorted const sorted = events.slice().sort(compareNostrEvents); expect(events).toStrictEqual(sorted); } // check if events are fetched from only specified relays for each author assert( eventsPerAuthor .get(pkA) ?.every(({ content }) => content.includes("test1") || content.includes("test2")), ); assert( eventsPerAuthor .get(pkB) ?.every(({ content }) => content.includes("test2") || content.includes("test3")), ); assert( eventsPerAuthor .get(pkC) ?.every(({ content }) => content.includes("test3") || content.includes("test1")), ); }); test("relay set per author (w/ duplicated key)", async () => { const relaySetPerAuthor: [string, string[]][] = [ [pkA, ["wss://per-author1/", "wss://per-author2/"]], [pkA, ["wss://per-author2/"]], [pkB, ["wss://per-author2/", "wss://per-author3/"]], [pkB, ["wss://per-author3/"]], [pkC, ["wss://per-author3/", "wss://per-author1/"]], [pkC, ["wss://per-author1/"]], ]; const eventsPerAuthor = new Map(); const iter = fetcher.fetchLatestEventsPerKey("authors", relaySetPerAuthor, {}, 5); for await (const { key: author, events } of iter) { eventsPerAuthor.set(author, events); expect(events.length).toBe(5); assert(events.every((ev) => ev.pubkey === author)); // check if events are sorted const sorted = events.slice().sort(compareNostrEvents); expect(events).toStrictEqual(sorted); } // check if events are fetched from only specified relays for each author assert( eventsPerAuthor .get(pkA) ?.every(({ content }) => content.includes("test1") || content.includes("test2")), ); assert( eventsPerAuthor .get(pkB) ?.every(({ content }) => content.includes("test2") || content.includes("test3")), ); assert( eventsPerAuthor .get(pkC) ?.every(({ content }) => content.includes("test3") || content.includes("test1")), ); }); test("asOf works correctly", async () => { const iter = fetcher.fetchLatestEventsPerKey( "authors", { keys: [pkA, pkB, pkC], relayUrls: ["wss://per-author-as-of1/", "wss://per-author-as-of2/"], }, {}, 10, { asOf: 500 }, ); for await (const { events } of iter) { expect(events.length).toBe(10); assert(events.every((ev) => ev.content.includes("middle"))); // check if events are sorted const sorted = events.slice().sort(compareNostrEvents); expect(events).toStrictEqual(sorted); } }); test("withSeenOn: true works correctly", async () => { const iter = fetcher.fetchLatestEventsPerKey( "authors", { keys: [pubkeyFromAuthorName("test")], relayUrls: ["wss://dup1/", "wss://dup2/"] }, {}, 1, { withSeenOn: true, }, ); const res = await collectAsyncIter(iter); expect(res.length).toBe(1); expect(res[0]?.events[0]?.seenOn).toEqual( expect.arrayContaining(["wss://dup1/", "wss://dup2/"]), ); }); test("fetch events per kind", async () => { const iter = fetcher.fetchLatestEventsPerKey( "kinds", { keys: [1, 7, 42], relayUrls: ["wss://kinds-1/", "wss://kinds-2/"] }, {}, 10, ); for await (const { key: kind, events } of iter) { expect(events.length).toBe(10); assert(events.every((ev) => ev.kind === kind)); // check if events are sorted const sorted = events.slice().sort(compareNostrEvents); expect(events).toStrictEqual(sorted); } }); test("fetch events per p-tag", async () => { const iter = fetcher.fetchLatestEventsPerKey( "#p", { keys: ["alice", "bob", "cat"], relayUrls: ["wss://single-ptag/", "wss://multi-ptag/"] }, {}, 5, ); for await (const { key: pTag, events } of iter) { expect(events.length).toBe(5); assert( events.every((ev) => ev.tags.filter((t) => t[0] === "p").some((t) => t[1] === pTag)), ); } }); }); describe.concurrent("fetchLastEventPerkey", () => { const pkA = pubkeyFromAuthorName("alice"); const pkB = pubkeyFromAuthorName("bob"); const pkC = pubkeyFromAuthorName("cat"); test("single relay set for all authors", async () => { const iter = fetcher.fetchLastEventPerKey( "authors", { keys: [pkA, pkB, pkC], relayUrls: ["wss://per-author1/", "wss://per-author2/", "wss://per-author3/"], }, {}, ); const authors: string[] = []; for await (const { key: author, event } of iter) { authors.push(author); // check if the fetched event is actually the last event from the author assert(event !== undefined && event.pubkey === author && event.content.includes("last")); } // check if we got the last event for all authors expect(authors).toEqual(expect.arrayContaining([pkA, pkB, pkC])); }); test("relay set per author", async () => { // for each author, only the 2nd-last event can be found const relaySetPerAuthor = new Map([ [pkA, ["wss://per-author1/", "wss://per-author2/"]], [pkB, ["wss://per-author2/", "wss://per-author3/"]], [pkC, ["wss://per-author3/", "wss://per-author1/"]], ]); const iter = fetcher.fetchLastEventPerKey("authors", relaySetPerAuthor, {}); const authors: string[] = []; for await (const { key: author, event } of iter) { authors.push(author); // check if the fetched event is the 2nd-last event from the author assert(event !== undefined && event.pubkey === author && event.content.includes("2nd")); } // check if we got the last event for all authors expect(authors).toEqual(expect.arrayContaining([pkA, pkB, pkC])); }); }); });