Implement modular architecture with core services and Nostr integration

- Introduce a modular application structure with a new app configuration file to manage module settings and features.
- Implement a dependency injection container for service management across modules.
- Create a plugin manager to handle module registration, installation, and lifecycle management.
- Develop a global event bus for inter-module communication, enhancing loose coupling between components.
- Add core modules including base functionalities, Nostr feed, and PWA services, with support for dynamic loading and configuration.
- Establish a Nostr client hub for managing WebSocket connections and event handling.
- Enhance user experience with a responsive Nostr feed component, integrating admin announcements and community posts.
- Refactor existing components to align with the new modular architecture, improving maintainability and scalability.
This commit is contained in:
padreug 2025-09-04 23:43:33 +02:00
parent 2d8215a35e
commit 519a9003d4
16 changed files with 2520 additions and 14 deletions

View file

@ -0,0 +1,80 @@
// Copy the existing auth logic into a service class
import { ref } from 'vue'
import { eventBus } from '@/core/event-bus'
export class AuthService {
public isAuthenticated = ref(false)
public user = ref<any>(null)
public isLoading = ref(false)
async initialize(): Promise<void> {
console.log('🔑 Initializing auth service...')
// Check for existing auth state
this.checkAuth()
if (this.isAuthenticated.value) {
eventBus.emit('auth:login', { user: this.user.value }, 'auth-service')
}
}
checkAuth(): boolean {
// Implement your existing auth check logic here
// For now, we'll use a simple localStorage check
const authData = localStorage.getItem('auth')
if (authData) {
try {
const parsed = JSON.parse(authData)
this.isAuthenticated.value = true
this.user.value = parsed
return true
} catch (error) {
console.error('Invalid auth data in localStorage:', error)
this.logout()
}
}
this.isAuthenticated.value = false
this.user.value = null
return false
}
async login(credentials: any): Promise<void> {
this.isLoading.value = true
try {
// Implement your login logic here
// For demo purposes, we'll accept any credentials
this.user.value = credentials
this.isAuthenticated.value = true
// Store auth state
localStorage.setItem('auth', JSON.stringify(credentials))
eventBus.emit('auth:login', { user: credentials }, 'auth-service')
} catch (error) {
console.error('Login failed:', error)
eventBus.emit('auth:login-failed', { error }, 'auth-service')
throw error
} finally {
this.isLoading.value = false
}
}
logout(): void {
this.user.value = null
this.isAuthenticated.value = false
localStorage.removeItem('auth')
eventBus.emit('auth:logout', {}, 'auth-service')
}
async refresh(): Promise<void> {
// Implement token refresh logic if needed
console.log('Refreshing auth token...')
}
}
// Export singleton instance
export const auth = new AuthService()

65
src/modules/base/index.ts Normal file
View file

@ -0,0 +1,65 @@
import type { App } from 'vue'
import type { ModulePlugin } from '@/core/types'
import { container, SERVICE_TOKENS } from '@/core/di-container'
import { relayHub } from './nostr/relay-hub'
import { nostrclientHub } from './nostr/nostrclient-hub'
// Import auth services
import { auth } from './auth/auth-service'
// Import PWA services
import { pwaService } from './pwa/pwa-service'
/**
* Base Module Plugin
* Provides core infrastructure: Nostr, Auth, PWA, and UI components
*/
export const baseModule: ModulePlugin = {
name: 'base',
version: '1.0.0',
async install(_app: App, options?: any) {
console.log('🔧 Installing base module...')
// Register core Nostr services
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.NOSTR_CLIENT_HUB, nostrclientHub)
// Register auth service
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
// Register PWA service
container.provide('pwaService', pwaService)
// Initialize core services
await relayHub.initialize(options?.nostr?.relays || [])
await auth.initialize()
console.log('✅ Base module installed successfully')
},
async uninstall() {
console.log('🗑️ Uninstalling base module...')
// Cleanup Nostr connections
relayHub.disconnect()
nostrclientHub.disconnect?.()
console.log('✅ Base module uninstalled')
},
services: {
relayHub,
nostrclientHub,
auth,
pwaService
},
// No routes - base module is pure infrastructure
routes: [],
// No UI components at module level - they'll be imported as needed
components: {}
}
export default baseModule

View file

@ -0,0 +1,16 @@
// Re-export Nostr infrastructure from base module
export { RelayHub } from './relay-hub'
export { NostrclientHub } from './nostrclient-hub'
export { relayHub } from './relay-hub'
export { nostrclientHub } from './nostrclient-hub'
// Re-export types
export type {
RelayConfig,
SubscriptionConfig,
RelayStatus
} from './relay-hub'
export type {
NostrclientConfig
} from './nostrclient-hub'

View file

@ -0,0 +1,362 @@
import type { Filter, Event } from 'nostr-tools'
// Simple EventEmitter for browser compatibility
class EventEmitter {
private events: { [key: string]: Function[] } = {}
on(event: string, listener: Function) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(listener)
}
emit(event: string, ...args: any[]) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(...args))
}
}
removeAllListeners(event?: string) {
if (event) {
delete this.events[event]
} else {
this.events = {}
}
}
}
export interface NostrclientConfig {
url: string
privateKey?: string // For private WebSocket endpoint
}
export interface SubscriptionConfig {
id: string
filters: Filter[]
onEvent?: (event: Event) => void
onEose?: () => void
onClose?: () => void
}
export interface RelayStatus {
url: string
connected: boolean
lastSeen: number
error?: string
}
export class NostrclientHub extends EventEmitter {
private ws: WebSocket | null = null
private config: NostrclientConfig
private subscriptions: Map<string, SubscriptionConfig> = new Map()
private reconnectInterval?: number
private reconnectAttempts = 0
private readonly maxReconnectAttempts = 5
private readonly reconnectDelay = 5000
// Connection state
private _isConnected = false
private _isConnecting = false
constructor(config: NostrclientConfig) {
super()
this.config = config
}
get isConnected(): boolean {
return this._isConnected
}
get isConnecting(): boolean {
return this._isConnecting
}
get totalSubscriptionCount(): number {
return this.subscriptions.size
}
get subscriptionDetails(): Array<{ id: string; filters: Filter[] }> {
return Array.from(this.subscriptions.values()).map(sub => ({
id: sub.id,
filters: sub.filters
}))
}
/**
* Initialize and connect to nostrclient WebSocket
*/
async initialize(): Promise<void> {
console.log('🔧 NostrclientHub: Initializing connection to', this.config.url)
await this.connect()
}
/**
* Connect to the nostrclient WebSocket
*/
async connect(): Promise<void> {
if (this._isConnecting || this._isConnected) {
return
}
this._isConnecting = true
this.reconnectAttempts++
try {
console.log('🔧 NostrclientHub: Connecting to nostrclient WebSocket')
// Determine WebSocket endpoint
const wsUrl = this.config.privateKey
? `${this.config.url}/${this.config.privateKey}` // Private endpoint
: `${this.config.url}/relay` // Public endpoint
this.ws = new WebSocket(wsUrl)
this.ws.onopen = () => {
console.log('🔧 NostrclientHub: WebSocket connected')
this._isConnected = true
this._isConnecting = false
this.reconnectAttempts = 0
this.emit('connected')
// Resubscribe to existing subscriptions
this.resubscribeAll()
}
this.ws.onmessage = (event) => {
this.handleMessage(event.data)
}
this.ws.onclose = (event) => {
console.log('🔧 NostrclientHub: WebSocket closed:', event.code, event.reason)
this._isConnected = false
this._isConnecting = false
this.emit('disconnected', event)
// Schedule reconnection
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect()
} else {
this.emit('maxReconnectionAttemptsReached')
}
}
this.ws.onerror = (error) => {
console.error('🔧 NostrclientHub: WebSocket error:', error)
this.emit('error', error)
}
} catch (error) {
this._isConnecting = false
console.error('🔧 NostrclientHub: Connection failed:', error)
this.emit('connectionError', error)
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect()
}
}
}
/**
* Disconnect from the WebSocket
*/
disconnect(): void {
if (this.reconnectInterval) {
clearTimeout(this.reconnectInterval)
this.reconnectInterval = undefined
}
if (this.ws) {
this.ws.close()
this.ws = null
}
this._isConnected = false
this._isConnecting = false
this.subscriptions.clear()
this.emit('disconnected')
}
/**
* Subscribe to events
*/
subscribe(config: SubscriptionConfig): () => void {
if (!this._isConnected) {
throw new Error('Not connected to nostrclient')
}
// Store subscription
this.subscriptions.set(config.id, config)
// Send REQ message
const reqMessage = JSON.stringify([
'REQ',
config.id,
...config.filters
])
this.ws?.send(reqMessage)
console.log('🔧 NostrclientHub: Subscribed to', config.id)
// Return unsubscribe function
return () => {
this.unsubscribe(config.id)
}
}
/**
* Unsubscribe from events
*/
unsubscribe(subscriptionId: string): void {
if (!this._isConnected) {
return
}
// Send CLOSE message
const closeMessage = JSON.stringify(['CLOSE', subscriptionId])
this.ws?.send(closeMessage)
// Remove from subscriptions
this.subscriptions.delete(subscriptionId)
console.log('🔧 NostrclientHub: Unsubscribed from', subscriptionId)
}
/**
* Publish an event
*/
async publishEvent(event: Event): Promise<void> {
if (!this._isConnected) {
throw new Error('Not connected to nostrclient')
}
const eventMessage = JSON.stringify(['EVENT', event])
this.ws?.send(eventMessage)
console.log('🔧 NostrclientHub: Published event', event.id)
this.emit('eventPublished', { eventId: event.id })
}
/**
* Query events (one-time fetch)
*/
async queryEvents(filters: Filter[]): Promise<Event[]> {
return new Promise((resolve, reject) => {
if (!this._isConnected) {
reject(new Error('Not connected to nostrclient'))
return
}
const queryId = `query-${Date.now()}`
const events: Event[] = []
let eoseReceived = false
// Create temporary subscription for query
const tempSubscription = this.subscribe({
id: queryId,
filters,
onEvent: (event) => {
events.push(event)
},
onEose: () => {
eoseReceived = true
this.unsubscribe(queryId)
resolve(events)
},
onClose: () => {
if (!eoseReceived) {
reject(new Error('Query subscription closed unexpectedly'))
}
}
})
// Timeout after 30 seconds
setTimeout(() => {
if (!eoseReceived) {
tempSubscription()
reject(new Error('Query timeout'))
}
}, 30000)
})
}
/**
* Handle incoming WebSocket messages
*/
private handleMessage(data: string): void {
try {
const message = JSON.parse(data)
if (Array.isArray(message) && message.length >= 2) {
const [type, subscriptionId, ...rest] = message
switch (type) {
case 'EVENT':
const event = rest[0] as Event
const subscription = this.subscriptions.get(subscriptionId)
if (subscription?.onEvent) {
subscription.onEvent(event)
}
this.emit('event', { subscriptionId, event })
break
case 'EOSE':
const eoseSubscription = this.subscriptions.get(subscriptionId)
if (eoseSubscription?.onEose) {
eoseSubscription.onEose()
}
this.emit('eose', { subscriptionId })
break
case 'NOTICE':
console.log('🔧 NostrclientHub: Notice from relay:', rest[0])
this.emit('notice', { message: rest[0] })
break
default:
console.log('🔧 NostrclientHub: Unknown message type:', type)
}
}
} catch (error) {
console.error('🔧 NostrclientHub: Failed to parse message:', error)
}
}
/**
* Resubscribe to all existing subscriptions after reconnection
*/
private resubscribeAll(): void {
for (const [id, config] of this.subscriptions) {
const reqMessage = JSON.stringify([
'REQ',
id,
...config.filters
])
this.ws?.send(reqMessage)
}
console.log('🔧 NostrclientHub: Resubscribed to', this.subscriptions.size, 'subscriptions')
}
/**
* Schedule automatic reconnection
*/
private scheduleReconnect(): void {
if (this.reconnectInterval) {
clearTimeout(this.reconnectInterval)
}
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
console.log(`🔧 NostrclientHub: Scheduling reconnection in ${delay}ms`)
this.reconnectInterval = setTimeout(async () => {
await this.connect()
}, delay) as unknown as number
}
}
// Export singleton instance
export const nostrclientHub = new NostrclientHub({
url: import.meta.env.VITE_NOSTRCLIENT_URL || 'wss://localhost:5000/nostrclient/api/v1'
})
// Ensure global export
;(globalThis as any).nostrclientHub = nostrclientHub

View file

@ -0,0 +1,524 @@
import { SimplePool, type Filter, type Event, type Relay } from 'nostr-tools'
// Simple EventEmitter implementation for browser compatibility
class EventEmitter {
private events: { [key: string]: Function[] } = {}
on(event: string, listener: Function): void {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(listener)
}
off(event: string, listener: Function): void {
if (!this.events[event]) return
const index = this.events[event].indexOf(listener)
if (index > -1) {
this.events[event].splice(index, 1)
}
}
emit(event: string, ...args: any[]): void {
if (!this.events[event]) return
this.events[event].forEach(listener => listener(...args))
}
removeAllListeners(event?: string): void {
if (event) {
delete this.events[event]
} else {
this.events = {}
}
}
}
export interface RelayConfig {
url: string
read: boolean
write: boolean
priority?: number // Lower number = higher priority
}
export interface SubscriptionConfig {
id: string
filters: Filter[]
relays?: string[] // If not specified, uses all connected relays
onEvent?: (event: Event) => void
onEose?: () => void
onClose?: () => void
}
export interface RelayStatus {
url: string
connected: boolean
lastSeen: number
error?: string
latency?: number
}
export class RelayHub extends EventEmitter {
private pool: SimplePool
private relayConfigs: Map<string, RelayConfig> = new Map()
private connectedRelays: Map<string, Relay> = new Map()
private subscriptions: Map<string, any> = new Map()
public isInitialized = false
private reconnectInterval?: number
private healthCheckInterval?: number
private mobileVisibilityHandler?: () => void
// Connection state
private _isConnected = false
private _connectionAttempts = 0
private readonly maxReconnectAttempts = 5
private readonly reconnectDelay = 5000 // 5 seconds
private readonly healthCheckIntervalMs = 30000 // 30 seconds
constructor() {
super()
this.pool = new SimplePool()
this.setupMobileVisibilityHandling()
}
get isConnected(): boolean {
return this._isConnected
}
get connectedRelayCount(): number {
// Return the actual size of connectedRelays map
return this.connectedRelays.size
}
get totalRelayCount(): number {
return this.relayConfigs.size
}
get totalSubscriptionCount(): number {
return this.subscriptions.size
}
get subscriptionDetails(): Array<{ id: string; filters: any[]; relays?: string[] }> {
return Array.from(this.subscriptions.entries()).map(([id, subscription]) => {
// Try to extract subscription details if available
return {
id,
filters: subscription.filters || [],
relays: subscription.relays || []
}
})
}
get relayStatuses(): RelayStatus[] {
return Array.from(this.relayConfigs.values()).map(config => {
const relay = this.connectedRelays.get(config.url)
return {
url: config.url,
connected: !!relay,
lastSeen: relay ? Date.now() : 0,
error: relay ? undefined : 'Not connected',
latency: relay ? 0 : undefined // TODO: Implement actual latency measurement
}
})
}
/**
* Initialize the relay hub with relay configurations
*/
async initialize(relayUrls: string[]): Promise<void> {
if (this.isInitialized) {
console.warn('RelayHub already initialized')
return
}
console.log('🔧 RelayHub: Initializing with URLs:', relayUrls)
// Convert URLs to relay configs
this.relayConfigs.clear()
relayUrls.forEach((url, index) => {
this.relayConfigs.set(url, {
url,
read: true,
write: true,
priority: index
})
})
console.log('🔧 RelayHub: Relay configs created:', Array.from(this.relayConfigs.values()))
// Start connection management
console.log('🔧 RelayHub: Starting connection...')
await this.connect()
this.startHealthCheck()
this.isInitialized = true
console.log('🔧 RelayHub: Initialization complete')
}
/**
* Connect to all configured relays
*/
async connect(): Promise<void> {
if (this.relayConfigs.size === 0) {
throw new Error('No relay configurations found. Call initialize() first.')
}
console.log('🔧 RelayHub: Connecting to', this.relayConfigs.size, 'relays')
try {
this._connectionAttempts++
console.log('🔧 RelayHub: Connection attempt', this._connectionAttempts)
// Connect to relays in priority order
const sortedRelays = Array.from(this.relayConfigs.values())
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
console.log('🔧 RelayHub: Attempting connections to:', sortedRelays.map(r => r.url))
const connectionPromises = sortedRelays.map(async (config) => {
try {
console.log('🔧 RelayHub: Connecting to relay:', config.url)
const relay = await this.pool.ensureRelay(config.url)
this.connectedRelays.set(config.url, relay)
console.log('🔧 RelayHub: Successfully connected to:', config.url)
return { url: config.url, success: true }
} catch (error) {
console.error(`🔧 RelayHub: Failed to connect to relay ${config.url}:`, error)
return { url: config.url, success: false, error }
}
})
const results = await Promise.allSettled(connectionPromises)
const successfulConnections = results.filter(
result => result.status === 'fulfilled' && result.value.success
)
console.log('🔧 RelayHub: Connection results:', {
total: results.length,
successful: successfulConnections.length,
failed: results.length - successfulConnections.length
})
if (successfulConnections.length > 0) {
this._isConnected = true
this._connectionAttempts = 0
console.log('🔧 RelayHub: Connection successful, connected to', successfulConnections.length, 'relays')
this.emit('connected', successfulConnections.length)
} else {
console.error('🔧 RelayHub: Failed to connect to any relay')
throw new Error('Failed to connect to any relay')
}
} catch (error) {
this._isConnected = false
console.error('🔧 RelayHub: Connection failed with error:', error)
this.emit('connectionError', error)
// Schedule reconnection if we haven't exceeded max attempts
if (this._connectionAttempts < this.maxReconnectAttempts) {
console.log('🔧 RelayHub: Scheduling reconnection attempt', this._connectionAttempts + 1)
this.scheduleReconnect()
} else {
this.emit('maxReconnectionAttemptsReached')
console.error('🔧 RelayHub: Max reconnection attempts reached')
}
}
}
/**
* Disconnect from all relays
*/
disconnect(): void {
// Clear intervals
if (this.reconnectInterval) {
clearTimeout(this.reconnectInterval)
this.reconnectInterval = undefined
}
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval)
this.healthCheckInterval = undefined
}
// Close all subscriptions
this.subscriptions.forEach(sub => sub.close())
this.subscriptions.clear()
// Close all relay connections
this.pool.close(Array.from(this.relayConfigs.keys()))
this.connectedRelays.clear()
this._isConnected = false
this.emit('disconnected')
}
/**
* Subscribe to events from relays
*/
subscribe(config: SubscriptionConfig): () => void {
if (!this.isInitialized) {
throw new Error('RelayHub not initialized. Call initialize() first.')
}
if (!this._isConnected) {
throw new Error('Not connected to any relays')
}
// Determine which relays to use
const targetRelays = config.relays || Array.from(this.connectedRelays.keys())
const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
if (availableRelays.length === 0) {
throw new Error('No available relays for subscription')
}
// Create subscription using the pool
const subscription = this.pool.subscribeMany(availableRelays, config.filters, {
onevent: (event: Event) => {
config.onEvent?.(event)
this.emit('event', { subscriptionId: config.id, event, relay: 'unknown' })
},
oneose: () => {
config.onEose?.()
this.emit('eose', { subscriptionId: config.id })
}
})
// Store subscription for cleanup
this.subscriptions.set(config.id, subscription)
// Emit subscription created event
this.emit('subscriptionCreated', { id: config.id, count: this.subscriptions.size })
// Return unsubscribe function
return () => {
this.unsubscribe(config.id)
}
}
/**
* Unsubscribe from a specific subscription
*/
unsubscribe(subscriptionId: string): void {
const subscription = this.subscriptions.get(subscriptionId)
if (subscription) {
subscription.close()
this.subscriptions.delete(subscriptionId)
// Emit subscription removed event
this.emit('subscriptionRemoved', { id: subscriptionId, count: this.subscriptions.size })
}
}
/**
* Publish an event to all connected relays
*/
async publishEvent(event: Event): Promise<{ success: number; total: number }> {
if (!this._isConnected) {
throw new Error('Not connected to any relays')
}
const relayUrls = Array.from(this.connectedRelays.keys())
const results = await Promise.allSettled(
relayUrls.map(relay => this.pool.publish([relay], event))
)
const successful = results.filter(result => result.status === 'fulfilled').length
const total = results.length
this.emit('eventPublished', { eventId: event.id, success: successful, total })
return { success: successful, total }
}
/**
* Query events from relays (one-time fetch)
*/
async queryEvents(filters: Filter[], relays?: string[]): Promise<Event[]> {
if (!this._isConnected) {
throw new Error('Not connected to any relays')
}
const targetRelays = relays || Array.from(this.connectedRelays.keys())
const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
if (availableRelays.length === 0) {
throw new Error('No available relays for query')
}
try {
// Query each filter separately and combine results
const allEvents: Event[] = []
for (const filter of filters) {
const events = await this.pool.querySync(availableRelays, filter)
allEvents.push(...events)
}
return allEvents
} catch (error) {
console.error('Query failed:', error)
throw error
}
}
/**
* Get a specific relay instance
*/
getRelay(url: string): Relay | undefined {
return this.connectedRelays.get(url)
}
/**
* Check if a specific relay is connected
*/
isRelayConnected(url: string): boolean {
return this.connectedRelays.has(url)
}
/**
* Force reconnection to all relays
*/
async reconnect(): Promise<void> {
this.disconnect()
await this.connect()
}
/**
* Schedule automatic reconnection
*/
private scheduleReconnect(): void {
if (this.reconnectInterval) {
clearTimeout(this.reconnectInterval)
}
this.reconnectInterval = setTimeout(async () => {
await this.connect()
}, this.reconnectDelay) as unknown as number
}
/**
* Start health check monitoring
*/
private startHealthCheck(): void {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval)
}
this.healthCheckInterval = setInterval(() => {
this.performHealthCheck()
}, this.healthCheckIntervalMs) as unknown as number
}
/**
* Perform health check on all relays
*/
private async performHealthCheck(): Promise<void> {
if (!this._isConnected) return
const disconnectedRelays: string[] = []
// Check each relay connection
for (const [url] of this.connectedRelays) {
try {
// Try to send a ping or check connection status
// For now, we'll just check if the relay is still in our connected relays map
if (!this.connectedRelays.has(url)) {
disconnectedRelays.push(url)
}
} catch (error) {
console.warn(`Health check failed for relay ${url}:`, error)
disconnectedRelays.push(url)
}
}
// Remove disconnected relays
disconnectedRelays.forEach(url => {
this.connectedRelays.delete(url)
})
// Update connection status
if (this.connectedRelays.size === 0) {
this._isConnected = false
this.emit('allRelaysDisconnected')
console.warn('All relays disconnected, attempting reconnection...')
await this.connect()
} else if (this.connectedRelays.size < this.relayConfigs.size) {
this.emit('partialDisconnection', {
connected: this.connectedRelays.size,
total: this.relayConfigs.size
})
}
}
/**
* Setup mobile visibility handling for better WebSocket management
*/
private setupMobileVisibilityHandling(): void {
// Handle page visibility changes (mobile app backgrounding)
if (typeof document !== 'undefined') {
this.mobileVisibilityHandler = () => {
if (document.hidden) {
// Keep connections alive but reduce activity
} else {
console.log('Page visible, resuming normal WebSocket activity')
// Resume normal activity and check connections
this.performHealthCheck()
}
}
document.addEventListener('visibilitychange', this.mobileVisibilityHandler)
}
// Handle online/offline events
if (typeof window !== 'undefined') {
window.addEventListener('online', () => {
this.performHealthCheck()
})
window.addEventListener('offline', () => {
console.log('Network offline, marking as disconnected...')
this._isConnected = false
this.emit('networkOffline')
})
}
}
/**
* Cleanup resources
*/
destroy(): void {
// Remove event listeners
if (this.mobileVisibilityHandler && typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', this.mobileVisibilityHandler)
}
if (typeof window !== 'undefined') {
window.removeEventListener('online', () => {})
window.removeEventListener('offline', () => {})
}
// Disconnect and cleanup
this.disconnect()
this.removeAllListeners()
this.isInitialized = false
}
}
// Export singleton instance
export const relayHub = new RelayHub()
// Ensure global export
;(globalThis as any).relayHub = relayHub

View file

@ -0,0 +1,45 @@
// PWA service for base module
export class PWAService {
private deferredPrompt: any = null
async initialize(): Promise<void> {
console.log('📱 Initializing PWA service...')
// Listen for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (e) => {
console.log('PWA install prompt available')
e.preventDefault()
this.deferredPrompt = e
})
// Listen for app installed event
window.addEventListener('appinstalled', () => {
console.log('PWA was installed')
this.deferredPrompt = null
})
}
canInstall(): boolean {
return this.deferredPrompt !== null
}
async install(): Promise<boolean> {
if (!this.deferredPrompt) {
return false
}
this.deferredPrompt.prompt()
const result = await this.deferredPrompt.userChoice
if (result.outcome === 'accepted') {
console.log('User accepted PWA install')
} else {
console.log('User dismissed PWA install')
}
this.deferredPrompt = null
return result.outcome === 'accepted'
}
}
export const pwaService = new PWAService()