All files / shared/utils ring-buffer.ts

100% Statements 52/52
100% Branches 13/13
100% Functions 11/11
100% Lines 43/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160                                94x 94x 94x             94x             150x             1x                 107x     106x 106x     106x   106x     105x 40809x     105x 105x   105x               87x     57x 57x 13109x       57x     57x 57x   57x               89x   52x 52x   52x 13579x     52x   52x 52x   52x               3x   2x 2x 8x     2x             79x 79x             6160x             1x             1x      
/**
 * Ring Buffer (Circular Buffer)
 * 
 * High-performance binary buffer for efficient serial output batching.
 * Eliminates string accumulation overhead and prevents O(n²) garbage collection pressure.
 * 
 * Features:
 * - Fixed-size Uint8Array allocation (no dynamic reallocation)
 * - Write pointer advances without shifting data (circular semantics)
 * - Fast write/read without string copies
 * - Efficient extraction to string (single decoder pass)
 * - Memory-bounded (no unbounded growth)
 */
 
export class RingBuffer {
  private buffer: Uint8Array;
  private writePos = 0;
  private readPos = 0;
  private size = 0; // Current number of bytes in buffer
 
  /**
   * Create a ring buffer with fixed capacity
   * @param capacity Maximum bytes to hold (e.g., 8192 for typical serial batching)
   */
  constructor(capacity: number = 8192) {
    this.buffer = new Uint8Array(capacity);
  }
 
  /**
   * Get current number of bytes in buffer
   */
  getSize(): number {
    return this.size;
  }
 
  /**
   * Get buffer capacity
   */
  getCapacity(): number {
    return this.buffer.length;
  }
 
  /**
   * Write string data to buffer as UTF-8 bytes
   * If buffer would overflow, returns number of bytes actually written (may be less than input)
   * @returns bytes written
   */
  write(data: string): number {
    if (!data.length) return 0;
 
    // Encode string to UTF-8 bytes
    const encoded = new TextEncoder().encode(data);
    const availableSpace = this.buffer.length - this.size;
 
    // Truncate if overflow
    const bytesToWrite = Math.min(encoded.length, availableSpace);
 
    if (bytesToWrite === 0) return 0;
 
    // Write to circular buffer without modifying readPos
    for (let i = 0; i < bytesToWrite; i++) {
      this.buffer[(this.writePos + i) % this.buffer.length] = encoded[i];
    }
 
    this.writePos = (this.writePos + bytesToWrite) % this.buffer.length;
    this.size += bytesToWrite;
 
    return bytesToWrite;
  }
 
  /**
   * Read and extract all buffered data as string, clearing the buffer
   * @returns extracted string
   */
  readAll(): string {
    if (this.size === 0) return '';
 
    // Extract bytes in order (handling wrap-around)
    const result = new Uint8Array(this.size);
    for (let i = 0; i < this.size; i++) {
      result[i] = this.buffer[(this.readPos + i) % this.buffer.length];
    }
 
    // Decode to string once
    const str = new TextDecoder().decode(result);
 
    // Clear buffer
    this.readPos = this.writePos;
    this.size = 0;
 
    return str;
  }
 
  /**
   * Read up to maxBytes and extract as string, clearing those bytes from buffer
   * @returns extracted string
   */
  read(maxBytes: number): string {
    if (this.size === 0 || maxBytes <= 0) return '';
 
    const bytesToRead = Math.min(maxBytes, this.size);
    const result = new Uint8Array(bytesToRead);
 
    for (let i = 0; i < bytesToRead; i++) {
      result[i] = this.buffer[(this.readPos + i) % this.buffer.length];
    }
 
    const str = new TextDecoder().decode(result);
 
    this.readPos = (this.readPos + bytesToRead) % this.buffer.length;
    this.size -= bytesToRead;
 
    return str;
  }
 
  /**
   * Peek at buffered data without clearing it
   * @returns string view of all buffered data
   */
  peek(): string {
    if (this.size === 0) return '';
 
    const result = new Uint8Array(this.size);
    for (let i = 0; i < this.size; i++) {
      result[i] = this.buffer[(this.readPos + i) % this.buffer.length];
    }
 
    return new TextDecoder().decode(result);
  }
 
  /**
   * Clear all buffered data
   */
  clear(): void {
    this.readPos = this.writePos;
    this.size = 0;
  }
 
  /**
   * Check if buffer is empty
   */
  isEmpty(): boolean {
    return this.size === 0;
  }
 
  /**
   * Check if buffer is at capacity
   */
  isFull(): boolean {
    return this.size === this.buffer.length;
  }
 
  /**
   * Get available space for writing
   */
  getAvailableSpace(): number {
    return this.buffer.length - this.size;
  }
}