XoverClient times replies only, not idle time between commands
Test and deploy / test (push) Successful in 1m12s
Test and deploy / deploy (push) Successful in 1m37s

A seeding node that waited over two minutes for the seed API (restarting)
had its idle NNTP connection closed by its own timeout, and gave its range
back. Now the timeout runs only while a reply is awaited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-05 13:40:15 +02:00
1 parent 00f6187b8f
commit 04f945055f
2 files changed
+41 -5

No files matched your search

+28 -5
View File
@@ -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<Overview[]> {
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<T>(exchange: () => Promise<T>): Promise<T> {
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<Overview[]> {
private async overviews(from: number, to: number): Promise<Overview[]> {
const reply = await this.command(`XOVER ${from}-${to}`)
if (reply.startsWith('420') || reply.startsWith('423')) return []
await this.expect(reply, ['224'])
+13
View File
@@ -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()
}
})
})