Implement EuphVault
This commit is contained in:
parent
58c6c90055
commit
dcbad0a739
7 changed files with 362 additions and 7 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -832,6 +832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
|
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"chrono",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ crossterm = "0.23.2"
|
||||||
directories = "4.0.1"
|
directories = "4.0.1"
|
||||||
edit = "0.1.4"
|
edit = "0.1.4"
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
rusqlite = "0.27.0"
|
rusqlite = { version = "0.27.0", features = ["chrono"] }
|
||||||
serde = { version = "1.0.137", features = ["derive"] }
|
serde = { version = "1.0.137", features = ["derive"] }
|
||||||
serde_json = "1.0.81"
|
serde_json = "1.0.81"
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ use std::convert::Infallible;
|
||||||
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
pub use api::{Message, SessionView, Snowflake, Time, UserId};
|
||||||
|
|
||||||
enum Request {}
|
enum Request {}
|
||||||
|
|
||||||
pub struct EuphRoom {
|
pub struct EuphRoom {
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ pub struct SessionView {
|
||||||
/// A 13-character string, usually used as aunique identifier for some type of object.
|
/// A 13-character string, usually used as aunique identifier for some type of object.
|
||||||
///
|
///
|
||||||
/// It is the base-36 encoding of an unsigned, 64-bit integer.
|
/// It is the base-36 encoding of an unsigned, 64-bit integer.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Snowflake(pub u64);
|
pub struct Snowflake(pub u64);
|
||||||
|
|
||||||
impl Serialize for Snowflake {
|
impl Serialize for Snowflake {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod euph;
|
||||||
mod migrate;
|
mod migrate;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -6,9 +7,11 @@ use std::{fs, thread};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
use self::euph::{EuphRequest, EuphVault};
|
||||||
|
|
||||||
enum Request {
|
enum Request {
|
||||||
Close(oneshot::Sender<()>),
|
Close(oneshot::Sender<()>),
|
||||||
Nop,
|
Euph(EuphRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Vault {
|
pub struct Vault {
|
||||||
|
|
@ -21,24 +24,30 @@ impl Vault {
|
||||||
let _ = self.tx.send(Request::Close(tx)).await;
|
let _ = self.tx.send(Request::Close(tx)).await;
|
||||||
let _ = rx.await;
|
let _ = rx.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn euph(&self, room: String) -> EuphVault {
|
||||||
|
EuphVault {
|
||||||
|
tx: self.tx.clone(),
|
||||||
|
room,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(conn: Connection, mut rx: mpsc::Receiver<Request>) -> anyhow::Result<()> {
|
fn run(conn: Connection, mut rx: mpsc::Receiver<Request>) {
|
||||||
while let Some(request) = rx.blocking_recv() {
|
while let Some(request) = rx.blocking_recv() {
|
||||||
match request {
|
match request {
|
||||||
Request::Close(tx) => {
|
Request::Close(tx) => {
|
||||||
println!("Optimizing vault");
|
println!("Optimizing vault");
|
||||||
conn.execute_batch("PRAGMA optimize")?;
|
let _ = conn.execute_batch("PRAGMA optimize");
|
||||||
// Ensure `Vault::close` exits only after the sqlite connection
|
// Ensure `Vault::close` exits only after the sqlite connection
|
||||||
// has been closed properly.
|
// has been closed properly.
|
||||||
drop(conn);
|
drop(conn);
|
||||||
drop(tx);
|
drop(tx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Request::Nop => {}
|
Request::Euph(r) => r.perform(&conn),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch(path: &Path) -> rusqlite::Result<Vault> {
|
pub fn launch(path: &Path) -> rusqlite::Result<Vault> {
|
||||||
|
|
|
||||||
329
cove-tui/src/vault/euph.rs
Normal file
329
cove-tui/src/vault/euph.rs
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
|
use crate::euph::Snowflake;
|
||||||
|
use crate::store::{Msg, MsgStore, Path, Tree};
|
||||||
|
|
||||||
|
use super::Request;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EuphMsg {
|
||||||
|
id: Snowflake,
|
||||||
|
parent: Option<Snowflake>,
|
||||||
|
time: DateTime<Utc>,
|
||||||
|
nick: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Msg for EuphMsg {
|
||||||
|
type Id = Snowflake;
|
||||||
|
|
||||||
|
fn id(&self) -> Self::Id {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parent(&self) -> Option<Self::Id> {
|
||||||
|
self.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time(&self) -> DateTime<Utc> {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nick(&self) -> String {
|
||||||
|
self.nick.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(&self) -> String {
|
||||||
|
self.content.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EuphRequest> for Request {
|
||||||
|
fn from(r: EuphRequest) -> Self {
|
||||||
|
Self::Euph(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EuphVault {
|
||||||
|
pub(super) tx: mpsc::Sender<Request>,
|
||||||
|
pub(super) room: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MsgStore<EuphMsg> for EuphVault {
|
||||||
|
async fn path(&self, id: &Snowflake) -> Path<Snowflake> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::Path {
|
||||||
|
room: self.room.clone(),
|
||||||
|
id: *id,
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.tx.send(request.into()).await;
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tree(&self, root: &Snowflake) -> Tree<EuphMsg> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::Tree {
|
||||||
|
room: self.room.clone(),
|
||||||
|
root: *root,
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.tx.send(request.into()).await;
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prev_tree(&self, root: &Snowflake) -> Option<Snowflake> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::PrevTree {
|
||||||
|
room: self.room.clone(),
|
||||||
|
root: *root,
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.tx.send(request.into()).await;
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn next_tree(&self, root: &Snowflake) -> Option<Snowflake> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::NextTree {
|
||||||
|
room: self.room.clone(),
|
||||||
|
root: *root,
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.tx.send(request.into()).await;
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn first_tree(&self) -> Option<Snowflake> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::FirstTree {
|
||||||
|
room: self.room.clone(),
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.tx.send(request.into()).await;
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn last_tree(&self) -> Option<Snowflake> {
|
||||||
|
// TODO vault::Error
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let request = EuphRequest::LastTree {
|
||||||
|
room: self.room.clone(),
|
||||||
|
result: tx,
|
||||||
|
};
|
||||||
|
let _ = self.tx.send(request.into()).await;
|
||||||
|
rx.await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) enum EuphRequest {
|
||||||
|
Path {
|
||||||
|
room: String,
|
||||||
|
id: Snowflake,
|
||||||
|
result: oneshot::Sender<Path<Snowflake>>,
|
||||||
|
},
|
||||||
|
Tree {
|
||||||
|
room: String,
|
||||||
|
root: Snowflake,
|
||||||
|
result: oneshot::Sender<Tree<EuphMsg>>,
|
||||||
|
},
|
||||||
|
PrevTree {
|
||||||
|
room: String,
|
||||||
|
root: Snowflake,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
},
|
||||||
|
NextTree {
|
||||||
|
room: String,
|
||||||
|
root: Snowflake,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
},
|
||||||
|
FirstTree {
|
||||||
|
room: String,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
},
|
||||||
|
LastTree {
|
||||||
|
room: String,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EuphRequest {
|
||||||
|
pub(super) fn perform(self, conn: &Connection) {
|
||||||
|
let _ = match self {
|
||||||
|
EuphRequest::Path { room, id, result } => Self::path(conn, room, id, result),
|
||||||
|
EuphRequest::Tree { room, root, result } => Self::tree(conn, room, root, result),
|
||||||
|
EuphRequest::PrevTree { room, root, result } => {
|
||||||
|
Self::prev_tree(conn, room, root, result)
|
||||||
|
}
|
||||||
|
EuphRequest::NextTree { room, root, result } => {
|
||||||
|
Self::next_tree(conn, room, root, result)
|
||||||
|
}
|
||||||
|
EuphRequest::FirstTree { room, result } => Self::first_tree(conn, room, result),
|
||||||
|
EuphRequest::LastTree { room, result } => Self::last_tree(conn, room, result),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
id: Snowflake,
|
||||||
|
result: oneshot::Sender<Path<Snowflake>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let path = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
WITH RECURSIVE path (room, id) = (
|
||||||
|
VALUES (?, ?)
|
||||||
|
UNION
|
||||||
|
SELECT (room, parent)
|
||||||
|
FROM euph_msgs
|
||||||
|
JOIN path USING (room, id)
|
||||||
|
)
|
||||||
|
SELECT id
|
||||||
|
FROM path
|
||||||
|
ORDER BY id ASC
|
||||||
|
",
|
||||||
|
)?
|
||||||
|
.query_map(params![room, id.0], |row| row.get(0).map(Snowflake))?
|
||||||
|
.collect::<rusqlite::Result<_>>()?;
|
||||||
|
let path = Path::new(path);
|
||||||
|
let _ = result.send(path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tree(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
root: Snowflake,
|
||||||
|
result: oneshot::Sender<Tree<EuphMsg>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let msgs = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
WITH RECURSIVE tree (room, id) = (
|
||||||
|
VALUES (?, ?)
|
||||||
|
UNION
|
||||||
|
SELECT (euph_msgs.room, euph_msgs.id)
|
||||||
|
FROM euph_msgs
|
||||||
|
JOIN tree
|
||||||
|
ON tree.room = euph_msgs.room
|
||||||
|
AND tree.id = euph_msgs.parent
|
||||||
|
)
|
||||||
|
SELECT (id, parent, time, name, content)
|
||||||
|
FROM euph_msg
|
||||||
|
JOIN tree USING (room, id)
|
||||||
|
ORDER BY id ASC
|
||||||
|
",
|
||||||
|
)?
|
||||||
|
.query_map(params![room, root.0], |row| {
|
||||||
|
Ok(EuphMsg {
|
||||||
|
id: Snowflake(row.get(0)?),
|
||||||
|
parent: row.get::<_, Option<u64>>(1)?.map(Snowflake),
|
||||||
|
time: row.get(2)?,
|
||||||
|
nick: row.get(3)?,
|
||||||
|
content: row.get(4)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<rusqlite::Result<_>>()?;
|
||||||
|
let tree = Tree::new(root, msgs);
|
||||||
|
let _ = result.send(tree);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_tree(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
root: Snowflake,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let tree = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT id
|
||||||
|
FROM euph_trees
|
||||||
|
WHERE room = ?
|
||||||
|
AND id < ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
",
|
||||||
|
)?
|
||||||
|
.query_row(params![room, root.0], |row| row.get(0).map(Snowflake))
|
||||||
|
.optional()?;
|
||||||
|
let _ = result.send(tree);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_tree(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
root: Snowflake,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let tree = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT id
|
||||||
|
FROM euph_trees
|
||||||
|
WHERE room = ?
|
||||||
|
AND id > ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
",
|
||||||
|
)?
|
||||||
|
.query_row(params![room, root.0], |row| row.get(0).map(Snowflake))
|
||||||
|
.optional()?;
|
||||||
|
let _ = result.send(tree);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_tree(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let tree = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT id
|
||||||
|
FROM euph_trees
|
||||||
|
WHERE room = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
",
|
||||||
|
)?
|
||||||
|
.query_row([room], |row| row.get(0).map(Snowflake))
|
||||||
|
.optional()?;
|
||||||
|
let _ = result.send(tree);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_tree(
|
||||||
|
conn: &Connection,
|
||||||
|
room: String,
|
||||||
|
result: oneshot::Sender<Option<Snowflake>>,
|
||||||
|
) -> rusqlite::Result<()> {
|
||||||
|
let tree = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
SELECT id
|
||||||
|
FROM euph_trees
|
||||||
|
WHERE room = ?
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
",
|
||||||
|
)?
|
||||||
|
.query_row([room], |row| row.get(0).map(Snowflake))
|
||||||
|
.optional()?;
|
||||||
|
let _ = result.send(tree);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,20 @@ fn m1(tx: &mut Transaction) -> rusqlite::Result<()> {
|
||||||
FOREIGN KEY (room, start) REFERENCES euph_msgs (room, start),
|
FOREIGN KEY (room, start) REFERENCES euph_msgs (room, start),
|
||||||
FOREIGN KEY (room, end) REFERENCES euph_msgs (room, end)
|
FOREIGN KEY (room, end) REFERENCES euph_msgs (room, end)
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE VIEW euph_trees (room, id) AS
|
||||||
|
SELECT room, id
|
||||||
|
FROM euph_msgs
|
||||||
|
WHERE parent IS NULL
|
||||||
|
UNION
|
||||||
|
(
|
||||||
|
SELECT room, parent
|
||||||
|
FROM euph_msgs
|
||||||
|
WHERE parent IS NOT NULL
|
||||||
|
EXCEPT
|
||||||
|
SELECT room, id
|
||||||
|
FROM euph_msgs
|
||||||
|
)
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue