@cplieger/web-terminal-engine 2.3.4 → 2.4.0

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": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "exports": {
5
5
  ".": "./src/index.ts"
6
6
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc -p tsconfig.json",
@@ -11,6 +11,9 @@
11
11
  "devDependencies": {
12
12
  "@eslint/js": "10.0.1",
13
13
  "@playwright/test": "1.61.1",
14
+ "@stryker-mutator/core": "9.6.1",
15
+ "@stryker-mutator/typescript-checker": "9.6.1",
16
+ "@stryker-mutator/vitest-runner": "9.6.1",
14
17
  "@types/pngjs": "6.0.5",
15
18
  "@typescript/native": "npm:typescript@7.0.2",
16
19
  "@vitest/coverage-v8": "4.1.10",
package/src/connection.ts CHANGED
@@ -34,6 +34,13 @@ type ConnState =
34
34
  | { status: "connected"; sock: WebSocket; abort: AbortController }
35
35
  | { status: "reconnecting"; timer: ReturnType<typeof setTimeout>; delayMs: number };
36
36
 
37
+ // The server's application close code for "the session's child process has
38
+ // exited" (terminal/terminal.go statusProcessExited). It marks a close as
39
+ // definitive — the session cannot produce output again — as opposed to a
40
+ // transient network drop that the backoff reconnect should heal. Private
41
+ // application range (4000-4999) per RFC 6455.
42
+ const PROCESS_EXITED_CLOSE_CODE = 4001;
43
+
37
44
  let connState: ConnState = { status: "disconnected" };
38
45
  let reconnectDelay = INITIAL_DELAY_MS;
39
46
  let lastSentCols = 0;
@@ -179,6 +186,18 @@ export interface Callbacks {
179
186
  onClose(): void;
180
187
  onConnecting?(): void;
181
188
  onOutboxFull?(): void;
189
+ /** Fired instead of onClose when the server closes with the
190
+ * process-exited application close code (4001): the session's child
191
+ * process has ended, so the close is DEFINITIVE, not a transient drop.
192
+ * When this callback is wired, the module does NOT auto-reconnect that
193
+ * socket — reconnecting a dead session just replays its final screen and
194
+ * earns another 4001 (the endless "Reconnecting…" flash) — and does not
195
+ * call onClose for it. The consumer decides what "ended" looks like
196
+ * (banner, tab state) and may still reconnect explicitly (setSession /
197
+ * reconnectNow) to re-view the final screen. When absent, every close
198
+ * keeps the legacy transient treatment (onClose + backoff reconnect), so
199
+ * existing consumers are unaffected until they opt in. */
200
+ onProcessExit?(): void;
182
201
  /** Fired when the server's boot-epoch in resumeAck differs from the
183
202
  * one observed on a previous connection — i.e. the server has
184
203
  * restarted. By the time this fires, the connection module has
@@ -696,7 +715,7 @@ export function connect(): void {
696
715
 
697
716
  sock.addEventListener(
698
717
  "close",
699
- () => {
718
+ (ev: CloseEvent) => {
700
719
  // Only the active sock's close should drive reconnect logic; an
701
720
  // already-superseded sock has been aborted and this listener
702
721
  // wouldn't fire (signal removes it). The check stays as a belt-
@@ -709,6 +728,16 @@ export function connect(): void {
709
728
  }
710
729
  stopHeartbeat();
711
730
  connState = { status: "disconnected" };
731
+ // A process-exited close (4001) is definitive: the child is gone, so a
732
+ // backoff reconnect can only replay the final screen and collect another
733
+ // 4001 — an endless, pointless churn that reads as a flapping
734
+ // "Reconnecting…" banner. Route it to onProcessExit (no reconnect, no
735
+ // onClose) when the consumer wired it; without the callback, keep the
736
+ // legacy transient treatment so existing consumers see no change.
737
+ if (ev.code === PROCESS_EXITED_CLOSE_CODE && cb?.onProcessExit) {
738
+ cb.onProcessExit();
739
+ return;
740
+ }
712
741
  cb?.onClose();
713
742
  scheduleReconnect();
714
743
  },