Skip to content

Use core to parse json fragments #1152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 36 additions & 39 deletions Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// LiveViewCoordinator.swift
//
//
//
// Created by Carson Katri on 1/6/23.
//
Expand All @@ -27,31 +27,31 @@ private let logger = Logger(subsystem: "LiveViewNative", category: "LiveViewCoor
@MainActor
public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
@Published internal private(set) var internalState: LiveSessionState = .notConnected

var state: LiveSessionState {
internalState
}

let session: LiveSessionCoordinator<R>
var url: URL

private var channel: Channel?

@Published var document: LiveViewNativeCore.Document?
let elementChanged = PassthroughSubject<NodeRef, Never>()
private var rendered: Root!
internal let builder = ViewTreeBuilder<R>()

private var currentConnectionToken: ConnectionAttemptToken?
private var currentConnectionTask: Task<Void, Error>?

private(set) internal var eventSubject = PassthroughSubject<(String, Payload), Never>()
private(set) internal var eventHandlers = Set<AnyCancellable>()

init(session: LiveSessionCoordinator<R>, url: URL) {
self.session = session
self.url = url

self.handleEvent("native_redirect") { [weak self] payload in
guard let self,
let redirect = LiveRedirect(from: payload, relativeTo: self.url)
Expand All @@ -61,7 +61,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
}
}
}

/// Pushes a LiveView event with the given name and payload to the server.
///
/// This is an asynchronous function that completes when the server finishes processing the event and returns a response.
Expand All @@ -82,7 +82,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
"cid": target as Any
])
}

public func pushEvent(type: String, event: String, value: some Encodable, target: Int? = nil) async throws {
try await pushEvent(
type: type,
Expand All @@ -91,12 +91,12 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
target: target
)
}

internal func doPushEvent(_ event: String, payload: Payload) async throws {
guard let channel = channel else {
return
}

let token = self.currentConnectionToken

let replyPayload = try await withCheckedThrowingContinuation({ [weak channel] continuation in
Expand All @@ -108,12 +108,12 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
continuation.resume(throwing: LiveConnectionError.eventError(message))
}
})

guard currentConnectionToken === token else {
// TODO: maybe this should just silently fail?
throw CancellationError()
}

if let diffPayload = replyPayload["diff"] as? Payload {
try self.handleDiff(payload: diffPayload, baseURL: self.url)
} else if session.configuration.navigationMode.permitsRedirects,
Expand All @@ -126,7 +126,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
try await session.redirect(.init(kind: .push, to: destination, mode: .replaceTop))
}
}

/// Creates a publisher that can be used to listen for server-sent LiveView events.
///
/// - Parameter event: The event name that is being listened for.
Expand Down Expand Up @@ -172,7 +172,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
.filter { $0.0 == event }
.map(\.1)
}

/// Permanently registers a handler for a server-sent LiveView event.
///
/// - Parameter event: The event name that is being listened for.
Expand All @@ -185,14 +185,12 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
.sink(receiveValue: handler)
.store(in: &eventHandlers)
}

private func handleDiff(payload: Payload, baseURL: URL) throws {
handleEvents(payload: payload)
let diff = try RootDiff(from: FragmentDecoder(data: payload))
self.rendered = try self.rendered.merge(with: diff)
self.document?.merge(with: try Document.parse(self.rendered.buildString()))
try self.document?.merge_fragment_json(payload: payload)
}

private func handleEvents(payload: Payload) {
guard let events = payload["e"] as? [[Any]] else {
return
Expand All @@ -205,19 +203,19 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
eventSubject.send((name, value))
}
}

private nonisolated func parseDOM(html: String, baseURL: URL) throws -> Elements {
let document = try Parser.htmlParser().parseInput(html, baseURL.absoluteString)
return document.children()
}

func connect(domValues: LiveSessionCoordinator<R>.DOMValues, redirect: Bool) async throws {
await self.disconnect()

self.internalState = .connecting

guard let socket = session.socket else { return }

var connectParams = session.configuration.connectParams?(self.url) ?? [:]
connectParams["_mounts"] = 0
connectParams["_csrf_token"] = domValues.phxCSRFToken
Expand All @@ -230,7 +228,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
(redirect ? "redirect": "url"): self.url.absoluteString,
"params": connectParams,
]

let channel = socket.channel("lv:\(domValues.phxID)", params: params)
self.channel = channel
channel.onError { message in
Expand Down Expand Up @@ -277,7 +275,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
self?.internalState = .notConnected
}
}

let joinTask = Task {
for try await joinResult in join(channel: channel) {
switch joinResult {
Expand All @@ -292,16 +290,16 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
self.url = liveRedirect.to
try await self.connect(domValues: domValues, redirect: true)
}

self.internalState = .connected
}
}

channel.onClose { _ in
joinTask.cancel()
}
}

func disconnect() async {
if let channel,
!channel.isClosed
Expand All @@ -319,7 +317,7 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
self?.document = nil
}
}

enum JoinResult {
case rendered(Payload)
case redirect(LiveRedirect)
Expand Down Expand Up @@ -349,15 +347,15 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
continuation.finish(throwing: LiveConnectionError.viewCoordinatorReleased)
return
}

Task { [weak self] in
guard let self else {
continuation.finish(throwing: LiveConnectionError.viewCoordinatorReleased)
return
}

await self.disconnect()

if self.session.configuration.navigationMode.permitsRedirects,
let redirect = (message.payload["live_redirect"] as? Payload).flatMap({ LiveRedirect(from: $0, relativeTo: self.url) }) {
continuation.yield(.redirect(redirect))
Expand All @@ -374,11 +372,10 @@ public class LiveViewCoordinator<R: RootRegistry>: ObservableObject {
}
}
}

private func handleJoinPayload(renderedPayload: Payload) {
// todo: what should happen if decoding or parsing fails?
self.rendered = try! Root(from: FragmentDecoder(data: renderedPayload))
self.document = try! LiveViewNativeCore.Document.parse(rendered.buildString())
self.document = try! LiveViewNativeCore.Document.parse_fragment_json(payload: renderedPayload)
self.document?.on(.changed) { [unowned self] doc, nodeRef in
switch doc[nodeRef].data {
case .root:
Expand Down