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

92.3% Statements 12/13
72.72% Branches 8/11
100% Functions 3/3
91.66% Lines 11/12

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                                3x 3x   3x 3x                                                                                           4x                   7x 3x 3x 3x                           5x 5x    
/**
 * @module parliamentary-data/mcp-retry-queue/persistence
 * @description On-disk queue state — schema constants, types, load/save
 * primitives for the file-backed deferred MCP retry queue. Pure I/O with no
 * MCP client coupling.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
 
import type { MCPCoverageState, MCPToolInvocationDiagnostic } from '../../types/mcp.js';
 
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
 
export const MCP_RETRY_QUEUE_SCHEMA = 'riksdagsmonitor-mcp-retry-queue/1.0';
export const DEFAULT_MCP_RETRY_QUEUE_PATH = path.join(
  REPO_ROOT,
  'data',
  'mcp-retry-queue.json',
);
 
export interface MCPRetryQueueEntry {
  resourceType: 'document_fulltext' | 'voteringar_search';
  resourceId: string;
  tool: string;
  docType?: string | null;
  coverageState: MCPCoverageState;
  requestedAt: string;
  expiresAt: string;
  attemptCount: number;
  params: Record<string, unknown>;
  reason?: string;
  lastAttemptAt?: string;
}
 
export interface MCPRetryQueueFile {
  schema: string;
  updatedAt: string;
  entries: MCPRetryQueueEntry[];
}
 
export interface MCPRetryDrainResult {
  queue: MCPRetryQueueFile;
  processed: number;
  resolved: number;
  retained: number;
  expired: number;
  resolvedDocuments: Record<string, Record<string, unknown>>;
  /**
   * Voting rows recovered from previously-deferred voteringar searches,
   * keyed by the queue entry's `resourceId` (the exact query payload).
   * Surfaced so callers can re-inject the rows into the run output even if
   * the original query is no longer selected by the current date/filter.
   */
  resolvedVoteringar: Record<string, unknown[]>;
  diagnostics: MCPToolInvocationDiagnostic[];
}
 
export function emptyQueue(
  initialTimestamp = new Date(0).toISOString(),
): MCPRetryQueueFile {
  return {
    schema: MCP_RETRY_QUEUE_SCHEMA,
    updatedAt: initialTimestamp,
    entries: [],
  };
}
 
export function loadMcpRetryQueue(
  queuePath: string = DEFAULT_MCP_RETRY_QUEUE_PATH,
): MCPRetryQueueFile {
  if (!fs.existsSync(queuePath)) return emptyQueue();
  try {
    const parsed = JSON.parse(fs.readFileSync(queuePath, 'utf8')) as Partial<MCPRetryQueueFile>;
    return {
      schema: parsed.schema ?? MCP_RETRY_QUEUE_SCHEMA,
      updatedAt: parsed.updatedAt ?? new Date(0).toISOString(),
      entries: Array.isArray(parsed.entries) ? parsed.entries as MCPRetryQueueEntry[] : [],
    };
  } catch {
    return emptyQueue();
  }
}
 
export function saveMcpRetryQueue(
  queue: MCPRetryQueueFile,
  queuePath: string = DEFAULT_MCP_RETRY_QUEUE_PATH,
): void {
  fs.mkdirSync(path.dirname(queuePath), { recursive: true });
  fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2) + '\n', 'utf8');
}