fixed format issues

This commit is contained in:
Boki 2025-06-26 16:12:27 -04:00
parent a700818a06
commit 08f713d98b
55 changed files with 5680 additions and 5533 deletions

View file

@ -2,8 +2,8 @@
* Handler registration for data pipeline service
*/
import type { IServiceContainer } from '@stock-bot/types';
import { getLogger } from '@stock-bot/logger';
import type { IServiceContainer } from '@stock-bot/types';
// Import handlers directly
import { ExchangesHandler } from './exchanges/exchanges.handler';
import { SymbolsHandler } from './symbols/symbols.handler';
@ -18,10 +18,7 @@ export async function initializeAllHandlers(container: IServiceContainer): Promi
try {
// Register handlers manually
const handlers = [
ExchangesHandler,
SymbolsHandler,
];
const handlers = [ExchangesHandler, SymbolsHandler];
for (const Handler of handlers) {
try {

View file

@ -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'
);
}
/**

View file

@ -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;
@ -68,9 +68,7 @@ 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();
}

View file

@ -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 = {
@ -72,13 +73,15 @@ 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);
@ -145,7 +148,7 @@ 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();
@ -172,7 +175,7 @@ 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) => {
@ -206,7 +209,9 @@ 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(() => {

View file

@ -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 {

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import { RedisConnectionManager } from '../src/connection-manager';
describe('RedisConnectionManager', () => {

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { NamespacedCache, CacheAdapter } from '../src/namespaced-cache';
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', () => {

View file

@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from 'bun:test';
import { beforeEach, describe, expect, it } from 'bun:test';
import { RedisCache } from '../src/redis-cache';
import type { CacheOptions } from '../src/types';

View file

@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from 'bun:test';
import { beforeEach, describe, expect, it } from 'bun:test';
import { RedisCache } from '../src/redis-cache';
import type { CacheOptions } from '../src/types';

View file

@ -28,10 +28,7 @@ export class ConfigManager<T = Record<string, unknown>> {
this.loaders = options.loaders;
} else {
const configPath = options.configPath || join(process.cwd(), 'config');
this.loaders = [
new FileLoader(configPath, this.environment),
new EnvLoader(''),
];
this.loaders = [new FileLoader(configPath, this.environment), new EnvLoader('')];
}
}
@ -61,7 +58,11 @@ export class ConfigManager<T = Record<string, unknown>> {
const mergedConfig = this.merge(...configs) as T;
// Add environment if not present
if (typeof mergedConfig === 'object' && mergedConfig !== null && !('environment' in mergedConfig)) {
if (
typeof mergedConfig === 'object' &&
mergedConfig !== null &&
!('environment' in mergedConfig)
) {
(mergedConfig as Record<string, unknown>)['environment'] = this.environment;
}

View file

@ -18,6 +18,9 @@ export {
} from './schemas';
// createAppConfig function for apps/stock
export function createAppConfig<T>(schema: unknown, options?: ConfigManagerOptions): ConfigManager<T> {
export function createAppConfig<T>(
schema: unknown,
options?: ConfigManagerOptions
): ConfigManager<T> {
return new ConfigManager<T>(options);
}

View file

@ -133,10 +133,7 @@ export class EnvLoader implements ConfigLoader {
private shouldPreserveStringForKey(key: string): boolean {
// Keys that should preserve string values even if they look like numbers
const preserveStringKeys = [
'QM_WEBMASTER_ID',
'IB_MARKET_DATA_TYPE'
];
const preserveStringKeys = ['QM_WEBMASTER_ID', 'IB_MARKET_DATA_TYPE'];
return preserveStringKeys.includes(key);
}

View file

@ -24,23 +24,27 @@ export const eodProviderConfigSchema = baseProviderConfigSchema.extend({
// Interactive Brokers provider
export const ibProviderConfigSchema = baseProviderConfigSchema.extend({
gateway: z.object({
host: z.string().default('localhost'),
port: z.number().default(5000),
clientId: z.number().default(1),
}).default({
host: 'localhost',
port: 5000,
clientId: 1,
}),
account: z.string().optional(),
marketDataType: z.union([
z.enum(['live', 'delayed', 'frozen']),
z.enum(['1', '2', '3']).transform((val) => {
const mapping = { '1': 'live', '2': 'frozen', '3': 'delayed' } as const;
return mapping[val];
gateway: z
.object({
host: z.string().default('localhost'),
port: z.number().default(5000),
clientId: z.number().default(1),
})
.default({
host: 'localhost',
port: 5000,
clientId: 1,
}),
]).default('delayed'),
account: z.string().optional(),
marketDataType: z
.union([
z.enum(['live', 'delayed', 'frozen']),
z.enum(['1', '2', '3']).transform(val => {
const mapping = { '1': 'live', '2': 'frozen', '3': 'delayed' } as const;
return mapping[val];
}),
])
.default('delayed'),
});
// QuoteMedia provider

View file

@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { z } from 'zod';
import { ConfigManager } from '../src/config-manager';
import { ConfigError, ConfigValidationError } from '../src/errors';
@ -11,7 +11,7 @@ mock.module('@stock-bot/logger', () => ({
error: mock(() => {}),
warn: mock(() => {}),
debug: mock(() => {}),
})
}),
}));
// Mock loader class
@ -49,12 +49,12 @@ describe('ConfigManager', () => {
it('should handle various environment values', () => {
const envMap: Record<string, Environment> = {
'production': 'production',
'prod': 'production',
'test': 'test',
'development': 'development',
'dev': 'development',
'unknown': 'development',
production: 'production',
prod: 'production',
test: 'test',
development: 'development',
dev: 'development',
unknown: 'development',
};
for (const [input, expected] of Object.entries(envMap)) {
@ -348,7 +348,9 @@ describe('ConfigManager', () => {
environment: z.string(),
});
manager = new ConfigManager({ loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })] });
manager = new ConfigManager({
loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })],
});
manager.initialize(schema);
// Valid update

View file

@ -1,10 +1,6 @@
import { beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod';
import {
baseAppSchema,
ConfigManager,
createAppConfig,
} from '../src';
import { baseAppSchema, ConfigManager, createAppConfig } from '../src';
import { ConfigError, ConfigValidationError } from '../src/errors';
// Mock loader for testing
@ -160,7 +156,6 @@ describe('ConfigManager', () => {
expect(validated).toEqual({ name: 'test', port: 3000 });
});
it('should add environment if not present', () => {
const mockManager = new ConfigManager({
environment: 'test',
@ -172,7 +167,6 @@ describe('ConfigManager', () => {
});
});
describe('Config Builders', () => {
it('should create app config with schema', () => {
const schema = z.object({

View file

@ -1,11 +1,11 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import { readFileSync } from 'fs';
import { EnvLoader } from '../src/loaders/env.loader';
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { ConfigLoaderError } from '../src/errors';
import { EnvLoader } from '../src/loaders/env.loader';
// Mock fs module
mock.module('fs', () => ({
readFileSync: mock(() => '')
readFileSync: mock(() => ''),
}));
describe('EnvLoader', () => {
@ -133,9 +133,9 @@ describe('EnvLoader', () => {
APP: {
NAME: 'myapp',
CONFIG: {
PORT: 3000
}
}
PORT: 3000,
},
},
});
});
@ -152,9 +152,9 @@ describe('EnvLoader', () => {
host: 'localhost',
port: 5432,
credentials: {
user: 'admin'
}
}
user: 'admin',
},
},
});
});
@ -422,7 +422,7 @@ KEY_WITHOUT_VALUE=
const config = { readonly: 'original' };
Object.defineProperty(config, 'readonly', {
writable: false,
configurable: false
configurable: false,
});
process.env.READONLY = 'new_value';
@ -463,11 +463,11 @@ KEY_WITHOUT_VALUE=
c: {
d: {
e: {
f: 'deep'
}
}
}
}
f: 'deep',
},
},
},
},
});
});
@ -513,16 +513,18 @@ KEY_WITHOUT_VALUE=
loader = new EnvLoader();
const config = loader.load();
expect(config.providers).toEqual(expect.objectContaining({
qm: {
username: 'testuser',
password: 'testpass',
baseUrl: 'https://api.quotemedia.com',
webmasterId: '12345',
enabled: true,
priority: 5,
},
}));
expect(config.providers).toEqual(
expect.objectContaining({
qm: {
username: 'testuser',
password: 'testpass',
baseUrl: 'https://api.quotemedia.com',
webmasterId: '12345',
enabled: true,
priority: 5,
},
})
);
});
it('should handle Yahoo Finance provider mappings', () => {
@ -535,15 +537,17 @@ KEY_WITHOUT_VALUE=
loader = new EnvLoader();
const config = loader.load();
expect(config.providers).toEqual(expect.objectContaining({
yahoo: {
baseUrl: 'https://finance.yahoo.com',
cookieJar: '/path/to/cookies',
crumb: 'abc123',
enabled: false,
priority: 10,
},
}));
expect(config.providers).toEqual(
expect.objectContaining({
yahoo: {
baseUrl: 'https://finance.yahoo.com',
cookieJar: '/path/to/cookies',
crumb: 'abc123',
enabled: false,
priority: 10,
},
})
);
});
it('should handle additional provider mappings', () => {
@ -557,14 +561,18 @@ KEY_WITHOUT_VALUE=
loader = new EnvLoader();
const config = loader.load();
expect(config.webshare).toEqual(expect.objectContaining({
apiUrl: 'https://api.webshare.io',
}));
expect(config.providers?.ib).toEqual(expect.objectContaining({
account: 'DU123456',
marketDataType: '1',
priority: 3,
}));
expect(config.webshare).toEqual(
expect.objectContaining({
apiUrl: 'https://api.webshare.io',
})
);
expect(config.providers?.ib).toEqual(
expect.objectContaining({
account: 'DU123456',
marketDataType: '1',
priority: 3,
})
);
expect(config.version).toBe('1.2.3');
expect(config.debug).toBe(true);
});
@ -610,7 +618,7 @@ KEY_WITHOUT_VALUE=
// CONFIG should be an object with nested value
expect((config as any).config).toEqual({
nested: 'nested_value'
nested: 'nested_value',
});
});
@ -620,7 +628,7 @@ KEY_WITHOUT_VALUE=
Object.defineProperty(testConfig, 'protected', {
value: 'immutable',
writable: false,
configurable: false
configurable: false,
});
process.env.PROTECTED_NESTED_VALUE = 'test';

View file

@ -1,12 +1,12 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { existsSync, readFileSync } from 'fs';
import { FileLoader } from '../src/loaders/file.loader';
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { ConfigLoaderError } from '../src/errors';
import { FileLoader } from '../src/loaders/file.loader';
// Mock fs module
mock.module('fs', () => ({
existsSync: mock(() => false),
readFileSync: mock(() => '')
readFileSync: mock(() => ''),
}));
describe('FileLoader', () => {

View file

@ -1,27 +1,27 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import { z } from 'zod';
import {
baseConfigSchema,
environmentSchema,
serviceConfigSchema,
loggingConfigSchema,
queueConfigSchema,
httpConfigSchema,
webshareConfigSchema,
browserConfigSchema,
proxyConfigSchema,
postgresConfigSchema,
questdbConfigSchema,
mongodbConfigSchema,
dragonflyConfigSchema,
databaseConfigSchema,
baseProviderConfigSchema,
browserConfigSchema,
databaseConfigSchema,
dragonflyConfigSchema,
environmentSchema,
eodProviderConfigSchema,
httpConfigSchema,
ibProviderConfigSchema,
qmProviderConfigSchema,
yahooProviderConfigSchema,
webshareProviderConfigSchema,
loggingConfigSchema,
mongodbConfigSchema,
postgresConfigSchema,
providerConfigSchema,
proxyConfigSchema,
qmProviderConfigSchema,
questdbConfigSchema,
queueConfigSchema,
serviceConfigSchema,
webshareConfigSchema,
webshareProviderConfigSchema,
yahooProviderConfigSchema,
} from '../src/schemas';
describe('Config Schemas', () => {
@ -202,7 +202,7 @@ describe('Config Schemas', () => {
describe('queueConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = queueConfigSchema.parse({
redis: {}, // redis is required, but its properties have defaults
redis: {}, // redis is required, but its properties have defaults
});
expect(config).toEqual({
enabled: true,
@ -493,19 +493,23 @@ describe('Config Schemas', () => {
});
it('should validate poolSize range', () => {
expect(() => postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
poolSize: 0,
})).toThrow();
expect(() =>
postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
poolSize: 0,
})
).toThrow();
expect(() => postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
poolSize: 101,
})).toThrow();
expect(() =>
postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
poolSize: 101,
})
).toThrow();
});
});
@ -574,24 +578,30 @@ describe('Config Schemas', () => {
});
it('should validate URI format', () => {
expect(() => mongodbConfigSchema.parse({
uri: 'invalid-uri',
database: 'testdb',
})).toThrow();
expect(() =>
mongodbConfigSchema.parse({
uri: 'invalid-uri',
database: 'testdb',
})
).toThrow();
});
it('should validate poolSize range', () => {
expect(() => mongodbConfigSchema.parse({
uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 0,
})).toThrow();
expect(() =>
mongodbConfigSchema.parse({
uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 0,
})
).toThrow();
expect(() => mongodbConfigSchema.parse({
uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 101,
})).toThrow();
expect(() =>
mongodbConfigSchema.parse({
uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 101,
})
).toThrow();
});
});
@ -703,11 +713,13 @@ describe('Config Schemas', () => {
});
it('should validate tier values', () => {
expect(() => eodProviderConfigSchema.parse({
name: 'eod',
apiKey: 'test-key',
tier: 'premium',
})).toThrow();
expect(() =>
eodProviderConfigSchema.parse({
name: 'eod',
apiKey: 'test-key',
tier: 'premium',
})
).toThrow();
const validTiers = ['free', 'fundamentals', 'all-in-one'];
for (const tier of validTiers) {
@ -759,10 +771,12 @@ describe('Config Schemas', () => {
});
it('should validate marketDataType', () => {
expect(() => ibProviderConfigSchema.parse({
name: 'ib',
marketDataType: 'realtime',
})).toThrow();
expect(() =>
ibProviderConfigSchema.parse({
name: 'ib',
marketDataType: 'realtime',
})
).toThrow();
const validTypes = ['live', 'delayed', 'frozen'];
for (const type of validTypes) {
@ -777,9 +791,11 @@ describe('Config Schemas', () => {
describe('qmProviderConfigSchema', () => {
it('should require all credentials', () => {
expect(() => qmProviderConfigSchema.parse({
name: 'qm',
})).toThrow();
expect(() =>
qmProviderConfigSchema.parse({
name: 'qm',
})
).toThrow();
const config = qmProviderConfigSchema.parse({
name: 'qm',

View file

@ -1,21 +1,21 @@
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod';
import {
SecretValue,
secret,
checkRequiredEnvVars,
COMMON_SECRET_PATTERNS,
createStrictSchema,
formatValidationResult,
isSecret,
redactSecrets,
isSecretEnvVar,
wrapSecretEnvVars,
mergeSchemas,
redactSecrets,
secret,
secretSchema,
secretStringSchema,
COMMON_SECRET_PATTERNS,
validateConfig,
checkRequiredEnvVars,
SecretValue,
validateCompleteness,
formatValidationResult,
createStrictSchema,
mergeSchemas,
validateConfig,
wrapSecretEnvVars,
type ValidationResult,
} from '../src';
@ -443,9 +443,7 @@ describe('Config Utils', () => {
it('should format warnings', () => {
const result: ValidationResult = {
valid: true,
warnings: [
{ path: 'deprecated.feature', message: 'This feature is deprecated' },
],
warnings: [{ path: 'deprecated.feature', message: 'This feature is deprecated' }],
};
const formatted = formatValidationResult(result);

View file

@ -166,82 +166,102 @@ export class ServiceApplication {
private registerShutdownHandlers(): void {
// Priority 1: Queue system (highest priority)
if (this.serviceConfig.enableScheduledJobs) {
this.shutdown.onShutdown(async () => {
this.logger.info('Shutting down queue system...');
try {
const queueManager = this.container?.resolve('queueManager');
if (queueManager) {
await queueManager.shutdown();
this.shutdown.onShutdown(
async () => {
this.logger.info('Shutting down queue system...');
try {
const queueManager = this.container?.resolve('queueManager');
if (queueManager) {
await queueManager.shutdown();
}
this.logger.info('Queue system shut down');
} catch (error) {
this.logger.error('Error shutting down queue system', { error });
}
this.logger.info('Queue system shut down');
} catch (error) {
this.logger.error('Error shutting down queue system', { error });
}
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'Queue System');
},
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
'Queue System'
);
}
// Priority 1: HTTP Server (high priority)
this.shutdown.onShutdown(async () => {
if (this.server) {
this.logger.info('Stopping HTTP server...');
try {
this.server.stop();
this.logger.info('HTTP server stopped');
} catch (error) {
this.logger.error('Error stopping HTTP server', { error });
this.shutdown.onShutdown(
async () => {
if (this.server) {
this.logger.info('Stopping HTTP server...');
try {
this.server.stop();
this.logger.info('HTTP server stopped');
} catch (error) {
this.logger.error('Error stopping HTTP server', { error });
}
}
}
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'HTTP Server');
},
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
'HTTP Server'
);
// Custom shutdown hook
if (this.hooks.onBeforeShutdown) {
this.shutdown.onShutdown(async () => {
try {
await this.hooks.onBeforeShutdown!();
} catch (error) {
this.logger.error('Error in custom shutdown hook', { error });
}
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'Custom Shutdown');
this.shutdown.onShutdown(
async () => {
try {
await this.hooks.onBeforeShutdown!();
} catch (error) {
this.logger.error('Error in custom shutdown hook', { error });
}
},
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
'Custom Shutdown'
);
}
// Priority 2: Services and connections (medium priority)
this.shutdown.onShutdown(async () => {
this.logger.info('Disposing services and connections...');
try {
if (this.container) {
// Disconnect database clients
const mongoClient = this.container.resolve('mongoClient');
if (mongoClient?.disconnect) {
await mongoClient.disconnect();
}
this.shutdown.onShutdown(
async () => {
this.logger.info('Disposing services and connections...');
try {
if (this.container) {
// Disconnect database clients
const mongoClient = this.container.resolve('mongoClient');
if (mongoClient?.disconnect) {
await mongoClient.disconnect();
}
const postgresClient = this.container.resolve('postgresClient');
if (postgresClient?.disconnect) {
await postgresClient.disconnect();
}
const postgresClient = this.container.resolve('postgresClient');
if (postgresClient?.disconnect) {
await postgresClient.disconnect();
}
const questdbClient = this.container.resolve('questdbClient');
if (questdbClient?.disconnect) {
await questdbClient.disconnect();
}
const questdbClient = this.container.resolve('questdbClient');
if (questdbClient?.disconnect) {
await questdbClient.disconnect();
}
this.logger.info('All services disposed successfully');
this.logger.info('All services disposed successfully');
}
} catch (error) {
this.logger.error('Error disposing services', { error });
}
} catch (error) {
this.logger.error('Error disposing services', { error });
}
}, SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, 'Services');
},
SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY,
'Services'
);
// Priority 3: Logger shutdown (lowest priority - runs last)
this.shutdown.onShutdown(async () => {
try {
this.logger.info('Shutting down loggers...');
await shutdownLoggers();
// Don't log after shutdown
} catch {
// Silently ignore logger shutdown errors
}
}, SHUTDOWN_DEFAULTS.LOW_PRIORITY, 'Loggers');
this.shutdown.onShutdown(
async () => {
try {
this.logger.info('Shutting down loggers...');
await shutdownLoggers();
// Don't log after shutdown
} catch {
// Silently ignore logger shutdown errors
}
},
SHUTDOWN_DEFAULTS.LOW_PRIORITY,
'Loggers'
);
}
/**

View file

@ -1,5 +1,10 @@
import { describe, it, expect } from 'bun:test';
import type { ServiceDefinitions, ServiceContainer, ServiceCradle, ServiceContainerOptions } from '../src/awilix-container';
import { describe, expect, it } from 'bun:test';
import type {
ServiceContainer,
ServiceContainerOptions,
ServiceCradle,
ServiceDefinitions,
} from '../src/awilix-container';
describe('Awilix Container Types', () => {
it('should export ServiceDefinitions interface', () => {

View file

@ -31,10 +31,18 @@ mock.module('@stock-bot/config', () => ({
}
// Copy flat configs to nested if they exist
if (result.redis) {result.database.dragonfly = result.redis;}
if (result.mongodb) {result.database.mongodb = result.mongodb;}
if (result.postgres) {result.database.postgres = result.postgres;}
if (result.questdb) {result.database.questdb = result.questdb;}
if (result.redis) {
result.database.dragonfly = result.redis;
}
if (result.mongodb) {
result.database.mongodb = result.mongodb;
}
if (result.postgres) {
result.database.postgres = result.postgres;
}
if (result.questdb) {
result.database.questdb = result.questdb;
}
return result;
},

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import * as diExports from '../src/index';
describe('DI Package Exports', () => {

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import type { BaseAppConfig } from '@stock-bot/config';
import { ServiceApplication } from '../src/service-application';
import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application';
import type { BaseAppConfig } from '@stock-bot/config';
// Mock logger module
const mockLogger = {
@ -193,7 +193,6 @@ describe.skip('ServiceApplication', () => {
app = new ServiceApplication(configWithoutServiceName as any, serviceConfig);
expect(app).toBeDefined();
});
});
describe('start method', () => {
@ -228,7 +227,7 @@ describe.skip('ServiceApplication', () => {
const { Hono } = require('hono');
const routes = new Hono();
// Add a simple test route
routes.get('/test', (c) => c.json({ test: true }));
routes.get('/test', c => c.json({ test: true }));
return routes;
});
const mockHandlerInitializer = mock(() => Promise.resolve());
@ -243,9 +242,11 @@ describe.skip('ServiceApplication', () => {
await app.start(mockContainerFactory, mockRouteFactory);
expect(mockContainerFactory).toHaveBeenCalledWith(expect.objectContaining({
service: expect.objectContaining({ serviceName: 'test-service' }),
}));
expect(mockContainerFactory).toHaveBeenCalledWith(
expect.objectContaining({
service: expect.objectContaining({ serviceName: 'test-service' }),
})
);
expect(mockRouteFactory).toHaveBeenCalledWith({ test: 'container' });
expect(mockLogger.info).toHaveBeenCalledWith('test-service service started on port 3000');
});
@ -260,10 +261,12 @@ describe.skip('ServiceApplication', () => {
await app.start(mockContainerFactory, mockRouteFactory, mockHandlerInitializer);
expect(mockHandlerInitializer).toHaveBeenCalledWith(expect.objectContaining({
test: 'container',
_diContainer: mockContainer,
}));
expect(mockHandlerInitializer).toHaveBeenCalledWith(
expect.objectContaining({
test: 'container',
_diContainer: mockContainer,
})
);
expect(mockLogger.info).toHaveBeenCalledWith('Handlers initialized');
});
@ -300,7 +303,9 @@ describe.skip('ServiceApplication', () => {
app = new ServiceApplication(mockConfig, serviceConfig);
await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow('Container creation failed');
await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow(
'Container creation failed'
);
expect(mockLogger.error).toHaveBeenCalledWith('DETAILED ERROR:', expect.any(Error));
});
@ -311,17 +316,23 @@ describe.skip('ServiceApplication', () => {
};
const mockHandlerRegistry = {
getAllHandlersWithSchedule: () => new Map([
['testHandler', {
scheduledJobs: [{
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
payload: { test: true },
}],
}],
]),
getAllHandlersWithSchedule: () =>
new Map([
[
'testHandler',
{
scheduledJobs: [
{
operation: 'processData',
cronPattern: '0 * * * *',
priority: 5,
immediately: false,
payload: { test: true },
},
],
},
],
]),
getHandlerService: () => 'test-service',
getHandlerNames: () => ['testHandler'],
getOperation: () => ({ name: 'processData' }),
@ -339,9 +350,15 @@ describe.skip('ServiceApplication', () => {
const containerWithJobs = {
resolve: mock((name: string) => {
if (name === 'serviceContainer') {return { test: 'container' };}
if (name === 'handlerRegistry') {return mockHandlerRegistry;}
if (name === 'queueManager') {return mockQueueManager;}
if (name === 'serviceContainer') {
return { test: 'container' };
}
if (name === 'handlerRegistry') {
return mockHandlerRegistry;
}
if (name === 'queueManager') {
return mockQueueManager;
}
return null;
}),
};
@ -359,7 +376,7 @@ describe.skip('ServiceApplication', () => {
'processData',
{ handler: 'testHandler', operation: 'processData', payload: { test: true } },
'0 * * * *',
expect.objectContaining({ priority: 5, repeat: { immediately: false } }),
expect.objectContaining({ priority: 5, repeat: { immediately: false } })
);
expect(mockQueueManager.startAllWorkers).toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalledWith('Scheduled jobs created', { totalJobs: 1 });
@ -451,18 +468,30 @@ describe.skip('ServiceApplication', () => {
const mockContainer = {
resolve: mock((name: string) => {
if (name === 'serviceContainer') {return { test: 'container' };}
if (name === 'handlerRegistry') {return {
getAllHandlersWithSchedule: () => new Map(),
getHandlerNames: () => [],
};}
if (name === 'queueManager') {return {
shutdown: mock(() => Promise.resolve()),
startAllWorkers: mock(() => {}),
};}
if (name === 'mongoClient') {return { disconnect: mock(() => Promise.resolve()) };}
if (name === 'postgresClient') {return { disconnect: mock(() => Promise.resolve()) };}
if (name === 'questdbClient') {return { disconnect: mock(() => Promise.resolve()) };}
if (name === 'serviceContainer') {
return { test: 'container' };
}
if (name === 'handlerRegistry') {
return {
getAllHandlersWithSchedule: () => new Map(),
getHandlerNames: () => [],
};
}
if (name === 'queueManager') {
return {
shutdown: mock(() => Promise.resolve()),
startAllWorkers: mock(() => {}),
};
}
if (name === 'mongoClient') {
return { disconnect: mock(() => Promise.resolve()) };
}
if (name === 'postgresClient') {
return { disconnect: mock(() => Promise.resolve()) };
}
if (name === 'questdbClient') {
return { disconnect: mock(() => Promise.resolve()) };
}
return null;
}),
};

View file

@ -1,15 +1,15 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import type {
GenericClientConfig,
ConnectionPoolConfig,
MongoDBPoolConfig,
PostgreSQLPoolConfig,
CachePoolConfig,
QueuePoolConfig,
ConnectionFactory,
ConnectionFactoryConfig,
ConnectionPool,
ConnectionPoolConfig,
GenericClientConfig,
MongoDBPoolConfig,
PoolMetrics,
ConnectionFactory,
PostgreSQLPoolConfig,
QueuePoolConfig,
} from '../src/types';
describe('DI Types', () => {
@ -197,7 +197,7 @@ describe('DI Types', () => {
describe('ConnectionFactory', () => {
it('should define connection factory interface', () => {
const mockFactory: ConnectionFactory = {
createMongoDB: async (config) => ({
createMongoDB: async config => ({
name: config.name,
client: {},
metrics: {
@ -211,7 +211,7 @@ describe('DI Types', () => {
health: async () => true,
dispose: async () => {},
}),
createPostgreSQL: async (config) => ({
createPostgreSQL: async config => ({
name: config.name,
client: {},
metrics: {
@ -225,7 +225,7 @@ describe('DI Types', () => {
health: async () => true,
dispose: async () => {},
}),
createCache: async (config) => ({
createCache: async config => ({
name: config.name,
client: {},
metrics: {
@ -239,7 +239,7 @@ describe('DI Types', () => {
health: async () => true,
dispose: async () => {},
}),
createQueue: async (config) => ({
createQueue: async config => ({
name: config.name,
client: {},
metrics: {

View file

@ -80,7 +80,6 @@ export class HandlerRegistry {
return this.handlers.has(handlerName);
}
/**
* Set service ownership for a handler
*/
@ -107,7 +106,10 @@ export class HandlerRegistry {
getServiceHandlers(serviceName: string): HandlerMetadata[] {
const handlers: HandlerMetadata[] = [];
for (const [handlerName, metadata] of this.handlers) {
if (this.handlerServices.get(handlerName) === serviceName || metadata.service === serviceName) {
if (
this.handlerServices.get(handlerName) === serviceName ||
metadata.service === serviceName
) {
handlers.push(metadata);
}
}

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import * as handlerRegistryExports from '../src';
import { HandlerRegistry } from '../src';
@ -50,8 +50,8 @@ describe('Handler Registry Package Exports', () => {
totalOperations: 10,
totalSchedules: 3,
handlersByService: {
'service1': 2,
'service2': 3,
service1: 2,
service2: 3,
},
};

View file

@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { JobHandler, ScheduledJob } from '@stock-bot/types';
import { HandlerRegistry } from '../src/registry';
import type {
HandlerConfiguration,
@ -6,7 +7,6 @@ import type {
OperationMetadata,
ScheduleMetadata,
} from '../src/types';
import type { JobHandler, ScheduledJob } from '@stock-bot/types';
describe('HandlerRegistry Edge Cases', () => {
let registry: HandlerRegistry;
@ -324,9 +324,7 @@ describe('HandlerRegistry Edge Cases', () => {
it('should count schedules from metadata', () => {
const metadata: HandlerMetadata = {
name: 'ScheduledHandler',
operations: [
{ name: 'op1', method: 'method1' },
],
operations: [{ name: 'op1', method: 'method1' }],
schedules: [
{ operation: 'op1', cronPattern: '* * * * *' },
{ operation: 'op1', cronPattern: '0 * * * *' },

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
import { BaseHandler } from '../src/base/BaseHandler';
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import type { IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
describe('Auto Registration - Simple Tests', () => {
describe('autoRegisterHandlers', () => {
@ -33,7 +33,7 @@ describe('Auto Registration - Simple Tests', () => {
it('should handle excluded patterns', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices, {
exclude: ['test']
exclude: ['test'],
});
expect(result.registered).toEqual([]);
@ -43,7 +43,7 @@ describe('Auto Registration - Simple Tests', () => {
it('should accept custom pattern', async () => {
const mockServices = {} as IServiceContainer;
const result = await autoRegisterHandlers('./test', mockServices, {
pattern: '.custom.'
pattern: '.custom.',
});
expect(result.registered).toEqual([]);
@ -66,10 +66,7 @@ describe('Auto Registration - Simple Tests', () => {
const mockServices = {} as IServiceContainer;
const registry = createAutoHandlerRegistry(mockServices);
const result = await registry.registerDirectories([
'./non-existent-1',
'./non-existent-2'
]);
const result = await registry.registerDirectories(['./non-existent-1', './non-existent-2']);
expect(result.registered).toEqual([]);
expect(result.failed).toEqual([]);

View file

@ -52,27 +52,18 @@ describe('Auto Registration Unit Tests', () => {
];
const pattern = '.handler.';
const filtered = files.filter(file =>
file.includes(pattern) &&
file.endsWith('.ts') &&
!file.startsWith('.')
const filtered = files.filter(
file => file.includes(pattern) && file.endsWith('.ts') && !file.startsWith('.')
);
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
});
it('should handle different patterns', () => {
const files = [
'test.handler.ts',
'test.custom.ts',
'another.custom.ts',
];
const files = ['test.handler.ts', 'test.custom.ts', 'another.custom.ts'];
const customPattern = '.custom.';
const filtered = files.filter(file =>
file.includes(customPattern) &&
file.endsWith('.ts')
);
const filtered = files.filter(file => file.includes(customPattern) && file.endsWith('.ts'));
expect(filtered).toEqual(['test.custom.ts', 'another.custom.ts']);
});
@ -158,16 +149,10 @@ describe('Auto Registration Unit Tests', () => {
describe('Options Handling', () => {
it('should apply exclude patterns', () => {
const files = [
'test.handler.ts',
'excluded.handler.ts',
'another.handler.ts',
];
const files = ['test.handler.ts', 'excluded.handler.ts', 'another.handler.ts'];
const exclude = ['excluded'];
const filtered = files.filter(file =>
!exclude.some(ex => file.includes(ex))
);
const filtered = files.filter(file => !exclude.some(ex => file.includes(ex)));
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
});

View file

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
import { BaseHandler } from '../src/base/BaseHandler';
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
describe('Auto Registration', () => {
describe('autoRegisterHandlers', () => {
@ -89,7 +89,7 @@ describe('Auto Registration', () => {
const mockServices = {} as any;
const result = await autoRegisterHandlers('./test', mockServices, {
serviceName: 'test-service'
serviceName: 'test-service',
});
expect(result).toBeDefined();
@ -107,7 +107,7 @@ describe('Auto Registration', () => {
it('should handle excluded files', async () => {
const mockServices = {} as any;
const result = await autoRegisterHandlers('./test', mockServices, {
exclude: ['test']
exclude: ['test'],
});
expect(result).toBeDefined();

View file

@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
// Test handler with metadata
class ConfigTestHandler extends BaseHandler {

View file

@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
// Test handler implementation
class TestHandler extends BaseHandler {
@ -96,14 +96,18 @@ describe('BaseHandler Edge Cases', () => {
const handler = new TestHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow('Unknown operation: unknownOp');
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow(
'Unknown operation: unknownOp'
);
});
it('should handle operation with no operations metadata', async () => {
const handler = new EmptyHandler(mockServices);
const context: ExecutionContext = { type: 'queue', metadata: {} };
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow('Unknown operation: anyOp');
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow(
'Unknown operation: anyOp'
);
});
it('should throw when method is not a function', async () => {
@ -122,7 +126,7 @@ describe('BaseHandler Edge Cases', () => {
const context: ExecutionContext = {
type: 'queue',
metadata: { source: 'test' }
metadata: { source: 'test' },
};
const result = await handler.execute('test', { data: 'test' }, context);
@ -271,9 +275,7 @@ describe('BaseHandler Edge Cases', () => {
it('should create handler config with operations', () => {
const HandlerWithMeta = class extends BaseHandler {
static __handlerName = 'config-handler';
static __operations = [
{ name: 'process', method: 'processData' },
];
static __operations = [{ name: 'process', method: 'processData' }];
static __schedules = [];
};

View file

@ -1,7 +1,7 @@
import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
import * as utils from '@stock-bot/utils';
import { BaseHandler } from '../src/base/BaseHandler';
// Mock fetch
const mockFetch = mock();
@ -70,7 +70,8 @@ describe('BaseHandler HTTP Methods', () => {
await handler.testGet('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
method: 'GET',
logger: expect.any(Object),
@ -88,12 +89,13 @@ describe('BaseHandler HTTP Methods', () => {
mockFetch.mockResolvedValue(mockResponse);
await handler.testGet('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer token' },
headers: { Authorization: 'Bearer token' },
});
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: { 'Authorization': 'Bearer token' },
headers: { Authorization: 'Bearer token' },
method: 'GET',
logger: expect.any(Object),
})
@ -115,7 +117,8 @@ describe('BaseHandler HTTP Methods', () => {
const data = { name: 'test', value: 123 };
await handler.testPost('https://api.example.com/create', data);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/create',
expect.objectContaining({
method: 'POST',
body: JSON.stringify(data),
@ -134,11 +137,16 @@ describe('BaseHandler HTTP Methods', () => {
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testPost('https://api.example.com/create', { test: 'data' }, {
headers: { 'X-Custom': 'value' },
});
await handler.testPost(
'https://api.example.com/create',
{ test: 'data' },
{
headers: { 'X-Custom': 'value' },
}
);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/create',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ test: 'data' }),
@ -165,7 +173,8 @@ describe('BaseHandler HTTP Methods', () => {
const data = { id: 1, name: 'updated' };
await handler.testPut('https://api.example.com/update/1', data);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update/1',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/update/1',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify(data),
@ -184,12 +193,17 @@ describe('BaseHandler HTTP Methods', () => {
};
mockFetch.mockResolvedValue(mockResponse);
await handler.testPut('https://api.example.com/update', { data: 'test' }, {
headers: { 'If-Match': 'etag' },
timeout: 5000,
});
await handler.testPut(
'https://api.example.com/update',
{ data: 'test' },
{
headers: { 'If-Match': 'etag' },
timeout: 5000,
}
);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/update',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({ data: 'test' }),
@ -216,7 +230,8 @@ describe('BaseHandler HTTP Methods', () => {
await handler.testDelete('https://api.example.com/delete/1');
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/delete/1',
expect.objectContaining({
method: 'DELETE',
logger: expect.any(Object),
@ -234,12 +249,13 @@ describe('BaseHandler HTTP Methods', () => {
mockFetch.mockResolvedValue(mockResponse);
await handler.testDelete('https://api.example.com/delete/1', {
headers: { 'Authorization': 'Bearer token' },
headers: { Authorization: 'Bearer token' },
});
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/delete/1',
expect.objectContaining({
headers: { 'Authorization': 'Bearer token' },
headers: { Authorization: 'Bearer token' },
method: 'DELETE',
logger: expect.any(Object),
})
@ -251,7 +267,9 @@ describe('BaseHandler HTTP Methods', () => {
it('should propagate fetch errors', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow('Network error');
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow(
'Network error'
);
});
it('should handle non-ok responses', async () => {

View file

@ -1,6 +1,12 @@
import { describe, it, expect } from 'bun:test';
import { Handler, Operation, QueueSchedule, ScheduledOperation, Disabled } from '../src/decorators/decorators';
import { describe, expect, it } from 'bun:test';
import { BaseHandler } from '../src/base/BaseHandler';
import {
Disabled,
Handler,
Operation,
QueueSchedule,
ScheduledOperation,
} from '../src/decorators/decorators';
describe('Decorators Edge Cases', () => {
describe('Handler Decorator', () => {
@ -22,10 +28,9 @@ describe('Decorators Edge Cases', () => {
});
it('should work with context parameter', () => {
const HandlerClass = Handler('with-context')(
class TestClass extends BaseHandler {},
{ kind: 'class' }
);
const HandlerClass = Handler('with-context')(class TestClass extends BaseHandler {}, {
kind: 'class',
});
const ctor = HandlerClass as any;
expect(ctor.__handlerName).toBe('with-context');
@ -72,7 +77,7 @@ describe('Decorators Edge Cases', () => {
delayInHours: 24,
priority: 5,
direct: false,
}
},
})
batchMethod() {}
}
@ -93,7 +98,7 @@ describe('Decorators Edge Cases', () => {
batch: {
enabled: true,
size: 50,
}
},
})
partialBatchMethod() {}
}
@ -288,10 +293,7 @@ describe('Decorators Edge Cases', () => {
});
it('should work with context parameter', () => {
const DisabledClass = Disabled()(
class TestClass extends BaseHandler {},
{ kind: 'class' }
);
const DisabledClass = Disabled()(class TestClass extends BaseHandler {}, { kind: 'class' });
const ctor = DisabledClass as any;
expect(ctor.__disabled).toBe(true);
@ -326,11 +328,18 @@ describe('Decorators Edge Cases', () => {
// Operations (3 total - simple, batch, and combined)
expect(ctor.__operations).toHaveLength(3);
expect(ctor.__operations.map((op: any) => op.name)).toEqual(['simple-op', 'batch-op', 'combined']);
expect(ctor.__operations.map((op: any) => op.name)).toEqual([
'simple-op',
'batch-op',
'combined',
]);
// Schedules (2 total - scheduledOnly and combined)
expect(ctor.__schedules).toHaveLength(2);
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual(['scheduledOnly', 'combinedMethod']);
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual([
'scheduledOnly',
'combinedMethod',
]);
});
it('should handle disabled handler with operations', () => {
@ -372,7 +381,11 @@ describe('Decorators Edge Cases', () => {
}
const ctor = TestHandler as any;
expect(ctor.__operations.map((op: any) => op.method)).toEqual(['toString', 'valueOf', 'hasOwnProperty']);
expect(ctor.__operations.map((op: any) => op.method)).toEqual([
'toString',
'valueOf',
'hasOwnProperty',
]);
});
});
});

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import * as handlersExports from '../src';
import { BaseHandler, ScheduledHandler } from '../src';

View file

@ -10,7 +10,6 @@ export {
parseQueueName,
} from './service-utils';
// Batch processing
export { processBatchJob, processItems } from './batch-processor';

View file

@ -219,10 +219,14 @@ export class QueueManager {
ttl: 86400, // 24 hours default
enableMetrics: true,
logger: {
info: (...args: unknown[]) => this.logger.info(String(args[0]), args[1] as Record<string, unknown>),
error: (...args: unknown[]) => this.logger.error(String(args[0]), args[1] as Record<string, unknown>),
warn: (...args: unknown[]) => this.logger.warn(String(args[0]), args[1] as Record<string, unknown>),
debug: (...args: unknown[]) => this.logger.debug(String(args[0]), args[1] as Record<string, unknown>),
info: (...args: unknown[]) =>
this.logger.info(String(args[0]), args[1] as Record<string, unknown>),
error: (...args: unknown[]) =>
this.logger.error(String(args[0]), args[1] as Record<string, unknown>),
warn: (...args: unknown[]) =>
this.logger.warn(String(args[0]), args[1] as Record<string, unknown>),
debug: (...args: unknown[]) =>
this.logger.debug(String(args[0]), args[1] as Record<string, unknown>),
},
});
this.caches.set(queueName, cacheProvider);

View file

@ -33,8 +33,14 @@ export class Shutdown {
/**
* Register a cleanup callback
*/
onShutdown(callback: ShutdownCallback, priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, name?: string): void {
if (this.isShuttingDown) { return };
onShutdown(
callback: ShutdownCallback,
priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY,
name?: string
): void {
if (this.isShuttingDown) {
return;
}
this.callbacks.push({ callback, priority, name });
}
@ -42,7 +48,9 @@ export class Shutdown {
* Initiate graceful shutdown
*/
async shutdown(): Promise<void> {
if (this.isShuttingDown) { return };
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
@ -71,7 +79,9 @@ export class Shutdown {
}
private setupSignalHandlers(): void {
if (this.signalHandlersRegistered) { return };
if (this.signalHandlersRegistered) {
return;
}
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];

View file

@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { describe, expect, it } from 'bun:test';
import * as shutdownExports from '../src';
import { Shutdown } from '../src';

View file

@ -10,8 +10,8 @@ import {
onShutdownMedium,
resetShutdown,
setShutdownTimeout,
shutdownAndExit,
Shutdown,
shutdownAndExit,
} from '../src';
import type { ShutdownOptions, ShutdownResult } from '../src/types';
@ -104,7 +104,9 @@ describe('Shutdown Comprehensive Tests', () => {
it('should handle negative timeout values', () => {
// Should throw for negative values
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be a positive number');
expect(() => setShutdownTimeout(-1000)).toThrow(
'Shutdown timeout must be a positive number'
);
});
it('should handle zero timeout', () => {

View file

@ -16,7 +16,9 @@ export class SimpleMongoDBClient {
}
async find(collection: string, filter: any = {}): Promise<any[]> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
const docs = this.collections.get(collection) || [];
// Simple filter matching
@ -26,7 +28,9 @@ export class SimpleMongoDBClient {
return docs.filter(doc => {
for (const [key, value] of Object.entries(filter)) {
if (doc[key] !== value) {return false;}
if (doc[key] !== value) {
return false;
}
}
return true;
});
@ -38,7 +42,9 @@ export class SimpleMongoDBClient {
}
async insert(collection: string, doc: any): Promise<void> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
const docs = this.collections.get(collection) || [];
docs.push({ ...doc, _id: Math.random().toString(36) });
this.collections.set(collection, docs);
@ -51,10 +57,14 @@ export class SimpleMongoDBClient {
}
async update(collection: string, filter: any, update: any): Promise<number> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
const docs = await this.find(collection, filter);
if (docs.length === 0) {return 0;}
if (docs.length === 0) {
return 0;
}
const doc = docs[0];
if (update.$set) {
@ -65,7 +75,9 @@ export class SimpleMongoDBClient {
}
async updateMany(collection: string, filter: any, update: any): Promise<number> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
const docs = await this.find(collection, filter);
for (const doc of docs) {
@ -78,11 +90,15 @@ export class SimpleMongoDBClient {
}
async delete(collection: string, filter: any): Promise<number> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
const allDocs = this.collections.get(collection) || [];
const toDelete = await this.find(collection, filter);
if (toDelete.length === 0) {return 0;}
if (toDelete.length === 0) {
return 0;
}
const remaining = allDocs.filter(doc => !toDelete.includes(doc));
this.collections.set(collection, remaining);
@ -91,7 +107,9 @@ export class SimpleMongoDBClient {
}
async deleteMany(collection: string, filter: any): Promise<number> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
const allDocs = this.collections.get(collection) || [];
const toDelete = await this.find(collection, filter);
@ -102,7 +120,9 @@ export class SimpleMongoDBClient {
}
async batchUpsert(collection: string, documents: any[], uniqueKeys: string[]): Promise<void> {
if (!this.connected) {await this.connect();}
if (!this.connected) {
await this.connect();
}
for (const doc of documents) {
const filter: any = {};

View file

@ -22,18 +22,24 @@ export class SimplePostgresClient {
break;
}
}
if (match) {return row;}
if (match) {
return row;
}
}
return null;
}
async find(table: string, where: any): Promise<any[]> {
const rows = this.tables.get(table) || [];
if (Object.keys(where).length === 0) {return rows;}
if (Object.keys(where).length === 0) {
return rows;
}
return rows.filter(row => {
for (const [key, value] of Object.entries(where)) {
if (row[key] !== value) {return false;}
if (row[key] !== value) {
return false;
}
}
return true;
});
@ -72,7 +78,9 @@ export class SimplePostgresClient {
const rows = this.tables.get(table) || [];
const remaining = rows.filter(row => {
for (const [key, value] of Object.entries(where)) {
if (row[key] !== value) {return true;}
if (row[key] !== value) {
return true;
}
}
return false;
});

View file

@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { SimpleBrowser } from '../src/simple-browser';
describe('Browser', () => {
let browser: SimpleBrowser;
const logger = {

View file

@ -128,9 +128,9 @@ describe('Enhanced Fetch', () => {
});
});
await expect(
fetch('https://api.example.com/data', { timeout: 50 })
).rejects.toThrow('The operation was aborted');
await expect(fetch('https://api.example.com/data', { timeout: 50 })).rejects.toThrow(
'The operation was aborted'
);
});
it('should clear timeout on success', async () => {
@ -147,9 +147,9 @@ describe('Enhanced Fetch', () => {
it('should clear timeout on error', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
await expect(
fetch('https://api.example.com/data', { timeout: 1000 })
).rejects.toThrow('Network error');
await expect(fetch('https://api.example.com/data', { timeout: 1000 })).rejects.toThrow(
'Network error'
);
});
});
@ -188,9 +188,9 @@ describe('Enhanced Fetch', () => {
const error = new Error('Connection failed');
mockFetch.mockRejectedValue(error);
await expect(
fetch('https://api.example.com/data', { logger: mockLogger })
).rejects.toThrow('Connection failed');
await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toThrow(
'Connection failed'
);
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
url: 'https://api.example.com/data',
@ -264,17 +264,15 @@ describe('Enhanced Fetch', () => {
const error = new TypeError('Failed to fetch');
mockFetch.mockRejectedValue(error);
await expect(fetch('https://api.example.com/data')).rejects.toThrow(
'Failed to fetch'
);
await expect(fetch('https://api.example.com/data')).rejects.toThrow('Failed to fetch');
});
it('should handle non-Error objects', async () => {
mockFetch.mockRejectedValue('string error');
await expect(
fetch('https://api.example.com/data', { logger: mockLogger })
).rejects.toBe('string error');
await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toBe(
'string error'
);
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
url: 'https://api.example.com/data',