diff --git a/abstract-relay.ts b/abstract-relay.ts index a88f7d7..2fad759 100644 --- a/abstract-relay.ts +++ b/abstract-relay.ts @@ -141,7 +141,11 @@ export class AbstractRelay { connectionTimeoutHandle = setTimeout(() => { reject('connection timed out') this.connectionPromise = undefined - this.skipReconnection = true + // Only give up on the initial connect; a slow reconnect + // should fall through to the next backoff slot. + if (this.reconnectAttempts === 0) { + this.skipReconnection = true + } this.onclose?.() this.handleHardClose('relay connection timed out') }, opts.timeout) @@ -193,7 +197,12 @@ export class AbstractRelay { clearTimeout(connectionTimeoutHandle) reject('connection failed') this.connectionPromise = undefined - this.skipReconnection = true + // Only give up on the initial connect. A failed reconnect + // attempt must fall through so the next backoff slot fires; + // otherwise one failed retry tears down every subscription. + if (this.reconnectAttempts === 0) { + this.skipReconnection = true + } this.onclose?.() this.handleHardClose('relay connection failed') } diff --git a/relay.test.ts b/relay.test.ts index d50d56d..a6bf83f 100644 --- a/relay.test.ts +++ b/relay.test.ts @@ -338,6 +338,45 @@ test('reconnect on disconnect', async () => { expect(closes).toBe(1) // should not have closed again }) +test('reconnect survives a failed reconnect attempt and recovers when the relay returns', async () => { + const mockRelay = new MockRelay() + const relay = new Relay(mockRelay.url, { enableReconnect: true }) + relay.resubscribeBackoff = [50, 50, 100, 100] // short backoff for testing + + await relay.connect() + expect(relay.connected).toBeTrue() + + relay.subscribe([{ kinds: [1] }], { onevent: () => {} }) + expect(relay.openSubs.size).toBe(1) + + // Drop server + close live socket so the scheduled reconnect fails. + ;(mockRelay as any)._server.stop() + ;(relay as any).ws?.close() + + // Past one failed reconnect attempt: the sub must still be alive + // (buggy code clears openSubs here via skipReconnection). + await new Promise(resolve => setTimeout(resolve, 300)) + expect(relay.connected).toBeFalse() + expect(relay.openSubs.size).toBe(1) + + // Bring the relay back; the next backoff slot must reconnect. + new MockRelay(mockRelay.url) + + await new Promise((resolve, reject) => { + const deadline = setTimeout(() => reject(new Error('relay never reconnected')), 2000) + const interval = setInterval(() => { + if (relay.connected) { + clearTimeout(deadline) + clearInterval(interval) + resolve() + } + }, 10) + }) + expect(relay.openSubs.size).toBe(1) + + relay.close() +}) + test('oninvalidevent is called for malformed events', async done => { const mockRelay = new MockRelay() const relay = new Relay(mockRelay.url)