npm version License: MIT CI codecov
High-performance circular buffer/queue for TypeScript and React. Zero dependencies (React optional) — perfect for logs, streaming data, rolling windows, and real-time UI updates.
- 🔄 CircularBuffer (Low-level) — direction-based circular buffer primitive (HEAD/TAIL)
- 📦 BufferManager (High-level) — convenient API (push/pop single or arrays, peek helpers, utilities)
- ⚛️ React Hook —
useCircularBufferfor automatic re-rendering in React - 🎯 Type-Safe — full TypeScript generics support
- ⚡ Fast — O(1) push/pop/peek operations
- 🪶 Zero Dependencies — pure TypeScript implementation (React is optional)
- 🔧 Flexible — logs, streaming feeds, caching, undo/redo, rolling averages, etc.
npm install circular-queue-react # or yarn add circular-queue-react # or pnpm add circular-queue-react
React Support: React 16.8+, 17, 18, 19 ✅ (React is required only if you use
useCircularBuffer.)
CircularBuffer is a minimal primitive:
push(item, direction)— insertpop(direction)— remove 1get(direction, count?)— peek (non-destructive)iterable(oldest → newest)
import { CircularBuffer, Direction } from "circular-queue-react"; const buf = new CircularBuffer<string>(3); // Push to TAIL (newest side) buf.push("A", Direction.TAIL); buf.push("B", Direction.TAIL); // Push to HEAD (oldest side) buf.push("Z", Direction.HEAD); console.log(buf.get(Direction.HEAD)); // "Z" (oldest) console.log(buf.get(Direction.TAIL)); // "B" (newest) // Peek many console.log(buf.get(Direction.HEAD, 2)); // ["Z","A"] (oldest -> newer) console.log(buf.get(Direction.TAIL, 2)); // ["B","A"] (newest -> older) // Iterate (oldest -> newest) for (const x of buf) console.log(x); // Pop console.log(buf.pop(Direction.HEAD)); // removes oldest ("Z") console.log(buf.pop(Direction.TAIL)); // removes newest ("B") // Resize (logical capacity) buf.resize(10); // Clear buf.clear();
BufferManager wraps CircularBuffer and provides a friendly API:
pushHead/pushTail(single item or array)popHead/popTail(single or count)getHead/getTail(single or count)- utilities:
getAll,replaceAll,forEach/map/filter,getInfo, etc.
import { BufferManager } from "circular-queue-react"; const b = new BufferManager<string>(3); // pushTail keeps newest at the end (TAIL) b.pushTail(["A", "B", "C", "D"]); console.log(b.getAll()); // ["B","C","D"] (keeps last 3) // pushHead inserts at HEAD, preserves input order b.pushHead(["X", "Y"]); console.log(b.getAll()); // ["X","Y","B"] (oldest -> newest) // Peek console.log(b.getHead()); // "X" console.log(b.getTail()); // "B" console.log(b.getHead(2)); // ["X","Y"] (oldest -> newer) console.log(b.getTail(2)); // ["B","Y"] (newest -> older) // Pop console.log(b.popHead()); // "X" console.log(b.popTail(2)); // ["B","Y"] (newest -> older) // Replace all (keeps last capacity if overflow) b.replaceAll(["1", "2", "3", "4"]); console.log(b.getAll()); // ["2","3","4"]
import { createBuffer } from "circular-queue-react"; const buf = createBuffer<number>(3); // returns BufferManager<number> buf.pushTail([1, 2, 3, 4]); // keeps [2,3,4]
useCircularBuffer provides a BufferManager-TAILed stateful hook:
- data auto-updates after mutations
pushHead/pushTail/popHead/popTail/replaceAll/clear/resize
import { useCircularBuffer } from "circular-queue-react"; export function LogViewer() { const { data, pushTail, popHead, clear, size, capacity, available, isFull } = useCircularBuffer<string>(100); return ( <div> <button onClick={() => pushTail(`Log @ ${new Date().toISOString()}`)}> Add Log </button> <button onClick={() => popHead()}>Pop Oldest</button> <button onClick={clear}>Clear</button> <div> {size}/{capacity} (available: {available}) {isFull && "⚠️ FULL"} </div> {data.map((log, i) => ( <div key={i}>{log}</div> ))} </div> ); }
const { data } = useCircularBuffer<number>(10, { initialItems: [1, 2, 3, 4, 5], });
This library uses consistent ordering rules:
getAll()always returns oldest → newestgetHead(n)returns oldest → newergetTail(n)returns newest → olderpopHead(n)removes/returns oldest → newerpopTail(n)removes/returns newest → older
T itself is an array type, the array overload for pushHead/pushTail cannot be used.
TypeScript cannot distinguish between:
T(whenT = number[])readonly T[](which would bereadonly number[][])
Both resolve to array types, making overload resolution ambiguous.
// ❌ PROBLEMATIC: T = number[] const buf = new BufferManager<number[]>(5); // This will fail! TypeScript cannot tell if you mean: // 1. Push a single item (which happens to be an array): number[] // 2. Push multiple items: readonly number[][] buf.pushTail([[1, 2], [3, 4]]); // ✅ SOLUTION: Push one item at a time buf.pushTail([1, 2]); // Push single array buf.pushTail([3, 4]); // Push another single array
When T is an array type, always push items one at a time instead of using the array overload:
const items: number[][] = [[1, 2], [3, 4], [5, 6]]; // ❌ Don't do this: // buf.pushTail(items); // ✅ Do this instead: for (const item of items) { buf.pushTail(item); } // Or use a wrapper type: type Item = { data: number[] }; const typedBuf = new BufferManager<Item>(5); typedBuf.pushTail([ { data: [1, 2] }, { data: [3, 4] } ]); // ✅ Works!
This limitation applies to:
BufferManager.pushHead()BufferManager.pushTail()useCircularBufferhook'spushHeadandpushTail
Direction.HEAD; // head / oldest side Direction.TAIL; // tail / newest side
new CircularBuffer<T>(capacity: number)
-
push(item: T, direction: Direction): void -
pop(direction: Direction): T | undefined -
get(direction: Direction): T | undefined -
get(direction: Direction, count: number): T[]HEAD count: oldest → newer, TAIL count: newest → older -
clear(): void -
resize(newCapacity: number): void(logical capacity) -
getSize(): number -
getCapacity(): number(physical storage) -
getLogicalCapacity(): number -
[Symbol.iterator](): Iterator<T>(oldest → newest)
High-level managed buffer built on top of CircularBuffer.
pushHead(item: T): voidpushHead(items: readonly T[]): voidpushTail(item: T): voidpushTail(items: readonly T[]): void
popHead(): T | undefinedpopHead(count: number): T[](oldest → newer)popTail(): T | undefinedpopTail(count: number): T[](newest → older)
getHead(): T | undefinedgetHead(count: number): T[](oldest → newer)getTail(): T | undefinedgetTail(count: number): T[](newest → older)getAll(): T[](oldest → newest)
clear(): voidresize(newCapacity: number): voidreplaceAll(items: readonly T[]): voidsize(): numbercapacity(): numberisEmpty(): booleanisFull(): booleanavailable(): number
getFirstAndLast(): { first: T | undefined; last: T | undefined }getInfo(): { data: T[]; totalCount: number }forEach(cb): voidmap(cb): U[]filter(cb): T[]Iterable(oldest → newest)
function useCircularBuffer<T>( capacity: number, options?: { initialItems?: readonly T[] } ): { data: T[]; pushHead: (input: T | readonly T[]) => void; pushTail: (input: T | readonly T[]) => void; popHead: { (): T | undefined; (count: number): T[] }; popTail: { (): T | undefined; (count: number): T[] }; getHead: () => T | undefined; getTail: () => T | undefined; clear: () => void; replaceAll: (items: readonly T[]) => void; resize: (newCapacity: number) => void; size: number; capacity: number; isEmpty: boolean; isFull: boolean; available: number; getFirstAndLast: () => { first: T | undefined; last: T | undefined }; // advanced: manager: BufferManager<T>; };
import { useCircularBuffer } from "circular-queue-react"; type LogEntry = { ts: number; level: "info" | "warn" | "error"; message: string; }; export function LogViewer() { const { data, pushTail, clear, size, isFull } = useCircularBuffer<LogEntry>(1000); const add = (level: LogEntry["level"], message: string) => pushTail({ ts: Date.now(), level, message }); const errors = data.filter((x) => x.level === "error"); return ( <div> <h2> Logs ({size}/1000) {isFull && "⚠️ FULL"} </h2> <button onClick={() => add("info", "hello")}>Add</button> <button onClick={() => add("error", "oops")}>Add Error</button> <button onClick={clear}>Clear</button> <h3>Errors: {errors.length}</h3> {data.map((x, i) => ( <div key={i}> [{x.level}] {x.message} </div> ))} </div> ); }
import { BufferManager } from "circular-queue-react"; class RollingAverage { private buf = new BufferManager<number>(5); add(v: number) { this.buf.pushTail(v); } avg() { const a = this.buf.getAll(); return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; } }
| Operation | Complexity |
|---|---|
| push / pop / get (peek) | O(1) |
| get(count) | O(k) |
| getAll / iteration snapshot | O(n) |
| resize | O(n) |
| clear | O(1) |
This package includes comprehensive test coverage using Vitest and React Testing Library.
# Run all tests npm test # Run tests with coverage npm run test:coverage
The project maintains high test coverage:
- CircularBuffer: 100% coverage (32 tests)
- BufferManager: 100% coverage (47 tests)
- React Hook (useCircularBuffer): 100% coverage (29 tests)
Total: 108 tests across 3 test suites
Coverage reports are generated in the coverage/ directory. Open coverage/index.html in your browser to view detailed coverage information.
This project uses GitHub Actions for continuous integration:
- Automated Testing: Tests run on Node.js 18, 20, and 22
- Build Verification: Ensures TypeScript compilation succeeds
- Coverage Reporting: Automatic upload to Codecov
See .github/workflows/ci.yml for the complete CI configuration.
See CHANGELOG.md for version history and release notes.
MIT
PRs are welcome!
If you find a bug or want a feature, please open an issue.