@@ -128,13 +128,17 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
128128 false
129129 }
130130 if next. last!. coordinator. url != next. last!. url || isDisconnected {
131- Task {
132- if prev . count > next . count {
133- // back navigation
131+ if prev . count > next . count {
132+ // back navigation
133+ Task ( priority : . userInitiated ) {
134134 try await next. last!. coordinator. connect ( domValues: self . domValues, redirect: true )
135- } else if next. count > prev. count && prev. count > 0 {
136- // forward navigation (from `redirect` or `<NavigationLink>`)
135+ }
136+ } else if next. count > prev. count && prev. count > 0 {
137+ // forward navigation (from `redirect` or `<NavigationLink>`)
138+ Task {
137139 await prev. last? . coordinator. disconnect ( )
140+ }
141+ Task ( priority: . userInitiated) {
138142 try await next. last? . coordinator. connect ( domValues: self . domValues, redirect: true )
139143 }
140144 }
@@ -193,7 +197,8 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
193197 var request = URLRequest ( url: originalURL)
194198 request. httpMethod = httpMethod
195199 request. httpBody = httpBody
196- let ( html, response) = try await deadRender ( for: request)
200+ let ( html, response) = try await deadRender ( for: request, domValues: self . domValues)
201+
197202 // update the URL if redirects happened.
198203 let url : URL
199204 if let responseURL = response. url {
@@ -209,27 +214,37 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
209214 } else {
210215 url = originalURL
211216 }
212-
217+
213218 let doc = try SwiftSoup . parse ( html, url. absoluteString, SwiftSoup . Parser. xmlParser ( ) . settings ( . init( true , true ) ) )
214- let domValues = try self . extractDOMValues ( doc)
219+ self . domValues = try self . extractDOMValues ( doc)
220+
215221 // extract the root layout, removing anything within the `<div data-phx-main>`.
216222 let mainDiv = try doc. select ( " div[data-phx-main] " ) [ 0 ]
217223 try mainDiv. replaceWith ( doc. createElement ( " phx-main " ) )
218- async let stylesheet = withThrowingTaskGroup ( of: ( Data, URLResponse) . self) { group in
224+
225+ self . rootLayout = try LiveViewNativeCore . Document. parse ( doc. outerHtml ( ) )
226+
227+ async let stylesheet = withThrowingTaskGroup ( of: Stylesheet< R> . self ) { group in
219228 for style in try doc. select ( " Style " ) {
220229 guard let url = URL ( string: try style. attr ( " url " ) , relativeTo: url)
221230 else { continue }
222- group. addTask { try await self . urlSession. data ( from: url) }
231+ group. addTask {
232+ if let cachedStylesheet = await StylesheetCache . shared. read ( for: url, registry: R . self) {
233+ return cachedStylesheet
234+ } else {
235+ let ( data, _) = try await self . urlSession. data ( from: url)
236+ guard let contents = String ( data: data, encoding: . utf8)
237+ else { return Stylesheet < R > ( content: [ ] , classes: [ : ] ) }
238+ let stylesheet = try Stylesheet < R > ( from: contents, in: . init( ) )
239+ await StylesheetCache . shared. write ( stylesheet, for: url, registry: R . self)
240+ return stylesheet
241+ }
242+ }
223243 }
224244 return try await group. reduce ( Stylesheet < R > ( content: [ ] , classes: [ : ] ) ) { result, next in
225- guard let contents = String ( data: next. 0 , encoding: . utf8)
226- else { return result }
227- return result. merge ( with: try Stylesheet < R > ( from: contents, in: . init( ) ) )
245+ return result. merge ( with: next)
228246 }
229247 }
230- self . rootLayout = try LiveViewNativeCore . Document. parse ( doc. outerHtml ( ) )
231-
232- self . domValues = domValues
233248
234249 if socket == nil {
235250 try await self . connectSocket ( domValues)
@@ -259,13 +274,20 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
259274 }
260275
261276 private func disconnect( preserveNavigationPath: Bool = false ) async {
262- for entry in self . navigationPath {
263- await entry. coordinator. disconnect ( )
264- if !preserveNavigationPath {
265- entry. coordinator. document = nil
277+ // disconnect all views
278+ await withTaskGroup ( of: Void . self) { group in
279+ for entry in self . navigationPath {
280+ group. addTask {
281+ await entry. coordinator. disconnect ( )
282+ }
266283 }
267284 }
285+ // reset all documents if navigation path is being reset.
268286 if !preserveNavigationPath {
287+ for entry in self . navigationPath {
288+ entry. coordinator. document = nil
289+ }
290+
269291 self . navigationPath = [ self . navigationPath. first!]
270292 }
271293 self . socket? . disconnect ( )
@@ -320,10 +342,16 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
320342 /// Request the dead render with the given `request`.
321343 ///
322344 /// Returns the dead render HTML and the HTTP response information (including the final URL after redirects).
323- func deadRender( for request: URLRequest ) async throws -> ( String , HTTPURLResponse ) {
345+ nonisolated func deadRender(
346+ for request: URLRequest ,
347+ domValues: DOMValues ?
348+ ) async throws -> ( String , HTTPURLResponse ) {
349+
324350 var request = request
325- request. url = request. url!. appendingLiveViewItems ( R . self)
326- if domValues != nil {
351+ request. url = request. url!. appendingLiveViewItems ( )
352+ request. allHTTPHeaderFields = configuration. headers
353+
354+ if let domValues {
327355 request. setValue ( domValues. phxCSRFToken, forHTTPHeaderField: " x-csrf-token " )
328356 }
329357
@@ -361,7 +389,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
361389 let liveReloadEnabled : Bool
362390 }
363391
364- private func extractLiveReloadFrame( _ doc: SwiftSoup . Document ) throws -> Bool {
392+ nonisolated private func extractLiveReloadFrame( _ doc: SwiftSoup . Document ) throws -> Bool {
365393 !( try doc. select ( " iframe[src= \" /phoenix/live_reload/frame \" ] " ) . isEmpty ( ) )
366394 }
367395
@@ -395,9 +423,12 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
395423 var wsEndpoint = URLComponents ( url: self . url, resolvingAgainstBaseURL: true ) !
396424 wsEndpoint. scheme = self . url. scheme == " https " ? " wss " : " ws "
397425 wsEndpoint. path = " /live/websocket "
426+ let configuration = self . urlSession. configuration
398427 let socket = Socket (
399428 endPoint: wsEndpoint. string!,
400- transport: { [ unowned self] in URLSessionTransport ( url: $0, configuration: self . urlSession. configuration) } ,
429+ transport: {
430+ URLSessionTransport ( url: $0, configuration: configuration)
431+ } ,
401432 paramsClosure: {
402433 [
403434 " _csrf_token " : domValues. phxCSRFToken,
@@ -479,11 +510,12 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
479510 } . receive ( " error " ) { msg in
480511 logger. debug ( " [LiveReload] error connecting to channel: \( msg. payload) " )
481512 }
482- self . liveReloadChannel!. on ( " assets_change " ) { [ unowned self] _ in
513+ self . liveReloadChannel!. on ( " assets_change " ) { [ weak self] _ in
483514 logger. debug ( " [LiveReload] assets changed, reloading " )
484515 Task {
516+ await StylesheetCache . shared. removeAll ( )
485517 // need to fully reconnect (rather than just re-join channel) because the elixir code reloader only triggers on http reqs
486- await self . reconnect ( )
518+ await self ? . reconnect ( )
487519 }
488520 }
489521 }
@@ -532,12 +564,12 @@ class LiveSessionURLSessionDelegate<R: RootRegistry>: NSObject, URLSessionTaskDe
532564 }
533565
534566 var newRequest = request
535- newRequest. url = await url. appendingLiveViewItems ( R . self )
567+ newRequest. url = await url. appendingLiveViewItems ( )
536568 return newRequest
537569 }
538570}
539571
540- extension LiveSessionCoordinator {
572+ enum LiveSessionParameters {
541573 static var platform : String { " swiftui " }
542574 static var platformParams : [ String : Any ] {
543575 [
@@ -639,18 +671,8 @@ extension LiveSessionCoordinator {
639671 " time_zone " : TimeZone . autoupdatingCurrent. identifier,
640672 ]
641673 }
642- }
643-
644- fileprivate extension URL {
645- @MainActor
646- func appendingLiveViewItems< R: RootRegistry > ( _: R . Type = R . self) -> Self {
647- var result = self
648- let components = URLComponents ( url: self , resolvingAgainstBaseURL: false )
649- if !( components? . queryItems? . contains ( where: { $0. name == " _format " } ) ?? false ) {
650- result. append ( queryItems: [
651- . init( name: " _format " , value: LiveSessionCoordinator< R> . platform)
652- ] )
653- }
674+
675+ static var queryItems : [ URLQueryItem ] = {
654676 /// Create a nested structure of query items.
655677 ///
656678 /// `_root[key][nested_key]=value`
@@ -665,12 +687,24 @@ fileprivate extension URL {
665687 }
666688 }
667689 }
668- for queryItem in queryParameters ( for : LiveSessionCoordinator < R > . platformParams ) {
669- let name = " _interface \( queryItem . name ) "
670- if ! ( components ? . queryItems ? . contains ( where : { $0 . name == name } ) ?? false ) {
671- result . append ( queryItems : [ . init ( name: name , value: queryItem. value) ] )
690+
691+ return queryParameters ( for : platformParams )
692+ . map { queryItem in
693+ URLQueryItem ( name : " _interface \( queryItem . name) " , value: queryItem. value)
672694 }
673- }
695+ + [ . init( name: " _format " , value: platform) ]
696+ } ( )
697+ }
698+
699+ fileprivate extension URL {
700+ nonisolated func appendingLiveViewItems( ) -> Self {
701+ var result = self
702+ let components = URLComponents ( url: self , resolvingAgainstBaseURL: false )
703+ let existingQueryItems = ( components? . queryItems ?? [ ] ) . reduce ( into: Set < String > ( ) ) { $0. insert ( $1) }
704+ result. append (
705+ queryItems: LiveSessionParameters . queryItems
706+ . filter ( { !existingQueryItems. contains ( $0. name) } )
707+ )
674708 return result
675709 }
676710}
0 commit comments