feat: Integrate Relay Hub for centralized Nostr connection management
- Introduce a new composable, useRelayHub, to manage all Nostr WebSocket connections, enhancing connection stability and performance. - Update existing components and composables to utilize the Relay Hub for connecting, publishing events, and subscribing to updates, streamlining the overall architecture. - Add a RelayHubStatus component to display connection status and health metrics, improving user feedback on the connection state. - Implement a RelayHubDemo page to showcase the functionality of the Relay Hub, including connection tests and subscription management. - Ensure proper error handling and logging throughout the integration process to facilitate debugging and user experience.
This commit is contained in:
parent
df7e461c91
commit
7d7bee8e77
14 changed files with 1982 additions and 955 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import { ref, readonly } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
|
||||
import { useNostrStore } from '@/stores/nostr'
|
||||
import { useMarketStore, type Market, type Stall, type Product } from '@/stores/market'
|
||||
import { useMarketStore } from '@/stores/market'
|
||||
import { useRelayHub } from '@/composables/useRelayHub'
|
||||
import { config } from '@/lib/config'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
|
||||
// Nostr event kinds for market functionality
|
||||
const MARKET_EVENT_KINDS = {
|
||||
|
|
@ -15,120 +15,89 @@ const MARKET_EVENT_KINDS = {
|
|||
export function useMarket() {
|
||||
const nostrStore = useNostrStore()
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
const relayHub = useRelayHub()
|
||||
|
||||
// State
|
||||
const isLoading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
const isConnected = ref(false)
|
||||
|
||||
// Track processed event IDs to prevent duplicates (like nostr-market-app)
|
||||
const processedEventIds = new Set<string>()
|
||||
|
||||
// Queue for products that arrive before their stalls
|
||||
const pendingProducts = ref<Array<{event: any, productData: any}>>([])
|
||||
|
||||
// Market loading state
|
||||
const activeMarket = computed(() => marketStore.activeMarket)
|
||||
const markets = computed(() => marketStore.markets)
|
||||
const stalls = computed(() => marketStore.stalls)
|
||||
const products = computed(() => marketStore.products)
|
||||
const orders = computed(() => marketStore.orders)
|
||||
|
||||
// Connection state
|
||||
const connectionStatus = computed(() => {
|
||||
if (isConnected.value) return 'connected'
|
||||
if (nostrStore.isConnecting) return 'connecting'
|
||||
if (nostrStore.error) return 'error'
|
||||
return 'disconnected'
|
||||
})
|
||||
|
||||
// Load market from naddr
|
||||
const loadMarket = async (naddr: string) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
marketStore.setLoading(true)
|
||||
marketStore.setError(null)
|
||||
|
||||
console.log('Loading market with naddr:', naddr)
|
||||
|
||||
// Decode naddr
|
||||
const { type, data } = nip19.decode(naddr)
|
||||
console.log('Decoded naddr:', { type, data })
|
||||
|
||||
if (type !== 'naddr' || data.kind !== MARKET_EVENT_KINDS.MARKET) {
|
||||
throw new Error('Invalid market naddr')
|
||||
error.value = null
|
||||
|
||||
console.log('Loading market from naddr:', naddr)
|
||||
|
||||
// Parse naddr to get market data
|
||||
const marketData = {
|
||||
identifier: naddr.split(':')[2] || 'default',
|
||||
pubkey: naddr.split(':')[1] || nostrStore.account?.pubkey || ''
|
||||
}
|
||||
|
||||
console.log('About to load market data...')
|
||||
// Load market data from Nostr
|
||||
await loadMarketData(data)
|
||||
console.log('Market data loaded successfully')
|
||||
|
||||
|
||||
if (!marketData.pubkey) {
|
||||
throw new Error('No pubkey available for market')
|
||||
}
|
||||
|
||||
await loadMarketData(marketData)
|
||||
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error('Failed to load market')
|
||||
console.error('Error loading market:', err)
|
||||
marketStore.setError(err instanceof Error ? err.message : 'Failed to load market')
|
||||
// Don't throw error, let the UI handle it gracefully
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
marketStore.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load market data from Nostr events
|
||||
const loadMarketData = async (marketData: any) => {
|
||||
try {
|
||||
console.log('Starting loadMarketData...')
|
||||
console.log('Got Nostr client')
|
||||
|
||||
// Load market configuration
|
||||
console.log('Loading market config...')
|
||||
await loadMarketConfig(marketData)
|
||||
console.log('Market config loaded')
|
||||
|
||||
// Load stalls for this market
|
||||
console.log('Loading stalls...')
|
||||
await loadStalls()
|
||||
console.log('Stalls loaded')
|
||||
|
||||
// Load products for all stalls
|
||||
console.log('Loading products...')
|
||||
await loadProducts()
|
||||
console.log('Products loaded')
|
||||
|
||||
// Subscribe to real-time updates
|
||||
console.log('Subscribing to updates...')
|
||||
try {
|
||||
subscribeToMarketUpdates()
|
||||
console.log('Subscribed to updates')
|
||||
} catch (err) {
|
||||
console.warn('Failed to subscribe to updates:', err)
|
||||
// Don't fail the entire load process if subscription fails
|
||||
}
|
||||
|
||||
// Clear any error state since we successfully loaded the market data
|
||||
marketStore.setError(null)
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading market data:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const loadMarketConfig = async (marketData: any) => {
|
||||
try {
|
||||
const client = nostrStore.getClient()
|
||||
|
||||
console.log('Loading market config for:', marketData)
|
||||
|
||||
console.log('Loading market data for:', marketData)
|
||||
|
||||
// Fetch market configuration event
|
||||
const events = await client.fetchEvents({
|
||||
kinds: [MARKET_EVENT_KINDS.MARKET],
|
||||
authors: [marketData.pubkey],
|
||||
'#d': [marketData.identifier]
|
||||
})
|
||||
|
||||
const events = await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [MARKET_EVENT_KINDS.MARKET],
|
||||
authors: [marketData.pubkey],
|
||||
'#d': [marketData.identifier]
|
||||
}
|
||||
])
|
||||
|
||||
console.log('Found market events:', events.length)
|
||||
|
||||
|
||||
if (events.length > 0) {
|
||||
const marketEvent = events[0]
|
||||
console.log('Market event:', marketEvent)
|
||||
|
||||
const market: Market = {
|
||||
|
||||
const market = {
|
||||
d: marketData.identifier,
|
||||
pubkey: marketData.pubkey,
|
||||
relays: config.market.supportedRelays,
|
||||
selected: true,
|
||||
opts: JSON.parse(marketEvent.content)
|
||||
}
|
||||
|
||||
|
||||
marketStore.addMarket(market)
|
||||
marketStore.setActiveMarket(market)
|
||||
} else {
|
||||
console.warn('No market events found')
|
||||
// Create a default market if none exists
|
||||
const market: Market = {
|
||||
const market = {
|
||||
d: marketData.identifier,
|
||||
pubkey: marketData.pubkey,
|
||||
relays: config.market.supportedRelays,
|
||||
|
|
@ -140,15 +109,15 @@ export function useMarket() {
|
|||
ui: {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
marketStore.addMarket(market)
|
||||
marketStore.setActiveMarket(market)
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading market config:', err)
|
||||
console.error('Error loading market data:', err)
|
||||
// Don't throw error, create default market instead
|
||||
const market: Market = {
|
||||
const market = {
|
||||
d: marketData.identifier,
|
||||
pubkey: marketData.pubkey,
|
||||
relays: config.market.supportedRelays,
|
||||
|
|
@ -160,496 +129,416 @@ export function useMarket() {
|
|||
ui: {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
marketStore.addMarket(market)
|
||||
marketStore.setActiveMarket(market)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load stalls from market merchants
|
||||
const loadStalls = async () => {
|
||||
try {
|
||||
const client = nostrStore.getClient()
|
||||
|
||||
// Get the active market to filter by its merchants
|
||||
const activeMarket = marketStore.activeMarket
|
||||
if (!activeMarket) {
|
||||
console.warn('No active market found')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const merchants = [...(activeMarket.opts.merchants || [])]
|
||||
console.log('Loading stalls from market merchants:', merchants)
|
||||
|
||||
|
||||
if (merchants.length === 0) {
|
||||
console.log('No merchants in market, skipping stall loading')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Fetch stall events from market merchants only
|
||||
const events = await client.fetchEvents({
|
||||
kinds: [MARKET_EVENT_KINDS.STALL],
|
||||
authors: merchants
|
||||
})
|
||||
|
||||
const events = await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [MARKET_EVENT_KINDS.STALL],
|
||||
authors: merchants
|
||||
}
|
||||
])
|
||||
|
||||
console.log('Found stall events:', events.length)
|
||||
|
||||
|
||||
// Group events by stall ID and keep only the most recent version
|
||||
const stallGroups = new Map<string, any[]>()
|
||||
|
||||
events.forEach(event => {
|
||||
try {
|
||||
const stallData = JSON.parse(event.content)
|
||||
const stallId = stallData.id
|
||||
|
||||
events.forEach((event: any) => {
|
||||
const stallId = event.tags.find((tag: any) => tag[0] === 'd')?.[1]
|
||||
if (stallId) {
|
||||
if (!stallGroups.has(stallId)) {
|
||||
stallGroups.set(stallId, [])
|
||||
}
|
||||
stallGroups.get(stallId)!.push({ event, stallData })
|
||||
stallGroups.get(stallId)!.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
// Process each stall group
|
||||
stallGroups.forEach((stallEvents, stallId) => {
|
||||
// Sort by created_at and take the most recent
|
||||
const latestEvent = stallEvents.sort((a: any, b: any) => b.created_at - a.created_at)[0]
|
||||
|
||||
try {
|
||||
const stallData = JSON.parse(latestEvent.content)
|
||||
const stall = {
|
||||
id: stallId,
|
||||
pubkey: latestEvent.pubkey,
|
||||
name: stallData.name || 'Unnamed Stall',
|
||||
description: stallData.description || '',
|
||||
created_at: latestEvent.created_at,
|
||||
...stallData
|
||||
}
|
||||
|
||||
marketStore.addStall(stall)
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse stall event:', err)
|
||||
console.warn('Failed to parse stall data:', err)
|
||||
}
|
||||
})
|
||||
|
||||
// Process each stall group, keeping only the most recent version
|
||||
stallGroups.forEach((stallEvents, _stallId) => {
|
||||
// Sort by created_at timestamp (most recent first)
|
||||
stallEvents.sort((a, b) => b.event.created_at - a.event.created_at)
|
||||
|
||||
// Take the most recent version
|
||||
const { event, stallData } = stallEvents[0]
|
||||
|
||||
console.log('Processing most recent stall event:', event)
|
||||
console.log('Parsed stall data:', stallData)
|
||||
|
||||
const stall: Stall = {
|
||||
id: stallData.id, // Use the stall's unique ID from content, not the Nostr event ID
|
||||
pubkey: event.pubkey,
|
||||
name: stallData.name,
|
||||
description: stallData.description,
|
||||
logo: stallData.logo,
|
||||
categories: stallData.categories,
|
||||
shipping: stallData.shipping
|
||||
}
|
||||
|
||||
console.log('Created stall (most recent version):', stall)
|
||||
marketStore.addStall(stall)
|
||||
})
|
||||
|
||||
// Process any pending products that might match the loaded stalls
|
||||
processPendingProducts()
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading stalls:', err)
|
||||
// Don't throw error, continue without stalls
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load products from market stalls
|
||||
const loadProducts = async () => {
|
||||
try {
|
||||
const client = nostrStore.getClient()
|
||||
|
||||
// Get the active market to filter by its merchants
|
||||
const activeMarket = marketStore.activeMarket
|
||||
if (!activeMarket) {
|
||||
console.warn('No active market found')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const merchants = [...(activeMarket.opts.merchants || [])]
|
||||
console.log('Loading products from market merchants:', merchants)
|
||||
|
||||
if (merchants.length === 0) {
|
||||
console.log('No merchants in market, skipping product loading')
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch product events from market merchants only
|
||||
const events = await client.fetchEvents({
|
||||
kinds: [MARKET_EVENT_KINDS.PRODUCT],
|
||||
authors: merchants
|
||||
})
|
||||
|
||||
console.log('Found product events from market merchants:', events.length)
|
||||
|
||||
|
||||
// Fetch product events from market merchants
|
||||
const events = await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [MARKET_EVENT_KINDS.PRODUCT],
|
||||
authors: merchants
|
||||
}
|
||||
])
|
||||
|
||||
console.log('Found product events:', events.length)
|
||||
|
||||
// Group events by product ID and keep only the most recent version
|
||||
const productGroups = new Map<string, any[]>()
|
||||
|
||||
events.forEach(event => {
|
||||
try {
|
||||
const productData = JSON.parse(event.content)
|
||||
const productId = productData.id
|
||||
|
||||
events.forEach((event: any) => {
|
||||
const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1]
|
||||
if (productId) {
|
||||
if (!productGroups.has(productId)) {
|
||||
productGroups.set(productId, [])
|
||||
}
|
||||
productGroups.get(productId)!.push({ event, productData })
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse product event:', err)
|
||||
productGroups.get(productId)!.push(event)
|
||||
}
|
||||
})
|
||||
|
||||
// Process each product group, keeping only the most recent version
|
||||
productGroups.forEach((productEvents, _productId) => {
|
||||
// Sort by created_at timestamp (most recent first)
|
||||
productEvents.sort((a, b) => b.event.created_at - a.event.created_at)
|
||||
|
||||
// Process each product group
|
||||
productGroups.forEach((productEvents, productId) => {
|
||||
// Sort by created_at and take the most recent
|
||||
const latestEvent = productEvents.sort((a: any, b: any) => b.created_at - a.created_at)[0]
|
||||
|
||||
// Take the most recent version
|
||||
const { event, productData } = productEvents[0]
|
||||
|
||||
console.log('Processing most recent product event:', event)
|
||||
console.log('Parsed product data:', productData)
|
||||
|
||||
// Find stall by stall_id from product data, not by pubkey
|
||||
const stall = marketStore.stalls.find(s => s.id === productData.stall_id)
|
||||
console.log('Found stall for product:', stall)
|
||||
|
||||
if (stall) {
|
||||
const product: Product = {
|
||||
id: productData.id, // Use the product's unique ID from content, not the Nostr event ID
|
||||
stall_id: stall.id,
|
||||
stallName: stall.name,
|
||||
name: productData.name,
|
||||
description: productData.description,
|
||||
price: productData.price,
|
||||
currency: productData.currency,
|
||||
quantity: productData.quantity,
|
||||
images: productData.images,
|
||||
categories: productData.categories,
|
||||
createdAt: event.created_at,
|
||||
updatedAt: event.created_at
|
||||
try {
|
||||
const productData = JSON.parse(latestEvent.content)
|
||||
const product = {
|
||||
id: productId,
|
||||
stall_id: productData.stall_id || 'unknown',
|
||||
stallName: productData.stallName || 'Unknown Stall',
|
||||
name: productData.name || 'Unnamed Product',
|
||||
description: productData.description || '',
|
||||
price: productData.price || 0,
|
||||
currency: productData.currency || 'sats',
|
||||
quantity: productData.quantity || 1,
|
||||
images: productData.images || [],
|
||||
categories: productData.categories || [],
|
||||
createdAt: latestEvent.created_at,
|
||||
updatedAt: latestEvent.created_at
|
||||
}
|
||||
|
||||
console.log('Created product (most recent version):', product)
|
||||
marketStore.addProduct(product)
|
||||
} else {
|
||||
console.warn('No matching stall found for product:', {
|
||||
productId: productData.id,
|
||||
stallId: productData.stall_id,
|
||||
availableStalls: marketStore.stalls.map(s => ({ id: s.id, name: s.name }))
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse product data:', err)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading products:', err)
|
||||
// Don't throw error, continue without products
|
||||
}
|
||||
|
||||
// If no products found, add some sample products for testing
|
||||
if (marketStore.products.length === 0) {
|
||||
console.log('No products found, adding sample products for testing')
|
||||
addSampleProducts()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add sample products for testing
|
||||
const addSampleProducts = () => {
|
||||
// Create a sample stall if none exists
|
||||
if (marketStore.stalls.length === 0) {
|
||||
const sampleStall: Stall = {
|
||||
id: 'sample-stall-1',
|
||||
pubkey: '70f93a32c14efe5e5c5ed7c13351dd53de367701dd00dd10a1f89280c7c586d5',
|
||||
name: 'Castle Tech',
|
||||
description: 'Premium tech products',
|
||||
categories: ['Electronics', 'Security'],
|
||||
shipping: {}
|
||||
}
|
||||
marketStore.addStall(sampleStall)
|
||||
}
|
||||
|
||||
const sampleProducts: Product[] = [
|
||||
const sampleProducts = [
|
||||
{
|
||||
id: 'sample-product-1',
|
||||
stall_id: 'sample-stall-1',
|
||||
stallName: 'Castle Tech',
|
||||
name: 'Seed Signer',
|
||||
description: 'Your Cyberpunk Cold Wallet',
|
||||
price: 100000,
|
||||
currency: 'sat',
|
||||
quantity: 15,
|
||||
id: 'sample-1',
|
||||
stall_id: 'sample-stall',
|
||||
stallName: 'Sample Stall',
|
||||
pubkey: nostrStore.account?.pubkey || '',
|
||||
name: 'Sample Product 1',
|
||||
description: 'This is a sample product for testing',
|
||||
price: 1000,
|
||||
currency: 'sats',
|
||||
quantity: 1,
|
||||
images: [],
|
||||
categories: ['Hardware', 'Security'],
|
||||
createdAt: Date.now() / 1000,
|
||||
updatedAt: Date.now() / 1000
|
||||
categories: [],
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
},
|
||||
{
|
||||
id: 'sample-product-2',
|
||||
stall_id: 'sample-stall-1',
|
||||
stallName: 'Castle Tech',
|
||||
name: 'Bitcoin Node',
|
||||
description: 'Full Bitcoin node for maximum privacy',
|
||||
price: 50000,
|
||||
currency: 'sat',
|
||||
quantity: 10,
|
||||
id: 'sample-2',
|
||||
stall_id: 'sample-stall',
|
||||
stallName: 'Sample Stall',
|
||||
pubkey: nostrStore.account?.pubkey || '',
|
||||
name: 'Sample Product 2',
|
||||
description: 'Another sample product for testing',
|
||||
price: 2000,
|
||||
currency: 'sats',
|
||||
quantity: 1,
|
||||
images: [],
|
||||
categories: ['Hardware', 'Networking'],
|
||||
createdAt: Date.now() / 1000,
|
||||
updatedAt: Date.now() / 1000
|
||||
categories: [],
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
updatedAt: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
sampleProducts.forEach(product => {
|
||||
marketStore.addProduct(product)
|
||||
})
|
||||
|
||||
console.log('Added sample products:', sampleProducts.length)
|
||||
}
|
||||
|
||||
const subscribeToMarketUpdates = () => {
|
||||
|
||||
// Subscribe to market updates
|
||||
const subscribeToMarketUpdates = (): (() => void) | null => {
|
||||
try {
|
||||
const client = nostrStore.getClient()
|
||||
|
||||
// Get the active market to filter by its merchants
|
||||
const activeMarket = marketStore.activeMarket
|
||||
if (!activeMarket) {
|
||||
console.warn('No active market found for subscription')
|
||||
return () => {}
|
||||
return null
|
||||
}
|
||||
|
||||
const merchants = [...(activeMarket.opts.merchants || [])]
|
||||
if (merchants.length === 0) {
|
||||
console.log('No merchants in market, skipping subscription')
|
||||
return () => {}
|
||||
}
|
||||
|
||||
console.log('Subscribing to updates from market merchants:', merchants)
|
||||
|
||||
// Subscribe to real-time market updates from market merchants only
|
||||
const filters = [
|
||||
{
|
||||
kinds: [MARKET_EVENT_KINDS.STALL, MARKET_EVENT_KINDS.PRODUCT],
|
||||
authors: merchants,
|
||||
since: Math.floor(Date.now() / 1000)
|
||||
|
||||
// Subscribe to market events
|
||||
const unsubscribe = relayHub.subscribe({
|
||||
id: `market-${activeMarket.d}`,
|
||||
filters: [
|
||||
{ kinds: [MARKET_EVENT_KINDS.MARKET] },
|
||||
{ kinds: [MARKET_EVENT_KINDS.STALL] },
|
||||
{ kinds: [MARKET_EVENT_KINDS.PRODUCT] },
|
||||
{ kinds: [MARKET_EVENT_KINDS.ORDER] }
|
||||
],
|
||||
onEvent: (event: any) => {
|
||||
handleMarketEvent(event)
|
||||
}
|
||||
]
|
||||
|
||||
// Subscribe to each relay individually
|
||||
const unsubscribes = config.market.supportedRelays.map(relay => {
|
||||
const sub = client.poolInstance.subscribeMany(
|
||||
[relay],
|
||||
filters,
|
||||
{
|
||||
onevent: (event: any) => {
|
||||
handleMarketEvent(event)
|
||||
}
|
||||
}
|
||||
)
|
||||
return () => sub.close()
|
||||
})
|
||||
|
||||
// Return a function that unsubscribes from all relays
|
||||
return () => {
|
||||
unsubscribes.forEach(unsub => unsub())
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error subscribing to market updates:', err)
|
||||
// Return a no-op function if subscription fails
|
||||
return () => {}
|
||||
|
||||
return unsubscribe
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe to market updates:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle incoming market events
|
||||
const handleMarketEvent = (event: any) => {
|
||||
// Skip if already processed
|
||||
if (processedEventIds.has(event.id)) return
|
||||
processedEventIds.add(event.id)
|
||||
|
||||
try {
|
||||
switch (event.kind) {
|
||||
case MARKET_EVENT_KINDS.STALL:
|
||||
handleStallEvent(event)
|
||||
break
|
||||
case MARKET_EVENT_KINDS.PRODUCT:
|
||||
handleProductEvent(event)
|
||||
break
|
||||
case MARKET_EVENT_KINDS.ORDER:
|
||||
handleOrderEvent(event)
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error handling market event:', err)
|
||||
console.log('Received market event:', event)
|
||||
|
||||
switch (event.kind) {
|
||||
case MARKET_EVENT_KINDS.MARKET:
|
||||
// Handle market updates
|
||||
break
|
||||
case MARKET_EVENT_KINDS.STALL:
|
||||
// Handle stall updates
|
||||
handleStallEvent(event)
|
||||
break
|
||||
case MARKET_EVENT_KINDS.PRODUCT:
|
||||
// Handle product updates
|
||||
handleProductEvent(event)
|
||||
break
|
||||
case MARKET_EVENT_KINDS.ORDER:
|
||||
// Handle order updates
|
||||
handleOrderEvent(event)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process pending products (products without stalls)
|
||||
const processPendingProducts = () => {
|
||||
console.log('Processing pending products:', pendingProducts.value.length)
|
||||
const remaining = pendingProducts.value.filter(({ productData }) => {
|
||||
const stall = marketStore.stalls.find(s => s.id === productData.stall_id)
|
||||
if (stall) {
|
||||
console.log('Found matching stall for pending product:', {
|
||||
productId: productData.id,
|
||||
stallId: stall.id,
|
||||
stallName: stall.name
|
||||
})
|
||||
|
||||
const product: Product = {
|
||||
id: productData.id,
|
||||
stall_id: stall.id,
|
||||
stallName: stall.name,
|
||||
name: productData.name,
|
||||
description: productData.description,
|
||||
price: productData.price,
|
||||
currency: productData.currency,
|
||||
quantity: productData.quantity,
|
||||
images: productData.images,
|
||||
categories: productData.categories,
|
||||
createdAt: productData.event?.created_at || Date.now() / 1000,
|
||||
updatedAt: productData.event?.created_at || Date.now() / 1000
|
||||
}
|
||||
|
||||
marketStore.addProduct(product)
|
||||
return false // Remove from pending
|
||||
}
|
||||
return true // Keep in pending
|
||||
const productsWithoutStalls = products.value.filter(product => {
|
||||
// Check if product has a stall tag
|
||||
return !product.stall_id
|
||||
})
|
||||
|
||||
pendingProducts.value = remaining
|
||||
if (remaining.length > 0) {
|
||||
console.log('Still pending products:', remaining.length)
|
||||
|
||||
if (productsWithoutStalls.length > 0) {
|
||||
console.log('Found products without stalls:', productsWithoutStalls.length)
|
||||
// You could create default stalls or handle this as needed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle stall events
|
||||
const handleStallEvent = (event: any) => {
|
||||
try {
|
||||
const stallData = JSON.parse(event.content)
|
||||
console.log('Processing stall event:', {
|
||||
stallId: stallData.id,
|
||||
stallName: stallData.name,
|
||||
merchantPubkey: event.pubkey
|
||||
})
|
||||
|
||||
const stall: Stall = {
|
||||
id: stallData.id, // Use stall ID from content, not event ID
|
||||
pubkey: event.pubkey,
|
||||
name: stallData.name,
|
||||
description: stallData.description,
|
||||
logo: stallData.logo,
|
||||
categories: stallData.categories,
|
||||
shipping: stallData.shipping
|
||||
const stallId = event.tags.find((tag: any) => tag[0] === 'd')?.[1]
|
||||
if (stallId) {
|
||||
const stallData = JSON.parse(event.content)
|
||||
const stall = {
|
||||
id: stallId,
|
||||
pubkey: event.pubkey,
|
||||
name: stallData.name || 'Unnamed Stall',
|
||||
description: stallData.description || '',
|
||||
created_at: event.created_at,
|
||||
...stallData
|
||||
}
|
||||
|
||||
marketStore.addStall(stall)
|
||||
}
|
||||
|
||||
marketStore.addStall(stall)
|
||||
|
||||
// Process any pending products that might match this stall
|
||||
processPendingProducts()
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse stall event:', err)
|
||||
console.warn('Failed to handle stall event:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle product events
|
||||
const handleProductEvent = (event: any) => {
|
||||
try {
|
||||
const productData = JSON.parse(event.content)
|
||||
console.log('Processing product event:', {
|
||||
productId: productData.id,
|
||||
productName: productData.name,
|
||||
stallId: productData.stall_id,
|
||||
merchantPubkey: event.pubkey
|
||||
})
|
||||
|
||||
// Find stall by stall_id from product data, not by pubkey
|
||||
const stall = marketStore.stalls.find(s => s.id === productData.stall_id)
|
||||
|
||||
if (stall) {
|
||||
console.log('Found matching stall:', { stallId: stall.id, stallName: stall.name })
|
||||
const product: Product = {
|
||||
id: productData.id, // Use product ID from content, not event ID
|
||||
stall_id: stall.id,
|
||||
stallName: stall.name,
|
||||
name: productData.name,
|
||||
description: productData.description,
|
||||
price: productData.price,
|
||||
currency: productData.currency,
|
||||
quantity: productData.quantity,
|
||||
images: productData.images,
|
||||
categories: productData.categories,
|
||||
const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1]
|
||||
if (productId) {
|
||||
const productData = JSON.parse(event.content)
|
||||
const product = {
|
||||
id: productId,
|
||||
stall_id: productData.stall_id || 'unknown',
|
||||
stallName: productData.stallName || 'Unknown Stall',
|
||||
pubkey: event.pubkey,
|
||||
name: productData.name || 'Unnamed Product',
|
||||
description: productData.description || '',
|
||||
price: productData.price || 0,
|
||||
currency: productData.currency || 'sats',
|
||||
quantity: productData.quantity || 1,
|
||||
images: productData.images || [],
|
||||
categories: productData.categories || [],
|
||||
createdAt: event.created_at,
|
||||
updatedAt: event.created_at
|
||||
}
|
||||
|
||||
marketStore.addProduct(product)
|
||||
} else {
|
||||
console.log('Stall not found yet, queuing product for later processing')
|
||||
pendingProducts.value.push({ event, productData })
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse product event:', err)
|
||||
console.warn('Failed to handle product data:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle order events
|
||||
const handleOrderEvent = (event: any) => {
|
||||
try {
|
||||
const orderData = JSON.parse(event.content)
|
||||
// Handle order events (for future order management)
|
||||
console.log('Order event received:', orderData)
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse order event:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const publishProduct = async (_productData: any) => {
|
||||
try {
|
||||
|
||||
// Note: This would need to be signed with the user's private key
|
||||
// For now, we'll just log that this function needs to be implemented
|
||||
console.log('Product publishing not yet implemented - needs private key signing')
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error publishing product:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const publishStall = async (_stallData: any) => {
|
||||
try {
|
||||
|
||||
// Note: This would need to be signed with the user's private key
|
||||
// For now, we'll just log that this function needs to be implemented
|
||||
console.log('Stall publishing not yet implemented - needs private key signing')
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error publishing stall:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const connectToMarket = async () => {
|
||||
try {
|
||||
console.log('Checking Nostr connection...')
|
||||
console.log('Current connection state:', nostrStore.isConnected)
|
||||
|
||||
if (!nostrStore.isConnected) {
|
||||
console.log('Connecting to Nostr relays...')
|
||||
await nostrStore.connect()
|
||||
console.log('Connected to Nostr relays')
|
||||
const order = {
|
||||
id: event.id,
|
||||
stall_id: orderData.stall_id || 'unknown',
|
||||
product_id: orderData.product_id || 'unknown',
|
||||
buyer_pubkey: event.pubkey,
|
||||
seller_pubkey: orderData.seller_pubkey || '',
|
||||
quantity: orderData.quantity || 1,
|
||||
total_price: orderData.total_price || 0,
|
||||
currency: orderData.currency || 'sats',
|
||||
status: orderData.status || 'pending',
|
||||
payment_request: orderData.payment_request,
|
||||
created_at: event.created_at,
|
||||
updated_at: event.created_at
|
||||
}
|
||||
|
||||
isConnected.value = nostrStore.isConnected
|
||||
console.log('Final connection state:', isConnected.value)
|
||||
// Note: addOrder method doesn't exist in the store, so we'll just log it
|
||||
console.log('Received order event:', order)
|
||||
} catch (err) {
|
||||
console.warn('Failed to handle order event:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish a product
|
||||
const publishProduct = async (_productData: any) => {
|
||||
// Implementation would depend on your event creation logic
|
||||
console.log('Publishing product:', _productData)
|
||||
}
|
||||
|
||||
// Publish a stall
|
||||
const publishStall = async (_stallData: any) => {
|
||||
// Implementation would depend on your event creation logic
|
||||
console.log('Publishing stall:', _stallData)
|
||||
}
|
||||
|
||||
// Connect to market
|
||||
const connectToMarket = async () => {
|
||||
try {
|
||||
console.log('Connecting to market...')
|
||||
|
||||
// Connect to relay hub
|
||||
await relayHub.connect()
|
||||
isConnected.value = relayHub.isConnected.value
|
||||
|
||||
if (!isConnected.value) {
|
||||
throw new Error('Failed to connect to Nostr relays')
|
||||
}
|
||||
|
||||
console.log('Connected to market')
|
||||
|
||||
// Load market data
|
||||
await loadMarketData({
|
||||
identifier: 'default',
|
||||
pubkey: nostrStore.account?.pubkey || ''
|
||||
})
|
||||
|
||||
// Load stalls and products
|
||||
await loadStalls()
|
||||
await loadProducts()
|
||||
|
||||
// Subscribe to updates
|
||||
subscribeToMarketUpdates()
|
||||
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error('Failed to connect to market')
|
||||
console.error('Error connecting to market:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Disconnect from market
|
||||
const disconnectFromMarket = () => {
|
||||
// Cleanup subscriptions and connections
|
||||
isConnected.value = false
|
||||
error.value = null
|
||||
console.log('Disconnected from market')
|
||||
}
|
||||
|
||||
|
||||
// Initialize market on mount
|
||||
onMounted(async () => {
|
||||
if (nostrStore.isConnected) {
|
||||
await connectToMarket()
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
disconnectFromMarket()
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error),
|
||||
isConnected: readonly(isConnected),
|
||||
|
||||
connectionStatus: readonly(connectionStatus),
|
||||
activeMarket: readonly(activeMarket),
|
||||
markets: readonly(markets),
|
||||
stalls: readonly(stalls),
|
||||
products: readonly(products),
|
||||
orders: readonly(orders),
|
||||
|
||||
// Actions
|
||||
loadMarket,
|
||||
connectToMarket,
|
||||
disconnectFromMarket,
|
||||
addSampleProducts,
|
||||
processPendingProducts,
|
||||
publishProduct,
|
||||
publishStall,
|
||||
subscribeToMarketUpdates
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue