diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 5baf9207..c53b9f98 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -33,8 +33,10 @@ import { FlywheelKPICard } from '@/components/dashboard/flywheel-kpi-card' import { AutomationEvidenceCard } from '@/components/dashboard/automation-evidence-card' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' -const STATUS_CHAIN_PREFETCH_LIMIT = 25 -const HOMEPAGE_INCIDENT_LIMIT = STATUS_CHAIN_PREFETCH_LIMIT +const HOMEPAGE_STATUS_CHAIN_PREFETCH_LIMIT = 8 +const HOMEPAGE_STATUS_CHAIN_CONCURRENCY = 2 +const HOMEPAGE_STATUS_CHAIN_TIMEOUT_MS = 6000 +const HOMEPAGE_INCIDENT_LIMIT = HOMEPAGE_STATUS_CHAIN_PREFETCH_LIMIT const HOMEPAGE_BLUEPRINT_STAGE_KEYS = ['signal', 'intake', 'ai', 'mcp', 'playbook', 'ansible', 'approval', 'verify'] as const type HomepageBlueprintStageKey = typeof HOMEPAGE_BLUEPRINT_STAGE_KEYS[number] @@ -712,7 +714,9 @@ export default function Home({ params }: { params: { locale: string } }) { isLoading: isStatusChainsLoading, } = useIncidentStatusChains({ incidentIds: incidents?.map(incident => incident.incident_id) ?? [], - limit: STATUS_CHAIN_PREFETCH_LIMIT, + limit: HOMEPAGE_STATUS_CHAIN_PREFETCH_LIMIT, + concurrency: HOMEPAGE_STATUS_CHAIN_CONCURRENCY, + timeoutMs: HOMEPAGE_STATUS_CHAIN_TIMEOUT_MS, refreshKey: incidentsLastUpdated?.toISOString() ?? null, }) @@ -864,8 +868,19 @@ export default function Home({ params }: { params: { locale: string } }) { const unavailableValue = tDashboard('automationDelivery.unavailableValue') const loadingStatus = tDashboard('automationDelivery.status.loading') const unavailableStatus = tDashboard('automationDelivery.status.unavailable') - const formatLiveEvidenceTime = (value: string | null | undefined) => { - if (!value) return automationBriefLoaded ? unavailableValue : loadingStatus + const hasAutomationBriefPart = (key: keyof HomepageAutomationBriefSnapshot) => ( + Object.prototype.hasOwnProperty.call(automationBrief, key) + ) + const callbackRepliesLoaded = hasAutomationBriefPart('callbackReplies') + const aiRouteStatusLoaded = hasAutomationBriefPart('aiRouteStatus') + const kmStaleCandidatesLoaded = hasAutomationBriefPart('kmStaleCandidates') + const dossierCoverageLoaded = hasAutomationBriefPart('dossierCoverage') + const eventRecurrenceLoaded = hasAutomationBriefPart('eventRecurrence') + const runsListLoaded = hasAutomationBriefPart('runsList') + const cicdEventsLoaded = hasAutomationBriefPart('cicdEvents') + const kmOwnerReviewBurndownLoaded = hasAutomationBriefPart('kmOwnerReviewBurndown') + const formatLiveEvidenceTime = (value: string | null | undefined, loaded = automationBriefLoaded) => { + if (!value) return loaded ? unavailableValue : loadingStatus const date = new Date(value) if (Number.isNaN(date.getTime())) return unavailableValue return date.toLocaleString(locale === 'en' ? 'en-US' : 'zh-TW', { @@ -917,7 +932,7 @@ export default function Home({ params }: { params: { locale: string } }) { const callbackTraceSummary = automationBrief.callbackReplies?.summary ?? null const missingTraceRecent24h = callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total ?? 0 const traceRecoveryStatus = callbackTraceSummary?.outbound_reply_markup_trace_ref_gap_recovery_status - ?? (automationBriefLoaded ? unavailableValue : loadingStatus) + ?? (callbackRepliesLoaded ? unavailableValue : loadingStatus) const traceRecoveryCount = callbackTraceSummary?.outbound_reply_markup_trace_ref_after_gap_total const aiRouteSelectedProvider = automationBrief.aiRouteStatus?.selected_provider ?? '--' const aiRouteLaneMode = automationBrief.aiRouteStatus?.lane_mode ?? '--' @@ -986,7 +1001,7 @@ export default function Home({ params }: { params: { locale: string } }) { const kmBurndownSnapshot = kmOwnerReviewBurndown?.current_snapshot ?? null const staleRatioLabel = typeof kmBurndownSnapshot?.stale_ratio === 'number' ? `${(kmBurndownSnapshot.stale_ratio * 100).toFixed(1)}%` - : automationBriefLoaded ? unavailableValue : loadingStatus + : kmOwnerReviewBurndownLoaded ? unavailableValue : loadingStatus const automationWorkToneStyle: Record = { live: { bg: '#f0faf2', border: '#9bc7a4', color: '#17602a' }, progress: { bg: '#fff7e8', border: '#d9b36f', color: '#8a5a08' }, @@ -1019,11 +1034,11 @@ export default function Home({ params }: { params: { locale: string } }) { title: tDashboard('automationDelivery.delivered.callbackEvidence.title'), status: hasCallbackEvidence ? tDashboard('automationDelivery.status.live') - : automationBriefLoaded + : callbackRepliesLoaded ? unavailableStatus : loadingStatus, detail: tDashboard('automationDelivery.delivered.callbackEvidence.detail', { - total: formatAutomationNumber(automationBrief.callbackReplies?.total, automationBriefLoaded), + total: formatAutomationNumber(automationBrief.callbackReplies?.total, callbackRepliesLoaded), }), href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, tone: hasCallbackEvidence ? 'live' : 'watching', @@ -1033,15 +1048,15 @@ export default function Home({ params }: { params: { locale: string } }) { title: tDashboard('automationDelivery.delivered.callbackTrace.title'), status: callbackTraceSummary && traceRecoveryStatus === 'recovered_after_gap' ? tDashboard('automationDelivery.status.progress') - : !automationBriefLoaded + : !callbackRepliesLoaded ? loadingStatus : callbackTraceSummary ? tDashboard('automationDelivery.status.watching') : unavailableStatus, detail: tDashboard('automationDelivery.delivered.callbackTrace.detail', { status: traceRecoveryStatus, - recovered: formatAutomationNumber(traceRecoveryCount, automationBriefLoaded), - recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), + recovered: formatAutomationNumber(traceRecoveryCount, callbackRepliesLoaded), + recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, callbackRepliesLoaded), }), href: `/${locale}/awooop/work-items?project_id=awoooi`, tone: traceRecoveryStatus === 'recovered_after_gap' ? 'progress' : 'watching', @@ -1118,7 +1133,7 @@ export default function Home({ params }: { params: { locale: string } }) { { key: 'kmGovernance', title: tDashboard('automationDelivery.remaining.kmGovernance.title'), - status: !automationBriefLoaded + status: !kmStaleCandidatesLoaded ? loadingStatus : typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? tDashboard('automationDelivery.status.progress') @@ -1126,8 +1141,8 @@ export default function Home({ params }: { params: { locale: string } }) { ? tDashboard('automationDelivery.status.live') : unavailableStatus, detail: tDashboard('automationDelivery.remaining.kmGovernance.detail', { - stale: formatAutomationNumber(kmStaleTotal, automationBriefLoaded), - days: formatAutomationNumber(kmStaleThreshold, automationBriefLoaded), + stale: formatAutomationNumber(kmStaleTotal, kmStaleCandidatesLoaded), + days: formatAutomationNumber(kmStaleThreshold, kmStaleCandidatesLoaded), }), href: `/${locale}/awooop/work-items?project_id=awoooi`, tone: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? 'progress' : hasKmStaleCandidates ? 'live' : 'watching', @@ -1135,7 +1150,7 @@ export default function Home({ params }: { params: { locale: string } }) { { key: 'callbackBacklogDecay', title: tDashboard('automationDelivery.remaining.callbackBacklogDecay.title'), - status: !automationBriefLoaded + status: !callbackRepliesLoaded ? loadingStatus : callbackTraceSummary && missingTraceRecent24h > 0 ? tDashboard('automationDelivery.status.progress') @@ -1143,9 +1158,9 @@ export default function Home({ params }: { params: { locale: string } }) { ? tDashboard('automationDelivery.status.live') : unavailableStatus, detail: tDashboard('automationDelivery.remaining.callbackBacklogDecay.detail', { - recent1h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_1h_total, automationBriefLoaded), - recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), - missing: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_total, automationBriefLoaded), + recent1h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_1h_total, callbackRepliesLoaded), + recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, callbackRepliesLoaded), + missing: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_total, callbackRepliesLoaded), }), href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, tone: callbackTraceSummary && missingTraceRecent24h > 0 ? 'progress' : callbackTraceSummary ? 'live' : 'watching', @@ -1210,10 +1225,9 @@ export default function Home({ params }: { params: { locale: string } }) { key: 'signal', title: tDashboard('automationDiagrams.workspace.flow.stages.signal'), status: tDashboard('automationDelivery.status.live'), - detail: tDashboard('automationDiagrams.workspace.values.callback', { - missing: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_total, automationBriefLoaded), - recent1h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_1h_total, automationBriefLoaded), - recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), + detail: tDashboard('automationDiagrams.workspace.liveEvidence.signal.metric', { + sources: formatAutomationNumber(dossierCoverageSummary?.source_count, dossierCoverageLoaded), + refs: formatAutomationNumber(dossierCoverageSummary?.source_ref_total, dossierCoverageLoaded), }), href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, tone: 'live', @@ -1222,18 +1236,18 @@ export default function Home({ params }: { params: { locale: string } }) { nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.signal.nextAction'), liveEvidence: { metric: tDashboard('automationDiagrams.workspace.liveEvidence.signal.metric', { - sources: formatAutomationNumber(dossierCoverageSummary?.source_count, automationBriefLoaded), - refs: formatAutomationNumber(dossierCoverageSummary?.source_ref_total, automationBriefLoaded), + sources: formatAutomationNumber(dossierCoverageSummary?.source_count, dossierCoverageLoaded), + refs: formatAutomationNumber(dossierCoverageSummary?.source_ref_total, dossierCoverageLoaded), }), detail: tDashboard('automationDiagrams.workspace.liveEvidence.signal.detail', { - missing: formatAutomationNumber(dossierCoverageSummary?.missing_source_refs_total, automationBriefLoaded), - duplicates: formatAutomationNumber(dossierCoverageSummary?.duplicate_total, automationBriefLoaded), - alert: formatAutomationNumber(dossierCoverageSummary?.alert_ref_total, automationBriefLoaded), - sentry: formatAutomationNumber(dossierCoverageSummary?.sentry_ref_total, automationBriefLoaded), - signoz: formatAutomationNumber(dossierCoverageSummary?.signoz_ref_total, automationBriefLoaded), + missing: formatAutomationNumber(dossierCoverageSummary?.missing_source_refs_total, dossierCoverageLoaded), + duplicates: formatAutomationNumber(dossierCoverageSummary?.duplicate_total, dossierCoverageLoaded), + alert: formatAutomationNumber(dossierCoverageSummary?.alert_ref_total, dossierCoverageLoaded), + sentry: formatAutomationNumber(dossierCoverageSummary?.sentry_ref_total, dossierCoverageLoaded), + signoz: formatAutomationNumber(dossierCoverageSummary?.signoz_ref_total, dossierCoverageLoaded), }), source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.dossierCoverage'), - updated: formatLiveEvidenceTime(dossierCoverageSummary?.latest_received_at), + updated: formatLiveEvidenceTime(dossierCoverageSummary?.latest_received_at, dossierCoverageLoaded), }, }, { @@ -1248,17 +1262,17 @@ export default function Home({ params }: { params: { locale: string } }) { nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.intake.nextAction'), liveEvidence: { metric: tDashboard('automationDiagrams.workspace.liveEvidence.intake.metric', { - runs: formatAutomationNumber(automationBrief.runsList?.total, automationBriefLoaded), - linked: formatAutomationNumber(recurrenceSummary?.linked_run_total, automationBriefLoaded), + runs: formatAutomationNumber(automationBrief.runsList?.total, runsListLoaded), + linked: formatAutomationNumber(recurrenceSummary?.linked_run_total, eventRecurrenceLoaded), }), detail: tDashboard('automationDiagrams.workspace.liveEvidence.intake.detail', { - stage: latestCicdEvent?.stage ?? (automationBriefLoaded ? unavailableValue : loadingStatus), - status: latestCicdEvent?.status ?? (automationBriefLoaded ? unavailableValue : loadingStatus), - commit: latestCicdEvent?.commit_sha?.slice(0, 7) ?? (automationBriefLoaded ? unavailableValue : loadingStatus), - attention: formatAutomationNumber(cicdNeedsAttention, automationBriefLoaded), + stage: latestCicdEvent?.stage ?? (cicdEventsLoaded ? unavailableValue : loadingStatus), + status: latestCicdEvent?.status ?? (cicdEventsLoaded ? unavailableValue : loadingStatus), + commit: latestCicdEvent?.commit_sha?.slice(0, 7) ?? (cicdEventsLoaded ? unavailableValue : loadingStatus), + attention: formatAutomationNumber(cicdNeedsAttention, cicdEventsLoaded), }), source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.runsAndCicd'), - updated: formatLiveEvidenceTime(latestCicdEvent?.created_at ?? latestRun?.created_at), + updated: formatLiveEvidenceTime(latestCicdEvent?.created_at ?? latestRun?.created_at, cicdEventsLoaded || runsListLoaded), }, }, { @@ -1280,12 +1294,12 @@ export default function Home({ params }: { params: { locale: string } }) { provider: aiRouteSelectedProvider, }), detail: tDashboard('automationDiagrams.workspace.liveEvidence.ai.detail', { - skipped: formatAutomationNumber(automationBrief.aiRouteStatus?.skipped_lanes?.length, automationBriefLoaded), - action: automationBrief.aiRouteStatus?.operator_action?.action ?? (automationBriefLoaded ? unavailableValue : loadingStatus), - reason: automationBrief.aiRouteStatus?.operator_action?.reason ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + skipped: formatAutomationNumber(automationBrief.aiRouteStatus?.skipped_lanes?.length, aiRouteStatusLoaded), + action: automationBrief.aiRouteStatus?.operator_action?.action ?? (aiRouteStatusLoaded ? unavailableValue : loadingStatus), + reason: automationBrief.aiRouteStatus?.operator_action?.reason ?? (aiRouteStatusLoaded ? unavailableValue : loadingStatus), }), source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.aiRouteStatus'), - updated: automationBriefLoaded ? tDashboard('automationDiagrams.workspace.liveEvidence.realtime') : loadingStatus, + updated: aiRouteStatusLoaded ? tDashboard('automationDiagrams.workspace.liveEvidence.realtime') : loadingStatus, }, }, { @@ -1295,7 +1309,7 @@ export default function Home({ params }: { params: { locale: string } }) { ? tDashboard('automationDelivery.status.live') : tDashboard('automationDelivery.status.watching'), detail: tDashboard('automationDiagrams.workspace.liveEvidence.mcp.metric', { - observations: formatAutomationNumber(runEvidenceSummary.mcpTotal, automationBriefLoaded), + observations: formatAutomationNumber(runEvidenceSummary.mcpTotal, runsListLoaded), gateway: formatAutomationNumber(statusChainEvidenceSummary.mcpGateway, true), }), href: `/${locale}/awooop/runs?project_id=awoooi`, @@ -1305,17 +1319,17 @@ export default function Home({ params }: { params: { locale: string } }) { nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.nextAction'), liveEvidence: { metric: tDashboard('automationDiagrams.workspace.liveEvidence.mcp.metric', { - observations: formatAutomationNumber(runEvidenceSummary.mcpTotal, automationBriefLoaded), + observations: formatAutomationNumber(runEvidenceSummary.mcpTotal, runsListLoaded), gateway: formatAutomationNumber(statusChainEvidenceSummary.mcpGateway, true), }), detail: tDashboard('automationDiagrams.workspace.liveEvidence.mcp.detail', { - success: formatAutomationNumber(runEvidenceSummary.mcpSuccess + statusChainEvidenceSummary.mcpGatewaySuccess, automationBriefLoaded), - failed: formatAutomationNumber(runEvidenceSummary.mcpFailed + statusChainEvidenceSummary.mcpGatewayFailed, automationBriefLoaded), - server: runEvidenceSummary.latestMcpServer ?? (automationBriefLoaded ? unavailableValue : loadingStatus), - route: runEvidenceSummary.latestRoute ?? (automationBriefLoaded ? unavailableValue : loadingStatus), + success: formatAutomationNumber(runEvidenceSummary.mcpSuccess + statusChainEvidenceSummary.mcpGatewaySuccess, runsListLoaded), + failed: formatAutomationNumber(runEvidenceSummary.mcpFailed + statusChainEvidenceSummary.mcpGatewayFailed, runsListLoaded), + server: runEvidenceSummary.latestMcpServer ?? (runsListLoaded ? unavailableValue : loadingStatus), + route: runEvidenceSummary.latestRoute ?? (runsListLoaded ? unavailableValue : loadingStatus), }), source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.runsAndStatusChain'), - updated: formatLiveEvidenceTime(latestRun?.created_at), + updated: formatLiveEvidenceTime(latestRun?.created_at, runsListLoaded), }, }, { @@ -1406,8 +1420,8 @@ export default function Home({ params }: { params: { locale: string } }) { title: tDashboard('automationDiagrams.workspace.flow.stages.verify'), status: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? tDashboard('automationDelivery.status.progress') : tDashboard('automationDelivery.status.live'), detail: tDashboard('automationDiagrams.workspace.values.km', { - stale: formatAutomationNumber(kmStaleTotal, automationBriefLoaded), - days: formatAutomationNumber(kmStaleThreshold, automationBriefLoaded), + stale: formatAutomationNumber(kmStaleTotal, kmStaleCandidatesLoaded), + days: formatAutomationNumber(kmStaleThreshold, kmStaleCandidatesLoaded), }), href: `/${locale}/knowledge-base`, tone: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? 'progress' : hasKmStaleCandidates ? 'live' : 'watching', @@ -1416,16 +1430,16 @@ export default function Home({ params }: { params: { locale: string } }) { nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.verify.nextAction'), liveEvidence: { metric: tDashboard('automationDiagrams.workspace.liveEvidence.verify.metric', { - stale: formatAutomationNumber(kmBurndownSnapshot?.stale_count ?? kmStaleTotal, automationBriefLoaded), + stale: formatAutomationNumber(kmBurndownSnapshot?.stale_count ?? kmStaleTotal, kmOwnerReviewBurndownLoaded || kmStaleCandidatesLoaded), ratio: staleRatioLabel, }), detail: tDashboard('automationDiagrams.workspace.liveEvidence.verify.detail', { - pending: formatAutomationNumber(kmOwnerReviewBurndown?.pending_owner_reviews, automationBriefLoaded), - completed: formatAutomationNumber(kmOwnerReviewBurndown?.completed_owner_reviews, automationBriefLoaded), - remaining: formatAutomationNumber(kmOwnerReviewBurndown?.entries_to_threshold, automationBriefLoaded), + pending: formatAutomationNumber(kmOwnerReviewBurndown?.pending_owner_reviews, kmOwnerReviewBurndownLoaded), + completed: formatAutomationNumber(kmOwnerReviewBurndown?.completed_owner_reviews, kmOwnerReviewBurndownLoaded), + remaining: formatAutomationNumber(kmOwnerReviewBurndown?.entries_to_threshold, kmOwnerReviewBurndownLoaded), }), source: tDashboard('automationDiagrams.workspace.liveEvidence.sources.kmBurndown'), - updated: formatLiveEvidenceTime(kmOwnerReviewBurndown?.generated_at), + updated: formatLiveEvidenceTime(kmOwnerReviewBurndown?.generated_at, kmOwnerReviewBurndownLoaded), }, }, ] diff --git a/apps/web/src/hooks/useIncidentStatusChains.ts b/apps/web/src/hooks/useIncidentStatusChains.ts index cdaa9076..a9525783 100644 --- a/apps/web/src/hooks/useIncidentStatusChains.ts +++ b/apps/web/src/hooks/useIncidentStatusChains.ts @@ -8,6 +8,7 @@ import { API_V1_URL } from '@/lib/config' interface UseIncidentStatusChainsOptions { incidentIds: string[] limit?: number + concurrency?: number projectId?: string refreshKey?: string | number | Date | null timeoutMs?: number @@ -22,6 +23,7 @@ interface UseIncidentStatusChainsResult { export function useIncidentStatusChains({ incidentIds, limit = 25, + concurrency = 4, projectId = 'awoooi', refreshKey = null, timeoutMs = 12000, @@ -44,40 +46,63 @@ export function useIncidentStatusChains({ let active = true const controller = new AbortController() const timeout = window.setTimeout(() => controller.abort(), timeoutMs) + const requestConcurrency = Math.max(1, Math.min(concurrency, requestedIncidentIds.length)) + let nextIndex = 0 + let activeRequests = 0 + let completedRequests = 0 setIsLoading(true) + setStatusChains(Object.fromEntries(requestedIncidentIds.map(incidentId => [incidentId, null]))) - Promise.all( - requestedIncidentIds.map(async (incidentId): Promise<[string, AwoooPStatusChain | null]> => { - const params = new URLSearchParams({ project_id: projectId, incident_id: incidentId }) - try { - const response = await fetch(`${API_V1_URL}/platform/status-chain?${params.toString()}`, { - cache: 'no-store', - signal: controller.signal, - }) - if (!response.ok) return [incidentId, null] - return [incidentId, await response.json() as AwoooPStatusChain] - } catch { - return [incidentId, null] - } - }) - ) - .then(entries => { - if (active) setStatusChains(Object.fromEntries(entries)) - }) - .catch(() => { - if (active) setStatusChains({}) - }) - .finally(() => { + const fetchStatusChain = async (incidentId: string): Promise => { + const params = new URLSearchParams({ project_id: projectId, incident_id: incidentId }) + try { + const response = await fetch(`${API_V1_URL}/platform/status-chain?${params.toString()}`, { + cache: 'no-store', + signal: controller.signal, + }) + if (!response.ok) return null + return await response.json() as AwoooPStatusChain + } catch { + return null + } + } + + const finish = () => { + completedRequests += 1 + if (completedRequests >= requestedIncidentIds.length) { window.clearTimeout(timeout) if (active) setIsLoading(false) - }) + } + } + + const pump = () => { + if (!active) return + while (activeRequests < requestConcurrency && nextIndex < requestedIncidentIds.length) { + const incidentId = requestedIncidentIds[nextIndex] + nextIndex += 1 + activeRequests += 1 + fetchStatusChain(incidentId) + .then(statusChain => { + if (active) { + setStatusChains(previous => ({ ...previous, [incidentId]: statusChain })) + } + }) + .finally(() => { + activeRequests -= 1 + finish() + pump() + }) + } + } + + pump() return () => { active = false window.clearTimeout(timeout) controller.abort() } - }, [incidentKey, projectId, requestedIncidentIds, refreshKey, timeoutMs]) + }, [concurrency, incidentKey, projectId, requestedIncidentIds, refreshKey, timeoutMs]) return { statusChains, requestedIncidentIds, isLoading } }