use gpui::{AppContext, BorrowAppContext}; use std::{any::Any, collections::HashMap, marker::PhantomData, ops::Deref}; pub(crate) trait QueryFn: Clone + 'static { type Data: 'static; type Error: std::fmt::Debug + 'static; type Context: Context; fn key(&self) -> &'static str; async fn run(&self, c: &Self::Context) -> Result; } pub(crate) trait QueryAppContext: gpui::AppContext { fn ready(value: T) -> Self::Result; fn map_result(result: Self::Result, f: impl FnOnce(T) -> U) -> Self::Result; } pub(crate) struct Query { key: &'static str, data: QueryData, } enum QueryData { Pending, Loading, Stale, Some(Box), Err(Box), } pub enum QueryStatus<'a, Data, Error> { Loading, Loaded(&'a Data), Err(&'a Error), } #[derive(Clone)] pub(crate) struct Entity where F: QueryFn, { raw: gpui::Entity, _marker: PhantomData F>, } pub fn use_query(query_fn: F, cx: &mut gpui::Context) -> Entity where F: QueryFn, T: 'static, Store: gpui::Global, { let ent = cx.update_global::, _>(|store, cx| { let ent = store.entity_for(&query_fn, cx); store.ensure_query_data(&query_fn, cx); ent }); cx.observe(&ent, move |_, ent, cx| { let query = ent.read(cx); if matches!(query.data, QueryData::Stale) { cx.update_global::, _>(|store, cx| { store.ensure_query_data(&query_fn, cx); }); } cx.notify(); }) .detach(); ent } pub fn use_lazy_query(query_fn: F, cx: &mut gpui::Context) -> Entity where F: QueryFn, T: 'static, Store: gpui::Global, { let ent = cx.update_global::, _>(|store, cx| store.entity_for(&query_fn, cx)); cx.observe(&ent, move |_, ent, cx| { let query = ent.read(cx); if matches!(query.data, QueryData::Stale) { cx.update_global::, _>(|store, cx| { store.ensure_query_data(&query_fn, cx); }); } cx.notify(); }) .detach(); ent } pub async fn fetch_query(query_fn: F, cx: &mut gpui::AsyncApp) -> anyhow::Result> where F: QueryFn, { let ent = cx.update(|cx| { cx.update_global::, _>(|store, cx| store.ensure_query_data(&query_fn, cx)) })?; enum WaitState { Cached, Waiting { rx: futures::channel::oneshot::Receiver<()>, sub: gpui::Subscription, }, } loop { let wait_state = cx.update(|cx| { let is_done = ent.read_with(cx, |query, _| { matches!(query.data, QueryData::Some(_) | QueryData::Err(_)) }); if is_done { WaitState::Cached } else { let (tx, rx) = futures::channel::oneshot::channel(); let ent = ent.clone(); let mut tx = Some(tx); let sub = cx.observe(&ent, move |ent, cx| { let is_done = ent.read_with(cx, |query, _| { matches!(query.data, QueryData::Some(_) | QueryData::Err(_)) }); if is_done && let Some(tx) = tx.take() { _ = tx.send(()); } }); WaitState::Waiting { rx, sub } } })?; match wait_state { WaitState::Cached => { return Ok(ent); } WaitState::Waiting { rx, sub } => { _ = sub; _ = rx.await; } } } } impl Entity where F: QueryFn, Store: gpui::Global, { pub fn refetch(&self, cx: &mut gpui::Context) where E: 'static, { cx.update_global::, _>(|store, cx| { store.invalidate_query(self, cx); }); } } impl Deref for Entity where F: QueryFn, { type Target = gpui::Entity; fn deref(&self) -> &Self::Target { &self.raw } } pub fn read_query<'a, F>(query: &Entity, cx: &'a gpui::App) -> QueryStatus<'a, F::Data, F::Error> where F: QueryFn, { let state = query.raw.read(cx); match &state.data { QueryData::Loading | QueryData::Pending | QueryData::Stale => QueryStatus::Loading, QueryData::Some(data) => QueryStatus::Loaded(data.downcast_ref::().unwrap()), QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::().unwrap()), } } // ================= Store ================== pub(crate) trait Context: Clone {} pub struct Store where C: Context, { query_data: HashMap>, query_context: C, } impl Store where C: Context + 'static, { pub fn new(ctx: C) -> Self { Self { query_context: ctx, query_data: std::collections::HashMap::new(), } } pub(crate) fn update_query_context(&mut self, f: impl FnOnce(&mut C)) { f(&mut self.query_context); } fn entity_for(&mut self, query: &Q, cx: &mut CX) -> CX::Result> where Q: QueryFn, CX: QueryAppContext, { if let Some(raw) = self.query_data.get(query.key()) { return CX::ready(Entity { raw: raw.clone(), _marker: PhantomData, }); } let key = query.key(); CX::map_result( cx.new(|_| Query { key: query.key(), data: QueryData::Pending, }), |raw| { self.query_data.insert(key.into(), raw.clone()); Entity { raw, _marker: PhantomData, } }, ) } fn ensure_query_data(&mut self, query: &Q, cx: &mut gpui::App) -> Entity where Q: QueryFn, { let entity = self.entity_for(query, cx); let should_execute = entity.raw.read_with(cx, |state, _| { matches!(state.data, QueryData::Pending | QueryData::Stale) }); if should_execute { self.execute_query_detached(query, cx).detach(); } entity } fn execute_query_detached( &mut self, query: &Q, cx: &mut gpui::App, ) -> gpui::Task> where Q: QueryFn, { let entity = self.entity_for(query, cx); entity.raw.update(cx, |state, cx| { state.data = QueryData::Loading; cx.notify(); }); let q = query.clone(); let query_context = self.query_context.clone(); cx.spawn(async move |cx| { println!("[query] {}", q.key()); let result = q.run(&query_context).await; entity.raw.update(cx, |state, cx| { state.data = match result { Ok(data) => { println!("[query] OK {}", q.key()); QueryData::Some(Box::new(data)) } Err(err) => { println!("[query] ERR {:?}: {:?}", q.key(), err); QueryData::Err(Box::new(err)) } }; cx.notify(); })?; anyhow::Ok(()) }) } async fn execute_query<'a, F>(&mut self, query_fn: &F, cx: &'a mut gpui::AsyncApp) -> Entity where F: QueryFn, { let entity = self.entity_for(query_fn, cx).unwrap(); entity.update(cx, |query, cx| { query.data = QueryData::Loading; cx.notify(); }); let result = query_fn.run(&self.query_context).await; entity .raw .update(cx, |query, cx| { query.data = match result { Ok(data) => QueryData::Some(Box::new(data)), Err(err) => QueryData::Err(Box::new(err)), }; cx.notify(); true }) .unwrap(); entity } fn invalidate_query(&self, entity: &Entity, cx: &mut gpui::Context) where E: 'static, F: QueryFn, { let entity = entity.raw.read(cx); if let Some(entity) = self.query_data.get(entity.key) { entity.update(cx, |query, cx| { if !matches!(query.data, QueryData::Loading) { query.data = QueryData::Stale; cx.notify() } }) } } } impl QueryAppContext for gpui::App { fn ready(value: T) -> Self::Result { value } fn map_result(result: Self::Result, f: impl FnOnce(T) -> U) -> Self::Result { f(result) } } impl QueryAppContext for gpui::AsyncApp { fn ready(value: T) -> Self::Result { Ok(value) } fn map_result(result: Self::Result, f: impl FnOnce(T) -> U) -> Self::Result { result.map(f) } } impl<'a, E> QueryAppContext for gpui::Context<'a, E> { fn ready(value: T) -> Self::Result { value } fn map_result(result: Self::Result, f: impl FnOnce(T) -> U) -> Self::Result { f(result) } } impl gpui::Global for Store where C: Context + 'static {}