@studnicky/sample-buffer
Fixed-capacity circular buffer for numeric samples with percentile calculation.
Install
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:
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); // 0Try 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.
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:
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.
| Hook | When it fires | Args |
|---|---|---|
onOverflow(value) | Push onto a full buffer, before eviction | value: number — the incoming sample |
onEvict(oldValue) | Full-buffer push path, after overflow, before overwrite | oldValue: number — the sample being replaced |
onPush(value, evicted) | End of push(), after length/head updated | value: 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 sort | length: number, sorted: readonly number[] |
onPercentile(pct, result) | Before returning from percentile(), non-empty buffer only | pct: number, result: number |
onClear() | Start of clear(), before state reset | none |
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.
import { SampleBufferOptionsEntity } from '@studnicky/sample-buffer/entities';Interfaces
@studnicky/sample-buffer/interfaces exports the sample-buffer contract.
import type { SampleBufferInterface } from '@studnicky/sample-buffer/interfaces';Exports
| Symbol | Purpose | Import path |
|---|---|---|
SampleBuffer | Stores a fixed window of numeric samples and calculates percentiles. | @studnicky/sample-buffer |
SampleBufferError | Represents sample-buffer failures. | @studnicky/sample-buffer |