Lifecycle hooks

Merinaa classes can declare four lifecycle hooks by implementing one or more of these interfaces. The framework calls them in a deterministic order during startup.

Interface When
OnModuleInit After the DI container instantiates the class and before any @OnClient / @OnServer handlers are wired
OnApplicationBootstrap After every module has finished onModuleInit — the full graph is live
OnReady After the database is connected and migrations are complete (use for DB seeding)
OnApplicationShutdown On resource stop — flush caches, close sockets

Order

1. DI container instantiates all providers / controllers
2. For every instance: onModuleInit()
3. Event handlers (@OnClient, @OnServer, @On) are wired to the event bus
4. For every instance: onApplicationBootstrap()
5. DatabaseModule connects + runs migrations
6. For every method decorated with @OnReady: the method is invoked
7. Resource stop: for every instance with onApplicationShutdown: the method is invoked

Example

import { Controller, OnReady } from '@merinaa/core';
import type { OnModuleInit, OnApplicationShutdown } from '@merinaa/core';

@Controller()
export class BankController implements OnModuleInit, OnApplicationShutdown {
    private tickId: number | null = null;

    onModuleInit(): void {
        // DI has wired dependencies but no events are live yet.
        this.loadStaticData();
    }

    @OnReady()
    async seedBankAccounts(): Promise<void> {
        // DB is connected. Safe to query.
        await this.bankService.seedDefaultAccounts();
    }

    onApplicationShutdown(): void {
        if (this.tickId !== null) clearInterval(this.tickId);
    }
}

When to use which

  • Config-only work (read ConfigService, cache a setting): onModuleInit.

  • Cross-module coordination (need another module's service to exist): onApplicationBootstrap.

  • Database-dependent setup (seed rows, backfill data): @OnReady().

  • Cleanup (stop timers, close sockets): onApplicationShutdown.

@OnReady vs lifecycle interfaces

@OnReady() is a method decorator, not a class interface. It lets you pick which specific method runs when the DB is ready — useful when a controller has multiple startup responsibilities and you want them at different phases.

@Controller()
export class WorldController {
    onModuleInit() { /* static setup */ }

    @OnReady()
    async loadDynamicData() { /* DB query */ }
}