mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 22:55:56 +08:00
`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.
297 lines
18 KiB
Markdown
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
|
|
```
|