diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 1bff7a999a..51cafc95f0 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 9F578D901D8D2CB300C0EA36 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F578D8F1D8D2CB300C0EA36 /* Utilities.swift */; }; 9F65B1211EC106F30090B25F /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; 9F69FFA91D42855900E000B1 /* NetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */; }; + 9F7BA89922927A3700999B3B /* ResponsePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7BA89822927A3700999B3B /* ResponsePath.swift */; }; 9F8622F81EC2004200C38162 /* ReadWriteFromStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8622F71EC2004200C38162 /* ReadWriteFromStoreTests.swift */; }; 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */; }; 9F86B68B1E6438D700B885FF /* GraphQLSelectionSetMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F86B68A1E6438D700B885FF /* GraphQLSelectionSetMapper.swift */; }; @@ -36,6 +37,7 @@ 9F8A958D1EC0FFAB00304A2D /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; }; 9F8A95901EC0FFC100304A2D /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; }; 9F8A95931EC0FFD100304A2D /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; }; + 9F8F334C229044A200C0E83B /* Decoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8F334B229044A200C0E83B /* Decoding.swift */; }; 9F91CF8F1F6C0DB2008DD0BE /* MutatingResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */; }; 9FA6ABCC1EC0A9F7000017BE /* FetchQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6ABC51EC0A9F7000017BE /* FetchQueryTests.swift */; }; 9FA6ABCD1EC0A9F7000017BE /* LoadQueryFromStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6ABC61EC0A9F7000017BE /* LoadQueryFromStoreTests.swift */; }; @@ -256,6 +258,7 @@ 9F55347A1DE1DB2100E54264 /* ApolloStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloStore.swift; sourceTree = ""; }; 9F578D8F1D8D2CB300C0EA36 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 9F69FFA81D42855900E000B1 /* NetworkTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkTransport.swift; sourceTree = ""; }; + 9F7BA89822927A3700999B3B /* ResponsePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponsePath.swift; sourceTree = ""; }; 9F8622F71EC2004200C38162 /* ReadWriteFromStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteFromStoreTests.swift; sourceTree = ""; }; 9F8622F91EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FragmentConstructionAndConversionTests.swift; sourceTree = ""; }; 9F8622FE1EC44A8600C38162 /* HeroNameAndAppearsIn.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = HeroNameAndAppearsIn.graphql; sourceTree = ""; }; @@ -267,6 +270,7 @@ 9F8A95811EC0FD3300304A2D /* XCTAssertHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTAssertHelpers.swift; sourceTree = ""; }; 9F8A95831EC0FD6100304A2D /* XCTestCase+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Promise.swift"; sourceTree = ""; }; 9F8A95851EC0FD9800304A2D /* TestCacheProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCacheProvider.swift; sourceTree = ""; }; + 9F8F334B229044A200C0E83B /* Decoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decoding.swift; sourceTree = ""; }; 9F91CF8E1F6C0DB2008DD0BE /* MutatingResultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutatingResultsTests.swift; sourceTree = ""; }; 9FA6ABBC1EC0A988000017BE /* ApolloCacheDependentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ApolloCacheDependentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9FA6ABC01EC0A988000017BE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -438,6 +442,7 @@ isa = PBXGroup; children = ( 9FF90A5C1DDDEB100034C3B6 /* GraphQLExecutor.swift */, + 9F8F334B229044A200C0E83B /* Decoding.swift */, 9FA6F3671E65DF4700BF8D73 /* GraphQLResultAccumulator.swift */, 9F86B68A1E6438D700B885FF /* GraphQLSelectionSetMapper.swift */, 9F295E371E277B2A00A24949 /* GraphQLResultNormalizer.swift */, @@ -445,6 +450,7 @@ 9F86B68F1E65533D00B885FF /* GraphQLResponseGenerator.swift */, 9FC9A9C41E2D6CE70023C4D5 /* GraphQLSelectionSet.swift */, 9FC9A9C11E2D3CAF0023C4D5 /* GraphQLInputValue.swift */, + 9F7BA89822927A3700999B3B /* ResponsePath.swift */, ); name = Execution; sourceTree = ""; @@ -774,6 +780,7 @@ buildPhases = ( 9FC6312F1E6AE2080062707E /* Sources */, 9FC631301E6AE2080062707E /* Frameworks */, + 9FD67EBA2290208500FD8DD2 /* Resources */, ); buildRules = ( ); @@ -912,7 +919,6 @@ }; 9FD637E01E6ACF88001EDBC8 = { CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Manual; }; }; }; @@ -961,6 +967,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9FD67EBA2290208500FD8DD2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1060,6 +1073,7 @@ files = ( 9FF33D811E48B98200F608A4 /* HTTPNetworkTransport.swift in Sources */, 9FCE2CEE1E6BE2D900E34457 /* NormalizedCache.swift in Sources */, + 9F8F334C229044A200C0E83B /* Decoding.swift in Sources */, 9FADC84A1E6B0B2300C677E6 /* Locking.swift in Sources */, 9F295E381E277B2A00A24949 /* GraphQLResultNormalizer.swift in Sources */, 9F86B68B1E6438D700B885FF /* GraphQLSelectionSetMapper.swift in Sources */, @@ -1078,6 +1092,7 @@ 9FC4B9201D2A6F8D0046A641 /* JSON.swift in Sources */, 9FEC15B41E681DAD00D461B4 /* Collections.swift in Sources */, 9F578D901D8D2CB300C0EA36 /* Utilities.swift in Sources */, + 9F7BA89922927A3700999B3B /* ResponsePath.swift in Sources */, 9FC9A9BD1E2C271C0023C4D5 /* RecordSet.swift in Sources */, 9FEC15B91E6965E300D461B4 /* Result.swift in Sources */, 9FADC84F1E6B865E00C677E6 /* DataLoader.swift in Sources */, diff --git a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/09B0B19E-F5A6-42CF-AD3D-082A3B8B31B3.plist b/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/09B0B19E-F5A6-42CF-AD3D-082A3B8B31B3.plist new file mode 100644 index 0000000000..dac628f3f6 --- /dev/null +++ b/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/09B0B19E-F5A6-42CF-AD3D-082A3B8B31B3.plist @@ -0,0 +1,22 @@ + + + + + classNames + + ParsingTests + + testLargeResponse() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.88797 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/22F19570-674E-47F1-A426-4EFB879DC0E3.plist b/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/22F19570-674E-47F1-A426-4EFB879DC0E3.plist new file mode 100644 index 0000000000..8a9640db1e --- /dev/null +++ b/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/22F19570-674E-47F1-A426-4EFB879DC0E3.plist @@ -0,0 +1,22 @@ + + + + + classNames + + ParsingTests + + testLargeResponse() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 1.1037 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/Info.plist b/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/Info.plist new file mode 100644 index 0000000000..b1d57c292e --- /dev/null +++ b/Apollo.xcodeproj/xcshareddata/xcbaselines/9FC631321E6AE2080062707E.xcbaseline/Info.plist @@ -0,0 +1,52 @@ + + + + + runDestinationsByUUID + + 09B0B19E-F5A6-42CF-AD3D-082A3B8B31B3 + + targetArchitecture + arm64e + targetDevice + + modelCode + iPhone11,2 + platformIdentifier + com.apple.platform.iphoneos + + + 22F19570-674E-47F1-A426-4EFB879DC0E3 + + localComputer + + busSpeedInMHz + 100 + cpuCount + 1 + cpuKind + Intel Core i7 + cpuSpeedInMHz + 2700 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro13,3 + physicalCPUCoresPerPackage + 4 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + targetDevice + + modelCode + iPhone11,2 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloPerformanceTests.xcscheme b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloPerformanceTests.xcscheme index cbc307e516..009b02c17c 100644 --- a/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloPerformanceTests.xcscheme +++ b/Apollo.xcodeproj/xcshareddata/xcschemes/ApolloPerformanceTests.xcscheme @@ -8,8 +8,8 @@ = (_ result: GraphQLResult?, _ error: Error?) -> Void +public typealias GraphQLResultHandler = (_ result: GraphQLResult?, _ error: Error?) -> Void + +@available(*, deprecated, renamed: "GraphQLResultHandler") +public typealias OperationResultHandler = GraphQLResultHandler /// The `ApolloClient` class provides the core API for Apollo. This API provides methods to fetch and watch queries, and to perform mutations. public class ApolloClient { @@ -54,8 +59,9 @@ public class ApolloClient { self.networkTransport = networkTransport self.store = store - queue = DispatchQueue(label: "com.apollographql.ApolloClient", attributes: .concurrent) + queue = DispatchQueue(label: "com.apollographql.ApolloClient") operationQueue = OperationQueue() + operationQueue.underlyingQueue = queue } /// Creates a client with an HTTP network transport connecting to the specified URL. @@ -81,17 +87,15 @@ public class ApolloClient { /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when query results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress fetch. - @discardableResult public func fetch(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler? = nil) -> Cancellable { - return _fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler) - } - - func _fetch(query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler?) -> Cancellable { - // If we don't have to go through the cache, there is no need to create an operation + @discardableResult public func fetch(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + let resultHandler = wrapResultHandler(resultHandler, queue: queue) + + // If we don't have to go through the cache, there is no need to create an operation // and we can return a network task directly - if cachePolicy == .fetchIgnoringCacheData { - return send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler) + if cachePolicy == .fetchIgnoringCacheData || cachePolicy == .fetchIgnoringCacheCompletely { + return send(operation: query, fetchHTTPMethod: fetchHTTPMethod, shouldPublishResultToStore: cachePolicy != .fetchIgnoringCacheCompletely, context: context, resultHandler: resultHandler) } else { - let operation = FetchQueryOperation(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler) + let operation = FetchQueryOperation(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: context, resultHandler: resultHandler) operationQueue.addOperation(operation) return operation } @@ -106,8 +110,8 @@ public class ApolloClient { /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when query results are available or when an error occurs. /// - Returns: A query watcher object that can be used to control the watching behavior. - public func watch(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler) -> GraphQLQueryWatcher { - let watcher = GraphQLQueryWatcher(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, handlerQueue: queue, resultHandler: resultHandler) + public func watch(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher { + let watcher = GraphQLQueryWatcher(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, resultHandler: wrapResultHandler(resultHandler, queue: queue)) watcher.fetch(cachePolicy: cachePolicy) return watcher } @@ -120,12 +124,8 @@ public class ApolloClient { /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress mutation. - @discardableResult public func perform(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler? = nil) -> Cancellable { - return _perform(mutation: mutation, fetchHTTPMethod: fetchHTTPMethod, queue: queue, resultHandler: resultHandler) - } - - func _perform(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler?) -> Cancellable { - return send(operation: mutation, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler) + @discardableResult public func perform(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod = .POST, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler? = nil) -> Cancellable { + return send(operation: mutation, fetchHTTPMethod: fetchHTTPMethod, shouldPublishResultToStore: true, context: context, resultHandler: wrapResultHandler(resultHandler, queue: queue)) } /// Subscribe to a subscription @@ -136,64 +136,72 @@ public class ApolloClient { /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs. /// - Returns: An object that can be used to cancel an in progress subscription. - @discardableResult public func subscribe(subscription: Subscription, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler) -> Cancellable { - return send(operation: subscription, fetchHTTPMethod: fetchHTTPMethod, context: nil, handlerQueue: queue, resultHandler: resultHandler) + @discardableResult public func subscribe(subscription: Subscription, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping GraphQLResultHandler) -> Cancellable { + return send(operation: subscription, fetchHTTPMethod: fetchHTTPMethod, shouldPublishResultToStore: true, context: nil, resultHandler: wrapResultHandler(resultHandler, queue: queue)) } - - fileprivate func send(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler?) -> Cancellable { - func notifyResultHandler(result: GraphQLResult?, error: Error?) { - guard let resultHandler = resultHandler else { return } - - handlerQueue.async { - resultHandler(result, error) - } - } - + fileprivate func send(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, shouldPublishResultToStore: Bool, context: UnsafeMutableRawPointer?, resultHandler: @escaping GraphQLResultHandler) -> Cancellable { return networkTransport.send(operation: operation, fetchHTTPMethod: fetchHTTPMethod) { (response, error) in guard let response = response else { - notifyResultHandler(result: nil, error: error) + resultHandler(nil, error) return } - handlerQueue.async { - - firstly { - try response.parseResult(cacheKeyForObject: self.cacheKeyForObject) - }.andThen { (result, records) in - notifyResultHandler(result: result, error: nil) - - if let records = records { - self.store.publish(records: records, context: context).catch { error in - preconditionFailure(String(describing: error)) - } - } - }.catch { error in - notifyResultHandler(result: nil, error: error) + // If there is no need to publish the result to the store, we can use a fast path. + if !shouldPublishResultToStore { + do { + let result = try response.parseResultFast() + resultHandler(result, nil) + } catch { + resultHandler(nil, error) } + return + } + + firstly { + try response.parseResult(cacheKeyForObject: self.cacheKeyForObject) + }.andThen { (result, records) in + if let records = records { + self.store.publish(records: records, context: context).catch { error in + preconditionFailure(String(describing: error)) + } + } + resultHandler(result, nil) + }.catch { error in + resultHandler(nil, error) } } } } +private func wrapResultHandler(_ resultHandler: GraphQLResultHandler?, queue handlerQueue: DispatchQueue) -> GraphQLResultHandler { + guard let resultHandler = resultHandler else { + return { _, _ in } + } + + return { (result, error) in + handlerQueue.async { + resultHandler(result, error) + } + } +} + private final class FetchQueryOperation: AsynchronousOperation, Cancellable { let client: ApolloClient let query: Query let fetchHTTPMethod: FetchHTTPMethod let cachePolicy: CachePolicy let context: UnsafeMutableRawPointer? - let handlerQueue: DispatchQueue - let resultHandler: OperationResultHandler? + let resultHandler: GraphQLResultHandler private var networkTask: Cancellable? - init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler?) { + init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, resultHandler: @escaping GraphQLResultHandler) { self.client = client self.query = query self.fetchHTTPMethod = fetchHTTPMethod self.cachePolicy = cachePolicy self.context = context - self.handlerQueue = handlerQueue self.resultHandler = resultHandler } @@ -212,7 +220,7 @@ private final class FetchQueryOperation: AsynchronousOperat client.store.load(query: query) { (result, error) in if error == nil { - self.notifyResultHandler(result: result, error: nil) + self.resultHandler(result, nil) if self.cachePolicy != .returnCacheDataAndFetch { self.state = .finished @@ -226,7 +234,7 @@ private final class FetchQueryOperation: AsynchronousOperat } if self.cachePolicy == .returnCacheDataDontFetch { - self.notifyResultHandler(result: nil, error: nil) + self.resultHandler(nil, nil) self.state = .finished return } @@ -236,8 +244,8 @@ private final class FetchQueryOperation: AsynchronousOperat } func fetchFromNetwork() { - networkTask = client.send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: handlerQueue) { (result, error) in - self.notifyResultHandler(result: result, error: error) + networkTask = client.send(operation: query, fetchHTTPMethod: fetchHTTPMethod, shouldPublishResultToStore: true, context: context) { (result, error) in + self.resultHandler(result, error) self.state = .finished return } @@ -247,12 +255,4 @@ private final class FetchQueryOperation: AsynchronousOperat super.cancel() networkTask?.cancel() } - - func notifyResultHandler(result: GraphQLResult?, error: Error?) { - guard let resultHandler = resultHandler else { return } - - handlerQueue.async { - resultHandler(result, error) - } - } } diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index 30510e6b79..db7794d27e 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -64,7 +64,7 @@ public final class ApolloStore { }.andThen { changedKeys in self.didChangeKeys(changedKeys, context: context) fulfill(()) - } + }.wait() } } } @@ -129,7 +129,7 @@ public final class ApolloStore { } } - public func load(query: Query, resultHandler: @escaping OperationResultHandler) { + public func load(query: Query, resultHandler: @escaping GraphQLResultHandler) { load(query: query).andThen { result in resultHandler(result, nil) }.catch { error in diff --git a/Sources/Apollo/DataLoader.swift b/Sources/Apollo/DataLoader.swift index 6b3d05a67f..378115a634 100644 --- a/Sources/Apollo/DataLoader.swift +++ b/Sources/Apollo/DataLoader.swift @@ -1,6 +1,6 @@ import Dispatch -public final class DataLoader { +final class DataLoader { public typealias BatchLoad = ([Key]) -> Promise<[Value]> typealias Load = (key: Key, fulfill: (Value) -> Void, reject: (Error) -> Void) diff --git a/Sources/Apollo/Decoding.swift b/Sources/Apollo/Decoding.swift new file mode 100644 index 0000000000..06dbbe04ce --- /dev/null +++ b/Sources/Apollo/Decoding.swift @@ -0,0 +1,123 @@ +private typealias GroupedFields = GroupedSequence + +func decode(selectionSet: SelectionSet.Type, from object: JSONObject, variables: GraphQLMap? = nil) throws -> SelectionSet { + var groupedFields = GroupedFields() + try collectFields(from: selectionSet.selections, into: &groupedFields, variables: variables) + let resultMap = try decode(groupedFields: groupedFields, from: object, path: [], variables: variables) + + return SelectionSet.init(unsafeResultMap: resultMap) +} + +private func decode(groupedFields: GroupedFields, from object: JSONObject, path: ResponsePath, variables: GraphQLMap?) throws -> ResultMap { + var fieldEntries: [(String, Any?)] = [] + fieldEntries.reserveCapacity(groupedFields.keys.count) + + for (responseName, fields) in groupedFields { + let fieldEntry = try decode(fields: fields, from: object, path: path + responseName, variables: variables) + fieldEntries.append((responseName, fieldEntry)) + } + + return ResultMap(fieldEntries) +} + +/// Before execution, the selection set is converted to a grouped field set. Each entry in the grouped field set is a list of fields that share a response key. This ensures all fields with the same response key (alias or field name) included via referenced fragments are executed at the same time. +private func collectFields(from selections: [GraphQLSelection], forRuntimeType runtimeType: String? = nil, into groupedFields: inout GroupedFields, variables: GraphQLMap?) throws { + for selection in selections { + switch selection { + case let field as GraphQLField: + _ = groupedFields.append(value: field, forKey: field.responseKey) + case let booleanCondition as GraphQLBooleanCondition: + guard let value = variables?[booleanCondition.variableName] else { + throw GraphQLError("Variable \(booleanCondition.variableName) was not provided.") + } + if value as? Bool == !booleanCondition.inverted { + try collectFields(from: booleanCondition.selections, forRuntimeType: runtimeType, into: &groupedFields, variables: variables) + } + case let fragmentSpread as GraphQLFragmentSpread: + let fragment = fragmentSpread.fragment + + if let runtimeType = runtimeType, fragment.possibleTypes.contains(runtimeType) { + try collectFields(from: fragment.selections, forRuntimeType: runtimeType, into: &groupedFields, variables: variables) + } + case let typeCase as GraphQLTypeCase: + let selections: [GraphQLSelection] + if let runtimeType = runtimeType { + selections = typeCase.variants[runtimeType] ?? typeCase.default + } else { + selections = typeCase.default + } + try collectFields(from: selections, forRuntimeType: runtimeType, into: &groupedFields, variables: variables) + default: + preconditionFailure() + } + } +} + +/// Each field requested in the grouped field set that is defined on the selected objectType will result in an entry in the response map. Field execution first coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. +private func decode(fields: [GraphQLField], from object: JSONObject, path: ResponsePath, variables: GraphQLMap?) throws -> Any? { + // GraphQL validation makes sure all fields sharing the same response key have the same arguments and are of the same type, so we only need to resolve one field. + let firstField = fields[0] + + do { + guard let value = object[firstField.responseKey] else { + throw JSONDecodingError.missingValue + } + + return try complete(value: value, ofType: firstField.type, fields: fields, path: path, variables: variables) + } catch { + if !(error is GraphQLResultError) { + throw GraphQLResultError(path: path, underlying: error) + } else { + throw error + } + } +} + +/// After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the field execution process continues recursively. +private func complete(value: JSONValue, ofType returnType: GraphQLOutputType, fields: [GraphQLField], path: ResponsePath, variables: GraphQLMap?) throws -> Any? { + if case .nonNull(let innerType) = returnType { + if value is NSNull { + throw JSONDecodingError.nullValue + } + + return try complete(value: value, ofType: innerType, fields: fields, path: path, variables: variables) + } + + if value is NSNull { + return nil + } + + switch returnType { + case .scalar(let decodable): + return try decodable.init(jsonValue: value) + case .list(let innerType): + guard let array = value as? [JSONValue] else { throw JSONDecodingError.wrongType } + + return try array.enumerated().map { index, element -> Any? in + var path = path + path.append(String(index)) + return try complete(value: element, ofType: innerType, fields: fields, path: path, variables: variables) + } + case .object: + guard let object = value as? JSONObject else { throw JSONDecodingError.wrongType } + guard let runtimeType = object["__typename"] as? String else { + throw GraphQLResultError(path: path + "__typename", underlying: JSONDecodingError.missingValue) + } + // The merged selection set is a list of fields from all sub‐selection sets of the original fields. + let subFields = try collectSubfields(from: fields, forRuntimeType: runtimeType, variables: variables) + // We execute the merged selection set on the object to complete the value. This is the recursive step in the GraphQL execution model. + return try decode(groupedFields: subFields, from: object, path: path, variables: variables) + default: + preconditionFailure() + } +} + +private func collectSubfields(from fields: [GraphQLField], forRuntimeType runtimeType: String, variables: GraphQLMap?) throws -> GroupedFields { + var groupedFields = GroupedFields() + for field in fields { + if case let .object(subSelections) = field.type.namedType { + try collectFields(from: subSelections, forRuntimeType: runtimeType, into: &groupedFields, variables: variables) + } + } + return groupedFields +} diff --git a/Sources/Apollo/GraphQLDependencyTracker.swift b/Sources/Apollo/GraphQLDependencyTracker.swift index 879ee6f2b9..9358f275b1 100644 --- a/Sources/Apollo/GraphQLDependencyTracker.swift +++ b/Sources/Apollo/GraphQLDependencyTracker.swift @@ -2,19 +2,19 @@ final class GraphQLDependencyTracker: GraphQLResultAccumulator { private var dependentKeys: Set = Set() func accept(scalar: JSONValue, info: GraphQLResolveInfo) { - dependentKeys.insert(joined(path: info.cachePath)) + dependentKeys.insert(info.cachePath.joined) } func acceptNullValue(info: GraphQLResolveInfo) { - dependentKeys.insert(joined(path: info.cachePath)) + dependentKeys.insert(info.cachePath.joined) } func accept(list: [Void], info: GraphQLResolveInfo) { - dependentKeys.insert(joined(path: info.cachePath)) + dependentKeys.insert(info.cachePath.joined) } func accept(fieldEntry: Void, info: GraphQLResolveInfo) { - dependentKeys.insert(joined(path: info.cachePath)) + dependentKeys.insert(info.cachePath.joined) } func accept(fieldEntries: [Void], info: GraphQLResolveInfo) { diff --git a/Sources/Apollo/GraphQLExecutor.swift b/Sources/Apollo/GraphQLExecutor.swift index 966b20a307..9a40565f07 100644 --- a/Sources/Apollo/GraphQLExecutor.swift +++ b/Sources/Apollo/GraphQLExecutor.swift @@ -1,16 +1,15 @@ -import Foundation import Dispatch /// A resolver is responsible for resolving a value for a field. -public typealias GraphQLResolver = (_ object: JSONObject, _ info: GraphQLResolveInfo) -> ResultOrPromise +typealias GraphQLResolver = (_ object: JSONObject, _ info: GraphQLResolveInfo) -> ResultOrPromise -public struct GraphQLResolveInfo { +struct GraphQLResolveInfo { let variables: GraphQLMap? - var responsePath: [String] = [] + var responsePath: ResponsePath = [] var responseKeyForField: String = "" - var cachePath: [String] = [] + var cachePath: ResponsePath = [] var cacheKeyForField: String = "" var fields: [GraphQLField] = [] @@ -24,22 +23,18 @@ public struct GraphQLResolveInfo { } } -func joined(path: [String]) -> String { - return path.joined(separator: ".") -} - -public struct GraphQLResultError: Error, LocalizedError { - let path: [String] +struct GraphQLResultError: Error, LocalizedError { + let path: ResponsePath let underlying: Error public var errorDescription: String? { - return "Error at path \"\(joined(path: path))\": \(underlying)" + return "Error at path \"\(path))\": \(underlying)" } } /// A GraphQL executor is responsible for executing a selection set and generating a result. It is initialized with a resolver closure that gets called repeatedly to resolve field values. /// -/// An executor is used both to parse a response received from the server, and to read from the normalized cache. It can also be configured with a accumulator that receives events during execution, and these execution events are used by `GraphQLResultNormalizer` to normalize a response into a flat set of records and keep track of dependent keys. +/// An executor is used both to parse a response received from the server, and to read from the normalized cache. It can also be configured with a accumulator that receives events during execution, and these execution events are used by `GraphQLResultNormalizer` to normalize a response into a flat set of records and by `GraphQLDependencyTracker` keep track of dependent keys. /// /// The methods in this class closely follow the [execution algorithm described in the GraphQL specification](https://facebook.github.io/graphql/#sec-Execution), but an important difference is that execution returns a value for every selection in a selection set, not the merged fields. This means we get a separate result for every fragment, even though all fields that share a response key are still executed at the same time for efficiency. /// @@ -87,7 +82,7 @@ public struct GraphQLResultError: Error, LocalizedError { /// /// These values then get passed into a generated `GraphQLMappable` initializer, and this is how type safe results get built up. /// -public final class GraphQLExecutor { +final class GraphQLExecutor { private let queue: DispatchQueue private let resolver: GraphQLResolver @@ -98,7 +93,7 @@ public final class GraphQLExecutor { var shouldComputeCachePath = true /// Creates a GraphQLExecutor that resolves field values by calling the provided resolver. - public init(resolver: @escaping GraphQLResolver) { + init(resolver: @escaping GraphQLResolver) { queue = DispatchQueue(label: "com.apollographql.GraphQLExecutor") self.resolver = resolver @@ -193,11 +188,11 @@ public final class GraphQLExecutor { let firstField = fields[0] var info = info - + let responseKey = firstField.responseKey info.responseKeyForField = responseKey info.responsePath.append(responseKey) - + if shouldComputeCachePath { let cacheKey = try firstField.cacheKey(with: info.variables) info.cacheKeyForField = cacheKey @@ -246,7 +241,7 @@ public final class GraphQLExecutor { return try whenAll(array.enumerated().map { index, element -> ResultOrPromise in var info = info - + let indexSegment = String(index) info.responsePath.append(indexSegment) info.cachePath.append(indexSegment) diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 9c88452d52..62dbb338a0 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -5,8 +5,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo weak var client: ApolloClient? let query: Query let fetchHTTPMethod: FetchHTTPMethod - let handlerQueue: DispatchQueue - let resultHandler: OperationResultHandler + let resultHandler: GraphQLResultHandler private var context = 0 @@ -14,11 +13,10 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo private var dependentKeys: Set? - init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler) { + init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, resultHandler: @escaping GraphQLResultHandler) { self.client = client self.query = query self.fetchHTTPMethod = fetchHTTPMethod - self.handlerQueue = handlerQueue self.resultHandler = resultHandler client.store.subscribe(self) @@ -30,7 +28,7 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo } func fetch(cachePolicy: CachePolicy) { - fetching = client?._fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: &context, queue: handlerQueue) { [weak self] (result, error) in + fetching = client?.fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: &context) { [weak self] (result, error) in guard let `self` = self else { return } self.dependentKeys = result?.dependentKeys diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index 8a01d8a964..d0002a8ce9 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -37,4 +37,21 @@ public final class GraphQLResponse { return Promise(fulfilled: (GraphQLResult(data: nil, errors: errors, source: .server, dependentKeys: nil), nil)) } } + + func parseResultFast() throws -> GraphQLResult { + let errors: [GraphQLError]? + + if let errorsEntry = body["errors"] as? [JSONObject] { + errors = errorsEntry.map(GraphQLError.init) + } else { + errors = nil + } + + if let dataEntry = body["data"] as? JSONObject { + let data = try decode(selectionSet: Operation.Data.self, from: dataEntry, variables: operation.variables) + return GraphQLResult(data: data, errors: errors, source: .server, dependentKeys: nil) + } else { + return GraphQLResult(data: nil, errors: errors, source: .server, dependentKeys: nil) + } + } } diff --git a/Sources/Apollo/GraphQLResultAccumulator.swift b/Sources/Apollo/GraphQLResultAccumulator.swift index b79a2fe4e1..0296ca3f80 100644 --- a/Sources/Apollo/GraphQLResultAccumulator.swift +++ b/Sources/Apollo/GraphQLResultAccumulator.swift @@ -1,4 +1,4 @@ -public protocol GraphQLResultAccumulator: class { +protocol GraphQLResultAccumulator: class { associatedtype PartialResult associatedtype FieldEntry associatedtype ObjectResult @@ -14,19 +14,19 @@ public protocol GraphQLResultAccumulator: class { func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult } -public func zip(_ accumulator1: Accumulator1, _ accumulator2: Accumulator2) -> Zip2Accumulator { +func zip(_ accumulator1: Accumulator1, _ accumulator2: Accumulator2) -> Zip2Accumulator { return Zip2Accumulator(accumulator1, accumulator2) } -public func zip(_ accumulator1: Accumulator1, _ accumulator2: Accumulator2, _ accumulator3: Accumulator3) -> Zip3Accumulator { +func zip(_ accumulator1: Accumulator1, _ accumulator2: Accumulator2, _ accumulator3: Accumulator3) -> Zip3Accumulator { return Zip3Accumulator(accumulator1, accumulator2, accumulator3) } -public final class Zip2Accumulator: GraphQLResultAccumulator { - public typealias PartialResult = (Accumulator1.PartialResult, Accumulator2.PartialResult) - public typealias FieldEntry = (Accumulator1.FieldEntry, Accumulator2.FieldEntry) - public typealias ObjectResult = (Accumulator1.ObjectResult, Accumulator2.ObjectResult) - public typealias FinalResult = (Accumulator1.FinalResult, Accumulator2.FinalResult) +final class Zip2Accumulator: GraphQLResultAccumulator { + typealias PartialResult = (Accumulator1.PartialResult, Accumulator2.PartialResult) + typealias FieldEntry = (Accumulator1.FieldEntry, Accumulator2.FieldEntry) + typealias ObjectResult = (Accumulator1.ObjectResult, Accumulator2.ObjectResult) + typealias FinalResult = (Accumulator1.FinalResult, Accumulator2.FinalResult) private let accumulator1: Accumulator1 private let accumulator2: Accumulator2 @@ -36,38 +36,38 @@ public final class Zip2Accumulator PartialResult { + func accept(scalar: JSONValue, info: GraphQLResolveInfo) throws -> PartialResult { return (try accumulator1.accept(scalar: scalar, info: info), try accumulator2.accept(scalar: scalar, info: info)) } - public func acceptNullValue(info: GraphQLResolveInfo) throws -> PartialResult { + func acceptNullValue(info: GraphQLResolveInfo) throws -> PartialResult { return (try accumulator1.acceptNullValue(info: info), try accumulator2.acceptNullValue(info: info)) } - public func accept(list: [PartialResult], info: GraphQLResolveInfo) throws -> PartialResult { + func accept(list: [PartialResult], info: GraphQLResolveInfo) throws -> PartialResult { let (list1, list2) = unzip(list) return (try accumulator1.accept(list: list1, info: info), try accumulator2.accept(list: list2, info: info)) } - public func accept(fieldEntry: PartialResult, info: GraphQLResolveInfo) throws -> FieldEntry { + func accept(fieldEntry: PartialResult, info: GraphQLResolveInfo) throws -> FieldEntry { return (try accumulator1.accept(fieldEntry: fieldEntry.0, info: info), try accumulator2.accept(fieldEntry: fieldEntry.1, info: info)) } - public func accept(fieldEntries: [FieldEntry], info: GraphQLResolveInfo) throws -> ObjectResult { + func accept(fieldEntries: [FieldEntry], info: GraphQLResolveInfo) throws -> ObjectResult { let (fieldEntries1, fieldEntries2) = unzip(fieldEntries) return (try accumulator1.accept(fieldEntries: fieldEntries1, info: info), try accumulator2.accept(fieldEntries: fieldEntries2, info: info)) } - public func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult { + func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult { return (try accumulator1.finish(rootValue: rootValue.0, info: info), try accumulator2.finish(rootValue: rootValue.1, info: info)) } } -public final class Zip3Accumulator: GraphQLResultAccumulator { - public typealias PartialResult = (Accumulator1.PartialResult, Accumulator2.PartialResult, Accumulator3.PartialResult) - public typealias FieldEntry = (Accumulator1.FieldEntry, Accumulator2.FieldEntry, Accumulator3.FieldEntry) - public typealias ObjectResult = (Accumulator1.ObjectResult, Accumulator2.ObjectResult, Accumulator3.ObjectResult) - public typealias FinalResult = (Accumulator1.FinalResult, Accumulator2.FinalResult, Accumulator3.FinalResult) +final class Zip3Accumulator: GraphQLResultAccumulator { + typealias PartialResult = (Accumulator1.PartialResult, Accumulator2.PartialResult, Accumulator3.PartialResult) + typealias FieldEntry = (Accumulator1.FieldEntry, Accumulator2.FieldEntry, Accumulator3.FieldEntry) + typealias ObjectResult = (Accumulator1.ObjectResult, Accumulator2.ObjectResult, Accumulator3.ObjectResult) + typealias FinalResult = (Accumulator1.FinalResult, Accumulator2.FinalResult, Accumulator3.FinalResult) private let accumulator1: Accumulator1 private let accumulator2: Accumulator2 @@ -80,29 +80,29 @@ public final class Zip3Accumulator PartialResult { + func accept(scalar: JSONValue, info: GraphQLResolveInfo) throws -> PartialResult { return (try accumulator1.accept(scalar: scalar, info: info), try accumulator2.accept(scalar: scalar, info: info), try accumulator3.accept(scalar: scalar, info: info)) } - public func acceptNullValue(info: GraphQLResolveInfo) throws -> PartialResult { + func acceptNullValue(info: GraphQLResolveInfo) throws -> PartialResult { return (try accumulator1.acceptNullValue(info: info), try accumulator2.acceptNullValue(info: info), try accumulator3.acceptNullValue(info: info)) } - public func accept(list: [PartialResult], info: GraphQLResolveInfo) throws -> PartialResult { + func accept(list: [PartialResult], info: GraphQLResolveInfo) throws -> PartialResult { let (list1, list2, list3) = unzip(list) return (try accumulator1.accept(list: list1, info: info), try accumulator2.accept(list: list2, info: info), try accumulator3.accept(list: list3, info: info)) } - public func accept(fieldEntry: PartialResult, info: GraphQLResolveInfo) throws -> FieldEntry { + func accept(fieldEntry: PartialResult, info: GraphQLResolveInfo) throws -> FieldEntry { return (try accumulator1.accept(fieldEntry: fieldEntry.0, info: info), try accumulator2.accept(fieldEntry: fieldEntry.1, info: info), try accumulator3.accept(fieldEntry: fieldEntry.2, info: info)) } - public func accept(fieldEntries: [FieldEntry], info: GraphQLResolveInfo) throws -> ObjectResult { + func accept(fieldEntries: [FieldEntry], info: GraphQLResolveInfo) throws -> ObjectResult { let (fieldEntries1, fieldEntries2, fieldEntries3) = unzip(fieldEntries) return (try accumulator1.accept(fieldEntries: fieldEntries1, info: info), try accumulator2.accept(fieldEntries: fieldEntries2, info: info), try accumulator3.accept(fieldEntries: fieldEntries3, info: info)) } - public func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult { + func finish(rootValue: ObjectResult, info: GraphQLResolveInfo) throws -> FinalResult { return (try accumulator1.finish(rootValue: rootValue.0, info: info), try accumulator2.finish(rootValue: rootValue.1, info: info), try accumulator3.finish(rootValue: rootValue.2, info: info)) } } diff --git a/Sources/Apollo/GraphQLResultNormalizer.swift b/Sources/Apollo/GraphQLResultNormalizer.swift index 18789688dc..2f30c9f18b 100644 --- a/Sources/Apollo/GraphQLResultNormalizer.swift +++ b/Sources/Apollo/GraphQLResultNormalizer.swift @@ -20,7 +20,7 @@ final class GraphQLResultNormalizer: GraphQLResultAccumulator { } func accept(fieldEntries: [(key: String, value: JSONValue)], info: GraphQLResolveInfo) throws -> JSONValue { - let cachePath = joined(path: info.cachePath) + let cachePath = info.cachePath.joined let object = JSONObject(fieldEntries) records.merge(record: Record(key: cachePath, object)) diff --git a/Sources/Apollo/ResponsePath.swift b/Sources/Apollo/ResponsePath.swift new file mode 100644 index 0000000000..322e334142 --- /dev/null +++ b/Sources/Apollo/ResponsePath.swift @@ -0,0 +1,56 @@ +/// A response path is stored as a linked list because using an array turned out to be +/// a performance bottleneck during decoding/execution. +struct ResponsePath: ExpressibleByArrayLiteral { + typealias Key = String + + private final class Node { + let previous: Node? + let key: Key + + init(previous: Node?, key: Key) { + self.previous = previous + self.key = key + } + + lazy var joined: String = { + if let previous = previous { + return previous.joined + ".\(key)" + } else { + return key + } + }() + } + + private var head: Node? + var joined: String { + return head?.joined ?? "" + } + + init(arrayLiteral segments: Key...) { + for segment in segments { + append(segment) + } + } + + mutating func append(_ key: Key) { + head = Node(previous: head, key: key) + } + + static func + (lhs: ResponsePath, rhs: Key) -> ResponsePath { + var lhs = lhs + lhs.append(rhs) + return lhs + } +} + +extension ResponsePath: CustomStringConvertible { + var description: String { + return joined + } +} + +extension ResponsePath: Equatable { + static func == (lhs: ResponsePath, rhs: ResponsePath) -> Bool { + return lhs.joined == rhs.joined + } +} diff --git a/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift b/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift index 92619b4a77..6690056ea9 100644 --- a/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift +++ b/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift @@ -212,7 +212,7 @@ class LoadQueryFromStoreTests: XCTestCase { // MARK: - Helpers - private func load(query: Query, resultHandler: @escaping OperationResultHandler) { + private func load(query: Query, resultHandler: @escaping GraphQLResultHandler) { let expectation = self.expectation(description: "Loading query from store") store.load(query: query) { (result, error) in diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 01a210229e..bf3a6fb1c7 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -28,7 +28,7 @@ class WatchQueryTests: XCTestCase { let store = ApolloStore(cache: cache) let client = ApolloClient(networkTransport: networkTransport, store: store) - var verifyResult: OperationResultHandler + var verifyResult: GraphQLResultHandler verifyResult = { (result, error) in XCTAssertNil(error) @@ -92,7 +92,7 @@ class WatchQueryTests: XCTestCase { let query = HeroAndFriendsNamesQuery() - var verifyResult: OperationResultHandler + var verifyResult: GraphQLResultHandler verifyResult = { (result, error) in XCTAssertNil(error) @@ -220,7 +220,7 @@ class WatchQueryTests: XCTestCase { let client = ApolloClient(networkTransport: networkTransport, store: store) client.store.cacheKeyForObject = { $0["id"] } - var verifyResult: OperationResultHandler + var verifyResult: GraphQLResultHandler verifyResult = { (result, error) in XCTAssertNil(error) @@ -276,7 +276,7 @@ class WatchQueryTests: XCTestCase { let client = ApolloClient(networkTransport: networkTransport, store: store) let query = HeroAndFriendsNamesQuery() - var verifyResult: OperationResultHandler + var verifyResult: GraphQLResultHandler verifyResult = { (result, error) in XCTAssertNil(error)