@@ -13,19 +13,30 @@ import AWSPluginsCore
13
13
/// Publishes a mutation event to the specified Cloud API. Upon receipt of the API response, validates to ensure it is
14
14
/// not a retriable error. If it is, attempts a retry until either success or terminal failure. Upon success or
15
15
/// terminal failure, publishes the event response to the appropriate ModelReconciliationQueue subject.
16
+ @available ( iOS 13 . 0 , * )
16
17
class SyncMutationToCloudOperation : Operation {
17
18
18
19
private weak var api : APICategoryGraphQLBehavior ?
19
20
private let mutationEvent : MutationEvent
20
21
private var mutationOperation : GraphQLOperation < MutationSync < AnyModel > > ?
22
+ private let networkReachabilityPublisher : AnyPublisher < ReachabilityUpdate , DataStoreError > ?
21
23
private let completion : GraphQLOperation < MutationSync < AnyModel > > . EventListener
22
-
23
- init ( mutationEvent: MutationEvent , api: APICategoryGraphQLBehavior ,
24
+ private var mutationRetryNotifier : MutationRetryNotifier ?
25
+ private var requestRetryablePolicy : RequestRetryablePolicy
26
+ private var currentAttemptNumber : Int
27
+
28
+ init ( mutationEvent: MutationEvent ,
29
+ api: APICategoryGraphQLBehavior ,
30
+ networkReachabilityPublisher: AnyPublisher < ReachabilityUpdate , DataStoreError > ? ,
31
+ currentAttemptNumber: Int = 1 ,
32
+ requestRetryablePolicy: RequestRetryablePolicy ? = RequestRetryablePolicy ( ) ,
24
33
completion: @escaping GraphQLOperation < MutationSync < AnyModel > > . EventListener ) {
25
34
self . mutationEvent = mutationEvent
26
35
self . api = api
36
+ self . networkReachabilityPublisher = networkReachabilityPublisher
27
37
self . completion = completion
28
-
38
+ self . currentAttemptNumber = currentAttemptNumber
39
+ self . requestRetryablePolicy = requestRetryablePolicy ?? RequestRetryablePolicy ( )
29
40
super. init ( )
30
41
}
31
42
@@ -42,6 +53,13 @@ class SyncMutationToCloudOperation: Operation {
42
53
sendMutationToCloud ( )
43
54
}
44
55
56
+ override func cancel( ) {
57
+ mutationOperation? . cancel ( )
58
+ mutationRetryNotifier? . cancel ( )
59
+ let apiError = APIError . unknown ( " Operation cancelled " , " " )
60
+ finish ( result: . failed( apiError) )
61
+ }
62
+
45
63
private func sendMutationToCloud( ) {
46
64
guard !isCancelled else {
47
65
mutationOperation? . cancel ( )
@@ -51,14 +69,6 @@ class SyncMutationToCloudOperation: Operation {
51
69
}
52
70
53
71
log. debug ( #function)
54
- guard let api = api else {
55
- // TODO: This should be part of our error handling routines
56
- log. error ( " \( #function) : API unexpectedly nil " )
57
- let apiError = APIError . unknown ( " API unexpectedly nil " , " " )
58
- finish ( result: . failed( apiError) )
59
- return
60
- }
61
-
62
72
guard let mutationType = GraphQLMutationType ( rawValue: mutationEvent. mutationType) else {
63
73
let dataStoreError = DataStoreError . decodingError (
64
74
" Invalid mutation type " ,
@@ -67,11 +77,19 @@ class SyncMutationToCloudOperation: Operation {
67
77
match any known GraphQL mutation type. Ensure you only send valid mutation types:
68
78
\( GraphQLMutationType . allCases)
69
79
"""
70
- )
80
+ )
71
81
log. error ( error: dataStoreError)
82
+ let apiError = APIError . unknown ( " Invalid mutation type " , " " , dataStoreError)
83
+ finish ( result: . failed( apiError) )
72
84
return
73
85
}
74
86
87
+ if let apiRequest = createAPIRequest ( mutationType: mutationType) {
88
+ makeAPIRequest ( apiRequest)
89
+ }
90
+ }
91
+
92
+ func createAPIRequest( mutationType: GraphQLMutationType ) -> GraphQLRequest < MutationSync < AnyModel > > ? {
75
93
let request : GraphQLRequest < MutationSync < AnyModel > >
76
94
do {
77
95
if mutationType == . delete {
@@ -82,11 +100,21 @@ class SyncMutationToCloudOperation: Operation {
82
100
} catch {
83
101
let apiError = APIError . unknown ( " Couldn't decode model " , " " , error)
84
102
finish ( result: . failed( apiError) )
85
- return
103
+ return nil
86
104
}
105
+ return request
106
+ }
87
107
88
- log. verbose ( " \( #function) sending mutation with sync data: \( request) " )
89
- mutationOperation = api. mutate ( request: request) { asyncEvent in
108
+ func makeAPIRequest( _ apiRequest: GraphQLRequest < MutationSync < AnyModel > > ) {
109
+ guard let api = api else {
110
+ // TODO: This should be part of our error handling routines
111
+ log. error ( " \( #function) : API unexpectedly nil " )
112
+ let apiError = APIError . unknown ( " API unexpectedly nil " , " " )
113
+ finish ( result: . failed( apiError) )
114
+ return
115
+ }
116
+ log. verbose ( " \( #function) sending mutation with sync data: \( apiRequest) " )
117
+ mutationOperation = api. mutate ( request: apiRequest) { asyncEvent in
90
118
self . log. verbose ( " sendMutationToCloud received asyncEvent: \( asyncEvent) " )
91
119
self . validateResponseFromCloud ( asyncEvent: asyncEvent)
92
120
}
@@ -126,7 +154,17 @@ class SyncMutationToCloudOperation: Operation {
126
154
return
127
155
}
128
156
129
- // TODO: Wire in actual event validation and retriability
157
+ if case . failed( let error) = asyncEvent {
158
+ let advice = getRetryAdviceIfRetryable ( error: error)
159
+ if advice. shouldRetry {
160
+ self . scheduleRetry ( advice: advice)
161
+ } else {
162
+ self . finish ( result: . failed( error) )
163
+ }
164
+ return
165
+ }
166
+
167
+ // TODO: Wire in actual event validation
130
168
131
169
// This doesn't belong here--need to add a `delete` API to the MutationEventSource and pass a
132
170
// reference into the mutation queue.
@@ -139,7 +177,36 @@ class SyncMutationToCloudOperation: Operation {
139
177
self . finish ( result: asyncEvent)
140
178
}
141
179
}
180
+ }
181
+
182
+ private func getRetryAdviceIfRetryable( error: APIError ) -> RequestRetryAdvice {
183
+ var advice = RequestRetryAdvice ( shouldRetry: false , retryInterval: DispatchTimeInterval . never)
184
+
185
+ switch error {
186
+ case . networkError( _, _, let error) :
187
+ //currently expecting APIOperationResponse to be an URLError
188
+ let urlError = error as? URLError
189
+ advice = requestRetryablePolicy. retryRequestAdvice ( urlError: urlError,
190
+ httpURLResponse: nil ,
191
+ attemptNumber: currentAttemptNumber)
192
+ case . httpStatusError( _, let httpURLResponse) :
193
+ advice = requestRetryablePolicy. retryRequestAdvice ( urlError: nil ,
194
+ httpURLResponse: httpURLResponse,
195
+ attemptNumber: currentAttemptNumber)
196
+ default :
197
+ break
198
+ }
199
+ return advice
200
+ }
142
201
202
+ private func scheduleRetry( advice: RequestRetryAdvice ) {
203
+ log. verbose ( " \( #function) scheduling retry for mutation " )
204
+ mutationRetryNotifier = MutationRetryNotifier ( advice: advice,
205
+ networkReachabilityPublisher: networkReachabilityPublisher) {
206
+ self . sendMutationToCloud ( )
207
+ self . mutationRetryNotifier = nil
208
+ }
209
+ currentAttemptNumber += 1
143
210
}
144
211
145
212
private func finish( result: AsyncEvent < Void , GraphQLResponse < MutationSync < AnyModel > > , APIError > ) {
@@ -152,4 +219,5 @@ class SyncMutationToCloudOperation: Operation {
152
219
}
153
220
}
154
221
222
+ @available ( iOS 13 . 0 , * )
155
223
extension SyncMutationToCloudOperation : DefaultLogger { }
0 commit comments