Initial biting implementation

- Rough (untyped) multi-edges implementation
- Impl of bites in terms of multi-edges
This commit is contained in:
Riley Apeldoorn 2024-04-21 12:28:35 +02:00
parent ad62fec21f
commit 523e9a7479
6 changed files with 121 additions and 7 deletions

View file

@ -7,7 +7,7 @@ use puppy::{
Error, Error,
}, },
tl::Post, tl::Post,
Key, Store, Bite, Key, Store,
}; };
fn main() -> store::Result<()> { fn main() -> store::Result<()> {
@ -21,7 +21,7 @@ fn main() -> store::Result<()> {
puppy::create_post(&db, riley, "@linen <3")?; puppy::create_post(&db, riley, "@linen <3")?;
puppy::create_post(&db, linen, "@riley <3")?; puppy::create_post(&db, linen, "@riley <3")?;
} }
if true { if false {
println!("making riley follow linen"); println!("making riley follow linen");
if !db.exists::<Follows>((riley, linen))? { if !db.exists::<Follows>((riley, linen))? {
println!("follow relation does not exist yet"); println!("follow relation does not exist yet");
@ -57,6 +57,12 @@ fn main() -> store::Result<()> {
let (_, Profile { account_name, .. }) = db.lookup(id)?; let (_, Profile { account_name, .. }) = db.lookup(id)?;
println!("- @{account_name} ({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 store::OK
} }

View file

@ -1,10 +1,11 @@
#![feature(iterator_try_collect)]
pub use store::{self, Key, Store}; pub use store::{self, Key, Store};
use store::{ use store::{
alias::Username, alias::Username,
arrow::AuthorOf, arrow::{self, multi::MultiArrow, AuthorOf},
mixin::{Content, Profile}, mixin::{Content, Profile},
util::IterExt, util::IterExt,
Keylike, Keylike, Tag,
}; };
mod tags { mod tags {
@ -14,6 +15,7 @@ mod tags {
pub const ACTOR: Tag = Tag(0); pub const ACTOR: Tag = Tag(0);
pub const POST: Tag = Tag(1); 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<Key> { pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result<Key> {
@ -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<Key> {
db.transaction(|tx| {
// Bites are represented as multiedges.
let key = arrow::multi::insert::<Bite>(&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<Vec<Bite>> {
db.transaction(|tx| {
arrow::multi::list_incoming::<Bite>(&tx, victim)
.map_ok(|(biter, id)| Bite { id, biter, victim })
.try_collect()
})
}
pub mod tl { pub mod tl {
//! Timelines //! Timelines

View file

@ -4,6 +4,45 @@ use bincode::{Decode, Encode};
use crate::Space; 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<A>(tx: &Transaction<'_>, origin: Key, target: Key) -> Result<Key>
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<Item = Result<(Key, Key)>> + 'db
where
A: MultiArrow,
{
tx.quiver(A::TYPE).list_incoming(target)
}
pub trait MultiArrow {
const TYPE: Tag;
}
}
/// A directed edge between two vertices. /// A directed edge between two vertices.
pub trait Arrow: Encode + Decode { pub trait Arrow: Encode + Decode {
const SPACE: (Space, Space); const SPACE: (Space, Space);

View file

@ -1,20 +1,23 @@
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use chrono::{DateTime, Utc};
use ulid::Ulid;
use crate::{Alias, Error, Result, Transaction}; use crate::{Alias, Error, Result, Transaction};
/// A unique identifier for vertices in the database. /// A unique identifier for vertices in the database.
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Key([u8; 16]); pub struct Key(pub(crate) [u8; 16]);
impl Display for Key { impl Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 { impl Debug for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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.copy_from_slice(&buf);
Key(key) Key(key)
} }
pub fn timestamp(self) -> DateTime<Utc> {
let ms = Ulid::from_bytes(self.0).timestamp_ms();
DateTime::from_timestamp_millis(ms as i64).unwrap()
}
/// Join two keys together. /// Join two keys together.
pub(crate) fn fuse(self, other: Key) -> [u8; 32] { pub(crate) fn fuse(self, other: Key) -> [u8; 32] {
let mut buf = [0; 32]; let mut buf = [0; 32];

View file

@ -48,6 +48,9 @@ const SPACES: &[&'static str] = &[
"created-by/r", "created-by/r",
"pending-fr/l", "pending-fr/l",
"pending-fr/r", "pending-fr/r",
"multi:id-map",
"multi:index/l",
"multi:index/r",
#[cfg(test)] #[cfg(test)]
"test-arrow/l", "test-arrow/l",
#[cfg(test)] #[cfg(test)]

View file

@ -303,6 +303,9 @@ impl Transaction<'_> {
decode(v.as_ref()).map(|label| (other, label)) decode(v.as_ref()).map(|label| (other, label))
}) })
} }
pub(crate) fn quiver(&self, tag: Tag) -> Quiver<'_> {
Quiver { tag, tx: &self }
}
} }
impl Transaction<'_> { 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<Item = Result<(Key, Key)>> + 'db {
self.tx
.with("multi:index/r")
.scan(target)
.map_ok(|(k, _)| Key::split(&k.as_ref()[16..]))
}
}
fn encode(data: impl Encode) -> Result<Vec<u8>> { fn encode(data: impl Encode) -> Result<Vec<u8>> {
bincode::encode_to_vec(data, bincode::config::standard()).map_err(Error::Encoding) bincode::encode_to_vec(data, bincode::config::standard()).map_err(Error::Encoding)
} }