From 32aae50b6c4bfc5b6052102696e978277d9448cb Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 26 Oct 2025 18:29:24 +0100 Subject: [PATCH] clean up console 1 --- LOGGING.md | 119 ++++++++++++++++++ src/PageManager.tsx | 9 +- src/components/NormalFeed/index.tsx | 3 +- src/components/Note/index.tsx | 5 +- src/components/NoteList/index.tsx | 11 +- .../Profile/ProfileBookmarksAndHashtags.tsx | 13 +- src/components/ReplyNoteList/index.tsx | 25 +++- src/components/TrendingNotes/index.tsx | 67 +++++----- src/lib/debug-utils.ts | 61 +++++++++ src/lib/logger.ts | 114 +++++++++++++++++ src/main.tsx | 1 + src/pages/primary/DiscussionsPage/index.tsx | 21 ++-- src/pages/primary/NoteListPage/RelaysFeed.tsx | 7 +- src/pages/primary/NoteListPage/index.tsx | 5 +- src/providers/FeedProvider.tsx | 27 ++-- src/providers/NostrProvider/index.tsx | 9 +- src/providers/NotificationProvider.tsx | 2 +- src/services/client.service.ts | 58 ++++++--- src/services/note-stats.service.ts | 69 ++++++---- 19 files changed, 505 insertions(+), 121 deletions(-) create mode 100644 LOGGING.md create mode 100644 src/lib/debug-utils.ts create mode 100644 src/lib/logger.ts diff --git a/LOGGING.md b/LOGGING.md new file mode 100644 index 0000000..b0f4cc5 --- /dev/null +++ b/LOGGING.md @@ -0,0 +1,119 @@ +# Logging System + +This document describes the logging system implemented to reduce console noise and improve performance. + +## Overview + +The application now uses a centralized logging system that: +- Reduces console noise in production +- Provides conditional debug logging +- Improves performance by removing debug logs in production builds +- Allows developers to enable debug logging when needed + +## Usage + +### For Developers + +In development mode, you can control logging from the browser console: + +```javascript +// Enable debug logging +jumbleDebug.enable() + +// Disable debug logging +jumbleDebug.disable() + +// Check current status +jumbleDebug.status() + +// Use debug logging directly +jumbleDebug.log('Debug message', data) +jumbleDebug.warn('Warning message', data) +jumbleDebug.error('Error message', data) +jumbleDebug.perf('Performance message', data) +``` + +### For Code + +Use the logger instead of direct console statements: + +```typescript +import logger from '@/lib/logger' + +// Debug logging (only shows in dev mode with debug enabled) +logger.debug('Debug information', data) + +// Info logging (always shows) +logger.info('Important information', data) + +// Warning logging (always shows) +logger.warn('Warning message', data) + +// Error logging (always shows) +logger.error('Error message', data) + +// Performance logging (only in dev mode) +logger.perf('Performance metric', data) +``` + +## Log Levels + +- **debug**: Development debugging information (disabled in production) +- **info**: Important application information (always enabled) +- **warn**: Warning messages (always enabled) +- **error**: Error messages (always enabled) +- **perf**: Performance metrics (development only) + +## Configuration + +The logger automatically configures itself based on: + +1. **Environment**: Debug logging is disabled in production builds +2. **Local Storage**: `jumble-debug=true` enables debug mode +3. **Environment Variable**: `VITE_DEBUG=true` enables debug mode + +## Performance Impact + +- **Production**: Debug logs are completely removed, improving performance +- **Development**: Debug logs are conditionally enabled, reducing noise +- **Console Operations**: Reduced console.log calls improve browser performance + +## Migration + +The following files have been updated to use the new logging system: + +- `src/providers/FeedProvider.tsx` - Feed initialization and switching +- `src/pages/primary/DiscussionsPage/index.tsx` - Vote counting and event fetching +- `src/services/client.service.ts` - Relay operations and circuit breaker +- `src/providers/NostrProvider/index.tsx` - Event signing and validation +- `src/components/Note/index.tsx` - Component rendering +- `src/PageManager.tsx` - Page rendering + +## Benefits + +1. **Reduced Console Noise**: Debug logs are hidden by default +2. **Better Performance**: Fewer console operations in production +3. **Developer Control**: Easy to enable debug logging when needed +4. **Consistent Logging**: Centralized logging with consistent format +5. **Production Ready**: Debug logs are completely removed in production builds + +## Debug Mode + +To enable debug mode: + +1. **In Browser Console** (development only): + ```javascript + jumbleDebug.enable() + ``` + +2. **Via Local Storage**: + ```javascript + localStorage.setItem('jumble-debug', 'true') + ``` + +3. **Via Environment Variable**: + ```bash + VITE_DEBUG=true npm run dev + ``` + +Debug mode will show all debug-level logs with timestamps and log levels. diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 2151bf0..51cd9a8 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -1,6 +1,7 @@ import Sidebar from '@/components/Sidebar' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' +import logger from '@/lib/logger' import { ChevronLeft } from 'lucide-react' import NoteListPage from '@/pages/primary/NoteListPage' import HomePage from '@/pages/secondary/HomePage' @@ -339,7 +340,7 @@ function MainContentArea({ }) { const { showRecommendedRelaysPanel } = useUserPreferences() - console.log('MainContentArea rendering:', { + logger.debug('MainContentArea rendering:', { currentPrimaryPage, primaryPages: primaryPages.map(p => p.name), showRecommendedRelaysPanel, @@ -383,7 +384,7 @@ function MainContentArea({ // Show normal primary pages primaryPages.map(({ name, element, props }) => { const isCurrentPage = currentPrimaryPage === name - console.log(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage }) + logger.debug(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage }) return (
{(() => { try { - console.log(`Rendering ${name} component`) + logger.debug(`Rendering ${name} component`) return props ? cloneElement(element as React.ReactElement, props) : element } catch (error) { - console.error(`Error rendering ${name} component:`, error) + logger.error(`Error rendering ${name} component:`, error) return
Error rendering {name}: {error instanceof Error ? error.message : String(error)}
} })()} diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index 1e17d6e..9164afb 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -1,5 +1,6 @@ import NoteList, { TNoteListRef } from '@/components/NoteList' import Tabs from '@/components/Tabs' +import logger from '@/lib/logger' import { isTouchDevice } from '@/lib/utils' import { useKindFilter } from '@/providers/KindFilterProvider' import { useUserTrust } from '@/providers/UserTrustProvider' @@ -20,7 +21,7 @@ const NormalFeed = forwardRef } else if (mutePubkeySet.has(event.pubkey) && !showMuted) { content = setShowMuted(true)} /> @@ -87,7 +88,7 @@ export default function Note({ try { content = } catch (error) { - console.error('Note component - Error rendering Highlight component:', error) + logger.error('Note component - Error rendering Highlight component:', error) content =
HIGHLIGHT ERROR:
Error: {String(error)}
diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 46d0fd6..8eaf142 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -7,6 +7,7 @@ import { isReplyNoteEvent } from '@/lib/event' import { getZapInfoFromEvent } from '@/lib/event-metadata' +import logger from '@/lib/logger' import { isTouchDevice } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider' @@ -33,7 +34,7 @@ import { toast } from 'sonner' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' const LIMIT = 200 -const ALGO_LIMIT = 500 +const ALGO_LIMIT = 200 const SHOW_COUNT = 10 const NoteList = forwardRef( @@ -156,7 +157,7 @@ const NoteList = forwardRef( useImperativeHandle(ref, () => ({ scrollToTop, refresh }), []) useEffect(() => { - console.log('NoteList useEffect:', { subRequests, subRequestsLength: subRequests.length }) + logger.debug('NoteList useEffect:', { subRequests, subRequestsLength: subRequests.length }) if (!subRequests.length) return async function init() { @@ -172,7 +173,7 @@ const NoteList = forwardRef( return () => {} } - console.log('NoteList subscribing to timeline with:', subRequests.map(({ urls, filter }) => ({ + logger.debug('NoteList subscribing to timeline with:', subRequests.map(({ urls, filter }) => ({ urls, filter: { kinds: showKinds, @@ -192,7 +193,7 @@ const NoteList = forwardRef( })), { onEvents: (events, eosed) => { - console.log('NoteList received events:', { eventsCount: events.length, eosed }) + logger.debug('NoteList received events:', { eventsCount: events.length, eosed }) if (events.length > 0) { setEvents(events) // Stop loading as soon as we have events, don't wait for all relays @@ -220,7 +221,7 @@ const NoteList = forwardRef( } }, onClose: (url, reason) => { - console.log('Relay connection closed:', { url, reason }) + logger.debug('Relay connection closed:', { url, reason }) if (!showRelayCloseReason) return // ignore reasons from nostr-tools if ( diff --git a/src/components/Profile/ProfileBookmarksAndHashtags.tsx b/src/components/Profile/ProfileBookmarksAndHashtags.tsx index 8138781..edc88be 100644 --- a/src/components/Profile/ProfileBookmarksAndHashtags.tsx +++ b/src/components/Profile/ProfileBookmarksAndHashtags.tsx @@ -5,6 +5,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' +import logger from '@/lib/logger' import { normalizeUrl } from '@/lib/url' import NoteCard from '../NoteCard' import { Skeleton } from '../ui/skeleton' @@ -94,12 +95,12 @@ export default function ProfileBookmarksAndHashtags({ // Use the same comprehensive relay list we built for the bookmark list event const events = await client.fetchEvents(comprehensiveRelays, { ids: eventIds, - limit: 500 + limit: 100 }) - console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'bookmark events') + logger.debug('[ProfileBookmarksAndHashtags] Fetched', events.length, 'bookmark events') setBookmarkEvents(events) } catch (error) { - console.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark events:', error) + logger.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark events:', error) setBookmarkEvents([]) } } else { @@ -212,12 +213,12 @@ export default function ProfileBookmarksAndHashtags({ // Use the same comprehensive relay list we built for the pin list event const events = await client.fetchEvents(comprehensiveRelays, { ids: eventIds, - limit: 500 + limit: 100 }) - console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'pin events') + logger.debug('[ProfileBookmarksAndHashtags] Fetched', events.length, 'pin events') setPinEvents(events) } catch (error) { - console.warn('[ProfileBookmarksAndHashtags] Error fetching pin events:', error) + logger.warn('[ProfileBookmarksAndHashtags] Error fetching pin events:', error) setPinEvents([]) } } else { diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 3d91f88..b0c60e2 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -9,6 +9,7 @@ import { isReplaceableEvent, isReplyNoteEvent } from '@/lib/event' +import logger from '@/lib/logger' import { toNote } from '@/lib/link' import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' import { normalizeUrl } from '@/lib/url' @@ -98,7 +99,12 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even } - while (parentEventKeys.length > 0) { + const processedEventIds = new Set() // Prevent infinite loops + let iterationCount = 0 + const MAX_ITERATIONS = 10 // Prevent infinite loops + + while (parentEventKeys.length > 0 && iterationCount < MAX_ITERATIONS) { + iterationCount++ const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || []) events.forEach((evt) => { @@ -113,7 +119,18 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even replyIdSet.add(evt.id) replyEvents.push(evt) }) - parentEventKeys = events.map((evt) => evt.id) + + // Prevent infinite loops by tracking processed event IDs + const newParentEventKeys = events + .map((evt) => evt.id) + .filter((id) => !processedEventIds.has(id)) + + newParentEventKeys.forEach((id) => processedEventIds.add(id)) + parentEventKeys = newParentEventKeys + } + + if (iterationCount >= MAX_ITERATIONS) { + logger.warn('ReplyNoteList: Maximum iterations reached, possible circular reference in replies') } @@ -335,10 +352,10 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even }, [rootInfo, currentIndex, index, onNewReply]) useEffect(() => { - if (replies.length === 0) { + if (replies.length === 0 && !loading && timelineKey) { loadMore() } - }, [replies]) + }, [replies.length, loading, timelineKey]) // More specific dependencies to prevent infinite loops useEffect(() => { const options = { diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index 803fab5..01b3c1a 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -11,6 +11,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useZap } from '@/providers/ZapProvider' import noteStatsService from '@/services/note-stats.service' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants' +import logger from '@/lib/logger' import { normalizeUrl } from '@/lib/url' const SHOW_COUNT = 10 @@ -54,12 +55,12 @@ export default function TrendingNotes() { // Debug: Track cacheEvents changes useEffect(() => { - console.log('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events') + logger.debug('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events') }, [cacheEvents]) // Debug: Track cacheLoading changes useEffect(() => { - console.log('[TrendingNotes] cacheLoading state changed:', cacheLoading) + logger.debug('[TrendingNotes] cacheLoading state changed:', cacheLoading) }, [cacheLoading]) @@ -128,7 +129,7 @@ export default function TrendingNotes() { const flattenedIds = allEventIds.flat() setFollowsBookmarkEventIds(flattenedIds) } catch (error) { - console.error('Error fetching follows bookmarks:', error) + logger.error('Error fetching follows bookmarks:', error) } } @@ -137,7 +138,7 @@ export default function TrendingNotes() { // Calculate popular hashtags from cache events (all events from relays) const calculatePopularHashtags = useMemo(() => { - console.log('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length) + logger.debug('[TrendingNotes] calculatePopularHashtags - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length) // Use cache events if available, otherwise fallback to trending notes let eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : trendingNotes @@ -180,8 +181,8 @@ export default function TrendingNotes() { .slice(0, 10) .map(([hashtag]) => hashtag) - console.log('[TrendingNotes] calculatePopularHashtags - found hashtags:', result) - console.log('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags) + logger.debug('[TrendingNotes] calculatePopularHashtags - found hashtags:', result) + logger.debug('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags) return result }, [cacheEvents, trendingNotes, activeTab, hashtagFilter, pubkey]) // Use cacheEvents and trendingNotes as dependencies @@ -212,14 +213,14 @@ export default function TrendingNotes() { // Update popular hashtags when trending notes change useEffect(() => { - console.log('[TrendingNotes] calculatePopularHashtags result:', calculatePopularHashtags) + logger.debug('[TrendingNotes] calculatePopularHashtags result:', calculatePopularHashtags) setPopularHashtags(calculatePopularHashtags) }, [calculatePopularHashtags]) // Fallback: populate cacheEvents from trendingNotes if cache is empty useEffect(() => { if (activeTab === 'hashtags' && cacheEvents.length === 0 && trendingNotes.length > 0) { - console.log('[TrendingNotes] Fallback: populating cacheEvents from trendingNotes') + logger.debug('[TrendingNotes] Fallback: populating cacheEvents from trendingNotes') setCacheEvents(trendingNotes) } }, [activeTab, cacheEvents.length, trendingNotes]) @@ -233,13 +234,19 @@ export default function TrendingNotes() { return } + // Prevent re-initialization if cache is already populated + if (cacheEvents.length > 0) { + logger.debug('[TrendingNotes] Cache already populated, skipping initialization') + return + } + const now = Date.now() // Check if cache is still valid if (cachedCustomEvents && (now - cachedCustomEvents.timestamp) < CACHE_DURATION) { // If cache is valid, set cacheEvents to ALL events from cache const allEvents = cachedCustomEvents.events.map(item => item.event) - console.log('[TrendingNotes] Using existing cache - loading', allEvents.length, 'events') + logger.debug('[TrendingNotes] Using existing cache - loading', allEvents.length, 'events') setCacheEvents(allEvents) setCacheLoading(false) // Ensure loading state is cleared return @@ -251,14 +258,14 @@ export default function TrendingNotes() { // Set a timeout to prevent infinite loading const timeoutId = setTimeout(() => { - console.log('[TrendingNotes] Cache initialization timeout - forcing completion') + logger.debug('[TrendingNotes] Cache initialization timeout - forcing completion') isInitializing = false setCacheLoading(false) }, 180000) // 3 minute timeout // Prevent running if we have no relays if (relays.length === 0) { - console.log('[TrendingNotes] No relays available, skipping cache initialization') + logger.debug('[TrendingNotes] No relays available, skipping cache initialization') clearTimeout(timeoutId) isInitializing = false setCacheLoading(false) @@ -269,7 +276,7 @@ export default function TrendingNotes() { const allEvents: NostrEvent[] = [] const twentyFourHoursAgo = Math.floor(Date.now() / 1000) - 24 * 60 * 60 - console.log('[TrendingNotes] Starting cache initialization with', relays.length, 'relays:', relays) + logger.debug('[TrendingNotes] Starting cache initialization with', relays.length, 'relays:', relays) // 1. Fetch top-level posts from last 24 hours - batch requests to avoid overwhelming relays const batchSize = 3 // Process 3 relays at a time @@ -277,18 +284,18 @@ export default function TrendingNotes() { for (let i = 0; i < relays.length; i += batchSize) { const batch = relays.slice(i, i + batchSize) - console.log('[TrendingNotes] Processing batch', Math.floor(i/batchSize) + 1, 'of', Math.ceil(relays.length/batchSize), 'relays:', batch) + logger.debug('[TrendingNotes] Processing batch', Math.floor(i/batchSize) + 1, 'of', Math.ceil(relays.length/batchSize), 'relays:', batch) const batchPromises = batch.map(async (relay) => { try { const events = await client.fetchEvents([relay], { kinds: [1, 11, 30023, 9802, 20, 21, 22], since: twentyFourHoursAgo, - limit: 500 + limit: 100 }) - console.log('[TrendingNotes] Fetched', events.length, 'events from relay', relay) + logger.debug('[TrendingNotes] Fetched', events.length, 'events from relay', relay) return events } catch (error) { - console.warn(`[TrendingNotes] Error fetching from relay ${relay}:`, error) + logger.warn(`[TrendingNotes] Error fetching from relay ${relay}:`, error) return [] } }) @@ -296,7 +303,7 @@ export default function TrendingNotes() { const batchResults = await Promise.all(batchPromises) const batchEvents = batchResults.flat() recentEvents.push(...batchEvents) - console.log('[TrendingNotes] Batch completed, total events so far:', recentEvents.length) + logger.debug('[TrendingNotes] Batch completed, total events so far:', recentEvents.length) // Add a small delay between batches to be respectful to relays if (i + batchSize < relays.length) { @@ -332,7 +339,7 @@ export default function TrendingNotes() { try { const pinEvents = await client.fetchEvents(relays, { ids: pinEventIds, - limit: 500 + limit: 100 }) allEvents.push(...pinEvents) } catch (error) { @@ -388,17 +395,17 @@ export default function TrendingNotes() { // Fetch stats for events in batches with longer delays const eventsNeedingStats = filteredEvents.filter(event => !noteStatsService.getNoteStats(event.id)) - console.log('[TrendingNotes] Need to fetch stats for', eventsNeedingStats.length, 'events') + logger.debug('[TrendingNotes] Need to fetch stats for', eventsNeedingStats.length, 'events') if (eventsNeedingStats.length > 0) { const batchSize = 10 // Increased batch size to speed up const totalBatches = Math.ceil(eventsNeedingStats.length / batchSize) - console.log('[TrendingNotes] Fetching stats in', totalBatches, 'batches') + logger.debug('[TrendingNotes] Fetching stats in', totalBatches, 'batches') for (let i = 0; i < eventsNeedingStats.length; i += batchSize) { const batch = eventsNeedingStats.slice(i, i + batchSize) const batchNum = Math.floor(i / batchSize) + 1 - console.log('[TrendingNotes] Fetching stats batch', batchNum, 'of', totalBatches) + logger.debug('[TrendingNotes] Fetching stats batch', batchNum, 'of', totalBatches) await Promise.all(batch.map(event => noteStatsService.fetchNoteStats(event, undefined, favoriteRelays).catch(() => {}) @@ -408,11 +415,11 @@ export default function TrendingNotes() { await new Promise(resolve => setTimeout(resolve, 200)) // Reduced delay } } - console.log('[TrendingNotes] Stats fetching completed') + logger.debug('[TrendingNotes] Stats fetching completed') } // Score events - console.log('[TrendingNotes] Scoring', filteredEvents.length, 'events') + logger.debug('[TrendingNotes] Scoring', filteredEvents.length, 'events') const scoredEvents = filteredEvents.map((event) => { const stats = noteStatsService.getNoteStats(event.id) let score = 0 @@ -438,7 +445,7 @@ export default function TrendingNotes() { }) // Update cache - console.log('[TrendingNotes] Updating cache with', scoredEvents.length, 'scored events') + logger.debug('[TrendingNotes] Updating cache with', scoredEvents.length, 'scored events') cachedCustomEvents = { events: scoredEvents, timestamp: now, @@ -448,10 +455,10 @@ export default function TrendingNotes() { // Store ALL events from the cache for hashtag analysis // This includes all events from relays, not just the trending ones - console.log('[TrendingNotes] Cache initialization complete - storing', filteredEvents.length, 'events') + logger.debug('[TrendingNotes] Cache initialization complete - storing', filteredEvents.length, 'events') setCacheEvents(filteredEvents) } catch (error) { - console.error('[TrendingNotes] Error initializing cache:', error) + logger.error('[TrendingNotes] Error initializing cache:', error) } finally { clearTimeout(timeoutId) isInitializing = false @@ -475,12 +482,12 @@ export default function TrendingNotes() { } else if (activeTab === 'relays') { // "on your relays" tab: use cache events from user's relays sourceEvents = cacheEvents - console.log('[TrendingNotes] Relays tab - cacheEvents.length:', cacheEvents.length, 'cacheLoading:', cacheLoading) + logger.debug('[TrendingNotes] Relays tab - cacheEvents.length:', cacheEvents.length, 'cacheLoading:', cacheLoading) } else if (activeTab === 'hashtags') { // Hashtags tab: use cache events for hashtag analysis sourceEvents = cacheEvents.length > 0 ? cacheEvents : trendingNotes - console.log('[TrendingNotes] Hashtags tab - using ALL events from cache') - console.log('[TrendingNotes] Hashtags tab - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length) + logger.debug('[TrendingNotes] Hashtags tab - using ALL events from cache') + logger.debug('[TrendingNotes] Hashtags tab - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length) } @@ -631,7 +638,7 @@ export default function TrendingNotes() { setSortOrder('most-popular') // If cache is empty and not loading, log the issue for debugging if (cacheEvents.length === 0 && !cacheLoading && !isInitializing) { - console.log('[TrendingNotes] Relays tab selected but cache is empty - this should not happen if cache initialization completed') + logger.debug('[TrendingNotes] Relays tab selected but cache is empty - this should not happen if cache initialization completed') } } else if (activeTab === 'hashtags') { setSortOrder('most-popular') diff --git a/src/lib/debug-utils.ts b/src/lib/debug-utils.ts new file mode 100644 index 0000000..c324844 --- /dev/null +++ b/src/lib/debug-utils.ts @@ -0,0 +1,61 @@ +/** + * Debug utilities for development and troubleshooting + * + * Usage in browser console: + * - jumbleDebug.enable() - Enable debug logging + * - jumbleDebug.disable() - Disable debug logging + * - jumbleDebug.status() - Check current debug status + */ + +import logger from './logger' + +interface DebugUtils { + enable: () => void + disable: () => void + status: () => { enabled: boolean; level: string } + log: (message: string, ...args: any[]) => void + warn: (message: string, ...args: any[]) => void + error: (message: string, ...args: any[]) => void + perf: (message: string, ...args: any[]) => void +} + +const debugUtils: DebugUtils = { + enable: () => { + logger.setDebugMode(true) + console.log('🔧 Jumble debug logging enabled') + }, + + disable: () => { + logger.setDebugMode(false) + console.log('🔧 Jumble debug logging disabled') + }, + + status: () => { + const enabled = logger.isDebugEnabled() + console.log(`🔧 Jumble debug status: ${enabled ? 'ENABLED' : 'DISABLED'}`) + return { enabled, level: enabled ? 'debug' : 'info' } + }, + + log: (message: string, ...args: any[]) => { + logger.debug(message, ...args) + }, + + warn: (message: string, ...args: any[]) => { + logger.warn(message, ...args) + }, + + error: (message: string, ...args: any[]) => { + logger.error(message, ...args) + }, + + perf: (message: string, ...args: any[]) => { + logger.perf(message, ...args) + } +} + +// Expose debug utilities globally in development +if (import.meta.env.DEV) { + ;(window as any).jumbleDebug = debugUtils +} + +export default debugUtils diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..9186b21 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,114 @@ +/** + * Centralized logging utility to reduce console noise and improve performance + * + * Usage: + * - Use logger.debug() for development debugging (only shows in dev mode) + * - Use logger.info() for important information (always shows) + * - Use logger.warn() for warnings (always shows) + * - Use logger.error() for errors (always shows) + * + * In production builds, debug logs are completely removed to improve performance. + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +interface LoggerConfig { + level: LogLevel + enableDebug: boolean + enablePerformance: boolean +} + +class Logger { + private config: LoggerConfig + + constructor() { + // In production, disable debug logging for better performance + const isDev = import.meta.env.DEV + const isDebugEnabled = isDev && (localStorage.getItem('jumble-debug') === 'true' || import.meta.env.VITE_DEBUG === 'true') + + this.config = { + level: isDebugEnabled ? 'debug' : 'info', + enableDebug: isDebugEnabled, + enablePerformance: isDev + } + } + + private shouldLog(level: LogLevel): boolean { + const levels = ['debug', 'info', 'warn', 'error'] + const currentLevelIndex = levels.indexOf(this.config.level) + const messageLevelIndex = levels.indexOf(level) + return messageLevelIndex >= currentLevelIndex + } + + private formatMessage(level: LogLevel, message: string, ...args: any[]): [string, ...any[]] { + const timestamp = new Date().toISOString().substring(11, 23) // HH:mm:ss.SSS + const prefix = `[${timestamp}] [${level.toUpperCase()}]` + return [`${prefix} ${message}`, ...args] + } + + debug(message: string, ...args: any[]): void { + if (!this.config.enableDebug || !this.shouldLog('debug')) return + console.log(...this.formatMessage('debug', message, ...args)) + } + + info(message: string, ...args: any[]): void { + if (!this.shouldLog('info')) return + console.log(...this.formatMessage('info', message, ...args)) + } + + warn(message: string, ...args: any[]): void { + if (!this.shouldLog('warn')) return + console.warn(...this.formatMessage('warn', message, ...args)) + } + + error(message: string, ...args: any[]): void { + if (!this.shouldLog('error')) return + console.error(...this.formatMessage('error', message, ...args)) + } + + // Performance logging for development + perf(message: string, ...args: any[]): void { + if (!this.config.enablePerformance) return + console.log(`[PERF] ${message}`, ...args) + } + + // Group logging for related operations + group(label: string, fn: () => void): void { + if (!this.config.enableDebug) { + fn() + return + } + console.group(label) + fn() + console.groupEnd() + } + + // Conditional logging based on environment + dev(message: string, ...args: any[]): void { + if (import.meta.env.DEV) { + console.log(message, ...args) + } + } + + // Enable/disable debug mode at runtime + setDebugMode(enabled: boolean): void { + this.config.enableDebug = enabled + this.config.level = enabled ? 'debug' : 'info' + localStorage.setItem('jumble-debug', enabled.toString()) + } + + // Check if debug mode is enabled + isDebugEnabled(): boolean { + return this.config.enableDebug + } +} + +// Create singleton instance +const logger = new Logger() + +// Expose debug toggle for development +if (import.meta.env.DEV) { + ;(window as any).jumbleLogger = logger +} + +export default logger diff --git a/src/main.tsx b/src/main.tsx index 9584f90..bff635f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import './index.css' import './polyfill' import './services/lightning.service' import './lib/error-suppression' +import './lib/debug-utils' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index 9582469..a1accb2 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -5,6 +5,7 @@ import { useNostr } from '@/providers/NostrProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useSmartNoteNavigation } from '@/PageManager' import { toNote } from '@/lib/link' +import logger from '@/lib/logger' import { NostrEvent, Event as NostrEventType } from 'nostr-tools' import { kinds } from 'nostr-tools' import { normalizeUrl } from '@/lib/url' @@ -44,14 +45,14 @@ function countVotesForThread(threadId: string, reactions: NostrEvent[], threadAu return 'emoji' } - console.log('[DiscussionsPage] Counting votes for thread', threadId.substring(0, 8), 'with', reactions.length, 'reactions') + logger.debug('[DiscussionsPage] Counting votes for thread', threadId.substring(0, 8), 'with', reactions.length, 'reactions') // Process all reactions for this thread reactions.forEach(reaction => { const eTags = reaction.tags.filter(tag => tag[0] === 'e' && tag[1]) eTags.forEach(tag => { if (tag[1] === threadId) { - console.log('[DiscussionsPage] Found reaction for thread', threadId.substring(0, 8), ':', { + logger.debug('[DiscussionsPage] Found reaction for thread', threadId.substring(0, 8), ':', { content: reaction.content, pubkey: reaction.pubkey.substring(0, 8), isSelf: reaction.pubkey === threadAuthor, @@ -60,12 +61,12 @@ function countVotesForThread(threadId: string, reactions: NostrEvent[], threadAu // Skip self-votes if (reaction.pubkey === threadAuthor) { - console.log('[DiscussionsPage] Skipping self-vote') + logger.debug('[DiscussionsPage] Skipping self-vote') return } const normalizedReaction = normalizeReaction(reaction.content) - console.log('[DiscussionsPage] Normalized reaction:', normalizedReaction) + logger.debug('[DiscussionsPage] Normalized reaction:', normalizedReaction) if (normalizedReaction === '+' || normalizedReaction === '-') { const existingVote = userVotes.get(reaction.pubkey) @@ -187,11 +188,11 @@ const DiscussionsPage = forwardRef(() => { const discussionThreads = await client.fetchEvents(allRelays, [ { kinds: [11], // ExtendedKind.DISCUSSION - limit: 500 + limit: 100 } ]) - console.log('[DiscussionsPage] Fetched', discussionThreads.length, 'discussion threads') + logger.debug('[DiscussionsPage] Fetched', discussionThreads.length, 'discussion threads') // Step 2: Get thread IDs and fetch related comments and reactions const threadIds = discussionThreads.map((thread: NostrEvent) => thread.id) @@ -238,7 +239,7 @@ const DiscussionsPage = forwardRef(() => { // Debug: Log vote stats for threads with votes if (voteStats.upVotes > 0 || voteStats.downVotes > 0) { - console.log('[DiscussionsPage] Thread', threadId.substring(0, 8), 'has votes:', voteStats) + logger.debug('[DiscussionsPage] Thread', threadId.substring(0, 8), 'has votes:', voteStats) } // Extract topics @@ -274,19 +275,19 @@ const DiscussionsPage = forwardRef(() => { }) }) - console.log('[DiscussionsPage] Built event map with', newEventMap.size, 'threads') + logger.debug('[DiscussionsPage] Built event map with', newEventMap.size, 'threads') // Log vote counts for debugging newEventMap.forEach((entry, threadId) => { if (entry.upVotes > 0 || entry.downVotes > 0) { - console.log('[DiscussionsPage] Thread', threadId.substring(0, 8) + '...', 'has', entry.upVotes, 'upvotes,', entry.downVotes, 'downvotes') + logger.debug('[DiscussionsPage] Thread', threadId.substring(0, 8) + '...', 'has', entry.upVotes, 'upvotes,', entry.downVotes, 'downvotes') } }) setAllEventMap(newEventMap) } catch (error) { - console.error('[DiscussionsPage] Error fetching events:', error) + logger.error('[DiscussionsPage] Error fetching events:', error) } finally { setLoading(false) setIsRefreshing(false) diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx index 0818a4b..eb01d6d 100644 --- a/src/pages/primary/NoteListPage/RelaysFeed.tsx +++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx @@ -1,17 +1,18 @@ import NormalFeed from '@/components/NormalFeed' import { checkAlgoRelay } from '@/lib/relay' +import logger from '@/lib/logger' import { useFeed } from '@/providers/FeedProvider' import relayInfoService from '@/services/relay-info.service' import { useEffect, useState } from 'react' export default function RelaysFeed() { - console.log('RelaysFeed component rendering') + logger.debug('RelaysFeed component rendering') const { feedInfo, relayUrls } = useFeed() const [isReady, setIsReady] = useState(false) const [areAlgoRelays, setAreAlgoRelays] = useState(false) // Debug logging - console.log('RelaysFeed debug:', { + logger.debug('RelaysFeed debug:', { feedInfo, relayUrls, isReady @@ -35,7 +36,7 @@ export default function RelaysFeed() { } const subRequests = [{ urls: relayUrls, filter: {} }] - console.log('RelaysFeed rendering NormalFeed with:', { subRequests, relayUrls, areAlgoRelays }) + logger.debug('RelaysFeed rendering NormalFeed with:', { subRequests, relayUrls, areAlgoRelays }) return ( { - console.log('NoteListPage component rendering') + logger.debug('NoteListPage component rendering') const { t } = useTranslation() const { addRelayUrls, removeRelayUrls } = useCurrentRelays() const layoutRef = useRef(null) @@ -53,7 +54,7 @@ const NoteListPage = forwardRef((_, ref) => { }, [relayUrls]) // Debug logging - console.log('NoteListPage debug:', { + logger.debug('NoteListPage debug:', { isReady, feedInfo, relayUrls, diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index 30bae34..fd77640 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -1,5 +1,6 @@ import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { getRelaySetFromEvent } from '@/lib/event-metadata' +import logger from '@/lib/logger' import { isWebsocketUrl, normalizeUrl } from '@/lib/url' import indexedDb from '@/services/indexed-db.service' import storage from '@/services/local-storage.service' @@ -42,7 +43,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const init = async () => { - console.log('FeedProvider init:', { isInitialized, pubkey }) + logger.debug('FeedProvider init:', { isInitialized, pubkey }) if (!isInitialized) { return } @@ -53,11 +54,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { feedType: 'relay', id: visibleRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0] } - console.log('Initial feedInfo setup:', { visibleRelays, favoriteRelays, blockedRelays, feedInfo }) + logger.debug('Initial feedInfo setup:', { visibleRelays, favoriteRelays, blockedRelays, feedInfo }) if (pubkey) { const storedFeedInfo = storage.getFeedInfo(pubkey) - console.log('Stored feed info:', storedFeedInfo) + logger.debug('Stored feed info:', storedFeedInfo) if (storedFeedInfo) { feedInfo = storedFeedInfo } @@ -70,7 +71,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { if (feedInfo.feedType === 'relay') { // Check if the stored relay is blocked, if so use first visible relay instead if (feedInfo.id && blockedRelays.includes(feedInfo.id)) { - console.log('Stored relay is blocked, using first visible relay instead') + logger.debug('Stored relay is blocked, using first visible relay instead') feedInfo.id = visibleRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0] } return await switchFeed('relay', { relay: feedInfo.id }) @@ -86,7 +87,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { } if (feedInfo.feedType === 'all-favorites') { - console.log('Initializing all-favorites feed') + logger.debug('Initializing all-favorites feed') return await switchFeed('all-favorites') } } @@ -99,7 +100,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { if (feedInfo.feedType === 'all-favorites') { // Filter out blocked relays const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay)) - console.log('Updating relay URLs for all-favorites:', visibleRelays) + logger.debug('Updating relay URLs for all-favorites:', visibleRelays) setRelayUrls(visibleRelays) } }, [favoriteRelays, blockedRelays, feedInfo.feedType]) @@ -112,33 +113,33 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { relay?: string | null } = {} ) => { - console.log('switchFeed called:', { feedType, options }) + logger.debug('switchFeed called:', { feedType, options }) setIsReady(false) if (feedType === 'relay') { const normalizedUrl = normalizeUrl(options.relay ?? '') - console.log('Relay switchFeed:', { normalizedUrl, isWebsocketUrl: isWebsocketUrl(normalizedUrl), blockedRelays }) + logger.debug('Relay switchFeed:', { normalizedUrl, isWebsocketUrl: isWebsocketUrl(normalizedUrl), blockedRelays }) if (!normalizedUrl || !isWebsocketUrl(normalizedUrl)) { - console.log('Invalid relay URL, setting isReady to true') + logger.debug('Invalid relay URL, setting isReady to true') setIsReady(true) return } // Don't allow selecting a blocked relay as feed if (blockedRelays.includes(normalizedUrl)) { - console.warn('Cannot select blocked relay as feed:', normalizedUrl) + logger.warn('Cannot select blocked relay as feed:', normalizedUrl) setIsReady(true) return } const newFeedInfo = { feedType, id: normalizedUrl } - console.log('Setting relay feed info:', newFeedInfo) + logger.debug('Setting relay feed info:', newFeedInfo) setFeedInfo(newFeedInfo) feedInfoRef.current = newFeedInfo setRelayUrls([normalizedUrl]) storage.setFeedInfo(newFeedInfo, pubkey) setIsReady(true) - console.log('Relay feed setup complete, isReady set to true') + logger.debug('Relay feed setup complete, isReady set to true') return } if (feedType === 'relays') { @@ -189,7 +190,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { if (feedType === 'all-favorites') { // Filter out blocked relays const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay)) - console.log('Switching to all-favorites, favoriteRelays:', visibleRelays) + logger.debug('Switching to all-favorites, favoriteRelays:', visibleRelays) const newFeedInfo = { feedType } setFeedInfo(newFeedInfo) feedInfoRef.current = newFeedInfo diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index ce15b36..c278717 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -13,6 +13,7 @@ import { minePow } from '@/lib/event' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' +import logger from '@/lib/logger' import { normalizeUrl } from '@/lib/url' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' @@ -125,7 +126,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { try { (signer as any).disconnect() } catch (error) { - console.warn('Failed to disconnect signer:', error) + logger.warn('Failed to disconnect signer:', error) } } } @@ -141,7 +142,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } } } catch (error) { - console.warn('Extension cleanup failed:', error) + logger.warn('Extension cleanup failed:', error) } } @@ -716,7 +717,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } // Debug: Log the signed event - console.log('Signed event:', { + logger.debug('Signed event:', { id: event.id, pubkey: event.pubkey, sig: event.sig, @@ -728,7 +729,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { // Validate the event before publishing const isValid = validateEvent(event) if (!isValid) { - console.error('Event validation failed:', event) + logger.error('Event validation failed:', event) throw new Error('Event validation failed - invalid signature or format. Please try logging in again.') } diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index ab7817a..7e7e11a 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -267,7 +267,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } const size = 64 canvas.width = size canvas.height = size - const ctx = canvas.getContext('2d') + const ctx = canvas.getContext('2d', { willReadFrequently: true }) // Optimize for frequent readback operations if (!ctx) return // Draw tree emoji as text diff --git a/src/services/client.service.ts b/src/services/client.service.ts index b6637f4..27ae028 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -6,6 +6,7 @@ import { isReplaceableEvent } from '@/lib/event' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' +import logger from '@/lib/logger' import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' import { isWebsocketUrl, normalizeUrl } from '@/lib/url' @@ -58,11 +59,13 @@ class ClientService extends EventTarget { ) private trendingNotesCache: NEvent[] | null = null private requestThrottle = new Map() // Track request timestamps per relay - private readonly REQUEST_COOLDOWN = 2000 // 2 second cooldown between requests to prevent "too many REQs" + private readonly REQUEST_COOLDOWN = 5000 // 5 second cooldown between requests to prevent "too many REQs" private failureCount = new Map() // Track consecutive failures per relay - private readonly MAX_FAILURES = 3 // Max failures before exponential backoff + private readonly MAX_FAILURES = 2 // Max failures before exponential backoff (reduced from 3) private circuitBreaker = new Map() // Track when relays are temporarily disabled - private readonly CIRCUIT_BREAKER_TIMEOUT = 60000 // 1 minute timeout for circuit breaker + private readonly CIRCUIT_BREAKER_TIMEOUT = 120000 // 2 minute timeout for circuit breaker (increased) + private concurrentRequests = new Map() // Track concurrent requests per relay + private readonly MAX_CONCURRENT_REQUESTS = 2 // Max concurrent requests per relay private userIndex = new FlexSearch.Index({ tokenize: 'forward' @@ -199,7 +202,7 @@ class ClientService extends EventTarget { let fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays) - console.log('Relay hint failed, trying fallback relays:', fallbackRelays) + logger.debug('Relay hint failed, trying fallback relays:', fallbackRelays) const fallbackResult = await this._publishToRelays(fallbackRelays, event) // Combine relay statuses from both attempts @@ -214,7 +217,7 @@ class ClientService extends EventTarget { } } catch (error) { // If relay hint throws an error, try fallback relays - console.log('Relay hint threw error, trying fallback relays:', error) + logger.debug('Relay hint threw error, trying fallback relays:', error) // Extract relay statuses from the error if available let hintRelayStatuses: any[] = [] @@ -227,7 +230,7 @@ class ClientService extends EventTarget { let fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays) - console.log('Trying fallback relays:', fallbackRelays) + logger.debug('Trying fallback relays:', fallbackRelays) const fallbackResult = await this._publishToRelays(fallbackRelays, event) // Combine relay statuses from both attempts @@ -385,7 +388,7 @@ class ClientService extends EventTarget { error instanceof Error && error.message.includes('too many concurrent REQs') ) { - console.log(`⚠ Relay ${url} is overloaded, skipping retry`) + logger.debug(`⚠ Relay ${url} is overloaded, skipping retry`) errors.push({ url, error: new Error('Relay overloaded - too many concurrent requests') }) finishedCount++ @@ -1825,7 +1828,7 @@ class ClientService extends EventTarget { // Skip relays with open circuit breaker if (this.isCircuitBreakerOpen(url)) { - console.warn(`Skipping relay with open circuit breaker: ${url}`) + logger.debug(`Skipping relay with open circuit breaker: ${url}`) return false } @@ -1838,14 +1841,14 @@ class ClientService extends EventTarget { return true } catch (error) { - console.warn(`Skipping invalid relay URL: ${url}`, error) + logger.debug(`Skipping invalid relay URL: ${url}`, error) return false } }) - // Limit to 8 relays to prevent "too many concurrent REQs" errors - // Increased from 3 to 8 for discussions to include more relay sources - return validRelays.slice(0, 8) + // Limit to 4 relays to prevent "too many concurrent REQs" errors + // Reduced from 8 to 4 to reduce relay load + return validRelays.slice(0, 4) } // ================= Utils ================= @@ -1854,6 +1857,16 @@ class ClientService extends EventTarget { const now = Date.now() const lastRequest = this.requestThrottle.get(relayUrl) || 0 const failures = this.failureCount.get(relayUrl) || 0 + const concurrent = this.concurrentRequests.get(relayUrl) || 0 + + // Check concurrent request limit + if (concurrent >= this.MAX_CONCURRENT_REQUESTS) { + logger.debug(`Relay ${relayUrl} has ${concurrent} concurrent requests, waiting...`) + // Wait for a concurrent request to complete + while (this.concurrentRequests.get(relayUrl) || 0 >= this.MAX_CONCURRENT_REQUESTS) { + await new Promise(resolve => setTimeout(resolve, 1000)) + } + } // Calculate delay based on failures (exponential backoff) let delay = this.REQUEST_COOLDOWN @@ -1867,12 +1880,19 @@ class ClientService extends EventTarget { await new Promise(resolve => setTimeout(resolve, delay)) } + // Increment concurrent request counter + this.concurrentRequests.set(relayUrl, (this.concurrentRequests.get(relayUrl) || 0) + 1) this.requestThrottle.set(relayUrl, Date.now()) } private recordSuccess(relayUrl: string): void { // Reset failure count on success this.failureCount.delete(relayUrl) + // Decrement concurrent request counter + const current = this.concurrentRequests.get(relayUrl) || 0 + if (current > 0) { + this.concurrentRequests.set(relayUrl, current - 1) + } } private recordFailure(relayUrl: string): void { @@ -1880,10 +1900,16 @@ class ClientService extends EventTarget { const newFailures = currentFailures + 1 this.failureCount.set(relayUrl, newFailures) + // Decrement concurrent request counter + const current = this.concurrentRequests.get(relayUrl) || 0 + if (current > 0) { + this.concurrentRequests.set(relayUrl, current - 1) + } + // Activate circuit breaker if too many failures - if (newFailures >= 5) { + if (newFailures >= 3) { this.circuitBreaker.set(relayUrl, Date.now()) - console.log(`🔴 Circuit breaker activated for ${relayUrl} (${newFailures} failures)`) + logger.debug(`🔴 Circuit breaker activated for ${relayUrl} (${newFailures} failures)`) } } @@ -1896,13 +1922,15 @@ class ClientService extends EventTarget { // Circuit breaker timeout expired, reset it this.circuitBreaker.delete(relayUrl) this.failureCount.delete(relayUrl) - console.log(`🟢 Circuit breaker reset for ${relayUrl}`) + this.concurrentRequests.delete(relayUrl) // Clean up concurrent counter + logger.debug(`🟢 Circuit breaker reset for ${relayUrl}`) return false } return true } + async generateSubRequestsForPubkeys(pubkeys: string[], myPubkey?: string | null) { // Privacy: Only use user's own relays + defaults, never fetch other users' relays let urls = BIG_RELAY_URLS diff --git a/src/services/note-stats.service.ts b/src/services/note-stats.service.ts index dd1287f..600823a 100644 --- a/src/services/note-stats.service.ts +++ b/src/services/note-stats.service.ts @@ -1,6 +1,7 @@ import { BIG_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getZapInfoFromEvent } from '@/lib/event-metadata' +import logger from '@/lib/logger' import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag' import { normalizeUrl } from '@/lib/url' import client from '@/services/client.service' @@ -28,6 +29,8 @@ class NoteStatsService { static instance: NoteStatsService private noteStatsMap: Map> = new Map() private noteStatsSubscribers = new Map void>>() + private processingCache = new Set() // Prevent duplicate processing + private lastProcessedTime = new Map() // Rate limiting constructor() { if (!NoteStatsService.instance) { @@ -37,11 +40,31 @@ class NoteStatsService { } async fetchNoteStats(event: Event, pubkey?: string | null, favoriteRelays?: string[]) { - const oldStats = this.noteStatsMap.get(event.id) - let since: number | undefined - if (oldStats?.updatedAt) { - since = oldStats.updatedAt + const eventId = event.id + + // Rate limiting: Don't process the same event more than once per 5 seconds + const now = Date.now() + const lastProcessed = this.lastProcessedTime.get(eventId) + if (lastProcessed && now - lastProcessed < 5000) { + logger.debug('[NoteStats] Skipping duplicate fetch for event', eventId.substring(0, 8), 'too soon') + return + } + + // Prevent concurrent processing of the same event + if (this.processingCache.has(eventId)) { + logger.debug('[NoteStats] Skipping concurrent fetch for event', eventId.substring(0, 8)) + return } + + this.processingCache.add(eventId) + this.lastProcessedTime.set(eventId, now) + + try { + const oldStats = this.noteStatsMap.get(eventId) + let since: number | undefined + if (oldStats?.updatedAt) { + since = oldStats.updatedAt + } // Privacy: Only use current user's relays + defaults, never connect to other users' relays const [relayList, authorProfile] = await Promise.all([ pubkey ? client.fetchRelayList(pubkey) : Promise.resolve({ write: [], read: [] }), @@ -66,7 +89,7 @@ class NoteStatsService { const relayTypes = pubkey ? 'inboxes kind 10002 + favorites kind 10012 + big relays' : 'big relays + fast read relays + searchable relays (anonymous user)' - console.log('[NoteStats] Using', finalRelayUrls.length, 'relays for stats (' + relayTypes + '):', finalRelayUrls) + logger.debug('[NoteStats] Using', finalRelayUrls.length, 'relays for stats (' + relayTypes + '):', finalRelayUrls) const replaceableCoordinate = isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) @@ -76,7 +99,7 @@ class NoteStatsService { { '#e': [event.id], kinds: [kinds.Reaction], - limit: 500 + limit: 100 }, { '#e': [event.id], @@ -86,17 +109,17 @@ class NoteStatsService { { '#e': [event.id], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], - limit: 500 + limit: 100 }, { '#q': [event.id], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], - limit: 500 + limit: 100 }, { '#e': [event.id], kinds: [kinds.Highlights], - limit: 500 + limit: 100 } ] @@ -105,7 +128,7 @@ class NoteStatsService { { '#a': [replaceableCoordinate], kinds: [kinds.Reaction], - limit: 500 + limit: 100 }, { '#a': [replaceableCoordinate], @@ -115,17 +138,17 @@ class NoteStatsService { { '#a': [replaceableCoordinate], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], - limit: 500 + limit: 100 }, { '#q': [replaceableCoordinate], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], - limit: 500 + limit: 100 }, { '#a': [replaceableCoordinate], kinds: [kinds.Highlights], - limit: 500 + limit: 100 } ) } @@ -134,14 +157,14 @@ class NoteStatsService { filters.push({ '#e': [event.id], kinds: [kinds.Zap], - limit: 500 + limit: 100 }) if (replaceableCoordinate) { filters.push({ '#a': [replaceableCoordinate], kinds: [kinds.Zap], - limit: 500 + limit: 100 }) } } @@ -184,26 +207,30 @@ class NoteStatsService { }) } const events: Event[] = [] - console.log('[NoteStats] Fetching stats for event', event.id, 'from', finalRelayUrls.length, 'relays') + logger.debug('[NoteStats] Fetching stats for event', event.id.substring(0, 8), 'from', finalRelayUrls.length, 'relays') await client.fetchEvents(finalRelayUrls, filters, { onevent: (evt) => { this.updateNoteStatsByEvents([evt], event.pubkey) events.push(evt) } }) - console.log('[NoteStats] Fetched', events.length, 'events for stats') + logger.debug('[NoteStats] Fetched', events.length, 'events for stats') // Debug: Count events by kind const eventsByKind = events.reduce((acc, evt) => { acc[evt.kind] = (acc[evt.kind] || 0) + 1 return acc }, {} as Record) - console.log('[NoteStats] Events by kind:', eventsByKind) + logger.debug('[NoteStats] Events by kind:', eventsByKind) this.noteStatsMap.set(event.id, { ...(this.noteStatsMap.get(event.id) ?? {}), updatedAt: dayjs().unix() }) return this.noteStatsMap.get(event.id) ?? {} + } finally { + // Clean up processing cache + this.processingCache.delete(eventId) + } } subscribeNoteStats(noteId: string, callback: () => void) { @@ -394,7 +421,7 @@ class NoteStatsService { }) if (parentETag) { originalEventId = parentETag[1] - console.log('[NoteStats] Found reply with root/reply marker:', evt.id, '->', originalEventId) + logger.debug('[NoteStats] Found reply with root/reply marker:', evt.id, '->', originalEventId) } else { // Look for the last E tag that's not a mention const embeddedEventIds = this.getEmbeddedNoteBech32Ids(evt) @@ -434,14 +461,14 @@ class NoteStatsService { // Skip self-interactions - don't count replies from the original event author if (originalEventAuthor && originalEventAuthor === evt.pubkey) { - console.log('[NoteStats] Skipping self-reply from', evt.pubkey, 'to event', originalEventId) + logger.debug('[NoteStats] Skipping self-reply from', evt.pubkey, 'to event', originalEventId) return } replyIdSet.add(evt.id) replies.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at }) this.noteStatsMap.set(originalEventId, { ...old, replyIdSet, replies }) - console.log('[NoteStats] Added reply:', evt.id, 'to event:', originalEventId, 'total replies:', replies.length) + logger.debug('[NoteStats] Added reply:', evt.id, 'to event:', originalEventId, 'total replies:', replies.length) return originalEventId }