Files
CherryHQ-cherry-studio/docs/references/lifecycle/application-overview.md
fullex 9b451b87a9 refactor(paths): add @application path alias for main/core/application
Align with the existing @logger alias convention by introducing
@application as a short alias for src/main/core/application. This
reduces import verbosity across ~130 main-process files while keeping
4 intentional sub-path imports (@main/core/application/Application)
unchanged to preserve the vi.mock bypass mechanism in tests.

Configured in tsconfig.node.json and electron.vite.config.ts; Vitest
inherits the alias automatically.

Signed-off-by: fullex <0xfullex@gmail.com>
2026-04-11 22:06:00 -07:00

257 lines
9.5 KiB
Markdown

# 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](./lifecycle-overview.md).
## Quick Start
Both `application` and `serviceList` are barrel-exported from the `@application` path alias (configured in `tsconfig.node.json` and `electron.vite.config.ts`):
```typescript
import { application, serviceList } from '@application'
// 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-quit``will-quit``shutdown()`.
## Service Registry
Services are registered in `serviceRegistry.ts`. Adding a service is one line:
```typescript
// 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:
```typescript
// ✗ 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.
```typescript
// ✗ 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:
```typescript
// 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.
```typescript
// 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
```typescript
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`.
```typescript
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-quit``will-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`:
```typescript
// 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.
```typescript
// Safe to import anywhere, even at module scope
import { application } from '@application'
```
## File Structure
```
application/
├── Application.ts # Application singleton + lazy proxy
├── serviceRegistry.ts # Central service registry (add services here)
└── index.ts # Barrel export
```