All files / scripts/parliamentary-data/mcp-retry-queue index.ts

77.5% Statements 31/40
65.62% Branches 21/32
100% Functions 1/1
81.08% Lines 30/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133                                                                                                                    3x 3x 3x 3x 3x 3x 3x 3x   3x 3x 3x 3x   3x 3x       3x       3x         3x     3x           3x 2x 2x 2x 2x     1x 1x     3x                   3x 3x 2x     3x                      
/**
 * @module parliamentary-data/mcp-retry-queue
 * @description Queue orchestrator — drains the file-backed deferred MCP retry
 * queue against a live `MCPClient`, classifies each entry as resolved or
 * retained, and writes the updated queue back to disk.
 *
 * Public surface (re-exports for backward compatibility):
 *  - `MCP_RETRY_QUEUE_SCHEMA`, `DEFAULT_MCP_RETRY_QUEUE_PATH`
 *  - Types: `MCPRetryQueueEntry`, `MCPRetryQueueFile`, `MCPRetryDrainResult`
 *  - I/O: `loadMcpRetryQueue`, `saveMcpRetryQueue`
 *  - Build/merge: `createRetryQueueEntry`, `enqueueRetryEntries`
 *  - Drain: `drainMcpRetryQueue`
 *
 * Original 373-line `mcp-retry-queue.ts` was split into this directory; the
 * top-level file is now a re-export shim that preserves the historic import
 * path used by `tests/mcp-retry-queue.test.ts` and downstream callers.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import type { MCPClient } from '../../mcp-client/client.js';
import type { MCPToolInvocationDiagnostic } from '../../types/mcp.js';
 
import {
  drainDocumentFulltextEntry,
  drainVoteringarSearchEntry,
} from './classifier.js';
import {
  DEFAULT_MCP_RETRY_QUEUE_PATH,
  MCP_RETRY_QUEUE_SCHEMA,
  loadMcpRetryQueue,
  saveMcpRetryQueue,
  type MCPRetryDrainResult,
  type MCPRetryQueueEntry,
  type MCPRetryQueueFile,
} from './persistence.js';
 
export {
  DEFAULT_MCP_RETRY_QUEUE_PATH,
  MCP_RETRY_QUEUE_SCHEMA,
  loadMcpRetryQueue,
  saveMcpRetryQueue,
  type MCPRetryDrainResult,
  type MCPRetryQueueEntry,
  type MCPRetryQueueFile,
};
export { createRetryQueueEntry, enqueueRetryEntries } from './retry-policy.js';
 
export async function drainMcpRetryQueue(
  client: MCPClient,
  options: {
    docType?: string | null;
    queuePath?: string;
    now?: Date;
    maxEntries?: number;
  } = {},
): Promise<MCPRetryDrainResult> {
  const queuePath = options.queuePath ?? DEFAULT_MCP_RETRY_QUEUE_PATH;
  const now = options.now ?? new Date();
  const queue = loadMcpRetryQueue(queuePath);
  const remaining: MCPRetryQueueEntry[] = [];
  const resolvedDocuments: Record<string, Record<string, unknown>> = {};
  const resolvedVoteringar: Record<string, unknown[]> = {};
  const diagnostics: MCPToolInvocationDiagnostic[] = [];
  const originalEntryCount = queue.entries.length;
 
  let processed = 0;
  let resolved = 0;
  let retained = 0;
  let expired = 0;
 
  for (const entry of queue.entries) {
    Iif (options.docType && entry.docType && entry.docType !== options.docType) {
      remaining.push(entry);
      continue;
    }
    Iif (new Date(entry.expiresAt).getTime() < now.getTime()) {
      expired++;
      continue;
    }
    Iif (options.maxEntries && processed >= options.maxEntries) {
      remaining.push(entry);
      continue;
    }
 
    processed++;
 
    const outcome =
      entry.resourceType === 'document_fulltext'
        ? await drainDocumentFulltextEntry(client, entry, now)
        : await drainVoteringarSearchEntry(client, entry, now);
 
    if (outcome.diagnostic) diagnostics.push(outcome.diagnostic);
 
    if (outcome.kind === 'resolved') {
      resolved++;
      Eif (outcome.document) resolvedDocuments[entry.resourceId] = outcome.document;
      Iif (outcome.voteringar) resolvedVoteringar[entry.resourceId] = outcome.voteringar;
      continue;
    }
 
    remaining.push(outcome.entry);
    retained++;
  }
 
  const updatedQueue: MCPRetryQueueFile = {
    schema: MCP_RETRY_QUEUE_SCHEMA,
    updatedAt: now.toISOString(),
    entries: remaining,
  };
 
  // Avoid touching the queue file when the queue was already empty AND we
  // had nothing to process. Without this guard, every news workflow would
  // dirty `data/mcp-retry-queue.json` with a fresh `updatedAt` even when no
  // retry work occurred, producing noisy PR diffs and merge conflicts.
  const hadWork = originalEntryCount > 0 || processed > 0 || expired > 0 || remaining.length > 0;
  if (hadWork) {
    saveMcpRetryQueue(updatedQueue, queuePath);
  }
 
  return {
    queue: updatedQueue,
    processed,
    resolved,
    retained,
    expired,
    resolvedDocuments,
    resolvedVoteringar,
    diagnostics,
  };
}