Files
NetMesh/electron/preload/terminalDataBacklog.cjs

140 lines
4.2 KiB
JavaScript
Raw Normal View History

"use strict";
const { mergeTerminalDataMeta } = require("./terminalDataMeta.cjs");
function hasPluginPipelineIngress(meta) {
return Number.isFinite(meta?.pluginPipelineIngressBytes)
&& Number(meta.pluginPipelineIngressBytes) > 0;
}
function hasPluginPipelineIngressMarker(meta) {
return Number.isFinite(meta?.pluginPipelineIngressBytes)
&& Number(meta.pluginPipelineIngressBytes) >= 0;
}
function createTerminalDataBacklog(options = {}) {
const maxBytesPerSession = options.maxBytesPerSession ?? 64 * 1024;
const pendingBySession = new Map();
function trimToLimit(value) {
if (value.length <= maxBytesPerSession) return value;
return value.slice(value.length - maxBytesPerSession);
}
function append(sessionId, data, meta) {
if (!sessionId || (!data && !hasPluginPipelineIngressMarker(meta))) return;
const previous = pendingBySession.get(sessionId) || { data: "", meta: undefined };
const nextData = trimToLimit(previous.data + data);
const preserveTerminalPerf = previous.data.length === 0 && nextData === data;
let previousMeta = previous.meta;
let nextChunkMeta = meta;
const previousHasIngress = Number.isFinite(previousMeta?.pluginPipelineIngressBytes);
const nextChunkHasIngress = Number.isFinite(nextChunkMeta?.pluginPipelineIngressBytes);
// Once one merged chunk carries explicit original-ingress accounting, the
// metadata must cover every raw flow unit in the same replay entry. Flow
// control is intentionally charged in JavaScript string length, not UTF-8
// bytes, throughout the terminal renderer/worker path. Otherwise a
// processed chunk followed or preceded by ordinary output would cause the
// renderer to acknowledge only the annotated subset.
if (previousHasIngress && !nextChunkHasIngress && data) {
nextChunkMeta = {
...(nextChunkMeta || {}),
pluginPipelineIngressBytes: data.length,
};
} else if (!previousHasIngress && nextChunkHasIngress && previous.data) {
previousMeta = {
...(previousMeta || {}),
pluginPipelineIngressBytes: previous.data.length,
};
}
const nextMeta = mergeTerminalDataMeta(previousMeta, nextChunkMeta, { preserveTerminalPerf });
pendingBySession.set(sessionId, {
data: nextData,
meta: nextMeta,
});
}
function takeEntry(sessionId) {
const entry = pendingBySession.get(sessionId) || { data: "", meta: undefined };
pendingBySession.delete(sessionId);
return entry;
}
function take(sessionId) {
return takeEntry(sessionId).data;
}
function clear(sessionId) {
pendingBySession.delete(sessionId);
}
function size(sessionId) {
return pendingBySession.get(sessionId)?.data.length ?? 0;
}
return {
append,
take,
takeEntry,
clear,
size,
};
}
function hasSessionListeners(listenersBySession, sessionId) {
return (listenersBySession.get(sessionId)?.size ?? 0) > 0;
}
function createTerminalDataDispatcher({
dataListeners,
displayDataListeners,
terminalDataBacklog,
onCallbackError = console.error,
shouldDropSession = () => false,
}) {
return function deliverToListeners(sessionId, data, meta) {
if (!data && !hasPluginPipelineIngressMarker(meta)) return;
if (shouldDropSession(sessionId)) return;
if (!hasSessionListeners(displayDataListeners, sessionId)) {
terminalDataBacklog?.append?.(sessionId, data, meta);
}
const set = dataListeners.get(sessionId);
if (!set || set.size === 0) return;
set.forEach((cb) => {
try {
cb(data, meta);
} catch (err) {
onCallbackError("Data callback failed", err);
}
});
};
}
function clearTerminalDataSession({
dataListeners,
displayDataListeners,
terminalDataBacklog,
}, sessionId) {
dataListeners?.delete?.(sessionId);
displayDataListeners?.delete?.(sessionId);
terminalDataBacklog?.clear?.(sessionId);
}
function clearTerminalDataBacklog({
terminalDataBacklog,
}, sessionId) {
terminalDataBacklog?.clear?.(sessionId);
}
module.exports = {
clearTerminalDataBacklog,
createTerminalDataBacklog,
createTerminalDataDispatcher,
clearTerminalDataSession,
hasPluginPipelineIngress,
hasPluginPipelineIngressMarker,
};