Skip to content

@studnicky/circular-buffer

Generic ring buffer with O(1) push and shift operations.

Install

bash
pnpm add @studnicky/circular-buffer

Usage

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:

ts
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).

Loading example…

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:

ts
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.

HookWhen it firesArgs
onOverflow(item)Push onto a full buffer in overwrite mode, before the oldest item is evicteditem: T — the incoming item
onEvict(item)Push onto a full buffer in overwrite mode, after overflow is detected, before the slot is overwrittenitem: 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
ts
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.

Source on GitHub

Entities

@studnicky/circular-buffer/entities exports every schema namespace in src/entities.

typescript
import { CircularBufferOptionsEntity } from '@studnicky/circular-buffer/entities';

Interfaces

@studnicky/circular-buffer/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.

typescript
import type { CircularBufferInterface } from '@studnicky/circular-buffer/interfaces';

Exports

SymbolPurposeImport path
CircularBufferProvides circular buffer functionality.@studnicky/circular-buffer
CircularBufferErrorRepresents circular buffer failures.@studnicky/circular-buffer