feat: add beforeExit natural unmount + stdout write barrier
Register process.once('beforeExit') in waitUntilExit() so TUI apps
that finish naturally (no explicit exit/unmount) clean up before the
event loop drains. Add stdout write barrier in resolveExit() to ensure
all output is flushed before settling the exit promise.
Update testing render helper to await the additional async tick
introduced by the write barrier so early-error detection still works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
let mountedAppContext: AppContext | null = null;
|
||||
let mountedResizeHandler: (() => void) | null = null;
|
||||
let mountedExitListener: (() => void) | null = null;
|
||||
let mountedBeforeExitHandler: (() => void) | null = null;
|
||||
let mountedDebug = false;
|
||||
let mountedInteractive = true;
|
||||
let mountedRawMode = false;
|
||||
@@ -125,6 +126,31 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
// even though it's not used until mount.
|
||||
let scheduledCommit: () => void = () => {};
|
||||
|
||||
// Pending exit state — stored so resolveExit() can flush stdout before
|
||||
// settling the exit promise.
|
||||
let pendingExitError: unknown = undefined;
|
||||
let pendingExitResult: unknown = undefined;
|
||||
|
||||
function resolveExit() {
|
||||
const stdout = mountedAppContext?.stdout ?? process.stdout;
|
||||
const canWrite = stdout && !stdout.destroyed && !(stdout as any).writableEnded;
|
||||
const hasWritableState = (stdout as any)._writableState !== undefined;
|
||||
|
||||
const finish = () => {
|
||||
if (pendingExitError instanceof Error) {
|
||||
exitReject(pendingExitError);
|
||||
} else {
|
||||
exitResolve(pendingExitResult);
|
||||
}
|
||||
};
|
||||
|
||||
if (canWrite && hasWritableState) {
|
||||
stdout.write("", () => finish());
|
||||
} else {
|
||||
setImmediate(() => finish());
|
||||
}
|
||||
}
|
||||
|
||||
let teardownStarted = false;
|
||||
function teardown() {
|
||||
if (teardownStarted) return;
|
||||
@@ -159,6 +185,10 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
if (mountedExitListener) {
|
||||
process.off("exit", mountedExitListener);
|
||||
}
|
||||
if (mountedBeforeExitHandler) {
|
||||
process.off("beforeExit", mountedBeforeExitHandler);
|
||||
mountedBeforeExitHandler = null;
|
||||
}
|
||||
if (mountedRawMode && mountedAppContext) {
|
||||
mountedAppContext.setRawMode(false);
|
||||
mountedRawMode = false;
|
||||
@@ -296,10 +326,11 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
queueMicrotask(() => {
|
||||
teardown();
|
||||
if (errorOrResult instanceof Error) {
|
||||
exitReject(errorOrResult);
|
||||
pendingExitError = errorOrResult;
|
||||
} else {
|
||||
exitResolve(errorOrResult);
|
||||
pendingExitResult = errorOrResult;
|
||||
}
|
||||
resolveExit();
|
||||
});
|
||||
},
|
||||
stdout,
|
||||
@@ -486,10 +517,16 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
|
||||
app.unmount = function unmount(): void {
|
||||
teardown();
|
||||
exitResolve();
|
||||
resolveExit();
|
||||
};
|
||||
|
||||
app.waitUntilExit = function waitUntilExit(): Promise<unknown> {
|
||||
if (!mountedBeforeExitHandler) {
|
||||
mountedBeforeExitHandler = () => {
|
||||
app.unmount();
|
||||
};
|
||||
process.once("beforeExit", mountedBeforeExitHandler);
|
||||
}
|
||||
return exitPromise;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user