Files
NetMesh/electron/bridges/terminalFlowAck.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

233 lines
7.3 KiB
JavaScript

"use strict";
const {
FLOW_HIGH_WATER_MARK,
FLOW_LOW_WATER_MARK,
} = require("../../infrastructure/config/terminalFlowConstants.cjs");
const { logTerminalOutputPerf } = require("./terminalPerformanceDiagnostics.cjs");
function getFlowTarget(session) {
return session?.stream || session?.proc || session?.socket || session?.serialPort || null;
}
function ensureFlowState(session) {
if (!session.flowState) {
session.flowState = {
rendererPaused: false,
unackedBytes: 0,
bufferedBytes: 0,
appliedPause: false,
outputPaused: false,
firstUnackedAt: 0,
lastEmittedAt: 0,
lastAckAt: 0,
lastPerfAckLogAt: 0,
sessionId: null,
};
}
const state = session.flowState;
state.rendererPaused = Boolean(state.rendererPaused);
state.unackedBytes = Number.isFinite(state.unackedBytes) ? Math.max(0, state.unackedBytes) : 0;
state.bufferedBytes = Number.isFinite(state.bufferedBytes) ? Math.max(0, state.bufferedBytes) : 0;
state.appliedPause = Boolean(state.appliedPause);
state.firstUnackedAt = Number.isFinite(state.firstUnackedAt) ? Math.max(0, state.firstUnackedAt) : 0;
state.lastEmittedAt = Number.isFinite(state.lastEmittedAt) ? Math.max(0, state.lastEmittedAt) : 0;
state.lastAckAt = Number.isFinite(state.lastAckAt) ? Math.max(0, state.lastAckAt) : 0;
state.lastPerfAckLogAt = Number.isFinite(state.lastPerfAckLogAt) ? Math.max(0, state.lastPerfAckLogAt) : 0;
state.sessionId = typeof state.sessionId === "string" && state.sessionId ? state.sessionId : null;
if (typeof state.outputPaused !== "boolean") {
state.outputPaused = state.appliedPause && (state.rendererPaused || state.unackedBytes >= FLOW_HIGH_WATER_MARK);
}
return session.flowState;
}
function applyPause(session, target) {
try {
target.pause?.();
} catch (err) {
if (err?.code !== "EPIPE" && err?.code !== "ERR_STREAM_DESTROYED") {
console.warn("Flow control pause failed", err);
}
}
}
function applyResume(session, target) {
try {
target.resume?.();
} catch (err) {
if (err?.code !== "EPIPE" && err?.code !== "ERR_STREAM_DESTROYED") {
console.warn("Flow control resume failed", err);
}
}
}
function getFlowPerfDetails(session, extra = {}) {
const state = ensureFlowState(session);
return {
sessionId: state.sessionId || session?.id || session?.sessionId || null,
protocol: session?.protocol || session?.type || null,
rendererPaused: state.rendererPaused,
outputPaused: state.outputPaused,
appliedPause: state.appliedPause,
unackedBytes: state.unackedBytes,
bufferedBytes: state.bufferedBytes,
highWaterMark: FLOW_HIGH_WATER_MARK,
lowWaterMark: FLOW_LOW_WATER_MARK,
...extra,
};
}
function rememberSessionId(state, sessionId) {
if (typeof sessionId === "string" && sessionId) {
state.sessionId = sessionId;
}
}
// NOTE on FLOW_HIGH_WATER_MARK size (issue #1961): for SSH sessions the flow
// target is the ssh2 channel, so pause() stops the remote until resume() plus a
// full round-trip. The watermark is kept near ssh2's own 2MB channel window
// (WINDOW_THRESHOLD 1MB); a small watermark pauses/resumes dozens of times
// during a multi-MB dump (e.g. `tail -2000f big.log`) and adds ~1 RTT per cycle
// on WAN links, which is what made those dumps crawl.
function reconcileSessionFlow(session) {
if (!session) return;
const state = ensureFlowState(session);
const target = getFlowTarget(session);
if (!target) return;
if (!state.outputPaused && (state.rendererPaused || state.unackedBytes >= FLOW_HIGH_WATER_MARK)) {
state.outputPaused = true;
} else if (state.outputPaused && !state.rendererPaused && state.unackedBytes <= FLOW_LOW_WATER_MARK) {
state.outputPaused = false;
}
const pendingBytes = state.unackedBytes + state.bufferedBytes;
const shouldPause = state.outputPaused || pendingBytes >= FLOW_HIGH_WATER_MARK;
const shouldResume = !state.outputPaused && pendingBytes <= FLOW_LOW_WATER_MARK;
if (!state.appliedPause && shouldPause) {
logTerminalOutputPerf("backend-flow-pause", getFlowPerfDetails(session, { pendingBytes }));
applyPause(session, target);
state.appliedPause = true;
return;
}
if (state.appliedPause && shouldResume) {
logTerminalOutputPerf("backend-flow-resume", getFlowPerfDetails(session, { pendingBytes }));
applyResume(session, target);
state.appliedPause = false;
}
}
function setRendererFlowPaused(session, paused) {
if (!session) return;
const state = ensureFlowState(session);
state.rendererPaused = Boolean(paused);
reconcileSessionFlow(session);
}
function trackEmitted(session, bytes, sessionId) {
if (!session || !Number.isFinite(bytes) || bytes <= 0) return;
const state = ensureFlowState(session);
rememberSessionId(state, sessionId);
const now = Date.now();
if (state.unackedBytes === 0) {
state.firstUnackedAt = now;
}
state.lastEmittedAt = now;
state.unackedBytes += bytes;
reconcileSessionFlow(session);
}
function trackAck(session, bytes, sessionId) {
if (!session || !Number.isFinite(bytes) || bytes <= 0) return;
const state = ensureFlowState(session);
rememberSessionId(state, sessionId);
const now = Date.now();
const unackedBefore = state.unackedBytes;
state.unackedBytes = Math.max(0, state.unackedBytes - bytes);
state.lastAckAt = now;
const ackAgeMs = state.firstUnackedAt ? now - state.firstUnackedAt : 0;
if (
unackedBefore > 0
&& (
now - state.lastPerfAckLogAt >= 1000
|| (
state.unackedBytes === 0
&& (ackAgeMs >= 250 || unackedBefore >= 64 * 1024)
)
)
) {
state.lastPerfAckLogAt = now;
logTerminalOutputPerf("backend-flow-ack", getFlowPerfDetails(session, {
bytes,
unackedBefore,
unackedAfter: state.unackedBytes,
ackAgeMs,
sinceLastEmitMs: state.lastEmittedAt ? now - state.lastEmittedAt : null,
}));
}
if (state.unackedBytes === 0) {
state.firstUnackedAt = 0;
}
reconcileSessionFlow(session);
}
function setBufferedOutputBytes(session, bytes) {
if (!session) return;
const state = ensureFlowState(session);
state.bufferedBytes = Number.isFinite(bytes) ? Math.max(0, bytes) : 0;
reconcileSessionFlow(session);
}
function shouldAcceptSessionOutput(session) {
if (!session) return true;
const state = ensureFlowState(session);
return !state.outputPaused;
}
function isTransferSentryActive(transferSentry) {
try {
return Boolean(transferSentry?.isActive?.());
} catch {
return false;
}
}
function shouldProcessSessionOutput(session, transferSentry) {
return Boolean(session) || isTransferSentryActive(transferSentry);
}
function clearSessionFlowState(session, options = {}) {
if (!session?.flowState) return;
const target = getFlowTarget(session);
if (session.flowState.appliedPause && target && options.resume !== false) {
applyResume(session, target);
}
session.flowState = {
rendererPaused: false,
unackedBytes: 0,
bufferedBytes: 0,
appliedPause: false,
outputPaused: false,
firstUnackedAt: 0,
lastEmittedAt: 0,
lastAckAt: 0,
lastPerfAckLogAt: 0,
sessionId: null,
};
}
module.exports = {
FLOW_HIGH_WATER_MARK,
FLOW_LOW_WATER_MARK,
setRendererFlowPaused,
setBufferedOutputBytes,
trackEmitted,
trackAck,
shouldAcceptSessionOutput,
shouldProcessSessionOutput,
clearSessionFlowState,
reconcileSessionFlow,
};