feat: smart relay pool

This commit is contained in:
codytseng 2026-01-05 18:20:32 +08:00
parent d1b3a8c4c7
commit 695f2fe017
4 changed files with 59 additions and 11 deletions

48
src/lib/smart-pool.ts Normal file
View file

@ -0,0 +1,48 @@
import { SimplePool } from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
const DEFAULT_CONNECTION_TIMEOUT = 10 * 1000 // 10 seconds
const CLEANUP_THRESHOLD = 15 // number of relays to trigger cleanup
const CLEANUP_INTERVAL = 5 * 1000 // 5 seconds
const IDLE_TIMEOUT = 10 * 1000 // 10 seconds
export class SmartPool extends SimplePool {
private relayIdleTracker = new Map<string, number>()
constructor() {
super({ enablePing: true, enableReconnect: true })
// Periodically clean up idle relays
setInterval(() => this.cleanIdleRelays(), CLEANUP_INTERVAL)
}
ensureRelay(url: string): Promise<AbstractRelay> {
// If relay is new and we have many relays, trigger cleanup
if (!this.relayIdleTracker.has(url) && this.relayIdleTracker.size > CLEANUP_THRESHOLD) {
this.cleanIdleRelays()
}
// Update last activity time
this.relayIdleTracker.set(url, Date.now())
return super.ensureRelay(url, { connectionTimeout: DEFAULT_CONNECTION_TIMEOUT })
}
private cleanIdleRelays() {
const idleRelays: string[] = []
this.relays.forEach((relay, url) => {
// If relay is disconnected or has active subscriptions, skip
if (!relay.connected || relay.openSubs.size > 0) return
const lastActivity = this.relayIdleTracker.get(url) ?? 0
// If relay active recently, skip
if (Date.now() - lastActivity < IDLE_TIMEOUT) return
idleRelays.push(url)
this.relayIdleTracker.delete(url)
})
if (idleRelays.length > 0) {
console.log('[SmartPool] Closing idle relays:', idleRelays)
this.close(idleRelays)
}
}
}

View file

@ -8,6 +8,7 @@ import {
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { filterOutBigRelays, getDefaultRelayUrls } from '@/lib/relay'
import { SmartPool } from '@/lib/smart-pool'
import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag'
import { mergeTimelines } from '@/lib/timeline'
import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url'
@ -25,7 +26,6 @@ import {
matchFilters,
Event as NEvent,
nip19,
SimplePool,
VerifiedEvent
} from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
@ -40,7 +40,7 @@ class ClientService extends EventTarget {
signer?: ISigner
pubkey?: string
currentRelays: string[] = []
private pool: SimplePool
private pool: SmartPool
private externalSeenOn = new Map<string, Set<string>>()
private timelines: Record<
@ -70,7 +70,7 @@ class ClientService extends EventTarget {
constructor() {
super()
this.pool = new SimplePool()
this.pool = new SmartPool()
this.pool.trackRelays = true
}
@ -200,7 +200,7 @@ class ClientService extends EventTarget {
uniqueRelayUrls.map(async (url) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
const relay = await this.pool.ensureRelay(url, { connectionTimeout: 5_000 }).catch(() => {
const relay = await this.pool.ensureRelay(url).catch(() => {
return undefined
})
if (!relay) {
@ -436,7 +436,7 @@ class ClientService extends EventTarget {
subPromises.push(startSub())
async function startSub() {
const relay = await that.pool.ensureRelay(url, { connectionTimeout: 5_000 }).catch(() => {
const relay = await that.pool.ensureRelay(url).catch(() => {
return undefined
})
// cannot connect to relay