Files
Bingbing 8a1dc1ef62 perf: rewrite command palette search to eliminate UI freezes and stale results (#10257)
* perf: rewrite command palette search to eliminate UI freezes and stale results

Problems fixed:
- clientLoader blocked navigation on every palette open, freezing the UI during
  full sequential DB scan
- no AbortController: stale searches could overwrite newer results mid-type
- no debounce: every keystroke triggered a full DB traversal immediately
- React Aria's default contains filter re-filtered server-side fuzzy results,
  silently dropping valid matches and making fuzzy work redundant
- no warm baseline: palette started blank until the entire load completed

Changes:
- move fuzzyMatch/fuzzyMatchAll → insomnia-data/common-src/search.ts (shared,
  testable in node)
- move search logic → insomnia-data/node-src/services/helpers/command-search.ts
  - per-request AbortController cancellation via abort(requestId)
  - recursion depth guard (max 20) on request group traversal
  - safeParent() replaces non-null assertions, safe against orphaned documents
- add CommandSearchResult type in insomnia-data/src/command-search-types.ts
- add useCommandSearch hook: 250ms debounce + abort on filter change + warm
  baseline on mount
- add defaultFilter={() => true} to ComboBox, delegating all filtering to the
  service
- delete routes/commands.tsx

* fix

* fix
2026-07-17 01:07:38 +00:00

73 lines
1.5 KiB
TypeScript

import fuzzysort from 'fuzzysort';
export interface FuzzyMatchOptions {
splitSpace?: boolean;
loose?: boolean;
}
export function fuzzyMatch(
searchString: string,
text: string,
options: FuzzyMatchOptions = {},
): null | {
score: number;
indexes: number[];
} {
return fuzzyMatchAll(searchString, [text], options);
}
export function fuzzyMatchAll(searchString: string, allText: string[], options: FuzzyMatchOptions = {}) {
if (!searchString || !searchString.trim()) {
return null;
}
const words = searchString.split(' ').filter(w => w.trim());
const terms = options.splitSpace ? [...words, searchString] : [searchString];
let maxScore: number | null = null;
let indexes: number[] = [];
let termsMatched = 0;
for (const term of terms) {
let matchedTerm = false;
for (const text of allText.filter(t => t && t.trim())) {
const result = fuzzysort.single(term, text);
if (!result) {
continue;
}
// Don't match garbage
if (result.score < -8000) {
continue;
}
if (maxScore === null || result.score > maxScore) {
maxScore = result.score;
}
indexes = [...indexes, ...result.indexes];
matchedTerm = true;
}
if (matchedTerm) {
termsMatched++;
}
}
// Make sure we match all provided terms except the last (full) one
if (!options.loose && termsMatched < terms.length - 1) {
return null;
}
if (maxScore === null) {
return null;
}
return {
score: maxScore,
indexes,
target: allText.join(' '),
};
}