| 1 | import type { MarkdownBlock } from "./markdownPipeline"; |
| 2 | |
| 3 | export interface ParsedMarkdownValue { |
| 4 | source: string; |
| 5 | blocks: MarkdownBlock[]; |
| 6 | selectionText: string; |
| 7 | selectionRevision: number; |
| 8 | /** Source/projection UTF-16 bytes plus the estimated HAST weight. */ |
| 9 | bytes: number; |
| 10 | } |
| 11 | |
| 12 | /** Byte-bounded LRU whose active selection entries may be pinned. */ |
| 13 | export class TranscriptMarkdownCache { |
| 14 | private readonly entries = new Map<string, { value: ParsedMarkdownValue; bytes: number }>(); |
| 15 | private readonly pins = new Map<string, number>(); |
| 16 | bytes = 0; |
| 17 | evictions = 0; |
| 18 | |
| 19 | constructor(readonly budgetBytes: number) {} |
| 20 | |
| 21 | private key(entryId: string, revision: number): string { |
| 22 | return `${entryId}@${revision}`; |
| 23 | } |
| 24 | |
| 25 | get(entryId: string, revision: number): ParsedMarkdownValue | undefined { |
| 26 | const key = this.key(entryId, revision); |
| 27 | const entry = this.entries.get(key); |
| 28 | if (!entry) return undefined; |
| 29 | this.entries.delete(key); |
| 30 | this.entries.set(key, entry); |
| 31 | return entry.value; |
| 32 | } |
| 33 | |
| 34 | set(entryId: string, revision: number, value: ParsedMarkdownValue): void { |
| 35 | const key = this.key(entryId, revision); |
| 36 | const previous = this.entries.get(key); |
| 37 | if (previous) this.bytes -= previous.bytes; |
| 38 | const bytes = Math.max(0, value.bytes); |
| 39 | this.entries.set(key, { value, bytes }); |
| 40 | this.bytes += bytes; |
| 41 | this.enforceBudget(); |
| 42 | } |
| 43 | |
| 44 | pin(entryId: string, revision: number): () => void { |
| 45 | const key = this.key(entryId, revision); |
| 46 | this.pins.set(key, (this.pins.get(key) ?? 0) + 1); |
| 47 | let released = false; |
| 48 | return () => { |
| 49 | if (released) return; |
| 50 | released = true; |
| 51 | const count = this.pins.get(key) ?? 0; |
| 52 | if (count <= 1) this.pins.delete(key); |
| 53 | else this.pins.set(key, count - 1); |
| 54 | this.enforceBudget(); |
| 55 | }; |
| 56 | } |
| 57 | |
| 58 | size(): number { |
| 59 | return this.entries.size; |
| 60 | } |
| 61 | |
| 62 | private enforceBudget(): void { |
| 63 | while (this.bytes > this.budgetBytes && this.entries.size > 1) { |
| 64 | const victimKey = Array.from(this.entries.keys()).find((key) => !this.pins.has(key)); |
| 65 | if (!victimKey) break; |
| 66 | const victim = this.entries.get(victimKey); |
| 67 | if (victim) this.bytes -= victim.bytes; |
| 68 | this.entries.delete(victimKey); |
| 69 | this.evictions += 1; |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 |