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; onStopRuntime: () => void | Promise; }; /** * 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(); private activity: DesktopActivity = { visible: true, module: null }; private idleTimer: ReturnType | 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 { 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(); } } }