Files
CherryHQ-cherry-studio/docs/references/lifecycle/application-overview.md
fullex 88f141790c refactor(core): dissolve paths & application barrels, seal lifecycle (naming §6.4)
Three core barrels, three diagnoses under naming-conventions §6.4:

- paths: remove the barrel — it only re-exported types while the real entry
  is application.getPath(). constants.ts and pathRegistry.ts are independent
  building blocks deep-imported by their specific consumers (rules 2/3/4).
- lifecycle: keep the barrel (a cohesive framework consumed as a set by 80+
  services), route the last deep-import leaks through it, and prune 13
  framework-internal machinery exports (resolver, decorator getters, capability
  guards) that no consumer touches — tightening the door to its public surface.
- application: remove the barrel and repoint @application directly at
  Application.ts. The barrel bundled the hot `application` locator (~218
  consumers) with the bootstrap-only serviceRegistry, so every @application
  import dragged in the whole service graph — the root of the JobManager-to-
  serviceRegistry eval-time cycle. The locator now loads on its own.

Relocate SHUTDOWN_TIMEOUT_MS from an Application static into
lifecycle/constants.ts: a shared shutdown-grace policy honored by both
Application and JobManager, so JobManager no longer imports the Application
class at all.

Docs (paths/README, application/README, application-overview) and per-file
test mocks (vi.mock('@main/core/application') to '@application';
Application.getPath vi.unmock) are updated in lockstep.
2026-07-05 22:06:52 -07:00

9.8 KiB

Application Overview

Application is the top-level orchestrator that ties together the lifecycle system and the Electron app. It is the single entry point for bootstrapping, shutting down, and controlling services at runtime.

Relationship to Lifecycle

Application          — "what to do" (register services, bootstrap, shutdown, runtime control)
  └── lifecycle/     — "how to do it" (IoC container, dependency resolution, state machine)

Application does not duplicate lifecycle logic. It delegates to ServiceContainer and LifecycleManager internally, while providing a clean, app-level API.

For lifecycle internals (phases, hooks, states, decorators, events), see Lifecycle Overview.

Quick Start

application is imported from the @application path alias, which resolves directly to Application.ts (configured in tsconfig.node.json and electron.vite.config.ts). The bootstrap-internal serviceList is imported directly from serviceRegistry.ts — it is not part of the @application surface, so importing the locator never pulls in the whole service graph:

import { application } from '@application'
import { serviceList } from '@main/core/application/serviceRegistry'

// 1. Register all services
application.registerAll(serviceList)

// 2. Bootstrap (handles all three phases + Electron lifecycle)
await application.bootstrap()

// 3. Access a service
const dbService = application.get('DbService')

Bootstrap Flow

application.bootstrap() orchestrates the full startup sequence:

setupSignalHandlers()                    ← SIGINT/SIGTERM → graceful shutdown
setupQuitHandlers()                      ← before-quit (preventQuit gate) + will-quit (shutdown)
    │
    ├── startPhase(Background)           ← fire-and-forget (non-blocking)
    │
    ├── startPhase(BeforeReady)  ─┐
    │                             ├──── run in parallel
    └── app.whenReady()          ─┘
            │
            ├── setupElectronHandlers()  ← window-all-closed, preventQuit IPC
            │
            ├── startPhase(WhenReady)    ← services requiring Electron API
            │
            ├── await Background         ← ensure background services finished
            │
            └── allReady()               ← notify all services the system is fully ready

If a fail-fast service throws during bootstrap, a dialog is shown offering Exit or Restart.

Shutdown Flow

application.shutdown() is called automatically on:

  • will-quit (Electron event, after all windows closed)
  • SIGINT / SIGTERM (with 5-second force-exit timeout, bypasses Electron event chain)
shutdown()
    ├── bootConfigService.flush()   ← save pending debounced writes
    ├── stopAll()                   ← onStop() in reverse initialization order
    ├── destroyAll()                ← onDestroy() in reverse initialization order
    └── loggerService.finish()      ← close logger (must be last)

On non-macOS, window-all-closed triggers application.quit() which flows through before-quitwill-quitshutdown().

Service Registry

Services are registered in serviceRegistry.ts. Adding a service is one line:

// serviceRegistry.ts
import { NewService } from '@main/services/NewService'

export const services = {
  // ... existing services
  NewService,    // ← add one line, types are auto-derived
} as const

This gives you type-safe access via application.get('NewService').

Service Access Rules

Services managed by the lifecycle system must not export singleton instances. The service CLASS is exported for type references only (e.g., ServiceRegistry, @DependsOn). All runtime access goes through application.get() (unconditional services) or application.getOptional() (conditional services with @Conditional).

Assign to a local variable before use

Do not chain application.get('...') with method calls directly. Assign the service to a local variable first, then use it:

// ✗ BAD: chained calls
application.get('PreferenceService').get('app.zoom_factor')
application.get('PreferenceService').set('app.zoom_factor', 1)

// ✓ GOOD: assign first, then use
const preferenceService = application.get('PreferenceService')
preferenceService.get('app.zoom_factor')
preferenceService.set('app.zoom_factor', 1)

This improves readability, avoids repeated container lookups, and makes the code easier to refactor.

Conditional service access

Services with @Conditional must be accessed via getOptional(), which returns T | undefined. Using get() on a conditional service throws an error, even when the service is active on the current platform — this prevents cross-platform bugs.

// ✗ BAD: get() on conditional service — throws even if service is active
const menu = application.get('AppMenuService')

// ✓ GOOD: getOptional() for conditional services
const menu = application.getOptional('AppMenuService')
menu?.buildMenu()

Runtime Service Control

Control individual services at runtime without restarting the app:

// Stop a service (cascades to dependents)
await application.stop('HeavyComputeService')

// Start a stopped service (re-runs onInit, cascades to dependents)
await application.start('HeavyComputeService')

// Restart = stop + start
await application.restart('HeavyComputeService')

// Pause/Resume (service must implement Pausable interface)
await application.pause('RealTimeService')
await application.resume('RealTimeService')

All operations cascade through the dependency graph automatically.

Cascade Operations

When pausing/stopping a service, all services that depend on it are automatically paused/stopped first. When resuming/starting, dependent services are restored in reverse order.

// If PreferenceService depends on DbService:
await application.stop('DbService')
// → PreferenceService is stopped first, then DbService

await application.start('DbService')
// → DbService is started first, then PreferenceService

Important: For pause/resume, ALL services in the cascade chain must implement Pausable. If any dependent service doesn't, the operation is aborted with an error log.

App Relaunch

Always use application.relaunch() instead of calling app.relaunch() directly. It handles:

  • Dev mode detection: Shows a dialog and exits gracefully (auto-relaunch is not possible in dev)
  • Platform fixes: Linux AppImage execPath rewrite, Windows Portable executable path
import { application } from '@application'

// Simple relaunch
application.relaunch()

// With custom options (forwarded to Electron's app.relaunch)
application.relaunch({ args: ['--safe-mode'] })

App Quit

Always use application.quit() or application.forceExit() instead of calling app.quit() / app.exit() directly. An ESLint rule (no-restricted-properties) will warn if app.quit() or app.exit() is used in src/main/ outside of Application.ts.

import { application } from '@application'

// Graceful quit — triggers the Electron before-quit / will-quit event chain
application.quit()

// Force exit — skips the event chain, for fatal/unrecoverable errors only
application.forceExit(1)

// Mark as quitting without triggering quit — for external quit flows (e.g. autoUpdater)
application.markQuitting()

// Prevent quit during critical operations (e.g. data migration)
const hold = application.preventQuit('Migrating data')
try { /* critical work */ } finally { hold.dispose() }

// Check quit status
if (application.isQuitting) { /* ... */ }
Method Event chain Use case
quit() Triggers before-quitwill-quit Normal user-initiated quit
forceExit(code) Skipped Fatal errors, repeated renderer crash
markQuitting() None (flag only) autoUpdater.quitAndInstall() owns its own quit flow
preventQuit(reason) Blocks before-quit Critical operations (returns hold with dispose())

Exceptions (where direct app.quit() is acceptable):

  • Before application is initialized (e.g., single-instance lock failure in index.ts)
  • Migration files (src/main/data/migration/) that run before the full app lifecycle

Renderer Usage

The renderer accesses application lifecycle methods via window.api.application:

// Quit the app (triggers before-quit → will-quit event chain)
await window.api.application.quit()

// Relaunch the app
await window.api.application.relaunch()
await window.api.application.relaunch({ args: ['--safe-mode'] })

// Prevent quit during critical operations (returns opaque holdId)
const holdId = await window.api.application.preventQuit('Migrating user data')
try {
  await performCriticalWork()
} finally {
  await window.api.application.allowQuit(holdId)
}
Method Returns Description
quit() Promise<void> Graceful quit via Electron event chain
relaunch(options?) Promise<void> Relaunch the app (with optional args)
preventQuit(reason) Promise<string> (holdId) Block app quit until released
allowQuit(holdId) Promise<void> Release a specific quit prevention hold

The application Proxy

The exported application constant is a lazy proxy — safe to import at module top level before bootstrap() is called. The actual Application instance is created on first property access.

// Safe to import anywhere, even at module scope
import { application } from '@application'

File Structure

application/
├── Application.ts      # Application singleton + lazy proxy — the `@application` alias target
└── serviceRegistry.ts  # Central service registry (add services here); imported directly, no barrel