Skip to content

Commit

Permalink
fix: allow multiple SSRed Leptos apps on same server (closes #1806) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sbihel authored Jan 7, 2024
1 parent 855a3c6 commit f5bf539
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 24 deletions.
14 changes: 13 additions & 1 deletion router/src/components/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ use cfg_if::cfg_if;
use leptos::*;
#[cfg(feature = "transition")]
use leptos_reactive::use_transition;
use std::{cell::RefCell, rc::Rc};
use std::{
cell::RefCell,
rc::Rc,
sync::atomic::{AtomicUsize, Ordering},
};
use thiserror::Error;
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;

static GLOBAL_ROUTERS_COUNT: AtomicUsize = AtomicUsize::new(0);

/// Provides for client-side and server-side routing. This should usually be somewhere near
/// the root of the application.
#[component]
Expand Down Expand Up @@ -51,6 +57,7 @@ pub struct RouterContext {
pub(crate) inner: Rc<RouterContextInner>,
}
pub(crate) struct RouterContextInner {
id: usize,
pub location: Location,
pub base: RouteContext,
pub possible_routes: RefCell<Option<Vec<Branch>>>,
Expand Down Expand Up @@ -165,6 +172,7 @@ impl RouterContext {
});

let inner = Rc::new(RouterContextInner {
id: GLOBAL_ROUTERS_COUNT.fetch_add(1, Ordering::SeqCst),
base_path: base_path.into_owned(),
path_stack: store_value(vec![location.pathname.get_untracked()]),
location,
Expand Down Expand Up @@ -203,6 +211,10 @@ impl RouterContext {
self.inner.base.clone()
}

pub(crate) fn id(&self) -> usize {
self.inner.id
}

/// A list of all possible routes this router can match.
pub fn possible_branches(&self) -> Vec<Branch> {
self.inner
Expand Down
45 changes: 29 additions & 16 deletions router/src/components/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
borrow::Cow,
cell::{Cell, RefCell},
cmp::Reverse,
collections::HashMap,
Expand Down Expand Up @@ -76,15 +77,16 @@ pub fn Routes(
) -> impl IntoView {
let router = use_context::<RouterContext>()
.expect("<Routes/> component should be nested within a <Router/>.");
let router_id = router.id();

let base_route = router.base();
let base = base.unwrap_or_default();

Branches::initialize(&base, children());
Branches::initialize(router_id, &base, children());

#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
Branches::with(&base, |branches| {
Branches::with(router_id, &base, |branches| {
*context.0.borrow_mut() = branches.to_vec()
});
}
Expand All @@ -93,7 +95,8 @@ pub fn Routes(
let current_route = next_route;

let root_equal = Rc::new(Cell::new(true));
let route_states = route_states(base, &router, current_route, &root_equal);
let route_states =
route_states(router_id, base, &router, current_route, &root_equal);
provide_context(route_states);

let id = HydrationCtx::id();
Expand Down Expand Up @@ -156,15 +159,16 @@ pub fn AnimatedRoutes(
) -> impl IntoView {
let router = use_context::<RouterContext>()
.expect("<Routes/> component should be nested within a <Router/>.");
let router_id = router.id();

let base_route = router.base();
let base = base.unwrap_or_default();

Branches::initialize(&base, children());
Branches::initialize(router_id, &base, children());

#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
Branches::with(&base, |branches| {
Branches::with(router_id, &base, |branches| {
*context.0.borrow_mut() = branches.to_vec()
});
}
Expand Down Expand Up @@ -193,8 +197,9 @@ pub fn AnimatedRoutes(
let prev_matches = prev
.map(|(_, r)| r)
.cloned()
.map(|location| get_route_matches(&base, location));
let matches = get_route_matches(&base, next_route.clone());
.map(|location| get_route_matches(router_id, &base, location));
let matches =
get_route_matches(router_id, &base, next_route.clone());
let same_route = prev_matches
.and_then(|p| p.first().as_ref().map(|r| r.route.key.clone()))
== matches.first().as_ref().map(|r| r.route.key.clone());
Expand All @@ -221,7 +226,8 @@ pub fn AnimatedRoutes(
let current_route = create_memo(move |_| animation_and_route.get().1);

let root_equal = Rc::new(Cell::new(true));
let route_states = route_states(base, &router, current_route, &root_equal);
let route_states =
route_states(router_id, base, &router, current_route, &root_equal);

let root = root_route(base_route, route_states, root_equal);
let node_ref = create_node_ref::<html::Div>();
Expand Down Expand Up @@ -266,12 +272,13 @@ pub fn AnimatedRoutes(

pub(crate) struct Branches;

type BranchesCacheKey = (usize, Cow<'static, str>);
thread_local! {
static BRANCHES: RefCell<HashMap<String, Vec<Branch>>> = RefCell::new(HashMap::new());
static BRANCHES: RefCell<HashMap<BranchesCacheKey, Vec<Branch>>> = RefCell::new(HashMap::new());
}

impl Branches {
pub fn initialize(base: &str, children: Fragment) {
pub fn initialize(router_id: usize, base: &str, children: Fragment) {
BRANCHES.with(|branches| {
#[cfg(debug_assertions)]
{
Expand All @@ -286,7 +293,7 @@ impl Branches {
}

let mut current = branches.borrow_mut();
if !current.contains_key(base) {
if !current.contains_key(&(router_id, Cow::from(base))) {
let mut branches = Vec::new();
let children = children
.as_children()
Expand Down Expand Up @@ -316,15 +323,19 @@ impl Branches {
true,
base,
);
current.insert(base.to_string(), branches);
current.insert((router_id, Cow::Owned(base.into())), branches);
}
})
}

pub fn with<T>(base: &str, cb: impl FnOnce(&[Branch]) -> T) -> T {
pub fn with<T>(
router_id: usize,
base: &str,
cb: impl FnOnce(&[Branch]) -> T,
) -> T {
BRANCHES.with(|branches| {
let branches = branches.borrow();
let branches = branches.get(base).expect(
let branches = branches.get(&(router_id, Cow::from(base))).expect(
"Branches::initialize() should be called before \
Branches::with()",
);
Expand All @@ -334,14 +345,16 @@ impl Branches {
}

fn route_states(
router_id: usize,
base: String,
router: &RouterContext,
current_route: Memo<String>,
root_equal: &Rc<Cell<bool>>,
) -> Memo<RouterState> {
// whenever path changes, update matches
let matches =
create_memo(move |_| get_route_matches(&base, current_route.get()));
let matches = create_memo(move |_| {
get_route_matches(router_id, &base, current_route.get())
});

// iterate over the new matches, reusing old routes when they are the same
// and replacing them with new routes when they differ
Expand Down
22 changes: 15 additions & 7 deletions router/src/matching/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,39 @@ pub(crate) struct RouteMatch {
}

pub(crate) fn get_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
#[cfg(feature = "ssr")]
{
use lru::LruCache;
use std::{cell::RefCell, num::NonZeroUsize};
type RouteMatchCache = LruCache<(usize, String), Rc<Vec<RouteMatch>>>;
thread_local! {
static ROUTE_MATCH_CACHE: RefCell<LruCache<String, Rc<Vec<RouteMatch>>>> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
static ROUTE_MATCH_CACHE: RefCell<RouteMatchCache> = RefCell::new(LruCache::new(NonZeroUsize::new(32).unwrap()));
}

ROUTE_MATCH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
Rc::clone(cache.get_or_insert(location.clone(), || {
build_route_matches(base, location)
}))
Rc::clone(
cache.get_or_insert((router_id, location.clone()), || {
build_route_matches(router_id, base, location)
}),
)
})
}

#[cfg(not(feature = "ssr"))]
build_route_matches(base, location)
build_route_matches(router_id, base, location)
}

fn build_route_matches(base: &str, location: String) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(base, |branches| {
fn build_route_matches(
router_id: usize,
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(router_id, base, |branches| {
for branch in branches {
if let Some(matches) = branch.matcher(&location) {
return matches;
Expand Down

0 comments on commit f5bf539

Please sign in to comment.