Skip to content

@studnicky/sample-buffer

Fixed-capacity circular buffer for numeric samples with percentile calculation.

Install

bash
pnpm add @studnicky/sample-buffer

@studnicky/sample-buffer exposes runtime operations at its root, schemas at @studnicky/sample-buffer/entities, and type contracts at @studnicky/sample-buffer/interfaces.

Usage

Create a SampleBuffer with a fixed capacity, push numeric samples into it, and read back percentiles. When full, the oldest sample is evicted to make room for each new one:

ts
import { SampleBuffer } from '../src/index.js';

const buffer = SampleBuffer.create({ 'capacity': 5 });

// Fill to capacity
buffer.push(10);
buffer.push(20);
buffer.push(30);
buffer.push(40);
buffer.push(50);

console.log('length:', buffer.length);   // 5
console.log('isFull:', buffer.isFull);   // true

// Percentile on a full buffer
const median = buffer.percentile(50);
const p95 = buffer.percentile(95);

console.log('p50:', median);
console.log('p95:', p95);

// Push beyond capacity — oldest samples are evicted
buffer.push(60); // evicts 10
buffer.push(70); // evicts 20

console.log('length after overflow:', buffer.length); // still 5

// Clear
buffer.clear();
console.log('length after clear:', buffer.length); // 0

Try it

Lifecycle hooks

TracedSampleBuffer subclasses SampleBuffer and overrides seven hooks: onOverflow, onEvict, onPush, onComputeStart, onComputeComplete, onPercentile, and onClear. With capacity=3 and 5 pushes, watch two overflow+eviction pairs fire. The first percentile(50) triggers computeStart and computeComplete; the second call is a cache hit so those hooks do not fire again.

Loading example…

Public API

The package root exports SampleBuffer and SampleBufferError. Import schemas from @studnicky/sample-buffer/entities and the buffer contract from @studnicky/sample-buffer/interfaces. Construct buffers with SampleBuffer.create({ capacity }).

Extending

Subclass SampleBuffer and override any lifecycle hook to observe buffer events. All hooks are no-ops by default; only override what you need:

ts
import { SampleBuffer } from '../src/index.js';

class EvictionLog extends SampleBuffer {
  readonly evicted: number[] = [];

  protected override onEvict(oldValue: number): void {
    this.evicted.push(oldValue);
  }
}

const log = EvictionLog.create({ 'capacity': 3 });

// First three fill the buffer — no evictions
log.push(1);
log.push(2);
log.push(3);

// Next two each trigger an eviction
log.push(4); // evicts 1
log.push(5); // evicts 2

console.log('evicted:', log.evicted); // [1, 2]

Observability hooks

Subclass SampleBuffer and override any hook to observe the full push/evict/compute lifecycle. All hooks fire synchronously, after state mutation, with no try/catch.

HookWhen it firesArgs
onOverflow(value)Push onto a full buffer, before evictionvalue: number — the incoming sample
onEvict(oldValue)Full-buffer push path, after overflow, before overwriteoldValue: number — the sample being replaced
onPush(value, evicted)End of push(), after length/head updatedvalue: number, evicted: boolean
onComputeStart(length)Start of buildSortedSamples() — cache miss in percentile()length: number — samples about to be sorted
onComputeComplete(length, sorted)End of buildSortedSamples(), after sortlength: number, sorted: readonly number[]
onPercentile(pct, result)Before returning from percentile(), non-empty buffer onlypct: number, result: number
onClear()Start of clear(), before state resetnone
ts
import { SampleBuffer } from '../src/index.js';

class TracedSampleBuffer extends SampleBuffer {
  readonly overflowLog: number[] = [];
  readonly evictLog: number[] = [];
  readonly pushLog: { 'evicted': boolean; 'value': number }[] = [];
  readonly computeStartLog: number[] = [];
  readonly computeCompleteLog: { 'length': number; 'sorted': readonly number[] }[] = [];
  readonly percentileLog: { 'pct': number; 'result': number }[] = [];
  clearCount = 0;

  protected override onOverflow(value: number): void {
    console.log(`[sample-buffer] overflow value=${String(value)} capacity=${String(this.capacity)}`);
    this.overflowLog.push(value);
  }

  protected override onEvict(oldValue: number): void {
    console.log(`[sample-buffer] evict oldValue=${String(oldValue)}`);
    this.evictLog.push(oldValue);
  }

  protected override onPush(value: number, evicted: boolean): void {
    console.log(`[sample-buffer] push value=${String(value)} evicted=${String(evicted)} length=${String(this.length)}`);
    this.pushLog.push({ 'evicted': evicted, 'value': value });
  }

  protected override onComputeStart(length: number): void {
    console.log(`[sample-buffer] computeStart length=${String(length)}`);
    this.computeStartLog.push(length);
  }

  protected override onComputeComplete(length: number, sorted: readonly number[]): void {
    console.log(`[sample-buffer] computeComplete length=${String(length)} sorted=[${sorted.join(',')}]`);
    this.computeCompleteLog.push({ 'length': length, 'sorted': sorted });
  }

  protected override onPercentile(pct: number, result: number): void {
    console.log(`[sample-buffer] percentile pct=${String(pct)} result=${String(result)}`);
    this.percentileLog.push({ 'pct': pct, 'result': result });
  }

  protected override onClear(): void {
    console.log(`[sample-buffer] clear length=${String(this.length)}`);
    this.clearCount++;
  }
}

const buffer = TracedSampleBuffer.create({ 'capacity': 3 });

// Fill the buffer (3 pushes, no overflow)
buffer.push(10);
buffer.push(20);
buffer.push(30);

// Push past capacity — triggers overflow + eviction
buffer.push(40); // evicts 10
buffer.push(50); // evicts 20

// Compute a percentile (triggers computeStart + computeComplete + percentile hook)
const p50 = buffer.percentile(50);

// Second call uses cache — no computeStart/computeComplete
const p50Cached = buffer.percentile(50);

// Clear
buffer.clear();

The base class never calls any logger or metrics library. All hooks are no-ops by default.

Entities

@studnicky/sample-buffer/entities exports buffer option and observable-state schemas.

typescript
import { SampleBufferOptionsEntity } from '@studnicky/sample-buffer/entities';

Interfaces

@studnicky/sample-buffer/interfaces exports the sample-buffer contract.

typescript
import type { SampleBufferInterface } from '@studnicky/sample-buffer/interfaces';

Exports

SymbolPurposeImport path
SampleBufferStores a fixed window of numeric samples and calculates percentiles.@studnicky/sample-buffer
SampleBufferErrorRepresents sample-buffer failures.@studnicky/sample-buffer

Source on GitHub