mirror of https://github.com/buster-so/buster.git
Add lru-cache dependency and export cache management functions
- Added `lru-cache` as a dependency in the access-controls package. - Exported new cache management functions from `chats-cached` module, including `canUserAccessChatCached`, `getCacheStats`, `resetCacheStats`, `clearCache`, `invalidateAccess`, `invalidateUserAccess`, and `invalidateChatAccess`.
This commit is contained in:
parent
d44277d1a1
commit
2c92d70dca
|
@ -2,8 +2,6 @@
|
|||
push:
|
||||
branches:
|
||||
- mastra-braintrust
|
||||
paths:
|
||||
- apps/api/**
|
||||
name: Deploy to mastra-braintrust-api
|
||||
jobs:
|
||||
porter-deploy:
|
||||
|
|
|
@ -24,9 +24,10 @@
|
|||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@buster/database": "workspace:*",
|
||||
"@buster/typescript-config": "workspace:*",
|
||||
"@buster/vitest-config": "workspace:*",
|
||||
"@buster/database": "workspace:*",
|
||||
"lru-cache": "^11.1.0",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { LRUCache } from 'lru-cache';
|
||||
import { canUserAccessChat } from './chats';
|
||||
|
||||
// Cache configuration
|
||||
const cache = new LRUCache<string, boolean>({
|
||||
max: 10000, // Maximum 10k entries
|
||||
ttl: 30 * 1000, // 30 seconds
|
||||
updateAgeOnGet: true, // Refresh TTL on access
|
||||
});
|
||||
|
||||
// Metrics
|
||||
let cacheHits = 0;
|
||||
let cacheMisses = 0;
|
||||
|
||||
/**
|
||||
* Cached version of canUserAccessChat
|
||||
* Caches the boolean result for userId:chatId for 30 seconds
|
||||
* TTL is refreshed on each access
|
||||
*/
|
||||
export async function canUserAccessChatCached({
|
||||
userId,
|
||||
chatId,
|
||||
}: {
|
||||
userId: string;
|
||||
chatId: string;
|
||||
}): Promise<boolean> {
|
||||
const cacheKey = `${userId}:${chatId}`;
|
||||
|
||||
// Check cache
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached !== undefined) {
|
||||
cacheHits++;
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Cache miss - call the original function
|
||||
cacheMisses++;
|
||||
const hasAccess = await canUserAccessChat({ userId, chatId });
|
||||
|
||||
// Store the boolean result
|
||||
cache.set(cacheKey, hasAccess);
|
||||
|
||||
return hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
export function getCacheStats() {
|
||||
const total = cacheHits + cacheMisses;
|
||||
const hitRate = total > 0 ? (cacheHits / total) * 100 : 0;
|
||||
|
||||
return {
|
||||
hits: cacheHits,
|
||||
misses: cacheMisses,
|
||||
total,
|
||||
hitRate: `${hitRate.toFixed(2)}%`,
|
||||
size: cache.size,
|
||||
maxSize: cache.max,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache statistics (useful for testing)
|
||||
*/
|
||||
export function resetCacheStats() {
|
||||
cacheHits = 0;
|
||||
cacheMisses = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire cache (useful for testing)
|
||||
*/
|
||||
export function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a specific user:chat combination
|
||||
*/
|
||||
export function invalidateAccess(userId: string, chatId: string) {
|
||||
const cacheKey = `${userId}:${chatId}`;
|
||||
cache.delete(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all cached entries for a specific user
|
||||
*/
|
||||
export function invalidateUserAccess(userId: string) {
|
||||
for (const key of cache.keys()) {
|
||||
if (key.startsWith(`${userId}:`)) {
|
||||
cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all cached entries for a specific chat
|
||||
*/
|
||||
export function invalidateChatAccess(chatId: string) {
|
||||
for (const key of cache.keys()) {
|
||||
if (key.endsWith(`:${chatId}`)) {
|
||||
cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,5 +19,16 @@ export {
|
|||
|
||||
export { canUserAccessChat } from './chats';
|
||||
|
||||
// Export cached version and cache management functions
|
||||
export {
|
||||
canUserAccessChatCached,
|
||||
getCacheStats,
|
||||
resetCacheStats,
|
||||
clearCache,
|
||||
invalidateAccess,
|
||||
invalidateUserAccess,
|
||||
invalidateChatAccess,
|
||||
} from './chats-cached';
|
||||
|
||||
// Export utility functions
|
||||
export { formatPermissionName, buildAccessQuery } from './utils';
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { canUserAccessChat } from '../../src/chats';
|
||||
import {
|
||||
canUserAccessChatCached,
|
||||
clearCache,
|
||||
getCacheStats,
|
||||
resetCacheStats,
|
||||
} from '../../src/chats-cached';
|
||||
|
||||
// We'll use real function but spy on it to count calls
|
||||
vi.mock('../../src/chats', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../src/chats')>('../../src/chats');
|
||||
return {
|
||||
canUserAccessChat: vi.fn(actual.canUserAccessChat),
|
||||
};
|
||||
});
|
||||
|
||||
describe('canUserAccessChatCached Integration Tests', () => {
|
||||
let spiedCanUserAccessChat: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
clearCache();
|
||||
resetCacheStats();
|
||||
|
||||
const chatsModule = await import('../../src/chats');
|
||||
spiedCanUserAccessChat = vi.mocked(chatsModule.canUserAccessChat);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('should cache results and reduce database calls', async () => {
|
||||
// Use non-existent IDs so we get consistent false results
|
||||
const userId = '00000000-0000-0000-0000-000000000001';
|
||||
const chatId = '00000000-0000-0000-0000-000000000002';
|
||||
|
||||
// First call - should hit database
|
||||
const result1 = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result1).toBe(false);
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Make 10 more calls - all should hit cache
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const result = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
|
||||
// Should still only have called database once
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Check stats
|
||||
const stats = getCacheStats();
|
||||
expect(stats.hits).toBe(10);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.hitRate).toBe('90.91%');
|
||||
});
|
||||
|
||||
test('should handle multiple concurrent requests efficiently', async () => {
|
||||
const userId = '00000000-0000-0000-0000-000000000003';
|
||||
const chatId = '00000000-0000-0000-0000-000000000004';
|
||||
|
||||
// Make 5 concurrent requests
|
||||
const promises = Array(5)
|
||||
.fill(null)
|
||||
.map(() => canUserAccessChatCached({ userId, chatId }));
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// All should return false
|
||||
for (const result of results) {
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
|
||||
// Should only call database once even with concurrent requests
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Stats might vary due to race conditions, but we should have some hits
|
||||
const stats = getCacheStats();
|
||||
expect(stats.total).toBe(5);
|
||||
expect(stats.misses).toBeGreaterThanOrEqual(1);
|
||||
expect(stats.hits).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
test('should simulate high-frequency burst scenario', async () => {
|
||||
// Simulate 100 requests across 3 users and 3 chats
|
||||
const users = [
|
||||
'10000000-0000-0000-0000-000000000001',
|
||||
'20000000-0000-0000-0000-000000000002',
|
||||
'30000000-0000-0000-0000-000000000003',
|
||||
];
|
||||
const chats = [
|
||||
'40000000-0000-0000-0000-000000000001',
|
||||
'50000000-0000-0000-0000-000000000002',
|
||||
'60000000-0000-0000-0000-000000000003',
|
||||
];
|
||||
|
||||
const startTime = Date.now();
|
||||
let totalRequests = 0;
|
||||
|
||||
// Simulate requests
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const userId = users[i % users.length];
|
||||
const chatId = chats[Math.floor(i / 33) % chats.length];
|
||||
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
totalRequests++;
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Should complete quickly due to caching
|
||||
expect(duration).toBeLessThan(500); // 500ms for 100 requests
|
||||
|
||||
// Check cache effectiveness
|
||||
const stats = getCacheStats();
|
||||
expect(stats.total).toBe(100);
|
||||
|
||||
// Should have at most 9 unique combinations (3 users × 3 chats)
|
||||
expect(spiedCanUserAccessChat.mock.calls.length).toBeLessThanOrEqual(9);
|
||||
|
||||
// Cache hit rate should be high
|
||||
const hitRate = Number.parseFloat(stats.hitRate);
|
||||
expect(hitRate).toBeGreaterThan(80); // At least 80% hit rate
|
||||
});
|
||||
|
||||
test('should expire cache entries after TTL', async () => {
|
||||
// This test verifies TTL behavior by using fake timers
|
||||
vi.useFakeTimers();
|
||||
|
||||
const userId = '00000000-0000-0000-0000-000000000005';
|
||||
const chatId = '00000000-0000-0000-0000-000000000006';
|
||||
|
||||
// Initial call
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Call again immediately - should use cache
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance time by 20 seconds - should still use cache
|
||||
vi.advanceTimersByTime(20 * 1000);
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Advance time to just over 30 seconds total - cache should expire
|
||||
vi.advanceTimersByTime(11 * 1000); // Total: 31 seconds
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Next call should use new cache entry
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should refresh TTL on access', async () => {
|
||||
// This test verifies that TTL is refreshed when updateAgeOnGet is true
|
||||
vi.useFakeTimers();
|
||||
|
||||
const userId = '00000000-0000-0000-0000-000000000007';
|
||||
const chatId = '00000000-0000-0000-0000-000000000008';
|
||||
|
||||
// Initial call
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// After 25 seconds, access the cache
|
||||
vi.advanceTimersByTime(25 * 1000);
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1); // Still cached
|
||||
|
||||
// After another 25 seconds (total 50 seconds), should still be cached
|
||||
// because the TTL was refreshed at 25 seconds
|
||||
vi.advanceTimersByTime(25 * 1000);
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(1); // Still cached
|
||||
|
||||
// After another 31 seconds (total 81 seconds), should expire
|
||||
// because last access was at 50 seconds, so 50 + 31 > 30 second TTL
|
||||
vi.advanceTimersByTime(31 * 1000);
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(spiedCanUserAccessChat).toHaveBeenCalledTimes(2); // Expired, fetched again
|
||||
});
|
||||
});
|
|
@ -0,0 +1,251 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
canUserAccessChatCached,
|
||||
getCacheStats,
|
||||
resetCacheStats,
|
||||
clearCache,
|
||||
invalidateAccess,
|
||||
invalidateUserAccess,
|
||||
invalidateChatAccess,
|
||||
} from '../../src/chats-cached';
|
||||
|
||||
// Mock the original canUserAccessChat function
|
||||
vi.mock('../../src/chats', () => ({
|
||||
canUserAccessChat: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('canUserAccessChatCached', () => {
|
||||
let mockCanUserAccessChat: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
clearCache();
|
||||
resetCacheStats();
|
||||
|
||||
// Get the mocked function
|
||||
const chatsModule = await import('../../src/chats');
|
||||
mockCanUserAccessChat = vi.mocked(chatsModule.canUserAccessChat);
|
||||
});
|
||||
|
||||
it('should return cached result on second call', async () => {
|
||||
// Setup
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '223e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// First call - should hit database
|
||||
const result1 = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result1).toBe(true);
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Second call - should hit cache
|
||||
const result2 = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result2).toBe(true);
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(1); // Still 1, not called again
|
||||
|
||||
// Check cache stats
|
||||
const stats = getCacheStats();
|
||||
expect(stats.hits).toBe(1);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.hitRate).toBe('50.00%');
|
||||
});
|
||||
|
||||
it('should cache false results', async () => {
|
||||
mockCanUserAccessChat.mockResolvedValue(false);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '223e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// First call
|
||||
const result1 = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result1).toBe(false);
|
||||
|
||||
// Second call - should use cache
|
||||
const result2 = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result2).toBe(false);
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle different user/chat combinations independently', async () => {
|
||||
const user1 = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const user2 = '323e4567-e89b-12d3-a456-426614174000';
|
||||
const chat1 = '423e4567-e89b-12d3-a456-426614174000';
|
||||
const chat2 = '523e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Setup different responses
|
||||
mockCanUserAccessChat
|
||||
.mockResolvedValueOnce(true) // user1:chat1
|
||||
.mockResolvedValueOnce(false) // user1:chat2
|
||||
.mockResolvedValueOnce(false) // user2:chat1
|
||||
.mockResolvedValueOnce(true); // user2:chat2
|
||||
|
||||
// Make calls
|
||||
expect(await canUserAccessChatCached({ userId: user1, chatId: chat1 })).toBe(true);
|
||||
expect(await canUserAccessChatCached({ userId: user1, chatId: chat2 })).toBe(false);
|
||||
expect(await canUserAccessChatCached({ userId: user2, chatId: chat1 })).toBe(false);
|
||||
expect(await canUserAccessChatCached({ userId: user2, chatId: chat2 })).toBe(true);
|
||||
|
||||
// All should be cached now
|
||||
expect(await canUserAccessChatCached({ userId: user1, chatId: chat1 })).toBe(true);
|
||||
expect(await canUserAccessChatCached({ userId: user1, chatId: chat2 })).toBe(false);
|
||||
expect(await canUserAccessChatCached({ userId: user2, chatId: chat1 })).toBe(false);
|
||||
expect(await canUserAccessChatCached({ userId: user2, chatId: chat2 })).toBe(true);
|
||||
|
||||
// Should have called original function 4 times (once per unique combination)
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should invalidate specific user:chat combination', async () => {
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '223e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Cache the result
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Use cached result
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Invalidate
|
||||
invalidateAccess(userId, chatId);
|
||||
|
||||
// Should call database again
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should invalidate all entries for a user', async () => {
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chat1 = '223e4567-e89b-12d3-a456-426614174000';
|
||||
const chat2 = '323e4567-e89b-12d3-a456-426614174000';
|
||||
const otherUserId = '423e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Cache results for multiple chats
|
||||
await canUserAccessChatCached({ userId, chatId: chat1 });
|
||||
await canUserAccessChatCached({ userId, chatId: chat2 });
|
||||
await canUserAccessChatCached({ userId: otherUserId, chatId: chat1 });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Use cached results
|
||||
await canUserAccessChatCached({ userId, chatId: chat1 });
|
||||
await canUserAccessChatCached({ userId, chatId: chat2 });
|
||||
await canUserAccessChatCached({ userId: otherUserId, chatId: chat1 });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(3); // Still 3
|
||||
|
||||
// Invalidate all entries for userId
|
||||
invalidateUserAccess(userId);
|
||||
|
||||
// Should call database for invalidated user
|
||||
await canUserAccessChatCached({ userId, chatId: chat1 });
|
||||
await canUserAccessChatCached({ userId, chatId: chat2 });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(5); // +2
|
||||
|
||||
// Other user should still be cached
|
||||
await canUserAccessChatCached({ userId: otherUserId, chatId: chat1 });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(5); // Still 5
|
||||
});
|
||||
|
||||
it('should invalidate all entries for a chat', async () => {
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const user1 = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const user2 = '223e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '323e4567-e89b-12d3-a456-426614174000';
|
||||
const otherChatId = '423e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Cache results for multiple users
|
||||
await canUserAccessChatCached({ userId: user1, chatId });
|
||||
await canUserAccessChatCached({ userId: user2, chatId });
|
||||
await canUserAccessChatCached({ userId: user1, chatId: otherChatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Invalidate all entries for chatId
|
||||
invalidateChatAccess(chatId);
|
||||
|
||||
// Should call database for invalidated chat
|
||||
await canUserAccessChatCached({ userId: user1, chatId });
|
||||
await canUserAccessChatCached({ userId: user2, chatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(5); // +2
|
||||
|
||||
// Other chat should still be cached
|
||||
await canUserAccessChatCached({ userId: user1, chatId: otherChatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(5); // Still 5
|
||||
});
|
||||
|
||||
it('should clear entire cache', async () => {
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '223e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Cache a result
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clear cache
|
||||
clearCache();
|
||||
|
||||
// Should call database again
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should track cache statistics correctly', async () => {
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '223e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Initial stats
|
||||
let stats = getCacheStats();
|
||||
expect(stats.hits).toBe(0);
|
||||
expect(stats.misses).toBe(0);
|
||||
expect(stats.total).toBe(0);
|
||||
expect(stats.hitRate).toBe('0.00%');
|
||||
expect(stats.size).toBe(0);
|
||||
|
||||
// First call - miss
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
stats = getCacheStats();
|
||||
expect(stats.hits).toBe(0);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.hitRate).toBe('0.00%');
|
||||
expect(stats.size).toBe(1);
|
||||
|
||||
// Second call - hit
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
stats = getCacheStats();
|
||||
expect(stats.hits).toBe(1);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.hitRate).toBe('50.00%');
|
||||
|
||||
// Third call - hit
|
||||
await canUserAccessChatCached({ userId, chatId });
|
||||
stats = getCacheStats();
|
||||
expect(stats.hits).toBe(2);
|
||||
expect(stats.misses).toBe(1);
|
||||
expect(stats.hitRate).toBe('66.67%');
|
||||
|
||||
// Reset stats
|
||||
resetCacheStats();
|
||||
stats = getCacheStats();
|
||||
expect(stats.hits).toBe(0);
|
||||
expect(stats.misses).toBe(0);
|
||||
expect(stats.size).toBe(1); // Cache still has entries
|
||||
});
|
||||
|
||||
it('should handle errors from the original function', async () => {
|
||||
const error = new Error('Database error');
|
||||
mockCanUserAccessChat.mockRejectedValue(error);
|
||||
const userId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const chatId = '223e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
// Should propagate the error
|
||||
await expect(canUserAccessChatCached({ userId, chatId })).rejects.toThrow('Database error');
|
||||
|
||||
// Should not cache errors
|
||||
mockCanUserAccessChat.mockResolvedValue(true);
|
||||
const result = await canUserAccessChatCached({ userId, chatId });
|
||||
expect(result).toBe(true);
|
||||
expect(mockCanUserAccessChat).toHaveBeenCalledTimes(2); // Called again, error wasn't cached
|
||||
});
|
||||
});
|
|
@ -606,6 +606,9 @@ importers:
|
|||
'@buster/vitest-config':
|
||||
specifier: workspace:*
|
||||
version: link:../vitest-config
|
||||
lru-cache:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 3.25.67
|
||||
|
|
Loading…
Reference in New Issue