perf: optimize app startup and background lifecycles
This commit is contained in:
111
electron/main/background-lifecycle.ts
Normal file
111
electron/main/background-lifecycle.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
export type DesktopModule = 'programming' | 'painting' | 'learning' | 'robot' | 'other';
|
||||
|
||||
export type DesktopActivity = {
|
||||
visible: boolean;
|
||||
module: DesktopModule | null;
|
||||
};
|
||||
|
||||
/** Public contract names used by Main-owned lifecycle integrations. */
|
||||
export type DesktopActivityState = DesktopActivity;
|
||||
|
||||
export type BackgroundLease = {
|
||||
id: string;
|
||||
kind: string;
|
||||
};
|
||||
|
||||
export type ModuleLease = BackgroundLease;
|
||||
export type BackgroundTaskLease = BackgroundLease;
|
||||
|
||||
export type BackgroundLifecycleOptions = {
|
||||
idleStopMs?: number;
|
||||
onSleep: () => void | Promise<void>;
|
||||
onStopRuntime: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main-owned activity/lease state machine. A hidden or inactive window gets a
|
||||
* grace period, while a lease held by an active generation, prompt or build
|
||||
* prevents the runtime from being stopped underneath the work.
|
||||
*/
|
||||
export class BackgroundLifecycleController extends EventEmitter {
|
||||
private readonly idleStopMs: number;
|
||||
private readonly leases = new Map<string, BackgroundLease>();
|
||||
private activity: DesktopActivity = { visible: true, module: null };
|
||||
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private stopping = false;
|
||||
|
||||
constructor(private readonly options: BackgroundLifecycleOptions) {
|
||||
super();
|
||||
this.idleStopMs = options.idleStopMs ?? 60_000;
|
||||
}
|
||||
|
||||
getActivity(): DesktopActivity {
|
||||
return { ...this.activity };
|
||||
}
|
||||
|
||||
getLeaseCount(): number {
|
||||
return this.leases.size;
|
||||
}
|
||||
|
||||
setActivity(activity: DesktopActivity): void {
|
||||
this.activity = {
|
||||
visible: Boolean(activity.visible),
|
||||
module: activity.module ?? null,
|
||||
};
|
||||
this.emit('activity', this.getActivity());
|
||||
if (this.shouldSleep()) this.scheduleIdleStop();
|
||||
else this.cancelIdleStop();
|
||||
}
|
||||
|
||||
acquireLease(lease: BackgroundLease): () => void {
|
||||
if (!lease.id.trim()) throw new Error('Background lease id is required');
|
||||
this.leases.set(lease.id, { ...lease });
|
||||
this.cancelIdleStop();
|
||||
this.emit('lease-count', this.leases.size);
|
||||
return () => this.releaseLease(lease.id);
|
||||
}
|
||||
|
||||
releaseLease(id: string): void {
|
||||
if (!this.leases.delete(id)) return;
|
||||
this.emit('lease-count', this.leases.size);
|
||||
if (this.shouldSleep()) this.scheduleIdleStop();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cancelIdleStop();
|
||||
this.leases.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
private shouldSleep(): boolean {
|
||||
return !this.activity.visible || this.activity.module !== 'programming';
|
||||
}
|
||||
|
||||
private cancelIdleStop(): void {
|
||||
if (this.idleTimer !== null) clearTimeout(this.idleTimer);
|
||||
this.idleTimer = null;
|
||||
}
|
||||
|
||||
private scheduleIdleStop(): void {
|
||||
if (this.idleTimer !== null || this.stopping) return;
|
||||
this.idleTimer = setTimeout(() => {
|
||||
this.idleTimer = null;
|
||||
void this.sleepIfIdle();
|
||||
}, this.idleStopMs);
|
||||
}
|
||||
|
||||
private async sleepIfIdle(): Promise<void> {
|
||||
if (!this.shouldSleep() || this.leases.size > 0 || this.stopping) return;
|
||||
this.stopping = true;
|
||||
try {
|
||||
await this.options.onSleep();
|
||||
await this.options.onStopRuntime();
|
||||
this.emit('sleep');
|
||||
} finally {
|
||||
this.stopping = false;
|
||||
if (this.shouldSleep() && this.leases.size === 0) this.scheduleIdleStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user