use gpui::{AppContext, BorrowAppContext}; use std::{any::Any, collections::HashMap, marker::PhantomData}; pub trait QueryFn: Clone + 'static { type Data: 'static; type Error: 'static; type Context: Context; fn key(&self) -> &'static str; async fn run(&self, c: &Self::Context) -> Result; } 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| store.ensure_query_data(&query_fn, cx)); cx.observe(&ent.raw, |_, _, cx| { cx.notify(); }) .detach(); let query_context = cx.global::>().query_context.clone(); cx.observe(&query_context, move |_, _, cx| { cx.update_global::, _>(|store, cx| { store.invalidate_query(&query_fn, cx); store.ensure_query_data(&query_fn, cx); }) }) .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.raw, |_, ent, cx| { cx.notify(); }) .detach(); let query_context = cx.global::>().query_context.clone(); cx.observe(&query_context, move |_, _, cx| { cx.update_global::, _>(|store, cx| { store.invalidate_query(&query_fn, cx); store.ensure_query_data(&query_fn, cx); }) }) .detach(); ent } pub fn read_query<'a, F, T>( query: &Entity, cx: &'a gpui::Context, ) -> QueryStatus<'a, F::Data, F::Error> where F: QueryFn, T: 'static, { 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: gpui::Entity, } impl Store where C: Context + 'static, { pub fn new(ctx: C, cx: &mut gpui::App) -> Self { Self { query_data: std::collections::HashMap::new(), query_context: cx.new(|_| ctx), } } fn entity_for(&mut self, query: &Q, cx: &mut gpui::Context) -> Entity where Q: QueryFn, T: 'static, { let raw = self .query_data .entry(query.key().into()) .or_insert_with(|| { cx.new(|_| Query { key: query.key(), data: QueryData::Pending, }) }) .clone(); Entity { raw, _marker: PhantomData, } } fn ensure_query_data(&mut self, query: &Q, cx: &mut gpui::Context) -> Entity where T: 'static, 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(query, cx).detach(); } entity } fn execute_query( &mut self, query: &Q, cx: &mut gpui::Context, ) -> gpui::Task> where Q: QueryFn, T: 'static, { 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| { let c = query_context.read_with(cx, |c, _| c.clone())?; let result = q.run(&c).await; entity.raw.update(cx, |state, cx| { state.data = match result { Ok(data) => QueryData::Some(Box::new(data)), Err(err) => QueryData::Err(Box::new(err)), }; cx.notify(); })?; anyhow::Ok(()) }) } fn invalidate_query(&mut self, query: &Q, cx: &mut gpui::Context) where Q: QueryFn, T: 'static, { if let Some(entity) = self.query_data.get(query.key()) { entity.update(cx, |query, cx| { if !matches!(query.data, QueryData::Loading) { query.data = QueryData::Stale; cx.notify() } }) } } } impl gpui::Global for Store where C: Context + 'static {}