Request

public protocol Request : AnyObject

An API request. Provides notification hooks about the status of the request, and allows cancellation.

Note that this represents only a single request, whereas ResourceObservers receive notifications about all resource load requests, no matter who initiated them. Note also that these hooks are available for all requests, whereas ResourceObservers only receive notifications about changes triggered by load(), loadIfNeeded(), and overrideLocalData(...).

Request guarantees that it will call any given callback at most one time.

Callbacks are always called on the main thread.

Note

There is no race condition between a callback being added and a response arriving. If you add a callback after the response has already arrived, the callback is still called as usual. In other words, when attaching a hook, you do not need to worry about where the request is in its lifecycle. Except for how soon it’s called, your hook will see the same behavior regardless of whether the request has not started yet, is in progress, or is completed.
  • Call the closure once when the request finishes for any reason.

    Declaration

    Swift

    @discardableResult
    func onCompletion(_ callback: @escaping (ResponseInfo) -> Void) -> Request
  • Call the closure once if the request succeeds.

    Declaration

    Swift

    @discardableResult
    func onSuccess(_ callback: @escaping (Entity<Any>) -> Void) -> Request
  • Call the closure once if the request succeeds and the data changed.

    Declaration

    Swift

    @discardableResult
    func onNewData(_ callback: @escaping (Entity<Any>) -> Void) -> Request
  • Call the closure once if the request succeeds with a 304.

    Declaration

    Swift

    @discardableResult
    func onNotModified(_ callback: @escaping () -> Void) -> Request
  • Call the closure once if the request fails for any reason.

    Declaration

    Swift

    @discardableResult
    func onFailure(_ callback: @escaping (RequestError) -> Void) -> Request
  • Immediately start this request if it was deferred. Does nothing if the request is already started.

    You rarely need to call this method directly, because most requests are started for you automatically:

    When do you need this method, then? It’s rare. There are two situations:

    • Configuration.decorateRequests(...) can defer a request by hanging on to it while returning a different request. You can use this method to manually start a request that was deferred this way.
    • Request.repeated() does not automatically start the request it returns. This is to allow you to implement time-delayed retries.

    Declaration

    Swift

    @discardableResult
    func start() -> Request
  • Indicates whether this request is waiting to be started, in progress, or completed and has a response already.

    Note

    It is always valid to call any of Request’s methods, including hooks (onCompletion(...) and friends), no matter what state the request is in. You do not need to defensively check this property before working with a request; in fact, if you find yourself wanting it at all, you are probably doing something awkward and unnecessarily complicated.

    Declaration

    Swift

    var state: RequestState { get }
  • An estimate of the progress of the request, taking into account request transfer, response transfer, and latency. Result is either in [0…1], or is NAN if insufficient information is available.

    The property will always be 1 if a request is completed. Note that the converse is not true: a value of 1 does not necessarily mean the request is completed; it means only that we estimate the request should be completed by now. Use the isCompleted property to test for actual completion.

    Declaration

    Swift

    var progress: Double { get }
  • Call the given closure with progress updates at regular intervals while the request is in progress. Will always receive a call with a value of 1 when the request completes.

    Declaration

    Swift

    @discardableResult
    func onProgress(_ callback: @escaping (Double) -> Void) -> Request
  • Cancel the request if it is still in progress. Has no effect if a response has already been received.

    If this method is called while the request is in progress, it immediately triggers the failure/completion callbacks, with the error’s cause set to RequestError.Cause.RequestCancelled.

    Note that cancel() is not guaranteed to stop the request from reaching the server. In fact, it is not guaranteed to have any effect at all on the underlying request, subject to the whims of the NetworkingProvider. Therefore, after calling this method on a mutating request (POST, PUT, etc.), you should consider the service-side state of the resource to be unknown. Is it safest to immediately call either Resource.load() or Resource.wipe().

    This method does guarantee, however, that after it is called, even if a network response does arrive it will be ignored and not trigger any callbacks.

    Declaration

    Swift

    func cancel()
  • Send the same request again, returning a new Request instance for the new attempt. You can combine this with Request.chained(...) to retry failed requests with updated headers.

    The returned request is not already started. You must call start() when you are ready for it to begin.

    Warning

    Use with caution! Repeating a failed request for any HTTP method other than GET is potentially unsafe, because you do not always know whether the server processed your request before the error occurred. Ensure that it is safe to repeat a request before calling this method.

    This method picks up certain contextual changes:

    • It will honor any changes to Configuration.headers made since the original request.
    • It will rerun the requestMutation closure you passed to Resource.request(...) (if you passed one).
    • It will not redecorate the request, and will not pick up any changes to Configuration.decorateRequests(...) since the original call. This is so that a request wrapper can safely retry its nested request without triggering a brain-bending hall of mirrors effect.

    Note that this means the new request may not be indentical to the original one.

    Warning

    Because repeated() will pick up header changes from configuration, it is possible for a request to run again with different auth credentials. This is intentional: one of the primary use cases for this dangerous method is automatically retrying a request with an updated auth token. However, the onus is on you to ensure that you do not hold on to and repeat a request after a user logs out. Put those safety goggles on.

    Note

    The new Request does not attach all the callbacks (e.g. onCompletion(_:)) from the old one. Doing so would violate the API contract of Request that any callback will be called at most once.

    After calling repeated(), you will need to attach new callbacks to the new request. Otherwise nobody will hear about the response when it arrives. (Q: If a request completes and nobody’s around to hear it, does it make a response? A: Yes, because it still uses bandwidth, and potentially changes state on the server.)

    By the same principle, repeating a load() request will trigger a second network call, but will not cause the resource’s state to be updated again with the result.

    Declaration

    Swift

    func repeated() -> Request
  • chained(whenCompleted:) Extension method

    Gathers multiple requests into a request chain, a wrapper that appears from the outside to be a single request. You can use this to add behavior to a request in a way that is transparent to outside observers. For example, you can transparently renew expired tokens.

    Note

    This returns a new Request, and does not alter the original one (thus chained and not chain). Any hooks attached to the original request will still see that request complete, and will not see any of the chaining behavior.

    In this pseudocode:

    let chainedRequest = underlyingRequest.chained {
      response in whenCompleted
    }
    

    …the following things happen, in this order:

    • The chain waits for underlyingRequest to complete.
    • The response (no matter whether success or failure) gets passed to whenCompleted.
    • The whenCompleted closure examines that response, and returns a RequestChainAction.
      • If it returns .useResponse or .useThisResponse, the chain is now done, and any hooks attached to chainedRequest see that response.
      • If it returns .passTo(newRequest), then the chain will wait for newRequest (which may itself be a chain), and yield whatever repsonse it produces.

    Calling cancel() on chainedRequest cancels the currently executing request and immediately stops the chain, never executing your whenCompleted closure. (Note, however, that calling cancel() on underlyingRequest does not stop the chain; instead, the cancellation error is passed to your whenCompleted just like any other error.)

    Warning

    This cancellation behavior means that your whenCompleted closure may never execute. If you want guaranteed execution of cleanup code, attach a handler to the chained request:

    let foo = ThingThatNeedsCleanup()
    request
      .chained { some logic }           // May not be called if chain is cancelled
      .onCompletion{ _ in foo.cleanUp() } // Guaranteed to be called exactly once
    

    Chained requests currently do not support progress. If you are reading these words and want that feature, please file an issue on Github!

    Declaration

    Swift

    public func chained(whenCompleted callback: @escaping (ResponseInfo) -> RequestChainAction) -> Request