@@ -34,71 +34,71 @@ private let logger = Logger(subsystem: "LiveViewNative", category: "LiveSessionC
34
34
public class LiveSessionCoordinator < R: RootRegistry > : ObservableObject {
35
35
/// The current state of the live view connection.
36
36
@Published public private( set) var state = LiveSessionState . notConnected
37
-
37
+
38
38
/// The current URL this live view is connected to.
39
39
public private( set) var url : URL
40
-
40
+
41
41
@Published var navigationPath = [ LiveNavigationEntry < R > ] ( )
42
-
42
+
43
43
internal let configuration : LiveSessionConfiguration
44
-
44
+
45
45
@Published private( set) var rootLayout : LiveViewNativeCore . Document ?
46
46
@Published private( set) var stylesheet : Stylesheet < R > ?
47
-
47
+
48
48
// Socket connection
49
- var socket : Socket ?
50
-
49
+ var socket : SwiftPhoenixClient . Socket ?
50
+
51
51
private var domValues : DOMValues !
52
-
53
- private var liveReloadSocket : Socket ?
54
- private var liveReloadChannel : Channel ?
55
-
52
+
53
+ private var liveReloadSocket : SwiftPhoenixClient . Socket ?
54
+ private var liveReloadChannel : SwiftPhoenixClient . Channel ?
55
+
56
56
private var cancellables = Set < AnyCancellable > ( )
57
-
57
+
58
58
private var mergedEventSubjects : AnyCancellable ?
59
59
private var eventSubject = PassthroughSubject < ( LiveViewCoordinator < R > , ( String , Payload ) ) , Never > ( )
60
60
private var eventHandlers = Set < AnyCancellable > ( )
61
-
61
+
62
62
/// Delegate for the ``urlSession``.
63
63
///
64
64
/// This delegate will add the `_format` and other necessary query params to any redirects.
65
65
private var urlSessionDelegate : LiveSessionURLSessionDelegate < R >
66
-
66
+
67
67
/// The ``URLSession`` instance to use for all HTTP requests.
68
68
///
69
69
/// This session is created using the ``LiveSessionConfiguration/urlSessionConfiguration``.
70
70
private var urlSession : URLSession
71
-
71
+
72
72
public convenience init ( _ host: some LiveViewHost , config: LiveSessionConfiguration = . init( ) , customRegistryType: R . Type = R . self) {
73
73
self . init ( host. url, config: config, customRegistryType: customRegistryType)
74
74
}
75
-
75
+
76
76
/// Creates a new coordinator with a custom registry.
77
77
/// - Parameter url: The URL of the page to establish the connection to.
78
78
/// - Parameter config: The configuration for this coordinator.
79
79
/// - Parameter customRegistryType: The type of the registry of custom views this coordinator will use when building the SwiftUI view tree from the DOM. This can generally be inferred automatically.
80
80
public init ( _ url: URL , config: LiveSessionConfiguration = . init( ) , customRegistryType _: R . Type = R . self) {
81
81
self . url = url. appending ( path: " " ) . absoluteURL
82
-
82
+
83
83
self . configuration = config
84
-
84
+
85
85
config. urlSessionConfiguration. httpCookieStorage = . shared
86
86
self . urlSessionDelegate = . init( )
87
87
self . urlSession = . init(
88
88
configuration: config. urlSessionConfiguration,
89
89
delegate: self . urlSessionDelegate,
90
90
delegateQueue: nil
91
91
)
92
-
92
+
93
93
self . navigationPath = [ . init( url: url, coordinator: . init( session: self , url: self . url) ) ]
94
-
94
+
95
95
self . mergedEventSubjects = self . navigationPath. first!. coordinator. eventSubject. compactMap ( { [ weak self] value in
96
96
self . map ( { ( $0. navigationPath. first!. coordinator, value) } )
97
97
} )
98
98
. sink ( receiveValue: { [ weak self] value in
99
99
self ? . eventSubject. send ( value)
100
100
} )
101
-
101
+
102
102
$navigationPath. scan ( ( [ LiveNavigationEntry < R > ] ( ) , [ LiveNavigationEntry < R > ] ( ) ) , { ( $0. 1 , $1) } ) . sink { [ weak self] prev, next in
103
103
guard let self else { return }
104
104
let isDisconnected : Bool
@@ -137,14 +137,14 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
137
137
}
138
138
} . store ( in: & cancellables)
139
139
}
140
-
140
+
141
141
/// Creates a new coordinator without a custom registry.
142
142
/// - Parameter url: The URL of the page to establish the connection to.
143
143
/// - Parameter config: The configuration for this coordinator.
144
144
public convenience init ( _ url: URL , config: LiveSessionConfiguration = . init( ) ) where R == EmptyRegistry {
145
145
self . init ( url, config: config, customRegistryType: EmptyRegistry . self)
146
146
}
147
-
147
+
148
148
/// Connects this coordinator to the LiveView channel.
149
149
///
150
150
/// You generally do not call this function yourself. It is called automatically when the ``LiveView`` appears.
@@ -159,13 +159,13 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
159
159
guard case . notConnected = state else {
160
160
return
161
161
}
162
-
162
+
163
163
let originalURL = self . navigationPath. last!. url
164
-
164
+
165
165
logger. debug ( " Connecting to \( originalURL. absoluteString) " )
166
-
166
+
167
167
state = . connecting
168
-
168
+
169
169
do {
170
170
var request = URLRequest ( url: originalURL)
171
171
request. httpMethod = httpMethod
@@ -186,7 +186,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
186
186
} else {
187
187
url = originalURL
188
188
}
189
-
189
+
190
190
let doc = try SwiftSoup . parse ( html, url. absoluteString, SwiftSoup . Parser. xmlParser ( ) . settings ( . init( true , true ) ) )
191
191
let domValues = try self . extractDOMValues ( doc)
192
192
// extract the root layout, removing anything within the `<div data-phx-main>`.
@@ -205,23 +205,23 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
205
205
}
206
206
}
207
207
self . rootLayout = try LiveViewNativeCore . Document. parse ( doc. outerHtml ( ) )
208
-
208
+
209
209
self . domValues = domValues
210
-
210
+
211
211
if socket == nil {
212
212
try await self . connectSocket ( domValues)
213
213
}
214
-
214
+
215
215
self . stylesheet = try await stylesheet
216
-
216
+
217
217
try await navigationPath. last!. coordinator. connect ( domValues: domValues, redirect: false )
218
218
} catch {
219
219
self . state = . connectionFailed( error)
220
220
logger. log ( level: . error, " \( error. localizedDescription) " )
221
221
return
222
222
}
223
223
}
224
-
224
+
225
225
private func disconnect( preserveNavigationPath: Bool = false ) async {
226
226
for entry in self . navigationPath {
227
227
await entry. coordinator. disconnect ( )
@@ -236,7 +236,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
236
236
self . socket = nil
237
237
self . state = . notConnected
238
238
}
239
-
239
+
240
240
/// Forces the session to disconnect then connect.
241
241
///
242
242
/// All state will be lost when the reload occurs, as an entirely new LiveView is mounted.
@@ -252,7 +252,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
252
252
}
253
253
await self . connect ( httpMethod: httpMethod, httpBody: httpBody)
254
254
}
255
-
255
+
256
256
/// Creates a publisher that can be used to listen for server-sent LiveView events.
257
257
///
258
258
/// - Parameter event: The event name that is being listened for.
@@ -266,7 +266,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
266
266
. filter { $0. 1 . 0 == event }
267
267
. map ( { ( $0. 0 , $0. 1 . 1 ) } )
268
268
}
269
-
269
+
270
270
/// Permanently registers a handler for a server-sent LiveView event.
271
271
///
272
272
/// - Parameter event: The event name that is being listened for.
@@ -280,7 +280,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
280
280
. sink ( receiveValue: handler)
281
281
. store ( in: & eventHandlers)
282
282
}
283
-
283
+
284
284
/// Request the dead render with the given `request`.
285
285
///
286
286
/// Returns the dead render HTML and the HTTP response information (including the final URL after redirects).
@@ -292,15 +292,15 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
292
292
if domValues != nil {
293
293
request. setValue ( domValues. phxCSRFToken, forHTTPHeaderField: " x-csrf-token " )
294
294
}
295
-
295
+
296
296
let data : Data
297
297
let response : URLResponse
298
298
do {
299
299
( data, response) = try await urlSession. data ( for: request)
300
300
} catch {
301
301
throw LiveConnectionError . initialFetchError ( error)
302
302
}
303
-
303
+
304
304
guard let response = response as? HTTPURLResponse ,
305
305
response. statusCode == 200 ,
306
306
let html = String ( data: data, encoding: . utf8)
@@ -317,7 +317,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
317
317
}
318
318
return ( html, response)
319
319
}
320
-
320
+
321
321
struct DOMValues {
322
322
let phxCSRFToken : String
323
323
let phxSession : String
@@ -326,17 +326,17 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
326
326
let phxID : String
327
327
let liveReloadEnabled : Bool
328
328
}
329
-
329
+
330
330
private func extractLiveReloadFrame( _ doc: SwiftSoup . Document ) throws -> Bool {
331
331
!( try doc. select ( " iframe[src= \" /phoenix/live_reload/frame \" ] " ) . isEmpty ( ) )
332
332
}
333
-
333
+
334
334
private func extractDOMValues( _ doc: SwiftSoup . Document ) throws -> DOMValues {
335
335
let csrfToken = try doc. select ( " csrf-token " )
336
336
guard !csrfToken. isEmpty ( ) else {
337
337
throw LiveConnectionError . initialParseError ( missingOrInvalid: . csrfToken)
338
338
}
339
-
339
+
340
340
let mainDivRes = try doc. select ( " div[data-phx-main] " )
341
341
guard !mainDivRes. isEmpty ( ) else {
342
342
throw LiveConnectionError . initialParseError ( missingOrInvalid: . phxMain)
@@ -357,7 +357,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
357
357
guard let self else {
358
358
return continuation. resume ( throwing: LiveConnectionError . sessionCoordinatorReleased)
359
359
}
360
-
360
+
361
361
var wsEndpoint = URLComponents ( url: self . url, resolvingAgainstBaseURL: true ) !
362
362
wsEndpoint. scheme = self . url. scheme == " https " ? " wss " : " ws "
363
363
wsEndpoint. path = " /live/websocket "
@@ -371,7 +371,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
371
371
]
372
372
}
373
373
)
374
-
374
+
375
375
// set to `reconnecting` when the socket asks for the delay duration.
376
376
socket. reconnectAfter = { [ weak self] tries in
377
377
Task {
@@ -389,9 +389,9 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
389
389
}
390
390
}
391
391
}
392
-
392
+
393
393
var refs = [ String] ( )
394
-
394
+
395
395
refs. append ( socket. onOpen { [ weak self, weak socket] in
396
396
guard let socket else { return }
397
397
guard self != nil else {
@@ -416,20 +416,20 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
416
416
}
417
417
self . socket? . onClose { logger. debug ( " [Socket] Closed " ) }
418
418
self . socket? . logger = { message in logger. debug ( " [Socket] \( message) " ) }
419
-
419
+
420
420
self . state = . connected
421
-
421
+
422
422
if domValues. liveReloadEnabled {
423
423
await self . connectLiveReloadSocket ( urlSessionConfiguration: urlSession. configuration)
424
424
}
425
425
}
426
-
426
+
427
427
private func connectLiveReloadSocket( urlSessionConfiguration: URLSessionConfiguration ) async {
428
428
if let liveReloadSocket = self . liveReloadSocket {
429
429
liveReloadSocket. disconnect ( )
430
430
self . liveReloadSocket = nil
431
431
}
432
-
432
+
433
433
logger. debug ( " [LiveReload] attempting to connect... " )
434
434
435
435
var liveReloadEndpoint = URLComponents ( url: self . url, resolvingAgainstBaseURL: true ) !
@@ -453,7 +453,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
453
453
}
454
454
}
455
455
}
456
-
456
+
457
457
func redirect( _ redirect: LiveRedirect ) async throws {
458
458
switch redirect. mode {
459
459
case . replaceTop:
@@ -497,7 +497,7 @@ class LiveSessionURLSessionDelegate<R: RootRegistry>: NSObject, URLSessionTaskDe
497
497
return request
498
498
}
499
499
let components = URLComponents ( url: url, resolvingAgainstBaseURL: false )
500
-
500
+
501
501
var newRequest = request
502
502
if !( components? . queryItems? . contains ( where: { $0. name == " _format " } ) ?? false ) {
503
503
newRequest. url = url. appending ( queryItems: [ . init( name: " _format " , value: await LiveSessionCoordinator< R> . platform) ] )
@@ -519,19 +519,19 @@ extension LiveSessionCoordinator {
519
519
" target " : getTarget ( )
520
520
]
521
521
}
522
-
522
+
523
523
private static func getAppVersion( ) -> String {
524
524
let dictionary = Bundle . main. infoDictionary!
525
525
526
526
return dictionary [ " CFBundleShortVersionString " ] as! String
527
527
}
528
-
528
+
529
529
private static func getAppBuild( ) -> String {
530
530
let dictionary = Bundle . main. infoDictionary!
531
531
532
532
return dictionary [ " CFBundleVersion " ] as! String
533
533
}
534
-
534
+
535
535
private static func getBundleID( ) -> String {
536
536
let dictionary = Bundle . main. infoDictionary!
537
537
@@ -558,7 +558,7 @@ extension LiveSessionCoordinator {
558
558
let majorVersion = operatingSystemVersion. majorVersion
559
559
let minorVersion = operatingSystemVersion. minorVersion
560
560
let patchVersion = operatingSystemVersion. patchVersion
561
-
561
+
562
562
return " \( majorVersion) . \( minorVersion) . \( patchVersion) "
563
563
#else
564
564
return UIDevice . current. systemVersion
0 commit comments