From acc2d15216fb6781bb3c68d0783b5891b6a6ab1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Tue, 2 Dec 2025 22:56:33 +0100 Subject: [PATCH] Optimize --- src/Command/CacheLatestArticlesCommand.php | 37 ++++++++- src/Command/NostrRelayPoolStatsCommand.php | 27 +++++- src/Controller/DefaultController.php | 48 ++++++----- src/Service/NostrClient.php | 24 ++++-- src/Service/NostrRelayPool.php | 95 ++++++++++++++++++---- 5 files changed, 188 insertions(+), 43 deletions(-) diff --git a/src/Command/CacheLatestArticlesCommand.php b/src/Command/CacheLatestArticlesCommand.php index 3983060..95ff542 100644 --- a/src/Command/CacheLatestArticlesCommand.php +++ b/src/Command/CacheLatestArticlesCommand.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Command; +use App\Service\RedisCacheService; +use App\Util\NostrKeyUtil; use FOS\ElasticaBundle\Finder\FinderInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -23,13 +25,20 @@ class CacheLatestArticlesCommand extends Command private FinderInterface $finder; private CacheItemPoolInterface $articlesCache; private ParameterBagInterface $params; + private RedisCacheService $redisCacheService; - public function __construct(FinderInterface $finder, CacheItemPoolInterface $articlesCache, ParameterBagInterface $params) + public function __construct( + FinderInterface $finder, + CacheItemPoolInterface $articlesCache, + ParameterBagInterface $params, + RedisCacheService $redisCacheService + ) { parent::__construct(); $this->finder = $finder; $this->articlesCache = $articlesCache; $this->params = $params; + $this->redisCacheService = $redisCacheService; } protected function execute(InputInterface $input, OutputInterface $output): int @@ -70,10 +79,34 @@ class CacheLatestArticlesCommand extends Command $articles = $this->finder->find($query); + // Pre-fetch and cache author metadata for all articles + $authorPubkeys = []; + foreach ($articles as $article) { + // Debug: check what type of object we have + if ($article instanceof \App\Entity\Article) { + $pubkey = $article->getPubkey(); + if ($pubkey && NostrKeyUtil::isHexPubkey($pubkey)) { + $authorPubkeys[] = $pubkey; + } + } elseif (is_object($article)) { + // Elastica result object + if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) { + $authorPubkeys[] = $article->pubkey; + } elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) { + $authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub); + } + } + } + $authorPubkeys = array_unique($authorPubkeys); + + $output->writeln('Pre-fetching metadata for ' . count($authorPubkeys) . ' authors...'); + $authorsMetadata = $this->redisCacheService->getMultipleMetadata($authorPubkeys); + $output->writeln('Fetched ' . count($authorsMetadata) . ' author profiles.'); + $cacheItem->set($articles); $cacheItem->expiresAfter(3600); // Cache for 1 hour $this->articlesCache->save($cacheItem); - $output->writeln('Cached ' . count($articles) . ' articles.'); + $output->writeln('Cached ' . count($articles) . ' articles with author metadata.'); // } else { // $output->writeln('Cache already exists for key: ' . $cacheKey . ''); // } diff --git a/src/Command/NostrRelayPoolStatsCommand.php b/src/Command/NostrRelayPoolStatsCommand.php index ad060a6..ba9d870 100644 --- a/src/Command/NostrRelayPoolStatsCommand.php +++ b/src/Command/NostrRelayPoolStatsCommand.php @@ -26,10 +26,35 @@ class NostrRelayPoolStatsCommand extends Command $io = new SymfonyStyle($input, $output); $stats = $this->relayPool->getStats(); + $localRelay = $this->relayPool->getLocalRelay(); + $defaultRelays = $this->relayPool->getDefaultRelays(); $io->title('Nostr Relay Pool Statistics'); - $io->section('Overview'); + $io->section('Configuration'); + $io->table( + ['Setting', 'Value'], + [ + ['Local Relay', $localRelay ?: '(not configured)'], + ['Default Relays', count($defaultRelays)], + ] + ); + + if (!empty($defaultRelays)) { + $io->section('Default Relay List (Priority Order)'); + $rows = []; + foreach ($defaultRelays as $index => $relay) { + $isLocal = $localRelay && $relay === $localRelay; + $rows[] = [ + $index + 1, + $relay, + $isLocal ? '✓ Local' : 'Public' + ]; + } + $io->table(['Priority', 'Relay URL', 'Type'], $rows); + } + + $io->section('Connection Pool'); $io->table( ['Metric', 'Value'], [ diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 7105450..9d93b49 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -61,28 +61,28 @@ class DefaultController extends AbstractController CacheItemPoolInterface $articlesCache ): Response { - set_time_limit(300); - ini_set('max_execution_time', '300'); - $env = $this->getParameter('kernel.environment'); // Reuse previous latest list cache key to show same set as old 'latest' $cacheKey = 'latest_articles_list_' . $env; $cacheItem = $articlesCache->getItem($cacheKey); - $key = new Key(); - $excludedPubkeys = [ - $key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'), - $key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'), - $key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'), - $key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'), - $key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'), - $key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'), - $key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'), - $key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'), - ]; - if (!$cacheItem->isHit()) { // Fallback: run query now if command hasn't populated cache yet + set_time_limit(300); + ini_set('max_execution_time', '300'); + + $key = new Key(); + $excludedPubkeys = [ + $key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'), + $key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'), + $key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'), + $key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'), + $key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'), + $key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'), + $key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'), + $key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'), + ]; + $boolQuery = new BoolQuery(); $boolQuery->addMustNot(new Query\Terms('pubkey', $excludedPubkeys)); $query = new Query($boolQuery); @@ -99,12 +99,22 @@ class DefaultController extends AbstractController $articles = $cacheItem->get(); + // Fetch author metadata - this is now much faster because + // metadata is pre-cached by the CacheLatestArticlesCommand $authorPubkeys = []; foreach ($articles as $article) { - if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) { - $authorPubkeys[] = $article->pubkey; - } elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) { - $authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub); + if ($article instanceof \App\Entity\Article) { + $pubkey = $article->getPubkey(); + if ($pubkey && NostrKeyUtil::isHexPubkey($pubkey)) { + $authorPubkeys[] = $pubkey; + } + } elseif (is_object($article)) { + // Elastica result object fallback + if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) { + $authorPubkeys[] = $article->pubkey; + } elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) { + $authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub); + } } } $authorPubkeys = array_unique($authorPubkeys); diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index dc3d944..ee0e0c3 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -129,13 +129,13 @@ class NostrClient */ public function getPubkeyMetadata($pubkey): \stdClass { - // Use relay pool for all relays including purplepag.es - $relayUrls = [ + // Use relay pool for all relays including purplepag.es, with local relay prioritized + $relayUrls = $this->relayPool->ensureLocalRelayInList([ 'wss://theforest.nostr1.com', 'wss://nostr.land', 'wss://relay.primal.net', 'wss://purplepag.es' - ]; + ]); $relaySet = $this->createRelaySet($relayUrls); $this->logger->info('Getting metadata for pubkey ' . $pubkey ); @@ -168,6 +168,9 @@ class NostrClient if (empty($relays)) { $key = new Key(); $relays = $this->getTopReputableRelaysForAuthor($key->convertPublicKeyToBech32($event->getPublicKey()), 5); + } else { + // Ensure local relay is included when publishing + $relays = $this->relayPool->ensureLocalRelayInList($relays); } // Use relay pool instead of creating new Relay instances @@ -206,8 +209,9 @@ class NostrClient } $requestMessage = new RequestMessage($subscriptionId, [$filter]); - // Create relay set from all reputable relays on record - $relaySet = $this->createRelaySet(self::REPUTABLE_RELAYS); + // Create relay set from all reputable relays on record, with local relay prioritized + $relayUrls = $this->relayPool->ensureLocalRelayInList(self::REPUTABLE_RELAYS); + $relaySet = $this->createRelaySet($relayUrls); $request = new Request($relaySet, $requestMessage); @@ -281,6 +285,9 @@ class NostrClient { $this->logger->info('Getting event by ID', ['event_id' => $eventId, 'relays' => $relays]); + // Ensure local relay is included in the relay list + $relays = $this->relayPool->ensureLocalRelayInList($relays); + // Merge relays with reputable ones $allRelays = array_unique(array_merge($relays, self::REPUTABLE_RELAYS)); // Loop over reputable relays and bail as soon as you get a valid event back @@ -341,6 +348,11 @@ class NostrClient $this->logger->info('Getting events by IDs', ['event_ids' => $eventIds, 'relays' => $relays]); + // Ensure local relay is included + if (!empty($relays)) { + $relays = $this->relayPool->ensureLocalRelayInList($relays); + } + // Use provided relays or default if empty $relaySet = empty($relays) ? $this->defaultRelaySet : $this->createRelaySet($relays); @@ -578,7 +590,7 @@ class NostrClient $parts = explode(':', $coordinate, 3); $pubkey = $parts[1]; - // Get author's relays for better chances of finding zaps + // Get author's relays for better chances of finding zaps (includes local relay) $authorRelays = $this->getTopReputableRelaysForAuthor($pubkey); $relaySet = $this->createRelaySet($authorRelays); diff --git a/src/Service/NostrRelayPool.php b/src/Service/NostrRelayPool.php index 900e48e..de4559a 100644 --- a/src/Service/NostrRelayPool.php +++ b/src/Service/NostrRelayPool.php @@ -23,7 +23,10 @@ class NostrRelayPool private array $lastConnected = []; /** @var array */ - private array $defaultRelays = [ + private array $defaultRelays = []; + + /** @var array Default public relays to fall back to */ + private const PUBLIC_RELAYS = [ 'wss://theforest.nostr1.com', 'wss://nostr.land', 'wss://relay.primal.net', @@ -39,11 +42,39 @@ class NostrRelayPool private readonly string $nostrDefaultRelay, array $defaultRelays = [] ) { - $relayList = $defaultRelays ?: $this->defaultRelays; - if ($this->nostrDefaultRelay && !in_array($this->nostrDefaultRelay, $relayList, true)) { - array_unshift($relayList, $this->nostrDefaultRelay); + // Build relay list: prioritize local relay, then custom relays, then public relays + $relayList = []; + + // Add local relay first if configured + if ($this->nostrDefaultRelay) { + $relayList[] = $this->nostrDefaultRelay; + $this->logger->info('NostrRelayPool: Local relay configured as primary', [ + 'relay' => $this->nostrDefaultRelay + ]); + } + + // Add custom relays (excluding local relay if already added) + if (!empty($defaultRelays)) { + foreach ($defaultRelays as $relay) { + if (!in_array($relay, $relayList, true)) { + $relayList[] = $relay; + } + } + } else { + // Use public relays as fallback + foreach (self::PUBLIC_RELAYS as $relay) { + if (!in_array($relay, $relayList, true)) { + $relayList[] = $relay; + } + } } + $this->defaultRelays = $relayList; + + $this->logger->info('NostrRelayPool initialized with relay priority', [ + 'relay_count' => count($this->defaultRelays), + 'relays' => $this->defaultRelays + ]); } /** @@ -88,7 +119,7 @@ class NostrRelayPool } /** - * Get multiple relay connections, prioritizing default relay + * Get multiple relay connections, prioritizing local relay (primary default relay) * * @param array $relayUrls * @return array @@ -96,26 +127,26 @@ class NostrRelayPool public function getRelays(array $relayUrls): array { $relays = []; - $defaultRelay = $this->defaultRelays[0] ?? null; + $localRelay = $this->nostrDefaultRelay ? $this->normalizeRelayUrl($this->nostrDefaultRelay) : null; $relayUrlsNormalized = array_map([$this, 'normalizeRelayUrl'], $relayUrls); - $defaultRelayNormalized = $defaultRelay ? $this->normalizeRelayUrl($defaultRelay) : null; - // Try default relay first if present in requested URLs - if ($defaultRelayNormalized && in_array($defaultRelayNormalized, $relayUrlsNormalized, true)) { + // Always try local relay first if configured, regardless of whether it's in the requested URLs + if ($localRelay) { try { - $relays[] = $this->getRelay($defaultRelayNormalized); + $relays[] = $this->getRelay($localRelay); + $this->logger->debug('Using local relay (priority)', ['relay' => $localRelay]); } catch (\Throwable $e) { - $this->logger->warning('Default relay unavailable, falling back to others', [ - 'relay' => $defaultRelayNormalized, + $this->logger->warning('Local relay unavailable, falling back to others', [ + 'relay' => $localRelay, 'error' => $e->getMessage() ]); } } - // Add other relays except the default + // Add other relays except the local relay foreach ($relayUrlsNormalized as $url) { - if ($url === $defaultRelayNormalized) { - continue; + if ($url === $localRelay) { + continue; // Skip local relay as we already added it } try { $relays[] = $this->getRelay($url); @@ -126,6 +157,7 @@ class NostrRelayPool ]); } } + return $relays; } @@ -340,4 +372,37 @@ class NostrRelayPool { return $this->defaultRelays; } + + /** + * Get local relay URL if configured + */ + public function getLocalRelay(): ?string + { + return $this->nostrDefaultRelay ?: null; + } + + /** + * Ensure local relay is included in a list of relay URLs + * Adds local relay at the beginning if configured and not already in the list + */ + public function ensureLocalRelayInList(array $relayUrls): array + { + if (!$this->nostrDefaultRelay) { + return $relayUrls; + } + + $normalizedLocal = $this->normalizeRelayUrl($this->nostrDefaultRelay); + $normalizedUrls = array_map([$this, 'normalizeRelayUrl'], $relayUrls); + + // If local relay not in list, add it at the beginning + if (!in_array($normalizedLocal, $normalizedUrls, true)) { + array_unshift($relayUrls, $this->nostrDefaultRelay); + $this->logger->debug('Added local relay to relay list', [ + 'local_relay' => $this->nostrDefaultRelay, + 'total_relays' => count($relayUrls) + ]); + } + + return $relayUrls; + } }