Skip to main content

repose_app/
lifecycle.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3use std::sync::{
4    Arc, Mutex,
5    atomic::{AtomicU8, AtomicU64, Ordering},
6};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum AppLifecycle {
10    Foreground,
11    Background,
12}
13
14type LifecycleCallback = Box<dyn Fn(AppLifecycle) + Send>;
15type DeeplinkCallback = Box<dyn Fn(Vec<u8>) + Send>;
16
17struct LifecycleSlot {
18    callback: Mutex<Option<LifecycleCallback>>,
19}
20
21struct DeeplinkSlot {
22    callback: Mutex<Option<DeeplinkCallback>>,
23}
24
25#[derive(Clone)]
26struct LifecycleEntry {
27    id: u64,
28    slot: Arc<LifecycleSlot>,
29}
30
31#[derive(Clone)]
32struct DeeplinkEntry {
33    id: u64,
34    slot: Arc<DeeplinkSlot>,
35}
36
37pub struct LifecycleDispatcher {
38    current: AtomicU8,
39    primary: Mutex<Option<Arc<LifecycleSlot>>>,
40    pending: Mutex<Vec<AppLifecycle>>,
41    listeners: Mutex<Vec<LifecycleEntry>>,
42    next_id: AtomicU64,
43    processing: Mutex<()>,
44}
45
46impl LifecycleDispatcher {
47    const fn new() -> Self {
48        Self {
49            current: AtomicU8::new(0),
50            primary: Mutex::new(None),
51            pending: Mutex::new(Vec::new()),
52            listeners: Mutex::new(Vec::new()),
53            next_id: AtomicU64::new(1),
54            processing: Mutex::new(()),
55        }
56    }
57
58    pub fn set_callback(&self, callback: Box<dyn Fn(AppLifecycle) + Send>) {
59        *lock(&self.primary) = Some(Arc::new(LifecycleSlot {
60            callback: Mutex::new(Some(callback)),
61        }));
62    }
63
64    pub fn add_listener(&self, callback: Box<dyn Fn(AppLifecycle) + Send>) -> u64 {
65        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
66        lock(&self.listeners).push(LifecycleEntry {
67            id,
68            slot: Arc::new(LifecycleSlot {
69                callback: Mutex::new(Some(callback)),
70            }),
71        });
72        id
73    }
74
75    pub fn remove_listener(&self, id: u64) -> bool {
76        let mut listeners = lock(&self.listeners);
77        let before = listeners.len();
78        listeners.retain(|entry| entry.id != id);
79        listeners.len() != before
80    }
81
82    pub fn current(&self) -> Option<AppLifecycle> {
83        lifecycle_from_code(self.current.load(Ordering::Relaxed))
84    }
85
86    pub fn push(&self, state: AppLifecycle) {
87        self.current.store(lifecycle_code(state), Ordering::Relaxed);
88        lock(&self.pending).push(state);
89    }
90
91    pub fn process(&self) {
92        let _processing = match self.processing.try_lock() {
93            Ok(guard) => guard,
94            Err(std::sync::TryLockError::Poisoned(error)) => error.into_inner(),
95            Err(std::sync::TryLockError::WouldBlock) => return,
96        };
97        let batch = std::mem::take(&mut *lock(&self.pending));
98        if batch.is_empty() {
99            return;
100        }
101        let mut retained = Vec::new();
102        for state in batch {
103            let primary = lock(&self.primary).clone();
104            let mut handled = primary.as_ref().is_some_and(|slot| {
105                invoke_lifecycle(slot, state, || {
106                    lock(&self.primary)
107                        .as_ref()
108                        .is_some_and(|current| Arc::ptr_eq(current, slot))
109                })
110            });
111            let listeners = lock(&self.listeners).clone();
112            for entry in &listeners {
113                if lifecycle_listener_is_current(&self.listeners, entry.id, &entry.slot)
114                    && invoke_lifecycle(&entry.slot, state, || {
115                        lifecycle_listener_is_current(&self.listeners, entry.id, &entry.slot)
116                    })
117                {
118                    handled = true;
119                }
120            }
121            if !handled {
122                retained.push(state);
123            }
124        }
125        if !retained.is_empty() {
126            lock(&self.pending).splice(0..0, retained);
127        }
128    }
129}
130
131impl Default for LifecycleDispatcher {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[derive(Clone)]
138pub struct RuntimeDispatchers {
139    pub lifecycle: Arc<LifecycleDispatcher>,
140    pub deeplink: Arc<DeeplinkDispatcher>,
141}
142
143pub struct ActiveDispatcherScope {
144    previous: Option<RuntimeDispatchers>,
145}
146
147static EXTERNAL_LIFECYCLE: Mutex<Vec<AppLifecycle>> = Mutex::new(Vec::new());
148static EXTERNAL_DEEPLINKS: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
149
150thread_local! {
151    static ACTIVE_DISPATCHERS: RefCell<Option<RuntimeDispatchers>> = const { RefCell::new(None) };
152}
153
154pub fn enter_dispatchers(dispatchers: RuntimeDispatchers) -> ActiveDispatcherScope {
155    let lifecycle = std::mem::take(&mut *lock(&EXTERNAL_LIFECYCLE));
156    for state in lifecycle {
157        dispatchers.lifecycle.push(state);
158    }
159    let deeplinks = std::mem::take(&mut *lock(&EXTERNAL_DEEPLINKS));
160    for data in deeplinks {
161        dispatchers.deeplink.push(data);
162    }
163    let previous = ACTIVE_DISPATCHERS.with(|slot| slot.borrow_mut().replace(dispatchers));
164    ActiveDispatcherScope { previous }
165}
166
167impl Drop for ActiveDispatcherScope {
168    fn drop(&mut self) {
169        ACTIVE_DISPATCHERS.with(|slot| {
170            *slot.borrow_mut() = self.previous.take();
171        });
172    }
173}
174
175fn active_dispatchers() -> Option<RuntimeDispatchers> {
176    ACTIVE_DISPATCHERS.with(|slot| slot.borrow().clone())
177}
178
179pub struct DeeplinkDispatcher {
180    primary: Mutex<Option<Arc<DeeplinkSlot>>>,
181    pending: Mutex<Vec<Vec<u8>>>,
182    listeners: Mutex<Vec<DeeplinkEntry>>,
183    next_id: AtomicU64,
184    processing: Mutex<()>,
185}
186
187impl DeeplinkDispatcher {
188    const fn new() -> Self {
189        Self {
190            primary: Mutex::new(None),
191            pending: Mutex::new(Vec::new()),
192            listeners: Mutex::new(Vec::new()),
193            next_id: AtomicU64::new(1),
194            processing: Mutex::new(()),
195        }
196    }
197
198    pub fn set_callback(&self, callback: Box<dyn Fn(Vec<u8>) + Send>) {
199        *lock(&self.primary) = Some(Arc::new(DeeplinkSlot {
200            callback: Mutex::new(Some(callback)),
201        }));
202    }
203
204    pub fn add_listener(&self, callback: Box<dyn Fn(Vec<u8>) + Send>) -> u64 {
205        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
206        lock(&self.listeners).push(DeeplinkEntry {
207            id,
208            slot: Arc::new(DeeplinkSlot {
209                callback: Mutex::new(Some(callback)),
210            }),
211        });
212        id
213    }
214
215    pub fn remove_listener(&self, id: u64) -> bool {
216        let mut listeners = lock(&self.listeners);
217        let before = listeners.len();
218        listeners.retain(|entry| entry.id != id);
219        listeners.len() != before
220    }
221
222    pub fn push(&self, data: Vec<u8>) {
223        lock(&self.pending).push(data);
224    }
225
226    pub fn process(&self) {
227        let _processing = match self.processing.try_lock() {
228            Ok(guard) => guard,
229            Err(std::sync::TryLockError::Poisoned(error)) => error.into_inner(),
230            Err(std::sync::TryLockError::WouldBlock) => return,
231        };
232        let batch = std::mem::take(&mut *lock(&self.pending));
233        if batch.is_empty() {
234            return;
235        }
236        let mut retained = Vec::new();
237        for data in batch {
238            let primary = lock(&self.primary).clone();
239            let mut handled = primary.as_ref().is_some_and(|slot| {
240                invoke_deeplink(slot, &data, || {
241                    lock(&self.primary)
242                        .as_ref()
243                        .is_some_and(|current| Arc::ptr_eq(current, slot))
244                })
245            });
246            let listeners = lock(&self.listeners).clone();
247            for entry in &listeners {
248                if deeplink_listener_is_current(&self.listeners, entry.id, &entry.slot)
249                    && invoke_deeplink(&entry.slot, &data, || {
250                        deeplink_listener_is_current(&self.listeners, entry.id, &entry.slot)
251                    })
252                {
253                    handled = true;
254                }
255            }
256            if !handled {
257                retained.push(data);
258            }
259        }
260        if !retained.is_empty() {
261            lock(&self.pending).splice(0..0, retained);
262        }
263    }
264}
265
266impl Default for DeeplinkDispatcher {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272static LIFECYCLE_DISPATCHER: LifecycleDispatcher = LifecycleDispatcher::new();
273static DEEPLINK_DISPATCHER: DeeplinkDispatcher = DeeplinkDispatcher::new();
274
275thread_local! {
276    static PRE_REDRAW: RefCell<Option<Rc<RefCell<Option<Box<dyn FnMut(&repose_core::RenderContext)>>>>>> =
277        const { RefCell::new(None) };
278}
279
280pub fn set_pre_redraw(cb: Option<Box<dyn FnMut(&repose_core::RenderContext)>>) {
281    PRE_REDRAW.with(|current| {
282        *current.borrow_mut() = cb.map(|callback| Rc::new(RefCell::new(Some(callback))));
283    });
284}
285
286pub fn run_pre_redraw(ctx: &repose_core::RenderContext) {
287    PRE_REDRAW.with(|current| {
288        let Some(slot) = current.borrow().clone() else {
289            return;
290        };
291        let callback = slot.borrow_mut().take();
292        let Some(mut callback) = callback else {
293            return;
294        };
295        callback(ctx);
296        let still_current = current
297            .borrow()
298            .as_ref()
299            .is_some_and(|active| Rc::ptr_eq(active, &slot));
300        if still_current && slot.borrow().is_none() {
301            *slot.borrow_mut() = Some(callback);
302        }
303    });
304}
305
306pub fn set_on_lifecycle(callback: Box<dyn Fn(AppLifecycle) + Send>) {
307    if let Some(dispatchers) = active_dispatchers() {
308        dispatchers.lifecycle.set_callback(callback);
309    } else {
310        LIFECYCLE_DISPATCHER.set_callback(callback);
311    }
312}
313
314pub fn add_lifecycle_listener(callback: Box<dyn Fn(AppLifecycle) + Send>) -> u64 {
315    if let Some(dispatchers) = active_dispatchers() {
316        dispatchers.lifecycle.add_listener(callback)
317    } else {
318        LIFECYCLE_DISPATCHER.add_listener(callback)
319    }
320}
321
322pub fn remove_lifecycle_listener(id: u64) -> bool {
323    active_dispatchers()
324        .map(|dispatchers| dispatchers.lifecycle.remove_listener(id))
325        .unwrap_or_else(|| LIFECYCLE_DISPATCHER.remove_listener(id))
326}
327
328pub fn current_lifecycle() -> Option<AppLifecycle> {
329    active_dispatchers()
330        .map(|dispatchers| dispatchers.lifecycle.current())
331        .unwrap_or_else(|| LIFECYCLE_DISPATCHER.current())
332}
333
334pub fn push_lifecycle(state: AppLifecycle) {
335    if let Some(dispatchers) = active_dispatchers() {
336        dispatchers.lifecycle.push(state);
337    } else {
338        LIFECYCLE_DISPATCHER.push(state);
339        lock(&EXTERNAL_LIFECYCLE).push(state);
340    }
341}
342
343pub fn process_lifecycle() {
344    if let Some(dispatchers) = active_dispatchers() {
345        dispatchers.lifecycle.process();
346    } else {
347        LIFECYCLE_DISPATCHER.process();
348    }
349}
350
351pub fn set_on_deeplink(callback: Box<dyn Fn(Vec<u8>) + Send>) {
352    if let Some(dispatchers) = active_dispatchers() {
353        dispatchers.deeplink.set_callback(callback);
354    } else {
355        DEEPLINK_DISPATCHER.set_callback(callback);
356    }
357}
358
359pub fn add_deeplink_listener(callback: Box<dyn Fn(Vec<u8>) + Send>) -> u64 {
360    if let Some(dispatchers) = active_dispatchers() {
361        dispatchers.deeplink.add_listener(callback)
362    } else {
363        DEEPLINK_DISPATCHER.add_listener(callback)
364    }
365}
366
367pub fn remove_deeplink_listener(id: u64) -> bool {
368    active_dispatchers()
369        .map(|dispatchers| dispatchers.deeplink.remove_listener(id))
370        .unwrap_or_else(|| DEEPLINK_DISPATCHER.remove_listener(id))
371}
372
373pub fn push_deeplink(data: Vec<u8>) {
374    if let Some(dispatchers) = active_dispatchers() {
375        dispatchers.deeplink.push(data);
376    } else {
377        DEEPLINK_DISPATCHER.push(data.clone());
378        lock(&EXTERNAL_DEEPLINKS).push(data);
379    }
380}
381
382pub fn process_deeplinks() {
383    if let Some(dispatchers) = active_dispatchers() {
384        dispatchers.deeplink.process();
385    } else {
386        DEEPLINK_DISPATCHER.process();
387    }
388}
389
390fn lifecycle_code(state: AppLifecycle) -> u8 {
391    match state {
392        AppLifecycle::Foreground => 1,
393        AppLifecycle::Background => 2,
394    }
395}
396
397fn lifecycle_from_code(code: u8) -> Option<AppLifecycle> {
398    match code {
399        1 => Some(AppLifecycle::Foreground),
400        2 => Some(AppLifecycle::Background),
401        _ => None,
402    }
403}
404
405fn lifecycle_listener_is_current(
406    listeners: &Mutex<Vec<LifecycleEntry>>,
407    id: u64,
408    slot: &Arc<LifecycleSlot>,
409) -> bool {
410    lock(listeners)
411        .iter()
412        .any(|entry| entry.id == id && Arc::ptr_eq(&entry.slot, slot))
413}
414
415fn deeplink_listener_is_current(
416    listeners: &Mutex<Vec<DeeplinkEntry>>,
417    id: u64,
418    slot: &Arc<DeeplinkSlot>,
419) -> bool {
420    lock(listeners)
421        .iter()
422        .any(|entry| entry.id == id && Arc::ptr_eq(&entry.slot, slot))
423}
424
425fn invoke_lifecycle(slot: &LifecycleSlot, state: AppLifecycle, restore: impl Fn() -> bool) -> bool {
426    let callback = slot
427        .callback
428        .lock()
429        .unwrap_or_else(|error| error.into_inner())
430        .take();
431    let Some(callback) = callback else {
432        return false;
433    };
434    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(state)));
435    if let Err(error) = result {
436        log::error!("lifecycle callback panicked: {}", panic_message(&error));
437    }
438    if restore()
439        && slot
440            .callback
441            .lock()
442            .unwrap_or_else(|error| error.into_inner())
443            .is_none()
444    {
445        *slot
446            .callback
447            .lock()
448            .unwrap_or_else(|error| error.into_inner()) = Some(callback);
449    }
450    true
451}
452
453fn invoke_deeplink(slot: &DeeplinkSlot, data: &[u8], restore: impl Fn() -> bool) -> bool {
454    let callback = slot
455        .callback
456        .lock()
457        .unwrap_or_else(|error| error.into_inner())
458        .take();
459    let Some(callback) = callback else {
460        return false;
461    };
462    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(data.to_vec())));
463    if let Err(error) = result {
464        log::error!("deeplink callback panicked: {}", panic_message(&error));
465    }
466    if restore()
467        && slot
468            .callback
469            .lock()
470            .unwrap_or_else(|error| error.into_inner())
471            .is_none()
472    {
473        *slot
474            .callback
475            .lock()
476            .unwrap_or_else(|error| error.into_inner()) = Some(callback);
477    }
478    true
479}
480
481fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
482    mutex.lock().unwrap_or_else(|error| error.into_inner())
483}
484
485fn panic_message(error: &Box<dyn std::any::Any + Send>) -> &str {
486    error
487        .downcast_ref::<String>()
488        .map(String::as_str)
489        .or_else(|| error.downcast_ref::<&str>().copied())
490        .unwrap_or("unknown")
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use std::sync::atomic::{AtomicUsize, Ordering};
497
498    #[test]
499    fn active_dispatchers_receive_legacy_global_calls() {
500        let lifecycle = Arc::new(LifecycleDispatcher::default());
501        let deeplink = Arc::new(DeeplinkDispatcher::default());
502        let seen = Arc::new(AtomicUsize::new(0));
503        let callback_seen = seen.clone();
504        {
505            let _scope = enter_dispatchers(RuntimeDispatchers {
506                lifecycle: lifecycle.clone(),
507                deeplink: deeplink.clone(),
508            });
509            set_on_lifecycle(Box::new(move |_state| {
510                callback_seen.fetch_add(1, Ordering::Relaxed);
511            }));
512            push_lifecycle(AppLifecycle::Foreground);
513            process_lifecycle();
514        }
515        assert_eq!(seen.load(Ordering::Relaxed), 1);
516    }
517}