diff --git a/ui/src/ui/controllers/skills.ts b/ui/src/ui/controllers/skills.ts index f619bd81fd0..1737e34125f 100644 --- a/ui/src/ui/controllers/skills.ts +++ b/ui/src/ui/controllers/skills.ts @@ -87,6 +87,31 @@ function getErrorMessage(err: unknown) { return String(err); } +async function runStaleAwareRequest( + isCurrent: () => boolean, + request: () => Promise, + onSuccess: (value: T) => void, + onError: (err: unknown) => void, + onFinally: () => void, +) { + try { + const result = await request(); + if (!isCurrent()) { + return; + } + onSuccess(result); + } catch (err) { + if (!isCurrent()) { + return; + } + onError(err); + } finally { + if (isCurrent()) { + onFinally(); + } + } +} + export function setClawHubSearchQuery(state: SkillsState, query: string) { state.clawhubSearchQuery = query; state.clawhubInstallMessage = null; @@ -202,56 +227,53 @@ export async function searchClawHub(state: SkillsState, query: string) { state.clawhubSearchLoading = false; return; } + const client = state.client; // Clear stale entries as soon as a new search begins so the UI cannot act on // results that no longer match the current query while the next request is in flight. state.clawhubSearchResults = null; state.clawhubSearchLoading = true; state.clawhubSearchError = null; - try { - const res = await state.client.request<{ results: ClawHubSearchResult[] }>("skills.search", { - query, - limit: 20, - }); - if (query !== state.clawhubSearchQuery) { - return; - } - state.clawhubSearchResults = res?.results ?? []; - } catch (err) { - if (query !== state.clawhubSearchQuery) { - return; - } - state.clawhubSearchError = getErrorMessage(err); - } finally { - if (query === state.clawhubSearchQuery) { + await runStaleAwareRequest( + () => query === state.clawhubSearchQuery, + () => + client.request<{ results: ClawHubSearchResult[] }>("skills.search", { + query, + limit: 20, + }), + (res) => { + state.clawhubSearchResults = res?.results ?? []; + }, + (err) => { + state.clawhubSearchError = getErrorMessage(err); + }, + () => { state.clawhubSearchLoading = false; - } - } + }, + ); } export async function loadClawHubDetail(state: SkillsState, slug: string) { if (!state.client || !state.connected) { return; } + const client = state.client; state.clawhubDetailSlug = slug; state.clawhubDetailLoading = true; state.clawhubDetailError = null; state.clawhubDetail = null; - try { - const res = await state.client.request("skills.detail", { slug }); - if (slug !== state.clawhubDetailSlug) { - return; - } - state.clawhubDetail = res ?? null; - } catch (err) { - if (slug !== state.clawhubDetailSlug) { - return; - } - state.clawhubDetailError = getErrorMessage(err); - } finally { - if (slug === state.clawhubDetailSlug) { + await runStaleAwareRequest( + () => slug === state.clawhubDetailSlug, + () => client.request("skills.detail", { slug }), + (res) => { + state.clawhubDetail = res ?? null; + }, + (err) => { + state.clawhubDetailError = getErrorMessage(err); + }, + () => { state.clawhubDetailLoading = false; - } - } + }, + ); } export function closeClawHubDetail(state: SkillsState) { diff --git a/ui/src/ui/controllers/usage.ts b/ui/src/ui/controllers/usage.ts index 38717f13c41..392b51cf0c0 100644 --- a/ui/src/ui/controllers/usage.ts +++ b/ui/src/ui/controllers/usage.ts @@ -29,10 +29,8 @@ export type UsageState = { settings?: { gatewayUrl?: string }; }; -type DateInterpretationMode = "utc" | "gateway" | "specific"; - type UsageDateInterpretationParams = { - mode: DateInterpretationMode; + mode: "utc" | "specific"; utcOffset?: string; }; @@ -105,17 +103,15 @@ function normalizeGatewayCompatibilityKey(gatewayUrl?: string): string { } } -function resolveGatewayCompatibilityKey(state: UsageState): string { - return normalizeGatewayCompatibilityKey(state.settings?.gatewayUrl); -} - function shouldSendLegacyDateInterpretation(state: UsageState): boolean { - return !getLegacyUsageDateParamsCache().has(resolveGatewayCompatibilityKey(state)); + return !getLegacyUsageDateParamsCache().has( + normalizeGatewayCompatibilityKey(state.settings?.gatewayUrl), + ); } function rememberLegacyDateInterpretation(state: UsageState) { const cache = getLegacyUsageDateParamsCache(); - cache.add(resolveGatewayCompatibilityKey(state)); + cache.add(normalizeGatewayCompatibilityKey(state.settings?.gatewayUrl)); persistLegacyUsageDateParamsCache(cache); } @@ -143,11 +139,7 @@ const formatUtcOffset = (timezoneOffsetMinutes: number): string => { const buildDateInterpretationParams = ( timeZone: "local" | "utc", - includeDateInterpretation: boolean, -): UsageDateInterpretationParams | undefined => { - if (!includeDateInterpretation) { - return undefined; - } +): UsageDateInterpretationParams => { if (timeZone === "utc") { return { mode: "utc" }; } @@ -174,6 +166,15 @@ function toErrorMessage(err: unknown): string { return "request failed"; } +function applyUsageResults(state: UsageState, sessionsRes: unknown, costRes: unknown) { + if (sessionsRes) { + state.usageResult = sessionsRes as SessionsUsageResult; + } + if (costRes) { + state.usageCostSummary = costRes as CostUsageSummary; + } +} + export async function loadUsage( state: UsageState, overrides?: { @@ -192,10 +193,9 @@ export async function loadUsage( const startDate = overrides?.startDate ?? state.usageStartDate; const endDate = overrides?.endDate ?? state.usageEndDate; const runUsageRequests = (includeDateInterpretation: boolean) => { - const dateInterpretation = buildDateInterpretationParams( - state.usageTimeZone, - includeDateInterpretation, - ); + const dateInterpretation = includeDateInterpretation + ? buildDateInterpretationParams(state.usageTimeZone) + : undefined; return Promise.all([ client.request("sessions.usage", { startDate, @@ -212,26 +212,17 @@ export async function loadUsage( ]); }; - const applyUsageResults = (sessionsRes: unknown, costRes: unknown) => { - if (sessionsRes) { - state.usageResult = sessionsRes as SessionsUsageResult; - } - if (costRes) { - state.usageCostSummary = costRes as CostUsageSummary; - } - }; - const includeDateInterpretation = shouldSendLegacyDateInterpretation(state); try { const [sessionsRes, costRes] = await runUsageRequests(includeDateInterpretation); - applyUsageResults(sessionsRes, costRes); + applyUsageResults(state, sessionsRes, costRes); } catch (err) { if (includeDateInterpretation && isLegacyDateInterpretationUnsupportedError(err)) { // Older gateways reject `mode`/`utcOffset` in `sessions.usage`. // Remember this per gateway and retry once without those fields. rememberLegacyDateInterpretation(state); const [sessionsRes, costRes] = await runUsageRequests(false); - applyUsageResults(sessionsRes, costRes); + applyUsageResults(state, sessionsRes, costRes); } else { throw err; }