Initial Commit
This commit is contained in:
79
packages/identity/auth/permissions/src/cache/permission-cache/permission-cache.ts
vendored
Normal file
79
packages/identity/auth/permissions/src/cache/permission-cache/permission-cache.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user