This commit is contained in:
padreug 2025-02-14 23:12:34 +01:00
parent d694f9b645
commit d27f66e95d
5 changed files with 230 additions and 162 deletions

View file

@ -38,9 +38,6 @@ const DEFAULT_RELAYS = [
'wss://nostr.atitlan.io'
]
// Get support agent's public key from environment variable
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
// Helper functions
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> {
return Promise.race([
@ -132,54 +129,33 @@ export const useNostrStore = defineStore('nostr', () => {
profiles.value.clear()
processedMessageIds.value.clear()
// Close existing connections
relayPool.value.forEach(relay => {
try {
relay.close()
} catch (err) {
console.error('Error closing relay:', err)
}
})
relayPool.value = []
// Connect to relays with timeout
const connectionPromises = account.value.relays.map(async relay => {
try {
return await withTimeout(connectToRelay(relay.url), 5000)
} catch (err) {
console.error(`Timeout connecting to ${relay.url}:`, err)
return null
}
})
const connectedRelays = await Promise.all(connectionPromises)
relayPool.value = connectedRelays.filter((relay): relay is NonNullable<typeof relay> => relay !== null)
// Connect to relays
relayPool.value = (await Promise.all(
account.value.relays.map(async relay => {
console.log('Connecting to relay:', relay.url)
const connection = await connectToRelay(relay.url)
if (!connection) {
console.error('Failed to connect to relay:', relay.url)
}
return connection
})
)).filter((relay): relay is any => relay !== null)
if (relayPool.value.length === 0) {
throw new Error('Failed to connect to any relays')
}
// Set active chat to support agent first
activeChat.value = SUPPORT_NPUB
// Setup visibility change handler
setupVisibilityHandler()
// Subscribe to messages with shorter timeout
try {
await withTimeout(subscribeToMessages(), 10000)
} catch (err) {
console.error('Failed to subscribe to messages:', err)
// Continue even if subscription fails
}
// Subscribe to messages in the background
subscribeToMessages().catch(err => {
console.error('Background subscription failed:', err)
})
// Load profiles with shorter timeout
try {
await withTimeout(loadProfiles(), 5000)
} catch (err) {
console.error('Failed to load profiles:', err)
// Continue even if profile loading fails
}
} catch (err) {
console.error('Failed to initialize:', err)
throw new Error('Failed to connect to the network. Please try again.')
throw err
}
}
@ -198,43 +174,9 @@ export const useNostrStore = defineStore('nostr', () => {
relays: DEFAULT_RELAYS.map(url => ({ url, read: true, write: true }))
}
try {
// Initialize connection with a global timeout
await withTimeout(init(), 30000) // 30 second total timeout for the entire login process
} catch (err) {
// If initialization fails, clear the account
account.value = null
throw err
}
}
async function loadProfiles() {
if (!account.value) return
const filter = {
kinds: [0],
authors: Array.from(messages.value.keys())
}
if (filter.authors.length === 0) return
relayPool.value.forEach(relay => {
const sub = relay.sub([filter])
sub.on('event', (event: NostrEvent) => {
try {
const profile = JSON.parse(event.content)
profiles.value.set(event.pubkey, {
pubkey: event.pubkey,
name: profile.name,
picture: profile.picture,
about: profile.about,
nip05: profile.nip05
})
} catch (err) {
console.error('Failed to parse profile:', err)
}
})
// Initialize connection in the background
init().catch(err => {
console.error('Background initialization failed:', err)
})
}
@ -301,55 +243,42 @@ export const useNostrStore = defineStore('nostr', () => {
async function subscribeToMessages() {
if (!account.value) return
// Cleanup existing subscription if any
if (currentSubscription.value) {
unsubscribeFromMessages()
}
// Cleanup existing subscription
unsubscribeFromMessages()
let hasReceivedMessage = false
const subscriptionTimeout = setTimeout(() => {
if (!hasReceivedMessage) {
console.log('No messages received, considering subscription successful')
hasReceivedMessage = true
}
}, 5000)
// Get timestamp from 24 hours ago
const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60)
try {
const subscribeToRelay = (relay: any) => {
return new Promise((resolve) => {
let subs: any[] = []
let resolved = false
// Set a timeout for the entire subscription
const timeout = setTimeout(() => {
if (!resolved) {
console.log('Subscription timeout, but continuing...')
resolved = true
resolve(true)
}
}, 8000)
const subs: any[] = []
let messageReceived = false
let isResolved = false
try {
// Filter for received messages with history
const receivedFilter = {
kinds: [4],
'#p': [account.value!.pubkey],
since: 0
}
// Filter for sent messages with history
const sentFilter = {
kinds: [4],
authors: [account.value!.pubkey],
since: 0
}
console.log('Setting up subscriptions for relay...')
// Subscribe to received messages
const receivedSub = relay.sub([receivedFilter])
const receivedSub = relay.sub([{
kinds: [4],
'#p': [account.value!.pubkey],
since,
}])
subs.push(receivedSub)
// Subscribe to sent messages
const sentSub = relay.sub([{
kinds: [4],
authors: [account.value!.pubkey],
since,
}])
subs.push(sentSub)
// Handle received messages
receivedSub.on('event', async (event: NostrEvent) => {
hasReceivedMessage = true
messageReceived = true
if (isResolved) return // Don't process events after resolution
try {
if (processedMessageIds.value.has(event.id)) return
@ -369,21 +298,15 @@ export const useNostrStore = defineStore('nostr', () => {
await addMessage(event.pubkey, dm)
processedMessageIds.value.add(event.id)
if (!profiles.value.has(event.pubkey)) {
await loadProfiles()
}
} catch (err) {
console.error('Failed to decrypt received message:', err)
}
})
// Subscribe to sent messages
const sentSub = relay.sub([sentFilter])
subs.push(sentSub)
// Handle sent messages
sentSub.on('event', async (event: NostrEvent) => {
hasReceivedMessage = true
messageReceived = true
if (isResolved) return // Don't process events after resolution
try {
if (processedMessageIds.value.has(event.id)) return
@ -414,50 +337,56 @@ export const useNostrStore = defineStore('nostr', () => {
// Store subscriptions for cleanup
currentSubscription.value = {
unsub: () => {
clearTimeout(timeout)
subs.forEach(sub => {
try {
if (sub && typeof sub.unsub === 'function') {
sub.unsub()
}
} catch (err) {
console.error('Failed to unsubscribe:', err)
console.debug('Failed to unsubscribe:', err)
}
})
}
}
// Consider subscription successful immediately
if (!resolved) {
resolved = true
resolve(true)
}
// Consider subscription successful after a short delay
setTimeout(() => {
if (!isResolved) {
isResolved = true
console.debug(messageReceived ?
'Subscription successful with messages' :
'Subscription successful, no messages yet'
)
resolve(true)
}
}, 3000)
} catch (err) {
console.error('Error in subscription:', err)
if (!resolved) {
resolved = true
resolve(true)
console.debug('Error in subscription setup:', err)
if (!isResolved) {
isResolved = true
resolve(false)
}
}
})
}
// Wait for relays
await Promise.all(
relayPool.value.map(relay =>
withTimeout(subscribeToRelay(relay), 10000)
.catch(() => true)
)
// Wait for all relays to set up subscriptions
const results = await Promise.all(
relayPool.value.map(relay => subscribeToRelay(relay))
)
clearTimeout(subscriptionTimeout)
return true
// Consider success if at least one relay worked
const success = results.some(result => result)
if (!success) {
console.debug('No relays successfully subscribed')
return false // Return false instead of throwing
}
} catch (err) {
clearTimeout(subscriptionTimeout)
console.error('Failed to subscribe to messages:', err)
return true
} catch (err) {
console.debug('Subscription process failed:', err)
return false // Return false instead of throwing
}
}
@ -472,6 +401,115 @@ export const useNostrStore = defineStore('nostr', () => {
}
}
async function loadProfiles() {
if (!account.value) return
const pubkeysToLoad = new Set<string>()
// Collect all unique pubkeys from messages
for (const [pubkey] of messages.value.entries()) {
if (!profiles.value.has(pubkey)) {
pubkeysToLoad.add(pubkey)
}
}
if (pubkeysToLoad.size === 0) return
try {
const filter = {
kinds: [0],
authors: Array.from(pubkeysToLoad)
}
const loadFromRelay = (relay: any) => {
return new Promise<void>((resolve) => {
const sub = relay.sub([filter])
sub.on('event', (event: NostrEvent) => {
try {
const profile = JSON.parse(event.content)
profiles.value.set(event.pubkey, {
pubkey: event.pubkey,
name: profile.name,
picture: profile.picture,
about: profile.about,
nip05: profile.nip05
})
} catch (err) {
console.error('Failed to parse profile:', err)
}
})
// Resolve after receiving EOSE (End of Stored Events)
sub.on('eose', () => {
resolve()
})
// Set a timeout in case EOSE is not received
setTimeout(() => {
resolve()
}, 5000)
})
}
// Load profiles from all relays concurrently
await Promise.all(relayPool.value.map(relay => loadFromRelay(relay)))
} catch (err) {
console.error('Failed to load profiles:', err)
}
}
// Add a reconnection function
async function reconnectToRelays() {
if (!account.value) return
console.log('Attempting to reconnect to relays...')
// Close existing connections
relayPool.value.forEach(relay => {
try {
relay.close()
} catch (err) {
console.error('Error closing relay:', err)
}
})
relayPool.value = []
// Reconnect
relayPool.value = (await Promise.all(
account.value.relays.map(async relay => {
console.log('Reconnecting to relay:', relay.url)
const connection = await connectToRelay(relay.url)
if (!connection) {
console.error('Failed to reconnect to relay:', relay.url)
}
return connection
})
)).filter((relay): relay is any => relay !== null)
if (relayPool.value.length === 0) {
throw new Error('Failed to connect to any relays')
}
// Resubscribe to messages
await subscribeToMessages()
}
// Add visibility change handler
function setupVisibilityHandler() {
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && account.value) {
console.log('Page became visible, reconnecting...')
try {
await reconnectToRelays()
} catch (err) {
console.error('Failed to reconnect:', err)
}
}
})
}
return {
account,
profiles,
@ -484,6 +522,7 @@ export const useNostrStore = defineStore('nostr', () => {
logout,
sendMessage,
subscribeToMessages,
unsubscribeFromMessages
unsubscribeFromMessages,
loadProfiles
}
})