From f9fcfb1c9e70a73acfda90ce4d1c1215623972df Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 21 Aug 2024 00:16:21 +0500 Subject: [PATCH] fix: manage pending relay connection requests Also remove the getMostPopularRelays function and use a hardcoded list of relays Further, if metadata event is not found from relays cache an empty metadata for that pubkey --- src/controllers/MetadataController.ts | 90 +++++++++++---------------- src/controllers/RelayController.ts | 55 +++++++++++----- src/store/actionTypes.ts | 1 - src/store/relays/action.ts | 8 --- src/store/relays/reducer.ts | 4 -- src/store/relays/types.ts | 7 --- src/utils/const.ts | 17 ++++- src/utils/relays.ts | 76 ++++++---------------- 8 files changed, 107 insertions(+), 151 deletions(-) diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index bf1b3d8..d3bd506 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -15,15 +15,16 @@ import { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelaySet, - getMostPopularRelays, getUserRelaySet, - isOlderThanOneWeek, + isOlderThanOneDay, unixNow } from '../utils' +import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const' export class MetadataController extends EventEmitter { private nostrController: NostrController private specialMetadataRelay = 'wss://purplepag.es' + private pendingFetches = new Map>() // Track pending fetches constructor() { super() @@ -42,70 +43,55 @@ export class MetadataController extends EventEmitter { hexKey: string, currentEvent: Event | null ): Promise { - // Define the event filter to only include metadata events authored by the given key - const eventFilter: Filter = { - kinds: [kinds.Metadata], // Only metadata events - authors: [hexKey] // Authored by the specified key + // Return the ongoing fetch promise if one exists for the same hexKey + if (this.pendingFetches.has(hexKey)) { + return this.pendingFetches.get(hexKey)! } - // Try to get the metadata event from a special relay (wss://purplepag.es) - const metadataEvent = await relayController - .fetchEvent(eventFilter, [this.specialMetadataRelay]) + // Define the event filter to only include metadata events authored by the given key + const eventFilter: Filter = { + kinds: [kinds.Metadata], + authors: [hexKey] + } + + const fetchPromise = relayController + .fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST) .catch((err) => { - console.error(err) // Log any errors - return null // Return null if an error occurs + console.error(err) + return null + }) + .finally(() => { + this.pendingFetches.delete(hexKey) }) - // If a valid metadata event is found from the special relay + this.pendingFetches.set(hexKey, fetchPromise) + + const metadataEvent = await fetchPromise + if ( metadataEvent && - validateEvent(metadataEvent) && // Validate the event - verifyEvent(metadataEvent) // Verify the event's authenticity + validateEvent(metadataEvent) && + verifyEvent(metadataEvent) ) { - // If there's no current event or the new metadata event is more recent if ( !currentEvent || metadataEvent.created_at >= currentEvent.created_at ) { - // Handle the new metadata event this.handleNewMetadataEvent(metadataEvent) } - return metadataEvent } - // If no valid metadata event is found from the special relay, get the most popular relays - const mostPopularRelays = await getMostPopularRelays() + // todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST + // try to query user relay list - // Query the most popular relays for metadata events - - const events = await relayController - .fetchEvents(eventFilter, mostPopularRelays) - .catch((err) => { - console.error(err) // Log any errors - return null // Return null if an error occurs - }) - - // If events are found from the popular relays - if (events && events.length) { - events.sort((a, b) => b.created_at - a.created_at) // Sort events by creation date (descending) - - // Iterate through the events - for (const event of events) { - // If the event is valid, authentic, and more recent than the current event - if ( - validateEvent(event) && - verifyEvent(event) && - (!currentEvent || event.created_at > currentEvent.created_at) - ) { - // Handle the new metadata event - this.handleNewMetadataEvent(event) - return event - } - } + // if current event is null we should cache empty metadata event for provided hexKey + if (!currentEvent) { + const emptyMetadata = this.getEmptyMetadataEvent(hexKey) + this.handleNewMetadataEvent(emptyMetadata as VerifiedEvent) } - return currentEvent // Return the current event if no newer event is found + return currentEvent } /** @@ -131,7 +117,7 @@ export class MetadataController extends EventEmitter { // If cached metadata is found, check its validity if (cachedMetadataEvent) { // Check if the cached metadata is older than one week - if (isOlderThanOneWeek(cachedMetadataEvent.cachedAt)) { + if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) { // If older than one week, find the metadata from relays in background this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event) @@ -161,11 +147,7 @@ export class MetadataController extends EventEmitter { public findRelayListMetadata = async (hexKey: string): Promise => { const relayEvent = (await findRelayListInCache(hexKey)) || - (await findRelayListAndUpdateCache( - [this.specialMetadataRelay], - hexKey - )) || - (await findRelayListAndUpdateCache(await getMostPopularRelays(), hexKey)) + (await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey)) return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet() } @@ -216,13 +198,13 @@ export class MetadataController extends EventEmitter { public validate = (event: Event) => validateEvent(event) && verifyEvent(event) - public getEmptyMetadataEvent = (): Event => { + public getEmptyMetadataEvent = (pubkey?: string): Event => { return { content: '', created_at: new Date().valueOf(), id: '', kind: 0, - pubkey: '', + pubkey: pubkey || '', sig: '', tags: [] } diff --git a/src/controllers/RelayController.ts b/src/controllers/RelayController.ts index 83d8ab3..4137e86 100644 --- a/src/controllers/RelayController.ts +++ b/src/controllers/RelayController.ts @@ -7,6 +7,7 @@ import { SIGIT_RELAY } from '../utils/const' */ export class RelayController { private static instance: RelayController + private pendingConnections = new Map>() // Track pending connections public connectedRelays = new Map() private constructor() {} @@ -35,23 +36,26 @@ export class RelayController { * @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails. */ public connectRelay = async (relayUrl: string): Promise => { - // Check if a relay with the same URL is already connected const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl) const relay = this.connectedRelays.get(normalizedWebSocketURL) if (relay) { - // If a relay is found in connectedRelay map and is connected, just return it if (relay.connected) return relay // If relay is found in connectedRelay map but not connected, // remove it from map and call connectRelay method again this.connectedRelays.delete(relayUrl) - return this.connectRelay(relayUrl) } - // Attempt to connect to the relay using the provided URL - const newRelay = await Relay.connect(relayUrl) + // Check if there's already a pending connection for this relay URL + if (this.pendingConnections.has(relayUrl)) { + // Return the existing promise to avoid making another connection + return this.pendingConnections.get(relayUrl)! + } + + // Create a new connection promise and store it in pendingConnections + const connectionPromise = Relay.connect(relayUrl) .then((relay) => { if (relay.connected) { // Add the newly connected relay to the connected relays map @@ -70,8 +74,13 @@ export class RelayController { // Return null to indicate connection failure return null }) + .finally(() => { + // Remove the connection from pendingConnections once it settles + this.pendingConnections.delete(relayUrl) + }) - return newRelay + this.pendingConnections.set(relayUrl, connectionPromise) + return connectionPromise } /** @@ -86,8 +95,13 @@ export class RelayController { filter: Filter, relayUrls: string[] = [] ): Promise => { - // Add app relay to relays array and connect to all specified relays - const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) => + if (!relayUrls.includes(SIGIT_RELAY)) { + // Add app relay to relays array if not exists already + relayUrls.push(SIGIT_RELAY) + } + + // connect to all specified relays + const relayPromises = relayUrls.map((relayUrl) => this.connectRelay(relayUrl) ) @@ -201,11 +215,15 @@ export class RelayController { relayUrls: string[] = [], eventHandler: (event: Event) => void ) => { - // Add app relay to relays array and connect to all specified relays + if (!relayUrls.includes(SIGIT_RELAY)) { + // Add app relay to relays array if not exists already + relayUrls.push(SIGIT_RELAY) + } - const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) => - this.connectRelay(relayUrl) - ) + // connect to all specified relays + const relayPromises = relayUrls.map((relayUrl) => { + return this.connectRelay(relayUrl) + }) // Use Promise.allSettled to wait for all promises to settle const results = await Promise.allSettled(relayPromises) @@ -258,11 +276,16 @@ export class RelayController { event: Event, relayUrls: string[] = [] ): Promise => { - // Add app relay to relays array and connect to all specified relays + if (!relayUrls.includes(SIGIT_RELAY)) { + // Add app relay to relays array if not exists already + relayUrls.push(SIGIT_RELAY) + } - const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) => - this.connectRelay(relayUrl) - ) + // connect to all specified relays + const relayPromises = relayUrls.map((relayUrl) => { + console.log('being called from publish events') + return this.connectRelay(relayUrl) + }) // Use Promise.allSettled to wait for all promises to settle const results = await Promise.allSettled(relayPromises) diff --git a/src/store/actionTypes.ts b/src/store/actionTypes.ts index 01ecf99..f0e6698 100644 --- a/src/store/actionTypes.ts +++ b/src/store/actionTypes.ts @@ -15,7 +15,6 @@ export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE' export const SET_RELAY_MAP = 'SET_RELAY_MAP' export const SET_RELAY_INFO = 'SET_RELAY_INFO' export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED' -export const SET_MOST_POPULAR_RELAYS = 'SET_MOST_POPULAR_RELAYS' export const UPDATE_USER_APP_DATA = 'UPDATE_USER_APP_DATA' export const UPDATE_PROCESSED_GIFT_WRAPS = 'UPDATE_PROCESSED_GIFT_WRAPS' diff --git a/src/store/relays/action.ts b/src/store/relays/action.ts index 7552565..fdfffca 100644 --- a/src/store/relays/action.ts +++ b/src/store/relays/action.ts @@ -1,7 +1,6 @@ import * as ActionTypes from '../actionTypes' import { SetRelayMapAction, - SetMostPopularRelaysAction, SetRelayInfoAction, SetRelayMapUpdatedAction } from './types' @@ -19,13 +18,6 @@ export const setRelayInfoAction = ( payload }) -export const setMostPopularRelaysAction = ( - payload: string[] -): SetMostPopularRelaysAction => ({ - type: ActionTypes.SET_MOST_POPULAR_RELAYS, - payload -}) - export const setRelayMapUpdatedAction = (): SetRelayMapUpdatedAction => ({ type: ActionTypes.SET_RELAY_MAP_UPDATED }) diff --git a/src/store/relays/reducer.ts b/src/store/relays/reducer.ts index 68f18a0..1f7fd31 100644 --- a/src/store/relays/reducer.ts +++ b/src/store/relays/reducer.ts @@ -4,7 +4,6 @@ import { RelaysDispatchTypes, RelaysState } from './types' const initialState: RelaysState = { map: undefined, mapUpdated: undefined, - mostPopular: undefined, info: undefined } @@ -25,9 +24,6 @@ const reducer = ( info: { ...state.info, ...action.payload } } - case ActionTypes.SET_MOST_POPULAR_RELAYS: - return { ...state, mostPopular: [...action.payload] } - case ActionTypes.RESTORE_STATE: return action.payload.relays diff --git a/src/store/relays/types.ts b/src/store/relays/types.ts index e90ca3b..1e0d399 100644 --- a/src/store/relays/types.ts +++ b/src/store/relays/types.ts @@ -5,7 +5,6 @@ import { RelayMap, RelayInfoObject } from '../../types' export type RelaysState = { map?: RelayMap mapUpdated?: number - mostPopular?: string[] info?: RelayInfoObject } @@ -14,11 +13,6 @@ export interface SetRelayMapAction { payload: RelayMap } -export interface SetMostPopularRelaysAction { - type: typeof ActionTypes.SET_MOST_POPULAR_RELAYS - payload: string[] -} - export interface SetRelayInfoAction { type: typeof ActionTypes.SET_RELAY_INFO payload: RelayInfoObject @@ -32,5 +26,4 @@ export type RelaysDispatchTypes = | SetRelayMapAction | SetRelayInfoAction | SetRelayMapUpdatedAction - | SetMostPopularRelaysAction | RestoreState diff --git a/src/utils/const.ts b/src/utils/const.ts index 930ab16..2940399 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -11,7 +11,18 @@ export const DEFLATE = 'DEFLATE' /** * Number of milliseconds in one week. - * Calc based on: 7 * 24 * 60 * 60 * 1000 */ -export const ONE_WEEK_IN_MS: number = 604800000 -export const SIGIT_RELAY: string = 'wss://relay.sigit.io' +export const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000 + +/** + * Number of milliseconds in one day. + */ +export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000 + +export const SIGIT_RELAY = 'wss://relay.sigit.io' + +export const DEFAULT_LOOK_UP_RELAY_LIST = [ + SIGIT_RELAY, + 'wss://user.kindpag.es', + 'wss://purplepag.es' +] diff --git a/src/utils/relays.ts b/src/utils/relays.ts index 32bfdd4..6c4b4e9 100644 --- a/src/utils/relays.ts +++ b/src/utils/relays.ts @@ -1,13 +1,14 @@ -import axios from 'axios' import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools' import { RelayList } from 'nostr-tools/kinds' import { getRelayInfo, unixNow } from '.' import { NostrController, relayController } from '../controllers' import { localCache } from '../services' -import { setMostPopularRelaysAction } from '../store/actions' -import store from '../store/store' -import { RelayMap, RelayReadStats, RelaySet, RelayStats } from '../types' -import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const' +import { RelayMap, RelaySet } from '../types' +import { + DEFAULT_LOOK_UP_RELAY_LIST, + ONE_WEEK_IN_MS, + SIGIT_RELAY +} from './const' const READ_MARKER = 'read' const WRITE_MARKER = 'write' @@ -29,6 +30,7 @@ const findRelayListAndUpdateCache = async ( authors: [hexKey] } + console.count('findRelayListAndUpdateCache') const event = await relayController.fetchEvent(eventFilter, lookUpRelays) if (event) { await localCache.addUserRelayListMetadata(event) @@ -90,6 +92,10 @@ const isOlderThanOneWeek = (cachedAt: number) => { return Date.now() - cachedAt < ONE_WEEK_IN_MS } +const isOlderThanOneDay = (cachedAt: number) => { + return Date.now() - cachedAt < ONE_WEEK_IN_MS +} + const isRelayTag = (tag: string[]): boolean => tag[0] === 'r' const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => { @@ -110,51 +116,6 @@ const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => { return obj } -/** - * Provides most popular relays. - * @param numberOfTopRelays - number representing how many most popular relays to provide - * @returns - promise that resolves into an array of most popular relays - */ -const getMostPopularRelays = async ( - numberOfTopRelays: number = 30 -): Promise => { - const mostPopularRelaysState = store.getState().relays?.mostPopular - - // return most popular relays from app state if present - if (mostPopularRelaysState) return mostPopularRelaysState - - // relays in env - const { VITE_MOST_POPULAR_RELAYS } = import.meta.env - const hardcodedPopularRelays = (VITE_MOST_POPULAR_RELAYS || '').split(' ') - const url = `https://stats.nostr.band/stats_api?method=stats` - - const response = await axios.get(url).catch(() => undefined) - - if (!response) { - return hardcodedPopularRelays //return hardcoded relay list - } - - const data = response.data - - if (!data) { - return hardcodedPopularRelays //return hardcoded relay list - } - - const apiTopRelays = data.relay_stats.user_picks.read_relays - .slice(0, numberOfTopRelays) - .map((relay: RelayReadStats) => relay.d) - - if (!apiTopRelays.length) { - return Promise.reject(`Couldn't fetch popular relays.`) - } - - if (store.getState().auth?.loggedIn) { - store.dispatch(setMostPopularRelaysAction(apiTopRelays)) - } - - return apiTopRelays -} - /** * Provides relay map. * @param npub - user's npub @@ -163,16 +124,15 @@ const getMostPopularRelays = async ( const getRelayMap = async ( npub: string ): Promise<{ map: RelayMap; mapUpdated?: number }> => { - const mostPopularRelays = await getMostPopularRelays() - // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md const eventFilter: Filter = { kinds: [kinds.RelayList], authors: [npub] } + console.count('getRelayMap') const event = await relayController - .fetchEvent(eventFilter, mostPopularRelays) + .fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST) .catch((err) => { return Promise.reject(err) }) @@ -196,6 +156,7 @@ const getRelayMap = async ( }) Object.keys(relaysMap).forEach((relayUrl) => { + console.log('being called from getRelayMap') relayController.connectRelay(relayUrl) }) @@ -255,9 +216,8 @@ const publishRelayMap = async ( // If relay map is empty, use most popular relay URIs if (!relaysToPublish.length) { - relaysToPublish = await getMostPopularRelays() + relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST } - const publishResult = await relayController.publish( signedEvent, relaysToPublish @@ -277,9 +237,9 @@ export { findRelayListInCache, getDefaultRelayMap, getDefaultRelaySet, - getMostPopularRelays, getRelayMap, - publishRelayMap, getUserRelaySet, - isOlderThanOneWeek + isOlderThanOneDay, + isOlderThanOneWeek, + publishRelayMap }