fixed format issues
This commit is contained in:
parent
a700818a06
commit
08f713d98b
55 changed files with 5680 additions and 5533 deletions
12
libs/core/cache/src/cache-factory.ts
vendored
12
libs/core/cache/src/cache-factory.ts
vendored
|
|
@ -44,11 +44,13 @@ export function createNamespacedCache(
|
|||
* Type guard to check if cache is available
|
||||
*/
|
||||
export function isCacheAvailable(cache: unknown): cache is CacheProvider {
|
||||
return cache !== null &&
|
||||
cache !== undefined &&
|
||||
typeof cache === 'object' &&
|
||||
'get' in cache &&
|
||||
typeof (cache as CacheProvider).get === 'function';
|
||||
return (
|
||||
cache !== null &&
|
||||
cache !== undefined &&
|
||||
typeof cache === 'object' &&
|
||||
'get' in cache &&
|
||||
typeof (cache as CacheProvider).get === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
14
libs/core/cache/src/connection-manager.ts
vendored
14
libs/core/cache/src/connection-manager.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import Redis from 'ioredis';
|
||||
import type { RedisConfig } from './types';
|
||||
import { REDIS_DEFAULTS } from './constants';
|
||||
import type { RedisConfig } from './types';
|
||||
|
||||
interface ConnectionConfig {
|
||||
name: string;
|
||||
|
|
@ -29,7 +29,7 @@ export class RedisConnectionManager {
|
|||
*/
|
||||
getConnection(config: ConnectionConfig): Redis {
|
||||
const { name, singleton = true, redisConfig } = config;
|
||||
|
||||
|
||||
if (singleton) {
|
||||
const existing = RedisConnectionManager.connections.get(name);
|
||||
if (existing) {
|
||||
|
|
@ -38,11 +38,11 @@ export class RedisConnectionManager {
|
|||
}
|
||||
|
||||
const connection = this.createConnection(redisConfig);
|
||||
|
||||
|
||||
if (singleton) {
|
||||
RedisConnectionManager.connections.set(name, connection);
|
||||
}
|
||||
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
|
@ -68,10 +68,8 @@ export class RedisConnectionManager {
|
|||
* Close all connections
|
||||
*/
|
||||
static async closeAll(): Promise<void> {
|
||||
const promises = Array.from(this.connections.values()).map(conn =>
|
||||
conn.quit().catch(() => {})
|
||||
);
|
||||
const promises = Array.from(this.connections.values()).map(conn => conn.quit().catch(() => {}));
|
||||
await Promise.all(promises);
|
||||
this.connections.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
libs/core/cache/src/constants.ts
vendored
32
libs/core/cache/src/constants.ts
vendored
|
|
@ -1,16 +1,16 @@
|
|||
// Cache constants
|
||||
export const CACHE_DEFAULTS = {
|
||||
TTL: 3600, // 1 hour in seconds
|
||||
KEY_PREFIX: 'cache:',
|
||||
SCAN_COUNT: 100,
|
||||
} as const;
|
||||
|
||||
// Redis connection constants
|
||||
export const REDIS_DEFAULTS = {
|
||||
DB: 0,
|
||||
MAX_RETRIES: 3,
|
||||
RETRY_DELAY: 100,
|
||||
CONNECT_TIMEOUT: 10000,
|
||||
COMMAND_TIMEOUT: 5000,
|
||||
KEEP_ALIVE: 0,
|
||||
} as const;
|
||||
// Cache constants
|
||||
export const CACHE_DEFAULTS = {
|
||||
TTL: 3600, // 1 hour in seconds
|
||||
KEY_PREFIX: 'cache:',
|
||||
SCAN_COUNT: 100,
|
||||
} as const;
|
||||
|
||||
// Redis connection constants
|
||||
export const REDIS_DEFAULTS = {
|
||||
DB: 0,
|
||||
MAX_RETRIES: 3,
|
||||
RETRY_DELAY: 100,
|
||||
CONNECT_TIMEOUT: 10000,
|
||||
COMMAND_TIMEOUT: 5000,
|
||||
KEEP_ALIVE: 0,
|
||||
} as const;
|
||||
|
|
|
|||
2
libs/core/cache/src/index.ts
vendored
2
libs/core/cache/src/index.ts
vendored
|
|
@ -40,4 +40,4 @@ export function createCache(options: CacheOptions): CacheProvider {
|
|||
// Export only what's actually used
|
||||
export type { CacheProvider, CacheStats } from './types';
|
||||
export { NamespacedCache } from './namespaced-cache';
|
||||
export { createNamespacedCache } from './cache-factory';
|
||||
export { createNamespacedCache } from './cache-factory';
|
||||
|
|
|
|||
51
libs/core/cache/src/redis-cache.ts
vendored
51
libs/core/cache/src/redis-cache.ts
vendored
|
|
@ -8,7 +8,8 @@ import type { CacheOptions, CacheProvider, CacheStats } from './types';
|
|||
*/
|
||||
export class RedisCache implements CacheProvider {
|
||||
private redis: Redis;
|
||||
private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } = console;
|
||||
private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } =
|
||||
console;
|
||||
private defaultTTL: number;
|
||||
private keyPrefix: string;
|
||||
private stats: CacheStats = {
|
||||
|
|
@ -28,7 +29,7 @@ export class RedisCache implements CacheProvider {
|
|||
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
const name = options.name || 'CACHE';
|
||||
|
||||
|
||||
this.redis = manager.getConnection({
|
||||
name: `${name}-SERVICE`,
|
||||
singleton: options.shared ?? true,
|
||||
|
|
@ -72,19 +73,21 @@ export class RedisCache implements CacheProvider {
|
|||
async set<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?: number | {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}
|
||||
options?:
|
||||
| number
|
||||
| {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const fullKey = this.getKey(key);
|
||||
const serialized = JSON.stringify(value);
|
||||
const opts = typeof options === 'number' ? { ttl: options } : options || {};
|
||||
|
||||
|
||||
let oldValue: T | null = null;
|
||||
if (opts.getOldValue) {
|
||||
const existing = await this.redis.get(fullKey);
|
||||
|
|
@ -92,9 +95,9 @@ export class RedisCache implements CacheProvider {
|
|||
oldValue = JSON.parse(existing);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const ttl = opts.ttl ?? this.defaultTTL;
|
||||
|
||||
|
||||
if (opts.onlyIfExists) {
|
||||
const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'XX');
|
||||
if (!result) {
|
||||
|
|
@ -115,7 +118,7 @@ export class RedisCache implements CacheProvider {
|
|||
} else {
|
||||
await this.redis.setex(fullKey, ttl, serialized);
|
||||
}
|
||||
|
||||
|
||||
return oldValue;
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
|
|
@ -145,21 +148,21 @@ export class RedisCache implements CacheProvider {
|
|||
try {
|
||||
const stream = this.redis.scanStream({
|
||||
match: `${this.keyPrefix}*`,
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT,
|
||||
});
|
||||
|
||||
|
||||
const pipeline = this.redis.pipeline();
|
||||
stream.on('data', (keys: string[]) => {
|
||||
if (keys.length) {
|
||||
keys.forEach(key => pipeline.del(key));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.on('end', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
|
||||
|
||||
await pipeline.exec();
|
||||
} catch (error) {
|
||||
this.updateStats(false, true);
|
||||
|
|
@ -172,9 +175,9 @@ export class RedisCache implements CacheProvider {
|
|||
const keys: string[] = [];
|
||||
const stream = this.redis.scanStream({
|
||||
match: `${this.keyPrefix}${pattern}`,
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT,
|
||||
});
|
||||
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.on('data', (resultKeys: string[]) => {
|
||||
keys.push(...resultKeys.map(k => k.replace(this.keyPrefix, '')));
|
||||
|
|
@ -182,7 +185,7 @@ export class RedisCache implements CacheProvider {
|
|||
stream.on('end', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
|
||||
|
||||
return keys;
|
||||
} catch {
|
||||
this.updateStats(false, true);
|
||||
|
|
@ -206,8 +209,10 @@ export class RedisCache implements CacheProvider {
|
|||
}
|
||||
|
||||
async waitForReady(timeout = 5000): Promise<void> {
|
||||
if (this.redis.status === 'ready') {return;}
|
||||
|
||||
if (this.redis.status === 'ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Redis connection timeout after ${timeout}ms`));
|
||||
|
|
@ -223,4 +228,4 @@ export class RedisCache implements CacheProvider {
|
|||
isReady(): boolean {
|
||||
return this.redis.status === 'ready';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
libs/core/cache/src/types.ts
vendored
7
libs/core/cache/src/types.ts
vendored
|
|
@ -113,7 +113,12 @@ export interface CacheOptions {
|
|||
name?: string; // Name for connection identification
|
||||
shared?: boolean; // Whether to use shared connection
|
||||
redisConfig: RedisConfig;
|
||||
logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void };
|
||||
logger?: {
|
||||
info?: (...args: unknown[]) => void;
|
||||
error?: (...args: unknown[]) => void;
|
||||
warn?: (...args: unknown[]) => void;
|
||||
debug?: (...args: unknown[]) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
|
|
|
|||
188
libs/core/cache/test/connection-manager.test.ts
vendored
188
libs/core/cache/test/connection-manager.test.ts
vendored
|
|
@ -1,94 +1,94 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { RedisConnectionManager } from '../src/connection-manager';
|
||||
|
||||
describe('RedisConnectionManager', () => {
|
||||
it('should be a singleton', () => {
|
||||
const instance1 = RedisConnectionManager.getInstance();
|
||||
const instance2 = RedisConnectionManager.getInstance();
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
|
||||
it('should create connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
const connection = manager.getConnection({
|
||||
name: 'test',
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(connection).toBeDefined();
|
||||
expect(connection.options.host).toBe('localhost');
|
||||
expect(connection.options.port).toBe(6379);
|
||||
});
|
||||
|
||||
it('should reuse singleton connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
const conn1 = manager.getConnection({
|
||||
name: 'shared',
|
||||
singleton: true,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
const conn2 = manager.getConnection({
|
||||
name: 'shared',
|
||||
singleton: true,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(conn1).toBe(conn2);
|
||||
});
|
||||
|
||||
it('should create separate non-singleton connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
const conn1 = manager.getConnection({
|
||||
name: 'separate1',
|
||||
singleton: false,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
const conn2 = manager.getConnection({
|
||||
name: 'separate2',
|
||||
singleton: false,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(conn1).not.toBe(conn2);
|
||||
});
|
||||
|
||||
it('should close all connections', async () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
// Create a few connections
|
||||
manager.getConnection({
|
||||
name: 'close-test-1',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
manager.getConnection({
|
||||
name: 'close-test-2',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
// Close all
|
||||
await RedisConnectionManager.closeAll();
|
||||
|
||||
// Should not throw
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { RedisConnectionManager } from '../src/connection-manager';
|
||||
|
||||
describe('RedisConnectionManager', () => {
|
||||
it('should be a singleton', () => {
|
||||
const instance1 = RedisConnectionManager.getInstance();
|
||||
const instance2 = RedisConnectionManager.getInstance();
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
|
||||
it('should create connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
const connection = manager.getConnection({
|
||||
name: 'test',
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(connection).toBeDefined();
|
||||
expect(connection.options.host).toBe('localhost');
|
||||
expect(connection.options.port).toBe(6379);
|
||||
});
|
||||
|
||||
it('should reuse singleton connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
const conn1 = manager.getConnection({
|
||||
name: 'shared',
|
||||
singleton: true,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
const conn2 = manager.getConnection({
|
||||
name: 'shared',
|
||||
singleton: true,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(conn1).toBe(conn2);
|
||||
});
|
||||
|
||||
it('should create separate non-singleton connections', () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
const conn1 = manager.getConnection({
|
||||
name: 'separate1',
|
||||
singleton: false,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
const conn2 = manager.getConnection({
|
||||
name: 'separate2',
|
||||
singleton: false,
|
||||
redisConfig: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
|
||||
expect(conn1).not.toBe(conn2);
|
||||
});
|
||||
|
||||
it('should close all connections', async () => {
|
||||
const manager = RedisConnectionManager.getInstance();
|
||||
|
||||
// Create a few connections
|
||||
manager.getConnection({
|
||||
name: 'close-test-1',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
manager.getConnection({
|
||||
name: 'close-test-2',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
// Close all
|
||||
await RedisConnectionManager.closeAll();
|
||||
|
||||
// Should not throw
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
858
libs/core/cache/test/namespaced-cache.test.ts
vendored
858
libs/core/cache/test/namespaced-cache.test.ts
vendored
|
|
@ -1,429 +1,429 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { NamespacedCache, CacheAdapter } from '../src/namespaced-cache';
|
||||
import type { CacheProvider, ICache } from '../src/types';
|
||||
|
||||
describe('NamespacedCache', () => {
|
||||
let mockCache: CacheProvider;
|
||||
let namespacedCache: NamespacedCache;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock base cache
|
||||
mockCache = {
|
||||
get: mock(async () => null),
|
||||
set: mock(async () => null),
|
||||
del: mock(async () => {}),
|
||||
exists: mock(async () => false),
|
||||
clear: mock(async () => {}),
|
||||
keys: mock(async () => []),
|
||||
getStats: mock(() => ({
|
||||
hits: 100,
|
||||
misses: 20,
|
||||
errors: 5,
|
||||
hitRate: 0.83,
|
||||
total: 120,
|
||||
uptime: 3600,
|
||||
})),
|
||||
health: mock(async () => true),
|
||||
waitForReady: mock(async () => {}),
|
||||
isReady: mock(() => true),
|
||||
};
|
||||
|
||||
// Create namespaced cache
|
||||
namespacedCache = new NamespacedCache(mockCache, 'test-namespace');
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should set namespace and prefix correctly', () => {
|
||||
expect(namespacedCache.getNamespace()).toBe('test-namespace');
|
||||
expect(namespacedCache.getFullPrefix()).toBe('test-namespace:');
|
||||
});
|
||||
|
||||
it('should handle empty namespace', () => {
|
||||
const emptyNamespace = new NamespacedCache(mockCache, '');
|
||||
expect(emptyNamespace.getNamespace()).toBe('');
|
||||
expect(emptyNamespace.getFullPrefix()).toBe(':');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should prefix key when getting', async () => {
|
||||
const testData = { value: 'test' };
|
||||
(mockCache.get as any).mockResolvedValue(testData);
|
||||
|
||||
const result = await namespacedCache.get('mykey');
|
||||
|
||||
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:mykey');
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should handle null values', async () => {
|
||||
(mockCache.get as any).mockResolvedValue(null);
|
||||
|
||||
const result = await namespacedCache.get('nonexistent');
|
||||
|
||||
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:nonexistent');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should prefix key when setting with ttl number', async () => {
|
||||
const value = { data: 'test' };
|
||||
const ttl = 3600;
|
||||
|
||||
await namespacedCache.set('mykey', value, ttl);
|
||||
|
||||
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, ttl);
|
||||
});
|
||||
|
||||
it('should prefix key when setting with options object', async () => {
|
||||
const value = 'test-value';
|
||||
const options = { ttl: 7200 };
|
||||
|
||||
await namespacedCache.set('mykey', value, options);
|
||||
|
||||
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, options);
|
||||
});
|
||||
|
||||
it('should handle set without TTL', async () => {
|
||||
const value = [1, 2, 3];
|
||||
|
||||
await namespacedCache.set('mykey', value);
|
||||
|
||||
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', () => {
|
||||
it('should prefix key when deleting', async () => {
|
||||
await namespacedCache.del('mykey');
|
||||
|
||||
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:mykey');
|
||||
});
|
||||
|
||||
it('should handle multiple deletes', async () => {
|
||||
await namespacedCache.del('key1');
|
||||
await namespacedCache.del('key2');
|
||||
|
||||
expect(mockCache.del).toHaveBeenCalledTimes(2);
|
||||
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key1');
|
||||
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should prefix key when checking existence', async () => {
|
||||
(mockCache.exists as any).mockResolvedValue(true);
|
||||
|
||||
const result = await namespacedCache.exists('mykey');
|
||||
|
||||
expect(mockCache.exists).toHaveBeenCalledWith('test-namespace:mykey');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existent keys', async () => {
|
||||
(mockCache.exists as any).mockResolvedValue(false);
|
||||
|
||||
const result = await namespacedCache.exists('nonexistent');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys', () => {
|
||||
it('should prefix pattern and strip prefix from results', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:key1',
|
||||
'test-namespace:key2',
|
||||
'test-namespace:key3',
|
||||
]);
|
||||
|
||||
const keys = await namespacedCache.keys('*');
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
||||
expect(keys).toEqual(['key1', 'key2', 'key3']);
|
||||
});
|
||||
|
||||
it('should handle specific patterns', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:user:123',
|
||||
'test-namespace:user:456',
|
||||
]);
|
||||
|
||||
const keys = await namespacedCache.keys('user:*');
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:user:*');
|
||||
expect(keys).toEqual(['user:123', 'user:456']);
|
||||
});
|
||||
|
||||
it('should filter out keys from other namespaces', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:key1',
|
||||
'other-namespace:key2',
|
||||
'test-namespace:key3',
|
||||
]);
|
||||
|
||||
const keys = await namespacedCache.keys('*');
|
||||
|
||||
expect(keys).toEqual(['key1', 'key3']);
|
||||
});
|
||||
|
||||
it('should handle empty results', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([]);
|
||||
|
||||
const keys = await namespacedCache.keys('nonexistent*');
|
||||
|
||||
expect(keys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear only namespaced keys', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:key1',
|
||||
'test-namespace:key2',
|
||||
'test-namespace:key3',
|
||||
]);
|
||||
|
||||
await namespacedCache.clear();
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
||||
expect(mockCache.del).toHaveBeenCalledTimes(3);
|
||||
expect(mockCache.del).toHaveBeenCalledWith('key1');
|
||||
expect(mockCache.del).toHaveBeenCalledWith('key2');
|
||||
expect(mockCache.del).toHaveBeenCalledWith('key3');
|
||||
});
|
||||
|
||||
it('should handle empty namespace', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([]);
|
||||
|
||||
await namespacedCache.clear();
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
||||
expect(mockCache.del).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delegated methods', () => {
|
||||
it('should delegate getStats', () => {
|
||||
const stats = namespacedCache.getStats();
|
||||
|
||||
expect(mockCache.getStats).toHaveBeenCalled();
|
||||
expect(stats).toEqual({
|
||||
hits: 100,
|
||||
misses: 20,
|
||||
errors: 5,
|
||||
hitRate: 0.83,
|
||||
total: 120,
|
||||
uptime: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
it('should delegate health', async () => {
|
||||
const health = await namespacedCache.health();
|
||||
|
||||
expect(mockCache.health).toHaveBeenCalled();
|
||||
expect(health).toBe(true);
|
||||
});
|
||||
|
||||
it('should delegate waitForReady', async () => {
|
||||
await namespacedCache.waitForReady(5000);
|
||||
|
||||
expect(mockCache.waitForReady).toHaveBeenCalledWith(5000);
|
||||
});
|
||||
|
||||
it('should delegate isReady', () => {
|
||||
const ready = namespacedCache.isReady();
|
||||
|
||||
expect(mockCache.isReady).toHaveBeenCalled();
|
||||
expect(ready).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle special characters in namespace', () => {
|
||||
const specialNamespace = new NamespacedCache(mockCache, 'test:namespace:with:colons');
|
||||
expect(specialNamespace.getFullPrefix()).toBe('test:namespace:with:colons:');
|
||||
});
|
||||
|
||||
it('should handle very long keys', async () => {
|
||||
const longKey = 'a'.repeat(1000);
|
||||
await namespacedCache.get(longKey);
|
||||
|
||||
expect(mockCache.get).toHaveBeenCalledWith(`test-namespace:${longKey}`);
|
||||
});
|
||||
|
||||
it('should handle errors from underlying cache', async () => {
|
||||
const error = new Error('Cache error');
|
||||
(mockCache.get as any).mockRejectedValue(error);
|
||||
|
||||
await expect(namespacedCache.get('key')).rejects.toThrow('Cache error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheAdapter', () => {
|
||||
let mockICache: ICache;
|
||||
let adapter: CacheAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
mockICache = {
|
||||
get: mock(async () => null),
|
||||
set: mock(async () => {}),
|
||||
del: mock(async () => {}),
|
||||
exists: mock(async () => false),
|
||||
clear: mock(async () => {}),
|
||||
keys: mock(async () => []),
|
||||
ping: mock(async () => true),
|
||||
isConnected: mock(() => true),
|
||||
has: mock(async () => false),
|
||||
ttl: mock(async () => -1),
|
||||
type: 'memory' as const,
|
||||
};
|
||||
|
||||
adapter = new CacheAdapter(mockICache);
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should delegate to ICache.get', async () => {
|
||||
const data = { value: 'test' };
|
||||
(mockICache.get as any).mockResolvedValue(data);
|
||||
|
||||
const result = await adapter.get('key');
|
||||
|
||||
expect(mockICache.get).toHaveBeenCalledWith('key');
|
||||
expect(result).toEqual(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should handle TTL as number', async () => {
|
||||
await adapter.set('key', 'value', 3600);
|
||||
|
||||
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 3600);
|
||||
});
|
||||
|
||||
it('should handle TTL as options object', async () => {
|
||||
await adapter.set('key', 'value', { ttl: 7200 });
|
||||
|
||||
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 7200);
|
||||
});
|
||||
|
||||
it('should handle no TTL', async () => {
|
||||
await adapter.set('key', 'value');
|
||||
|
||||
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', undefined);
|
||||
});
|
||||
|
||||
it('should always return null', async () => {
|
||||
const result = await adapter.set('key', 'value');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', () => {
|
||||
it('should delegate to ICache.del', async () => {
|
||||
await adapter.del('key');
|
||||
|
||||
expect(mockICache.del).toHaveBeenCalledWith('key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should delegate to ICache.exists', async () => {
|
||||
(mockICache.exists as any).mockResolvedValue(true);
|
||||
|
||||
const result = await adapter.exists('key');
|
||||
|
||||
expect(mockICache.exists).toHaveBeenCalledWith('key');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should delegate to ICache.clear', async () => {
|
||||
await adapter.clear();
|
||||
|
||||
expect(mockICache.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys', () => {
|
||||
it('should delegate to ICache.keys', async () => {
|
||||
const keys = ['key1', 'key2'];
|
||||
(mockICache.keys as any).mockResolvedValue(keys);
|
||||
|
||||
const result = await adapter.keys('*');
|
||||
|
||||
expect(mockICache.keys).toHaveBeenCalledWith('*');
|
||||
expect(result).toEqual(keys);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return default stats', () => {
|
||||
const stats = adapter.getStats();
|
||||
|
||||
expect(stats).toEqual({
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('health', () => {
|
||||
it('should use ping for health check', async () => {
|
||||
(mockICache.ping as any).mockResolvedValue(true);
|
||||
|
||||
const result = await adapter.health();
|
||||
|
||||
expect(mockICache.ping).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle ping failures', async () => {
|
||||
(mockICache.ping as any).mockResolvedValue(false);
|
||||
|
||||
const result = await adapter.health();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForReady', () => {
|
||||
it('should succeed if connected', async () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(true);
|
||||
|
||||
await expect(adapter.waitForReady()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if not connected', async () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(false);
|
||||
|
||||
await expect(adapter.waitForReady()).rejects.toThrow('Cache not connected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isReady', () => {
|
||||
it('should delegate to isConnected', () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(true);
|
||||
|
||||
const result = adapter.isReady();
|
||||
|
||||
expect(mockICache.isConnected).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when not connected', () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(false);
|
||||
|
||||
const result = adapter.isReady();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { CacheAdapter, NamespacedCache } from '../src/namespaced-cache';
|
||||
import type { CacheProvider, ICache } from '../src/types';
|
||||
|
||||
describe('NamespacedCache', () => {
|
||||
let mockCache: CacheProvider;
|
||||
let namespacedCache: NamespacedCache;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock base cache
|
||||
mockCache = {
|
||||
get: mock(async () => null),
|
||||
set: mock(async () => null),
|
||||
del: mock(async () => {}),
|
||||
exists: mock(async () => false),
|
||||
clear: mock(async () => {}),
|
||||
keys: mock(async () => []),
|
||||
getStats: mock(() => ({
|
||||
hits: 100,
|
||||
misses: 20,
|
||||
errors: 5,
|
||||
hitRate: 0.83,
|
||||
total: 120,
|
||||
uptime: 3600,
|
||||
})),
|
||||
health: mock(async () => true),
|
||||
waitForReady: mock(async () => {}),
|
||||
isReady: mock(() => true),
|
||||
};
|
||||
|
||||
// Create namespaced cache
|
||||
namespacedCache = new NamespacedCache(mockCache, 'test-namespace');
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should set namespace and prefix correctly', () => {
|
||||
expect(namespacedCache.getNamespace()).toBe('test-namespace');
|
||||
expect(namespacedCache.getFullPrefix()).toBe('test-namespace:');
|
||||
});
|
||||
|
||||
it('should handle empty namespace', () => {
|
||||
const emptyNamespace = new NamespacedCache(mockCache, '');
|
||||
expect(emptyNamespace.getNamespace()).toBe('');
|
||||
expect(emptyNamespace.getFullPrefix()).toBe(':');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should prefix key when getting', async () => {
|
||||
const testData = { value: 'test' };
|
||||
(mockCache.get as any).mockResolvedValue(testData);
|
||||
|
||||
const result = await namespacedCache.get('mykey');
|
||||
|
||||
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:mykey');
|
||||
expect(result).toEqual(testData);
|
||||
});
|
||||
|
||||
it('should handle null values', async () => {
|
||||
(mockCache.get as any).mockResolvedValue(null);
|
||||
|
||||
const result = await namespacedCache.get('nonexistent');
|
||||
|
||||
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:nonexistent');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should prefix key when setting with ttl number', async () => {
|
||||
const value = { data: 'test' };
|
||||
const ttl = 3600;
|
||||
|
||||
await namespacedCache.set('mykey', value, ttl);
|
||||
|
||||
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, ttl);
|
||||
});
|
||||
|
||||
it('should prefix key when setting with options object', async () => {
|
||||
const value = 'test-value';
|
||||
const options = { ttl: 7200 };
|
||||
|
||||
await namespacedCache.set('mykey', value, options);
|
||||
|
||||
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, options);
|
||||
});
|
||||
|
||||
it('should handle set without TTL', async () => {
|
||||
const value = [1, 2, 3];
|
||||
|
||||
await namespacedCache.set('mykey', value);
|
||||
|
||||
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', () => {
|
||||
it('should prefix key when deleting', async () => {
|
||||
await namespacedCache.del('mykey');
|
||||
|
||||
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:mykey');
|
||||
});
|
||||
|
||||
it('should handle multiple deletes', async () => {
|
||||
await namespacedCache.del('key1');
|
||||
await namespacedCache.del('key2');
|
||||
|
||||
expect(mockCache.del).toHaveBeenCalledTimes(2);
|
||||
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key1');
|
||||
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should prefix key when checking existence', async () => {
|
||||
(mockCache.exists as any).mockResolvedValue(true);
|
||||
|
||||
const result = await namespacedCache.exists('mykey');
|
||||
|
||||
expect(mockCache.exists).toHaveBeenCalledWith('test-namespace:mykey');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existent keys', async () => {
|
||||
(mockCache.exists as any).mockResolvedValue(false);
|
||||
|
||||
const result = await namespacedCache.exists('nonexistent');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys', () => {
|
||||
it('should prefix pattern and strip prefix from results', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:key1',
|
||||
'test-namespace:key2',
|
||||
'test-namespace:key3',
|
||||
]);
|
||||
|
||||
const keys = await namespacedCache.keys('*');
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
||||
expect(keys).toEqual(['key1', 'key2', 'key3']);
|
||||
});
|
||||
|
||||
it('should handle specific patterns', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:user:123',
|
||||
'test-namespace:user:456',
|
||||
]);
|
||||
|
||||
const keys = await namespacedCache.keys('user:*');
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:user:*');
|
||||
expect(keys).toEqual(['user:123', 'user:456']);
|
||||
});
|
||||
|
||||
it('should filter out keys from other namespaces', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:key1',
|
||||
'other-namespace:key2',
|
||||
'test-namespace:key3',
|
||||
]);
|
||||
|
||||
const keys = await namespacedCache.keys('*');
|
||||
|
||||
expect(keys).toEqual(['key1', 'key3']);
|
||||
});
|
||||
|
||||
it('should handle empty results', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([]);
|
||||
|
||||
const keys = await namespacedCache.keys('nonexistent*');
|
||||
|
||||
expect(keys).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear only namespaced keys', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([
|
||||
'test-namespace:key1',
|
||||
'test-namespace:key2',
|
||||
'test-namespace:key3',
|
||||
]);
|
||||
|
||||
await namespacedCache.clear();
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
||||
expect(mockCache.del).toHaveBeenCalledTimes(3);
|
||||
expect(mockCache.del).toHaveBeenCalledWith('key1');
|
||||
expect(mockCache.del).toHaveBeenCalledWith('key2');
|
||||
expect(mockCache.del).toHaveBeenCalledWith('key3');
|
||||
});
|
||||
|
||||
it('should handle empty namespace', async () => {
|
||||
(mockCache.keys as any).mockResolvedValue([]);
|
||||
|
||||
await namespacedCache.clear();
|
||||
|
||||
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
||||
expect(mockCache.del).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delegated methods', () => {
|
||||
it('should delegate getStats', () => {
|
||||
const stats = namespacedCache.getStats();
|
||||
|
||||
expect(mockCache.getStats).toHaveBeenCalled();
|
||||
expect(stats).toEqual({
|
||||
hits: 100,
|
||||
misses: 20,
|
||||
errors: 5,
|
||||
hitRate: 0.83,
|
||||
total: 120,
|
||||
uptime: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
it('should delegate health', async () => {
|
||||
const health = await namespacedCache.health();
|
||||
|
||||
expect(mockCache.health).toHaveBeenCalled();
|
||||
expect(health).toBe(true);
|
||||
});
|
||||
|
||||
it('should delegate waitForReady', async () => {
|
||||
await namespacedCache.waitForReady(5000);
|
||||
|
||||
expect(mockCache.waitForReady).toHaveBeenCalledWith(5000);
|
||||
});
|
||||
|
||||
it('should delegate isReady', () => {
|
||||
const ready = namespacedCache.isReady();
|
||||
|
||||
expect(mockCache.isReady).toHaveBeenCalled();
|
||||
expect(ready).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle special characters in namespace', () => {
|
||||
const specialNamespace = new NamespacedCache(mockCache, 'test:namespace:with:colons');
|
||||
expect(specialNamespace.getFullPrefix()).toBe('test:namespace:with:colons:');
|
||||
});
|
||||
|
||||
it('should handle very long keys', async () => {
|
||||
const longKey = 'a'.repeat(1000);
|
||||
await namespacedCache.get(longKey);
|
||||
|
||||
expect(mockCache.get).toHaveBeenCalledWith(`test-namespace:${longKey}`);
|
||||
});
|
||||
|
||||
it('should handle errors from underlying cache', async () => {
|
||||
const error = new Error('Cache error');
|
||||
(mockCache.get as any).mockRejectedValue(error);
|
||||
|
||||
await expect(namespacedCache.get('key')).rejects.toThrow('Cache error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheAdapter', () => {
|
||||
let mockICache: ICache;
|
||||
let adapter: CacheAdapter;
|
||||
|
||||
beforeEach(() => {
|
||||
mockICache = {
|
||||
get: mock(async () => null),
|
||||
set: mock(async () => {}),
|
||||
del: mock(async () => {}),
|
||||
exists: mock(async () => false),
|
||||
clear: mock(async () => {}),
|
||||
keys: mock(async () => []),
|
||||
ping: mock(async () => true),
|
||||
isConnected: mock(() => true),
|
||||
has: mock(async () => false),
|
||||
ttl: mock(async () => -1),
|
||||
type: 'memory' as const,
|
||||
};
|
||||
|
||||
adapter = new CacheAdapter(mockICache);
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should delegate to ICache.get', async () => {
|
||||
const data = { value: 'test' };
|
||||
(mockICache.get as any).mockResolvedValue(data);
|
||||
|
||||
const result = await adapter.get('key');
|
||||
|
||||
expect(mockICache.get).toHaveBeenCalledWith('key');
|
||||
expect(result).toEqual(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should handle TTL as number', async () => {
|
||||
await adapter.set('key', 'value', 3600);
|
||||
|
||||
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 3600);
|
||||
});
|
||||
|
||||
it('should handle TTL as options object', async () => {
|
||||
await adapter.set('key', 'value', { ttl: 7200 });
|
||||
|
||||
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 7200);
|
||||
});
|
||||
|
||||
it('should handle no TTL', async () => {
|
||||
await adapter.set('key', 'value');
|
||||
|
||||
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', undefined);
|
||||
});
|
||||
|
||||
it('should always return null', async () => {
|
||||
const result = await adapter.set('key', 'value');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('del', () => {
|
||||
it('should delegate to ICache.del', async () => {
|
||||
await adapter.del('key');
|
||||
|
||||
expect(mockICache.del).toHaveBeenCalledWith('key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should delegate to ICache.exists', async () => {
|
||||
(mockICache.exists as any).mockResolvedValue(true);
|
||||
|
||||
const result = await adapter.exists('key');
|
||||
|
||||
expect(mockICache.exists).toHaveBeenCalledWith('key');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should delegate to ICache.clear', async () => {
|
||||
await adapter.clear();
|
||||
|
||||
expect(mockICache.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys', () => {
|
||||
it('should delegate to ICache.keys', async () => {
|
||||
const keys = ['key1', 'key2'];
|
||||
(mockICache.keys as any).mockResolvedValue(keys);
|
||||
|
||||
const result = await adapter.keys('*');
|
||||
|
||||
expect(mockICache.keys).toHaveBeenCalledWith('*');
|
||||
expect(result).toEqual(keys);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return default stats', () => {
|
||||
const stats = adapter.getStats();
|
||||
|
||||
expect(stats).toEqual({
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('health', () => {
|
||||
it('should use ping for health check', async () => {
|
||||
(mockICache.ping as any).mockResolvedValue(true);
|
||||
|
||||
const result = await adapter.health();
|
||||
|
||||
expect(mockICache.ping).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle ping failures', async () => {
|
||||
(mockICache.ping as any).mockResolvedValue(false);
|
||||
|
||||
const result = await adapter.health();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForReady', () => {
|
||||
it('should succeed if connected', async () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(true);
|
||||
|
||||
await expect(adapter.waitForReady()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if not connected', async () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(false);
|
||||
|
||||
await expect(adapter.waitForReady()).rejects.toThrow('Cache not connected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isReady', () => {
|
||||
it('should delegate to isConnected', () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(true);
|
||||
|
||||
const result = adapter.isReady();
|
||||
|
||||
expect(mockICache.isConnected).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when not connected', () => {
|
||||
(mockICache.isConnected as any).mockReturnValue(false);
|
||||
|
||||
const result = adapter.isReady();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
74
libs/core/cache/test/redis-cache-simple.test.ts
vendored
74
libs/core/cache/test/redis-cache-simple.test.ts
vendored
|
|
@ -1,37 +1,37 @@
|
|||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import { RedisCache } from '../src/redis-cache';
|
||||
import type { CacheOptions } from '../src/types';
|
||||
|
||||
describe('RedisCache Simple', () => {
|
||||
let cache: RedisCache;
|
||||
|
||||
beforeEach(() => {
|
||||
const options: CacheOptions = {
|
||||
keyPrefix: 'test:',
|
||||
ttl: 3600,
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
};
|
||||
cache = new RedisCache(options);
|
||||
});
|
||||
|
||||
describe('Core functionality', () => {
|
||||
it('should create cache instance', () => {
|
||||
expect(cache).toBeDefined();
|
||||
expect(cache.isReady).toBeDefined();
|
||||
expect(cache.get).toBeDefined();
|
||||
expect(cache.set).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have stats tracking', () => {
|
||||
const stats = cache.getStats();
|
||||
expect(stats).toMatchObject({
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
});
|
||||
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { RedisCache } from '../src/redis-cache';
|
||||
import type { CacheOptions } from '../src/types';
|
||||
|
||||
describe('RedisCache Simple', () => {
|
||||
let cache: RedisCache;
|
||||
|
||||
beforeEach(() => {
|
||||
const options: CacheOptions = {
|
||||
keyPrefix: 'test:',
|
||||
ttl: 3600,
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
};
|
||||
cache = new RedisCache(options);
|
||||
});
|
||||
|
||||
describe('Core functionality', () => {
|
||||
it('should create cache instance', () => {
|
||||
expect(cache).toBeDefined();
|
||||
expect(cache.isReady).toBeDefined();
|
||||
expect(cache.get).toBeDefined();
|
||||
expect(cache.set).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have stats tracking', () => {
|
||||
const stats = cache.getStats();
|
||||
expect(stats).toMatchObject({
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
});
|
||||
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
420
libs/core/cache/test/redis-cache.test.ts
vendored
420
libs/core/cache/test/redis-cache.test.ts
vendored
|
|
@ -1,210 +1,210 @@
|
|||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import { RedisCache } from '../src/redis-cache';
|
||||
import type { CacheOptions } from '../src/types';
|
||||
|
||||
describe('RedisCache', () => {
|
||||
let cache: RedisCache;
|
||||
|
||||
beforeEach(() => {
|
||||
const options: CacheOptions = {
|
||||
keyPrefix: 'test:',
|
||||
ttl: 3600,
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
};
|
||||
cache = new RedisCache(options);
|
||||
});
|
||||
|
||||
describe('Core functionality', () => {
|
||||
it('should create cache instance', () => {
|
||||
expect(cache).toBeDefined();
|
||||
expect(cache.isReady).toBeDefined();
|
||||
expect(cache.get).toBeDefined();
|
||||
expect(cache.set).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have stats tracking', () => {
|
||||
const stats = cache.getStats();
|
||||
expect(stats).toMatchObject({
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
});
|
||||
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic operations', () => {
|
||||
it('should handle get/set operations', async () => {
|
||||
const key = 'test-key';
|
||||
const value = { foo: 'bar' };
|
||||
|
||||
// Should return null for non-existent key
|
||||
const miss = await cache.get(key);
|
||||
expect(miss).toBeNull();
|
||||
|
||||
// Should set and retrieve value
|
||||
await cache.set(key, value);
|
||||
const retrieved = await cache.get(key);
|
||||
expect(retrieved).toEqual(value);
|
||||
|
||||
// Should delete key
|
||||
await cache.del(key);
|
||||
const deleted = await cache.get(key);
|
||||
expect(deleted).toBeNull();
|
||||
});
|
||||
|
||||
it('should check key existence', async () => {
|
||||
const key = 'existence-test';
|
||||
|
||||
expect(await cache.exists(key)).toBe(false);
|
||||
|
||||
await cache.set(key, 'value');
|
||||
expect(await cache.exists(key)).toBe(true);
|
||||
|
||||
await cache.del(key);
|
||||
expect(await cache.exists(key)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle TTL in set operations', async () => {
|
||||
const key = 'ttl-test';
|
||||
const value = 'test-value';
|
||||
|
||||
// Set with custom TTL as number
|
||||
await cache.set(key, value, 1);
|
||||
expect(await cache.get(key)).toBe(value);
|
||||
|
||||
// Set with custom TTL in options
|
||||
await cache.set(key, value, { ttl: 2 });
|
||||
expect(await cache.get(key)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advanced set options', () => {
|
||||
it('should handle onlyIfExists option', async () => {
|
||||
const key = 'conditional-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Should not set if key doesn't exist
|
||||
await cache.set(key, value1, { onlyIfExists: true });
|
||||
expect(await cache.get(key)).toBeNull();
|
||||
|
||||
// Create the key
|
||||
await cache.set(key, value1);
|
||||
|
||||
// Should update if key exists
|
||||
await cache.set(key, value2, { onlyIfExists: true });
|
||||
expect(await cache.get(key)).toBe(value2);
|
||||
});
|
||||
|
||||
it('should handle onlyIfNotExists option', async () => {
|
||||
const key = 'nx-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Should set if key doesn't exist
|
||||
await cache.set(key, value1, { onlyIfNotExists: true });
|
||||
expect(await cache.get(key)).toBe(value1);
|
||||
|
||||
// Should not update if key exists
|
||||
await cache.set(key, value2, { onlyIfNotExists: true });
|
||||
expect(await cache.get(key)).toBe(value1);
|
||||
});
|
||||
|
||||
it('should handle preserveTTL option', async () => {
|
||||
const key = 'preserve-ttl-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Set with short TTL
|
||||
await cache.set(key, value1, 10);
|
||||
|
||||
// Update preserving TTL
|
||||
await cache.set(key, value2, { preserveTTL: true });
|
||||
expect(await cache.get(key)).toBe(value2);
|
||||
});
|
||||
|
||||
it('should handle getOldValue option', async () => {
|
||||
const key = 'old-value-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Should return null when no old value
|
||||
const oldValue1 = await cache.set(key, value1, { getOldValue: true });
|
||||
expect(oldValue1).toBeNull();
|
||||
|
||||
// Should return old value
|
||||
const oldValue2 = await cache.set(key, value2, { getOldValue: true });
|
||||
expect(oldValue2).toBe(value1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle errors gracefully in get', async () => {
|
||||
// Force an error by using invalid JSON
|
||||
const badCache = new RedisCache({
|
||||
keyPrefix: 'bad:',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
// This would normally throw but should return null
|
||||
const result = await badCache.get('non-existent');
|
||||
expect(result).toBeNull();
|
||||
|
||||
// Check stats updated
|
||||
const stats = badCache.getStats();
|
||||
expect(stats.misses).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pattern operations', () => {
|
||||
it('should find keys by pattern', async () => {
|
||||
// Clear first to ensure clean state
|
||||
await cache.clear();
|
||||
|
||||
await cache.set('user:1', { id: 1 });
|
||||
await cache.set('user:2', { id: 2 });
|
||||
await cache.set('post:1', { id: 1 });
|
||||
|
||||
const userKeys = await cache.keys('user:*');
|
||||
expect(userKeys).toHaveLength(2);
|
||||
expect(userKeys).toContain('user:1');
|
||||
expect(userKeys).toContain('user:2');
|
||||
|
||||
const allKeys = await cache.keys('*');
|
||||
expect(allKeys.length).toBeGreaterThanOrEqual(3);
|
||||
expect(allKeys).toContain('user:1');
|
||||
expect(allKeys).toContain('user:2');
|
||||
expect(allKeys).toContain('post:1');
|
||||
});
|
||||
|
||||
it('should clear all keys with prefix', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
await cache.set('key2', 'value2');
|
||||
|
||||
await cache.clear();
|
||||
|
||||
const keys = await cache.keys('*');
|
||||
expect(keys).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health checks', () => {
|
||||
it('should check health', async () => {
|
||||
const healthy = await cache.health();
|
||||
expect(healthy).toBe(true);
|
||||
});
|
||||
|
||||
it('should check if ready', () => {
|
||||
// May not be ready immediately
|
||||
const ready = cache.isReady();
|
||||
expect(typeof ready).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should wait for ready', async () => {
|
||||
await expect(cache.waitForReady(1000)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { RedisCache } from '../src/redis-cache';
|
||||
import type { CacheOptions } from '../src/types';
|
||||
|
||||
describe('RedisCache', () => {
|
||||
let cache: RedisCache;
|
||||
|
||||
beforeEach(() => {
|
||||
const options: CacheOptions = {
|
||||
keyPrefix: 'test:',
|
||||
ttl: 3600,
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
};
|
||||
cache = new RedisCache(options);
|
||||
});
|
||||
|
||||
describe('Core functionality', () => {
|
||||
it('should create cache instance', () => {
|
||||
expect(cache).toBeDefined();
|
||||
expect(cache.isReady).toBeDefined();
|
||||
expect(cache.get).toBeDefined();
|
||||
expect(cache.set).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have stats tracking', () => {
|
||||
const stats = cache.getStats();
|
||||
expect(stats).toMatchObject({
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
});
|
||||
expect(stats.uptime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Basic operations', () => {
|
||||
it('should handle get/set operations', async () => {
|
||||
const key = 'test-key';
|
||||
const value = { foo: 'bar' };
|
||||
|
||||
// Should return null for non-existent key
|
||||
const miss = await cache.get(key);
|
||||
expect(miss).toBeNull();
|
||||
|
||||
// Should set and retrieve value
|
||||
await cache.set(key, value);
|
||||
const retrieved = await cache.get(key);
|
||||
expect(retrieved).toEqual(value);
|
||||
|
||||
// Should delete key
|
||||
await cache.del(key);
|
||||
const deleted = await cache.get(key);
|
||||
expect(deleted).toBeNull();
|
||||
});
|
||||
|
||||
it('should check key existence', async () => {
|
||||
const key = 'existence-test';
|
||||
|
||||
expect(await cache.exists(key)).toBe(false);
|
||||
|
||||
await cache.set(key, 'value');
|
||||
expect(await cache.exists(key)).toBe(true);
|
||||
|
||||
await cache.del(key);
|
||||
expect(await cache.exists(key)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle TTL in set operations', async () => {
|
||||
const key = 'ttl-test';
|
||||
const value = 'test-value';
|
||||
|
||||
// Set with custom TTL as number
|
||||
await cache.set(key, value, 1);
|
||||
expect(await cache.get(key)).toBe(value);
|
||||
|
||||
// Set with custom TTL in options
|
||||
await cache.set(key, value, { ttl: 2 });
|
||||
expect(await cache.get(key)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advanced set options', () => {
|
||||
it('should handle onlyIfExists option', async () => {
|
||||
const key = 'conditional-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Should not set if key doesn't exist
|
||||
await cache.set(key, value1, { onlyIfExists: true });
|
||||
expect(await cache.get(key)).toBeNull();
|
||||
|
||||
// Create the key
|
||||
await cache.set(key, value1);
|
||||
|
||||
// Should update if key exists
|
||||
await cache.set(key, value2, { onlyIfExists: true });
|
||||
expect(await cache.get(key)).toBe(value2);
|
||||
});
|
||||
|
||||
it('should handle onlyIfNotExists option', async () => {
|
||||
const key = 'nx-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Should set if key doesn't exist
|
||||
await cache.set(key, value1, { onlyIfNotExists: true });
|
||||
expect(await cache.get(key)).toBe(value1);
|
||||
|
||||
// Should not update if key exists
|
||||
await cache.set(key, value2, { onlyIfNotExists: true });
|
||||
expect(await cache.get(key)).toBe(value1);
|
||||
});
|
||||
|
||||
it('should handle preserveTTL option', async () => {
|
||||
const key = 'preserve-ttl-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Set with short TTL
|
||||
await cache.set(key, value1, 10);
|
||||
|
||||
// Update preserving TTL
|
||||
await cache.set(key, value2, { preserveTTL: true });
|
||||
expect(await cache.get(key)).toBe(value2);
|
||||
});
|
||||
|
||||
it('should handle getOldValue option', async () => {
|
||||
const key = 'old-value-test';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
// Should return null when no old value
|
||||
const oldValue1 = await cache.set(key, value1, { getOldValue: true });
|
||||
expect(oldValue1).toBeNull();
|
||||
|
||||
// Should return old value
|
||||
const oldValue2 = await cache.set(key, value2, { getOldValue: true });
|
||||
expect(oldValue2).toBe(value1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle errors gracefully in get', async () => {
|
||||
// Force an error by using invalid JSON
|
||||
const badCache = new RedisCache({
|
||||
keyPrefix: 'bad:',
|
||||
redisConfig: { host: 'localhost', port: 6379 },
|
||||
});
|
||||
|
||||
// This would normally throw but should return null
|
||||
const result = await badCache.get('non-existent');
|
||||
expect(result).toBeNull();
|
||||
|
||||
// Check stats updated
|
||||
const stats = badCache.getStats();
|
||||
expect(stats.misses).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pattern operations', () => {
|
||||
it('should find keys by pattern', async () => {
|
||||
// Clear first to ensure clean state
|
||||
await cache.clear();
|
||||
|
||||
await cache.set('user:1', { id: 1 });
|
||||
await cache.set('user:2', { id: 2 });
|
||||
await cache.set('post:1', { id: 1 });
|
||||
|
||||
const userKeys = await cache.keys('user:*');
|
||||
expect(userKeys).toHaveLength(2);
|
||||
expect(userKeys).toContain('user:1');
|
||||
expect(userKeys).toContain('user:2');
|
||||
|
||||
const allKeys = await cache.keys('*');
|
||||
expect(allKeys.length).toBeGreaterThanOrEqual(3);
|
||||
expect(allKeys).toContain('user:1');
|
||||
expect(allKeys).toContain('user:2');
|
||||
expect(allKeys).toContain('post:1');
|
||||
});
|
||||
|
||||
it('should clear all keys with prefix', async () => {
|
||||
await cache.set('key1', 'value1');
|
||||
await cache.set('key2', 'value2');
|
||||
|
||||
await cache.clear();
|
||||
|
||||
const keys = await cache.keys('*');
|
||||
expect(keys).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health checks', () => {
|
||||
it('should check health', async () => {
|
||||
const healthy = await cache.health();
|
||||
expect(healthy).toBe(true);
|
||||
});
|
||||
|
||||
it('should check if ready', () => {
|
||||
// May not be ready immediately
|
||||
const ready = cache.isReady();
|
||||
expect(typeof ready).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should wait for ready', async () => {
|
||||
await expect(cache.waitForReady(1000)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue