function trieInsert(trie, string, item) { let walker = trie; for (let i = 0; i < string.length; i++) { const char = string[i]; // add child node if not already present if (walker.children[char] == null) { walker.children[char] = { parent: walker, children: {}, candidates: [], depth: 0, }; } // log max depth of this subtree walker.depth = Math.max(walker.depth, string.length - i); // step into child node walker = walker.children[char]; } walker.candidates.push(item); } function trieRemove(trie, string, item) { let walker = trie; for (let i = 0; i < string.length; i++) { const char = string[i]; // string is not in trie if (walker.children[char] == null) { return false; } walker = walker.children[char]; } const indexOfCandidate = walker.candidates.indexOf(item); // candidate item is not in trie if (indexOfCandidate === -1) { return false; } // remove candidate from candidate list walker.candidates.splice(indexOfCandidate, 1); while (walker.parent != null) { const count = walker.candidates.length + walker.children.length; // remove this node if it has no children/candidates if (count === 0) { const indexOfChild = walker.parent.children.indexOf(walker); walker.parent.children.splice(indexOfChild, 1); } // update max depth of subtries walker = walker.parent; walker.depth = Math.max(...walker.children.map((c) => c.depth)); } return true; } export class TrieStoragePlugin { constructor(candidates = []) { for (const [string, item] of candidates) { trieInsert(this.#trie, string, item); } this.#count = candidates.length; } add(candidates = []) { for (const [string, item] of candidates) { trieInsert(this.#trie, string, item); } this.#count += candidates.length; } remove(string, item) { if (trieRemove(this.#trie, string, item)) { this.#count--; } } get count() { return this.#count; } #trie = {children: {}, candidates: [], depth: 0}; #count = 0; }