From 047327439133b2282b64321becb89c74610ec497 Mon Sep 17 00:00:00 2001 From: Guy MANDINA Date: Wed, 12 Aug 2026 09:10:55 +0200 Subject: [PATCH] perf(dedup): eliminate per-pair array allocation in compareLocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compareLocations ran on every candidate pair during deduplication and allocated a merged `[...aTokens, ...bTokens]` array just to run a single .some() membership check against the 3-entry REMOTE_LOCATION_TOKENS set. Replace it with hasAnyToken(), which iterates the tiny constant needles set and does O(1) Set.has checks against each token set individually — zero allocations. This is consistent with the file's existing allocation-avoidance pattern (intersectionSize, jaccardSimilarity). Semantics are identical: the OR of (any remote token in A) or (any remote token in B) is equivalent to (any remote token in A∪B). Adds a regression test that exercises the remote-context fallback with disjoint location token sets ('Paris' vs 'Teletravail'). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/extension/src/lib/core/scoring/dedup.ts | 19 ++++++++++++- .../tests/unit/scoring/dedup.test.ts | 28 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/extension/src/lib/core/scoring/dedup.ts b/apps/extension/src/lib/core/scoring/dedup.ts index 07d45176..de871dea 100644 --- a/apps/extension/src/lib/core/scoring/dedup.ts +++ b/apps/extension/src/lib/core/scoring/dedup.ts @@ -139,6 +139,22 @@ const tokenize = (text: string | null | undefined): Set => .filter((token) => token.length > 1 && !STOP_WORDS.has(token)) ); +/** + * True if any member of `needles` is present in `tokens`. Iterates `needles` + * (typically a tiny constant set such as REMOTE_LOCATION_TOKENS) and does O(1) + * membership checks against `tokens`, allocating nothing. Replaces the previous + * `[...aTokens, ...bTokens].some(...)` in compareLocations, which allocated a + * merged array on every candidate pair — a hot path during deduplication. + */ +const hasAnyToken = (tokens: Set, needles: Set): boolean => { + for (const needle of needles) { + if (tokens.has(needle)) { + return true; + } + } + return false; +}; + /** * Counts shared elements between two sets, iterating the smaller set to minimise * membership checks. Allocates no intermediate arrays/sets — important because @@ -316,7 +332,8 @@ const compareLocations = ( const hasRemoteContext = a.remote === 'full' || b.remote === 'full' || - [...aTokens, ...bTokens].some((token) => REMOTE_LOCATION_TOKENS.has(token)); + hasAnyToken(aTokens, REMOTE_LOCATION_TOKENS) || + hasAnyToken(bTokens, REMOTE_LOCATION_TOKENS); return { compatible: hasRemoteContext, score: hasRemoteContext ? 0.4 : 0 }; }; diff --git a/apps/extension/tests/unit/scoring/dedup.test.ts b/apps/extension/tests/unit/scoring/dedup.test.ts index 0d99d0e2..502c6e13 100644 --- a/apps/extension/tests/unit/scoring/dedup.test.ts +++ b/apps/extension/tests/unit/scoring/dedup.test.ts @@ -312,6 +312,34 @@ describe('deduplicateMissions', () => { expect(result).toHaveLength(2); }); + it('dedupes across disjoint locations when one carries a remote token', () => { + // Exercises the compareLocations remote-context fallback: location token + // sets are disjoint (no weighted similarity), but "teletravail" is in + // REMOTE_LOCATION_TOKENS, so the pair stays compatible and merges. + // Regression guard for the allocation-free hasAnyToken implementation. + const missions = [ + makeMission({ + id: 'paris', + title: 'Developpeur React Senior', + stack: ['React', 'TypeScript'], + location: 'Paris', + remote: null, + }), + makeMission({ + id: 'remote', + title: 'Developpeur React Senior', + stack: ['React'], + location: 'Teletravail', + remote: null, + }), + ]; + + const result = deduplicateMissionsDetailed(missions); + expect(result.missions).toHaveLength(1); + expect(result.duplicateRelations).toHaveLength(1); + expect(result.duplicateRelations[0].confidence).toBeGreaterThanOrEqual(0.8); + }); + it('handles complex duplicate scenarios', () => { const missions = [ makeMission({