Skip to content

Commit 6e67f86

Browse files
committed
fix(core): surface sync stop failures on default quit
1 parent bd896da commit 6e67f86

File tree

2 files changed

+41
-2
lines changed

2 files changed

+41
-2
lines changed

packages/core/src/app/__tests__/resilience.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,40 @@ test("handled ctrl+c copy in input does not trigger default quit", async () => {
231231
assert.equal(backend.disposeCalls, 0);
232232
});
233233

234+
test("unhandled quit emits fatal when stop throws synchronously", async () => {
235+
class ThrowingStopBackend extends StubBackend {
236+
override stop(): Promise<void> {
237+
this.stopCalls++;
238+
this.callLog.push("stop");
239+
throw new Error("sync-stop-fail");
240+
}
241+
}
242+
243+
const backend = new ThrowingStopBackend();
244+
const app = createApp({ backend, initialState: {} });
245+
const fatals: string[] = [];
246+
247+
app.view(() => ui.text("hello"));
248+
app.onEvent((ev) => {
249+
if (ev.kind === "fatal") fatals.push(`${ev.code}:${ev.detail}`);
250+
});
251+
252+
await app.start();
253+
await emitResize(backend, 1);
254+
await settleNextFrame(backend);
255+
256+
await pushEvents(backend, [{ kind: "text", timeMs: 3, codepoint: 113 }]);
257+
await flushMicrotasks(30);
258+
259+
assert.equal(fatals.length >= 1, true);
260+
assert.equal(
261+
fatals[0]?.startsWith("ZRUI_BACKEND_ERROR:stop threw after unhandled quit input:"),
262+
true,
263+
);
264+
assert.equal(backend.disposeCalls, 1);
265+
assert.equal(backend.stopCalls >= 2, true);
266+
});
267+
234268
test("custom q keybinding overrides default unhandled quit behavior", async () => {
235269
const backend = new StubBackend();
236270
const app = createApp({ backend, initialState: {} });

packages/core/src/app/createApp.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -964,8 +964,13 @@ export function createApp<S>(opts: CreateAppStateOptions<S> | CreateAppRoutesOnl
964964
let stopPromise: Promise<void>;
965965
try {
966966
stopPromise = app.stop();
967-
} catch {
968-
// ignore
967+
} catch (e: unknown) {
968+
// Late events can race while a stop is already in-flight; avoid double-fatal.
969+
if (lifecycleBusy === "stop") return;
970+
enqueueFatal(
971+
"ZRUI_BACKEND_ERROR",
972+
`stop threw after unhandled quit input: ${describeThrown(e)}`,
973+
);
969974
return;
970975
}
971976
void stopPromise.catch((e: unknown) => {

0 commit comments

Comments
 (0)