1use std::sync::Arc;
2
3use tessera_ui::{State, StateOwner};
4
5use crate::{RouterDestination, ShardState, ShardStateLifeCycle};
6
7struct RouteEntry {
8 id: u64,
9 destination: Arc<dyn RouterDestination>,
10 owner: StateOwner,
11}
12
13pub struct RouterController {
15 route_stack: Vec<RouteEntry>,
16 next_route_id: u64,
17 scope: StateOwner,
18 marker: State<()>,
19}
20
21impl RouterController {
22 pub fn new() -> Self {
24 let scope = StateOwner::new();
25 let marker = scope.state_with_key("controller", || ());
26 Self {
27 route_stack: Vec::new(),
28 next_route_id: 0,
29 scope,
30 marker,
31 }
32 }
33
34 pub fn with_root(root_dest: impl RouterDestination + 'static) -> Self {
36 let mut router = Self::new();
37 router.push(root_dest);
38 router
39 }
40
41 pub fn with_root_shared(root_dest: Arc<dyn RouterDestination>) -> Self {
43 let mut router = Self::new();
44 router.push_shared(root_dest);
45 router
46 }
47
48 pub fn push<T: RouterDestination + 'static>(&mut self, destination: T) {
50 self.push_shared(Arc::new(destination));
51 }
52
53 pub fn push_shared(&mut self, destination: Arc<dyn RouterDestination>) {
55 self.route_stack.push(RouteEntry {
56 id: self.next_route_id,
57 destination,
58 owner: StateOwner::new(),
59 });
60 self.next_route_id += 1;
61 }
62
63 pub fn pop(&mut self) -> Option<Arc<dyn RouterDestination>> {
65 let removed = self.route_stack.pop()?;
66 removed.owner.close();
67 Some(removed.destination)
68 }
69
70 pub fn replace<T: RouterDestination + 'static>(
72 &mut self,
73 destination: T,
74 ) -> Option<Arc<dyn RouterDestination>> {
75 let previous = self.pop();
76 self.push(destination);
77 previous
78 }
79
80 pub fn replace_shared(
82 &mut self,
83 destination: Arc<dyn RouterDestination>,
84 ) -> Option<Arc<dyn RouterDestination>> {
85 let previous = self.pop();
86 self.push_shared(destination);
87 previous
88 }
89
90 pub fn is_empty(&self) -> bool {
92 self.route_stack.is_empty()
93 }
94 pub fn len(&self) -> usize {
96 self.route_stack.len()
97 }
98 pub fn last(&self) -> Option<&dyn RouterDestination> {
100 self.route_stack.last().map(|entry| &*entry.destination)
101 }
102 pub fn current_is<D: RouterDestination + 'static>(&self) -> bool {
104 self.last()
105 .is_some_and(|current| current.type_id() == std::any::TypeId::of::<D>())
106 }
107
108 pub(crate) fn route_at_depth(&self, depth: usize) -> Option<(u64, Arc<dyn RouterDestination>)> {
109 self.route_stack
110 .iter()
111 .rev()
112 .nth(depth)
113 .map(|entry| (entry.id, entry.destination.clone()))
114 }
115
116 pub(crate) fn state_context(&self, lifecycle: ShardStateLifeCycle) -> (StateOwner, State<()>) {
117 self.route_state_context(lifecycle, None)
118 }
119
120 pub(crate) fn route_state_context(
121 &self,
122 lifecycle: ShardStateLifeCycle,
123 route: Option<u64>,
124 ) -> (StateOwner, State<()>) {
125 let owner = match lifecycle {
126 ShardStateLifeCycle::Scope => &self.scope,
127 ShardStateLifeCycle::Shard => {
128 let entry = match route {
129 Some(id) => self.route_stack.iter().rev().find(|entry| entry.id == id),
130 None => self.route_stack.last(),
131 };
132 &entry
133 .expect("route-scoped shard state requires a non-empty router stack")
134 .owner
135 }
136 };
137 (owner.clone(), self.marker)
138 }
139
140 pub fn init_or_get<T, F, R>(&self, id: &str, f: F) -> R
142 where
143 T: Default + Send + Sync + 'static,
144 F: FnOnce(ShardState<T>) -> R,
145 {
146 self.init_or_get_with_lifecycle(id, ShardStateLifeCycle::Shard, f)
147 }
148
149 pub fn init_or_get_with_lifecycle<T, F, R>(
151 &self,
152 id: &str,
153 lifecycle: ShardStateLifeCycle,
154 f: F,
155 ) -> R
156 where
157 T: Default + Send + Sync + 'static,
158 F: FnOnce(ShardState<T>) -> R,
159 {
160 let (owner, marker) = self.state_context(lifecycle);
161 f(ShardState::new(
162 owner.state_with_key(("shard", id), T::default),
163 marker,
164 ))
165 }
166
167 pub fn clear(&mut self) {
169 self.route_stack.clear();
170 }
171 pub fn reset(&mut self, root_dest: impl RouterDestination + 'static) {
173 self.clear();
174 self.push(root_dest);
175 }
176 pub fn reset_shared(&mut self, root_dest: Arc<dyn RouterDestination>) {
178 self.clear();
179 self.push_shared(root_dest);
180 }
181}
182
183impl Default for RouterController {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188impl Drop for RouterController {
189 fn drop(&mut self) {
190 self.route_stack.clear();
191 self.scope.close();
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use std::panic::{AssertUnwindSafe, catch_unwind};
198
199 use super::RouterController;
200 use crate::{RouterDestination, ShardStateLifeCycle};
201
202 #[derive(Default)]
203 struct CounterState {
204 value: usize,
205 }
206
207 struct DummyDestination;
208
209 impl RouterDestination for DummyDestination {
210 fn exec_component(&self, _: tessera_ui::State<RouterController>) {}
211
212 fn exec_component_handle(
213 &self,
214 ) -> std::sync::Arc<dyn Fn(tessera_ui::State<RouterController>) + Send + Sync> {
215 std::sync::Arc::new(|_| {})
216 }
217
218 fn destination_id() -> &'static str {
219 "dummy"
220 }
221 }
222
223 fn increment_state(
224 router: &RouterController,
225 shard_id: &str,
226 life_cycle: ShardStateLifeCycle,
227 ) -> usize {
228 router.init_or_get_with_lifecycle::<CounterState, _, _>(shard_id, life_cycle, |state| {
229 state.with_mut(|value| {
230 value.value += 1;
231 value.value
232 })
233 })
234 }
235
236 #[test]
237 fn route_scoped_state_is_released_on_pop() {
238 let session = tessera_ui::RuntimeSession::new();
239 let _session = session.enter();
240 let shard_id = "route_scoped";
241 let mut router = RouterController::with_root(DummyDestination);
242
243 assert_eq!(
244 increment_state(&router, shard_id, ShardStateLifeCycle::Shard),
245 1
246 );
247 assert_eq!(
248 increment_state(&router, shard_id, ShardStateLifeCycle::Shard),
249 2
250 );
251
252 assert!(router.pop().is_some());
253 router.push(DummyDestination);
254 assert_eq!(
255 increment_state(&router, shard_id, ShardStateLifeCycle::Shard),
256 1
257 );
258 }
259
260 #[test]
261 fn scope_scoped_state_persists_inside_scope_but_resets_across_scopes() {
262 let session = tessera_ui::RuntimeSession::new();
263 let _session = session.enter();
264 let shard_id = "scope_scoped";
265 let mut router = RouterController::with_root(DummyDestination);
266
267 assert_eq!(
268 increment_state(&router, shard_id, ShardStateLifeCycle::Scope),
269 1
270 );
271
272 router.push(DummyDestination);
273 assert_eq!(
274 increment_state(&router, shard_id, ShardStateLifeCycle::Scope),
275 2
276 );
277
278 assert!(router.pop().is_some());
279 assert_eq!(
280 increment_state(&router, shard_id, ShardStateLifeCycle::Scope),
281 3
282 );
283
284 drop(router);
285
286 let router = RouterController::with_root(DummyDestination);
287 assert_eq!(
288 increment_state(&router, shard_id, ShardStateLifeCycle::Scope),
289 1
290 );
291 }
292
293 #[test]
294 fn route_scoped_state_requires_active_route() {
295 let session = tessera_ui::RuntimeSession::new();
296 let _session = session.enter();
297 let shard_id = "route_context_required";
298 let router = RouterController::new();
299 let result = catch_unwind(AssertUnwindSafe(|| {
300 let _ = increment_state(&router, shard_id, ShardStateLifeCycle::Shard);
301 }));
302 assert!(result.is_err());
303 }
304
305 #[test]
306 fn controllers_do_not_share_or_alias_slots() {
307 let session = tessera_ui::RuntimeSession::new();
308 let _session = session.enter();
309 let shard_id = "isolated";
310 let left = RouterController::with_root(DummyDestination);
311 let right = RouterController::with_root(DummyDestination);
312
313 left.init_or_get::<CounterState, _, _>(shard_id, |state| {
314 state.with_mut(|value| value.value = 11);
315 });
316 right.init_or_get::<CounterState, _, _>(shard_id, |state| {
317 state.with_mut(|value| value.value = 22);
318 });
319
320 assert_eq!(
321 left.init_or_get::<CounterState, _, _>(shard_id, |state| {
322 state.with(|value| value.value)
323 }),
324 11
325 );
326 assert_eq!(
327 right.init_or_get::<CounterState, _, _>(shard_id, |state| {
328 state.with(|value| value.value)
329 }),
330 22
331 );
332
333 let stale = left.init_or_get::<CounterState, _, _>(shard_id, |state| state);
334 drop(left);
335
336 right.init_or_get::<CounterState, _, _>(shard_id, |state| {
337 state.with_mut(|value| value.value = 33);
338 });
339 assert!(stale.try_with(|value| value.value).is_err());
340 assert_eq!(
341 right.init_or_get::<CounterState, _, _>(shard_id, |state| {
342 state.with(|value| value.value)
343 }),
344 33
345 );
346 }
347 #[test]
348 fn panicking_mutation_restores_the_live_shard_value() {
349 let session = tessera_ui::RuntimeSession::new();
350 let _session = session.enter();
351 let router = RouterController::with_root(DummyDestination);
352 let state = router.init_or_get::<usize, _, _>("panic", |state| state);
353 let result = catch_unwind(AssertUnwindSafe(|| {
354 state.with_mut(|value| {
355 *value = 42;
356 panic!("abort mutation");
357 });
358 }));
359 assert!(result.is_err());
360 assert_eq!(state.get(), 42);
361 state.set(43);
362 assert_eq!(state.get(), 43);
363 }
364 type Observed = (
365 tessera_ui::State<RouterController>,
366 crate::ShardState<usize>,
367 usize,
368 );
369
370 #[tessera_ui::tessera]
371 fn reactive_fixture(output: tessera_ui::testing::TestProbe<Option<Observed>>) {
372 let router = tessera_ui::remember(|| RouterController::with_root(DummyDestination));
373 let state = crate::router::with_current_router_shard_state(
374 "reactive",
375 ShardStateLifeCycle::Shard,
376 router,
377 |state: crate::ShardState<usize>| state,
378 );
379 output.set(Some((router, state, state.get())));
380 }
381
382 #[test]
383 fn shard_writes_rebuild_consumers_and_route_owners_preserve_only_live_state() {
384 use tessera_ui::testing::{
385 TestProbe, layout_test, pump_layout_test_session, start_layout_test_session,
386 };
387
388 let output = TestProbe::default();
389 let probe = output.clone();
390 let mut session = start_layout_test_session(layout_test(move || {
391 reactive_fixture(probe.clone());
392 }));
393 pump_layout_test_session(&mut session, 0);
394 let (router, root_state, value) = output.get().unwrap();
395 assert_eq!(value, 0);
396 pump_layout_test_session(&mut session, 1);
397 assert_eq!(
398 session
399 .work()
400 .count("reactive_fixture", tessera_ui::WorkPhase::Build),
401 0
402 );
403 session.run(|| root_state.set(7));
404 pump_layout_test_session(&mut session, 2);
405 assert_eq!(output.get().unwrap().2, 7);
406 assert_eq!(
407 session
408 .work()
409 .count("reactive_fixture", tessera_ui::WorkPhase::Build),
410 1
411 );
412
413 session.run(|| router.with_mut(|router| router.push(DummyDestination)));
414 pump_layout_test_session(&mut session, 3);
415 let (_, top_state, value) = output.get().unwrap();
416 assert_eq!(value, 0);
417 assert!(top_state != root_state);
418 session.run(|| top_state.set(12));
419 pump_layout_test_session(&mut session, 4);
420 assert_eq!(output.get().unwrap().2, 12);
421 session.run(|| {
422 router.with_mut(|router| {
423 router.pop();
424 })
425 });
426 pump_layout_test_session(&mut session, 5);
427 assert!(output.get().unwrap().1 == root_state);
428 assert_eq!(output.get().unwrap().2, 7);
429 assert_eq!(
430 session.run(|| top_state.try_with(|value| *value)),
431 Err(crate::state::ShardStateError::Stale)
432 );
433 session.run(|| {
434 router.with_mut(|router| {
435 router.clear();
436 })
437 });
438 assert_eq!(
439 session.run(|| root_state.try_with(|value| *value)),
440 Err(crate::state::ShardStateError::Stale)
441 );
442 }
443}