Skip to content

Commit b79060f

Browse files
committed
Introduce discovery
1 parent fb3e3f1 commit b79060f

27 files changed

+3885
-12
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[workspace]
2-
members = ["packages/*"]
2+
members = ["packages/app", "packages/shared"]

packages/app/src/discovery/engine.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashMap;
22

33
use axum::async_trait;
4-
use enstate_shared::core::Profile;
4+
use enstate_shared::core::lookup_data::LookupInfo;
5+
use enstate_shared::core::{ENSService, Profile};
56
use enstate_shared::discovery::Discovery;
67
use ethers::providers::namehash;
8+
use futures::future::join_all;
79
use serde::{Deserialize, Serialize};
810

911
pub struct DiscoveryEngine {
@@ -92,7 +94,43 @@ impl Discovery for DiscoveryEngine {
9294
}
9395
}
9496

95-
async fn query_search(&self, query: String) -> Result<Vec<Profile>, ()> {
96-
todo!()
97+
async fn query_search(&self, service: &ENSService, query: String) -> Result<Vec<Profile>, ()> {
98+
let index = self.client.index("profiles");
99+
100+
// Create search with query and limit to 5 results
101+
let search = index.search()
102+
.with_query(&query)
103+
.with_limit(5)
104+
.build();
105+
106+
// Execute the search
107+
match search.execute::<MeiliProfileDocument>().await {
108+
Ok(search_results) => {
109+
tracing::info!("Search results: found {} matches", search_results.hits.len());
110+
111+
// Return empty vector if no results
112+
if search_results.hits.is_empty() {
113+
return Ok(vec![]);
114+
}
115+
116+
// Extract the name for each result to use with resolve_name
117+
let names: Vec<String> = search_results.hits
118+
.into_iter()
119+
.map(|hit| hit.result.name)
120+
.collect();
121+
122+
let profiles = names.into_iter().map(|name| async move {
123+
service.resolve_profile(LookupInfo::Name(name), false).await.unwrap()
124+
}).collect::<Vec<_>>();
125+
126+
let profiles = join_all(profiles).await;
127+
128+
Ok(profiles)
129+
},
130+
Err(e) => {
131+
tracing::error!("Error searching profiles: {}", e);
132+
Err(())
133+
}
134+
}
97135
}
98136
}

packages/app/src/http.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tower_http::trace::TraceLayer;
1515
use tracing::{info, info_span};
1616

1717
use crate::routes;
18+
use crate::routes::v2::setup_v2_router;
1819
use crate::state::AppState;
1920
use crate::telemetry::metrics::{self};
2021

@@ -190,6 +191,8 @@ pub fn setup(mut state: AppState) -> App {
190191

191192
let state = Arc::new(state);
192193

194+
let v2 = setup_v2_router(state.clone());
195+
193196
let router = Router::new()
194197
.route("/", get(|| async { Redirect::temporary("/docs") }))
195198
.nest("/docs", docs)
@@ -214,6 +217,7 @@ pub fn setup(mut state: AppState) -> App {
214217
"/sse/u",
215218
get(routes::universal::get_bulk_sse).post(routes::universal::post_bulk_sse),
216219
)
220+
.nest("/v2", v2)
217221
.route("/metrics", get(metrics::handle))
218222
.fallback(routes::four_oh_four::handler)
219223
.layer(middleware::from_fn_with_state(

packages/app/src/routes/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ pub mod image;
1818
pub mod name;
1919
pub mod root;
2020
pub mod universal;
21-
22-
// TODO (@antony1060): cleanup file
21+
pub mod v2;
2322

2423
#[derive(Deserialize)]
2524
pub struct FreshQuery {

packages/app/src/routes/universal.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,22 @@ pub async fn get(
5555
fresh: query,
5656
queries: vec![name_or_address],
5757
}),
58-
State(state),
58+
State(state.clone()),
5959
)
6060
.await
6161
.map(|mut res| {
62+
63+
// TODO: +1 on cache hit popularity discover
64+
for profile in &res.response {
65+
if let BulkResponse::Ok(profile) = profile {
66+
let profile = profile.clone();
67+
let _ = state.service.cache.cache_hit(&profile.name);
68+
if let Some(discovery) = &state.service.discovery {
69+
let _ = discovery.discover_name(&profile);
70+
}
71+
}
72+
}
73+
6274
Result::<_, _>::from(res.0.response.remove(0))
6375
.map(Json)
6476
.map_err(RouteError::from)
@@ -146,8 +158,7 @@ pub async fn get_bulk_sse(
146158
Qs(query): Qs<UniversalGetBulkQuery>,
147159
State(state): State<Arc<crate::AppState>>,
148160
) -> impl IntoResponse {
149-
let queries =
150-
validate_bulk_input(&query.queries, state.service.max_bulk_size).unwrap();
161+
let queries = validate_bulk_input(&query.queries, state.service.max_bulk_size).unwrap();
151162

152163
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel::<Result<Event, Infallible>>();
153164

@@ -179,7 +190,7 @@ pub async fn get_bulk_sse(
179190
}
180191

181192
/// /sse/u
182-
///
193+
///
183194
/// Same as the GET version, but using POST with a JSON body instead of query parameters allowing for larger requests.
184195
#[utoipa::path(
185196
post,

packages/app/src/routes/v2/mod.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use std::convert::Infallible;
2+
use std::sync::Arc;
3+
use std::time::Duration;
4+
5+
use axum::response::sse::Event;
6+
use axum::response::{IntoResponse, Sse};
7+
use axum::{routing::get, Router};
8+
use axum::{
9+
extract::{Path, Query, State},
10+
http::StatusCode,
11+
Json,
12+
};
13+
use enstate_shared::core::lookup_data::LookupInfo;
14+
use enstate_shared::core::Profile;
15+
use ethers_core::types::Address;
16+
use futures::future::join_all;
17+
use serde::Deserialize;
18+
use tokio_stream::wrappers::UnboundedReceiverStream;
19+
use tracing::info;
20+
21+
use crate::models::bulk::{BulkResponse, ListResponse};
22+
use crate::models::sse::SSEResponse;
23+
use crate::routes::{
24+
http_simple_status_error, profile_http_error_mapper, validate_bulk_input, FreshQuery, Qs,
25+
RouteError,
26+
};
27+
28+
29+
pub fn setup_v2_router(state: Arc<crate::AppState>) -> Router<Arc<crate::AppState>> {
30+
Router::new()
31+
.route("/discover/search", get(discovery_search)).with_state(state)
32+
}
33+
34+
#[derive(Deserialize)]
35+
pub struct SearchQuery {
36+
s: String,
37+
}
38+
39+
/// /a/{address}
40+
///
41+
/// Here is an example of a valid request that looks up an address:
42+
/// ```url
43+
/// /a/0x225f137127d9067788314bc7fcc1f36746a3c3B5
44+
/// ```
45+
#[utoipa::path(
46+
get,
47+
tag = "Single Profile",
48+
path = "/v2/discover/search",
49+
responses(
50+
(status = 200, description = "Successfully found address.", body = ENSProfile),
51+
(status = BAD_REQUEST, description = "Invalid address.", body = ErrorResponse),
52+
(status = NOT_FOUND, description = "No name was associated with this address.", body = ErrorResponse),
53+
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
54+
),
55+
params(
56+
("address" = String, Path, description = "Address to lookup name data for"),
57+
)
58+
)]
59+
pub async fn discovery_search(
60+
Query(query): Query<SearchQuery>,
61+
State(state): State<Arc<crate::AppState>>,
62+
) -> Result<Json<Vec<Profile>>, RouteError> {
63+
64+
info!("query: {:?}", query.s);
65+
66+
if let Some(discovery) = &state.service.discovery {
67+
let profiles = discovery.query_search(&state.service, query.s).await.unwrap();
68+
return Ok(Json(profiles));
69+
}
70+
// get_bulk(
71+
// Qs(AddressGetBulkQuery {
72+
// fresh: query,
73+
// addresses: vec![address],
74+
// }),
75+
// State(state),
76+
// )
77+
// .await
78+
// .map(|mut res| {
79+
// Result::from(res.0.response.remove(0))
80+
// .map(Json)
81+
// .map_err(RouteError::from)
82+
// })?
83+
todo!()
84+
}

packages/search/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_API_URL=http://localhost:3000

packages/search/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
.env

packages/search/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="./src/index.tsx"></script>
11+
</body>
12+
</html>

packages/search/package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "search",
3+
"version": "1.0.0",
4+
"description": "Search application",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"keywords": [],
12+
"author": "",
13+
"license": "ISC",
14+
"dependencies": {
15+
"@tanstack/react-query": "^5.24.1",
16+
"@tanstack/react-router": "^1.16.5",
17+
"axios": "^1.6.7",
18+
"react": "^18.2.0",
19+
"react-dom": "^18.2.0",
20+
"react-icons": "^5.5.0",
21+
"use-debounce": "^10.0.4",
22+
"use-enstate": "0.0.1-10"
23+
},
24+
"devDependencies": {
25+
"@tanstack/router-vite-plugin": "^1.16.5",
26+
"@types/react": "^18.2.55",
27+
"@types/react-dom": "^18.2.19",
28+
"@vitejs/plugin-react": "^4.2.1",
29+
"autoprefixer": "^10.4.17",
30+
"postcss": "^8.4.35",
31+
"tailwindcss": "^3.4.1",
32+
"typescript": "^5.3.3",
33+
"vite": "^5.1.3"
34+
}
35+
}

0 commit comments

Comments
 (0)