Skip to content

Client refactor 0.4.1-rc-3 #1560

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
.package(url: "https://github.com/liveview-native/liveview-native-core", exact: "0.4.1-rc-2"),
.package(url: "https://github.com/liveview-native/liveview-native-core", exact: "0.4.1-rc-3"),

.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),

Expand Down
281 changes: 120 additions & 161 deletions Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
@Published private(set) var rootLayout: LiveViewNativeCore.Document?
@Published private(set) var stylesheet: Stylesheet<R>?

// Socket connection
var liveSocket: LiveViewNativeCore.LiveSocket?
var socket: LiveViewNativeCore.Socket?
private var persistence: SimplePersistentStore
private var eventHandler: SimpleEventHandler
private var patchHandler: SimplePatchHandler
private var navHandler: SimpleNavHandler

private var liveviewClient: LiveViewClient?
private var builder: LiveViewClientBuilder

private var liveReloadChannel: LiveViewNativeCore.LiveChannel?
private var liveReloadListenerLoop: Task<(), any Error>?

private var cancellables = Set<AnyCancellable>()
Expand All @@ -63,7 +66,14 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
private var eventSubject = PassthroughSubject<(LiveViewCoordinator<R>, (String, Json)), Never>()
private var eventHandlers = Set<AnyCancellable>()

private var reconnectAttempts = 0
deinit {
let client = liveviewClient
Task { @MainActor in
if let client {
client.shutdown()
}
}
}

/// Positions for `<ScrollView>` elements with an explicit ID.
///
Expand All @@ -85,6 +95,10 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
public convenience init(_ host: some LiveViewHost, config: LiveSessionConfiguration = .init(), customRegistryType: R.Type = R.self) {
self.init(host.url, config: config, customRegistryType: customRegistryType)
}

public func status() -> LiveViewClientStatus {
(try? self.liveviewClient?.status()) ?? .connecting
}

/// Creates a new coordinator with a custom registry.
/// - Parameter url: The URL of the page to establish the connection to.
Expand All @@ -95,6 +109,51 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {

self.configuration = config

let adapter = ReconnectStrategyAdapter(self.configuration.reconnectBehavior)

self.patchHandler = SimplePatchHandler()
self.eventHandler = SimpleEventHandler()
self.navHandler = SimpleNavHandler()
self.persistence = SimplePersistentStore()

self.builder = LiveViewClientBuilder();

self.builder.setPatchHandler(patchHandler)
self.builder.setNavigationHandler(navHandler)
self.builder.setPersistenceProvider(persistence)
self.builder.setLiveChannelEventHandler(eventHandler)
self.builder.setLogLevel(.debug)
self.builder.setSocketReconnectStrategy(adapter)

self.eventHandler.viewReloadSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] newView in
guard let self else { return }
guard let last = self.navigationPath.last else { return }
switch newView {
case .disconnected:
self.state = .disconnected
case .connecting:
self.state = .connecting
case .reconnecting:
self.state = .reconnecting
case .connected(channelStatus: let channelStatus):
switch channelStatus {
case .connected(let doc):
self.state = .connected
case .reconnecting:
self.state = .reconnecting
}
case .error(error: let error):
self.state = .connectionFailed(error)
}
if stylesheet != nil && rootLayout != nil {
if let client = self.liveviewClient {
last.coordinator.join(client, newView, self.patchHandler)
}
}
}.store(in: &cancellables)

// load cookies into core
for cookie in HTTPCookieStorage.shared.cookies(for: url) ?? [] {
try? LiveViewNativeCore.storeSessionCookie("\(cookie.name)=\(cookie.value)", self.url.absoluteString)
Expand All @@ -111,38 +170,27 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {

$navigationPath.scan(([LiveNavigationEntry<R>](), [LiveNavigationEntry<R>]()), { ($0.1, $1) }).sink { [weak self] prev, next in
guard let self else { return }
guard let client = liveviewClient else { return }
Task {
try await prev.last?.coordinator.disconnect()
prev.last?.coordinator.disconnect()
if prev.count > next.count {
let targetEntry = self.liveSocket!.getEntries()[next.count - 1]
next.last?.coordinator.join(
try await self.liveSocket!.traverseTo(targetEntry.id,
.some([
"_format": .str(string: LiveSessionParameters.platform),
"_interface": .object(object: LiveSessionParameters.platformParams)
]),
nil)
)

var opts = NavActionOptions()
opts.joinParams = .some([ "_interface": .object(object: LiveSessionParameters.platformParams)])
let targetEntry = client.getEntries()[next.count - 1]
let _ = try await client.traverseTo(targetEntry.id, opts)

} else if next.count > prev.count && prev.count > 0 {
// forward navigation (from `redirect` or `<NavigationLink>`)
next.last?.coordinator.join(
try await self.liveSocket!.navigate(next.last!.url.absoluteString,
.some([
"_format": .str(string: LiveSessionParameters.platform),
"_interface": .object(object: LiveSessionParameters.platformParams)
]),
NavOptions(action: .push))
)
var opts = NavOptions()
opts.joinParams = .some([ "_interface": .object(object: LiveSessionParameters.platformParams)])
opts.action = .push
let _ = try await client.navigate(next.last!.url.absoluteString, opts)
} else if next.count == prev.count {
guard let liveChannel =
try await self.liveSocket?.navigate(next.last!.url.absoluteString,
.some([
"_format": .str(string: LiveSessionParameters.platform),
"_interface": .object(object: LiveSessionParameters.platformParams)
]),
NavOptions(action: .replace))
else { return }
next.last?.coordinator.join(liveChannel)
var opts = NavOptions()
opts.joinParams = .some([ "_interface": .object(object: LiveSessionParameters.platformParams)])
opts.action = .replace
let _ = try await client.navigate(next.last!.url.absoluteString, opts)
}
}
}.store(in: &cancellables)
Expand Down Expand Up @@ -171,19 +219,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
self.init(url, config: config, customRegistryType: EmptyRegistry.self)
}

deinit {
let socket = socket
let liveReloadChannel = liveReloadChannel
Task {
do {
try await socket?.shutdown()
}
do {
try await liveReloadChannel?.shutdownParentSocket()
}
}
}


/// Connects this coordinator to the LiveView channel.
///
/// You generally do not call this function yourself. It is called automatically when the ``LiveView`` appears.
Expand All @@ -195,6 +231,7 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
/// - Parameter httpMethod: The HTTP method to use for the dead render. Defaults to `GET`.
/// - Parameter httpBody: The HTTP body to send when requesting the dead render.
public func connect(httpMethod: String? = nil, httpBody: Data? = nil, additionalHeaders: [String: String]? = nil) async {

do {
switch state {
case .setup, .disconnected, .connectionFailed:
Expand All @@ -212,37 +249,21 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
let headers = (configuration.headers ?? [:])
.merging(additionalHeaders ?? [:]) { $1 }

if let socket {
try await socket.shutdown()
}

let adapter = ReconnectStrategyAdapter(self.configuration.reconnectBehavior)

self.liveSocket = try await LiveSocket(
originalURL.absoluteString,
LiveSessionParameters.platform,
ConnectOpts(
headers: headers,
body: httpBody,
method: httpMethod.flatMap(Method.init(_:)),
timeoutMs: 10_000
),
adapter
)

// save cookies to storage
HTTPCookieStorage.shared.setCookies(
(self.liveSocket!.joinHeaders()["set-cookie"] ?? []).flatMap {
HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": $0], for: URL(string: self.liveSocket!.joinUrl())!)
},
for: self.url,
mainDocumentURL: nil
let opts = ClientConnectOpts(
joinParams: .some([ "_interface": .object(object: LiveSessionParameters.platformParams)]),
headers: .some(headers),
method: Method.init(httpMethod ?? "Get"),
requestBody: httpBody
)

self.socket = self.liveSocket?.socket()

self.rootLayout = self.liveSocket!.deadRender()
let styleURLs = self.liveSocket!.styleUrls()

if let client = self.liveviewClient {
try await client.reconnect(originalURL.absoluteString, opts)
} else {
self.liveviewClient = try await self.builder.connect(originalURL.absoluteString, opts)
}

self.rootLayout = try self.liveviewClient!.deadRender()
let styleURLs = try self.liveviewClient!.styleUrls()

self.stylesheet = try await withThrowingTaskGroup(of: Stylesheet<R>.self) { @Sendable group in
for style in styleURLs {
Expand All @@ -266,77 +287,19 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
}
}

try await self.joinLiveViewChannel()

self.state = .connected

if let liveReloadChannel {
try await liveReloadChannel.shutdownParentSocket()
self.liveReloadChannel = nil
}

if self.liveSocket!.hasLiveReload() {
self.liveReloadChannel = try await self.liveSocket!.joinLivereloadChannel()
bindLiveReloadListener()
}
self.navigationPath.last!.coordinator.join(self.liveviewClient!, self.liveviewClient!.status(), self.patchHandler)

} catch {
self.rootLayout = nil
self.stylesheet = nil
self.state = .connectionFailed(error)
}
}

func overrideLiveReloadChannel(channel: LiveChannel) async throws {

if let liveReloadChannel {
try await liveReloadChannel.shutdownParentSocket()
self.liveReloadChannel = nil
}

self.liveReloadChannel = channel
self.bindLiveReloadListener()
}

func bindLiveReloadListener() {
let eventListener = self.liveReloadChannel!.channel().events()
self.liveReloadListenerLoop = Task { @MainActor [weak self] in
while !Task.isCancelled {
let event = try await eventListener.event()
guard let self else { return }
let currentTime = Date()

guard currentTime.timeIntervalSince(lastReloadTime) >= debounceTime else {
continue
}

if case .user(user: "assets_change") = event.event {
Task { @MainActor in
await self.disconnect()
self.navigationPath = [.init(url: self.url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)]
await self.connect()
self.lastReloadTime = Date()
}
return
}
}

}
}

func joinLiveViewChannel() async throws {
let liveChannel = try await self.liveSocket!.joinLiveviewChannel(
.some([
"_format": .str(string: LiveSessionParameters.platform),
"_interface": .object(object: LiveSessionParameters.platformParams)
]),
nil
)

self.navigationPath.last?.coordinator.join(liveChannel)
}

private func disconnect(preserveNavigationPath: Bool = false) async {
do {
for entry in self.navigationPath {
try await entry.coordinator.disconnect()
entry.coordinator.disconnect()
if !preserveNavigationPath {
entry.coordinator.document = nil
}
Expand All @@ -350,19 +313,17 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
self.navigationPath = [self.navigationPath.first!]
}

if self.liveReloadChannel?.channel().status() == .joined {
try await self.liveReloadChannel?.shutdownParentSocket()
}


if let liveReloadListenerLoop {
liveReloadListenerLoop.cancel()
}

self.liveReloadChannel = nil

try await self.socket?.shutdown()
self.socket = nil
self.liveSocket = nil
if let client = self.liveviewClient {
try await client.disconnect()
}

self.state = .disconnected
} catch {
self.state = .connectionFailed(error)
Expand All @@ -381,22 +342,6 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
self.navigationPath = [.init(url: self.url, coordinator: self.navigationPath.first!.coordinator, navigationTransition: nil, pendingView: nil)]
}
await self.connect(httpMethod: httpMethod, httpBody: httpBody, additionalHeaders: headers)
// do {
// if let url {
// try await self.disconnect(preserveNavigationPath: false)
// self.url = url
// self.navigationPath = [.init(url: self.url, coordinator: self.navigationPath.first!.coordinator, navigationTransition: nil, pendingView: nil)]
// } else {
// // preserve the navigation path, but still clear the stale documents, since they're being completely replaced.
// try await self.disconnect(preserveNavigationPath: true)
// for entry in self.navigationPath {
// entry.coordinator.document = nil
// }
// }
// try await self.connect(httpMethod: httpMethod, httpBody: httpBody, additionalHeaders: headers)
// } catch {
// self.state = .connectionFailed(error)
// }
}

/// Creates a publisher that can be used to listen for server-sent LiveView events.
Expand Down Expand Up @@ -436,6 +381,18 @@ public class LiveSessionCoordinator<R: RootRegistry>: ObservableObject {
.store(in: &eventHandlers)
}

public func postFormData(
url: Url,
formData: [String: String]
) async throws {
if let client = self.liveviewClient {
try await client.postForm(url.absoluteString,
formData,
.some([ "_interface": .object(object: LiveSessionParameters.platformParams)]),
nil)
}
}

func redirect(
_ redirect: LiveRedirect,
navigationTransition: Any? = nil,
Expand Down Expand Up @@ -619,3 +576,5 @@ fileprivate extension URL {

extension Socket: @unchecked Sendable {}
extension Channel: @unchecked Sendable {}
extension LiveViewClient: @unchecked Sendable {}
extension LiveViewClientBuilder: @unchecked Sendable {}
Loading
Loading