Skip to content

Instantly share code, notes, and snippets.

@matux
Created November 16, 2017 21:16
Show Gist options
  • Select an option

  • Save matux/29208d18e365f0d856c9a45407583536 to your computer and use it in GitHub Desktop.

Select an option

Save matux/29208d18e365f0d856c9a45407583536 to your computer and use it in GitHub Desktop.
Network request deferring system that facilitates offline support while offering a simple interface.
import Foundation
import LambdaKit
private let kDefaultIntervalRate: TimeInterval = 5.0
/// An interface defining a value that can be requested as a URL.
public protocol Routable {
/// The URL String for the route
var urlString: String { get }
/// Returns the extra HTTP headers needed for this Router. Authentication should be included here as well
/// as API versioning.
///
/// - returns: A dictionary of key, values to append to the HTTP request headers.
func extraHTTPHeaders() -> [String: String]
}
extension Routable {
public func extraHTTPHeaders() -> [String: String] { return [:] }
}
extension URL: Routable {
public var urlString: String {
return self.absoluteString
}
}
/// Used to check if a request should be deferred or not based on its response.
///
/// - parameter responseStatus: The response status type (see ResponseType for the list).
///
/// - returns: A boolean indicating if the request should be deferred or not.
public typealias ShouldDeferRequestClosureType = (_ responseStatus: ResponseType) -> Bool
/// Describes a Request that can be deferred by the RequestDeferrer.
public struct DeferrableRequest {
typealias Persistable = [DeferrableRequest]
/// API Route for this request.
public let route: Routable
/// HTTPMethod for this request.
public let method: HTTPMethod
/// Parameters for this request.
public let parameters: [String: Any]?
/// Closure to add extra parameters before sending the request.
public let extraParameters: (() -> [String: Any]?)?
/// Completion closure to be called when the request is sent. Note that this closure won't be called if
/// the request gets deferred again due to an error matching what's specified in `shouldDeferRequest`.
public let completion: HTTPCompletionClosureType?
/// Creates a new DeferrableRequest.
///
/// - parameter route: API Route for this request.
/// - parameter method: HTTPMethod for this request.
/// - parameter parameters: Parameters for this request.
/// - parameter extraParameters: Closure to add extra parameters before sending the request.
/// - parameter completion: Completion closure to be called when the request is sent.
///
/// - returns: New instance of DeferrableRequest.
public init(route: Routable, method: HTTPMethod, parameters: [String: Any]?,
extraParameters: (() -> [String: Any]?)?, completion: HTTPCompletionClosureType?)
{
self.route = route
self.method = method
self.parameters = parameters
self.extraParameters = extraParameters
self.completion = completion
}
}
/// Defers `HTTPRequests` to be sent at different intervals until certain conditions have been met.
public final class RequestDeferrer: Persister {
typealias Persistable = [DeferrableRequest]
/// Singleton impl. RequestDeferrer will always be persistent throughout the entire app life.
public static let sharedDeferrer = RequestDeferrer(identifier: "shared")
fileprivate let identifier: String
private var isConsumingQueue = false
private var deferredRequests = [DeferrableRequest]() {
didSet { self.store(self.deferredRequests) }
}
/// `HTTPClient` we want this `RequestDeferrer` dispatching requests to.
public weak var client: HTTPClient?
/// Interval rate at which we try to send the deferred requests in seconds.
public var intervalRate: TimeInterval = kDefaultIntervalRate
/// Whether or not the manager has deferred requests on its queue
public var hasDeferredRequests: Bool {
return !self.deferredRequests.isEmpty
}
/// Used to check if a request should be deferred or not based on its response. Defaults to `false`,
/// ie. unless assigned, requests will never be deferred.
public var shouldDeferRequest: ShouldDeferRequestClosureType = { _ in
return false
}
/// Creates a new RequestDeferrer.
///
/// - parameter identifier: An identifier is necessary to keep persisted requests collections from
/// colliding.
///
/// - returns: An instance of RequestDeferrer.
public init(identifier: String) {
self.identifier = identifier
self.deferredRequests = self.fetch()
}
/// Defers a `DeferrableRequest` to be dispatched at a later point.
///
/// - parameter request: Request you want to defer.
public func deferRequest(_ request: DeferrableRequest) {
self.deferredRequests.append(request)
self.performNextRequest()
}
/// Start consuming requests from the deferred queue.
public func start() {
self.performNextRequest()
}
/// Clears the deferred request queue, cancels the queue interval timer and removes any persisted
/// requests.
public func purge() {
self.deferredRequests = []
self.isConsumingQueue = false
}
// MARK: Private functions
private func performNextRequest() {
guard !self.isConsumingQueue, let request = self.deferredRequests.first else {
return
}
let errorHandler: HTTPErrorClosureType = { [weak self] status in
// Don't let the HTTPClient handle the error if we want to keep deferring based on the status.
return self?.shouldDeferRequest(status) == true
}
let parameters = self.parameters(forRequest: request, extraParameters: request.extraParameters?())
self.isConsumingQueue = true
self.client?.request(request.method, request.route, parameters: parameters,
error: errorHandler, completion: { response, status in
self.isConsumingQueue = false
if self.shouldDeferRequest(status) {
executeAfter(self.intervalRate, closure: self.performNextRequest)
return
}
self.deferredRequests.removeFirst()
request.completion?(response, status)
if self.hasDeferredRequests {
self.performNextRequest()
}
})
}
fileprivate func parameters(forRequest request: DeferrableRequest,
extraParameters: [String: Any]?) -> [String: Any]?
{
var parameters = request.parameters
parameters?["deferred"] = true
guard let extraParameters = extraParameters else {
return parameters
}
return parameters?.updated(extraParameters)
}
}
// MARK: - Persistence layer
private protocol Persister {
associatedtype Persistable
var storageURL: URL? { get }
func store(_ persistable: Persistable)
func fetch() -> Persistable
}
private extension Persister where Self: RequestDeferrer {
var storageURL: URL? {
do {
let documentURL = try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask, appropriateFor: nil, create: true)
let subpath = "requests/\(self.identifier)_deferred.plist"
return documentURL.appendingPathComponent(subpath)
} catch {
Logger.logError("Cannot fetch .DocumentDirectory: \(error)")
return nil
}
}
func store(_ persistable: [DeferrableRequest]) {
guard let storageURL = self.storageURL else {
return
}
let storagePath = storageURL.path
do {
let storageDirectory = storageURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: storageDirectory,
withIntermediateDirectories: true, attributes: nil)
} catch {
Logger.logError("Cannot create storage directory: \(error)")
return
}
let requests = persistable.map { request in
return [
"path": request.route.urlString,
"method": request.method.rawValue,
"parameters": request.parameters ?? [:],
"extraHTTPHeaders": request.route.extraHTTPHeaders(),
]
}
NSKeyedArchiver.archiveRootObject(requests, toFile: storagePath)
}
func fetch() -> [DeferrableRequest] {
let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(withFile: self.storageURL?.path ?? "")
guard let requestDictionaries = unarchivedObject as? [[String: Any]] else {
return []
}
return requestDictionaries.flatMap { self.deferrableRequest(fromDictionary: $0) }
}
// MARK: - Private functions
private func deferrableRequest(fromDictionary request: [String: Any]) -> DeferrableRequest? {
guard let path = request["path"] as? String,
let method = (request["method"] as? String).flatMap({ HTTPMethod(rawValue: $0) }),
let parameters = request["parameters"] as? [String: Any],
let extraHTTPHeaders = request["extraHTTPHeaders"] as? [String: String] else
{
return nil
}
let route = URLRoute(urlString: path, additionalHTTPHeaders: extraHTTPHeaders)
return DeferrableRequest(route: route, method: method, parameters: parameters, extraParameters: nil,
completion: nil)
}
}
private struct URLRoute: Routable {
fileprivate let urlString: String
fileprivate let additionalHTTPHeaders: [String: String]
fileprivate func extraHTTPHeaders() -> [String: String] {
return self.additionalHTTPHeaders
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment