perf(access): TTL-кэш getAccessibleTaskIds с точечной инвалидацией
Шаг 0.12 плана production-готовности: - accessibleTasksCache (45с) per userId:orgId внутри storage-метода - инвалидация в storage-слое: роли, делегирования, form access, assignees, createTask/updateTask, appRole пользователя - 9 новых юнит-тестов (65/65 зелёные)
This commit is contained in:
103
tests/accessible-tasks-cache.test.ts
Normal file
103
tests/accessible-tasks-cache.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
TtlCache,
|
||||
accessibleTasksCache,
|
||||
accessibleTasksKey,
|
||||
invalidateAccessibleTasksForUser,
|
||||
invalidateAccessibleTasksForOrg,
|
||||
} from '../server/utils/cache';
|
||||
|
||||
describe('TtlCache', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('возвращает значение в пределах TTL и undefined после истечения', () => {
|
||||
const cache = new TtlCache(30);
|
||||
cache.set('k', new Set([1, 2, 3]));
|
||||
expect(cache.get<Set<number>>('k')?.has(2)).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(29_000);
|
||||
expect(cache.get('k')).toBeDefined();
|
||||
|
||||
vi.advanceTimersByTime(2_000);
|
||||
expect(cache.get('k')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('кэширует null как значение (отличая от промаха undefined)', () => {
|
||||
const cache = new TtlCache(30);
|
||||
cache.set('admin', null);
|
||||
expect(cache.get('admin')).toBeNull();
|
||||
expect(cache.get('missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('invalidatePrefix удаляет ключи по префиксу', () => {
|
||||
const cache = new TtlCache(30);
|
||||
cache.set('tasks:1:minimal:a', 1);
|
||||
cache.set('tasks:1:minimal:b', 2);
|
||||
cache.set('tasks:2:minimal:a', 3);
|
||||
cache.invalidatePrefix('tasks:1:minimal:');
|
||||
expect(cache.get('tasks:1:minimal:a')).toBeUndefined();
|
||||
expect(cache.get('tasks:1:minimal:b')).toBeUndefined();
|
||||
expect(cache.get('tasks:2:minimal:a')).toBe(3);
|
||||
});
|
||||
|
||||
it('invalidateSuffix удаляет ключи по суффиксу', () => {
|
||||
const cache = new TtlCache(30);
|
||||
cache.set('accessible:5:1', 'a');
|
||||
cache.set('accessible:7:1', 'b');
|
||||
cache.set('accessible:5:2', 'c');
|
||||
cache.invalidateSuffix(':1');
|
||||
expect(cache.get('accessible:5:1')).toBeUndefined();
|
||||
expect(cache.get('accessible:7:1')).toBeUndefined();
|
||||
expect(cache.get('accessible:5:2')).toBe('c');
|
||||
});
|
||||
|
||||
it('вытесняет самый старый ключ при переполнении maxSize', () => {
|
||||
const cache = new TtlCache(30, 2);
|
||||
cache.set('a', 1);
|
||||
cache.set('b', 2);
|
||||
cache.set('c', 3);
|
||||
expect(cache.get('a')).toBeUndefined();
|
||||
expect(cache.get('b')).toBe(2);
|
||||
expect(cache.get('c')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessibleTasksCache helpers', () => {
|
||||
beforeEach(() => {
|
||||
accessibleTasksCache.clear();
|
||||
});
|
||||
|
||||
it('ключ имеет формат accessible:<userId>:<organizationId>', () => {
|
||||
expect(accessibleTasksKey(12, 3)).toBe('accessible:12:3');
|
||||
});
|
||||
|
||||
it('точечная инвалидация сбрасывает только одного пользователя', () => {
|
||||
accessibleTasksCache.set(accessibleTasksKey(1, 1), new Set([10]));
|
||||
accessibleTasksCache.set(accessibleTasksKey(2, 1), new Set([20]));
|
||||
invalidateAccessibleTasksForUser(1, 1);
|
||||
expect(accessibleTasksCache.get(accessibleTasksKey(1, 1))).toBeUndefined();
|
||||
expect(accessibleTasksCache.get<Set<number>>(accessibleTasksKey(2, 1))?.has(20)).toBe(true);
|
||||
});
|
||||
|
||||
it('инвалидация по организации сбрасывает всех её пользователей, не трогая другие организации', () => {
|
||||
accessibleTasksCache.set(accessibleTasksKey(1, 1), new Set([10]));
|
||||
accessibleTasksCache.set(accessibleTasksKey(2, 1), null);
|
||||
accessibleTasksCache.set(accessibleTasksKey(1, 2), new Set([30]));
|
||||
invalidateAccessibleTasksForOrg(1);
|
||||
expect(accessibleTasksCache.get(accessibleTasksKey(1, 1))).toBeUndefined();
|
||||
expect(accessibleTasksCache.get(accessibleTasksKey(2, 1))).toBeUndefined();
|
||||
expect(accessibleTasksCache.get<Set<number>>(accessibleTasksKey(1, 2))?.has(30)).toBe(true);
|
||||
});
|
||||
|
||||
it('null (admin/view_all) кэшируется и корректно читается', () => {
|
||||
accessibleTasksCache.set(accessibleTasksKey(9, 1), null);
|
||||
expect(accessibleTasksCache.get(accessibleTasksKey(9, 1))).toBeNull();
|
||||
expect(accessibleTasksCache.get(accessibleTasksKey(9, 2))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user