Skip to content

Commit 6034e58

Browse files
authored
Merge pull request #51 from AsenaJs/asena-config-improvments
Asena config improvments
2 parents e57f21c + a77f351 commit 6034e58

File tree

8 files changed

+276
-7
lines changed

8 files changed

+276
-7
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# @asenajs/asena
22

3+
## 0.6.3
4+
5+
### Patch Changes
6+
7+
- Improve type safety and documentation for server configuration
8+
- Add type-safe AsenaServerOptions excluding framework-managed properties (fetch, routes, websocket, error)
9+
- Add comprehensive Config.md documentation with Bun serve and WebSocket configuration examples
10+
- Improve async PostConstruct tests with 6 additional test cases for inheritance scenarios
11+
312
## 0.6.2
413

514
### Patch Changes

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Asena
66

7-
[![Version](https://img.shields.io/badge/version-0.6.2-blue.svg)](https://asena.sh)
7+
[![Version](https://img.shields.io/badge/version-0.6.3-blue.svg)](https://asena.sh)
88
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
99
[![Bun Version](https://img.shields.io/badge/Bun-1.3.2%2B-blueviolet)](https://bun.sh)
1010

bunfig.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ coverageSkipTestFiles = true
66

77
coveragePathIgnorePatterns = [
88
"test/utils/*.ts",
9+
"dist/**"
910
]
1011

1112

lib/adapter/types/ServeOptions.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
import type { ServeOptions } from 'bun';
22
import type { WSOptions } from '../../server/web/websocket';
33

4-
export type AsenaServerOptions = ServeOptions;
4+
/**
5+
* AsenaServerOptions excludes framework-managed properties from Bun's ServeOptions.
6+
*
7+
* Excluded properties (managed internally by AsenaJS):
8+
* - `fetch`: Managed by HTTP adapter (e.g., HonoAdapter)
9+
* - `routes`: Managed by AsenaJS routing decorators (@Get, @Post, etc.)
10+
* - `websocket`: Managed by AsenaWebsocketAdapter
11+
* - `error`: Managed through AsenaConfig.onError()
12+
*
13+
* Available options include:
14+
* - Network: `hostname`, `port`, `unix`, `reusePort`, `ipv6Only`
15+
* - Security: `tls`
16+
* - Performance: `maxRequestBodySize`, `idleTimeout`
17+
* - Development: `development`, `id`
18+
*/
19+
export type AsenaServerOptions = Omit<ServeOptions, 'fetch' | 'routes' | 'websocket' | 'error'>;
520

21+
/**
22+
* Complete configuration for AsenaJS server.
23+
*
24+
* @property serveOptions - Bun server options (excluding framework-managed properties)
25+
* @property wsOptions - AsenaJS WebSocket-specific options
26+
*/
627
export interface AsenaServeOptions {
728
serveOptions?: AsenaServerOptions;
829
wsOptions?: WSOptions;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@asenajs/asena",
3-
"version": "0.6.2",
3+
"version": "0.6.3",
44
"author": "LibirSoft",
55
"repository": {
66
"type": "git",

test/ioc/Container.test.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,88 @@ class ChildWithOverridePostConstruct extends ParentWithPostConstruct {
144144
}
145145
}
146146

147+
// Additional test classes for comprehensive async PostConstruct testing
148+
@Component()
149+
class GrandChildWithAsyncPostConstruct extends ChildWithAsyncPostConstruct {
150+
// async initialize inherited through 2 levels
151+
}
152+
153+
@Component()
154+
class MultipleAsyncPostConstruct {
155+
public step1Complete = false;
156+
157+
public step2Complete = false;
158+
159+
public step3Complete = false;
160+
161+
@PostConstruct()
162+
public async initStep1() {
163+
await new Promise((resolve) => {
164+
setTimeout(resolve, 50);
165+
});
166+
this.step1Complete = true;
167+
}
168+
169+
@PostConstruct()
170+
public async initStep2() {
171+
await new Promise((resolve) => {
172+
setTimeout(resolve, 50);
173+
});
174+
this.step2Complete = true;
175+
}
176+
177+
@PostConstruct()
178+
public async initStep3() {
179+
await new Promise((resolve) => {
180+
setTimeout(resolve, 50);
181+
});
182+
this.step3Complete = true;
183+
}
184+
}
185+
186+
@Component()
187+
class MixedSyncAsyncPostConstruct {
188+
public syncComplete = false;
189+
190+
public asyncComplete = false;
191+
192+
public executionOrder: string[] = [];
193+
194+
@PostConstruct()
195+
public syncInit() {
196+
this.syncComplete = true;
197+
this.executionOrder.push('sync');
198+
}
199+
200+
@PostConstruct()
201+
public async asyncInit() {
202+
await new Promise((resolve) => {
203+
setTimeout(resolve, 100);
204+
});
205+
this.asyncComplete = true;
206+
this.executionOrder.push('async');
207+
}
208+
}
209+
210+
@Component()
211+
class AsyncPostConstructWithDependency {
212+
public isInitialized = false;
213+
214+
public dependencyValue = '';
215+
216+
@Inject(TestClass)
217+
private testClass!: TestClass;
218+
219+
@PostConstruct()
220+
public async init() {
221+
await new Promise((resolve) => {
222+
setTimeout(resolve, 50);
223+
});
224+
this.dependencyValue = this.testClass.testMethod();
225+
this.isInitialized = true;
226+
}
227+
}
228+
147229
describe('Container', () => {
148230
let container: Container;
149231

@@ -301,12 +383,20 @@ describe('Container', () => {
301383
});
302384

303385
test('should properly await async PostConstruct for inherited async methods', async () => {
386+
const startTime = Date.now();
387+
304388
await container.register('ChildWithAsyncPostConstruct', ChildWithAsyncPostConstruct, true);
305389

306390
const instance = (await container.resolve('ChildWithAsyncPostConstruct')) as ChildWithAsyncPostConstruct;
307391

308392
expect(instance).toBeInstanceOf(ChildWithAsyncPostConstruct);
309393
expect(instance.isReady).toBe(true);
394+
expect(instance.initTime).toBeGreaterThan(0);
395+
396+
// Verify that async PostConstruct was actually awaited (at least 95ms delay, with some tolerance)
397+
const elapsedTime = Date.now() - startTime;
398+
399+
expect(elapsedTime).toBeGreaterThanOrEqual(95);
310400
});
311401

312402
test('should execute overridden PostConstruct correctly', async () => {
@@ -421,4 +511,115 @@ describe('Container', () => {
421511
expect(userService.logger).toBe(logger);
422512
expect(userService.getUser()).toBe('LOG: Getting user');
423513
});
514+
515+
// Comprehensive async PostConstruct tests
516+
test('should properly await async PostConstruct through multiple inheritance levels', async () => {
517+
const startTime = Date.now();
518+
519+
await container.register('GrandChildWithAsyncPostConstruct', GrandChildWithAsyncPostConstruct, true);
520+
521+
const instance = (await container.resolve(
522+
'GrandChildWithAsyncPostConstruct',
523+
)) as GrandChildWithAsyncPostConstruct;
524+
525+
expect(instance).toBeInstanceOf(GrandChildWithAsyncPostConstruct);
526+
expect(instance.isReady).toBe(true);
527+
expect(instance.initTime).toBeGreaterThan(0);
528+
529+
// Verify async PostConstruct was awaited through 2 inheritance levels
530+
const elapsedTime = Date.now() - startTime;
531+
532+
expect(elapsedTime).toBeGreaterThanOrEqual(100);
533+
});
534+
535+
test('should properly await multiple async PostConstruct methods', async () => {
536+
const startTime = Date.now();
537+
538+
await container.register('MultipleAsyncPostConstruct', MultipleAsyncPostConstruct, true);
539+
540+
const instance = (await container.resolve('MultipleAsyncPostConstruct')) as MultipleAsyncPostConstruct;
541+
542+
expect(instance).toBeInstanceOf(MultipleAsyncPostConstruct);
543+
expect(instance.step1Complete).toBe(true);
544+
expect(instance.step2Complete).toBe(true);
545+
expect(instance.step3Complete).toBe(true);
546+
547+
// All three PostConstruct methods should have been awaited (3 * 50ms = 150ms minimum)
548+
const elapsedTime = Date.now() - startTime;
549+
550+
expect(elapsedTime).toBeGreaterThanOrEqual(150);
551+
});
552+
553+
test('should properly handle mixed sync and async PostConstruct methods', async () => {
554+
await container.register('MixedSyncAsyncPostConstruct', MixedSyncAsyncPostConstruct, true);
555+
556+
const instance = (await container.resolve('MixedSyncAsyncPostConstruct')) as MixedSyncAsyncPostConstruct;
557+
558+
expect(instance).toBeInstanceOf(MixedSyncAsyncPostConstruct);
559+
expect(instance.syncComplete).toBe(true);
560+
expect(instance.asyncComplete).toBe(true);
561+
562+
// Both sync and async PostConstruct should execute
563+
expect(instance.executionOrder).toContain('sync');
564+
expect(instance.executionOrder).toContain('async');
565+
expect(instance.executionOrder.length).toBe(2);
566+
});
567+
568+
test('should inject dependencies before async PostConstruct execution', async () => {
569+
await container.register('AsyncPostConstructWithDependency', AsyncPostConstructWithDependency, true);
570+
571+
const instance = (await container.resolve('AsyncPostConstructWithDependency')) as AsyncPostConstructWithDependency;
572+
573+
expect(instance).toBeInstanceOf(AsyncPostConstructWithDependency);
574+
expect(instance.isInitialized).toBe(true);
575+
// Dependency should have been injected before PostConstruct
576+
expect(instance.dependencyValue).toBe('test');
577+
});
578+
579+
test('should handle sequential resolution of multiple prototype instances with async PostConstruct', async () => {
580+
await container.register('BaseWithAsyncPostConstruct', BaseWithAsyncPostConstruct, false); // prototype
581+
582+
// Resolve multiple instances sequentially
583+
const instance1 = (await container.resolve('BaseWithAsyncPostConstruct')) as BaseWithAsyncPostConstruct;
584+
const instance2 = (await container.resolve('BaseWithAsyncPostConstruct')) as BaseWithAsyncPostConstruct;
585+
const instance3 = (await container.resolve('BaseWithAsyncPostConstruct')) as BaseWithAsyncPostConstruct;
586+
587+
// All instances should be properly initialized
588+
expect(instance1.isReady).toBe(true);
589+
expect(instance2.isReady).toBe(true);
590+
expect(instance3.isReady).toBe(true);
591+
592+
// They should be different instances
593+
expect(instance1).not.toBe(instance2);
594+
expect(instance2).not.toBe(instance3);
595+
expect(instance1).not.toBe(instance3);
596+
597+
// Each should have their own initTime
598+
expect(instance1.initTime).toBeGreaterThan(0);
599+
expect(instance2.initTime).toBeGreaterThan(0);
600+
expect(instance3.initTime).toBeGreaterThan(0);
601+
602+
// Later instances should have equal or later initTime
603+
expect(instance2.initTime).toBeGreaterThanOrEqual(instance1.initTime);
604+
expect(instance3.initTime).toBeGreaterThanOrEqual(instance2.initTime);
605+
});
606+
607+
test('should handle async PostConstruct in prototype scope correctly', async () => {
608+
await container.register('ChildWithAsyncPostConstruct', ChildWithAsyncPostConstruct, false); // prototype
609+
610+
const instance1 = (await container.resolve('ChildWithAsyncPostConstruct')) as ChildWithAsyncPostConstruct;
611+
const instance2 = (await container.resolve('ChildWithAsyncPostConstruct')) as ChildWithAsyncPostConstruct;
612+
613+
// Both instances should be fully initialized
614+
expect(instance1.isReady).toBe(true);
615+
expect(instance2.isReady).toBe(true);
616+
617+
// They should be different instances
618+
expect(instance1).not.toBe(instance2);
619+
620+
// Each should have their own initTime
621+
expect(instance1.initTime).toBeGreaterThan(0);
622+
expect(instance2.initTime).toBeGreaterThan(0);
623+
expect(instance2.initTime).toBeGreaterThanOrEqual(instance1.initTime);
624+
});
424625
});

test/server/src/PrepareConfigService.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, describe, expect, mock, test } from 'bun:test';
22
import { PrepareConfigService } from '../../../lib/server/src/services/PrepareConfigService';
3-
import { ComponentType } from '../../../lib/ioc/types';
3+
import { ComponentType } from '../../../lib/ioc';
44
import type { AsenaConfig } from '../../../lib/server/config';
55
import { Config } from '../../../lib/server/decorators';
66
import { yellow } from '../../../lib/logger';

test/test/mockComponent.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe('mockComponent', () => {
134134

135135
describe('options.injections', () => {
136136
test('should only mock specified fields', () => {
137-
const { mocks } = mockComponent(AuthService, {
137+
const { mocks } = mockComponent(AuthService, {
138138
injections: ['userService'],
139139
});
140140

@@ -173,7 +173,7 @@ describe('mockComponent', () => {
173173
login: async () => ({ token: 'custom-token', userId: '1' }),
174174
};
175175

176-
const { mocks } = mockComponent(AuthService, {
176+
const { mocks } = mockComponent(AuthService, {
177177
overrides: {
178178
loginService: customMock,
179179
},
@@ -263,7 +263,7 @@ describe('mockComponent', () => {
263263

264264
let hookCalled = false;
265265

266-
const { mocks } = mockComponent(AuthService, {
266+
const { mocks } = mockComponent(AuthService, {
267267
injections: ['userService', 'loginService'],
268268
overrides: {
269269
loginService: customLoginMock,
@@ -278,5 +278,42 @@ describe('mockComponent', () => {
278278
expect(mocks['loginService']).toBe(customLoginMock);
279279
expect(mocks['userService']).toBeDefined();
280280
});
281+
282+
test('should mock both inherited and own dependencies', () => {
283+
@Component()
284+
class LoggerService {
285+
log(_message: string) {
286+
return 'logged';
287+
}
288+
}
289+
290+
@Component()
291+
class DatabaseService {
292+
query(_sql: string) {
293+
return 'result';
294+
}
295+
}
296+
297+
@Component()
298+
class BaseService {
299+
@Inject(LoggerService)
300+
protected logger!: LoggerService;
301+
}
302+
303+
@Component()
304+
class UserService extends BaseService {
305+
@Inject(DatabaseService)
306+
// @ts-ignore
307+
private database!: DatabaseService;
308+
}
309+
310+
const { instance, mocks } = mockComponent(UserService);
311+
312+
// Both inherited and own dependencies should be mocked
313+
expect(mocks['logger']).toBeDefined();
314+
expect(mocks['database']).toBeDefined();
315+
expect((instance as any).logger).toBe(mocks['logger']);
316+
expect((instance as any).database).toBe(mocks['database']);
317+
});
281318
});
282319
});

0 commit comments

Comments
 (0)