@cplieger/web-terminal-engine 5.0.5 → 5.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "5.0.5",
3
+ "version": "5.0.9",
4
4
  "license": "MPL-2.0",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "5.0.5",
3
+ "version": "5.0.9",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc -p tsconfig.json",
@@ -17,15 +17,17 @@
17
17
  "@stryker-mutator/vitest-runner": "10.0.0",
18
18
  "@types/pngjs": "6.0.5",
19
19
  "@typescript/native": "npm:typescript@7.0.2",
20
+ "@vitest/browser": "4.1.11",
21
+ "@vitest/browser-playwright": "4.1.11",
20
22
  "@vitest/coverage-v8": "4.1.11",
21
23
  "esbuild": "0.28.2",
22
- "eslint": "10.8.1",
24
+ "eslint": "10.9.1",
23
25
  "fast-check": "4.9.0",
24
- "happy-dom": "20.11.2",
26
+ "playwright": "1.62.1",
25
27
  "pngjs": "7.0.0",
26
28
  "prettier": "3.9.6",
27
29
  "typescript": "npm:@typescript/typescript6@6.0.2",
28
- "typescript-eslint": "8.67.0",
30
+ "typescript-eslint": "8.68.0",
29
31
  "vitest": "4.1.11"
30
32
  },
31
33
  "overrides": {
package/src/connection.ts CHANGED
@@ -700,10 +700,6 @@ export function sendResize(): void {
700
700
  sendControl({ type: "resize", cols, rows });
701
701
  }
702
702
 
703
- // resetLedger drops a session's reliable-input accounting. Called when the
704
- // server's boot epoch changes (restart) or when the server no longer recognizes
705
- // the session -- both invalidate the local bytesSent/bytesAcked/outbox state.
706
- // The three entry points below differ only in whether the UI hears about it.
707
703
  // resetLedger drops a session's reliable-input accounting. Called whenever the
708
704
  // server no longer holds the ledger these counters were keyed to (a restart, an
709
705
  // idle-GC'd resume key, a session the server does not recognize): its
@@ -782,7 +778,21 @@ function retransmitOutbox(sock: WebSocket, upgraded: boolean, st: ResumeState):
782
778
  }
783
779
 
784
780
  function scheduleReconnect(): void {
785
- if (connState.status === "reconnecting") {
781
+ // Nothing to schedule when the state already owns a path back to a socket: a
782
+ // backoff timer is pending ("reconnecting"), or a socket exists that will
783
+ // drive its own retry from its close handler or its connect timeout
784
+ // ("connecting"/"connected"). Both callers set "disconnected" before calling
785
+ // cb.onClose(), so a consumer that reconnects from its own close hook lands
786
+ // here at "connecting" — and scheduling over it would OVERWRITE connState,
787
+ // hiding that socket from connect()'s double-call guard, which then never
788
+ // aborts it: the timer's connect() adds a second live socket with the first
789
+ // one's listeners still bound, i.e. the duplicate server connection and
790
+ // double delivery documented inside connect().
791
+ if (
792
+ connState.status === "reconnecting" ||
793
+ connState.status === "connecting" ||
794
+ connState.status === "connected"
795
+ ) {
786
796
  return;
787
797
  }
788
798
  const step = nextBackoffDelay(reconnectDelay);
package/src/render.ts CHANGED
@@ -1355,9 +1355,7 @@ function flushRender(): void {
1355
1355
  const anchor = captureReadAnchor();
1356
1356
  try {
1357
1357
  flushRenderInner();
1358
- // A clean pass means the error condition (if any) has cleared, so give a
1359
- // later transient error its full retry budget again. Any backlog reschedule
1360
- // was already issued from flushRenderInner's own end-of-body.
1358
+ // Clean pass: give a later transient error its full retry budget again.
1361
1359
  renderNoProgressStreak = 0;
1362
1360
  } catch (err) {
1363
1361
  console.error("vterm: render error", err);
@@ -1525,7 +1523,6 @@ function flushRenderInner(): void {
1525
1523
  }
1526
1524
  }
1527
1525
 
1528
- // Refresh cursor state from the window.
1529
1526
  const win = store.getWindow();
1530
1527
  const newCursorAbs = win.base + win.cursorRow;
1531
1528
  cursorAbs = newCursorAbs;
@@ -1538,11 +1535,9 @@ function flushRenderInner(): void {
1538
1535
  // resulting mode is unchanged.
1539
1536
  syncCursorBlink();
1540
1537
 
1541
- // Alt screen: render the ephemeral grid instead of the absolute buffer.
1542
- // Returning here skips the dirtyLines queueing below — safe by invariant:
1543
- // main-buffer rows never repaint while alt is active, and the alt-exit
1544
- // branch below rebuilds EVERYTHING via queueRowsViewportFirst, so any line
1545
- // dirtied during the alt session is repainted at exit.
1538
+ // Skips the dirtyLines queueing below — safe: main-buffer rows never
1539
+ // repaint while alt is active, and alt-exit rebuilds via
1540
+ // queueRowsViewportFirst, repainting any line dirtied during the alt session.
1546
1541
  if (store.isAlt()) {
1547
1542
  const altRows = store.getAltRows();
1548
1543
  renderAlt(altRows);
@@ -1570,7 +1565,6 @@ function flushRenderInner(): void {
1570
1565
  queueRowsViewportFirst();
1571
1566
  }
1572
1567
 
1573
- // Queue this frame's changed rows for building.
1574
1568
  for (const abs of ch.dirtyLines) {
1575
1569
  renderQueue.add(abs);
1576
1570
  }
package/src/store.ts CHANGED
@@ -1826,6 +1826,17 @@ export class LineStore {
1826
1826
  if (this.highest < 0) {
1827
1827
  return;
1828
1828
  }
1829
+ // No window established yet — the descriptor is still the initial base
1830
+ // 0/height 0, whose bottom row is -1, below every index a line can hold.
1831
+ // This whole pass rests on "the window's bottom row is the most recent line
1832
+ // in the terminal", which says nothing at all when there is no window: a
1833
+ // resume replays history BEFORE the screen frame describing the window, so
1834
+ // taking -1 as the tail bound strands that replay and forgets it, silently
1835
+ // (this path is not a trim, so no marker explains it). Same test `inWindow`
1836
+ // and applyScroll's alt gate use to ask whether a window exists at all.
1837
+ if (this.win.height <= 0) {
1838
+ return;
1839
+ }
1829
1840
  const windowBottom = this.win.base + this.win.height - 1;
1830
1841
  if (this.highest <= windowBottom) {
1831
1842
  return;
@@ -2008,6 +2019,18 @@ export class LineStore {
2008
2019
  }
2009
2020
 
2010
2021
  private updateWindow(msg: ScreenMessage): void {
2022
+ // A base that is not a non-negative integer is not a screen position — the
2023
+ // same validity test markUnconfirmedFrom applies to this very value — so it
2024
+ // must not redefine the window's shape, for the same reason a rows-less
2025
+ // frame must not: `base + height - 1` lands below every retained index, and
2026
+ // truncateBelowWindow then hands the WHOLE store over as stranded past the
2027
+ // live window. That is the silent wipe applyScreen's rows-less guard
2028
+ // records, reached with rows present, and just as silent (this path is not a
2029
+ // trim, so no marker explains the missing transcript). The frame's
2030
+ // addressable rows still apply through applyLine's guard 1.
2031
+ if (!Number.isInteger(msg.base) || msg.base < 0) {
2032
+ return;
2033
+ }
2011
2034
  const next: WindowState = {
2012
2035
  base: msg.base,
2013
2036
  height: msg.rows.length,