From 5cb00fb4d45969a2780fd40041b191ed938a0ddf Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:33:13 +0800 Subject: [PATCH] fix(frontend): ignore stale account grants --- chat2db-community-client/package.json | 3 +- .../accountGrantsRequest.test.ts | 120 ++++++++++++++++++ .../accountGrantsRequest.ts | 27 ++++ .../GlobalExtendComponents/index.tsx | 35 ++--- 4 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.test.ts create mode 100644 chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.ts diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index cbac2c87be..24f7a46e6a 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -13,7 +13,7 @@ "build:desktop": "npm run build:web:desktop", "build:prod": "npm run build:web:prod", "build:web": "umi build", - "prebuild:web:community": "yarn test:runtime-edition-storage && yarn test:chat-answer-update && yarn test:tree-title-highlight && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:console-tab-name && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:editor-close && yarn test:invoice-routing && yarn test:result-set-ui && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:result-set-editor", + "prebuild:web:community": "yarn test:runtime-edition-storage && yarn test:chat-answer-update && yarn test:tree-title-highlight && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:data-source-authorization && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:console-tab-name && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:editor-close && yarn test:invoice-routing && yarn test:result-set-ui && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:result-set-editor && yarn test:account-grants-request", "postbuild:web:community": "node ./scripts/verify-production-bundles.cjs", "build:web:2java": "yarn run build:web:prod && rm -rf ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/* && cp -r dist/index.html ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/", "build:web:desktop": "cross-env UMI_ENV=desktop cross-env APP_NAME=chat2db-pro cross-env APP_VERSION=${npm_config_app_version} cross-env PRINT_LOGS=${npm_config_print_logs} cross-env APP_PORT=${npm_config_app_port} umi build", @@ -90,6 +90,7 @@ "test:task-center": "tsx src/store/importExport/taskCenterUtils.test.ts", "test:workspace-tab-drag": "tsx src/pages/main/workspace/components/WorkspaceTabs/workspaceTabDrop.test.ts", "test:verification-code-countdown": "tsx src/utils/verificationCodeCountdown.test.ts && tsx src/utils/latestRequest.test.ts", + "test:account-grants-request": "tsx src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.test.ts", "modify:package:name": "node ./scripts/modify-package-name.js", "start": "APP_VERSION=${npm_config_app_version} UMI_DEV_SERVER_COMPRESS=none umi dev", "start:desktop:hot": "cross-env UMI_ENV=desktop cross-env APP_VERSION=${npm_config_app_version} PORT=8888 umi dev", diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.test.ts b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.test.ts new file mode 100644 index 0000000000..092a169ba0 --- /dev/null +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.test.ts @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { invalidateLatestRequest } from '@/utils/latestRequest'; +import { loadLatestAccountGrants } from './accountGrantsRequest'; + +interface Deferred { + promise: Promise; + resolve: (value: T) => void; + reject: (error: unknown) => void; +} + +function deferred(): Deferred { + let resolve!: (value: T) => void; + let reject!: (error: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +async function testStaleResponseCannotReplaceLatestAccount() { + const requestGenerationRef = { current: 0 }; + const firstRequest = deferred(); + const secondRequest = deferred(); + let displayedGrants = ['initial']; + let settleCount = 0; + + const runFirstRequest = loadLatestAccountGrants( + requestGenerationRef, + () => firstRequest.promise, + (grants) => { + displayedGrants = grants; + }, + () => { + settleCount += 1; + }, + ); + const runSecondRequest = loadLatestAccountGrants( + requestGenerationRef, + () => secondRequest.promise, + (grants) => { + displayedGrants = grants; + }, + () => { + settleCount += 1; + }, + ); + + firstRequest.resolve(['old-account-grant']); + await runFirstRequest; + + assert.deepEqual(displayedGrants, ['initial']); + assert.equal(settleCount, 0, 'an old request must not stop the latest request spinner'); + + secondRequest.resolve(['new-account-grant']); + await runSecondRequest; + + assert.deepEqual(displayedGrants, ['new-account-grant']); + assert.equal(settleCount, 1); +} + +async function testLatestFailureClearsGrantsAndSettles() { + const requestGenerationRef = { current: 0 }; + let displayedGrants = ['previous-grant']; + let settled = false; + + await loadLatestAccountGrants( + requestGenerationRef, + async () => { + throw new Error('request failed'); + }, + (grants) => { + displayedGrants = grants; + }, + () => { + settled = true; + }, + ); + + assert.deepEqual(displayedGrants, []); + assert.equal(settled, true); +} + +async function testUnmountedRequestCannotUpdateState() { + const requestGenerationRef = { current: 0 }; + const request = deferred(); + let updateCount = 0; + let settleCount = 0; + + const runRequest = loadLatestAccountGrants( + requestGenerationRef, + () => request.promise, + () => { + updateCount += 1; + }, + () => { + settleCount += 1; + }, + ); + + invalidateLatestRequest(requestGenerationRef); + request.resolve(['ignored-grant']); + await runRequest; + + assert.equal(updateCount, 0); + assert.equal(settleCount, 0); +} + +Promise.all([ + testStaleResponseCannotReplaceLatestAccount(), + testLatestFailureClearsGrantsAndSettles(), + testUnmountedRequestCannotUpdateState(), +]) + .then(() => { + console.log('Account grants request tests passed'); + }) + .catch((error) => { + console.error(error); + process.exitCode = 1; + }); diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.ts b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.ts new file mode 100644 index 0000000000..a3155a9b3d --- /dev/null +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/accountGrantsRequest.ts @@ -0,0 +1,27 @@ +import { beginLatestRequest, isLatestRequest, type RequestGenerationRef } from '@/utils/latestRequest'; + +type AccountGrantsLoader = () => Promise; + +export async function loadLatestAccountGrants( + requestGenerationRef: RequestGenerationRef, + loadGrants: AccountGrantsLoader, + updateGrants: (grants: string[]) => void, + settleLoading: () => void, +) { + const requestGeneration = beginLatestRequest(requestGenerationRef); + + try { + const grants = await loadGrants(); + if (isLatestRequest(requestGenerationRef, requestGeneration)) { + updateGrants(grants || []); + } + } catch (_error) { + if (isLatestRequest(requestGenerationRef, requestGeneration)) { + updateGrants([]); + } + } finally { + if (isLatestRequest(requestGenerationRef, requestGeneration)) { + settleLoading(); + } + } +} diff --git a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/index.tsx b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/index.tsx index 13f14980ed..152bdb9e88 100644 --- a/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/index.tsx +++ b/chat2db-community-client/src/pages/main/workspace/components/WorkspaceExtend/GlobalExtendComponents/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useWorkspaceStore } from '@/store/workspace'; import { useTreeStore } from '@/store/tree'; import { GlobalComponents } from '../config'; @@ -11,6 +11,8 @@ import { Spin } from 'antd'; import i18n from '@/i18n'; import type { TreeNodeData } from '@/typings/tree'; import accountAdminService from '@/service/accountAdmin'; +import { invalidateLatestRequest } from '@/utils/latestRequest'; +import { loadLatestAccountGrants } from './accountGrantsRequest'; const GlobalExtendComponents = () => { const { styles } = useStyles(); @@ -101,28 +103,29 @@ const AccountGrants = ({ data }: AccountGrantsProps) => { const { styles } = useStyles(); const [loading, setLoading] = useState(false); const [grants, setGrants] = useState([]); + const requestGenerationRef = useRef(0); useEffect(() => { if (!data?.dataSourceId || !data.user || !data.host) { setGrants([]); + setLoading(false); return; } setLoading(true); - accountAdminService - .grants({ - dataSourceId: data.dataSourceId, - user: data.user, - host: data.host, - }) - .then((res) => { - setGrants(res || []); - }) - .catch(() => { - setGrants([]); - }) - .finally(() => { - setLoading(false); - }); + void loadLatestAccountGrants( + requestGenerationRef, + () => + accountAdminService.grants({ + dataSourceId: data.dataSourceId, + user: data.user, + host: data.host, + }), + setGrants, + () => setLoading(false), + ); + return () => { + invalidateLatestRequest(requestGenerationRef); + }; }, [data?.dataSourceId, data?.user, data?.host]); return (