web-app/src/modules/nostr-feed/services/FeedService.ts
padreug a5e6c301e1 Update FeedService to use direct connection status check
- Changed the connection status check in FeedService from an asynchronous health check to a direct property access on relayHub.
- Added a console log to output the current connection status for better debugging and monitoring.

These changes improve the efficiency of the connection status verification process in the FeedService.
2025-09-23 23:59:37 +02:00

315 lines
No EOL
9.3 KiB
TypeScript

import { ref, computed } from 'vue'
import { BaseService } from '@/core/base/BaseService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { eventBus } from '@/core/event-bus'
import type { Event as NostrEvent, Filter } from 'nostr-tools'
export interface FeedPost {
id: string
pubkey: string
content: string
created_at: number
tags: string[][]
mentions: string[]
isReply: boolean
replyTo?: string
}
export interface FeedConfig {
feedType: 'announcements' | 'general' | 'mentions' | 'events' | 'all'
maxPosts?: number
adminPubkeys?: string[]
}
export class FeedService extends BaseService {
protected readonly metadata = {
name: 'FeedService',
version: '1.0.0',
dependencies: []
}
protected relayHub: any = null
protected visibilityService: any = null
// Event ID tracking for deduplication
private seenEventIds = new Set<string>()
// Feed state
private _posts = ref<FeedPost[]>([])
private _isLoading = ref(false)
private _error = ref<string | null>(null)
// Current subscription state
private currentSubscription: string | null = null
private currentUnsubscribe: (() => void) | null = null
private currentConfig: FeedConfig | null = null
// Public reactive state
public readonly posts = computed(() => this._posts.value)
public readonly isLoading = computed(() => this._isLoading.value)
public readonly error = computed(() => this._error.value)
protected async onInitialize(): Promise<void> {
console.log('FeedService: Starting initialization...')
this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
this.visibilityService = injectService(SERVICE_TOKENS.VISIBILITY_SERVICE)
console.log('FeedService: RelayHub injected:', !!this.relayHub)
console.log('FeedService: VisibilityService injected:', !!this.visibilityService)
if (!this.relayHub) {
throw new Error('RelayHub service not available')
}
// Register with visibility service for proper connection management
if (this.visibilityService) {
this.visibilityService.registerService(
'FeedService',
this.onResume.bind(this),
this.onPause.bind(this)
)
}
console.log('FeedService: Initialization complete')
}
/**
* Subscribe to feed with deduplication
*/
async subscribeFeed(config: FeedConfig): Promise<void> {
// If already subscribed with same config, don't resubscribe
if (this.currentSubscription && this.currentConfig &&
JSON.stringify(this.currentConfig) === JSON.stringify(config)) {
return
}
// Unsubscribe from previous feed if exists
await this.unsubscribeFeed()
this.currentConfig = config
this._isLoading.value = true
this._error.value = null
try {
// Check if RelayHub is connected
if (!this.relayHub) {
throw new Error('RelayHub not available')
}
if (!this.relayHub.isConnected) {
console.log('RelayHub not connected, attempting to connect...')
await this.relayHub.connect()
}
if (!this.relayHub.isConnected) {
throw new Error('Unable to connect to relays')
}
// Create subscription ID
const subscriptionId = `feed-service-${config.feedType}-${Date.now()}`
// Create filter
const filter: Filter = {
kinds: [1], // Text notes
limit: config.maxPosts || 50
}
if (config.feedType === 'announcements') {
if (config.adminPubkeys && config.adminPubkeys.length > 0) {
filter.authors = config.adminPubkeys
} else {
// No admin pubkeys configured for announcements - don't subscribe
console.log('No admin pubkeys configured for announcements feed')
this._isLoading.value = false
return
}
}
console.log(`Creating feed subscription for ${config.feedType} with filter:`, filter)
// Subscribe to events with deduplication
const unsubscribe = this.relayHub.subscribe({
id: subscriptionId,
filters: [filter],
onEvent: (event: NostrEvent) => {
this.handleNewEvent(event, config)
},
onEose: () => {
console.log(`Feed subscription ${subscriptionId} end of stored events`)
console.log('FeedService: Setting isLoading to false')
this._isLoading.value = false
console.log('FeedService: isLoading is now:', this._isLoading.value)
},
onClose: () => {
console.log(`Feed subscription ${subscriptionId} closed`)
}
})
// Store the subscription info for later cleanup
this.currentSubscription = subscriptionId
this.currentUnsubscribe = unsubscribe
// Set a timeout to stop loading if no EOSE is received
setTimeout(() => {
console.log(`Feed subscription ${subscriptionId} timeout check: isLoading=${this._isLoading.value}, currentSub=${this.currentSubscription}`)
if (this._isLoading.value && this.currentSubscription === subscriptionId) {
console.log(`Feed subscription ${subscriptionId} timeout, stopping loading`)
this._isLoading.value = false
}
}, 5000) // 5 second timeout (reduced for testing)
} catch (err) {
console.error('Failed to subscribe to feed:', err)
this._error.value = err instanceof Error ? err.message : 'Failed to subscribe to feed'
this._isLoading.value = false
}
}
/**
* Handle new event with robust deduplication
*/
private handleNewEvent(event: NostrEvent, config: FeedConfig): void {
// Skip if event already seen
if (this.seenEventIds.has(event.id)) {
return
}
// Add to seen events
this.seenEventIds.add(event.id)
// Check if event should be included based on feed type
if (!this.shouldIncludeEvent(event, config)) {
return
}
// Transform to FeedPost
const post: FeedPost = {
id: event.id,
pubkey: event.pubkey,
content: event.content,
created_at: event.created_at,
tags: event.tags || [],
mentions: event.tags?.filter((tag: string[]) => tag[0] === 'p').map((tag: string[]) => tag[1]) || [],
isReply: event.tags?.some((tag: string[]) => tag[0] === 'e' && tag[3] === 'reply') || false,
replyTo: event.tags?.find((tag: string[]) => tag[0] === 'e' && tag[3] === 'reply')?.[1]
}
// Add to posts (newest first)
this._posts.value = [post, ...this._posts.value]
// Limit array size and clean up seen IDs
const maxPosts = config.maxPosts || 100
if (this._posts.value.length > maxPosts) {
const removedPosts = this._posts.value.slice(maxPosts)
this._posts.value = this._posts.value.slice(0, maxPosts)
// Clean up seen IDs for removed posts
removedPosts.forEach(post => {
this.seenEventIds.delete(post.id)
})
}
// Emit event for other modules
eventBus.emit('nostr-feed:new-post', {
event,
feedType: config.feedType
}, 'nostr-feed')
}
/**
* Check if event should be included in feed
*/
private shouldIncludeEvent(event: NostrEvent, config: FeedConfig): boolean {
const isAdminPost = config.adminPubkeys?.includes(event.pubkey) || false
switch (config.feedType) {
case 'announcements':
return isAdminPost
case 'general':
return !isAdminPost
case 'events':
// Events feed could show all posts for now, or implement event-specific filtering
return true
case 'mentions':
// TODO: Implement mention detection if needed
return true
case 'all':
default:
return true
}
}
/**
* Unsubscribe from current feed
*/
async unsubscribeFeed(): Promise<void> {
if (this.currentUnsubscribe) {
this.currentUnsubscribe()
this.currentSubscription = null
this.currentUnsubscribe = null
this.currentConfig = null
}
}
/**
* Refresh feed (clear and reload)
*/
async refreshFeed(): Promise<void> {
if (!this.currentConfig) return
// Clear existing state
this._posts.value = []
this.seenEventIds.clear()
// Resubscribe
await this.subscribeFeed(this.currentConfig)
}
/**
* Get filtered posts for specific feed type
*/
getFilteredPosts(config: FeedConfig): FeedPost[] {
return this._posts.value
.filter(post => this.shouldIncludeEvent({
id: post.id,
pubkey: post.pubkey,
content: post.content,
created_at: post.created_at,
tags: post.tags
} as NostrEvent, config))
.sort((a, b) => b.created_at - a.created_at)
.slice(0, config.maxPosts || 100)
}
/**
* Visibility service callbacks
*/
private async onResume(): Promise<void> {
console.log('FeedService: App resumed, checking connections')
// Check if we need to reconnect
if (this.currentConfig && this.relayHub) {
const isConnected = this.relayHub.isConnected.value
console.log('FeedService: RelayHub connection status:', isConnected)
if (!isConnected) {
console.log('FeedService: Reconnecting after resume')
await this.subscribeFeed(this.currentConfig)
}
}
}
private onPause(): void {
console.log('FeedService: App paused, maintaining state')
// Don't clear state, just log for debugging
}
/**
* Cleanup
*/
protected async onDestroy(): Promise<void> {
await this.unsubscribeFeed()
this.seenEventIds.clear()
this._posts.value = []
}
}