Metautil Data Structures
Import from the package root. Prefer these ADTs over ad-hoc Array/Object when the discipline (FIFO, LIFO, persistence, schema) matters.
const metautil = require('metautil');
const { ConsList, cons, uncons, Struct, Deque, Queue, Stack } = metautil;
const { List, ListNode, CircularBuffer, UnrolledList, Trie } = metautil;
Choose the right structure
- FIFO jobs / BFS / sliding failure window:
Queue - LIFO undo / nested layers / brackets:
Stack - Both ends O(1), capped window:
DequeorCircularBuffer - Index + mid-sequence edit/move/group:
List - Immutable history / branching / sharing:
ConsList - Typed sealed/frozen records:
Struct - Prefix autocomplete / string keys:
Trie - Extreme enqueue/dequeue volume only:
UnrolledList(no peek / interop) - Random access on a ring:
CircularBuffer.at(notDeque)
Complexity and surface
CircularBuffer— ends O(1), index O(1) viaatDeque/Queue/Stack— ends O(1); no index (atonly on buffer)UnrolledList— ends O(1); no peek / interopList— ends O(1), index O(n)ConsList—prependO(1), walk O(n)Trie— string-key prefix map
Who has what (beyond size / isEmpty where present):
fromArray/toArray/ iterator: all exceptUnrolledList(andTrie)peek:Queue,Stackat:CircularBufferonlyevery/reduce:CircularBuffer,Deque,Stack(notQueue)clear/includes: ring family +List+Trie.clear; notConsList/UnrolledList
Interoperability
Convert through Array:
const list = List.fromArray([1, 2, 3, 4, 5]);
const queue = Queue.fromArray(list.filter((n) => n % 2 === 0).toArray());
const consList = ConsList.fromArray(queue.toArray());
Deque, Queue, and Stack are thin facades over CircularBuffer (same ring storage).
Struct
Schema is inferred from default literals. Immutable uses Object.freeze; mutable uses Object.seal. Class name becomes constructor.name. Statics: fields, schema, mutable.
undefined: schemaunknown, accepts anythingnull: schemaref, accepts null, object, function[]/{}: schemaarray/object, accepts arrays / plain objects (fresh copy per instance)- primitive: schema
typeof, accepts that primitive
null (ref) ≠ {} (object); arrays fail object and plain objects fail array.
const Money = Struct.immutable('Money', { currency: 'USD', amountCents: 0 });
const a = Money.create({ amountCents: 500 });
const b = a.fork({ amountCents: 700 }); // new frozen record
// a.update(...) throws; use fork or branch
const Reading = Struct.mutable('Reading', {
deviceId: '',
millicelsius: 0,
processed: false,
});
const r = new Reading({ deviceId: 'sensor-7' });
r.update({ processed: true }); // in-place, mutable only
fork(updates)— full shallow copy viatoObject()+ updates (independent instance)branch(updates)—Object.create(this)overlay; cheap sharing of unset fields- On a mutable branch,
updateonly works for fields the branch itself defined - Unknown field or wrong type throws
TypeError
ConsList
Immutable singly-linked cons cells with structural sharing. prepend is O(1); never mutate. ConsList.empty is a singleton (===).
const shared = ConsList.of(3, 4, 5);
const branch1 = shared.prepend(2).prepend(1); // [1, 2, 3, 4, 5]
const branch2 = shared.prepend(99); // [99, 3, 4, 5]
// branch1.tail.tail === shared; branch2.tail === shared
const { value, tail } = branch1.uncons(); // same as uncons(branch1)
const rebuilt = cons(value, tail); // same as tail.prepend(value)
let history = ConsList.of('draft v1');
history = history.prepend('draft v2');
const undone = history.tail; // O(1) restore; suffix still shared
merge(...lists)— join in argument order; shares the last list as suffix; O(n) over all but the lastmember(value)— first shared suffix whose head===value, orempty(not a boolean)equals/includes— element-wise===- Also:
of/fromArray/fromIterable,map/filter/find/some/every,reverse,toArray - Empty
reducewithout seed throwsTypeError; emptyeveryistrue
CircularBuffer / Deque / Queue / Stack
Growable power-of-two ring (initial capacity 16, doubles on full). End ops O(1); CircularBuffer.at(i) is O(1) (-1 = last). clear() resets capacity to initial.
const WINDOW = 5;
const trace = new Deque();
trace.push(point);
if (trace.size > WINDOW) trace.shift();
const q = Queue.fromArray(jobs);
q.enqueue(job);
const next = q.dequeue(); // undefined if empty
q.peek();
const undo = new Stack();
undo.push(command);
const last = undo.pop();
undo.peek();
CircularBuffer/Deque:unshift/push,shift/pop; index only on buffer viaatQueue:enqueue/dequeue/peek(front)Stack:push/pop/peek(back)- Empty
everyistrue; emptyreducewithout seed throwsTypeError
List / ListNode
Mutable doubly-linked sequence. Ends O(1); index ops O(n). Public API is value/index based. Non-integer indexes are no-ops; negatives count from the end.
const route = List.of(a, b, c);
route.insert(1, rush);
route.move(from, to);
route.rotate(1); // default 1; positive = left
const { before, after } = route.splitAt(2);
const byZone = route.groupBy((stop) => stop.zone);
const removed = route.remove(x, y); // count removed
route.sum((x) => x.cents);
Mutating vs copying:
- Mutate:
append/prepend/insert/delete/drop/rotate/swap/move/reverse/sort/replace/clear - Copy:
take/slice/splitAt/map/flatMap/filter/toReversed/toSorted/clone/merge take/slicereturnnullfor empty range,take(0), or bad bounds;dropmutates in placeinsertclamps out-of-range indexes;delete/swap/moveout-of-range are no-opsreduce(fn, initial)— initial is required (unlike ConsList / CircularBuffer)avgon empty is0;min/maxon empty areundefined- Also:
at/set,indexOf/lastIndexOf,includes,some/every/find/findIndex
ListNode is low-level (create / append / prepend / unlink / seek / fromArray / copy / link) for custom structures that own head/tail/size. seek non-integer returns null. Do not mutate prev/next of nodes owned by a List.
UnrolledList
High-throughput FIFO: pooled fixed-size array nodes. Use when volume is high and you only need enqueue/dequeue.
const pending = new UnrolledList({ nodeSize: 256, poolSize: 4 });
pending.enqueue(task);
let task = pending.dequeue();
while (task !== undefined) {
task();
task = pending.dequeue();
}
- Defaults:
nodeSize: 1024,poolSize: 2 - Raise
nodeSizefor larger bursts; raisepoolSizeif drain/refill churns nodes - No
peek,fromArray, iterator, orincludes enqueue(undefined)is a real item — emptydequeue()is indistinguishable from that value
Trie
Prefix tree for string keys.
const trie = new Trie();
trie.insert('cat');
trie.insert('car', 42);
trie.insert('gone', undefined);
trie.has('gone'); // true — present; get returns undefined
trie.get('car'); // 42
trie.complete('ca'); // order may vary
trie.delete('car'); // prunes empty branches
- Default value is
truewhen omitted; re-insert same key does not bumpsize - Empty string
''is a valid key insertnon-string throwsTypeError;has/get/deletenon-string soft-fail (false/undefined/false)- Use
hasto distinguish missing from storedundefined
Contracts
- Empty
pop/dequeue/shiftreturnundefined - Empty
reducewithout seed throwsTypeError(ConsList / CircularBuffer family); List always needs a seed - Equality /
includes/memberuse strict===(NaNdoes not match) - Prefer structure APIs over inventing Array adapters; convert via
toArray/fromArraywhen crossing types