From e14d8848cb4f1179a9d820052d719a9ca37afcdf Mon Sep 17 00:00:00 2001 From: Maya Gore Date: Thu, 5 Mar 2026 22:06:41 -0600 Subject: [PATCH 1/3] Handle individual function retrieve failures gracefully One failing Functions.retrieve() call (deleted repo, etc.) no longer crashes the entire functions list or landing page. Each call is wrapped in try/catch so failing functions are skipped instead of breaking Promise.all. Co-Authored-By: Claude Opus 4.6 --- objectiveai-web/app/functions/page.tsx | 59 ++++++++++++++------------ objectiveai-web/app/page.tsx | 43 ++++++++++--------- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/objectiveai-web/app/functions/page.tsx b/objectiveai-web/app/functions/page.tsx index 01ef8cd49..84d11688f 100644 --- a/objectiveai-web/app/functions/page.tsx +++ b/objectiveai-web/app/functions/page.tsx @@ -60,34 +60,39 @@ export default function FunctionsPage() { } } - // Fetch details for each unique function via API route - const functionItems: FunctionItem[] = await Promise.all( - Array.from(uniqueFunctions.values()).map(async (fn) => { - const slug = `${fn.owner}/${fn.repository}`; - - // Fetch full function details via SDK - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); - - const category = deriveCategory(details); - const name = deriveDisplayName(fn.repository); - - // Extract tags from repository name - const tags = fn.repository.split("-").filter((t: string) => t.length > 2); - if (details.type === "vector.function") tags.push("ranking"); - else tags.push("scoring"); - - return { - slug, - owner: fn.owner, - repository: fn.repository, - commit: fn.commit, - name, - description: details.description || `${name} function`, - category, - tags, - }; + // Fetch details for each unique function (skip any that fail) + const functionItems: FunctionItem[] = (await Promise.all( + Array.from(uniqueFunctions.values()).map(async (fn): Promise => { + try { + const slug = `${fn.owner}/${fn.repository}`; + + // Fetch full function details via SDK + const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); + + const category = deriveCategory(details); + const name = deriveDisplayName(fn.repository); + + // Extract tags from repository name + const tags = fn.repository.split("-").filter((t: string) => t.length > 2); + if (details.type === "vector.function") tags.push("ranking"); + else tags.push("scoring"); + + return { + slug, + owner: fn.owner, + repository: fn.repository, + commit: fn.commit, + name, + description: details.description || `${name} function`, + category, + tags, + }; + } catch { + // Skip functions that fail to load (deleted repo, etc.) + return null; + } }) - ); + )).filter((fn): fn is FunctionItem => fn !== null); setFunctions(functionItems); } catch (err) { diff --git a/objectiveai-web/app/page.tsx b/objectiveai-web/app/page.tsx index c8fcadf6c..b36ebbf57 100644 --- a/objectiveai-web/app/page.tsx +++ b/objectiveai-web/app/page.tsx @@ -52,29 +52,34 @@ export default function Home() { // Limit to FEATURED_COUNT const limitedFunctions = Array.from(uniqueFunctions.values()).slice(0, FEATURED_COUNT); - const functionItems: FeaturedFunction[] = await Promise.all( - limitedFunctions.map(async (fn) => { - const slug = `${fn.owner}/${fn.repository}`; - // Fetch full function details via SDK - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); + const functionItems: FeaturedFunction[] = (await Promise.all( + limitedFunctions.map(async (fn): Promise => { + try { + const slug = `${fn.owner}/${fn.repository}`; + // Fetch full function details via SDK + const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); - const category = deriveCategory(details); - const name = deriveDisplayName(fn.repository); + const category = deriveCategory(details); + const name = deriveDisplayName(fn.repository); - // Extract tags from repository name - const tags = fn.repository.split("-").filter((t: string) => t.length > 2); - if (details.type === "vector.function") tags.push("ranking"); - else tags.push("scoring"); + // Extract tags from repository name + const tags = fn.repository.split("-").filter((t: string) => t.length > 2); + if (details.type === "vector.function") tags.push("ranking"); + else tags.push("scoring"); - return { - slug, - name, - description: details.description || `${name} function`, - category, - tags, - }; + return { + slug, + name, + description: details.description || `${name} function`, + category, + tags, + }; + } catch { + // Skip functions that fail to load + return null; + } }) - ); + )).filter((fn): fn is FeaturedFunction => fn !== null); setFunctions(functionItems); } catch { From 87e9a326de3331d5207faf3199243b14de17695c Mon Sep 17 00:00:00 2001 From: Maya Gore Date: Thu, 5 Mar 2026 22:23:32 -0600 Subject: [PATCH 2/3] Add 5s abort timeout to function retrieve calls Prevents the landing page and functions list from hanging indefinitely if a single Functions.retrieve() call is slow or unresponsive. Co-Authored-By: Claude Opus 4.6 --- objectiveai-web/app/functions/page.tsx | 6 ++++-- objectiveai-web/app/page.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/objectiveai-web/app/functions/page.tsx b/objectiveai-web/app/functions/page.tsx index 84d11688f..fd90c586c 100644 --- a/objectiveai-web/app/functions/page.tsx +++ b/objectiveai-web/app/functions/page.tsx @@ -66,8 +66,10 @@ export default function FunctionsPage() { try { const slug = `${fn.owner}/${fn.repository}`; - // Fetch full function details via SDK - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }); + clearTimeout(timeout); const category = deriveCategory(details); const name = deriveDisplayName(fn.repository); diff --git a/objectiveai-web/app/page.tsx b/objectiveai-web/app/page.tsx index b36ebbf57..23b9d379e 100644 --- a/objectiveai-web/app/page.tsx +++ b/objectiveai-web/app/page.tsx @@ -56,8 +56,10 @@ export default function Home() { limitedFunctions.map(async (fn): Promise => { try { const slug = `${fn.owner}/${fn.repository}`; - // Fetch full function details via SDK - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }); + clearTimeout(timeout); const category = deriveCategory(details); const name = deriveDisplayName(fn.repository); From 00aa914e21701487501f5bd06defc342e2513082 Mon Sep 17 00:00:00 2001 From: Maya Gore Date: Fri, 6 Mar 2026 17:47:37 -0600 Subject: [PATCH 3/3] =?UTF-8?q?Progressive=20function=20loading=20?= =?UTF-8?q?=E2=80=94=20show=20cards=20as=20each=20detail=20resolves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of waiting for all Functions.retrieve() calls to complete before rendering anything, fire all fetches concurrently and update state as each resolves. This means: - First card appears in ~500ms instead of waiting 5s+ for slowest - 3 functions that timeout at 5s no longer block the other 17 - Skeleton cards shown during initial list fetch - Landing page slots fill in progressively (skeleton → real card) - Pagination independent of detail loading state - Empty state only shown after all fetches have settled Co-Authored-By: Claude Opus 4.6 --- objectiveai-web/app/functions/page.tsx | 120 ++++++++---- objectiveai-web/app/page.tsx | 260 ++++++++++++------------- 2 files changed, 206 insertions(+), 174 deletions(-) diff --git a/objectiveai-web/app/functions/page.tsx b/objectiveai-web/app/functions/page.tsx index fd90c586c..854d8c425 100644 --- a/objectiveai-web/app/functions/page.tsx +++ b/objectiveai-web/app/functions/page.tsx @@ -7,7 +7,7 @@ import { createPublicClient } from "../../lib/client"; import { deriveCategory, deriveDisplayName } from "../../lib/objectiveai"; import { NAV_HEIGHT_CALCULATION_DELAY_MS, STICKY_BAR_HEIGHT, STICKY_SEARCH_OVERLAP } from "../../lib/constants"; import { useResponsive } from "../../hooks/useResponsive"; -import { LoadingSpinner, ErrorAlert, EmptyState } from "../../components/ui"; +import { ErrorAlert, EmptyState } from "../../components/ui"; // Function item type for UI interface FunctionItem { @@ -28,7 +28,8 @@ const LOAD_MORE_COUNT = 6; export default function FunctionsPage() { const [functions, setFunctions] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [isListLoading, setIsListLoading] = useState(true); + const [detailsLoaded, setDetailsLoaded] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("All"); @@ -40,12 +41,16 @@ export default function FunctionsPage() { const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT); const searchRef = useRef(null); - // Fetch functions from API + // Fetch functions from API — progressive loading useEffect(() => { + let cancelled = false; + async function fetchFunctions() { try { - setIsLoading(true); + setIsListLoading(true); + setDetailsLoaded(false); setError(null); + setFunctions([]); // Get all functions via SDK const client = createPublicClient(); @@ -60,51 +65,67 @@ export default function FunctionsPage() { } } - // Fetch details for each unique function (skip any that fail) - const functionItems: FunctionItem[] = (await Promise.all( - Array.from(uniqueFunctions.values()).map(async (fn): Promise => { - try { - const slug = `${fn.owner}/${fn.repository}`; + if (cancelled) return; + setIsListLoading(false); + + // Fire all detail fetches concurrently — update state as each resolves + const entries = Array.from(uniqueFunctions.values()); + let settled = 0; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }); + entries.forEach((fn) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }) + .then((details) => { clearTimeout(timeout); + if (cancelled) return; - const category = deriveCategory(details); const name = deriveDisplayName(fn.repository); - - // Extract tags from repository name const tags = fn.repository.split("-").filter((t: string) => t.length > 2); if (details.type === "vector.function") tags.push("ranking"); else tags.push("scoring"); - return { - slug, + const item: FunctionItem = { + slug: `${fn.owner}/${fn.repository}`, owner: fn.owner, repository: fn.repository, commit: fn.commit, name, description: details.description || `${name} function`, - category, + category: deriveCategory(details), tags, }; - } catch { - // Skip functions that fail to load (deleted repo, etc.) - return null; - } - }) - )).filter((fn): fn is FunctionItem => fn !== null); - - setFunctions(functionItems); + + setFunctions(prev => [...prev, item]); + }) + .catch(() => { + clearTimeout(timeout); + // Skip functions that fail to load + }) + .finally(() => { + settled++; + if (settled === entries.length && !cancelled) { + setDetailsLoaded(true); + } + }); + }); + + // Edge case: no functions to fetch details for + if (entries.length === 0) { + setDetailsLoaded(true); + } } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load functions"); - } finally { - setIsLoading(false); + if (!cancelled) { + setError(err instanceof Error ? err.message : "Failed to load functions"); + setIsListLoading(false); + setDetailsLoaded(true); + } } } fetchFunctions(); + return () => { cancelled = true; }; }, []); // Load pinned functions from localStorage @@ -292,8 +313,37 @@ export default function FunctionsPage() { flexDirection: 'column', width: '100%', }}> - {/* Only render grid when we have results */} - {!isLoading && !error && visibleFunctions.length > 0 && ( + {/* Skeleton grid while list is loading */} + {isListLoading && ( +
+ {Array.from({ length: INITIAL_VISIBLE_COUNT }).map((_, i) => ( +
+
+
+
+
+ ))} +
+ )} + + {/* Function cards — render progressively as details arrive */} + {!isListLoading && !error && visibleFunctions.length > 0 && ( <>
)} - {isLoading && ( - - )} - - {error && !isLoading && ( + {error && !isListLoading && ( )} - {!isLoading && !error && filteredFunctions.length === 0 && ( + {!isListLoading && !error && detailsLoaded && filteredFunctions.length === 0 && ( )}
diff --git a/objectiveai-web/app/page.tsx b/objectiveai-web/app/page.tsx index 23b9d379e..d14489d84 100644 --- a/objectiveai-web/app/page.tsx +++ b/objectiveai-web/app/page.tsx @@ -27,20 +27,23 @@ interface FeaturedFunction { export default function Home() { const { isMobile } = useResponsive(); - const [functions, setFunctions] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [slots, setSlots] = useState<(FeaturedFunction | null)[]>( + Array.from({ length: FEATURED_COUNT }, () => null) + ); + const [isListLoading, setIsListLoading] = useState(true); - // Fetch functions from API + // Fetch functions from API — progressive loading useEffect(() => { + let cancelled = false; + async function fetchFunctions() { try { - setIsLoading(true); + setIsListLoading(true); - // Fetch functions list via SDK const client = createPublicClient(); const result = await Functions.list(client); - // Deduplicate by owner/repository (same function may have multiple commits) + // Deduplicate by owner/repository const uniqueFunctions = new Map(); for (const fn of result.data) { const key = `${fn.owner}/${fn.repository}`; @@ -49,49 +52,55 @@ export default function Home() { } } - // Limit to FEATURED_COUNT - const limitedFunctions = Array.from(uniqueFunctions.values()).slice(0, FEATURED_COUNT); + const entries = Array.from(uniqueFunctions.values()).slice(0, FEATURED_COUNT); + if (cancelled) return; + + // Initialize slots to match actual count + setSlots(Array.from({ length: entries.length }, () => null)); + setIsListLoading(false); - const functionItems: FeaturedFunction[] = (await Promise.all( - limitedFunctions.map(async (fn): Promise => { - try { - const slug = `${fn.owner}/${fn.repository}`; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }); + // Fire all detail fetches — fill slots as each resolves + entries.forEach((fn, index) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }) + .then((details) => { clearTimeout(timeout); + if (cancelled) return; - const category = deriveCategory(details); const name = deriveDisplayName(fn.repository); - - // Extract tags from repository name const tags = fn.repository.split("-").filter((t: string) => t.length > 2); if (details.type === "vector.function") tags.push("ranking"); else tags.push("scoring"); - return { - slug, + const item: FeaturedFunction = { + slug: `${fn.owner}/${fn.repository}`, name, description: details.description || `${name} function`, - category, + category: deriveCategory(details), tags, }; - } catch { - // Skip functions that fail to load - return null; - } - }) - )).filter((fn): fn is FeaturedFunction => fn !== null); - setFunctions(functionItems); + setSlots(prev => { + const next = [...prev]; + next[index] = item; + return next; + }); + }) + .catch(() => { + clearTimeout(timeout); + }); + }); } catch { - // Silent failure - page still renders, just without featured functions - } finally { - setIsLoading(false); + if (!cancelled) { + setIsListLoading(false); + } } } fetchFunctions(); + return () => { cancelled = true; }; }, []); return ( @@ -186,122 +195,99 @@ export default function Home() {
- {/* Function Cards Grid */} + {/* Function Cards Grid — slots fill progressively */}
- {isLoading ? ( - // Loading skeleton - Array.from({ length: FEATURED_COUNT }).map((_, i) => ( -
fn ? ( + +
+ + {fn.category} + +

+ {fn.name} +

+

+ {fn.description} +

-
+ display: 'flex', + flexWrap: 'wrap', + gap: '4px', + marginBottom: '10px', + }}> + {fn.tags.slice(0, 2).map(tag => ( + + {tag} + + ))} + {fn.tags.length > 2 && ( + + +{fn.tags.length - 2} + + )} +
-
- )) - ) : functions.length > 0 ? ( - functions.map(fn => ( - -
- - {fn.category} - -

- {fn.name} -

-

- {fn.description} -

-
- {fn.tags.slice(0, 2).map(tag => ( - - {tag} - - ))} - {fn.tags.length > 2 && ( - - +{fn.tags.length - 2} - - )} -
-
- Open -
+ Open
- - )) +
+ ) : ( - // Empty state +
+
+
+
+
+ ))} + {!isListLoading && slots.length === 0 && (