Files
CherryHQ-cherry-studio/docs/references/lifecycle/lifecycle-overview.md
fullex 72b9105364 refactor(lifecycle): make onAllReady a fire-and-forget supplement
`LifecycleManager.allReady()` was holding `await Promise.allSettled(_doAllReady)`,
making every `onAllReady` body a synchronous bootstrap dependency. This
contradicted the hook's JSDoc ("post-bootstrap supplement, not a critical
initialization gate") and gave any in-hook deferred work an oversize blast
radius — a 60 s wait inside one service stalled `ALL_SERVICES_READY` and
delayed bootstrap completion.

Align the implementation with the JSDoc:
- `allReady()` becomes `void`. It synchronously invokes every initialized
  service's `_doAllReady()`, attaches an async `.catch` that re-emits
  `SERVICE_ERROR`, then emits `ALL_SERVICES_READY` immediately.
- `Application.bootstrap()` drops its `await` on `allReady()`.
- `LifecycleManager` tests adjusted: drop redundant `await`s, rewrite
  `resolves.toBeUndefined()` as `not.toThrow()`, drain microtasks before
  asserting on the now-async `SERVICE_ERROR` emit, and add a test
  exercising the fire-and-forget contract with a never-resolving hook.

`ALL_SERVICES_READY` now fires when hooks are *invoked*, not when they
complete. Docs reflect the contract change: a "Hook vs Event" comparison
in `lifecycle-overview.md`, two new Common Mistakes in
`lifecycle-decision-guide.md`, and an `onAllReady` business-work pattern
template in `lifecycle-usage.md` showing the \`setTimeout\` + signal +
\`onStop\` join model used by JobManager.
2026-05-20 06:14:24 -07:00

297 lines
18 KiB
Markdown

# 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<T>` / `Event<T>` | MainWindowService fires `onMainWindowCreated` |
| "Service A completed runtime work, others react" (one-shot) | `Signal<T>` | 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<T>` (private) and exposes its `Event<T>` (public). Consumers subscribe and get a `Disposable` for automatic cleanup via `registerDisposable()`.
### Signal (One-shot)
A `Signal<T>` resolves exactly once. It implements `PromiseLike<T>` 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<T>, Event<T>, Disposable — typed inter-service events
├── signal.ts # Signal<T> — 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
```