| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 |
- /** A bounded output queue for one Remote stream generation. */
- import { Deque } from '@deepseek-ai/dsh-deque'
- import type { TerminalFrame } from './types.ts'
- /** Slow followers fail explicitly; a later attachment recovers from the screen. */
- export class TerminalFollower {
- private readonly queue = new Deque<{ frame: TerminalFrame; bytes: number }>()
- private bytes = 0
- private wake: (() => void) | undefined
- private closed = false
- private finished = false
- private failure: Error | undefined
- /** @param maxBytes - maximum queued UTF-8 bytes for this follower. */
- constructor(private readonly maxBytes: number) {}
- /**
- * Queue a frame or fail this follower when its byte limit is exceeded.
- * @param frame - next ordered frame.
- */
- push(frame: TerminalFrame): void {
- if (this.closed || this.finished) return
- const bytes = Buffer.byteLength(JSON.stringify(frame), 'utf8')
- if (this.bytes + bytes > this.maxBytes) {
- this.failure = new Error('Terminal output consumer exceeded its buffer; reconnect to recover the current screen')
- this.close()
- return
- }
- this.queue.pushBack({ frame, bytes })
- this.bytes += bytes
- this.wake?.()
- }
- /** Finish after delivering every queued frame, including the final exit state. */
- finish(): void {
- this.finished = true
- this.wake?.()
- }
- /** Stop this follower without stopping its terminal. */
- close(): void {
- this.closed = true
- this.queue.clear()
- this.bytes = 0
- this.wake?.()
- }
- /**
- * Drain until detached or failed.
- * @param signal - Remote generation cancellation.
- * @returns ordered terminal frames.
- */
- async *read(signal: AbortSignal): AsyncIterable<TerminalFrame> {
- const abort = (): void => { this.close() }
- signal.addEventListener('abort', abort, { once: true })
- if (signal.aborted) abort()
- try {
- while (!this.closed) {
- const next = this.queue.popFront()
- if (next !== undefined) {
- this.bytes -= next.bytes
- yield next.frame
- } else {
- if (this.finished) break
- await new Promise<void>((resolve) => { this.wake = resolve })
- this.wake = undefined
- }
- }
- if (this.failure !== undefined) throw this.failure
- } finally {
- signal.removeEventListener('abort', abort)
- this.close()
- }
- }
- }
|