/** Server-layer projection cache keyed by path, mtime, and size. */ import { DocumentObject, OutlineNode } from "@adeu/core"; import type { ProjectionBundle } from "./response-builders.js"; export type ProgressFn = (message: string, progress: number, total: number) => void | Promise; /** * Boundary-owned loader: the handler passes its loadDocxOrThrow wrapper so * container errors keep their agent-facing diagnosis (QA 2026-07-23 F19) * and the cache stays free of error-shaping policy. */ export type LoadDocFn = (buf: Buffer, opts?: Parameters[1]) => Promise; export interface DocCacheEntry { key: string; file_path: string; /** Raw projection, includeAppendix=true — what full/search/appendix modes read. */ raw_text: string; raw_bundle: ProjectionBundle; outline_nodes: OutlineNode[]; /** Clean projection (accepted view); null until the background fill lands. */ clean_text: string | null; /** Lazily derived on the first clean_view paginated/search read. */ clean_bundle: ProjectionBundle | null; /** Clean-view heading map; lazily derived (see ensureCleanOutline). */ clean_outline_nodes: OutlineNode[] | null; /** In-flight background clean fill; ensureCleanText awaits it. */ clean_fill: Promise | null; /** Set by ensureCleanText: skip the quiet-period wait and fill NOW. */ _fill_forced?: boolean; } export declare class DocCache { private maxEntries; private hotTtlMs; private entries; private inflight; /** Observability for tests: how many full ingests ran. */ ingest_count: number; /** Timestamp of the most recent cache request — the clean fill's * quietness signal (the extraction is one synchronous block, so it must * not start while requests are actively arriving). */ private lastTouch; /** * Hot-DOM slot (docs/PERFORMANCE.md §5 "stop re-parsing"): the parsed * DocumentObject of the most recently ingested/edited document version, * kept so the NEXT operation on the same version (typically a * process_document_batch right after a read, or a chained edit on a batch * output) skips the multi-second disk parse entirely. * * Single slot, consume-on-take (an edit mutates the DOM, so it can never * be shared), TTL-evicted, and guarded by a heap-headroom check so a * multi-GB DOM is only pinned when there is room for it. */ private hot; private hotTimer; /** Test observability: how many edits reused a hot DOM. */ hot_hits: number; /** Priming jobs by key: a read arriving mid-prime joins instead of * re-ingesting from disk. */ private priming; constructor(maxEntries?: number, hotTtlMs?: number); /** * Heap headroom check for the pin-release VALVE. Pinning a DOM never * grows the heap (it only delays release of memory the ingest already * allocated), so storing is always safe; the hazard is holding a pinned * multi-GB tree WHILE a new ingest allocates another one. Before any * ingest, the pin is dropped when headroom is low. */ private heapHasRoom; /** Drop the pinned DOM if a new multi-GB allocation needs the room. */ private releaseHotIfPressured; private storeHotDoc; /** Attach a background job to the hot slot iff it still holds `doc`. */ private registerHotJob; /** * Hand the parsed DOM of the CURRENT version of `file_path` to a mutating * consumer (the edit path), or null when the slot holds a different * version/file. Consume-on-take: the slot is cleared; all background jobs * reading the DOM are forced to completion first. */ takeHotDoc(file_path: string): Promise; /** * Put a DOM back in the slot after an operation that provably left it * equal to the on-disk file (a rolled-back failed batch — restored via * the engine's transactional snapshot). * * "Provably" is the caller's obligation, and it is not satisfied by the * operation merely having thrown: the engine reports whether its rollback * verified (RedlineEngine.rollback_verified), and only that answer makes a * post-failure DOM safe to re-pin. Pinning an unverified one publishes a * half-applied document under the UNCHANGED file's cache key, where the * next call picks it up as if it had come from disk. */ restoreHotDoc(file_path: string, doc: DocumentObject): void; /** Stat-derived identity of the CURRENT file version. */ private keyFor; /** * Drop every cached product for `resolvedPath`, whatever version it was * keyed under. * * `store()` already keeps one live version per path, but only when a NEW * version is stored. That is exactly what a colliding key defeats: when a * rewrite preserves both mtime and size — a size-neutral edit inside one * filesystem timestamp tick — no new entry is ever built, so `get()` keeps * returning the pre-write entry (its `entries` hit-check runs before the * `priming` join). Writers must therefore evict explicitly. * * Entries carry the resolved path, so they are matched exactly, as in * `store()`. The hot/priming/inflight maps are keyed by string only, and * keyFor always emits `${resolvedPath}|…`, so a whole-prefix compare is * exact there — a '|' inside a POSIX filename can neither miss nor * over-match. */ private purgePath; /** * Forget everything cached for `file_path`. Callers that write a document * without handing the post-write DOM to primeFromDoc MUST call this, or a * later read can be served the pre-write projection. */ invalidate(file_path: string): void; /** * Returns the products for the current version of `file_path`, ingesting * at most once per version (single-flight). `readBytes` is called only on * a miss and owns the file-not-found error shape; `onProgress` is invoked * only during a cold ingest. */ get(file_path: string, readBytes: () => Buffer, loadDoc: LoadDocFn, onProgress?: ProgressFn): Promise; private ingest; /** * Products of one loaded document: shared by the disk-ingest path and by * primeFromDoc (which starts from the in-memory post-edit document). * * Deliberately does NOT pin the DOM or schedule the clean fill — the caller * decides. When primeFromDoc's deferred job runs, it may be running because * takeHotDoc forced it, and takeHotDoc has already cleared the hot slot and * is about to hand this DOM to a mutating consumer. Pinning here would * re-publish a DOM that is about to be edited, and scheduleCleanFill's * registerHotJob would silently no-op (the slot is null), leaving an * unguarded fill that later extracts the half-edited document and stores it * as the clean projection of the PRE-edit version. */ private buildEntry; /** * Background warm-up of the clean view AFTER the caller's response is * flushed. The clean extraction is ONE synchronous block (seconds on a * huge document), so it must not start while requests are still * arriving — the VVBIG bench caught an innocent warm page-turn stalling * 2.1 s behind it. Wait for a quiet period (no cache request for * QUIET_MS), bounded by MAX_WAIT_MS; ensureCleanText sets _fill_forced * to skip the wait — the clean_view requester pays for clean view, * nobody else does. Failures leave clean_text null — the on-demand path * in ensureCleanText rebuilds from bytes instead. */ private scheduleCleanFill; /** * After a successful batch write: adopt the in-memory post-edit document * as the cache state of the OUTPUT file. The DOM is pinned for chained * edits immediately; the text products are built after a quiet period * (forced early if a read arrives and joins via the priming map). The * agent's read-after-edit therefore never re-parses what the server just * had in memory. * * SAFETY GATE: primed products must byte-equal what a fresh parse of the * written file would produce — guaranteed by the deterministic * serialize→parse round-trip and enforced by the prime-equivalence tests. */ primeFromDoc(file_path: string, doc: DocumentObject): void; /** * Clean-view text for an entry: already warm, else await the in-flight * background fill, else (fill failed / entry from a crashed fill) rebuild * from bytes without caching a DOM. */ ensureCleanText(entry: DocCacheEntry, readBytes: () => Buffer, loadDoc: LoadDocFn): Promise; /** Clean-view bundle, derived once from the clean text. */ ensureCleanBundle(entry: DocCacheEntry, readBytes: () => Buffer, loadDoc: LoadDocFn): Promise; /** * Clean-view heading map. The raw outline cannot stand in for it: its page * numbers and CriticMarkup-bearing heading text describe a different * projection. Python fills outline nodes per VIEW (doc_cache.py:155-188), so * both engines' clean-view readers get a real heading map. * * Costs a parse: the paragraph-offset map the extractor needs references * live elements from ITS OWN parse, so it can never be derived from the * cached clean string. Cached on the entry, so at most once per version. */ ensureCleanOutline(entry: DocCacheEntry, readBytes: () => Buffer, loadDoc: LoadDocFn): Promise; private store; clear(): void; } /** Process-wide cache: one stdio server serves one session. */ export declare const docCache: DocCache; //# sourceMappingURL=doc-cache.d.ts.map