@co0ontty/wand 4.72.0 → 4.72.1
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/dist/build-info.json +3 -3
- package/dist/process-manager.d.ts +0 -2
- package/dist/process-manager.js +0 -10
- package/dist/pty-terminal-state.d.ts +7 -0
- package/dist/pty-terminal-state.js +139 -22
- package/dist/server.js +0 -2
- package/dist/structured-session-manager.d.ts +10 -0
- package/dist/structured-session-manager.js +164 -15
- package/dist/terminal-daemon-client.js +0 -36
- package/dist/terminal-daemon-server.d.ts +12 -0
- package/dist/terminal-daemon-server.js +53 -7
- package/dist/terminal-host.d.ts +0 -2
- package/dist/terminal-host.js +0 -6
- package/dist/web-ui/content/scripts.js +70 -70
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/sidebar-layout.d.ts +0 -4
- package/dist/web-ui/sidebar-layout.js +2 -2
- package/dist/ws-broadcast.d.ts +14 -6
- package/dist/ws-broadcast.js +54 -40
- package/package.json +1 -1
|
@@ -16,10 +16,6 @@
|
|
|
16
16
|
* 在 `max-width: 639.98px` 归零,成对出现,不能各自为政——否则会出现
|
|
17
17
|
* 「JS 认为停靠、CSS 不补 padding」的覆盖态。
|
|
18
18
|
*/
|
|
19
|
-
/** 停靠所需的最小宽度,与 CSS `min-width: 640px` 断点一致。 */
|
|
20
|
-
export declare const SIDEBAR_PUSH_MIN_WIDTH = 640;
|
|
21
|
-
/** 触摸设备停靠所需的最小高度,与原生端 `usesWideListDetail` 一致。 */
|
|
22
|
-
export declare const SIDEBAR_PUSH_MIN_HEIGHT = 480;
|
|
23
19
|
export interface SidebarLayoutViewport {
|
|
24
20
|
readonly width: number;
|
|
25
21
|
readonly height: number;
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* 「JS 认为停靠、CSS 不补 padding」的覆盖态。
|
|
18
18
|
*/
|
|
19
19
|
/** 停靠所需的最小宽度,与 CSS `min-width: 640px` 断点一致。 */
|
|
20
|
-
|
|
20
|
+
const SIDEBAR_PUSH_MIN_WIDTH = 640;
|
|
21
21
|
/** 触摸设备停靠所需的最小高度,与原生端 `usesWideListDetail` 一致。 */
|
|
22
|
-
|
|
22
|
+
const SIDEBAR_PUSH_MIN_HEIGHT = 480;
|
|
23
23
|
/** true = 抽屉(覆盖主内容);false = 停靠(推开主内容)。 */
|
|
24
24
|
export function usesSidebarDrawer(viewport) {
|
|
25
25
|
if (!(viewport.width >= SIDEBAR_PUSH_MIN_WIDTH))
|
package/dist/ws-broadcast.d.ts
CHANGED
|
@@ -13,8 +13,6 @@ export interface WsSessionPort {
|
|
|
13
13
|
getTerminalState?(id: string): PtyTerminalSnapshot | null;
|
|
14
14
|
sendPtyInput?(id: string, input: string, shortcutKey?: string, userInput?: boolean): void;
|
|
15
15
|
resizePty?(id: string, cols: number, rows: number): void;
|
|
16
|
-
pausePtyOutput?(id: string): void;
|
|
17
|
-
resumePtyOutput?(id: string): void;
|
|
18
16
|
}
|
|
19
17
|
export declare class WsBroadcastManager {
|
|
20
18
|
private wss;
|
|
@@ -23,7 +21,6 @@ export declare class WsBroadcastManager {
|
|
|
23
21
|
private heartbeatTimer?;
|
|
24
22
|
private disposed;
|
|
25
23
|
private port?;
|
|
26
|
-
private readonly ptyPauseRefs;
|
|
27
24
|
private getCardDefaults;
|
|
28
25
|
private useHttps;
|
|
29
26
|
private authService?;
|
|
@@ -68,9 +65,20 @@ export declare class WsBroadcastManager {
|
|
|
68
65
|
*/
|
|
69
66
|
private resumeAndQueueResyncNotices;
|
|
70
67
|
private discardClient;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 客户端 ack 掉一段已渲染字节。积压回落到低水位就从「降级」恢复,并请该客户端
|
|
70
|
+
* 用终端快照重建屏幕(降级期间它的屏幕停在最后一帧,中间帧已经丢掉了)。
|
|
71
|
+
*/
|
|
72
|
+
private handlePtyAck;
|
|
73
|
+
/**
|
|
74
|
+
* 客户端积压追赶完成 → 请它用终端快照重建屏幕。
|
|
75
|
+
*
|
|
76
|
+
* 走客户端已支持的 `resync_required` 通道(客户端会回 `resync`,服务端用
|
|
77
|
+
* init 下发 terminalState 快照),不引入新协议。限流见 PTY_RESYNC_MIN_INTERVAL_MS。
|
|
78
|
+
*/
|
|
79
|
+
private requestPtyResync;
|
|
80
|
+
/** 把一条 resync 通知塞进该客户端的发送队列(不绕过队列,也不绕过背压)。 */
|
|
81
|
+
private queueResyncNotice;
|
|
74
82
|
private sendPtyError;
|
|
75
83
|
private processWsQueue;
|
|
76
84
|
}
|
package/dist/ws-broadcast.js
CHANGED
|
@@ -26,6 +26,12 @@ const HEARTBEAT_INTERVAL_MS = 20_000;
|
|
|
26
26
|
const HEARTBEAT_STALE_MS = 45_000;
|
|
27
27
|
const PTY_UNACKED_HIGH_WATER = 512 * 1024;
|
|
28
28
|
const PTY_UNACKED_LOW_WATER = 128 * 1024;
|
|
29
|
+
/**
|
|
30
|
+
* 同一个(客户端, 会话)两次「请重建屏幕」的最小间隔。慢性慢客户端追上来后会
|
|
31
|
+
* 立刻再积压,没有这个下限就会变成「重建 → 立刻积压 → 再重建」的循环,每次重建
|
|
32
|
+
* 都要下发一份终端快照。
|
|
33
|
+
*/
|
|
34
|
+
const PTY_RESYNC_MIN_INTERVAL_MS = 5_000;
|
|
29
35
|
const MAX_PTY_INPUT_CHARS = 128 * 1024;
|
|
30
36
|
function asEventData(data) {
|
|
31
37
|
return data && typeof data === "object" && !Array.isArray(data)
|
|
@@ -60,7 +66,6 @@ export class WsBroadcastManager {
|
|
|
60
66
|
heartbeatTimer;
|
|
61
67
|
disposed = false;
|
|
62
68
|
port;
|
|
63
|
-
ptyPauseRefs = new Map();
|
|
64
69
|
getCardDefaults;
|
|
65
70
|
useHttps;
|
|
66
71
|
authService;
|
|
@@ -116,7 +121,6 @@ export class WsBroadcastManager {
|
|
|
116
121
|
sendQueue: [],
|
|
117
122
|
sendInProgress: false,
|
|
118
123
|
backpressurePaused: false,
|
|
119
|
-
lastOutputBySession: new Map(),
|
|
120
124
|
outputSeqBySession: new Map(),
|
|
121
125
|
pendingResyncSessions: new Set(),
|
|
122
126
|
lastSeenAt: Date.now(),
|
|
@@ -147,16 +151,13 @@ export class WsBroadcastManager {
|
|
|
147
151
|
}
|
|
148
152
|
// 默认仍是历史的“切换当前会话”;分屏池显式用 mode:add 叠加订阅。
|
|
149
153
|
if (msg.mode !== "add") {
|
|
150
|
-
this.releaseAllPtyPauses(client);
|
|
151
154
|
client.ptySubscriptions.clear();
|
|
152
155
|
}
|
|
153
|
-
else {
|
|
154
|
-
this.releasePtyPause(client, msg.sessionId);
|
|
155
|
-
}
|
|
156
156
|
client.ptySubscriptions.set(msg.sessionId, {
|
|
157
157
|
supportsAck: msg.capabilities?.ptyAck === true,
|
|
158
158
|
unackedBytes: 0,
|
|
159
|
-
|
|
159
|
+
degraded: false,
|
|
160
|
+
lastResyncNoticeAt: 0,
|
|
160
161
|
});
|
|
161
162
|
this.flushOutput(msg.sessionId);
|
|
162
163
|
const snapshot = this.port?.getSession(msg.sessionId) ?? null;
|
|
@@ -172,7 +173,6 @@ export class WsBroadcastManager {
|
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
else if (msg.type === "unsubscribe" && msg.sessionId) {
|
|
175
|
-
this.releasePtyPause(client, msg.sessionId);
|
|
176
176
|
client.ptySubscriptions.delete(msg.sessionId);
|
|
177
177
|
}
|
|
178
178
|
else if (msg.type === "resync" && msg.sessionId) {
|
|
@@ -204,13 +204,7 @@ export class WsBroadcastManager {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
else if (msg.type === "pty_ack" && msg.sessionId && Number.isFinite(msg.bytes)) {
|
|
207
|
-
|
|
208
|
-
if (!subscription?.supportsAck)
|
|
209
|
-
return;
|
|
210
|
-
subscription.unackedBytes = Math.max(0, subscription.unackedBytes - Math.max(0, Math.floor(msg.bytes)));
|
|
211
|
-
if (subscription.paused && subscription.unackedBytes <= PTY_UNACKED_LOW_WATER) {
|
|
212
|
-
this.releasePtyPause(client, msg.sessionId);
|
|
213
|
-
}
|
|
207
|
+
this.handlePtyAck(client, msg.sessionId, msg.bytes);
|
|
214
208
|
}
|
|
215
209
|
else if (msg.type === "pong") {
|
|
216
210
|
// 应用层 pong(对我们下发的 {type:"ping"} 的响应)。lastSeenAt
|
|
@@ -436,12 +430,22 @@ export class WsBroadcastManager {
|
|
|
436
430
|
}
|
|
437
431
|
const subscription = client.ptySubscriptions.get(event.sessionId);
|
|
438
432
|
const usesPtyAckFlowControl = isRawPtyOutput && subscription?.supportsAck === true;
|
|
433
|
+
if (usesPtyAckFlowControl && subscription.degraded) {
|
|
434
|
+
// 已经积压到高水位的客户端:后面这些原始字节对它是**过期的 TUI 帧**(终端
|
|
435
|
+
// 每帧整屏重绘,中间帧没有任何保留价值),停发这些分片;等它 ack 追上来
|
|
436
|
+
// 再由 requestPtyResync 让它按终端快照重建屏幕。
|
|
437
|
+
//
|
|
438
|
+
// 历史实现在这里是「暂停整个会话的 PTY 读取」,结果是:一个客户端
|
|
439
|
+
// (手机切后台 / WebView 被节流 / 标签页休眠)停止 ack,整个会话对所有
|
|
440
|
+
// 客户端冻结,连会话日志和落库也一起停,而且只能靠那个客户端 ack 回落
|
|
441
|
+
// 才恢复;手机不回来就永久冻住。降级只影响积压的这一个客户端。
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
439
444
|
if (usesPtyAckFlowControl) {
|
|
440
445
|
outgoing = { ...outgoing, ptyBytes };
|
|
441
446
|
subscription.unackedBytes += ptyBytes;
|
|
442
|
-
if (subscription.unackedBytes >= PTY_UNACKED_HIGH_WATER)
|
|
443
|
-
|
|
444
|
-
}
|
|
447
|
+
if (subscription.unackedBytes >= PTY_UNACKED_HIGH_WATER)
|
|
448
|
+
subscription.degraded = true;
|
|
445
449
|
}
|
|
446
450
|
// Backpressure only gates new business messages. The send pump must
|
|
447
451
|
// keep draining messages that were already accepted; otherwise a queue
|
|
@@ -490,7 +494,6 @@ export class WsBroadcastManager {
|
|
|
490
494
|
}
|
|
491
495
|
}
|
|
492
496
|
discardClient(client, terminate) {
|
|
493
|
-
this.releaseAllPtyPauses(client);
|
|
494
497
|
client.sendQueue.length = 0;
|
|
495
498
|
client.pendingResyncSessions.clear();
|
|
496
499
|
client.sendInProgress = false;
|
|
@@ -503,34 +506,45 @@ export class WsBroadcastManager {
|
|
|
503
506
|
catch { /* ignore */ }
|
|
504
507
|
}
|
|
505
508
|
}
|
|
506
|
-
|
|
509
|
+
/**
|
|
510
|
+
* 客户端 ack 掉一段已渲染字节。积压回落到低水位就从「降级」恢复,并请该客户端
|
|
511
|
+
* 用终端快照重建屏幕(降级期间它的屏幕停在最后一帧,中间帧已经丢掉了)。
|
|
512
|
+
*/
|
|
513
|
+
handlePtyAck(client, sessionId, bytes) {
|
|
507
514
|
const subscription = client.ptySubscriptions.get(sessionId);
|
|
508
|
-
if (!subscription
|
|
515
|
+
if (!subscription?.supportsAck)
|
|
509
516
|
return;
|
|
510
|
-
subscription.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
517
|
+
subscription.unackedBytes = Math.max(0, subscription.unackedBytes - Math.max(0, Math.floor(bytes)));
|
|
518
|
+
if (subscription.degraded && subscription.unackedBytes <= PTY_UNACKED_LOW_WATER) {
|
|
519
|
+
subscription.degraded = false;
|
|
520
|
+
this.requestPtyResync(client, sessionId);
|
|
521
|
+
}
|
|
515
522
|
}
|
|
516
|
-
|
|
523
|
+
/**
|
|
524
|
+
* 客户端积压追赶完成 → 请它用终端快照重建屏幕。
|
|
525
|
+
*
|
|
526
|
+
* 走客户端已支持的 `resync_required` 通道(客户端会回 `resync`,服务端用
|
|
527
|
+
* init 下发 terminalState 快照),不引入新协议。限流见 PTY_RESYNC_MIN_INTERVAL_MS。
|
|
528
|
+
*/
|
|
529
|
+
requestPtyResync(client, sessionId) {
|
|
517
530
|
const subscription = client.ptySubscriptions.get(sessionId);
|
|
518
|
-
if (!subscription
|
|
531
|
+
if (!subscription)
|
|
519
532
|
return;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
this.ptyPauseRefs.set(sessionId, next);
|
|
528
|
-
}
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
if (now - subscription.lastResyncNoticeAt < PTY_RESYNC_MIN_INTERVAL_MS)
|
|
535
|
+
return;
|
|
536
|
+
subscription.lastResyncNoticeAt = now;
|
|
537
|
+
this.queueResyncNotice(client, sessionId, "pty_backlog_drop");
|
|
529
538
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
539
|
+
/** 把一条 resync 通知塞进该客户端的发送队列(不绕过队列,也不绕过背压)。 */
|
|
540
|
+
queueResyncNotice(client, sessionId, reason) {
|
|
541
|
+
if (client.ws.readyState !== WebSocket.OPEN)
|
|
542
|
+
return;
|
|
543
|
+
client.sendQueue.push(JSON.stringify({ type: "resync_required", sessionId, reason }));
|
|
544
|
+
if (client.sendQueue.length >= MAX_QUEUE_SIZE) {
|
|
545
|
+
client.backpressurePaused = true;
|
|
533
546
|
}
|
|
547
|
+
this.processWsQueue(client);
|
|
534
548
|
}
|
|
535
549
|
sendPtyError(client, sessionId, error) {
|
|
536
550
|
if (client.ws.readyState !== WebSocket.OPEN)
|