71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
/** Bounded host-side projection of a complete output file retained in E2B. */
|
|
|
|
import { Buffer } from 'node:buffer'
|
|
import type { SubprocessOutputRead, SubprocessOutputReader } from '@deepseek-ai/dsh-subprocess'
|
|
|
|
/** Offset reader used for one collect-mode E2B stream. */
|
|
export class E2BOutputReader implements SubprocessOutputReader {
|
|
private chunks: Buffer[] = []
|
|
private retainedBytes = 0
|
|
private totalBytes = 0
|
|
|
|
/**
|
|
* Create a bounded reader over one remote spill path.
|
|
* @param maxBytes - In-memory tail cap.
|
|
* @param maxSpillBytes - Maximum complete remote file size the caller accepts.
|
|
* @param spillPath - Remote full-output path.
|
|
*/
|
|
constructor(
|
|
private readonly maxBytes: number,
|
|
private readonly maxSpillBytes: number | undefined,
|
|
private readonly spillPath: string,
|
|
) {}
|
|
|
|
/** Total bytes observed from the SDK stream. */
|
|
get size(): number {
|
|
return this.totalBytes
|
|
}
|
|
|
|
/**
|
|
* Append one decoded SDK output event.
|
|
* @param text - Event text delivered by E2B.
|
|
*/
|
|
push(text: string): void {
|
|
if (text.length === 0) return
|
|
const chunk = Buffer.from(text)
|
|
this.totalBytes += chunk.length
|
|
this.chunks.push(chunk)
|
|
this.retainedBytes += chunk.length
|
|
while (this.retainedBytes > this.maxBytes) {
|
|
const head = this.chunks[0] as Buffer
|
|
const excess = this.retainedBytes - this.maxBytes
|
|
if (head.length <= excess) {
|
|
this.chunks.shift()
|
|
this.retainedBytes -= head.length
|
|
} else {
|
|
this.chunks[0] = head.subarray(excess)
|
|
this.retainedBytes -= excess
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
readFrom(fromByte: number): SubprocessOutputRead {
|
|
if (!Number.isSafeInteger(fromByte) || fromByte < 0) {
|
|
throw new Error('subprocess output offset must be a non-negative safe integer')
|
|
}
|
|
const retained = Buffer.concat(this.chunks, this.retainedBytes)
|
|
const firstRetained = this.totalBytes - this.retainedBytes
|
|
const lossy = fromByte < firstRetained
|
|
const start = lossy ? 0 : Math.min(retained.length, Math.max(0, fromByte - firstRetained))
|
|
return {
|
|
text: retained.subarray(start).toString('utf8'),
|
|
nextOffset: this.totalBytes,
|
|
lossy,
|
|
...(lossy && this.maxSpillBytes !== undefined && this.totalBytes <= this.maxSpillBytes
|
|
? { spillPath: this.spillPath }
|
|
: {}),
|
|
}
|
|
}
|
|
}
|