diff --git a/src/parser/tokenizer.spec.ts b/src/parser/tokenizer.spec.ts index c8d4f6311..08cf084b7 100644 --- a/src/parser/tokenizer.spec.ts +++ b/src/parser/tokenizer.spec.ts @@ -522,6 +522,14 @@ describe('Tokenizer', function () { expect(new Tokenizer('contains b').matchTrie(opTrie)).toBe(8) }) }) + describe('#createTrie()', function () { + it('should return the same trie for the same input', () => { + expect(createTrie(defaultOperators)).toBe(createTrie(defaultOperators)) + }) + it('should return distinct tries for distinct inputs', () => { + expect(createTrie({ foo: 1 })).not.toBe(createTrie({ foo: 1 })) + }) + }) describe('#readLiquidTagTokens', () => { it('should read newline terminated tokens', () => { const tokenizer = new Tokenizer('echo \'hello\'') diff --git a/src/util/operator-trie.ts b/src/util/operator-trie.ts index f94d7138a..0ecbaa21d 100644 --- a/src/util/operator-trie.ts +++ b/src/util/operator-trie.ts @@ -10,7 +10,16 @@ export type Trie = { needBoundary?: true } & Record +// Tries are built once per input object and reused: the Tokenizer rebuilds them +// on every instantiation, but `input` (operators/literalValues) is a stable +// reference. WeakMap-keying by `input` lets short-lived operator objects (and +// their tries) be garbage collected. The returned trie is treated as read-only +// by callers (matchTrie only reads it); do not mutate it. +const trieCache = new WeakMap, Trie>() + export function createTrie (input: TrieInput): Trie { + const cached = trieCache.get(input) + if (cached) return cached const trie: Trie = {} for (const [name, data] of Object.entries(input)) { let node = trie @@ -29,5 +38,6 @@ export function createTrie (input: TrieInput): Trie { node.data = data node.end = true } + trieCache.set(input, trie) return trie }