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 @@
# 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 @@
import Sidebar from '@/components/Sidebar' import Sidebar from '@/components/Sidebar'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import logger from '@/lib/logger'
import { ChevronLeft } from 'lucide-react' import { ChevronLeft } from 'lucide-react'
import NoteListPage from '@/pages/primary/NoteListPage' import NoteListPage from '@/pages/primary/NoteListPage'
import HomePage from '@/pages/secondary/HomePage' import HomePage from '@/pages/secondary/HomePage'
@ -339,7 +340,7 @@ function MainContentArea({
}) { }) {
const { showRecommendedRelaysPanel } = useUserPreferences() const { showRecommendedRelaysPanel } = useUserPreferences()
console.log('MainContentArea rendering:', { logger.debug('MainContentArea rendering:', {
currentPrimaryPage, currentPrimaryPage,
primaryPages: primaryPages.map(p => p.name), primaryPages: primaryPages.map(p => p.name),
showRecommendedRelaysPanel, showRecommendedRelaysPanel,
@ -383,7 +384,7 @@ function MainContentArea({
// Show normal primary pages // Show normal primary pages
primaryPages.map(({ name, element, props }) => { primaryPages.map(({ name, element, props }) => {
const isCurrentPage = currentPrimaryPage === name const isCurrentPage = currentPrimaryPage === name
console.log(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage }) logger.debug(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage })
return ( return (
<div <div
key={name} key={name}
@ -394,10 +395,10 @@ function MainContentArea({
> >
{(() => { {(() => {
try { try {
console.log(`Rendering ${name} component`) logger.debug(`Rendering ${name} component`)
return props ? cloneElement(element as React.ReactElement, props) : element return props ? cloneElement(element as React.ReactElement, props) : element
} catch (error) { } 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> return <div>Error rendering {name}: {error instanceof Error ? error.message : String(error)}</div>
} }
})()} })()}

3
src/components/NormalFeed/index.tsx

@ -1,5 +1,6 @@
import NoteList, { TNoteListRef } from '@/components/NoteList' import NoteList, { TNoteListRef } from '@/components/NoteList'
import Tabs from '@/components/Tabs' import Tabs from '@/components/Tabs'
import logger from '@/lib/logger'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useKindFilter } from '@/providers/KindFilterProvider' import { useKindFilter } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
@ -20,7 +21,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
isMainFeed = false, isMainFeed = false,
showRelayCloseReason = false showRelayCloseReason = false
}, ref) { }, ref) {
console.log('NormalFeed component rendering with:', { subRequests, areAlgoRelays, isMainFeed }) logger.debug('NormalFeed component rendering with:', { subRequests, areAlgoRelays, isMainFeed })
const { hideUntrustedNotes } = useUserTrust() const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter() const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)

5
src/components/Note/index.tsx

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

11
src/components/NoteList/index.tsx

@ -7,6 +7,7 @@ import {
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
@ -33,7 +34,7 @@ import { toast } from 'sonner'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
const LIMIT = 200 const LIMIT = 200
const ALGO_LIMIT = 500 const ALGO_LIMIT = 200
const SHOW_COUNT = 10 const SHOW_COUNT = 10
const NoteList = forwardRef( const NoteList = forwardRef(
@ -156,7 +157,7 @@ const NoteList = forwardRef(
useImperativeHandle(ref, () => ({ scrollToTop, refresh }), []) useImperativeHandle(ref, () => ({ scrollToTop, refresh }), [])
useEffect(() => { useEffect(() => {
console.log('NoteList useEffect:', { subRequests, subRequestsLength: subRequests.length }) logger.debug('NoteList useEffect:', { subRequests, subRequestsLength: subRequests.length })
if (!subRequests.length) return if (!subRequests.length) return
async function init() { async function init() {
@ -172,7 +173,7 @@ const NoteList = forwardRef(
return () => {} return () => {}
} }
console.log('NoteList subscribing to timeline with:', subRequests.map(({ urls, filter }) => ({ logger.debug('NoteList subscribing to timeline with:', subRequests.map(({ urls, filter }) => ({
urls, urls,
filter: { filter: {
kinds: showKinds, kinds: showKinds,
@ -192,7 +193,7 @@ const NoteList = forwardRef(
})), })),
{ {
onEvents: (events, eosed) => { 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) { if (events.length > 0) {
setEvents(events) setEvents(events)
// Stop loading as soon as we have events, don't wait for all relays // Stop loading as soon as we have events, don't wait for all relays
@ -220,7 +221,7 @@ const NoteList = forwardRef(
} }
}, },
onClose: (url, reason) => { onClose: (url, reason) => {
console.log('Relay connection closed:', { url, reason }) logger.debug('Relay connection closed:', { url, reason })
if (!showRelayCloseReason) return if (!showRelayCloseReason) return
// ignore reasons from nostr-tools // ignore reasons from nostr-tools
if ( if (

13
src/components/Profile/ProfileBookmarksAndHashtags.tsx

@ -5,6 +5,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' 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 { normalizeUrl } from '@/lib/url'
import NoteCard from '../NoteCard' import NoteCard from '../NoteCard'
import { Skeleton } from '../ui/skeleton' 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 // Use the same comprehensive relay list we built for the bookmark list event
const events = await client.fetchEvents(comprehensiveRelays, { const events = await client.fetchEvents(comprehensiveRelays, {
ids: eventIds, ids: eventIds,
limit: 500 limit: 100
}) })
console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'bookmark events') logger.debug('[ProfileBookmarksAndHashtags] Fetched', events.length, 'bookmark events')
setBookmarkEvents(events) setBookmarkEvents(events)
} catch (error) { } catch (error) {
console.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark events:', error) logger.warn('[ProfileBookmarksAndHashtags] Error fetching bookmark events:', error)
setBookmarkEvents([]) setBookmarkEvents([])
} }
} else { } else {
@ -212,12 +213,12 @@ export default function ProfileBookmarksAndHashtags({
// Use the same comprehensive relay list we built for the pin list event // Use the same comprehensive relay list we built for the pin list event
const events = await client.fetchEvents(comprehensiveRelays, { const events = await client.fetchEvents(comprehensiveRelays, {
ids: eventIds, ids: eventIds,
limit: 500 limit: 100
}) })
console.log('[ProfileBookmarksAndHashtags] Fetched', events.length, 'pin events') logger.debug('[ProfileBookmarksAndHashtags] Fetched', events.length, 'pin events')
setPinEvents(events) setPinEvents(events)
} catch (error) { } catch (error) {
console.warn('[ProfileBookmarksAndHashtags] Error fetching pin events:', error) logger.warn('[ProfileBookmarksAndHashtags] Error fetching pin events:', error)
setPinEvents([]) setPinEvents([])
} }
} else { } else {

25
src/components/ReplyNoteList/index.tsx

@ -9,6 +9,7 @@ import {
isReplaceableEvent, isReplaceableEvent,
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import logger from '@/lib/logger'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url' 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<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 || []) const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || [])
events.forEach((evt) => { events.forEach((evt) => {
@ -113,7 +119,18 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
replyIdSet.add(evt.id) replyIdSet.add(evt.id)
replyEvents.push(evt) 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]) }, [rootInfo, currentIndex, index, onNewReply])
useEffect(() => { useEffect(() => {
if (replies.length === 0) { if (replies.length === 0 && !loading && timelineKey) {
loadMore() loadMore()
} }
}, [replies]) }, [replies.length, loading, timelineKey]) // More specific dependencies to prevent infinite loops
useEffect(() => { useEffect(() => {
const options = { const options = {

67
src/components/TrendingNotes/index.tsx

@ -11,6 +11,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useZap } from '@/providers/ZapProvider' import { useZap } from '@/providers/ZapProvider'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
const SHOW_COUNT = 10 const SHOW_COUNT = 10
@ -54,12 +55,12 @@ export default function TrendingNotes() {
// Debug: Track cacheEvents changes // Debug: Track cacheEvents changes
useEffect(() => { useEffect(() => {
console.log('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events') logger.debug('[TrendingNotes] cacheEvents state changed:', cacheEvents.length, 'events')
}, [cacheEvents]) }, [cacheEvents])
// Debug: Track cacheLoading changes // Debug: Track cacheLoading changes
useEffect(() => { useEffect(() => {
console.log('[TrendingNotes] cacheLoading state changed:', cacheLoading) logger.debug('[TrendingNotes] cacheLoading state changed:', cacheLoading)
}, [cacheLoading]) }, [cacheLoading])
@ -128,7 +129,7 @@ export default function TrendingNotes() {
const flattenedIds = allEventIds.flat() const flattenedIds = allEventIds.flat()
setFollowsBookmarkEventIds(flattenedIds) setFollowsBookmarkEventIds(flattenedIds)
} catch (error) { } 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) // Calculate popular hashtags from cache events (all events from relays)
const calculatePopularHashtags = useMemo(() => { 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 // Use cache events if available, otherwise fallback to trending notes
let eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : trendingNotes let eventsToAnalyze = cacheEvents.length > 0 ? cacheEvents : trendingNotes
@ -180,8 +181,8 @@ export default function TrendingNotes() {
.slice(0, 10) .slice(0, 10)
.map(([hashtag]) => hashtag) .map(([hashtag]) => hashtag)
console.log('[TrendingNotes] calculatePopularHashtags - found hashtags:', result) logger.debug('[TrendingNotes] calculatePopularHashtags - found hashtags:', result)
console.log('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags) logger.debug('[TrendingNotes] calculatePopularHashtags - eventsWithHashtags:', eventsWithHashtags)
return result return result
}, [cacheEvents, trendingNotes, activeTab, hashtagFilter, pubkey]) // Use cacheEvents and trendingNotes as dependencies }, [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 // Update popular hashtags when trending notes change
useEffect(() => { useEffect(() => {
console.log('[TrendingNotes] calculatePopularHashtags result:', calculatePopularHashtags) logger.debug('[TrendingNotes] calculatePopularHashtags result:', calculatePopularHashtags)
setPopularHashtags(calculatePopularHashtags) setPopularHashtags(calculatePopularHashtags)
}, [calculatePopularHashtags]) }, [calculatePopularHashtags])
// Fallback: populate cacheEvents from trendingNotes if cache is empty // Fallback: populate cacheEvents from trendingNotes if cache is empty
useEffect(() => { useEffect(() => {
if (activeTab === 'hashtags' && cacheEvents.length === 0 && trendingNotes.length > 0) { 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) setCacheEvents(trendingNotes)
} }
}, [activeTab, cacheEvents.length, trendingNotes]) }, [activeTab, cacheEvents.length, trendingNotes])
@ -233,13 +234,19 @@ export default function TrendingNotes() {
return 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() const now = Date.now()
// Check if cache is still valid // Check if cache is still valid
if (cachedCustomEvents && (now - cachedCustomEvents.timestamp) < CACHE_DURATION) { if (cachedCustomEvents && (now - cachedCustomEvents.timestamp) < CACHE_DURATION) {
// If cache is valid, set cacheEvents to ALL events from cache // If cache is valid, set cacheEvents to ALL events from cache
const allEvents = cachedCustomEvents.events.map(item => item.event) 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) setCacheEvents(allEvents)
setCacheLoading(false) // Ensure loading state is cleared setCacheLoading(false) // Ensure loading state is cleared
return return
@ -251,14 +258,14 @@ export default function TrendingNotes() {
// Set a timeout to prevent infinite loading // Set a timeout to prevent infinite loading
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
console.log('[TrendingNotes] Cache initialization timeout - forcing completion') logger.debug('[TrendingNotes] Cache initialization timeout - forcing completion')
isInitializing = false isInitializing = false
setCacheLoading(false) setCacheLoading(false)
}, 180000) // 3 minute timeout }, 180000) // 3 minute timeout
// Prevent running if we have no relays // Prevent running if we have no relays
if (relays.length === 0) { if (relays.length === 0) {
console.log('[TrendingNotes] No relays available, skipping cache initialization') logger.debug('[TrendingNotes] No relays available, skipping cache initialization')
clearTimeout(timeoutId) clearTimeout(timeoutId)
isInitializing = false isInitializing = false
setCacheLoading(false) setCacheLoading(false)
@ -269,7 +276,7 @@ export default function TrendingNotes() {
const allEvents: NostrEvent[] = [] const allEvents: NostrEvent[] = []
const twentyFourHoursAgo = Math.floor(Date.now() / 1000) - 24 * 60 * 60 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 // 1. Fetch top-level posts from last 24 hours - batch requests to avoid overwhelming relays
const batchSize = 3 // Process 3 relays at a time 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) { for (let i = 0; i < relays.length; i += batchSize) {
const batch = relays.slice(i, 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) => { const batchPromises = batch.map(async (relay) => {
try { try {
const events = await client.fetchEvents([relay], { const events = await client.fetchEvents([relay], {
kinds: [1, 11, 30023, 9802, 20, 21, 22], kinds: [1, 11, 30023, 9802, 20, 21, 22],
since: twentyFourHoursAgo, 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 return events
} catch (error) { } catch (error) {
console.warn(`[TrendingNotes] Error fetching from relay ${relay}:`, error) logger.warn(`[TrendingNotes] Error fetching from relay ${relay}:`, error)
return [] return []
} }
}) })
@ -296,7 +303,7 @@ export default function TrendingNotes() {
const batchResults = await Promise.all(batchPromises) const batchResults = await Promise.all(batchPromises)
const batchEvents = batchResults.flat() const batchEvents = batchResults.flat()
recentEvents.push(...batchEvents) 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 // Add a small delay between batches to be respectful to relays
if (i + batchSize < relays.length) { if (i + batchSize < relays.length) {
@ -332,7 +339,7 @@ export default function TrendingNotes() {
try { try {
const pinEvents = await client.fetchEvents(relays, { const pinEvents = await client.fetchEvents(relays, {
ids: pinEventIds, ids: pinEventIds,
limit: 500 limit: 100
}) })
allEvents.push(...pinEvents) allEvents.push(...pinEvents)
} catch (error) { } catch (error) {
@ -388,17 +395,17 @@ export default function TrendingNotes() {
// Fetch stats for events in batches with longer delays // Fetch stats for events in batches with longer delays
const eventsNeedingStats = filteredEvents.filter(event => !noteStatsService.getNoteStats(event.id)) 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) { if (eventsNeedingStats.length > 0) {
const batchSize = 10 // Increased batch size to speed up const batchSize = 10 // Increased batch size to speed up
const totalBatches = Math.ceil(eventsNeedingStats.length / batchSize) 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) { for (let i = 0; i < eventsNeedingStats.length; i += batchSize) {
const batch = eventsNeedingStats.slice(i, i + batchSize) const batch = eventsNeedingStats.slice(i, i + batchSize)
const batchNum = Math.floor(i / batchSize) + 1 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 => await Promise.all(batch.map(event =>
noteStatsService.fetchNoteStats(event, undefined, favoriteRelays).catch(() => {}) noteStatsService.fetchNoteStats(event, undefined, favoriteRelays).catch(() => {})
@ -408,11 +415,11 @@ export default function TrendingNotes() {
await new Promise(resolve => setTimeout(resolve, 200)) // Reduced delay await new Promise(resolve => setTimeout(resolve, 200)) // Reduced delay
} }
} }
console.log('[TrendingNotes] Stats fetching completed') logger.debug('[TrendingNotes] Stats fetching completed')
} }
// Score events // Score events
console.log('[TrendingNotes] Scoring', filteredEvents.length, 'events') logger.debug('[TrendingNotes] Scoring', filteredEvents.length, 'events')
const scoredEvents = filteredEvents.map((event) => { const scoredEvents = filteredEvents.map((event) => {
const stats = noteStatsService.getNoteStats(event.id) const stats = noteStatsService.getNoteStats(event.id)
let score = 0 let score = 0
@ -438,7 +445,7 @@ export default function TrendingNotes() {
}) })
// Update cache // Update cache
console.log('[TrendingNotes] Updating cache with', scoredEvents.length, 'scored events') logger.debug('[TrendingNotes] Updating cache with', scoredEvents.length, 'scored events')
cachedCustomEvents = { cachedCustomEvents = {
events: scoredEvents, events: scoredEvents,
timestamp: now, timestamp: now,
@ -448,10 +455,10 @@ export default function TrendingNotes() {
// Store ALL events from the cache for hashtag analysis // Store ALL events from the cache for hashtag analysis
// This includes all events from relays, not just the trending ones // 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) setCacheEvents(filteredEvents)
} catch (error) { } catch (error) {
console.error('[TrendingNotes] Error initializing cache:', error) logger.error('[TrendingNotes] Error initializing cache:', error)
} finally { } finally {
clearTimeout(timeoutId) clearTimeout(timeoutId)
isInitializing = false isInitializing = false
@ -475,12 +482,12 @@ export default function TrendingNotes() {
} else if (activeTab === 'relays') { } else if (activeTab === 'relays') {
// "on your relays" tab: use cache events from user's relays // "on your relays" tab: use cache events from user's relays
sourceEvents = cacheEvents 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') { } else if (activeTab === 'hashtags') {
// Hashtags tab: use cache events for hashtag analysis // Hashtags tab: use cache events for hashtag analysis
sourceEvents = cacheEvents.length > 0 ? cacheEvents : trendingNotes sourceEvents = cacheEvents.length > 0 ? cacheEvents : trendingNotes
console.log('[TrendingNotes] Hashtags tab - using ALL events from cache') logger.debug('[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 - cacheEvents.length:', cacheEvents.length, 'trendingNotes.length:', trendingNotes.length)
} }
@ -631,7 +638,7 @@ export default function TrendingNotes() {
setSortOrder('most-popular') setSortOrder('most-popular')
// If cache is empty and not loading, log the issue for debugging // If cache is empty and not loading, log the issue for debugging
if (cacheEvents.length === 0 && !cacheLoading && !isInitializing) { 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') { } else if (activeTab === 'hashtags') {
setSortOrder('most-popular') setSortOrder('most-popular')

61
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

114
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

1
src/main.tsx

@ -3,6 +3,7 @@ import './index.css'
import './polyfill' import './polyfill'
import './services/lightning.service' import './services/lightning.service'
import './lib/error-suppression' import './lib/error-suppression'
import './lib/debug-utils'
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'

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

@ -5,6 +5,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useSmartNoteNavigation } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import logger from '@/lib/logger'
import { NostrEvent, Event as NostrEventType } from 'nostr-tools' import { NostrEvent, Event as NostrEventType } from 'nostr-tools'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
@ -44,14 +45,14 @@ function countVotesForThread(threadId: string, reactions: NostrEvent[], threadAu
return 'emoji' 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 // Process all reactions for this thread
reactions.forEach(reaction => { reactions.forEach(reaction => {
const eTags = reaction.tags.filter(tag => tag[0] === 'e' && tag[1]) const eTags = reaction.tags.filter(tag => tag[0] === 'e' && tag[1])
eTags.forEach(tag => { eTags.forEach(tag => {
if (tag[1] === threadId) { 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, content: reaction.content,
pubkey: reaction.pubkey.substring(0, 8), pubkey: reaction.pubkey.substring(0, 8),
isSelf: reaction.pubkey === threadAuthor, isSelf: reaction.pubkey === threadAuthor,
@ -60,12 +61,12 @@ function countVotesForThread(threadId: string, reactions: NostrEvent[], threadAu
// Skip self-votes // Skip self-votes
if (reaction.pubkey === threadAuthor) { if (reaction.pubkey === threadAuthor) {
console.log('[DiscussionsPage] Skipping self-vote') logger.debug('[DiscussionsPage] Skipping self-vote')
return return
} }
const normalizedReaction = normalizeReaction(reaction.content) const normalizedReaction = normalizeReaction(reaction.content)
console.log('[DiscussionsPage] Normalized reaction:', normalizedReaction) logger.debug('[DiscussionsPage] Normalized reaction:', normalizedReaction)
if (normalizedReaction === '+' || normalizedReaction === '-') { if (normalizedReaction === '+' || normalizedReaction === '-') {
const existingVote = userVotes.get(reaction.pubkey) const existingVote = userVotes.get(reaction.pubkey)
@ -187,11 +188,11 @@ const DiscussionsPage = forwardRef(() => {
const discussionThreads = await client.fetchEvents(allRelays, [ const discussionThreads = await client.fetchEvents(allRelays, [
{ {
kinds: [11], // ExtendedKind.DISCUSSION 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 // Step 2: Get thread IDs and fetch related comments and reactions
const threadIds = discussionThreads.map((thread: NostrEvent) => thread.id) const threadIds = discussionThreads.map((thread: NostrEvent) => thread.id)
@ -238,7 +239,7 @@ const DiscussionsPage = forwardRef(() => {
// Debug: Log vote stats for threads with votes // Debug: Log vote stats for threads with votes
if (voteStats.upVotes > 0 || voteStats.downVotes > 0) { 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 // 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 // Log vote counts for debugging
newEventMap.forEach((entry, threadId) => { newEventMap.forEach((entry, threadId) => {
if (entry.upVotes > 0 || entry.downVotes > 0) { 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) setAllEventMap(newEventMap)
} catch (error) { } catch (error) {
console.error('[DiscussionsPage] Error fetching events:', error) logger.error('[DiscussionsPage] Error fetching events:', error)
} finally { } finally {
setLoading(false) setLoading(false)
setIsRefreshing(false) setIsRefreshing(false)

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

@ -1,17 +1,18 @@
import NormalFeed from '@/components/NormalFeed' import NormalFeed from '@/components/NormalFeed'
import { checkAlgoRelay } from '@/lib/relay' import { checkAlgoRelay } from '@/lib/relay'
import logger from '@/lib/logger'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import relayInfoService from '@/services/relay-info.service' import relayInfoService from '@/services/relay-info.service'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export default function RelaysFeed() { export default function RelaysFeed() {
console.log('RelaysFeed component rendering') logger.debug('RelaysFeed component rendering')
const { feedInfo, relayUrls } = useFeed() const { feedInfo, relayUrls } = useFeed()
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
const [areAlgoRelays, setAreAlgoRelays] = useState(false) const [areAlgoRelays, setAreAlgoRelays] = useState(false)
// Debug logging // Debug logging
console.log('RelaysFeed debug:', { logger.debug('RelaysFeed debug:', {
feedInfo, feedInfo,
relayUrls, relayUrls,
isReady isReady
@ -35,7 +36,7 @@ export default function RelaysFeed() {
} }
const subRequests = [{ urls: relayUrls, filter: {} }] const subRequests = [{ urls: relayUrls, filter: {} }]
console.log('RelaysFeed rendering NormalFeed with:', { subRequests, relayUrls, areAlgoRelays }) logger.debug('RelaysFeed rendering NormalFeed with:', { subRequests, relayUrls, areAlgoRelays })
return ( return (
<NormalFeed <NormalFeed

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

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

27
src/providers/FeedProvider.tsx

@ -1,5 +1,6 @@
import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { getRelaySetFromEvent } from '@/lib/event-metadata' import { getRelaySetFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url' import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
@ -42,7 +43,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
console.log('FeedProvider init:', { isInitialized, pubkey }) logger.debug('FeedProvider init:', { isInitialized, pubkey })
if (!isInitialized) { if (!isInitialized) {
return return
} }
@ -53,11 +54,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
feedType: 'relay', feedType: 'relay',
id: visibleRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0] 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) { if (pubkey) {
const storedFeedInfo = storage.getFeedInfo(pubkey) const storedFeedInfo = storage.getFeedInfo(pubkey)
console.log('Stored feed info:', storedFeedInfo) logger.debug('Stored feed info:', storedFeedInfo)
if (storedFeedInfo) { if (storedFeedInfo) {
feedInfo = storedFeedInfo feedInfo = storedFeedInfo
} }
@ -70,7 +71,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
if (feedInfo.feedType === 'relay') { if (feedInfo.feedType === 'relay') {
// Check if the stored relay is blocked, if so use first visible relay instead // Check if the stored relay is blocked, if so use first visible relay instead
if (feedInfo.id && blockedRelays.includes(feedInfo.id)) { 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] feedInfo.id = visibleRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0]
} }
return await switchFeed('relay', { relay: feedInfo.id }) return await switchFeed('relay', { relay: feedInfo.id })
@ -86,7 +87,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
} }
if (feedInfo.feedType === 'all-favorites') { if (feedInfo.feedType === 'all-favorites') {
console.log('Initializing all-favorites feed') logger.debug('Initializing all-favorites feed')
return await switchFeed('all-favorites') return await switchFeed('all-favorites')
} }
} }
@ -99,7 +100,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
if (feedInfo.feedType === 'all-favorites') { if (feedInfo.feedType === 'all-favorites') {
// Filter out blocked relays // Filter out blocked relays
const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay)) 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) setRelayUrls(visibleRelays)
} }
}, [favoriteRelays, blockedRelays, feedInfo.feedType]) }, [favoriteRelays, blockedRelays, feedInfo.feedType])
@ -112,33 +113,33 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
relay?: string | null relay?: string | null
} = {} } = {}
) => { ) => {
console.log('switchFeed called:', { feedType, options }) logger.debug('switchFeed called:', { feedType, options })
setIsReady(false) setIsReady(false)
if (feedType === 'relay') { if (feedType === 'relay') {
const normalizedUrl = normalizeUrl(options.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)) { if (!normalizedUrl || !isWebsocketUrl(normalizedUrl)) {
console.log('Invalid relay URL, setting isReady to true') logger.debug('Invalid relay URL, setting isReady to true')
setIsReady(true) setIsReady(true)
return return
} }
// Don't allow selecting a blocked relay as feed // Don't allow selecting a blocked relay as feed
if (blockedRelays.includes(normalizedUrl)) { if (blockedRelays.includes(normalizedUrl)) {
console.warn('Cannot select blocked relay as feed:', normalizedUrl) logger.warn('Cannot select blocked relay as feed:', normalizedUrl)
setIsReady(true) setIsReady(true)
return return
} }
const newFeedInfo = { feedType, id: normalizedUrl } const newFeedInfo = { feedType, id: normalizedUrl }
console.log('Setting relay feed info:', newFeedInfo) logger.debug('Setting relay feed info:', newFeedInfo)
setFeedInfo(newFeedInfo) setFeedInfo(newFeedInfo)
feedInfoRef.current = newFeedInfo feedInfoRef.current = newFeedInfo
setRelayUrls([normalizedUrl]) setRelayUrls([normalizedUrl])
storage.setFeedInfo(newFeedInfo, pubkey) storage.setFeedInfo(newFeedInfo, pubkey)
setIsReady(true) setIsReady(true)
console.log('Relay feed setup complete, isReady set to true') logger.debug('Relay feed setup complete, isReady set to true')
return return
} }
if (feedType === 'relays') { if (feedType === 'relays') {
@ -189,7 +190,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
if (feedType === 'all-favorites') { if (feedType === 'all-favorites') {
// Filter out blocked relays // Filter out blocked relays
const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay)) 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 } const newFeedInfo = { feedType }
setFeedInfo(newFeedInfo) setFeedInfo(newFeedInfo)
feedInfoRef.current = newFeedInfo feedInfoRef.current = newFeedInfo

9
src/providers/NostrProvider/index.tsx

@ -13,6 +13,7 @@ import {
minePow minePow
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
@ -125,7 +126,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
try { try {
(signer as any).disconnect() (signer as any).disconnect()
} catch (error) { } 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) { } 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 // Debug: Log the signed event
console.log('Signed event:', { logger.debug('Signed event:', {
id: event.id, id: event.id,
pubkey: event.pubkey, pubkey: event.pubkey,
sig: event.sig, sig: event.sig,
@ -728,7 +729,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
// Validate the event before publishing // Validate the event before publishing
const isValid = validateEvent(event) const isValid = validateEvent(event)
if (!isValid) { 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.') 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 }
const size = 64 const size = 64
canvas.width = size canvas.width = size
canvas.height = size canvas.height = size
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d', { willReadFrequently: true }) // Optimize for frequent readback operations
if (!ctx) return if (!ctx) return
// Draw tree emoji as text // Draw tree emoji as text

58
src/services/client.service.ts

@ -6,6 +6,7 @@ import {
isReplaceableEvent isReplaceableEvent
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url' import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
@ -58,11 +59,13 @@ class ClientService extends EventTarget {
) )
private trendingNotesCache: NEvent[] | null = null private trendingNotesCache: NEvent[] | null = null
private requestThrottle = new Map<string, number>() // Track request timestamps per relay 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 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 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({ private userIndex = new FlexSearch.Index({
tokenize: 'forward' 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 let fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS
fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays) 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) const fallbackResult = await this._publishToRelays(fallbackRelays, event)
// Combine relay statuses from both attempts // Combine relay statuses from both attempts
@ -214,7 +217,7 @@ class ClientService extends EventTarget {
} }
} catch (error) { } catch (error) {
// If relay hint throws an error, try fallback relays // 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 // Extract relay statuses from the error if available
let hintRelayStatuses: any[] = [] 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 let fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS
fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays) fallbackRelays = this.filterBlockedRelays(fallbackRelays, blockedRelays)
console.log('Trying fallback relays:', fallbackRelays) logger.debug('Trying fallback relays:', fallbackRelays)
const fallbackResult = await this._publishToRelays(fallbackRelays, event) const fallbackResult = await this._publishToRelays(fallbackRelays, event)
// Combine relay statuses from both attempts // Combine relay statuses from both attempts
@ -385,7 +388,7 @@ class ClientService extends EventTarget {
error instanceof Error && error instanceof Error &&
error.message.includes('too many concurrent REQs') 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') }) errors.push({ url, error: new Error('Relay overloaded - too many concurrent requests') })
finishedCount++ finishedCount++
@ -1825,7 +1828,7 @@ class ClientService extends EventTarget {
// Skip relays with open circuit breaker // Skip relays with open circuit breaker
if (this.isCircuitBreakerOpen(url)) { if (this.isCircuitBreakerOpen(url)) {
console.warn(`Skipping relay with open circuit breaker: ${url}`) logger.debug(`Skipping relay with open circuit breaker: ${url}`)
return false return false
} }
@ -1838,14 +1841,14 @@ class ClientService extends EventTarget {
return true return true
} catch (error) { } catch (error) {
console.warn(`Skipping invalid relay URL: ${url}`, error) logger.debug(`Skipping invalid relay URL: ${url}`, error)
return false return false
} }
}) })
// Limit to 8 relays to prevent "too many concurrent REQs" errors // Limit to 4 relays to prevent "too many concurrent REQs" errors
// Increased from 3 to 8 for discussions to include more relay sources // Reduced from 8 to 4 to reduce relay load
return validRelays.slice(0, 8) return validRelays.slice(0, 4)
} }
// ================= Utils ================= // ================= Utils =================
@ -1854,6 +1857,16 @@ class ClientService extends EventTarget {
const now = Date.now() const now = Date.now()
const lastRequest = this.requestThrottle.get(relayUrl) || 0 const lastRequest = this.requestThrottle.get(relayUrl) || 0
const failures = this.failureCount.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) // Calculate delay based on failures (exponential backoff)
let delay = this.REQUEST_COOLDOWN let delay = this.REQUEST_COOLDOWN
@ -1867,12 +1880,19 @@ class ClientService extends EventTarget {
await new Promise(resolve => setTimeout(resolve, delay)) 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()) this.requestThrottle.set(relayUrl, Date.now())
} }
private recordSuccess(relayUrl: string): void { private recordSuccess(relayUrl: string): void {
// Reset failure count on success // Reset failure count on success
this.failureCount.delete(relayUrl) 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 { private recordFailure(relayUrl: string): void {
@ -1880,10 +1900,16 @@ class ClientService extends EventTarget {
const newFailures = currentFailures + 1 const newFailures = currentFailures + 1
this.failureCount.set(relayUrl, newFailures) 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 // Activate circuit breaker if too many failures
if (newFailures >= 5) { if (newFailures >= 3) {
this.circuitBreaker.set(relayUrl, Date.now()) 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 // Circuit breaker timeout expired, reset it
this.circuitBreaker.delete(relayUrl) this.circuitBreaker.delete(relayUrl)
this.failureCount.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 false
} }
return true return true
} }
async generateSubRequestsForPubkeys(pubkeys: string[], myPubkey?: string | null) { async generateSubRequestsForPubkeys(pubkeys: string[], myPubkey?: string | null) {
// Privacy: Only use user's own relays + defaults, never fetch other users' relays // Privacy: Only use user's own relays + defaults, never fetch other users' relays
let urls = BIG_RELAY_URLS let urls = BIG_RELAY_URLS

63
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 { BIG_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS, FAST_READ_RELAY_URLS } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger'
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag' import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -28,6 +29,8 @@ class NoteStatsService {
static instance: NoteStatsService static instance: NoteStatsService
private noteStatsMap: Map<string, Partial<TNoteStats>> = new Map() private noteStatsMap: Map<string, Partial<TNoteStats>> = new Map()
private noteStatsSubscribers = new Map<string, Set<() => void>>() private noteStatsSubscribers = new Map<string, Set<() => void>>()
private processingCache = new Set<string>() // Prevent duplicate processing
private lastProcessedTime = new Map<string, number>() // Rate limiting
constructor() { constructor() {
if (!NoteStatsService.instance) { if (!NoteStatsService.instance) {
@ -37,7 +40,27 @@ class NoteStatsService {
} }
async fetchNoteStats(event: Event, pubkey?: string | null, favoriteRelays?: string[]) { 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 let since: number | undefined
if (oldStats?.updatedAt) { if (oldStats?.updatedAt) {
since = oldStats.updatedAt since = oldStats.updatedAt
@ -66,7 +89,7 @@ class NoteStatsService {
const relayTypes = pubkey const relayTypes = pubkey
? 'inboxes kind 10002 + favorites kind 10012 + big relays' ? 'inboxes kind 10002 + favorites kind 10012 + big relays'
: 'big relays + fast read relays + searchable relays (anonymous user)' : '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) const replaceableCoordinate = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event) ? getReplaceableCoordinateFromEvent(event)
@ -76,7 +99,7 @@ class NoteStatsService {
{ {
'#e': [event.id], '#e': [event.id],
kinds: [kinds.Reaction], kinds: [kinds.Reaction],
limit: 500 limit: 100
}, },
{ {
'#e': [event.id], '#e': [event.id],
@ -86,17 +109,17 @@ class NoteStatsService {
{ {
'#e': [event.id], '#e': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500 limit: 100
}, },
{ {
'#q': [event.id], '#q': [event.id],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500 limit: 100
}, },
{ {
'#e': [event.id], '#e': [event.id],
kinds: [kinds.Highlights], kinds: [kinds.Highlights],
limit: 500 limit: 100
} }
] ]
@ -105,7 +128,7 @@ class NoteStatsService {
{ {
'#a': [replaceableCoordinate], '#a': [replaceableCoordinate],
kinds: [kinds.Reaction], kinds: [kinds.Reaction],
limit: 500 limit: 100
}, },
{ {
'#a': [replaceableCoordinate], '#a': [replaceableCoordinate],
@ -115,17 +138,17 @@ class NoteStatsService {
{ {
'#a': [replaceableCoordinate], '#a': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500 limit: 100
}, },
{ {
'#q': [replaceableCoordinate], '#q': [replaceableCoordinate],
kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT], kinds: [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
limit: 500 limit: 100
}, },
{ {
'#a': [replaceableCoordinate], '#a': [replaceableCoordinate],
kinds: [kinds.Highlights], kinds: [kinds.Highlights],
limit: 500 limit: 100
} }
) )
} }
@ -134,14 +157,14 @@ class NoteStatsService {
filters.push({ filters.push({
'#e': [event.id], '#e': [event.id],
kinds: [kinds.Zap], kinds: [kinds.Zap],
limit: 500 limit: 100
}) })
if (replaceableCoordinate) { if (replaceableCoordinate) {
filters.push({ filters.push({
'#a': [replaceableCoordinate], '#a': [replaceableCoordinate],
kinds: [kinds.Zap], kinds: [kinds.Zap],
limit: 500 limit: 100
}) })
} }
} }
@ -184,26 +207,30 @@ class NoteStatsService {
}) })
} }
const events: Event[] = [] 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, { await client.fetchEvents(finalRelayUrls, filters, {
onevent: (evt) => { onevent: (evt) => {
this.updateNoteStatsByEvents([evt], event.pubkey) this.updateNoteStatsByEvents([evt], event.pubkey)
events.push(evt) 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 // Debug: Count events by kind
const eventsByKind = events.reduce((acc, evt) => { const eventsByKind = events.reduce((acc, evt) => {
acc[evt.kind] = (acc[evt.kind] || 0) + 1 acc[evt.kind] = (acc[evt.kind] || 0) + 1
return acc return acc
}, {} as Record<number, number>) }, {} 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.set(event.id, {
...(this.noteStatsMap.get(event.id) ?? {}), ...(this.noteStatsMap.get(event.id) ?? {}),
updatedAt: dayjs().unix() updatedAt: dayjs().unix()
}) })
return this.noteStatsMap.get(event.id) ?? {} return this.noteStatsMap.get(event.id) ?? {}
} finally {
// Clean up processing cache
this.processingCache.delete(eventId)
}
} }
subscribeNoteStats(noteId: string, callback: () => void) { subscribeNoteStats(noteId: string, callback: () => void) {
@ -394,7 +421,7 @@ class NoteStatsService {
}) })
if (parentETag) { if (parentETag) {
originalEventId = parentETag[1] 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 { } else {
// Look for the last E tag that's not a mention // Look for the last E tag that's not a mention
const embeddedEventIds = this.getEmbeddedNoteBech32Ids(evt) const embeddedEventIds = this.getEmbeddedNoteBech32Ids(evt)
@ -434,14 +461,14 @@ class NoteStatsService {
// Skip self-interactions - don't count replies from the original event author // Skip self-interactions - don't count replies from the original event author
if (originalEventAuthor && originalEventAuthor === evt.pubkey) { 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 return
} }
replyIdSet.add(evt.id) replyIdSet.add(evt.id)
replies.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at }) replies.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at })
this.noteStatsMap.set(originalEventId, { ...old, replyIdSet, replies }) 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 return originalEventId
} }

Loading…
Cancel
Save