# Lifecycle Overview IoC container + service lifecycle management with phased bootstrap and parallel initialization. > For the **user-facing API** (registration, bootstrap, service access, runtime control), see [Application Overview](./application-overview.md). Application delegates to lifecycle internally — you should rarely need to use `ServiceContainer` or `LifecycleManager` directly. ## Bootstrap Phases Services are initialized in three phases: | Phase | Description | Timing | Await | | ------------- | ----------------------------------------- | ------------------------ | ----- | | `BeforeReady` | Services not requiring Electron API | Before `app.whenReady()` | Yes | | `Background` | Independent services, fire-and-forget | Immediately | No | | `WhenReady` | Services requiring Electron API (default) | After `app.whenReady()` | Yes | ### Bootstrap Timeline ``` |--Background (fire-and-forget)------------| |--BeforeReady--------| | |--app.whenReady()--------| | |--WhenReady--| | isBootstrapped = true |--await Background--| allReady (fire-and-forget) → ALL_SERVICES_READY ``` After all three phases complete (including Background), `LifecycleManager.allReady()` invokes `onAllReady()` on every initialized service in parallel and **immediately** emits `ALL_SERVICES_READY` — it does **not** await the hooks. `onAllReady` is a post-bootstrap supplement (see [Hook Descriptions](#hook-descriptions)), so bootstrap does not block on services running deferred work inside it. ### Phase Selection Guide #### How Phases are Bootstrapped ``` 1 Background starts (fire-and-forget) ──────────────────────────────────┐ 2 BeforeReady starts ──────────┐ │ 2 app.whenReady() ─────────────┤ │ ├─ both complete │ ▼ │ 3 WhenReady starts ────────────┐ │ ├─ complete → isBootstrapped = true │ ▼ │ 4 await Background ◄────────────────────────────────────────────────────┘ 5 onAllReady() called on ALL services → ALL_SERVICES_READY emitted ``` Key points: - **BeforeReady** runs in parallel with Electron's own initialization (`app.whenReady()`), providing "free time" — work here doesn't add to startup latency as long as it finishes before Electron is ready. - **WhenReady** runs only after both BeforeReady and Electron are ready — the only phase where Electron APIs are safe to use. - **Background** runs completely independently. It does not block any other phase, and no other phase can depend on it. #### Choosing the Right Phase ``` ┌──────────────────────┐ │ Does it use Electron │ │ APIs directly? │ └──────┬─────────┬─────┘ yes │ │ no ▼ ▼ ┌───────────┐ ┌───────────────────────────┐ │ WhenReady │ │ Is it on the critical │ └───────────┘ │ startup path? (other │ │ services depend on it) │ └─────┬──────────┬──────────┘ yes │ │ no ▼ ▼ ┌─────────────┐ ┌────────────┐ │ BeforeReady │ │ Background │ └─────────────┘ └────────────┘ ``` **BeforeReady** — Maximize parallelism with Electron init - Runs in parallel with `app.whenReady()`, so initialization here is essentially "free" if it completes before Electron is ready. - Best for: database connections, config loading, data migrations, schema validation — anything that WhenReady services will depend on. - Cannot use any Electron API (the app is not ready yet). - Can only depend on other BeforeReady services. **WhenReady** — The safe default - Runs after both BeforeReady and `app.whenReady()` have completed. - Full access to Electron APIs (`BrowserWindow`, `Tray`, `screen`, `nativeTheme`, `dialog`, `globalShortcut`, etc.). - Can depend on other WhenReady services. - Best for: window management, tray, system shortcuts, theme management, IPC handlers that need Electron APIs. - This is the default phase — if you omit `@ServicePhase`, the service is placed here. > **⚠️ Cross-phase dependencies are automatic.** BeforeReady services (`PreferenceService`, `DbService`, `CacheService`, `DataApiService`) are guaranteed to finish before WhenReady starts. Do **not** declare `@DependsOn('PreferenceService')` (or similar) on a WhenReady service — it is redundant and misleading. Only use `@DependsOn` for same-phase coupling. **Background** — Fire-and-forget - Starts immediately but runs completely independently, never blocking other phases. - Other phases' services **cannot** depend on Background services (and vice versa). - Background errors are caught and logged but never abort bootstrap. - Best for: telemetry reporting, non-critical data pre-fetching, background cleanup tasks. - Use `onAllReady()` if a Background service needs to interact with services from other phases after bootstrap. ### Dependency Rules | Phase | Can Depend On | Cannot Depend On | | ----------- | ---------------------- | ---------------------- | | BeforeReady | BeforeReady | Background, WhenReady | | Background | Background | BeforeReady, WhenReady | | WhenReady | BeforeReady, WhenReady | Background | **Cross-phase dependencies are implicit** — the "Can Depend On" column means those services are guaranteed to be ready, **not** that you should declare them via `@DependsOn`. Reserve `@DependsOn` for same-phase ordering; cross-phase readiness is enforced automatically by `LifecycleManager.startPhase()`. **Invalid dependencies are auto-corrected** with a warning log: ``` [WARN] Service 'X' declared as Background but depends on BeforeReady service 'Y', adjusted to BeforeReady ``` ## Parallel Initialization Services within the same phase that have no inter-dependencies are initialized in parallel: ``` Phase: WhenReady Layer 1: [DbService, ConfigService] <- parallel (no inter-dependency) Layer 2: [PreferenceService] <- sequential (depends on layer 1) Layer 3: [MainWindowService] <- sequential (depends on layer 2) ``` ## Lifecycle Hooks ``` Created → Initializing → Ready ⇄ Paused ↓ ↓ ↓ onInit() onReady() onPause()/onResume() ↑ ↓ │ Stopping → Stopped → Destroyed │ ↓ ↓ ↓ │ onStop() [restart] onDestroy() └───────────────────────┘ After all phases complete: Ready ──── onAllReady() (called once, no state change) ``` ### Hook Descriptions | Hook | When Called | Can Override | | -------------- | -------------------------------------------------------- | ------------ | | `onInit()` | During initialization (and re-initialization on restart) | Yes | | `onReady()` | Immediately after `onInit()` completes | Yes | | `onAllReady()` | Once after ALL services across ALL phases are ready | Yes | | `onStop()` | When the service is being stopped | Yes | | `onDestroy()` | Final cleanup, service cannot be reused | Yes | | `onPause()` | When the service is being paused (requires `Pausable`) | Yes | | `onResume()` | When the service is being resumed (requires `Pausable`) | Yes | ### Automatic Resource Cleanup BaseService uses a single unified Disposable tracking mechanism. All resources — IPC handlers, event subscriptions, recurring timers, signals, cleanup functions — are tracked as Disposables and cleaned up together during the stop lifecycle. `registerDisposable()` accepts both `Disposable` objects and plain `() => void` cleanup functions: ```typescript this.registerDisposable(someEmitter.on('event', handler)) // Disposable object this.registerDisposable(() => externalBus.off('topic', fn)) // Cleanup function ``` `ipcHandle()`, `ipcOn()`, and `registerInterval()` all return a `Disposable` registered through this same channel — IPC handlers and recurring timers are not separate cleanup categories. Cleanup flow: ``` onStop() → all disposables disposed → state = Stopped ``` `_doDestroy` is idempotent — calling it on an already-destroyed service is a safe no-op. See [IPC Handler Management](./lifecycle-usage.md#ipc-handler-management) and [Service Events](./lifecycle-usage.md#service-events-emitter--event) for usage details. ### onAllReady (System-wide Readiness) Called once after **all** services across all bootstrap phases have completed initialization. Unlike `onReady()` (which fires when the individual service is ready), `onAllReady()` fires when the entire system is ready — safe to access any service regardless of `@DependsOn` declarations. ```typescript @Injectable('BackgroundReporterService') class BackgroundReporterService extends BaseService { protected onAllReady() { // Safe to access any service — the entire system is ready const preferenceService = application.get('PreferenceService') } } ``` **Key behaviors:** - `onAllReady` is a **post-bootstrap supplement**, not part of initialization. It does not change `LifecycleState` — the service stays in `Ready` throughout. - `LifecycleManager.allReady()` invokes every service's hook in parallel and **does not await completion** (fire-and-forget). Bootstrap proceeds as soon as every hook has been invoked. - `ALL_SERVICES_READY` is emitted **immediately after all hooks have been invoked**, not after they finish. Listeners MUST NOT assume `onAllReady` side effects have completed when this event fires. - Called at most once per service instance — `restart()` does **not** re-trigger it (guarded by `_allReadyCalled`). - Errors thrown synchronously or via the returned Promise are caught by an async `.catch` in the framework, logged, and emitted as `SERVICE_ERROR` (in a microtask) — they never propagate to bootstrap. - **Do not `await` long-running business work directly in `onAllReady`.** Because the framework no longer awaits the hook, in-hook `await`s become silent background work. If a service needs deferred business work (e.g. a quiet window then recovery), schedule it via `setTimeout`, track the resulting Promise on the instance, and join it from `onStop`. See [Lifecycle Usage — onAllReady patterns](./lifecycle-usage.md#onallready-business-work-pattern). ### `onAllReady` Hook vs `ALL_SERVICES_READY` Event Same readiness moment, two delivery channels. Both fire from one synchronous `LifecycleManager.allReady()` call — the framework first invokes every service's `onAllReady`, then emits `ALL_SERVICES_READY`. They are microseconds apart on the same JS tick. | | `onAllReady` hook | `ALL_SERVICES_READY` event | |---|---|---| | Mechanism | Push — framework calls every service once | Pub/sub — only `.on(...)` subscribers receive | | Audience | The service itself, via method override | Anyone with a `LifecycleManager` reference | | Failure handling | Caught by framework, re-emitted as `SERVICE_ERROR` | Standard `EventEmitter` behaviour | **Rule of thumb**: a service reacts via its own `onAllReady`; non-service code (diagnostics, telemetry, ad-hoc listeners) subscribes to the event. Neither signals when a *specific service's* deferred work finishes — for that, expose a per-service `Signal`. ## Service States | State | Description | | -------------- | --------------------------------------- | | `Created` | Instance created, not initialized | | `Initializing` | Currently running `onInit()` | | `Ready` | Fully initialized and operational | | `Pausing` | Currently running `onPause()` | | `Paused` | Temporarily suspended | | `Resuming` | Currently running `onResume()` | | `Stopping` | Currently running `onStop()` | | `Stopped` | Stopped, can be restarted via `start()` | | `Destroyed` | Released, cannot be reused | ## Lifecycle Events (Internal API) > For most use cases, prefer the `onAllReady()` hook or `application.get()` over raw event listening. These events are primarily for infrastructure code (e.g., diagnostics, logging). For the hook vs event tradeoff, see [`onAllReady` Hook vs `ALL_SERVICES_READY` Event](#onallready-hook-vs-all_services_ready-event). Listen to lifecycle events via the `LifecycleManager` (extends `EventEmitter`): ```typescript import { LifecycleEvents, LifecycleManager } from '@main/core/lifecycle' const manager = LifecycleManager.getInstance() manager.on(LifecycleEvents.SERVICE_READY, (payload) => { logger.info(`${payload.name} is ready`) }) manager.on(LifecycleEvents.ALL_SERVICES_READY, () => { logger.info('All services ready') }) ``` | Event | Payload | Description | | ---------------------- | ------------------------ | ------------------------------------- | | `SERVICE_INITIALIZING` | `{ name, state }` | Service is starting initialization | | `SERVICE_READY` | `{ name, state }` | Service completed initialization | | `SERVICE_PAUSING` | `{ name, state }` | Service is being paused | | `SERVICE_PAUSED` | `{ name, state }` | Service is paused | | `SERVICE_RESUMING` | `{ name, state }` | Service is being resumed | | `SERVICE_RESUMED` | `{ name, state }` | Service is resumed | | `SERVICE_STOPPING` | `{ name, state }` | Service is being stopped | | `SERVICE_STOPPED` | `{ name, state }` | Service is stopped | | `SERVICE_DESTROYED` | `{ name, state }` | Service is destroyed | | `SERVICE_ERROR` | `{ name, state, error }` | Service encountered an error | | `ALL_SERVICES_READY` | (none) | All `onAllReady` hooks have been invoked (NOT necessarily completed — see [onAllReady](#onallready-system-wide-readiness)) | ## Inter-Service Communication `@DependsOn` guarantees initialization order, but some services need to react to work completed by other services at **runtime** (after `onInit()`). For example, `ShortcutService` needs to know when `MainWindowService` creates the main window — which happens after all services have initialized. The lifecycle system provides two typed primitives for this, avoiding ad-hoc `EventEmitter` patterns (no type safety, magic strings, manual cleanup): | Communication Pattern | Mechanism | Example | |---|---|---| | "Service B must init after Service A" | `@DependsOn` | PreferenceService depends on DbService | | "Service A completed runtime work, others react" (repeatable) | `Emitter` / `Event` | MainWindowService fires `onMainWindowCreated` | | "Service A completed runtime work, others react" (one-shot) | `Signal` | DbService signals `migrationComplete` | | "Tell a specific service to do something" | Direct method call via `application.get()` | `windowService.showMainWindow()` | ### Emitter / Event (Repeatable) A producer service owns an `Emitter` (private) and exposes its `Event` (public). Consumers subscribe and get a `Disposable` for automatic cleanup via `registerDisposable()`. ### Signal (One-shot) A `Signal` resolves exactly once. It implements `PromiseLike` so consumers can `await` it directly. Late subscribers receive the resolved value immediately. For full usage patterns and code examples, see [Service Events](./lifecycle-usage.md#service-events-emitter--event) and [Signal](./lifecycle-usage.md#signal-one-shot-completion). ## File Structure ``` lifecycle/ ├── types.ts # Phase, LifecycleState, ServiceMetadata, Pausable, errors ├── decorators.ts # @Injectable, @ServicePhase, @DependsOn, @Priority, etc. ├── BaseService.ts # Abstract base class with lifecycle hooks ├── event.ts # Emitter, Event, Disposable — typed inter-service events ├── signal.ts # Signal — one-shot deferred value (PromiseLike) ├── ServiceContainer.ts # IoC container with DI and conditional activation ├── DependencyResolver.ts # Topological sort, layered parallel resolution ├── LifecycleManager.ts # Phased bootstrap, shutdown, pause/resume/stop/start ├── index.ts # Barrel export └── __tests__/ # Unit tests for all components ```