Files
peardock/libs/termInput.js
T
snxraven 25ba70cce8
CI / test (push) Successful in 9m59s
Optimize terminal input: coalesce keystrokes and hot-path RPC
Batch typing into short frames, send UTF-8 instead of base64 for text,
exempt stream methods from the general rate limit, and skip heavy
middleware on terminalInput/resize for lower latency.
2026-07-10 21:39:55 -04:00

117 lines
2.8 KiB
JavaScript

/**
* Coalesced terminal input sender.
*
* Typing generates many tiny onData events. Instead of one RPC event per
* keystroke (JSON + encode + rate-limit slot), we batch into short frames:
* - flush on rAF / ~8ms timer
* - flush immediately on Enter, paste-sized bursts, or max buffer
* - send UTF-8 strings in JSON (no base64) unless binary
*/
/**
* @param {(payload: { data: string, encoding?: string }) => void} send
* @param {{ maxDelayMs?: number, maxChars?: number }} [opts]
*/
export function createInputCoalescer(send, opts = {}) {
const maxDelayMs = opts.maxDelayMs ?? 8
const maxChars = opts.maxChars ?? 4096
let buf = ''
let timer = null
let raf = null
let closed = false
const clearTimers = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
if (raf) {
cancelAnimationFrame(raf)
raf = null
}
}
const flush = () => {
clearTimers()
if (!buf || closed) return
const data = buf
buf = ''
try {
send({ data, encoding: 'utf8' })
} catch {
// drop on closed connection
}
}
const schedule = () => {
if (timer || raf || closed) return
// Prefer animation frame for typing smoothness, with hard timeout
if (typeof requestAnimationFrame === 'function') {
raf = requestAnimationFrame(() => {
raf = null
flush()
})
}
timer = setTimeout(() => {
timer = null
flush()
}, maxDelayMs)
}
/**
* @param {string} chunk
*/
const push = (chunk) => {
if (closed || !chunk) return
buf += chunk
// Immediate flush for line endings / large pastes / control-heavy bursts
const immediate =
buf.length >= maxChars ||
chunk.includes('\r') ||
chunk.includes('\n') ||
chunk.includes('\x03') || // Ctrl+C
chunk.includes('\x04') || // Ctrl+D
chunk.length > 64 // paste-ish
if (immediate) flush()
else schedule()
}
/**
* Binary paste path (base64 once per flush of binary string).
* @param {string} binaryString - string of char codes 0-255
*/
const pushBinary = (binaryString) => {
if (closed || !binaryString) return
flush() // keep ordering vs pending text
let b64 = ''
try {
b64 = btoa(binaryString)
} catch {
// chunk large
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i) & 0xff
let bin = ''
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
}
b64 = btoa(bin)
}
try {
send({ data: b64, encoding: 'base64' })
} catch {
// ignore
}
}
const destroy = () => {
closed = true
clearTimers()
buf = ''
}
return { push, pushBinary, flush, destroy }
}