From 209d8424e2dd0474a1d0c2451f374f56f8d7311a Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Fri, 3 Jan 2025 20:56:51 -0800 Subject: [PATCH 1/6] use previous channel --- .../Coordinators/LiveSessionCoordinator.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index ab7632123..35d15182b 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -107,20 +107,21 @@ public class LiveSessionCoordinator: ObservableObject { $navigationPath.scan(([LiveNavigationEntry](), [LiveNavigationEntry]()), { ($0.1, $1) }).sink { [weak self] prev, next in guard let self else { return } Task { + let last_channel = prev.last?.coordinator.liveChannel! try await prev.last?.coordinator.disconnect() if prev.count > next.count { // back navigation (we could be going back multiple pages at once, so use `traverseTo` instead of `back`) let targetEntry = self.liveSocket!.getEntries()[next.count - 1] next.last?.coordinator.join( - try await self.liveSocket!.traverseTo(targetEntry.id, next.last!.coordinator.liveChannel, nil) + try await self.liveSocket!.traverseTo(targetEntry.id, last_channel, nil) ) } else if next.count > prev.count && prev.count > 0 { // forward navigation (from `redirect` or ``) next.last?.coordinator.join( - try await self.liveSocket!.navigate(next.last!.url.absoluteString, next.last!.coordinator.liveChannel, NavOptions(action: .push)) + try await self.liveSocket!.navigate(next.last!.url.absoluteString, last_channel, NavOptions(action: .push)) ) } else if next.count == prev.count { - guard let liveChannel = try await self.liveSocket?.navigate(next.last!.url.absoluteString, next.last!.coordinator.liveChannel, NavOptions(action: .replace)) + guard let liveChannel = try await self.liveSocket?.navigate(next.last!.url.absoluteString, last_channel, NavOptions(action: .replace)) else { return } next.last?.coordinator.join(liveChannel) } From c9fc1c15dbec87927ffcd1bb6765492125014b05 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Wed, 29 Jan 2025 14:10:20 -0800 Subject: [PATCH 2/6] refactor support changes --- .../Coordinators/LiveSessionCoordinator.swift | 241 +++++++++++------ .../Coordinators/LiveViewCoordinator.swift | 251 +++++++++++------- Sources/LiveViewNative/Live/LiveView.swift | 2 +- Sources/LiveViewNative/ViewModel.swift | 72 ++--- 4 files changed, 349 insertions(+), 217 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 35d15182b..089c55d76 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -45,8 +45,8 @@ public class LiveSessionCoordinator: ObservableObject { @Published private(set) var stylesheet: Stylesheet? // Socket connection - var liveSocket: LiveViewNativeCore.LiveSocket? - var socket: LiveViewNativeCore.Socket? + //var liveSocket: LiveViewNativeCore.LiveSocket? + //var socket: LiveViewNativeCore.Socket? private var liveReloadChannel: LiveViewNativeCore.LiveChannel? private var liveReloadListener: Channel.EventStream? @@ -60,6 +60,16 @@ public class LiveSessionCoordinator: ObservableObject { private var reconnectAttempts = 0 + // TODO: Once this works sub out the rest + private var persistence: SimpleStore? + private var eventHandler: SimpleEventHandler + private var patchHandler: SimplePatchHandler + + private var liveviewClient: LiveViewClient? + private var builder: LiveViewClientBuilder + + + /// Positions for `` elements with an explicit ID. /// /// These positions are used for scroll restoration on back navigation. @@ -80,7 +90,15 @@ public class LiveSessionCoordinator: 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 clientChannel() -> LiveViewClientChannel? { + self.liveviewClient?.channel() + } + + public func status() -> SocketStatus { + (try? self.liveviewClient?.status()) ?? .disconnected + } + /// Creates a new coordinator with a custom registry. /// - Parameter url: The URL of the page to establish the connection to. /// - Parameter config: The configuration for this coordinator. @@ -88,12 +106,24 @@ public class LiveSessionCoordinator: ObservableObject { public init(_ url: URL, config: LiveSessionConfiguration = .init(), customRegistryType _: R.Type = R.self) { self.url = url.appending(path: "").absoluteURL + + self.patchHandler = SimplePatchHandler({ _, _, _ in }) + self.eventHandler = SimpleEventHandler( + event_callback: { _ in }, + chan_status_callback: { _ in }, + sock_status_callback: { _ in }, + change_callback: { _, _, _, _ in } + ) + + self.builder = LiveViewClientBuilder(); + + self.builder.setPatchHandler(patchHandler) +// self.builder.setPersistenceProvider(persistence) + self.builder.setLiveChannelEventHandler(eventHandler) + self.builder.setLogLevel(.debug) + self.configuration = config - // load cookies into core - for cookie in HTTPCookieStorage.shared.cookies(for: url) ?? [] { - try? LiveViewNativeCore.storeSessionCookie("\(cookie.name)=\(cookie.value)", self.url.absoluteString) - } self.navigationPath = [.init(url: url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)] @@ -103,28 +133,49 @@ public class LiveSessionCoordinator: ObservableObject { .sink(receiveValue: { [weak self] value in self?.eventSubject.send(value) }) + + 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 } + if let client = self.liveviewClient { + last.coordinator.join(client, self.eventHandler, self.patchHandler) + } + }.store(in: &cancellables) $navigationPath.scan(([LiveNavigationEntry](), [LiveNavigationEntry]()), { ($0.1, $1) }).sink { [weak self] prev, next in guard let self else { return } Task { - let last_channel = prev.last?.coordinator.liveChannel! - try await prev.last?.coordinator.disconnect() - if prev.count > next.count { - // back navigation (we could be going back multiple pages at once, so use `traverseTo` instead of `back`) - let targetEntry = self.liveSocket!.getEntries()[next.count - 1] - next.last?.coordinator.join( - try await self.liveSocket!.traverseTo(targetEntry.id, last_channel, nil) - ) - } else if next.count > prev.count && prev.count > 0 { - // forward navigation (from `redirect` or ``) - next.last?.coordinator.join( - try await self.liveSocket!.navigate(next.last!.url.absoluteString, last_channel, NavOptions(action: .push)) - ) - } else if next.count == prev.count { - guard let liveChannel = try await self.liveSocket?.navigate(next.last!.url.absoluteString, last_channel, NavOptions(action: .replace)) - else { return } - next.last?.coordinator.join(liveChannel) - } + + //working nav code + let next_url = next.last!.url.absoluteString +// try await prev.last?.coordinator.disconnect() + var opts = NavOptions() + opts.joinParams = .some([ "_interface": .object(object: LiveSessionParameters.platformParams)]) + if let client = self.liveviewClient { + try await client.navigate(next_url, opts) + } + + //next.last?.coordinator.join(ch) +// let last_channel = prev.last?.coordinator.liveChannel +// try await prev.last?.coordinator.disconnect() +// if prev.count > next.count { +// // back navigation (we could be going back multiple pages at once, so use `traverseTo` instead of `back`) +// let targetEntry = self.liveSocket!.getEntries()[next.count - 1] +// next.last?.coordinator.join( +// try await self.liveSocket!.traverseTo(targetEntry.id, last_channel!, nil) +// ) +// } else if next.count > prev.count && prev.count > 0 { +// // forward navigation (from `redirect` or ``) +// next.last?.coordinator.join( +// try await self.liveSocket!.navigate(next.last!.url.absoluteString, last_channel!, NavOptions(action: .push)) +// ) +// } else if next.count == prev.count { +// guard let liveChannel = try await self.liveSocket?.navigate(next.last!.url.absoluteString, last_channel!, NavOptions(action: .replace)) +// else { return } +// next.last?.coordinator.join(liveChannel) +// } } }.store(in: &cancellables) // Receive events from all live views. @@ -183,31 +234,38 @@ public class LiveSessionCoordinator: ObservableObject { let headers = (configuration.headers ?? [:]) .merging(additionalHeaders ?? [:]) { $1 } - - self.liveSocket = try await LiveSocket( - originalURL.absoluteString, - LiveSessionParameters.platform, - ConnectOpts( - headers: headers, - body: httpBody.flatMap({ String(data: $0, encoding: .utf8) }), - method: httpMethod.flatMap(Method.init(_:)), - timeoutMs: 10_000 - ) + + let opts = ClientConnectOpts( + joinParams: .some([ "_interface": .object(object: LiveSessionParameters.platformParams)]), + headers: .some(headers), + method: Method.init(httpMethod ?? "Get"), + requestBody: httpBody ) - // 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 - ) + if let client = self.liveviewClient { + try await client.reconnect(originalURL.absoluteString, opts) + } else { + self.liveviewClient = try await self.builder.connect(originalURL.absoluteString, opts) + self.navigationPath.last!.coordinator.join(self.liveviewClient!, self.eventHandler, self.patchHandler) + } + + +// self.liveSocket = try await LiveSocket( +// originalURL.absoluteString, +// LiveSessionParameters.platform, +// ConnectOpts( +// headers: headers, +// body: httpBody.flatMap({ String(data: $0, encoding: .utf8) }), +// method: httpMethod.flatMap(Method.init(_:)), +// timeoutMs: 10_000 +// ) +// ) +// - self.socket = self.liveSocket?.socket() + //self.socket = self.liveSocket?.socket() - self.rootLayout = self.liveSocket!.deadRender() - let styleURLs = self.liveSocket!.styleUrls() + self.rootLayout = try self.liveviewClient!.deadRender() + let styleURLs = try self.liveviewClient!.styleUrls() self.stylesheet = try await withThrowingTaskGroup(of: Stylesheet.self) { @Sendable group in for style in styleURLs { @@ -230,44 +288,44 @@ public class LiveSessionCoordinator: ObservableObject { } } - let liveChannel = try await self.liveSocket!.joinLiveviewChannel( - .some([ - "_format": .str(string: LiveSessionParameters.platform), - "_interface": .object(object: LiveSessionParameters.platformParams) - ]), - nil - ) +// 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) self.state = .connected - if self.liveSocket!.hasLiveReload() { - self.liveReloadChannel = try await self.liveSocket!.joinLivereloadChannel() - bindLiveReloadListener() - } +// if let liveReloadChannel = try self.liveviewClient?.liveReloadChannel() { +// self.liveReloadChannel = liveReloadChannel +// bindLiveReloadListener() +// } + } catch { self.state = .connectionFailed(error) } } - func bindLiveReloadListener() { - let eventListener = self.liveReloadChannel!.channel().eventStream() - self.liveReloadListener = eventListener - self.liveReloadListenerLoop = Task { @MainActor [weak self] in - for try await event in eventListener { - guard let self else { return } - switch event.event { - case .user(user: "assets_change"): - try await self.disconnect() - self.navigationPath = [.init(url: self.url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)] - try await self.connect() - default: - continue - } - } - } - } +// func bindLiveReloadListener() { +// let eventListener = self.liveReloadChannel!.channel().eventStream() +// self.liveReloadListener = eventListener +// self.liveReloadListenerLoop = Task { @MainActor [weak self] in +// for try await event in eventListener { +// guard let self else { return } +// switch event.event { +// case .user(user: "assets_change"): +// await self.disconnect() +// self.navigationPath = [.init(url: self.url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)] +// await self.connect() +// default: +// continue +// } +// } +// } +// } private func disconnect(preserveNavigationPath: Bool = false) async { do { @@ -285,11 +343,14 @@ public class LiveSessionCoordinator: ObservableObject { self.navigationPath = [self.navigationPath.first!] } - try await self.liveReloadChannel?.channel().leave() - self.liveReloadChannel = nil - try await self.socket?.disconnect() - self.socket = nil - self.liveSocket = nil + //try await self.liveReloadChannel?.channel().leave() + //self.liveReloadChannel = nil + //try await self.socket?.disconnect() + //self.socket = nil + //self.liveSocket = nil + if let client = self.liveviewClient { + try await client.disconnect() + } self.state = .disconnected } catch { self.state = .connectionFailed(error) @@ -307,7 +368,7 @@ public class LiveSessionCoordinator: ObservableObject { self.url = url self.navigationPath = [.init(url: self.url, coordinator: self.navigationPath.first!.coordinator, navigationTransition: nil, pendingView: nil)] } - try await self.connect(httpMethod: httpMethod, httpBody: httpBody, additionalHeaders: headers) + await self.connect(httpMethod: httpMethod, httpBody: httpBody, additionalHeaders: headers) // do { // if let url { // try await self.disconnect(preserveNavigationPath: false) @@ -354,6 +415,22 @@ public class LiveSessionCoordinator: 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, @@ -537,3 +614,5 @@ fileprivate extension URL { extension Socket: @unchecked Sendable {} extension Channel: @unchecked Sendable {} +extension LiveViewClient: @unchecked Sendable {} +extension LiveViewClientBuilder: @unchecked Sendable {} diff --git a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift index 6bc59ca9e..fa94f826e 100644 --- a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift @@ -33,17 +33,21 @@ public class LiveViewCoordinator: ObservableObject { @_spi(LiveForm) public private(set) weak var session: LiveSessionCoordinator! - var url: URL + private var liveviewClient: LiveViewClient? + private var channel: LiveViewClientChannel? - private(set) var liveChannel: LiveViewNativeCore.LiveChannel? - private var channel: LiveViewNativeCore.Channel? + var url: URL + // private(set) var liveChannel: LiveViewNativeCore.LiveChannel? + // private var channel: LiveViewNativeCore.Channel? + @Published var document: LiveViewNativeCore.Document? { didSet { elementChangedSubjects.removeAll() uploadRef = 0 } } + private var elementChangedSubjects = [NodeRef:ObjectWillChangePublisher]() func elementChanged(_ ref: NodeRef) -> ObjectWillChangePublisher { guard let subject = elementChangedSubjects[ref] else { @@ -59,9 +63,9 @@ public class LiveViewCoordinator: ObservableObject { private(set) internal var eventHandlers = Set() // private var eventListener: Channel.EventStream? - private var eventListenerLoop: Task<(), any Error>? + // private var eventListenerLoop: Task<(), any Error>? // private var statusListener: Channel.StatusStream? - private var statusListenerLoop: Task<(), any Error>? + //private var statusListenerLoop: Task<(), any Error>? private(set) internal var liveViewModel = LiveViewModel() @@ -78,8 +82,8 @@ public class LiveViewCoordinator: ObservableObject { } deinit { - self.eventListenerLoop?.cancel() - self.statusListenerLoop?.cancel() + //self.eventListenerLoop?.cancel() + //self.statusListenerLoop?.cancel() } /// Pushes a LiveView event with the given name and payload to the server. @@ -114,17 +118,16 @@ public class LiveViewCoordinator: ObservableObject { @discardableResult internal func doPushEvent(_ event: String, payload: LiveViewNativeCore.Payload) async throws -> [String:Any]? { - guard let channel = channel else { - return nil - } - guard case .joined = channel.status() else { + guard case .connected = state else { throw LiveSocketError.DisconnectionError } - let replyPayload = try await channel.call(event: .user(user: event), payload: payload, timeout: PUSH_TIMEOUT) - - return try await handleEventReplyPayload(replyPayload) + if let replyPayload = try await channel?.call(event, payload) { + return try await handleEventReplyPayload(replyPayload) + } else { + return nil + } } /// Creates a publisher that can be used to listen for server-sent LiveView events. @@ -186,10 +189,10 @@ public class LiveViewCoordinator: ObservableObject { .store(in: &eventHandlers) } - private func handleDiff(payload: LiveViewNativeCore.Json, baseURL: URL) throws { - handleEvents(payload) - try self.document?.mergeFragmentJson(String(data: try JSONEncoder().encode(payload), encoding: .utf8)!) - } +// private func handleDiff(payload: LiveViewNativeCore.Json, baseURL: URL) throws { +// handleEvents(payload) +// try self.document?.mergeFragmentJson(String(data: try JSONEncoder().encode(payload), encoding: .utf8)!) +// } func handleEventReplyPayload(_ replyPayload: LiveViewNativeCore.Payload) async throws -> [String:Any]? { switch replyPayload { @@ -197,14 +200,16 @@ public class LiveViewCoordinator: ObservableObject { switch json { case let .object(object): if case let .object(diff) = object["diff"] { - try self.handleDiff(payload: .object(object: diff), baseURL: self.url) + //try self.handleDiff(payload: .object(object: diff), baseURL: self.url) if case let .object(reply) = diff["r"] { return reply } + //TODO: remove this and move it inside the client } else if case let .object(redirectObject) = object["live_redirect"], let redirect = LiveRedirect(from: redirectObject, relativeTo: self.url) { try await session.redirect(redirect) + //TODO: remove this and move it inside the client } else if case let .object(redirectObject) = object["redirect"], case let .str(destinationString) = redirectObject["to"], let destination = URL(string: destinationString, relativeTo: self.url) @@ -237,98 +242,140 @@ public class LiveViewCoordinator: ObservableObject { } } - func bindEventListener() { - self.eventListenerLoop = Task { [weak self, weak channel] in - guard let channel else { return } - let eventListener = channel.eventStream() - for try await event in eventListener { - guard let self else { return } - guard !Task.isCancelled else { return } - do { - switch event.event { - case .user(user: "diff"): - switch event.payload { - case let .jsonPayload(json): - try self.handleDiff(payload: json, baseURL: self.url) - case .binary: - fatalError() - } - case .user(user: "live_redirect"): - guard case let .jsonPayload(json) = event.payload, - case let .object(payload) = json, - let redirect = LiveRedirect(from: payload, relativeTo: self.url) - else { break } - try await self.session.redirect(redirect) - case .user(user: "live_patch"): - guard case let .jsonPayload(json) = event.payload, - case let .object(payload) = json, - let redirect = LiveRedirect(from: payload, relativeTo: self.url, mode: .patch) - else { return } - try await self.session.redirect(redirect) - case .user(user: "redirect"): - guard case let .jsonPayload(json) = event.payload, - case let .object(payload) = json, - let destination = (payload["to"] as? String).flatMap({ URL.init(string: $0, relativeTo: self.url) }) - else { return } - try await self.session.redirect(.init(kind: .push, to: destination, mode: .replaceTop)) - default: - logger.error("Unhandled event: \(String(describing: event))") - } - } catch { - logger.error("Event handling error: \(error.localizedDescription)") - } - } - } - } +// func bindEventListener() { +// self.eventListenerLoop = Task { [weak self, weak channel] in +// guard let channel else { return } +// let eventListener = channel.eventStream() +// for try await event in eventListener { +// guard let self else { return } +// guard !Task.isCancelled else { return } +// do { +// switch event.event { +// case .user(user: "diff"): +// switch event.payload { +// case let .jsonPayload(json): +// try self.handleDiff(payload: json, baseURL: self.url) +// case .binary: +// fatalError() +// } +// case .user(user: "live_redirect"): +// guard case let .jsonPayload(json) = event.payload, +// case let .object(payload) = json, +// let redirect = LiveRedirect(from: payload, relativeTo: self.url) +// else { break } +// try await self.session.redirect(redirect) +// case .user(user: "live_patch"): +// guard case let .jsonPayload(json) = event.payload, +// case let .object(payload) = json, +// let redirect = LiveRedirect(from: payload, relativeTo: self.url, mode: .patch) +// else { return } +// try await self.session.redirect(redirect) +// case .user(user: "redirect"): +// guard case let .jsonPayload(json) = event.payload, +// case let .object(payload) = json, +// let destination = (payload["to"] as? String).flatMap({ URL.init(string: $0, relativeTo: self.url) }) +// else { return } +// try await self.session.redirect(.init(kind: .push, to: destination, mode: .replaceTop)) +// default: +// logger.error("Unhandled event: \(String(describing: event))") +// } +// } catch { +// logger.error("Event handling error: \(error.localizedDescription)") +// } +// } +// } +// } - func bindDocumentListener() { - self.document?.on(.changed) { [weak self] nodeRef, nodeData, parent in - guard let self else { return } - switch nodeData { +// func bindDocumentListener() { +// self.liveviewClient?.onDocument(.changed, { [weak self] nodeRef, nodeData, parent in +// guard let self else { return } +// switch nodeData { +// case .root: +// // when the root changes, update the `NavStackEntry` itself. +// self.objectWillChange.send() +// case .leaf: +// // text nodes don't have their own views, changes to them need to be handled by the parent Text view +// if let parent { +// self.elementChanged(nodeRef).send() +// } else { +// self.elementChanged(nodeRef).send() +// } +// case .nodeElement: +// // when a single element changes, send an update only to that element. +// self.elementChanged(nodeRef).send() +// } +// }, { doc in }) +// } + + func join(_ client: LiveViewNativeCore.LiveViewClient, + _ eventListener: SimpleEventHandler, + _ docHandler: SimplePatchHandler + ) { + self.liveviewClient = client + self.channel = client.channel() + self.document = try! client.document() + //self.liveChannel = liveChannel + //self.channel = channel + + eventListener.channelStatusSubject + .receive(on: DispatchQueue.main) + .sink { event in + self.internalState = switch event.status { + case .joined: + .connected + case .joining, .waitingForSocketToConnect, .waitingToJoin: + .connecting + case .waitingToRejoin: + .reconnecting + case .leaving, .left, .shuttingDown, .shutDown: + .disconnected + } + }.store(in: &eventHandlers) + + + docHandler.patchEventSubject + .receive(on: DispatchQueue.main) + .sink { event in + switch event.data { case .root: // when the root changes, update the `NavStackEntry` itself. self.objectWillChange.send() case .leaf: // text nodes don't have their own views, changes to them need to be handled by the parent Text view - if let parent { - self.elementChanged(nodeRef).send() + // note: aren't these branches the same? + if event.parent != nil { + self.elementChanged(event.node).send() } else { - self.elementChanged(nodeRef).send() + self.elementChanged(event.node).send() } case .nodeElement: // when a single element changes, send an update only to that element. - self.elementChanged(nodeRef).send() + self.elementChanged(event.node).send() } - } - } + }.store(in: &eventHandlers) - func join(_ liveChannel: LiveViewNativeCore.LiveChannel) { - self.liveChannel = liveChannel - let channel = liveChannel.channel() - self.channel = channel - statusListenerLoop = Task { @MainActor [weak self, unowned channel] in - let statusListener = channel.statusStream() - for try await status in statusListener { - self?.internalState = switch status { - case .joined: - .connected - case .joining, .waitingForSocketToConnect, .waitingToJoin: - .connecting - case .waitingToRejoin: - .reconnecting - case .leaving, .left, .shuttingDown, .shutDown: - .disconnected - } - } - } +// statusListenerLoop = Task { @MainActor [weak self, unowned liveviewClient] in +// statusListener = eventListener.channelStatusSubject; +// for try await status in statusListener { +// self?.internalState = switch status { +// case .joined: +// .connected +// case .joining, .waitingForSocketToConnect, .waitingToJoin: +// .connecting +// case .waitingToRejoin: +// .reconnecting +// case .leaving, .left, .shuttingDown, .shutDown: +// .disconnected +// } +// } +// } - self.bindEventListener() + // self.bindEventListener() - self.document = liveChannel.document() - self.bindDocumentListener() + //self.bindDocumentListener() - switch liveChannel.joinPayload() { + switch try! client.joinPayload() { case let .jsonPayload(.object(payload)): self.handleEvents(payload["rendered"]!) default: @@ -338,13 +385,15 @@ public class LiveViewCoordinator: ObservableObject { self.internalState = .connected } - func disconnect() async throws { - try await self.channel?.leave() - self.eventListenerLoop = nil - self.statusListenerLoop = nil - self.liveChannel = nil - self.channel = nil + func disconnect() { + //try await self.channel?.leave() + //self.eventListenerLoop = nil + //self.statusListenerLoop = nil + //self.liveChannel = nil + //self.channel = nil + self.liveviewClient = nil + self.channel = nil self.internalState = .setup } } diff --git a/Sources/LiveViewNative/Live/LiveView.swift b/Sources/LiveViewNative/Live/LiveView.swift index 7018e242a..e9a2cd738 100644 --- a/Sources/LiveViewNative/Live/LiveView.swift +++ b/Sources/LiveViewNative/Live/LiveView.swift @@ -237,7 +237,7 @@ public struct LiveView< .onChange(of: scenePhase) { newValue in guard case .active = newValue else { return } - if case .connected = session.socket?.status() { + if case .connected = session.status() { return } Task { diff --git a/Sources/LiveViewNative/ViewModel.swift b/Sources/LiveViewNative/ViewModel.swift index 3c77307d0..4e5299ba5 100644 --- a/Sources/LiveViewNative/ViewModel.swift +++ b/Sources/LiveViewNative/ViewModel.swift @@ -164,14 +164,7 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible { } public func buildFormURLComponents() throws -> URLComponents { - let data = try data.mapValues { value in - if let value = value as? String { - return value - } else { - return try value.formQueryEncoded() - } - } - + let data = try toDictionary() var components = URLComponents() components.queryItems = data.map { URLQueryItem(name: $0.key, value: $0.value) @@ -179,6 +172,16 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible { return components } + + public func toDictionary() throws -> [String: String] { + return try data.mapValues { value in + if let value = value as? String { + return value + } else { + return try value.formQueryEncoded() + } + } + } @MainActor private func pushFormEvent(_ event: String) async throws { @@ -269,8 +272,8 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible { fileName: String, coordinator: LiveViewCoordinator ) async throws { - guard let liveChannel = coordinator.liveChannel - else { return } + //guard let liveChannel = coordinator.liveChannel + //else { return } let file = LiveFile( contents, @@ -280,34 +283,35 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible { id ) if let changeEventName { - let replyPayload = try await coordinator.liveChannel!.channel().call( - event: .user(user: "event"), - payload: .jsonPayload(json: .object(object: [ - "type": .str(string: "form"), - "event": .str(string: changeEventName), - "value": .str(string: "_target=\(name)"), - "uploads": .object(object: [ - id: .array(array: [ - .object(object: [ - "path": .str(string: fileName), - "ref": .str(string: String(coordinator.nextUploadRef())), - "last_modified": .numb(number: .posInt(pos: UInt64(Date().timeIntervalSince1970 * 1000))), // in milliseconds - "name": .str(string: fileName), - "relative_path": .str(string: ""), - "type": .str(string: fileType.preferredMIMEType!), - "size": .numb(number: .posInt(pos: UInt64(contents.count))) - ]) - ]) - ]) - ])), - timeout: 10_000 - ) - try await coordinator.handleEventReplyPayload(replyPayload) +// let replyPayload = try await coordinator.liveChannel!.channel().call( +// event: .user(user: "event"), +// payload: .jsonPayload(json: .object(object: [ +// "type": .str(string: "form"), +// "event": .str(string: changeEventName), +// "value": .str(string: "_target=\(name)"), +// "uploads": .object(object: [ +// id: .array(array: [ +// .object(object: [ +// "path": .str(string: fileName), +// "ref": .str(string: String(coordinator.nextUploadRef())), +// "last_modified": .numb(number: .posInt(pos: UInt64(Date().timeIntervalSince1970 * 1000))), // in milliseconds +// "name": .str(string: fileName), +// "relative_path": .str(string: ""), +// "type": .str(string: fileType.preferredMIMEType!), +// "size": .numb(number: .posInt(pos: UInt64(contents.count))) +// ]) +// ]) +// ]) +// ])), +// timeout: 10_000 +// ) +// try await coordinator.handleEventReplyPayload(replyPayload) } self.fileUploads.append(.init( id: id, data: contents, - upload: { try await liveChannel.uploadFile(file) } + // TODO: put the uploadFile function on the pseudo channel + upload: { /* try await liveChannel.uploadFile(file) */ } )) } } From 20abc2df33927d16b69075d9c81bee0a28ffda14 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Tue, 4 Feb 2025 00:18:35 -0800 Subject: [PATCH 3/6] move features into core --- .../Coordinators/LiveSessionCoordinator.swift | 41 ++++++++++++++----- .../Coordinators/LiveViewCoordinator.swift | 25 ++++++----- .../lvn.swiftui.gen/core_components.ex | 2 +- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 089c55d76..3b152b94e 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -61,10 +61,11 @@ public class LiveSessionCoordinator: ObservableObject { private var reconnectAttempts = 0 // TODO: Once this works sub out the rest - private var persistence: SimpleStore? + private var persistence: SimplePersistentStore private var eventHandler: SimpleEventHandler private var patchHandler: SimplePatchHandler - + private var navHandler: SimpleNavHandler + private var liveviewClient: LiveViewClient? private var builder: LiveViewClientBuilder @@ -107,18 +108,16 @@ public class LiveSessionCoordinator: ObservableObject { self.url = url.appending(path: "").absoluteURL - self.patchHandler = SimplePatchHandler({ _, _, _ in }) - self.eventHandler = SimpleEventHandler( - event_callback: { _ in }, - chan_status_callback: { _ in }, - sock_status_callback: { _ in }, - change_callback: { _, _, _, _ in } - ) + self.patchHandler = SimplePatchHandler() + self.eventHandler = SimpleEventHandler() + self.navHandler = SimpleNavHandler() + self.persistence = SimplePersistentStore() self.builder = LiveViewClientBuilder(); self.builder.setPatchHandler(patchHandler) -// self.builder.setPersistenceProvider(persistence) + self.builder.setNavigationHandler(navHandler) + self.builder.setPersistenceProvider(persistence) self.builder.setLiveChannelEventHandler(eventHandler) self.builder.setLogLevel(.debug) @@ -133,7 +132,25 @@ public class LiveSessionCoordinator: ObservableObject { .sink(receiveValue: { [weak self] value in self?.eventSubject.send(value) }) - + + // TODO: move all navigation path manipulation into this watcher. + self.navHandler.navEventSubject + .receive(on: DispatchQueue.main) + .sink { [weak self] navEvent in + switch navEvent.event { + case .push: + print("push") + case .replace: + print("replace") + case .reload: + print("reload") + case .traverse: + print("traverse") + case .patch: + print("patch") + } + }.store(in: &cancellables) + self.eventHandler.viewReloadSubject .receive(on: DispatchQueue.main) .sink { [weak self] newView in @@ -153,6 +170,8 @@ public class LiveSessionCoordinator: ObservableObject { // try await prev.last?.coordinator.disconnect() var opts = NavOptions() opts.joinParams = .some([ "_interface": .object(object: LiveSessionParameters.platformParams)]) + // TODO: Replace all redirects which would modify the navigation path with the proper calls + // TODO: to client navigation. use the listener on line 136 for modifying the path. if let client = self.liveviewClient { try await client.navigate(next_url, opts) } diff --git a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift index fa94f826e..53c5d7add 100644 --- a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift @@ -204,20 +204,19 @@ public class LiveViewCoordinator: ObservableObject { if case let .object(reply) = diff["r"] { return reply } - //TODO: remove this and move it inside the client - } else if case let .object(redirectObject) = object["live_redirect"], - let redirect = LiveRedirect(from: redirectObject, relativeTo: self.url) - { - try await session.redirect(redirect) - //TODO: remove this and move it inside the client - } else if case let .object(redirectObject) = object["redirect"], - case let .str(destinationString) = redirectObject["to"], - let destination = URL(string: destinationString, relativeTo: self.url) - { - try await session.redirect(.init(kind: .push, to: destination, mode: .replaceTop)) - } else { - return nil } +// else if case let .object(redirectObject) = object["live_redirect"], +// let redirect = LiveRedirect(from: redirectObject, relativeTo: self.url) +// { +// try await session.redirect(redirect) +// } else if case let .object(redirectObject) = object["redirect"], +// case let .str(destinationString) = redirectObject["to"], +// let destination = URL(string: destinationString, relativeTo: self.url) +// { +// try await session.redirect(.init(kind: .push, to: destination, mode: .replaceTop)) +// } else { + return nil + // } default: logger.error("unhandled event reply: \(String(reflecting: replyPayload))") } diff --git a/priv/templates/lvn.swiftui.gen/core_components.ex b/priv/templates/lvn.swiftui.gen/core_components.ex index 0ab5e8847..3330a70ee 100644 --- a/priv/templates/lvn.swiftui.gen/core_components.ex +++ b/priv/templates/lvn.swiftui.gen/core_components.ex @@ -352,7 +352,7 @@ defmodule <%= inspect context.web_module %>.CoreComponents.<%= inspect context.m {@rest} > {msg} - + """ end From 83e63035c70343bd7ecc1b2a945ead9812b37eb2 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Wed, 5 Feb 2025 12:36:28 -0800 Subject: [PATCH 4/6] remove commented sections --- .../Coordinators/LiveSessionCoordinator.swift | 138 ++++-------------- .../Coordinators/LiveViewCoordinator.swift | 124 +--------------- 2 files changed, 30 insertions(+), 232 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 3b152b94e..6ed4a0ab5 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -133,24 +133,6 @@ public class LiveSessionCoordinator: ObservableObject { self?.eventSubject.send(value) }) - // TODO: move all navigation path manipulation into this watcher. - self.navHandler.navEventSubject - .receive(on: DispatchQueue.main) - .sink { [weak self] navEvent in - switch navEvent.event { - case .push: - print("push") - case .replace: - print("replace") - case .reload: - print("reload") - case .traverse: - print("traverse") - case .patch: - print("patch") - } - }.store(in: &cancellables) - self.eventHandler.viewReloadSubject .receive(on: DispatchQueue.main) .sink { [weak self] newView in @@ -163,38 +145,28 @@ public class LiveSessionCoordinator: ObservableObject { $navigationPath.scan(([LiveNavigationEntry](), [LiveNavigationEntry]()), { ($0.1, $1) }).sink { [weak self] prev, next in guard let self else { return } + guard let client = liveviewClient else { return } Task { - - //working nav code - let next_url = next.last!.url.absoluteString -// try await prev.last?.coordinator.disconnect() - var opts = NavOptions() - opts.joinParams = .some([ "_interface": .object(object: LiveSessionParameters.platformParams)]) - // TODO: Replace all redirects which would modify the navigation path with the proper calls - // TODO: to client navigation. use the listener on line 136 for modifying the path. - if let client = self.liveviewClient { - try await client.navigate(next_url, opts) - } - - //next.last?.coordinator.join(ch) -// let last_channel = prev.last?.coordinator.liveChannel -// try await prev.last?.coordinator.disconnect() -// if prev.count > next.count { -// // back navigation (we could be going back multiple pages at once, so use `traverseTo` instead of `back`) -// let targetEntry = self.liveSocket!.getEntries()[next.count - 1] -// next.last?.coordinator.join( -// try await self.liveSocket!.traverseTo(targetEntry.id, last_channel!, nil) -// ) -// } else if next.count > prev.count && prev.count > 0 { -// // forward navigation (from `redirect` or ``) -// next.last?.coordinator.join( -// try await self.liveSocket!.navigate(next.last!.url.absoluteString, last_channel!, NavOptions(action: .push)) -// ) -// } else if next.count == prev.count { -// guard let liveChannel = try await self.liveSocket?.navigate(next.last!.url.absoluteString, last_channel!, NavOptions(action: .replace)) -// else { return } -// next.last?.coordinator.join(liveChannel) -// } + prev.last?.coordinator.disconnect() + if prev.count > next.count { + // back navigation (we could be going back multiple pages at once, so use `traverseTo` instead of `back`) + 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 ``) + 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 { + // TODO: this will fire on a patch event! this should not fire on a patch event + 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) // Receive events from all live views. @@ -269,19 +241,6 @@ public class LiveSessionCoordinator: ObservableObject { } -// self.liveSocket = try await LiveSocket( -// originalURL.absoluteString, -// LiveSessionParameters.platform, -// ConnectOpts( -// headers: headers, -// body: httpBody.flatMap({ String(data: $0, encoding: .utf8) }), -// method: httpMethod.flatMap(Method.init(_:)), -// timeoutMs: 10_000 -// ) -// ) -// - - //self.socket = self.liveSocket?.socket() self.rootLayout = try self.liveviewClient!.deadRender() let styleURLs = try self.liveviewClient!.styleUrls() @@ -307,49 +266,21 @@ public class LiveSessionCoordinator: ObservableObject { } } -// let liveChannel = try await self.liveSocket!.joinLiveviewChannel( -// .some([ -// "_format": .str(string: LiveSessionParameters.platform), -// "_interface": .object(object: LiveSessionParameters.platformParams) -// ]), -// nil -// ) self.state = .connected -// if let liveReloadChannel = try self.liveviewClient?.liveReloadChannel() { -// self.liveReloadChannel = liveReloadChannel -// bindLiveReloadListener() -// } - } catch { self.state = .connectionFailed(error) } } -// func bindLiveReloadListener() { -// let eventListener = self.liveReloadChannel!.channel().eventStream() -// self.liveReloadListener = eventListener -// self.liveReloadListenerLoop = Task { @MainActor [weak self] in -// for try await event in eventListener { -// guard let self else { return } -// switch event.event { -// case .user(user: "assets_change"): -// await self.disconnect() -// self.navigationPath = [.init(url: self.url, coordinator: .init(session: self, url: self.url), navigationTransition: nil, pendingView: nil)] -// await self.connect() -// default: -// continue -// } -// } -// } -// } + 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 } @@ -362,11 +293,7 @@ public class LiveSessionCoordinator: ObservableObject { self.navigationPath = [self.navigationPath.first!] } - //try await self.liveReloadChannel?.channel().leave() - //self.liveReloadChannel = nil - //try await self.socket?.disconnect() - //self.socket = nil - //self.liveSocket = nil + if let client = self.liveviewClient { try await client.disconnect() } @@ -388,22 +315,7 @@ public class LiveSessionCoordinator: 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. diff --git a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift index 53c5d7add..74f75d6f8 100644 --- a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift @@ -38,9 +38,6 @@ public class LiveViewCoordinator: ObservableObject { var url: URL - // private(set) var liveChannel: LiveViewNativeCore.LiveChannel? - // private var channel: LiveViewNativeCore.Channel? - @Published var document: LiveViewNativeCore.Document? { didSet { elementChangedSubjects.removeAll() @@ -62,10 +59,7 @@ public class LiveViewCoordinator: ObservableObject { private(set) internal var eventSubject = PassthroughSubject<(String, Payload), Never>() private(set) internal var eventHandlers = Set() -// private var eventListener: Channel.EventStream? - // private var eventListenerLoop: Task<(), any Error>? -// private var statusListener: Channel.StatusStream? - //private var statusListenerLoop: Task<(), any Error>? + private(set) internal var liveViewModel = LiveViewModel() @@ -81,10 +75,6 @@ public class LiveViewCoordinator: ObservableObject { self.url = url } - deinit { - //self.eventListenerLoop?.cancel() - //self.statusListenerLoop?.cancel() - } /// Pushes a LiveView event with the given name and payload to the server. /// @@ -189,10 +179,7 @@ public class LiveViewCoordinator: ObservableObject { .store(in: &eventHandlers) } -// private func handleDiff(payload: LiveViewNativeCore.Json, baseURL: URL) throws { -// handleEvents(payload) -// try self.document?.mergeFragmentJson(String(data: try JSONEncoder().encode(payload), encoding: .utf8)!) -// } + func handleEventReplyPayload(_ replyPayload: LiveViewNativeCore.Payload) async throws -> [String:Any]? { switch replyPayload { @@ -200,23 +187,11 @@ public class LiveViewCoordinator: ObservableObject { switch json { case let .object(object): if case let .object(diff) = object["diff"] { - //try self.handleDiff(payload: .object(object: diff), baseURL: self.url) if case let .object(reply) = diff["r"] { return reply } } -// else if case let .object(redirectObject) = object["live_redirect"], -// let redirect = LiveRedirect(from: redirectObject, relativeTo: self.url) -// { -// try await session.redirect(redirect) -// } else if case let .object(redirectObject) = object["redirect"], -// case let .str(destinationString) = redirectObject["to"], -// let destination = URL(string: destinationString, relativeTo: self.url) -// { -// try await session.redirect(.init(kind: .push, to: destination, mode: .replaceTop)) -// } else { - return nil - // } + return nil default: logger.error("unhandled event reply: \(String(reflecting: replyPayload))") } @@ -241,70 +216,7 @@ public class LiveViewCoordinator: ObservableObject { } } -// func bindEventListener() { -// self.eventListenerLoop = Task { [weak self, weak channel] in -// guard let channel else { return } -// let eventListener = channel.eventStream() -// for try await event in eventListener { -// guard let self else { return } -// guard !Task.isCancelled else { return } -// do { -// switch event.event { -// case .user(user: "diff"): -// switch event.payload { -// case let .jsonPayload(json): -// try self.handleDiff(payload: json, baseURL: self.url) -// case .binary: -// fatalError() -// } -// case .user(user: "live_redirect"): -// guard case let .jsonPayload(json) = event.payload, -// case let .object(payload) = json, -// let redirect = LiveRedirect(from: payload, relativeTo: self.url) -// else { break } -// try await self.session.redirect(redirect) -// case .user(user: "live_patch"): -// guard case let .jsonPayload(json) = event.payload, -// case let .object(payload) = json, -// let redirect = LiveRedirect(from: payload, relativeTo: self.url, mode: .patch) -// else { return } -// try await self.session.redirect(redirect) -// case .user(user: "redirect"): -// guard case let .jsonPayload(json) = event.payload, -// case let .object(payload) = json, -// let destination = (payload["to"] as? String).flatMap({ URL.init(string: $0, relativeTo: self.url) }) -// else { return } -// try await self.session.redirect(.init(kind: .push, to: destination, mode: .replaceTop)) -// default: -// logger.error("Unhandled event: \(String(describing: event))") -// } -// } catch { -// logger.error("Event handling error: \(error.localizedDescription)") -// } -// } -// } -// } - -// func bindDocumentListener() { -// self.liveviewClient?.onDocument(.changed, { [weak self] nodeRef, nodeData, parent in -// guard let self else { return } -// switch nodeData { -// case .root: -// // when the root changes, update the `NavStackEntry` itself. -// self.objectWillChange.send() -// case .leaf: -// // text nodes don't have their own views, changes to them need to be handled by the parent Text view -// if let parent { -// self.elementChanged(nodeRef).send() -// } else { -// self.elementChanged(nodeRef).send() -// } -// case .nodeElement: -// // when a single element changes, send an update only to that element. -// self.elementChanged(nodeRef).send() -// } -// }, { doc in }) -// } + func join(_ client: LiveViewNativeCore.LiveViewClient, _ eventListener: SimpleEventHandler, @@ -313,8 +225,6 @@ public class LiveViewCoordinator: ObservableObject { self.liveviewClient = client self.channel = client.channel() self.document = try! client.document() - //self.liveChannel = liveChannel - //self.channel = channel eventListener.channelStatusSubject .receive(on: DispatchQueue.main) @@ -354,25 +264,7 @@ public class LiveViewCoordinator: ObservableObject { }.store(in: &eventHandlers) -// statusListenerLoop = Task { @MainActor [weak self, unowned liveviewClient] in -// statusListener = eventListener.channelStatusSubject; -// for try await status in statusListener { -// self?.internalState = switch status { -// case .joined: -// .connected -// case .joining, .waitingForSocketToConnect, .waitingToJoin: -// .connecting -// case .waitingToRejoin: -// .reconnecting -// case .leaving, .left, .shuttingDown, .shutDown: -// .disconnected -// } -// } -// } - - // self.bindEventListener() - - //self.bindDocumentListener() + switch try! client.joinPayload() { case let .jsonPayload(.object(payload)): @@ -385,12 +277,6 @@ public class LiveViewCoordinator: ObservableObject { } func disconnect() { - //try await self.channel?.leave() - //self.eventListenerLoop = nil - //self.statusListenerLoop = nil - //self.liveChannel = nil - //self.channel = nil - self.liveviewClient = nil self.channel = nil self.internalState = .setup From 59ca85f4a9aed8ad81429de66ab99b423865c5c2 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Wed, 5 Feb 2025 13:40:37 -0800 Subject: [PATCH 5/6] fix upload --- .../Coordinators/LiveViewCoordinator.swift | 21 ++++++++ Sources/LiveViewNative/ViewModel.swift | 52 +++++++++---------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift index 74f75d6f8..8908a90b3 100644 --- a/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveViewCoordinator.swift @@ -119,6 +119,27 @@ public class LiveViewCoordinator: ObservableObject { return nil } } + + @discardableResult + public func call(event: String, payload: LiveViewNativeCore.Payload) async throws -> LiveViewNativeCore.Payload? { + guard case .connected = state else { + throw LiveSocketError.DisconnectionError + } + + if let replyPayload = try await channel?.call(event, payload) { + return replyPayload + } else { + return nil + } + } + + public func uploadFile(file: LiveViewNativeCore.LiveFile) async throws { + guard case .connected = state else { + throw LiveSocketError.DisconnectionError + } + + try await liveviewClient?.uploadFiles([file]); + } /// Creates a publisher that can be used to listen for server-sent LiveView events. /// diff --git a/Sources/LiveViewNative/ViewModel.swift b/Sources/LiveViewNative/ViewModel.swift index 4e5299ba5..119c850e6 100644 --- a/Sources/LiveViewNative/ViewModel.swift +++ b/Sources/LiveViewNative/ViewModel.swift @@ -272,8 +272,6 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible { fileName: String, coordinator: LiveViewCoordinator ) async throws { - //guard let liveChannel = coordinator.liveChannel - //else { return } let file = LiveFile( contents, @@ -283,35 +281,35 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible { id ) if let changeEventName { -// let replyPayload = try await coordinator.liveChannel!.channel().call( -// event: .user(user: "event"), -// payload: .jsonPayload(json: .object(object: [ -// "type": .str(string: "form"), -// "event": .str(string: changeEventName), -// "value": .str(string: "_target=\(name)"), -// "uploads": .object(object: [ -// id: .array(array: [ -// .object(object: [ -// "path": .str(string: fileName), -// "ref": .str(string: String(coordinator.nextUploadRef())), -// "last_modified": .numb(number: .posInt(pos: UInt64(Date().timeIntervalSince1970 * 1000))), // in milliseconds -// "name": .str(string: fileName), -// "relative_path": .str(string: ""), -// "type": .str(string: fileType.preferredMIMEType!), -// "size": .numb(number: .posInt(pos: UInt64(contents.count))) -// ]) -// ]) -// ]) -// ])), -// timeout: 10_000 -// ) -// try await coordinator.handleEventReplyPayload(replyPayload) + let replyPayload = try await coordinator.call( + event: "event", + payload: .jsonPayload(json: .object(object: [ + "type": .str(string: "form"), + "event": .str(string: changeEventName), + "value": .str(string: "_target=\(name)"), + "uploads": .object(object: [ + id: .array(array: [ + .object(object: [ + "path": .str(string: fileName), + "ref": .str(string: String(coordinator.nextUploadRef())), + "last_modified": .numb(number: .posInt(pos: UInt64(Date().timeIntervalSince1970 * 1000))), // in milliseconds + "name": .str(string: fileName), + "relative_path": .str(string: ""), + "type": .str(string: fileType.preferredMIMEType!), + "size": .numb(number: .posInt(pos: UInt64(contents.count))) + ]) + ]) + ]) + ])) + ); + if let payload = replyPayload { + try await coordinator.handleEventReplyPayload(payload) + } } self.fileUploads.append(.init( id: id, data: contents, - // TODO: put the uploadFile function on the pseudo channel - upload: { /* try await liveChannel.uploadFile(file) */ } + upload: { try await coordinator.uploadFile(file: file) } )) } } From b15ff9605f9657cefa0673adf375ab420a5d79d4 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Wed, 5 Feb 2025 13:45:00 -0800 Subject: [PATCH 6/6] remove commented let block --- .../LiveViewNative/Coordinators/LiveSessionCoordinator.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift index 6ed4a0ab5..1cb3b2b11 100644 --- a/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift +++ b/Sources/LiveViewNative/Coordinators/LiveSessionCoordinator.swift @@ -44,10 +44,6 @@ public class LiveSessionCoordinator: ObservableObject { @Published private(set) var rootLayout: LiveViewNativeCore.Document? @Published private(set) var stylesheet: Stylesheet? - // Socket connection - //var liveSocket: LiveViewNativeCore.LiveSocket? - //var socket: LiveViewNativeCore.Socket? - private var liveReloadChannel: LiveViewNativeCore.LiveChannel? private var liveReloadListener: Channel.EventStream? private var liveReloadListenerLoop: Task<(), any Error>?