From 695f2fe01707e474ee1c571c169b12032d4ce7b3 Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 5 Jan 2026 18:20:32 +0800 Subject: [PATCH] feat: smart relay pool --- package-lock.json | 8 +++--- package.json | 4 +-- src/lib/smart-pool.ts | 48 ++++++++++++++++++++++++++++++++++ src/services/client.service.ts | 10 +++---- 4 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 src/lib/smart-pool.ts diff --git a/package-lock.json b/package-lock.json index 8f178e2..4f77f74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", "next-themes": "^0.4.6", - "nostr-tools": "^2.17.0", + "nostr-tools": "^2.19.1", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", "qr-code-styling": "^1.9.2", @@ -9930,9 +9930,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.0.tgz", - "integrity": "sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==", + "version": "2.19.4", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.4.tgz", + "integrity": "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", diff --git a/package.json b/package.json index 9e6afa2..09801a9 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", "next-themes": "^0.4.6", - "nostr-tools": "^2.17.0", + "nostr-tools": "^2.19.1", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", "qr-code-styling": "^1.9.2", @@ -111,4 +111,4 @@ "vite": "^6.0.3", "vite-plugin-pwa": "^0.21.1" } -} +} \ No newline at end of file diff --git a/src/lib/smart-pool.ts b/src/lib/smart-pool.ts new file mode 100644 index 0000000..35282f3 --- /dev/null +++ b/src/lib/smart-pool.ts @@ -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() + + constructor() { + super({ enablePing: true, enableReconnect: true }) + + // Periodically clean up idle relays + setInterval(() => this.cleanIdleRelays(), CLEANUP_INTERVAL) + } + + ensureRelay(url: string): Promise { + // 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) + } + } +} diff --git a/src/services/client.service.ts b/src/services/client.service.ts index c9f9f96..19b43ec 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -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>() 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