This is an automated email from the ASF dual-hosted git repository.

dengliming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new c035003c fix: clear loading state when an effect throws or is 
cancelled (#711)
c035003c is described below

commit c035003cea34b40c0efdfe3f77a9849cf11b2e87
Author: Animesh Kumar <[email protected]>
AuthorDate: Sat Oct 10 22:16:30 2026 +0530

    fix: clear loading state when an effect throws or is cancelled (#711)
    
    * fix: clear loading state when an effect throws or is cancelled
    
    dva-loading 2.x dispatches HIDE only after the wrapped effect returns.
    With no onError hook registered, dva-core rethrows effect errors, so a
    rejected request skips HIDE and leaves loading.effects, the model flag
    and the global flag stuck at true. Wrap dva-loading so HIDE is always
    dispatched from a finally block, keeping its reducer and only/except
    filtering.
    
    Fixes #602
    
    * fix: return the wrapped effect's result and await the cancelled task in 
the test
---
 src/index.js              |   2 +-
 src/utils/loading.js      |  48 ++++++++++++++++++
 src/utils/loading.test.js | 122 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 171 insertions(+), 1 deletion(-)

diff --git a/src/index.js b/src/index.js
index def17205..42bed748 100644
--- a/src/index.js
+++ b/src/index.js
@@ -21,7 +21,7 @@ import dva from "dva";
 import { createHashHistory as createHistory } from "history";
 // user BrowserHistory
 // import createHistory from 'history/createBrowserHistory';
-import createLoading from "dva-loading";
+import createLoading from "./utils/loading";
 import { initIntl } from "./utils/IntlUtils";
 
 // import 'moment/locale/zh-cn';
diff --git a/src/utils/loading.js b/src/utils/loading.js
new file mode 100644
index 00000000..6aef91ee
--- /dev/null
+++ b/src/utils/loading.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import createDvaLoading from "dva-loading";
+
+// Action types used by dva-loading's reducer (not exported by the package).
+const SHOW = "@@DVA_LOADING/SHOW";
+const HIDE = "@@DVA_LOADING/HIDE";
+
+// dva-loading 2.x dispatches HIDE only after the effect returns, so an effect
+// that throws (e.g. a rejected request) or is cancelled leaves its loading
+// flags set forever. Keep its reducer and only/except filtering, but always
+// dispatch HIDE from a finally block.
+export default function createLoading(opts = {}) {
+  const loading = createDvaLoading(opts);
+
+  function onEffect(effect, sagaEffects, model, actionType) {
+    if (loading.onEffect(effect, sagaEffects, model, actionType) === effect) {
+      return effect;
+    }
+    const { put } = sagaEffects;
+    const payload = { namespace: model.namespace, actionType };
+    return function* loadingEffect(...args) {
+      yield put({ type: SHOW, payload });
+      try {
+        return yield effect(...args);
+      } finally {
+        yield put({ type: HIDE, payload });
+      }
+    };
+  }
+
+  return { ...loading, onEffect };
+}
diff --git a/src/utils/loading.test.js b/src/utils/loading.test.js
new file mode 100644
index 00000000..dde2c16a
--- /dev/null
+++ b/src/utils/loading.test.js
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { runSaga, effects } from "dva/saga";
+import createLoading from "./loading";
+import globalModel from "../models/global";
+import { queryPlatform } from "../services/api";
+
+jest.mock("../services/api", () => ({ queryPlatform: jest.fn() }));
+jest.mock("antd", () => ({ message: { warn: jest.fn(), success: jest.fn() } 
}));
+jest.mock("../utils/IntlUtils", () => ({ getIntlContent: (key) => key }));
+jest.mock("../components/_utils/utils", () => ({
+  defaultNamespaceId: "default-namespace",
+}));
+
+function setup(opts) {
+  const loading = createLoading(opts);
+  const reducer = loading.extraReducers.loading;
+  let state = reducer(undefined, { type: "@@INIT" });
+  const actions = [];
+  const dispatch = (action) => {
+    actions.push(action.type);
+    state = reducer(state, action);
+  };
+  const run = (effect, model, actionType) =>
+    runSaga(
+      { dispatch, getState: () => ({}), onError: () => {} },
+      loading.onEffect(effect, effects, model, actionType),
+      { type: actionType },
+      effects,
+    );
+  return { loading, run, actions, getState: () => state };
+}
+
+describe("createLoading", () => {
+  it("hides loading after an effect resolves", async () => {
+    const { run, actions, getState } = setup();
+    function* ok() {
+      yield effects.call(() => Promise.resolve());
+    }
+
+    await run(ok, { namespace: "demo" }, "demo/ok").done;
+
+    expect(actions).toEqual(["@@DVA_LOADING/SHOW", "@@DVA_LOADING/HIDE"]);
+    expect(getState()).toEqual({
+      global: false,
+      models: { demo: false },
+      effects: { "demo/ok": false },
+    });
+  });
+
+  it("returns the wrapped effect's result", async () => {
+    const { run } = setup();
+    function* withResult() {
+      return yield effects.call(() => Promise.resolve("result"));
+    }
+
+    await expect(
+      run(withResult, { namespace: "demo" }, "demo/result").done,
+    ).resolves.toBe("result");
+  });
+
+  it("hides loading when a request rejects inside an effect", async () => {
+    queryPlatform.mockRejectedValue(new Error("network down"));
+    const { run, actions, getState } = setup();
+
+    await expect(
+      run(
+        globalModel.effects.fetchPlatform,
+        globalModel,
+        "global/fetchPlatform",
+      ).done,
+    ).rejects.toThrow("network down");
+
+    expect(actions).toEqual(["@@DVA_LOADING/SHOW", "@@DVA_LOADING/HIDE"]);
+    expect(getState()).toEqual({
+      global: false,
+      models: { global: false },
+      effects: { "global/fetchPlatform": false },
+    });
+  });
+
+  it("hides loading when an effect is cancelled", async () => {
+    const { run, getState } = setup();
+    function* pending() {
+      yield effects.call(() => new Promise(() => {}));
+    }
+
+    const task = run(pending, { namespace: "demo" }, "demo/pending");
+    expect(getState().effects["demo/pending"]).toBe(true);
+    task.cancel();
+    await task.done;
+
+    expect(getState().global).toBe(false);
+    expect(getState().effects["demo/pending"]).toBe(false);
+  });
+
+  it("leaves effects excluded by only/except unwrapped", () => {
+    const { loading } = setup({ only: ["demo/tracked"] });
+    function* untracked() {
+      yield effects.call(() => Promise.resolve());
+    }
+
+    expect(
+      loading.onEffect(untracked, effects, { namespace: "demo" }, 
"demo/other"),
+    ).toBe(untracked);
+  });
+});

Reply via email to