use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Json, Router};
use dashmap::DashSet;
use serde::{Deserialize, Serialize};
use crate::protocols::SharedCacheHits;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SharedCacheQueryRequest {
pub block_hashes: Vec<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SharedCacheQueryResponse {
pub ranges: Vec<[u32; 2]>,
}
#[derive(Deserialize)]
pub struct StoreRequest {
pub block_hashes: Vec<u64>,
}
#[derive(Deserialize)]
pub struct RemoveRequest {
pub block_hashes: Vec<u64>,
}
pub struct SharedCacheStore {
blocks: DashSet<u64>,
}
impl Default for SharedCacheStore {
fn default() -> Self {
Self::new()
}
}
impl SharedCacheStore {
pub fn new() -> Self {
Self {
blocks: DashSet::new(),
}
}
pub fn store(&self, hashes: &[u64]) {
for &h in hashes {
self.blocks.insert(h);
}
}
pub fn remove(&self, hashes: &[u64]) {
for &h in hashes {
self.blocks.remove(&h);
}
}
pub fn check_blocks(&self, block_hashes: &[u64]) -> SharedCacheHits {
let hits: Vec<bool> = block_hashes
.iter()
.map(|h| self.blocks.contains(h))
.collect();
SharedCacheHits::from_hits(&hits)
}
pub fn len(&self) -> usize {
self.blocks.len()
}
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
}
pub struct AppState {
pub store: Arc<SharedCacheStore>,
}
async fn check_blocks(
State(state): State<Arc<AppState>>,
Json(req): Json<SharedCacheQueryRequest>,
) -> impl IntoResponse {
let hits = state.store.check_blocks(&req.block_hashes);
let ranges: Vec<[u32; 2]> = hits.ranges.iter().map(|r| [r.start, r.end]).collect();
(StatusCode::OK, Json(SharedCacheQueryResponse { ranges }))
}
async fn store_blocks(
State(state): State<Arc<AppState>>,
Json(req): Json<StoreRequest>,
) -> impl IntoResponse {
let count = req.block_hashes.len();
state.store.store(&req.block_hashes);
(
StatusCode::CREATED,
Json(serde_json::json!({
"status": "ok",
"stored": count,
"total": state.store.len(),
})),
)
}
async fn remove_blocks(
State(state): State<Arc<AppState>>,
Json(req): Json<RemoveRequest>,
) -> impl IntoResponse {
let count = req.block_hashes.len();
state.store.remove(&req.block_hashes);
(
StatusCode::OK,
Json(serde_json::json!({
"status": "ok",
"removed": count,
"total": state.store.len(),
})),
)
}
async fn health() -> StatusCode {
StatusCode::OK
}
async fn stats(State(state): State<Arc<AppState>>) -> impl IntoResponse {
Json(serde_json::json!({
"total_blocks": state.store.len(),
}))
}
pub fn create_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/check_blocks", post(check_blocks))
.route("/store", post(store_blocks))
.route("/remove", post(remove_blocks))
.route("/health", get(health))
.route("/stats", get(stats))
.with_state(state)
}
#[cfg(test)]
mod tests {
use axum::body::{Body, to_bytes};
use axum::http::{Request, StatusCode, header};
use tower::ServiceExt;
use super::*;
#[test]
fn test_remove_blocks() {
let store = SharedCacheStore::new();
store.store(&[10, 20, 30]);
store.remove(&[20]);
let hits = store.check_blocks(&[10, 20, 30]);
assert_eq!(hits.total_hits, 2);
assert_eq!(hits.ranges, vec![0..1, 2..3]);
}
#[tokio::test]
async fn check_blocks_returns_wire_format() {
let store = Arc::new(SharedCacheStore::new());
store.store(&[10, 30]);
let app = create_router(Arc::new(AppState { store }));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/check_blocks")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"block_hashes":[10,20,30]}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&body).unwrap(),
serde_json::json!({"ranges": [[0, 1], [2, 3]]})
);
}
}