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 ResourceObserver
s receive notifications about
all resource load requests, no matter who initiated them. Note also that these hooks are available for all
requests, whereas ResourceObserver
s 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:
- Any request you receive from
Resource.request(...)
orResource.load()
is already started. - Requests start automatically when you use
RequestChainAction.passTo
in a chain.
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
- Any request you receive from
-
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 ofRequest
’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’scause
set toRequestError.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 theNetworkingProvider
. 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 eitherResource.load()
orResource.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 withRequest.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 toResource.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 ofRequest
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
- It will honor any changes to
-
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 newRequest
, and does not alter the original one (thuschained
and notchain
). 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 thatresponse
, and returns aRequestChainAction
.- If it returns
.useResponse
or.useThisResponse
, the chain is now done, and any hooks attached tochainedRequest
see that response. - If it returns
.passTo(newRequest)
, then the chain will wait fornewRequest
(which may itself be a chain), and yield whatever repsonse it produces.
- If it returns
Calling
cancel()
onchainedRequest
cancels the currently executing request and immediately stops the chain, never executing yourwhenCompleted
closure. (Note, however, that callingcancel()
onunderlyingRequest
does not stop the chain; instead, the cancellation error is passed to yourwhenCompleted
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!
See also
Configuration.decorateRequests(...)
Declaration
Swift
public func chained(whenCompleted callback: @escaping (ResponseInfo) -> RequestChainAction) -> Request
- The chain waits for