first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
/* global module, setTimeout, clearTimeout */
function utf8ByteLength(text) {
const value = String(text || "");
let total = 0;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code <= 0x7f) {
total += 1;
continue;
}
if (code <= 0x7ff) {
total += 2;
continue;
}
if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
const next = value.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
total += 4;
index += 1;
continue;
}
}
total += 3;
}
return total;
}
/**
* 统一规范终端渲染选项:
* 目前页面层只暴露 `sendResize`,后续若增加其他布尔开关,也应在这里集中合并。
*/
function mergeTerminalRenderOptions(base, incoming) {
const previous = base && typeof base === "object" ? base : {};
const next = incoming && typeof incoming === "object" ? incoming : {};
return {
sendResize: !!(previous.sendResize || next.sendResize)
};
}
function normalizeStdoutSample(sample) {
const source = sample && typeof sample === "object" ? sample : {};
const text = String(source.text || "");
return {
text,
rawBytes: utf8ByteLength(text),
appendStartedAt: Number(source.appendStartedAt) || 0,
visibleBytes: Number(source.visibleBytes) || 0,
visibleFrameCount: Number(source.visibleFrameCount) || 0
};
}
function createPendingRequest(now) {
return {
options: mergeTerminalRenderOptions(),
callbacks: [],
stdoutSamples: [],
requestedAt: now()
};
}
/**
* 终端渲染调度器职责只有两件事:
* 1. stdout 高频输出时,按一个很短的窗口合并成一轮真实渲染;
* 2. 若上一轮渲染尚未完成,只保留“下一轮需要再跑一次”的脏标记,避免把 scroll-view 刷新堆成风暴。
*
* 注意:
* - 调度器不负责真正的布局/overlay 逻辑,页面层通过 `runRender` 注入;
* - stdout 合批会把多段文本交给页面层一次性处理,再统一进入 layout/overlay。
*/
function createTerminalRenderScheduler(options) {
const config = options && typeof options === "object" ? options : {};
const now = typeof config.now === "function" ? config.now : () => Date.now();
const setTimer = typeof config.setTimer === "function" ? config.setTimer : setTimeout;
const clearTimer = typeof config.clearTimer === "function" ? config.clearTimer : clearTimeout;
const onError = typeof config.onError === "function" ? config.onError : null;
const batchWindowMs =
Number.isFinite(Number(config.batchWindowMs)) && Number(config.batchWindowMs) >= 0
? Math.round(Number(config.batchWindowMs))
: 16;
const runRender = typeof config.runRender === "function" ? config.runRender : null;
if (!runRender) {
throw new TypeError("terminal render scheduler 缺少 runRender");
}
let inFlight = false;
let stdoutTimer = null;
let pendingRequest = null;
let activeRequest = null;
function reportError(error) {
if (onError) {
onError(error);
return;
}
throw error;
}
function ensurePendingRequest() {
if (!pendingRequest) {
pendingRequest = createPendingRequest(now);
}
return pendingRequest;
}
function clearStdoutTimer() {
if (!stdoutTimer) return;
clearTimer(stdoutTimer);
stdoutTimer = null;
}
function finalizeRequestCallbacks(request, result) {
const callbacks = Array.isArray(request && request.callbacks) ? request.callbacks.slice() : [];
callbacks.forEach((callback) => {
if (typeof callback !== "function") return;
try {
callback(result, request);
} catch (error) {
reportError(error);
}
});
}
function buildRequestSnapshot(request, timestamp) {
if (!request || typeof request !== "object") {
return null;
}
const samples = Array.isArray(request.stdoutSamples) ? request.stdoutSamples : [];
return {
reason: String(request.reason || ""),
requestedAt: Number(request.requestedAt) || 0,
startedAt: Number(request.startedAt) || 0,
waitMs:
timestamp && Number(request.requestedAt)
? Math.max(0, (Number(request.startedAt) || Number(timestamp)) - Number(request.requestedAt))
: 0,
ageMs:
timestamp && Number(request.startedAt)
? Math.max(0, Number(timestamp) - Number(request.startedAt))
: 0,
stdoutSampleCount: samples.length,
stdoutRawBytes: samples.reduce(
(sum, sample) => sum + Math.max(0, Number(sample && sample.rawBytes) || 0),
0
),
stdoutVisibleBytes: samples.reduce(
(sum, sample) => sum + Math.max(0, Number(sample && sample.visibleBytes) || 0),
0
),
callbackCount: Array.isArray(request.callbacks) ? request.callbacks.length : 0
};
}
function startNextRun(reason) {
if (inFlight || !pendingRequest) {
return false;
}
clearStdoutTimer();
const request = pendingRequest;
pendingRequest = null;
request.reason = String(reason || "");
request.startedAt = now();
inFlight = true;
activeRequest = request;
try {
runRender(request, (result) => {
request.completedAt = now();
inFlight = false;
activeRequest = null;
finalizeRequestCallbacks(request, result);
if (pendingRequest) {
startNextRun(pendingRequest.stdoutSamples.length > 0 ? "pending_stdout" : "pending_immediate");
}
});
} catch (error) {
inFlight = false;
activeRequest = null;
reportError(error);
if (pendingRequest) {
startNextRun("recover_after_error");
}
}
return true;
}
function scheduleStdoutFlush() {
if (inFlight || stdoutTimer || !pendingRequest) {
return;
}
if (batchWindowMs <= 0) {
startNextRun("stdout_immediate");
return;
}
stdoutTimer = setTimer(() => {
stdoutTimer = null;
startNextRun("stdout_batch");
}, batchWindowMs);
}
return {
requestImmediate(options, callback) {
const request = ensurePendingRequest();
request.options = mergeTerminalRenderOptions(request.options, options);
if (typeof callback === "function") {
request.callbacks.push(callback);
}
clearStdoutTimer();
if (!inFlight) {
startNextRun("immediate");
}
},
requestStdout(sample) {
const request = ensurePendingRequest();
request.stdoutSamples.push(normalizeStdoutSample(sample));
scheduleStdoutFlush();
},
clearPending() {
clearStdoutTimer();
pendingRequest = null;
},
getSnapshot() {
const timestamp = now();
return {
inFlight,
pending: buildRequestSnapshot(pendingRequest, timestamp),
active: buildRequestSnapshot(activeRequest, timestamp)
};
}
};
}
module.exports = {
createTerminalRenderScheduler,
mergeTerminalRenderOptions
};