diff --git a/readme.md b/readme.md index 0d7871d..20ca115 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ Only javascript dependancy is [nostr-tools](https://github.com/nbd-wtf/nostr-too ## Features -Supported profile events: kind `0`, `2`, `10002` and `3`. +Supported profile events: kind `0`, `10002` and `3`. ##### Backup and Restore @@ -44,7 +44,6 @@ Supported profile events: kind `0`, `2`, `10002` and `3`. ##### Lightweight and Efficent - [ ] only javascript dependancy is nostr-tools (TODO: remove timeago) - [x] connects to the minimum number of relays - - [ ] connect relays specified in `10002` or `2` - - [ ] if no `10002` or `2` events are found it crawls through a number of popular relays to ensure it has your latest profile events. (currently it just connects to damus) + - [x] connect relays specified in `10002` or 3 default relays - [x] minimises the number of open websockets -- [ ] use blastr relay to send profile events far and wide +- [x] use blastr relay to send profile events far and wide diff --git a/src/RelayManagement.ts b/src/RelayManagement.ts index eaa7cc1..009fee3 100644 --- a/src/RelayManagement.ts +++ b/src/RelayManagement.ts @@ -1,23 +1,25 @@ -import { Event, Relay, relayInit } from 'nostr-tools'; +import { Event, SimplePool } from 'nostr-tools'; -let drelay: Relay; -export const setupDefaultRelays = async ():Promise => { - if (typeof drelay !== 'undefined') return new Promise((r) => { r(); }); - drelay = relayInit('wss://relay.damus.io'); - return drelay.connect(); -}; -/** setupMyRelays TODO */ -export const setupMyRelays = async () => setupDefaultRelays(); +const pool = new SimplePool(); +let currentrelays = [ + 'wss://relay.damus.io', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr-relay.wlvs.space', +]; export const requestMyProfileFromRelays = async ( pubkey:string, eventProcesser: (event: Event) => void, + relays?:string[], ) => { - await setupDefaultRelays(); - const sub = drelay.sub([{ - kinds: [0, 2, 10002, 3], - authors: [pubkey], - }]); + if (relays) currentrelays = relays; + const sub = pool.sub( + currentrelays, + [{ + kinds: [0, 2, 10002, 3], + authors: [pubkey], + }], + ); return new Promise((r) => { sub.on('event', (event:Event) => { if ( @@ -28,15 +30,13 @@ export const requestMyProfileFromRelays = async ( } }); sub.on('eose', () => { - // sub.unsub(); r(); }); }); }; export const publishEventToRelay = async (event:Event):Promise => { - await setupDefaultRelays(); - const pub = drelay.publish(event); + const pub = pool.publish(currentrelays, event); return new Promise((r) => { pub.on('ok', () => r(true)); pub.on('failed', () => r(false)); diff --git a/src/fetchEvents.test.ts b/src/fetchEvents.test.ts index 5148fe2..c3dc1ea 100644 --- a/src/fetchEvents.test.ts +++ b/src/fetchEvents.test.ts @@ -205,41 +205,171 @@ describe('', () => { }); }); describe('when isUptodate returns false', () => { - const mockstoreMyProfileEvent = jest.spyOn(FetchEvents, 'storeMyProfileEvent'); - beforeEach(async () => { - mockisUptodate.mockReturnValue(false); - mockrequestMyProfileFromRelays.mockReset() - .mockImplementation(async (_pubkey, eventProcessor) => { - eventProcessor({ ...SampleEvents.kind0 }); - eventProcessor({ ...SampleEvents.kind3 }); + describe('and when cached 10002 event is present', () => { + const doBefore = async () => { + mockisUptodate.mockReturnValue(false); + mockrequestMyProfileFromRelays.mockReset() + .mockImplementation(async (_pubkey, eventProcessor) => { + eventProcessor({ ...SampleEvents.kind0 }); + eventProcessor({ ...SampleEvents.kind3 }); + }); + await fetchMyProfileEvents(SampleEvents.kind0.pubkey, mockEventProcessor); + }; + test('1 write relays, function called with custom relay and 2 default relays + blaster', async () => { + storeMyProfileEvent({ + ...SampleEvents.kind10002, + tags: [ + ['r', 'wss://alicerelay.example.com'], + ], }); - await fetchMyProfileEvents(SampleEvents.kind0.pubkey, mockEventProcessor); + await doBefore(); + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + expect.anything(), + expect.anything(), + [ + 'wss://alicerelay.example.com', + 'wss://relay.damus.io', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr.mutinywallet.com', + ], + ); + }); + test('2 write relays function called with custom relays and 1 default relays + blaster', async () => { + storeMyProfileEvent({ + ...SampleEvents.kind10002, + tags: [ + ['r', 'wss://alicerelay.example.com'], + ['r', 'wss://expensive-relay.example2.com', 'write'], + ], + }); + await doBefore(); + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + expect.anything(), + expect.anything(), + [ + 'wss://alicerelay.example.com', + 'wss://expensive-relay.example2.com', + 'wss://relay.damus.io', + 'wss://nostr.mutinywallet.com', + ], + ); + }); + test('2 write relays including first defauly relay. function called with custom relays and 1 different default relays + blaster', async () => { + storeMyProfileEvent({ + ...SampleEvents.kind10002, + tags: [ + ['r', 'wss://relay.damus.io'], + ['r', 'wss://expensive-relay.example2.com', 'write'], + ], + }); + await doBefore(); + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + expect.anything(), + expect.anything(), + [ + 'wss://relay.damus.io', + 'wss://expensive-relay.example2.com', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr.mutinywallet.com', + ], + ); + }); + test('with 4 write relays function called with all custom relays + blaster', async () => { + storeMyProfileEvent({ + ...SampleEvents.kind10002, + tags: [ + ['r', 'wss://alicerelay.example.com'], + ['r', 'wss://brando-relay.com'], + ['r', 'wss://expensive-relay.example2.com', 'write'], + ['r', 'wss://alicerelay.example3.com'], + ], + }); + await doBefore(); + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + expect.anything(), + expect.anything(), + [ + 'wss://alicerelay.example.com', + 'wss://brando-relay.com', + 'wss://expensive-relay.example2.com', + 'wss://alicerelay.example3.com', + 'wss://nostr.mutinywallet.com', + ], + ); + }); + test('custom read relays ignored', async () => { + storeMyProfileEvent({ + ...SampleEvents.kind10002, + tags: [ + ['r', 'wss://alicerelay.example.com'], + ['r', 'wss://brando-relay.com'], + ['r', 'wss://expensive-relay.example2.com', 'write'], + ['r', 'wss://nostr-relay.example.com', 'read'], + ], + }); + await doBefore(); + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + expect.anything(), + expect.anything(), + [ + 'wss://alicerelay.example.com', + 'wss://brando-relay.com', + 'wss://expensive-relay.example2.com', + 'wss://nostr.mutinywallet.com', + ], + ); + }); }); - test('updateLastFetchDate called once', () => { - expect(mockupdateLastFetchDate).toBeCalledTimes(1); - }); - test('fetchCachedProfileEvent never called', () => { - expect(fetchCachedProfileEventSpy).toBeCalledTimes(0); - }); - test('requestMyProfileFromRelays called', () => { - expect(mockrequestMyProfileFromRelays).toBeCalledTimes(1); - }); - test('mockrequestMyProfileFromRelays called with correct pubkey', () => { - expect(mockrequestMyProfileFromRelays).toBeCalledWith( - SampleEvents.kind0.pubkey, - expect.anything(), - ); - }); - test('eventProcessor called with events passed through by requestMyProfileFromRelays\'s event processor', async () => { - expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind0 }); - expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind3 }); - }); - test('storeMyProfileEvent called with events passed through by requestMyProfileFromRelays\'s event processor', async () => { - expect(mockstoreMyProfileEvent).toBeCalledWith({ ...SampleEvents.kind0 }); - expect(mockstoreMyProfileEvent).toBeCalledWith({ ...SampleEvents.kind3 }); - }); - test('eventProcessor not called when profile event kind isn\'t found by requestMyProfileFromRelays', async () => { - expect(mockEventProcessor).toBeCalledTimes(2); + describe('and when no cached 10002 events are present', () => { + const mockstoreMyProfileEvent = jest.spyOn(FetchEvents, 'storeMyProfileEvent'); + beforeEach(async () => { + mockisUptodate.mockReturnValue(false); + mockrequestMyProfileFromRelays.mockReset() + .mockImplementation(async (_pubkey, eventProcessor) => { + eventProcessor({ ...SampleEvents.kind0 }); + eventProcessor({ ...SampleEvents.kind3 }); + }); + await fetchMyProfileEvents(SampleEvents.kind0.pubkey, mockEventProcessor); + }); + test('updateLastFetchDate called once', () => { + expect(mockupdateLastFetchDate).toBeCalledTimes(1); + }); + test('fetchCachedProfileEvent only to be called once to getRelays', () => { + expect(fetchCachedProfileEventSpy).toBeCalledTimes(1); + }); + test('requestMyProfileFromRelays called', () => { + expect(mockrequestMyProfileFromRelays).toBeCalledTimes(1); + }); + test('mockrequestMyProfileFromRelays called with correct pubkey', () => { + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + SampleEvents.kind0.pubkey, + expect.anything(), + expect.anything(), + ); + }); + test('mockrequestMyProfileFromRelays called with correct default relays', () => { + expect(mockrequestMyProfileFromRelays).toBeCalledWith( + expect.anything(), + expect.anything(), + [ + 'wss://relay.damus.io', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr-relay.wlvs.space', + 'wss://nostr.mutinywallet.com', + ], + ); + }); + test('eventProcessor called with events passed through by requestMyProfileFromRelays\'s event processor', async () => { + expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind0 }); + expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind3 }); + }); + test('storeMyProfileEvent called with events passed through by requestMyProfileFromRelays\'s event processor', async () => { + expect(mockstoreMyProfileEvent).toBeCalledWith({ ...SampleEvents.kind0 }); + expect(mockstoreMyProfileEvent).toBeCalledWith({ ...SampleEvents.kind3 }); + }); + test('eventProcessor not called when profile event kind isn\'t found by requestMyProfileFromRelays', async () => { + expect(mockEventProcessor).toBeCalledTimes(2); + }); }); }); }); diff --git a/src/fetchEvents.ts b/src/fetchEvents.ts index c7b7d0c..44d990f 100644 --- a/src/fetchEvents.ts +++ b/src/fetchEvents.ts @@ -92,6 +92,21 @@ export const fetchCachedProfileEvent = (kind: 0 | 2 | 10002 | 3): null | Event = return a[0]; }; +const getRelays = () => { + const e = fetchCachedProfileEvent(10002); + const mywriterelays = !e ? [] : e.tags.filter((r) => !r[2] || r[2] === 'write').map((r) => r[1]); + // return minimum of 3 relays + blastr, filling in with default relays (removing duplicates) + return [ + ...(mywriterelays.length > 3 ? mywriterelays : [...new Set([ + ...mywriterelays, + 'wss://relay.damus.io', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr-relay.wlvs.space', + ])].slice(0, 3)), + 'wss://nostr.mutinywallet.com', // blastr + ]; +}; + /** get my latest profile events either from cache (if isUptodate) or from relays */ export const fetchMyProfileEvents = async ( pubkey:string, @@ -99,14 +114,10 @@ export const fetchMyProfileEvents = async ( ): Promise => { // get events from relays, store them and run profileEventProcesser if (!isUptodate()) { - /** - * TODO also run this if we havn't checked for x minutes and we aren't already - * listening on my write relays - */ await requestMyProfileFromRelays(pubkey, (event: Event) => { storeMyProfileEvent(event); profileEventProcesser(event); - }); + }, getRelays()); // update last-fetch-from-relays date updateLastFetchDate(); } else { diff --git a/src/index.ts b/src/index.ts index 8d935b0..bbc9849 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import { Event, UnsignedEvent } from 'nostr-tools'; import { generateLogoHero, LoadProfileHome } from './LoadProfileHome'; -import { setupDefaultRelays, setupMyRelays } from './RelayManagement'; -import { fetchCachedProfileEvent, fetchMyProfileEvents } from './fetchEvents'; +import { fetchMyProfileEvents } from './fetchEvents'; import { localStorageGetItem, localStorageSetItem } from './LocalStorage'; import { LoadMetadataPage } from './LoadMetadataPage'; import LoadContactsPage from './LoadContactsPage'; @@ -25,13 +24,6 @@ const loadProfile = async () => { (document.getElementById('navrelays') as HTMLElement).onclick = LoadRelaysPage; // load profile page (in loading mode) LoadProfileHome(); - // if my relays are known, connect to them - if ( - fetchCachedProfileEvent(10002) !== null - || fetchCachedProfileEvent(2) !== null - ) await setupMyRelays(); - // otherwise connect to default relays - else await setupDefaultRelays(); // load profile data await fetchMyProfileEvents( localStorageGetItem('pubkey') as string,