From 0ee615f4357f4a584103c6b72db14a8d8ed8d16e Mon Sep 17 00:00:00 2001 From: Siim Sams Date: Fri, 3 Apr 2026 16:49:19 +0300 Subject: [PATCH] fix: move css cache onto container to allow parallel runs (#252) --- packages/beasties/src/dom.ts | 33 ++++++------ packages/beasties/test/beasties.test.ts | 24 +++++++++ packages/beasties/test/dom.test.ts | 70 +++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 packages/beasties/test/dom.test.ts diff --git a/packages/beasties/src/dom.ts b/packages/beasties/src/dom.ts index 45529cd..97b2560 100644 --- a/packages/beasties/src/dom.ts +++ b/packages/beasties/src/dom.ts @@ -26,12 +26,9 @@ import { DomUtils, parseDocument } from 'htmlparser2' type ParsedDocument = ReturnType -let classCache: null | Set = null -let idCache: null | Set = null - -function buildCache(container: Node | HTMLDocument) { - classCache = new Set() - idCache = new Set() +function buildCache(container: Node) { + container._classCache = new Set() + container._idCache = new Set() const queue = [container] while (queue.length) { @@ -40,17 +37,17 @@ function buildCache(container: Node | HTMLDocument) { if (node.hasAttribute?.('class')) { const classList = node.getAttribute('class').trim().split(' ') classList.forEach((cls) => { - classCache!.add(cls) + container._classCache!.add(cls) }) } if (node.hasAttribute?.('id')) { const id = node.getAttribute('id').trim() - idCache.add(id) + container._idCache!.add(id) } if ('children' in node) { - queue.push(...node.children.filter(child => child.type === 'tag')) + queue.push(...(node as NodeWithChildren).children.filter(child => child.type === 'tag')) } } } @@ -112,6 +109,8 @@ declare module 'domhandler' { $$name?: string $$reduce?: boolean $$links?: ChildNode[] + _classCache?: Set + _idCache?: Set } } @@ -346,23 +345,23 @@ function extendDocument(document: ParsedDocument): asserts document is HTMLDocum // so that it's disposed with it. const selectorTokensCache = new Map() -function cachedQuerySelector(sel: string, node: Node | Node[]) { +function cachedQuerySelector(sel: string, node: Node) { let selectorTokens = selectorTokensCache.get(sel) if (selectorTokens === undefined) { selectorTokens = parseRelevantSelectors(sel) selectorTokensCache.set(sel, selectorTokens) } - if (selectorTokens) { + if (selectorTokens && node._classCache && node._idCache) { for (const token of selectorTokens) { - // Check if the selector is a class selector - if (token.name === 'class') { - return classCache!.has(token.value) + if (token.name === 'class' && !node._classCache.has(token.value)) { + return false } - if (token.name === 'id') { - return idCache!.has(token.value) + if (token.name === 'id' && !node._idCache.has(token.value)) { + return false } } + return true } return !!selectOne(sel, node) @@ -375,7 +374,7 @@ function parseRelevantSelectors(sel: string): AttributeSelector[] | null { for (let i = 0; i < tokens.length; i++) { const tokenGroup = tokens[i] if (tokenGroup?.length !== 1) { - continue + return null } const token = tokenGroup[0] diff --git a/packages/beasties/test/beasties.test.ts b/packages/beasties/test/beasties.test.ts index 7d9591d..2b83768 100644 --- a/packages/beasties/test/beasties.test.ts +++ b/packages/beasties/test/beasties.test.ts @@ -1062,4 +1062,28 @@ describe('beasties', () => { // Clean up temporary directory fs.rmSync(tmpDir, { recursive: true }) }) + + it('should not share class/id caches between concurrent process() calls', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'beasties-concurrent-')) + fs.mkdirSync(path.join(tmpDir, 'static'), { recursive: true }) + fs.writeFileSync(path.join(tmpDir, 'static', 'style.css'), '.foo{color:red}#bar{color:blue}#missing{color:green}') + + const html1 = '
hello
' + const html2 = '

no css

' + + const b1 = new Beasties({ path: tmpDir, logLevel: 'silent' }) + const b2 = new Beasties({ path: tmpDir, logLevel: 'silent' }) + + const [result1] = await Promise.all([ + b1.process(html1), + b2.process(html2), + ]) + + expect(result1).toContain('