From 478d92d25193b2d0d1e1647a582d43a853d3547c Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Thu, 6 Feb 2025 11:53:38 -0800 Subject: [PATCH 01/12] rename handlers --- crates/core/src/client/inner/event_loop/mod.rs | 6 +++--- crates/core/src/client/inner/event_loop/state.rs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/core/src/client/inner/event_loop/mod.rs b/crates/core/src/client/inner/event_loop/mod.rs index 11a193fa..9e33f56e 100644 --- a/crates/core/src/client/inner/event_loop/mod.rs +++ b/crates/core/src/client/inner/event_loop/mod.rs @@ -118,18 +118,18 @@ impl EventLoop { error!("Failure while handling server reply: {e:?}"); } - state.on_event(payload); + state.user_event_callback(payload); } // connectivity changes new_status = chan_status => { match new_status { - Ok(status) => state.on_channel_status(status.into()), + Ok(status) => state.user_channel_callback(status.into()), Err(e) => error!("Error fetching liveview status: {e}"), } } new_status = socket_status => { match new_status { - Ok(status) => state.on_socket_status(status), + Ok(status) => state.user_socket_callback(status), Err(e) => error!("Error fetching liveview status: {e}"), } } diff --git a/crates/core/src/client/inner/event_loop/state.rs b/crates/core/src/client/inner/event_loop/state.rs index e5858e62..1e892beb 100644 --- a/crates/core/src/client/inner/event_loop/state.rs +++ b/crates/core/src/client/inner/event_loop/state.rs @@ -119,7 +119,7 @@ impl EventLoopState { let new_livereload_channel = self.client_state.livereload_channel.lock().unwrap().clone(); self.live_reload = new_livereload_channel.map(ChannelState::from); - self.on_reload( + self.user_reload_callback( self.document.clone().into(), self.live_view_channel.channel.clone(), self.live_view_channel.channel.socket.clone(), @@ -168,25 +168,25 @@ impl EventLoopState { } /// Call the user provided call back for receiving a - pub fn on_event(&self, event: EventPayload) { + pub(super) fn user_event_callback(&self, event: EventPayload) { if let Some(handler) = &self.network_handler { handler.handle_event(event); } } - pub fn on_channel_status(&self, status: LiveChannelStatus) { + pub(super) fn user_channel_callback(&self, status: LiveChannelStatus) { if let Some(handler) = &self.network_handler { handler.handle_channel_status_change(status); } } - pub fn on_socket_status(&self, status: SocketStatus) { + pub(super) fn user_socket_callback(&self, status: SocketStatus) { if let Some(handler) = &self.network_handler { handler.handle_socket_status_change(status); } } - pub fn on_reload( + pub(super) fn user_reload_callback( &self, new_document: Arc, new_channel: Arc, @@ -228,7 +228,7 @@ impl EventLoopState { payload: reply.clone(), }; - self.on_event(event); + self.user_event_callback(event); let _ = response_tx.send(Ok(reply)); } @@ -257,7 +257,7 @@ impl EventLoopState { payload, }; - self.on_event(event); + self.user_event_callback(event); let _ = tx.send(result); } From 80cbd6e5824193fe9ea8d029931f259e4c3e1ce7 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Thu, 6 Feb 2025 12:45:44 -0800 Subject: [PATCH 02/12] verify reload in tests --- crates/core/src/client/inner/event_loop/mod.rs | 5 +++++ crates/core/src/client/inner/event_loop/state.rs | 8 ++++++-- crates/core/src/client/inner/mod.rs | 12 +++++++++++- crates/core/src/client/tests/lifecycle.rs | 6 ++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/core/src/client/inner/event_loop/mod.rs b/crates/core/src/client/inner/event_loop/mod.rs index 9e33f56e..cdacaa68 100644 --- a/crates/core/src/client/inner/event_loop/mod.rs +++ b/crates/core/src/client/inner/event_loop/mod.rs @@ -136,6 +136,7 @@ impl EventLoop { } } + dbg!(view_refresh_needed); if view_refresh_needed { state.refresh_view(socket_reconnected); } @@ -180,6 +181,10 @@ impl EventLoop { retry_count += 1; } + if result.is_ok() { + self.refresh_view(true); + } + result } Err(e) => Err(e), diff --git a/crates/core/src/client/inner/event_loop/state.rs b/crates/core/src/client/inner/event_loop/state.rs index 1e892beb..e85daa28 100644 --- a/crates/core/src/client/inner/event_loop/state.rs +++ b/crates/core/src/client/inner/event_loop/state.rs @@ -267,8 +267,8 @@ impl EventLoopState { async fn handle_redirect( &self, redirect: &JSON, - _channel_updated: &mut bool, - _socket_updated: &mut bool, + channel_updated: &mut bool, + socket_updated: &mut bool, ) -> Result { let json = redirect.clone().into(); let redirect: LiveRedirect = serde_json::from_value(json)?; @@ -287,6 +287,10 @@ impl EventLoopState { }; let res = self.client_state.navigate(url.to_string(), opts).await?; + + *channel_updated = true; + *socket_updated = res.websocket_reconnected; + Ok(res.history_id) } diff --git a/crates/core/src/client/inner/mod.rs b/crates/core/src/client/inner/mod.rs index 34c16ad6..cb316d6e 100644 --- a/crates/core/src/client/inner/mod.rs +++ b/crates/core/src/client/inner/mod.rs @@ -14,7 +14,7 @@ use channel_init::*; use cookie_store::PersistentCookieStore; use event_loop::EventLoop; pub(crate) use event_loop::LiveViewClientChannel; -use log::debug; +use log::{debug, warn}; use logging::*; use navigation::NavCtx; use phoenix_channels_client::{Payload, Socket, SocketStatus, JSON}; @@ -62,6 +62,7 @@ pub struct LiveViewClientInner { event_loop: EventLoop, } +#[derive(Debug, Clone)] struct NavigationSummary { history_id: HistoryId, websocket_reconnected: bool, @@ -232,6 +233,15 @@ impl LiveViewClientState { init_log(config.log_level); debug!("Initializing LiveViewClient."); debug!("LiveViewCore Version: {}", env!("CARGO_PKG_VERSION")); + + if config.network_event_handler.is_none() { + warn!("Network event handler is not set: You will not be able to instrument events such as view reloads and server push events.") + } + + if config.navigation_handler.is_none() { + warn!("Navigation handler is not set: you will not be able to instrument internal and external calls to `navigate`, `traverse`, `back` and `forward`.") + } + debug!("Configuration: {config:?}"); let cookie_store: Arc<_> = diff --git a/crates/core/src/client/tests/lifecycle.rs b/crates/core/src/client/tests/lifecycle.rs index 8ffc561a..d9cc8a67 100644 --- a/crates/core/src/client/tests/lifecycle.rs +++ b/crates/core/src/client/tests/lifecycle.rs @@ -145,11 +145,13 @@ impl NetworkEventHandler for MockNetworkEventHandler { async fn test_navigation_handler() { let store = Arc::new(MockMessageStore::new()); let nav_handler = Arc::new(MockNavEventHandler::new(store.clone())); + let net_handler = Arc::new(MockNetworkEventHandler::new(store.clone())); let url = format!("http://{HOST}/nav/first_page"); let mut config = LiveViewClientConfiguration::default(); config.format = Platform::Swiftui; config.navigation_handler = Some(nav_handler); + config.network_event_handler = Some(net_handler); let client = LiveViewClient::initial_connect(config, url.clone(), Default::default()) .await @@ -169,6 +171,8 @@ async fn test_navigation_handler() { to: NavHistoryEntry::new(next_url, 2, None), info: None, })); + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + assert_any!(store, |m| { matches!(m, MockMessage::ViewReload { .. }) }); } #[tokio::test] @@ -213,6 +217,8 @@ async fn test_redirect_internals() { false }); + assert_any!(store, |m| { matches!(m, MockMessage::ViewReload { .. }) }); + store.clear(); let url = format!("http://{HOST}/push_navigate?redirect_type=patch"); From 0d35652f67e0d3c37a05fa7f6977d3b8f5d475ea Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Thu, 6 Feb 2025 15:41:04 -0800 Subject: [PATCH 03/12] reloads contain metadata about issuer --- .../Sources/LiveViewNativeCore/Support.swift | 5 +- crates/core/src/callbacks.rs | 37 ++++++ .../core/src/client/inner/event_loop/mod.rs | 38 +++--- .../core/src/client/inner/event_loop/state.rs | 113 ++++++++++-------- crates/core/src/client/inner/mod.rs | 33 +++-- crates/core/src/client/tests/lifecycle.rs | 13 +- 6 files changed, 162 insertions(+), 77 deletions(-) diff --git a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift index d2129cc6..543c99e3 100644 --- a/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift +++ b/crates/core/liveview-native-core-swift/Sources/LiveViewNativeCore/Support.swift @@ -1905,6 +1905,7 @@ public struct SocketStatusEvent { } public struct ViewReloadEvent { + public let issuer: Issuer public let document: Document public let channel: LiveChannel public let socket: Socket @@ -1953,10 +1954,12 @@ public final class SimpleEventHandler: NetworkEventHandler { } public func handleViewReloaded( - _ newDocument: Document, _ newChannel: LiveChannel, _ currentSocket: Socket, + _ issuer: Issuer, _ newDocument: Document, _ newChannel: LiveChannel, + _ currentSocket: Socket, _ socketIsNew: Bool ) { let event = ViewReloadEvent( + issuer: issuer, document: newDocument, channel: newChannel, socket: currentSocket, diff --git a/crates/core/src/callbacks.rs b/crates/core/src/callbacks.rs index 0c5c769a..d21fdc04 100644 --- a/crates/core/src/callbacks.rs +++ b/crates/core/src/callbacks.rs @@ -138,6 +138,42 @@ pub enum ControlFlow { ContinueListening, } +#[derive(Clone, Debug, PartialEq, uniffi::Enum)] +pub enum NavigationCall { + /// calls to [LiveViewClient::initial_connect] + Initialization, + /// calls to [LiveViewClient::navigate] + Navigate, + /// calls to [LiveViewClient::forward] + Forward, + /// calls to [LiveViewClient::back] + Back, + /// calls to [LiveViewClient::traverse_to] + Traverse, + /// calls to [LiveViewClient::reload] + Reload, + /// calls to [LiveViewClient::disconnect] + Disconnect, + /// calls to [LiveViewClient::reconnect] and [LiveViewClient::post_form] + Reconnect, +} + +/// The issuer of the event that triggered a given live reload +#[derive(Clone, Debug, PartialEq, uniffi::Enum)] +pub enum Issuer { + /// An external function call from the [LiveViewClient] external API + External(NavigationCall), + /// A "live_reload" message on any channel. + LiveReload, + /// A "redirect" message on any channel. + Redirect, + /// A "live_redirect" message on any channel. + LiveRedirect, + /// An "asset_change" message on a live reload channel. + AssetChange, + Other(String), +} + /// Implements the change handling logic for inbound virtual dom /// changes. Your logic for handling document patches should go here. #[uniffi::export(callback_interface)] @@ -181,6 +217,7 @@ pub trait NetworkEventHandler: Send + Sync { /// If the socket was reconnected for any reason `socket_is_new` will be true. fn handle_view_reloaded( &self, + issuer: Issuer, new_document: Arc, new_channel: Arc, current_socket: Arc, diff --git a/crates/core/src/client/inner/event_loop/mod.rs b/crates/core/src/client/inner/event_loop/mod.rs index cdacaa68..bbbc9d47 100644 --- a/crates/core/src/client/inner/event_loop/mod.rs +++ b/crates/core/src/client/inner/event_loop/mod.rs @@ -8,7 +8,9 @@ use phoenix_channels_client::{CallError, Event, Payload}; use state::{EventLoopState, ReplyAction}; use tokio::sync::mpsc; -use super::{HistoryId, LiveViewClientState, NavigationSummary, NetworkEventHandler}; +use super::{ + HistoryId, Issuer, LiveViewClientState, NavigationCall, NavigationSummary, NetworkEventHandler, +}; use crate::error::LiveSocketError; const MAX_REDIRECTS: u32 = 10; @@ -59,7 +61,10 @@ pub enum ClientMessage { /// Send a message and don't wait for a response Cast { event: Event, payload: Payload }, /// Replace the current channel - RefreshView { socket_reconnected: bool }, + RefreshView { + socket_reconnected: bool, + issuer: Issuer, + }, /// For internal use, error events are not broadcast /// from the socket when you attempt to connect to a channel. /// So we reinject them into the event loop with these messages @@ -79,12 +84,12 @@ impl EventLoop { let (msg_tx, mut msg_rx) = mpsc::unbounded_channel(); let mut state = EventLoopState::new(client_state); - state.refresh_view(true); + state.refresh_view(Issuer::External(NavigationCall::Initialization), true); let main_background_task = tokio::spawn(async move { // the main event loop loop { - let mut view_refresh_needed = false; + let mut view_refresh_issuer = None; let mut socket_reconnected = false; { @@ -105,7 +110,7 @@ impl EventLoop { error!("All client message handlers dropped."); continue; }; - let _ = state.handle_client_message(msg, &mut view_refresh_needed, &mut socket_reconnected).await; + let _ = state.handle_client_message(msg, &mut view_refresh_issuer, &mut socket_reconnected).await; } // networks events from the server event = server_event => { @@ -114,7 +119,7 @@ impl EventLoop { continue; }; - if let Err(e) = state.handle_server_event(&payload, &mut view_refresh_needed, &mut socket_reconnected).await { + if let Err(e) = state.handle_server_event(&payload, &mut view_refresh_issuer, &mut socket_reconnected).await { error!("Failure while handling server reply: {e:?}"); } @@ -136,9 +141,8 @@ impl EventLoop { } } - dbg!(view_refresh_needed); - if view_refresh_needed { - state.refresh_view(socket_reconnected); + if let Some(issuer) = view_refresh_issuer { + state.refresh_view(issuer, socket_reconnected); } } }); @@ -154,19 +158,21 @@ impl EventLoop { /// listeners to refresh events will also be notified. `socket_reconnected` is /// the equivalent of a full liveview reload like a `live_redirect` or an web /// page reload. - pub fn refresh_view(&self, socket_reconnected: bool) { - let _ = self - .msg_tx - .send(ClientMessage::RefreshView { socket_reconnected }); + pub fn refresh_view(&self, socket_reconnected: bool, issuer: Issuer) { + let _ = self.msg_tx.send(ClientMessage::RefreshView { + socket_reconnected, + issuer, + }); } pub async fn handle_navigation_summary( &self, summary: Result, + issuer: Issuer, ) -> Result { match summary { Ok(res) => { - self.refresh_view(res.websocket_reconnected); + self.refresh_view(res.websocket_reconnected, issuer); Ok(res.history_id) } Err(LiveSocketError::JoinRejection { error }) => { @@ -182,7 +188,7 @@ impl EventLoop { } if result.is_ok() { - self.refresh_view(true); + self.refresh_view(true, issuer); } result @@ -210,7 +216,7 @@ impl EventLoop { })??; match action { - ReplyAction::Redirected { id } => Ok(id), + ReplyAction::Redirected { summary, .. } => Ok(summary.history_id), _ => Err(LiveSocketError::JoinRejection { error: payload.clone(), }), diff --git a/crates/core/src/client/inner/event_loop/state.rs b/crates/core/src/client/inner/event_loop/state.rs index e85daa28..5019812c 100644 --- a/crates/core/src/client/inner/event_loop/state.rs +++ b/crates/core/src/client/inner/event_loop/state.rs @@ -10,7 +10,7 @@ use tokio::select; use super::{ClientMessage, LiveViewClientState, NetworkEventHandler}; use crate::{ - client::{HistoryId, LiveChannelStatus}, + client::{inner::NavigationSummary, HistoryId, Issuer, LiveChannelStatus}, dom::ffi::{self, Document}, error::LiveSocketError, live_socket::{ @@ -21,7 +21,10 @@ use crate::{ }; pub enum ReplyAction { - Redirected { id: HistoryId }, + Redirected { + summary: NavigationSummary, + issuer: Issuer, + }, DiffMerged, None, } @@ -106,7 +109,7 @@ impl EventLoopState { /// Called when the owning `LiveViewClient` has been updated /// and has a new valid live channel - livereaload channel, and/or live socket. - pub fn refresh_view(&mut self, socket_reconnect: bool) { + pub fn refresh_view(&mut self, issuer: Issuer, socket_reconnect: bool) { let new_live_channel = self.client_state.liveview_channel.lock().unwrap().clone(); self.socket_statuses = new_live_channel.socket.statuses(); self.live_view_channel = ChannelState::from(new_live_channel.clone()); @@ -120,6 +123,7 @@ impl EventLoopState { self.live_reload = new_livereload_channel.map(ChannelState::from); self.user_reload_callback( + issuer, self.document.clone().into(), self.live_view_channel.channel.clone(), self.live_view_channel.channel.socket.clone(), @@ -188,20 +192,27 @@ impl EventLoopState { pub(super) fn user_reload_callback( &self, + issuer: Issuer, new_document: Arc, new_channel: Arc, current_socket: Arc, socket_is_new: bool, ) { if let Some(handler) = &self.network_handler { - handler.handle_view_reloaded(new_document, new_channel, current_socket, socket_is_new); + handler.handle_view_reloaded( + issuer, + new_document, + new_channel, + current_socket, + socket_is_new, + ); } } pub async fn handle_client_message( &self, message: ClientMessage, - channel_updated: &mut bool, + channel_updated: &mut Option, socket_updated: &mut bool, ) { match message { @@ -214,11 +225,17 @@ impl EventLoopState { match call_result { Ok(reply) => { - if let Err(e) = self - .handle_reply(&reply, channel_updated, socket_updated) - .await - { - error!("Failure while handling server reply: {e:?}"); + let reply_action = self.handle_reply(&reply).await; + + match &reply_action { + Ok(ReplyAction::Redirected { summary, issuer }) => { + *channel_updated = Some(issuer.clone()); + *socket_updated = summary.websocket_reconnected; + } + Ok(_) => {} + Err(e) => { + error!("Failure while handling server reply: {e:?}"); + } } let event = EventPayload { @@ -241,14 +258,26 @@ impl EventLoopState { ClientMessage::Cast { event, payload } => { let _ = self.cast(event, payload).await; } - ClientMessage::RefreshView { socket_reconnected } => { - *channel_updated = true; + ClientMessage::RefreshView { + socket_reconnected, + issuer, + } => { + *channel_updated = Some(issuer); *socket_updated = socket_reconnected } ClientMessage::HandleSocketReply { payload, tx } => { - let result = self - .handle_reply(&payload, channel_updated, socket_updated) - .await; + let result = self.handle_reply(&payload).await; + + match &result { + Ok(ReplyAction::Redirected { summary, issuer }) => { + *channel_updated = Some(issuer.clone()); + *socket_updated = summary.websocket_reconnected; + } + Ok(_) => {} + Err(e) => { + error!("Failure while handling server reply: {e:?}"); + } + } let event = EventPayload { event: Event::Phoenix { @@ -264,12 +293,7 @@ impl EventLoopState { } } - async fn handle_redirect( - &self, - redirect: &JSON, - channel_updated: &mut bool, - socket_updated: &mut bool, - ) -> Result { + async fn handle_redirect(&self, redirect: &JSON) -> Result { let json = redirect.clone().into(); let redirect: LiveRedirect = serde_json::from_value(json)?; let url = self.client_state.session_data.try_lock()?.url.clone(); @@ -286,20 +310,10 @@ impl EventLoopState { ..NavOptions::default() }; - let res = self.client_state.navigate(url.to_string(), opts).await?; - - *channel_updated = true; - *socket_updated = res.websocket_reconnected; - - Ok(res.history_id) + self.client_state.navigate(url.to_string(), opts).await } - async fn handle_reply( - &self, - reply: &Payload, - channel_updated: &mut bool, - socket_updated: &mut bool, - ) -> Result { + async fn handle_reply(&self, reply: &Payload) -> Result { let Payload::JSONPayload { json: JSON::Object { object }, } = reply @@ -308,18 +322,19 @@ impl EventLoopState { }; if let Some(object) = object.get("live_redirect") { - let id = self - .handle_redirect(object, channel_updated, socket_updated) - .await?; - - return Ok(ReplyAction::Redirected { id }); + let summary = self.handle_redirect(object).await?; + return Ok(ReplyAction::Redirected { + summary, + issuer: Issuer::LiveRedirect, + }); } if let Some(object) = object.get("redirect") { - let id = self - .handle_redirect(object, channel_updated, socket_updated) - .await?; - return Ok(ReplyAction::Redirected { id }); + let summary = self.handle_redirect(object).await?; + return Ok(ReplyAction::Redirected { + summary, + issuer: Issuer::Redirect, + }); } if let Some(diff) = object.get("diff") { @@ -335,7 +350,7 @@ impl EventLoopState { pub async fn handle_server_event( &self, event: &EventPayload, - channel_updated: &mut bool, + channel_updated: &mut Option, socket_updated: &mut bool, ) -> Result<(), LiveSocketError> { match &event.event { @@ -371,7 +386,7 @@ impl EventLoopState { .await?; *socket_updated = true; - *channel_updated = true; + *channel_updated = Some(Issuer::AssetChange); } "live_patch" => { let Payload::JSONPayload { json, .. } = &event.payload else { @@ -391,8 +406,9 @@ impl EventLoopState { }; // respect `to` `kind` and `mode` relative to current url base - self.handle_redirect(json, channel_updated, socket_updated) - .await?; + let result = self.handle_redirect(json).await?; + *channel_updated = Some(Issuer::LiveRedirect); + *socket_updated = result.websocket_reconnected; } "redirect" => { let Payload::JSONPayload { json, .. } = &event.payload else { @@ -401,8 +417,9 @@ impl EventLoopState { }; // navigate replacing top, using `to` relative to current url base - self.handle_redirect(json, channel_updated, socket_updated) - .await?; + let result = self.handle_redirect(json).await?; + *channel_updated = Some(Issuer::Redirect); + *socket_updated = result.websocket_reconnected; } _ => {} }, diff --git a/crates/core/src/client/inner/mod.rs b/crates/core/src/client/inner/mod.rs index cb316d6e..d352e9c4 100644 --- a/crates/core/src/client/inner/mod.rs +++ b/crates/core/src/client/inner/mod.rs @@ -82,6 +82,7 @@ impl LiveViewClientInner { Ok(out) } + // not for internal use pub(crate) async fn reconnect( &self, url: String, @@ -89,14 +90,17 @@ impl LiveViewClientInner { join_params: Option>, ) -> Result<(), LiveSocketError> { self.state.reconnect(url, opts, join_params).await?; - self.event_loop.refresh_view(true); + self.event_loop + .refresh_view(true, Issuer::External(NavigationCall::Reconnect)); Ok(()) } + // not for internal use pub(crate) async fn disconnect(&self) -> Result<(), LiveSocketError> { let socket = self.state.socket.try_lock()?.clone(); let _ = socket.disconnect().await; - self.event_loop.refresh_view(true); + self.event_loop + .refresh_view(true, Issuer::External(NavigationCall::Disconnect)); Ok(()) } @@ -161,37 +165,52 @@ impl LiveViewClientInner { Ok(self.state.socket.try_lock()?.status()) } + // not for internal use pub async fn navigate( &self, url: String, opts: NavOptions, ) -> Result { let res = self.state.navigate(url, opts).await; - self.event_loop.handle_navigation_summary(res).await + self.event_loop + .handle_navigation_summary(res, Issuer::External(NavigationCall::Navigate)) + .await } + // not for internal use pub async fn reload(&self, info: NavActionOptions) -> Result { let res = self.state.reload(info).await; - self.event_loop.handle_navigation_summary(res).await + self.event_loop + .handle_navigation_summary(res, Issuer::External(NavigationCall::Reload)) + .await } + // not for internal use pub async fn back(&self, info: NavActionOptions) -> Result { let res = self.state.back(info).await; - self.event_loop.handle_navigation_summary(res).await + self.event_loop + .handle_navigation_summary(res, Issuer::External(NavigationCall::Back)) + .await } + // not for internal use pub async fn forward(&self, info: NavActionOptions) -> Result { let res = self.state.forward(info).await; - self.event_loop.handle_navigation_summary(res).await + self.event_loop + .handle_navigation_summary(res, Issuer::External(NavigationCall::Forward)) + .await } + // not for internal use pub async fn traverse_to( &self, id: HistoryId, info: NavActionOptions, ) -> Result { let res = self.state.traverse_to(id, info).await; - self.event_loop.handle_navigation_summary(res).await + self.event_loop + .handle_navigation_summary(res, Issuer::External(NavigationCall::Traverse)) + .await } pub fn can_go_back(&self) -> bool { diff --git a/crates/core/src/client/tests/lifecycle.rs b/crates/core/src/client/tests/lifecycle.rs index d9cc8a67..43202d65 100644 --- a/crates/core/src/client/tests/lifecycle.rs +++ b/crates/core/src/client/tests/lifecycle.rs @@ -7,8 +7,8 @@ use serde_json::json; use super::{json_payload, HOST}; use crate::{ client::{ - HandlerResponse, LiveChannelStatus, LiveViewClientConfiguration, NavEvent, NavEventHandler, - NavEventType, NavHistoryEntry, NetworkEventHandler, Platform, + HandlerResponse, Issuer, LiveChannelStatus, LiveViewClientConfiguration, NavEvent, + NavEventHandler, NavEventType, NavHistoryEntry, NetworkEventHandler, Platform, }, dom::{self}, live_socket::LiveChannel, @@ -21,7 +21,7 @@ pub enum MockMessage { NetworkEvent(Event, Payload), ChannelStatus(LiveChannelStatus), SocketStatus(SocketStatus), - ViewReload { socket_is_new: bool }, + ViewReload { issuer: Issuer, socket_is_new: bool }, } #[macro_export] @@ -131,13 +131,16 @@ impl NetworkEventHandler for MockNetworkEventHandler { fn handle_view_reloaded( &self, + issuer: Issuer, _new_document: Arc, _new_channel: Arc, _current_socket: Arc, socket_is_new: bool, ) { - self.message_store - .add_message(MockMessage::ViewReload { socket_is_new }); + self.message_store.add_message(MockMessage::ViewReload { + issuer, + socket_is_new, + }); } } From 1ce178e00a9ad4606de0c414417687e0ff2d2ad0 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Thu, 6 Feb 2025 15:47:32 -0800 Subject: [PATCH 04/12] clippy --- crates/core/src/client/inner/event_loop/state.rs | 2 +- crates/core/src/dom/mod.rs | 4 ++-- crates/core/src/live_socket/socket.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/core/src/client/inner/event_loop/state.rs b/crates/core/src/client/inner/event_loop/state.rs index 5019812c..60bbcee4 100644 --- a/crates/core/src/client/inner/event_loop/state.rs +++ b/crates/core/src/client/inner/event_loop/state.rs @@ -10,7 +10,7 @@ use tokio::select; use super::{ClientMessage, LiveViewClientState, NetworkEventHandler}; use crate::{ - client::{inner::NavigationSummary, HistoryId, Issuer, LiveChannelStatus}, + client::{inner::NavigationSummary, Issuer, LiveChannelStatus}, dom::ffi::{self, Document}, error::LiveSocketError, live_socket::{ diff --git a/crates/core/src/dom/mod.rs b/crates/core/src/dom/mod.rs index 31fb2e86..29e37e49 100644 --- a/crates/core/src/dom/mod.rs +++ b/crates/core/src/dom/mod.rs @@ -586,7 +586,7 @@ impl Document { .filter(|attr| { attr.name.name == *"name" && attr.value == Some("csrf-token".to_string()) }) - .last() + .next_back() .is_some() }) // We now need the "content" value @@ -595,7 +595,7 @@ impl Document { .iter() .filter(|attr| attr.name.name == *"content") .map(|attr| attr.value.clone()) - .last() + .next_back() .flatten() }) .last() diff --git a/crates/core/src/live_socket/socket.rs b/crates/core/src/live_socket/socket.rs index 07d83248..fa57e0d9 100644 --- a/crates/core/src/live_socket/socket.rs +++ b/crates/core/src/live_socket/socket.rs @@ -228,7 +228,7 @@ impl SessionData { .iter() .filter(|attr| attr.name.name == "url") .map(|attr| attr.value.clone()) - .last() + .next_back() .flatten() }) .collect(); @@ -246,7 +246,7 @@ impl SessionData { .iter() .filter(|attr| attr.name.name == "src") .map(|attr| attr.value.clone()) - .last() + .next_back() .flatten() }) .filter(|iframe_src| iframe_src == "/phoenix/live_reload/frame") From edd572de0680ab13d9220b8db30d4b8429d603d7 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Fri, 7 Feb 2025 10:33:13 -0800 Subject: [PATCH 05/12] remove tests --- crates/core/src/client/inner/dead_render.rs | 0 crates/core/src/client/inner/navigation.rs | 25 +- crates/core/src/live_socket/mod.rs | 3 - crates/core/src/live_socket/tests/error.rs | 21 -- crates/core/src/live_socket/tests/mod.rs | 213 ------------ .../core/src/live_socket/tests/navigation.rs | 308 ------------------ .../core/src/live_socket/tests/streaming.rs | 65 ---- crates/core/src/live_socket/tests/upload.rs | 233 ------------- 8 files changed, 14 insertions(+), 854 deletions(-) create mode 100644 crates/core/src/client/inner/dead_render.rs delete mode 100644 crates/core/src/live_socket/tests/error.rs delete mode 100644 crates/core/src/live_socket/tests/mod.rs delete mode 100644 crates/core/src/live_socket/tests/navigation.rs delete mode 100644 crates/core/src/live_socket/tests/streaming.rs delete mode 100644 crates/core/src/live_socket/tests/upload.rs diff --git a/crates/core/src/client/inner/dead_render.rs b/crates/core/src/client/inner/dead_render.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/core/src/client/inner/navigation.rs b/crates/core/src/client/inner/navigation.rs index e21572fe..3f7c239e 100644 --- a/crates/core/src/client/inner/navigation.rs +++ b/crates/core/src/client/inner/navigation.rs @@ -329,18 +329,16 @@ mod test { let url = Url::parse(url_str).expect("URL failed to parse"); ctx.navigate(url, NavOptions::default(), true); + let ev = handler.last_event().expect("Missing Event"); assert_eq!( - NavEvent { - event: NavEventType::Push, - to: NavHistoryEntry { - state: None, - id: 1, - url: url_str.to_string(), - }, - ..NavEvent::empty() + NavHistoryEntry { + state: None, + id: 1, + url: url_str.to_string(), }, - handler.last_event().expect("Missing Event") + ev.to ); + assert_eq!(NavEventType::Push, ev.event); } #[test] @@ -373,7 +371,9 @@ mod test { url: first_url_str.to_string(), } .into(), - ..NavEvent::empty() + event: NavEventType::Push, + same_document: false, + info: None, }, handler.last_event().expect("Missing Event") ); @@ -394,7 +394,10 @@ mod test { url: url_str.to_string(), } .into(), - ..NavEvent::empty() + + event: NavEventType::Push, + same_document: false, + info: None, }, handler.last_event().expect("Missing Event") ); diff --git a/crates/core/src/live_socket/mod.rs b/crates/core/src/live_socket/mod.rs index 05afd267..51c304e6 100644 --- a/crates/core/src/live_socket/mod.rs +++ b/crates/core/src/live_socket/mod.rs @@ -2,9 +2,6 @@ mod channel; pub mod navigation; mod socket; -#[cfg(test)] -mod tests; - pub use channel::{LiveChannel, LiveFile}; pub use socket::{ConnectOpts, LiveSocket, Method, SessionData}; diff --git a/crates/core/src/live_socket/tests/error.rs b/crates/core/src/live_socket/tests/error.rs deleted file mode 100644 index 3bcb7ae2..00000000 --- a/crates/core/src/live_socket/tests/error.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; -use crate::error::*; - -#[tokio::test] -async fn dead_render_error() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/doesnt-exist"); - let live_socket_err = - LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()).await; - assert!(live_socket_err.is_err()); - let live_socket_err = live_socket_err.err().unwrap(); - assert!(matches!( - live_socket_err, - LiveSocketError::ConnectionError { .. } - )); - log::debug!("ERROR HTML: {live_socket_err}"); -} diff --git a/crates/core/src/live_socket/tests/mod.rs b/crates/core/src/live_socket/tests/mod.rs deleted file mode 100644 index 1c8df0b8..00000000 --- a/crates/core/src/live_socket/tests/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use super::*; -use crate::{ - callbacks::*, - dom::{NodeData, NodeRef}, -}; -mod error; -mod navigation; -mod streaming; -mod upload; - -#[cfg(target_os = "android")] -const HOST: &str = "10.0.2.2:4001"; - -#[cfg(not(target_os = "android"))] -const HOST: &str = "127.0.0.1:4001"; - -use phoenix_channels_client::ChannelStatus; -use pretty_assertions::assert_eq; - -macro_rules! assert_doc_eq { - ($gold:expr, $test:expr) => {{ - use crate::dom::Document; - let gold = Document::parse($gold).expect("Gold document failed to parse"); - let test = Document::parse($test).expect("Test document failed to parse"); - assert_eq!(gold.to_string(), test.to_string()); - }}; -} - -pub(crate) use assert_doc_eq; -use tokio::sync::mpsc::*; - -struct Inspector { - tx: UnboundedSender<(ChangeType, NodeData)>, - doc: crate::dom::ffi::Document, -} - -impl Inspector { - pub fn new( - doc: crate::dom::ffi::Document, - ) -> (Self, UnboundedReceiver<(ChangeType, NodeData)>) { - let (tx, rx) = unbounded_channel(); - let out = Self { tx, doc }; - (out, rx) - } -} - -/// An extremely simple change handler that reports diffs in order -/// over an unbounded channel -impl DocumentChangeHandler for Inspector { - fn handle_document_change( - &self, - change_type: ChangeType, - _node_ref: Arc, - node_data: NodeData, - _parent: Option>, - ) { - let doc = self.doc.inner(); - - let _test = doc - .try_lock() - .expect("document was locked during change handler!"); - - self.tx - .send((change_type, node_data)) - .expect("Message Never Received."); - } -} - -#[tokio::test] -async fn channels_drop_on_shutdown() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/hello"); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join channel"); - let chan_clone = live_channel.channel().clone(); - let handle = tokio::spawn(async move { - live_channel - .merge_diffs() - .await - .expect("Failed to merge diffs"); - }); - - live_socket - .socket() - .shutdown() - .await - .expect("shutdown error"); - - assert!(handle.is_finished()); - assert_eq!(chan_clone.status(), ChannelStatus::ShutDown); -} - -#[tokio::test] -async fn join_redirect() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/redirect_from"); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join channel"); - - let join_doc = live_channel - .join_document() - .expect("Failed to render join payload"); - - let expected = r#" - - - - Redirected! - -"#; - assert_doc_eq!(expected, join_doc.to_string()); - - let _live_channel = live_socket - .join_livereload_channel() - .await - .expect("Failed to join channel"); -} - -#[tokio::test] -async fn join_live_view() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/hello"); - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let style_urls = live_socket.style_urls(); - let expected_style_urls = vec!["/assets/app.swiftui.styles".to_string()]; - assert_eq!(style_urls, expected_style_urls); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join channel"); - - let join_doc = live_channel - .join_document() - .expect("Failed to render join payload"); - let rendered = format!("{}", join_doc); - let expected = r#" - - - - Hello SwiftUI! - -"#; - assert_doc_eq!(expected, rendered); - - let _live_channel = live_socket - .join_livereload_channel() - .await - .expect("Failed to join channel"); -} - -#[tokio::test] -async fn channel_redirect() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/hello"); - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join channel"); - - //live_channel.channel().shutdown().await.expect("Failed to leave live channel"); - // - // Leave should be: ["4","13","lv:phx-F_azBZxXhBqPjAAm","phx_leave",{}] - live_channel - .channel() - .leave() - .await - .expect("Failed to leave live channel"); - let redirect = format!("http://{HOST}/upload"); - let _live_channel = live_socket - .join_liveview_channel(None, Some(redirect)) - .await - .expect("Failed to join channel"); -} diff --git a/crates/core/src/live_socket/tests/navigation.rs b/crates/core/src/live_socket/tests/navigation.rs deleted file mode 100644 index f0b759d7..00000000 --- a/crates/core/src/live_socket/tests/navigation.rs +++ /dev/null @@ -1,308 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use pretty_assertions::assert_eq; -use reqwest::Url; -use serde::{Deserialize, Serialize}; - -use super::assert_doc_eq; -use crate::{ - callbacks::*, - live_socket::{ - navigation::{NavCtx, NavOptions}, - LiveSocket, - }, -}; - -// Mock event handler used to validate the internal -// navigation objects state. -pub struct NavigationInspector { - last_event: Mutex>, -} - -#[derive(Serialize, Deserialize)] -pub struct EventMetadata { - prevent_default: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct HistoryState { - name: String, -} - -impl NavEventHandler for NavigationInspector { - fn handle_event(&self, event: NavEvent) -> HandlerResponse { - *self.last_event.lock().expect("Lock poisoned!") = Some(event); - HandlerResponse::Default - } -} - -impl NavigationInspector { - pub fn new() -> Self { - Self { - last_event: None.into(), - } - } - - pub fn last_event(&self) -> Option { - self.last_event.lock().expect("Lock poisoned!").clone() - } -} - -impl NavEvent { - // utility function so I can sugar out boiler plate code in tests. - pub fn empty() -> Self { - Self { - to: NavHistoryEntry { - url: String::new(), - id: 0, - state: None, - }, - event: NavEventType::Push, - same_document: false, - from: None, - info: None, - } - } -} - -#[test] -fn basic_internal_nav() { - let handler = Arc::new(NavigationInspector::new()); - let mut ctx = NavCtx::default(); - ctx.set_event_handler(handler.clone()); - - // simple push nav - let url_str = "https://www.website.com/live"; - let url = Url::parse(url_str).expect("URL failed to parse"); - ctx.navigate(url, NavOptions::default(), true); - - assert_eq!( - NavEvent { - event: NavEventType::Push, - to: NavHistoryEntry { - state: None, - id: 1, - url: url_str.to_string(), - }, - ..NavEvent::empty() - }, - handler.last_event().expect("Missing Event") - ); -} - -#[test] -fn basic_internal_navigate_back() { - let handler = Arc::new(NavigationInspector::new()); - let mut ctx = NavCtx::default(); - ctx.set_event_handler(handler.clone()); - - // initial page - let first_url_str = "https://www.website.com/first"; - let url = Url::parse(first_url_str).expect("URL failed to parse"); - ctx.navigate(url, NavOptions::default(), true); - - // second page - let url_str = "https://www.website.com/second"; - let url = Url::parse(url_str).expect("URL failed to parse"); - ctx.navigate(url, NavOptions::default(), true) - .expect("Failed."); - - assert_eq!( - NavEvent { - to: NavHistoryEntry { - state: None, - id: 2, - url: url_str.to_string(), - }, - from: NavHistoryEntry { - state: None, - id: 1, - url: first_url_str.to_string(), - } - .into(), - ..NavEvent::empty() - }, - handler.last_event().expect("Missing Event") - ); - - //go back one view - ctx.back(None, true).expect("Failed Back."); - - assert_eq!( - NavEvent { - to: NavHistoryEntry { - state: None, - id: 1, - url: first_url_str.to_string(), - }, - from: NavHistoryEntry { - state: None, - id: 2, - url: url_str.to_string(), - } - .into(), - ..NavEvent::empty() - }, - handler.last_event().expect("Missing Event") - ); -} - -#[test] -fn test_navigation_with_state() { - let handler = Arc::new(NavigationInspector::new()); - let mut ctx = NavCtx::default(); - ctx.set_event_handler(handler.clone()); - - let url = Url::parse("https://example.com").expect("parse"); - let state = vec![1, 2, 3]; - let info = vec![4, 5, 6]; - - let opts = NavOptions { - state: Some(state.clone()), - extra_event_info: Some(info.clone()), - ..Default::default() - }; - - let id = ctx.navigate(url.clone(), opts, true).expect("nav"); - - let last_ev = handler.last_event().expect("no event."); - assert_eq!(last_ev.info, Some(info)); - - let current = ctx.current().expect("current"); - assert_eq!(current.id, id); - assert_eq!(current.state, Some(state)); -} - -#[test] -fn test_navigation_stack() { - let mut ctx = NavCtx::default(); - let first = Url::parse("https://example.com/first").expect("parse first"); - let second = Url::parse("https://example.com/second").expect("parse second"); - let third = Url::parse("https://example.com/third").expect("parse third"); - - let id1 = ctx - .navigate(first.clone(), NavOptions::default(), true) - .expect("nav first"); - let id2 = ctx - .navigate(second.clone(), NavOptions::default(), true) - .expect("nav second"); - let id3 = ctx - .navigate(third.clone(), NavOptions::default(), true) - .expect("nav third"); - - assert_eq!(ctx.current().expect("current").url, third.to_string()); - - let prev_id = ctx.back(None, true).expect("back"); - assert_eq!(prev_id, id2); - assert_eq!(ctx.current().expect("current").url, second.to_string()); - assert_eq!(ctx.entries().len(), 3); - - let next_id = ctx.forward(None, true).expect("forward"); - assert_eq!(next_id, id3); - assert_eq!(ctx.current().expect("current").url, third.to_string()); - assert_eq!(ctx.entries().len(), 3); - - ctx.traverse_to(id1, None, true) - .expect("Failed to traverse"); - assert_eq!(ctx.current().expect("current").url, first.to_string()); - assert_eq!(ctx.entries().len(), 3); - - ctx.traverse_to(id3, None, true) - .expect("Failed to traverse"); - assert_eq!(ctx.current().expect("current").url, third.to_string()); - assert_eq!(ctx.entries().len(), 3); -} - -#[cfg(target_os = "android")] -const HOST: &str = "10.0.2.2:4001"; - -#[cfg(not(target_os = "android"))] -const HOST: &str = "127.0.0.1:4001"; - -#[test] -fn test_navigation_rollback_forward() { - let mut ctx = NavCtx::default(); - let first = Url::parse("https://example.com/first").expect("parse first"); - let second = Url::parse("https://example.com/second").expect("parse second"); - - let id1 = ctx - .navigate(first.clone(), NavOptions::default(), true) - .expect("nav first"); - - let id2 = ctx - .navigate(second.clone(), NavOptions::default(), true) - .expect("nav second"); - - ctx.back(None, true).expect("back"); - assert_eq!(ctx.current().expect("current").id, id1); - - ctx.forward(None, true).expect("forward"); - assert_eq!(ctx.current().expect("current").id, id2); -} - -#[tokio::test] -async fn basic_nav_flow() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let first = "first_page"; - let second = "second_page"; - let url = format!("http://{HOST}/nav/{first}"); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join channel"); - - let join_doc = live_channel - .join_document() - .expect("Failed to render join payload"); - - let expected = r#" - - - - first_page - - - - NEXT - - - -"#; - - assert_doc_eq!(expected, join_doc.to_string()); - - let url = format!("http://{HOST}/nav/{second}"); - let live_channel = live_socket - .navigate(url, None, Default::default()) - .await - .expect("navigate"); - - let join_doc = live_channel - .join_document() - .expect("Failed to render join payload"); - - let expected = r#" - - - - second_page - - - - NEXT - - - -"#; - - assert_doc_eq!(expected, join_doc.to_string()); -} diff --git a/crates/core/src/live_socket/tests/streaming.rs b/crates/core/src/live_socket/tests/streaming.rs deleted file mode 100644 index fd6c4711..00000000 --- a/crates/core/src/live_socket/tests/streaming.rs +++ /dev/null @@ -1,65 +0,0 @@ -use tokio::sync::mpsc::error::TryRecvError::Empty; - -use super::*; - -// As of this commit the server sends a -// stream even every 10_000 ms -// This sampling interval should catch one -const MAX_TRIES: u64 = 120; -const MS_DELAY: u64 = 100; - -// Tests that streaming connects, and succeeds at parsing at least one delta. -#[tokio::test] -async fn streaming_connect() -> Result<(), String> { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/stream"); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .map_err(|e| format!("Failed to get liveview socket {e}"))?; - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .map_err(|e| format!("Failed to join the liveview channel {e}"))?; - - let doc = live_channel.document(); - let (inspector, mut rx) = Inspector::new(doc); - live_channel.set_event_handler(Box::new(inspector)); - - let chan_clone = live_channel.channel().clone(); - tokio::spawn(async move { - live_channel - .merge_diffs() - .await - .expect("Failed to merge diffs"); - }); - - for _ in 0..MAX_TRIES { - match rx.try_recv() { - Ok(_) => { - chan_clone - .leave() - .await - .map_err(|e| format!("Failed to leave channel {e}"))?; - - return Ok(()); - } - Err(Empty) => { - tokio::time::sleep(Duration::from_millis(MS_DELAY)).await; - } - Err(_) => { - return Err(String::from("Merging Panicked")); - } - } - } - - Err(format!( - "Exceeded {MAX_TRIES} Max tries, waited {} ms", - MAX_TRIES * MS_DELAY - )) -} diff --git a/crates/core/src/live_socket/tests/upload.rs b/crates/core/src/live_socket/tests/upload.rs deleted file mode 100644 index 9489f06d..00000000 --- a/crates/core/src/live_socket/tests/upload.rs +++ /dev/null @@ -1,233 +0,0 @@ -use channel::LiveFile; - -use super::*; -use crate::error::{LiveSocketError, UploadError}; - -// This is from -// https://github.com/image-rs/image/blob/4989d5f83a4a1aaaf7b1fd1f33f7b4db1d3404d3/examples/tile/main.rs -fn get_image(imgx: u32, imgy: u32, suffix: String) -> Vec { - use image::RgbaImage; - let mut img = RgbaImage::new(imgx, imgy); - let tile = image::load_from_memory_with_format( - include_bytes!("../../../tests/support/tinycross.png"), - image::ImageFormat::Png, - ) - .expect("Failed to load example image"); - - use tempfile::tempdir; - let tmp_dir = tempdir().expect("Failed to get tempdir"); - let file_path = tmp_dir.path().join(format!("image-{imgx}-{imgy}.{suffix}")); - - image::imageops::tile(&mut img, &tile); - img.save(file_path.clone()).unwrap(); - - // The format is deduced from the path. - std::fs::read(file_path).expect("Failed to get image") -} - -#[tokio::test] -async fn single_chunk_file() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/upload"); - let image_bytes = get_image(100, 100, "png".to_string()); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join the liveview channel"); - - let phx_upload_id = live_channel - .get_phx_upload_id("avatar") - .expect("No ID for avatar"); - - let gh_favicon = LiveFile::new( - image_bytes.clone(), - "image/png".to_string(), - "avatar".to_string(), - "tile.png".to_string(), - phx_upload_id, - ); - - live_channel - .upload_file(&gh_favicon) - .await - .expect("Failed to upload"); -} - -#[tokio::test] -async fn multi_chunk_text() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/upload"); - let text_bytes = Vec::from_iter(std::iter::repeat_n(b'a', 48_000)); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join the liveview channel"); - - let phx_upload_id = live_channel - .get_phx_upload_id("sample_text") - .expect("No ID for avatar"); - - let me = LiveFile::new( - text_bytes, - "text/plain".to_string(), - "sample_text".to_string(), - "lots_or_as.txt".to_string(), - phx_upload_id, - ); - - live_channel - .upload_file(&me) - .await - .expect("Failed to upload"); -} - -#[tokio::test] -async fn multi_chunk_file() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/upload"); - let image_bytes = get_image(2000, 2000, "png".to_string()); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join the liveview channel"); - - let phx_upload_id = live_channel - .get_phx_upload_id("avatar") - .expect("No ID for avatar"); - - let me = LiveFile::new( - image_bytes.clone(), - "image/png".to_string(), - "avatar".to_string(), - "tile.png".to_string(), - phx_upload_id, - ); - - live_channel - .upload_file(&me) - .await - .expect("Failed to upload"); -} - -#[tokio::test] -async fn error_file_too_large() { - let _ = env_logger::builder() - .parse_default_env() - .filter_level(log::LevelFilter::Debug) - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/upload"); - - // For this file we want to use tiff because it's much biggger than a png. - let image_bytes = get_image(2000, 2000, "tiff".to_string()); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join the liveview channel"); - - let phx_upload_id = live_channel - .get_phx_upload_id("avatar") - .expect("No ID for avatar"); - - let me = LiveFile::new( - image_bytes.clone(), - "avatar".to_string(), - "image/png".to_string(), - "tile.png".to_string(), - phx_upload_id, - ); - - let out = live_channel - .upload_file(&me) - .await - .expect_err("This file is too big and should have failed"); - - match out { - LiveSocketError::Upload { - error: UploadError::FileTooLarge, - } => {} - e => { - panic!("This should be a FileTooLarge, Error instead was: {e:?}"); - } - } -} - -#[tokio::test] -async fn error_incorrect_file_type() { - let _ = env_logger::builder() - .parse_default_env() - .is_test(true) - .try_init(); - - let url = format!("http://{HOST}/upload"); - - // For this file we want to use tiff because it's much biggger than a png. - let image_bytes = get_image(100, 100, "png".to_string()); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join the liveview channel"); - - let phx_upload_id = live_channel - .get_phx_upload_id("avatar") - .expect("No ID for avatar"); - - let me = LiveFile::new( - image_bytes.clone(), - "avatar".to_string(), - "image/png".to_string(), - "tile.png".to_string(), - phx_upload_id, - ); - - let out = live_channel - .upload_file(&me) - .await - .expect_err("This should be an incorrect file error"); - // This hack is required because LiveSocketError doesn't derive from PartialEq - if let LiveSocketError::Upload { - error: UploadError::FileNotAccepted, - } = out - { - } else { - panic!("This should be a FileNotAccepted Error"); - } -} From 4ca675d79b60814e28f4e2162c74e87d6c884566 Mon Sep 17 00:00:00 2001 From: mobile-bungalow Date: Fri, 7 Feb 2025 13:12:47 -0800 Subject: [PATCH 06/12] remove all livesocket --- crates/core/examples/streaming.rs | 26 - crates/core/src/callbacks.rs | 32 +- crates/core/src/client/config.rs | 112 ++- .../{live_socket => client/inner}/channel.rs | 163 +++-- crates/core/src/client/inner/channel_init.rs | 119 --- crates/core/src/client/inner/dead_render.rs | 336 +++++++++ .../core/src/client/inner/event_loop/state.rs | 8 +- crates/core/src/client/inner/mod.rs | 43 +- crates/core/src/client/inner/navigation.rs | 4 +- crates/core/src/client/mod.rs | 10 +- crates/core/src/client/tests/lifecycle.rs | 5 +- crates/core/src/client/tests/upload.rs | 3 +- crates/core/src/lib.rs | 3 - crates/core/src/live_socket/mod.rs | 23 - crates/core/src/live_socket/navigation/ffi.rs | 286 -------- crates/core/src/live_socket/navigation/mod.rs | 268 ------- crates/core/src/live_socket/socket.rs | 684 ------------------ 17 files changed, 629 insertions(+), 1496 deletions(-) delete mode 100644 crates/core/examples/streaming.rs rename crates/core/src/{live_socket => client/inner}/channel.rs (80%) delete mode 100644 crates/core/src/client/inner/channel_init.rs delete mode 100644 crates/core/src/live_socket/mod.rs delete mode 100644 crates/core/src/live_socket/navigation/ffi.rs delete mode 100644 crates/core/src/live_socket/navigation/mod.rs delete mode 100644 crates/core/src/live_socket/socket.rs diff --git a/crates/core/examples/streaming.rs b/crates/core/examples/streaming.rs deleted file mode 100644 index 43b0fc01..00000000 --- a/crates/core/examples/streaming.rs +++ /dev/null @@ -1,26 +0,0 @@ -use liveview_native_core::live_socket::LiveSocket; - -#[cfg(target_os = "android")] -const HOST: &str = "10.0.2.2:4001"; - -#[cfg(not(target_os = "android"))] -const HOST: &str = "127.0.0.1:4001"; - -#[tokio::main] -async fn main() { - let _ = env_logger::builder().parse_default_env().try_init(); - - let url = format!("http://{HOST}/stream"); - - let live_socket = LiveSocket::new(url.to_string(), "swiftui".into(), Default::default()) - .await - .expect("Failed to get liveview socket"); - let live_channel = live_socket - .join_liveview_channel(None, None) - .await - .expect("Failed to join the liveview channel"); - live_channel - .merge_diffs() - .await - .expect("Failed to merge diffs"); -} diff --git a/crates/core/src/callbacks.rs b/crates/core/src/callbacks.rs index d21fdc04..91f82639 100644 --- a/crates/core/src/callbacks.rs +++ b/crates/core/src/callbacks.rs @@ -2,10 +2,14 @@ use std::sync::Arc; #[cfg(feature = "liveview-channels")] use phoenix_channels_client::{Socket, SocketStatus}; +use reqwest::Url; -use crate::dom::{NodeData, NodeRef}; #[cfg(feature = "liveview-channels")] -use crate::{dom::ffi::Document, live_socket::LiveChannel}; +use crate::dom::ffi::Document; +use crate::{ + client::LiveChannel, + dom::{NodeData, NodeRef}, +}; /// Provides secure persistent storage for session data like cookies. /// Implementations should handle platform-specific storage (e.g. NSUserDefaults on iOS) @@ -90,6 +94,30 @@ pub struct NavEvent { pub info: Option>, } +impl NavEvent { + pub(crate) fn new( + event: NavEventType, + to: NavHistoryEntry, + from: Option, + info: Option>, + ) -> Self { + let new_url = Url::parse(&to.url).ok(); + let old_url = from.as_ref().and_then(|dest| Url::parse(&dest.url).ok()); + + let same_document = old_url + .zip(new_url) + .is_some_and(|(old, new)| old.path() == new.path()); + + NavEvent { + event, + same_document, + from, + to, + info, + } + } +} + #[derive(Copy, Clone, Debug, Eq, PartialEq, uniffi::Enum)] pub enum LiveChannelStatus { /// [Channel] is waiting for the [Socket](crate::Socket) to diff --git a/crates/core/src/client/config.rs b/crates/core/src/client/config.rs index 787a46c5..78dcf79c 100644 --- a/crates/core/src/client/config.rs +++ b/crates/core/src/client/config.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use phoenix_channels_client::JSON; -use crate::{callbacks::*, live_socket::Method}; +use crate::callbacks::*; #[derive(uniffi::Enum, Debug, Clone, Default, Copy)] pub enum LogLevel { @@ -135,3 +135,113 @@ impl std::fmt::Debug for LiveViewClientConfiguration { .finish() } } + +/// An action taken with respect to the history stack +/// when [NavCtx::navigate] is executed. defaults to +/// Push behavior. +#[derive(uniffi::Enum, Default, Clone)] +pub enum NavAction { + /// Push the navigation event onto the history stack. + #[default] + Push, + /// Replace the current top of the history stack with this navigation event. + Replace, +} + +/// Options for calls to [NavCtx::navigate] and the external [LiveViewClient::navigate] function +/// Slightly different from [NavActionOptions] +#[derive(Default, uniffi::Record)] +pub struct NavOptions { + /// Additional params to be passed upon joining the liveview channel. + #[uniffi(default = None)] + pub join_params: Option>, + /// see [NavAction], defaults to [NavAction::Push]. + #[uniffi(default = None)] + pub action: Option, + /// Ephemeral extra information to be pushed to the even handler. + #[uniffi(default = None)] + pub extra_event_info: Option>, + /// Persistent state, intended to be deserialized for user specific purposes when + /// revisiting a given view. + #[uniffi(default = None)] + pub state: Option>, +} + +#[derive(Default, uniffi::Record)] +pub struct NavActionOptions { + /// Additional params to be passed upon joining the liveview channel. + #[uniffi(default = None)] + pub join_params: Option>, + /// Ephemeral extra information to be pushed to the even handler. + #[uniffi(default = None)] + pub extra_event_info: Option>, +} + +/// Connection Options for the initial dead render fetch +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] +pub struct DeadRenderFetchOpts { + #[uniffi(default = None)] + pub headers: Option>, + #[uniffi(default = None)] + pub body: Option>, + #[uniffi(default = None)] + pub method: Option, +} + +impl Default for DeadRenderFetchOpts { + fn default() -> Self { + Self { + headers: None, + body: None, + method: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)] +#[repr(u8)] +pub enum Method { + Get = 0, + Options, + Post, + Put, + Delete, + Head, + Trace, + Connect, + Patch, +} + +use reqwest::Method as ReqMethod; +impl From for ReqMethod { + fn from(val: Method) -> ReqMethod { + match val { + Method::Options => ReqMethod::OPTIONS, + Method::Get => ReqMethod::GET, + Method::Post => ReqMethod::POST, + Method::Put => ReqMethod::PUT, + Method::Delete => ReqMethod::DELETE, + Method::Head => ReqMethod::HEAD, + Method::Trace => ReqMethod::TRACE, + Method::Connect => ReqMethod::CONNECT, + Method::Patch => ReqMethod::PATCH, + } + } +} + +pub struct UploadConfig { + pub chunk_size: u64, + pub max_file_size: u64, + pub max_entries: u64, +} + +/// Defaults from https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#allow_upload/3 +impl Default for UploadConfig { + fn default() -> Self { + Self { + chunk_size: 64_000, + max_file_size: 8000000, + max_entries: 1, + } + } +} diff --git a/crates/core/src/live_socket/channel.rs b/crates/core/src/client/inner/channel.rs similarity index 80% rename from crates/core/src/live_socket/channel.rs rename to crates/core/src/client/inner/channel.rs index f3257f04..20558f19 100644 --- a/crates/core/src/live_socket/channel.rs +++ b/crates/core/src/client/inner/channel.rs @@ -1,15 +1,19 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; -use futures::{future::FutureExt, pin_mut, select}; -use log::{debug, error}; +use log::{debug, error, trace}; use phoenix_channels_client::{Channel, Event, Number, Payload, Socket, Topic, JSON}; -use super::UploadConfig; +use super::{dead_render::SessionData, LiveViewClientConfiguration}; use crate::{ callbacks::*, + client::UploadConfig, diff::fragment::{Root, RootDiff}, dom::{ffi::Document as FFiDocument, AttributeName, AttributeValue, Document, Selector}, - error::*, + error::{LiveSocketError, *}, }; #[derive(uniffi::Object)] @@ -162,52 +166,6 @@ impl LiveChannel { Ok(upload_id) } - /// Blocks indefinitely, processing changes to the document using the user provided callback - /// In `set_event_handler` - pub async fn merge_diffs(&self) -> Result<(), LiveSocketError> { - // TODO: This should probably take the event closure to send changes back to swift/kotlin - let document = self.document.clone(); - let events = self.channel.events(); - let statuses = self.channel.statuses(); - loop { - let event = events.event().fuse(); - let status = statuses.status().fuse(); - - pin_mut!(event, status); - - select! { - e = event => { - let e = e?; - match e.event { - Event::Phoenix { phoenix } => { - error!("Phoenix Event for {phoenix:?} is unimplemented"); - } - Event::User { user } => { - if user == "diff" { - let Payload::JSONPayload { json } = e.payload else { - error!("Diff was not json!"); - continue; - }; - - debug!("PAYLOAD: {json:?}"); - // This function merges and uses the event handler set in `set_event_handler` - // which will call back into the Swift/Kotlin. - document.merge_fragment_json(&json.to_string())?; - } - } - }; - } - new_status = status => { - match new_status? { - phoenix_channels_client::ChannelStatus::Left => return Ok(()), - phoenix_channels_client::ChannelStatus::ShutDown => return Ok(()), - _ => {}, - } - } - }; - } - } - pub fn join_payload(&self) -> Payload { self.join_payload.clone() } @@ -451,3 +409,106 @@ impl LiveChannel { Ok(()) } } + +const LVN_VSN: &str = "2.0.0"; +const LVN_VSN_KEY: &str = "vsn"; + +/// TODO: Post refactor turn this into a private constructor on a LiveChannel +pub async fn join_liveview_channel( + socket: &Mutex>, + session_data: &Mutex, + additional_params: &Option>, + redirect: Option, + ws_timeout: std::time::Duration, +) -> Result, LiveSocketError> { + let sock = socket.try_lock()?.clone(); + sock.connect(ws_timeout).await?; + + let sent_join_payload = session_data + .try_lock()? + .create_join_payload(additional_params, redirect); + let topic = Topic::from_string(format!("lv:{}", session_data.try_lock()?.phx_id)); + let channel = sock.channel(topic, Some(sent_join_payload)).await?; + + let join_payload = channel.join(ws_timeout).await?; + + trace!("Join payload: {join_payload:#?}"); + let document = match join_payload { + Payload::JSONPayload { + json: JSON::Object { ref object }, + } => { + if let Some(rendered) = object.get("rendered") { + let rendered = rendered.to_string(); + let root: RootDiff = serde_json::from_str(rendered.as_str())?; + trace!("root diff: {root:#?}"); + let root: Root = root.try_into()?; + let rendered: String = root.clone().try_into()?; + let mut document = Document::parse(&rendered)?; + document.fragment_template = Some(root); + Some(document) + } else { + None + } + } + _ => None, + } + .ok_or(LiveSocketError::NoDocumentInJoinPayload)?; + + Ok(LiveChannel { + channel, + join_payload, + join_params: additional_params.clone().unwrap_or_default(), + socket: socket.try_lock()?.clone(), + document: document.into(), + timeout: ws_timeout, + } + .into()) +} + +pub async fn join_livereload_channel( + config: &LiveViewClientConfiguration, + socket: &Mutex>, + session_data: &Mutex, + cookies: Option>, +) -> Result, LiveSocketError> { + let ws_timeout = Duration::from_millis(config.websocket_timeout); + + let mut url = session_data.try_lock()?.url.clone(); + + let websocket_scheme = match url.scheme() { + "https" => "wss", + "http" => "ws", + scheme => { + return Err(LiveSocketError::SchemeNotSupported { + scheme: scheme.to_string(), + }) + } + }; + let _ = url.set_scheme(websocket_scheme); + url.set_path("phoenix/live_reload/socket/websocket"); + url.query_pairs_mut().append_pair(LVN_VSN_KEY, LVN_VSN); + + let new_socket = Socket::spawn(url.clone(), cookies).await?; + new_socket.connect(ws_timeout).await?; + + debug!("Joining live reload channel on url {url}"); + let channel = new_socket + .channel(Topic::from_string("phoenix:live_reload".to_string()), None) + .await?; + + debug!("Created channel for live reload socket"); + let join_payload = channel.join(ws_timeout).await?; + let document = Document::empty(); + + Ok(LiveChannel { + channel, + join_params: Default::default(), + join_payload, + // Q: I copy pasted this from the old implementation, + // why use the old socket ? + socket: socket.try_lock()?.clone(), + document: document.into(), + timeout: ws_timeout, + } + .into()) +} diff --git a/crates/core/src/client/inner/channel_init.rs b/crates/core/src/client/inner/channel_init.rs deleted file mode 100644 index 5177e8e6..00000000 --- a/crates/core/src/client/inner/channel_init.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, - time::Duration, -}; - -use log::{debug, trace}; -use phoenix_channels_client::{Payload, Socket, Topic, JSON}; - -use super::LiveViewClientConfiguration; -use crate::{ - diff::fragment::{Root, RootDiff}, - dom::Document, - error::LiveSocketError, - live_socket::{LiveChannel, SessionData}, -}; - -const LVN_VSN: &str = "2.0.0"; -const LVN_VSN_KEY: &str = "vsn"; - -/// TODO: Post refactor turn this into a private constructor on a LiveChannel -pub async fn join_liveview_channel( - socket: &Mutex>, - session_data: &Mutex, - additional_params: &Option>, - redirect: Option, - ws_timeout: std::time::Duration, -) -> Result, LiveSocketError> { - let sock = socket.try_lock()?.clone(); - sock.connect(ws_timeout).await?; - - let sent_join_payload = session_data - .try_lock()? - .create_join_payload(additional_params, redirect); - let topic = Topic::from_string(format!("lv:{}", session_data.try_lock()?.phx_id)); - let channel = sock.channel(topic, Some(sent_join_payload)).await?; - - let join_payload = channel.join(ws_timeout).await?; - - trace!("Join payload: {join_payload:#?}"); - let document = match join_payload { - Payload::JSONPayload { - json: JSON::Object { ref object }, - } => { - if let Some(rendered) = object.get("rendered") { - let rendered = rendered.to_string(); - let root: RootDiff = serde_json::from_str(rendered.as_str())?; - trace!("root diff: {root:#?}"); - let root: Root = root.try_into()?; - let rendered: String = root.clone().try_into()?; - let mut document = Document::parse(&rendered)?; - document.fragment_template = Some(root); - Some(document) - } else { - None - } - } - _ => None, - } - .ok_or(LiveSocketError::NoDocumentInJoinPayload)?; - - Ok(LiveChannel { - channel, - join_payload, - join_params: additional_params.clone().unwrap_or_default(), - socket: socket.try_lock()?.clone(), - document: document.into(), - timeout: ws_timeout, - } - .into()) -} - -pub async fn join_livereload_channel( - config: &LiveViewClientConfiguration, - socket: &Mutex>, - session_data: &Mutex, - cookies: Option>, -) -> Result, LiveSocketError> { - let ws_timeout = Duration::from_millis(config.websocket_timeout); - - let mut url = session_data.try_lock()?.url.clone(); - - let websocket_scheme = match url.scheme() { - "https" => "wss", - "http" => "ws", - scheme => { - return Err(LiveSocketError::SchemeNotSupported { - scheme: scheme.to_string(), - }) - } - }; - let _ = url.set_scheme(websocket_scheme); - url.set_path("phoenix/live_reload/socket/websocket"); - url.query_pairs_mut().append_pair(LVN_VSN_KEY, LVN_VSN); - - let new_socket = Socket::spawn(url.clone(), cookies).await?; - new_socket.connect(ws_timeout).await?; - - debug!("Joining live reload channel on url {url}"); - let channel = new_socket - .channel(Topic::from_string("phoenix:live_reload".to_string()), None) - .await?; - - debug!("Created channel for live reload socket"); - let join_payload = channel.join(ws_timeout).await?; - let document = Document::empty(); - - Ok(LiveChannel { - channel, - join_params: Default::default(), - join_payload, - // Q: I copy pasted this from the old implementation, - // why use the old socket ? - socket: socket.try_lock()?.clone(), - document: document.into(), - timeout: ws_timeout, - } - .into()) -} diff --git a/crates/core/src/client/inner/dead_render.rs b/crates/core/src/client/inner/dead_render.rs index e69de29b..9d642cbf 100644 --- a/crates/core/src/client/inner/dead_render.rs +++ b/crates/core/src/client/inner/dead_render.rs @@ -0,0 +1,336 @@ +use core::str; +use std::{collections::HashMap, time::Duration}; + +use log::{debug, trace}; +use phoenix_channels_client::{Payload, JSON}; +use reqwest::{header::LOCATION, Client, Url}; +use serde::Serialize; + +use crate::{ + client::{DeadRenderFetchOpts, Method}, + dom::{AttributeName, Document, ElementName, Selector}, + error::LiveSocketError, +}; + +const MAX_REDIRECTS: usize = 10; +const LVN_VSN: &str = "2.0.0"; +const LVN_VSN_KEY: &str = "vsn"; +const CSRF_KEY: &str = "_csrf_token"; +const MOUNT_KEY: &str = "_mounts"; +const FMT_KEY: &str = "_format"; + +/// Static information ascertained from the dead render when connecting. +#[derive(Clone, Debug)] +pub struct SessionData { + pub connect_opts: DeadRenderFetchOpts, + /// Cross site request forgery, security token, sent with dead render. + pub csrf_token: String, + /// The id of the phoenix channel to join. + pub phx_id: String, + pub phx_static: String, + pub phx_session: String, + pub url: Url, + /// One of `swift`, `kotlin` or `html` indicating the developer platform. + pub format: String, + /// An html page that on the web would be used to bootstrap the web socket connection. + pub dead_render: Document, + pub style_urls: Vec, + /// Whether or not the dead render contains a live reload iframe for development mode. + pub has_live_reload: bool, +} + +//TODO: Move this into the protocol module when it exists +/// The expected structure of a json payload send upon joining a liveview channel +#[derive(Serialize)] +struct JoinRequestPayload { + #[serde(rename = "static")] + static_token: String, + session: String, + #[serde(flatten)] + url_or_redirect: UrlOrRedirect, + params: HashMap, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum UrlOrRedirect { + Url { url: String }, + Redirect { redirect: String }, +} + +impl SessionData { + pub async fn request( + url: &Url, + format: &String, + timeout: Duration, + connect_opts: DeadRenderFetchOpts, + client: Client, + ) -> Result { + // NEED: + // these from inside data-phx-main + // data-phx-session, + // data-phx-static + // id + // + // Top level: + // csrf-token + // "iframe[src=\"/phoenix/live_reload/frame\"]" + + let (dead_render, url) = + get_dead_render(url, format, &connect_opts, timeout, client).await?; + //TODO: remove cookies, pull it from the cookie client cookie store. + + log::trace!("dead render retrieved:\n {dead_render}"); + let csrf_token = dead_render + .get_csrf_token() + .ok_or(LiveSocketError::CSRFTokenMissing)?; + + let mut phx_id: Option = None; + let mut phx_static: Option = None; + let mut phx_session: Option = None; + + let main_div_attributes = dead_render + .select(Selector::Attribute(AttributeName { + name: "data-phx-main".into(), + namespace: None, + })) + .last(); + + trace!("main div attributes: {main_div_attributes:?}"); + + let main_div_attributes = dead_render + .select(Selector::Attribute(AttributeName { + namespace: None, + name: "data-phx-main".into(), + })) + .last() + .map(|node_ref| dead_render.get(node_ref)) + .map(|main_div| main_div.attributes()) + .ok_or(LiveSocketError::PhoenixMainMissing)?; + + for attr in main_div_attributes { + if attr.name.name == "id" { + phx_id.clone_from(&attr.value) + } else if attr.name.name == "data-phx-session" { + phx_session.clone_from(&attr.value) + } else if attr.name.name == "data-phx-static" { + phx_static.clone_from(&attr.value) + } + } + let phx_id = phx_id.ok_or(LiveSocketError::PhoenixIDMissing)?; + let phx_static = phx_static.ok_or(LiveSocketError::PhoenixStaticMissing)?; + let phx_session = phx_session.ok_or(LiveSocketError::PhoenixSessionMissing)?; + trace!("phx_id = {phx_id:?}, session = {phx_session:?}, static = {phx_static:?}"); + + // A Style looks like: + //