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

93.75% Statements 15/16
90.9% Branches 10/11
100% Functions 3/3
93.75% Lines 15/16

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                                                3x                         5x 5x     5x                                   3x 3x   3x       3x 5x 5x 5x                     3x     1x   3x 3x    
/**
 * @module parliamentary-data/mcp-retry-queue/retry-policy
 * @description Backoff + cap policy for queue entries. Builds new entries
 * (`createRetryQueueEntry`) and merges them into the on-disk queue with
 * deduplication (`enqueueRetryEntries`).
 *
 * Default expiry: 7 days. Entries are keyed by `${resourceType}:${resourceId}`
 * so re-enqueuing the same resource does not duplicate the row but preserves
 * the original `requestedAt` / `expiresAt` and `attemptCount`.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
import type { MCPCoverageState } from '../../types/mcp.js';
import {
  DEFAULT_MCP_RETRY_QUEUE_PATH,
  MCP_RETRY_QUEUE_SCHEMA,
  loadMcpRetryQueue,
  saveMcpRetryQueue,
  type MCPRetryQueueEntry,
  type MCPRetryQueueFile,
} from './persistence.js';
 
const DEFAULT_EXPIRY_DAYS = 7;
 
export function createRetryQueueEntry(options: {
  resourceType: MCPRetryQueueEntry['resourceType'];
  resourceId: string;
  tool: string;
  coverageState: MCPCoverageState;
  params: Record<string, unknown>;
  docType?: string | null;
  reason?: string;
  requestedAt?: string;
  expiresInDays?: number;
}): MCPRetryQueueEntry {
  const requestedAt = options.requestedAt ?? new Date().toISOString();
  const expiresAt = new Date(
    new Date(requestedAt).getTime() + ((options.expiresInDays ?? DEFAULT_EXPIRY_DAYS) * 86400000),
  ).toISOString();
  return {
    resourceType: options.resourceType,
    resourceId: options.resourceId,
    tool: options.tool,
    docType: options.docType ?? null,
    coverageState: options.coverageState,
    requestedAt,
    expiresAt,
    attemptCount: 0,
    params: { ...options.params },
    ...(options.reason ? { reason: options.reason } : {}),
  };
}
 
export function enqueueRetryEntries(
  entries: MCPRetryQueueEntry[],
  queuePath: string = DEFAULT_MCP_RETRY_QUEUE_PATH,
): MCPRetryQueueFile {
  const queue = loadMcpRetryQueue(queuePath);
  const deduped = new Map<string, MCPRetryQueueEntry>();
 
  for (const existing of queue.entries) {
    deduped.set(`${existing.resourceType}:${existing.resourceId}`, existing);
  }
 
  for (const entry of entries) {
    const key = `${entry.resourceType}:${entry.resourceId}`;
    const previous = deduped.get(key);
    deduped.set(key, previous
      ? {
          ...previous,
          ...entry,
          attemptCount: previous.attemptCount,
          requestedAt: previous.requestedAt,
          expiresAt: previous.expiresAt,
        }
      : entry);
  }
 
  const updated: MCPRetryQueueFile = {
    schema: MCP_RETRY_QUEUE_SCHEMA,
    updatedAt: new Date().toISOString(),
    entries: [...deduped.values()].sort((a, b) => a.resourceId.localeCompare(b.resourceId)),
  };
  saveMcpRetryQueue(updated, queuePath);
  return updated;
}