pyreon

@pyreon/sized-map

A bounded-Map<K, V> primitive that evicts the oldest entry when a size cap is exceeded. Single implementation, two configurations — FIFO or LRU-on-read.

Install

bun add @pyreon/sized-map

Why

Bounded maps are the standard fix for "Class C — unbounded caches" leaks (see Memory Leak Classes). Before this package, 9 Pyreon files each carried their own ~10-line FIFO eviction snippet — same map.keys().next().value + map.delete(oldest) shape, but each grown with its own subtle bugs over time. @pyreon/sized-map collapses the 9 implementations to one. The eviction code lives in exactly one place.

Used internally by:

  • @pyreon/runtime-dom_tplCache (FIFO; template cloneNode cache)

  • @pyreon/styler — class-string splitter, elClassCache

  • @pyreon/router — loader cache

  • @pyreon/lintAstCache (LRU-on-read)

  • @pyreon/zero — ISR createMemoryStore (LRU-on-read)

  • @pyreon/rocketstyle — theme memo (LRU-on-read)

  • @pyreon/elements — element-bundle intern

The package is published independently and safe to use directly.

Two modes

Modelru option.get() behaviourEviction dropsFits
FIFOfalse (default)does NOT touch orderingfirst-inserted entryhot paths where re-insert-on-read would dominate (template cache, route cache)
LRU-on-readtruemoves entry to the tail (most-recently-used)least-recently-used entrystandard cache semantics (AST cache, theme memo, ISR store)

.set() ALWAYS treats a key collision as a recency hit in both modes — the old entry is removed and the new entry is appended at the tail. Callers depend on this to keep "just-written" entries from being evicted on the very next call.

Quick start

import { SizedMap } from '@pyreon/sized-map'

// FIFO — hot path
const tplCache = new SizedMap<string, HTMLTemplateElement>({ maxEntries: 1024 })

tplCache.set('div.foo', tpl)
tplCache.get('div.foo') // tpl — does not reorder

// LRU-on-read — frequently-read entries survive cap pressure
const themeMemo = new SizedMap<string, RsMemoEntry>({
  maxEntries: 128,
  lru: true,
})

themeMemo.set('primary|md', entry)
themeMemo.get('primary|md') // entry — bumped to MRU; survives the next eviction

API

interface SizedMapOptions {
  /** Max entries before oldest is evicted. */
  maxEntries: number
  /**
   * When true, `.get()` moves the entry to the tail (LRU).
   * When false, `.get()` does NOT touch the entry (pure FIFO).
   * Default: false.
   */
  lru?: boolean
}

class SizedMap<K, V> {
  constructor(opts: SizedMapOptions)
  get(key: K): V | undefined
  set(key: K, value: V): void
  delete(key: K): boolean
  has(key: K): boolean
  clear(): void
  readonly size: number
  keys(): IterableIterator<K>
  values(): IterableIterator<V>
  entries(): IterableIterator<[K, V]>
  [Symbol.iterator](): IterableIterator<[K, V]>
}

Iteration order

Both modes iterate in insertion order (oldest first → newest last). Map's native iteration order is preserved across .set()/.delete(). For LRU-on-read, .get() re-orders the entry to the tail, which IS visible to subsequent iteration.

Choosing FIFO vs LRU-on-read

Default to FIFO unless you have evidence that re-ordering on read is worth the write. Each .get() in LRU mode performs an internal delete + set to move the entry — measurable cost in tight loops. Use LRU only when read frequency genuinely correlates with future access (e.g. AST cache where 80% of files are read repeatedly during a session).

For high-churn caches where most reads are followed by a write anyway (template cache, route cache), FIFO is strictly cheaper and behaviourally equivalent.

When NOT to reach for @pyreon/sized-map

  • Tiny caps (≤16) — a plain Map + manual eviction may be smaller code than the import. Use this package when the cap is ≥32 entries OR the cache is shared across multiple call sites.

  • TTL-based evictionSizedMap is size-bounded, not time-bounded. For TTL-based caches (revalidation, ISR), pair this with a per-entry timestamp or use @pyreon/zero's createISRHandler cache.

  • WeakMap semantics — when entries should be eligible for GC when the key has no other references, use WeakMap (no eviction needed, no cap).

See also

@pyreon/sized-map