mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
export const SEARCH_SUBPROCESS_MAX_LINE_BYTES = 64 * 1024 * 1024
|
|
const SEARCH_SUBPROCESS_INITIAL_LINE_BUFFER_BYTES = 4 * 1024
|
|
|
|
export class SearchSubprocessLineAccumulator {
|
|
private buffer: Buffer | null = null
|
|
private bytes = 0
|
|
private discardingLine = false
|
|
|
|
constructor(
|
|
private readonly maxLineBytes = SEARCH_SUBPROCESS_MAX_LINE_BYTES,
|
|
private readonly oversizedLine: 'reject' | 'discard' = 'reject'
|
|
) {
|
|
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes < 0) {
|
|
throw new RangeError('Search line limit must be a non-negative safe integer')
|
|
}
|
|
}
|
|
|
|
/** False reports an oversized line, even when discard mode continues with later lines. */
|
|
push(rawChunk: Buffer | string, onLine: (line: string) => void): boolean {
|
|
// Three bytes per UTF-16 code unit bounds UTF-8 size without re-encoding complete batches.
|
|
if (
|
|
typeof rawChunk === 'string' &&
|
|
this.bytes === 0 &&
|
|
!this.discardingLine &&
|
|
rawChunk.endsWith('\n') &&
|
|
rawChunk.length * 3 <= this.maxLineBytes
|
|
) {
|
|
const lines = rawChunk.split('\n')
|
|
lines.pop()
|
|
for (const line of lines) {
|
|
onLine(line)
|
|
}
|
|
return true
|
|
}
|
|
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk, 'utf8')
|
|
let cursor = 0
|
|
let overflowed = false
|
|
while (cursor < chunk.length) {
|
|
const newline = chunk.indexOf(0x0a, cursor)
|
|
const end = newline === -1 ? chunk.length : newline
|
|
const segmentBytes = end - cursor
|
|
if (this.discardingLine) {
|
|
if (newline === -1) {
|
|
return !overflowed
|
|
}
|
|
this.discardingLine = false
|
|
cursor = newline + 1
|
|
continue
|
|
}
|
|
if (this.bytes + segmentBytes > this.maxLineBytes) {
|
|
this.clear()
|
|
if (this.oversizedLine === 'reject') {
|
|
return false
|
|
}
|
|
overflowed = true
|
|
this.discardingLine = newline === -1
|
|
if (newline === -1) {
|
|
return false
|
|
}
|
|
cursor = newline + 1
|
|
continue
|
|
}
|
|
|
|
if (newline !== -1 && this.bytes === 0) {
|
|
onLine(chunk.toString('utf8', cursor, end))
|
|
} else if (segmentBytes > 0) {
|
|
this.append(chunk.subarray(cursor, end))
|
|
if (newline !== -1) {
|
|
onLine(this.takeLine())
|
|
}
|
|
} else if (newline !== -1) {
|
|
onLine(this.takeLine())
|
|
}
|
|
|
|
if (newline === -1) {
|
|
return !overflowed
|
|
}
|
|
cursor = newline + 1
|
|
}
|
|
return !overflowed
|
|
}
|
|
|
|
finish(): string | null {
|
|
if (this.discardingLine) {
|
|
this.clear()
|
|
}
|
|
return this.bytes > 0 ? this.takeLine() : null
|
|
}
|
|
|
|
clear(): void {
|
|
this.buffer = null
|
|
this.bytes = 0
|
|
this.discardingLine = false
|
|
}
|
|
|
|
/** Capacity of the retained growable buffer, or null once it has been released. */
|
|
retainedCapacityBytes(): number | null {
|
|
return this.buffer?.length ?? null
|
|
}
|
|
|
|
private append(segment: Buffer): void {
|
|
const requiredBytes = this.bytes + segment.length
|
|
if (!this.buffer || this.buffer.length < requiredBytes) {
|
|
const doubledCapacity = this.buffer?.length ? this.buffer.length * 2 : 0
|
|
const nextCapacity = Math.min(
|
|
this.maxLineBytes,
|
|
Math.max(SEARCH_SUBPROCESS_INITIAL_LINE_BUFFER_BYTES, doubledCapacity, requiredBytes)
|
|
)
|
|
const next = Buffer.allocUnsafe(nextCapacity)
|
|
this.buffer?.copy(next, 0, 0, this.bytes)
|
|
this.buffer = next
|
|
}
|
|
segment.copy(this.buffer, this.bytes)
|
|
this.bytes = requiredBytes
|
|
}
|
|
|
|
private takeLine(): string {
|
|
const line = this.buffer?.toString('utf8', 0, this.bytes) ?? ''
|
|
this.clear()
|
|
return line
|
|
}
|
|
}
|