8.6 KiB
Market Module Recursion Issue - Technical Analysis Report
Executive Summary
A critical recursion issue was discovered in the market module that caused "Maximum recursive updates exceeded" errors, leading to page crashes in production. The issue was traced to multiple overlapping causes in the Vue 3 reactive system, particularly around event processing, component initialization, and search result handling.
Problem Description
Initial Symptoms
- Error Message:
Maximum recursive updates exceeded in component <MarketPage> - Environment: Both development (
npm run dev) and production - Impact: Complete page crash in production, infinite console logging in development
- Trigger: Opening the
/marketroute
Observable Behavior
🛒 Loading market data for: { identifier: "default", pubkey: "..." }
🛒 Found 0 market events
🛒 Loading stalls...
🛒 Found 3 stall events for 1 merchants
🛒 Loading products...
🛒 Found 6 product events for 1 merchants
[Repeated 4+ times simultaneously]
Root Cause Analysis
Primary Causes
1. Multiple useMarket() Composable Instances
Location: src/modules/market/composables/useMarket.ts
The useMarket() composable contained an onMounted() hook that was being called from multiple places:
MarketPage.vuecomponentuseMarketPreloadercomposable
// PROBLEMATIC CODE (removed)
onMounted(() => {
if (needsToLoadMarket.value) {
loadMarket()
} else if (marketPreloader.isPreloaded.value) {
unsubscribe = market.subscribeToMarketUpdates()
}
})
Issue: Each instance created separate initialization cycles, leading to:
- Multiple simultaneous market loading operations
- Overlapping Nostr event subscriptions
- Race conditions in state updates
2. Nostr Event Processing Loop
Location: src/modules/market/composables/useMarket.ts:428-451
Events were being processed multiple times due to lack of deduplication:
// ORIGINAL PROBLEMATIC CODE
const handleMarketEvent = (event: any) => {
// No deduplication - same events processed repeatedly
switch (event.kind) {
case MARKET_EVENT_KINDS.PRODUCT:
handleProductEvent(event) // This triggered store updates
break
// ...
}
}
Chain Reaction:
subscribeToMarketUpdates()receives eventhandleMarketEvent()processes eventhandleProductEvent()callsmarketStore.addProduct()- Store update triggers reactive effects
- Effects trigger new subscriptions or event processing
- Loop continues indefinitely
3. Circular Dependency in Search Results
Location: src/modules/market/views/MarketPage.vue:306-347
The computed property productsToDisplay created a circular dependency:
// PROBLEMATIC LOGIC
const productsToDisplay = computed(() => {
// Always used search results, even when empty search
let baseProducts = searchResults.value // Always reactive to search changes
// Category filtering then triggered more search updates
if (!hasActiveFilters.value) {
return baseProducts
}
// ...filtering logic that could trigger search updates
})
4. MarketFuzzySearch Watcher Loop
Location: src/modules/market/components/MarketFuzzySearch.vue:359-363
A watcher was immediately emitting results, creating circular updates:
// REMOVED - CAUSED CIRCULAR DEPENDENCY
watch(filteredItems, (items) => {
emit('results', items)
}, { immediate: true })
Loop: Component emits → Parent updates → Child re-renders → Watcher fires → Component emits
Resolution Steps
Step 1: Remove Multiple Composable Instances
// FIXED: Removed onMounted from useMarket composable
// Added initialization guards
const isInitialized = ref(false)
const isInitializing = ref(false)
const connectToMarket = async () => {
if (isInitialized.value || isInitializing.value) {
console.log('🛒 Market already connected/connecting, skipping...')
return { isConnected: isConnected.value }
}
isInitializing.value = true
// ... initialization logic
}
Step 2: Implement Event Deduplication
// FIXED: Added event deduplication
const processedEvents = ref(new Set<string>())
const handleMarketEvent = (event: any) => {
const eventId = event.id
if (processedEvents.value.has(eventId)) {
return // Skip already processed events
}
processedEvents.value.add(eventId)
// ... process event
}
Step 3: Fix Search Results Logic
// FIXED: Only use search results when actively searching
const productsToDisplay = computed(() => {
let baseProducts: Product[]
// Only use search results if there's an actual search query
if (searchQuery.value && searchQuery.value.trim().length > 0) {
baseProducts = searchResults.value
} else {
baseProducts = [...marketStore.products] as Product[]
}
// ... category filtering
})
Step 4: Remove Problematic Watcher
// REMOVED: Circular dependency watcher
// Results now only emitted on explicit user actions:
// - handleSearchChange()
// - handleClear()
// - applySuggestion()
Technical Details
Vue 3 Reactive System Behavior
The issue exploited several Vue 3 reactive system characteristics:
- Effect Scheduling: Computed properties and watchers are scheduled in microtasks
- Circular Detection: Vue tracks effect dependencies and detects when effects mutate their own dependencies
- Recursion Limit: Vue has a built-in limit (100 iterations) to prevent infinite loops
Nostr Protocol Considerations
- Event Kinds: 30017 (stalls), 30018 (products), 30019 (markets)
- Real-time Updates: Nostr subscriptions provide real-time events
- Event Persistence: Same events can be received multiple times from different relays
State Management Impact
- Pinia Store Reactivity: Store mutations trigger all dependent computed properties
- Cross-Component Effects: State changes in one component affect others through shared store
- Subscription Overlap: Multiple subscriptions to same Nostr filters cause duplicate events
Lessons Learned
1. Composable Design Patterns
- Avoid side effects in composable initialization: Don't use
onMountedin reusable composables - Implement initialization guards: Prevent multiple simultaneous initializations
- Clear lifecycle management: Explicit
initialize()andcleanup()methods
2. Event Handling Best Practices
- Always implement deduplication: Track processed events by ID
- Idempotent operations: Ensure repeated operations don't cause issues
- Defensive programming: Handle unexpected event duplicates gracefully
3. Vue Reactivity Guidelines
- Minimize circular dependencies: Separate concerns between computed properties
- Careful watcher usage: Avoid immediate watchers that emit results
- State isolation: Keep reactive state changes predictable and isolated
4. Real-time Systems
- Connection management: Implement proper connection lifecycle
- Event ordering: Handle out-of-order or duplicate events
- Resource cleanup: Properly unsubscribe from real-time updates
Prevention Strategies
Code Review Checklist
- No
onMountedhooks in reusable composables - Event deduplication implemented for real-time systems
- Computed properties don't create circular dependencies
- Watchers don't immediately emit results that trigger parent updates
- Initialization guards prevent race conditions
Testing Recommendations
- Stress testing: Open/close routes repeatedly to detect initialization issues
- Network simulation: Test with duplicate/delayed Nostr events
- Mobile testing: Test on resource-constrained devices where issues are more likely
Monitoring & Debugging
- Performance monitoring: Track recursive update warnings in production
- Event logging: Log all Nostr event processing with deduplication status
- State transitions: Monitor store state changes for unexpected patterns
Conclusion
The recursion issue was caused by a perfect storm of multiple reactive system anti-patterns:
- Multiple composable instances creating overlapping effects
- Lack of event deduplication in real-time systems
- Circular dependencies in computed properties
- Immediate watchers causing emission loops
The resolution required systematic identification and elimination of each contributing factor. The fixes implement industry best practices for Vue 3 reactive systems and real-time event processing, making the system more robust and maintainable.
This incident highlights the importance of careful reactive system design, especially when combining real-time data streams with complex UI state management.