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;
|
||||
};
|
||||
|
||||
|
||||
@@ -77,11 +77,17 @@ export async function render(
|
||||
earlyError = e as Error;
|
||||
});
|
||||
|
||||
// Flush the Vue queue. Chain: onErrorCaptured → nextTick → exit → queueMicrotask → reject
|
||||
// Flush the Vue queue. Chain: onErrorCaptured → nextTick → exit → queueMicrotask
|
||||
// → teardown → resolveExit() → stdout.write("", callback) → reject.
|
||||
// The stdout write barrier fires via process.nextTick (inside stream internals),
|
||||
// so we need setImmediate (runs after all process.nextTick callbacks), then one
|
||||
// more microtask yield so the .catch() handler on exitPromise can set earlyError.
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await new Promise<void>((r) => setImmediate(r));
|
||||
await Promise.resolve();
|
||||
|
||||
if (earlyError) {
|
||||
throw earlyError;
|
||||
|
||||
Reference in New Issue
Block a user