Skip to content

Commit

Permalink
chore: tmp commit before rollback
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Apr 30, 2022
1 parent 295954e commit 15187b5
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 70 deletions.
39 changes: 35 additions & 4 deletions examples/core/basic/src/templates/index.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
use perseus::{Html, RenderFnResultWithCause, SsrNode, Template};
use sycamore::prelude::{view, Scope, View};
use perseus::{Html, RenderFnResultWithCause, SsrNode, Template, state::{Freeze, MakeRx, MakeUnrx}};
use serde::{Deserialize, Serialize};
use sycamore::prelude::{view, Scope, View, Signal};

#[perseus::make_rx(IndexPageStateRx)]
// #[perseus::make_rx(IndexPageStateRx)]
#[derive(Serialize, Deserialize)]
pub struct IndexPageState {
pub greeting: String,
}
// Prepend lifetime to generics of reactive version
#[derive(Clone)]
pub struct IndexPageStateRx<'rx> {
pub greeting: &'rx Signal<String>
}
impl<'rx> MakeRx<'rx> for IndexPageState {
type Rx = IndexPageStateRx<'rx>;
fn make_rx(self, cx: Scope<'rx>) -> Self::Rx {
IndexPageStateRx {
greeting: sycamore::prelude::create_signal(cx, self.greeting)
}
}
}
impl<'rx> MakeUnrx<'rx> for IndexPageStateRx<'rx> {
type Unrx = IndexPageState;
fn make_unrx(self) -> Self::Unrx {
IndexPageState {
greeting: (*self.greeting.get_untracked()).clone()
}
}
}
impl Freeze for IndexPageStateRx<'_> {
fn freeze(&self) -> ::std::string::String {
let unrx = IndexPageState {
greeting: (*self.greeting.get_untracked()).clone()
};
::serde_json::to_string(&unrx).unwrap()
}
}

#[perseus::template_rx]
pub fn index_page<G: Html>(cx: Scope, state: IndexPageStateRx) -> View<G> {
pub fn index_page<'rx, G: Html>(cx: Scope<'rx>, state: IndexPageStateRx<'rx>) -> View<G> {
view! { cx,
p { (state.greeting.get()) }
a(href = "about", id = "about-link") { "About!" }
Expand Down
19 changes: 12 additions & 7 deletions packages/perseus-macro/src/rx_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;

use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Ident, ItemStruct, Lit, Meta, NestedMeta, Result};
use syn::{Ident, ItemStruct, Lifetime, LifetimeDef, Lit, Meta, NestedMeta, Result, GenericParam};

pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
// So that we don't have to worry about unit structs or unnamed fields, we'll just copy the struct and change the parts we want to
Expand Down Expand Up @@ -85,6 +85,9 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
}
orig_struct.attrs = filtered_attrs_orig;
new_struct.attrs = filtered_attrs_new;
// Now add the `'rx` lifetime to the new `struct`'s generics (we'll need it for all the `Signal`s)
new_struct.generics.params.insert(0, GenericParam::Lifetime(LifetimeDef::new(Lifetime::new("'rx", Span::call_site()))));
let new_generics = &new_struct.generics;

match new_struct.fields {
syn::Fields::Named(ref mut fields) => {
Expand All @@ -95,7 +98,7 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
field.ty = if let Some(wrapper_ty) = wrapper_ty {
syn::Type::Verbatim(quote!(#wrapper_ty))
} else {
syn::Type::Verbatim(quote!(::sycamore::prelude::RcSignal<#orig_ty>))
syn::Type::Verbatim(quote!(&'rx ::sycamore::prelude::Signal<#orig_ty>))
};
// Remove any `serde` attributes (Serde can't be used with the reactive version)
let mut new_attrs = Vec::new();
Expand Down Expand Up @@ -133,7 +136,8 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
})
} else {
field_assignments.extend(quote! {
#field_name: ::sycamore::prelude::create_rc_signal(self.#field_name),
// The `cx` parameter will be available where we interpolate this
#field_name: ::sycamore::prelude::create_signal(self.#field_name, cx),
});
}
}
Expand Down Expand Up @@ -180,23 +184,24 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
// We add a Serde derivation because it will always be necessary for Perseus on the original `struct`, and it's really difficult and brittle to filter it out
#[derive(::serde::Serialize, ::serde::Deserialize, ::std::clone::Clone)]
#orig_struct
// BUG The lifetime parameter `'rx` is unconstrained here
impl #generics ::perseus::state::MakeRx for #ident #generics {
type Rx = #name #generics;
fn make_rx(self) -> #name #generics {
type Rx = #name #new_generics;
fn make_rx<'rx>(self, cx: ::sycamore::prelude::Scope<'rx>) -> #name #new_generics {
use ::perseus::state::MakeRx;
#make_rx_fields
}
}
#[derive(::std::clone::Clone)]
#new_struct
impl #generics ::perseus::state::MakeUnrx for #name #generics {
impl #new_generics ::perseus::state::MakeUnrx for #name #new_generics {
type Unrx = #ident #generics;
fn make_unrx(self) -> #ident #generics {
use ::perseus::state::MakeUnrx;
#make_unrx_fields
}
}
impl #generics ::perseus::state::Freeze for #name #generics {
impl #new_generics ::perseus::state::Freeze for #name #new_generics {
fn freeze(&self) -> ::std::string::String {
use ::perseus::state::MakeUnrx;
let unrx = #make_unrx_fields;
Expand Down
29 changes: 13 additions & 16 deletions packages/perseus-macro/src/template_rx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// This isn't completely pointless, this method mutates as well to set up the global state as appropriate
// If there's no active or frozen global state, then we'll fall back to the generated one from the server (which we know will be there, since if this is `None` we must be
// the first page to access the global state).
if render_ctx.get_active_or_frozen_global_state::<#global_state_rx>().is_none() {
if render_ctx.get_active_or_frozen_global_state::<#global_state_rx>(cx).is_none() {
// Because this came from the server, we assume it's valid
render_ctx.register_global_state_str::<#global_state_rx>(&props.global_state.unwrap()).unwrap();
render_ctx.register_global_state_str::<#global_state_rx>(&props.global_state.unwrap(), cx).unwrap();
}

#live_reload_frag
Expand All @@ -224,7 +224,6 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// The user's function
// We know this won't be async because Sycamore doesn't allow that
#(#attrs)*
#[::sycamore::component]
// WARNING: I removed the `#state_arg` here because the new Sycamore throws errors for unit type props (possible consequences?)
fn #component_name #generics(#cx_arg) -> #return_type {
let #global_state_arg_pat: #global_state_rx = {
Expand All @@ -236,7 +235,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
#block
}

#component_name(cx, ())
#component_name(cx)
}
},
// This template takes its own state and global state
Expand All @@ -249,9 +248,9 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// This isn't completely pointless, this method mutates as well to set up the global state as appropriate
// If there's no active or frozen global state, then we'll fall back to the generated one from the server (which we know will be there, since if this is `None` we must be
// the first page to access the global state).
if render_ctx.get_active_or_frozen_global_state::<#global_state_rx>().is_none() {
if render_ctx.get_active_or_frozen_global_state::<#global_state_rx>(cx).is_none() {
// Because this came from the server, we assume it's valid
render_ctx.register_global_state_str::<#global_state_rx>(&props.global_state.unwrap()).unwrap();
render_ctx.register_global_state_str::<#global_state_rx>(&props.global_state.unwrap(), cx).unwrap();
}

#live_reload_frag
Expand All @@ -262,10 +261,9 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// The user's function
// We know this won't be async because Sycamore doesn't allow that
#(#attrs)*
#[::sycamore::component]
fn #component_name #generics(#cx_arg, #state_arg) -> #return_type {
let #global_state_arg_pat: #global_state_rx = {
let global_state = ::perseus::get_render_ctx!(cx).global_state.get().0;
let global_state = &::perseus::get_render_ctx!(cx).global_state.get().0;
// We can guarantee that it will downcast correctly now, because we'll only invoke the component from this function, which sets up the global state correctly
let global_state_ref = global_state.as_any().downcast_ref::<#global_state_rx>().unwrap();
(*global_state_ref).clone()
Expand All @@ -278,14 +276,14 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// If they are, we'll use them (so state persists for templates across the whole app)
let mut render_ctx = ::perseus::get_render_ctx!(cx);
// The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do!
match render_ctx.get_active_or_frozen_page_state::<#rx_props_ty>(&props.path) {
match render_ctx.get_active_or_frozen_page_state::<#rx_props_ty>(&props.path, cx) {
::std::option::Option::Some(existing_state) => existing_state,
// Again, frozen state has been dealt with already, so we'll fall back to generated state
::std::option::Option::None => {
// Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking)
// We also assume that any state we have is valid because it comes from the server
// The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message
render_ctx.register_page_state_str::<#rx_props_ty>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap()
render_ctx.register_page_state_str::<#rx_props_ty>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string)), cx).unwrap()
}
}
};
Expand All @@ -306,7 +304,8 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
};
let name_string = name.to_string();
quote! {
#vis fn #name<G: ::sycamore::prelude::Html>(cx: ::sycamore::prelude::Scope, props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
// BUG: Remove this `'rx` here and get the lifetimes to work properly (DO NOT SHIP THIS!!!)
#vis fn #name<'rx, G: ::sycamore::prelude::Html>(cx: ::sycamore::prelude::Scope, props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
use ::perseus::state::MakeRx;

#[cfg(target_arch = "wasm32")]
Expand All @@ -317,7 +316,6 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// The user's function, with Sycamore component annotations and the like preserved
// We know this won't be async because Sycamore doesn't allow that
#(#attrs)*
#[::sycamore::component]
fn #component_name #generics(#cx_arg, #arg) -> #return_type {
#block
}
Expand All @@ -327,14 +325,14 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// If they are, we'll use them (so state persists for templates across the whole app)
let mut render_ctx = ::perseus::get_render_ctx!(cx);
// The render context will automatically handle prioritizing frozen or active state for us for this page as long as we have a reactive state type, which we do!
match render_ctx.get_active_or_frozen_page_state::<#rx_props_ty>(&props.path) {
match render_ctx.get_active_or_frozen_page_state::<#rx_props_ty>(&props.path, cx) {
::std::option::Option::Some(existing_state) => existing_state,
// Again, frozen state has been dealt with already, so we'll fall back to generated state
::std::option::Option::None => {
// Again, the render context can do the heavy lifting for us (this returns what we need, and can do type checking)
// We also assume that any state we have is valid because it comes from the server
// The user really should have a generation function, but if they don't then they'd get a panic, so give them a nice error message
render_ctx.register_page_state_str::<#rx_props_ty>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string))).unwrap()
render_ctx.register_page_state_str::<#rx_props_ty>(&props.path, &props.state.unwrap_or_else(|| panic!("template `{}` takes a state, but no state generation functions were provided (please add at least one to use state)", #name_string)), cx).unwrap()
}
}
};
Expand All @@ -358,12 +356,11 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// The user's function, with Sycamore component annotations and the like preserved
// We know this won't be async because Sycamore doesn't allow that
#(#attrs)*
#[::sycamore::component]
fn #component_name #generics(#cx_arg) -> #return_type {
#block
}

#component_name(cx, ())
#component_name(cx)
}
}
} else {
Expand Down
10 changes: 5 additions & 5 deletions packages/perseus/src/state/page_state_store.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use sycamore::prelude::{use_context, Scope, Signal};

use crate::{state::AnyFreeze, utils::provide_context_signal_replace};
use crate::{state::Freeze, utils::provide_context_signal_replace};
use std::collections::HashMap;
use std::rc::Rc;

// TODO Change this to a direct reference if possible (using `Signal`s etc.)
#[derive(Clone)]
struct PssMap(HashMap<String, Rc<dyn AnyFreeze>>);
struct PssMap<'a>(HashMap<String, Rc<dyn Freeze + 'a>>);

/// A container for page state in Perseus. This is designed as a context store, in which one of each type can be stored. Therefore, it acts very similarly to Sycamore's context system,
/// though it's specifically designed for each page to store one reactive properties object. In theory, you could interact with this entirely independently of Perseus' state interface,
Expand All @@ -18,7 +18,7 @@ pub struct PageStateStore<'a> {
/// A map of type IDs to anything, allowing one storage of each type (each type is intended to a properties `struct` for a template). Entries must be `Clone`able because we assume them
/// to be `Signal`s or `struct`s composed of `Signal`s.
// Technically, this should be `Any + Clone`, but that's not possible without something like `dyn_clone`, and we don't need it because we can restrict on the methods instead!
map: &'a Signal<PssMap>,
map: &'a Signal<PssMap<'a>>,
}
impl<'a> PageStateStore<'a> {
/// Creates a new, empty `PageStateStore`. This inserts the properties into the context of the given reactive scope and then mirrors them.
Expand All @@ -34,15 +34,15 @@ impl<'a> PageStateStore<'a> {
}
}
/// Gets an element out of the state by its type and URL. If the element stored for the given URL doesn't match the provided type, `None` will be returned.
pub fn get<T: AnyFreeze + Clone>(&self, url: &str) -> Option<T> {
pub fn get<T: Freeze + Clone>(&self, url: &str) -> Option<T> {
self.map
.get()
.0
.get(url)
.and_then(|val| val.as_any().downcast_ref::<T>().map(|val| (*val).clone()))
}
/// Adds a new element to the state by its URL. Any existing element with the same URL will be silently overriden (use `.contains()` to check first if needed).
pub fn add<T: AnyFreeze + Clone>(&self, url: &str, val: T) {
pub fn add<T: Freeze + Clone + 'a>(&self, url: &str, val: T) {
self.map.modify().0.insert(url.to_string(), Rc::new(val));
}
/// Checks if the state contains an entry for the given URL.
Expand Down
13 changes: 7 additions & 6 deletions packages/perseus/src/state/rx_state.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use serde::{Deserialize, Serialize};
use sycamore::prelude::Scope;
use std::any::Any;

/// A trait for `struct`s that can be made reactive. Typically, this will be derived with the `#[make_rx]` macro, though it can be implemented manually if you have more niche requirements.
pub trait MakeRx {
pub trait MakeRx<'rx> {
/// The type of the reactive version that we'll convert to. By having this as an associated type, we can associate the reactive type with the unreactive, meaning greater inference
/// and fewer arguments that the user needs to provide to macros.
type Rx: MakeUnrx;
/// Transforms an instance of the `struct` into its reactive version.
fn make_rx(self) -> Self::Rx;
type Rx: MakeUnrx<'rx> + 'rx;
/// Transforms an instance of the `struct` into its reactive version, allocating `Signal`s on the given reactive scope.
fn make_rx(self, cx: Scope<'rx>) -> Self::Rx;
}

/// A trait for reactive `struct`s that can be made un-reactive. This is the opposite of `MakeRx`, and is intended particularly for state freezing. Like `MakeRx`, this will usually be derived
/// automatically with the `#[make_rx]` macro, but you can also implement it manually.
pub trait MakeUnrx {
pub trait MakeUnrx<'rx> {
/// The type of the unreactive version that we'll convert to.
type Unrx: Serialize + for<'de> Deserialize<'de> + MakeRx;
type Unrx: Serialize + for<'de> Deserialize<'de> + MakeRx<'rx>;
/// Transforms an instance of the `struct` into its unreactive version. By having this as an associated type, we can associate the reactive type with the unreactive, meaning greater inference
/// and fewer arguments that the user needs to provide to macros.
fn make_unrx(self) -> Self::Unrx;
Expand Down
Loading

0 comments on commit 15187b5

Please sign in to comment.