Add a parallel array container for execute handlers (codes < 0x18)
alongside the existing object. Setters populate both, the EXE
fast-path reads from the array with nullish coalescing to avoid
branching. ExecuteHandlerType updated to accept optional ident param
to allow the ?? pattern with the fallback handler.
Replaces 10 separate reset()+addParam(0) pairs across the parser with
a single resetZdm() method that inlines both operations, avoiding two
calls to a foreign object per sequence reset.
Remove intermediate byte handling from the fast-path, bail to table
lookup instead. Remove redundant per-step length checks by requiring
i + 2 < length upfront and letting the for-loop guard the rest.
Replace three !== checks (OSC_STRING, DCS_PASSTHROUGH, APC_STRING)
with one < OSC_STRING comparison. States below OSC_STRING are all
safe for the CSI fast-path; states above need the normal table path
for proper subparser abort propagation.
Address reviewer feedback:
- CSI fast-path is now a condition at the top of the codepoint loop
instead of inside CLEAR, avoiding table lookups for the common case
- EXE bytes < 0x18 get a fast-path before the CSI check so control
bytes don't pay the penalty
- PRINT loop unrolling replaced with while-based version that uses
trueish eval and spares individual length checks in the unrolled case
- Removed misplaced PRINT scan-ahead from CSI dispatch
The CSI fast-path check previously ran on every byte of input, causing
3-18% regressions on ESCAPE, OSC, DCS, and other non-CSI sequences.
Moving the check into the CLEAR action handler means it only evaluates
after the state machine already identified an ESC byte. The fast-path
body is extracted into a separate _csiFast method to keep the switch
case body small for V8 optimization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three related disposable-registration gaps that leak xterm Terminal
instances when the host application unmounts a Terminal. Evidence +
full writeup at juspay/kolu#606.
1. addon-webgl/WebglRenderer._cursorBlinkStateManager was declared
`= new MutableDisposable()` without the `this._register(...)`
wrapper that every sibling MutableDisposable in the class uses.
CursorBlinkStateManager runs a setInterval for the cursor blink
and its `dispose()` correctly clears it — but dispose() was never
called because the disposable wasn't registered. The live timer
kept the renderer (and thus the Terminal) alive past host
unmount.
2. common/TaskQueue.DebouncedIdleTask had no `dispose()` method.
Added one that clears the internal IdleTaskQueue (cancelling any
pending idle callback).
3. browser/services/RenderService._pausedResizeTask (a
DebouncedIdleTask) was not registered. Wrapped with
`this._register(...)` so RenderService.dispose propagates.
Verified in a downstream app (kolu) with a repro that toggles Focus
vs Canvas layout modes:
- Pre-fix: 24/28 disposed xterm Terminals retained past host unmount
+ forced GC.
- Post-fix: 0/28 retained.
Retention chain (before fix):
DOMTimer → ScheduledAction → V8Function
→ CursorBlinkStateManager._renderCallback → WebglRenderer
→ Terminal
Terminal.resize() already no-ops internally when dimensions haven't
changed. The addon's guard prevented resize() from being called at all,
forcing callers using ResizeObserver-based layouts to use perturbation
workarounds (resize to rows-1, then fit) that race with the observer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace `window` with `globalThis` in IdleTaskQueue export.
In Bun and Deno, `isNode` is false but `window` is undefined, causing
a ReferenceError. Using `globalThis` is safe in all JS runtimes and
makes the `isNode` guard unnecessary.
This is the only `in window` expression in code bundled into
@xterm/headless.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>