random stuff

This commit is contained in:
tezlm 2024-03-04 16:31:16 -08:00
parent bd9184d9cc
commit 3428100fe0
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
17 changed files with 950 additions and 75 deletions

48
Cargo.lock generated
View file

@ -244,6 +244,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -491,12 +497,14 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"thiserror", "thiserror",
"tokio",
] ]
[[package]] [[package]]
name = "dag-resolve-impls" name = "dag-resolve-impls"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-recursion",
"dag-resolve", "dag-resolve",
"futures", "futures",
"rusqlite", "rusqlite",
@ -1145,6 +1153,39 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "logos"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-codegen"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a"
dependencies = [
"beef",
"fnv",
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax",
"syn 2.0.48",
]
[[package]]
name = "logos-derive"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a"
dependencies = [
"logos-codegen",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@ -1422,6 +1463,13 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "query"
version = "0.1.0"
dependencies = [
"logos",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.35"

View file

@ -4,6 +4,7 @@ members = [
"crates/proto", "crates/proto",
"crates/impls", "crates/impls",
"crates/cli", "crates/cli",
"crates/query",
] ]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -21,8 +21,6 @@ use dag_resolve_impls::{
}; };
use server::{HasRequest, HasResponse, ServerState, SyncRequest, SyncResponse}; use server::{HasRequest, HasResponse, ServerState, SyncRequest, SyncResponse};
use crate::server::exec;
#[derive(Parser)] #[derive(Parser)]
#[clap(version, about)] #[clap(version, about)]
enum Cli { enum Cli {
@ -101,7 +99,7 @@ async fn resolve_state<R: Resolver>(room: &RoomOld<R>, db: &Database) -> Result<
room.resolve_state(ResolveInput::AllEvents(events))? room.resolve_state(ResolveInput::AllEvents(events))?
} }
dag_resolve::room::ResolveState::ProcessEvent(idx, events, (step, req)) => { dag_resolve::room::ResolveState::ProcessEvent(idx, events, (step, req)) => {
room.resolve_state(ResolveInput::ProcessEvent(idx, events, (step, exec(req, &db).await)))? room.resolve_state(ResolveInput::ProcessEvent(idx, events, (step, db.exec(req).await)))?
}, },
dag_resolve::room::ResolveState::Done => break, dag_resolve::room::ResolveState::Done => break,
} }
@ -193,11 +191,11 @@ async fn send_event<R: Resolver + Debug>(state: &mut State<R>, data: &str) -> Re
}, },
AppendState::Step2(step, event, req) => { AppendState::Step2(step, event, req) => {
match req { match req {
DatabaseRequest::Done => { DatabaseRequest::Noop => {
AppendInput::Step3(event) AppendInput::Step3(event)
}, },
_ => { _ => {
let res = exec(req, &state.db).await; let res = state.db.exec(req).await;
AppendInput::Step2(step, res, event) AppendInput::Step2(step, res, event)
} }
} }

View file

@ -94,37 +94,6 @@ async fn route_has<R: Resolver>(
Json(HasResponse { disjoint }) Json(HasResponse { disjoint })
} }
#[async_recursion::async_recursion]
pub async fn exec(req: DatabaseRequest, db: &Database) -> DatabaseResponse {
match req {
DatabaseRequest::Done => DatabaseResponse::Done,
DatabaseRequest::Reset => {
db.reset().await;
DatabaseResponse::Done
},
DatabaseRequest::QuerySingle(_, _) => todo!(),
DatabaseRequest::QueryAll(_, _) => todo!(),
DatabaseRequest::QueryIter(_, _) => todo!(),
DatabaseRequest::QueryCount(_, _) => todo!(),
DatabaseRequest::QueryIterReverse(_, _) => todo!(),
DatabaseRequest::Get(table, key) => {
let kv = db.table(table).get(&key).await;
DatabaseResponse::Single(kv)
},
DatabaseRequest::Put(table, key, value) => {
db.table(table).put(&key, &value).await;
DatabaseResponse::Done
},
DatabaseRequest::Delete(table, key) => {
db.table(table).delete(&key).await;
DatabaseResponse::Done
},
DatabaseRequest::WithData(data, req) => {
DatabaseResponse::WithData(data, Box::new(exec(*req, db).await))
},
}
}
async fn append<R>(room: &mut RoomOld<R>, db: &Database, event: Event<R::EventType>) where R: Resolver { async fn append<R>(room: &mut RoomOld<R>, db: &Database, event: Event<R::EventType>) where R: Resolver {
let mut step = room.append_event(AppendInput::Step1(event)).unwrap(); let mut step = room.append_event(AppendInput::Step1(event)).unwrap();
loop { loop {
@ -134,8 +103,8 @@ async fn append<R>(room: &mut RoomOld<R>, db: &Database, event: Event<R::EventTy
}, },
AppendState::Step2(step, event, req) => { AppendState::Step2(step, event, req) => {
match req { match req {
DatabaseRequest::Done => { DatabaseRequest::Noop => {
let res = exec(req, &db).await; let res = db.exec(req).await;
room.append_event(AppendInput::Step2(step, res, event)).unwrap() room.append_event(AppendInput::Step2(step, res, event)).unwrap()
}, },
_ => { _ => {

View file

@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
async-recursion = "1.0.5"
dag-resolve = { version = "0.1.0", path = "../proto" } dag-resolve = { version = "0.1.0", path = "../proto" }
futures = "0.3.30" futures = "0.3.30"
rusqlite = { version = "0.30.0", features = ["bundled"] } rusqlite = { version = "0.30.0", features = ["bundled"] }

View file

@ -1,7 +1,7 @@
//! Store state in a sqlite database //! Store state in a sqlite database
use dag_resolve::{ use dag_resolve::{
actor::{ActorId, ActorSecret}, event::EventId, proto::table, room::{TABLE_EVENTS, TABLE_HEADS} actor::{ActorId, ActorSecret}, event::EventId, proto::table::{self, DatabaseRequest, DatabaseResponse}, room::{TABLE_EVENTS, TABLE_HEADS}
}; };
use futures::{stream, Stream}; use futures::{stream, Stream};
use sqlx::{query, sqlite::SqliteConnectOptions, SqlitePool}; use sqlx::{query, sqlite::SqliteConnectOptions, SqlitePool};
@ -304,3 +304,36 @@ impl<'a> Query<'a> {
self.get_all().await.len() as u64 self.get_all().await.len() as u64
} }
} }
impl Database {
#[async_recursion::async_recursion]
pub async fn exec(&self, req: DatabaseRequest) -> DatabaseResponse {
match req {
DatabaseRequest::Noop => DatabaseResponse::Done,
DatabaseRequest::Reset => {
self.reset().await;
DatabaseResponse::Done
},
DatabaseRequest::QuerySingle(_, _) => todo!(),
DatabaseRequest::QueryAll(_, _) => todo!(),
DatabaseRequest::QueryIter(_, _) => todo!(),
DatabaseRequest::QueryCount(_, _) => todo!(),
DatabaseRequest::QueryIterReverse(_, _) => todo!(),
DatabaseRequest::Get(table, key) => {
let kv = self.table(table).get(&key).await;
DatabaseResponse::Single(kv)
},
DatabaseRequest::Put(table, key, value) => {
self.table(table).put(&key, &value).await;
DatabaseResponse::Done
},
DatabaseRequest::Delete(table, key) => {
self.table(table).delete(&key).await;
DatabaseResponse::Done
},
DatabaseRequest::WithData(data, req) => {
DatabaseResponse::WithData(data, Box::new(self.exec(*req).await))
},
}
}
}

View file

@ -17,3 +17,4 @@ serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113" serde_json = "1.0.113"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.56" thiserror = "1.0.56"
tokio = { version = "1.36.0", features = ["test-util", "macros"] }

View file

@ -12,6 +12,8 @@ use std::{
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use super::room2::{Room as _, RoomIdle};
#[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "&str", into = "String")] #[serde(try_from = "&str", into = "String")]
pub struct EventId(EventIdData); pub struct EventId(EventIdData);
@ -176,6 +178,36 @@ impl<T: Debug + Serialize + Clone> Event<T> {
Ok(()) Ok(())
} }
pub fn verify_room2<R: Resolver<EventType = T>>(
&self,
room: &RoomIdle<R>,
) -> Result<()> {
self.verify()?;
let room_config = match &room.get_root().content {
EventContent::Create(c) => c,
_ => unreachable!("the root should always be a create event"),
};
if room_config.hasher != self.id.get_type() {
return Err(Error::MismatchedHasher {
expected: room_config.hasher,
got: self.id.get_type(),
});
}
if room_config.signer != self.author.get_type()
|| room_config.signer != self.signature.get_type()
{
return Err(Error::MismatchedSigner {
expected: room_config.signer,
got: self.author.get_type(),
});
}
Ok(())
}
pub fn id(&self) -> &EventId { pub fn id(&self) -> &EventId {
&self.id &self.id

View file

@ -5,5 +5,6 @@ pub mod resolver;
pub mod room; pub mod room;
pub mod table; pub mod table;
pub mod atoms; pub mod atoms;
mod room2; pub mod room2;
mod room3;
mod rpc; mod rpc;

View file

@ -145,7 +145,7 @@ impl<R: Resolver> RoomOld<R> {
// ideally i don't get *all* events up front, and query the db as needed // ideally i don't get *all* events up front, and query the db as needed
let resolver = self.get_resolver(); let resolver = self.get_resolver();
let sorted = sort(|a, b| resolver.tiebreak(a, b), &events).into_iter().cloned().collect(); let sorted = sort(|a, b| resolver.tiebreak(a, b), &events).into_iter().cloned().collect();
Ok(ResolveState::ProcessEvent(0, sorted, (0, DatabaseRequest::Done))) Ok(ResolveState::ProcessEvent(0, sorted, (0, DatabaseRequest::Noop)))
}, },
ResolveInput::ProcessEvent(idx, events, state) => { ResolveInput::ProcessEvent(idx, events, state) => {
let resolver = self.get_resolver(); let resolver = self.get_resolver();
@ -153,7 +153,7 @@ impl<R: Resolver> RoomOld<R> {
match resolver.resolve(state, &events[idx]) { match resolver.resolve(state, &events[idx]) {
Ok(()) => { Ok(()) => {
if idx + 1 < events.len() { if idx + 1 < events.len() {
Ok(ResolveState::ProcessEvent(idx + 1, events, (0, DatabaseRequest::Done))) Ok(ResolveState::ProcessEvent(idx + 1, events, (0, DatabaseRequest::Noop)))
} else { } else {
Ok(ResolveState::Done) Ok(ResolveState::Done)
} }

View file

@ -1,15 +1,59 @@
#![allow(unused)]
//! Possibly another way rooms could be handled? //! Possibly another way rooms could be handled?
//! A state machine representing a room //! A state machine representing a room
use super::table::Selector; use std::collections::VecDeque;
struct RoomIdle; use crate::{
struct RoomRead; actor::ActorSecret,
struct RoomWrite; event::{Event, EventContent, EventId},
proto::table::DatabaseResponse,
resolver::{sort, Resolver, Verification},
room::{TABLE_EVENTS, TABLE_HEADS},
};
enum RejectReason { use super::table::{DatabaseRequest, Selector};
pub struct RoomIdle<R: Resolver> {
data: RoomData<R>,
}
pub struct RoomResolveReset<R: Resolver> {
data: RoomData<R>,
}
pub struct RoomResolveAllEvents<R: Resolver> {
data: RoomData<R>,
req: DatabaseRequest,
}
pub struct RoomResolveStep<R: Resolver> {
data: RoomData<R>,
events: Vec<Event<R::EventType>>,
event_idx: u64,
event_step: u64,
event_res: Option<DatabaseResponse>,
}
pub struct RoomCreateHeads<R: Resolver> {
data: RoomData<R>,
req: DatabaseRequest,
event_content: EventContent<R::EventType>,
secret: ActorSecret,
}
pub struct RoomAppendVerify<R: Resolver> {
data: RoomData<R>,
req: DatabaseRequest,
step: usize,
event: Event<R::EventType>,
}
pub struct RoomAppendWrite<R: Resolver> {
data: RoomData<R>,
reqs: VecDeque<DatabaseRequest>,
}
pub enum RejectReason {
/// The event has invalid data /// The event has invalid data
Invalid, Invalid,
@ -23,38 +67,274 @@ enum RejectReason {
Panic, Panic,
} }
enum RoomOutput { /// the internal data of the room
/// The event is valid and has been added struct RoomData<R: Resolver> {
Accept(RoomIdle), root: Event<R::EventType>,
resolver: R,
/// The event has been rejected (but shouldn't necessarily be dropped)
Reject(RejectReason, RoomIdle),
/// The room state machine is requesting to read data
Read(RoomRead),
/// The room state machine is requesting to write data
Write(RoomWrite),
} }
impl RoomIdle { pub trait Room<R: Resolver> {
/// Enters into the event processing state. Pretend its a coroutine for processing an event. fn get_root(&self) -> &Event<R::EventType>;
pub fn process_event(self) -> RoomOutput {
fn get_resolver(&self) -> &R;
}
macro_rules! impl_room {
($state:ident) => {
impl<R: Resolver> Room<R> for $state<R> {
fn get_root(&self) -> &Event<<R as Resolver>::EventType> {
&self.data.root
}
fn get_resolver(&self) -> &R {
&self.data.resolver
}
}
};
}
impl_room!(RoomIdle);
impl_room!(RoomResolveReset);
impl_room!(RoomResolveStep);
impl_room!(RoomResolveAllEvents);
impl_room!(RoomCreateHeads);
impl_room!(RoomAppendVerify);
impl_room!(RoomAppendWrite);
impl<R: Resolver> RoomIdle<R> {
/// Process a new event from the network or appended locally.
pub fn process_event(self, event: Event<R::EventType>) -> RoomAppendVerifyOutput<R> {
// try to append event here // try to append event here
todo!() event
.verify_room2(&self)
.expect("event failed verification");
let a = (0, DatabaseResponse::Done);
match self.get_resolver().verify(a, &event) {
Ok(verification) => match verification {
Verification::Valid => RoomAppendVerifyOutput::Write(RoomAppendWrite {
data: self.data,
reqs: get_reqs::<R>(&event),
}),
Verification::Unauthorized => panic!("unauthorized"),
Verification::Invalid => panic!("invalid data"),
},
Err(req) => {
if req.is_read() {
RoomAppendVerifyOutput::Verify(RoomAppendVerify {
data: self.data,
req,
step: 0,
event,
})
} else {
panic!("can't write during verify");
}
}
}
}
pub fn resolve_state(self) -> RoomResolveReset<R> {
RoomResolveReset { data: self.data }
}
/// creates a new event. call `append_event` afterwards
pub fn create_event(self, event_content: EventContent<R::EventType>, secret: &ActorSecret) -> RoomCreateHeads<R> {
RoomCreateHeads {
data: self.data,
req: DatabaseRequest::QueryAll(TABLE_HEADS.into(), Selector::Any),
event_content,
secret: secret.clone(),
}
} }
} }
impl RoomRead { impl<R: Resolver> RoomCreateHeads<R> {
pub fn done(self, res: DatabaseResponse) -> (RoomIdle<R>, Event<R::EventType>) {
let heads: Vec<_> = match res {
DatabaseResponse::Many(records) => records
.into_iter()
.map(|(id, _)| EventId::from_bytes(&id))
.collect(),
_ => panic!(),
};
let event = Event::builder(self.event_content, &self.secret)
.with_references(heads)
.then_hash()
.unwrap()
.and_sign()
.unwrap();
let idle = RoomIdle { data: self.data };
(idle, event)
}
/// what to do
pub fn what(&self) -> &DatabaseRequest {
&self.req
}
}
pub enum RoomAppendVerifyOutput<R: Resolver> {
Verify(RoomAppendVerify<R>),
Write(RoomAppendWrite<R>),
}
pub enum RoomAppendWriteOutput<R: Resolver> {
Write(RoomAppendWrite<R>),
Idle(RoomIdle<R>),
}
fn get_reqs<R: Resolver>(event: &Event<R::EventType>) -> VecDeque<DatabaseRequest> {
let mut reqs = VecDeque::new();
for head in event.references() {
reqs.push_back(DatabaseRequest::Delete(TABLE_HEADS.into(), head.to_bytes()));
}
reqs.push_back(DatabaseRequest::Put(
TABLE_EVENTS.into(),
event.id().to_bytes(),
serde_json::to_vec(&event).unwrap(),
));
reqs.push_back(DatabaseRequest::Put(
TABLE_HEADS.into(),
event.id().to_bytes(),
vec![],
));
reqs
}
impl<R: Resolver> RoomAppendVerify<R> {
/// Done reading (call with DatabaseResponse::Done first)
pub fn done(self, response: DatabaseResponse) -> RoomAppendVerifyOutput<R> {
let a = (self.step as u64, response);
match self.get_resolver().verify(a, &self.event) {
Ok(verification) => match verification {
Verification::Valid => RoomAppendVerifyOutput::Write(RoomAppendWrite {
data: self.data,
reqs: get_reqs::<R>(&self.event),
}),
Verification::Unauthorized => panic!("unauthorized"),
Verification::Invalid => panic!("invalid data"),
},
Err(req) => {
if req.is_read() {
RoomAppendVerifyOutput::Verify(RoomAppendVerify {
data: self.data,
req,
step: self.step + 1,
event: self.event,
})
} else {
panic!("can't write during verify");
}
}
}
}
/// what to do
pub fn what(&self) -> &DatabaseRequest {
&self.req
}
}
impl<R: Resolver> RoomAppendWrite<R> {
/// Done reading /// Done reading
pub fn done(self, data: Vec<u8>) -> RoomOutput { pub fn done(mut self, response: DatabaseResponse) -> RoomAppendWriteOutput<R> {
todo!() match response {
DatabaseResponse::Done => {},
_ => panic!(),
}
if self.reqs.pop_front().is_none() {
RoomAppendWriteOutput::Idle(RoomIdle { data: self.data })
} else {
RoomAppendWriteOutput::Write(self)
}
}
/// what to do
pub fn what(&self) -> &DatabaseRequest {
self.reqs.front().as_ref().unwrap()
} }
} }
impl RoomWrite { impl<R: Resolver> RoomResolveReset<R> {
/// Done writing /// Done reading
pub fn done(self) -> RoomOutput { pub fn done(self, res: DatabaseResponse) -> RoomResolveAllEvents<R> {
todo!() match res {
DatabaseResponse::Done => RoomResolveAllEvents {
data: self.data,
req: DatabaseRequest::QueryAll(TABLE_EVENTS.into(), Selector::Any),
},
_ => panic!(),
}
}
/// what to do
pub fn what(&self) -> &DatabaseRequest {
&DatabaseRequest::Reset
}
}
impl<R: Resolver> RoomResolveAllEvents<R> {
/// Done reading
pub fn done(self, res: DatabaseResponse) -> RoomResolveStep<R> {
match res {
DatabaseResponse::Many(records) => {
// ideally i don't get *all* events up front, and query the db as needed
let events: Vec<_> = records.into_iter().map(|(_, data)| serde_json::from_slice(&data).unwrap()).collect();
let resolver = self.get_resolver();
let sorted = sort(|a, b| resolver.tiebreak(a, b), &events).into_iter().cloned().collect();
RoomResolveStep {
data: self.data,
events: sorted,
event_idx: 0,
event_step: 0,
event_res: Some(DatabaseResponse::Done),
}
},
_ => panic!(),
}
}
/// what to do
pub fn what(&self) -> &DatabaseRequest {
&self.req
}
}
pub enum RoomResolveStepOutput<R: Resolver> {
Step(RoomResolveStep<R>),
Done(RoomIdle<R>),
}
impl<R: Resolver> RoomResolveStep<R> {
/// Done with io
pub fn done(mut self, res: DatabaseResponse) -> RoomResolveStepOutput<R> {
if (self.event_idx as usize) < self.events.len() {
assert!(self.event_res.is_none());
self.event_res = Some(res);
RoomResolveStepOutput::Step(self)
} else {
RoomResolveStepOutput::Done(RoomIdle { data: self.data })
}
}
/// what to do
pub fn what(&mut self) -> DatabaseRequest {
let res = self.event_res.take().unwrap();
match self.get_resolver().resolve((self.event_step, res), &self.events[self.event_idx as usize]) {
Ok(()) => {
self.event_idx += 1;
if (self.event_idx as usize) < self.events.len() {
self.event_res = Some(DatabaseResponse::Done);
self.what()
} else {
DatabaseRequest::Noop
}
},
Err(req) => {
self.event_step += 1;
req
},
}
} }
} }

View file

@ -0,0 +1,146 @@
#![allow(unused)]
use std::{
cell::OnceCell,
future::IntoFuture,
pin::Pin,
sync::{Arc, OnceLock},
task::{Context, Poll, Wake, Waker},
};
use futures::{Future, FutureExt};
async fn verify<S: DbSync, A: DbAsync>(db: Db<S, A>) {
println!("getting...");
assert_eq!(db.get().await, vec![123]);
println!("...got!");
}
struct CommandGet<'a, S: DbSync, A: DbAsync> {
db: &'a Db<S, A>,
data: Option<Vec<u8>>,
}
enum DbType<S: DbSync, A: DbAsync> {
Sync(S),
Async(A),
}
struct FakeSync;
impl DbSync for FakeSync {
fn get(&self) -> Vec<u8> {
todo!()
}
}
struct FakeAsync;
impl DbAsync for FakeAsync {
async fn get(&self) -> Vec<u8> {
todo!()
}
}
impl<'a, S: DbSync, A: DbAsync> Future for CommandGet<'a, S, A> {
type Output = Vec<u8>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(data) = self.data.take() {
Poll::Ready(data)
} else {
match &self.db.inner {
DbType::Sync(s) => {
self.data = Some(s.get());
cx.waker().wake_by_ref();
Poll::Pending
},
DbType::Async(a) => {
Box::pin(a.get().into_future()).poll_unpin(cx)
},
}
}
}
}
struct Db<S: DbSync, A: DbAsync> {
inner: DbType<S, A>,
}
impl<S: DbSync, A: DbAsync> Db<S, A> {
fn new(inner: DbType<S, A>) -> Self {
Db { inner }
}
fn get(&self) -> CommandGet<'_, S, A> {
CommandGet {
db: self,
data: None,
}
}
}
trait DbImpl {
fn get(&self, cb: &mut dyn FnMut(Vec<u8>));
}
trait DbSync {
fn get(&self) -> Vec<u8>;
}
trait DbAsync {
async fn get(&self) -> Vec<u8>;
}
struct DbWaker;
impl Wake for DbWaker {
fn wake(self: Arc<Self>) {
// nothing yet...
}
}
fn run_sync<D: DbSync + 'static>(d: D) {
let db = Db::new(DbType::<_, FakeAsync>::Sync(d));
let mut ver = Box::pin(verify(db));
let waker = Waker::from(Arc::new(DbWaker));
let mut ctx = Context::from_waker(&waker);
loop {
match ver.as_mut().poll(&mut ctx) {
Poll::Ready(_) => break,
Poll::Pending => {}
}
}
}
async fn run_async<D: DbAsync + 'static>(d: D) {
verify(Db::new(DbType::<FakeSync, _>::Async(d))).await;
}
#[test]
fn test_sync() {
struct TestDb;
impl DbSync for TestDb {
fn get(&self) -> Vec<u8> {
vec![123]
}
}
impl DbImpl for TestDb {
fn get(&self, cb: &mut dyn FnMut(Vec<u8>)) {
cb(DbSync::get(self))
}
}
run_sync(TestDb);
}
#[tokio::test]
async fn test_async() {
struct TestDb;
impl DbAsync for TestDb {
async fn get(&self) -> Vec<u8> {
vec![123]
}
}
run_async(TestDb).await;
}

View file

@ -3,9 +3,10 @@
//! an experiment in making the database a graph db? //! an experiment in making the database a graph db?
/* /*
each database is a list of facts and indexes matching a schema each database is a list of facts, rules, and indexes matching a schema
each fact is an (entity, property, value) tuple each fact is an (entity, property, value) tuple
each fact can only be looked up by an index or reference each fact can only be looked up by an index or reference
each rule generates more facts (like a materialized view)
*/ */
struct Database { struct Database {

View file

@ -2,7 +2,7 @@ type Bytes = Vec<u8>;
#[derive(Debug)] #[derive(Debug)]
pub enum DatabaseRequest { pub enum DatabaseRequest {
Done, Noop,
Reset, Reset,
QuerySingle(String, Selector), QuerySingle(String, Selector),
QueryAll(String, Selector), QueryAll(String, Selector),
@ -15,7 +15,7 @@ pub enum DatabaseRequest {
WithData(Vec<u8>, Box<DatabaseRequest>), WithData(Vec<u8>, Box<DatabaseRequest>),
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum DatabaseResponse { pub enum DatabaseResponse {
Done, Done,
Single(Option<(Bytes, Bytes)>), Single(Option<(Bytes, Bytes)>),
@ -37,3 +37,37 @@ impl From<Vec<u8>> for Selector {
Selector::Exact(value) Selector::Exact(value)
} }
} }
impl DatabaseRequest {
pub fn is_read(&self) -> bool {
match self {
DatabaseRequest::Noop => true,
DatabaseRequest::Reset => false,
DatabaseRequest::QuerySingle(_, _) => true,
DatabaseRequest::QueryAll(_, _) => true,
DatabaseRequest::QueryIter(_, _) => true,
DatabaseRequest::QueryCount(_, _) => true,
DatabaseRequest::QueryIterReverse(_, _) => true,
DatabaseRequest::Get(_, _) => true,
DatabaseRequest::Put(_, _, _) => false,
DatabaseRequest::Delete(_, _) => false,
DatabaseRequest::WithData(_, q) => q.is_read(),
}
}
}
// trait DatabaseImpl {
// fn reset(&self, callback: dyn FnOnce());
// fn query(&self, table: &str, selector: Selector, callback: dyn FnOnce(Vec<(Bytes, Bytes)>));
// fn get(&self, table: &str, key: Bytes, callback: dyn FnOnce(Vec<(Bytes, Bytes)>));
// fn put(&self, table: &str, key: Bytes, value: Bytes, callback: dyn FnOnce());
// fn delete(&self, table: &str, key: Bytes, callback: dyn FnOnce());
// }
// trait NetworkImpl {
// fn foo();
// }
// trait RoomImpl {
// fn handle();
// }

9
crates/query/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "query"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
logos = "0.14.0"

291
crates/query/src/main.rs Normal file
View file

@ -0,0 +1,291 @@
#![allow(unused)]
use logos::{Lexer, Logos};
#[derive(Logos, Debug, PartialEq)]
#[logos(skip r"[ \t\n\f]+")] // Ignore this regex pattern between tokens
enum Token {
#[token("[")]
OpenBracket,
#[token("]")]
CloseBracket,
#[token("_")]
Blank,
#[regex(":[a-zA-Z/]+")]
Atom,
#[regex("\\?[a-zA-Z]+")]
Thing,
#[regex("[a-zA-Z]+")]
Identifier,
#[regex("[0-9]+")]
Number,
}
#[derive(Debug, PartialEq)]
struct Parsed {
root: Data,
}
#[derive(Debug, PartialEq)]
enum Data {
Atom(String),
Thing(String),
Identifier(String),
Number(u64),
Tuple(Vec<Data>),
Blank,
}
fn parse(lex: &mut Lexer<'_, Token>, token: Token) -> Data {
match token {
Token::OpenBracket => {
let mut values = vec![];
loop {
let data = match lex.next().expect("missing token").unwrap() {
Token::CloseBracket => break,
token => parse(lex, token),
};
values.push(data);
}
Data::Tuple(values)
},
Token::CloseBracket => panic!("unexpected closing bracket"),
Token::Atom => Data::Atom(lex.slice().to_string()),
Token::Thing => Data::Thing(lex.slice().to_string()),
Token::Identifier => Data::Identifier(lex.slice().to_string()),
Token::Number => Data::Number(lex.slice().parse().unwrap()),
Token::Blank => Data::Blank,
}
}
#[test]
fn basic() {
let mut lex = Token::lexer("
[:find ?name :where
[?post :post/id 123]
[?post :post/author ?author]
[?author :user/name ?name]]");
let token = lex.next().expect("unexpected eof").unwrap();
let parsed = Parsed {
root: parse(&mut lex, token),
};
use Data::*;
let target = Parsed {
root: Tuple(vec![
Atom(":find".into()), Thing("?name".into()), Atom(":where".into()),
Tuple(vec![Thing("?post".into()), Atom(":post/id".into()), Number(123)]),
Tuple(vec![Thing("?post".into()), Atom(":post/author".into()), Thing("?author".into())]),
Tuple(vec![Thing("?author".into()), Atom(":user/name".into()), Thing("?name".into())]),
],
),
};
assert_eq!(parsed, target);
}
enum Command {
/// define a schema
Schema(CommandSchema),
/// query the database
Find(CommandFind),
/// insert some triples
Insert(CommandInsert),
/// update or delete some triples
Update(CommandUpdate),
/// get info and stats
Info,
}
struct CommandFind {
/// return these items
select: Vec<Selection>,
/// using these inputs
inputs: Vec<usize>,
/// with these filters
filter: Vec<Filter>,
/// ordered by these keys
order: Vec<usize>,
}
struct CommandSchema {
item: SchemaItem,
}
struct CommandInsert {
triples: Vec<Triple>,
}
struct CommandUpdate {
/// using these inputs
inputs: Vec<usize>,
/// with these filters
filter: Vec<Filter>,
/// do this upate
update: Vec<Update>,
}
enum Update {
/// add a triple to an entity
Append {
property: String,
data: Data,
},
/// set a triple
Set {
property: String,
data: Data,
},
/// update a triple
Update {
property: String,
func: Operator,
},
/// remote a triple
Remove {
property: String,
data: Option<Data>,
},
/// remote the entity a triple points to
RemoveEntity,
}
struct Triple {
entity: u64,
property: String,
data: Data,
}
enum SchemaItem {
/// a new thing
Thing {
name: String,
documentation: Option<String>,
cardinality: Cardinality,
},
/// a value on a thing
Value {
thing: String,
name: String,
ty: Type,
cardinality: Cardinality,
documentation: Option<String>,
unique: bool, // implemented as a custom predicate?
predicate: Filter,
index: Vec<Index>,
// at: Option<EventId>,
},
/// materialized view/stored procedure
Rule {
name: String,
find: CommandFind,
},
/// like a rule but asserts data instead of generating it
Check {
name: String,
find: Vec<Filter>,
},
}
enum Index {
Fulltext,
Lookup,
Ordered,
}
enum Filter {
Equals {
left: Data,
right: Data,
name: Data,
},
Operator(Operator),
}
enum Operator {
Not(Box<Operator>),
Any(Vec<Operator>),
All(Vec<Operator>),
Less(Data, Data),
Greater(Data, Data),
LessEqual(Data, Data),
GreaterEqual(Data, Data),
Add(Data, Data),
Sub(Data, Data),
Mul(Data, Data),
Div(Data, Data),
FullText(Data, Data),
Missing(Data, Data),
Rule(Vec<Data>),
}
enum Selection {
Thing(usize),
Count(usize),
Min(usize),
Max(usize),
Sum(usize),
First(usize),
Last(usize),
Head(usize),
Tail(usize),
}
enum Type {
UInt,
Int,
Float,
Instant,
String,
Text,
Bytes,
Ref,
/// `[:enum rel/follows rel/blocks]` to only allow one or the other
/// `[:enum none [some value]]` where `value` is another column
/// overrides that column's cardinality? how does this work?
Enum,
}
#[derive(Debug, Default, PartialEq, Eq)]
enum Cardinality {
One,
ZeroOne,
OnePlus,
#[default]
ZeroPlus,
}
fn main() {
let mut lex = Token::lexer("
[:find ?name :where
[?post :post/id 123]
[?post :post/author ?author]
[?author :user/name ?name]]");
let token = lex.next().expect("unexpected eof").unwrap();
let parsed = Parsed {
root: parse(&mut lex, token),
};
dbg!(parsed);
}

30
docs/protocol.md Normal file
View file

@ -0,0 +1,30 @@
# protocol
there's the core protocol. each room contains a directed acyclic graph
of events. each event is signed and represents one atomic mutation. each
room has some immutable config set by the root event. resolvers contain
tiebreaking, a verifier, a reducer. the reducer takes in the events and
outputs state/a database. rooms can be synced between any two nodes,
whether they're server-server, client-server, or peer-peer.
the database. not sure if it will be key value, relational, graph,
triplestore? it needs to be fairly flexible to support a wide variety of
use cases. query peers for database directly without needing to sync room,
for peeking or custom feeds/search indexes. most features are implemented
as special server-generated databases.
the frontend. email/forum-like discussion and documents like the web or
a git repo are the two main planned things. ideally it would be efficient
with your time, bringing back inboxes.
## database
key/value sucks in general.
relational is well established and works pretty well. i think a
relational database with datalog-like querying would be pretty good
overall, but having relations be tables is still a bit annoying and
relational dbs don't seem to work with tagged enums particularily well.
i think triplestores with a datalog dialect would be interesting. this
is the best contender right now, with much simpler joins than sql.