@cplieger/web-terminal-engine 1.3.2 → 1.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 +1 -1
- package/package.json +10 -5
- package/src/connection.ts +1 -0
- package/src/modes.ts +11 -0
- package/src/mouse.ts +26 -9
- package/src/render.ts +6 -0
- package/src/store.ts +71 -0
- package/src/types.ts +24 -5
- package/src/wire-binary.ts +12 -0
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsgo -p tsconfig.json",
|
|
7
7
|
"typecheck:tests": "tsgo -p tsconfig.test.json",
|
|
8
|
-
"test": "vitest --run"
|
|
8
|
+
"test": "vitest --run",
|
|
9
|
+
"test:e2e": "playwright test"
|
|
9
10
|
},
|
|
10
11
|
"devDependencies": {
|
|
11
12
|
"@eslint/js": "10.0.1",
|
|
13
|
+
"@playwright/test": "1.61.1",
|
|
14
|
+
"@types/pngjs": "6.0.5",
|
|
12
15
|
"@vitest/coverage-v8": "4.1.9",
|
|
16
|
+
"esbuild": "0.28.1",
|
|
13
17
|
"eslint": "10.6.0",
|
|
14
18
|
"fast-check": "4.8.0",
|
|
15
19
|
"happy-dom": "20.10.6",
|
|
16
|
-
"
|
|
20
|
+
"pngjs": "7.0.0",
|
|
21
|
+
"prettier": "3.9.3",
|
|
17
22
|
"typescript": "6.0.3",
|
|
18
|
-
"typescript-eslint": "8.62.
|
|
23
|
+
"typescript-eslint": "8.62.1",
|
|
19
24
|
"vitest": "4.1.9"
|
|
20
25
|
},
|
|
21
26
|
"description": "Browser terminal engine: VT500 screen buffer, DOM renderer, and binary wire protocol",
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
"files": [
|
|
36
41
|
"src/",
|
|
37
42
|
"!src/**/*.test.ts",
|
|
38
|
-
"!src/**/
|
|
43
|
+
"!src/**/test-helpers",
|
|
39
44
|
"!src/**/fc-strict-setup.ts",
|
|
40
45
|
"jsr.json"
|
|
41
46
|
]
|
package/src/connection.ts
CHANGED
package/src/modes.ts
CHANGED
|
@@ -20,6 +20,7 @@ let applicationCursor = false;
|
|
|
20
20
|
let applicationKeypad = false;
|
|
21
21
|
let mouseMode = 0; // 0=off, 1000=normal, 1002=button-event, 1003=any-event
|
|
22
22
|
let mouseSGR = false;
|
|
23
|
+
let mousePixels = false; // DEC 1016: report pixel coords instead of cell coords
|
|
23
24
|
let focusReporting = false;
|
|
24
25
|
let reverseVideo = false;
|
|
25
26
|
|
|
@@ -36,6 +37,7 @@ export function setModes(
|
|
|
36
37
|
mMode?: number,
|
|
37
38
|
appKeypad?: boolean,
|
|
38
39
|
revVideo?: boolean,
|
|
40
|
+
mPixels?: boolean,
|
|
39
41
|
): void {
|
|
40
42
|
bracketedPaste = bracketed;
|
|
41
43
|
applicationCursor = appCursor;
|
|
@@ -54,6 +56,9 @@ export function setModes(
|
|
|
54
56
|
if (revVideo !== undefined) {
|
|
55
57
|
reverseVideo = revVideo;
|
|
56
58
|
}
|
|
59
|
+
if (mPixels !== undefined) {
|
|
60
|
+
mousePixels = mPixels;
|
|
61
|
+
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/** True when the server has DEC 2004 (bracketed paste) enabled. */
|
|
@@ -79,6 +84,12 @@ export function isMouseSGR(): boolean {
|
|
|
79
84
|
return mouseSGR;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
/** True when the server has DEC 1016 (SGR-pixels mouse) enabled: mouse reports
|
|
88
|
+
* carry pixel coordinates instead of cell coordinates. */
|
|
89
|
+
export function isMousePixels(): boolean {
|
|
90
|
+
return mousePixels;
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
/** True when the server has DEC 1004 (focus event reporting) enabled. */
|
|
83
94
|
export function isFocusReporting(): boolean {
|
|
84
95
|
return focusReporting;
|
package/src/mouse.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// - urxvt encoding (mode 1015): not supported.
|
|
15
15
|
// - DEFAULT encoding (raw bytes): not supported; only SGR 1006.
|
|
16
16
|
|
|
17
|
-
import { getMouseMode, isMouseSGR, isFocusReporting } from "./modes.js";
|
|
17
|
+
import { getMouseMode, isMouseSGR, isMousePixels, isFocusReporting } from "./modes.js";
|
|
18
18
|
|
|
19
19
|
const ESC = "\x1b";
|
|
20
20
|
|
|
@@ -59,12 +59,14 @@ export function encodeSGR(code: number, col: number, row: number, release: boole
|
|
|
59
59
|
|
|
60
60
|
/** Map DOM button index to xterm button number. */
|
|
61
61
|
function domButtonToXterm(button: number): number {
|
|
62
|
-
// DOM: 0=left, 1=middle, 2=right
|
|
63
|
-
// xterm: 0
|
|
62
|
+
// DOM MouseEvent.button: 0=left, 1=middle, 2=right, 3=back, 4=forward.
|
|
63
|
+
// xterm: 0/1/2 for left/middle/right; the "additional buttons" (X11 8-11,
|
|
64
|
+
// reached via DOM back/forward) use the +128 extended-button encoding, so
|
|
65
|
+
// back (DOM 3 = X11 8) → 128 and forward (DOM 4 = X11 9) → 129.
|
|
64
66
|
if (button <= 2) {
|
|
65
67
|
return button;
|
|
66
68
|
}
|
|
67
|
-
return
|
|
69
|
+
return 128 + (button - 3);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -100,12 +102,25 @@ export function init(h: MouseInputHandler): void {
|
|
|
100
102
|
el.addEventListener("focusout", onFocusOut);
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
// pixelToCell returns the coordinate pair to report for a mouse event. Normally
|
|
106
|
+
// that's the 1-based cell column/row; under DEC 1016 (SGR-pixels) it's instead
|
|
107
|
+
// the 1-based pixel offset within the terminal element (same SGR grammar, so
|
|
108
|
+
// encodeSGR is reused unchanged). The {col,row} field names carry whichever the
|
|
109
|
+
// active mode reports.
|
|
103
110
|
function pixelToCell(e: MouseEvent): { col: number; row: number } | null {
|
|
104
111
|
if (!handler) {
|
|
105
112
|
return null;
|
|
106
113
|
}
|
|
107
114
|
const el = handler.termElement();
|
|
108
115
|
const rect = el.getBoundingClientRect();
|
|
116
|
+
if (isMousePixels()) {
|
|
117
|
+
const px = Math.round(e.clientX - rect.left);
|
|
118
|
+
const py = Math.round(e.clientY - rect.top);
|
|
119
|
+
if (px < 0 || py < 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return { col: px + 1, row: py + 1 }; // 1-based pixel offsets
|
|
123
|
+
}
|
|
109
124
|
const { width, height } = handler.cellSize();
|
|
110
125
|
if (width <= 0 || height <= 0) {
|
|
111
126
|
return null;
|
|
@@ -120,7 +135,7 @@ function pixelToCell(e: MouseEvent): { col: number; row: number } | null {
|
|
|
120
135
|
|
|
121
136
|
function onMouseDown(e: MouseEvent): void {
|
|
122
137
|
const mode = getMouseMode();
|
|
123
|
-
if (mode === 0 || !isMouseSGR() || !handler) {
|
|
138
|
+
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
124
139
|
return;
|
|
125
140
|
}
|
|
126
141
|
const pos = pixelToCell(e);
|
|
@@ -135,7 +150,7 @@ function onMouseDown(e: MouseEvent): void {
|
|
|
135
150
|
|
|
136
151
|
function onMouseUp(e: MouseEvent): void {
|
|
137
152
|
const mode = getMouseMode();
|
|
138
|
-
if (mode === 0 || !isMouseSGR() || !handler) {
|
|
153
|
+
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
139
154
|
return;
|
|
140
155
|
}
|
|
141
156
|
const pos = pixelToCell(e);
|
|
@@ -151,7 +166,7 @@ function onMouseUp(e: MouseEvent): void {
|
|
|
151
166
|
|
|
152
167
|
function onMouseMove(e: MouseEvent): void {
|
|
153
168
|
const mode = getMouseMode();
|
|
154
|
-
if (mode === 0 || !isMouseSGR() || !handler) {
|
|
169
|
+
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
155
170
|
return;
|
|
156
171
|
}
|
|
157
172
|
// mode 1000: no motion events
|
|
@@ -167,14 +182,16 @@ function onMouseMove(e: MouseEvent): void {
|
|
|
167
182
|
if (!pos) {
|
|
168
183
|
return;
|
|
169
184
|
}
|
|
170
|
-
|
|
185
|
+
// No button held (bare motion in mode 1003) reports xterm's "no button"
|
|
186
|
+
// code 3, not 0 — code 0 would be read as a left-button drag during hover.
|
|
187
|
+
const btn = e.buttons ? (e.buttons & 1 ? 0 : e.buttons & 4 ? 1 : e.buttons & 2 ? 2 : 0) : 3;
|
|
171
188
|
const code = buttonCode(btn, true, e.shiftKey, e.altKey, e.ctrlKey);
|
|
172
189
|
handler.send(encodeSGR(code, pos.col, pos.row, false));
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
function onWheel(e: WheelEvent): void {
|
|
176
193
|
const mode = getMouseMode();
|
|
177
|
-
if (mode === 0 || !isMouseSGR() || !handler) {
|
|
194
|
+
if (mode === 0 || !(isMouseSGR() || isMousePixels()) || !handler) {
|
|
178
195
|
return;
|
|
179
196
|
}
|
|
180
197
|
const pos = pixelToCell(e);
|
package/src/render.ts
CHANGED
|
@@ -408,6 +408,12 @@ function buildRowSpans(runs: WireRun[], cursorAt: number): (HTMLSpanElement | HT
|
|
|
408
408
|
prev.style.letterSpacing = `${cellWidth * 2 - w}px`;
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
+
// The spacer occupies the wide char's second cell, so it advances the
|
|
412
|
+
// column exactly like a printed cell. The engine reports cursor_col in
|
|
413
|
+
// true cell coordinates (a wide glyph moves curX by 2), so col must
|
|
414
|
+
// count this cell too — otherwise a visible cursor positioned after a
|
|
415
|
+
// wide char lands one cell too far right per preceding wide char.
|
|
416
|
+
col++;
|
|
411
417
|
continue;
|
|
412
418
|
}
|
|
413
419
|
if (col === cursorAt) {
|
package/src/store.ts
CHANGED
|
@@ -183,6 +183,12 @@ export class LineStore {
|
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
this.exitAltIfNeeded();
|
|
186
|
+
if (msg.scrollbackCleared) {
|
|
187
|
+
// ED3 (erase scrollback): the app cleared its saved lines (kiro-cli does
|
|
188
|
+
// this on every resize redraw). Drop our matching history below the live
|
|
189
|
+
// window so the previous frame doesn't accumulate beneath the new one.
|
|
190
|
+
this.dropHistoryBelow(msg.base);
|
|
191
|
+
}
|
|
186
192
|
this.updateWindow(msg);
|
|
187
193
|
for (const y of msg.changed) {
|
|
188
194
|
const row = msg.rows[y];
|
|
@@ -190,6 +196,7 @@ export class LineStore {
|
|
|
190
196
|
this.applyLine(msg.base + y, row);
|
|
191
197
|
}
|
|
192
198
|
}
|
|
199
|
+
this.truncateBelowWindow();
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
/** Apply a decoded scroll/history frame: each line at firstIndex + i. */
|
|
@@ -297,6 +304,70 @@ export class LineStore {
|
|
|
297
304
|
this.enforceCap();
|
|
298
305
|
}
|
|
299
306
|
|
|
307
|
+
// dropHistoryBelow evicts all retained lines with an absolute index below
|
|
308
|
+
// `base` — the scrollback history under the live window. Called when the
|
|
309
|
+
// server signals ED3 (erase scrollback): an inline TUI (kiro-cli) clears its
|
|
310
|
+
// saved lines on every resize redraw, so the client drops its matching
|
|
311
|
+
// history to stop the previous frame accumulating beneath the new one. Window
|
|
312
|
+
// rows (>= base) are kept and refreshed by the frame carrying the signal.
|
|
313
|
+
// everEvictedThrough is left untouched: the app discarded the lines
|
|
314
|
+
// deliberately (not a cap trim), so no "earlier output trimmed" marker fits.
|
|
315
|
+
private dropHistoryBelow(base: number): void {
|
|
316
|
+
if (this.oldest < 0 || this.oldest >= base) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
for (let abs = this.oldest; abs < base; abs++) {
|
|
320
|
+
if (this.lines.delete(abs)) {
|
|
321
|
+
this.evicted.add(abs);
|
|
322
|
+
this.dirty.delete(abs);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (base > this.highest) {
|
|
326
|
+
this.oldest = -1;
|
|
327
|
+
this.highest = -1;
|
|
328
|
+
} else {
|
|
329
|
+
this.oldest = base;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// truncateBelowWindow evicts every retained line past the window's bottom row.
|
|
334
|
+
// The window's bottom row is the most recent line in the terminal, so no line
|
|
335
|
+
// can exist at a higher absolute index. A resize that SHRINKS the screen (the
|
|
336
|
+
// iOS soft keyboard opening) moves the window bottom up while the taller
|
|
337
|
+
// screen's former bottom rows stay in the store at higher indices — stranded
|
|
338
|
+
// below the live window. Cap eviction only trims the top/oldest, so nothing
|
|
339
|
+
// removes them: they linger as phantom blank rows beneath the real content, an
|
|
340
|
+
// "empty" region the user can scroll into and the reason the content never
|
|
341
|
+
// appears to shrink to fit on a short viewport. Evicting them keeps the tail
|
|
342
|
+
// invariant (highest === window bottom); it is the tail-side complement to the
|
|
343
|
+
// top-side cap eviction.
|
|
344
|
+
private truncateBelowWindow(): void {
|
|
345
|
+
if (this.highest < 0) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const windowBottom = this.win.base + this.win.height - 1;
|
|
349
|
+
if (this.highest <= windowBottom) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
for (let abs = windowBottom + 1; abs <= this.highest; abs++) {
|
|
353
|
+
if (this.lines.delete(abs)) {
|
|
354
|
+
this.evicted.add(abs);
|
|
355
|
+
this.dirty.delete(abs);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// New highest is the greatest retained index at or below the window bottom.
|
|
359
|
+
let h = windowBottom;
|
|
360
|
+
while (h >= this.oldest && !this.lines.has(h)) {
|
|
361
|
+
h--;
|
|
362
|
+
}
|
|
363
|
+
if (h >= this.oldest) {
|
|
364
|
+
this.highest = h;
|
|
365
|
+
} else {
|
|
366
|
+
this.highest = -1;
|
|
367
|
+
this.oldest = -1;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
300
371
|
private enforceCap(): void {
|
|
301
372
|
while (this.lines.size > this.maxLines && this.oldest >= 0 && this.oldest < this.highest) {
|
|
302
373
|
const victim = this.oldest;
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,13 @@ export interface ScreenMessage {
|
|
|
59
59
|
cursorBlink?: boolean;
|
|
60
60
|
/** True if the server emitted a BEL since the last screen update. */
|
|
61
61
|
bell?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* True when the app issued ED3 (`CSI 3 J`, "erase saved lines") this frame:
|
|
64
|
+
* the client drops its scrollback history (all lines below `base`), matching
|
|
65
|
+
* a real terminal. Inline TUIs (kiro-cli) emit ED3 on every resize redraw to
|
|
66
|
+
* discard the previous frame; honoring it stops stale frames accumulating.
|
|
67
|
+
*/
|
|
68
|
+
scrollbackCleared?: boolean;
|
|
62
69
|
/** Server-confirmed bytesReceived for the input ACK protocol. */
|
|
63
70
|
inputAck?: number;
|
|
64
71
|
}
|
|
@@ -122,6 +129,8 @@ export interface ModesMessage {
|
|
|
122
129
|
focusReporting: boolean;
|
|
123
130
|
/** DEC 5: screen is in reverse-video mode. */
|
|
124
131
|
reverseVideo: boolean;
|
|
132
|
+
/** DEC 1016: mouse reports carry pixel coordinates instead of cell coords. */
|
|
133
|
+
mousePixels: boolean;
|
|
125
134
|
/** Mouse tracking: 0=off, 1000=normal, 1002=button-event, 1003=any-event. */
|
|
126
135
|
mouseMode: number;
|
|
127
136
|
/** Server-confirmed bytesReceived for the input ACK protocol. */
|
|
@@ -141,13 +150,23 @@ export interface TitleMessage {
|
|
|
141
150
|
inputAck?: number;
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Clipboard text an app copied via OSC 52 (`OSC 52 ; c ; <base64> ST`). The
|
|
155
|
+
* client writes it to the system clipboard. Set-only: the query form is
|
|
156
|
+
* refused server-side, so this message never carries a read request.
|
|
157
|
+
*/
|
|
158
|
+
export interface ClipboardMessage {
|
|
159
|
+
/** Discriminator — always `"clipboard"`. */
|
|
160
|
+
type: "clipboard";
|
|
161
|
+
/** Text to place on the system clipboard. */
|
|
162
|
+
text: string;
|
|
163
|
+
/** Server-confirmed bytesReceived for the input ACK protocol. */
|
|
164
|
+
inputAck?: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
/** Discriminated union of all messages the server can send to the client. */
|
|
145
168
|
export type ServerMessage =
|
|
146
|
-
|
|
|
147
|
-
| ScrollMessage
|
|
148
|
-
| ResumeAckMessage
|
|
149
|
-
| ModesMessage
|
|
150
|
-
| TitleMessage;
|
|
169
|
+
ScreenMessage | ScrollMessage | ResumeAckMessage | ModesMessage | TitleMessage | ClipboardMessage;
|
|
151
170
|
|
|
152
171
|
/**
|
|
153
172
|
* Discriminated union of all control messages the client can send to the
|
package/src/wire-binary.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ResumeAckMessage,
|
|
11
11
|
ModesMessage,
|
|
12
12
|
TitleMessage,
|
|
13
|
+
ClipboardMessage,
|
|
13
14
|
ServerMessage,
|
|
14
15
|
} from "./types.js";
|
|
15
16
|
|
|
@@ -20,6 +21,7 @@ const MSG_RESUME_ACK = 2;
|
|
|
20
21
|
const MSG_MODES = 3;
|
|
21
22
|
const MSG_TITLE = 4;
|
|
22
23
|
const MSG_PONG = 5;
|
|
24
|
+
const MSG_CLIPBOARD = 6;
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Wire protocol version. Sent by the client in the `resume` control message so
|
|
@@ -35,6 +37,7 @@ const MODE_FLAG_MOUSE_SGR = 4;
|
|
|
35
37
|
const MODE_FLAG_FOCUS_REPORTING = 8;
|
|
36
38
|
const MODE_FLAG_APP_KEYPAD = 16;
|
|
37
39
|
const MODE_FLAG_REVERSE_VIDEO = 32;
|
|
40
|
+
const MODE_FLAG_MOUSE_PIXELS = 64;
|
|
38
41
|
|
|
39
42
|
class Cursor {
|
|
40
43
|
view: DataView;
|
|
@@ -177,6 +180,7 @@ function decodeWireBinaryInner(buf: ArrayBuffer): ServerMessage | null {
|
|
|
177
180
|
const bell = (cursorFlags & 2) !== 0;
|
|
178
181
|
const cursorBlink = (cursorFlags & 4) !== 0;
|
|
179
182
|
const altActive = (cursorFlags & 8) !== 0;
|
|
183
|
+
const scrollbackCleared = (cursorFlags & 16) !== 0;
|
|
180
184
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- pre-allocated array filled below
|
|
181
185
|
const rows: WireRun[][] = new Array(screenHeight);
|
|
182
186
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- pre-allocated array filled below
|
|
@@ -197,6 +201,7 @@ function decodeWireBinaryInner(buf: ArrayBuffer): ServerMessage | null {
|
|
|
197
201
|
cursorHidden,
|
|
198
202
|
cursorBlink,
|
|
199
203
|
bell,
|
|
204
|
+
scrollbackCleared,
|
|
200
205
|
inputAck,
|
|
201
206
|
};
|
|
202
207
|
return msg;
|
|
@@ -223,6 +228,7 @@ function decodeWireBinaryInner(buf: ArrayBuffer): ServerMessage | null {
|
|
|
223
228
|
mouseSGR: (flags & MODE_FLAG_MOUSE_SGR) !== 0,
|
|
224
229
|
focusReporting: (flags & MODE_FLAG_FOCUS_REPORTING) !== 0,
|
|
225
230
|
reverseVideo: (flags & MODE_FLAG_REVERSE_VIDEO) !== 0,
|
|
231
|
+
mousePixels: (flags & MODE_FLAG_MOUSE_PIXELS) !== 0,
|
|
226
232
|
mouseMode,
|
|
227
233
|
inputAck,
|
|
228
234
|
};
|
|
@@ -234,6 +240,12 @@ function decodeWireBinaryInner(buf: ArrayBuffer): ServerMessage | null {
|
|
|
234
240
|
const msg: TitleMessage = { type: "title", title, inputAck };
|
|
235
241
|
return msg;
|
|
236
242
|
}
|
|
243
|
+
if (msgType === MSG_CLIPBOARD) {
|
|
244
|
+
const textLen = c.u16();
|
|
245
|
+
const text = c.utf8(textLen);
|
|
246
|
+
const msg: ClipboardMessage = { type: "clipboard", text, inputAck };
|
|
247
|
+
return msg;
|
|
248
|
+
}
|
|
237
249
|
if (msgType === MSG_PONG) {
|
|
238
250
|
// Liveness pong: no payload to deliver. The message listener already
|
|
239
251
|
// refreshed the activity clock on arrival, so there is nothing to do
|