nostr-relaypool
Version:
A Nostr RelayPool implementation in TypeScript using only nostr-tools library as a dependency.
925 lines (847 loc) • 22.7 kB
text/typescript
/* 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);
});