import { parseHttpUrl } from '../utils/validation'; export interface FeedUrlIdentity { id: number; xmlUrl: string; } export type CanonicalizeFeedUrl = (url: string) => string | undefined; function canonicalizeFeedUrl(url: string): string | undefined { const parsed = parseHttpUrl(url); return parsed.valid ? parsed.value : undefined; } /** * In-memory Feed URL identity index used by bulk add operations. * * Rows must be supplied in ascending id order. The first canonical-equivalent * row is retained, which preserves the lookup priority of the legacy scan * while making every subsequent lookup constant time. */ export class FeedUrlIndex { private readonly exact = new Map(); private readonly canonicalEquivalent = new Map(); private readonly claims = new Set(); constructor(rows: readonly T[], canonicalize: CanonicalizeFeedUrl = canonicalizeFeedUrl) { for (const row of rows) { this.exact.set(row.xmlUrl, row); const canonical = canonicalize(row.xmlUrl); if (canonical !== undefined && !this.canonicalEquivalent.has(canonical)) { this.canonicalEquivalent.set(canonical, row); } } } find(canonicalUrl: string, rawUrl = canonicalUrl): T | undefined { return ( this.exact.get(canonicalUrl) ?? (rawUrl !== canonicalUrl ? this.exact.get(rawUrl) : undefined) ?? this.canonicalEquivalent.get(canonicalUrl) ); } /** Reserve an identity for this session. Returns false for any duplicate. */ claim(canonicalUrl: string, rawUrl = canonicalUrl): boolean { if (this.find(canonicalUrl, rawUrl) !== undefined || this.claims.has(canonicalUrl)) return false; this.claims.add(canonicalUrl); return true; } release(canonicalUrl: string): void { this.claims.delete(canonicalUrl); } /** Add a successfully inserted row without re-canonicalizing existing data. */ record(row: T, canonicalUrl: string): void { this.claims.delete(canonicalUrl); this.exact.set(row.xmlUrl, row); if (!this.canonicalEquivalent.has(canonicalUrl)) { this.canonicalEquivalent.set(canonicalUrl, row); } } }