fix: address PR review — flush hang on cancel, BSU/ESU incremental wrapping

- scheduler.cancel() now resolves a pending flush() waiter, preventing
  waitUntilRenderFlush() from hanging when the app unmounts mid-flush
- BSU/ESU now wrap the actual stream writes instead of being embedded in
  the frame string, so synchronization survives log-update's incremental
  line diffing (matches Ink ink.tsx:1059-1097). Normal branch guarded by
  willRender() to avoid empty synchronized-update pairs on unchanged frames

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-28 10:20:11 +08:00
parent 1ffb847a65
commit 1eb30703f9
2 changed files with 20 additions and 5 deletions
+15 -5
View File
@@ -490,19 +490,29 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
});
if (shouldClear) {
// Direct write: clearTerminal + accumulated static + raw output
const content = ansiEscapes.clearTerminal + frameState.fullStaticOutput + output;
stdout.write(synchronize ? bsu + content + esu : content);
// Direct write: clearTerminal + accumulated static + raw output.
// BSU/ESU wrap the actual stream writes (not embedded in the frame
// string) so synchronization survives log-update's line diffing.
if (synchronize) stdout.write(bsu);
stdout.write(ansiEscapes.clearTerminal + frameState.fullStaticOutput + output);
// Sync log-update state so next render computes correct erase
writer.sync(outputToRender);
if (synchronize) stdout.write(esu);
} else if (hasStaticOutput) {
// Clear frame -> write static -> re-render frame via log-update
if (synchronize) stdout.write(bsu);
writer.clear();
stdout.write(staticOutput);
writer.write(synchronize ? outputToRender + esu : outputToRender);
writer.write(outputToRender);
if (synchronize) stdout.write(esu);
} else if (synchronize && writer.willRender(outputToRender)) {
// Only emit BSU/ESU when log-update will actually write, so unchanged
// frames don't produce empty synchronized-update pairs.
stdout.write(bsu);
writer.write(outputToRender);
stdout.write(esu);
} else {
writer.write(synchronize ? bsu + outputToRender + esu : outputToRender);
writer.write(outputToRender);
}
frameState.lastOutput = output;
+5
View File
@@ -98,6 +98,11 @@ export function createCommitScheduler(
}
hasPendingFlag = false;
scheduled = false;
// Resolve any waiter blocked on flush() — the pending commit will never
// fire now, so leaving resolveFlush unsettled would hang waitUntilRenderFlush.
const r = resolveFlush;
resolveFlush = null;
r?.();
}
return { schedule, flush, hasPending, cancel };