diff --git a/src/container/Container.ts b/src/container/Container.ts index a69f6c5..5d6ab1e 100644 --- a/src/container/Container.ts +++ b/src/container/Container.ts @@ -19,8 +19,13 @@ interface ParamType { name?: string; } -type LifecycleHook = "onModuleInit" | "onModuleDestroy"; -type LifecycleHookFn = () => Promise | void; +type LifecycleHook = + | "onModuleInit" + | "onApplicationBootstrap" + | "onModuleDestroy" + | "beforeApplicationShutdown" + | "onApplicationShutdown"; +type LifecycleHookFn = (signal?: string) => Promise | void; type LifecycleAware = Partial>; export class Container { @@ -172,7 +177,8 @@ export class Container { } public async callLifecycleHook( - hook: "onModuleInit" | "onModuleDestroy", + hook: LifecycleHook, + signal?: string, ): Promise { for (const instances of this.instances.values()) { for (const instance of instances.values()) { @@ -180,7 +186,7 @@ export class Container { const lifecycleInstance = instance as LifecycleAware; const hookFn = lifecycleInstance[hook]; if (typeof hookFn === "function") { - await hookFn.call(lifecycleInstance); + await hookFn.call(lifecycleInstance, signal); } } } diff --git a/src/factory/AlveoApplication.ts b/src/factory/AlveoApplication.ts index f38d42e..e493903 100644 --- a/src/factory/AlveoApplication.ts +++ b/src/factory/AlveoApplication.ts @@ -19,18 +19,26 @@ export class AlveoApplication { } /** - * Initializes the application by calling onModuleInit hooks on all providers. + * Initializes the application by calling lifecycle hooks on all providers. * This is called automatically by AlveoFactory.create(). */ public async init(): Promise { await this.container.callLifecycleHook("onModuleInit"); + await this.container.callLifecycleHook("onApplicationBootstrap"); return this; } /** - * Gracefully shuts down the application by calling onModuleDestroy hooks. + * Gracefully shuts down the application by calling lifecycle hooks. + * + * @param signal The signal that triggered the shutdown */ - public async close(): Promise { - await this.container.callLifecycleHook("onModuleDestroy"); + public async close(signal?: string): Promise { + await this.container.callLifecycleHook("onModuleDestroy", signal); + await this.container.callLifecycleHook( + "beforeApplicationShutdown", + signal, + ); + await this.container.callLifecycleHook("onApplicationShutdown", signal); } } diff --git a/src/lifecycle/index.ts b/src/lifecycle/index.ts index 9f3e9e3..5f723f8 100644 --- a/src/lifecycle/index.ts +++ b/src/lifecycle/index.ts @@ -6,6 +6,14 @@ export interface OnModuleInit { onModuleInit(): void | Promise; } +/** + * Interface defining a lifecycle hook that is called once all modules have been initialized, + * but before the application starts listening for connections. + */ +export interface OnApplicationBootstrap { + onApplicationBootstrap(): void | Promise; +} + /** * Interface defining a lifecycle hook that is called when the application is shutting down. * Useful for cleanup logic like closing database connections or stopping intervals. @@ -13,3 +21,17 @@ export interface OnModuleInit { export interface OnModuleDestroy { onModuleDestroy(): void | Promise; } + +/** + * Interface defining a lifecycle hook that is called after all onModuleDestroy() handlers have completed. + */ +export interface BeforeApplicationShutdown { + beforeApplicationShutdown(signal?: string): void | Promise; +} + +/** + * Interface defining a lifecycle hook that is called after connections close (app.close() resolves). + */ +export interface OnApplicationShutdown { + onApplicationShutdown(signal?: string): void | Promise; +} diff --git a/test/container.test.ts b/test/container.test.ts index 9b79ab8..4f0937b 100644 --- a/test/container.test.ts +++ b/test/container.test.ts @@ -373,4 +373,44 @@ describe("Alveo Container & DI", () => { expect(service.config).toBe("async_dynamic_config"); }); + + test("should call all lifecycle hooks in the correct order", async () => { + const callOrder: string[] = []; + + @Injectable() + class FullLifecycleService { + async onModuleInit() { + callOrder.push("onModuleInit"); + } + async onApplicationBootstrap() { + callOrder.push("onApplicationBootstrap"); + } + async onModuleDestroy() { + callOrder.push("onModuleDestroy"); + } + async beforeApplicationShutdown(signal?: string) { + callOrder.push(`beforeApplicationShutdown:${signal}`); + } + async onApplicationShutdown(signal?: string) { + callOrder.push(`onApplicationShutdown:${signal}`); + } + } + + @Module({ + providers: [FullLifecycleService], + }) + class RootModule {} + + const app = await AlveoFactory.create(RootModule); + expect(callOrder).toEqual(["onModuleInit", "onApplicationBootstrap"]); + + await app.close("SIGTERM"); + expect(callOrder).toEqual([ + "onModuleInit", + "onApplicationBootstrap", + "onModuleDestroy", + "beforeApplicationShutdown:SIGTERM", + "onApplicationShutdown:SIGTERM", + ]); + }); });