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