| 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 | /** |
| 13 | * Byte-bounded LRU whose active selection entries may be pinned. Entries are |
| 14 | * addressed by source content, not by row id: a parse is a pure function of |
| 15 | * its text, and row ids differ between the live and history projections. |
| 16 | */ |
| 17 | export class TranscriptMarkdownCache { |
| 18 | private readonly entries = new Map<number, { value: ParsedMarkdownValue; bytes: number }>(); |
| 19 | private readonly pins = new Map<number, number>(); |
| 20 | bytes = 0; |
| 21 | evictions = 0; |
| 22 | |
| 23 | constructor(readonly budgetBytes: number) {} |
| 24 | |
| 25 | get(source: string, revision: number): ParsedMarkdownValue | undefined { |
| 26 | const entry = this.entries.get(revision); |
| 27 | if (!entry || entry.value.source !== source) return undefined; |
| 28 | this.entries.delete(revision); |
| 29 | this.entries.set(revision, entry); |
| 30 | return entry.value; |
| 31 | } |
| 32 | |
| 33 | set(revision: number, value: ParsedMarkdownValue): void { |
| 34 | const previous = this.entries.get(revision); |
| 35 | if (previous) this.bytes -= previous.bytes; |
| 36 | const bytes = Math.max(0, value.bytes); |
| 37 | this.entries.set(revision, { value, bytes }); |
| 38 | this.bytes += bytes; |
| 39 | this.enforceBudget(); |
| 40 | } |
| 41 | |
| 42 | pin(revision: number): () => void { |
| 43 | this.pins.set(revision, (this.pins.get(revision) ?? 0) + 1); |
| 44 | let released = false; |
| 45 | return () => { |
| 46 | if (released) return; |
| 47 | released = true; |
| 48 | const count = this.pins.get(revision) ?? 0; |
| 49 | if (count <= 1) this.pins.delete(revision); |
| 50 | else this.pins.set(revision, count - 1); |
| 51 | this.enforceBudget(); |
| 52 | }; |
| 53 | } |
| 54 | |
| 55 | size(): number { |
| 56 | return this.entries.size; |
| 57 | } |
| 58 | |
| 59 | private enforceBudget(): void { |
| 60 | while (this.bytes > this.budgetBytes && this.entries.size > 1) { |
| 61 | const victimKey = Array.from(this.entries.keys()).find((key) => !this.pins.has(key)); |
| 62 | if (victimKey === undefined) break; |
| 63 | const victim = this.entries.get(victimKey); |
| 64 | if (victim) this.bytes -= victim.bytes; |
| 65 | this.entries.delete(victimKey); |
| 66 | this.evictions += 1; |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 |