From 514678b1024ec8e1c5b0284adac26a2d98c54ff2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 13 Jul 2026 11:24:56 -0300 Subject: [PATCH] automatically closed unused relay connections after a while. --- abstract-pool.ts | 3 +++ abstract-relay.ts | 36 ++++++++++++++++++++++++++++++++++-- jsr.json | 2 +- package.json | 2 +- relay.ts | 7 +++++-- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/abstract-pool.ts b/abstract-pool.ts index 9124e11..edd886c 100644 --- a/abstract-pool.ts +++ b/abstract-pool.ts @@ -49,6 +49,7 @@ export class AbstractSimplePool { public verifyEvent: Nostr['verifyEvent'] public enablePing: boolean | undefined public enableReconnect: boolean + public idleTimeout: number = 20000 public automaticallyAuth?: (relayURL: string) => null | ((event: EventTemplate) => Promise) public trustedRelayURLs: Set = new Set() public onRelayConnectionFailure?: (url: string) => void @@ -63,6 +64,7 @@ export class AbstractSimplePool { this._WebSocket = opts.websocketImplementation this.enablePing = opts.enablePing this.enableReconnect = opts.enableReconnect || false + if (opts.idleTimeout) this.idleTimeout = opts.idleTimeout this.automaticallyAuth = opts.automaticallyAuth this.onRelayConnectionFailure = opts.onRelayConnectionFailure this.onRelayConnectionSuccess = opts.onRelayConnectionSuccess @@ -86,6 +88,7 @@ export class AbstractSimplePool { websocketImplementation: this._WebSocket, enablePing: this.enablePing, enableReconnect: this.enableReconnect, + idleTimeout: this.idleTimeout, }) relay.onclose = () => { this.relays.delete(url) diff --git a/abstract-relay.ts b/abstract-relay.ts index f5612fd..b351061 100644 --- a/abstract-relay.ts +++ b/abstract-relay.ts @@ -16,6 +16,7 @@ export type AbstractRelayConstructorOptions = { websocketImplementation?: typeof WebSocket enablePing?: boolean enableReconnect?: boolean + idleTimeout?: number } export class SendingOnClosedConnection extends Error { @@ -41,12 +42,14 @@ export class AbstractRelay { public openSubs: Map = new Map() public enablePing: boolean | undefined public enableReconnect: boolean + public idleTimeout: number = 0 // auto-close after ms of inactivity, 0 = disabled 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 private skipReconnection: boolean = false + private idleTimeoutHandle: ReturnType | undefined private connectionPromise: Promise | undefined private openCountRequests = new Map() @@ -65,6 +68,7 @@ export class AbstractRelay { this._WebSocket = opts.websocketImplementation || WebSocket this.enablePing = opts.enablePing this.enableReconnect = opts.enableReconnect || false + if (opts.idleTimeout) this.idleTimeout = opts.idleTimeout } static async connect( @@ -97,6 +101,24 @@ export class AbstractRelay { return this._connected } + private clearIdleTimeout() { + if (this.idleTimeoutHandle) { + clearTimeout(this.idleTimeoutHandle) + this.idleTimeoutHandle = undefined + } + } + + public scheduleIdleClose() { + this.clearIdleTimeout() + if (this.idleTimeout > 0) { + this.idleTimeoutHandle = setTimeout(() => { + if (this.ongoingOperations === 0 && this.idleSince) { + this.close() + } + }, this.idleTimeout) + } + } + private async reconnect(): Promise { const backoff = this.resubscribeBackoff[Math.min(this.reconnectAttempts, this.resubscribeBackoff.length - 1)] this.reconnectAttempts++ @@ -119,6 +141,7 @@ export class AbstractRelay { this._connected = false this.connectionPromise = undefined this.idleSince = undefined + this.clearIdleTimeout() if (this.enableReconnect && !this.skipReconnection) { this.reconnect() @@ -311,6 +334,7 @@ export class AbstractRelay { public async publish(event: Event): Promise { this.idleSince = undefined + this.clearIdleTimeout() this.ongoingOperations++ const ret = new Promise((resolve, reject) => { @@ -335,7 +359,10 @@ export class AbstractRelay { // compute idleness state this.ongoingOperations-- - if (this.ongoingOperations === 0) this.idleSince = Date.now() + if (this.ongoingOperations === 0) { + this.idleSince = Date.now() + this.scheduleIdleClose() + } return ret } @@ -371,6 +398,7 @@ export class AbstractRelay { ): Subscription { if (params.label !== '') { this.idleSince = undefined + this.clearIdleTimeout() this.ongoingOperations++ } @@ -408,6 +436,7 @@ export class AbstractRelay { this.closeAllSubscriptions('relay connection closed by us') this._connected = false this.idleSince = undefined + this.clearIdleTimeout() this.onclose?.() if (this.ws?.readyState === this._WebSocket.OPEN) { this.ws?.close() @@ -622,7 +651,10 @@ export class Subscription { // compute idleness state this.relay.ongoingOperations-- - if (this.relay.ongoingOperations === 0) this.relay.idleSince = Date.now() + if (this.relay.ongoingOperations === 0) { + this.relay.idleSince = Date.now() + this.relay.scheduleIdleClose() + } this.onclose?.(reason) } diff --git a/jsr.json b/jsr.json index 44ebf70..f3936ad 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@nostr/tools", - "version": "2.23.10", + "version": "2.23.11", "exports": { ".": "./index.ts", "./core": "./core.ts", diff --git a/package.json b/package.json index b88189a..746bbc3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "nostr-tools", - "version": "2.23.9", + "version": "2.23.11", "description": "Tools for making a Nostr client.", "repository": { "type": "git", diff --git a/relay.ts b/relay.ts index 6561cac..0de9818 100644 --- a/relay.ts +++ b/relay.ts @@ -14,13 +14,16 @@ export function useWebSocketImplementation(websocketImplementation: any) { } export class Relay extends AbstractRelay { - constructor(url: string, options?: Pick) { + constructor( + url: string, + options?: Pick, + ) { super(url, { verifyEvent, websocketImplementation: _WebSocket, ...options }) } static async connect( url: string, - options?: Pick, + options?: Pick, ): Promise { const relay = new Relay(url, options) await relay.connect()