wayver's git archive


a last value key/value store
git clone https://git.wayver.dev/afterimage

client/src/lib.rs@main

raw
Date Commit Message Author Files + -
2026-05-02 14:31 initial mvp wayverd 9 1052 0
...

1use std::path::PathBuf;
2
3use http_body_util::{BodyExt as _, Full};
4use hyper::{Method, Request, StatusCode, Uri, body::Bytes, client::conn};
5use hyper_util::rt::TokioIo;
6use tokio::net::UnixStream;
7
8#[derive(Debug, thiserror::Error)]
9pub enum GetError {
10    #[error("failed to connect to unix socket")]
11    SocketConnect(#[source] std::io::Error),
12    #[error("failed to complete http handshake")]
13    HttpHandshake(#[source] hyper::Error),
14    #[error("given key-value key was invalid")]
15    InvalidKey(#[source] serde_urlencoded::ser::Error),
16    #[error("failed to create request url")]
17    InvalidUrl(#[source] hyper::http::Error),
18    #[error("failed to build http request")]
19    BuildRequest(#[source] hyper::http::Error),
20    #[error("failed to send http request")]
21    SendRequest(#[source] hyper::Error),
22    #[error("http request returned with a failed response: {0}")]
23    FailedResponse(StatusCode),
24    #[error("failed to collect response body")]
25    CollectBody(#[source] hyper::Error),
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum SetError {
30    #[error("failed to connect to unix socket")]
31    SocketConnect(#[source] std::io::Error),
32    #[error("failed to complete http handshake")]
33    HttpHandshake(#[source] hyper::Error),
34    #[error("given key-value key was invalid")]
35    InvalidKey(#[source] serde_urlencoded::ser::Error),
36    #[error("failed to create request url")]
37    InvalidUrl(#[source] hyper::http::Error),
38    #[error("failed to build http request")]
39    BuildRequest(#[source] hyper::http::Error),
40    #[error("failed to send http request")]
41    SendRequest(#[source] hyper::Error),
42    #[error("http request returned with a failed response: {0}")]
43    FailedResponse(StatusCode),
44}
45
46pub struct Client {
47    path: PathBuf,
48}
49
50impl Client {
51    pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, GetError> {
52        let stream = UnixStream::connect(&self.path)
53            .await
54            .map_err(GetError::SocketConnect)?;
55        let io = TokioIo::new(stream);
56
57        let (mut sender, conn) = conn::http1::handshake(io)
58            .await
59            .map_err(GetError::HttpHandshake)?;
60
61        tokio::task::spawn(async move {
62            if let Err(err) = conn.await {
63                tracing::error!("Connection failed: {:?}", err);
64            }
65        });
66
67        let query = write_query(key).map_err(GetError::InvalidKey)?;
68
69        let uri = Uri::builder()
70            .scheme("http")
71            .authority("localhost.invalid-tld")
72            .path_and_query(query)
73            .build()
74            .map_err(GetError::InvalidUrl)?;
75
76        let req = Request::builder()
77            .method(Method::GET)
78            .uri(uri)
79            .body(Full::new(Bytes::from_static(&[])))
80            .map_err(GetError::BuildRequest)?;
81
82        let res = sender
83            .send_request(req)
84            .await
85            .map_err(GetError::SendRequest)?;
86
87        if matches!(res.status(), StatusCode::BAD_REQUEST) {
88            return Err(GetError::FailedResponse(res.status()));
89        }
90        if matches!(res.status(), StatusCode::NOT_FOUND) {
91            return Ok(None);
92        }
93
94        let body = res
95            .into_body()
96            .collect()
97            .await
98            .map_err(GetError::CollectBody)?
99            .to_bytes();
100
101        Ok(Some(body.to_vec()))
102    }
103
104    pub async fn set(&self, key: &str, value: &[u8]) -> Result<(), SetError> {
105        let stream = UnixStream::connect(&self.path)
106            .await
107            .map_err(SetError::SocketConnect)?;
108        let io = TokioIo::new(stream);
109
110        let (mut sender, conn) = conn::http1::handshake(io)
111            .await
112            .map_err(SetError::HttpHandshake)?;
113
114        tokio::task::spawn(async move {
115            if let Err(err) = conn.await {
116                tracing::error!("Connection failed: {:?}", err);
117            }
118        });
119
120        let query = write_query(key).map_err(SetError::InvalidKey)?;
121
122        let uri = Uri::builder()
123            .scheme("http")
124            .authority("localhost.invalid-tld")
125            .path_and_query(query)
126            .build()
127            .map_err(SetError::BuildRequest)?;
128
129        let body = Full::new(Bytes::from(value.to_vec()));
130
131        let req = Request::builder()
132            .method(Method::POST)
133            .uri(uri)
134            .body(body)
135            .map_err(SetError::BuildRequest)?;
136
137        let res = sender
138            .send_request(req)
139            .await
140            .map_err(SetError::SendRequest)?;
141
142        if matches!(res.status(), StatusCode::BAD_REQUEST) {
143            return Err(SetError::FailedResponse(res.status()));
144        }
145
146        Ok(())
147    }
148}
149
150impl Default for Client {
151    fn default() -> Self {
152        Self {
153            path: PathBuf::from("/tmp/last-sky.sock"),
154        }
155    }
156}
157
158#[inline]
159fn write_query(key: &str) -> Result<String, serde_urlencoded::ser::Error> {
160    #[derive(serde::Serialize)]
161    struct Key<'k> {
162        key: &'k str,
163    }
164
165    let mut output = "/?".to_string();
166
167    let mut encoder = form_urlencoded::Serializer::new(&mut output);
168    let serializer = serde_urlencoded::Serializer::new(&mut encoder);
169
170    <Key<'_> as serde::Serialize>::serialize(&Key { key }, serializer)?;
171
172    Ok(output)
173}
174