// One ArchiveReader per origin, for the code that reads an archive from a // BROWSER. // // The viewer's component caches are keyed by origin ("" = same-origin, a full // origin like "https://x.example" for a federated hub member — see // components/originId.ts). Each one used to carry its own hand-written walk of // the shard scheme; now each one calls a RemoteSource from here, so the URL // shape is the contract's and the promise-coalescing/LRU behaviour is the // reader's rather than eight near-copies of it. // // WHY THIS LIVES IN lib/archive/ AND NOT components/: it is the reader // registry, not a React concern — no hooks, no JSX, no node imports — and a new // components/*.ts file would need its own `exports` entry in common's // package.json. lib/* is wildcarded, so this file costs nothing to reach from // editor/ or export/. // // RemoteSource with an EMPTY base is the same-origin case: archiveUrl() leaves a // path root-relative when there is no base, which is byte-for-byte the // `${idBaseUrl(origin)}/transcripts/…` the caches built before. So the // single-site export app's URLs do not move at all. import { RemoteSource, type ChannelRef } from "./reader"; // A BROWSER'S page-cache budget, which is not a server's. // // RemoteSource defaults to 48 MB (pageCacheBudgetBytes) and holds TWO page // caches — transcripts and subs — so an origin costs 96 MB. That is a sane // ceiling for one long-lived MCP process reading one corpus. It is not one for // a tab, and it is emphatically not one for a hub page, which holds a reader // per member site with no shared ceiling and no way for a browser to turn the // env knob down. // // 8 MB, so an origin is 16 MB and five federated members are 80 MB rather than // 480 MB. The reason it can be this small without costing reads: the viewer's // caches memoise every RECORD they have ever seen (transcriptCache.resolved, // plus IndexedDB), so an evicted page is only re-fetched for a video nobody has // opened yet — the case that was going to be a fetch anyway. PageCache's // MIN_CACHED_PAGES floor still keeps two entries even when one page of a // VOD-sized corpus exceeds the whole budget on its own. // // The MCP path does NOT come through here: mcp/src/source.ts constructs // RemoteSource directly and keeps the 48 MB default, so no bench counter moves. const BROWSER_PAGE_CACHE_BYTES = 8 * 1024 * 1024; const byOrigin = new Map(); // The reader for an origin, created on first use and kept for the life of the // page — the readers hold the manifest/page caches, so a fresh one per call // would be a fresh cache per call. export function readerFor(origin = ""): RemoteSource { let reader = byOrigin.get(origin); if (reader === undefined) { reader = new RemoteSource(origin, BROWSER_PAGE_CACHE_BYTES); byOrigin.set(origin, reader); } return reader; } // A minimal ChannelRef for the per-channel reader calls. The viewer addresses a // channel by slug alone; `key`/`name` exist for the MCP's channel list and are // not read on this path. Reader caches key off `slug`, so a fresh object per // call is free. export function channelRef(slug: string, origin = ""): ChannelRef { return origin ? { key: slug, slug, name: slug, siteUrl: origin } : { key: slug, slug, name: slug }; } // Drop every reader and its caches. For tests, and for any future "the archive // was rebuilt under us" reset — not called on a normal page. export function resetReaders(): void { byOrigin.clear(); }