@console-one/collections
v0.2.1
Published
A home for collection data structures with different referencing and observation techniques. Ships LinkedQueue, ObservableQueue, IndexMap, LinkedHeap, SingleEntryObject, and HeapMap.
Maintainers
Readme
@console-one/collections
A home for collection data structures with different referencing and observation techniques. Package grows over time as new queue variants and collection types are needed.
Current contents
| Class | File | Purpose |
|---|---|---|
| LinkedQueue<T> | src/linked-queue.ts | Plain doubly-linked FIFO queue. No events, no insert-at-node. Use when you just want a fast queue. |
| ObservableQueue<T> | src/observable-queue.ts | Doubly-linked FIFO queue with on(event, handler) push listeners and insert(data).before(node) / .after(node) for positional insertion. Use when you need to react to pushes or splice items by node reference. |
| IndexMap<T> | src/index-map.ts | Map keyed by a monotonic counter — keys are unique and never reused. lock() reserves a key before the value is ready; set(key, value) fills it later. indexLock() returns an IndexLock<T> handle carrying a back-reference so a listener can un-register itself from inside its own closure. Useful for subscription IDs, allocation slots, and self-removing callbacks. |
| LinkedHeap<T> | src/linked-heap.ts | Key-addressable binary heap whose items are exposed as HeapNode handles. Callers can update(data) in-place (preserving identity across rebalance), remove(key) by key, detach a node without reshaping, and async-iterate neighbors via nextNode() / prevNode(). Suitable for schedulers, priority queues whose priorities change, and reactive UIs. |
Future variants planned: different backing structures (array ring, chunked), different reference techniques (weak refs, handle-based), different observation models (per-item subscriptions, batched flush events).
Install
npm install @console-one/collectionsUsage
LinkedQueue — the minimal case
import { LinkedQueue } from '@console-one/collections'
const q = LinkedQueue.of(1, 2, 3)
q.shift() // 1
q.shift() // 2
q.peak() // 3 (peek without removing)
q.length // 1ObservableQueue — when you need push hooks or node-positional inserts
import { ObservableQueue } from '@console-one/collections'
const q = new ObservableQueue<string>()
// React to pushes
q.on('push', (unsub) => (newNodes, queue) => {
console.log('pushed', newNodes.length, 'items; queue size:', queue.length)
})
q.push('alpha')
q.push('beta', 'gamma')
// Splice by node reference
q.push(1, 2, 4) // [1, 2, 4]
const lastNode = q.last!
q.insert(3).before(lastNode) // [1, 2, 3, 4]IndexMap — stable handles before values exist
import { IndexMap } from '@console-one/collections'
const subs = new IndexMap<(msg: string) => void>()
const id = subs.store((msg) => console.log(msg))
// ... later
subs.delete(id) // keys are never reused — safe to hand to external code
// Reserve a slot, fill it later:
const slot = subs.lock()
// ... wiring happens, the handler is built
subs.set(slot, (msg) => console.log('late', msg))
// IndexLock — self-unsubscribing listener
const lock = subs.indexLock()
lock.set((msg) => {
if (msg === 'done') lock.remove() // callback un-registers itself
else handle(msg)
})LinkedHeap — priority queue with stable handles
import { LinkedHeap, HeapNode } from '@console-one/collections'
const heap = new LinkedHeap<{ id: string; priority: number }>(
(a, b) => a.data.priority - b.data.priority,
(data) => data.id, // mapper: derive the key from the value
)
const job = heap.push({ id: 'job-1', priority: 5 })
heap.push({ id: 'job-2', priority: 1 })
heap.peak() // { id: 'job-2', priority: 1 }
job.update({ id: 'job-1', priority: 0 }) // in-place reprioritize
heap.peak() // { id: 'job-1', priority: 0 }
// Async neighbor iteration — yields existing then future pushes
for await (const neighbor of heap.startNode!.nextNode()) {
console.log('neighbor', neighbor.data)
}Subpath imports
You can import each class directly if you want to avoid loading the others:
import { LinkedQueue } from '@console-one/collections/linked-queue'
import { ObservableQueue } from '@console-one/collections/observable-queue'
import { IndexMap, IndexLock } from '@console-one/collections/index-map'
import { LinkedHeap, HeapNode } from '@console-one/collections/linked-heap'Shared API
Both queues implement the same base FIFO interface. They only differ in the extras that ObservableQueue adds.
Common methods
| Method | Description |
|---|---|
| push(...items) | Append one or more items to the tail. Returns a queue of any items that were flushed due to maxSize overflow. |
| prepend(item) | Insert an item at the head. |
| shift() / pull() | Remove and return the head item. Applies pipe option if configured. Throws if empty. |
| remove() | Like shift() but without applying pipe. Throws if empty. |
| peak() | Return the head item without removing it. Throws if empty. |
| isEmpty() | True iff there are no items. |
| length / size | Current number of items. |
| concat(other) | Append all items from another queue. |
| clear() | Remove everything. Chainable. |
| toArray() | Materialize as a JS array, head first. |
| toString() | Pretty-printed JSON of toArray(). |
| headIs(pred) | Safe predicate check on the head; returns false if empty. |
| flush(n) | Generator that pops up to n items. |
| readHead(n?) | Generator over head-first items without consuming. |
| readTail(n?) | Generator over tail-first items without consuming. |
| [Symbol.iterator]() | Iterable, head first. |
| static describes(x) | Type guard for instances of this queue class. |
| static fromArray(arr) | Construct from an array. |
| static of(...items) | Construct from varargs. |
Constructor options
Both queues accept the same options:
new LinkedQueue<T>(maxSize?: number, { pipe?, chain? })maxSize— bound the queue. Items past the bound are flushed from the head. DefaultNumber.POSITIVE_INFINITY.pipe— transform applied onshift()/pull(). Leavesremove()untouched.chain— factory for the "overflow" queue returned bypush()when a bounded queue discards items.
ObservableQueue extras
| Method | Description |
|---|---|
| on(event, create) | Subscribe to a queue event ('push'). create(unsub) must return the actual handler; you get an unsubscribe function to close over. |
| insert(data).before(node) / .after(node) | Splice a new item at a specific node position. Takes a QueueItem reference (from q.first, q.last, or walking .next / .prev). |
Known quirks
nodes()and some iterator loops use a!==vs||mix that can look infinite at a glance. They terminate becausenextbecomesundefined. Don't usenodes()as a general-purpose iterator — usereadHead()/[Symbol.iterator]instead.maxSizeoverflow pushes into a secondary "chain" queue rather than silently dropping. The chain is created fresh on eachpush()call, so you don't accumulate one global overflow buffer.pipeonly runs onshift()/pull(), not onremove()or iterators.
Tests
npm test46 tests covering FIFO semantics, insertion, options, overflow, event hooks, index-map lock/set/delete + IndexLock self-unsubscribe, and linked-heap push/pop/update/remove/async iteration.
