import { Action, ActionPanel, Cache, Color, Icon, List, showToast, Toast, } from "@vicinae/api"; import { useCallback, useEffect, useMemo, useState } from "react"; import { parseRfcIndex, RFC_INDEX_URL, type Rfc, rfcUrl, type StatusFilter, searchRfcs, } from "./rfc-index"; const cache = new Cache({ namespace: "rfc-index", capacity: 30 * 1024 * 1024 }); const CACHE_KEY = "index-v1"; const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; type CachedIndex = { updatedAt: number; rfcs: Rfc[]; }; const STATUS_OPTIONS = [ ["all", "All statuses"], ["INTERNET STANDARD", "Internet Standard"], ["PROPOSED STANDARD", "Proposed Standard"], ["DRAFT STANDARD", "Draft Standard"], ["BEST CURRENT PRACTICE", "Best Current Practice"], ["INFORMATIONAL", "Informational"], ["EXPERIMENTAL", "Experimental"], ["HISTORIC", "Historic"], ["UNKNOWN", "Unknown"], ] as const; function readCachedIndex(): CachedIndex | undefined { const stored = cache.get(CACHE_KEY); if (!stored) return undefined; try { const parsed = JSON.parse(stored) as CachedIndex; if (!Array.isArray(parsed.rfcs) || typeof parsed.updatedAt !== "number") { throw new Error("Invalid cached index"); } return parsed; } catch { cache.remove(CACHE_KEY); return undefined; } } function labelForStatus(status: string): string { return STATUS_OPTIONS.find(([value]) => value === status)?.[1] ?? status; } function escapeMarkdown(value: string): string { return value.replace(/([\\`*_{}[\]()#+.!|>-])/g, "\\$1"); } function relationText(ids: string[]): string { return ids.join(", "); } function RfcDetail({ rfc }: { rfc: Rfc }) { const obsoleteWarning = rfc.obsoletedBy.length ? `> **Obsoleted by ${relationText(rfc.obsoletedBy)}.** Prefer the newer specification unless you specifically need this RFC.\n\n` : ""; const markdown = `# ${rfc.docId}\n\n## ${escapeMarkdown(rfc.title)}\n\n${obsoleteWarning}${escapeMarkdown(rfc.abstract || "No abstract is available for this RFC.")}`; return ( {rfc.pageCount ? ( ) : null} {rfc.workingGroup ? ( ) : null} {rfc.isAlso.length ? ( ) : null} {rfc.obsoletes.length ? ( ) : null} {rfc.obsoletedBy.length ? ( ) : null} {rfc.updates.length ? ( ) : null} {rfc.updatedBy.length ? ( ) : null} {rfc.keywords.length ? ( {rfc.keywords.map((keyword) => ( ))} ) : null} } /> ); } function RfcActions({ rfc, refresh }: { rfc: Rfc; refresh: () => void }) { const htmlUrl = rfcUrl(rfc, "html"); return ( {rfc.formats .filter((format) => format !== "HTML") .map((format) => ( ))} {rfc.errataUrl ? ( ) : null} {rfc.doi ? ( ) : null} ); } export default function SearchRfc() { const cached = useMemo(readCachedIndex, []); const [rfcs, setRfcs] = useState(cached?.rfcs ?? []); const [query, setQuery] = useState(""); const [status, setStatus] = useState("all"); const [isLoading, setIsLoading] = useState(!cached); const [error, setError] = useState(); const loadIndex = useCallback(async (force = false, signal?: AbortSignal) => { const existing = readCachedIndex(); if (!force && existing && Date.now() - existing.updatedAt < MAX_CACHE_AGE) { setRfcs(existing.rfcs); setIsLoading(false); return; } setIsLoading(true); setError(undefined); try { const response = await fetch(RFC_INDEX_URL, { signal }); if (!response.ok) throw new Error(`RFC Editor returned HTTP ${response.status}`); const nextRfcs = parseRfcIndex(await response.text()); const nextCache = { updatedAt: Date.now(), rfcs: nextRfcs }; cache.set(CACHE_KEY, JSON.stringify(nextCache)); setRfcs(nextRfcs); } catch (cause) { if (cause instanceof DOMException && cause.name === "AbortError") return; const message = cause instanceof Error ? cause.message : "Unknown error"; if (existing) { setRfcs(existing.rfcs); await showToast( Toast.Style.Failure, "Couldn't refresh the RFC index", "Showing cached data", ); } else { setError(message); } } finally { setIsLoading(false); } }, []); useEffect(() => { const controller = new AbortController(); void loadIndex(false, controller.signal); return () => controller.abort(); }, [loadIndex]); const results = useMemo( () => searchRfcs(rfcs, query, status), [rfcs, query, status], ); const refresh = () => void loadIndex(true); return ( 0} filtering={false} throttle searchText={query} onSearchTextChange={setQuery} searchBarPlaceholder="Search by RFC number, title, author, keyword, or abstract…" searchBarAccessory={ setStatus(value as StatusFilter)} > {STATUS_OPTIONS.map(([value, label]) => ( ))} } > {error ? ( } /> ) : !isLoading && results.length === 0 ? ( ) : ( {results.map((rfc) => ( } actions={} /> ))} )} ); }