Files
Codeman/src/utils/buffer-accumulator.ts
T
480584de63 chore: project setup and tooling cleanup (#26)
* add project setup

* add tools to eslint

* remove contributing.md

* update claude md

* chore: simplify CI to single Node.js version (22)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* use node 20

* adjust formatting

* fix linting

* format

* fix layout

* fix: align CI node version to .nvmrc (22), remove redundant gotcha

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: arkon <arkon.85@hotmail.com>
2026-02-27 13:16:23 +01:00

196 lines
5.4 KiB
TypeScript

/**
* @fileoverview High-performance buffer accumulator for string data.
*
* This utility reduces GC pressure by avoiding repeated string concatenation (`+=`).
* Instead, chunks are pushed to an array and joined only when needed.
* Automatically trims when size limits are exceeded.
*
* @module utils/buffer-accumulator
*/
import type { BufferConfig } from '../types.js';
/**
* High-performance buffer accumulator using array-based collection.
*
* Features:
* - Efficient append via array push (avoids O(n) string concat)
* - Lazy join on read (consolidates only when accessed)
* - Automatic trimming when max size exceeded
* - Optional callback on trim for logging/metrics
*
* @example
* ```typescript
* const buffer = new BufferAccumulator({
* maxSize: 2 * 1024 * 1024, // 2MB
* trimSize: 1.5 * 1024 * 1024, // Trim to 1.5MB
* onTrim: (bytes) => console.log(`Trimmed ${bytes} bytes`)
* });
*
* buffer.append('chunk1');
* buffer.append('chunk2');
* console.log(buffer.value); // Joins and returns full content
* ```
*/
export class BufferAccumulator {
private chunks: string[] = [];
private totalLength: number = 0;
private readonly maxSize: number;
private readonly trimSize: number;
private readonly onTrim?: (trimmedBytes: number) => void;
/**
* Creates a new BufferAccumulator.
*
* @param config - Buffer configuration with max/trim sizes
*/
constructor(config: BufferConfig);
/**
* Creates a new BufferAccumulator with simple size parameters.
*
* @param maxSize - Maximum buffer size before trimming
* @param trimSize - Size to trim to when max is exceeded
*/
constructor(maxSize: number, trimSize: number);
constructor(configOrMaxSize: BufferConfig | number, trimSize?: number) {
if (typeof configOrMaxSize === 'number') {
this.maxSize = configOrMaxSize;
this.trimSize = trimSize!;
this.onTrim = undefined;
} else {
this.maxSize = configOrMaxSize.maxSize;
this.trimSize = configOrMaxSize.trimSize;
this.onTrim = configOrMaxSize.onTrim;
}
}
/**
* Append data to the buffer.
* Automatically trims if maxSize is exceeded.
*
* @param data - String data to append
*/
append(data: string): void {
if (!data) return;
this.chunks.push(data);
this.totalLength += data.length;
// Trim if exceeded max size
if (this.totalLength > this.maxSize) {
this.trim();
}
}
/**
* Get the full buffer content.
* Consolidates chunks on access for efficient subsequent reads.
*/
get value(): string {
if (this.chunks.length === 0) return '';
if (this.chunks.length === 1) return this.chunks[0];
// Consolidate chunks on access
const result = this.chunks.join('');
this.chunks = [result];
return result;
}
/**
* Get current buffer length without joining chunks.
*/
get length(): number {
return this.totalLength;
}
/**
* Check if buffer is empty.
*/
get isEmpty(): boolean {
return this.totalLength === 0;
}
/**
* Clear the buffer completely.
*/
clear(): void {
this.chunks = [];
this.totalLength = 0;
}
/**
* Set buffer to a specific value, replacing all content.
*
* @param value - New buffer content
*/
set(value: string): void {
this.chunks = value ? [value] : [];
this.totalLength = value?.length || 0;
}
/**
* Get the last N characters from the buffer.
* Useful for checking recent content without reading the entire buffer.
*
* @param n - Number of characters to get from the end
* @returns The last N characters (or entire buffer if smaller)
*/
tail(n: number): string {
if (n >= this.totalLength) {
return this.value;
}
const full = this.value;
return full.slice(-n);
}
/**
* Check if buffer ends with a specific string.
*
* @param suffix - String to check for
* @returns True if buffer ends with the suffix
*/
endsWith(suffix: string): boolean {
if (!suffix) return true; // All strings end with empty string
if (suffix.length > this.totalLength) return false;
return this.tail(suffix.length) === suffix;
}
/**
* Search for a pattern in the buffer (from the end).
*
* @param pattern - String or RegExp to search for
* @param fromEnd - Number of characters from end to search within (default: entire buffer)
* @returns True if pattern is found
*/
contains(pattern: string | RegExp, fromEnd?: number): boolean {
const searchIn = fromEnd ? this.tail(fromEnd) : this.value;
if (typeof pattern === 'string') {
return searchIn.includes(pattern);
}
return pattern.test(searchIn);
}
/**
* Trim buffer to keep only the most recent data.
* Called automatically when maxSize is exceeded.
*/
private trim(): void {
const full = this.chunks.join('');
const trimmedBytes = full.length - this.trimSize;
let trimmed = full.slice(-this.trimSize);
// Avoid starting mid-ANSI-escape: advance to first newline within 4KB.
// A partial escape at the buffer start causes xterm.js to misparse
// subsequent cursor movements, corrupting Ink's redraw rendering.
const firstNewline = trimmed.indexOf('\n');
if (firstNewline > 0 && firstNewline < 4096) {
trimmed = trimmed.slice(firstNewline + 1);
}
this.chunks = [trimmed];
this.totalLength = trimmed.length;
// Notify callback if configured
if (this.onTrim && trimmedBytes > 0) {
this.onTrim(trimmedBytes);
}
}
}