1use db::kvp::KeyValueStore;
2use gpui::{App, AppContext as _, Context, Subscription, Task, WindowId};
3use util::ResultExt;
4
5pub struct Session {
6 session_id: String,
7 old_session_id: Option<String>,
8 old_window_ids: Option<Vec<WindowId>>,
9}
10
11const SESSION_ID_KEY: &str = "session_id";
12const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack";
13
14impl Session {
15 pub async fn new(session_id: String, db: KeyValueStore) -> Self {
16 let old_session_id = db.read_kvp(SESSION_ID_KEY).ok().flatten();
17
18 db.write_kvp(SESSION_ID_KEY.to_string(), session_id.clone())
19 .await
20 .log_err();
21
22 let old_window_ids = db
23 .read_kvp(SESSION_WINDOW_STACK_KEY)
24 .ok()
25 .flatten()
26 .and_then(|json| serde_json::from_str::<Vec<u64>>(&json).ok())
27 .map(|vec: Vec<u64>| {
28 vec.into_iter()
29 .map(WindowId::from)
30 .collect::<Vec<WindowId>>()
31 });
32
33 Self {
34 session_id,
35 old_session_id,
36 old_window_ids,
37 }
38 }
39
40 #[cfg(any(test, feature = "test-support"))]
41 pub fn test() -> Self {
42 Self {
43 session_id: uuid::Uuid::new_v4().to_string(),
44 old_session_id: None,
45 old_window_ids: None,
46 }
47 }
48
49 #[cfg(any(test, feature = "test-support"))]
50 pub fn test_with_old_session(old_session_id: String) -> Self {
51 Self {
52 session_id: uuid::Uuid::new_v4().to_string(),
53 old_session_id: Some(old_session_id),
54 old_window_ids: None,
55 }
56 }
57
58 pub fn id(&self) -> &str {
59 &self.session_id
60 }
61}
62
63pub struct AppSession {
64 session: Session,
65 _serialization_task: Task<()>,
66 _subscriptions: Vec<Subscription>,
67}
68
69impl AppSession {
70 pub fn new(session: Session, cx: &Context<Self>) -> Self {
71 let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)];
72
73 #[cfg(not(any(test, feature = "test-support")))]
74 let _serialization_task = {
75 let db = KeyValueStore::global(cx);
76 cx.spawn(async move |_, cx| {
77 // Disabled in tests: the infinite loop bypasses "parking forbidden" checks,
78 // causing tests to hang instead of panicking.
79 {
80 let mut current_window_stack = Vec::new();
81 loop {
82 if let Some(windows) = cx.update(|cx| window_stack(cx))
83 && windows != current_window_stack
84 {
85 store_window_stack(db.clone(), &windows).await;
86 current_window_stack = windows;
87 }
88
89 cx.background_executor()
90 .timer(std::time::Duration::from_millis(500))
91 .await;
92 }
93 }
94 })
95 };
96
97 #[cfg(any(test, feature = "test-support"))]
98 let _serialization_task = Task::ready(());
99
100 Self {
101 session,
102 _subscriptions,
103 _serialization_task,
104 }
105 }
106
107 fn app_will_quit(&mut self, cx: &mut Context<Self>) -> Task<()> {
108 if let Some(window_stack) = window_stack(cx) {
109 let db = KeyValueStore::global(cx);
110 cx.background_spawn(async move { store_window_stack(db, &window_stack).await })
111 } else {
112 Task::ready(())
113 }
114 }
115
116 pub fn id(&self) -> &str {
117 self.session.id()
118 }
119
120 pub fn last_session_id(&self) -> Option<&str> {
121 self.session.old_session_id.as_deref()
122 }
123
124 #[cfg(any(test, feature = "test-support"))]
125 pub fn replace_session_for_test(&mut self, session: Session) {
126 self.session = session;
127 }
128
129 pub fn last_session_window_stack(&self) -> Option<Vec<WindowId>> {
130 self.session.old_window_ids.clone()
131 }
132}
133
134fn window_stack(cx: &App) -> Option<Vec<u64>> {
135 Some(
136 cx.window_stack()?
137 .into_iter()
138 .map(|window| window.window_id().as_u64())
139 .collect(),
140 )
141}
142
143async fn store_window_stack(db: KeyValueStore, windows: &[u64]) {
144 if let Ok(window_ids_json) = serde_json::to_string(windows) {
145 db.write_kvp(SESSION_WINDOW_STACK_KEY.to_string(), window_ids_json)
146 .await
147 .log_err();
148 }
149}