Compare commits
2 commits
9100867f51
...
3f652d13a8
Author | SHA1 | Date | |
---|---|---|---|
3f652d13a8 | |||
820abbc089 |
16 changed files with 673 additions and 413 deletions
235
Cargo.lock
generated
235
Cargo.lock
generated
|
@ -359,6 +359,21 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.83"
|
||||
|
@ -526,6 +541,19 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.4.0"
|
||||
|
@ -625,6 +653,31 @@ version = "0.8.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot 0.12.1",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
|
@ -708,6 +761,7 @@ dependencies = [
|
|||
"dag-resolve",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -777,6 +831,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
|
@ -1071,7 +1131,7 @@ checksum = "5e87caa7459145f5e5f167bf34db4532901404c679e62339fb712a0e3ccf722a"
|
|||
dependencies = [
|
||||
"cosmic-text",
|
||||
"etagere",
|
||||
"lru",
|
||||
"lru 0.11.1",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
|
@ -1407,6 +1467,12 @@ dependencies = [
|
|||
"hashbrown 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
|
@ -1430,6 +1496,15 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.10"
|
||||
|
@ -1571,6 +1646,15 @@ dependencies = [
|
|||
"hashbrown 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
|
@ -1984,6 +2068,12 @@ dependencies = [
|
|||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
@ -2193,6 +2283,26 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "795915a3930a5d6bafd9053d37602fea3e61be2e5d4d788983a8ba9654c1c6f2"
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"lru 0.12.2",
|
||||
"paste",
|
||||
"stability",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.5.2"
|
||||
|
@ -2318,6 +2428,12 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
|
||||
[[package]]
|
||||
name = "rustybuzz"
|
||||
version = "0.8.0"
|
||||
|
@ -2414,6 +2530,36 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
|
@ -2562,6 +2708,16 @@ dependencies = [
|
|||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stability"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
|
@ -2586,6 +2742,28 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
|
@ -2780,6 +2958,28 @@ version = "0.20.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossterm",
|
||||
"dag-resolve",
|
||||
"dag-resolve-impls",
|
||||
"ratatui",
|
||||
"tui-term",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-term"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a3a054e8a92d4b51f9c28e04712e18ef3721ea25552577e944ee9870cb7445b"
|
||||
dependencies = [
|
||||
"ratatui",
|
||||
"vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "1.6.3"
|
||||
|
@ -2881,6 +3081,39 @@ version = "0.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "vt100"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"log",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"utf8parse",
|
||||
"vte_generate_state_changes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte_generate_state_changes"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
|
|
@ -48,7 +48,7 @@ struct State<R: Resolver> {
|
|||
|
||||
impl<R: Resolver> State<R> {
|
||||
fn create_event(&mut self, content: CoreContent<R::EventType>) -> Result<()> {
|
||||
match self.room.create_event(content, &self.secret) {
|
||||
match self.room.create_event(&mut *self.store, content, &self.secret) {
|
||||
Ok(event) => {
|
||||
self.db.execute(
|
||||
"INSERT INTO _events (id, json) VALUES (?, ?)",
|
||||
|
@ -62,7 +62,7 @@ impl<R: Resolver> State<R> {
|
|||
}
|
||||
|
||||
fn append_event(&mut self, event: Event<R::EventType>) -> Result<()> {
|
||||
match self.room.append_event(event) {
|
||||
match self.room.append_event(&mut *self.store, event) {
|
||||
Ok(event) => {
|
||||
self.db.execute(
|
||||
"INSERT INTO _events (id, json) VALUES (?, ?)",
|
||||
|
@ -113,8 +113,7 @@ impl<R: Resolver> State<R> {
|
|||
)?;
|
||||
}
|
||||
let db = Rc::new(db);
|
||||
let store = Database::from_conn(db.clone())
|
||||
.init(room.get_resolver().get_state_config())?;
|
||||
let store = Database::from_conn(db.clone()).init()?;
|
||||
Ok(State {
|
||||
db,
|
||||
room,
|
||||
|
@ -145,17 +144,20 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
|
|||
CoreContent::Create(c) => match c.resolver.as_str() {
|
||||
"kv" => {
|
||||
let mut room = Room::from_root(KVResolver, serde_json::from_str(&event_json)?)?;
|
||||
let mut events = vec![];
|
||||
while let Some(row) = rows.next()? {
|
||||
let s: String = row.get(0)?;
|
||||
let ev = serde_json::from_str(&s)?;
|
||||
room.append_event(ev)?;
|
||||
events.push(ev);
|
||||
}
|
||||
drop(event);
|
||||
drop(rows);
|
||||
drop(q);
|
||||
let db = Rc::new(db);
|
||||
let store = Database::from_conn(db.clone())
|
||||
.init(room.get_resolver().get_state_config())?;
|
||||
let mut store = Database::from_conn(db.clone()).init()?;
|
||||
for event in events {
|
||||
room.append_event(&mut *store, event)?;
|
||||
}
|
||||
Ok(Opened::Kv(State {
|
||||
db,
|
||||
room,
|
||||
|
@ -165,17 +167,20 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
|
|||
},
|
||||
"forum-v0" => {
|
||||
let mut room = Room::from_root(ForumResolver, serde_json::from_str(&event_json)?)?;
|
||||
let mut events = vec![];
|
||||
while let Some(row) = rows.next()? {
|
||||
let s: String = row.get(0)?;
|
||||
let ev = serde_json::from_str(&s)?;
|
||||
room.append_event(ev)?;
|
||||
events.push(ev);
|
||||
}
|
||||
drop(event);
|
||||
drop(rows);
|
||||
drop(q);
|
||||
let db = Rc::new(db);
|
||||
let store = Database::from_conn(db.clone())
|
||||
.init(room.get_resolver().get_state_config())?;
|
||||
let mut store = Database::from_conn(db.clone()).init()?;
|
||||
for event in events {
|
||||
room.append_event(&mut *store, event)?;
|
||||
}
|
||||
Ok(Opened::Forum(State {
|
||||
db,
|
||||
room,
|
||||
|
@ -234,26 +239,28 @@ fn send_event<R: Resolver + Debug>(state: &mut State<R>, data: &str) -> Result<(
|
|||
|
||||
fn print_info<R: Resolver + Debug>(state: State<R>) -> Result<()> {
|
||||
let event_count: u64 = state.db.query_row("SELECT count(*) FROM _events", [], |r| r.get(0))?;
|
||||
let row_count: u64 = state.db.query_row("SELECT count(*) FROM data", [], |r| r.get(0))?;
|
||||
println!("room type: {}", state.room.get_resolver().name());
|
||||
println!("room events: {} total", event_count);
|
||||
println!("room state schema:");
|
||||
for (table_name, table_schema) in state.room.get_resolver().get_state_config().tables {
|
||||
print!(" {} (", table_name);
|
||||
for (idx, (column_name, ctype)) in table_schema.columns.iter().enumerate() {
|
||||
if idx != 0 {
|
||||
print!(", ");
|
||||
}
|
||||
print!("{} {:?}", column_name, ctype);
|
||||
}
|
||||
print!(") indexed on (");
|
||||
for (idx, index) in table_schema.indexes.iter().enumerate() {
|
||||
if idx != 0 {
|
||||
print!(", ");
|
||||
}
|
||||
print!("{} {:?}", index.column, index.index_type);
|
||||
}
|
||||
println!(")");
|
||||
}
|
||||
println!("room events: {}", event_count);
|
||||
println!("room db records: {}", row_count);
|
||||
// println!("room state schema:");
|
||||
// for (table_name, table_schema) in state.room.get_resolver().get_state_config().tables {
|
||||
// print!(" {} (", table_name);
|
||||
// for (idx, (column_name, ctype)) in table_schema.columns.iter().enumerate() {
|
||||
// if idx != 0 {
|
||||
// print!(", ");
|
||||
// }
|
||||
// print!("{} {:?}", column_name, ctype);
|
||||
// }
|
||||
// print!(") indexed on (");
|
||||
// for (idx, index) in table_schema.indexes.iter().enumerate() {
|
||||
// if idx != 0 {
|
||||
// print!(", ");
|
||||
// }
|
||||
// print!("{} {:?}", index.column, index.index_type);
|
||||
// }
|
||||
// println!(")");
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -9,4 +9,5 @@ edition = "2021"
|
|||
dag-resolve = { version = "0.1.0", path = "../proto" }
|
||||
rusqlite = { version = "0.30.0", features = ["bundled"] }
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
serde_json = "1.0.113"
|
||||
thiserror = "1.0.57"
|
||||
|
|
|
@ -7,10 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||
use dag_resolve::{
|
||||
actor::ActorId,
|
||||
event::{CoreContent, Event, EventId},
|
||||
proto::table::{
|
||||
ColumnType, IndexType, State, StateConfig, Table as _, TableConfig, TableRow,
|
||||
Text,
|
||||
},
|
||||
proto::{data::Text, table::Database},
|
||||
resolver::{Command, Resolver, Verification},
|
||||
};
|
||||
|
||||
|
@ -115,7 +112,7 @@ impl ForumMemberAcl {
|
|||
ForumMemberAcl::Operator => "operator",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"mute" => ForumMemberAcl::Mute,
|
||||
|
@ -150,42 +147,52 @@ impl ForumRoomAcl {
|
|||
impl Resolver for ForumResolver {
|
||||
type EventType = ForumEventContent;
|
||||
|
||||
fn resolve<S: State>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<Command> {
|
||||
fn resolve<S: Database>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<Command> {
|
||||
let row = match event.content() {
|
||||
CoreContent::Create(_) => Command::insert(
|
||||
"members",
|
||||
TableRow::new()
|
||||
.with("actor", event.author().to_owned())
|
||||
.with("acl", ForumMemberAcl::Operator.to_str()),
|
||||
),
|
||||
CoreContent::Custom(ForumEventContent::Post { .. }) => Command::insert(
|
||||
"replies",
|
||||
TableRow::new()
|
||||
.with("event", event.id().to_owned())
|
||||
.with("time", event.timestamp()),
|
||||
),
|
||||
CoreContent::Create(_) => {
|
||||
let author = event.author().to_string().as_bytes().to_vec();
|
||||
let key: Vec<_> = b"member\xff".into_iter().copied().chain(author).collect();
|
||||
let value = ForumMemberAcl::Operator.to_str().as_bytes();
|
||||
Command::put(key, value)
|
||||
}
|
||||
CoreContent::Custom(ForumEventContent::Post { .. }) => {
|
||||
let key: Vec<_> = b"posts\xff"
|
||||
.into_iter()
|
||||
.copied()
|
||||
.chain(event.id().to_string().as_bytes().to_vec())
|
||||
.collect();
|
||||
Command::put(key, b"")
|
||||
}
|
||||
CoreContent::Custom(ForumEventContent::Reply {
|
||||
post, reference, ..
|
||||
}) => Command::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, acl }) => Command::insert(
|
||||
"config",
|
||||
TableRow::new()
|
||||
.with("name", name.to_owned())
|
||||
.with("topic", topic.to_owned())
|
||||
.with("acl", acl.to_str()),
|
||||
),
|
||||
CoreContent::Custom(ForumEventContent::Member { id, acl }) => Command::insert(
|
||||
"members",
|
||||
TableRow::new()
|
||||
.with("actor", id.to_owned())
|
||||
.with("acl", acl.to_str()),
|
||||
),
|
||||
}) => {
|
||||
let key: Vec<_> = b"replies\xff"
|
||||
.into_iter()
|
||||
.copied()
|
||||
.chain(event.id().to_string().as_bytes().to_vec())
|
||||
.collect();
|
||||
let value: Vec<_> = vec![]
|
||||
.into_iter()
|
||||
.copied()
|
||||
.chain(post.to_string().as_bytes().to_vec())
|
||||
.chain([0xff])
|
||||
.chain(reference.to_string().as_bytes().to_vec())
|
||||
.collect();
|
||||
Command::put(key, value)
|
||||
}
|
||||
CoreContent::Custom(ForumEventContent::Config { name, topic, acl }) => {
|
||||
return vec![
|
||||
Command::put(b"name", serde_json::to_vec(name).unwrap()),
|
||||
Command::put(b"topic", serde_json::to_vec(topic).unwrap()),
|
||||
Command::put(b"acl", acl.to_str().as_bytes()),
|
||||
]
|
||||
}
|
||||
CoreContent::Custom(ForumEventContent::Member { id, acl }) => {
|
||||
let author = id.to_string().as_bytes().to_vec();
|
||||
let key: Vec<_> = b"member\xff".into_iter().copied().chain(author).collect();
|
||||
let value = acl.to_str().as_bytes();
|
||||
Command::put(key, value)
|
||||
}
|
||||
};
|
||||
vec![row]
|
||||
}
|
||||
|
@ -198,52 +205,21 @@ impl Resolver for ForumResolver {
|
|||
"forum-v0"
|
||||
}
|
||||
|
||||
fn get_state_config(&self) -> StateConfig {
|
||||
let config = TableConfig::new()
|
||||
.with_column("name", ColumnType::Text)
|
||||
.with_column("topic", ColumnType::Text)
|
||||
.with_index("name", IndexType::LookupUnique)
|
||||
.with_index("topic", IndexType::LookupUnique);
|
||||
let members = TableConfig::new()
|
||||
.with_column("actor", ColumnType::Actor)
|
||||
.with_column("acl", ColumnType::String)
|
||||
.with_index("actor", IndexType::LookupUnique);
|
||||
let posts = TableConfig::new()
|
||||
.with_column("event", ColumnType::Event)
|
||||
.with_column("time", ColumnType::Integer)
|
||||
.with_index("time", IndexType::Ordered);
|
||||
let replies = TableConfig::new()
|
||||
.with_column("event", ColumnType::Event)
|
||||
.with_column("post", ColumnType::Event)
|
||||
.with_column("parent", ColumnType::Event)
|
||||
.with_index("post", IndexType::Lookup);
|
||||
StateConfig::new()
|
||||
.with_table("config", config)
|
||||
.with_table("members", members)
|
||||
.with_table("posts", posts)
|
||||
.with_table("replies", replies)
|
||||
.verify()
|
||||
}
|
||||
|
||||
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification {
|
||||
fn verify<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Verification {
|
||||
let key: Vec<_> = b"members-"
|
||||
.into_iter()
|
||||
.copied()
|
||||
.chain(event.author().to_string().as_bytes().to_vec())
|
||||
.collect();
|
||||
let member_entry = state
|
||||
.table("members")
|
||||
.unwrap()
|
||||
.lookup_optional("actor", event.author().to_owned())
|
||||
.unwrap()
|
||||
.and_then(|r| r.values.get("acl").cloned())
|
||||
.and_then(|d| d.as_string().map(ToOwned::to_owned))
|
||||
.get(&key)
|
||||
.and_then(|b| String::from_utf8(b).ok())
|
||||
.map(|s| ForumMemberAcl::from_str(&s))
|
||||
.unwrap_or_default();
|
||||
let room_entry = state
|
||||
.table("config")
|
||||
.unwrap()
|
||||
.lookup_one("key", "acl")
|
||||
.unwrap()
|
||||
.values
|
||||
.get("acl")
|
||||
.and_then(|d| d.as_string())
|
||||
.map(|s| ForumRoomAcl::from_str(s))
|
||||
.get(b"acl")
|
||||
.and_then(|d| String::from_utf8(d).ok())
|
||||
.map(|s| ForumRoomAcl::from_str(&s))
|
||||
.unwrap_or_default();
|
||||
|
||||
match event.content() {
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use dag_resolve::{
|
||||
event::{CoreContent, Event},
|
||||
proto::table::{ColumnType, IndexType, State, StateConfig, Table, TableConfig, TableRow},
|
||||
resolver::{Command, Resolver, Verification},
|
||||
event::{CoreContent, Event}, proto::table::Database, resolver::{Command, Resolver, Verification}
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
|
@ -29,19 +27,14 @@ impl KVResolver {
|
|||
impl Resolver for KVResolver {
|
||||
type EventType = KVEventContent;
|
||||
|
||||
fn resolve<S: State>(&self, _state: &S, event: &Event<KVEventContent>) -> Vec<Command> {
|
||||
fn resolve<D: Database>(&self, _state: &D, event: &Event<KVEventContent>) -> Vec<Command> {
|
||||
match &event.content() {
|
||||
CoreContent::Create(_) => {
|
||||
let row = TableRow::new()
|
||||
.with("key", "owner".to_owned())
|
||||
.with("value", event.author().to_owned());
|
||||
vec![Command::insert("meta", row)]
|
||||
vec![Command::Put { key: b"owner".into(), value: event.author().to_string().as_bytes().to_vec() }]
|
||||
},
|
||||
CoreContent::Custom(KVEventContent::Set(k, v)) => {
|
||||
let row = TableRow::new()
|
||||
.with("key", k.to_owned())
|
||||
.with("value", v.to_owned());
|
||||
vec![Command::insert("kv", row)]
|
||||
let key = b"kv\xff".into_iter().chain(k.as_bytes()).cloned().collect();
|
||||
vec![Command::Put { key, value: v.as_bytes().to_owned() }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,23 +47,11 @@ impl Resolver for KVResolver {
|
|||
"kv"
|
||||
}
|
||||
|
||||
fn get_state_config(&self) -> StateConfig {
|
||||
let meta = TableConfig::new()
|
||||
.with_column("key", ColumnType::String)
|
||||
.with_column("value", ColumnType::String)
|
||||
.with_index("key", IndexType::LookupUnique);
|
||||
let table = TableConfig::new()
|
||||
.with_column("key", ColumnType::String)
|
||||
.with_column("value", ColumnType::String)
|
||||
.with_index("key", IndexType::LookupUnique);
|
||||
StateConfig::new().with_table("meta", meta).with_table("kv", table).verify()
|
||||
}
|
||||
|
||||
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification {
|
||||
let entry = state.table("meta").unwrap().lookup_optional("owner", event.author().to_owned()).unwrap();
|
||||
match entry {
|
||||
Some(_) => Verification::Valid,
|
||||
None => Verification::Unauthorized,
|
||||
fn verify<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Verification {
|
||||
if state.get(b"owner").unwrap() == event.author().to_string().as_bytes() {
|
||||
Verification::Valid
|
||||
} else {
|
||||
Verification::Invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! Contains premade stores to store state/data
|
||||
|
||||
pub mod memory;
|
||||
// pub mod memory;
|
||||
pub mod sqlite;
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
//! Store state in a sqlite database
|
||||
|
||||
use std::{collections::HashMap, path::Path, rc::Rc};
|
||||
use std::{path::Path, rc::Rc};
|
||||
|
||||
use dag_resolve::proto::table::{self, State};
|
||||
use rusqlite::params_from_iter;
|
||||
use dag_resolve::proto::table::{self, Database as _};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Database {
|
||||
connection: Rc<rusqlite::Connection>,
|
||||
config: Option<table::StateConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Table {
|
||||
connection: Rc<rusqlite::Connection>,
|
||||
name: String,
|
||||
config: table::TableConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
@ -27,249 +18,70 @@ impl Database {
|
|||
let db = rusqlite::Connection::open(path).unwrap();
|
||||
Ok(Database {
|
||||
connection: Rc::new(db),
|
||||
config: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_conn(conn: Rc<rusqlite::Connection>) -> Self {
|
||||
Database {
|
||||
connection: conn,
|
||||
config: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(mut self, config: table::StateConfig) -> Result<Box<Self>, Error> {
|
||||
self.config = Some(config);
|
||||
self.reset()?;
|
||||
pub fn init(self) -> Result<Box<Self>, Error> {
|
||||
self.reset();
|
||||
Ok(Box::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl table::State for Database {
|
||||
type Table = Table;
|
||||
type Err = Error;
|
||||
pub struct Query;
|
||||
|
||||
fn table(&self, name: &str) -> Result<Table, Self::Err> {
|
||||
let Some(table) = self.config.as_ref().unwrap().tables.get(name) else {
|
||||
panic!("table does not exist");
|
||||
};
|
||||
Ok(Table {
|
||||
connection: self.connection.clone(),
|
||||
name: name.to_owned(),
|
||||
config: table.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), Self::Err> {
|
||||
let mut sql = String::new();
|
||||
for (table_name, table_column) in &self.config.as_ref().unwrap().tables {
|
||||
// FIXME: don't drop and resolve from scratch every time
|
||||
sql.push_str("DROP TABLE IF EXISTS '");
|
||||
sql.push_str(&table_name.replace('\'', "''"));
|
||||
sql.push_str("';\n");
|
||||
sql.push_str("CREATE TABLE '");
|
||||
sql.push_str(&table_name.replace('\'', "''"));
|
||||
sql.push_str("' (");
|
||||
for (idx, (column_name, column_config)) in table_column.columns.iter().enumerate() {
|
||||
if idx != 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(&column_name.replace('\'', "''"));
|
||||
sql.push_str(match column_config {
|
||||
table::ColumnType::String => " TEXT",
|
||||
table::ColumnType::Text => " TEXT",
|
||||
table::ColumnType::Integer => " INT",
|
||||
table::ColumnType::Float => " REAL",
|
||||
table::ColumnType::Event => " TEXT",
|
||||
table::ColumnType::Actor => " TEXT",
|
||||
});
|
||||
}
|
||||
sql.push_str(");\n");
|
||||
}
|
||||
self.connection.execute_batch(&sql).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl table::Table for Table {
|
||||
type Err = Error;
|
||||
|
||||
fn all(&self) -> Result<Vec<table::TableRow>, Self::Err> {
|
||||
let mut sql = String::from("SELECT * FROM '");
|
||||
sql.push_str(&self.name.replace('\'', "''"));
|
||||
sql.push('\'');
|
||||
let mut sql_query = self.connection.prepare(&sql).unwrap();
|
||||
let mut sql_rows = sql_query.query([]).unwrap();
|
||||
let mut rows = vec![];
|
||||
while let Some(sql_row) = sql_rows.next().unwrap() {
|
||||
let mut map = HashMap::new();
|
||||
for (column_name, column_type) in &self.config.columns {
|
||||
let val = match column_type {
|
||||
table::ColumnType::String => {
|
||||
table::ColumnValue::String(sql_row.get(column_name.as_str()).unwrap())
|
||||
}
|
||||
table::ColumnType::Text => todo!(),
|
||||
table::ColumnType::Integer => {
|
||||
table::ColumnValue::Integer(sql_row.get(column_name.as_str()).unwrap())
|
||||
}
|
||||
table::ColumnType::Float => {
|
||||
table::ColumnValue::Float(sql_row.get(column_name.as_str()).unwrap())
|
||||
}
|
||||
table::ColumnType::Actor => todo!(),
|
||||
table::ColumnType::Event => todo!(),
|
||||
};
|
||||
map.insert(column_name.to_string(), val);
|
||||
}
|
||||
rows.push(table::TableRow { values: map });
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn lookup(
|
||||
&self,
|
||||
column: &str,
|
||||
value: impl Into<table::ColumnValue>,
|
||||
) -> Result<Vec<table::TableRow>, Self::Err> {
|
||||
let Some(column_config) = self.config.columns.get(column) else {
|
||||
panic!("does not exist");
|
||||
};
|
||||
let has_index = self.config.indexes.iter().any(|idx| {
|
||||
idx.column == column
|
||||
&& matches!(
|
||||
idx.index_type,
|
||||
table::IndexType::Lookup | table::IndexType::LookupUnique
|
||||
)
|
||||
});
|
||||
if !has_index {
|
||||
panic!("no lookup index");
|
||||
}
|
||||
let mut sql = String::from("SELECT * FROM ");
|
||||
sql.push_str(&self.name.replace('\'', "''"));
|
||||
sql.push_str(" WHERE ");
|
||||
sql.push_str(&column.replace('\'', "''"));
|
||||
sql.push_str(" = ");
|
||||
match (column_config, value.into()) {
|
||||
(table::ColumnType::String, table::ColumnValue::String(s)) => {
|
||||
sql.push('\'');
|
||||
sql.push_str(&s.replace('\'', "''"));
|
||||
sql.push('\'');
|
||||
}
|
||||
(table::ColumnType::Integer, table::ColumnValue::Integer(i)) => {
|
||||
sql.push_str(&i.to_string());
|
||||
}
|
||||
(_, _) => todo!(),
|
||||
};
|
||||
let mut sql_query = self.connection.prepare(&sql).unwrap();
|
||||
let mut sql_rows = sql_query.query([]).unwrap();
|
||||
let mut rows = vec![];
|
||||
while let Some(sql_row) = sql_rows.next().unwrap() {
|
||||
let mut map = HashMap::new();
|
||||
for (column_name, column_type) in &self.config.columns {
|
||||
let val = match column_type {
|
||||
table::ColumnType::String => {
|
||||
table::ColumnValue::String(sql_row.get(column_name.as_str()).unwrap())
|
||||
}
|
||||
table::ColumnType::Text => todo!(),
|
||||
table::ColumnType::Integer => {
|
||||
table::ColumnValue::Integer(sql_row.get(column_name.as_str()).unwrap())
|
||||
}
|
||||
table::ColumnType::Float => {
|
||||
table::ColumnValue::Float(sql_row.get(column_name.as_str()).unwrap())
|
||||
}
|
||||
table::ColumnType::Event => todo!(),
|
||||
table::ColumnType::Actor => todo!(),
|
||||
};
|
||||
map.insert(column_name.to_string(), val);
|
||||
}
|
||||
rows.push(table::TableRow { values: map });
|
||||
}
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn lookup_one(
|
||||
&self,
|
||||
column: &str,
|
||||
value: impl Into<table::ColumnValue>,
|
||||
) -> Result<table::TableRow, Self::Err> {
|
||||
Ok(self
|
||||
.lookup(column, value)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn lookup_optional(
|
||||
&self,
|
||||
column: &str,
|
||||
value: impl Into<table::ColumnValue>,
|
||||
) -> Result<Option<table::TableRow>, Self::Err> {
|
||||
Ok(self.lookup(column, value).unwrap().into_iter().next())
|
||||
}
|
||||
|
||||
fn range(
|
||||
&self,
|
||||
_column: &str,
|
||||
_paginate: table::Paginate,
|
||||
_limit: u64,
|
||||
) -> Result<Vec<table::TableRow>, Self::Err> {
|
||||
impl table::Query for Query {
|
||||
fn get_single(self) -> Option<Vec<u8>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn search_text(
|
||||
&self,
|
||||
_column: &str,
|
||||
_search: &str,
|
||||
_limit: u64,
|
||||
_after: u64,
|
||||
) -> Result<Vec<table::TableRow>, Self::Err> {
|
||||
fn get_all(self) -> Vec<Vec<u8>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn insert(&mut self, mut row: table::TableRow) -> Result<(), Self::Err> {
|
||||
let mut sql = String::from("INSERT INTO ");
|
||||
sql.push_str(&self.name.replace('\'', "''"));
|
||||
sql.push_str(" (");
|
||||
for (idx, name) in self.config.columns.keys().enumerate() {
|
||||
if idx != 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push_str(&name.replace('\'', "''"));
|
||||
}
|
||||
sql.push_str(") VALUES (");
|
||||
use rusqlite::types::Value;
|
||||
let mut params: Vec<Value> = vec![];
|
||||
for (idx, (name, column_type)) in self.config.columns.iter().enumerate() {
|
||||
if idx != 0 {
|
||||
sql.push_str(", ");
|
||||
}
|
||||
sql.push('?');
|
||||
match (column_type, row.values.remove(name).unwrap()) {
|
||||
(table::ColumnType::Text, table::ColumnValue::Text(t)) => {
|
||||
params.push(Value::Text(t.0))
|
||||
}
|
||||
(table::ColumnType::String, table::ColumnValue::String(s)) => {
|
||||
params.push(Value::Text(s))
|
||||
}
|
||||
(table::ColumnType::Integer, table::ColumnValue::Integer(i)) => {
|
||||
params.push(Value::Integer(i.try_into().unwrap()))
|
||||
}
|
||||
(table::ColumnType::Float, table::ColumnValue::Float(r)) => {
|
||||
params.push(Value::Real(r))
|
||||
}
|
||||
(table::ColumnType::Event, table::ColumnValue::Event(event_id)) => {
|
||||
params.push(Value::Text(event_id.to_string()))
|
||||
}
|
||||
(table::ColumnType::Actor, table::ColumnValue::Actor(actor_id)) => {
|
||||
params.push(Value::Text(actor_id.to_string()))
|
||||
}
|
||||
(column_type, column_value) => todo!("column_type={column_type:?} with value {column_value:?}"),
|
||||
};
|
||||
}
|
||||
sql.push(')');
|
||||
self.connection
|
||||
.execute(&sql, params_from_iter(params))
|
||||
.unwrap();
|
||||
Ok(())
|
||||
fn get_iter(self) -> impl Iterator<Item = Vec<u8>> {
|
||||
vec![].into_iter()
|
||||
}
|
||||
|
||||
fn count(self) -> u64 {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl table::Database for Database {
|
||||
type Query = Query;
|
||||
|
||||
fn reset(&self) {
|
||||
self.connection.execute_batch("
|
||||
DROP TABLE IF EXISTS data;
|
||||
CREATE TABLE data (key BLOB, value BLOB);
|
||||
").unwrap();
|
||||
}
|
||||
|
||||
fn query(&self, _selector: table::Selector) -> Self::Query {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn query_reverse(&self, _selector: table::Selector) -> Self::Query {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
|
||||
let mut s = self.connection.prepare("SELECT value FROM data WHERE key = ?").unwrap();
|
||||
s.query_row([key.to_vec()], |f| f.get(0)).ok()
|
||||
}
|
||||
|
||||
fn put(&self, key: &[u8], value: &[u8]) {
|
||||
let mut s = self.connection.prepare("INSERT INTO data (key, value) VALUES (?, ?)").unwrap();
|
||||
s.execute((key, value)).unwrap();
|
||||
}
|
||||
|
||||
fn delete(&self, _key: &[u8]) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
13
crates/proto/src/proto/data.rs
Normal file
13
crates/proto/src/proto/data.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
//! common data types
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Text(Vec<TextPart>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TextPart {
|
||||
pub lang: String,
|
||||
pub mime_type: String,
|
||||
pub content: String,
|
||||
}
|
|
@ -2,4 +2,5 @@ pub mod actor;
|
|||
pub mod event;
|
||||
pub mod resolver;
|
||||
pub mod room;
|
||||
pub mod data;
|
||||
pub mod table;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::table::{State, StateConfig, TableRow};
|
||||
// use super::table::{State, StateConfig, TableRow};
|
||||
use super::table::Database;
|
||||
use crate::event::{Event, EventId};
|
||||
use std::{cmp::Ordering, collections::HashSet, fmt::Debug};
|
||||
|
||||
|
@ -9,20 +10,17 @@ pub trait Resolver {
|
|||
type EventType: Clone + Debug + Serialize + for<'a> Deserialize<'a>;
|
||||
|
||||
/// Given a set of ordered events, resolve the final state
|
||||
fn resolve<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Vec<Command>;
|
||||
fn resolve<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Vec<Command>;
|
||||
|
||||
/// Given two events, decide which one comes first
|
||||
/// if Ordering::Equal is returned, the timestamp then event id is used
|
||||
fn tiebreak(&self, a: &Event<Self::EventType>, b: &Event<Self::EventType>) -> Ordering;
|
||||
|
||||
/// Verify if an event can be sent or not
|
||||
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification;
|
||||
fn verify<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Verification;
|
||||
|
||||
/// 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)]
|
||||
|
@ -41,17 +39,13 @@ pub enum Verification {
|
|||
/// Effects a resolver can produce
|
||||
pub enum Command {
|
||||
/// Insert a new row into the database
|
||||
Insert {
|
||||
table: String,
|
||||
row: TableRow,
|
||||
Put {
|
||||
key: Vec<u8>,
|
||||
value: Vec<u8>,
|
||||
},
|
||||
|
||||
// // TODO: how does delete/update work?
|
||||
// /// Delete an existing row
|
||||
// Delete { query: StateQuery },
|
||||
|
||||
// /// Update an existing row
|
||||
// Update {},
|
||||
/// Delete an existing row
|
||||
Delete { key: Vec<u8> },
|
||||
|
||||
// /// Notify someone outside of the room that they can join
|
||||
// Invite {},
|
||||
|
@ -62,8 +56,8 @@ pub enum Command {
|
|||
}
|
||||
|
||||
impl Command {
|
||||
pub fn insert(table: impl Into<String>, row: TableRow) -> Self {
|
||||
Self::Insert { table: table.into(), row }
|
||||
pub fn put(key: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) -> Self {
|
||||
Command::Put { key: key.into(), value: value.into() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,12 @@ use crate::{
|
|||
event::EventId,
|
||||
event::{CoreContent, CreateContent, Event, HashType, SignatureType},
|
||||
proto,
|
||||
resolver::{sort, Resolver, Command},
|
||||
resolver::{sort, Command, Resolver, Verification},
|
||||
Error, Result,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::table::Table;
|
||||
use super::table::Database;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Room<R: Resolver> {
|
||||
|
@ -65,27 +65,27 @@ impl<R: Resolver> Room<R> {
|
|||
&self.resolver
|
||||
}
|
||||
|
||||
pub fn resolve_state<S>(&self, state: &mut S)
|
||||
pub fn resolve_state<D>(&self, state: &mut D)
|
||||
where
|
||||
S: proto::table::State,
|
||||
D: proto::table::Database,
|
||||
{
|
||||
let resolver = self.get_resolver();
|
||||
let sorted = sort(|a, b| resolver.tiebreak(a, b), &self.events);
|
||||
state.reset().unwrap();
|
||||
state.reset();
|
||||
for event in sorted {
|
||||
let effects = resolver.resolve(state, event);
|
||||
for effect in effects {
|
||||
match effect {
|
||||
Command::Insert { table, row } => {
|
||||
state.table(&table).unwrap().insert(row).unwrap();
|
||||
},
|
||||
Command::Put { key, value } => state.put(&key, &value),
|
||||
Command::Delete { key } => state.delete(&key),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_event(
|
||||
pub fn create_event<D: Database>(
|
||||
&mut self,
|
||||
state: &mut D,
|
||||
event_content: CoreContent<R::EventType>,
|
||||
secret: &ActorSecret,
|
||||
) -> Result<&Event<R::EventType>> {
|
||||
|
@ -93,11 +93,16 @@ impl<R: Resolver> Room<R> {
|
|||
.with_references(std::mem::take(&mut self.heads))
|
||||
.then_hash()?
|
||||
.and_sign()?;
|
||||
self.append_event(event)
|
||||
self.append_event(state, event)
|
||||
}
|
||||
|
||||
pub fn append_event(&mut self, event: Event<R::EventType>) -> Result<&Event<R::EventType>> {
|
||||
pub fn append_event<D: Database>(&mut self, state: &mut D, event: Event<R::EventType>) -> Result<&Event<R::EventType>> {
|
||||
event.verify_room(self).expect("event failed verification");
|
||||
match self.get_resolver().verify(state, &event) {
|
||||
Verification::Valid => {},
|
||||
Verification::Unauthorized => panic!("unauthorized"),
|
||||
Verification::Invalid => panic!("invalid data"),
|
||||
}
|
||||
if self.events.iter().any(|p| p == &event) {
|
||||
return Err(Error::AlreadyExists);
|
||||
}
|
||||
|
|
26
crates/proto/src/proto/table/kv.rs
Normal file
26
crates/proto/src/proto/table/kv.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
pub trait Database {
|
||||
type Query: Query;
|
||||
|
||||
fn query(&self, selector: Selector) -> Self::Query;
|
||||
fn query_reverse(&self, selector: Selector) -> Self::Query;
|
||||
|
||||
fn reset(&self);
|
||||
fn get(&self, key: &[u8]) -> Option<Vec<u8>>;
|
||||
fn put(&self, key: &[u8], value: &[u8]);
|
||||
fn delete(&self, key: &[u8]);
|
||||
}
|
||||
|
||||
pub struct Selector(Vec<u8>);
|
||||
|
||||
pub trait Query {
|
||||
fn get_single(self) -> Option<Vec<u8>>;
|
||||
fn get_all(self) -> Vec<Vec<u8>>;
|
||||
fn get_iter(self) -> impl Iterator<Item = Vec<u8>>;
|
||||
fn count(self) -> u64;
|
||||
}
|
||||
|
||||
impl Into<Selector> for Vec<u8> {
|
||||
fn into(self) -> Selector {
|
||||
Selector(self)
|
||||
}
|
||||
}
|
13
crates/proto/src/proto/table/mod.rs
Normal file
13
crates/proto/src/proto/table/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
//! Each room has a small, well defined, domain specific relational
|
||||
//! database associated with it. The database's contents will then be defined
|
||||
//! by the events it receives. This module contains database traits and
|
||||
//! how the database's schema is defined.
|
||||
//!
|
||||
//! This is a work in progress; the goal is trying to find a good
|
||||
//! effort/payoff ratio in the database schema definition.
|
||||
|
||||
// mod old;
|
||||
mod kv;
|
||||
|
||||
// pub use old::*;
|
||||
pub use kv::*;
|
|
@ -1,11 +1,3 @@
|
|||
//! Each room has a small, well defined, domain specific relational
|
||||
//! database associated with it. The database's contents will then be defined
|
||||
//! by the events it receives. This module contains database traits and
|
||||
//! how the database's schema is defined.
|
||||
//!
|
||||
//! This is a work in progress; the goal is trying to find a good
|
||||
//! effort/payoff ratio in the database schema definition.
|
||||
|
||||
// TODO: properly type things
|
||||
// maybe replace TableRow with impl Serialize?
|
||||
// and make schema a trait instead of builder?
|
||||
|
@ -13,7 +5,7 @@
|
|||
use std::{collections::HashMap, fmt::Debug};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{actor::ActorId, event::EventId};
|
||||
use crate::{actor::ActorId, event::EventId, resolver::Command};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
// TODO: actually implement Text
|
||||
|
@ -88,7 +80,6 @@ pub struct IndexConfig {
|
|||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TableRow {
|
||||
pub id: u64,
|
||||
pub values: HashMap<String, ColumnValue>,
|
||||
}
|
||||
|
||||
|
@ -313,3 +304,111 @@ impl TableRow {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// alternative way of querying the db
|
||||
|
||||
/// A way to query the database
|
||||
pub struct Query;
|
||||
|
||||
// trait Queryable {}
|
||||
|
||||
pub enum QuerySelector {
|
||||
/// Select one specific value
|
||||
Lookup(ColumnValue),
|
||||
|
||||
/// Select starting from a specific (inclusive)
|
||||
From(ColumnValue),
|
||||
|
||||
/// Select ending at a specific value (inclusive)
|
||||
Until(ColumnValue),
|
||||
}
|
||||
|
||||
pub enum QuerySorter {
|
||||
/// Lowest to highest.
|
||||
Ascending,
|
||||
|
||||
/// Highest to lowest.
|
||||
Descending,
|
||||
}
|
||||
|
||||
// do i do sql?
|
||||
// how much of this becomes a trait, how much is a query struct?
|
||||
impl Query {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// sql FROM
|
||||
pub fn from(self, _table: impl Into<String>) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// sql WHERE
|
||||
pub fn matching(self, _column: impl Into<String>, _matcher: QuerySelector) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// sql ORDER BY
|
||||
pub fn sorted(self, _column: impl Into<String>, _sorter: QuerySorter) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// sql JOIN ON _column = other.column
|
||||
pub fn join_to(self, _column: impl Into<String>) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// sql JOIN ON _other._column = column
|
||||
pub fn join_from(self, _other: Query, _column: impl Into<String>) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Select a single item
|
||||
pub fn first(self) -> Option<TableRow> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Select all items
|
||||
// do i really want this? it allows unbounded access
|
||||
pub fn all(self) -> Vec<TableRow> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Iterate over items
|
||||
pub fn iter(self) -> impl Iterator<Item = TableRow> {
|
||||
todo!();
|
||||
#[allow(unused)]
|
||||
vec![].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
// these would only be available in the resolver
|
||||
impl Query {
|
||||
/// Create a set of commands that will update the selected rows
|
||||
pub fn update<F: FnMut(TableRow) -> ()>(self, _f: F) -> Vec<Command> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Create a set of commands that will replace the selected rows with new rows
|
||||
pub fn put(self, _rows: impl IntoIterator<Item = TableRow>) -> Vec<Command> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Create a command that will delete the selected rows
|
||||
pub fn delete(self) -> Command {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// impl TableRow {
|
||||
// /// creates a command that deletes the row
|
||||
// pub fn delete(self) -> Command {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// /// creates a command that updates the row with new data
|
||||
// pub fn update(&self, _row: TableRow) -> Command {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
14
crates/tui/Cargo.toml
Normal file
14
crates/tui/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
crossterm = "0.27.0"
|
||||
dag-resolve = { version = "0.1.0", path = "../proto" }
|
||||
dag-resolve-impls = { version = "0.1.0", path = "../impls" }
|
||||
ratatui = "0.26.1"
|
||||
tui-term = "0.1.8"
|
85
crates/tui/src/main.rs
Normal file
85
crates/tui/src/main.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{self, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
let mut state = State {
|
||||
view: View::Base
|
||||
};
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| ui(&state, f))?;
|
||||
if let Some(message) = handle_events(&state)? {
|
||||
match message {
|
||||
Message::Quit => break,
|
||||
Message::View(v) => state.view = v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Message {
|
||||
Quit,
|
||||
View(View),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
view: View,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum View {
|
||||
Base,
|
||||
Help,
|
||||
}
|
||||
|
||||
fn handle_events(state: &State) -> Result<Option<Message>> {
|
||||
if event::poll(Duration::from_millis(50))? {
|
||||
match event::read()? {
|
||||
event::Event::Key(key) if key.kind == KeyEventKind::Press => match (&state.view, key.code) {
|
||||
(View::Base, KeyCode::Char('q') | KeyCode::Esc) => return Ok(Some(Message::Quit)),
|
||||
(View::Base, KeyCode::Char('h' | '?')) => {
|
||||
return Ok(Some(Message::View(View::Help)))
|
||||
}
|
||||
(View::Help, KeyCode::Char('q')) => return Ok(Some(Message::View(View::Base))),
|
||||
(_, _) => Ok(None)
|
||||
},
|
||||
_ => Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(state: &State, f: &mut Frame) {
|
||||
match state.view {
|
||||
View::Base => {
|
||||
let p = Paragraph::new("hello world!").block(Block::bordered().title("hello"));
|
||||
f.render_widget(p, f.size())
|
||||
}
|
||||
View::Help => {
|
||||
let p = Paragraph::new("help info").block(Block::bordered().title("help"));
|
||||
f.render_widget(p, f.size())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue