use std::{cell::RefCell, fmt::Debug, ops::DerefMut, ptr};
use rustc_hash::FxHashMap;
use slotmap::SlotMap;
use crate::{
Error, PropId, SlabId,
context::Context,
prop::{ItemId, PropValue},
updater::{Updater, start_track_panicing},
};
type Cleaner<Ctx> = Box<dyn FnOnce(&mut Ctx)>;
pub struct SlabData<Ctx> {
pub props: Vec<ItemId>,
pub effects: Vec<ItemId>,
pub cleaner: Vec<Cleaner<Ctx>>,
}
impl<Ctx> Default for SlabData<Ctx> {
fn default() -> Self {
Self { props: Vec::new(), effects: Vec::new(), cleaner: Vec::new() }
}
}
impl<Ctx> Debug for SlabData<Ctx> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SlabData")
.field("props", &self.props)
.field("effects", &self.effects)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
pub struct TrackResult {
pub read: Vec<PropId<()>>,
pub written: Vec<PropId<()>>,
}
impl TrackResult {
pub(crate) fn destruct(self) -> (Vec<PropId<()>>, Vec<PropId<()>>) {
(self.read, self.written)
}
}
pub struct Store<Ctx> {
pub(crate) props: SlotMap<ItemId, PropValue>,
pub(crate) slabs: FxHashMap<SlabId, SlabData<Ctx>>,
next_slab: SlabId,
slabs_to_remove: Vec<SlabId>,
pub(crate) updater: Updater<Ctx>,
global_cleaners: Vec<Cleaner<Ctx>>,
is_dropped: bool,
tracking: RefCell<Option<TrackResult>>,
}
impl<Ctx: Context> Default for Store<Ctx> {
fn default() -> Self {
Store {
props: SlotMap::default(),
slabs: FxHashMap::default(),
next_slab: SlabId(0),
slabs_to_remove: Vec::new(),
updater: Updater::default(),
global_cleaners: Vec::new(),
is_dropped: false,
tracking: RefCell::new(None),
}
}
}
impl<Ctx> Debug for Store<Ctx> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Store")
.field("props", &self.props)
.field("slabs", &self.slabs)
.field("slabs_to_remove", &self.slabs_to_remove)
.field("effects", &self.updater.effects)
.field("tracking", &self.tracking)
.field("is_dropped", &self.is_dropped)
.finish()
}
}
impl<Ctx> PartialEq for Store<Ctx> {
fn eq(&self, other: &Self) -> bool {
ptr::eq(self, other)
}
}
impl<Ctx> Eq for Store<Ctx> {}
impl<Ctx: Context> Store<Ctx> {
pub fn prop<T: 'static>(&mut self, value: T) -> PropId<T> {
let id = self.props.insert(PropValue::new(value));
PropId::new(id)
}
pub fn prop_in<T: 'static>(
&mut self, slab: Option<SlabId>, value: T,
) -> Result<PropId<T>, Error> {
let Some(slab) = slab else {
return Ok(self.prop(value));
};
if !self.has_slab(slab) {
return Err(Error::Removed);
}
let id = self.prop(value);
self.slab(slab).props.push(id.0);
Ok(id)
}
pub fn contains<T>(&self, id: PropId<T>) -> bool {
self.props.contains_key(id.0)
}
}
impl<Ctx: Context> Store<Ctx> {
pub fn read<T: 'static>(&self, prop: PropId<T>) -> &T {
self.track_read(prop);
self.peek(prop)
}
pub fn get<T: 'static + Copy>(&self, prop: PropId<T>) -> T {
*self.read(prop)
}
pub fn peek<T: 'static>(&self, prop: PropId<T>) -> &T {
self.try_peek(prop).expect("reading removed property")
}
pub fn write<T: 'static>(&mut self, prop: PropId<T>, value: T) -> T {
self.try_write(prop, value).expect("writing removed property")
}
pub fn read_mut<T: 'static>(&mut self, prop: PropId<T>) -> &mut T {
self.try_read_mut(prop).expect("mutating removed property")
}
pub fn update<T: 'static>(&mut self, prop: PropId<T>, fun: impl FnOnce(&mut T)) {
self.try_update(prop, fun).expect("updating removed property");
}
pub fn read_disjoint_mut<'a, Props: PropsTuple>(
&'a mut self, props: Props,
) -> Props::ResultMut<'a> {
match self.try_read_disjoint_mut(props) {
Ok(props) => props,
Err(Error::Removed) => panic!("mutating removed property"),
Err(Error::NotDisjoint) => panic!("mutating non-disjoint properties"),
_ => unreachable!(),
}
}
}
impl<Ctx: Context> Store<Ctx> {
pub fn try_read<T: 'static>(&self, prop: PropId<T>) -> Option<&T> {
let value = self.try_peek(prop)?;
self.track_read(prop);
Some(value)
}
pub fn try_get<T: 'static + Copy>(&self, prop: PropId<T>) -> Option<T> {
self.try_read(prop).copied()
}
pub fn try_peek<T: 'static>(&self, prop: PropId<T>) -> Option<&T> {
self.props.get(prop.0).map(|p| p.get())
}
pub fn try_write<T: 'static>(&mut self, prop: PropId<T>, value: T) -> Result<T, Error> {
let prop = self.try_read_mut(prop).ok_or(Error::Removed)?;
Ok(std::mem::replace(prop, value))
}
pub fn try_read_mut<T: 'static>(&mut self, prop: PropId<T>) -> Option<&mut T> {
let value = self.props.get_mut(prop.0)?.get_mut();
Self::_track_write(&self.tracking, prop.0);
self.updater.push_update(prop.0);
Some(value)
}
pub fn try_update<T: 'static>(
&mut self, prop: PropId<T>, fun: impl FnOnce(&mut T),
) -> Result<(), Error> {
let prop = self.try_read_mut(prop).ok_or(Error::Removed)?;
fun(prop);
Ok(())
}
pub fn try_read_disjoint_mut<'a, Props: PropsTuple>(
&'a mut self, props: Props,
) -> Result<Props::ResultMut<'a>, Error> {
Props::read_disjoint_mut(self, props)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EffectDeps {
Tracked,
Manual {
read: Vec<PropId<()>>,
write: Vec<PropId<()>>,
init_run: bool,
},
}
impl<Ctx: Context> Store<Ctx> {
pub fn effect(
ctx: &mut Ctx, slab: Option<SlabId>, dep: EffectDeps, fun: impl FnMut(&mut Ctx) + 'static,
) -> Result<(), Error> {
if let Some(slab) = slab
&& !ctx.store().has_slab(slab)
{
return Err(Error::Removed);
}
let (deps, init_run) = match dep {
EffectDeps::Tracked => (None, true),
EffectDeps::Manual { read, write, init_run } => (Some((read, write)), init_run),
};
let id = Updater::add_effect(ctx, fun, deps, init_run);
if let Some(slab) = slab {
ctx.store().slab(slab).effects.push(id);
}
Ok(())
}
pub fn computed<T: 'static>(
ctx: &mut Ctx, slab: Option<SlabId>, mut fun: impl FnMut(&mut Ctx) -> T + 'static,
) -> Result<PropId<T>, Error> {
if let Some(slab) = slab
&& !ctx.store().has_slab(slab)
{
return Err(Error::Removed);
}
start_track_panicing(ctx.store_ref());
let value = fun(ctx);
let store = ctx.store();
let TrackResult { read, written } = store.end_track().unwrap();
if !written.is_empty() {
panic!("computed properties cannot write to any properties");
}
let id = store.prop(value);
let fun = move |ctx: &mut Ctx| {
let value = fun(ctx);
ctx.store().write(id, value);
};
let effect = Updater::add_effect(ctx, fun, Some((read, vec![id.erase_type()])), false);
if let Some(slab) = slab {
let slab = ctx.store().slab(slab);
slab.effects.push(effect);
slab.props.push(id.0);
}
Ok(id)
}
}
impl<Ctx: Context> Store<Ctx> {
pub fn create_slab(&mut self) -> SlabId {
let id = self.next_slab;
self.slabs.insert(id, SlabData::default());
self.next_slab = SlabId(id.0 + 1);
id
}
fn slab(&mut self, slab: SlabId) -> &mut SlabData<Ctx> {
self.slabs.get_mut(&slab).unwrap()
}
pub fn has_slab(&self, slab: SlabId) -> bool {
self.slabs.contains_key(&slab) && !self.slabs_to_remove.contains(&slab)
}
pub fn remove_slab(ctx: &mut Ctx, id: SlabId) -> Result<(), Error> {
let store = ctx.store();
if !store.has_slab(id) {
return Err(Error::Removed);
}
if store.updater.is_updating {
store.slabs_to_remove.push(id);
} else {
Store::drop_slab(ctx, id);
}
Ok(())
}
fn drop_slab(ctx: &mut Ctx, id: SlabId) {
while let Some(cleaner) = ctx.store().slab(id).cleaner.pop() {
cleaner(ctx)
}
let store = ctx.store();
let slab = &store.slabs.remove(&id).unwrap();
for id in &slab.props {
store.props.remove(*id);
}
store.updater.remove_items(&slab.effects, &slab.props);
}
}
impl<Ctx: Context> Store<Ctx> {
pub fn is_updating(&self) -> bool {
self.updater.is_updating
}
pub fn force_update<T: 'static>(&mut self, id: PropId<T>) {
if !self.updater.dirty_props.contains(&id.0) {
self.updater.dirty_props.push(id.0);
}
}
pub fn flush_updates(ctx: &mut Ctx) {
if ctx.store().updater.is_updating {
return;
}
Updater::update(ctx);
while let Some(slab) = ctx.store().slabs_to_remove.pop() {
Store::drop_slab(ctx, slab);
}
}
}
impl<Ctx: Context> Store<Ctx> {
pub fn is_tracking(&self) -> bool {
self.tracking.borrow().is_some()
}
pub fn start_track(&self) -> Result<(), Error> {
if self.is_tracking() {
return Err(Error::Tracking);
}
self.tracking.replace(Some(TrackResult::default()));
Ok(())
}
pub fn end_track(&self) -> Result<TrackResult, Error> {
let mut result = self.tracking.take().ok_or(Error::NotTracking)?;
result.read.sort_unstable();
result.read.dedup();
result.written.sort_unstable();
result.written.dedup();
Ok(result)
}
pub fn track_read<T: 'static>(&self, id: PropId<T>) {
if let Some(tracking) = self.tracking.borrow_mut().deref_mut() {
tracking.read.push(id.erase_type());
}
}
fn _track_write(tracking: &RefCell<Option<TrackResult>>, id: ItemId) {
if let Some(tracking) = tracking.borrow_mut().deref_mut() {
tracking.written.push(PropId::new(id));
}
}
pub fn track_write<T: 'static>(&self, id: PropId<T>) {
Self::_track_write(&self.tracking, id.0);
}
}
impl<Ctx: Context> Store<Ctx> {
pub fn add_cleaner(
&mut self, slab: Option<SlabId>, fun: impl FnOnce(&mut Ctx) + 'static,
) -> Result<(), Error> {
let Some(slab) = slab else {
self.global_cleaners.push(Box::new(fun));
return Ok(());
};
if !self.has_slab(slab) {
return Err(Error::Removed);
}
self.slab(slab).cleaner.push(Box::new(fun));
Ok(())
}
pub fn pre_drop(ctx: &mut Ctx) {
let store = ctx.store();
if store.is_dropped {
panic!("calling `Store::pre_drop` twice")
}
store.is_dropped = true;
while let Some(&slab) = ctx.store().slabs.keys().next() {
Store::remove_slab(ctx, slab).unwrap()
}
while let Some(cleaner) = ctx.store().global_cleaners.pop() {
cleaner(ctx)
}
}
}
impl<Ctx> Drop for Store<Ctx> {
fn drop(&mut self) {
if !self.is_dropped {
panic!("dropped without calling `Store::pre_drop`")
}
}
}
pub trait PropsTuple {
type ResultMut<'a>;
fn read_disjoint_mut<'a, Ctx: Context>(
store: &'a mut Store<Ctx>, props: Self,
) -> Result<Self::ResultMut<'a>, Error>;
}
macro_rules! impl_props_tuple {
[$($prop:ident),+] => {
#[allow(non_snake_case)]
impl<$($prop: 'static),+> PropsTuple for ($(PropId<$prop>),+,) {
type ResultMut<'a> = ($(&'a mut $prop),+,);
fn read_disjoint_mut<Ctx: Context>(
store: &mut Store<Ctx>, props: Self,
) -> Result<($(&mut $prop),+,), Error> {
let ($($prop),+,) = props;
let props_ids = [$($prop.0),+];
for prop in props_ids {
if !store.props.contains_key(prop) {
return Err(Error::Removed);
}
}
let Some(props) = store.props.get_disjoint_mut(props_ids) else {
return Err(Error::NotDisjoint);
};
for prop in props_ids {
Store::<Ctx>::_track_write(&store.tracking, prop);
store.updater.push_update(prop);
}
let [$($prop),+] = props;
Ok(($($prop.get_mut()),+,))
}
}
};
}
impl_props_tuple![A];
impl_props_tuple![A, B];
impl_props_tuple![A, B, C];
impl_props_tuple![A, B, C, D];
impl_props_tuple![A, B, C, D, E];
impl_props_tuple![A, B, C, D, E, F];
impl_props_tuple![A, B, C, D, E, F, G];
impl_props_tuple![A, B, C, D, E, F, G, H];
impl_props_tuple![A, B, C, D, E, F, G, H, I];
impl_props_tuple![A, B, C, D, E, F, G, H, I, J];
impl_props_tuple![A, B, C, D, E, F, G, H, I, J, K];
impl_props_tuple![A, B, C, D, E, F, G, H, I, J, K, L];