- 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.
315 lines
No EOL
9.3 KiB
TypeScript
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 = []
|
|
}
|
|
} |