Optimize catalog filter membership checks (#4448)

* Optimize catalog filter membership checks

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* perf: cache catalog filter bucket lookups

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* perf: reduce repeated category scans in search

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
emilus512-cmyk
2026-10-07 16:57:03 +11:00
committed by GitHub
co-authored by Copilot App
parent 804187ef0d
commit 0052323536
4 changed files with 55 additions and 18 deletions
@@ -152,13 +152,14 @@ export function AgentsCatalog({
const sortedAgents = useMemo(() => {
const copy = agents.filter((agent) => {
const models = modelsOf(agent);
const modelSet = new Set(modelsOf(agent));
const toolSet = new Set(agent.tools ?? []);
const modelOk =
filters.model.length === 0 ||
filters.model.some((model) => models.includes(model));
filters.model.some((model) => modelSet.has(model));
const toolsOk =
filters.tools.length === 0 ||
filters.tools.every((tool) => (agent.tools ?? []).includes(tool));
filters.tools.every((tool) => toolSet.has(tool));
const updatedOk =
filters.updated.length === 0 ||
filters.updated.includes(updatedBucketOf(daysSince(agent.lastUpdated)));
@@ -152,11 +152,12 @@ export function PluginsCatalog({
const sortedPlugins = useMemo(() => {
const copy = plugins.filter((plugin) => {
const tagSet = new Set(plugin.tags ?? []);
const sourceOk =
filters.source.length === 0 || filters.source.includes(sourceOf(plugin));
const categoryOk =
filters.category.length === 0 ||
filters.category.some((tag) => (plugin.tags ?? []).includes(tag));
filters.category.some((tag) => tagSet.has(tag));
const size = sizeLabelOf(plugin);
const sizeOk =
filters.size.length === 0 ||
+16 -7
View File
@@ -140,6 +140,7 @@ export function TopNavSearch({
const groups = useMemo<ResultGroup[]>(() => {
const query = trimmed.toLowerCase();
if (query.length === 0) return [];
const staticMatches = normalizedIndex
.filter(
({ title, description, category }) =>
@@ -148,25 +149,33 @@ export function TopNavSearch({
category.includes(query),
)
.map(({ item }) => item);
// Static hits win on ties: they carry curated titles and descriptions.
const seen = new Set(staticMatches.map((item) => hrefKey(item.href)));
const matches = [...staticMatches];
const matches: SearchItem[] = [...staticMatches];
for (const hit of pagefindHits) {
const key = hrefKey(hit.href);
if (seen.has(key)) continue;
seen.add(key);
matches.push(hit);
}
const matchesByCategory = new Map<SearchCategory, SearchItem[]>();
for (const item of matches) {
const bucket = matchesByCategory.get(item.category) ?? [];
bucket.push(item);
matchesByCategory.set(item.category, bucket);
}
let remaining = MAX_RESULTS;
const grouped: ResultGroup[] = [];
for (const category of CATEGORY_ORDER) {
if (remaining <= 0) break;
const items = matches
.filter((item) => item.category === category)
.slice(0, Math.min(MAX_PER_GROUP, remaining));
if (items.length === 0) continue;
remaining -= items.length;
grouped.push({ category, items });
const items = matchesByCategory.get(category) ?? [];
const slice = items.slice(0, Math.min(MAX_PER_GROUP, remaining));
if (slice.length === 0) continue;
remaining -= slice.length;
grouped.push({ category, items: slice });
}
return grouped;
}, [trimmed, normalizedIndex, pagefindHits]);
+33 -7
View File
@@ -15,15 +15,34 @@ export const updatedBuckets: { label: string; max: number }[] = [
];
/** Whole days between `lastUpdated` and now. Unparsable dates sort as oldest. */
const daysSinceCache = new Map<string, number>();
export function daysSince(lastUpdated: string | undefined): number {
if (!lastUpdated) return Number.POSITIVE_INFINITY;
const dayStamp = Math.floor(Date.now() / 86_400_000);
const cacheKey = `${lastUpdated}:${dayStamp}`;
const cached = daysSinceCache.get(cacheKey);
if (cached !== undefined) return cached;
const then = Date.parse(lastUpdated);
if (Number.isNaN(then)) return Number.POSITIVE_INFINITY;
return Math.max(0, Math.floor((Date.now() - then) / 86_400_000));
const days = Number.isNaN(then)
? Number.POSITIVE_INFINITY
: Math.max(0, Math.floor((Date.now() - then) / 86_400_000));
daysSinceCache.set(cacheKey, days);
return days;
}
const updatedBucketCache = new Map<number, string>();
export function updatedBucketOf(days: number): string {
return updatedBuckets.find((bucket) => days <= bucket.max)?.label ?? "Older";
const cached = updatedBucketCache.get(days);
if (cached) return cached;
const bucket = updatedBuckets.find((option) => days <= option.max)?.label ?? "Older";
updatedBucketCache.set(days, bucket);
return bucket;
}
export const fileBuckets: { label: string; min: number; max: number }[] = [
@@ -33,11 +52,18 @@ export const fileBuckets: { label: string; min: number; max: number }[] = [
{ label: "11+ files", min: 11, max: Number.POSITIVE_INFINITY },
];
const fileBucketCache = new Map<number, string>();
export function fileBucketOf(files: number): string {
return (
fileBuckets.find((bucket) => files >= bucket.min && files <= bucket.max)
?.label ?? "1–2 files"
);
const cached = fileBucketCache.get(files);
if (cached) return cached;
const bucket =
fileBuckets.find((option) => files >= option.min && files <= option.max)?.label ??
"1–2 files";
fileBucketCache.set(files, bucket);
return bucket;
}
/**