# permission-cache _(no submodules)_ ## Purpose `PermissionCache` provides a TTL-bounded, size-capped in-process cache for permission check results in `@suaveplan/permissions`. Permission checks can be expensive — they may traverse multi-level role inheritance chains and evaluate ABAC conditions — so caching their boolean outcome for frequently-checked user/resource/action triples significantly reduces latency on hot paths. The cache accepts an injectable `Clock` for deterministic testing: advance the fake clock to simulate TTL expiry without wall-clock waits. ## Features - TTL-based expiry: entries older than `ttl` ms are evicted on read - Size-capped: when full, the oldest entry is evicted before inserting a new one - `invalidate(userId)` — invalidates all entries for a specific user by key prefix - `invalidateAll()` — clears the entire cache (e.g. on role assignment change) - `size` getter — returns the current entry count (including not-yet-evicted expired entries) - Injectable `Clock` for deterministic test control ## Usage: Basic Example ```ts import { PermissionCache } from "@suaveplan/permissions"; const cache = new PermissionCache({ ttl: 60_000 }); cache.set("u1:posts:read", true); console.log(cache.get("u1:posts:read")); // true cache.invalidate("u1"); console.log(cache.get("u1:posts:read")); // undefined — invalidated ``` ## Usage: Advanced Example ```ts import { PermissionCache } from "@suaveplan/permissions"; let tick = 0; const cache = new PermissionCache({ ttl: 1000, clock: () => tick }); cache.set("u1:posts:read", true); tick = 999; console.log(cache.get("u1:posts:read")); // true — not yet expired tick = 1001; console.log(cache.get("u1:posts:read")); // undefined — expired // With PolicyEngine import { PolicyEngine, RoleManager } from "@suaveplan/permissions"; import { MemoryAdapter } from "@suaveplan/permissions"; const adapter = new MemoryAdapter(); const roleManager = new RoleManager(adapter); const policyEngine = new PolicyEngine( new PermissionManager(adapter), { cache: new PermissionCache({ ttl: 30_000 }) }, ); ``` ## API Reference ### `constructor(options: PermissionCacheOptions)` - `options.ttl` — entry TTL in milliseconds (required) - `options.maxSize` — maximum entry count before eviction (default: 1 000) - `options.clock` — injectable time source (default: `Date.now`) ### `get(key: string): boolean | undefined` Returns the cached value if present and not expired, or `undefined`. Auto-evicts expired entries on read. ### `set(key: string, value: boolean): void` Stores an entry. If the cache is at capacity, evicts the oldest entry first. ### `invalidate(userId: string): void` Removes all entries whose key starts with `userId:`. Use after role assignment changes. ### `invalidateAll(): void` Clears all entries. ### `size: number` Current entry count. May include entries that are expired but not yet read (lazy eviction). ## Implementation Notes The eviction strategy is "evict the oldest-inserted entry" (Map insertion order) rather than true LRU. This keeps the implementation O(1) for all operations without the overhead of a doubly-linked list. For most permission-check workloads — where the hot users are also the most recently active — insertion-order eviction provides good cache efficiency. The `invalidate(userId)` method iterates all keys but the cache is bounded by `maxSize`, so the worst case is O(maxSize) — acceptable for typical values (≤10 000).