From ab802c8dbe35d29feb732ba54e82a346c21c32e2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 2 Feb 2026 17:30:54 -0300 Subject: [PATCH] automatic prune broken relay objects and keep track of relay idleness so they can be pruned. --- abstract-pool.ts | 32 +++++++++++++++++++++++++------- abstract-relay.ts | 43 ++++++++++++++++++++++++++++--------------- jsr.json | 2 +- package.json | 2 +- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/abstract-pool.ts b/abstract-pool.ts index 03ca043..90aa9c6 100644 --- a/abstract-pool.ts +++ b/abstract-pool.ts @@ -88,9 +88,7 @@ export class AbstractSimplePool { enableReconnect: this.enableReconnect, }) relay.onclose = () => { - if (relay && !relay.enableReconnect) { - this.relays.delete(url) - } + this.relays.delete(url) } this.relays.set(url, relay) } @@ -102,10 +100,15 @@ export class AbstractSimplePool { } } - await relay.connect({ - timeout: params?.connectionTimeout, - abort: params?.abort, - }) + try { + await relay.connect({ + timeout: params?.connectionTimeout, + abort: params?.abort, + }) + } catch (err) { + this.relays.delete(url) + throw err + } return relay } @@ -380,4 +383,19 @@ export class AbstractSimplePool { this.relays.forEach(conn => conn.close()) this.relays = new Map() } + + pruneIdleRelays(idleThresholdMs: number = 10000): string[] { + const prunedUrls: string[] = [] + + // check each relay's idle status and prune if over threshold + for (const [url, relay] of this.relays) { + if (relay.idleSince && Date.now() - relay.idleSince >= idleThresholdMs) { + this.relays.delete(url) + prunedUrls.push(url) + relay.close() + } + } + + return prunedUrls + } } diff --git a/abstract-relay.ts b/abstract-relay.ts index abd7801..1f26c42 100644 --- a/abstract-relay.ts +++ b/abstract-relay.ts @@ -41,6 +41,8 @@ export class AbstractRelay { public openSubs: Map = new Map() public enablePing: boolean | undefined public enableReconnect: boolean + public idleSince: number | undefined = Date.now() // when undefined that means it isn't idle + public ongoingOperations: number = 0 // used to compute idleness private reconnectTimeoutHandle: ReturnType | undefined private pingIntervalHandle: ReturnType | undefined private reconnectAttempts: number = 0 @@ -116,12 +118,12 @@ export class AbstractRelay { this._connected = false this.connectionPromise = undefined - - this.onclose?.() + this.idleSince = undefined if (this.enableReconnect && !this.skipReconnection) { this.reconnect() } else { + this.onclose?.() this.closeAllSubscriptions(reason) } } @@ -227,7 +229,7 @@ export class AbstractRelay { const sub = this.subscribe( [{ ids: ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], limit: 0 }], { - label: 'forced-ping', + label: '', oneose: () => { resolve(true) sub.close() @@ -299,6 +301,9 @@ export class AbstractRelay { } public async publish(event: Event): Promise { + this.idleSince = undefined + this.ongoingOperations++ + const ret = new Promise((resolve, reject) => { const timeout = setTimeout(() => { const ep = this.openEventPublishes.get(event.id) as EventPublishResolver @@ -310,6 +315,11 @@ export class AbstractRelay { this.openEventPublishes.set(event.id, { resolve, reject, timeout }) }) this.send('["EVENT",' + JSON.stringify(event) + ']') + + // compute idleness state + this.ongoingOperations-- + if (this.ongoingOperations === 0) this.idleSince = Date.now() + return ret } @@ -327,7 +337,15 @@ export class AbstractRelay { filters: Filter[], params: Partial & { label?: string; id?: string }, ): Subscription { - const sub = this.prepareSubscription(filters, params) + if (params.label !== '') { + this.idleSince = undefined + this.ongoingOperations++ + } + + this.serial++ + const id = params.id || (params.label ? params.label + ':' : 'sub:') + this.serial + const sub = new Subscription(this, id, filters, params) + this.openSubs.set(id, sub) sub.fire() if (params.abort) { @@ -337,17 +355,6 @@ export class AbstractRelay { return sub } - public prepareSubscription( - filters: Filter[], - params: Partial & { label?: string; id?: string }, - ): Subscription { - this.serial++ - const id = params.id || (params.label ? params.label + ':' : 'sub:') + this.serial - const subscription = new Subscription(this, id, filters, params) - this.openSubs.set(id, subscription) - return subscription - } - public close() { this.skipReconnection = true if (this.reconnectTimeoutHandle) { @@ -360,6 +367,7 @@ export class AbstractRelay { } this.closeAllSubscriptions('relay connection closed by us') this._connected = false + this.idleSince = undefined this.onclose?.() if (this.ws?.readyState === this._WebSocket.OPEN) { this.ws?.close() @@ -549,6 +557,11 @@ export class Subscription { this.closed = true } this.relay.openSubs.delete(this.id) + + // compute idleness state + this.relay.ongoingOperations-- + if (this.relay.ongoingOperations === 0) this.relay.idleSince = Date.now() + this.onclose?.(reason) } } diff --git a/jsr.json b/jsr.json index be2d1d7..23010cb 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@nostr/tools", - "version": "2.22.2", + "version": "2.23.0", "exports": { ".": "./index.ts", "./core": "./core.ts", diff --git a/package.json b/package.json index 800c71b..123e199 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "nostr-tools", - "version": "2.22.2", + "version": "2.23.0", "description": "Tools for making a Nostr client.", "repository": { "type": "git",