web-app/src/composables/useMarket.ts
padreug 06bcc4b91e refactor: Remove console logging for error handling in useMarket and useMarketPreloader
- Eliminate console.log and console.warn statements to enhance code clarity and maintainability.
- Replace error logging with comments to silently handle errors during market, stall, and product loading processes.
- Streamline the preloading process by removing unnecessary logging, improving overall code readability.
2025-08-12 08:54:38 +02:00

534 lines
15 KiB
TypeScript

import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
import { useNostrStore } from '@/stores/nostr'
import { useMarketStore } from '@/stores/market'
import { useRelayHub } from '@/composables/useRelayHub'
import { config } from '@/lib/config'
// Nostr event kinds for market functionality
const MARKET_EVENT_KINDS = {
MARKET: 30019,
STALL: 30017,
PRODUCT: 30018,
ORDER: 30020
} as const
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)
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
error.value = null
// Load market from naddr
// Parse naddr to get market data
const marketData = {
identifier: naddr.split(':')[2] || 'default',
pubkey: naddr.split(':')[1] || nostrStore.account?.pubkey || ''
}
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')
throw err
} finally {
isLoading.value = false
}
}
// Load market data from Nostr events
const loadMarketData = async (marketData: any) => {
try {
// Load market data from Nostr events
// Fetch market configuration event
const events = await relayHub.queryEvents([
{
kinds: [MARKET_EVENT_KINDS.MARKET],
authors: [marketData.pubkey],
'#d': [marketData.identifier]
}
])
// Process market events
if (events.length > 0) {
const marketEvent = events[0]
// Process market event
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 {
// No market events found, create default
// Create a default market if none exists
const market = {
d: marketData.identifier,
pubkey: marketData.pubkey,
relays: config.market.supportedRelays,
selected: true,
opts: {
name: 'Ariège Market',
description: 'A communal market to sell your goods',
merchants: [],
ui: {}
}
}
marketStore.addMarket(market)
marketStore.setActiveMarket(market)
}
} catch (err) {
// Don't throw error, create default market instead
const market = {
d: marketData.identifier,
pubkey: marketData.pubkey,
relays: config.market.supportedRelays,
selected: true,
opts: {
name: 'Default Market',
description: 'A default market',
merchants: [],
ui: {}
}
}
marketStore.addMarket(market)
marketStore.setActiveMarket(market)
}
}
// Load stalls from market merchants
const loadStalls = async () => {
try {
// Get the active market to filter by its merchants
const activeMarket = marketStore.activeMarket
if (!activeMarket) {
return
}
const merchants = [...(activeMarket.opts.merchants || [])]
if (merchants.length === 0) {
return
}
// Fetch stall events from market merchants only
const events = await relayHub.queryEvents([
{
kinds: [MARKET_EVENT_KINDS.STALL],
authors: merchants
}
])
// Process stall events
// Group events by stall ID and keep only the most recent version
const stallGroups = new Map<string, any[]>()
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)
}
})
// 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) {
// Silently handle parse errors
}
})
} catch (err) {
// Silently handle stall loading errors
}
}
// Load products from market stalls
const loadProducts = async () => {
try {
const activeMarket = marketStore.activeMarket
if (!activeMarket) {
return
}
const merchants = [...(activeMarket.opts.merchants || [])]
if (merchants.length === 0) {
return
}
// Fetch product events from market merchants
const events = await relayHub.queryEvents([
{
kinds: [MARKET_EVENT_KINDS.PRODUCT],
authors: merchants
}
])
// Process product events
// Group events by product ID and keep only the most recent version
const productGroups = new Map<string, any[]>()
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)
}
})
// 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]
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
}
marketStore.addProduct(product)
} catch (err) {
// Silently handle parse errors
}
})
} catch (err) {
// Silently handle product loading errors
}
}
// Add sample products for testing
const addSampleProducts = () => {
const sampleProducts = [
{
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: [],
createdAt: Math.floor(Date.now() / 1000),
updatedAt: Math.floor(Date.now() / 1000)
},
{
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: [],
createdAt: Math.floor(Date.now() / 1000),
updatedAt: Math.floor(Date.now() / 1000)
}
]
sampleProducts.forEach(product => {
marketStore.addProduct(product)
})
}
// Subscribe to market updates
const subscribeToMarketUpdates = (): (() => void) | null => {
try {
const activeMarket = marketStore.activeMarket
if (!activeMarket) {
return null
}
// 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)
}
})
return unsubscribe
} catch (error) {
return null
}
}
// Handle incoming market events
const handleMarketEvent = (event: any) => {
// Process market 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 = () => {
const productsWithoutStalls = products.value.filter(product => {
// Check if product has a stall tag
return !product.stall_id
})
if (productsWithoutStalls.length > 0) {
// You could create default stalls or handle this as needed
}
}
// Handle stall events
const handleStallEvent = (event: any) => {
try {
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)
}
} catch (err) {
// Silently handle stall event errors
}
}
// Handle product events
const handleProductEvent = (event: any) => {
try {
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)
}
} catch (err) {
// Silently handle product event errors
}
}
// Handle order events
const handleOrderEvent = (event: any) => {
try {
const orderData = JSON.parse(event.content)
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
}
// Note: addOrder method doesn't exist in the store, so we'll just handle it silently
} catch (err) {
// Silently handle order event errors
}
}
// Publish a product
const publishProduct = async (_productData: any) => {
// Implementation would depend on your event creation logic
// TODO: Implement product publishing
}
// Publish a stall
const publishStall = async (_stallData: any) => {
// Implementation would depend on your event creation logic
// TODO: Implement stall publishing
}
// Connect to market
const connectToMarket = async () => {
try {
// Connect 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')
}
// Market connected successfully
// 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')
throw err
}
}
// Disconnect from market
const disconnectFromMarket = () => {
isConnected.value = false
error.value = null
// Market disconnected
}
// 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
}
}