@ -39,6 +39,9 @@ class NostrClient {
@@ -39,6 +39,9 @@ class NostrClient {
private readonly MAX_CONCURRENT_TOTAL = 3 ; // Max 3 total concurrent requests
private totalActiveRequests = 0 ;
// Track background refresh operations to prevent duplicates
private backgroundRefreshes : Set < string > = new Set ( ) ;
// Failed relay tracking with exponential backoff
private failedRelays : Map < string , { lastFailure : number ; retryAfter : number ; failureCount : number } > = new Map ( ) ;
private readonly INITIAL_RETRY_DELAY = 5000 ; // 5 seconds
@ -230,32 +233,32 @@ class NostrClient {
@@ -230,32 +233,32 @@ class NostrClient {
// Handle both uppercase and lowercase tag filters (Nostr spec allows both)
// Ignore relays that don't support the tag filter, they need to be corrected by the relay operator.
if ( filter [ '#e' ] && filter [ '#e' ] . length > 0 ) {
const eventTags = event . tags . filter ( t = > t [ 0 ] === 'e' || t [ 0 ] === 'E' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#e' ] . some ( id = > eventTags . includes ( id ) ) ) continue ;
const eventTags = event . tags . filter ( t = > ( t [ 0 ] === 'e' || t [ 0 ] === 'E' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( eventTags . length === 0 || ! filter [ '#e' ] . some ( id = > eventTags . includes ( id ) ) ) continue ;
}
if ( filter [ '#E' ] && filter [ '#E' ] . length > 0 ) {
const eventTags = event . tags . filter ( t = > t [ 0 ] === 'e' || t [ 0 ] === 'E' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#E' ] . some ( id = > eventTags . includes ( id ) ) ) continue ;
const eventTags = event . tags . filter ( t = > ( t [ 0 ] === 'e' || t [ 0 ] === 'E' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( eventTags . length === 0 || ! filter [ '#E' ] . some ( id = > eventTags . includes ( id ) ) ) continue ;
}
if ( filter [ '#p' ] && filter [ '#p' ] . length > 0 ) {
const pubkeyTags = event . tags . filter ( t = > t [ 0 ] === 'p' || t [ 0 ] === 'P' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#p' ] . some ( pk = > pubkeyTags . includes ( pk ) ) ) continue ;
const pubkeyTags = event . tags . filter ( t = > ( t [ 0 ] === 'p' || t [ 0 ] === 'P' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( pubkeyTags . length === 0 || ! filter [ '#p' ] . some ( pk = > pubkeyTags . includes ( pk ) ) ) continue ;
}
if ( filter [ '#P' ] && filter [ '#P' ] . length > 0 ) {
const pubkeyTags = event . tags . filter ( t = > t [ 0 ] === 'p' || t [ 0 ] === 'P' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#P' ] . some ( pk = > pubkeyTags . includes ( pk ) ) ) continue ;
const pubkeyTags = event . tags . filter ( t = > ( t [ 0 ] === 'p' || t [ 0 ] === 'P' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( pubkeyTags . length === 0 || ! filter [ '#P' ] . some ( pk = > pubkeyTags . includes ( pk ) ) ) continue ;
}
if ( filter [ '#a' ] && filter [ '#a' ] . length > 0 ) {
const aTags = event . tags . filter ( t = > t [ 0 ] === 'a' || t [ 0 ] === 'A' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#a' ] . some ( a = > aTags . includes ( a ) ) ) continue ;
const aTags = event . tags . filter ( t = > ( t [ 0 ] === 'a' || t [ 0 ] === 'A' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( aTags . length === 0 || ! filter [ '#a' ] . some ( a = > aTags . includes ( a ) ) ) continue ;
}
if ( filter [ '#A' ] && filter [ '#A' ] . length > 0 ) {
const aTags = event . tags . filter ( t = > t [ 0 ] === 'a' || t [ 0 ] === 'A' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#A' ] . some ( a = > aTags . includes ( a ) ) ) continue ;
const aTags = event . tags . filter ( t = > ( t [ 0 ] === 'a' || t [ 0 ] === 'A' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( aTags . length === 0 || ! filter [ '#A' ] . some ( a = > aTags . includes ( a ) ) ) continue ;
}
if ( filter [ '#d' ] && filter [ '#d' ] . length > 0 ) {
const dTags = event . tags . filter ( t = > t [ 0 ] === 'd' || t [ 0 ] === 'D' ) . map ( t = > t [ 1 ] ) ;
if ( ! filter [ '#d' ] . some ( d = > dTags . includes ( d ) ) ) continue ;
const dTags = event . tags . filter ( t = > ( t [ 0 ] === 'd' || t [ 0 ] === 'D' ) && t [ 1 ] ) . map ( t = > t [ 1 ] ) ;
if ( dTags . length === 0 || ! filter [ '#d' ] . some ( d = > dTags . includes ( d ) ) ) continue ;
}
// Use matchFilter for final validation
@ -568,19 +571,26 @@ class NostrClient {
@@ -568,19 +571,26 @@ class NostrClient {
// Return cached immediately, fetch fresh in background with delay
// Don't pass onUpdate to background fetch to avoid interfering with cached results
if ( cacheResults ) {
// Use a longer delay for background refresh to avoid interfering with initial load
setTimeout ( ( ) = > {
const bgKey = ` ${ fetchKey } _bg_ ${ Date . now ( ) } ` ;
// Only update cache, don't call onUpdate for background refresh
const bgPromise = this . fetchFromRelays ( filters , relays , { cacheResults , onUpdate : undefined , timeout } ) ;
this . activeFetches . set ( bgKey , bgPromise ) ;
bgPromise . finally ( ( ) = > {
this . activeFetches . delete ( bgKey ) ;
} ) . catch ( ( error ) = > {
// Log but don't throw - background refresh failures shouldn't affect cached results
console . debug ( '[nostr-client] Background refresh failed:' , error ) ;
} ) ;
} , 5000 ) ; // 5 second delay for background refresh to avoid interfering
// Prevent duplicate background refreshes for the same filter
if ( ! this . backgroundRefreshes . has ( fetchKey ) ) {
this . backgroundRefreshes . add ( fetchKey ) ;
// Use a longer delay for background refresh to avoid interfering with initial load
setTimeout ( ( ) = > {
// Only update cache, don't call onUpdate for background refresh
// This ensures cached events persist and are not cleared by background refresh
const bgPromise = this . fetchFromRelays ( filters , relays , { cacheResults : true , onUpdate : undefined , timeout } ) ;
bgPromise . finally ( ( ) = > {
// Remove from background refreshes set after a delay to allow re-refresh if needed
setTimeout ( ( ) = > {
this . backgroundRefreshes . delete ( fetchKey ) ;
} , 60000 ) ; // Allow re-refresh after 60 seconds
} ) . catch ( ( error ) = > {
// Log but don't throw - background refresh failures shouldn't affect cached results
console . debug ( '[nostr-client] Background refresh failed:' , error ) ;
this . backgroundRefreshes . delete ( fetchKey ) ;
} ) ;
} , 5000 ) ; // 5 second delay for background refresh to avoid interfering
}
}
return cachedEvents ;
} else {