diff --git a/libs/core/di/connection-factory.ts b/libs/core/di/src/connection-factory.ts similarity index 100% rename from libs/core/di/connection-factory.ts rename to libs/core/di/src/connection-factory.ts diff --git a/libs/core/di/index.ts b/libs/core/di/src/index.ts similarity index 100% rename from libs/core/di/index.ts rename to libs/core/di/src/index.ts diff --git a/libs/core/di/operation-context.ts b/libs/core/di/src/operation-context.ts similarity index 100% rename from libs/core/di/operation-context.ts rename to libs/core/di/src/operation-context.ts diff --git a/libs/core/di/pool-size-calculator.ts b/libs/core/di/src/pool-size-calculator.ts similarity index 100% rename from libs/core/di/pool-size-calculator.ts rename to libs/core/di/src/pool-size-calculator.ts diff --git a/libs/core/di/service-container.ts b/libs/core/di/src/service-container.ts similarity index 84% rename from libs/core/di/service-container.ts rename to libs/core/di/src/service-container.ts index 562ec2a..d25ac3d 100644 --- a/libs/core/di/service-container.ts +++ b/libs/core/di/src/service-container.ts @@ -30,11 +30,38 @@ export class ServiceContainer implements ServiceResolver { this.logger.debug('Service registered', { name: registration.name, singleton: registration.singleton }); } - resolve(name: string, options?: any): T { - const instance = this.resolveAsync(name, options); + resolve(name: string, _options?: any): T { + // Check scoped instances first + if (this.scopedInstances.has(name)) { + return this.scopedInstances.get(name); + } + + // Check singleton instances + if (this.instances.has(name)) { + return this.instances.get(name); + } + + // Get registration from this container or parent + const registration = this.getRegistration(name); + if (!registration) { + throw new Error(`Service ${name} not registered`); + } + + // Create instance synchronously + const instance = registration.factory(); + + // Check if factory returned a promise if (instance instanceof Promise) { throw new Error(`Service ${name} is async. Use resolveAsync() instead.`); } + + // Store based on singleton flag + if (registration.singleton) { + this.instances.set(name, instance); + } else { + this.scopedInstances.set(name, instance); + } + return instance as T; } diff --git a/libs/core/di/types.ts b/libs/core/di/src/types.ts similarity index 100% rename from libs/core/di/types.ts rename to libs/core/di/src/types.ts diff --git a/libs/core/di/test/di.test.ts b/libs/core/di/test/di.test.ts new file mode 100644 index 0000000..08201ad --- /dev/null +++ b/libs/core/di/test/di.test.ts @@ -0,0 +1,147 @@ +/** + * Test DI library functionality + */ +import { test, expect, describe } from 'bun:test'; +import { ServiceContainer, ConnectionFactory, OperationContext, PoolSizeCalculator } from '../src/index'; + +describe('DI Library', () => { + test('ServiceContainer - sync resolution', () => { + const container = new ServiceContainer('test'); + + container.register({ + name: 'testService', + factory: () => ({ value: 'test' }), + singleton: true, + }); + + const service = container.resolve<{ value: string }>('testService'); + expect(service.value).toBe('test'); + }); + + test('ServiceContainer - async resolution', async () => { + const container = new ServiceContainer('test'); + + container.register({ + name: 'asyncService', + factory: async () => ({ value: 'async-test' }), + singleton: true, + }); + + const service = await container.resolveAsync<{ value: string }>('asyncService'); + expect(service.value).toBe('async-test'); + }); + + test('ServiceContainer - scoped container', () => { + const container = new ServiceContainer('test'); + + container.register({ + name: 'testService', + factory: () => ({ value: 'test' }), + singleton: true, + }); + + const scopedContainer = container.createScope(); + const service = scopedContainer.resolve<{ value: string }>('testService'); + expect(service.value).toBe('test'); + }); + + test('ServiceContainer - error on unregistered service', () => { + const container = new ServiceContainer('test'); + + expect(() => { + container.resolve('nonexistent'); + }).toThrow('Service nonexistent not registered'); + }); + + test('ServiceContainer - async service throws error on sync resolve', () => { + const container = new ServiceContainer('test'); + + container.register({ + name: 'asyncService', + factory: async () => ({ value: 'async' }), + singleton: true, + }); + + expect(() => { + container.resolve('asyncService'); + }).toThrow('Service asyncService is async. Use resolveAsync() instead.'); + }); + + test('ServiceContainer - disposal', async () => { + const container = new ServiceContainer('test'); + let disposed = false; + + container.register({ + name: 'disposableService', + factory: () => ({ value: 'test' }), + singleton: true, + dispose: async () => { + disposed = true; + }, + }); + + // Create instance + container.resolve('disposableService'); + + // Dispose container + await container.dispose(); + expect(disposed).toBe(true); + }); + + test('ConnectionFactory - creation', () => { + const factory = new ConnectionFactory({ + service: 'test', + environment: 'development', + }); + + expect(factory).toBeDefined(); + expect(factory.listPools()).toEqual([]); + }); + + test('OperationContext - creation', () => { + const container = new ServiceContainer('test'); + const context = OperationContext.create('test-handler', 'test-operation', { + container, + }); + + expect(context).toBeDefined(); + expect(context.logger).toBeDefined(); + }); + + test('OperationContext - child context', () => { + const context = OperationContext.create('test-handler', 'test-operation'); + const child = context.createChild('child-operation'); + + expect(child).toBeDefined(); + expect(child.logger).toBeDefined(); + }); + + test('PoolSizeCalculator - service defaults', () => { + const poolSize = PoolSizeCalculator.calculate('data-ingestion'); + expect(poolSize).toEqual({ min: 5, max: 50, idle: 10 }); + }); + + test('PoolSizeCalculator - handler defaults', () => { + const poolSize = PoolSizeCalculator.calculate('unknown-service', 'batch-import'); + expect(poolSize).toEqual({ min: 10, max: 100, idle: 20 }); + }); + + test('PoolSizeCalculator - fallback defaults', () => { + const poolSize = PoolSizeCalculator.calculate('unknown-service', 'unknown-handler'); + expect(poolSize).toEqual({ min: 2, max: 10, idle: 3 }); + }); + + test('PoolSizeCalculator - custom config', () => { + const poolSize = PoolSizeCalculator.calculate('test-service', undefined, { + minConnections: 5, + maxConnections: 15, + }); + expect(poolSize).toEqual({ min: 5, max: 15, idle: 5 }); + }); + + test('PoolSizeCalculator - optimal size calculation', () => { + const optimalSize = PoolSizeCalculator.getOptimalPoolSize(10, 100, 50); + expect(optimalSize).toBeGreaterThan(0); + expect(typeof optimalSize).toBe('number'); + }); +}); \ No newline at end of file diff --git a/libs/core/di/tsconfig.json b/libs/core/di/tsconfig.json index 8a56d2d..b1b5979 100644 --- a/libs/core/di/tsconfig.json +++ b/libs/core/di/tsconfig.json @@ -1,15 +1,15 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { - "rootDir": "./", + "rootDir": "./src", "outDir": "./dist", "composite": true, "declaration": true, "declarationMap": true, "types": ["node", "bun-types"] }, - "include": ["./**/*.ts"], - "exclude": ["node_modules", "dist"], + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test"], "references": [ { "path": "../config" }, { "path": "../logger" } diff --git a/libs/utils/package.json b/libs/utils/package.json index 8539074..6412a4f 100644 --- a/libs/utils/package.json +++ b/libs/utils/package.json @@ -13,19 +13,8 @@ "@stock-bot/config": "workspace:*", "@stock-bot/logger": "workspace:*", "@stock-bot/cache": "workspace:*", - "@stock-bot/postgres": "workspace:*", - "@stock-bot/mongodb": "workspace:*", - "@stock-bot/di": "workspace:*", "@stock-bot/types": "workspace:*", - "@stock-bot/http": "workspace:*", - "cheerio": "^1.0.0", - "axios": "^1.7.7", - "axios-rate-limit": "^1.4.0", - "axios-retry": "^4.4.1", - "socks-proxy-agent": "^8.0.2", - "p-limit": "^6.1.0", - "zod": "^3.22.4", - "date-fns": "^2.30.0" + "@stock-bot/http": "workspace:*" }, "devDependencies": { "@types/node": "^20.11.0", diff --git a/libs/utils/tsconfig.json b/libs/utils/tsconfig.json index 380c1f4..d88dafd 100644 --- a/libs/utils/tsconfig.json +++ b/libs/utils/tsconfig.json @@ -14,9 +14,6 @@ { "path": "../data/cache" }, { "path": "../core/config" }, { "path": "../core/logger" }, - { "path": "../services/http" }, - { "path": "../core/di" }, - { "path": "../data/mongodb" }, - { "path": "../data/postgres" } + { "path": "../services/http" } ] }