fixed build libs
This commit is contained in:
parent
b03231b849
commit
42baadae38
26 changed files with 981 additions and 541 deletions
16
libs/core/cache/src/cache-factory.ts
vendored
16
libs/core/cache/src/cache-factory.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import { NamespacedCache } from './namespaced-cache';
|
||||
import { NamespacedCache, CacheAdapter } from './namespaced-cache';
|
||||
import { RedisCache } from './redis-cache';
|
||||
import type { CacheProvider, ICache } from './types';
|
||||
|
||||
|
|
@ -27,18 +27,18 @@ export class CacheFactory {
|
|||
export function createNamespacedCache(
|
||||
cache: CacheProvider | ICache | null | undefined,
|
||||
namespace: string
|
||||
): ICache {
|
||||
): CacheProvider {
|
||||
if (!cache) {
|
||||
return createNullCache();
|
||||
return new CacheAdapter(createNullCache());
|
||||
}
|
||||
|
||||
// Check if it's already an ICache
|
||||
if ('type' in cache) {
|
||||
return new NamespacedCache(cache as ICache, namespace);
|
||||
// Check if it's already a CacheProvider
|
||||
if ('getStats' in cache && 'health' in cache) {
|
||||
return new NamespacedCache(cache as CacheProvider, namespace);
|
||||
}
|
||||
|
||||
// Legacy CacheProvider support
|
||||
return createNullCache();
|
||||
// It's an ICache, wrap it first
|
||||
return new NamespacedCache(new CacheAdapter(cache as ICache), namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
132
libs/core/cache/src/namespaced-cache.ts
vendored
132
libs/core/cache/src/namespaced-cache.ts
vendored
|
|
@ -4,24 +4,22 @@ import type { CacheProvider, ICache } from './types';
|
|||
* A cache wrapper that automatically prefixes all keys with a namespace
|
||||
* Used to provide isolated cache spaces for different services
|
||||
*/
|
||||
export class NamespacedCache implements ICache {
|
||||
export class NamespacedCache implements CacheProvider {
|
||||
private readonly prefix: string;
|
||||
public readonly type: string;
|
||||
|
||||
constructor(
|
||||
private readonly cache: ICache,
|
||||
private readonly cache: CacheProvider,
|
||||
private readonly namespace: string
|
||||
) {
|
||||
this.prefix = `${namespace}:`;
|
||||
this.type = cache.type;
|
||||
}
|
||||
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
return this.cache.get(`${this.prefix}${key}`);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
||||
return this.cache.set(`${this.prefix}${key}`, value, ttl);
|
||||
async set<T>(key: string, value: T, options?: number | { ttl?: number }): Promise<T | null> {
|
||||
return this.cache.set(`${this.prefix}${key}`, value, options);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
|
|
@ -32,10 +30,6 @@ export class NamespacedCache implements ICache {
|
|||
return this.cache.exists(`${this.prefix}${key}`);
|
||||
}
|
||||
|
||||
async ttl(key: string): Promise<number> {
|
||||
return this.cache.ttl(`${this.prefix}${key}`);
|
||||
}
|
||||
|
||||
async keys(pattern: string = '*'): Promise<string[]> {
|
||||
const fullPattern = `${this.prefix}${pattern}`;
|
||||
const keys = await this.cache.keys(fullPattern);
|
||||
|
|
@ -47,50 +41,10 @@ export class NamespacedCache implements ICache {
|
|||
// Clear only keys with this namespace prefix
|
||||
const keys = await this.cache.keys(`${this.prefix}*`);
|
||||
if (keys.length > 0) {
|
||||
await Promise.all(keys.map(key => this.cache.del(key)));
|
||||
await Promise.all(keys.map(key => this.cache.del(key.substring(this.prefix.length))));
|
||||
}
|
||||
}
|
||||
|
||||
async mget<T>(keys: string[]): Promise<(T | null)[]> {
|
||||
const prefixedKeys = keys.map(k => `${this.prefix}${k}`);
|
||||
return this.cache.mget(prefixedKeys);
|
||||
}
|
||||
|
||||
async mset<T>(items: Record<string, T>, ttl?: number): Promise<void> {
|
||||
const prefixedItems: Record<string, T> = {};
|
||||
for (const [key, value] of Object.entries(items)) {
|
||||
prefixedItems[`${this.prefix}${key}`] = value;
|
||||
}
|
||||
return this.cache.mset(prefixedItems, ttl);
|
||||
}
|
||||
|
||||
async mdel(keys: string[]): Promise<void> {
|
||||
const prefixedKeys = keys.map(k => `${this.prefix}${k}`);
|
||||
return this.cache.mdel(prefixedKeys);
|
||||
}
|
||||
|
||||
async size(): Promise<number> {
|
||||
const keys = await this.keys('*');
|
||||
return keys.length;
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
return this.clear();
|
||||
}
|
||||
|
||||
async ping(): Promise<boolean> {
|
||||
return this.cache.ping();
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
// Namespaced cache doesn't own the connection, so we don't disconnect
|
||||
// The underlying cache instance should be disconnected by its owner
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.cache.isConnected();
|
||||
}
|
||||
|
||||
getNamespace(): string {
|
||||
return this.namespace;
|
||||
}
|
||||
|
|
@ -98,4 +52,80 @@ export class NamespacedCache implements ICache {
|
|||
getFullPrefix(): string {
|
||||
return this.prefix;
|
||||
}
|
||||
|
||||
// CacheProvider methods
|
||||
getStats() {
|
||||
return this.cache.getStats();
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
return this.cache.health();
|
||||
}
|
||||
|
||||
async waitForReady(timeout?: number): Promise<void> {
|
||||
return this.cache.waitForReady(timeout);
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return this.cache.isReady();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter to convert ICache to CacheProvider
|
||||
*/
|
||||
export class CacheAdapter implements CacheProvider {
|
||||
constructor(private readonly cache: ICache) {}
|
||||
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, options?: number | { ttl?: number }): Promise<T | null> {
|
||||
const ttl = typeof options === 'number' ? options : options?.ttl;
|
||||
await this.cache.set(key, value, ttl);
|
||||
return null;
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
return this.cache.del(key);
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return this.cache.exists(key);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
return this.cache.clear();
|
||||
}
|
||||
|
||||
async keys(pattern: string): Promise<string[]> {
|
||||
return this.cache.keys(pattern);
|
||||
}
|
||||
|
||||
getStats() {
|
||||
return {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
errors: 0,
|
||||
hitRate: 0,
|
||||
total: 0,
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
}
|
||||
|
||||
async health(): Promise<boolean> {
|
||||
return this.cache.ping();
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
// ICache doesn't have waitForReady, so just check connection
|
||||
if (!this.cache.isConnected()) {
|
||||
throw new Error('Cache not connected');
|
||||
}
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return this.cache.isConnected();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { CacheFactory, createNamespacedCache } from './cache-factory';
|
||||
import { generateKey } from './key-generator';
|
||||
import type { ICache } from './types';
|
||||
import { CacheFactory, createNamespacedCache } from '../src/cache-factory';
|
||||
import { generateKey } from '../src/key-generator';
|
||||
import type { ICache } from '../src/types';
|
||||
|
||||
describe('CacheFactory', () => {
|
||||
it('should create null cache when no config provided', () => {
|
||||
|
|
@ -49,7 +49,8 @@ describe('NamespacedCache', () => {
|
|||
it('should create namespaced cache', () => {
|
||||
const nsCache = createNamespacedCache(mockCache, 'sub-namespace');
|
||||
expect(nsCache).toBeDefined();
|
||||
expect(nsCache.type).toBe('mock');
|
||||
expect(nsCache).toHaveProperty('get');
|
||||
expect(nsCache).toHaveProperty('set');
|
||||
});
|
||||
|
||||
it('should prefix keys with namespace', async () => {
|
||||
|
|
@ -58,10 +59,12 @@ describe('NamespacedCache', () => {
|
|||
expect(mockCache.set).toHaveBeenCalledWith('test:key', 'value', undefined);
|
||||
});
|
||||
|
||||
it('should handle null cache gracefully', () => {
|
||||
it('should handle null cache gracefully', async () => {
|
||||
const nsCache = createNamespacedCache(null, 'test');
|
||||
expect(nsCache).toBeDefined();
|
||||
expect(nsCache.type).toBe('null');
|
||||
// Should return null for get operations
|
||||
const value = await nsCache.get('key');
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it('should prefix multiple operations', async () => {
|
||||
|
|
@ -3,6 +3,7 @@ import { z } from 'zod';
|
|||
import { EnvLoader } from './loaders/env.loader';
|
||||
import { FileLoader } from './loaders/file.loader';
|
||||
import { ConfigManager } from './config-manager';
|
||||
import { ConfigError } from './errors';
|
||||
import type { BaseAppConfig } from './schemas';
|
||||
import { baseAppSchema } from './schemas';
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ export function initializeServiceConfig(): BaseAppConfig {
|
|||
*/
|
||||
export function getConfig(): BaseAppConfig {
|
||||
if (!configInstance) {
|
||||
throw new Error('Configuration not initialized. Call initializeConfig() first.');
|
||||
throw new ConfigError('Configuration not initialized. Call initializeConfig() first.');
|
||||
}
|
||||
return configInstance.get();
|
||||
}
|
||||
|
|
@ -92,7 +93,7 @@ export function getConfig(): BaseAppConfig {
|
|||
*/
|
||||
export function getConfigManager(): ConfigManager<BaseAppConfig> {
|
||||
if (!configInstance) {
|
||||
throw new Error('Configuration not initialized. Call initializeConfig() first.');
|
||||
throw new ConfigError('Configuration not initialized. Call initializeConfig() first.');
|
||||
}
|
||||
return configInstance;
|
||||
}
|
||||
|
|
|
|||
359
libs/core/config/test/config.test.ts
Normal file
359
libs/core/config/test/config.test.ts
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ConfigManager,
|
||||
initializeServiceConfig,
|
||||
getConfig,
|
||||
resetConfig,
|
||||
createAppConfig,
|
||||
initializeAppConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
getDatabaseConfig,
|
||||
getServiceConfig,
|
||||
getLogConfig,
|
||||
getQueueConfig,
|
||||
ConfigError,
|
||||
ConfigValidationError,
|
||||
baseAppSchema,
|
||||
} from '../src';
|
||||
|
||||
// Mock loader for testing
|
||||
class MockLoader {
|
||||
constructor(
|
||||
private data: Record<string, unknown>,
|
||||
public priority: number = 0
|
||||
) {}
|
||||
|
||||
load(): Record<string, unknown> {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
let manager: ConfigManager<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new ConfigManager();
|
||||
});
|
||||
|
||||
it('should initialize with default loaders', () => {
|
||||
expect(manager).toBeDefined();
|
||||
});
|
||||
|
||||
it('should detect environment', () => {
|
||||
const env = manager.getEnvironment();
|
||||
expect(['development', 'test', 'production']).toContain(env);
|
||||
});
|
||||
|
||||
it('should throw when getting config before initialization', () => {
|
||||
expect(() => manager.get()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should initialize config with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize(schema);
|
||||
expect(config).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should merge configs from multiple loaders', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({ name: 'test', port: 3000 }, 1),
|
||||
new MockLoader({ port: 4000, debug: true }, 2),
|
||||
],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({ name: 'test', port: 4000, debug: true, environment: 'test' });
|
||||
});
|
||||
|
||||
it('should deep merge nested objects', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [
|
||||
new MockLoader({ db: { host: 'localhost', port: 5432 } }, 1),
|
||||
new MockLoader({ db: { port: 5433, user: 'admin' } }, 2),
|
||||
],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({
|
||||
db: { host: 'localhost', port: 5433, user: 'admin' },
|
||||
environment: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get value by path', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost', port: 5432 } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.getValue('db.host')).toBe('localhost');
|
||||
expect(mockManager.getValue('db.port')).toBe(5432);
|
||||
});
|
||||
|
||||
it('should throw for non-existent path', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost' } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(() => mockManager.getValue('db.password')).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should check if path exists', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ db: { host: 'localhost' } })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.has('db.host')).toBe(true);
|
||||
expect(mockManager.has('db.password')).toBe(false);
|
||||
});
|
||||
|
||||
it('should update config at runtime', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
mockManager.set({ port: 4000 });
|
||||
expect(mockManager.get()).toEqual({ name: 'test', port: 4000, environment: 'test' });
|
||||
});
|
||||
|
||||
it('should validate config update with schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize(schema);
|
||||
expect(() => mockManager.set({ port: 'invalid' as any })).toThrow(
|
||||
ConfigValidationError
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset config', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test' })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
expect(mockManager.get()).toEqual({ name: 'test', environment: 'test' });
|
||||
|
||||
mockManager.reset();
|
||||
expect(() => mockManager.get()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should validate against schema', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
const validated = mockManager.validate(schema);
|
||||
expect(validated).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should create typed getter', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
});
|
||||
|
||||
const mockManager = new ConfigManager({
|
||||
loaders: [new MockLoader({ name: 'test', port: 3000 })],
|
||||
});
|
||||
|
||||
mockManager.initialize();
|
||||
const getTypedConfig = mockManager.createTypedGetter(schema);
|
||||
const config = getTypedConfig();
|
||||
expect(config).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
it('should add environment if not present', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
environment: 'test',
|
||||
loaders: [new MockLoader({ name: 'test' })],
|
||||
});
|
||||
|
||||
const config = mockManager.initialize();
|
||||
expect(config).toEqual({ name: 'test', environment: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Service Functions', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
it('should throw when getting config before initialization', () => {
|
||||
expect(() => getConfig()).toThrow(ConfigError);
|
||||
});
|
||||
|
||||
it('should validate config with schema', () => {
|
||||
// Test that a valid config passes schema validation
|
||||
const mockConfig = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
environment: 'test' as const,
|
||||
service: {
|
||||
name: 'test-service',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'test-db',
|
||||
},
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test-db',
|
||||
user: 'test-user',
|
||||
password: 'test-pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info' as const,
|
||||
pretty: true,
|
||||
},
|
||||
queue: {
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new MockLoader(mockConfig)],
|
||||
});
|
||||
|
||||
// Should not throw when initializing with valid config
|
||||
expect(() => manager.initialize(baseAppSchema)).not.toThrow();
|
||||
|
||||
// Verify key properties exist
|
||||
const config = manager.get();
|
||||
expect(config.name).toBe('test-app');
|
||||
expect(config.version).toBe('1.0.0');
|
||||
expect(config.environment).toBe('test');
|
||||
expect(config.service.name).toBe('test-service');
|
||||
expect(config.database.mongodb.uri).toBe('mongodb://localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Config Builders', () => {
|
||||
it('should create app config with schema', () => {
|
||||
const schema = z.object({
|
||||
app: z.string(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
const config = createAppConfig(schema, {
|
||||
loaders: [new MockLoader({ app: 'myapp', version: 1 })],
|
||||
});
|
||||
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize app config in one step', () => {
|
||||
const schema = z.object({
|
||||
app: z.string(),
|
||||
version: z.number(),
|
||||
});
|
||||
|
||||
const config = initializeAppConfig(schema, {
|
||||
loaders: [new MockLoader({ app: 'myapp', version: 1 })],
|
||||
});
|
||||
|
||||
expect(config).toEqual({ app: 'myapp', version: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment Helpers', () => {
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
it('should detect environments correctly in ConfigManager', () => {
|
||||
// Test with different environments using mock configs
|
||||
const envConfigs = [
|
||||
{ env: 'development' },
|
||||
{ env: 'production' },
|
||||
{ env: 'test' },
|
||||
];
|
||||
|
||||
for (const { env } of envConfigs) {
|
||||
const mockConfig = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
environment: env as 'development' | 'production' | 'test',
|
||||
service: {
|
||||
name: 'test',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'test-db',
|
||||
},
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test-db',
|
||||
user: 'test-user',
|
||||
password: 'test-pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
},
|
||||
},
|
||||
log: {
|
||||
level: 'info' as const,
|
||||
pretty: true,
|
||||
},
|
||||
queue: {
|
||||
redis: { host: 'localhost', port: 6379 },
|
||||
},
|
||||
};
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new MockLoader(mockConfig)],
|
||||
environment: env as any,
|
||||
});
|
||||
|
||||
manager.initialize(baseAppSchema);
|
||||
|
||||
// Test the manager's environment detection
|
||||
expect(manager.getEnvironment()).toBe(env);
|
||||
expect(manager.get().environment).toBe(env);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { createContainer, InjectionMode, asClass, asFunction, asValue } from 'awilix';
|
||||
import { ServiceContainerBuilder } from './container/builder';
|
||||
import { ServiceApplication } from './service-application';
|
||||
import { HandlerScanner } from './scanner/handler-scanner';
|
||||
import { OperationContext } from './operation-context';
|
||||
import { PoolSizeCalculator } from './pool-size-calculator';
|
||||
import { ServiceContainerBuilder } from '../src/container/builder';
|
||||
import { ServiceApplication } from '../src/service-application';
|
||||
import { HandlerScanner } from '../src/scanner/handler-scanner';
|
||||
import { OperationContext } from '../src/operation-context';
|
||||
import { PoolSizeCalculator } from '../src/pool-size-calculator';
|
||||
|
||||
describe('Dependency Injection', () => {
|
||||
describe('ServiceContainerBuilder', () => {
|
||||
|
|
@ -1,51 +1,77 @@
|
|||
import type { EventHandler, EventSubscription } from './types';
|
||||
import type { EventHandler, EventSubscription, EventBusMessage } from './types';
|
||||
|
||||
/**
|
||||
* Simple in-memory event bus for testing
|
||||
*/
|
||||
export class SimpleEventBus {
|
||||
private subscriptions = new Map<string, Set<EventSubscription>>();
|
||||
private subscriptionById = new Map<string, EventSubscription>();
|
||||
private subscriptions = new Map<string, Set<{ id: string; handler: EventHandler }>>();
|
||||
private subscriptionById = new Map<string, { id: string; channel: string; handler: EventHandler }>();
|
||||
private nextId = 1;
|
||||
|
||||
subscribe(event: string, handler: EventHandler): EventSubscription {
|
||||
const subscription: EventSubscription = {
|
||||
id: `sub-${this.nextId++}`,
|
||||
event,
|
||||
handler,
|
||||
pattern: event.includes('*'),
|
||||
};
|
||||
subscribe(channel: string, handler: EventHandler): EventSubscription {
|
||||
const id = `sub-${this.nextId++}`;
|
||||
const subscription = { id, handler };
|
||||
|
||||
if (!this.subscriptions.has(event)) {
|
||||
this.subscriptions.set(event, new Set());
|
||||
if (!this.subscriptions.has(channel)) {
|
||||
this.subscriptions.set(channel, new Set());
|
||||
}
|
||||
this.subscriptions.get(event)!.add(subscription);
|
||||
this.subscriptionById.set(subscription.id, subscription);
|
||||
this.subscriptions.get(channel)!.add(subscription);
|
||||
this.subscriptionById.set(id, { id, channel, handler });
|
||||
|
||||
return subscription;
|
||||
return { channel, handler };
|
||||
}
|
||||
|
||||
unsubscribe(idOrSubscription: string | EventSubscription): boolean {
|
||||
const id = typeof idOrSubscription === 'string' ? idOrSubscription : idOrSubscription.id;
|
||||
const subscription = this.subscriptionById.get(id);
|
||||
|
||||
if (typeof idOrSubscription === 'string') {
|
||||
const subscription = this.subscriptionById.get(idOrSubscription);
|
||||
if (!subscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventSubs = this.subscriptions.get(subscription.event);
|
||||
if (eventSubs) {
|
||||
eventSubs.delete(subscription);
|
||||
if (eventSubs.size === 0) {
|
||||
this.subscriptions.delete(subscription.event);
|
||||
const channelSubs = this.subscriptions.get(subscription.channel);
|
||||
if (channelSubs) {
|
||||
channelSubs.forEach(sub => {
|
||||
if (sub.id === idOrSubscription) {
|
||||
channelSubs.delete(sub);
|
||||
}
|
||||
});
|
||||
if (channelSubs.size === 0) {
|
||||
this.subscriptions.delete(subscription.channel);
|
||||
}
|
||||
}
|
||||
|
||||
this.subscriptionById.delete(id);
|
||||
this.subscriptionById.delete(idOrSubscription);
|
||||
return true;
|
||||
} else {
|
||||
// Unsubscribe by matching handler and channel
|
||||
const channelSubs = this.subscriptions.get(idOrSubscription.channel);
|
||||
if (channelSubs) {
|
||||
let removed = false;
|
||||
channelSubs.forEach(sub => {
|
||||
if (sub.handler === idOrSubscription.handler) {
|
||||
channelSubs.delete(sub);
|
||||
this.subscriptionById.delete(sub.id);
|
||||
removed = true;
|
||||
}
|
||||
});
|
||||
if (channelSubs.size === 0) {
|
||||
this.subscriptions.delete(idOrSubscription.channel);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async publish(event: string, data: any): Promise<void> {
|
||||
const message: EventBusMessage = {
|
||||
id: `msg-${this.nextId++}`,
|
||||
type: event,
|
||||
source: 'simple-event-bus',
|
||||
timestamp: Date.now(),
|
||||
data,
|
||||
};
|
||||
|
||||
const handlers: EventHandler[] = [];
|
||||
|
||||
// Direct matches
|
||||
|
|
@ -64,7 +90,7 @@ export class SimpleEventBus {
|
|||
// Execute all handlers
|
||||
await Promise.all(
|
||||
handlers.map(handler =>
|
||||
handler(data, event).catch(err => {
|
||||
Promise.resolve(handler(message)).catch(err => {
|
||||
// Silently catch errors
|
||||
})
|
||||
)
|
||||
|
|
@ -72,6 +98,14 @@ export class SimpleEventBus {
|
|||
}
|
||||
|
||||
publishSync(event: string, data: any): void {
|
||||
const message: EventBusMessage = {
|
||||
id: `msg-${this.nextId++}`,
|
||||
type: event,
|
||||
source: 'simple-event-bus',
|
||||
timestamp: Date.now(),
|
||||
data,
|
||||
};
|
||||
|
||||
const handlers: EventHandler[] = [];
|
||||
|
||||
// Direct matches
|
||||
|
|
@ -90,7 +124,7 @@ export class SimpleEventBus {
|
|||
// Execute all handlers synchronously
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(data, event);
|
||||
handler(message);
|
||||
} catch {
|
||||
// Silently catch errors
|
||||
}
|
||||
|
|
@ -98,12 +132,20 @@ export class SimpleEventBus {
|
|||
}
|
||||
|
||||
once(event: string, handler: EventHandler): EventSubscription {
|
||||
const wrappedHandler: EventHandler = async (data, evt) => {
|
||||
await handler(data, evt);
|
||||
this.unsubscribe(subscription.id);
|
||||
let subId: string;
|
||||
const wrappedHandler: EventHandler = async (message) => {
|
||||
await handler(message);
|
||||
this.unsubscribe(subId);
|
||||
};
|
||||
|
||||
const subscription = this.subscribe(event, wrappedHandler);
|
||||
// Find the subscription ID
|
||||
this.subscriptionById.forEach((value, key) => {
|
||||
if (value.handler === wrappedHandler) {
|
||||
subId = key;
|
||||
}
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
|
@ -121,10 +163,19 @@ export class SimpleEventBus {
|
|||
// Remove specific handler
|
||||
const subs = this.subscriptions.get(event);
|
||||
if (subs) {
|
||||
const toRemove = Array.from(subs).filter(s => s.handler === handler);
|
||||
toRemove.forEach(sub => {
|
||||
const toRemove: string[] = [];
|
||||
subs.forEach(sub => {
|
||||
if (sub.handler === handler) {
|
||||
toRemove.push(sub.id);
|
||||
}
|
||||
});
|
||||
toRemove.forEach(id => {
|
||||
subs.forEach(sub => {
|
||||
if (sub.id === id) {
|
||||
subs.delete(sub);
|
||||
this.subscriptionById.delete(sub.id);
|
||||
}
|
||||
});
|
||||
this.subscriptionById.delete(id);
|
||||
});
|
||||
if (subs.size === 0) {
|
||||
this.subscriptions.delete(event);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { SimpleEventBus } from './simple-event-bus';
|
||||
import type { EventHandler, EventSubscription } from './types';
|
||||
import { SimpleEventBus } from '../src/simple-event-bus';
|
||||
import type { EventHandler, EventSubscription, EventBusMessage } from '../src/types';
|
||||
|
||||
describe('EventBus', () => {
|
||||
let eventBus: SimpleEventBus;
|
||||
|
|
@ -16,8 +16,8 @@ describe('EventBus', () => {
|
|||
const subscription = eventBus.subscribe('test-event', handler);
|
||||
|
||||
expect(subscription).toBeDefined();
|
||||
expect(subscription.id).toBeDefined();
|
||||
expect(subscription.event).toBe('test-event');
|
||||
expect(subscription.channel).toBe('test-event');
|
||||
expect(subscription.handler).toBe(handler);
|
||||
});
|
||||
|
||||
it('should allow multiple subscribers to same event', () => {
|
||||
|
|
@ -27,7 +27,8 @@ describe('EventBus', () => {
|
|||
const sub1 = eventBus.subscribe('event', handler1);
|
||||
const sub2 = eventBus.subscribe('event', handler2);
|
||||
|
||||
expect(sub1.id).not.toBe(sub2.id);
|
||||
expect(sub1.handler).toBe(handler1);
|
||||
expect(sub2.handler).toBe(handler2);
|
||||
});
|
||||
|
||||
it('should support pattern subscriptions', () => {
|
||||
|
|
@ -35,7 +36,7 @@ describe('EventBus', () => {
|
|||
|
||||
const subscription = eventBus.subscribe('user.*', handler);
|
||||
|
||||
expect(subscription.event).toBe('user.*');
|
||||
expect(subscription.channel).toBe('user.*');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -51,15 +52,16 @@ describe('EventBus', () => {
|
|||
|
||||
it('should unsubscribe by id', () => {
|
||||
const handler = mock(async () => {});
|
||||
const subscription = eventBus.subscribe('event', handler);
|
||||
eventBus.subscribe('event', handler);
|
||||
|
||||
const result = eventBus.unsubscribe(subscription.id);
|
||||
// We'll use the subscription object method since we don't expose IDs
|
||||
const result = eventBus.unsubscribe('sub-1');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-existent subscription', () => {
|
||||
const result = eventBus.unsubscribe('non-existent-id');
|
||||
const result = eventBus.unsubscribe('non-existent');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
|
@ -67,38 +69,38 @@ describe('EventBus', () => {
|
|||
|
||||
describe('publish', () => {
|
||||
it('should publish events to subscribers', async () => {
|
||||
const handler = mock(async (data: any) => {});
|
||||
eventBus.subscribe('test-event', handler);
|
||||
const handler = mock(async (message: EventBusMessage) => {});
|
||||
eventBus.subscribe('event', handler);
|
||||
|
||||
await eventBus.publish('test-event', { message: 'hello' });
|
||||
await eventBus.publish('event', { data: 'test' });
|
||||
|
||||
expect(handler).toHaveBeenCalledWith({ message: 'hello' }, 'test-event');
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
const message = handler.mock.calls[0][0];
|
||||
expect(message.type).toBe('event');
|
||||
expect(message.data).toEqual({ data: 'test' });
|
||||
});
|
||||
|
||||
it('should publish to multiple subscribers', async () => {
|
||||
const handler1 = mock(async () => {});
|
||||
const handler2 = mock(async () => {});
|
||||
|
||||
eventBus.subscribe('event', handler1);
|
||||
eventBus.subscribe('event', handler2);
|
||||
|
||||
await eventBus.publish('event', { data: 'test' });
|
||||
|
||||
expect(handler1).toHaveBeenCalledWith({ data: 'test' }, 'event');
|
||||
expect(handler2).toHaveBeenCalledWith({ data: 'test' }, 'event');
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should match pattern subscriptions', async () => {
|
||||
const handler = mock(async () => {});
|
||||
eventBus.subscribe('user.*', handler);
|
||||
|
||||
await eventBus.publish('user.created', { id: 1 });
|
||||
await eventBus.publish('user.updated', { id: 2 });
|
||||
await eventBus.publish('order.created', { id: 3 });
|
||||
await eventBus.publish('user.created', { userId: '123' });
|
||||
await eventBus.publish('user.updated', { userId: '123' });
|
||||
await eventBus.publish('order.created', { orderId: '456' });
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
expect(handler).toHaveBeenCalledWith({ id: 1 }, 'user.created');
|
||||
expect(handler).toHaveBeenCalledWith({ id: 2 }, 'user.updated');
|
||||
});
|
||||
|
||||
it('should handle errors in handlers gracefully', async () => {
|
||||
|
|
@ -110,37 +112,36 @@ describe('EventBus', () => {
|
|||
eventBus.subscribe('event', errorHandler);
|
||||
eventBus.subscribe('event', successHandler);
|
||||
|
||||
await eventBus.publish('event', {});
|
||||
await eventBus.publish('event', { data: 'test' });
|
||||
|
||||
expect(successHandler).toHaveBeenCalled();
|
||||
expect(errorHandler).toHaveBeenCalledTimes(1);
|
||||
expect(successHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishSync', () => {
|
||||
it('should publish synchronously', () => {
|
||||
const results: any[] = [];
|
||||
const handler = (data: any) => {
|
||||
results.push(data);
|
||||
};
|
||||
const handler = mock((message: EventBusMessage) => {});
|
||||
eventBus.subscribe('event', handler);
|
||||
|
||||
eventBus.subscribe('sync-event', handler as any);
|
||||
eventBus.publishSync('sync-event', { value: 42 });
|
||||
eventBus.publishSync('event', { data: 'test' });
|
||||
|
||||
expect(results).toEqual([{ value: 42 }]);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
const message = handler.mock.calls[0][0];
|
||||
expect(message.type).toBe('event');
|
||||
expect(message.data).toEqual({ data: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('once', () => {
|
||||
it('should subscribe for single event', async () => {
|
||||
const handler = mock(async () => {});
|
||||
eventBus.once('event', handler);
|
||||
|
||||
eventBus.once('once-event', handler);
|
||||
|
||||
await eventBus.publish('once-event', { first: true });
|
||||
await eventBus.publish('once-event', { second: true });
|
||||
await eventBus.publish('event', { data: 'first' });
|
||||
await eventBus.publish('event', { data: 'second' });
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({ first: true }, 'once-event');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -148,13 +149,12 @@ describe('EventBus', () => {
|
|||
it('should remove all handlers for event', async () => {
|
||||
const handler1 = mock(async () => {});
|
||||
const handler2 = mock(async () => {});
|
||||
|
||||
eventBus.subscribe('event', handler1);
|
||||
eventBus.subscribe('event', handler2);
|
||||
|
||||
eventBus.off('event');
|
||||
|
||||
await eventBus.publish('event', {});
|
||||
await eventBus.publish('event', { data: 'test' });
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
|
|
@ -163,16 +163,15 @@ describe('EventBus', () => {
|
|||
it('should remove specific handler', async () => {
|
||||
const handler1 = mock(async () => {});
|
||||
const handler2 = mock(async () => {});
|
||||
|
||||
eventBus.subscribe('event', handler1);
|
||||
eventBus.subscribe('event', handler2);
|
||||
|
||||
eventBus.off('event', handler1);
|
||||
|
||||
await eventBus.publish('event', {});
|
||||
await eventBus.publish('event', { data: 'test' });
|
||||
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).toHaveBeenCalled();
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -180,27 +179,31 @@ describe('EventBus', () => {
|
|||
it('should check for subscribers', () => {
|
||||
expect(eventBus.hasSubscribers('event')).toBe(false);
|
||||
|
||||
const sub = eventBus.subscribe('event', async () => {});
|
||||
const handler = mock(async () => {});
|
||||
eventBus.subscribe('event', handler);
|
||||
|
||||
expect(eventBus.hasSubscribers('event')).toBe(true);
|
||||
|
||||
eventBus.unsubscribe(sub);
|
||||
eventBus.off('event');
|
||||
|
||||
expect(eventBus.hasSubscribers('event')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear all subscriptions', async () => {
|
||||
const handler = mock(async () => {});
|
||||
|
||||
eventBus.subscribe('event1', handler);
|
||||
eventBus.subscribe('event2', handler);
|
||||
const handler1 = mock(async () => {});
|
||||
const handler2 = mock(async () => {});
|
||||
eventBus.subscribe('event1', handler1);
|
||||
eventBus.subscribe('event2', handler2);
|
||||
|
||||
eventBus.clear();
|
||||
|
||||
await eventBus.publish('event1', {});
|
||||
await eventBus.publish('event2', {});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
expect(handler1).not.toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,9 +11,7 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run build:clean && bun run build:tsc",
|
||||
"build:clean": "rm -rf dist",
|
||||
"build:tsc": "tsc",
|
||||
"build": "tsc",
|
||||
"test": "bun test",
|
||||
"clean": "rm -rf dist node_modules .turbo"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { HandlerRegistry } from './registry';
|
||||
import type { HandlerConfiguration, HandlerMetadata } from './types';
|
||||
import { HandlerRegistry } from '../src/registry';
|
||||
import type { HandlerConfiguration, HandlerMetadata } from '../src/types';
|
||||
|
||||
describe('HandlerRegistry', () => {
|
||||
let registry: HandlerRegistry;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
|
||||
import { BaseHandler } from './base/BaseHandler';
|
||||
import { Handler, Operation, QueueSchedule, ScheduledOperation } from './decorators/decorators';
|
||||
import { createJobHandler } from './utils/create-job-handler';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import { Handler, Operation, QueueSchedule, ScheduledOperation } from '../src/decorators/decorators';
|
||||
import { createJobHandler } from '../src/utils/create-job-handler';
|
||||
|
||||
// Mock service container
|
||||
const createMockServices = (): IServiceContainer => ({
|
||||
116
libs/core/logger/test/logger.test.ts
Normal file
116
libs/core/logger/test/logger.test.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { Logger, getLogger, shutdownLoggers, setLoggerConfig } from '../src/logger';
|
||||
|
||||
describe('Logger', () => {
|
||||
beforeEach(async () => {
|
||||
// Reset logger state
|
||||
await shutdownLoggers();
|
||||
});
|
||||
|
||||
it('should create a logger instance', () => {
|
||||
const logger = getLogger('test');
|
||||
expect(logger).toBeDefined();
|
||||
expect(logger).toBeInstanceOf(Logger);
|
||||
});
|
||||
|
||||
it('should use same pino instance for same name', async () => {
|
||||
await shutdownLoggers(); // Reset first
|
||||
const logger1 = getLogger('test');
|
||||
const logger2 = getLogger('test');
|
||||
// While Logger instances are different, they should share the same pino instance
|
||||
expect(logger1).not.toBe(logger2); // Different Logger instances
|
||||
// But they have the same service name
|
||||
expect((logger1 as any).serviceName).toBe((logger2 as any).serviceName);
|
||||
});
|
||||
|
||||
it('should create different instances for different names', () => {
|
||||
const logger1 = getLogger('test1');
|
||||
const logger2 = getLogger('test2');
|
||||
expect(logger1).not.toBe(logger2);
|
||||
});
|
||||
|
||||
it('should have logging methods', () => {
|
||||
const logger = getLogger('test');
|
||||
expect(typeof logger.info).toBe('function');
|
||||
expect(typeof logger.error).toBe('function');
|
||||
expect(typeof logger.warn).toBe('function');
|
||||
expect(typeof logger.debug).toBe('function');
|
||||
expect(typeof logger.trace).toBe('function');
|
||||
});
|
||||
|
||||
it('should create child logger', () => {
|
||||
const logger = getLogger('parent');
|
||||
const child = logger.child('child');
|
||||
expect(child).toBeDefined();
|
||||
expect(child).toBeInstanceOf(Logger);
|
||||
});
|
||||
|
||||
it('should accept metadata in log methods', () => {
|
||||
const logger = getLogger('test');
|
||||
|
||||
// These should not throw
|
||||
logger.info('Test message');
|
||||
logger.info('Test message', { key: 'value' });
|
||||
logger.error('Error message', { error: new Error('test') });
|
||||
logger.warn('Warning', { count: 5 });
|
||||
logger.debug('Debug info', { data: [1, 2, 3] });
|
||||
logger.trace('Trace details', { nested: { value: true } });
|
||||
});
|
||||
|
||||
it('should format log messages', () => {
|
||||
const logger = getLogger('test');
|
||||
|
||||
// Just verify the logger can log without errors
|
||||
// The actual format is handled by pino-pretty which outputs to stdout
|
||||
expect(() => {
|
||||
logger.info('Test message');
|
||||
logger.warn('Warning message');
|
||||
logger.error('Error message');
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should set logger config', () => {
|
||||
setLoggerConfig({
|
||||
level: 'debug',
|
||||
pretty: true,
|
||||
redact: ['password'],
|
||||
});
|
||||
|
||||
const logger = getLogger('test');
|
||||
expect(logger).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle shutdown', async () => {
|
||||
await shutdownLoggers(); // Reset first
|
||||
const logger1 = getLogger('test1');
|
||||
const logger2 = getLogger('test2');
|
||||
|
||||
// Store references
|
||||
const logger1Ref = logger1;
|
||||
|
||||
await shutdownLoggers();
|
||||
|
||||
// Should create new instances after shutdown
|
||||
const logger3 = getLogger('test1');
|
||||
expect(logger3).not.toBe(logger1Ref);
|
||||
});
|
||||
|
||||
it('should handle log levels', async () => {
|
||||
await shutdownLoggers(); // Reset first
|
||||
setLoggerConfig({ level: 'warn' });
|
||||
const logger = getLogger('test');
|
||||
|
||||
// Just verify that log methods exist and don't throw
|
||||
// The actual level filtering is handled by pino
|
||||
expect(() => {
|
||||
logger.trace('Trace'); // Should not log
|
||||
logger.debug('Debug'); // Should not log
|
||||
logger.info('Info'); // Should not log
|
||||
logger.warn('Warn'); // Should log
|
||||
logger.error('Error'); // Should log
|
||||
}).not.toThrow();
|
||||
|
||||
// Clean up
|
||||
await shutdownLoggers();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { Queue as BullQueue, type Job } from 'bullmq';
|
||||
import { createCache } from '@stock-bot/cache';
|
||||
import type { CacheProvider } from '@stock-bot/cache';
|
||||
import { createCache } from '@stock-bot/cache';
|
||||
import type { HandlerRegistry } from '@stock-bot/handler-registry';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { Queue as BullQueue, type Job } from 'bullmq';
|
||||
import { Queue, type QueueWorkerConfig } from './queue';
|
||||
import { QueueRateLimiter } from './rate-limiter';
|
||||
import { getFullQueueName, parseQueueName } from './service-utils';
|
||||
|
|
@ -478,13 +478,6 @@ export class QueueManager {
|
|||
* Start workers for all queues (used when delayWorkerStart is enabled)
|
||||
*/
|
||||
startAllWorkers(): void {
|
||||
if (!this.config.delayWorkerStart) {
|
||||
this.logger.info(
|
||||
'startAllWorkers() called but workers already started automatically (delayWorkerStart is false)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let workersStarted = 0;
|
||||
const queues = this.queues;
|
||||
|
||||
|
|
@ -525,7 +518,6 @@ export class QueueManager {
|
|||
service: this.serviceName || 'default',
|
||||
totalQueues: queues.size,
|
||||
queuesWithWorkers: workersStarted,
|
||||
delayWorkerStart: this.config.delayWorkerStart,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
203
libs/core/queue/test/queue.test.ts
Normal file
203
libs/core/queue/test/queue.test.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import {
|
||||
normalizeServiceName,
|
||||
generateCachePrefix,
|
||||
getFullQueueName,
|
||||
parseQueueName,
|
||||
} from '../src/service-utils';
|
||||
import { ServiceCache, createServiceCache } from '../src/service-cache';
|
||||
import type { BatchJobData } from '../src/types';
|
||||
|
||||
describe('Service Utilities', () => {
|
||||
describe('normalizeServiceName', () => {
|
||||
it('should normalize service names', () => {
|
||||
expect(normalizeServiceName('MyService')).toBe('my-service');
|
||||
expect(normalizeServiceName('webApi')).toBe('web-api');
|
||||
expect(normalizeServiceName('dataIngestion')).toBe('data-ingestion');
|
||||
expect(normalizeServiceName('data-pipeline')).toBe('data-pipeline');
|
||||
expect(normalizeServiceName('UPPERCASE')).toBe('uppercase');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(normalizeServiceName('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle special characters', () => {
|
||||
// The function only handles camelCase, not special characters
|
||||
expect(normalizeServiceName('my@service#123')).toBe('my@service#123');
|
||||
expect(normalizeServiceName('serviceWithCamelCase')).toBe('service-with-camel-case');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCachePrefix', () => {
|
||||
it('should generate cache prefix', () => {
|
||||
expect(generateCachePrefix('service')).toBe('cache:service');
|
||||
expect(generateCachePrefix('webApi')).toBe('cache:web-api');
|
||||
});
|
||||
|
||||
it('should handle empty parts', () => {
|
||||
expect(generateCachePrefix('')).toBe('cache:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFullQueueName', () => {
|
||||
it('should generate full queue name', () => {
|
||||
expect(getFullQueueName('service', 'handler')).toBe('{service_handler}');
|
||||
expect(getFullQueueName('webApi', 'handler')).toBe('{web-api_handler}');
|
||||
});
|
||||
|
||||
it('should normalize service name', () => {
|
||||
expect(getFullQueueName('MyService', 'handler')).toBe('{my-service_handler}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseQueueName', () => {
|
||||
it('should parse queue name', () => {
|
||||
expect(parseQueueName('{service_handler}')).toEqual({
|
||||
service: 'service',
|
||||
handler: 'handler',
|
||||
});
|
||||
expect(parseQueueName('{web-api_data-processor}')).toEqual({
|
||||
service: 'web-api',
|
||||
handler: 'data-processor',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid formats', () => {
|
||||
expect(parseQueueName('service:handler')).toBeNull();
|
||||
expect(parseQueueName('service')).toBeNull();
|
||||
expect(parseQueueName('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(parseQueueName('{}_handler')).toBeNull();
|
||||
expect(parseQueueName('{service_}')).toBeNull();
|
||||
expect(parseQueueName('not-a-valid-format')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceCache', () => {
|
||||
it('should create service cache', () => {
|
||||
const mockRedisConfig = {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
};
|
||||
|
||||
// Since ServiceCache constructor internally creates a real cache,
|
||||
// we can't easily test it without mocking the createCache function
|
||||
// For now, just test that the function exists and returns something
|
||||
const serviceCache = createServiceCache('myservice', mockRedisConfig);
|
||||
expect(serviceCache).toBeDefined();
|
||||
expect(serviceCache).toBeInstanceOf(ServiceCache);
|
||||
});
|
||||
|
||||
it('should handle cache prefix correctly', () => {
|
||||
const mockRedisConfig = {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
};
|
||||
|
||||
const serviceCache = createServiceCache('webApi', mockRedisConfig);
|
||||
expect(serviceCache).toBeDefined();
|
||||
// The prefix is set internally as cache:web-api
|
||||
expect(serviceCache.getKey('test')).toBe('cache:web-api:test');
|
||||
});
|
||||
|
||||
it('should support global cache option', () => {
|
||||
const mockRedisConfig = {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
};
|
||||
|
||||
const globalCache = createServiceCache('myservice', mockRedisConfig, { global: true });
|
||||
expect(globalCache).toBeDefined();
|
||||
// Global cache uses a different prefix
|
||||
expect(globalCache.getKey('test')).toBe('stock-bot:shared:test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Batch Processing', () => {
|
||||
it('should handle batch job data types', () => {
|
||||
const batchJob: BatchJobData = {
|
||||
items: [1, 2, 3],
|
||||
options: {
|
||||
batchSize: 10,
|
||||
concurrency: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(batchJob.items).toHaveLength(3);
|
||||
expect(batchJob.options.batchSize).toBe(10);
|
||||
expect(batchJob.options.concurrency).toBe(2);
|
||||
});
|
||||
|
||||
it('should process batch results', () => {
|
||||
const results = {
|
||||
totalItems: 10,
|
||||
successful: 8,
|
||||
failed: 2,
|
||||
errors: [
|
||||
{ item: 5, error: 'Failed to process' },
|
||||
{ item: 7, error: 'Invalid data' },
|
||||
],
|
||||
duration: 1000,
|
||||
};
|
||||
|
||||
expect(results.successful + results.failed).toBe(results.totalItems);
|
||||
expect(results.errors).toHaveLength(results.failed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', () => {
|
||||
it('should validate rate limit config', () => {
|
||||
const config = {
|
||||
rules: [
|
||||
{
|
||||
name: 'default',
|
||||
maxJobs: 100,
|
||||
window: 60000,
|
||||
},
|
||||
{
|
||||
name: 'api',
|
||||
maxJobs: 10,
|
||||
window: 1000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(config.rules).toHaveLength(2);
|
||||
expect(config.rules[0].name).toBe('default');
|
||||
expect(config.rules[1].maxJobs).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queue Types', () => {
|
||||
it('should validate job data structure', () => {
|
||||
const jobData = {
|
||||
handler: 'TestHandler',
|
||||
operation: 'process',
|
||||
payload: { data: 'test' },
|
||||
};
|
||||
|
||||
expect(jobData.handler).toBe('TestHandler');
|
||||
expect(jobData.operation).toBe('process');
|
||||
expect(jobData.payload).toBeDefined();
|
||||
});
|
||||
|
||||
it('should validate queue stats structure', () => {
|
||||
const stats = {
|
||||
waiting: 10,
|
||||
active: 2,
|
||||
completed: 100,
|
||||
failed: 5,
|
||||
delayed: 3,
|
||||
paused: false,
|
||||
workers: 4,
|
||||
};
|
||||
|
||||
expect(stats.waiting + stats.active + stats.completed + stats.failed + stats.delayed).toBe(120);
|
||||
expect(stats.paused).toBe(false);
|
||||
expect(stats.workers).toBe(4);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { Shutdown } from './shutdown';
|
||||
import { Shutdown } from '../src/shutdown';
|
||||
|
||||
describe('Shutdown', () => {
|
||||
let shutdown: Shutdown;
|
||||
|
|
@ -1,320 +0,0 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import type {
|
||||
// Service types
|
||||
ServiceType,
|
||||
ServiceOperationContext,
|
||||
ServiceContainer,
|
||||
ServiceConfig,
|
||||
|
||||
// Handler types
|
||||
HandlerClass,
|
||||
HandlerInstance,
|
||||
HandlerMetadata,
|
||||
HandlerRegistration,
|
||||
|
||||
// Queue types
|
||||
QueueMessage,
|
||||
QueueResult,
|
||||
QueueOptions,
|
||||
|
||||
// Market data types
|
||||
Quote,
|
||||
Bar,
|
||||
Trade,
|
||||
|
||||
// Options types
|
||||
OptionContract,
|
||||
OptionChain,
|
||||
|
||||
// Trading types
|
||||
Order,
|
||||
Position,
|
||||
Trade as TradingTrade,
|
||||
|
||||
// Portfolio types
|
||||
Portfolio,
|
||||
PortfolioStats,
|
||||
|
||||
// Risk types
|
||||
RiskMetrics,
|
||||
PositionRisk,
|
||||
} from './index';
|
||||
|
||||
describe('Type Guards and Utilities', () => {
|
||||
describe('Service Types', () => {
|
||||
it('should handle ServiceType enum values', () => {
|
||||
const services: ServiceType[] = [
|
||||
'WORKER' as ServiceType,
|
||||
'API' as ServiceType,
|
||||
'SCHEDULER' as ServiceType,
|
||||
];
|
||||
|
||||
expect(services).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should type ServiceOperationContext', () => {
|
||||
const context: ServiceOperationContext = {
|
||||
requestId: 'req-123',
|
||||
serviceType: 'WORKER' as ServiceType,
|
||||
serviceName: 'test-worker',
|
||||
operation: 'processData',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
expect(context.requestId).toBe('req-123');
|
||||
expect(context.operation).toBe('processData');
|
||||
});
|
||||
|
||||
it('should type ServiceContainer', () => {
|
||||
const container: Partial<ServiceContainer> = {
|
||||
config: { name: 'test' } as any,
|
||||
logger: console,
|
||||
cache: {} as any,
|
||||
};
|
||||
|
||||
expect(container.logger).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handler Types', () => {
|
||||
it('should type HandlerMetadata', () => {
|
||||
const metadata: HandlerMetadata = {
|
||||
name: 'TestHandler',
|
||||
service: 'test-service',
|
||||
operations: ['op1', 'op2'],
|
||||
schedules: [],
|
||||
};
|
||||
|
||||
expect(metadata.operations).toContain('op1');
|
||||
expect(metadata.operations).toContain('op2');
|
||||
});
|
||||
|
||||
it('should type HandlerRegistration', () => {
|
||||
const registration: HandlerRegistration = {
|
||||
name: 'TestHandler',
|
||||
service: 'test-service',
|
||||
operations: new Map([
|
||||
['op1', { operation: 'op1', handler: async () => {} }],
|
||||
]),
|
||||
schedules: [],
|
||||
};
|
||||
|
||||
expect(registration.operations.has('op1')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queue Types', () => {
|
||||
it('should type QueueMessage', () => {
|
||||
const message: QueueMessage = {
|
||||
id: 'msg-123',
|
||||
data: { test: true },
|
||||
metadata: {
|
||||
timestamp: new Date(),
|
||||
retries: 0,
|
||||
},
|
||||
};
|
||||
|
||||
expect(message.id).toBe('msg-123');
|
||||
expect(message.data.test).toBe(true);
|
||||
});
|
||||
|
||||
it('should type QueueOptions', () => {
|
||||
const options: QueueOptions = {
|
||||
priority: 5,
|
||||
delay: 1000,
|
||||
retries: 3,
|
||||
};
|
||||
|
||||
expect(options.priority).toBe(5);
|
||||
expect(options.delay).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Market Data Types', () => {
|
||||
it('should type Quote', () => {
|
||||
const quote: Quote = {
|
||||
symbol: 'AAPL',
|
||||
bid: 150.25,
|
||||
ask: 150.30,
|
||||
bidSize: 100,
|
||||
askSize: 200,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
expect(quote.symbol).toBe('AAPL');
|
||||
expect(quote.bid).toBe(150.25);
|
||||
expect(quote.ask).toBe(150.30);
|
||||
});
|
||||
|
||||
it('should type Bar', () => {
|
||||
const bar: Bar = {
|
||||
symbol: 'AAPL',
|
||||
open: 150.00,
|
||||
high: 151.00,
|
||||
low: 149.50,
|
||||
close: 150.75,
|
||||
volume: 1000000,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
expect(bar.symbol).toBe('AAPL');
|
||||
expect(bar.high).toBeGreaterThan(bar.low);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Options Types', () => {
|
||||
it('should type OptionContract', () => {
|
||||
const option: OptionContract = {
|
||||
symbol: 'AAPL230120C00150000',
|
||||
underlying: 'AAPL',
|
||||
strike: 150,
|
||||
expiration: new Date('2023-01-20'),
|
||||
type: 'call',
|
||||
bid: 2.50,
|
||||
ask: 2.55,
|
||||
volume: 1000,
|
||||
openInterest: 5000,
|
||||
impliedVolatility: 0.25,
|
||||
};
|
||||
|
||||
expect(option.type).toBe('call');
|
||||
expect(option.strike).toBe(150);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trading Types', () => {
|
||||
it('should type Order', () => {
|
||||
const order: Order = {
|
||||
id: 'order-123',
|
||||
symbol: 'AAPL',
|
||||
side: 'buy',
|
||||
quantity: 100,
|
||||
type: 'limit',
|
||||
price: 150.00,
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
expect(order.side).toBe('buy');
|
||||
expect(order.type).toBe('limit');
|
||||
expect(order.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should type Position', () => {
|
||||
const position: Position = {
|
||||
symbol: 'AAPL',
|
||||
quantity: 100,
|
||||
averagePrice: 150.00,
|
||||
currentPrice: 151.00,
|
||||
unrealizedPnL: 100,
|
||||
realizedPnL: 0,
|
||||
};
|
||||
|
||||
expect(position.quantity).toBe(100);
|
||||
expect(position.unrealizedPnL).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Portfolio Types', () => {
|
||||
it('should type Portfolio', () => {
|
||||
const portfolio: Portfolio = {
|
||||
id: 'portfolio-123',
|
||||
accountId: 'account-123',
|
||||
positions: [],
|
||||
cash: 10000,
|
||||
totalValue: 10000,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
expect(portfolio.cash).toBe(10000);
|
||||
expect(portfolio.positions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should type PortfolioStats', () => {
|
||||
const stats: PortfolioStats = {
|
||||
totalValue: 100000,
|
||||
cash: 10000,
|
||||
invested: 90000,
|
||||
dailyPnL: 500,
|
||||
totalPnL: 5000,
|
||||
winRate: 0.65,
|
||||
sharpeRatio: 1.5,
|
||||
};
|
||||
|
||||
expect(stats.winRate).toBe(0.65);
|
||||
expect(stats.sharpeRatio).toBe(1.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Risk Types', () => {
|
||||
it('should type RiskMetrics', () => {
|
||||
const metrics: RiskMetrics = {
|
||||
beta: 1.2,
|
||||
standardDeviation: 0.15,
|
||||
sharpeRatio: 1.5,
|
||||
maxDrawdown: 0.10,
|
||||
valueAtRisk: 1000,
|
||||
};
|
||||
|
||||
expect(metrics.beta).toBe(1.2);
|
||||
expect(metrics.maxDrawdown).toBe(0.10);
|
||||
});
|
||||
|
||||
it('should type PositionRisk', () => {
|
||||
const risk: PositionRisk = {
|
||||
symbol: 'AAPL',
|
||||
exposure: 15000,
|
||||
percentOfPortfolio: 0.15,
|
||||
beta: 1.1,
|
||||
delta: 100,
|
||||
gamma: 0,
|
||||
vega: 0,
|
||||
theta: 0,
|
||||
};
|
||||
|
||||
expect(risk.exposure).toBe(15000);
|
||||
expect(risk.percentOfPortfolio).toBe(0.15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type Composition', () => {
|
||||
it('should compose complex types', () => {
|
||||
// Test that types can be composed together
|
||||
type TradingSystem = {
|
||||
portfolio: Portfolio;
|
||||
activeOrders: Order[];
|
||||
riskMetrics: RiskMetrics;
|
||||
marketData: {
|
||||
quotes: Map<string, Quote>;
|
||||
bars: Map<string, Bar[]>;
|
||||
};
|
||||
};
|
||||
|
||||
const system: TradingSystem = {
|
||||
portfolio: {
|
||||
id: 'test',
|
||||
accountId: 'test',
|
||||
positions: [],
|
||||
cash: 10000,
|
||||
totalValue: 10000,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
activeOrders: [],
|
||||
riskMetrics: {
|
||||
beta: 1.0,
|
||||
standardDeviation: 0.1,
|
||||
sharpeRatio: 1.0,
|
||||
maxDrawdown: 0.05,
|
||||
valueAtRisk: 500,
|
||||
},
|
||||
marketData: {
|
||||
quotes: new Map(),
|
||||
bars: new Map(),
|
||||
},
|
||||
};
|
||||
|
||||
expect(system.portfolio.cash).toBe(10000);
|
||||
expect(system.riskMetrics.beta).toBe(1.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { SimpleMongoDBClient } from './simple-mongodb';
|
||||
import { SimpleMongoDBClient } from '../src/simple-mongodb';
|
||||
|
||||
describe('MongoDBClient', () => {
|
||||
let client: SimpleMongoDBClient;
|
||||
|
|
@ -188,7 +188,7 @@ export class SimpleTransactionManager {
|
|||
|
||||
async transaction<T>(fn: (client: any) => Promise<T>): Promise<T> {
|
||||
const mockClient = {
|
||||
query: async () => ({ rows: [], rowCount: 0 }),
|
||||
query: async (sql?: string) => ({ rows: [], rowCount: 0 }),
|
||||
release: () => {},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
SimplePostgresClient,
|
||||
SimpleQueryBuilder,
|
||||
SimpleTransactionManager,
|
||||
} from './simple-postgres';
|
||||
} from '../src/simple-postgres';
|
||||
|
||||
describe('PostgresClient', () => {
|
||||
let client: SimplePostgresClient;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { QuestDBClient } from './client';
|
||||
import { QuestDBHealthMonitor } from './health';
|
||||
import { QuestDBQueryBuilder } from './query-builder';
|
||||
import { QuestDBInfluxWriter } from './influx-writer';
|
||||
import { QuestDBSchemaManager } from './schema';
|
||||
import type { QuestDBClientConfig, OHLCVData, TradeData } from './types';
|
||||
import { QuestDBClient } from '../src/client';
|
||||
import { QuestDBHealthMonitor } from '../src/health';
|
||||
import { QuestDBQueryBuilder } from '../src/query-builder';
|
||||
import { QuestDBInfluxWriter } from '../src/influx-writer';
|
||||
import { QuestDBSchemaManager } from '../src/schema';
|
||||
import type { QuestDBClientConfig, OHLCVData, TradeData } from '../src/types';
|
||||
|
||||
// Simple in-memory QuestDB client for testing
|
||||
class SimpleQuestDBClient {
|
||||
|
|
@ -9,7 +9,7 @@ export class SimpleBrowser {
|
|||
private contexts = new Map<string, any>();
|
||||
private logger: any;
|
||||
private initialized = false;
|
||||
private options: BrowserOptions = {
|
||||
private _options: BrowserOptions = {
|
||||
headless: true,
|
||||
timeout: 30000,
|
||||
blockResources: false,
|
||||
|
|
@ -55,7 +55,7 @@ export class SimpleBrowser {
|
|||
}
|
||||
|
||||
// Merge options
|
||||
this.options = { ...this.options, ...options };
|
||||
this._options = { ...this._options, ...options };
|
||||
|
||||
this.logger.info('Initializing browser...');
|
||||
|
||||
|
|
@ -91,8 +91,8 @@ export class SimpleBrowser {
|
|||
const page = await context.newPage();
|
||||
|
||||
// Add resource blocking if enabled
|
||||
if (this.options?.blockResources) {
|
||||
await page.route('**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,css}', route => {
|
||||
if (this._options?.blockResources) {
|
||||
await page.route('**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,css}', (route: any) => {
|
||||
route.abort();
|
||||
});
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ export class SimpleBrowser {
|
|||
|
||||
async goto(page: Page, url: string, options?: any): Promise<void> {
|
||||
await page.goto(url, {
|
||||
timeout: this.options?.timeout || 30000,
|
||||
timeout: this._options?.timeout || 30000,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
@ -143,6 +143,7 @@ export class SimpleBrowser {
|
|||
success: false,
|
||||
error: error.message,
|
||||
url,
|
||||
data: {} as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -163,12 +164,4 @@ export class SimpleBrowser {
|
|||
this.initialized = false;
|
||||
}
|
||||
|
||||
private get options(): BrowserOptions {
|
||||
return {
|
||||
headless: true,
|
||||
timeout: 30000,
|
||||
blockResources: false,
|
||||
enableNetworkLogging: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { SimpleBrowser } from './simple-browser';
|
||||
import type { BrowserOptions } from './types';
|
||||
import { SimpleBrowser } from '../src/simple-browser';
|
||||
import type { BrowserOptions } from '../src/types';
|
||||
|
||||
describe('Browser', () => {
|
||||
let browser: SimpleBrowser;
|
||||
|
|
@ -1,14 +1,24 @@
|
|||
import type { ProxyInfo, ProxyConfig } from './types';
|
||||
import type { ProxyInfo } from './types';
|
||||
|
||||
export interface ProxyConfig {
|
||||
protocol: string;
|
||||
host: string;
|
||||
port: number;
|
||||
auth?: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple proxy manager for testing
|
||||
*/
|
||||
export class SimpleProxyManager {
|
||||
private proxies: ProxyInfo[] = [];
|
||||
private proxies: Array<ProxyInfo & { id: string; active: boolean }> = [];
|
||||
private currentIndex = 0;
|
||||
private activeProxyIndex = 0;
|
||||
|
||||
addProxy(proxy: ProxyInfo): void {
|
||||
addProxy(proxy: ProxyInfo & { id: string; active: boolean }): void {
|
||||
this.proxies.push(proxy);
|
||||
}
|
||||
|
||||
|
|
@ -23,15 +33,15 @@ export class SimpleProxyManager {
|
|||
}
|
||||
}
|
||||
|
||||
getProxies(): ProxyInfo[] {
|
||||
getProxies(): Array<ProxyInfo & { id: string; active: boolean }> {
|
||||
return [...this.proxies];
|
||||
}
|
||||
|
||||
getActiveProxies(): ProxyInfo[] {
|
||||
getActiveProxies(): Array<ProxyInfo & { id: string; active: boolean }> {
|
||||
return this.proxies.filter(p => p.active);
|
||||
}
|
||||
|
||||
getNextProxy(): ProxyInfo | null {
|
||||
getNextProxy(): (ProxyInfo & { id: string; active: boolean }) | null {
|
||||
const activeProxies = this.getActiveProxies();
|
||||
if (activeProxies.length === 0) {
|
||||
return null;
|
||||
|
|
@ -39,10 +49,10 @@ export class SimpleProxyManager {
|
|||
|
||||
const proxy = activeProxies[this.activeProxyIndex % activeProxies.length];
|
||||
this.activeProxyIndex++;
|
||||
return proxy;
|
||||
return proxy || null;
|
||||
}
|
||||
|
||||
getProxyConfig(proxy: ProxyInfo): ProxyConfig {
|
||||
getProxyConfig(proxy: ProxyInfo & { id: string; active: boolean }): ProxyConfig {
|
||||
const config: ProxyConfig = {
|
||||
protocol: proxy.protocol,
|
||||
host: proxy.host,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { SimpleProxyManager } from './simple-proxy-manager';
|
||||
import type { ProxyConfig, ProxyInfo } from './types';
|
||||
import { SimpleProxyManager } from '../src/simple-proxy-manager';
|
||||
import type { ProxyConfig, ProxyInfo } from '../src/types';
|
||||
|
||||
describe('ProxyManager', () => {
|
||||
let manager: SimpleProxyManager;
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
groupBySymbol,
|
||||
convertTimestamps,
|
||||
|
||||
} from './index';
|
||||
} from '../src/index';
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('common utilities', () => {
|
||||
|
|
@ -33,8 +33,11 @@ trap cleanup EXIT
|
|||
libs=(
|
||||
# Core Libraries - minimal dependencies
|
||||
"core/types" # Base types - no dependencies
|
||||
"core/config" # Configuration - depends on types
|
||||
"core/logger" # Logging utilities - depends on types
|
||||
"core/config" # Configuration - depends on types and logger
|
||||
|
||||
# Utils - needed by many libraries
|
||||
"utils" # Utilities - minimal dependencies
|
||||
|
||||
# Data access libraries
|
||||
"data/mongodb" # MongoDB client - depends on core libs
|
||||
|
|
@ -45,16 +48,14 @@ libs=(
|
|||
"core/shutdown" # Shutdown - no dependencies
|
||||
"core/cache" # Cache - depends on core libs
|
||||
"core/event-bus" # Event bus - depends on core libs
|
||||
"core/handlers" # Handlers - depends on core libs
|
||||
"core/queue" # Queue - depends on core libs, cache, and handlers
|
||||
"core/handler-registry" # Handler registry - depends on core libs
|
||||
"core/handlers" # Handlers - depends on core libs, handler-registry, and utils
|
||||
"core/queue" # Queue - depends on core libs, cache, handlers, and handler-registry
|
||||
|
||||
# Application services
|
||||
"services/browser" # Browser - depends on core libs
|
||||
"services/proxy" # Proxy manager - depends on core libs and cache
|
||||
|
||||
# Utils
|
||||
"utils" # Utilities - depends on many libs
|
||||
|
||||
# DI - dependency injection library
|
||||
"core/di" # Dependency injection - depends on data, service libs, and handlers
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue