04-querying-subscribing.ts raw

   1  /**
   2   * NDK Query and Subscription Patterns
   3   * 
   4   * Examples from: src/queries/orders.tsx, src/queries/payment.tsx
   5   */
   6  
   7  import NDK, { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk'
   8  
   9  // ============================================================
  10  // BASIC FETCH (ONE-TIME QUERY)
  11  // ============================================================
  12  
  13  const fetchNotes = async (ndk: NDK, authorPubkey: string, limit: number = 50) => {
  14    const filter: NDKFilter = {
  15      kinds: [1],  // Text notes
  16      authors: [authorPubkey],
  17      limit
  18    }
  19    
  20    // Fetch returns a Set
  21    const events = await ndk.fetchEvents(filter)
  22    
  23    // Convert to array and sort by timestamp
  24    const eventArray = Array.from(events).sort((a, b) =>
  25      (b.created_at || 0) - (a.created_at || 0)
  26    )
  27    
  28    return eventArray
  29  }
  30  
  31  // ============================================================
  32  // FETCH WITH MULTIPLE FILTERS
  33  // ============================================================
  34  
  35  const fetchProductsByMultipleAuthors = async (
  36    ndk: NDK,
  37    pubkeys: string[]
  38  ) => {
  39    const filter: NDKFilter = {
  40      kinds: [30402],  // Product listings
  41      authors: pubkeys,
  42      limit: 100
  43    }
  44    
  45    const events = await ndk.fetchEvents(filter)
  46    return Array.from(events)
  47  }
  48  
  49  // ============================================================
  50  // FETCH WITH TAG FILTERS
  51  // ============================================================
  52  
  53  const fetchOrderEvents = async (ndk: NDK, orderId: string) => {
  54    const filter: NDKFilter = {
  55      kinds: [16, 17],  // Order and payment receipt
  56      '#order': [orderId],  // Tag filter (note the # prefix)
  57    }
  58    
  59    const events = await ndk.fetchEvents(filter)
  60    return Array.from(events)
  61  }
  62  
  63  // ============================================================
  64  // FETCH WITH TIME RANGE
  65  // ============================================================
  66  
  67  const fetchRecentEvents = async (
  68    ndk: NDK,
  69    kind: number,
  70    hoursAgo: number = 24
  71  ) => {
  72    const now = Math.floor(Date.now() / 1000)
  73    const since = now - (hoursAgo * 3600)
  74    
  75    const filter: NDKFilter = {
  76      kinds: [kind],
  77      since,
  78      until: now,
  79      limit: 100
  80    }
  81    
  82    const events = await ndk.fetchEvents(filter)
  83    return Array.from(events)
  84  }
  85  
  86  // ============================================================
  87  // FETCH BY EVENT ID
  88  // ============================================================
  89  
  90  const fetchEventById = async (ndk: NDK, eventId: string) => {
  91    const filter: NDKFilter = {
  92      ids: [eventId]
  93    }
  94    
  95    const events = await ndk.fetchEvents(filter)
  96    
  97    if (events.size === 0) {
  98      return null
  99    }
 100    
 101    return Array.from(events)[0]
 102  }
 103  
 104  // ============================================================
 105  // BASIC SUBSCRIPTION (REAL-TIME)
 106  // ============================================================
 107  
 108  const subscribeToNotes = (
 109    ndk: NDK,
 110    authorPubkey: string,
 111    onEvent: (event: NDKEvent) => void
 112  ): NDKSubscription => {
 113    const filter: NDKFilter = {
 114      kinds: [1],
 115      authors: [authorPubkey]
 116    }
 117    
 118    const subscription = ndk.subscribe(filter, {
 119      closeOnEose: false  // Keep open for real-time updates
 120    })
 121    
 122    // Event handler
 123    subscription.on('event', (event: NDKEvent) => {
 124      onEvent(event)
 125    })
 126    
 127    // EOSE (End of Stored Events) handler
 128    subscription.on('eose', () => {
 129      console.log('✅ Received all stored events')
 130    })
 131    
 132    return subscription
 133  }
 134  
 135  // ============================================================
 136  // SUBSCRIPTION WITH CLEANUP
 137  // ============================================================
 138  
 139  const createManagedSubscription = (
 140    ndk: NDK,
 141    filter: NDKFilter,
 142    handlers: {
 143      onEvent: (event: NDKEvent) => void
 144      onEose?: () => void
 145      onClose?: () => void
 146    }
 147  ) => {
 148    const subscription = ndk.subscribe(filter, { closeOnEose: false })
 149    
 150    subscription.on('event', handlers.onEvent)
 151    
 152    if (handlers.onEose) {
 153      subscription.on('eose', handlers.onEose)
 154    }
 155    
 156    if (handlers.onClose) {
 157      subscription.on('close', handlers.onClose)
 158    }
 159    
 160    // Return cleanup function
 161    return () => {
 162      subscription.stop()
 163      console.log('✅ Subscription stopped')
 164    }
 165  }
 166  
 167  // ============================================================
 168  // MONITORING SPECIFIC EVENT
 169  // ============================================================
 170  
 171  const monitorPaymentReceipt = (
 172    ndk: NDK,
 173    orderId: string,
 174    invoiceId: string,
 175    onPaymentReceived: (preimage: string) => void
 176  ): NDKSubscription => {
 177    const sessionStart = Math.floor(Date.now() / 1000)
 178    
 179    const filter: NDKFilter = {
 180      kinds: [17],  // Payment receipt
 181      '#order': [orderId],
 182      '#payment-request': [invoiceId],
 183      since: sessionStart - 30  // 30 second buffer for clock skew
 184    }
 185    
 186    const subscription = ndk.subscribe(filter, { closeOnEose: false })
 187    
 188    subscription.on('event', (event: NDKEvent) => {
 189      // Verify event is recent
 190      if (event.created_at && event.created_at < sessionStart - 30) {
 191        console.log('⏰ Ignoring old receipt')
 192        return
 193      }
 194      
 195      // Verify it's the correct invoice
 196      const paymentRequestTag = event.tags.find(tag => tag[0] === 'payment-request')
 197      if (paymentRequestTag?.[1] !== invoiceId) {
 198        return
 199      }
 200      
 201      // Extract preimage
 202      const paymentTag = event.tags.find(tag => tag[0] === 'payment')
 203      const preimage = paymentTag?.[3] || 'external-payment'
 204      
 205      console.log('✅ Payment received!')
 206      subscription.stop()
 207      onPaymentReceived(preimage)
 208    })
 209    
 210    return subscription
 211  }
 212  
 213  // ============================================================
 214  // REACT INTEGRATION PATTERN
 215  // ============================================================
 216  
 217  import { useEffect, useState } from 'react'
 218  
 219  function useOrderSubscription(ndk: NDK | null, orderId: string) {
 220    const [events, setEvents] = useState<NDKEvent[]>([])
 221    const [eosed, setEosed] = useState(false)
 222    
 223    useEffect(() => {
 224      if (!ndk || !orderId) return
 225      
 226      const filter: NDKFilter = {
 227        kinds: [16, 17],
 228        '#order': [orderId]
 229      }
 230      
 231      const subscription = ndk.subscribe(filter, { closeOnEose: false })
 232      
 233      subscription.on('event', (event: NDKEvent) => {
 234        setEvents(prev => {
 235          // Avoid duplicates
 236          if (prev.some(e => e.id === event.id)) {
 237            return prev
 238          }
 239          return [...prev, event].sort((a, b) =>
 240            (a.created_at || 0) - (b.created_at || 0)
 241          )
 242        })
 243      })
 244      
 245      subscription.on('eose', () => {
 246        setEosed(true)
 247      })
 248      
 249      // Cleanup on unmount
 250      return () => {
 251        subscription.stop()
 252      }
 253    }, [ndk, orderId])
 254    
 255    return { events, eosed }
 256  }
 257  
 258  // ============================================================
 259  // REACT QUERY INTEGRATION
 260  // ============================================================
 261  
 262  import { useQuery, useQueryClient } from '@tanstack/react-query'
 263  
 264  // Query function
 265  const fetchProducts = async (ndk: NDK, pubkey: string) => {
 266    if (!ndk) throw new Error('NDK not initialized')
 267    
 268    const filter: NDKFilter = {
 269      kinds: [30402],
 270      authors: [pubkey]
 271    }
 272    
 273    const events = await ndk.fetchEvents(filter)
 274    return Array.from(events)
 275  }
 276  
 277  // Hook with subscription for real-time updates
 278  function useProductsWithSubscription(ndk: NDK | null, pubkey: string) {
 279    const queryClient = useQueryClient()
 280    
 281    // Initial query
 282    const query = useQuery({
 283      queryKey: ['products', pubkey],
 284      queryFn: () => fetchProducts(ndk!, pubkey),
 285      enabled: !!ndk && !!pubkey,
 286      staleTime: 30000
 287    })
 288    
 289    // Real-time subscription
 290    useEffect(() => {
 291      if (!ndk || !pubkey) return
 292      
 293      const filter: NDKFilter = {
 294        kinds: [30402],
 295        authors: [pubkey]
 296      }
 297      
 298      const subscription = ndk.subscribe(filter, { closeOnEose: false })
 299      
 300      subscription.on('event', () => {
 301        // Invalidate query to trigger refetch
 302        queryClient.invalidateQueries({ queryKey: ['products', pubkey] })
 303      })
 304      
 305      return () => {
 306        subscription.stop()
 307      }
 308    }, [ndk, pubkey, queryClient])
 309    
 310    return query
 311  }
 312  
 313  // ============================================================
 314  // ADVANCED: WAITING FOR SPECIFIC EVENT
 315  // ============================================================
 316  
 317  const waitForEvent = (
 318    ndk: NDK,
 319    filter: NDKFilter,
 320    condition: (event: NDKEvent) => boolean,
 321    timeoutMs: number = 30000
 322  ): Promise<NDKEvent | null> => {
 323    return new Promise((resolve) => {
 324      const subscription = ndk.subscribe(filter, { closeOnEose: false })
 325      
 326      // Timeout
 327      const timeout = setTimeout(() => {
 328        subscription.stop()
 329        resolve(null)
 330      }, timeoutMs)
 331      
 332      // Event handler
 333      subscription.on('event', (event: NDKEvent) => {
 334        if (condition(event)) {
 335          clearTimeout(timeout)
 336          subscription.stop()
 337          resolve(event)
 338        }
 339      })
 340    })
 341  }
 342  
 343  // Usage example
 344  async function waitForPayment(ndk: NDK, orderId: string, invoiceId: string) {
 345    const paymentEvent = await waitForEvent(
 346      ndk,
 347      {
 348        kinds: [17],
 349        '#order': [orderId],
 350        since: Math.floor(Date.now() / 1000)
 351      },
 352      (event) => {
 353        const tag = event.tags.find(t => t[0] === 'payment-request')
 354        return tag?.[1] === invoiceId
 355      },
 356      60000  // 60 second timeout
 357    )
 358    
 359    if (paymentEvent) {
 360      console.log('✅ Payment confirmed!')
 361      return paymentEvent
 362    } else {
 363      console.log('⏰ Payment timeout')
 364      return null
 365    }
 366  }
 367  
 368  // ============================================================
 369  // USAGE EXAMPLES
 370  // ============================================================
 371  
 372  async function queryExample(ndk: NDK) {
 373    // Fetch notes
 374    const notes = await fetchNotes(ndk, 'pubkey123', 50)
 375    console.log(`Found ${notes.length} notes`)
 376    
 377    // Subscribe to new notes
 378    const cleanup = subscribeToNotes(ndk, 'pubkey123', (event) => {
 379      console.log('New note:', event.content)
 380    })
 381    
 382    // Clean up after 60 seconds
 383    setTimeout(cleanup, 60000)
 384    
 385    // Monitor payment
 386    monitorPaymentReceipt(ndk, 'order-123', 'invoice-456', (preimage) => {
 387      console.log('Payment received:', preimage)
 388    })
 389  }
 390  
 391  export {
 392    fetchNotes,
 393    fetchProductsByMultipleAuthors,
 394    fetchOrderEvents,
 395    fetchRecentEvents,
 396    fetchEventById,
 397    subscribeToNotes,
 398    createManagedSubscription,
 399    monitorPaymentReceipt,
 400    useOrderSubscription,
 401    useProductsWithSubscription,
 402    waitForEvent
 403  }
 404  
 405