Skip to content

Commit 835e4fa

Browse files
committed
Added ring buffer.
1 parent a135a79 commit 835e4fa

10 files changed

Lines changed: 595 additions & 1 deletion

File tree

‎ARCHITECTURE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ src/ # All source code (ESM)
5050
├── queue.js # Queue — adapter class wrapping a ValueList
5151
├── stack.js # Stack — adapter class wrapping a ValueList
5252
├── deque.js # Deque — double-ended adapter wrapping a ValueList
53+
├── ring-buffer.js # RingBuffer — array-backed deque on a circular buffer
5354
├── skip-list.js # SkipList — probabilistic ordered container
5455
├── list-utils.js # Utility functions: push/append values, find, remove
5556
├── list-helpers.js # Node/range normalization helpers

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Data structures included:
1313
- **Heaps** — min heap, leftist heap, skew heap, pairing heap (O(1) push/merge, decrease-key by handle), indexed heap (O(1) membership, update/remove by handle).
1414
- **Caches** — LRU, LFU, FIFO, random eviction. Includes a decorator for functions, methods, and getters.
1515
- **Queue, Stack, and Deque** — list-backed adapters; the deque adds O(1) both-end operations and `rotate()`.
16+
- **Ring buffer** — array-backed deque on a circular buffer: fastest raw throughput, O(1) random access, optional keep-last-N bounded mode.
1617
- **Splay tree** — self-adjusting binary search tree.
1718
- **Skip list** — probabilistic ordered container: expected O(log n) search/insert/remove, floor/ceil, ordered and range iteration.
1819
- **Utilities** — push/append values, find, remove, validate, and more.

‎bench/bench-queues.js‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Deque from '../src/deque.js';
2+
import RingBuffer from '../src/ring-buffer.js';
3+
4+
const SIZE = 1_000,
5+
CYCLES = 10_000,
6+
data = Array.from({length: SIZE}, (_, i) => i);
7+
8+
// all three share push (back) / shift (front) names
9+
const churn = makeContainer => n => {
10+
let container;
11+
for (let i = 0; i < n; ++i) {
12+
container = makeContainer();
13+
for (let j = 0; j < CYCLES; ++j) {
14+
container.push(j);
15+
container.shift();
16+
}
17+
}
18+
return container;
19+
};
20+
21+
const fillDrain = makeContainer => n => {
22+
let container;
23+
for (let i = 0; i < n; ++i) {
24+
container = makeContainer();
25+
for (let j = 0; j < CYCLES; ++j) container.push(j);
26+
while (container.length || container.size) container.shift();
27+
}
28+
return container;
29+
};
30+
31+
const makeArray = () => data.slice(),
32+
makeDeque = () => {
33+
const deque = new Deque();
34+
deque.pushValuesBack(data);
35+
return deque;
36+
},
37+
makeRing = () => RingBuffer.from(data);
38+
39+
export default {
40+
'churn 1k: Array': churn(makeArray),
41+
'churn 1k: Deque (list)': churn(makeDeque),
42+
'churn 1k: RingBuffer': churn(makeRing),
43+
'fill+drain 10k: Array': fillDrain(() => []),
44+
'fill+drain 10k: Deque (list)': fillDrain(() => new Deque()),
45+
'fill+drain 10k: RingBuffer': fillDrain(() => new RingBuffer())
46+
};

‎llms-full.txt‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,54 @@ Accepts a list instance (adopted) or a list class (instantiated). The list must
665665

666666
---
667667

668+
## Module: RingBuffer (`list-toolkit/ring-buffer.js`)
669+
670+
Array-backed double-ended queue on a circular buffer — contiguous, O(1) at both ends, O(1) random access, no allocation at steady state. Shares the Deque API (drop-in replacements for each other); measured ~5× faster than Array/list-deque on steady-state queue churn.
671+
672+
```js
673+
import RingBuffer from 'list-toolkit/ring-buffer.js';
674+
675+
const queue = new RingBuffer();
676+
queue.push(1).push(2);
677+
queue.shift(); // 1 — Array-parity names without Array.shift's O(n)
678+
679+
const lastFive = new RingBuffer({capacity: 5}); // bounded: keeps the last 5 pushed
680+
```
681+
682+
### Constructor
683+
684+
```js
685+
new RingBuffer({capacity?, initialCapacity?} = {})
686+
```
687+
688+
`capacity` — hard bound: pushing onto a full buffer evicts from the opposite end (keep-last-N). Omit for a growable buffer. `initialCapacity` — allocation hint (default 16, rounded to a power of two).
689+
690+
### Properties
691+
692+
- `isEmpty` — true if empty.
693+
- `isFull` — true when a bounded buffer reached capacity.
694+
- `size` — number of elements.
695+
- `capacity` — the bound, or Infinity when growable.
696+
- `front` / `back` — first/last element value.
697+
698+
### Methods
699+
700+
- `peekFront()` / `peekBack()` — first/last value.
701+
- `at(index)` — O(1) random access; negative counts from the back (like `Array#at`).
702+
- `pushFront(value)` — add to front (evicts back when bounded+full). Aliases: `unshift`, `addFront`.
703+
- `pushBack(value)` — add to back (evicts front when bounded+full). Aliases: `push`, `add`, `addBack`.
704+
- `popFront()` — remove from front. Aliases: `shift`, `removeFront`.
705+
- `popBack()` — remove from back. Aliases: `pop`, `removeBack`.
706+
- `pushValuesFront(values)` / `pushValuesBack(values)` — bulk adds.
707+
- `rotate(n = 1)` — Python `deque.rotate` semantics; O(1) when the ring is physically full, else ≤ size/2 moves.
708+
- `clear()` — remove all (keeps the allocation, releases references).
709+
- `[Symbol.iterator]()` / `getReverseIterator()` — iterate front-to-back / back-to-front.
710+
- `RingBuffer.from(values, options?)` — create from an iterable.
711+
712+
Popped slots are cleared, so the ring never pins dead references. Choose the list-backed `Deque` when stable node identity matters; choose `RingBuffer` for throughput.
713+
714+
---
715+
668716
## Module: SplayTree (`list-toolkit/tree/splay-tree.js`)
669717

670718
Self-adjusting binary search tree. Frequently accessed elements move to the root.

‎llms.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ All heaps support `less` or `compare` for ordering. Common API: `push(value)`, `
7676
- **Queue** (`list-toolkit/queue.js`) — FIFO queue adapter. Methods: `add`/`push`/`enqueue`, `remove`/`pop`/`dequeue`, `peek`, `top`, `clear`.
7777
- **Stack** (`list-toolkit/stack.js`) — LIFO stack adapter. Methods: `push`, `pop`, `peek`, `top`, `clear`.
7878
- **Deque** (`list-toolkit/deque.js`) — double-ended queue adapter. Methods: `pushFront`/`unshift`, `pushBack`/`push`, `popFront`/`shift`, `popBack`/`pop`, `peekFront`/`peekBack`, `rotate(n)`, `clear`. O(1) at both ends.
79+
- **RingBuffer** (`list-toolkit/ring-buffer.js`) — array-backed deque on a circular buffer, same API as Deque plus `at(index)` (O(1) random access) and optional `{capacity}` bounded keep-last-N mode. Fastest queue/deque for raw throughput.
7980

8081
### Trees
8182

‎src/ring-buffer.d.ts‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/** Options for configuring a ring buffer. */
2+
export interface RingBufferOptions {
3+
/** Hard bound: pushing onto a full buffer evicts from the opposite end (keep-last-N semantics). Omit for an unbounded, growable buffer. */
4+
capacity?: number;
5+
/** Initial slot allocation hint for the growable mode (default 16; rounded up to a power of two). */
6+
initialCapacity?: number;
7+
}
8+
9+
/**
10+
* Array-backed double-ended queue on a circular buffer: contiguous storage,
11+
* O(1) push/pop/peek at both ends, O(1) random access via `at()`, and no
12+
* allocation at steady state. Shares the `Deque` API (including the Array-parity
13+
* aliases `push`/`pop`/`shift`/`unshift`), so the two are drop-in replacements
14+
* for each other; the list-backed `Deque` keeps stable node identity, the ring
15+
* buffer wins on throughput and memory locality.
16+
*
17+
* With `{capacity}` the buffer is bounded: pushing onto a full buffer evicts the
18+
* element at the opposite end — a "keep the last N" sliding window.
19+
*/
20+
export class RingBuffer<V = unknown> {
21+
/** Hard bound, or `Infinity` when growable. */
22+
capacity: number;
23+
/** The backing array (a power-of-two ring). */
24+
array: (V | undefined)[];
25+
/** Index of the front element in `array`. */
26+
head: number;
27+
/** Number of elements. */
28+
size: number;
29+
30+
/** @param options - Capacity semantics. */
31+
constructor(options?: RingBufferOptions);
32+
33+
/** Whether the buffer has no elements. */
34+
get isEmpty(): boolean;
35+
36+
/** Whether the buffer reached its `capacity` (always `false` when growable). */
37+
get isFull(): boolean;
38+
39+
/** The front element without removing it, or `undefined` if empty. */
40+
get front(): V | undefined;
41+
42+
/** The back element without removing it, or `undefined` if empty. */
43+
get back(): V | undefined;
44+
45+
/** The front element without removing it, or `undefined` if empty. */
46+
peekFront(): V | undefined;
47+
48+
/** The back element without removing it, or `undefined` if empty. */
49+
peekBack(): V | undefined;
50+
51+
/**
52+
* Random access by position in O(1). Negative indices count from the back
53+
* (like `Array.prototype.at`).
54+
* @param index - Position from the front (or from the back when negative).
55+
* @returns The element, or `undefined` when out of range.
56+
*/
57+
at(index: number): V | undefined;
58+
59+
/**
60+
* Add a value to the front. Evicts the back element when bounded and full.
61+
* @param value - Value to add.
62+
* @returns `this` for chaining.
63+
*/
64+
pushFront(value: V): this;
65+
66+
/** Alias for {@link pushFront}. */
67+
unshift(value: V): this;
68+
69+
/** Alias for {@link pushFront}. */
70+
addFront(value: V): this;
71+
72+
/**
73+
* Add a value to the back. Evicts the front element when bounded and full.
74+
* @param value - Value to add.
75+
* @returns `this` for chaining.
76+
*/
77+
pushBack(value: V): this;
78+
79+
/** Alias for {@link pushBack}. */
80+
push(value: V): this;
81+
82+
/** Alias for {@link pushBack}. */
83+
add(value: V): this;
84+
85+
/** Alias for {@link pushBack}. */
86+
addBack(value: V): this;
87+
88+
/**
89+
* Remove and return the front value.
90+
* @returns The front value, or `undefined` if empty.
91+
*/
92+
popFront(): V | undefined;
93+
94+
/** Alias for {@link popFront}. */
95+
shift(): V | undefined;
96+
97+
/** Alias for {@link popFront}. */
98+
removeFront(): V | undefined;
99+
100+
/**
101+
* Remove and return the back value.
102+
* @returns The back value, or `undefined` if empty.
103+
*/
104+
popBack(): V | undefined;
105+
106+
/** Alias for {@link popBack}. */
107+
pop(): V | undefined;
108+
109+
/** Alias for {@link popBack}. */
110+
removeBack(): V | undefined;
111+
112+
/**
113+
* Add multiple values to the front (in iteration order, so they end up reversed).
114+
* @param values - Iterable of values.
115+
* @returns `this` for chaining.
116+
*/
117+
pushValuesFront(values: Iterable<V>): this;
118+
119+
/**
120+
* Add multiple values to the back.
121+
* @param values - Iterable of values.
122+
* @returns `this` for chaining.
123+
*/
124+
pushValuesBack(values: Iterable<V>): this;
125+
126+
/**
127+
* Rotate: positive `n` moves `n` elements from the back to the front (Python
128+
* `deque.rotate` semantics), negative from the front to the back. O(1) when the
129+
* ring is physically full (a head shift), otherwise at most `size / 2` moves.
130+
* @param n - Number of positions (default 1).
131+
* @returns `this` for chaining.
132+
*/
133+
rotate(n?: number): this;
134+
135+
/**
136+
* Remove all elements (keeps the current allocation).
137+
* @returns `this` for chaining.
138+
*/
139+
clear(): this;
140+
141+
/**
142+
* Grow the backing array (doubling). Called automatically; exposed for pre-sizing.
143+
* @returns `this` for chaining.
144+
*/
145+
grow(): this;
146+
147+
/** Iterate over values from front to back. */
148+
[Symbol.iterator](): IterableIterator<V>;
149+
150+
/**
151+
* Iterate over values from back to front.
152+
* @returns An iterable of values.
153+
*/
154+
getReverseIterator(): Iterable<V>;
155+
156+
/**
157+
* Build a RingBuffer from an iterable.
158+
* @param values - Values to add at the back.
159+
* @param options - Capacity semantics.
160+
* @returns A new RingBuffer.
161+
*/
162+
static from<V = unknown>(values: Iterable<V>, options?: RingBufferOptions): RingBuffer<V>;
163+
}
164+
165+
export default RingBuffer;

0 commit comments

Comments
 (0)