effect-based state mutation

This commit is contained in:
tezlm 2024-02-11 00:50:26 -08:00
parent c249a7b0a5
commit 9b02e93906
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
5 changed files with 126 additions and 66 deletions

View file

@ -1,6 +1,6 @@
use serde::Serialize;
use super::table::{State, StateConfig};
use super::table::{State, StateConfig, TableRow};
use crate::event::{Event, EventId};
use std::{cmp::Ordering, collections::HashSet, fmt::Debug};
@ -9,18 +9,49 @@ pub trait Resolver {
type EventType: Debug + Serialize + Clone;
/// Given a set of ordered events, resolve the final state
fn resolve<S: State>(&self, state: &mut S, event: &Event<Self::EventType>);
fn resolve<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Vec<ResolverEffect>;
/// Given two events, decide which one comes first
/// if Ordering::Equal is returned, the event id is used
/// if Ordering::Equal is returned, the timestamp then event id is used
fn tiebreak(&self, a: &Event<Self::EventType>, b: &Event<Self::EventType>) -> Ordering;
/// TEMP: Get the name/id of this resolver
fn name(&self) -> &str;
/// Get the schema for state
fn get_state_config(&self) -> StateConfig;
}
#[derive(Debug)]
/// Effects a resolver can produce
pub enum ResolverEffect {
/// Insert a new row into the database
Insert {
table: String,
row: TableRow,
},
// // TODO: how does delete/update work?
// /// Delete an existing row
// Delete { query: StateQuery },
// /// Update an existing row
// Update {},
// /// Notify someone outside of the room that they can join
// Invite {},
// /// Add to notification counters (for mentions)
// // NOTE: maybe instead of this, there could be a special notifications table?
// Notify {},
}
impl ResolverEffect {
pub fn insert(table: impl Into<String>, row: TableRow) -> Self {
Self::Insert { table: table.into(), row }
}
}
/// topologically sort a list of events
pub fn sort<T: Debug + Serialize + Clone>(
tiebreak: impl Fn(&Event<T>, &Event<T>) -> Ordering,
@ -47,20 +78,13 @@ pub fn sort<T: Debug + Serialize + Clone>(
.into_iter()
.partition(|candidate| references.iter().any(|(_, child)| child == candidate.id()));
unsorted = new_unsorted;
children.sort_by(|a, b| tiebreak(a, b).then_with(|| a.id().cmp(b.id())));
children.sort_by(|a, b| {
tiebreak(a, b)
.then_with(|| a.timestamp().cmp(&b.timestamp()))
.then_with(|| a.id().cmp(b.id()))
});
heads.extend(children);
}
assert!(unsorted.is_empty());
sorted
}
// impl<T, S> Debug for dyn Resolver<T, S> {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!(
// f,
// "Resolver({} -> {})",
// std::any::type_name::<T>(),
// std::any::type_name::<S>()
// )
// }
// }

View file

@ -3,11 +3,13 @@ use crate::{
event::EventId,
event::{CoreContent, CreateContent, Event, HashType, SignatureType},
proto,
resolver::{sort, Resolver},
resolver::{sort, Resolver, ResolverEffect},
Error, Result,
};
use std::fmt::Debug;
use super::table::Table;
#[derive(Debug)]
pub struct Room<R: Resolver> {
pub events: Vec<Event<R::EventType>>,
@ -63,15 +65,22 @@ impl<R: Resolver> Room<R> {
&self.resolver
}
pub fn resolve_state<A>(&mut self, initial_state: A) -> A
pub fn resolve_state<S>(&mut self, initial_state: S) -> S
where
A: proto::table::State,
S: proto::table::State,
{
let resolver = self.get_resolver();
let sorted = sort(|a, b| resolver.tiebreak(a, b), &self.events);
let mut state = initial_state;
for event in sorted {
resolver.resolve(&mut state, event);
let effects = resolver.resolve(&mut state, event);
for effect in effects {
match effect {
ResolverEffect::Insert { table, row } => {
state.table(&table).unwrap().insert(row).unwrap();
},
}
}
}
state
}

View file

@ -7,7 +7,6 @@
//! effort/payoff ratio in the database schema definition.
use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize, Serialize};
use crate::event::EventId;
@ -186,6 +185,18 @@ impl From<f64> for ColumnValue {
}
}
impl From<EventId> for ColumnValue {
fn from(value: EventId) -> Self {
ColumnValue::Event(value)
}
}
impl From<Text> for ColumnValue {
fn from(value: Text) -> Self {
ColumnValue::Text(value)
}
}
impl StateConfig {
pub fn new() -> Self {
Self::default()
@ -251,3 +262,16 @@ impl TableConfig {
self
}
}
impl TableRow {
pub fn new() -> Self {
Self {
values: HashMap::new(),
}
}
pub fn with(mut self, column: impl Into<String>, value: impl Into<ColumnValue>) -> Self {
self.values.insert(column.into(), value.into());
self
}
}

View file

@ -1,13 +1,15 @@
//! A basic forum/mailing list-esque thing
use std::{cmp::Ordering, collections::HashMap};
use std::cmp::Ordering;
use serde::{Deserialize, Serialize};
use crate::{
event::{CoreContent, Event, EventId},
proto::table::{ColumnType, ColumnValue, IndexType, State, StateConfig, Table, TableConfig, TableRow, Text},
resolver::Resolver,
proto::table::{
ColumnType, IndexType, State, StateConfig, TableConfig, TableRow, Text,
},
resolver::{Resolver, ResolverEffect},
};
#[derive(Debug, Default)]
@ -16,37 +18,47 @@ pub struct ForumResolver;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ForumEventContent {
Post { subject: Text, body: Text },
Reply { post: EventId, reference: EventId, body: Text },
Config { name: Text, topic: Text },
Post {
subject: Text,
body: Text,
},
Reply {
post: EventId,
reference: EventId,
body: Text,
},
Config {
name: Text,
topic: Text,
},
}
impl Resolver for ForumResolver {
type EventType = ForumEventContent;
fn resolve<S: State>(&self, state: &mut S, event: &Event<Self::EventType>) {
match event.content() {
CoreContent::Create(_) => {},
CoreContent::Custom(ForumEventContent::Post { .. }) => {
state.table("replies").unwrap().insert(TableRow { values: HashMap::from_iter([
("event".into(), ColumnValue::Event(event.id().to_owned())),
("time".into(), ColumnValue::Integer(event.timestamp())),
]) }).unwrap();
},
CoreContent::Custom(ForumEventContent::Reply { post, reference, .. }) => {
state.table("replies").unwrap().insert(TableRow { values: HashMap::from_iter([
("event".into(), ColumnValue::Event(event.id().to_owned())),
("post".into(), ColumnValue::Event(post.to_owned())),
("reference".into(), ColumnValue::Event(reference.to_owned())),
]) }).unwrap();
},
fn resolve<S: State>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<ResolverEffect> {
let row = match event.content() {
CoreContent::Create(_) => return vec![],
CoreContent::Custom(ForumEventContent::Post { .. }) => ResolverEffect::insert(
"replies",
TableRow::new()
.with("event", event.id().to_owned())
.with("time", event.timestamp()),
),
CoreContent::Custom(ForumEventContent::Reply {
post, reference, ..
}) => ResolverEffect::insert(
"replies",
TableRow::new()
.with("event", event.id().to_owned())
.with("post", post.to_owned())
.with("reference", reference.to_owned()),
),
CoreContent::Custom(ForumEventContent::Config { name, topic }) => {
state.table("config").unwrap().insert(TableRow { values: HashMap::from_iter([
("name".into(), ColumnValue::Text(name.to_owned())),
("topic".into(), ColumnValue::Text(topic.to_owned())),
]) }).unwrap();
},
}
ResolverEffect::insert("config", TableRow::new().with("name", name.to_owned()).with("topic", topic.to_owned()))
}
};
vec![row]
}
fn tiebreak(&self, _a: &Event<Self::EventType>, _b: &Event<Self::EventType>) -> Ordering {

View file

@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize};
use crate::{
event::{CoreContent, Event},
proto::table::{ColumnType, IndexType, State, StateConfig, Table, TableConfig, TableRow},
resolver::Resolver,
proto::table::{ColumnType, IndexType, State, StateConfig, TableConfig, TableRow},
resolver::{Resolver, ResolverEffect},
};
use std::{cmp::Ordering, collections::HashMap, fmt::Debug};
use std::cmp::Ordering;
#[derive(Debug, Default)]
/// A basic key-value store
@ -27,24 +27,15 @@ impl KVResolver {
impl Resolver for KVResolver {
type EventType = KVEventContent;
fn resolve<S: State>(&self, state: &mut S, event: &Event<KVEventContent>) {
fn resolve<S: State>(&self, _state: &S, event: &Event<KVEventContent>) -> Vec<ResolverEffect> {
dbg!(event);
let mut table = match state.table("kv") {
Ok(t) => t,
Err(_) => panic!("no table exists"),
};
match &event.content() {
CoreContent::Create(_) => {}
CoreContent::Create(_) => vec![],
CoreContent::Custom(KVEventContent::Set(k, v)) => {
let res = table.insert(TableRow {
values: HashMap::from_iter([
("key".into(), k.to_owned().into()),
("value".into(), v.to_owned().into()),
]),
});
if res.is_err() {
panic!("could not insert");
}
let row = TableRow::new()
.with("key", k.to_owned())
.with("value", v.to_owned());
vec![ResolverEffect::insert("kv", row)]
}
}
}