use crate::client::InnerClient;
use crate::{Client, Error, Method, Params, Result};
use indexmap::IndexMap;
use mwapi_responses::normalize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use tokio::sync::oneshot;
use tokio::sync::Mutex;
use tokio::time;
use tracing::debug;
type Request = BTreeMap<String, String>;
type Sender = oneshot::Sender<Result<Value>>;
pub(crate) fn is_combinable(params: &Params) -> bool {
if params.get("action") != Some(&"query".to_string()) {
return false;
}
if let Some(titles) = params.get("titles") {
if titles.contains('|') {
return false;
}
} else {
return false;
}
if params.contains_key("list")
|| params.contains_key("meta")
|| params.contains_key("generator")
|| params.contains_key("redirects")
{
return false;
}
true
}
#[derive(Default, Debug)]
pub(crate) struct RequestQueue {
queue: Mutex<IndexMap<Request, HashMap<String, Sender>>>,
}
impl RequestQueue {
pub(crate) fn init(&self, client: &Client) {
let client = client.inner.clone();
tokio::spawn(async move { background_thread(client).await });
}
pub(crate) async fn push(&self, params: Params, sender: Sender) {
let mut params = params.map;
let title = params.remove("titles").unwrap();
let req: Request = params.into_iter().collect();
self.queue
.lock()
.await
.entry(req)
.or_default()
.insert(title, sender);
}
pub(crate) async fn pop(
&self,
) -> Option<(Request, HashMap<String, Sender>)> {
self.queue.lock().await.pop()
}
}
#[derive(Deserialize)]
struct Response {
query: QueryResponse,
}
#[derive(Deserialize)]
struct QueryResponse {
#[serde(default)]
normalized: Vec<normalize::Normalized>,
#[serde(default)]
redirects: Vec<normalize::Redirect>,
#[serde(default)]
pages: Vec<PageResponse>,
}
#[derive(Deserialize, Serialize)]
struct PageResponse {
title: String,
#[serde(flatten)]
extra: HashMap<String, Value>,
}
fn flipped_title_map(resp: &QueryResponse) -> HashMap<String, String> {
let redirects: HashMap<String, String> = resp
.redirects
.iter()
.map(|redir| (redir.to.to_string(), redir.from.to_string()))
.collect();
let mut normalized: HashMap<String, String> = resp
.normalized
.iter()
.map(|norm| (norm.to.to_string(), norm.from.to_string()))
.collect();
let mut titles = HashMap::new();
for (to, from) in redirects {
let real_from = normalized.remove(&from).unwrap_or(from);
titles.insert(to, real_from);
}
titles.extend(normalized);
titles
}
pub(crate) async fn background_thread(client: Arc<InnerClient>) {
let weak_client = Arc::downgrade(&client);
drop(client);
let mut interval = time::interval(time::Duration::from_micros(50_000));
loop {
let client = match weak_client.upgrade() {
Some(client) => client,
None => {
return;
}
};
let next = client.get_queue.pop().await;
let (params, mut senders) = match next {
Some(val) => val,
None => {
interval.tick().await;
continue;
}
};
let titles: Vec<_> = senders.keys().map(|t| t.to_string()).collect();
debug!("queue: {:?}", &titles);
for chunk in titles.chunks(25) {
let mut params2 = params.clone();
params2.insert("titles".to_string(), chunk.join("|"));
let result: Result<Response> =
client.request(Method::Get, params2).await;
match result {
Ok(response) => {
let title_map = flipped_title_map(&response.query);
for page in response.query.pages {
let title = page.title.to_string();
let unnormalized = match title_map.get(&title) {
Some(title) => title.to_string(),
None => title.to_string(),
};
if let Some(sender) = senders.remove(&unnormalized) {
let split = serde_json::json!({
"batchcomplete": true,
"query": {
"pages": [page]
}
});
if sender.send(Ok(split)).is_err() {
debug!("{} dropped receiver", &title);
}
}
}
}
Err(err) => {
for title in chunk {
if let Some(sender) = senders.remove(title) {
if sender.send(Err(err.clone())).is_err() {
debug!("{} dropped receiver", &title);
}
}
}
}
}
}
drop(client);
for (title, sender) in senders {
if sender
.send(Err(Error::Unknown(
"Couldnt find page response".to_string(),
)))
.is_err()
{
debug!(
"{} couldnt be found in response, and sender failed",
&title
);
}
}
}
}