api / frontend /trace-buffer.ts
gary-boon
Deploy Visualisable.ai backend with API protection
c6c8587
raw
history blame
6.29 kB
import { TraceData } from './types';
export type PlaybackMode = 'live' | 'paused' | 'slow' | 'fast' | 'catchup';
export interface BufferState {
mode: PlaybackMode;
speed: number; // Multiplier: 0.25, 0.5, 1, 2, 4
bufferSize: number;
currentSize: number;
writePosition: number;
readPosition: number;
droppedCount: number;
isPaused: boolean;
}
export interface TraceBufferConfig {
maxSize?: number;
updateInterval?: number;
batchSize?: number;
}
type BufferListener = (traces: TraceData[], state: BufferState) => void;
export class TraceBuffer {
private buffer: (TraceData | null)[];
private maxSize: number;
private writePos: number = 0;
private readPos: number = 0;
private currentSize: number = 0;
private droppedCount: number = 0;
private mode: PlaybackMode = 'live';
private speed: number = 1;
private isPaused: boolean = false;
private listeners: Set<BufferListener> = new Set();
private updateTimer: NodeJS.Timeout | null = null;
private updateInterval: number;
private batchSize: number;
private pendingTraces: TraceData[] = [];
private lastUpdateTime: number = 0;
private targetFPS: number = 60;
constructor(config: TraceBufferConfig = {}) {
this.maxSize = config.maxSize || 10000;
this.updateInterval = config.updateInterval || 16; // ~60fps
this.batchSize = config.batchSize || 10;
this.buffer = new Array(this.maxSize).fill(null);
this.startUpdateLoop();
}
private startUpdateLoop(): void {
const update = () => {
const now = Date.now();
const deltaTime = now - this.lastUpdateTime;
if (!this.isPaused && this.pendingTraces.length > 0) {
const effectiveInterval = this.updateInterval / this.speed;
if (deltaTime >= effectiveInterval) {
this.processPendingTraces();
this.lastUpdateTime = now;
}
}
this.updateTimer = setTimeout(update, Math.max(1, this.updateInterval));
};
update();
}
private processPendingTraces(): void {
if (this.pendingTraces.length === 0) return;
let tracesToProcess: TraceData[];
switch (this.mode) {
case 'live':
// Process in real-time batches
tracesToProcess = this.pendingTraces.splice(0, this.batchSize);
break;
case 'slow':
// Process one at a time for slow motion
tracesToProcess = this.pendingTraces.splice(0, 1);
break;
case 'fast':
// Process larger batches for fast forward
tracesToProcess = this.pendingTraces.splice(0, this.batchSize * 4);
break;
case 'catchup':
// Process all pending to catch up
tracesToProcess = this.pendingTraces.splice(0, this.pendingTraces.length);
this.mode = 'live'; // Return to live after catching up
break;
default:
tracesToProcess = [];
}
if (tracesToProcess.length > 0) {
this.notifyListeners(tracesToProcess);
}
}
public push(trace: TraceData): void {
// Add to ring buffer
if (this.currentSize >= this.maxSize) {
// Buffer is full, overwrite oldest
this.droppedCount++;
} else {
this.currentSize++;
}
this.buffer[this.writePos] = trace;
this.writePos = (this.writePos + 1) % this.maxSize;
// Add to pending traces for processing
if (!this.isPaused) {
this.pendingTraces.push(trace);
}
}
public setMode(mode: PlaybackMode): void {
this.mode = mode;
switch (mode) {
case 'paused':
this.isPaused = true;
break;
case 'catchup':
this.isPaused = false;
// Will process all pending traces
break;
default:
this.isPaused = false;
}
}
public setSpeed(speed: number): void {
this.speed = Math.max(0.25, Math.min(4, speed));
}
public pause(): void {
this.isPaused = true;
this.mode = 'paused';
}
public resume(): void {
this.isPaused = false;
this.mode = 'live';
}
public clear(): void {
this.buffer.fill(null);
this.writePos = 0;
this.readPos = 0;
this.currentSize = 0;
this.droppedCount = 0;
this.pendingTraces = [];
this.notifyListeners([]);
}
public getState(): BufferState {
return {
mode: this.mode,
speed: this.speed,
bufferSize: this.maxSize,
currentSize: this.currentSize,
writePosition: this.writePos,
readPosition: this.readPos,
droppedCount: this.droppedCount,
isPaused: this.isPaused,
};
}
public getRecentTraces(count: number = 100): TraceData[] {
const traces: TraceData[] = [];
let pos = (this.writePos - 1 + this.maxSize) % this.maxSize;
let collected = 0;
while (collected < count && collected < this.currentSize) {
const trace = this.buffer[pos];
if (trace) {
traces.unshift(trace);
collected++;
}
pos = (pos - 1 + this.maxSize) % this.maxSize;
}
return traces;
}
public subscribe(listener: BufferListener): () => void {
this.listeners.add(listener);
// Send current state immediately
listener(this.getRecentTraces(), this.getState());
// Return unsubscribe function
return () => {
this.listeners.delete(listener);
};
}
private notifyListeners(newTraces: TraceData[]): void {
const state = this.getState();
this.listeners.forEach(listener => {
try {
listener(newTraces, state);
} catch (error) {
console.error('Error in buffer listener:', error);
}
});
}
public destroy(): void {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
this.updateTimer = null;
}
this.listeners.clear();
this.buffer = [];
this.pendingTraces = [];
}
}
// Singleton instance
let bufferInstance: TraceBuffer | null = null;
export function getTraceBuffer(config?: TraceBufferConfig): TraceBuffer {
if (!bufferInstance) {
bufferInstance = new TraceBuffer(config);
}
return bufferInstance;
}
export function destroyTraceBuffer(): void {
if (bufferInstance) {
bufferInstance.destroy();
bufferInstance = null;
}
}