Шаг 0.12 плана production-готовности: - accessibleTasksCache (45с) per userId:orgId внутри storage-метода - инвалидация в storage-слое: роли, делегирования, form access, assignees, createTask/updateTask, appRole пользователя - 9 новых юнит-тестов (65/65 зелёные)
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
interface CacheEntry<T> {
|
|
data: T;
|
|
expiresAt: number;
|
|
}
|
|
|
|
export class TtlCache {
|
|
private store = new Map<string, CacheEntry<unknown>>();
|
|
private defaultTtlMs: number;
|
|
private maxSize: number;
|
|
|
|
constructor(ttlSeconds = 60, maxSize = 1000) {
|
|
this.defaultTtlMs = ttlSeconds * 1000;
|
|
this.maxSize = maxSize;
|
|
}
|
|
|
|
get<T>(key: string): T | undefined {
|
|
const entry = this.store.get(key) as CacheEntry<T> | undefined;
|
|
if (!entry) return undefined;
|
|
if (Date.now() > entry.expiresAt) {
|
|
this.store.delete(key);
|
|
return undefined;
|
|
}
|
|
// LRU: move to end on access
|
|
this.store.delete(key);
|
|
this.store.set(key, entry);
|
|
return entry.data;
|
|
}
|
|
|
|
set<T>(key: string, data: T, ttlMs?: number): void {
|
|
const effectiveTtl = ttlMs ?? this.defaultTtlMs;
|
|
// If key already exists, delete first to refresh insertion order
|
|
if (this.store.has(key)) {
|
|
this.store.delete(key);
|
|
} else if (this.store.size >= this.maxSize) {
|
|
// Evict oldest entry (first in Map insertion order)
|
|
const oldestKey = this.store.keys().next().value;
|
|
if (oldestKey !== undefined) {
|
|
this.store.delete(oldestKey);
|
|
}
|
|
}
|
|
this.store.set(key, { data, expiresAt: Date.now() + effectiveTtl });
|
|
}
|
|
|
|
/** Delete all keys that start with the given prefix */
|
|
invalidate(prefix: string): void {
|
|
for (const key of Array.from(this.store.keys())) {
|
|
if (key.startsWith(prefix)) {
|
|
this.store.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Alias for invalidate — delete all keys starting with prefix */
|
|
invalidatePrefix(prefix: string): void {
|
|
this.invalidate(prefix);
|
|
}
|
|
|
|
/** Delete all keys that end with the given suffix */
|
|
invalidateSuffix(suffix: string): void {
|
|
for (const key of Array.from(this.store.keys())) {
|
|
if (key.endsWith(suffix)) {
|
|
this.store.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Remove a single key */
|
|
delete(key: string): void {
|
|
this.store.delete(key);
|
|
}
|
|
|
|
/** Completely clear the cache */
|
|
clear(): void {
|
|
this.store.clear();
|
|
}
|
|
|
|
/** Current number of entries (including possibly expired ones) */
|
|
get size(): number {
|
|
return this.store.size;
|
|
}
|
|
}
|
|
|
|
export const formsCache = new TtlCache(60);
|
|
export const tasksMinimalCache = new TtlCache(60);
|
|
|
|
/**
|
|
* Кэш множеств доступных задач (getAccessibleTaskIds).
|
|
* Ключ: accessible:<userId>:<organizationId>.
|
|
* Значение: Set<number> | null (null = admin/view_all, фильтрация не нужна).
|
|
* TTL 45 сек + явная инвалидация при изменении ролей, делегирований,
|
|
* правил доступа к формам и назначений задач.
|
|
*/
|
|
export const accessibleTasksCache = new TtlCache(45);
|
|
|
|
export function accessibleTasksKey(userId: number, organizationId: number): string {
|
|
return `accessible:${userId}:${organizationId}`;
|
|
}
|
|
|
|
/** Точечная инвалидация для одного пользователя (назначение, членство в роли и т.п.) */
|
|
export function invalidateAccessibleTasksForUser(userId: number, organizationId: number): void {
|
|
accessibleTasksCache.delete(accessibleTasksKey(userId, organizationId));
|
|
}
|
|
|
|
/** Массовая инвалидация по организации (роли, правила доступа к формам и т.п.) */
|
|
export function invalidateAccessibleTasksForOrg(organizationId: number): void {
|
|
accessibleTasksCache.invalidateSuffix(`:${organizationId}`);
|
|
}
|