Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
import type { Clock } from "@suaveplan/types";
export interface PermissionCacheOptions {
/** Entry TTL in milliseconds. */
readonly ttl: number;
/** Maximum number of entries (default: 1 000). */
readonly maxSize?: number;
readonly clock?: Clock;
}
interface CacheEntry {
value: boolean;
expiresAt: number;
}
/**
* LRU-inspired TTL cache for permission check results.
*
* Stores `boolean` results keyed by a composite string (typically
* `${userId}:${resource}:${action}`). Supports per-user and global
* invalidation. Injectable clock for deterministic tests.
*/
export class PermissionCache {
protected readonly store: Map<string, CacheEntry> = new Map();
protected readonly clock: Clock;
protected readonly ttl: number;
protected readonly maxSize: number;
constructor(options: PermissionCacheOptions) {
this.ttl = options.ttl;
this.maxSize = options.maxSize ?? 1_000;
this.clock = options.clock ?? (() => Date.now());
}
/** Retrieve a cached result, or `undefined` if absent or expired. */
get(key: string): boolean | undefined {
const entry = this.store.get(key);
if (entry === undefined) return undefined;
if (entry.expiresAt <= this.clock()) {
this.store.delete(key);
return undefined;
}
return entry.value;
}
/** Store a result with TTL. Evicts the oldest entry when full. */
set(key: string, value: boolean): void {
if (this.store.size >= this.maxSize && !this.store.has(key)) {
// Evict oldest entry
const firstKey = this.store.keys().next().value;
// Map is non-empty (size >= maxSize > 0) so firstKey is always defined
this.store.delete(firstKey as string);
}
this.store.set(key, {
value,
expiresAt: this.clock() + this.ttl,
});
}
/** Invalidate all entries whose key starts with `userId:`. */
invalidate(userId: string): void {
const prefix = `${userId}:`;
for (const key of this.store.keys()) {
if (key.startsWith(prefix)) {
this.store.delete(key);
}
}
}
/** Remove all entries from the cache. */
invalidateAll(): void {
this.store.clear();
}
/** Current number of entries (including expired). */
get size(): number {
return this.store.size;
}
}