Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform two requests concurrently and combine their results #98

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Source/Extensions/Client+CombinedRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Result

extension Client {
@discardableResult
public func perform<A: Request, B: Request>(_ requestA: A, _ requestB: B, completionHandler: @escaping (Result<(A.ResponseObject, B.ResponseObject), SwishError>) -> Void) -> Cancelable {
return CombinedRequest(requestA, requestB, client: self)
.perform { [schedule = scheduler] result in
schedule {
completionHandler(result)
}
}
}
}
2 changes: 1 addition & 1 deletion Source/Models/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Argo
import Result

public struct APIClient {
public let scheduler: Scheduler
fileprivate let requestPerformer: RequestPerformer
fileprivate let deserializer: Deserializer
fileprivate let scheduler: Scheduler

public init(requestPerformer: RequestPerformer = NetworkRequestPerformer(), deserializer: Deserializer = JSONDeserializer(), scheduler: @escaping Scheduler = mainQueueScheduler) {
self.requestPerformer = requestPerformer
Expand Down
78 changes: 78 additions & 0 deletions Source/Models/CombinedRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Result

struct CombinedRequest<RequestA: Request, RequestB: Request> {
typealias ResponseA = RequestA.ResponseObject
typealias ResponseB = RequestB.ResponseObject

let requestA: RequestA
let requestB: RequestB
let client: Client

init(_ requestA: RequestA, _ requestB: RequestB, client: Client) {
self.requestA = requestA
self.requestB = requestB
self.client = client
}

func perform(completionHandler: @escaping (Result<(ResponseA, ResponseB), SwishError>) -> Void) -> Cancelable {
var start: DispatchWorkItem!

var cancelable = CompositeCancelable()
cancelable += { start.cancel() }

start = DispatchWorkItem { self.start(&cancelable, completionHandler) }

DispatchQueue.global().async(execute: start)

return cancelable
}

private func start(_ cancelable: inout CompositeCancelable, _ completionHandler: @escaping (Result<(ResponseA, ResponseB), SwishError>) -> Void) {
let initial = CombinedRequestState<RequestA.ResponseObject, RequestB.ResponseObject, SwishError>.response(.none, .none)
let group = RequestGroup(client: client, state: initial)

cancelable += { group.cancel() }

group.perform(requestA) { resultA, state, cancel in
switch (state, resultA) {
case (.error, _):
cancel = true
case let (.response(_, b), .success(a)):
state = .response(a, b)
case let (.response, .failure(error)):
state = .error(error)
cancel = true
}
}

group.perform(requestB) { resultB, state, cancel in
switch (state, resultB) {
case (.error, _):
cancel = true
case let (.response(a, _), .success(b)):
state = .response(a, b)
case let (.response, .failure(error)):
state = .error(error)
cancel = true
}
}

group.wait { combinedResults in
switch combinedResults {
case let .error(error):
completionHandler(.failure(error))

case let .response(a?, b?):
completionHandler(.success(a, b))

case .response:
fatalError("expected both requests to complete or error")
}
}
}
}

private enum CombinedRequestState<A, B, Error: Swift.Error> {
case error(Error)
case response(A?, B?)
}
42 changes: 42 additions & 0 deletions Source/Models/RequestGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Result

final class RequestGroup<State>: Cancelable {
let client: Client
private var state: State
private var requests = CompositeCancelable()
private let group = DispatchGroup()
private let queue = DispatchQueue(label: "com.thoughtbot.swish.RequestGroup<\(State.self)>")

init(client: Client, state: State) {
self.client = client
self.state = state
}

func perform<Request: Swish.Request>(_ request: Request, completionHandler: @escaping (_ result: Result<Request.ResponseObject, SwishError>, _ state: inout State, _ cancel: inout Bool) -> Void) {
group.enter()

requests += client.perform(request) { result in
var cancel = false

self.queue.sync {
completionHandler(result, &self.state, &cancel)
}

if cancel {
self.cancel()
}

self.group.leave()
}
}

func cancel() {
requests.cancel()
}

func wait(body: (State) -> Void) {
group.wait()
let state = queue.sync { self.state }
body(state)
}
}
43 changes: 43 additions & 0 deletions Source/Protocols/Cancelable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

public protocol Cancelable {
func cancel()
}

extension URLSessionTask: Cancelable {}

struct SimpleCancelable: Cancelable {
private let action: () -> Void

init(action: @escaping () -> Void) {
self.action = action
}

func cancel() {
action()
}
}

struct CompositeCancelable: Cancelable {
private var cancelables: [Cancelable] = []

mutating func add(_ cancelable: Cancelable) {
cancelables.append(cancelable)
}

mutating func add(_ action: @escaping () -> Void) {
add(SimpleCancelable(action: action))
}

func cancel() {
cancelables.forEach { $0.cancel() }
}

static func += (composite: inout CompositeCancelable, cancelable: Cancelable) {
composite.add(cancelable)
}

static func += (composite: inout CompositeCancelable, action: @escaping () -> Void) {
composite.add(action)
}
}
8 changes: 8 additions & 0 deletions Source/Protocols/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import Argo
import Result

public protocol Client {
var scheduler: Scheduler { get }

@discardableResult
func perform<T: Request>(_ request: T, completionHandler: @escaping (Result<T.ResponseObject, SwishError>) -> ()) -> URLSessionDataTask
}

extension Client {
public var scheduler: Scheduler {
return mainQueueScheduler
}
}
Loading