UNPKG

@cyanheads/pubmed-mcp-server

Version:

Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.

107 lines 4.19 kB
/** * @fileoverview Rate-limited request scheduler for Europe PMC calls. Caps * concurrent in-flight requests and enforces a minimum start-gap between * dispatches to stay polite with EBI's infrastructure. Independent rate * domain from NCBI's queue — Europe PMC runs on a different host with its * own limits. * @module src/services/europe-pmc/request-queue */ import { logger, requestContextService } from '@cyanheads/mcp-ts-core/utils'; const DEFAULT_MAX_CONCURRENT = 4; /** * Schedules Europe PMC requests against two independent ceilings: * * - **Throughput** (`minStartGapMs`): minimum delay between two consecutive * dispatch times. Defaults to 200ms to be polite with EBI. * - **Concurrency** (`maxConcurrent`): maximum simultaneous in-flight * requests. Decouples concurrency from rate so slow upstream responses * don't block new dispatches. * * Enqueue accepts an optional `AbortSignal` so callers can bound their total * time inside the scheduler — when the signal fires, a still-waiting task * rejects immediately instead of sitting behind a saturated worker. */ export class EuropePmcRequestQueue { waiters = []; minStartGapMs; maxConcurrent; inFlight = 0; lastStartTime = 0; nextDispatchTimer; constructor(minStartGapMs, maxConcurrent = DEFAULT_MAX_CONCURRENT) { this.minStartGapMs = minStartGapMs; this.maxConcurrent = maxConcurrent; } enqueue(task, label, signal) { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(signal.reason); return; } const waiter = { resolve, reject, task, label, ...(signal && { signal }), }; if (signal) { const onAbort = () => { const idx = this.waiters.indexOf(waiter); if (idx === -1) return; this.waiters.splice(idx, 1); reject(signal.reason); }; waiter.onAbort = onAbort; signal.addEventListener('abort', onAbort, { once: true }); } this.waiters.push(waiter); this.tryDispatch(); }); } tryDispatch() { if (this.nextDispatchTimer !== undefined) return; while (this.inFlight < this.maxConcurrent && this.waiters.length > 0) { const now = Date.now(); const gap = this.minStartGapMs - (now - this.lastStartTime); if (gap > 0) { this.nextDispatchTimer = setTimeout(() => { this.nextDispatchTimer = undefined; this.tryDispatch(); }, gap); return; } const waiter = this.waiters.shift(); if (!waiter) return; if (waiter.signal && waiter.onAbort) { waiter.signal.removeEventListener('abort', waiter.onAbort); } this.lastStartTime = Date.now(); this.inFlight += 1; logger.debug(`Executing Europe PMC request via queue: ${waiter.label}`, requestContextService.createRequestContext({ operation: 'EuropePmcQueueDispatch', label: waiter.label, inFlight: this.inFlight, queueDepth: this.waiters.length, })); Promise.resolve() .then(() => waiter.task()) .then(waiter.resolve, (err) => { logger.error('Error processing Europe PMC request from queue.', requestContextService.createRequestContext({ operation: 'EuropePmcQueueProcess', label: waiter.label, errorMessage: err instanceof Error ? err.message : String(err), })); waiter.reject(err); }) .finally(() => { this.inFlight -= 1; this.tryDispatch(); }); } } } //# sourceMappingURL=request-queue.js.map