Browse Source

clean up console 1

imwald
Silberengel 5 months ago
parent
commit
32aae50b6c
  1. 119
      LOGGING.md
  2. 9
      src/PageManager.tsx
  3. 3
      src/components/NormalFeed/index.tsx
  4. 5
      src/components/Note/index.tsx
  5. 11
      src/components/NoteList/index.tsx
  6. 13
      src/components/Profile/ProfileBookmarksAndHashtags.tsx
  7. 25
      src/components/ReplyNoteList/index.tsx
  8. 67
      src/components/TrendingNotes/index.tsx
  9. 61
      src/lib/debug-utils.ts
  10. 114
      src/lib/logger.ts
  11. 1
      src/main.tsx
  12. 21
      src/pages/primary/DiscussionsPage/index.tsx
  13. 7
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  14. 5
      src/pages/primary/NoteListPage/index.tsx
  15. 27
      src/providers/FeedProvider.tsx
  16. 9
      src/providers/NostrProvider/index.tsx
  17. 2
      src/providers/NotificationProvider.tsx
  18. 58
      src/services/client.service.ts
  19. 63
      src/services/note-stats.service.ts

119
LOGGING.md

@ -0,0 +1,119 @@ @@ -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.

9
src/PageManager.tsx

@ -1,6 +1,7 @@ @@ -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({ @@ -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({ @@ -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 (
<div
key={name}
@ -394,10 +395,10 @@ function MainContentArea({ @@ -394,10 +395,10 @@ function MainContentArea({
>
{(() => {
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 <div>Error rendering {name}: {error instanceof Error ? error.message : String(error)}</div>
}
})()}

3
src/components/NormalFeed/index.tsx

@ -1,5 +1,6 @@ @@ -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<TNoteListRef, { @@ -20,7 +21,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
isMainFeed = false,
showRelayCloseReason = false
}, ref) {
console.log('NormalFeed component rendering with:', { subRequests, areAlgoRelays, isMainFeed })
logger.debug('NormalFeed component rendering with:', { subRequests, areAlgoRelays, isMainFeed })
const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)

5
src/components/Note/index.tsx

@ -2,6 +2,7 @@ import { useSmartNoteNavigation } from '@/PageManager' @@ -2,6 +2,7 @@ import { useSmartNoteNavigation } from '@/PageManager'
import { ExtendedKind, SUPPORTED_KINDS } from '@/constants'
import { getParentBech32Id, isNsfwEvent } from '@/lib/event'
import { toNote } from '@/lib/link'
import logger from '@/lib/logger'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -76,7 +77,7 @@ export default function Note({ @@ -76,7 +77,7 @@ export default function Note({
if (!supportedKindsList.includes(event.kind)) {
console.log('Note component - rendering UnknownNote for unsupported kind:', event.kind)
logger.debug('Note component - rendering UnknownNote for unsupported kind:', event.kind)
content = <UnknownNote className="mt-2" event={event} />
} else if (mutePubkeySet.has(event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} />
@ -87,7 +88,7 @@ export default function Note({ @@ -87,7 +88,7 @@ export default function Note({
try {
content = <Highlight className="mt-2" event={event} />
} catch (error) {
console.error('Note component - Error rendering Highlight component:', error)
logger.error('Note component - Error rendering Highlight component:', error)
content = <div className="mt-2 p-4 bg-red-100 border border-red-500 rounded">
<div className="font-bold text-red-800">HIGHLIGHT ERROR:</div>
<div className="text-red-700">Error: {String(error)}</div>

11
src/components/NoteList/index.tsx

@ -7,6 +7,7 @@ import { @@ -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' @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 (

13
src/components/Profile/ProfileBookmarksAndHashtags.tsx

@ -5,6 +5,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -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({ @@ -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({ @@ -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 {

25
src/components/ReplyNoteList/index.tsx

@ -9,6 +9,7 @@ import { @@ -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 @@ -98,7 +99,12 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
}
while (parentEventKeys.length > 0) {
const processedEventIds = new Set<string>() // 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 @@ -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 @@ -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 = {

67
src/components/TrendingNotes/index.tsx

@ -11,6 +11,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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')

61
src/lib/debug-utils.ts

@ -0,0 +1,61 @@ @@ -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

114
src/lib/logger.ts

@ -0,0 +1,114 @@ @@ -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

1
src/main.tsx

@ -3,6 +3,7 @@ import './index.css' @@ -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'

21
src/pages/primary/DiscussionsPage/index.tsx

@ -5,6 +5,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -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 @@ -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 @@ -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(() => { @@ -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(() => { @@ -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(() => { @@ -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)

7
src/pages/primary/NoteListPage/RelaysFeed.tsx

@ -1,17 +1,18 @@ @@ -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() { @@ -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 (
<NormalFeed

5
src/pages/primary/NoteListPage/index.tsx

@ -26,9 +26,10 @@ import ExploreButton from '@/components/Titlebar/ExploreButton' @@ -26,9 +26,10 @@ import ExploreButton from '@/components/Titlebar/ExploreButton'
import AccountButton from '@/components/Titlebar/AccountButton'
import FollowingFeed from './FollowingFeed'
import RelaysFeed from './RelaysFeed'
import logger from '@/lib/logger'
const NoteListPage = forwardRef((_, ref) => {
console.log('NoteListPage component rendering')
logger.debug('NoteListPage component rendering')
const { t } = useTranslation()
const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
const layoutRef = useRef<TPageRef>(null)
@ -53,7 +54,7 @@ const NoteListPage = forwardRef((_, ref) => { @@ -53,7 +54,7 @@ const NoteListPage = forwardRef((_, ref) => {
}, [relayUrls])
// Debug logging
console.log('NoteListPage debug:', {
logger.debug('NoteListPage debug:', {
isReady,
feedInfo,
relayUrls,

27
src/providers/FeedProvider.tsx

@ -1,5 +1,6 @@ @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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

9
src/providers/NostrProvider/index.tsx

@ -13,6 +13,7 @@ import { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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.')
}

2
src/providers/NotificationProvider.tsx

@ -267,7 +267,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -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

58
src/services/client.service.ts

@ -6,6 +6,7 @@ import { @@ -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 { @@ -58,11 +59,13 @@ class ClientService extends EventTarget {
)
private trendingNotesCache: NEvent[] | null = null
private requestThrottle = new Map<string, number>() // 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<string, number>() // 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<string, number>() // 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<string, number>() // 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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

63
src/services/note-stats.service.ts

@ -1,6 +1,7 @@ @@ -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 { @@ -28,6 +29,8 @@ class NoteStatsService {
static instance: NoteStatsService
private noteStatsMap: Map<string, Partial<TNoteStats>> = new Map()
private noteStatsSubscribers = new Map<string, Set<() => void>>()
private processingCache = new Set<string>() // Prevent duplicate processing
private lastProcessedTime = new Map<string, number>() // Rate limiting
constructor() {
if (!NoteStatsService.instance) {
@ -37,7 +40,27 @@ class NoteStatsService { @@ -37,7 +40,27 @@ class NoteStatsService {
}
async fetchNoteStats(event: Event, pubkey?: string | null, favoriteRelays?: string[]) {
const oldStats = this.noteStatsMap.get(event.id)
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
@ -66,7 +89,7 @@ class NoteStatsService { @@ -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 { @@ -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 { @@ -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 { @@ -105,7 +128,7 @@ class NoteStatsService {
{
'#a': [replaceableCoordinate],
kinds: [kinds.Reaction],
limit: 500
limit: 100
},
{
'#a': [replaceableCoordinate],
@ -115,17 +138,17 @@ class NoteStatsService { @@ -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 { @@ -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 { @@ -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<number, number>)
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 { @@ -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 { @@ -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
}

Loading…
Cancel
Save