effect-based state mutation
This commit is contained in:
parent
c249a7b0a5
commit
9b02e93906
5 changed files with 126 additions and 66 deletions
|
@ -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>()
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue