@@ -84,7 +84,7 @@ actor HttpConnection: ConnectionProtocol {
84
84
private let negotiationRedirectionLimit = 100
85
85
86
86
private var connectionState : ConnectionState = . disconnected
87
- private var connectionStarted : Bool = false
87
+ private var connectionStartedSuccessfully : Bool = false
88
88
private let httpClient : AccessTokenHttpClient
89
89
private let logger : Logger
90
90
private var options : HttpConnectionOptions
@@ -107,6 +107,7 @@ actor HttpConnection: ConnectionProtocol {
107
107
private var onReceive : Transport . OnReceiveHandler ?
108
108
private var onClose : Transport . OnCloseHander ?
109
109
private let negotiateVersion = 1
110
+ private var closeDuringStartError : Error ? = nil
110
111
111
112
// MARK: - Initialization
112
113
@@ -142,39 +143,27 @@ actor HttpConnection: ConnectionProtocol {
142
143
// - If startInternalTask is nil, start will directly stop
143
144
// - If startInternalTask is not nil, wait it finish and then call the stop
144
145
guard connectionState == . disconnected else {
145
- throw SignalRError . invalidOperation ( " Cannot start an HttpConnection that is not in the 'Disconnected' state. " )
146
+ throw SignalRError . invalidOperation ( " Cannot start an HttpConnection that is not in the 'Disconnected' state. Currently it's \( connectionState ) " )
146
147
}
147
148
148
149
connectionState = . connecting
149
150
150
151
startInternalTask = Task {
151
- try await self . startInternal ( transferFormat: transferFormat)
152
- }
153
-
154
- do {
155
- try await startInternalTask? . value
156
- } catch {
157
- connectionState = . disconnected
158
- throw error
159
- }
160
-
161
- if connectionState == . disconnecting {
162
- let message = " Failed to start the HttpConnection before stop() was called. "
163
- logger. log ( level: . error, message: " \( message) " )
164
- await stopTask? . value
165
- throw SignalRError . failedToStartConnection ( message)
166
- } else if connectionState != . connected {
167
- let message = " HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state! "
168
- logger. log ( level: . error, message: " \( message) " )
169
- throw SignalRError . failedToStartConnection ( message)
152
+ do {
153
+ try await self . startInternal ( transferFormat: transferFormat)
154
+ connectionStartedSuccessfully = true
155
+ } catch {
156
+ connectionState = . disconnected
157
+ throw error
158
+ }
170
159
}
171
160
172
- connectionStarted = true
161
+ try await startInternalTask ? . value
173
162
}
174
163
175
164
func send( _ data: StringOrData ) async throws {
176
165
guard connectionState == . connected else {
177
- throw SignalRError . invalidOperation ( " Cannot send data if the connection is not in the 'Connected' State. " )
166
+ throw SignalRError . invalidOperation ( " Cannot send data if the connection is not in the 'Connected' State. Currently it's \( connectionState ) " )
178
167
}
179
168
180
169
try await transport? . send ( data)
@@ -204,6 +193,11 @@ actor HttpConnection: ConnectionProtocol {
204
193
// MARK: - Private Methods
205
194
206
195
private func startInternal( transferFormat: TransferFormat ) async throws {
196
+ guard connectionState == . connecting else {
197
+ throw SignalRError . connectionAborted
198
+ }
199
+ closeDuringStartError = nil
200
+
207
201
var url = baseUrl
208
202
await httpClient. setAccessTokenFactory ( factory: accessTokenFactory)
209
203
@@ -252,10 +246,14 @@ actor HttpConnection: ConnectionProtocol {
252
246
inherentKeepAlivePrivate = true
253
247
}
254
248
255
- if connectionState == . connecting {
256
- logger. log ( level: . debug, message: " The HttpConnection connected successfully. " )
257
- connectionState = . connected
249
+ guard closeDuringStartError == nil else {
250
+ throw closeDuringStartError!
258
251
}
252
+
253
+ // IMPORTANT: There should be no async code start from here. Otherwise, we may lost the control of the connection lifecycle
254
+
255
+ connectionState = . connected
256
+ logger. log ( level: . debug, message: " The HttpConnection connected successfully. " )
259
257
} catch {
260
258
logger. log ( level: . error, message: " Failed to start the connection: \( error) " )
261
259
connectionState = . disconnected
@@ -265,9 +263,17 @@ actor HttpConnection: ConnectionProtocol {
265
263
}
266
264
267
265
private func stopInternal( error: Error ? ) async {
266
+ guard connectionState != . disconnected else {
267
+ return
268
+ }
269
+
268
270
stopError = error
271
+ closeDuringStartError = error ?? SignalRError . connectionAborted
269
272
270
273
do {
274
+ // startInternalTask may have several cases:
275
+ // 1. Already finished. Just return immediately
276
+ // 2. Still in progress. Caused by closeDuringStartError, it will throw and set transport to nil
271
277
try await startInternalTask? . value
272
278
} catch {
273
279
// Ignore errors from startInternal
@@ -278,9 +284,8 @@ actor HttpConnection: ConnectionProtocol {
278
284
try await transport? . stop ( error: nil )
279
285
} catch {
280
286
logger. log ( level: . error, message: " HttpConnection.transport.stop() threw error ' \( error) '. " )
281
- await stopConnection ( error: error)
287
+ await handleConnectionClose ( error: error)
282
288
}
283
- transport = nil
284
289
} else {
285
290
logger. log ( level: . debug, message: " HttpConnection.transport is undefined in HttpConnection.stop() because start() failed. " )
286
291
}
@@ -367,46 +372,60 @@ actor HttpConnection: ConnectionProtocol {
367
372
await transport!. onReceive ( self . onReceive)
368
373
await transport!. onClose { [ weak self] error in
369
374
guard let self = self else { return }
370
- await self . stopConnection ( error: error)
375
+ await self . handleConnectionClose ( error: error)
371
376
}
372
377
373
- try await transport!. connect ( url: url, transferFormat: transferFormat)
378
+ do {
379
+ try await transport!. connect ( url: url, transferFormat: transferFormat)
380
+ } catch {
381
+ await transport!. onReceive ( nil )
382
+ await transport!. onClose ( nil )
383
+ throw error
384
+ }
374
385
}
375
386
376
- private func stopConnection ( error: Error ? ) async {
387
+ private func handleConnectionClose ( error: Error ? ) async {
377
388
logger. log ( level: . debug, message: " HttpConnection.stopConnection( \( String ( describing: error) ) ) called while in state \( connectionState) . " )
378
389
379
390
transport = nil
380
391
381
392
let finalError = stopError ?? error
382
393
stopError = nil
394
+ closeDuringStartError = finalError ?? SignalRError . connectionAborted
383
395
384
396
if connectionState == . disconnected {
385
397
logger. log ( level: . debug, message: " Call to HttpConnection.stopConnection( \( String ( describing: finalError) ) ) was ignored because the connection is already in the disconnected state. " )
386
398
return
387
399
}
388
400
389
- if connectionState == . connecting {
390
- logger. log ( level: . warning, message: " Call to HttpConnection.stopConnection( \( String ( describing: finalError) ) ) was ignored because the connection is still in the connecting state. " )
401
+ if ( connectionState == . connecting) {
402
+ // connecting means start still control the lifetime. As we set closeDuringStartError, it throws there.
403
+ logger. log ( level: . debug, message: " Call to HttpConnection.stopConnection( \( String ( describing: finalError) ) ) was ignored because the connection is already in the connecting state. " )
391
404
return
392
405
}
393
406
394
- if connectionState == . disconnecting {
395
- // Any stop() awaiters will be scheduled to continue after the onClose callback fires.
396
- }
397
-
398
407
if let error = finalError {
399
408
logger. log ( level: . error, message: " Connection disconnected with error ' \( error) '. " )
400
409
} else {
401
410
logger. log ( level: . information, message: " Connection disconnected. " )
402
411
}
403
412
404
413
connectionId = nil
414
+ await completeConnectionClose ( error: finalError)
415
+ }
416
+
417
+ // Should be called whenever connection is started (start() doesn't throw and connection is closed)
418
+ private func completeConnectionClose( error: Error ? ) async {
405
419
connectionState = . disconnected
406
420
407
- if connectionStarted {
408
- connectionStarted = false
409
- await onClose ? ( finalError)
421
+ // There's a chance that we call close() and status changed to disconnecting and startinteral throws.
422
+ // We should not call onclose again
423
+ if connectionStartedSuccessfully {
424
+ connectionStartedSuccessfully = false
425
+ Task { [ weak self] ( ) in
426
+ guard let self = self else { return }
427
+ await self . onClose ? ( error)
428
+ }
410
429
}
411
430
}
412
431
0 commit comments