mod alias;
mod aliases;
mod analytics;
mod collection;
mod collections;
mod conversations;
mod curation_set;
mod curation_sets;
mod key;
mod keys;
mod multi_search;
mod operations;
mod preset;
mod presets;
mod retry_policy;
mod stemming;
mod stopword;
mod stopwords;
mod synonym_set;
mod synonym_sets;
use crate::{Error, traits::Document};
use alias::Alias;
use aliases::Aliases;
use analytics::Analytics;
use collection::Collection;
use collections::Collections;
use conversations::Conversations;
use curation_set::CurationSet;
use curation_sets::CurationSets;
use key::Key;
use keys::Keys;
use operations::Operations;
use preset::Preset;
use presets::Presets;
use retry_policy::ClientRetryPolicy;
use stemming::Stemming;
use stopword::Stopword;
use stopwords::Stopwords;
use synonym_set::SynonymSet;
use synonym_sets::SynonymSets;
#[cfg(not(target_arch = "wasm32"))]
use reqwest_middleware::ClientBuilder as ReqwestMiddlewareClientBuilder;
#[cfg(not(target_arch = "wasm32"))]
use reqwest_retry::RetryTransientMiddleware;
pub use reqwest_retry::policies::ExponentialBackoff;
use ::std::{
borrow::Cow,
future::Future,
sync::{
RwLock,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
};
use serde::{Serialize, de::DeserializeOwned};
use typesense_codegen::apis::{self, configuration};
use web_time::{Duration, Instant};
#[macro_export]
macro_rules! execute_wrapper {
($self:ident, $call:expr) => {
$self.client.execute($call).await
};
($self:ident, $call:expr, $params:ident) => {
$self
.client
.execute(
|config: &typesense_codegen::apis::configuration::Configuration| {
$call(config, &$params)
},
)
.await
};
}
pub struct NodeConfig {
url: String,
http_builder: Option<Box<dyn FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder>>,
}
impl std::fmt::Debug for NodeConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeConfig")
.field("url", &self.url)
.field("http_builder", &self.http_builder.as_ref().map(|_| ".."))
.finish()
}
}
impl NodeConfig {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
http_builder: None,
}
}
pub fn http_builder(
mut self,
f: impl FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder + 'static,
) -> Self {
self.http_builder = Some(Box::new(f));
self
}
}
impl From<String> for NodeConfig {
fn from(url: String) -> Self {
Self::new(url)
}
}
impl<'a> From<&'a str> for NodeConfig {
fn from(url: &'a str) -> Self {
Self::new(url)
}
}
impl From<reqwest::Url> for NodeConfig {
fn from(url: reqwest::Url) -> Self {
Self::new(url)
}
}
#[derive(Debug)]
struct Node {
config: configuration::Configuration,
is_healthy: AtomicBool,
last_accessed: RwLock<Instant>,
}
impl Node {
#[inline]
fn set_health(&self, is_healthy: bool) {
*self.last_accessed.write().unwrap() = Instant::now();
self.is_healthy.store(is_healthy, Ordering::Relaxed);
}
}
#[derive(Debug)]
pub struct Client {
nodes: Vec<Node>,
is_nearest_node_set: bool,
healthcheck_interval: Duration,
current_node_index: AtomicUsize,
}
#[bon::bon]
impl Client {
#[builder]
pub fn new(
#[builder(into)]
api_key: String,
#[builder(
with = |iter: impl IntoIterator<Item = impl Into<NodeConfig>>|
iter.into_iter().map(Into::into).collect::<Vec<NodeConfig>>()
)]
nodes: Vec<NodeConfig>,
#[builder(into)]
nearest_node: Option<NodeConfig>,
#[builder(default = Duration::from_secs(60))]
healthcheck_interval: Duration,
#[builder(into, default)]
retry_policy: ClientRetryPolicy,
) -> Result<Self, &'static str> {
let is_nearest_node_set = nearest_node.is_some();
let nodes: Vec<_> = nodes
.into_iter()
.chain(nearest_node)
.map(|node_config| {
let builder = match node_config.http_builder {
Some(f) => f(reqwest::Client::builder()),
None => {
let b = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let b = b.connect_timeout(Duration::from_secs(5));
b
}
};
#[cfg(target_arch = "wasm32")]
let http_client = builder.build().expect("Failed to build reqwest client");
#[cfg(not(target_arch = "wasm32"))]
let mw_builder = ReqwestMiddlewareClientBuilder::new(
builder.build().expect("Failed to build reqwest client"),
);
#[cfg(not(target_arch = "wasm32"))]
let http_client = match retry_policy {
ClientRetryPolicy::Default(policy) => mw_builder
.with(RetryTransientMiddleware::new_with_policy(policy))
.build(),
ClientRetryPolicy::Timed(policy) => mw_builder
.with(RetryTransientMiddleware::new_with_policy(policy))
.build(),
};
let mut url = node_config.url;
if url.len() > 1 && matches!(url.chars().last(), Some('/')) {
url.pop();
}
let config = configuration::Configuration {
base_path: url,
api_key: Some(configuration::ApiKey {
prefix: None,
key: api_key.clone(),
}),
client: http_client,
..Default::default()
};
Node {
config,
is_healthy: AtomicBool::new(true),
last_accessed: RwLock::new(Instant::now()),
}
})
.collect();
if nodes.is_empty() {
return Err("Configuration must include at least one node or a nearest_node.");
}
Ok(Self {
nodes,
is_nearest_node_set,
healthcheck_interval,
current_node_index: AtomicUsize::new(0),
})
}
fn get_next_node(&self) -> &Node {
if self.nodes.len() == 1
&& let Some(first) = self.nodes.first()
{
return first;
}
let (nodes_len, mut index) = if self.is_nearest_node_set {
let last_node_index = self.nodes.len() - 1;
(last_node_index, last_node_index)
} else {
(
self.nodes.len(),
self.current_node_index.fetch_add(1, Ordering::Relaxed) % self.nodes.len(),
)
};
for _ in 0..self.nodes.len() {
let node = &self.nodes[index];
if node.is_healthy.load(Ordering::Relaxed)
|| node.last_accessed.read().unwrap().elapsed() >= self.healthcheck_interval
{
return node;
}
index = self.current_node_index.fetch_add(1, Ordering::Relaxed) % nodes_len;
}
index = self.current_node_index.load(Ordering::Relaxed) % self.nodes.len();
&self.nodes[index]
}
#[inline]
pub fn get_legacy_config(&self) -> &configuration::Configuration {
&self.get_next_node().config
}
pub(super) async fn execute<F, Fut, T, E, 'a>(&'a self, api_call: F) -> Result<T, Error<E>>
where
F: Fn(&'a configuration::Configuration) -> Fut,
Fut: Future<Output = Result<T, apis::Error<E>>>,
E: std::fmt::Debug + 'static,
apis::Error<E>: std::error::Error + 'static,
{
let mut last_api_error: Option<apis::Error<E>> = None;
for _ in 0..self.nodes.len() {
let node = self.get_next_node();
match api_call(&node.config).await {
Ok(response) => {
node.set_health(true);
return Ok(response);
}
Err(e) => {
if is_retriable(&e) {
node.set_health(false);
last_api_error = Some(e);
} else {
return Err(e.into());
}
}
}
}
Err(crate::Error::AllNodesFailed {
source: last_api_error
.expect("No nodes were available to try, or all errors were non-retriable."),
})
}
#[inline]
pub fn aliases(&self) -> Aliases<'_> {
Aliases::new(self)
}
#[inline]
pub fn alias<'a>(&'a self, alias_name: &'a str) -> Alias<'a> {
Alias::new(self, alias_name)
}
#[inline]
pub fn analytics(&self) -> Analytics<'_> {
Analytics::new(self)
}
#[inline]
pub fn collections(&self) -> Collections<'_> {
Collections::new(self)
}
#[inline]
pub fn collection_named<'c, D>(
&'c self,
collection_name: impl Into<Cow<'c, str>>,
) -> Collection<'c, D>
where
D: DeserializeOwned + Serialize,
{
Collection::new(self, collection_name)
}
#[inline]
pub fn collection<'c, D>(&'c self) -> Collection<'c, D>
where
D: Document,
{
Collection::new(self, D::COLLECTION_NAME)
}
#[inline]
pub fn collection_schemaless<'c>(
&'c self,
collection_name: impl Into<Cow<'c, str>>,
) -> Collection<'c, serde_json::Value> {
Collection::new(self, collection_name)
}
#[inline]
pub fn conversations(&self) -> Conversations<'_> {
Conversations::new(self)
}
#[inline]
pub fn curation_sets(&self) -> CurationSets<'_> {
CurationSets::new(self)
}
#[inline]
pub fn curation_set<'a>(&'a self, curation_set_name: &'a str) -> CurationSet<'a> {
CurationSet::new(self, curation_set_name)
}
#[inline]
pub fn keys(&self) -> Keys<'_> {
Keys::new(self)
}
#[inline]
pub fn key(&self, key_id: i64) -> Key<'_> {
Key::new(self, key_id)
}
#[inline]
pub fn multi_search(&self) -> multi_search::MultiSearch<'_> {
multi_search::MultiSearch::new(self)
}
#[inline]
pub fn operations(&self) -> Operations<'_> {
Operations::new(self)
}
#[inline]
pub fn presets(&self) -> Presets<'_> {
Presets::new(self)
}
#[inline]
pub fn preset<'a>(&'a self, preset_id: &'a str) -> Preset<'a> {
Preset::new(self, preset_id)
}
#[inline]
pub fn stemming(&self) -> Stemming<'_> {
Stemming::new(self)
}
#[inline]
pub fn stopwords(&self) -> Stopwords<'_> {
Stopwords::new(self)
}
#[inline]
pub fn stopword<'a>(&'a self, set_id: &'a str) -> Stopword<'a> {
Stopword::new(self, set_id)
}
#[inline]
pub fn synonym_sets(&self) -> SynonymSets<'_> {
SynonymSets::new(self)
}
#[inline]
pub fn synonym_set<'a>(&'a self, synonym_set_name: &'a str) -> SynonymSet<'a> {
SynonymSet::new(self, synonym_set_name)
}
}
fn is_retriable<E>(error: &apis::Error<E>) -> bool
where
E: std::fmt::Debug + 'static,
apis::Error<E>: std::error::Error + 'static,
{
match error {
apis::Error::ResponseError(content) => content.status.is_server_error(),
apis::Error::Reqwest(_) => true,
#[cfg(not(target_arch = "wasm32"))]
apis::Error::ReqwestMiddleware(_) => true,
_ => false,
}
}