120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import nntpConfig from '#config/nntp'
|
|
import { NNTP } from "nntp-js";
|
|
import { parseGroupResponse, type GroupInfo } from '#lib/nntp/group'
|
|
|
|
class NntpService {
|
|
private readonly poolSize: number;
|
|
private allConnections: Set<any>;
|
|
private idleConnections: any[];
|
|
private waiters: ((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;
|
|
}
|
|
|
|
public async acquire() {
|
|
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 => 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.waiters.shift();
|
|
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.waiters.shift();
|
|
if (resolve) {
|
|
this.createdCount++;
|
|
this._createConnection().then(resolve, () => this.waiters.unshift(resolve));
|
|
}
|
|
}
|
|
|
|
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<GroupInfo> {
|
|
const { resp } = await conn.group(name)
|
|
return parseGroupResponse(resp)
|
|
}
|
|
|
|
/** Fetches a raw (still dot-stuffed) article body as a Buffer. */
|
|
public async fetchBody(conn: any, messageId: string): Promise<Buffer> {
|
|
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.createdCount = 0;
|
|
}
|
|
}
|
|
|
|
export default new NntpService(nntpConfig.poolSize);
|