Files
cinny/src/app/utils/AsyncSearch.ts
T
Lotus Bot 23008670f3
CI / Build & Quality Checks (push) Successful in 10m13s
chore: upgrade i18next 26, prettier 3, fontsource-variable, domhandler 6, lint-staged 17
- i18next 23->26 + react-i18next 15->17
- prettier 2->3, reformat all files
- replace @fontsource/inter with @fontsource-variable/inter 5, update import path
- domhandler 5->6 (aligns with transitive deps)
- lint-staged 16->17
2026-05-21 23:30:50 -04:00

103 lines
3.1 KiB
TypeScript

export type NormalizeOption = {
caseSensitive?: boolean;
normalizeUnicode?: boolean;
ignoreWhitespace?: boolean;
};
export type MatchQueryOption = {
contain?: boolean;
};
export type AsyncSearchOption = {
limit?: number;
};
export type MatchHandler<TSearchItem extends object | string | number> = (
item: TSearchItem,
query: string,
) => boolean;
export type ResultHandler<TSearchItem extends object | string | number> = (
results: TSearchItem[],
query: string,
) => void;
export type AsyncSearchHandler = (query: string) => void;
export type TerminateAsyncSearch = () => void;
export const normalize = (str: string, options?: NormalizeOption) => {
let nStr = str.normalize((options?.normalizeUnicode ?? true) ? 'NFKC' : 'NFC');
if (!options?.caseSensitive) nStr = nStr.toLocaleLowerCase();
if (options?.ignoreWhitespace ?? true) nStr = nStr.replace(/\s/g, '');
return nStr;
};
export const matchQuery = (item: string, query: string, options?: MatchQueryOption): boolean => {
if (options?.contain) return item.indexOf(query) !== -1;
return item.startsWith(query);
};
export const AsyncSearch = <TSearchItem extends object | string | number>(
list: TSearchItem[],
match: MatchHandler<TSearchItem>,
onResult: ResultHandler<TSearchItem>,
options?: AsyncSearchOption,
): [AsyncSearchHandler, TerminateAsyncSearch] => {
let resultList: TSearchItem[] = [];
let searchIndex = 0;
let sessionStartTimestamp = 0;
let sessionScheduleId: number | undefined;
const terminateSearch: TerminateAsyncSearch = () => {
resultList = [];
searchIndex = 0;
sessionStartTimestamp = 0;
if (sessionScheduleId) clearTimeout(sessionScheduleId);
sessionScheduleId = undefined;
};
const find = (query: string, sessionTimestamp: number) => {
const findingCount = resultList.length;
sessionScheduleId = undefined;
// return if find session got reset
if (sessionTimestamp !== sessionStartTimestamp) return;
sessionStartTimestamp = window.performance.now();
for (; searchIndex < list.length; searchIndex += 1) {
if (match(list[searchIndex], query)) {
resultList.push(list[searchIndex]);
if (typeof options?.limit === 'number' && resultList.length >= options.limit) {
break;
}
}
const matchFinishTime = window.performance.now();
if (matchFinishTime - sessionStartTimestamp > 8) {
const currentFindingCount = resultList.length;
const thisSessionTimestamp = sessionStartTimestamp;
if (findingCount !== currentFindingCount) onResult(resultList, query);
searchIndex += 1;
sessionScheduleId = window.setTimeout(() => find(query, thisSessionTimestamp), 1);
return;
}
}
if (findingCount !== resultList.length || findingCount === 0) {
onResult(resultList, query);
}
terminateSearch();
};
const search: AsyncSearchHandler = (query: string) => {
terminateSearch();
if (query === '') {
onResult(resultList, query);
return;
}
find(query, sessionStartTimestamp);
};
return [search, terminateSearch];
};