diff --git a/lib/nntp/xover_client.ts b/lib/nntp/xover_client.ts index c54de44..b56b428 100644 --- a/lib/nntp/xover_client.ts +++ b/lib/nntp/xover_client.ts @@ -35,7 +35,10 @@ export class XoverClient { private failure?: Error compressed = false - private constructor(private sock: net.Socket) { + private constructor( + private sock: net.Socket, + private timeoutMs: number + ) { sock.on('data', (chunk: Buffer) => { this.chunks.push(chunk) this.length += chunk.length @@ -57,8 +60,8 @@ export class XoverClient { const sock = options.secure ? tls.connect({ host: options.host, port: options.port, servername: options.host }) : net.connect({ host: options.host, port: options.port }) - sock.setTimeout(options.timeoutMs ?? 120_000) - const client = new XoverClient(sock) + const client = new XoverClient(sock, options.timeoutMs ?? 120_000) + sock.setTimeout(client.timeoutMs) try { await client.expect(await client.line(), ['200', '201']) if (options.user) { @@ -71,18 +74,38 @@ export class XoverClient { client.close() throw error } + // Idle between commands is fine (a node waiting for the seed API); only replies are timed. + sock.setTimeout(0) return client } async group(name: string): Promise<{ count: number; low: number; high: number }> { + return this.timed(() => this.selectGroup(name)) + } + + /** Overviews of articles `from`..`to` in the current group; an empty range gives []. */ + async xover(from: number, to: number): Promise { + return this.timed(() => this.overviews(from, to)) + } + + /** A reply that stalls for timeoutMs fails the connection; idle time between commands doesn't count. */ + private async timed(exchange: () => Promise): Promise { + this.sock.setTimeout(this.timeoutMs) + try { + return await exchange() + } finally { + this.sock.setTimeout(0) + } + } + + private async selectGroup(name: string): Promise<{ count: number; low: number; high: number }> { const reply = await this.command(`GROUP ${name}`) await this.expect(reply, ['211']) const [, count, low, high] = reply.split(' ') return { count: Number(count), low: Number(low), high: Number(high) } } - /** Overviews of articles `from`..`to` in the current group; an empty range gives []. */ - async xover(from: number, to: number): Promise { + private async overviews(from: number, to: number): Promise { const reply = await this.command(`XOVER ${from}-${to}`) if (reply.startsWith('420') || reply.startsWith('423')) return [] await this.expect(reply, ['224']) diff --git a/tests/unit/xover_client.spec.ts b/tests/unit/xover_client.spec.ts index c00bfe0..dad2196 100644 --- a/tests/unit/xover_client.spec.ts +++ b/tests/unit/xover_client.spec.ts @@ -73,4 +73,17 @@ test.group('nntp / XoverClient', () => { } }) } + + test('idle time between commands does not count against the reply timeout', async ({ assert }) => { + const { port, close } = await server() + const client = await XoverClient.connect({ host: '127.0.0.1', port, user: 'u', password: 'p', compress: true, timeoutMs: 200 }) + try { + await client.group('a.b.test') + await new Promise((resolve) => setTimeout(resolve, 500)) + assert.lengthOf(await client.xover(101, 102), 2) + } finally { + client.close() + close() + } + }) })