/ utils / bufferedWriter.ts
bufferedWriter.ts
  1  type WriteFn = (content: string) => void
  2  
  3  export type BufferedWriter = {
  4    write: (content: string) => void
  5    flush: () => void
  6    dispose: () => void
  7  }
  8  
  9  export function createBufferedWriter({
 10    writeFn,
 11    flushIntervalMs = 1000,
 12    maxBufferSize = 100,
 13    maxBufferBytes = Infinity,
 14    immediateMode = false,
 15  }: {
 16    writeFn: WriteFn
 17    flushIntervalMs?: number
 18    maxBufferSize?: number
 19    maxBufferBytes?: number
 20    immediateMode?: boolean
 21  }): BufferedWriter {
 22    let buffer: string[] = []
 23    let bufferBytes = 0
 24    let flushTimer: NodeJS.Timeout | null = null
 25    // Batch detached by overflow that hasn't been written yet. Tracked so
 26    // flush()/dispose() can drain it synchronously if the process exits
 27    // before the setImmediate fires.
 28    let pendingOverflow: string[] | null = null
 29  
 30    function clearTimer(): void {
 31      if (flushTimer) {
 32        clearTimeout(flushTimer)
 33        flushTimer = null
 34      }
 35    }
 36  
 37    function flush(): void {
 38      if (pendingOverflow) {
 39        writeFn(pendingOverflow.join(''))
 40        pendingOverflow = null
 41      }
 42      if (buffer.length === 0) return
 43      writeFn(buffer.join(''))
 44      buffer = []
 45      bufferBytes = 0
 46      clearTimer()
 47    }
 48  
 49    function scheduleFlush(): void {
 50      if (!flushTimer) {
 51        flushTimer = setTimeout(flush, flushIntervalMs)
 52      }
 53    }
 54  
 55    // Detach the buffer synchronously so the caller never waits on writeFn.
 56    // writeFn may block (e.g. errorLogSink.ts appendFileSync) — if overflow fires
 57    // mid-render or mid-keystroke, deferring the write keeps the current tick
 58    // short. Timer-based flushes already run outside user code paths so they
 59    // stay synchronous.
 60    function flushDeferred(): void {
 61      if (pendingOverflow) {
 62        // A previous overflow write is still queued. Coalesce into it to
 63        // preserve ordering — writes land in a single setImmediate-ordered batch.
 64        pendingOverflow.push(...buffer)
 65        buffer = []
 66        bufferBytes = 0
 67        clearTimer()
 68        return
 69      }
 70      const detached = buffer
 71      buffer = []
 72      bufferBytes = 0
 73      clearTimer()
 74      pendingOverflow = detached
 75      setImmediate(() => {
 76        const toWrite = pendingOverflow
 77        pendingOverflow = null
 78        if (toWrite) writeFn(toWrite.join(''))
 79      })
 80    }
 81  
 82    return {
 83      write(content: string): void {
 84        if (immediateMode) {
 85          writeFn(content)
 86          return
 87        }
 88        buffer.push(content)
 89        bufferBytes += content.length
 90        scheduleFlush()
 91        if (buffer.length >= maxBufferSize || bufferBytes >= maxBufferBytes) {
 92          flushDeferred()
 93        }
 94      },
 95      flush,
 96      dispose(): void {
 97        flush()
 98      },
 99    }
100  }