import { createContext, useContext } from 'react'; export class TuiShutdownError extends Error { constructor() { super('TUI is shutting down'); this.name = 'TuiShutdownError'; } } export interface TuiLifecycle { readonly signal: AbortSignal; isAcceptingWork(): boolean; run(work: (signal: AbortSignal) => Promise): Promise; shutdown(): Promise; } export interface TuiShutdownHooks { unmount?: () => void | Promise; destroyRenderer: () => void | Promise; closeDatabase: () => void | Promise; } export interface TuiTerminationSignalRegistrar { signals?: readonly NodeJS.Signals[]; on(signal: NodeJS.Signals, listener: () => void): void; off(signal: NodeJS.Signals, listener: () => void): void; } export function isTuiShutdownError(error: unknown): boolean { return error instanceof TuiShutdownError; } export function createTuiLifecycle(hooks: TuiShutdownHooks): TuiLifecycle { const controller = new AbortController(); const active = new Set>(); let acceptingWork = true; let shutdownPromise: Promise | null = null; const lifecycle: TuiLifecycle = { signal: controller.signal, isAcceptingWork: () => acceptingWork, run(work: (signal: AbortSignal) => Promise): Promise { if (!acceptingWork) return Promise.reject(new TuiShutdownError()); const operation = Promise.resolve().then(() => { if (controller.signal.aborted) throw new TuiShutdownError(); return work(controller.signal); }); active.add(operation); void operation.then( () => active.delete(operation), () => active.delete(operation), ); return operation; }, shutdown(): Promise { if (shutdownPromise) return shutdownPromise; acceptingWork = false; controller.abort(new TuiShutdownError()); // Defer cleanup one microtask so shutdownPromise owns re-entrant calls // before unmount hooks can synchronously trigger any further cleanup. shutdownPromise = Promise.resolve().then(async () => { while (active.size > 0) { await Promise.allSettled([...active]); } try { await hooks.unmount?.(); } finally { try { await hooks.destroyRenderer(); } finally { await hooks.closeDatabase(); } } }); return shutdownPromise; }, }; return lifecycle; } export function createTuiTerminationHandler(lifecycle: TuiLifecycle): () => void { return () => { void lifecycle.shutdown().catch(() => { // A signal handler has no UI error surface. The lifecycle still attempts // every cleanup stage through nested finally blocks. }); }; } export function getTuiTerminationSignals(platform: NodeJS.Platform = process.platform): readonly NodeJS.Signals[] { return platform === 'win32' ? ['SIGINT', 'SIGTERM', 'SIGBREAK'] : ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']; } export function registerTuiTerminationSignals( lifecycle: TuiLifecycle, registrar: TuiTerminationSignalRegistrar = { on: (signal, listener) => process.on(signal, listener), off: (signal, listener) => process.off(signal, listener), }, ): () => void { const signals = registrar.signals ?? getTuiTerminationSignals(); const handler = createTuiTerminationHandler(lifecycle); for (const signal of signals) registrar.on(signal, handler); let registered = true; return () => { if (!registered) return; registered = false; for (const signal of signals) registrar.off(signal, handler); }; } const unmanagedController = new AbortController(); export const unmanagedTuiLifecycle: TuiLifecycle = { signal: unmanagedController.signal, isAcceptingWork: () => true, run: (work) => work(unmanagedController.signal), shutdown: async () => {}, }; export const TuiLifecycleContext = createContext(unmanagedTuiLifecycle); export function useTuiLifecycle(): TuiLifecycle { return useContext(TuiLifecycleContext); }