From b0c8b9dcd5459b444f03f1b28e660756f3355862 Mon Sep 17 00:00:00 2001 From: Christian-Bermejo Date: Sun, 14 Jun 2026 07:47:22 +0800 Subject: [PATCH] Cache compiled emoji regexes in countEmoji MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit countEmoji recompiled one RegExp per emoji (~70) on every call, and it runs once per tweet across several call sites (enrich, analyzeSentiment, analyzeTone) — so a large CSV recompiled hundreds of thousands of throwaway regexes. Compile each list's regexes once and cache them in a WeakMap keyed on the list array (POSITIVE_EMOJI / NEGATIVE_EMOJI are stable module constants, so they compile once total). Counting semantics are unchanged: same per-emoji regex + 'g' flag, and String.prototype.match with /g is stateless (resets lastIndex), so reusing the compiled regexes across calls is safe. Adds a repeated-call regression test. Build + 41 tests green. --- studio/src/lib/analysis.js | 20 ++++++++++++++++++-- studio/src/lib/analysis.test.js | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/studio/src/lib/analysis.js b/studio/src/lib/analysis.js index 6280729..79527ac 100644 --- a/studio/src/lib/analysis.js +++ b/studio/src/lib/analysis.js @@ -21,10 +21,26 @@ export function tokenize(text) { .filter(Boolean); } +// Compiled emoji regexes are cached per emoji-list array so countEmoji does not +// recompile ~70 RegExps on every call (it runs once per tweet, several times). +// String.prototype.match with a /g regex is stateless (it ignores and resets +// lastIndex), so reusing the same compiled regexes across calls is safe. +const _emojiRegexCache = new WeakMap(); + +function emojiRegexes(list) { + let regexes = _emojiRegexCache.get(list); + if (!regexes) { + regexes = list.map( + (e) => new RegExp(e.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"), + ); + _emojiRegexCache.set(list, regexes); + } + return regexes; +} + export function countEmoji(text, list) { let count = 0; - for (const e of list) { - const re = new RegExp(e.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"); + for (const re of emojiRegexes(list)) { const m = text.match(re); if (m) count += m.length; } diff --git a/studio/src/lib/analysis.test.js b/studio/src/lib/analysis.test.js index 111d8f7..b21a0ca 100644 --- a/studio/src/lib/analysis.test.js +++ b/studio/src/lib/analysis.test.js @@ -35,6 +35,13 @@ describe("countEmoji", () => { it("returns 0 when no listed emoji are present", () => { expect(countEmoji("plain text", POSITIVE_EMOJI)).toBe(0); }); + + it("stays correct across repeated calls with the same list (cached regexes are stateless)", () => { + expect(countEmoji("🔥🔥", POSITIVE_EMOJI)).toBe(2); + expect(countEmoji("🔥", POSITIVE_EMOJI)).toBe(1); + expect(countEmoji("no emoji here", POSITIVE_EMOJI)).toBe(0); + expect(countEmoji("🔥🔥🔥", POSITIVE_EMOJI)).toBe(3); + }); }); describe("analyzeSentiment", () => {