You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

152 lines
4.7 KiB

import { RELAY_POOL_IDLE_SWEEP_INTERVAL_MS, RELAY_POOL_SOCKET_IDLE_MS } from '@/constants'
import { isMetadataPolicyProfileRelay } from '@/lib/metadata-policy-curated-relays'
import { isRelayUrlInViewerMetadataLists } from '@/lib/read-only-relay-personal'
import logger from '@/lib/logger'
import { canonicalRelaySessionKey, normalizeAnyRelayUrl } from '@/lib/url'
import type { SimplePool } from 'nostr-tools'
type HasActiveSubsFn = (canonicalRelayKey: string) => boolean
let pool: SimplePool | null = null
let hasActiveSubs: HasActiveSubsFn | null = null
let sweepTimer: ReturnType<typeof setInterval> | null = null
const lastActivityMs = new Map<string, number>()
function canon(url: string): string {
return canonicalRelaySessionKey(normalizeAnyRelayUrl(url) || url.trim())
}
function shouldKeepProfileRelaySocketOpen(url: string): boolean {
return isMetadataPolicyProfileRelay(url)
}
/** Mark relay URL as recently used (connect, REQ, publish). */
export function touchRelayPoolActivity(url: string): void {
const key = canon(url)
if (!key) return
lastActivityMs.set(key, Date.now())
}
/**
* Wire idle socket sweeps to the app pool. Safe to call once from {@link ClientService} constructor.
*/
export function initRelayPoolIdle(nextPool: SimplePool, nextHasActiveSubs: HasActiveSubsFn): void {
pool = nextPool
hasActiveSubs = nextHasActiveSubs
if (sweepTimer != null) return
sweepTimer = setInterval(() => {
sweepIdleRelayPoolSockets()
}, RELAY_POOL_IDLE_SWEEP_INTERVAL_MS)
}
/** Close connected sockets that have no active SUBs and exceeded {@link RELAY_POOL_SOCKET_IDLE_MS}. */
export function sweepIdleRelayPoolSockets(): void {
if (!pool || !hasActiveSubs) return
const now = Date.now()
let status: Map<string, boolean>
try {
status = pool.listConnectionStatus()
} catch {
return
}
const toClose: string[] = []
for (const [url, connected] of status) {
if (!connected) continue
const key = canon(url)
if (!key) continue
if (shouldKeepProfileRelaySocketOpen(url)) continue
if (hasActiveSubs(key)) continue
const last = lastActivityMs.get(key) ?? 0
if (now - last < RELAY_POOL_SOCKET_IDLE_MS) continue
toClose.push(normalizeAnyRelayUrl(url) || url)
}
if (toClose.length === 0) return
try {
pool.close(toClose)
logger.debug('[RelayPoolIdle] closed idle sockets', { count: toClose.length, relays: toClose })
} catch {
/* ignore */
}
for (const url of toClose) {
lastActivityMs.delete(canon(url))
}
}
/** Close specific relays when they are connected, have no active SUBs, and are not session-parked hot. */
export function closeRelayPoolSocketsIfIdle(urls: readonly string[]): void {
if (!pool || !hasActiveSubs || urls.length === 0) return
let status: Map<string, boolean>
try {
status = pool.listConnectionStatus()
} catch {
return
}
const toClose: string[] = []
for (const raw of urls) {
const key = canon(raw)
if (!key || hasActiveSubs(key)) continue
if (shouldKeepProfileRelaySocketOpen(raw)) continue
const normalized = normalizeAnyRelayUrl(raw) || raw
const connected = [...status.entries()].some(
([u, ok]) => ok && canon(u) === key
)
if (connected) toClose.push(normalized)
}
if (toClose.length === 0) return
try {
pool.close(toClose)
logger.debug('[RelayPoolIdle] closed sockets after slow-park', { relays: toClose })
} catch {
/* ignore */
}
}
/**
* After publish: drop sockets opened for author outboxes, random NIP-66 picks, and other non-personal
* targets. Keeps profile index relays and the viewer's own list relays connected.
*/
export function closePublishTransientRelaySockets(urls: readonly string[]): void {
if (!pool || !hasActiveSubs || urls.length === 0) return
let status: Map<string, boolean>
try {
status = pool.listConnectionStatus()
} catch {
return
}
const toClose: string[] = []
for (const raw of urls) {
if (isRelayUrlInViewerMetadataLists(raw)) continue
if (isMetadataPolicyProfileRelay(raw)) continue
const key = canon(raw)
if (!key || hasActiveSubs(key)) continue
const normalized = normalizeAnyRelayUrl(raw) || raw
const connected = [...status.entries()].some(
([u, ok]) => ok && canon(u) === key
)
if (connected) toClose.push(normalized)
}
if (toClose.length === 0) return
try {
pool.close(toClose)
logger.debug('[RelayPoolIdle] closed publish transient sockets', { relays: toClose })
} catch {
/* ignore */
}
for (const url of toClose) {
lastActivityMs.delete(canon(url))
}
}
export function resetRelayPoolIdleForTests(): void {
if (sweepTimer != null) {
clearInterval(sweepTimer)
sweepTimer = null
}
pool = null
hasActiveSubs = null
lastActivityMs.clear()
}