Files
skills/docs/identity/auth/permissions/src/cache/permission-cache/permission-cache.md
2026-05-29 08:20:56 +01:00

3.5 KiB

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

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

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).