/** * Catalog acquisition: remote fetch with conditional requests, an on-disk * cache, and the seed snapshot shipped inside the package. * * Availability is layered so the marketplace is never blank: remote → cache → * seed. A failed refresh keeps whatever is already loaded; only a well-formed, * schema-compatible document ever replaces it. */ import { type Catalog, type CatalogSource } from './types.ts'; /** What the registry currently holds and where it came from. */ export interface CatalogState { readonly catalog: Catalog; readonly source: CatalogSource; /** The remote published a schema newer than this build understands. */ readonly upgradeRequired: boolean; } /** Construction inputs for {@link CatalogRegistry}. */ export interface RegistryOptions { /** Published catalog URL; empty disables remote refresh entirely. */ readonly registryUrl: string; /** Directory for the on-disk cache. */ readonly stateDir: string; readonly warn: (line: string) => void; } /** * Coerce an unknown value into a catalog. * * Remote data is untrusted input: a 200 with the wrong shape must be refused * rather than allowed to render as a broken list. Entries that fail validation * are dropped individually so one malformed row cannot void the whole feed. * @param value - parsed JSON from the network or the cache file. * @returns the catalog, or undefined when it is unusable. */ export declare function parseCatalog(value: unknown): Catalog | undefined; /** * Resolve a configured source to a local path, when it names one. * * The published catalog will eventually be an HTTPS URL, but the crawler runs * on the user's own machine and writes `data/v1/catalog.json` there. Pointing * `registryUrl` straight at that file closes the loop without a server, and * without waiting for the repository to go public. * @param source - the configured `registryUrl`. * @returns the filesystem path, or undefined when this is a network source. */ export declare function localSourcePath(source: string): string | undefined; /** Holds the current catalog and refreshes it from the published source. */ export declare class CatalogRegistry { private readonly options; private state; private etag; private inFlight; private readonly cachePath; constructor(options: RegistryOptions); /** The catalog currently served to the UI. */ snapshot(): CatalogState; /** * Refresh from the published URL. * * Concurrent calls share one request. Any failure — network, timeout, bad * shape, incompatible schema — leaves the current state untouched. * @returns the state after the attempt. */ refresh(): Promise; /** * Decide where this refresh should read from. * @returns the configured source, the local build, or undefined for neither. */ private resolveSource; /** * Perform one conditional fetch and adopt the result when it is usable. * @returns the state after the attempt. */ private fetchOnce; /** * Adopt a catalog the crawler wrote to this machine. * * mtime plays the part ETag plays for a network source: an unchanged file is * not re-parsed, which matters because the full document is several * megabytes and the refresh timer fires unattended. * @param path - the catalog file. * @returns the state after the attempt. */ private readLocalSource; /** * Load the best locally available catalog: cache first, then seed. * @returns the initial state. */ private loadLocal; /** * Read and validate a catalog document from disk. * @param path - the file to read. * @returns the catalog, or undefined when absent or unusable. */ private readJsonFile; /** * Persist a catalog to the cache, atomically. * @param catalog - the document to store. */ private writeCache; }