Skip to content

Commit c2f0670

Browse files
Refactor: Remove Fuse.js dependency and enhance fuzzy search implementation
1 parent b723573 commit c2f0670

File tree

5 files changed

+75
-50
lines changed

5 files changed

+75
-50
lines changed

package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"browserstack-local": "^1.5.6",
4040
"dotenv": "^16.5.0",
4141
"form-data": "^4.0.2",
42-
"fuse.js": "^7.1.0",
4342
"pino": "^9.6.0",
4443
"pino-pretty": "^13.0.0",
4544
"zod": "^3.24.3"

src/lib/fuzzy.ts

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,64 @@
1-
/**
2-
* Creates a configured Fuse instance for token-based fuzzy search.
3-
* @param list Array of items to search
4-
* @param keys Keys in each item to index
5-
* @param options Optional Fuse.js options overrides
6-
*/
7-
export async function createFuzzySearcher<T>(
8-
list: T[],
9-
keys: Array<keyof T | string>,
10-
options?: any,
11-
): Promise<any> {
12-
const { default: Fuse } = await import("fuse.js");
1+
// 1. Compute Levenshtein distance between two strings
2+
function levenshtein(a: string, b: string): number {
3+
const dp: number[][] = Array(a.length + 1)
4+
.fill(0)
5+
.map(() => Array(b.length + 1).fill(0));
6+
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
7+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
138

14-
const defaultOptions = {
15-
keys: keys as string[],
16-
threshold: 0.6,
17-
includeScore: true,
18-
useExtendedSearch: false,
19-
tokenize: true,
20-
matchAllTokens: false,
21-
...options,
22-
};
9+
for (let i = 1; i <= a.length; i++) {
10+
for (let j = 1; j <= b.length; j++) {
11+
dp[i][j] = Math.min(
12+
dp[i - 1][j] + 1, // deletion
13+
dp[i][j - 1] + 1, // insertion
14+
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1), // substitution
15+
);
16+
}
17+
}
18+
return dp[a.length][b.length];
19+
}
2320

24-
return new Fuse(list, defaultOptions);
21+
// 2. Score one item against the query (normalized score 0–1)
22+
function scoreItem<T>(
23+
item: T,
24+
keys: Array<keyof T | string>,
25+
queryTokens: string[],
26+
): number {
27+
let best = Infinity;
28+
for (const key of keys) {
29+
const field = String(item[key as keyof T] ?? "").toLowerCase();
30+
const fieldTokens = field.split(/\s+/);
31+
const tokenScores = queryTokens.map((qt) => {
32+
const minNormalized = Math.min(
33+
...fieldTokens.map((ft) => {
34+
const rawDist = levenshtein(ft, qt);
35+
const maxLen = Math.max(ft.length, qt.length);
36+
return maxLen === 0 ? 0 : rawDist / maxLen; // normalized 0–1
37+
}),
38+
);
39+
return minNormalized;
40+
});
41+
const avg = tokenScores.reduce((a, b) => a + b, 0) / tokenScores.length;
42+
best = Math.min(best, avg);
43+
}
44+
return best;
2545
}
2646

27-
/**
28-
* Performs a fuzzy token search over any list, with dynamic keys and options.
29-
*/
30-
export async function fuzzySearch<T>(
47+
// 3. The search entrypoint
48+
export function customFuzzySearch<T>(
3149
list: T[],
3250
keys: Array<keyof T | string>,
3351
query: string,
3452
limit: number = 5,
35-
options?: any,
36-
): Promise<T[]> {
37-
const fuse = await createFuzzySearcher(list, keys, options);
38-
return fuse.search(query, { limit }).map((result: any) => result.item);
53+
maxDistance: number = 0.6,
54+
): T[] {
55+
const q = query.toLowerCase().trim();
56+
const queryTokens = q.split(/\s+/);
57+
58+
return list
59+
.map((item) => ({ item, score: scoreItem(item, keys, queryTokens) }))
60+
.filter((x) => x.score <= maxDistance)
61+
.sort((a, b) => a.score - b.score)
62+
.slice(0, limit)
63+
.map((x) => x.item);
3964
}
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fuzzySearch } from "../../lib/fuzzy";
1+
import { customFuzzySearch } from "../../lib/fuzzy";
22
import { DeviceEntry } from "./start-session";
33

44
/**
@@ -9,12 +9,11 @@ export async function fuzzySearchDevices(
99
query: string,
1010
limit: number = 5,
1111
): Promise<DeviceEntry[]> {
12-
const top_match = await fuzzySearch(
12+
const top_match = customFuzzySearch(
1313
devices,
1414
["device", "display_name"],
1515
query,
1616
limit,
1717
);
18-
console.error("[fuzzySearchDevices] Top match:", top_match);
1918
return top_match;
2019
}

src/tools/applive-utils/start-session.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,19 @@ export async function startSession(args: StartSessionArgs): Promise<string> {
5050

5151
desiredPlatformVersion = requiredVersion;
5252
}
53-
const filtered = allDevices.filter(
54-
(d) => d.os === desiredPlatform && d.os_version === desiredPlatformVersion,
55-
);
53+
const filtered = allDevices.filter((d) => {
54+
if (d.os !== desiredPlatform) return false;
55+
56+
// Attempt to compare as floats
57+
try {
58+
const versionA = parseFloat(d.os_version);
59+
const versionB = parseFloat(desiredPlatformVersion);
60+
return versionA === versionB;
61+
} catch {
62+
// Fallback to exact string match if parsing fails
63+
return d.os_version === desiredPlatformVersion;
64+
}
65+
});
5666

5767
// Fuzzy match
5868
const matches = await fuzzySearchDevices(filtered, desiredPhone);
@@ -70,9 +80,11 @@ export async function startSession(args: StartSessionArgs): Promise<string> {
7080
matches.splice(0, matches.length, exactMatch); // Replace matches with the exact match
7181
} else if (matches.length >= 1) {
7282
const names = matches.map((d) => d.display_name).join(", ");
73-
throw new Error(
74-
`Multiple/Alternative devices found: [${names}]. Select one out of them.`,
75-
);
83+
const error_message =
84+
matches.length === 1
85+
? `Alternative device found: ${names}. Would you like to use it?`
86+
: `Multiple devices found: ${names}. Please select one.`;
87+
throw new Error(`${error_message}`);
7688
}
7789

7890
const { app_url } = await uploadApp(appPath);

0 commit comments

Comments
 (0)