From 523e9a74797fe2d3159af36cee8df9d5493cd2af Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Sun, 21 Apr 2024 12:28:35 +0200 Subject: [PATCH] Initial biting implementation - Rough (untyped) multi-edges implementation - Impl of bites in terms of multi-edges --- bin/pupctl/src/main.rs | 10 +++++++-- lib/puppy/src/lib.rs | 34 +++++++++++++++++++++++++++++-- lib/store/src/arrow.rs | 39 ++++++++++++++++++++++++++++++++++++ lib/store/src/key.rs | 13 +++++++++--- lib/store/src/lib.rs | 3 +++ lib/store/src/transaction.rs | 29 +++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 7 deletions(-) diff --git a/bin/pupctl/src/main.rs b/bin/pupctl/src/main.rs index 13fafe9..d97dc1a 100644 --- a/bin/pupctl/src/main.rs +++ b/bin/pupctl/src/main.rs @@ -7,7 +7,7 @@ use puppy::{ Error, }, tl::Post, - Key, Store, + Bite, Key, Store, }; fn main() -> store::Result<()> { @@ -21,7 +21,7 @@ fn main() -> store::Result<()> { puppy::create_post(&db, riley, "@linen <3")?; puppy::create_post(&db, linen, "@riley <3")?; } - if true { + if false { println!("making riley follow linen"); if !db.exists::((riley, linen))? { println!("follow relation does not exist yet"); @@ -57,6 +57,12 @@ fn main() -> store::Result<()> { let (_, Profile { account_name, .. }) = db.lookup(id)?; println!("- @{account_name} ({id})"); } + println!("Biting riley"); + puppy::bite_actor(&db, linen, riley).unwrap(); + for Bite { id, biter, .. } in puppy::bites_on(&db, riley).unwrap() { + let (_, Profile { account_name, .. }) = db.lookup(biter).unwrap(); + println!("riley was bitten by @{account_name} at {}", id.timestamp()); + } store::OK } diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index 0846f37..3075cd8 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -1,10 +1,11 @@ +#![feature(iterator_try_collect)] pub use store::{self, Key, Store}; use store::{ alias::Username, - arrow::AuthorOf, + arrow::{self, multi::MultiArrow, AuthorOf}, mixin::{Content, Profile}, util::IterExt, - Keylike, + Keylike, Tag, }; mod tags { @@ -14,6 +15,7 @@ mod tags { pub const ACTOR: Tag = Tag(0); pub const POST: Tag = Tag(1); + pub const BITE: Tag = Tag(2); } pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result { @@ -60,6 +62,34 @@ pub fn list_posts_by_author( }) } +pub struct Bite { + pub id: Key, + pub biter: Key, + pub victim: Key, +} + +impl MultiArrow for Bite { + const TYPE: Tag = tags::BITE; +} + +pub fn bite_actor(db: &Store, biter: Key, victim: Key) -> store::Result { + db.transaction(|tx| { + // Bites are represented as multiedges. + let key = arrow::multi::insert::(&tx, biter, victim)?; + // We can treat particular arrows in a quiver as a vertex by registering it. + tx.create_vertex(key, tags::BITE)?; + Ok(key) + }) +} + +pub fn bites_on(db: &Store, victim: Key) -> store::Result> { + db.transaction(|tx| { + arrow::multi::list_incoming::(&tx, victim) + .map_ok(|(biter, id)| Bite { id, biter, victim }) + .try_collect() + }) +} + pub mod tl { //! Timelines diff --git a/lib/store/src/arrow.rs b/lib/store/src/arrow.rs index 1cd602e..bd1ed99 100644 --- a/lib/store/src/arrow.rs +++ b/lib/store/src/arrow.rs @@ -4,6 +4,45 @@ use bincode::{Decode, Encode}; use crate::Space; +pub mod multi { + //! Managing multiedges. + //! + //! Unlike regular [`Arrow`]s, which don't have an identity (they are identified by the two nodes that + //! they connect), multiarrows can have their own [`Key`]. This allows one to have multiple arrows in + //! the same direction connecting the same two vertices, which isn't possible with normal arrows. + //! + //! Multiarrows can also be treated as if they were vertices, if their identity (`Key`) is registered as + //! one. + //! + //! This comes with a trade-off, though, specifically in both space and complexity. A multi-arrow also + //! can't have a label, like a typical arrow. + + use crate::{Key, Result, Tag, Transaction}; + + pub fn insert(tx: &Transaction<'_>, origin: Key, target: Key) -> Result + where + A: MultiArrow, + { + let key = Key::gen(); + tx.quiver(A::TYPE).insert(origin, target, key)?; + Ok(key) + } + + pub fn list_incoming<'db, A>( + tx: &'db Transaction<'db>, + target: Key, + ) -> impl Iterator> + 'db + where + A: MultiArrow, + { + tx.quiver(A::TYPE).list_incoming(target) + } + + pub trait MultiArrow { + const TYPE: Tag; + } +} + /// A directed edge between two vertices. pub trait Arrow: Encode + Decode { const SPACE: (Space, Space); diff --git a/lib/store/src/key.rs b/lib/store/src/key.rs index df0d23e..7036ee7 100644 --- a/lib/store/src/key.rs +++ b/lib/store/src/key.rs @@ -1,20 +1,23 @@ use std::fmt::{Debug, Display}; +use chrono::{DateTime, Utc}; +use ulid::Ulid; + use crate::{Alias, Error, Result, Transaction}; /// A unique identifier for vertices in the database. #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Key([u8; 16]); +pub struct Key(pub(crate) [u8; 16]); impl Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&ulid::Ulid::from_bytes(self.0), f) + Display::fmt(&Ulid::from_bytes(self.0), f) } } impl Debug for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Key({})", ulid::Ulid::from_bytes(self.0)) + write!(f, "Key({})", Ulid::from_bytes(self.0)) } } @@ -27,6 +30,10 @@ impl Key { key.copy_from_slice(&buf); Key(key) } + pub fn timestamp(self) -> DateTime { + let ms = Ulid::from_bytes(self.0).timestamp_ms(); + DateTime::from_timestamp_millis(ms as i64).unwrap() + } /// Join two keys together. pub(crate) fn fuse(self, other: Key) -> [u8; 32] { let mut buf = [0; 32]; diff --git a/lib/store/src/lib.rs b/lib/store/src/lib.rs index f6f3a32..32c4a39 100644 --- a/lib/store/src/lib.rs +++ b/lib/store/src/lib.rs @@ -48,6 +48,9 @@ const SPACES: &[&'static str] = &[ "created-by/r", "pending-fr/l", "pending-fr/r", + "multi:id-map", + "multi:index/l", + "multi:index/r", #[cfg(test)] "test-arrow/l", #[cfg(test)] diff --git a/lib/store/src/transaction.rs b/lib/store/src/transaction.rs index 94e3623..d8e479a 100644 --- a/lib/store/src/transaction.rs +++ b/lib/store/src/transaction.rs @@ -303,6 +303,9 @@ impl Transaction<'_> { decode(v.as_ref()).map(|label| (other, label)) }) } + pub(crate) fn quiver(&self, tag: Tag) -> Quiver<'_> { + Quiver { tag, tx: &self } + } } impl Transaction<'_> { @@ -386,6 +389,32 @@ impl<'db> Keyspace<'db> { } } +/// The quiver allows one to manipulate all parallel edges tagged with a particular type. +pub struct Quiver<'db> { + tx: &'db Transaction<'db>, + tag: Tag, +} + +impl<'db> Quiver<'db> { + pub fn insert(&self, origin: Key, target: Key, identity: Key) -> Result<()> { + let fused = origin.fuse(target); + self.tx.with("multi:id-map").set(identity, fused)?; + let mut triple = [0; 48]; + triple[..32].copy_from_slice(&fused); + triple[32..].copy_from_slice(identity.as_ref()); + self.tx.with("multi:index/l").set(triple, b"")?; + triple[..32].rotate_left(16); + self.tx.with("multi:index/r").set(triple, b"")?; + OK + } + pub fn list_incoming(&self, target: Key) -> impl Iterator> + 'db { + self.tx + .with("multi:index/r") + .scan(target) + .map_ok(|(k, _)| Key::split(&k.as_ref()[16..])) + } +} + fn encode(data: impl Encode) -> Result> { bincode::encode_to_vec(data, bincode::config::standard()).map_err(Error::Encoding) }