@studnicky/circular-buffer
Generic ring buffer with O(1) push and shift operations.
Install
pnpm add @studnicky/circular-bufferUsage
Fixed-capacity ring buffer. When the buffer is full, the oldest item is evicted and the new item takes its slot. Length stays at capacity:
import { CircularBuffer } from '../src/index.js';
// Fixed-capacity ring: capacity 3, overflow defaults to 'overwrite'
const buffer = CircularBuffer.create<number>({ 'capacity': 3 });
buffer.push(1);
buffer.push(2);
buffer.push(3);
buffer.push(4); // buffer is full — 1 is evicted, ring holds [2, 3, 4]
console.log(`length after 4 pushes into capacity-3 ring: ${buffer.length}`);
console.log(`shift: ${buffer.shift()}`); // 2 — oldest surviving item
console.log(`shift: ${buffer.shift()}`); // 3
console.log(`shift: ${buffer.shift()}`); // 4
console.log(`length after drain: ${buffer.length}`);Try it
Lifecycle hooks
TracingBuffer subclasses CircularBuffer and overrides five hooks: onOverflow, onEvict, onPush, onShift, and onGrow. Two scenarios run: an overwrite-mode ring (capacity 3, 5 pushes — watch 2 overflow and 2 eviction events) and a grow-mode ring (capacity 2, 3 pushes — watch the buffer double to capacity 4 instead of evicting).
Public API
Import CircularBuffer, CircularBufferOptionsEntity, CircularBufferStateEntity, CircularBufferError, and CircularBufferInterface from @studnicky/circular-buffer. Construct a ring through CircularBuffer.create({ capacity, overflow }). CircularBufferInterface.length composes the schema-derived field owned by CircularBufferStateEntity. The package declares separate root, entity, and interface import surfaces; storage constants are implementation details.
Extending
CircularBuffer is a class; subclass it to add domain-specific behavior. Override the protected hooks onEvict, onGrow, onPush, and onShift to observe lifecycle events without coupling business logic to the buffer internals:
import { CircularBuffer } from '../src/index.js';
// Default overwrite ring — observe evictions
class EvictTracker<T> extends CircularBuffer<T> {
readonly evictedItems: T[] = [];
protected override onEvict(item: T): void {
this.evictedItems.push(item);
}
}
const ring = EvictTracker.create<string, EvictTracker<string>>({ 'capacity': 2 });
ring.push('a');
ring.push('b');
ring.push('c'); // 'a' is evicted
console.log(`evicted items: ${ring.evictedItems.join(', ')}`);
console.log(`ring length: ${ring.length}`);
// Opt-in grow mode — observe capacity changes
class GrowTracker<T> extends CircularBuffer<T> {
readonly growEvents: number[] = [];
protected override onGrow(_old: number, newCap: number): void {
this.growEvents.push(newCap);
}
}
const growing = GrowTracker.create<string, GrowTracker<string>>({ 'capacity': 2, 'overflow': 'grow' });
growing.push('a');
growing.push('b');
growing.push('c'); // triggers grow: capacity 2 → 4
console.log(`grow events: ${growing.growEvents.length}`);
console.log(`new capacity after grow: ${growing.growEvents[0]}`);Observability hooks
Override any protected hook to observe lifecycle events without coupling to a logger or metrics library.
| Hook | When it fires | Args |
|---|---|---|
onOverflow(item) | Push onto a full buffer in overwrite mode, before the oldest item is evicted | item: T — the incoming item |
onEvict(item) | Push onto a full buffer in overwrite mode, after overflow is detected, before the slot is overwritten | item: T — the item being dropped |
onPush(item) | End of push(), after the item is inserted and length updated (fires in both modes) | item: T — the item pushed |
onShift(item) | Inside shift(), before returning the item (not called on empty buffer) | item: T — the item being removed |
onGrow(oldCapacity, newCapacity) | End of grow(), after the buffer has been resized (grow mode only) | oldCapacity: number, newCapacity: number |
import { CircularBuffer } from '../src/index.js';
class TracingBuffer<T> extends CircularBuffer<T> {
readonly overflowItems: T[] = [];
readonly evictItems: T[] = [];
readonly growEvents: { 'newCapacity': number; 'oldCapacity': number }[] = [];
readonly pushItems: T[] = [];
readonly shiftItems: T[] = [];
protected override onOverflow(item: T): void {
console.log(`[circular-buffer] overflow incoming=${String(item)}`);
this.overflowItems.push(item);
}
protected override onEvict(item: T): void {
console.log(`[circular-buffer] evict item=${String(item)}`);
this.evictItems.push(item);
}
protected override onGrow(oldCapacity: number, newCapacity: number): void {
console.log(`[circular-buffer] grow ${oldCapacity} → ${newCapacity}`);
this.growEvents.push({ 'newCapacity': newCapacity, 'oldCapacity': oldCapacity });
}
protected override onPush(item: T): void {
console.log(`[circular-buffer] push item=${String(item)} length=${this.length}`);
this.pushItems.push(item);
}
protected override onShift(item: T): void {
console.log(`[circular-buffer] shift item=${String(item)} length=${this.length}`);
this.shiftItems.push(item);
}
}
// Scenario 1: capacity-3 overwrite buffer, push 5 items (2 overflows/evictions)
console.log('--- overwrite mode (capacity 3) ---');
const ring = TracingBuffer.create<number, TracingBuffer<number>>({ 'capacity': 3 });
ring.push(1);
ring.push(2);
ring.push(3);
ring.push(4); // overflow: evicts 1
ring.push(5); // overflow: evicts 2
console.log('shifting all remaining items:');
while (ring.length > 0) { ring.shift(); }
// Scenario 2: capacity-2 grow-mode buffer
console.log('--- grow mode (capacity 2) ---');
const growing = TracingBuffer.create<number, TracingBuffer<number>>({ 'capacity': 2, 'overflow': 'grow' });
growing.push(10);
growing.push(20);
growing.push(30); // triggers grow 2 → 4
console.log('observedCircularBuffer: scenarios complete');The base class never calls any logger or metrics library. All hooks are no-ops by default.
Entities
@studnicky/circular-buffer/entities exports every schema namespace in src/entities.
import { CircularBufferOptionsEntity } from '@studnicky/circular-buffer/entities';Interfaces
@studnicky/circular-buffer/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.
import type { CircularBufferInterface } from '@studnicky/circular-buffer/interfaces';Exports
| Symbol | Purpose | Import path |
|---|---|---|
CircularBuffer | Provides circular buffer functionality. | @studnicky/circular-buffer |
CircularBufferError | Represents circular buffer failures. | @studnicky/circular-buffer |