stream.ts 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. /** A bounded output queue for one Remote stream generation. */
  2. import { Deque } from '@deepseek-ai/dsh-deque'
  3. import type { TerminalFrame } from './types.ts'
  4. /** Slow followers fail explicitly; a later attachment recovers from the screen. */
  5. export class TerminalFollower {
  6. private readonly queue = new Deque<{ frame: TerminalFrame; bytes: number }>()
  7. private bytes = 0
  8. private wake: (() => void) | undefined
  9. private closed = false
  10. private finished = false
  11. private failure: Error | undefined
  12. /** @param maxBytes - maximum queued UTF-8 bytes for this follower. */
  13. constructor(private readonly maxBytes: number) {}
  14. /**
  15. * Queue a frame or fail this follower when its byte limit is exceeded.
  16. * @param frame - next ordered frame.
  17. */
  18. push(frame: TerminalFrame): void {
  19. if (this.closed || this.finished) return
  20. const bytes = Buffer.byteLength(JSON.stringify(frame), 'utf8')
  21. if (this.bytes + bytes > this.maxBytes) {
  22. this.failure = new Error('Terminal output consumer exceeded its buffer; reconnect to recover the current screen')
  23. this.close()
  24. return
  25. }
  26. this.queue.pushBack({ frame, bytes })
  27. this.bytes += bytes
  28. this.wake?.()
  29. }
  30. /** Finish after delivering every queued frame, including the final exit state. */
  31. finish(): void {
  32. this.finished = true
  33. this.wake?.()
  34. }
  35. /** Stop this follower without stopping its terminal. */
  36. close(): void {
  37. this.closed = true
  38. this.queue.clear()
  39. this.bytes = 0
  40. this.wake?.()
  41. }
  42. /**
  43. * Drain until detached or failed.
  44. * @param signal - Remote generation cancellation.
  45. * @returns ordered terminal frames.
  46. */
  47. async *read(signal: AbortSignal): AsyncIterable<TerminalFrame> {
  48. const abort = (): void => { this.close() }
  49. signal.addEventListener('abort', abort, { once: true })
  50. if (signal.aborted) abort()
  51. try {
  52. while (!this.closed) {
  53. const next = this.queue.popFront()
  54. if (next !== undefined) {
  55. this.bytes -= next.bytes
  56. yield next.frame
  57. } else {
  58. if (this.finished) break
  59. await new Promise<void>((resolve) => { this.wake = resolve })
  60. this.wake = undefined
  61. }
  62. }
  63. if (this.failure !== undefined) throw this.failure
  64. } finally {
  65. signal.removeEventListener('abort', abort)
  66. this.close()
  67. }
  68. }
  69. }