use std::sync::Arc;
use std::sync::PoisonError;
use std::sync::RwLock;
use leptoskit_query::BoxFuture;
use leptoskit_query::MutationHandle;
use leptoskit_query::MutationScope;
use leptoskit_query::QueryHandle;
use leptoskit_query::QueryScope;
use leptoskit_query::use_mutation;
use leptoskit_query::use_query;
use serde_json::Value;
use crate::FnError;
use crate::Operation;
#[cfg_attr(
all(feature = "guest", target_arch = "wasm32"),
allow(dead_code, reason = "a native app sends through the HTTP plugin")
)]
mod browser;
mod keys;
pub mod live;
#[cfg(all(feature = "guest", target_arch = "wasm32"))]
mod native;
mod offline;
mod paginated;
mod protocol;
#[cfg(all(
feature = "server",
any(feature = "postgres", feature = "sqlite"),
not(target_arch = "wasm32")
))]
mod server;
mod stream;
#[cfg(test)]
mod tests;
pub use live::ChannelEvent;
pub use live::ChannelHandle;
pub use live::LiveClient;
pub use live::LiveState;
pub use live::LiveStatus;
pub use live::LiveStream;
pub use live::StreamEvent;
pub use live::live_mutation_scope;
pub use live::reauthenticate;
pub use live::use_channel;
pub use live::use_live;
pub use live::use_live_action;
pub use live::use_live_mutation;
pub use live::use_live_status;
pub use offline::MemoryQueue;
pub use offline::QueueStore;
pub use offline::QueuedCall;
pub use offline::QueuedKind;
pub use paginated::PaginatedHandle;
pub use paginated::use_live_paginated;
pub use protocol::Credentials;
pub use stream::StreamHandle;
pub use stream::StreamState;
pub use stream::use_stream;
#[derive(Clone, Default)]
pub struct ClientConfig {
pub origin: Option<String>,
pub credentials: Option<Arc<dyn Credentials>>,
pub offline_queue: bool,
pub offline_store: Option<Arc<dyn QueueStore>>,
}
impl ClientConfig {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.origin = Some(origin.into());
self
}
#[must_use]
pub fn credentials(mut self, credentials: impl Credentials) -> Self {
self.credentials = Some(Arc::new(credentials));
self
}
#[must_use]
pub fn offline_queue(mut self, enabled: bool) -> Self {
self.offline_queue = enabled;
self
}
#[must_use]
pub fn offline_store(mut self, store: impl QueueStore) -> Self {
self.offline_queue = true;
self.offline_store = Some(Arc::new(store));
self
}
}
impl std::fmt::Debug for ClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientConfig")
.field("origin", &self.origin)
.field("credentials", &self.credentials.is_some())
.field("offline_queue", &self.offline_queue)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CallOptions {
pub idempotency_key: Option<String>,
}
impl CallOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
self.idempotency_key = Some(key.into());
self
}
}
pub(crate) enum Key {
None,
Fresh,
Given(String),
}
impl Key {
pub(crate) async fn resolve(self) -> Option<String> {
match self {
Self::None => None,
Self::Fresh => Some(keys::new_key().await),
Self::Given(key) => Some(key),
}
}
}
pub(crate) trait Api: Send + Sync + 'static {
fn call(
&self,
name: &'static str,
arguments: Value,
key: Key,
) -> BoxFuture<'static, Result<Value, FnError>>;
}
static CLIENT: RwLock<Option<Arc<dyn Api>>> = RwLock::new(None);
pub fn configure(config: ClientConfig) {
live::install(&config);
install(backend(config));
}
pub(crate) fn install(api: Arc<dyn Api>) {
*CLIENT.write().unwrap_or_else(PoisonError::into_inner) = Some(api);
}
fn api() -> Arc<dyn Api> {
if let Some(api) = CLIENT
.read()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
{
return Arc::clone(api);
}
let mut client = CLIENT.write().unwrap_or_else(PoisonError::into_inner);
Arc::clone(client.get_or_insert_with(|| backend(ClientConfig::default())))
}
#[cfg(all(
feature = "server",
any(feature = "postgres", feature = "sqlite"),
not(target_arch = "wasm32")
))]
fn backend(_config: ClientConfig) -> Arc<dyn Api> {
Arc::new(server::InProcess)
}
#[cfg(all(feature = "guest", target_arch = "wasm32"))]
fn backend(config: ClientConfig) -> Arc<dyn Api> {
Arc::new(protocol::Remote::new(config, native::Plugin))
}
#[cfg(all(feature = "web", not(feature = "guest"), target_arch = "wasm32"))]
fn backend(config: ClientConfig) -> Arc<dyn Api> {
Arc::new(protocol::Remote::new(config, browser::Fetch))
}
#[cfg(not(any(
all(
feature = "server",
any(feature = "postgres", feature = "sqlite"),
not(target_arch = "wasm32")
),
all(any(feature = "guest", feature = "web"), target_arch = "wasm32"),
)))]
fn backend(config: ClientConfig) -> Arc<dyn Api> {
Arc::new(protocol::Remote::new(config, protocol::Unreachable))
}
#[expect(
clippy::needless_pass_by_value,
reason = "a call is written `call(Op { … })`: the arguments are built for it"
)]
pub fn call<O: Operation>(
op: O,
) -> impl Future<Output = Result<O::Output, FnError>> + Send + 'static {
let key = if O::KIND == crate::Kind::Mutation {
Key::Fresh
} else {
Key::None
};
send(&op, key)
}
#[expect(
clippy::needless_pass_by_value,
reason = "a call is written `call(Op { … })`: the arguments are built for it"
)]
pub fn call_with<O: Operation>(
op: O,
options: CallOptions,
) -> impl Future<Output = Result<O::Output, FnError>> + Send + 'static {
send(&op, options.idempotency_key.map_or(Key::None, Key::Given))
}
fn send<O: Operation>(
op: &O,
key: Key,
) -> impl Future<Output = Result<O::Output, FnError>> + Send + 'static + use<O> {
let pending = serde_json::to_value(op)
.map(|arguments| api().call(O::NAME, arguments, key))
.map_err(|error| FnError::Invalid(format!("the arguments aren't JSON: {error}")));
async move {
let value = pending?.await?;
serde_json::from_value(value)
.map_err(|error| FnError::Decode(format!("`{}` answered with {error}", O::NAME)))
}
}
pub struct Args<O> {
op: O,
json: Arc<str>,
}
impl<O: Operation> Args<O> {
pub fn new(op: O) -> Self {
let json = serde_json::to_string(&op)
.expect("an operation's arguments serialize, as they must to be sent");
Self {
op,
json: Arc::from(json),
}
}
pub fn get(&self) -> &O {
&self.op
}
pub fn into_inner(self) -> O {
self.op
}
}
impl<O: Operation> From<O> for Args<O> {
fn from(op: O) -> Self {
Self::new(op)
}
}
impl<O: Clone> Clone for Args<O> {
fn clone(&self) -> Self {
Self {
op: self.op.clone(),
json: Arc::clone(&self.json),
}
}
}
impl<O> PartialEq for Args<O> {
fn eq(&self, other: &Self) -> bool {
self.json == other.json
}
}
impl<O> Eq for Args<O> {}
impl<O> std::hash::Hash for Args<O> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.json.hash(state);
}
}
impl<O> std::fmt::Debug for Args<O> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.json)
}
}
pub fn query_scope<Q>() -> QueryScope<Args<Q>, Q::Output, FnError>
where
Q: Operation,
Q::Output: Clone,
{
QueryScope::new(|args: Args<Q>| call(args.into_inner()))
.with_name(&format!("rpc:{}", Q::NAME))
.with_ssr()
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
#[track_caller]
pub fn use_job(
id: impl Fn() -> String + Send + Sync + 'static,
) -> QueryHandle<crate::jobs::JobStatus, FnError> {
if live::available() {
return use_live(move || crate::jobs::JobStatusArgs { id: id() });
}
let scope = QueryScope::new(|args: Args<crate::jobs::JobStatusArgs>| call(args.into_inner()))
.with_name("rpc:job_status")
.with_refetch_interval(std::time::Duration::from_secs(1));
use_query(scope, move || {
Args::new(crate::jobs::JobStatusArgs { id: id() })
})
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
#[track_caller]
pub fn use_inbox() -> InboxHandle {
InboxHandle {
notices: use_live(|| crate::inbox::InboxArgs { limit: 50 }),
read: use_mutation(mutation_scope::<crate::inbox::MarkRead>()),
all_read: use_mutation(mutation_scope::<crate::inbox::MarkAllRead>()),
}
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
pub struct InboxHandle {
pub notices: QueryHandle<crate::inbox::Inbox, FnError>,
read: MutationHandle<crate::inbox::MarkRead, (), FnError>,
all_read: MutationHandle<crate::inbox::MarkAllRead, u64, FnError>,
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
impl InboxHandle {
pub fn unread(&self) -> u64 {
self.notices.data().map_or(0, |inbox| inbox.unread)
}
pub fn mark_read(&self, id: impl Into<String>) {
self.read.mutate(crate::inbox::MarkRead { id: id.into() });
}
pub fn mark_all_read(&self) {
self.all_read.mutate(crate::inbox::MarkAllRead {});
}
}
#[track_caller]
pub fn use_rpc_query<Q>(
args: impl Fn() -> Q + Send + Sync + 'static,
) -> QueryHandle<Q::Output, FnError>
where
Q: Operation,
Q::Output: Clone,
{
use_query(query_scope::<Q>(), move || Args::new(args()))
}
pub fn mutation_scope<M>() -> MutationScope<M, M::Output, FnError>
where
M: Operation,
M::Output: Clone,
{
MutationScope::new(|op: M| live::send_or_call(op)).with_name(&format!("rpc:{}", M::NAME))
}
pub fn action_scope<A>() -> MutationScope<A, A::Output, FnError>
where
A: Operation,
A::Output: Clone,
{
MutationScope::new(|op: A| call(op)).with_name(&format!("rpc:{}", A::NAME))
}
#[track_caller]
pub fn use_action<A>() -> MutationHandle<A, A::Output, FnError>
where
A: Operation,
A::Output: Clone,
{
use_mutation(action_scope::<A>())
}