11import { HEX_TABLE } from "#src/helpers/buffer" ;
22import { CHAR_HASH , DELIMITERS , WHITESPACE } from "#src/helpers/chars" ;
3- import { LRUCache } from "#src/helpers/lru-cache" ;
43import type { ByteWriter } from "#src/io/byte-writer" ;
54
65import type { PdfPrimitive } from "./pdf-primitive" ;
@@ -60,37 +59,52 @@ function escapeName(name: string): string {
6059}
6160
6261/**
63- * Default cache size for PdfName interning.
64- * Can be overridden via PdfName.setCacheSize().
65- */
66- const DEFAULT_NAME_CACHE_SIZE = 10000 ;
67-
68- /**
69- * PDF name object (interned).
62+ * PDF name object (interned via WeakRef).
7063 *
7164 * In PDF: `/Type`, `/Page`, `/Length`
7265 *
73- * Names are interned using an LRU cache to prevent unbounded memory growth.
74- * `PdfName.of("Type") === PdfName.of("Type")` as long as both are in cache.
75- * Use `.of()` to get or create instances.
66+ * Names are interned using a WeakRef cache: as long as any live object
67+ * (e.g. a PdfDict key) holds a strong reference to a PdfName, calling
68+ * `PdfName.of()` with the same string returns the *same instance*.
69+ * Once all strong references are dropped, the GC may collect the
70+ * PdfName and a FinalizationRegistry cleans up the cache entry.
71+ *
72+ * This avoids the correctness bug of LRU-based caching, where eviction
73+ * of a still-referenced name would break Map key identity in PdfDict.
7674 *
77- * Common PDF names (Type, Page, etc.) are pre-cached and always available.
75+ * Common PDF names (Type, Page, etc.) are held as static fields and
76+ * therefore never collected.
7877 */
7978export class PdfName implements PdfPrimitive {
8079 get type ( ) : "name" {
8180 return "name" ;
8281 }
8382
84- private static cache = new LRUCache < string , PdfName > ( { max : DEFAULT_NAME_CACHE_SIZE } ) ;
83+ /** WeakRef cache for interning. Entries are cleaned up by the FinalizationRegistry. */
84+ private static cache = new Map < string , WeakRef < PdfName > > ( ) ;
85+
86+ /** Cleans up dead WeakRef entries from the cache when a PdfName is GC'd. */
87+ private static registry = new FinalizationRegistry < string > ( name => {
88+ const ref = PdfName . cache . get ( name ) ;
89+
90+ // Only delete if the entry is actually dead — a new instance for the
91+ // same name may have been inserted since the old one was collected.
92+ if ( ref && ref . deref ( ) === undefined ) {
93+ PdfName . cache . delete ( name ) ;
94+ }
95+ } ) ;
8596
8697 /**
87- * Pre-cached common names that should never be evicted.
88- * These are stored separately from the LRU cache.
98+ * Pre-cached common names that are always available.
99+ * These are stored as static readonly fields, so they always have
100+ * strong references and their WeakRefs never die.
89101 */
90102 private static readonly permanentCache = new Map < string , PdfName > ( ) ;
91103
92104 // Common PDF names (pre-cached in permanent cache)
105+ // -- Document structure --
93106 static readonly Type = PdfName . createPermanent ( "Type" ) ;
107+ static readonly Subtype = PdfName . createPermanent ( "Subtype" ) ;
94108 static readonly Page = PdfName . createPermanent ( "Page" ) ;
95109 static readonly Pages = PdfName . createPermanent ( "Pages" ) ;
96110 static readonly Catalog = PdfName . createPermanent ( "Catalog" ) ;
@@ -100,9 +114,25 @@ export class PdfName implements PdfPrimitive {
100114 static readonly MediaBox = PdfName . createPermanent ( "MediaBox" ) ;
101115 static readonly Resources = PdfName . createPermanent ( "Resources" ) ;
102116 static readonly Contents = PdfName . createPermanent ( "Contents" ) ;
117+ static readonly Annots = PdfName . createPermanent ( "Annots" ) ;
118+ // -- Trailer / xref --
119+ static readonly Root = PdfName . createPermanent ( "Root" ) ;
120+ static readonly Size = PdfName . createPermanent ( "Size" ) ;
121+ static readonly Info = PdfName . createPermanent ( "Info" ) ;
122+ static readonly Prev = PdfName . createPermanent ( "Prev" ) ;
123+ static readonly ID = PdfName . createPermanent ( "ID" ) ;
124+ static readonly Encrypt = PdfName . createPermanent ( "Encrypt" ) ;
125+ // -- Streams --
103126 static readonly Length = PdfName . createPermanent ( "Length" ) ;
104127 static readonly Filter = PdfName . createPermanent ( "Filter" ) ;
105128 static readonly FlateDecode = PdfName . createPermanent ( "FlateDecode" ) ;
129+ // -- Fonts / resources --
130+ static readonly Font = PdfName . createPermanent ( "Font" ) ;
131+ static readonly BaseFont = PdfName . createPermanent ( "BaseFont" ) ;
132+ static readonly Encoding = PdfName . createPermanent ( "Encoding" ) ;
133+ static readonly XObject = PdfName . createPermanent ( "XObject" ) ;
134+ // -- Name trees --
135+ static readonly Names = PdfName . createPermanent ( "Names" ) ;
106136
107137 /** Cached serialized form (e.g. "/Type"). Computed lazily on first toBytes(). */
108138 private cachedBytes : Uint8Array | null = null ;
@@ -114,21 +144,31 @@ export class PdfName implements PdfPrimitive {
114144 * The leading `/` should NOT be included.
115145 */
116146 static of ( name : string ) : PdfName {
117- // Check permanent cache first (common names)
147+ // Check permanent cache first (common names — always alive )
118148 const permanent = PdfName . permanentCache . get ( name ) ;
149+
119150 if ( permanent ) {
120151 return permanent ;
121152 }
122153
123- // Check LRU cache
124- let cached = PdfName . cache . get ( name ) ;
154+ // Check WeakRef cache
155+ const ref = PdfName . cache . get ( name ) ;
156+
157+ if ( ref ) {
158+ const existing = ref . deref ( ) ;
125159
126- if ( ! cached ) {
127- cached = new PdfName ( name ) ;
128- PdfName . cache . set ( name , cached ) ;
160+ if ( existing ) {
161+ return existing ;
162+ }
129163 }
130164
131- return cached ;
165+ // Create new instance, store WeakRef, register for cleanup
166+ const instance = new PdfName ( name ) ;
167+
168+ PdfName . cache . set ( name , new WeakRef ( instance ) ) ;
169+ PdfName . registry . register ( instance , name ) ;
170+
171+ return instance ;
132172 }
133173
134174 /**
@@ -144,7 +184,9 @@ export class PdfName implements PdfPrimitive {
144184 }
145185
146186 /**
147- * Get the current size of the LRU cache.
187+ * Get the current number of entries in the WeakRef cache.
188+ * This includes entries whose targets may have been GC'd but whose
189+ * FinalizationRegistry callbacks haven't run yet.
148190 */
149191 static get cacheSize ( ) : number {
150192 return PdfName . cache . size ;
0 commit comments