Files
orca/mobile/scripts/bounded-process-line-reader.mjs
T
NeilandOrca 4be5bcd756 feat(memory-safety): bound OOM-prone ingress across transports, parsers and scans
Re-scoped from the reverted #10179 to the bound-at-ingress doctrine (see
src/shared/memory-safety/README.md): ceilings go where untrusted or
unpredictably-large input first enters a heap, not in the interior.

Kept: daemon/relay/SSH/mobile/IPC transport frames, PTY output flow control,
on-disk agent transcripts, document parsing, filesystem enumeration, provider
pagination, media decode, and long-lived accumulators keyed by user-growable
ids -- the class behind the reported renderer OOM (#9872).

Dropped: interior ceilings whose size is already implied by a bounded input,
second ceilings behind existing ones, and derived/presentational state.

Every file is reconciled against current main: check-no-merge-base-revert
reports 0 reverted and 0 missing across 169 overlapping files, so no shipped
work is lost. Typecheck is at main's own 2-error baseline.

Co-authored-by: Orca <help@stably.ai>
2026-07-25 23:43:37 -07:00

104 lines
2.6 KiB
JavaScript

import { Buffer } from 'node:buffer'
export const PROCESS_LINE_MAX_BYTES = 64 * 1024
export const PROCESS_OUTPUT_TAIL_MAX_CODE_UNITS = 64 * 1024
const TRUNCATED_LINE_SUFFIX = '… [line truncated]'
export function appendProcessOutputTail(
current,
line,
maxCodeUnits = PROCESS_OUTPUT_TAIL_MAX_CODE_UNITS
) {
if (!Number.isSafeInteger(maxCodeUnits) || maxCodeUnits < 0) {
throw new RangeError('Process output tail limit must be a non-negative safe integer')
}
if (maxCodeUnits === 0) {
return ''
}
const appended = `${current}${line}\n`
return appended.length <= maxCodeUnits ? appended : appended.slice(-maxCodeUnits)
}
export function attachBoundedProcessLineReader(
stream,
onLine,
maxLineBytes = PROCESS_LINE_MAX_BYTES
) {
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes < 0) {
throw new RangeError('Process line limit must be a non-negative safe integer')
}
const retained = Buffer.allocUnsafe(maxLineBytes)
let retainedBytes = 0
let truncated = false
let swallowLineFeed = false
let closed = false
const append = (bytes, start, end) => {
if (start >= end) {
return
}
const available = maxLineBytes - retainedBytes
const copied = Math.min(available, end - start)
if (copied > 0) {
bytes.copy(retained, retainedBytes, start, start + copied)
retainedBytes += copied
}
truncated ||= copied < end - start
}
const emit = () => {
const line = retained.subarray(0, retainedBytes).toString('utf8')
onLine(truncated ? `${line}${TRUNCATED_LINE_SUFFIX}` : line)
retainedBytes = 0
truncated = false
}
const onData = (chunk) => {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
let start = 0
if (swallowLineFeed) {
swallowLineFeed = false
if (bytes[0] === 0x0a) {
start = 1
}
}
for (let index = start; index < bytes.length; index += 1) {
const value = bytes[index]
if (value !== 0x0a && value !== 0x0d) {
continue
}
append(bytes, start, index)
emit()
if (value === 0x0d && index + 1 < bytes.length && bytes[index + 1] === 0x0a) {
index += 1
} else if (value === 0x0d && index + 1 === bytes.length) {
swallowLineFeed = true
}
start = index + 1
}
append(bytes, start, bytes.length)
}
const detach = () => {
if (closed) {
return
}
closed = true
stream.off('data', onData)
stream.off('end', onEnd)
}
const onEnd = () => {
if (retainedBytes > 0 || truncated) {
emit()
}
detach()
}
stream.on('data', onData)
stream.on('end', onEnd)
return detach
}