···11+import type { CidLink } from '@atcute/cid';
22+33+import { MSTNode, getKeyHeight } from './node.js';
44+import { NodeStore } from './node-store.js';
55+import Stack from './utils/stack.js';
66+77+interface StackFrame {
88+ node: MSTNode;
99+ lpath: string;
1010+ rpath: string;
1111+ idx: number;
1212+}
1313+1414+/**
1515+ * NodeWalker makes implementing tree diffing and other MST query ops more
1616+ * convenient (but it does not, itself, implement them).
1717+ *
1818+ * A NodeWalker starts off at the root of a tree, and can walk along or recurse
1919+ * down into subtrees.
2020+ *
2121+ * Walking "off the end" of a subtree brings you back up to its next non-empty parent.
2222+ *
2323+ * Recall MSTNode layout:
2424+ *
2525+ * ```
2626+ * keys: (lpath) (0, 1, 2, 3) (rpath)
2727+ * vals: (0, 1, 2, 3)
2828+ * subtrees: (0, 1, 2, 3, 4)
2929+ * ```
3030+ */
3131+export class NodeWalker {
3232+ static readonly PATH_MIN = ''; // string that compares less than all legal path strings
3333+ static readonly PATH_MAX = '\xff'; // string that compares greater than all legal path strings
3434+3535+ private store: NodeStore;
3636+ private stack: Stack<StackFrame>;
3737+ private rootHeight: number;
3838+ private trusted: boolean;
3939+4040+ private constructor(store: NodeStore, stack: Stack<StackFrame>, rootHeight: number, trusted: boolean) {
4141+ this.store = store;
4242+ this.stack = stack;
4343+ this.rootHeight = rootHeight;
4444+ this.trusted = trusted;
4545+ }
4646+4747+ /**
4848+ * create a new NodeWalker
4949+ * @param store NodeStore to fetch nodes from
5050+ * @param rootCid CID of the root node to start walking from
5151+ * @param lpath left boundary path (defaults to minimum)
5252+ * @param rpath right boundary path (defaults to maximum)
5353+ * @param trusted skip height validation checks if true (faster but unsafe for untrusted trees)
5454+ * @param rootHeight pre-computed root height (optional optimization)
5555+ * @returns a new NodeWalker instance
5656+ */
5757+ static async create(
5858+ store: NodeStore,
5959+ rootCid: string | null,
6060+ lpath: string = NodeWalker.PATH_MIN,
6161+ rpath: string = NodeWalker.PATH_MAX,
6262+ trusted: boolean = false,
6363+ rootHeight?: number,
6464+ ): Promise<NodeWalker> {
6565+ const node = await store.get(rootCid);
6666+ const height = rootHeight ?? (await node.height());
6767+6868+ if (height === null) {
6969+ throw new Error(`indeterminate node height; provide rootHeight if known`);
7070+ }
7171+7272+ const stack = new Stack<StackFrame>();
7373+ stack.push({
7474+ node,
7575+ lpath,
7676+ rpath,
7777+ idx: 0,
7878+ });
7979+8080+ return new NodeWalker(store, stack, height, trusted);
8181+ }
8282+8383+ /**
8484+ * create a new walker rooted at the current position's subtree.
8585+ * treats the subtree as an independent tree for traversal.
8686+ * @returns a new NodeWalker instance for the subtree
8787+ */
8888+ async createSubtreeWalker(): Promise<NodeWalker> {
8989+ return await NodeWalker.create(
9090+ this.store,
9191+ this.subtree?.$link ?? null,
9292+ this.lpath,
9393+ this.rpath,
9494+ this.trusted,
9595+ this.height - 1,
9696+ );
9797+ }
9898+9999+ /** current stack frame (internal) */
100100+ get frame(): StackFrame {
101101+ const frame = this.stack.peek();
102102+ if (frame === undefined) {
103103+ throw new Error(`stack is empty`);
104104+ }
105105+106106+ return frame;
107107+ }
108108+109109+ /** current height in the tree (decreases as you descend) */
110110+ get height(): number {
111111+ return this.rootHeight - (this.stack.size - 1);
112112+ }
113113+114114+ /** key/path to the left of current cursor position */
115115+ get lpath(): string {
116116+ return this.frame.idx === 0 ? this.frame.lpath : this.frame.node.keys[this.frame.idx - 1];
117117+ }
118118+119119+ /** value (CID) to the left of current cursor position */
120120+ get lval(): CidLink | null {
121121+ return this.frame.idx === 0 ? null : this.frame.node.values[this.frame.idx - 1];
122122+ }
123123+124124+ /** subtree CID at current cursor position (null if no subtree) */
125125+ get subtree(): CidLink | null {
126126+ const subtree = this.frame.node.subtrees[this.frame.idx];
127127+ return subtree ?? null;
128128+ }
129129+130130+ /** key/path to the right of current cursor position */
131131+ get rpath(): string {
132132+ return this.frame.idx === this.frame.node.keys.length
133133+ ? this.frame.rpath
134134+ : this.frame.node.keys[this.frame.idx];
135135+ }
136136+137137+ /** value (CID) to the right of current cursor position */
138138+ get rval(): CidLink | null {
139139+ return this.frame.idx === this.frame.node.values.length ? null : this.frame.node.values[this.frame.idx];
140140+ }
141141+142142+ /** whether the walker has reached the end of the tree */
143143+ get done(): boolean {
144144+ // is (not this.stack) really necessary here? is that a reachable state?
145145+ const bottom = this.stack.peekBottom();
146146+ return (
147147+ this.stack.size === 0 || (this.subtree === null && bottom !== undefined && this.rpath === bottom.rpath)
148148+ );
149149+ }
150150+151151+ /** whether the cursor can move right in current node */
152152+ get canGoRight(): boolean {
153153+ return this.frame.idx + 1 < this.frame.node.subtrees.length;
154154+ }
155155+156156+ /**
157157+ * move cursor right, or up if at end of current node.
158158+ * automatically recurses up through empty intermediates.
159159+ * @throws if attempting to navigate beyond root (check done first)
160160+ */
161161+ rightOrUp(): void {
162162+ if (!this.canGoRight) {
163163+ // we reached the end of this node, go up a level
164164+ this.stack.pop();
165165+ if (this.stack.size === 0) {
166166+ throw new Error(`cannot navigate beyond root; check .done before calling`);
167167+ }
168168+ return this.rightOrUp(); // we need to recurse, to skip over empty intermediates on the way back up
169169+ }
170170+ this.frame.idx += 1;
171171+ }
172172+173173+ /**
174174+ * move cursor right within current node.
175175+ * @throws if already at rightmost position (check canGoRight first)
176176+ */
177177+ right(): void {
178178+ if (!this.canGoRight) {
179179+ throw new Error(`cursor is already at rightmost position in node`);
180180+ }
181181+ this.frame.idx += 1;
182182+ }
183183+184184+ /**
185185+ * descend into the subtree at current cursor position.
186186+ * @throws if no subtree exists at current position
187187+ */
188188+ async down(): Promise<void> {
189189+ const subtree = this.frame.node.subtrees[this.frame.idx];
190190+ if (subtree === null) {
191191+ throw new Error(`cannot descend; no subtree at current position`);
192192+ }
193193+194194+ const subtreeNode = await this.store.get(subtree.$link);
195195+196196+ if (!this.trusted) {
197197+ // if we "trust" the source we can elide this check
198198+ // the "null" case occurs for empty intermediate nodes
199199+ const subtreeHeight = await subtreeNode.height();
200200+ if (subtreeHeight !== null && subtreeHeight !== this.height - 1) {
201201+ throw new Error(`inconsistent subtree height; got=${subtreeHeight} expected=${this.height - 1}`);
202202+ }
203203+ }
204204+205205+ this.stack.push({
206206+ node: subtreeNode,
207207+ lpath: this.lpath,
208208+ rpath: this.rpath,
209209+ idx: 0,
210210+ });
211211+ }
212212+213213+ /**
214214+ * advance to and return the next key-value pair in the tree.
215215+ * descends into all subtrees automatically.
216216+ * @returns Tuple of [key, value CID]
217217+ */
218218+ async nextEntry(): Promise<[string, CidLink]> {
219219+ while (this.subtree) {
220220+ // recurse down every subtree
221221+ await this.down();
222222+ }
223223+ this.rightOrUp();
224224+ return [this.lpath, this.lval!]; // the kv pair we just jumped over
225225+ }
226226+227227+ /**
228228+ * iterate over all key-value pairs in the tree in sorted order.
229229+ * @yields Tuples of [key, value CID]
230230+ */
231231+ async *entries(): AsyncIterableIterator<[string, CidLink]> {
232232+ while (!this.done) {
233233+ yield await this.nextEntry();
234234+ }
235235+ }
236236+237237+ /**
238238+ * iterate over all MST nodes from current position to the end of tree.
239239+ * @yields MSTNode instances
240240+ */
241241+ async *nodes(): AsyncIterableIterator<MSTNode> {
242242+ yield this.frame.node;
243243+244244+ while (!this.done) {
245245+ while (this.subtree) {
246246+ // recurse down every subtree
247247+ await this.down();
248248+ yield this.frame.node;
249249+ }
250250+251251+ this.rightOrUp();
252252+ }
253253+ }
254254+255255+ /**
256256+ * iterate over CIDs of all MST nodes from current position to end of tree.
257257+ * @yields CID links to nodes
258258+ */
259259+ async *nodeCids(): AsyncIterableIterator<CidLink> {
260260+ for await (const node of this.nodes()) {
261261+ yield await node.cid();
262262+ }
263263+ }
264264+265265+ /**
266266+ * iterate over key-value pairs within a specific key range.
267267+ * @param start start key (inclusive)
268268+ * @param end end key
269269+ * @param endInclusive whether end key is inclusive
270270+ * @yields tuples of [key, value CID] within range
271271+ */
272272+ async *entriesInRange(
273273+ start: string,
274274+ end: string,
275275+ endInclusive: boolean = false,
276276+ ): AsyncIterableIterator<[string, CidLink]> {
277277+ while (true) {
278278+ while (this.rpath < start) {
279279+ this.rightOrUp();
280280+ }
281281+ if (!this.subtree) {
282282+ break;
283283+ }
284284+ await this.down();
285285+ }
286286+287287+ for await (const [k, v] of this.entries()) {
288288+ if (k > end || (!endInclusive && k === end)) {
289289+ break;
290290+ }
291291+ yield [k, v];
292292+ }
293293+ }
294294+295295+ /**
296296+ * search for a specific key (rpath) in the tree.
297297+ * @param rpath key to search for
298298+ * @returns value CID if found, null otherwise
299299+ */
300300+ async findRpath(rpath: string): Promise<CidLink | null> {
301301+ const rpathHeight = await getKeyHeight(rpath);
302302+ while (true) {
303303+ // if the rpath we're looking for is higher than the current cursor,
304304+ // we're never going to find it (i.e. we early-exit)
305305+ if (rpathHeight > this.height) {
306306+ return null;
307307+ }
308308+309309+ while (this.rpath < rpath) {
310310+ // either look for the rpath, or the right point to go down
311311+ if (!this.canGoRight) {
312312+ return null;
313313+ }
314314+315315+ this.right();
316316+ }
317317+318318+ if (this.rpath === rpath) {
319319+ return this.rval; // found it!
320320+ }
321321+322322+ if (!this.subtree) {
323323+ return null; // need to go down, but we can't
324324+ }
325325+326326+ await this.down();
327327+ }
328328+ }
329329+}
···11+import { isBytes, type Bytes } from '@atcute/cbor';
22+import { isCidLink, type CidLink } from '@atcute/cid';
33+44+export interface TreeEntry {
55+ /** count of bytes shared with previous TreeEntry in this Node (if any) */
66+ p: number;
77+ /** remainder of key for this TreeEntry, after "prefixlen" have been removed */
88+ k: Bytes;
99+ /** link to a sub-tree Node at a lower level which has keys sorting after this TreeEntry's key (to the "right"), but before the next TreeEntry's key in this Node (if any) */
1010+ v: CidLink;
1111+ /** next subtree (to the right of leaf) */
1212+ t: CidLink | null;
1313+}
1414+1515+export const isTreeEntry = (value: unknown): value is TreeEntry => {
1616+ if (value === null || typeof value !== 'object') {
1717+ return false;
1818+ }
1919+2020+ const obj = value as Record<string, unknown>;
2121+2222+ return (
2323+ typeof obj.p === 'number' && isBytes(obj.k) && isCidLink(obj.v) && (obj.t === null || isCidLink(obj.t))
2424+ );
2525+};
2626+2727+export interface NodeData {
2828+ /** link to sub-tree Node on a lower level and with all keys sorting before keys at this node */
2929+ l: CidLink | null;
3030+ /** ordered list of TreeEntry objects */
3131+ e: TreeEntry[];
3232+}
3333+3434+export const isNodeData = (value: unknown): value is NodeData => {
3535+ if (value === null || typeof value !== 'object') {
3636+ return false;
3737+ }
3838+3939+ const obj = value as Record<string, unknown>;
4040+4141+ return (obj.l === null || isCidLink(obj.l)) && Array.isArray(obj.e) && obj.e.every(isTreeEntry);
4242+};
+232
packages/utilities/mst/lib/utils/lru.ts
···11+interface LRUNode<K, V> {
22+ key: K;
33+ value: V;
44+ prev: LRUNode<K, V> | null;
55+ next: LRUNode<K, V> | null;
66+}
77+88+/**
99+ * a least recently used (LRU) cache with fixed capacity
1010+ * evicts the least recently used items when capacity is exceeded
1111+ */
1212+class LRUCache<K, V> {
1313+ readonly #size: number;
1414+ #count = 0;
1515+1616+ #map = new Map<K, LRUNode<K, V>>();
1717+ #head: LRUNode<K, V> | null = null;
1818+ #tail: LRUNode<K, V> | null = null;
1919+2020+ /**
2121+ * creates a new LRU cache with the specified capacity
2222+ * @param size the maximum number of items the cache can hold
2323+ */
2424+ constructor(size: number) {
2525+ this.#size = size;
2626+ }
2727+2828+ /** the maximum capacity of the cache */
2929+ get size(): number {
3030+ return this.#size;
3131+ }
3232+3333+ /**
3434+ * gets a value without affecting its position in the cache
3535+ * @param key the key to look up
3636+ * @returns the value associated with the key, or undefined if not found
3737+ */
3838+ peek(key: K): V | undefined {
3939+ const node = this.#map.get(key);
4040+ if (node === undefined) {
4141+ return undefined;
4242+ }
4343+4444+ return node.value;
4545+ }
4646+4747+ /**
4848+ * gets a value and marks it as most recently used
4949+ * @param key the key to look up
5050+ * @returns the value associated with the key, or undefined if not found
5151+ */
5252+ get(key: K): V | undefined {
5353+ const node = this.#map.get(key);
5454+ if (node === undefined) {
5555+ return undefined;
5656+ }
5757+5858+ this.#moveToFront(node);
5959+ return node.value;
6060+ }
6161+6262+ /**
6363+ * stores a value for the given key, marking it as most recently used
6464+ * evicts the least recently used item if the cache is at capacity
6565+ * @param key the key to store
6666+ * @param value the value to associate with the key
6767+ */
6868+ put(key: K, value: V): void {
6969+ {
7070+ const existing = this.#map.get(key);
7171+7272+ if (existing !== undefined) {
7373+ existing.value = value;
7474+ this.#moveToFront(existing);
7575+ return;
7676+ }
7777+ }
7878+7979+ {
8080+ const node: LRUNode<K, V> = { key, value, prev: null, next: null };
8181+ this.#map.set(key, node);
8282+ this.#addToFront(node);
8383+8484+ this.#count++;
8585+ }
8686+8787+ this.#evict();
8888+ }
8989+9090+ /**
9191+ * removes a key from the cache
9292+ * @param key the key to remove
9393+ * @returns true if the key was found and removed, false otherwise
9494+ */
9595+ delete(key: K): boolean {
9696+ const node = this.#map.get(key);
9797+ if (node === undefined) {
9898+ return false;
9999+ }
100100+101101+ this.#map.delete(key);
102102+ this.#removeNode(node);
103103+ this.#count--;
104104+ return true;
105105+ }
106106+107107+ /**
108108+ * removes all items from the cache
109109+ */
110110+ clear(): void {
111111+ this.#map.clear();
112112+ this.#head = null;
113113+ this.#tail = null;
114114+ this.#count = 0;
115115+ }
116116+117117+ /**
118118+ * checks if a key exists in the cache
119119+ * @param key the key to check
120120+ * @returns true if the key exists, false otherwise
121121+ */
122122+ has(key: K): boolean {
123123+ return this.#map.has(key);
124124+ }
125125+126126+ /**
127127+ * iterates over the keys in LRU order (most to least recently used)
128128+ * @returns iterator of keys
129129+ */
130130+ *keys(): IterableIterator<K> {
131131+ let current = this.#head;
132132+ while (current !== null) {
133133+ yield current.key;
134134+ current = current.next;
135135+ }
136136+ }
137137+138138+ /**
139139+ * iterates over the values in LRU order (most to least recently used)
140140+ * @returns iterator of values
141141+ */
142142+ *values(): IterableIterator<V> {
143143+ let current = this.#head;
144144+ while (current !== null) {
145145+ yield current.value;
146146+ current = current.next;
147147+ }
148148+ }
149149+150150+ /**
151151+ * iterates over the key-value pairs in LRU order (most to least recently used)
152152+ * @returns iterator of [key, value] tuples
153153+ */
154154+ *entries(): IterableIterator<[K, V]> {
155155+ let current = this.#head;
156156+ while (current !== null) {
157157+ yield [current.key, current.value];
158158+ current = current.next;
159159+ }
160160+ }
161161+162162+ #moveToFront(node: LRUNode<K, V>): void {
163163+ if (this.#head === node) {
164164+ return;
165165+ }
166166+167167+ if (node.prev !== null) {
168168+ node.prev.next = node.next;
169169+ }
170170+171171+ if (node.next !== null) {
172172+ node.next.prev = node.prev;
173173+ } else {
174174+ this.#tail = node.prev;
175175+ }
176176+177177+ node.prev = null;
178178+ node.next = this.#head;
179179+180180+ // Safe because this method is only called when head exists
181181+ this.#head!.prev = node;
182182+ this.#head = node;
183183+ }
184184+185185+ #addToFront(node: LRUNode<K, V>): void {
186186+ node.next = this.#head;
187187+ node.prev = null;
188188+189189+ if (this.#head !== null) {
190190+ this.#head.prev = node;
191191+ } else {
192192+ this.#tail = node;
193193+ }
194194+195195+ this.#head = node;
196196+ }
197197+198198+ #removeNode(node: LRUNode<K, V>): void {
199199+ if (node.prev !== null) {
200200+ node.prev.next = node.next;
201201+ } else {
202202+ this.#head = node.next;
203203+ }
204204+205205+ if (node.next !== null) {
206206+ node.next.prev = node.prev;
207207+ } else {
208208+ this.#tail = node.prev;
209209+ }
210210+ }
211211+212212+ #evict(): void {
213213+ const excess = this.#count - this.#size;
214214+ if (excess <= 0) {
215215+ return;
216216+ }
217217+218218+ let current: LRUNode<K, V> = this.#tail!;
219219+220220+ for (let i = 0; i < excess; i++) {
221221+ this.#map.delete(current.key);
222222+ current = current.prev!;
223223+ }
224224+225225+ current.next = null;
226226+ this.#tail = current;
227227+228228+ this.#count -= excess;
229229+ }
230230+}
231231+232232+export default LRUCache;
+129
packages/utilities/mst/lib/utils/stack.ts
···11+interface Node<T> {
22+ value: T;
33+ next: Node<T> | undefined;
44+}
55+66+/** a stack data structure (lifo) */
77+class Stack<T> implements Iterable<T> {
88+ #head: Node<T> | undefined;
99+ #tail: Node<T> | undefined;
1010+ #size: number = 0;
1111+1212+ /** size of the stack */
1313+ get size(): number {
1414+ return this.#size;
1515+ }
1616+1717+ /**
1818+ * clear the stack
1919+ */
2020+ clear(): void {
2121+ this.#head = undefined;
2222+ this.#tail = undefined;
2323+ this.#size = 0;
2424+ }
2525+2626+ /**
2727+ * adds a value to the top of the stack
2828+ * @param value value to add
2929+ * @returns the stack instance
3030+ */
3131+ push(value: T): this {
3232+ const node: Node<T> = { value, next: this.#head };
3333+3434+ if (this.#head === undefined) {
3535+ this.#tail = node;
3636+ }
3737+3838+ this.#head = node;
3939+ this.#size++;
4040+ return this;
4141+ }
4242+4343+ /**
4444+ * removes the top value from the stack
4545+ * @returns last added value, or undefined if empty
4646+ */
4747+ pop(): T | undefined {
4848+ const head = this.#head;
4949+ if (head === undefined) {
5050+ return;
5151+ }
5252+5353+ this.#head = head.next;
5454+ this.#size--;
5555+5656+ if (this.#head === undefined) {
5757+ this.#tail = undefined;
5858+ }
5959+6060+ return head.value;
6161+ }
6262+6363+ /**
6464+ * get the top value without removing from stack
6565+ * @returns last added value, or undefined if empty
6666+ */
6767+ peek(): T | undefined {
6868+ return this.#head?.value;
6969+ }
7070+7171+ /**
7272+ * get the bottom value without removing from stack
7373+ * @returns first added value, or undefined if empty
7474+ */
7575+ peekBottom(): T | undefined {
7676+ return this.#tail?.value;
7777+ }
7878+7979+ /**
8080+ * returns an iterator that drains all the values from stack
8181+ */
8282+ drain(): IterableIterator<T, undefined, undefined> {
8383+ // deno-lint-ignore no-this-alias
8484+ const self = this;
8585+8686+ return {
8787+ next() {
8888+ const head = self.#head;
8989+ if (head === undefined) {
9090+ return { done: true, value: undefined };
9191+ }
9292+9393+ self.#head = head.next;
9494+ self.#size--;
9595+9696+ if (self.#head === undefined) {
9797+ self.#tail = undefined;
9898+ }
9999+100100+ return { done: false, value: head.value };
101101+ },
102102+ [Symbol.iterator]() {
103103+ return this;
104104+ },
105105+ };
106106+ }
107107+108108+ /**
109109+ * iterates over the stack without draining
110110+ */
111111+ [Symbol.iterator](): Iterator<T, undefined, undefined> {
112112+ let current = this.#head;
113113+114114+ return {
115115+ next() {
116116+ if (current === undefined) {
117117+ return { done: true, value: undefined };
118118+ }
119119+120120+ const value = current.value;
121121+ current = current.next;
122122+123123+ return { done: false, value: value };
124124+ },
125125+ };
126126+ }
127127+}
128128+129129+export default Stack;