Skip to content

Commit

Permalink
Don't force completion blocks to be scheduled on the main queue
Browse files Browse the repository at this point in the history
Different request performers are allowed to have different semantics
when it comes to the queue that results are delivered on. If a performer
already happens to deliver results on the main queue, `APIClient` is
injecting latency by scheduling an unnecessary async call. If a
performer delivers results on a background queue (i.e., `NetworkRequestPerformer`),
and there's further processing that would benefit from occurring in the
background (e.g., image processing or decoding a large object), callers
must dispatch back onto a background queue to avoid blocking the UI. It
also hampers composability of requests, because multiple chained
requests will incur a penalty from jumping back and forth from the main
queue to a background queue.

This breaking change removes the `dispatc_async`, replacing it with the
more abstract idea of a `Scheduler`, which is a block that schedules the
completion handler to be performed, either synchronously or
asynchronously. The `mainQueueScheduler` is the default, and an
`immediateScheduler` is provided to synchronously invoke the completion
block instead.
  • Loading branch information
sharplet committed May 20, 2016
1 parent b5235f0 commit 460f087
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 22 deletions.
5 changes: 0 additions & 5 deletions Source/Extensions/GCD.swift

This file was deleted.

8 changes: 5 additions & 3 deletions Source/Models/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@ import Result
public struct APIClient {
private let requestPerformer: RequestPerformer
private let deserializer: Deserializer
private let scheduler: Scheduler

public init(requestPerformer: RequestPerformer = NetworkRequestPerformer(), deserializer: Deserializer = JSONDeserializer()) {
public init(requestPerformer: RequestPerformer = NetworkRequestPerformer(), deserializer: Deserializer = JSONDeserializer(), scheduler: Scheduler = mainQueueScheduler) {
self.requestPerformer = requestPerformer
self.deserializer = deserializer
self.scheduler = scheduler
}
}

extension APIClient: Client {
public func performRequest<T: Request>(request: T, completionHandler: Result<T.ResponseObject, SwishError> -> Void) -> NSURLSessionDataTask {
return requestPerformer.performRequest(request.build()) { result in
return requestPerformer.performRequest(request.build()) { [schedule = scheduler] result in
let object = result
>>- self.validateResponse
>>- self.deserializer.deserialize
>>- T.ResponseParser.parse
>>- request.parse

onMain { completionHandler(object) }
schedule { completionHandler(object) }
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions Source/Models/Scheduler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public typealias Scheduler = ((() -> Void) -> Void)

public let immediateScheduler: Scheduler = { completion in
completion()
}

public let mainQueueScheduler: Scheduler = { completion in
dispatch_async(dispatch_get_main_queue(), completion)
}
23 changes: 13 additions & 10 deletions Swish.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
2C721E5C1BD5A7E800846414 /* NetworkRequestPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F806950D1B962C5300C61B4A /* NetworkRequestPerformer.swift */; };
2C721E5D1BD5A7E800846414 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ED06C1B96736B0069F56C /* APIClient.swift */; };
2C721E5E1BD5A83B00846414 /* Swish.h in Headers */ = {isa = PBXBuildFile; fileRef = F80695121B962C5300C61B4A /* Swish.h */; settings = {ATTRIBUTES = (Public, ); }; };
4A05CC281CEFBA460076955E /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A05CC271CEFBA460076955E /* Scheduler.swift */; };
4A05CC291CEFBA4B0076955E /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A05CC271CEFBA460076955E /* Scheduler.swift */; };
4A05CC2A1CEFBA4C0076955E /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A05CC271CEFBA460076955E /* Scheduler.swift */; };
4A05CC2B1CEFBA4C0076955E /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A05CC271CEFBA460076955E /* Scheduler.swift */; };
E506EC7E1BB5BE380032E941 /* NimbleMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E506EC7D1BB5BE380032E941 /* NimbleMatchers.swift */; };
E51722EF1CEE5D1000B0C915 /* JSONDeserializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58AD6361C91055200AD2CDE /* JSONDeserializer.swift */; };
E51722F01CEE5D1A00B0C915 /* Deserializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58AD6321C91049300AD2CDE /* Deserializer.swift */; };
Expand All @@ -37,7 +41,6 @@
E52E5DA61CE7AF2500023C91 /* NSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FB81BFD360A00FB2433 /* NSError.swift */; };
E52E5DA71CE7AF2500023C91 /* NSURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5915B201BDABC4B005E5D63 /* NSURLRequest.swift */; };
E52E5DA81CE7AF2500023C91 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ED0711B967C4A0069F56C /* Result.swift */; };
E52E5DA91CE7AF2500023C91 /* GCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FBB1BFD48F200FB2433 /* GCD.swift */; };
E52E5DAA1CE7AF2500023C91 /* SwishError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F59BB51C2B34B60020B5BE /* SwishError.swift */; };
E52E5DAB1CE7AF2500023C91 /* RequestMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = F867938B1BD29E37007D9E98 /* RequestMethod.swift */; };
E52E5DAC1CE7AF2500023C91 /* NetworkRequestPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F806950D1B962C5300C61B4A /* NetworkRequestPerformer.swift */; };
Expand All @@ -56,7 +59,6 @@
E52E5DD51CE7B36A00023C91 /* NSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FB81BFD360A00FB2433 /* NSError.swift */; };
E52E5DD61CE7B36A00023C91 /* NSURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5915B201BDABC4B005E5D63 /* NSURLRequest.swift */; };
E52E5DD71CE7B36A00023C91 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ED0711B967C4A0069F56C /* Result.swift */; };
E52E5DD81CE7B36A00023C91 /* GCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FBB1BFD48F200FB2433 /* GCD.swift */; };
E52E5DD91CE7B36A00023C91 /* SwishError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F59BB51C2B34B60020B5BE /* SwishError.swift */; };
E52E5DDA1CE7B36A00023C91 /* RequestMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = F867938B1BD29E37007D9E98 /* RequestMethod.swift */; };
E52E5DDB1CE7B36A00023C91 /* NetworkRequestPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F806950D1B962C5300C61B4A /* NetworkRequestPerformer.swift */; };
Expand Down Expand Up @@ -107,8 +109,6 @@
F8A7CADC1BFD5EBD008B9224 /* NSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FB81BFD360A00FB2433 /* NSError.swift */; };
F8C39B511B969A71005E065F /* FakeDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C39B501B969A71005E065F /* FakeDataTask.swift */; };
F8D09FB91BFD360A00FB2433 /* NSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FB81BFD360A00FB2433 /* NSError.swift */; };
F8D09FBC1BFD48F200FB2433 /* GCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FBB1BFD48F200FB2433 /* GCD.swift */; };
F8D09FBD1BFD48F200FB2433 /* GCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D09FBB1BFD48F200FB2433 /* GCD.swift */; };
F8DF3B861B964B20003177CD /* FakeSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF3B811B964B20003177CD /* FakeSession.swift */; };
F8DF3B881B964B20003177CD /* NetworkRequestPerformerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF3B851B964B20003177CD /* NetworkRequestPerformerSpec.swift */; };
F8DF3B8B1B964B83003177CD /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8DF3B891B964B83003177CD /* Nimble.framework */; };
Expand Down Expand Up @@ -144,6 +144,7 @@
/* Begin PBXFileReference section */
2C721E401BD5A77700846414 /* Swish.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Swish.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2C721E491BD5A77700846414 /* Swish-MacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Swish-MacTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
4A05CC271CEFBA460076955E /* Scheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scheduler.swift; sourceTree = "<group>"; };
E506EC7D1BB5BE380032E941 /* NimbleMatchers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleMatchers.swift; sourceTree = "<group>"; };
E52E5D8F1CE7AE3400023C91 /* Swish.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Swish.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E52E5D981CE7AE3400023C91 /* Swish-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Swish-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -180,7 +181,6 @@
F8A00DE61BB5C86200169A46 /* RequestSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
F8C39B501B969A71005E065F /* FakeDataTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeDataTask.swift; sourceTree = "<group>"; };
F8D09FB81BFD360A00FB2433 /* NSError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSError.swift; sourceTree = "<group>"; };
F8D09FBB1BFD48F200FB2433 /* GCD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCD.swift; sourceTree = "<group>"; };
F8DF3B811B964B20003177CD /* FakeSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeSession.swift; sourceTree = "<group>"; };
F8DF3B831B964B20003177CD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F8DF3B851B964B20003177CD /* NetworkRequestPerformerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkRequestPerformerSpec.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -323,6 +323,7 @@
F806950D1B962C5300C61B4A /* NetworkRequestPerformer.swift */,
F8DF3B8D1B964BED003177CD /* HTTPResponse.swift */,
F88ED06C1B96736B0069F56C /* APIClient.swift */,
4A05CC271CEFBA460076955E /* Scheduler.swift */,
E58AD6361C91055200AD2CDE /* JSONDeserializer.swift */,
);
path = Models;
Expand Down Expand Up @@ -355,7 +356,6 @@
F8D09FB81BFD360A00FB2433 /* NSError.swift */,
E5915B201BDABC4B005E5D63 /* NSURLRequest.swift */,
F88ED0711B967C4A0069F56C /* Result.swift */,
F8D09FBB1BFD48F200FB2433 /* GCD.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -698,6 +698,7 @@
E51722F01CEE5D1A00B0C915 /* Deserializer.swift in Sources */,
2C61A39E1BD726E10087B295 /* RequestMethod.swift in Sources */,
E51722F11CEE5D1D00B0C915 /* Parser.swift in Sources */,
4A05CC291CEFBA4B0076955E /* Scheduler.swift in Sources */,
2C721E5D1BD5A7E800846414 /* APIClient.swift in Sources */,
2C721E591BD5A7C500846414 /* Client.swift in Sources */,
E5915B221BDABC4B005E5D63 /* NSURLRequest.swift in Sources */,
Expand All @@ -706,7 +707,6 @@
2C721E5C1BD5A7E800846414 /* NetworkRequestPerformer.swift in Sources */,
2C721E571BD5A7C500846414 /* Result.swift in Sources */,
F84374CE1C4032020092E844 /* SwishError.swift in Sources */,
F8D09FBD1BFD48F200FB2433 /* GCD.swift in Sources */,
2C721E5B1BD5A7D400846414 /* Request.swift in Sources */,
2C721E5A1BD5A7D400846414 /* RequestPerformer.swift in Sources */,
F8A7CADC1BFD5EBD008B9224 /* NSError.swift in Sources */,
Expand Down Expand Up @@ -736,7 +736,7 @@
E52E5DA61CE7AF2500023C91 /* NSError.swift in Sources */,
E52E5DA71CE7AF2500023C91 /* NSURLRequest.swift in Sources */,
E52E5DA81CE7AF2500023C91 /* Result.swift in Sources */,
E52E5DA91CE7AF2500023C91 /* GCD.swift in Sources */,
4A05CC2A1CEFBA4C0076955E /* Scheduler.swift in Sources */,
E52E5DAA1CE7AF2500023C91 /* SwishError.swift in Sources */,
E52E5DAB1CE7AF2500023C91 /* RequestMethod.swift in Sources */,
E52E5DAC1CE7AF2500023C91 /* NetworkRequestPerformer.swift in Sources */,
Expand Down Expand Up @@ -774,7 +774,7 @@
E52E5DD51CE7B36A00023C91 /* NSError.swift in Sources */,
E52E5DD61CE7B36A00023C91 /* NSURLRequest.swift in Sources */,
E52E5DD71CE7B36A00023C91 /* Result.swift in Sources */,
E52E5DD81CE7B36A00023C91 /* GCD.swift in Sources */,
4A05CC2B1CEFBA4C0076955E /* Scheduler.swift in Sources */,
E52E5DD91CE7B36A00023C91 /* SwishError.swift in Sources */,
E52E5DDA1CE7B36A00023C91 /* RequestMethod.swift in Sources */,
E52E5DDB1CE7B36A00023C91 /* NetworkRequestPerformer.swift in Sources */,
Expand All @@ -796,6 +796,7 @@
F867938C1BD29E37007D9E98 /* RequestMethod.swift in Sources */,
F88EE3141B976747001EEB44 /* Result.swift in Sources */,
E5D7E0A11BBF2021002A3738 /* Client.swift in Sources */,
4A05CC281CEFBA460076955E /* Scheduler.swift in Sources */,
F8DF3B8E1B964BED003177CD /* HTTPResponse.swift in Sources */,
E58AD6371C91055200AD2CDE /* JSONDeserializer.swift in Sources */,
E58AD6351C91053000AD2CDE /* Parser.swift in Sources */,
Expand All @@ -804,7 +805,6 @@
F80695141B962C5300C61B4A /* RequestPerformer.swift in Sources */,
F88ED06D1B96736B0069F56C /* APIClient.swift in Sources */,
F8F59BB61C2B34B60020B5BE /* SwishError.swift in Sources */,
F8D09FBC1BFD48F200FB2433 /* GCD.swift in Sources */,
E5915B211BDABC4B005E5D63 /* NSURLRequest.swift in Sources */,
F80695131B962C5300C61B4A /* NetworkRequestPerformer.swift in Sources */,
F8D09FB91BFD360A00FB2433 /* NSError.swift in Sources */,
Expand Down Expand Up @@ -1229,6 +1229,7 @@
E52E5DA11CE7AE3400023C91 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E52E5DA51CE7AE3400023C91 /* Build configuration list for PBXNativeTarget "Swish-tvOSTests" */ = {
isa = XCConfigurationList;
Expand All @@ -1237,6 +1238,7 @@
E52E5DA31CE7AE3400023C91 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E52E5DD11CE7B2BA00023C91 /* Build configuration list for PBXNativeTarget "Swish-watchOS" */ = {
isa = XCConfigurationList;
Expand All @@ -1245,6 +1247,7 @@
E52E5DD31CE7B2BA00023C91 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F80694EB1B962BB900C61B4A /* Build configuration list for PBXProject "Swish" */ = {
isa = XCConfigurationList;
Expand Down
18 changes: 14 additions & 4 deletions Tests/Fakes/FakeRequestPerformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ extension ResponseData {
class FakeRequestPerformer: RequestPerformer {
let statusCode: Int
let data: NSData?
let background: Bool
var passedRequest: NSURLRequest?

init(responseData: ResponseData, statusCode: Int = 200) {
init(responseData: ResponseData, statusCode: Int = 200, background: Bool = false) {
self.data = responseData.data
self.statusCode = statusCode
self.background = background
}

func performRequest(request: NSURLRequest, completionHandler: Result<HTTPResponse, SwishError> -> Void) -> NSURLSessionDataTask {
Expand All @@ -38,9 +40,17 @@ class FakeRequestPerformer: RequestPerformer {
NSHTTPURLResponse(URL: $0, statusCode: statusCode, HTTPVersion: .None, headerFields: .None)
}

completionHandler(
.Success(HTTPResponse(data: data, response: response))
)
let complete = { [data] in
completionHandler(
.Success(HTTPResponse(data: data, response: response))
)
}

if background {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), complete)
} else {
complete()
}

return NSURLSessionDataTask()
}
Expand Down
45 changes: 45 additions & 0 deletions Tests/Tests/APIClientSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,51 @@ class APIClientSpec: QuickSpec {
expect(error).toEventually(equal(SwishError.ServerError(code: expectedCode, data: performer.data)))
}
}

describe("scheduling completion blocks") {
it("dispatches onto the main queue by default") {
let performer = FakeRequestPerformer(responseData: .JSON([:]))
let client = APIClient(requestPerformer: performer)

var isOnMain: Bool?
client.performRequest(FakeRequest()) { _ in
isOnMain = NSThread.isMainThread()
}

expect(isOnMain).toEventually(beTrue())
}

it("doesn't dispatch onto main queue when using the immediate scheduler") {
let performer = FakeRequestPerformer(responseData: .JSON([:]), background: true)
let client = APIClient(requestPerformer: performer, scheduler: immediateScheduler)

var isOnMain: Bool?
client.performRequest(FakeRequest()) { _ in
isOnMain = NSThread.isMainThread()
}

expect(isOnMain).toEventually(beFalse())
}

it("dispatches via a custom scheduler if set") {
var calledNoopScheduler = false
var completed = false

let noopScheduler: Scheduler = { _ in
calledNoopScheduler = true
}

let performer = FakeRequestPerformer(responseData: .JSON([:]), background: false)
let client = APIClient(requestPerformer: performer, scheduler: noopScheduler)

client.performRequest(FakeRequest()) { _ in
completed = true
}

expect(calledNoopScheduler) == true
expect(completed) == false
}
}
}
}
}

0 comments on commit 460f087

Please sign in to comment.