import nntpConfig from '#config/nntp' import { NNTP } from "#lib/nntp/nntp_js"; import { parseGroupResponse, type GroupInfo } from '#lib/nntp/group' class NntpService { private readonly poolSize: number; private allConnections: Set; private idleConnections: any[]; private waiters: ((conn: any) => void)[]; /** Background work (archive walks): served only when no one else waits. */ private lowWaiters: ((conn: any) => void)[] = []; private createdCount: number; constructor(poolSize = 10) { this.poolSize = poolSize; this.allConnections = new Set(); this.idleConnections = []; this.waiters = []; this.createdCount = 0; } private async _createConnection() { const conn = new NNTP(nntpConfig.host, nntpConfig.port); try { await conn.connect(); if (nntpConfig.user) { await conn.login(nntpConfig.user, nntpConfig.password?.release()); } } catch (error) { // Free the slot, otherwise a failed connect shrinks the pool for good. this.createdCount--; this._destroy(conn); throw error; } this.allConnections.add(conn); return conn; } /** A connection from the pool; with `low`, after every waiting normal caller. */ public async acquire(low = false) { if (this.idleConnections.length > 0) { return this.idleConnections.pop(); } if (this.createdCount < this.poolSize) { this.createdCount++; console.log(`Creating new connection (${this.createdCount}/${this.poolSize}).`); return this._createConnection(); } return new Promise(resolve => (low ? this.lowWaiters : this.waiters).push(resolve)); } /** * Returns a connection to the pool. Pass `failed` when a command on it * threw: the connection may be dead (e.g. closed by the server while idle), * so it is discarded instead of being handed to the next job. */ public release(conn: any, failed = false) { if (failed) { this.discard(conn); return; } const resolve = this.nextWaiter(); if (resolve) { resolve(conn); } else { this.idleConnections.push(conn); } } /** Closes a broken connection and frees its slot for a new one. */ public discard(conn: any) { this.allConnections.delete(conn); this.createdCount--; this._destroy(conn); // Someone is waiting for a slot: open a fresh connection for them. const resolve = this.nextWaiter(); if (resolve) { this.createdCount++; this._createConnection().then(resolve, () => this.waiters.unshift(resolve)); } } private nextWaiter() { return this.waiters.shift() ?? this.lowWaiters.shift(); } private _destroy(conn: any) { try { conn.sock?.destroy(); } catch { // already closed } } /** Selects a group and returns its correctly parsed article range. */ public async selectGroup(conn: any, name: string): Promise { const { resp } = await conn.group(name) return parseGroupResponse(resp) } /** * Rejects when `promise` hasn't settled within `ms`. nntp-js has no * timeouts: a connection the server stops answering (still ESTABLISHED, * nothing in flight) leaves its command waiting forever. The caller must * then release the connection as failed, so it's destroyed. */ public timed(promise: Promise, what: string, ms = nntpConfig.commandTimeoutMs): Promise { let timer: NodeJS.Timeout const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`NNTP ${what}: no answer within ${ms / 1000} s`)), ms) }) return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)) } /** Fetches a raw (still dot-stuffed) article body as a Buffer. */ public async fetchBody(conn: any, messageId: string): Promise { const id = messageId.startsWith('<') ? messageId : `<${messageId}>` const { data } = await conn.body(id) return Buffer.from(data.buffer, data.byteOffset, data.byteLength) } public async shutdown() { console.log('Shutting down all connections in the pool.'); // A connection the server already closed may never answer QUIT. const quit = (conn: any) => Promise.race([conn.quit(), new Promise((resolve) => setTimeout(resolve, 2000).unref())]) .catch(() => {}) .finally(() => this._destroy(conn)); await Promise.all([...this.allConnections].map(quit)); this.allConnections.clear(); this.idleConnections.length = 0; this.waiters.length = 0; this.lowWaiters.length = 0; this.createdCount = 0; } } export default new NntpService(nntpConfig.poolSize);