fix: prevent duplicate running status requests (#175)

This commit is contained in:
cxymds
2026-07-21 09:11:59 +08:00
committed by GitHub
parent dfcc795823
commit 102114fe3c
5 changed files with 41 additions and 5 deletions
+7 -3
View File
@@ -1,6 +1,6 @@
"use client"
import { createContext, useContext, useEffect, useState } from "react"
import { createContext, useContext, useEffect, useRef, useState } from "react"
import { usePathname, useRouter } from "next/navigation"
import { ApiClient } from "@/lib/api-client"
import { AwsClient } from "@/lib/aws4fetch"
@@ -22,6 +22,11 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
const [isReady, setIsReady] = useState(false)
const router = useRouter()
const pathname = usePathname()
const pathnameRef = useRef(pathname)
useEffect(() => {
pathnameRef.current = pathname
}, [pathname])
useEffect(() => {
if (!isAuthenticated || !credentials?.AccessKeyId) {
@@ -65,7 +70,7 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
return
}
if (pathname === "/403/") return
if (pathnameRef.current === "/403/") return
router.replace("/403/")
},
})
@@ -89,7 +94,6 @@ export function ApiProvider({ children }: { children: React.ReactNode }) {
credentials?.SecretAccessKey,
credentials?.SessionToken,
logout,
pathname,
router,
])
+6 -1
View File
@@ -3,6 +3,7 @@
import * as React from "react"
import { useCallback, useEffect, useState } from "react"
import { useSystem } from "@/hooks/use-system"
import { scheduleMicrotask } from "@/lib/schedule-microtask"
import {
normalizeDataUsageInfo,
normalizeMetricsInfo,
@@ -157,6 +158,7 @@ export function usePerformanceData() {
}, [getDataUsageInfo, getStorageInfo, getSystemInfo, getSystemMetrics])
useEffect(() => {
let cancelled = false
mountedRef.current = true
hasSystemDataRef.current = false
setSystemInfo({})
@@ -169,7 +171,9 @@ export function usePerformanceData() {
setSourceErrors({})
setLastUpdatedAt(null)
setMetricsUpdatedAt(null)
void refetch()
scheduleMicrotask(() => {
if (!cancelled) void refetch()
})
const interval = window.setInterval(() => {
if (document.visibilityState === "visible") void refetch()
@@ -181,6 +185,7 @@ export function usePerformanceData() {
document.addEventListener("visibilitychange", handleVisibilityChange)
return () => {
cancelled = true
mountedRef.current = false
requestVersionRef.current += 1
abortControllerRef.current?.abort()
+9 -1
View File
@@ -5,6 +5,7 @@ import { hasConsoleScopes, type ConsolePolicy } from "@/lib/console-policy-parse
import { hasConsoleCapability, type ConsoleCapability } from "@/lib/permission-capabilities"
import type { PermissionResourceContext } from "@/lib/permission-resources"
import { CONSOLE_SCOPES, PAGE_PERMISSIONS } from "@/lib/console-permissions"
import { scheduleMicrotask } from "@/lib/schedule-microtask"
import { useAuth } from "@/contexts/auth-context"
import { useApiOptional } from "@/contexts/api-context"
@@ -130,8 +131,15 @@ export function PermissionsProvider({ children }: { children: React.ReactNode })
)
useEffect(() => {
let cancelled = false
if (api && isAuthenticated && !hasResolvedAdmin) {
void fetchAdminStatus()
scheduleMicrotask(() => {
if (!cancelled) void fetchAdminStatus()
})
}
return () => {
cancelled = true
}
}, [api, isAuthenticated, hasResolvedAdmin, fetchAdminStatus])
@@ -5,6 +5,7 @@ import fs from "node:fs"
const rootLayout = fs.readFileSync("app/layout.tsx", "utf8")
const dashboardLayout = fs.readFileSync("app/(dashboard)/layout.tsx", "utf8")
const authContext = fs.readFileSync("contexts/auth-context.tsx", "utf8")
const apiContext = fs.readFileSync("contexts/api-context.tsx", "utf8")
const loginPage = fs.readFileSync("app/(auth)/auth/login/page.tsx", "utf8")
const oidcCallbackPage = fs.readFileSync("app/(auth)/auth/oidc-callback/page.tsx", "utf8")
const i18nProvider = fs.readFileSync("components/providers/i18n-provider.tsx", "utf8")
@@ -33,6 +34,17 @@ test("dashboard layout owns dashboard providers before the auth guard", () => {
assert.ok(dashboardLayout.indexOf("<PermissionsProvider>") < dashboardLayout.indexOf("<DashboardAuthGuard>"))
})
test("dashboard navigation does not recreate the API client", () => {
const apiSetupEffect = apiContext.match(
/useEffect\(\(\) => \{\n if \(!isAuthenticated[\s\S]*?\n \}, \[([\s\S]*?)\]\)/,
)
assert.ok(apiSetupEffect)
assert.doesNotMatch(apiSetupEffect[1], /pathname/)
assert.match(apiContext, /pathnameRef\.current = pathname/)
assert.match(apiContext, /pathnameRef\.current === "\/403\/"/)
})
test("auth routes do not depend on dashboard permission resolution", () => {
assert.doesNotMatch(loginPage, /useFirstAccessibleDashboardRoute/)
assert.doesNotMatch(oidcCallbackPage, /useFirstAccessibleDashboardRoute/)
+7
View File
@@ -4,6 +4,7 @@ import fs from "node:fs"
const hookSource = fs.readFileSync("hooks/use-performance-data.ts", "utf8")
const systemSource = fs.readFileSync("hooks/use-system.ts", "utf8")
const permissionsSource = fs.readFileSync("hooks/use-permissions.tsx", "utf8")
const pageSource = fs.readFileSync("app/(dashboard)/status/page.tsx", "utf8")
const serverSource = fs.readFileSync("app/(dashboard)/_components/performance-server-list.tsx", "utf8")
const usageSource = fs.readFileSync("app/(dashboard)/_components/performance-usage-card.tsx", "utf8")
@@ -26,6 +27,12 @@ test("performance refresh keeps partial data and rejects stale responses", () =>
assert.ok((systemSource.match(/suppress403Redirect: true/g) ?? []).length >= 3)
})
test("running status skips Strict Mode preview requests", () => {
assert.match(hookSource, /scheduleMicrotask\(\(\) => \{\s*if \(!cancelled\) void refetch\(\)/)
assert.match(hookSource, /return \(\) => \{\s*cancelled = true/)
assert.match(permissionsSource, /scheduleMicrotask\(\(\) => \{\s*if \(!cancelled\) void fetchAdminStatus\(\)/)
})
test("running status distinguishes unknown values and exposes recovery feedback", () => {
assert.match(pageSource, /role="alert"/)
assert.match(pageSource, /aria-busy=\{refreshing\}/)