web-app/docs/Market-Recursion-Analysis.md
2025-10-20 06:48:21 +02:00

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 /market route

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.vue component
  • useMarketPreloader composable
// 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:

  1. subscribeToMarketUpdates() receives event
  2. handleMarketEvent() processes event
  3. handleProductEvent() calls marketStore.addProduct()
  4. Store update triggers reactive effects
  5. Effects trigger new subscriptions or event processing
  6. 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:

  1. Effect Scheduling: Computed properties and watchers are scheduled in microtasks
  2. Circular Detection: Vue tracks effect dependencies and detects when effects mutate their own dependencies
  3. 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 onMounted in reusable composables
  • Implement initialization guards: Prevent multiple simultaneous initializations
  • Clear lifecycle management: Explicit initialize() and cleanup() 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 onMounted hooks 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:

  1. Multiple composable instances creating overlapping effects
  2. Lack of event deduplication in real-time systems
  3. Circular dependencies in computed properties
  4. 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.