/** * Cache + in-flight invalidation logic, extracted from the Engine class * so the class file stays focused on the eval pipeline. Every function * here takes the caches + invalidator explicitly so it can be unit-tested * without standing up a full Engine. */ import type { IamLRUCache } from '../../shared/cache' import type { AccessControl, IamRequest } from '../types' import type { IamEngineTypes } from './engine.types' export interface IEngineCacheBag { policyCache: IamLRUCache roleCache: IamLRUCache rbacPolicyCache: IamLRUCache mergedPolicyCache: IamLRUCache subjectCache: IamLRUCache inFlight: IEngineInFlightBag invalidator?: IamEngineTypes.IInvalidator } export interface IEngineInFlightBag { policies: { value: Promise | null } roles: { value: Promise | null } rbac: { value: Promise | null } merged: { value: Promise | null } subjects: Map> } export function invalidateAll(bag: IEngineCacheBag, opts: { broadcast?: boolean }): void { bag.policyCache.clear() bag.roleCache.clear() bag.rbacPolicyCache.clear() bag.subjectCache.clear() bag.inFlight.policies.value = null bag.inFlight.roles.value = null bag.inFlight.rbac.value = null bag.inFlight.merged.value = null bag.mergedPolicyCache.clear() bag.inFlight.subjects.clear() if (opts.broadcast !== false && bag.invalidator) { void bag.invalidator.publish({ kind: 'all' }) } } export function invalidateSubject( bag: IEngineCacheBag, subjectId: string, opts: { broadcast?: boolean }, ): void { if (typeof subjectId !== 'string' || subjectId.length === 0 || subjectId.length > 1024) return bag.subjectCache.delete(subjectId) bag.inFlight.subjects.delete(subjectId) if (opts.broadcast !== false && bag.invalidator) { void bag.invalidator.publish({ kind: 'subject', subjectId }) } } export function invalidatePolicies( bag: IEngineCacheBag, opts: { broadcast?: boolean }, ): void { bag.policyCache.clear() bag.inFlight.policies.value = null bag.inFlight.merged.value = null bag.mergedPolicyCache.clear() if (opts.broadcast !== false && bag.invalidator) { void bag.invalidator.publish({ kind: 'policies' }) } } export function invalidateRoles( bag: IEngineCacheBag, roleIdInput: TRole | undefined, opts: { broadcast?: boolean }, ): void { let roleId = roleIdInput if (roleId !== undefined && (typeof roleId !== 'string' || roleId.length === 0 || roleId.length > 1024)) { roleId = undefined } bag.roleCache.clear() bag.rbacPolicyCache.clear() bag.inFlight.roles.value = null bag.inFlight.rbac.value = null bag.inFlight.merged.value = null bag.mergedPolicyCache.clear() if (roleId === undefined) { bag.subjectCache.clear() bag.inFlight.subjects.clear() } else { for (const [subjectId, subject] of bag.subjectCache.entries()) { const inRoles = subject.roles.includes(roleId) const inScoped = subject.scopedRoles?.some((sr) => sr.role === roleId) ?? false if (inRoles || inScoped) { bag.subjectCache.delete(subjectId) bag.inFlight.subjects.delete(subjectId) } } } if (opts.broadcast !== false && bag.invalidator) { void bag.invalidator.publish({ kind: 'roles', roleId }) } } export function applyInvalidateEvent( bag: IEngineCacheBag, event: IamEngineTypes.IInvalidateEvent, ): void { switch (event.kind) { case 'all': invalidateAll(bag, { broadcast: false }) return case 'policies': invalidatePolicies(bag, { broadcast: false }) return case 'roles': invalidateRoles(bag, event.roleId, { broadcast: false }) return case 'subject': invalidateSubject(bag, event.subjectId, { broadcast: false }) return } }