···3344type BlockEntry = [cid: string, bytes: Uint8Array<ArrayBuffer>];
5566+/** a map from CID strings to their encoded block data */
67export type BlockMap = Map<string, Uint8Array<ArrayBuffer>>;
7899+/**
1010+ * encodes data as CBOR, computes its CID, and adds it to the map
1111+ * @param map the block map to add to
1212+ * @param data the data to encode and add
1313+ */
814export const add = async (map: BlockMap, data: unknown): Promise<void> => {
915 const encoded = CBOR.encode(data);
1016 const cid = await CID.create(0x71, encoded);
···1218 map.set(CID.toString(cid), encoded);
1319};
14202121+/**
2222+ * copies multiple blocks from an iterable into the map
2323+ * @param map the block map to add to
2424+ * @param entries the block entries to add
2525+ */
1526export const setMany = (map: BlockMap, entries: Iterable<Readonly<BlockEntry>>) => {
1627 for (const [cid, bytes] of entries) {
1728 map.set(cid, bytes);
1829 }
1930};
20313232+/**
3333+ * removes multiple blocks from the map by their CIDs
3434+ * @param map the block map to remove from
3535+ * @param cids the CID strings to remove
3636+ */
2137export const deleteMany = (map: BlockMap, cids: Iterable<string>) => {
2238 for (const cid of cids) {
2339 map.delete(cid);
+9
packages/utilities/mst/lib/errors.ts
···11+/**
22+ * thrown when an MST key is invalid or malformed
33+ */
14export class InvalidMstKeyError extends Error {
25 constructor(public key: string) {
36 super(`invalid mst key; key=${key}`);
47 }
58}
691010+/**
1111+ * thrown when a referenced block cannot be found in the store
1212+ */
713export class MissingBlockError extends Error {
814 constructor(
915 public cid: string,
···1319 }
1420}
15212222+/**
2323+ * thrown when a block's decoded object doesn't match the expected type
2424+ */
1625export class UnexpectedObjectError extends Error {
1726 constructor(
1827 public cid: string,
+16
packages/utilities/mst/lib/node-store.ts
···4455import LRUCache from './utils/lru.js';
6677+/**
88+ * manages caching and storage of MST nodes with LRU eviction
99+ */
710export class NodeStore {
1111+ /** underlying block store for persistent storage */
812 store: BlockStore;
1313+ /** LRU cache for recently accessed nodes */
914 cache = new LRUCache<string | null, MSTNode>(1024);
10151116 constructor(store: BlockStore) {
1217 this.store = store;
1318 }
14192020+ /**
2121+ * retrieves an MST node by its CID, using cache when available
2222+ * @param cid the CID of the node to retrieve, or null for empty node
2323+ * @returns the MST node
2424+ * @throws {MissingBlockError} if the node cannot be found in the store
2525+ */
1526 async get(cid: string | null): Promise<MSTNode> {
1627 let node = this.cache.get(cid);
1728 if (node === undefined) {
···3445 return node;
3546 }
36474848+ /**
4949+ * stores an MST node in both the cache and the underlying block store
5050+ * @param node the node to store
5151+ * @returns the same node that was passed in
5252+ */
3753 async put(node: MSTNode): Promise<MSTNode> {
3854 const cid = (await node.cid()).$link;
3955
+52-28
packages/utilities/mst/lib/node-walker.ts
···11import type { CidLink } from '@atcute/cid';
2233-import { MSTNode, getKeyHeight } from './node.js';
43import { NodeStore } from './node-store.js';
44+import { MSTNode, getKeyHeight } from './node.js';
55import Stack from './utils/stack.js';
6677-interface StackFrame {
77+/**
88+ * represents a single frame in the NodeWalker traversal stack
99+ * tracks position within a node and the current search boundaries
1010+ */
1111+export interface StackFrame {
1212+ /** current MST node */
813 node: MSTNode;
1414+ /** left boundary path for this frame */
915 lpath: string;
1616+ /** right boundary path for this frame */
1017 rpath: string;
1818+ /** current cursor index within the node */
1119 idx: number;
1220}
13211422/**
1515- * NodeWalker makes implementing tree diffing and other MST query ops more
1616- * convenient (but it does not, itself, implement them).
2323+ * provides a cursor-based interface for traversing MST nodes
2424+ * supports tree diffing and various MST query operations
1725 *
1818- * A NodeWalker starts off at the root of a tree, and can walk along or recurse
1919- * down into subtrees.
2626+ * a NodeWalker starts at the root of a tree and can walk along or recurse
2727+ * down into subtrees
2028 *
2121- * Walking "off the end" of a subtree brings you back up to its next non-empty parent.
2929+ * walking "off the end" of a subtree brings you back up to its next non-empty parent
2230 *
2323- * Recall MSTNode layout:
3131+ * recall MSTNode layout:
2432 *
2533 * ```
2634 * keys: (lpath) (0, 1, 2, 3) (rpath)
···3240 static readonly PATH_MIN = ''; // string that compares less than all legal path strings
3341 static readonly PATH_MAX = '\xff'; // string that compares greater than all legal path strings
34423535- private store: NodeStore;
3636- private stack: Stack<StackFrame>;
3737- private rootHeight: number;
3838- private trusted: boolean;
4343+ /**
4444+ * node store for fetching nodes
4545+ * @internal
4646+ */
4747+ _store: NodeStore;
4848+ /**
4949+ * stack of frames representing the traversal path
5050+ * @internal
5151+ */
5252+ _stack: Stack<StackFrame>;
5353+ /**
5454+ * height of the root node
5555+ * @internal
5656+ */
5757+ _rootHeight: number;
5858+ /**
5959+ * whether to skip height validation (for trusted trees)
6060+ * @internal
6161+ */
6262+ _trusted: boolean;
39634064 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;
6565+ this._store = store;
6666+ this._stack = stack;
6767+ this._rootHeight = rootHeight;
6868+ this._trusted = trusted;
4569 }
46704771 /**
···87111 */
88112 async createSubtreeWalker(): Promise<NodeWalker> {
89113 return await NodeWalker.create(
9090- this.store,
114114+ this._store,
91115 this.subtree?.$link ?? null,
92116 this.lpath,
93117 this.rpath,
9494- this.trusted,
118118+ this._trusted,
95119 this.height - 1,
96120 );
97121 }
981229999- /** current stack frame (internal) */
123123+ /** current stack frame */
100124 get frame(): StackFrame {
101101- const frame = this.stack.peek();
125125+ const frame = this._stack.peek();
102126 if (frame === undefined) {
103127 throw new Error(`stack is empty`);
104128 }
···108132109133 /** current height in the tree (decreases as you descend) */
110134 get height(): number {
111111- return this.rootHeight - (this.stack.size - 1);
135135+ return this._rootHeight - (this._stack.size - 1);
112136 }
113137114138 /** key/path to the left of current cursor position */
···142166 /** whether the walker has reached the end of the tree */
143167 get done(): boolean {
144168 // is (not this.stack) really necessary here? is that a reachable state?
145145- const bottom = this.stack.peekBottom();
169169+ const bottom = this._stack.peekBottom();
146170 return (
147147- this.stack.size === 0 || (this.subtree === null && bottom !== undefined && this.rpath === bottom.rpath)
171171+ this._stack.size === 0 || (this.subtree === null && bottom !== undefined && this.rpath === bottom.rpath)
148172 );
149173 }
150174···161185 rightOrUp(): void {
162186 if (!this.canGoRight) {
163187 // we reached the end of this node, go up a level
164164- this.stack.pop();
165165- if (this.stack.size === 0) {
188188+ this._stack.pop();
189189+ if (this._stack.size === 0) {
166190 throw new Error(`cannot navigate beyond root; check .done before calling`);
167191 }
168192 return this.rightOrUp(); // we need to recurse, to skip over empty intermediates on the way back up
···191215 throw new Error(`cannot descend; no subtree at current position`);
192216 }
193217194194- const subtreeNode = await this.store.get(subtree.$link);
218218+ const subtreeNode = await this._store.get(subtree.$link);
195219196196- if (!this.trusted) {
220220+ if (!this._trusted) {
197221 // if we "trust" the source we can elide this check
198222 // the "null" case occurs for empty intermediate nodes
199223 const subtreeHeight = await subtreeNode.height();
···202226 }
203227 }
204228205205- this.stack.push({
229229+ this._stack.push({
206230 node: subtreeNode,
207231 lpath: this.lpath,
208232 rpath: this.rpath,
+78-8
packages/utilities/mst/lib/node.ts
···5566import { isNodeData, type NodeData, type TreeEntry } from './types.js';
7788+/**
99+ * represents a node in a Merkle Search Tree (MST)
1010+ * stores sorted keys, their associated values (CIDs), and subtree pointers
1111+ */
812export class MSTNode {
99- /** @internal */
1313+ /**
1414+ * cached height of this node in the tree
1515+ * @internal
1616+ */
1017 _height: number | null | undefined;
1111- /** @internal */
1818+ /**
1919+ * cached CID for this node
2020+ * @internal
2121+ */
1222 _cid: CidLink | undefined;
1313- /** @internal */
2323+ /**
2424+ * cached serialized bytes for this node
2525+ * @internal
2626+ */
1427 _bytes: Uint8Array<ArrayBuffer> | undefined;
15281629 protected constructor(
3030+ /** sorted array of keys stored in this node */
1731 readonly keys: readonly string[],
3232+ /** array of value CIDs corresponding to each key */
1833 readonly values: readonly CidLink[],
3434+ /** array of subtree CIDs (length is keys.length + 1) */
1935 readonly subtrees: readonly (CidLink | null)[],
2036 ) {}
21373838+ /**
3939+ * creates a new MST node with validation
4040+ * @param keys sorted array of keys
4141+ * @param values array of value CIDs corresponding to keys
4242+ * @param subtrees array of subtree CIDs (length must be keys.length + 1)
4343+ * @returns a new validated MST node
4444+ * @throws {TypeError} if node structure is invalid or keys have inconsistent heights
4545+ */
2246 static async create(
2347 keys: readonly string[],
2448 values: readonly CidLink[],
···4569 return new MSTNode(keys, values, subtrees);
4670 }
47717272+ /**
7373+ * creates an empty MST node
7474+ * @returns a new empty node
7575+ */
4876 static empty(): MSTNode {
4977 return new MSTNode([], [], [null]);
5078 }
51798080+ /**
8181+ * deserializes an MST node from CBOR-encoded bytes
8282+ * @param bytes the CBOR-encoded node data
8383+ * @returns the deserialized MST node
8484+ * @throws {TypeError} if the bytes don't represent a valid MST node
8585+ */
5286 static async deserialize(bytes: Uint8Array): Promise<MSTNode> {
5387 const node = CBOR.decode(bytes);
5488 if (!isNodeData(node)) {
···87121 return await MSTNode.create(keys, values, subtrees);
88122 }
89123124124+ /**
125125+ * serializes the node to CBOR-encoded bytes with prefix compression
126126+ * @returns the CBOR-encoded node data
127127+ */
90128 async serialize(): Promise<Uint8Array<ArrayBuffer>> {
91129 let bytes = this._bytes;
92130 if (bytes === undefined) {
···122160 return bytes;
123161 }
124162163163+ /**
164164+ * whether the node is empty (no keys or values)
165165+ */
166166+ get isEmpty(): boolean {
167167+ return this.subtrees.length === 1 && this.subtrees[0] === null;
168168+ }
169169+170170+ /**
171171+ * computes the CID for this node
172172+ * @returns the CID link for this node
173173+ */
125174 async cid(): Promise<CidLink> {
126175 let cid = this._cid;
127176 if (cid === undefined) {
···131180 return cid;
132181 }
133182134134- isEmpty(): boolean {
135135- return this.subtrees.length === 1 && this.subtrees[0] === null;
136136- }
137137-183183+ /**
184184+ * computes the height of this node in the MST
185185+ * @returns the height, or null if indeterminate (empty intermediate node)
186186+ */
138187 async height(): Promise<number | null> {
139188 let height = this._height;
140189 if (height === undefined) {
141190 const keys = this.keys;
142191143143- if (this.isEmpty()) {
192192+ if (this.isEmpty) {
144193 height = 0;
145194 } else if (keys.length > 0) {
146195 height = await getKeyHeight(keys[0]);
···154203 return height;
155204 }
156205206206+ /**
207207+ * gets the node height, throwing if indeterminate
208208+ * @returns the height
209209+ * @throws {Error} if height cannot be determined
210210+ */
157211 async requireHeight(): Promise<number> {
158212 const height = await this.height();
159213 if (height === null) {
···163217 return height;
164218 }
165219220220+ /**
221221+ * finds the index of the first key >= the given key
222222+ * @param key the key to search for
223223+ * @returns the index of the lower bound
224224+ */
166225 lowerBound(key: string): number {
167226 const keys = this.keys;
168227 const len = keys.length;
···177236 }
178237}
179238239239+/**
240240+ * computes the MST height for a given key by counting leading zeros in its hash
241241+ * @param key the key to compute height for
242242+ * @returns the height (number of leading zero bits in 2-bit chunks)
243243+ */
180244export const getKeyHeight = async (key: string): Promise<number> => {
181245 const hash = await toSha256(encodeUtf8(key));
182246···204268 return lz;
205269};
206270271271+/**
272272+ * computes the length of the common prefix between two strings
273273+ * @param a first string
274274+ * @param b second string
275275+ * @returns length of common prefix
276276+ */
207277const commonPrefixLength = (a: string, b: string): number => {
208278 let idx = 0;
209279 for (let len = Math.min(a.length, b.length); idx < len; idx++) {
+87-1
packages/utilities/mst/lib/stores.ts
···33import { deleteMany, setMany, type BlockMap } from './blockmap.js';
44import { MissingBlockError, UnexpectedObjectError } from './errors.js';
5566+/**
77+ * a read-only interface for retrieving blocks by their CID
88+ */
69export interface ReadonlyBlockStore {
1010+ /**
1111+ * retrieves a single block by its CID
1212+ * @param cid the CID of the block to retrieve
1313+ * @returns the block data, or null if not found
1414+ */
715 get(cid: string): Promise<Uint8Array<ArrayBuffer> | null>;
1616+1717+ /**
1818+ * retrieves multiple blocks by their CIDs
1919+ * @param cids array of CIDs to retrieve
2020+ * @returns object containing found blocks and missing CIDs
2121+ */
822 getMany(cids: string[]): Promise<{ found: BlockMap; missing: string[] }>;
9232424+ /**
2525+ * checks if a block exists in the store
2626+ * @param cid the CID to check
2727+ * @returns true if the block exists, false otherwise
2828+ */
1029 has(cid: string): Promise<boolean>;
1130}
12313232+/**
3333+ * a writable block store supporting both read and write operations
3434+ */
1335export interface BlockStore extends ReadonlyBlockStore {
3636+ /**
3737+ * stores a single block
3838+ * @param cid the CID of the block
3939+ * @param bytes the block data to store
4040+ */
1441 put(cid: string, bytes: Uint8Array<ArrayBuffer>): Promise<void>;
4242+4343+ /**
4444+ * stores multiple blocks at once
4545+ * @param blocks map of CIDs to block data
4646+ */
1547 putMany(blocks: BlockMap): Promise<void>;
16484949+ /**
5050+ * removes a single block from the store
5151+ * @param cid the CID of the block to remove
5252+ */
1753 delete(cid: string): Promise<void>;
5454+5555+ /**
5656+ * removes multiple blocks from the store
5757+ * @param cids array of CIDs to remove
5858+ */
1859 deleteMany(cids: string[]): Promise<void>;
1960}
20616262+/**
6363+ * an in-memory read-only block store using a Map
6464+ */
2165export class ReadonlyMemoryBlockStore implements ReadonlyBlockStore {
6666+ /** underlying map storing CID to block data */
2267 blocks: BlockMap = new Map();
23686969+ /**
7070+ * creates a new read-only memory block store
7171+ * @param blocks optional initial blocks to populate the store with
7272+ */
2473 constructor(blocks?: BlockMap) {
2574 if (blocks !== undefined) {
2675 setMany(this.blocks, blocks);
···52101 }
53102}
54103104104+/**
105105+ * an in-memory writable block store using a Map
106106+ */
55107export class MemoryBlockStore extends ReadonlyMemoryBlockStore implements BlockStore {
56108 put(cid: string, bytes: Uint8Array<ArrayBuffer>): Promise<void> {
57109 this.blocks.set(cid, bytes);
···74126 }
75127}
76128129129+/**
130130+ * a block store that overlays one store on top of another
131131+ * reads check upper first, then fall back to lower
132132+ * all writes go to the upper store only
133133+ */
77134export class OverlayBlockStore implements BlockStore {
135135+ /** the writable upper layer store */
78136 upper: BlockStore;
137137+ /** the read-only lower layer store */
79138 lower: ReadonlyBlockStore;
80139140140+ /**
141141+ * creates a new overlay block store
142142+ * @param upper the writable upper layer store
143143+ * @param lower the read-only lower layer store
144144+ */
81145 constructor(upper: BlockStore, lower: ReadonlyBlockStore) {
82146 this.upper = upper;
83147 this.lower = lower;
···130194 }
131195}
132196197197+/**
198198+ * reads and decodes a block, validating it matches the expected type
199199+ * @param store block store to read from
200200+ * @param cid CID of the block to read
201201+ * @param def schema definition with name and validation function
202202+ * @returns the decoded and validated object
203203+ * @throws {MissingBlockError} if block is not found
204204+ * @throws {UnexpectedObjectError} if block doesn't match expected type
205205+ */
133206export const readObject = async <T>(store: ReadonlyBlockStore, cid: string, def: CheckDef<T>): Promise<T> => {
134207 const bytes = await store.get(cid);
135208 if (bytes === null) {
···144217 return decoded;
145218};
146219220220+/**
221221+ * reads and decodes a block without type validation
222222+ * @param store block store to read from
223223+ * @param cid CID of the block to read
224224+ * @returns the decoded object
225225+ * @throws {MissingBlockError} if block is not found
226226+ */
147227export const readRecord = async (store: ReadonlyBlockStore, cid: string): Promise<unknown> => {
148228 const bytes = await store.get(cid);
149229 if (bytes === null) {
···155235 return decoded;
156236};
157237158158-interface CheckDef<T> {
238238+/**
239239+ * defines a type validator for use with readObject
240240+ * combines a human-readable type name with a type guard function
241241+ */
242242+export interface CheckDef<T> {
243243+ /** human-readable name of the expected type */
159244 name: string;
245245+ /** type guard function to validate the decoded value */
160246 check: (value: unknown) => value is T;
161247}
+16
packages/utilities/mst/lib/types.ts
···11import { isBytes, type Bytes } from '@atcute/cbor';
22import { isCidLink, type CidLink } from '@atcute/cid';
3344+/**
55+ * represents a single entry in an MST node
66+ */
47export interface TreeEntry {
58 /** count of bytes shared with previous TreeEntry in this Node (if any) */
69 p: number;
···1215 t: CidLink | null;
1316}
14171818+/**
1919+ * validates that an unknown value is a valid TreeEntry
2020+ * @param value the value to check
2121+ * @returns true if value is a TreeEntry, false otherwise
2222+ */
1523export const isTreeEntry = (value: unknown): value is TreeEntry => {
1624 if (value === null || typeof value !== 'object') {
1725 return false;
···2432 );
2533};
26343535+/**
3636+ * represents the serialized data structure of an MST node
3737+ */
2738export interface NodeData {
2839 /** link to sub-tree Node on a lower level and with all keys sorting before keys at this node */
2940 l: CidLink | null;
···3142 e: TreeEntry[];
3243}
33444545+/**
4646+ * validates that an unknown value is valid NodeData
4747+ * @param value the value to check
4848+ * @returns true if value is NodeData, false otherwise
4949+ */
3450export const isNodeData = (value: unknown): value is NodeData => {
3551 if (value === null || typeof value !== 'object') {
3652 return false;
+1-1
packages/utilities/mst/lib/utils/lru.ts
···2525 this.#size = size;
2626 }
27272828- /** the maximum capacity of the cache */
2828+ /** maximum capacity of the cache */
2929 get size(): number {
3030 return this.#size;
3131 }