Swift Siesta - 如何将异步代码包含到请求链中?

Swift Siesta - How to include asynchronous code into a request chain?

我尝试使用 Siesta 装饰器启用一个流程,当登录用户获得 401 时,我的 authToken 会自动刷新。对于身份验证,我使用 Firebase。

在 Siesta 文档中有一个关于如何链接 Siesta 请求的直接示例,但我找不到如何返回异步 Firebase getIDTokenForcingRefresh:completion: working here. The problem is that Siesta always expects a Request or a RequestChainAction 的方法,这是不可能的Firebase 身份验证令牌刷新 api.

我知道请求链接主要是为仅 Siesta 用例完成的。但是有没有一种方法可以使用异步第三方 API,例如 FirebaseAuth,它并不完全适合图片?

代码如下:

init() {
    configure("**") {
        [=11=].headers["jwt"] = self.authToken
        
        [=11=].decorateRequests {
          self.refreshTokenOnAuthFailure(request: )
     }  
  }

func refreshTokenOnAuthFailure(request: Request) -> Request {
  return request.chained {
    guard case .failure(let error) = [=11=].response,  // Did request fail…
      error.httpStatusCode == 401 else {           // …because of expired token?
        return .useThisResponse                    // If not, use the response we got.
    }

    return .passTo(
      self.createAuthToken().chained {             // If so, first request a new token, then:
        if case .failure = [=11=].response {           // If token request failed…
          return .useThisResponse                  // …report that error.
        } else {
          return .passTo(request.repeated())       // We have a new token! Repeat the original request.
        }
      }
    )
  }
}

//What to do here? This should actually return a Siesta request
func createAuthToken() -> Void {
  let currentUser = Auth.auth().currentUser
  currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
    if let error = error {
      // Error
      return;
    }
    self.authToken = idToken
    self.invalidateConfiguration()
  }
}

编辑:

根据 Adrian 的建议答案,我尝试了以下解决方案。它仍然没有按预期工作:

如何确保仅在使用更新的 jwt 令牌发送原始请求后才调用 createUser 的回调? 这是我的无效解决方案 - 很高兴收到任何建议:

 // This ends up with a requestError "Request Cancelled" before the original request is triggered a second time with the refreshed jwt token.
    func createUser(user: UserModel, completion: @escaping CompletionHandler) {
    do {
        let userAsDict = try user.asDictionary()
        Api.sharedInstance.users.request(.post, json: userAsDict)
            .onSuccess {
                data in
                if let user: UserModel = data.content as? UserModel {
                    completion(user, nil)
                } else {
                    completion(nil, "Deserialization Error")
                }
        }.onFailure {
            requestError in
            completion(nil, requestError)
        }
    } catch let error {
        completion(nil, nil, "Serialization Error")
    }
}

Api class:

    class Api: Service {
    
    static let sharedInstance = Api()
    var jsonDecoder = JSONDecoder()
    var authToken: String? {
        didSet {
            // Rerun existing configuration closure using new value
            invalidateConfiguration()
            // Wipe any cached state if auth token changes
            wipeResources()
        }
    }
    
    init() {
        configureJSONDecoder(decoder: jsonDecoder)
        super.init(baseURL: Urls.baseUrl.rawValue, standardTransformers:[.text, .image])
        SiestaLog.Category.enabled = SiestaLog.Category.all
        
        configure("**") {
            [=13=].expirationTime = 1
            [=13=].headers["bearer-token"] = self.authToken
            [=13=].decorateRequests {
                self.refreshTokenOnAuthFailure(request: )
            }
        }
        
        self.configureTransformer("/users") {
            try self.jsonDecoder.decode(UserModel.self, from: [=13=].content)
        }
        
    }
    
    var users: Resource { return resource("/users") }
    
    func refreshTokenOnAuthFailure(request: Request) -> Request {
        return request.chained {
            guard case .failure(let error) = [=13=].response,  // Did request fail…
                error.httpStatusCode == 401 else {           // …because of expired token?
                    return .useThisResponse                    // If not, use the response we got.
            }
            return .passTo(
                self.refreshAuthToken(request: request).chained {          // If so, first request a new token, then:
                    if case .failure = [=13=].response {
                        return .useThisResponse                  // …report that error.
                    } else {
                        return .passTo(request.repeated())       // We have a new token! Repeat the original request.
                    }
                }
            )
        }
    }
    
    func refreshAuthToken(request: Request) -> Request {
        return Resource.prepareRequest(using: RefreshJwtRequest())
            .onSuccess {
                self.authToken = [=13=].text                  // …make future requests use it
        }
    }
}

RequestDelegate:

    class RefreshJwtRequest: RequestDelegate {

    func startUnderlyingOperation(passingResponseTo completionHandler: RequestCompletionHandler) {
        if let currentUser = Auth.auth().currentUser {
            currentUser.getIDTokenForcingRefresh(true) { idToken, error in
                if let error = error {
                    let reqError = RequestError(response: nil, content: nil, cause: error, userMessage: nil)
                    completionHandler.broadcastResponse(ResponseInfo(response: .failure(reqError)))
                    return;
                }
                let entity = Entity<Any>(content: idToken ?? "no token", contentType: "text/plain")
                completionHandler.broadcastResponse(ResponseInfo(response: .success(entity)))            }
        } else {
            let authError = RequestError(response: nil, content: nil, cause: AuthError.NOT_LOGGED_IN_ERROR, userMessage: "You are not logged in. Please login and try again.".localized())
            completionHandler.broadcastResponse(ResponseInfo(response: .failure(authError)))
        }
    }
    
    func cancelUnderlyingOperation() {}

    func repeated() -> RequestDelegate { RefreshJwtRequest() }

    private(set) var requestDescription: String = "CustomSiestaRequest"
}

首先,您应该按照 "How do I do request chaining with some arbitrary asynchronous code instead of a request?" 的思路重新表述问题的主旨,使其不是特定于 Firebase 的。这样对社区会更有用。然后您可以提及 Firebase 身份验证是您的特定用例。我将相应地回答你的问题。

(编辑:已经回答了这个问题,我现在看到保罗已经在这里回答了:)

Siesta 的 RequestDelegate 可以满足您的需求。引用文档:"This is useful for taking things that are not standard network requests, and wrapping them so they look to Siesta as if they are. To create a custom request, pass your delegate to Resource.prepareRequest(using:)."

您可以使用类似这样的东西作为一个粗略的起点 - 它 运行 是一个闭包(在您的情况下是 auth 调用),要么成功但没有输出,要么 returns 出现错误。根据用途,您可以调整它以使用实际内容填充实体。

// todo better name
class SiestaPseudoRequest: RequestDelegate {
    private let op: (@escaping (Error?) -> Void) -> Void

    init(op: @escaping (@escaping (Error?) -> Void) -> Void) {
        self.op = op
    }

    func startUnderlyingOperation(passingResponseTo completionHandler: RequestCompletionHandler) {
        op {
            if let error = [=10=] {
                // todo better
                let reqError = RequestError(response: nil, content: nil, cause: error, userMessage: nil)
                completionHandler.broadcastResponse(ResponseInfo(response: .failure(reqError)))
            }
            else {
                // todo you might well produce output at this point
                let ent = Entity<Any>(content: "", contentType: "text/plain")
                completionHandler.broadcastResponse(ResponseInfo(response: .success(ent)))
            }
        }
    }

    func cancelUnderlyingOperation() {}

    func repeated() -> RequestDelegate { SiestaPseudoRequest(op: op) }

    // todo better
    private(set) var requestDescription: String = "SiestaPseudoRequest"
}

我发现的一个问题是响应转换器不是 运行 用于这样的 "requests" - 转换器管道特定于 Siesta 的 NetworkRequest。 (这让我感到惊讶,我不确定我是否喜欢它,但 Siesta 似乎通常充满了正确的决定,所以我主要相信这是有充分理由的。)

可能值得留意其他非请求类行为。