fix(tests): Updating unit tests

This commit is contained in:
Chris Troutner
2025-11-09 19:39:21 -08:00
parent 78197762e4
commit 6096d52f84
16 changed files with 559 additions and 1738 deletions
-1
View File
@@ -20,7 +20,6 @@ class FullNodeRPCAdapter {
rpcPassword,
rpcTimeoutMs = 15000
} = this.config.fullNode
console.log('this.config.fullNode', this.config.fullNode)
this.requestIdPrefix = this.config.fullNode.rpcRequestIdPrefix || 'psf-bch-api'
-6
View File
@@ -5,9 +5,6 @@
*/
// Local libraries
// import PublishEventUseCase from './publish-event.js'
// import QueryEventsUseCase from './query-events.js'
// import ManageSubscriptionUseCase from './manage-subscription.js'
import BlockchainUseCases from './full-node-blockchain-use-cases.js'
class UseCases {
@@ -19,9 +16,6 @@ class UseCases {
)
}
// this.publishEvent = new PublishEventUseCase({ adapters: this.adapters })
// this.queryEvents = new QueryEventsUseCase({ adapters: this.adapters })
// this.manageSubscription = new ManageSubscriptionUseCase({ adapters: this.adapters })
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
}
-216
View File
@@ -1,216 +0,0 @@
/*
Use case: Manage subscriptions for Server-Sent Events (SSE).
This encapsulates the business logic for creating and managing subscriptions.
*/
import wlogger from '../adapters/wlogger.js'
class ManageSubscriptionUseCase {
constructor (localConfig = {}) {
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error('Adapters instance required')
}
if (!this.adapters.nostrRelays || !Array.isArray(this.adapters.nostrRelays) || this.adapters.nostrRelays.length === 0) {
throw new Error('NostrRelay adapters array required')
}
// Map subscriptionId to { relaySubscriptions: Map<relayIndex, subscriptionId>, handlers, seenEventIds }
this.activeSubscriptions = new Map()
}
/**
* Create a subscription for SSE streaming across all relays
* @param {string} subscriptionId - Unique subscription ID
* @param {Array} filters - Array of filter objects
* @param {Function} onEvent - Callback for events
* @param {Function} onEose - Callback for EOSE
* @param {Function} onClosed - Callback for CLOSED
* @returns {Promise<void>}
*/
async createSubscription (subscriptionId, filters, onEvent, onEose, onClosed) {
try {
if (this.activeSubscriptions.has(subscriptionId)) {
throw new Error(`Subscription ${subscriptionId} already exists`)
}
wlogger.info(`Creating subscription ${subscriptionId} across ${this.adapters.nostrRelays.length} relay(s)`)
// Track seen event IDs to de-duplicate across relays
const seenEventIds = new Set()
// Track EOSE and CLOSED status per relay
const relayStatuses = this.adapters.nostrRelays.map(() => ({
eoseReceived: false,
closedReceived: false
}))
// Create unified handlers that merge events from all relays
const handlers = {
onEvent: (event) => {
// De-duplicate events by ID across all relays
if (event && event.id && !seenEventIds.has(event.id)) {
seenEventIds.add(event.id)
if (onEvent) {
onEvent(event)
}
}
},
onEose: () => {
// Call onEose only once when all relays have sent EOSE
// This is called from the per-relay handler only when all relays have EOSE
if (onEose) {
onEose()
}
},
onClosed: (message) => {
if (onClosed) {
onClosed(message)
}
// Clean up subscription if any relay closes it
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
clearTimeout(subscriptionInfo.eoseTimeoutId)
}
this.activeSubscriptions.delete(subscriptionId)
}
}
// Create subscription per relay with unique subscription IDs
const relaySubscriptions = new Map()
const subscriptionPromises = this.adapters.nostrRelays.map(async (relay, index) => {
const relaySubscriptionId = `${subscriptionId}-relay-${index}`
relaySubscriptions.set(index, relaySubscriptionId)
// Create per-relay handlers that update shared state
const relayHandlers = {
onEvent: (event) => {
handlers.onEvent(event)
},
onEose: () => {
relayStatuses[index].eoseReceived = true
// Check if all relays have sent EOSE
if (relayStatuses.every(s => s.eoseReceived)) {
// Clear the timeout since we got EOSE from all relays
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
clearTimeout(subscriptionInfo.eoseTimeoutId)
subscriptionInfo.eoseTimeoutId = null
}
handlers.onEose()
}
},
onClosed: (message) => {
relayStatuses[index].closedReceived = true
handlers.onClosed(message)
}
}
await relay.sendReq(relaySubscriptionId, filters, relayHandlers)
})
// Store subscription info
this.activeSubscriptions.set(subscriptionId, {
relaySubscriptions,
handlers,
seenEventIds,
relayStatuses,
eoseTimeoutId: null
})
// Subscribe to all relays concurrently
const results = await Promise.allSettled(subscriptionPromises)
// Check if any relay subscription failed and clean up if so
const hasFailures = results.some(result => result.status === 'rejected')
if (hasFailures) {
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
clearTimeout(subscriptionInfo.eoseTimeoutId)
}
this.activeSubscriptions.delete(subscriptionId)
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason)
throw new Error(`Failed to create subscription on some relays: ${errors.map(e => e.message).join(', ')}`)
}
// Set up EOSE timeout fallback - if not all relays send EOSE within 10 seconds, call onEose anyway
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
const EOSE_TIMEOUT_MS = 10000 // 10 seconds
subscriptionInfo.eoseTimeoutId = setTimeout(() => {
// Check if subscription still exists and if all relays have sent EOSE
if (this.activeSubscriptions.has(subscriptionId)) {
const currentInfo = this.activeSubscriptions.get(subscriptionId)
const allEoseReceived = currentInfo.relayStatuses.every(s => s.eoseReceived)
if (!allEoseReceived) {
wlogger.warn(`EOSE timeout reached for subscription ${subscriptionId} - calling onEose callback anyway`)
if (handlers.onEose) {
handlers.onEose()
}
}
}
}, EOSE_TIMEOUT_MS)
} catch (err) {
wlogger.error('Error creating subscription:', err)
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
clearTimeout(subscriptionInfo.eoseTimeoutId)
}
this.activeSubscriptions.delete(subscriptionId)
throw err
}
}
/**
* Close a subscription across all relays
* @param {string} subscriptionId - Subscription ID to close
* @returns {Promise<void>}
*/
async closeSubscription (subscriptionId) {
try {
if (!this.activeSubscriptions.has(subscriptionId)) {
// Subscription doesn't exist - already closed, treat as success (idempotent)
wlogger.info(`Subscription ${subscriptionId} already closed or does not exist`)
return
}
wlogger.info(`Closing subscription ${subscriptionId} across all relays`)
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
const { relaySubscriptions } = subscriptionInfo
// Clear EOSE timeout if it exists
if (subscriptionInfo.eoseTimeoutId) {
clearTimeout(subscriptionInfo.eoseTimeoutId)
}
// Close subscriptions on all relays concurrently
const closePromises = Array.from(relaySubscriptions.entries()).map(async ([relayIndex, relaySubscriptionId]) => {
try {
await this.adapters.nostrRelays[relayIndex].sendClose(relaySubscriptionId)
} catch (err) {
wlogger.warn(`Error closing subscription on relay ${relayIndex}:`, err.message)
}
})
await Promise.allSettled(closePromises)
this.activeSubscriptions.delete(subscriptionId)
} catch (err) {
wlogger.error('Error closing subscription:', err)
// Clean up even if there's an error
this.activeSubscriptions.delete(subscriptionId)
throw err
}
}
/**
* Check if a subscription exists
* @param {string} subscriptionId - Subscription ID
* @returns {boolean}
*/
hasSubscription (subscriptionId) {
return this.activeSubscriptions.has(subscriptionId)
}
}
export default ManageSubscriptionUseCase
-87
View File
@@ -1,87 +0,0 @@
/*
Use case: Publish a Nostr event to the relay.
This encapsulates the business logic for publishing events.
*/
import Event from '../entities/event.js'
import wlogger from '../adapters/wlogger.js'
class PublishEventUseCase {
constructor (localConfig = {}) {
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error('Adapters instance required')
}
if (!this.adapters.nostrRelays || !Array.isArray(this.adapters.nostrRelays) || this.adapters.nostrRelays.length === 0) {
throw new Error('NostrRelay adapters array required')
}
}
/**
* Publish an event to all Nostr relays (broadcast)
* @param {Object} eventData - Event data (must be signed)
* @returns {Promise<Object>} Result with accepted status, message, and relay results
*/
async execute (eventData) {
try {
// Create event entity
const event = new Event(eventData)
// Validate event
if (!event.isValid()) {
throw new Error('Invalid event structure')
}
wlogger.info(`Publishing event ${event.id} (kind ${event.kind}) to ${this.adapters.nostrRelays.length} relay(s)`)
// Broadcast event to all relays
const results = await this.adapters.broadcastEvent(event.toJSON())
// Aggregate results
const acceptedRelays = results.filter(r => r.accepted)
const rejectedRelays = results.filter(r => !r.accepted)
const failedRelays = results.filter(r => !r.success)
const atLeastOneAccepted = acceptedRelays.length > 0
const allAccepted = acceptedRelays.length === results.length && failedRelays.length === 0
// Build aggregated message
let message = ''
if (allAccepted) {
message = `Accepted by all ${acceptedRelays.length} relay(s)`
} else if (atLeastOneAccepted) {
message = `Accepted by ${acceptedRelays.length}/${results.length} relay(s)`
if (rejectedRelays.length > 0) {
message += `, rejected by ${rejectedRelays.length} relay(s)`
}
if (failedRelays.length > 0) {
message += `, failed to reach ${failedRelays.length} relay(s)`
}
} else {
message = `Rejected or failed by all ${results.length} relay(s)`
if (rejectedRelays.length > 0) {
const rejectionMessages = rejectedRelays.map(r => r.message).filter(m => m).join('; ')
if (rejectionMessages) {
message += `: ${rejectionMessages}`
}
}
}
wlogger.info(`Event ${event.id} ${atLeastOneAccepted ? 'accepted' : 'rejected/failed'}: ${message}`)
return {
accepted: atLeastOneAccepted,
message,
eventId: event.id,
relayResults: results,
acceptedCount: acceptedRelays.length,
totalRelays: results.length
}
} catch (err) {
wlogger.error('Error in PublishEventUseCase:', err)
throw err
}
}
}
export default PublishEventUseCase
-41
View File
@@ -1,41 +0,0 @@
/*
Use case: Query events from the relay (stateless).
This encapsulates the business logic for querying events.
*/
import wlogger from '../adapters/wlogger.js'
class QueryEventsUseCase {
constructor (localConfig = {}) {
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error('Adapters instance required')
}
if (!this.adapters.nostrRelays || !Array.isArray(this.adapters.nostrRelays) || this.adapters.nostrRelays.length === 0) {
throw new Error('NostrRelay adapters array required')
}
}
/**
* Query events with filters from all relays (stateless - returns immediately)
* @param {Array} filters - Array of filter objects
* @param {string} subscriptionId - Unique subscription ID
* @returns {Promise<Array>} Array of events (merged and de-duplicated from all relays)
*/
async execute (filters, subscriptionId) {
try {
wlogger.info(`Querying events with subscription ${subscriptionId} from ${this.adapters.nostrRelays.length} relay(s)`)
// Query all relays concurrently and merge results
const events = await this.adapters.queryAllRelays(filters, subscriptionId)
wlogger.info(`Query returned ${events.length} events from ${this.adapters.nostrRelays.length} relay(s)`)
return events
} catch (err) {
wlogger.error('Error in QueryEventsUseCase:', err)
throw err
}
}
}
export default QueryEventsUseCase