From 81ddf80ea97eabbd42de101394f10dde4c22f9aa Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 2 Dec 2024 23:28:48 +0800 Subject: [PATCH 01/67] plan router v2 --- Cargo.toml | 1 + crates/core/Cargo.toml | 5 + crates/core/src/lib.rs | 2 - crates/legacy/src/lib.rs | 2 + src/legacy/internal/middleware.rs | 2 +- src/lib.rs | 26 +- src/procedure.rs | 75 ++++++ src/procedure_kind.rs | 21 ++ src/rewrite/error.rs | 11 - src/rewrite/infallible.rs | 30 --- src/rewrite/middleware.rs | 7 - src/rewrite/middleware/middleware.rs | 140 ---------- src/rewrite/middleware/next.rs | 30 --- src/rewrite/mod.rs | 21 -- src/rewrite/procedure.rs | 37 --- src/rewrite/procedure/builder.rs | 113 -------- src/rewrite/procedure/error.rs | 129 --------- src/rewrite/procedure/exec_input.rs | 94 ------- src/rewrite/procedure/input.rs | 65 ----- src/rewrite/procedure/meta.rs | 54 ---- src/rewrite/procedure/output.rs | 119 --------- src/rewrite/procedure/procedure.rs | 185 ------------- src/rewrite/procedure/resolver_input.rs | 54 ---- src/rewrite/procedure/resolver_output.rs | 105 -------- src/rewrite/procedure/stream.rs | 69 ----- src/rewrite/router.rs | 317 ----------------------- src/rewrite/state.rs | 95 ------- src/rewrite/stream.rs | 35 --- src/router.rs | 267 +++++++++++++++++++ 29 files changed, 395 insertions(+), 1716 deletions(-) create mode 100644 crates/legacy/src/lib.rs create mode 100644 src/procedure.rs create mode 100644 src/procedure_kind.rs delete mode 100644 src/rewrite/error.rs delete mode 100644 src/rewrite/infallible.rs delete mode 100644 src/rewrite/middleware.rs delete mode 100644 src/rewrite/middleware/middleware.rs delete mode 100644 src/rewrite/middleware/next.rs delete mode 100644 src/rewrite/mod.rs delete mode 100644 src/rewrite/procedure.rs delete mode 100644 src/rewrite/procedure/builder.rs delete mode 100644 src/rewrite/procedure/error.rs delete mode 100644 src/rewrite/procedure/exec_input.rs delete mode 100644 src/rewrite/procedure/input.rs delete mode 100644 src/rewrite/procedure/meta.rs delete mode 100644 src/rewrite/procedure/output.rs delete mode 100644 src/rewrite/procedure/procedure.rs delete mode 100644 src/rewrite/procedure/resolver_input.rs delete mode 100644 src/rewrite/procedure/resolver_output.rs delete mode 100644 src/rewrite/procedure/stream.rs delete mode 100644 src/rewrite/router.rs delete mode 100644 src/rewrite/state.rs delete mode 100644 src/rewrite/stream.rs create mode 100644 src/router.rs diff --git a/Cargo.toml b/Cargo.toml index 974582b8..7064f4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ tracing = ["dep:tracing"] [dependencies] # Public +rspc-core = { path = "./crates/core" } serde = { version = "1", features = ["derive"] } # TODO: Remove features futures = "0.3" specta = { version = "=2.0.0-rc.20", features = [ diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 09f40391..df729d06 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -4,6 +4,11 @@ version = "0.0.0" edition = "2021" publish = false # TODO: Metadata +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + # TODO: Disable all features for each of them [dependencies] # Public diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index a2f7669c..4c700f8b 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -14,8 +14,6 @@ // - `ProcedureStream::poll_next` - Keep or remove??? // - `Send` + `Sync` and the issues with single-threaded async runtimes // - `DynInput<'a, 'de>` should really be &'a Input<'de>` but that's hard. - -// TODO: Optional: // - Finish `Debug` impls // - Crate documentation diff --git a/crates/legacy/src/lib.rs b/crates/legacy/src/lib.rs new file mode 100644 index 00000000..6a3cf4b3 --- /dev/null +++ b/crates/legacy/src/lib.rs @@ -0,0 +1,2 @@ +//! TODO +// TODO: Crate icon and stuff diff --git a/src/legacy/internal/middleware.rs b/src/legacy/internal/middleware.rs index 37e27e18..2f9e85e2 100644 --- a/src/legacy/internal/middleware.rs +++ b/src/legacy/internal/middleware.rs @@ -176,7 +176,7 @@ where // TODO: Is this a duplicate of any type? // TODO: Move into public API cause it might be used in middleware -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProcedureKind { Query, Mutation, diff --git a/src/lib.rs b/src/lib.rs index 24e9ef3b..bf815f3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,8 +22,28 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] +mod procedure; +mod procedure_kind; +mod router; + +pub use procedure_kind::ProcedureKind2; +pub use router::Router2; + +// TODO: These will come in the future. +pub(crate) use procedure::Procedure2; +pub(crate) type State = (); + +// TODO: Expose everything from `rspc_core`? + +// Legacy stuff mod legacy; -#[doc(hidden)] -pub mod rewrite; -pub use legacy::*; +// These remain to respect semver but will all go with the next major. +#[allow(deprecated)] +pub use legacy::{ + internal, test_result_type, test_result_value, typedef, Config, DoubleArgMarker, + DoubleArgStreamMarker, Error, ErrorCode, ExecError, ExecKind, ExportError, FutureMarker, + Middleware, MiddlewareBuilder, MiddlewareContext, MiddlewareLike, + MiddlewareWithResponseHandler, RequestLayer, Resolver, ResultMarker, Router, RouterBuilder, + SerializeMarker, StreamResolver, +}; diff --git a/src/procedure.rs b/src/procedure.rs new file mode 100644 index 00000000..3bf5d06a --- /dev/null +++ b/src/procedure.rs @@ -0,0 +1,75 @@ +use specta::datatype::DataType; + +use crate::internal::ProcedureKind; + +/// Represents a single operations on the server that can be executed. +/// +/// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. +/// +pub struct Procedure2 { + pub(crate) inner: rspc_core::Procedure, + // pub(crate) setup + pub(crate) kind: ProcedureKind, + pub(crate) input: DataType, + pub(crate) result: DataType, +} + +// TODO: `Clone`, `Debug`, `PartialEq`, `Eq`, `Hash` + +// TODO: `Type` which should be like `Record` + +impl Procedure2 { + // TODO: `fn builder` + + pub fn kind(&self) -> ProcedureKind { + self.kind + } + + // TODO: Expose all fields + + // /// Export the [Specta](https://docs.rs/specta) types for this procedure. + // /// + // /// TODO - Use this with `rspc::typescript` + // /// + // /// # Usage + // /// + // /// ```rust + // /// todo!(); # TODO: Example + // /// ``` + // pub fn ty(&self) -> &ProcedureTypeDefinition { + // &self.ty + // } + + // /// Execute a procedure with the given context and input. + // /// + // /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. + // /// + // /// # Usage + // /// + // /// ```rust + // /// use serde_json::Value; + // /// + // /// fn run_procedure(procedure: Procedure) -> Vec { + // /// procedure + // /// .exec((), Value::Null) + // /// .collect::>() + // /// .await + // /// .into_iter() + // /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) + // /// .collect::>() + // /// } + // /// ``` + // pub fn exec<'de, T: ProcedureInput<'de>>( + // &self, + // ctx: TCtx, + // input: T, + // ) -> Result { + // match input.into_deserializer() { + // Ok(deserializer) => { + // let mut input = ::erase(deserializer); + // (self.handler)(ctx, &mut input) + // } + // Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), + // } + // } +} diff --git a/src/procedure_kind.rs b/src/procedure_kind.rs new file mode 100644 index 00000000..513a021d --- /dev/null +++ b/src/procedure_kind.rs @@ -0,0 +1,21 @@ +use std::fmt; + +use specta::Type; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] +#[specta(rename_all = "camelCase")] +pub enum ProcedureKind2 { + Query, + Mutation, + Subscription, +} + +impl fmt::Display for ProcedureKind2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Query => write!(f, "Query"), + Self::Mutation => write!(f, "Mutation"), + Self::Subscription => write!(f, "Subscription"), + } + } +} diff --git a/src/rewrite/error.rs b/src/rewrite/error.rs deleted file mode 100644 index 2ff445b6..00000000 --- a/src/rewrite/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::error; - -use serde::Serialize; -use specta::Type; - -pub trait Error: error::Error + Send + Serialize + Type + 'static { - // Warning: Returning > 400 will fallback to `500`. As redirects would be invalid and `200` would break matching. - fn status(&self) -> u16 { - 500 - } -} diff --git a/src/rewrite/infallible.rs b/src/rewrite/infallible.rs deleted file mode 100644 index 05ba0d6f..00000000 --- a/src/rewrite/infallible.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::fmt; - -use serde::Serialize; -use specta::Type; - -#[derive(Type, Debug)] -pub enum Infallible {} - -impl fmt::Display for Infallible { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl Serialize for Infallible { - fn serialize(&self, _: S) -> Result - where - S: serde::Serializer, - { - unreachable!() - } -} - -impl std::error::Error for Infallible {} - -impl super::Error for Infallible { - fn status(&self) -> u16 { - unreachable!() - } -} diff --git a/src/rewrite/middleware.rs b/src/rewrite/middleware.rs deleted file mode 100644 index 0a344264..00000000 --- a/src/rewrite/middleware.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod middleware; -mod next; - -pub use middleware::Middleware; -pub use next::Next; - -pub(crate) use middleware::MiddlewareHandler; diff --git a/src/rewrite/middleware/middleware.rs b/src/rewrite/middleware/middleware.rs deleted file mode 100644 index f5baaad7..00000000 --- a/src/rewrite/middleware/middleware.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! This comment contains an overview of the rationale behind the design of the middleware system. -//! NOTE: It is *not* included in the generated Rust docs! -//! -//! For future reference: -//! -//! Having a standalone middleware that is like `fn my_middleware() -> impl Middleware<...>` results in *really* bad error messages. -//! This is because the middleware is defined within the function and then *constrained* at the function boundary. -//! These places are different so the compiler is like lol trait xyz with generics iop does match the trait xyz with generics abc. -//! -//! Instead if the builder function takes a [`MiddlewareBuilder`] the constrain it applied prior to the middleware being defined. -//! This allows the compiler to constrain the types at the middleware definition site which leads to insanely better error messages. -//! -//! Be aware this talk about constraining and definition is just me speaking about what I have observed. -//! This could be completely wrong from a technical perspective. -//! -//! TODO: Explaining why inference across boundaries is not supported. -//! -//! TODO: Explain why we can't have `fn mw(...) -> Middleware` -> It's because of default generics!!! -//! -//! TODO: Why we can't use `const`'s for declaring middleware -> Boxing - -use std::{pin::Pin, sync::Arc}; - -use futures::Future; - -use crate::rewrite::{procedure::ProcedureMeta, State}; - -use super::Next; - -pub(crate) type MiddlewareHandler = Box< - dyn Fn( - TNextCtx, - TNextInput, - ProcedureMeta, - ) -> Pin> + Send + 'static>> - + Send - + Sync - + 'static, ->; - -/// An abstraction for common logic that can be applied to procedures. -/// -/// A middleware can be used to run custom logic and modify the context, input, and result of the next procedure. This makes is perfect for logging, authentication and many other things! -/// -/// Middleware are applied with [ProcedureBuilder::with](crate::procedure::ProcedureBuilder::with). -/// -/// # Generics -/// -/// - `TError` - The type of the error that can be returned by the middleware. Defined by [ProcedureBuilder::error](crate::procedure::ProcedureBuilder::error). -/// - `TThisCtx` - // TODO -/// - `TThisInput` - // TODO -/// - `TThisResult` - // TODO -/// - `TNextCtx` - // TODO -/// - `TNextInput` - // TODO -/// - `TNextResult` - // TODO -/// -/// TODO: [ -// Context of previous layer (`ctx`), -// Error type, -// The input to the middleware (`input`), -// The result of the middleware (return type of future), -// - This following will default to the input types if not explicitly provided // TODO: Will this be confusing or good? -// The context returned by the middleware (`next.exec({dis_bit}, ...)`), -// The input to the next layer (`next.exec(..., {dis_bit})`), -// The result of the next layer (`let _result: {dis_bit} = next.exec(...)`), -// ] -/// -/// ```rust -/// TODO: Example to show where the generics line up. -/// ``` -/// -/// # Stacking -/// -/// TODO: Guide the user through stacking. -/// -/// # Example -/// -/// TODO: -/// -// TODO: Explain why they are required -> inference not supported across boundaries. -pub struct Middleware< - TError, - TThisCtx, - TThisInput, - TThisResult, - TNextCtx = TThisCtx, - TNextInput = TThisInput, - TNextResult = TThisResult, -> { - pub(crate) setup: Option>, - pub(crate) inner: Box< - dyn FnOnce( - MiddlewareHandler, - ) -> MiddlewareHandler, - >, -} - -// TODO: Debug impl - -impl - Middleware -where - TError: 'static, - TNextCtx: 'static, - TNextInput: 'static, - TNextResult: 'static, -{ - pub fn new> + Send + 'static>( - func: impl Fn(TThisCtx, TThisInput, Next) -> F - + Send - + Sync - + 'static, - ) -> Self { - Self { - setup: None, - inner: Box::new(move |next| { - // TODO: Don't `Arc>` - let next = Arc::new(next); - - Box::new(move |ctx, input, meta| { - let f = func( - ctx, - input, - Next { - meta, - next: next.clone(), - }, - ); - - Box::pin(f) - }) - }), - } - } - - pub fn setup(mut self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { - self.setup = Some(Box::new(func)); - self - } -} diff --git a/src/rewrite/middleware/next.rs b/src/rewrite/middleware/next.rs deleted file mode 100644 index ab4a52fa..00000000 --- a/src/rewrite/middleware/next.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::{fmt, sync::Arc}; - -use crate::rewrite::{middleware::middleware::MiddlewareHandler, procedure::ProcedureMeta}; - -pub struct Next { - // TODO: `pub(super)` over `pub(crate)` - pub(crate) meta: ProcedureMeta, - pub(crate) next: Arc>, -} - -impl fmt::Debug for Next { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Next").finish() - } -} - -impl Next -where - TCtx: 'static, - TInput: 'static, - TReturn: 'static, -{ - pub fn meta(&self) -> ProcedureMeta { - self.meta.clone() - } - - pub async fn exec(&self, ctx: TCtx, input: TInput) -> Result { - (self.next)(ctx, input, self.meta.clone()).await - } -} diff --git a/src/rewrite/mod.rs b/src/rewrite/mod.rs deleted file mode 100644 index f4b6516d..00000000 --- a/src/rewrite/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod middleware; -pub mod procedure; - -mod error; -mod infallible; -mod router; -mod state; -mod stream; - -pub use error::Error; -pub use infallible::Infallible; -pub use router::{BuiltRouter, Router}; -pub use state::State; -pub use stream::Stream; - -#[doc(hidden)] -pub mod internal { - // To make versioning easier we reexport it so libraries such as `rspc_axum` don't need a direct dependency on `specta`. - pub use serde::Serialize; - pub use specta::{DataType, Type, TypeMap}; // TODO: Why does rspc_axum care again? -} diff --git a/src/rewrite/procedure.rs b/src/rewrite/procedure.rs deleted file mode 100644 index 2204cc4a..00000000 --- a/src/rewrite/procedure.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! A procedure holds a single operation that can be executed by the server. -//! -//! A procedure is built up from: -//! - any number of middleware -//! - a single resolver function (of type `query`, `mutation` or `subscription`) -//! -//! Features: -//! - Input types (Serde-compatible or custom) -//! - Result types (Serde-compatible or custom) -//! - [`Future`](#todo) or [`Stream`](#todo) results -//! - Typesafe error handling -//! -//! TODO: Request flow overview -//! TODO: Explain, what a procedure is, return type/struct, middleware, execution order, etc -//! - -mod builder; -mod error; -mod exec_input; -mod input; -mod meta; -mod output; -mod procedure; -mod resolver_input; -mod resolver_output; -mod stream; - -pub use builder::ProcedureBuilder; -pub use error::{InternalError, ResolverError}; -pub use exec_input::ProcedureExecInput; -pub use input::ProcedureInput; -pub use meta::{ProcedureKind, ProcedureMeta}; -pub use output::{ProcedureOutput, ProcedureOutputSerializeError}; -pub use procedure::{Procedure, ProcedureTypeDefinition, UnbuiltProcedure}; -pub use resolver_input::ResolverInput; -pub use resolver_output::ResolverOutput; -pub use stream::ProcedureStream; diff --git a/src/rewrite/procedure/builder.rs b/src/rewrite/procedure/builder.rs deleted file mode 100644 index a3f25f09..00000000 --- a/src/rewrite/procedure/builder.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::{fmt, future::Future}; - -use futures::FutureExt; - -use crate::rewrite::{ - middleware::{Middleware, MiddlewareHandler}, - Error, State, -}; - -use super::{ProcedureKind, ProcedureMeta, UnbuiltProcedure}; - -// TODO: Document the generics like `Middleware` -pub struct ProcedureBuilder { - pub(super) build: Box< - dyn FnOnce( - ProcedureKind, - Vec>, - MiddlewareHandler, - ) -> UnbuiltProcedure, - >, -} - -impl fmt::Debug - for ProcedureBuilder -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Procedure").finish() - } -} - -impl - ProcedureBuilder -where - TError: Error, - TRootCtx: 'static, - TCtx: 'static, - TInput: 'static, - TResult: 'static, -{ - pub fn with( - self, - mw: Middleware, - ) -> ProcedureBuilder - where - TNextCtx: 'static, - I: 'static, - R: 'static, - { - ProcedureBuilder { - build: Box::new(|ty, mut setups, handler| { - if let Some(setup) = mw.setup { - setups.push(setup); - } - - (self.build)(ty, setups, (mw.inner)(handler)) - }), - } - } - - pub fn setup(self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { - Self { - build: Box::new(|ty, mut setups, handler| { - setups.push(Box::new(func)); - (self.build)(ty, setups, handler) - }), - } - } - - pub fn query> + Send + 'static>( - self, - handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> UnbuiltProcedure { - (self.build)( - ProcedureKind::Query, - Vec::new(), - Box::new(move |ctx, input, _| Box::pin(handler(ctx, input))), - ) - } - - pub fn mutation> + Send + 'static>( - self, - handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> UnbuiltProcedure { - (self.build)( - ProcedureKind::Mutation, - Vec::new(), - Box::new(move |ctx, input, _| Box::pin(handler(ctx, input))), - ) - } -} - -impl - ProcedureBuilder> -where - TError: Error, - TRootCtx: 'static, - TCtx: 'static, - TInput: 'static, - S: futures::Stream> + Send + 'static, -{ - pub fn subscription> + Send + 'static>( - self, - handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> UnbuiltProcedure { - (self.build)( - ProcedureKind::Subscription, - Vec::new(), - Box::new(move |ctx, input, _| { - Box::pin(handler(ctx, input).map(|s| s.map(|s| crate::rewrite::Stream(s)))) - }), - ) - } -} diff --git a/src/rewrite/procedure/error.rs b/src/rewrite/procedure/error.rs deleted file mode 100644 index e267757f..00000000 --- a/src/rewrite/procedure/error.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::{ - any::{type_name, Any, TypeId}, - error, fmt, -}; - -use serde::{Serialize, Serializer}; - -use crate::rewrite::Error; - -use super::ProcedureOutputSerializeError; - -pub enum InternalError { - /// Attempted to deserialize input but found downcastable input. - ErrInputNotDeserializable(&'static str), - /// Attempted to downcast input but found deserializable input. - ErrInputNotDowncastable, - /// Error when deserializing input. - // Boxed to seal `erased_serde` from public API. - ErrDeserializingInput(Box), // TODO: Maybe seal so this type *has* to come from `erased_serde`??? -} - -impl fmt::Debug for InternalError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - InternalError::ErrInputNotDeserializable(type_name) => { - write!(f, "input is not deserializable, found type: {type_name}") - } - InternalError::ErrInputNotDowncastable => { - write!(f, "input is not downcastable") - } - InternalError::ErrDeserializingInput(err) => { - write!(f, "failed to deserialize input: {err}") - } - } - } -} - -impl fmt::Display for InternalError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl error::Error for InternalError {} - -trait ErasedError: error::Error + erased_serde::Serialize + Any + Send + 'static { - fn to_box_any(self: Box) -> Box; - - fn to_value(&self) -> Option>; -} -impl ErasedError for T { - fn to_box_any(self: Box) -> Box { - self - } - - fn to_value(&self) -> Option> { - Some(serde_value::to_value(self)) - } -} - -pub struct ResolverError { - status: u16, - type_name: &'static str, - type_id: TypeId, - inner: Box, -} - -impl ResolverError { - pub fn new(value: T) -> Self { - Self { - status: value.status(), - type_name: type_name::(), - type_id: TypeId::of::(), - inner: Box::new(value), - } - } - - pub fn status(&self) -> u16 { - if self.status > 400 || self.status < 600 { - return 500; - } - - self.status - } - - pub fn type_name(&self) -> &'static str { - self.type_name - } - - pub fn type_id(&self) -> TypeId { - self.type_id - } - - pub fn downcast(self) -> Option { - self.inner.to_box_any().downcast().map(|v| *v).ok() - } - - // TODO: Using `ProcedureOutputSerializeError`???? - pub fn serialize( - self, - ser: S, - ) -> Result> { - let value = self - .inner - .to_value() - .ok_or(ProcedureOutputSerializeError::ErrResultNotDeserializable( - self.type_name, - ))? - .expect("serde_value doesn't panic"); // TODO: This is false - - value - .serialize(ser) - .map_err(ProcedureOutputSerializeError::ErrSerializer) - } -} - -impl fmt::Debug for ResolverError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.inner) - } -} - -impl fmt::Display for ResolverError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.inner) - } -} - -impl error::Error for ResolverError {} diff --git a/src/rewrite/procedure/exec_input.rs b/src/rewrite/procedure/exec_input.rs deleted file mode 100644 index 39a8f907..00000000 --- a/src/rewrite/procedure/exec_input.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::{ - any::{type_name, Any, TypeId}, - marker::PhantomData, -}; - -use serde::de::DeserializeOwned; - -use super::{InternalError, ProcedureInput}; - -pub(super) trait InputValueInner<'de> { - fn into_deserializer(&mut self) -> Option<&mut dyn erased_serde::Deserializer<'de>>; - - fn get_type_name(&self) -> Option<&'static str> { - None - } - - fn get_type_id(&self) -> Option { - None - } - - fn into_dyn_any(&mut self) -> Option<&mut dyn Any> { - None - } -} - -pub(super) struct AnyInput(pub Option); -impl<'de, T: Any + 'static> InputValueInner<'de> for AnyInput { - fn into_deserializer(&mut self) -> Option<&mut dyn erased_serde::Deserializer<'de>> { - None - } - - fn get_type_name(&self) -> Option<&'static str> { - Some(type_name::()) - } - - fn get_type_id(&self) -> Option { - Some(TypeId::of::()) - } - - fn into_dyn_any(&mut self) -> Option<&mut dyn Any> { - Some(&mut self.0) - } -} - -impl<'de, D: erased_serde::Deserializer<'de>> InputValueInner<'de> for D { - fn into_deserializer(&mut self) -> Option<&mut dyn erased_serde::Deserializer<'de>> { - Some(self) - } -} - -pub struct ProcedureExecInput<'a, 'b, T>(&'a mut dyn InputValueInner<'b>, PhantomData); - -impl<'a, 'b, T> ProcedureExecInput<'a, 'b, T> { - pub(crate) fn new(value: &'a mut dyn InputValueInner<'b>) -> Self { - Self(value, PhantomData) - } - - pub fn type_name(&self) -> Option<&'static str> { - self.0.get_type_name() - } - - pub fn type_id(&self) -> Option { - self.0.get_type_id() - } - - // TODO: Should we have a generic downcast???? -> This is typesafe but it means the `TypeId` stuff can't be used for matching??? - pub fn downcast(self) -> Result - where - T: ProcedureInput<'b> + 'static, - { - Ok(self - .0 - .into_dyn_any() - .ok_or(InternalError::ErrInputNotDowncastable)? - .downcast_mut::>() - .expect("todo: this is typesafe") - .take() - .expect("value already taken")) - } - - pub fn deserialize(self) -> Result { - erased_serde::deserialize(match self.0.into_deserializer() { - Some(deserializer) => deserializer, - None => { - return Err(InternalError::ErrInputNotDeserializable( - self.0 - .get_type_name() - .expect("if it's not a serde type this must be valid"), - )) - } - }) - .map_err(|err| InternalError::ErrDeserializingInput(Box::new(err))) - } -} diff --git a/src/rewrite/procedure/input.rs b/src/rewrite/procedure/input.rs deleted file mode 100644 index 3edb4fd6..00000000 --- a/src/rewrite/procedure/input.rs +++ /dev/null @@ -1,65 +0,0 @@ -use serde::Deserializer; - -use super::ResolverInput; - -/// Any value that can be used as the input to [`Procedure::exec`](crate::procedure::Procedure::exec). -/// -/// This trait has a built in implementation for any [`Deserializer`](serde::Deserializer)'s so you can provide: -/// - [`serde_json::Value`] -/// - [`serde_value::Value`] -/// - [`serde_json::Deserializer::from_str`] -/// - etc. -/// -/// ## How this works? -/// -/// If you provide a type which implements [`Deserializer`](serde::Deserializer) we will use it to construct the [`ResolverInput`] value of the procedure, otherwise downcasting will be used. -/// -/// [`Self::Value`] be converted into a [`ProcedureInput`](super::ProcedureInput) which is provided to [`Input::from_value`] to allow deserializing or downcasting the value back into the correct type. -/// -/// ## Implementation for custom types -/// -/// ``` -/// pub struct MyCoolThing(pub String); -/// -/// impl<'de> Argument<'de> for MyCoolThing { -/// type Value = Self; -/// -/// fn into_value(self) -> Self::Value { -/// self -/// } -/// } -/// -/// fn usage_within_rspc(procedure: Procedure) { -/// let _ = procedure.exec((), MyCoolThing("Hello, World!".to_string())); -/// } -/// ``` -pub trait ProcedureInput<'de>: Sized { - /// The value which is available from your [`ResolverInput`] implementation to downcast from. - /// - /// This exists so your able to accept `SomeType` as an [`ProcedureInput`], but then type-erase to `SomeType>` so your [`ResolverInput`] implementation is able to downcast the value. - /// - /// It's recommended to set this to `Self` unless you hit the case described above. - type Value: ResolverInput; - - /// Convert self into `Self::Value` - fn into_value(self) -> Self::Value; - - /// Convert self into a [`Deserializer`](serde::Deserializer) if possible or return the original value. - /// - /// This will be executed and if it returns `Err(Self)` we will fallback to [`Self::into_value`] and use downcasting. - fn into_deserializer(self) -> Result, Self> { - Err::(self) - } -} - -impl<'de, D: Deserializer<'de>> ProcedureInput<'de> for D { - type Value = (); - - fn into_value(self) -> Self::Value { - unreachable!(); - } - - fn into_deserializer(self) -> Result, Self> { - Ok(self) - } -} diff --git a/src/rewrite/procedure/meta.rs b/src/rewrite/procedure/meta.rs deleted file mode 100644 index efc757c6..00000000 --- a/src/rewrite/procedure/meta.rs +++ /dev/null @@ -1,54 +0,0 @@ -use core::fmt; -use std::{borrow::Cow, sync::Arc}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, specta::Type)] -#[specta(rename_all = "camelCase")] -pub enum ProcedureKind { - Query, - Mutation, - Subscription, -} - -impl fmt::Display for ProcedureKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Query => write!(f, "Query"), - Self::Mutation => write!(f, "Mutation"), - Self::Subscription => write!(f, "Subscription"), - } - } -} - -#[derive(Debug, Clone)] -enum ProcedureName { - Static(&'static str), - Dynamic(Arc), -} - -#[derive(Debug, Clone)] -pub struct ProcedureMeta { - name: ProcedureName, - kind: ProcedureKind, -} - -impl ProcedureMeta { - pub(crate) fn new(name: Cow<'static, str>, kind: ProcedureKind) -> Self { - Self { - name: ProcedureName::Dynamic(Arc::new(name.into_owned())), - kind, - } - } -} - -impl ProcedureMeta { - pub fn name(&self) -> &str { - match &self.name { - ProcedureName::Static(name) => name, - ProcedureName::Dynamic(name) => name.as_str(), - } - } - - pub fn kind(&self) -> ProcedureKind { - self.kind - } -} diff --git a/src/rewrite/procedure/output.rs b/src/rewrite/procedure/output.rs deleted file mode 100644 index 05de5e71..00000000 --- a/src/rewrite/procedure/output.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::{ - any::{type_name, Any, TypeId}, - error, fmt, -}; - -use serde::{Serialize, Serializer}; - -trait Inner: Any + 'static { - fn to_box_any(self: Box) -> Box; - - fn to_value(&self) -> Option> { - None - } -} - -struct AnyT(T); -impl Inner for AnyT { - fn to_box_any(self: Box) -> Box { - Box::new(self.0) - } -} - -impl Inner for T { - fn to_box_any(self: Box) -> Box { - Box::new(self) - } - - fn to_value(&self) -> Option> { - Some(serde_value::to_value(self)) - } -} - -pub struct ProcedureOutput { - type_name: &'static str, - type_id: TypeId, - inner: Box, -} - -impl fmt::Debug for ProcedureOutput { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProcedureOutput") - .field("type_name", &self.type_name) - .field("type_id", &self.type_id) - .finish() - } -} - -impl ProcedureOutput { - pub fn new(value: T) -> Self { - Self { - type_name: type_name::(), - type_id: TypeId::of::(), - inner: Box::new(AnyT(value)), - } - } - - pub fn with_serde(value: T) -> Self { - Self { - type_name: type_name::(), - type_id: TypeId::of::(), - inner: Box::new(value), - } - } - - pub fn type_name(&self) -> &'static str { - self.type_name - } - - pub fn type_id(&self) -> TypeId { - self.type_id - } - - pub fn downcast(self) -> Option { - self.inner.to_box_any().downcast().map(|v| *v).ok() - } - - pub fn serialize( - self, - ser: S, - ) -> Result> { - let value = self - .inner - .to_value() - .ok_or(ProcedureOutputSerializeError::ErrResultNotDeserializable( - self.type_name, - ))? - .expect("serde_value doesn't panic"); // TODO: This is false - - value - .serialize(ser) - .map_err(ProcedureOutputSerializeError::ErrSerializer) - } -} - -pub enum ProcedureOutputSerializeError { - /// Attempted to deserialize input but found downcastable input. - ErrResultNotDeserializable(&'static str), - /// Error occurred in the serializer you provided. - ErrSerializer(S::Error), -} - -impl fmt::Debug for ProcedureOutputSerializeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::ErrResultNotDeserializable(type_name) => { - write!(f, "Result type {type_name} is not deserializable") - } - Self::ErrSerializer(err) => write!(f, "Serializer error: {err:?}"), - } - } -} - -impl fmt::Display for ProcedureOutputSerializeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl error::Error for ProcedureOutputSerializeError {} diff --git a/src/rewrite/procedure/procedure.rs b/src/rewrite/procedure/procedure.rs deleted file mode 100644 index b2b799f6..00000000 --- a/src/rewrite/procedure/procedure.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::{borrow::Cow, fmt, sync::Arc}; - -use futures::FutureExt; -use specta::{DataType, TypeMap}; - -use crate::rewrite::{Error, State}; - -use super::{ - exec_input::{AnyInput, InputValueInner}, - stream::ProcedureStream, - InternalError, ProcedureBuilder, ProcedureExecInput, ProcedureInput, ProcedureKind, - ProcedureMeta, ResolverInput, ResolverOutput, -}; - -pub(super) type InvokeFn = Arc< - dyn Fn(TCtx, &mut dyn InputValueInner) -> Result + Send + Sync, ->; - -/// Represents a single operations on the server that can be executed. -/// -/// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. -/// -pub struct Procedure { - kind: ProcedureKind, - ty: ProcedureTypeDefinition, - handler: InvokeFn, -} - -impl Clone for Procedure { - fn clone(&self) -> Self { - Self { - kind: self.kind, - ty: self.ty.clone(), - handler: self.handler.clone(), - } - } -} - -impl fmt::Debug for Procedure { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Procedure") - .field("kind", &self.kind) - .field("ty", &self.ty) - .field("handler", &"...") - .finish() - } -} - -impl Procedure -where - TCtx: 'static, -{ - /// Construct a new procedure using [`ProcedureBuilder`]. - pub fn builder() -> ProcedureBuilder - where - TError: Error, - // Only the first layer (middleware or the procedure) needs to be a valid input/output type - I: ResolverInput, - R: ResolverOutput, - { - ProcedureBuilder { - build: Box::new(|kind, setups, handler| { - // TODO: Don't be `Arc>` just `Arc<_>` - let handler = Arc::new(handler); - - UnbuiltProcedure::new(move |key, state, type_map| { - let meta = ProcedureMeta::new(key.clone(), kind); - for setup in setups { - setup(state, meta.clone()); - } - - Procedure { - kind, - ty: ProcedureTypeDefinition { - key, - kind, - input: I::data_type(type_map), - result: R::data_type(type_map), - }, - handler: Arc::new(move |ctx, input| { - let fut = handler( - ctx, - I::from_value(ProcedureExecInput::new(input))?, - meta.clone(), - ); - - Ok(R::into_procedure_stream(fut.into_stream())) - }), - } - }) - }), - } - } -} - -impl Procedure { - pub fn kind(&self) -> ProcedureKind { - self.kind - } - - /// Export the [Specta](https://docs.rs/specta) types for this procedure. - /// - /// TODO - Use this with `rspc::typescript` - /// - /// # Usage - /// - /// ```rust - /// todo!(); # TODO: Example - /// ``` - pub fn ty(&self) -> &ProcedureTypeDefinition { - &self.ty - } - - /// Execute a procedure with the given context and input. - /// - /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. - /// - /// # Usage - /// - /// ```rust - /// use serde_json::Value; - /// - /// fn run_procedure(procedure: Procedure) -> Vec { - /// procedure - /// .exec((), Value::Null) - /// .collect::>() - /// .await - /// .into_iter() - /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) - /// .collect::>() - /// } - /// ``` - pub fn exec<'de, T: ProcedureInput<'de>>( - &self, - ctx: TCtx, - input: T, - ) -> Result { - match input.into_deserializer() { - Ok(deserializer) => { - let mut input = ::erase(deserializer); - (self.handler)(ctx, &mut input) - } - Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ProcedureTypeDefinition { - // TODO: Should `key` move onto `Procedure` instead?s - pub key: Cow<'static, str>, - pub kind: ProcedureKind, - pub input: DataType, - pub result: DataType, -} - -pub struct UnbuiltProcedure( - Box, &mut State, &mut TypeMap) -> Procedure>, -); - -impl fmt::Debug for UnbuiltProcedure { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("UnbuiltProcedure").finish() - } -} - -impl UnbuiltProcedure { - pub(crate) fn new( - build_fn: impl FnOnce(Cow<'static, str>, &mut State, &mut TypeMap) -> Procedure + 'static, - ) -> Self { - Self(Box::new(build_fn)) - } - - /// Build the procedure invoking all the setup functions. - /// - /// Generally you will not need to call this directly as you can give a [ProcedureFactory] to the [RouterBuilder::procedure] and let it take care of the rest. - pub fn build( - self, - key: Cow<'static, str>, - state: &mut State, - type_map: &mut TypeMap, - ) -> Procedure { - (self.0)(key, state, type_map) - } -} diff --git a/src/rewrite/procedure/resolver_input.rs b/src/rewrite/procedure/resolver_input.rs deleted file mode 100644 index 5e851d66..00000000 --- a/src/rewrite/procedure/resolver_input.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::any::Any; - -use serde::de::DeserializeOwned; -use specta::{DataType, Generics, Type, TypeMap}; - -use super::{InternalError, ProcedureExecInput}; - -/// The input to a procedure which is derived from an [`ProcedureInput`](crate::procedure::Argument). -/// -/// This trait has a built in implementation for any type which implements [`DeserializeOwned`](serde::de::DeserializeOwned). -/// -/// ## How this works? -/// -/// [`Self::from_value`] will be provided with a [`ProcedureInput`] which wraps the [`Argument::Value`](super::Argument::Value) from the argument provided to the [`Procedure::exec`](super::Procedure) call. -/// -/// Input is responsible for converting this value into the type the user specified for the procedure. -/// -/// If the type implements [`DeserializeOwned`](serde::de::DeserializeOwned) we will use Serde, otherwise we will attempt to downcast the value. -/// -/// ## Implementation for custom types -/// -/// Say you have a type `MyCoolThing` which you want to use as an argument to an rspc procedure: -/// -/// ``` -/// pub struct MyCoolThing(pub String); -/// -/// impl ResolverInput for MyCoolThing { -/// fn from_value(value: ProcedureInput) -> Result { -/// Ok(todo!()) // Refer to ProcedureInput's docs -/// } -/// } -/// -/// // You should also implement `ProcedureInput`. -/// -/// fn usage_within_rspc() { -/// ::builder().query(|_, _: MyCoolThing| async move { () }); -/// } -/// ``` -pub trait ResolverInput: Sized + Any + Send + 'static { - fn data_type(type_map: &mut TypeMap) -> DataType; - - /// Convert the [`ProcedureInput`] into the type the user specified for the procedure. - fn from_value(value: ProcedureExecInput) -> Result; -} - -impl ResolverInput for T { - fn data_type(type_map: &mut TypeMap) -> DataType { - T::inline(type_map, Generics::Definition) - } - - fn from_value(value: ProcedureExecInput) -> Result { - Ok(value.deserialize()?) - } -} diff --git a/src/rewrite/procedure/resolver_output.rs b/src/rewrite/procedure/resolver_output.rs deleted file mode 100644 index 9fb15123..00000000 --- a/src/rewrite/procedure/resolver_output.rs +++ /dev/null @@ -1,105 +0,0 @@ -use futures::{stream::once, Stream, StreamExt}; -use serde::Serialize; -use specta::{DataType, Generics, Type, TypeMap}; - -use crate::rewrite::Error; - -use super::{ProcedureOutput, ProcedureStream}; - -/// A type which can be returned from a procedure. -/// -/// This has a default implementation for all [`Serialize`](serde::Serialize) types. -/// -/// ## How this works? -/// -/// We call [`Self::into_procedure_stream`] with the stream produced by the users handler and it will produce the [`ProcedureStream`] which is returned from the [`Procedure::exec`](super::Procedure::exec) call. If the user's handler was a [`Future`](std::future::Future) it will be converted into a [`Stream`](futures::Stream) by rspc. -/// -/// For each value the [`Self::into_procedure_stream`] implementation **must** defer to [`Self::into_procedure_result`] to convert the value into a [`ProcedureOutput`]. rspc provides a default implementation that takes care of this for you so don't override it unless you have a good reason. -/// -/// ## Implementation for custom types -/// -/// ```rust -/// pub struct MyCoolThing(pub String); -/// -/// impl ResolverOutput for MyCoolThing { -/// fn into_procedure_result(self) -> Result { -/// Ok(todo!()) // Refer to ProcedureOutput's docs -/// } -/// } -/// -/// fn usage_within_rspc() { -/// ::builder().query(|_, _: ()| async move { MyCoolThing("Hello, World!".to_string()) }); -/// } -/// ``` -// TODO: Do some testing and set this + add documentation link into it. -// #[diagnostic::on_unimplemented( -// message = "Your procedure must return a type that implements `serde::Serialize + specta::Type + 'static`", -// note = "ResolverOutput requires a `T where T: serde::Serialize + specta::Type + 'static` to be returned from your procedure" -// )] -pub trait ResolverOutput: Sized + Send + 'static { - /// Convert the procedure and any async part of the value into a [`ProcedureStream`]. - /// - /// This primarily exists so the [`rspc::Stream`](crate::Stream) implementation can merge it's stream into the procedure stream. - fn into_procedure_stream( - procedure: impl Stream> + Send + 'static, - ) -> ProcedureStream - where - TError: Error, - { - ProcedureStream::from_stream(procedure.map(|v| v?.into_procedure_result())) - } - - // TODO: Be an associated type instead so we can constrain later for better errors???? - fn data_type(type_map: &mut TypeMap) -> DataType; - - /// Convert the value from the user into a [`ProcedureOutput`]. - fn into_procedure_result(self) -> Result; -} - -impl ResolverOutput for T -where - T: Serialize + Type + Send + 'static, - TError: Error, -{ - fn data_type(type_map: &mut TypeMap) -> DataType { - T::inline(type_map, Generics::Definition) - } - - fn into_procedure_result(self) -> Result { - Ok(ProcedureOutput::with_serde(self)) - } -} - -impl ResolverOutput for crate::rewrite::Stream -where - TErr: Send, - S: Stream> + Send + 'static, - T: ResolverOutput, -{ - fn data_type(type_map: &mut TypeMap) -> DataType { - T::data_type(type_map) // TODO: Do we need to do anything special here so the frontend knows this is a stream? - } - - fn into_procedure_stream( - procedure: impl Stream> + Send + 'static, - ) -> ProcedureStream - where - TErr: Error, - { - ProcedureStream::from_stream( - procedure - .map(|v| match v { - Ok(s) => { - s.0.map(|v| v.and_then(|v| v.into_procedure_result())) - .right_stream() - } - Err(err) => once(async move { Err(err) }).left_stream(), - }) - .flatten(), - ) - } - - fn into_procedure_result(self) -> Result { - panic!("returning nested rspc::Stream's is not currently supported.") - } -} diff --git a/src/rewrite/procedure/stream.rs b/src/rewrite/procedure/stream.rs deleted file mode 100644 index e19c7652..00000000 --- a/src/rewrite/procedure/stream.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{ - error, - future::Future, - pin::Pin, - task::{Context, Poll}, -}; - -use futures::{Stream, TryFutureExt, TryStreamExt}; - -use super::{output::ProcedureOutput, ResolverError}; -use crate::rewrite::Error; - -enum Inner { - Value(Result), - Future(Pin> + Send>>), - Stream(Pin> + Send>>), -} - -pub struct ProcedureStream(Option); - -impl ProcedureStream { - pub fn from_value(value: Result) -> Self - where - TError: Error, - { - Self(Some(Inner::Value(value.map_err(ResolverError::new)))) - } - - pub fn from_future(future: F) -> Self - where - F: Future> + Send + 'static, - TError: Error, - { - Self(Some(Inner::Future(Box::pin( - future.map_err(ResolverError::new), - )))) - } - - pub fn from_stream(stream: S) -> Self - where - S: Stream> + Send + 'static, - TError: Error, - { - Self(Some(Inner::Stream(Box::pin( - stream.map_err(ResolverError::new), - )))) - } -} - -impl Stream for ProcedureStream { - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.0.as_mut() { - Some(Inner::Value(_)) => { - let Inner::Value(value) = self.0.take().expect("checked above") else { - panic!("checked above"); - }; - Poll::Ready(Some(value)) - } - Some(Inner::Future(future)) => future.as_mut().poll(cx).map(|v| { - self.0 = None; - Some(v) - }), - Some(Inner::Stream(stream)) => stream.as_mut().poll_next(cx), - None => Poll::Ready(None), - } - } -} diff --git a/src/rewrite/router.rs b/src/rewrite/router.rs deleted file mode 100644 index a971d4ab..00000000 --- a/src/rewrite/router.rs +++ /dev/null @@ -1,317 +0,0 @@ -use std::{ - borrow::Cow, - collections::{BTreeMap, HashMap}, - fmt, - path::{Path, PathBuf}, - sync::Arc, -}; - -use specta::{DataType, Language, Type, TypeMap}; -use specta_util::TypeCollection; - -use super::{ - procedure::{Procedure, ProcedureKind, UnbuiltProcedure}, - State, -}; - -pub struct Router { - setup: Vec>, - types: TypeCollection, - procedures: BTreeMap, UnbuiltProcedure>, - exports: Vec Result<(), Box>>>, -} - -impl Default for Router { - fn default() -> Self { - Self { - setup: Default::default(), - types: Default::default(), - procedures: Default::default(), - exports: Default::default(), - } - } -} - -impl fmt::Debug for Router { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Router").field(&self.procedures).finish() - } -} - -impl Router { - pub fn new() -> Router { - Self::default() - } - - pub fn procedure( - mut self, - key: impl Into>, - procedure: UnbuiltProcedure, - ) -> Self { - let name = key.into(); - self.procedures.insert(name, procedure); - self - } - - pub fn merge(mut self, prefix: impl Into>, mut other: Self) -> Self { - self.setup.append(&mut other.setup); - - let prefix = prefix.into(); - let prefix = if prefix.is_empty() { - Cow::Borrowed("") - } else { - format!("{prefix}.").into() - }; - - self.procedures.extend( - other - .procedures - .into_iter() - .map(|(k, v)| (format!("{prefix}{k}").into(), v)), - ); - - self - } - - // TODO: Document the order this is run in for `build` - pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { - self.setup.push(Box::new(func)); - self - } - - pub fn ext(mut self, types: TypeCollection) -> Self { - self.types = types; - self - } - - // TODO: Docs - that this is delayed until `Router::build` which means `Language::Error` type has to be erased. - pub fn export_to( - mut self, - language: impl Language + 'static, - path: impl Into, - ) -> Self { - let path = path.into(); - self.exports.push(Box::new(move |types| { - language - .export(types) - .and_then(|result| std::fs::write(path, result).map_err(Into::into)) - .map_err(Into::into) - })); - self - } - - pub fn build(self) -> Result, ()> { - self.build_with_state(State::default()) - } - - pub fn build_with_state(self, mut state: State) -> Result, ()> { - // TODO: Return errors on duplicate procedure names or restricted names - - for setup in self.setup { - setup(&mut state); - } - - let mut type_map = TypeMap::default(); - self.types.collect(&mut type_map); - let procedures: BTreeMap, _> = self - .procedures - .into_iter() - .map(|(key, procedure)| (key.clone(), procedure.build(key, &mut state, &mut type_map))) - .collect(); - - { - struct Procedure { - kind: String, - input: DataType, - result: DataType, - error: DataType, - } - - enum ProcedureOrProcedures { - Procedure(Procedure), - Procedures(HashMap, ProcedureOrProcedures>), - } - - impl Into for Procedure { - fn into(self) -> specta::DataType { - specta::DataType::Struct(specta::internal::construct::r#struct( - "".into(), - None, - vec![], - specta::internal::construct::struct_named( - vec![ - ( - "kind".into(), - specta::internal::construct::field( - false, - false, - None, - Default::default(), - Some(specta::DataType::Literal( - specta::datatype::LiteralType::String(self.kind), - )), - ), - ), - ( - "input".into(), - specta::internal::construct::field( - false, - false, - None, - Default::default(), - Some(self.input), - ), - ), - ( - "result".into(), - specta::internal::construct::field( - false, - false, - None, - Default::default(), - Some(self.result), - ), - ), - ( - "error".into(), - specta::internal::construct::field( - false, - false, - None, - Default::default(), - Some(self.error), - ), - ), - ], - None, - ), - )) - } - } - - impl Into for ProcedureOrProcedures { - fn into(self) -> specta::DataType { - match self { - Self::Procedure(procedure) => procedure.into(), - Self::Procedures(procedures) => { - specta::DataType::Struct(specta::internal::construct::r#struct( - "".into(), - None, - vec![], - specta::internal::construct::struct_named( - procedures - .into_iter() - .map(|(key, value)| { - ( - key, - specta::internal::construct::field( - false, - false, - None, - Default::default(), - Some(value.into()), - ), - ) - }) - .collect(), - None, - ), - )) - } - } - } - } - - let mut types: HashMap, ProcedureOrProcedures> = Default::default(); - - { - for (key, procedure) in &procedures { - let mut procedures_map = &mut types; - - let path = key.split(".").collect::>(); - let Some((key, path)) = path.split_last() else { - panic!("how is this empty"); - }; - - for segment in path { - let ProcedureOrProcedures::Procedures(nested_procedures_map) = - procedures_map - .entry(segment.to_string().into()) - .or_insert(ProcedureOrProcedures::Procedures(Default::default())) - else { - panic!(); - }; - - procedures_map = nested_procedures_map; - } - - procedures_map.insert( - key.to_string().into(), - ProcedureOrProcedures::Procedure(Procedure { - kind: match procedure.kind() { - ProcedureKind::Query => "query", - ProcedureKind::Mutation => "mutation", - ProcedureKind::Subscription => "subscription", - } - .to_string(), - input: procedure.ty().input.clone(), - result: procedure.ty().result.clone(), - error: DataType::Any, - }), - ); - } - } - - #[derive(specta::Type)] - struct Procedures; - - let mut named_type = - ::definition_named_data_type(&mut type_map); - - named_type.inner = ProcedureOrProcedures::Procedures(types).into(); - - type_map.insert(::sid(), named_type); - } - - // TODO: Customise the files header. It should says rspc not Specta! - - for export in self.exports { - export(type_map.clone()).unwrap(); // TODO: Error - } - - Ok(BuiltRouter { - state: Arc::new(state), - types: type_map, - procedures, - }) - } -} - -#[derive(Clone)] -pub struct BuiltRouter { - pub state: Arc, - pub types: TypeMap, - pub procedures: BTreeMap, Procedure>, -} - -impl fmt::Debug for BuiltRouter { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("BuiltRouter") - // TODO - .finish() - } -} - -impl BuiltRouter { - pub fn export(&self, language: L) -> Result { - language.export(self.types.clone()) - } - - pub fn export_to( - &self, - language: L, - path: impl AsRef, - ) -> Result<(), L::Error> { - std::fs::write(path, self.export(language)?).map_err(Into::into) - } -} diff --git a/src/rewrite/state.rs b/src/rewrite/state.rs deleted file mode 100644 index 52f0602a..00000000 --- a/src/rewrite/state.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{ - any::{Any, TypeId}, - collections::HashMap, - fmt, - hash::{BuildHasherDefault, Hasher}, -}; - -/// A hasher for `TypeId`s that takes advantage of its known characteristics. -/// -/// Author of `anymap` crate has done research on the topic: -/// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599 -#[derive(Debug, Default)] -struct NoOpHasher(u64); - -impl Hasher for NoOpHasher { - fn write(&mut self, _bytes: &[u8]) { - unimplemented!("This NoOpHasher can only handle u64s") - } - - fn write_u64(&mut self, i: u64) { - self.0 = i; - } - - fn finish(&self) -> u64 { - self.0 - } -} - -pub struct State( - HashMap, BuildHasherDefault>, -); - -impl fmt::Debug for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("State") - // .field("state", &self.state) // TODO - .finish() - } -} - -impl Default for State { - fn default() -> Self { - Self(Default::default()) - } -} - -impl State { - pub fn get(&self) -> Option<&T> { - self.0.get(&TypeId::of::()).map(|v| { - v.downcast_ref::() - .expect("unreachable: TypeId matches but downcast failed") - }) - } - - pub fn get_mut(&self) -> Option<&T> { - self.0.get(&TypeId::of::()).map(|v| { - v.downcast_ref::() - .expect("unreachable: TypeId matches but downcast failed") - }) - } - - pub fn get_or_init(&mut self, init: impl FnOnce() -> T) -> &T { - self.0 - .entry(TypeId::of::()) - .or_insert_with(|| Box::new(init())) - .downcast_ref::() - .expect("unreachable: TypeId matches but downcast failed") - } - - pub fn get_mut_or_init( - &mut self, - init: impl FnOnce() -> T, - ) -> &mut T { - self.0 - .entry(TypeId::of::()) - .or_insert_with(|| Box::new(init())) - .downcast_mut::() - .expect("unreachable: TypeId matches but downcast failed") - } - - pub fn contains_key(&self) -> bool { - self.0.contains_key(&TypeId::of::()) - } - - pub fn insert(&mut self, t: T) { - self.0.insert(TypeId::of::(), Box::new(t)); - } - - pub fn remove(&mut self) -> Option { - self.0.remove(&TypeId::of::()).map(|v| { - *v.downcast::() - .expect("unreachable: TypeId matches but downcast failed") - }) - } -} diff --git a/src/rewrite/stream.rs b/src/rewrite/stream.rs deleted file mode 100644 index 7bf42d8e..00000000 --- a/src/rewrite/stream.rs +++ /dev/null @@ -1,35 +0,0 @@ -/// Return a [`Stream`](futures::Stream) of values from a [`Procedure::query`](procedure::ProcedureBuilder::query) or [`Procedure::mutation`](procedure::ProcedureBuilder::mutation). -/// -/// ## Why not a subscription? -/// -/// A [`subscription`](procedure::ProcedureBuilder::subscription) must return a [`Stream`](futures::Stream) so it would be fair to question when you would use this. -/// -/// A [`query`](procedure::ProcedureBuilder::query) or [`mutation`](procedure::ProcedureBuilder::mutation) produce a single result where a subscription produces many discrete values. -/// -/// Using [`rspc::Stream`](Self) within a query or mutation will result in your procedure returning a collection (Eg. `Vec`) of [`Stream::Item`](futures::Stream) on the frontend. -/// -/// This means it would be well suited for streaming the result of a computation or database query while a subscription would be well suited for a chat room. -/// -/// ## Usage -/// **WARNING**: This example shows the low-level procedure API. You should refer to [`Rspc`](crate::Rspc) for the high-level API. -/// ```rust -/// use futures::stream::once; -/// -/// ::builder().query(|_, _: ()| async move { rspc::Stream(once(async move { 42 })) }); -/// ``` -/// -pub struct Stream(pub S); - -// WARNING: We can not add an implementation for `Debug` without breaking `rspc_tracing` - -impl Default for Stream { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Clone for Stream { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 00000000..2d57460e --- /dev/null +++ b/src/router.rs @@ -0,0 +1,267 @@ +use std::{borrow::Cow, collections::BTreeMap, fmt}; + +use specta::TypeMap; +use specta_util::TypeCollection; + +use rspc_core::Procedure; + +use crate::State; + +/// TODO: Examples exporting types and with `rspc_axum` +pub struct Router2 { + setup: Vec>, + types: TypeCollection, + procedures: BTreeMap>, // TODO: This must be a thing that holds a setup function, type and `Procedure`! +} + +impl Default for Router2 { + fn default() -> Self { + todo!() + } +} + +impl fmt::Debug for Router2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // f.debug_tuple("Router").field(&self.procedures).finish() + todo!(); + } +} + +impl Router2 { + pub fn new() -> Self { + Self::default() + } + + // TODO: Enforce unique across all methods (query, subscription, etc). Eg. `insert` should yield error if key already exists. + // pub fn procedure( + // mut self, + // key: impl Into>, + // procedure: UnbuiltProcedure, + // ) -> Self { + // let name = key.into(); + // self.procedures.insert(name, procedure); + // self + // } + + pub fn merge(mut self, prefix: impl Into>, mut other: Self) -> Self { + self.setup.append(&mut other.setup); + + let prefix = prefix.into(); + let prefix = if prefix.is_empty() { + Cow::Borrowed("") + } else { + format!("{prefix}.").into() + }; + + self.procedures.extend( + other + .procedures + .into_iter() + .map(|(k, v)| (format!("{prefix}{k}").into(), v)), + ); + + self + } + + // TODO: Document the order this is run in for `build` + // pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { + // self.setup.push(Box::new(func)); + // self + // } + + pub fn build( + self, + ) -> Result< + ( + impl Iterator)>, + TypeCollection, + ), + (), + > { + let mut state = (); + for setup in self.setup { + setup(&mut state); + } + + // let mut type_map = TypeMap::default(); + // self.types.collect(&mut type_map); + // let procedures: BTreeMap, _> = self + // .procedures + // .into_iter() + // .map(|(key, procedure)| (key.clone(), procedure.build(key, &mut state, &mut type_map))) + // .collect(); + + // { + // struct Procedure { + // kind: String, + // input: DataType, + // result: DataType, + // error: DataType, + // } + + // enum ProcedureOrProcedures { + // Procedure(Procedure), + // Procedures(HashMap, ProcedureOrProcedures>), + // } + + // impl Into for Procedure { + // fn into(self) -> specta::DataType { + // specta::DataType::Struct(specta::internal::construct::r#struct( + // "".into(), + // None, + // vec![], + // specta::internal::construct::struct_named( + // vec![ + // ( + // "kind".into(), + // specta::internal::construct::field( + // false, + // false, + // None, + // Default::default(), + // Some(specta::DataType::Literal( + // specta::datatype::LiteralType::String(self.kind), + // )), + // ), + // ), + // ( + // "input".into(), + // specta::internal::construct::field( + // false, + // false, + // None, + // Default::default(), + // Some(self.input), + // ), + // ), + // ( + // "result".into(), + // specta::internal::construct::field( + // false, + // false, + // None, + // Default::default(), + // Some(self.result), + // ), + // ), + // ( + // "error".into(), + // specta::internal::construct::field( + // false, + // false, + // None, + // Default::default(), + // Some(self.error), + // ), + // ), + // ], + // None, + // ), + // )) + // } + // } + + // impl Into for ProcedureOrProcedures { + // fn into(self) -> specta::DataType { + // match self { + // Self::Procedure(procedure) => procedure.into(), + // Self::Procedures(procedures) => { + // specta::DataType::Struct(specta::internal::construct::r#struct( + // "".into(), + // None, + // vec![], + // specta::internal::construct::struct_named( + // procedures + // .into_iter() + // .map(|(key, value)| { + // ( + // key, + // specta::internal::construct::field( + // false, + // false, + // None, + // Default::default(), + // Some(value.into()), + // ), + // ) + // }) + // .collect(), + // None, + // ), + // )) + // } + // } + // } + // } + + // let mut types: HashMap, ProcedureOrProcedures> = Default::default(); + + // { + // for (key, procedure) in &procedures { + // let mut procedures_map = &mut types; + + // let path = key.split(".").collect::>(); + // let Some((key, path)) = path.split_last() else { + // panic!("how is this empty"); + // }; + + // for segment in path { + // let ProcedureOrProcedures::Procedures(nested_procedures_map) = + // procedures_map + // .entry(segment.to_string().into()) + // .or_insert(ProcedureOrProcedures::Procedures(Default::default())) + // else { + // panic!(); + // }; + + // procedures_map = nested_procedures_map; + // } + + // procedures_map.insert( + // key.to_string().into(), + // ProcedureOrProcedures::Procedure(Procedure { + // kind: match procedure.kind() { + // ProcedureKind::Query => "query", + // ProcedureKind::Mutation => "mutation", + // ProcedureKind::Subscription => "subscription", + // } + // .to_string(), + // input: procedure.ty().input.clone(), + // result: procedure.ty().result.clone(), + // error: DataType::Any, + // }), + // ); + // } + // } + + // #[derive(specta::Type)] + // struct Procedures; + + // let mut named_type = + // ::definition_named_data_type(&mut type_map); + + // named_type.inner = ProcedureOrProcedures::Procedures(types).into(); + + // type_map.insert(::sid(), named_type); + // } + + todo!(); + + Ok(( + BTreeMap::>::new().into_iter(), + TypeCollection::default(), + )) + } +} + +// TODO: `Iterator` or `IntoIterator`? + +impl TryFrom> for Router2 { + type Error = (); + + fn try_from(value: crate::legacy::Router) -> Result { + // TODO: Enforce unique across all methods (query, subscription, etc) + + todo!() + } +} From 27afad0ac20cf1d47fc6ca03512ee1cd98f72771 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 4 Dec 2024 11:13:06 +0800 Subject: [PATCH 02/67] change 'merge' to 'nest' and key procedures by vec of strings --- crates/legacy/src/lib.rs | 2 -- src/router.rs | 22 ++++++++-------------- 2 files changed, 8 insertions(+), 16 deletions(-) delete mode 100644 crates/legacy/src/lib.rs diff --git a/crates/legacy/src/lib.rs b/crates/legacy/src/lib.rs deleted file mode 100644 index 6a3cf4b3..00000000 --- a/crates/legacy/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! TODO -// TODO: Crate icon and stuff diff --git a/src/router.rs b/src/router.rs index 2d57460e..1c33e614 100644 --- a/src/router.rs +++ b/src/router.rs @@ -11,7 +11,7 @@ use crate::State; pub struct Router2 { setup: Vec>, types: TypeCollection, - procedures: BTreeMap>, // TODO: This must be a thing that holds a setup function, type and `Procedure`! + procedures: BTreeMap>, Procedure>, // TODO: This must be a thing that holds a setup function, type and `Procedure`! } impl Default for Router2 { @@ -43,22 +43,16 @@ impl Router2 { // self // } - pub fn merge(mut self, prefix: impl Into>, mut other: Self) -> Self { + pub fn nest(mut self, prefix: impl Into>, mut other: Self) -> Self { self.setup.append(&mut other.setup); let prefix = prefix.into(); - let prefix = if prefix.is_empty() { - Cow::Borrowed("") - } else { - format!("{prefix}.").into() - }; - - self.procedures.extend( - other - .procedures - .into_iter() - .map(|(k, v)| (format!("{prefix}{k}").into(), v)), - ); + + self.procedures + .extend(other.procedures.into_iter().map(|(mut k, v)| { + k.push(prefix.clone()); + (k, v) + })); self } From 4016c8e432c29cb9377dbf85fc928b435694cdb0 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 4 Dec 2024 11:46:47 +0800 Subject: [PATCH 03/67] IntoIter + Debug --- src/router.rs | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/router.rs b/src/router.rs index 1c33e614..875479b6 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,17 +1,16 @@ use std::{borrow::Cow, collections::BTreeMap, fmt}; -use specta::TypeMap; use specta_util::TypeCollection; use rspc_core::Procedure; -use crate::State; +use crate::{internal::ProcedureKind, Procedure2, State}; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { setup: Vec>, types: TypeCollection, - procedures: BTreeMap>, Procedure>, // TODO: This must be a thing that holds a setup function, type and `Procedure`! + procedures: BTreeMap>, Procedure2>, // TODO: This must be a thing that holds a setup function, type and `Procedure`! } impl Default for Router2 { @@ -20,13 +19,6 @@ impl Default for Router2 { } } -impl fmt::Debug for Router2 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // f.debug_tuple("Router").field(&self.procedures).finish() - todo!(); - } -} - impl Router2 { pub fn new() -> Self { Self::default() @@ -248,7 +240,26 @@ impl Router2 { } } -// TODO: `Iterator` or `IntoIterator`? +impl fmt::Debug for Router2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let procedure_keys = |kind: ProcedureKind| { + self.procedures + .iter() + .filter(move |(_, p)| p.kind() == kind) + .map(|(k, _)| k.join("::")) + .collect::>() + }; + + f.debug_struct("Router") + .field("queries", &procedure_keys(ProcedureKind::Query)) + .field("mutations", &procedure_keys(ProcedureKind::Mutation)) + .field( + "subscriptions", + &procedure_keys(ProcedureKind::Subscription), + ) + .finish() + } +} impl TryFrom> for Router2 { type Error = (); @@ -259,3 +270,12 @@ impl TryFrom> for Router2 { todo!() } } + +impl<'a, TCtx> IntoIterator for &'a Router2 { + type Item = (&'a Vec>, &'a Procedure2); + type IntoIter = std::collections::btree_map::Iter<'a, Vec>, Procedure2>; + + fn into_iter(self) -> Self::IntoIter { + self.procedures.iter() + } +} From 2286acf009a549fb33943526b9abb0e71a0abcca Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 4 Dec 2024 13:46:23 +0800 Subject: [PATCH 04/67] legacy to modern router interop --- .gitignore | 1 + Cargo.toml | 9 +++ crates/axum/Cargo.toml | 3 +- crates/axum/src/lib.rs | 13 ++++ crates/legacy/src/lib.rs | 2 - examples/axum/Cargo.toml | 5 ++ examples/axum/src/main.rs | 106 ++++++++++++++----------- src/interop.rs | 147 +++++++++++++++++++++++++++++++++++ src/legacy/router.rs | 2 +- src/legacy/router_builder.rs | 22 +++--- src/lib.rs | 1 + src/router.rs | 40 +++++----- 12 files changed, 271 insertions(+), 80 deletions(-) delete mode 100644 crates/legacy/src/lib.rs create mode 100644 src/interop.rs diff --git a/.gitignore b/.gitignore index d3fa8e50..7378ac99 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ Cargo.lock # Typescript bindings exported by the examples bindings.ts +bindings-legacy.ts # Node node_modules diff --git a/Cargo.toml b/Cargo.toml index 47832161..4b7263b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,12 @@ typeid = "1.0.2" [workspace] members = ["./crates/*", "./examples", "./examples/axum", "crates/core"] + +[patch.crates-io] +specta = { git = "https://github.com/specta-rs/specta", rev = "a7bbe7b579c448ff4bcefb0b44489d48d8fd37ee" } +specta-util = { git = "https://github.com/specta-rs/specta", rev = "a7bbe7b579c448ff4bcefb0b44489d48d8fd37ee" } +specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "a7bbe7b579c448ff4bcefb0b44489d48d8fd37ee" } + +# specta = { path = "/Users/oscar/Desktop/specta/specta" } +# specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } +# specta-typescript = { path = "/Users/oscar/Desktop/specta/specta-typescript" } diff --git a/crates/axum/Cargo.toml b/crates/axum/Cargo.toml index ab4013e7..44d6f160 100644 --- a/crates/axum/Cargo.toml +++ b/crates/axum/Cargo.toml @@ -20,11 +20,12 @@ default = [] ws = ["dep:tokio", "axum/ws"] [dependencies] -rspc = { version = "0.3.1", path = "../.." } +rspc-core = { version = "0.0.0", path = "../core" } axum = "0.7.9" serde_json = "1.0.133" # TODO: Drop these +rspc = { version = "0.3.1", path = "../.." } form_urlencoded = "1.2.1" # TODO: use Axum's built in extractor futures = "0.3.31" # TODO: No blocking execution, etc tokio = { version = "1.41.1", optional = true } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. diff --git a/crates/axum/src/lib.rs b/crates/axum/src/lib.rs index 9eb3c126..e8a408e8 100644 --- a/crates/axum/src/lib.rs +++ b/crates/axum/src/lib.rs @@ -87,6 +87,19 @@ where ) } +pub fn endpoint2( + router: impl Iterator)>, + ctx_fn: TCtxFn, +) -> Router +where + S: Clone + Send + Sync + 'static, + TCtx: Send + Sync + 'static, + TCtxFnMarker: Send + Sync + 'static, + TCtxFn: TCtxFunc, +{ + todo!(); +} + async fn handle_http( ctx_fn: TCtxFn, kind: ProcedureKind, diff --git a/crates/legacy/src/lib.rs b/crates/legacy/src/lib.rs deleted file mode 100644 index 6a3cf4b3..00000000 --- a/crates/legacy/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! TODO -// TODO: Crate icon and stuff diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 879f28f5..5d52e96b 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -13,3 +13,8 @@ axum = { version = "0.7.9", features = ["ws"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } +specta-typescript = "0.0.7" +serde = { version = "1.0.215", features = ["derive"] } +specta = { version = "=2.0.0-rc.20", features = [ + "derive", +] } # TODO: Drop all features diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index f6dc1c8c..3c65246b 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -2,60 +2,78 @@ use std::{path::PathBuf, time::Duration}; use async_stream::stream; use axum::{http::request::Parts, routing::get}; -use rspc::Config; +use rspc::{Config, Router2}; +use serde::Serialize; +use specta::Type; +use specta_typescript::Typescript; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; struct Ctx {} +#[derive(Serialize, Type)] +pub struct MyCustomType(String); + #[tokio::main] async fn main() { - let router = - rspc::Router::::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), - )) - .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) - .query("echo", |t| t(|_, v: String| v)) - .query("error", |t| { - t(|_, _: ()| { - Err(rspc::Error::new( - rspc::ErrorCode::InternalServerError, - "Something went wrong".into(), - )) as Result - }) + let inner = rspc::Router::::new().query("hello", |t| t(|_, _: ()| "Hello World!")); + + let router = rspc::Router::::new() + .config(Config::new().export_ts_bindings( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings-legacy.ts"), + )) + .merge("nested", inner) + .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("echo", |t| t(|_, v: String| v)) + .query("error", |t| { + t(|_, _: ()| { + Err(rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Something went wrong".into(), + )) as Result }) - .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) - .mutation("sendMsg", |t| { - t(|_, v: String| { - println!("Client said '{}'", v); - v - }) + }) + .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) + .mutation("sendMsg", |t| { + t(|_, v: String| { + println!("Client said '{}'", v); + v }) - .subscription("pings", |t| { - t(|_ctx, _args: ()| { - stream! { - println!("Client subscribed to 'pings'"); - for i in 0..5 { - println!("Sending ping {}", i); - yield "ping".to_string(); - sleep(Duration::from_secs(1)).await; - } + }) + .mutation("anotherOne", |t| t(|_, v: String| Ok(MyCustomType(v)))) + .subscription("pings", |t| { + t(|_ctx, _args: ()| { + stream! { + println!("Client subscribed to 'pings'"); + for i in 0..5 { + println!("Sending ping {}", i); + yield "ping".to_string(); + sleep(Duration::from_secs(1)).await; } - }) + } }) - // TODO: Results being returned from subscriptions - // .subscription("errorPings", |t| t(|_ctx, _args: ()| { - // stream! { - // for i in 0..5 { - // yield Ok("ping".to_string()); - // sleep(Duration::from_secs(1)).await; - // } - // yield Err(rspc::Error::new(ErrorCode::InternalServerError, "Something went wrong".into())); - // } - // })) - .build() - .arced(); // This function is a shortcut to wrap the router in an `Arc`. + }) + // TODO: Results being returned from subscriptions + // .subscription("errorPings", |t| t(|_ctx, _args: ()| { + // stream! { + // for i in 0..5 { + // yield Ok("ping".to_string()); + // sleep(Duration::from_secs(1)).await; + // } + // yield Err(rspc::Error::new(ErrorCode::InternalServerError, "Something went wrong".into())); + // } + // })) + .build(); + + let (routes, types) = Router2::from(router).build().unwrap(); + + types + .export_to( + Typescript::default(), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + ) + .unwrap(); + return; // TODO // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! let cors = CorsLayer::new() @@ -67,7 +85,7 @@ async fn main() { .route("/", get(|| async { "Hello 'rspc'!" })) .nest( "/rspc", - rspc_axum::endpoint(router.clone(), |parts: Parts| { + rspc_axum::endpoint2(routes, |parts: Parts| { println!("Client requested operation '{}'", parts.uri.path()); Ctx {} }), diff --git a/src/interop.rs b/src/interop.rs new file mode 100644 index 00000000..0fd3a006 --- /dev/null +++ b/src/interop.rs @@ -0,0 +1,147 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use futures::{stream, FutureExt, StreamExt, TryStreamExt}; +use rspc_core::{ProcedureStream, ResolverError}; +use serde_json::Value; +use specta::{datatype::DataType, NamedType, SpectaID, Type}; + +use crate::{ + internal::{jsonrpc::JsonRPCError, Layer, ProcedureKind, RequestContext, ValueOrStream}, + Router, Router2, +}; + +pub fn legacy_to_modern(mut router: Router) -> Router2 { + let mut r = Router2::new(); + + let (types, layers): (Vec<_>, Vec<_>) = router + .queries + .store + .into_iter() + .map(|v| (ProcedureKind::Query, v)) + .chain( + router + .mutations + .store + .into_iter() + .map(|v| (ProcedureKind::Mutation, v)), + ) + .chain( + router + .subscriptions + .store + .into_iter() + .map(|v| (ProcedureKind::Subscription, v)), + ) + .map(|(ty, (key, p))| { + ( + ( + key.clone().into(), + literal_object( + "".into(), + None, + vec![ + ("input".into(), p.ty.arg_ty), + ("result".into(), p.ty.result_ty), + ] + .into_iter(), + ), + ), + (key, ty, p.exec), + ) + }) + .unzip(); + + for (key, ty, exec) in layers { + if r.interop_procedures() + .insert(key.clone(), layer_to_procedure(key.clone(), ty, exec)) + .is_some() + { + panic!("Attempted to mount '{key}' multiple times. Note: rspc no longer supports different operations (query/mutation/subscription) with overlapping names.") + } + } + + { + #[derive(Type)] + struct Procedures; + + let s = literal_object( + "Procedures".into(), + Some(Procedures::sid()), + types.into_iter(), + ); + let mut ndt = Procedures::definition_named_data_type(&mut r.interop_types()); + ndt.inner = s.into(); + r.interop_types().insert(Procedures::sid(), ndt); + } + + r.interop_types().extend(&mut router.type_map); + + r +} + +// TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` +fn literal_object( + name: Cow<'static, str>, + sid: Option, + fields: impl Iterator, DataType)>, +) -> DataType { + specta::internal::construct::r#struct( + name, + sid, + Default::default(), + specta::internal::construct::struct_named( + fields + .into_iter() + .map(|(name, ty)| { + ( + name.into(), + specta::internal::construct::field(false, false, None, "".into(), Some(ty)), + ) + }) + .collect(), + None, + ), + ) + .into() +} + +fn layer_to_procedure( + path: String, + kind: ProcedureKind, + value: Box>, +) -> rspc_core::Procedure { + rspc_core::Procedure::new(move |ctx, input| { + let input: Value = input.deserialize().unwrap(); // TODO: Error handling + let result = value + .call( + ctx, + input, + RequestContext { + kind: kind.clone(), + path: path.clone(), + }, + ) + .unwrap(); // TODO: Error handling + + ProcedureStream::from_stream( + async move { + let result = result.into_value_or_stream().await.unwrap(); // TODO: Error handling + + match result { + ValueOrStream::Value(value) => stream::once(async { Ok(value) }).boxed(), + ValueOrStream::Stream(s) => s + .map_err(|err| { + let err = JsonRPCError::from(err); + ResolverError::new( + err.code.try_into().unwrap(), + err, + None::, + ) + }) + .boxed(), + } + } + .flatten_stream(), + ) + }) +} diff --git a/src/legacy/router.rs b/src/legacy/router.rs index cebf50a7..5a61adb8 100644 --- a/src/legacy/router.rs +++ b/src/legacy/router.rs @@ -164,7 +164,7 @@ export type Procedures = {{ // Generate type exports (non-Procedures) for export in self .type_map - .iter() + .into_iter() .map(|(_, ty)| export_named_datatype(&config, ty, &self.type_map).unwrap()) { writeln!(file, "\n{}", export)?; diff --git a/src/legacy/router_builder.rs b/src/legacy/router_builder.rs index 06b52068..8f3319e0 100644 --- a/src/legacy/router_builder.rs +++ b/src/legacy/router_builder.rs @@ -56,7 +56,7 @@ where queries: ProcedureStore::new("query"), mutations: ProcedureStore::new("mutation"), subscriptions: ProcedureStore::new("subscription"), - type_map: TypeMap::default(), + type_map: Default::default(), phantom: PhantomData, } } @@ -93,7 +93,7 @@ where queries, mutations, subscriptions, - type_map: typ_store, + type_map, .. } = self; @@ -108,7 +108,7 @@ where queries, mutations, subscriptions, - type_map: typ_store, + type_map, phantom: PhantomData, } } @@ -252,9 +252,7 @@ where ); } - for (name, typ) in router.type_map.iter() { - self.type_map.insert(name, typ.clone()); - } + self.type_map.extend(&router.type_map); self } @@ -292,7 +290,7 @@ where mut queries, mut mutations, mut subscriptions, - type_map: mut typ_store, + mut type_map, .. } = self; @@ -320,9 +318,7 @@ where ); } - for (name, typ) in router.type_map.iter() { - typ_store.insert(name, typ.clone()); - } + type_map.extend(&router.type_map); RouterBuilder { config, @@ -334,7 +330,7 @@ where queries, mutations, subscriptions, - type_map: typ_store, + type_map, phantom: PhantomData, } } @@ -345,7 +341,7 @@ where queries, mutations, subscriptions, - type_map: typ_store, + type_map, .. } = self; @@ -355,7 +351,7 @@ where queries, mutations, subscriptions, - type_map: typ_store, + type_map, phantom: PhantomData, }; diff --git a/src/lib.rs b/src/lib.rs index bf815f3e..3d2d5f0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] +pub(crate) mod interop; mod procedure; mod procedure_kind; mod router; diff --git a/src/router.rs b/src/router.rs index 2d57460e..398cb1ab 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,7 +1,6 @@ use std::{borrow::Cow, collections::BTreeMap, fmt}; use specta::TypeMap; -use specta_util::TypeCollection; use rspc_core::Procedure; @@ -10,13 +9,17 @@ use crate::State; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { setup: Vec>, - types: TypeCollection, + types: TypeMap, procedures: BTreeMap>, // TODO: This must be a thing that holds a setup function, type and `Procedure`! } impl Default for Router2 { fn default() -> Self { - todo!() + Self { + setup: Default::default(), + types: Default::default(), + procedures: Default::default(), + } } } @@ -69,15 +72,7 @@ impl Router2 { // self // } - pub fn build( - self, - ) -> Result< - ( - impl Iterator)>, - TypeCollection, - ), - (), - > { + pub fn build(self) -> Result<(impl Iterator)>, TypeMap), ()> { let mut state = (); for setup in self.setup { setup(&mut state); @@ -245,23 +240,30 @@ impl Router2 { // type_map.insert(::sid(), named_type); // } - todo!(); + // todo!(); Ok(( BTreeMap::>::new().into_iter(), - TypeCollection::default(), + self.types, )) } } // TODO: `Iterator` or `IntoIterator`? -impl TryFrom> for Router2 { - type Error = (); +impl From> for Router2 { + fn from(router: crate::legacy::Router) -> Self { + crate::interop::legacy_to_modern(router) + } +} - fn try_from(value: crate::legacy::Router) -> Result { - // TODO: Enforce unique across all methods (query, subscription, etc) +// TODO: Remove this block with the interop system +impl Router2 { + pub(crate) fn interop_procedures(&mut self) -> &mut BTreeMap> { + &mut self.procedures + } - todo!() + pub(crate) fn interop_types(&mut self) -> &mut TypeMap { + &mut self.types } } From c288153c8d2ea263fcaf69c3db1422bfc91f224d Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 4 Dec 2024 14:56:30 +0800 Subject: [PATCH 05/67] `Router::merge` + some cleanup --- crates/axum/src/legacy.rs | 320 ++++++++++++++++++++++++++++++++ crates/axum/src/lib.rs | 342 +---------------------------------- crates/axum/src/v2.rs | 24 +++ crates/core/src/procedure.rs | 13 +- examples/axum/src/main.rs | 2 +- src/procedure.rs | 12 ++ src/router.rs | 71 +++++--- 7 files changed, 417 insertions(+), 367 deletions(-) create mode 100644 crates/axum/src/legacy.rs create mode 100644 crates/axum/src/v2.rs diff --git a/crates/axum/src/legacy.rs b/crates/axum/src/legacy.rs new file mode 100644 index 00000000..d46ded57 --- /dev/null +++ b/crates/axum/src/legacy.rs @@ -0,0 +1,320 @@ +use std::{collections::HashMap, sync::Arc}; + +use axum::{ + body::{to_bytes, Body}, + extract::{Request, State}, + http::{request::Parts, Method, Response, StatusCode}, + response::IntoResponse, + routing::{on, MethodFilter}, + RequestExt, Router, +}; +use rspc::internal::{ + jsonrpc::{self, handle_json_rpc, RequestId, Sender, SubscriptionMap}, + ProcedureKind, +}; +use rspc_core::Procedures; +use serde_json::Value; + +use crate::extractors::TCtxFunc; + +pub fn endpoint( + router: Arc>, + ctx_fn: TCtxFn, +) -> Router +where + S: Clone + Send + Sync + 'static, + TCtx: Send + Sync + 'static, + TCtxFnMarker: Send + Sync + 'static, + TCtxFn: TCtxFunc, +{ + Router::::new().route( + "/:id", + on( + MethodFilter::GET.or(MethodFilter::POST), + move |state: State, req: axum::extract::Request| { + let router = router.clone(); + + async move { + match (req.method(), &req.uri().path()[1..]) { + (&Method::GET, "ws") => { + #[cfg(feature = "ws")] + { + let mut req = req; + return req + .extract_parts::() + .await + .unwrap() // TODO: error handling + .on_upgrade(|socket| { + handle_websocket( + ctx_fn, + socket, + req.into_parts().0, + router, + state.0, + ) + }) + .into_response(); + } + + #[cfg(not(feature = "ws"))] + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("[]")) // TODO: Better error message which frontend is actually setup to handle. + .unwrap() + } + (&Method::GET, _) => { + handle_http(ctx_fn, ProcedureKind::Query, req, &router, state.0) + .await + .into_response() + } + (&Method::POST, _) => { + handle_http(ctx_fn, ProcedureKind::Mutation, req, &router, state.0) + .await + .into_response() + } + _ => unreachable!(), + } + } + }, + ), + ) +} + +async fn handle_http( + ctx_fn: TCtxFn, + kind: ProcedureKind, + req: Request, + router: &Arc>, + state: TState, +) -> impl IntoResponse +where + TCtx: Send + Sync + 'static, + TCtxFn: TCtxFunc, + TState: Send + Sync + 'static, +{ + let procedure_name = req.uri().path()[1..].to_string(); // Has to be allocated because `TCtxFn` takes ownership of `req` + let (parts, body) = req.into_parts(); + let input = match parts.method { + Method::GET => parts + .uri + .query() + .map(|query| form_urlencoded::parse(query.as_bytes())) + .and_then(|mut params| params.find(|e| e.0 == "input").map(|e| e.1)) + .map(|v| serde_json::from_str(&v)) + .unwrap_or(Ok(None as Option)), + Method::POST => { + // TODO: Limit body size? + let body = to_bytes(body, usize::MAX).await.unwrap(); // TODO: error handling + (!body.is_empty()) + .then(|| serde_json::from_slice(body.to_vec().as_slice())) + .unwrap_or(Ok(None)) + } + _ => unreachable!(), + }; + + let input = match input { + Ok(input) => input, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!( + "Error passing parameters to operation '{}' with key '{:?}': {}", + kind.to_str(), + procedure_name, + _err + ); + + return Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap(); + } + }; + + #[cfg(feature = "tracing")] + tracing::debug!( + "Executing operation '{}' with key '{}' with params {:?}", + kind.to_str(), + procedure_name, + input + ); + + let mut resp = Sender::Response(None); + + let ctx = match ctx_fn.exec(parts, &state).await { + Ok(ctx) => ctx, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error executing context function: {}", _err); + + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap(); + } + }; + + handle_json_rpc( + ctx, + jsonrpc::Request { + jsonrpc: None, + id: RequestId::Null, + inner: match kind { + ProcedureKind::Query => jsonrpc::RequestInner::Query { + path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? + input, + }, + ProcedureKind::Mutation => jsonrpc::RequestInner::Mutation { + path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? + input, + }, + ProcedureKind::Subscription => { + #[cfg(feature = "tracing")] + tracing::error!("Attempted to execute a subscription operation with HTTP"); + + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap(); + } + }, + }, + router, + &mut resp, + &mut SubscriptionMap::None, + ) + .await; + + match resp { + Sender::Response(Some(resp)) => match serde_json::to_vec(&resp) { + Ok(v) => Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(v)) + .unwrap(), + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error serializing response: {}", _err); + + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap() + } + }, + _ => unreachable!(), + } +} + +#[cfg(feature = "ws")] +async fn handle_websocket( + ctx_fn: TCtxFn, + mut socket: axum::extract::ws::WebSocket, + parts: Parts, + router: Arc>, + state: TState, +) where + TCtx: Send + Sync + 'static, + TCtxFn: TCtxFunc, + TState: Send + Sync, +{ + use axum::extract::ws::Message; + use futures::StreamExt; + use tokio::sync::mpsc; + + #[cfg(feature = "tracing")] + tracing::debug!("Accepting websocket connection"); + + let mut subscriptions = HashMap::new(); + let (mut tx, mut rx) = mpsc::channel::(100); + + loop { + tokio::select! { + biased; // Note: Order is important here + msg = rx.recv() => { + match socket.send(Message::Text(match serde_json::to_string(&msg) { + Ok(v) => v, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error serializing websocket message: {}", _err); + + continue; + } + })).await { + Ok(_) => {} + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error sending websocket message: {}", _err); + + continue; + } + } + } + msg = socket.next() => { + match msg { + Some(Ok(msg)) => { + let res = match msg { + Message::Text(text) => serde_json::from_str::(&text), + Message::Binary(binary) => serde_json::from_slice(&binary), + Message::Ping(_) | Message::Pong(_) | Message::Close(_) => { + continue; + } + }; + + match res.and_then(|v| match v.is_array() { + true => serde_json::from_value::>(v), + false => serde_json::from_value::(v).map(|v| vec![v]), + }) { + Ok(reqs) => { + for request in reqs { + let ctx = match ctx_fn.exec(parts.clone(), &state).await { + Ok(ctx) => { + ctx + }, + Err(_err) => { + + #[cfg(feature = "tracing")] + tracing::error!("Error executing context function: {}", _err); + + continue; + } + }; + + handle_json_rpc(ctx, request, &router, &mut Sender::Channel(&mut tx), + &mut SubscriptionMap::Ref(&mut subscriptions)).await; + } + }, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error parsing websocket message: {}", _err); + + // TODO: Send report of error to frontend + + continue; + } + }; + } + Some(Err(_err)) => { + #[cfg(feature = "tracing")] + tracing::error!("Error in websocket: {}", _err); + + // TODO: Send report of error to frontend + + continue; + }, + None => { + #[cfg(feature = "tracing")] + tracing::debug!("Shutting down websocket connection"); + + // TODO: Send report of error to frontend + + return; + }, + } + } + } + } +} diff --git a/crates/axum/src/lib.rs b/crates/axum/src/lib.rs index a2a6de11..e840c5ef 100644 --- a/crates/axum/src/lib.rs +++ b/crates/axum/src/lib.rs @@ -5,343 +5,9 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] -use std::{collections::HashMap, sync::Arc}; - -use axum::{ - body::{to_bytes, Body}, - extract::{Request, State}, - http::{request::Parts, Method, Response, StatusCode}, - response::IntoResponse, - routing::{on, MethodFilter}, - RequestExt, Router, -}; -use extractors::TCtxFunc; -use rspc::internal::{ - jsonrpc::{self, handle_json_rpc, RequestId, Sender, SubscriptionMap}, - ProcedureKind, -}; -use rspc_core::Procedures; -use serde_json::Value; - mod extractors; +mod legacy; +mod v2; -pub fn endpoint( - router: Arc>, - ctx_fn: TCtxFn, -) -> Router -where - S: Clone + Send + Sync + 'static, - TCtx: Send + Sync + 'static, - TCtxFnMarker: Send + Sync + 'static, - TCtxFn: TCtxFunc, -{ - Router::::new().route( - "/:id", - on( - MethodFilter::GET.or(MethodFilter::POST), - move |state: State, req: axum::extract::Request| { - let router = router.clone(); - - async move { - match (req.method(), &req.uri().path()[1..]) { - (&Method::GET, "ws") => { - #[cfg(feature = "ws")] - { - let mut req = req; - return req - .extract_parts::() - .await - .unwrap() // TODO: error handling - .on_upgrade(|socket| { - handle_websocket( - ctx_fn, - socket, - req.into_parts().0, - router, - state.0, - ) - }) - .into_response(); - } - - #[cfg(not(feature = "ws"))] - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("[]")) // TODO: Better error message which frontend is actually setup to handle. - .unwrap() - } - (&Method::GET, _) => { - handle_http(ctx_fn, ProcedureKind::Query, req, &router, state.0) - .await - .into_response() - } - (&Method::POST, _) => { - handle_http(ctx_fn, ProcedureKind::Mutation, req, &router, state.0) - .await - .into_response() - } - _ => unreachable!(), - } - } - }, - ), - ) -} - -// type ValidRouter = BTreeMap>; // TODO - -pub fn endpoint2( - router: impl Into>, - ctx_fn: TCtxFn, -) -> Router -where - S: Clone + Send + Sync + 'static, - TCtx: Send + Sync + 'static, - TCtxFnMarker: Send + Sync + 'static, - TCtxFn: TCtxFunc, -{ - let router = router.into(); - - // TODO: Flatten keys - - todo!(); -} - -async fn handle_http( - ctx_fn: TCtxFn, - kind: ProcedureKind, - req: Request, - router: &Arc>, - state: TState, -) -> impl IntoResponse -where - TCtx: Send + Sync + 'static, - TCtxFn: TCtxFunc, - TState: Send + Sync + 'static, -{ - let procedure_name = req.uri().path()[1..].to_string(); // Has to be allocated because `TCtxFn` takes ownership of `req` - let (parts, body) = req.into_parts(); - let input = match parts.method { - Method::GET => parts - .uri - .query() - .map(|query| form_urlencoded::parse(query.as_bytes())) - .and_then(|mut params| params.find(|e| e.0 == "input").map(|e| e.1)) - .map(|v| serde_json::from_str(&v)) - .unwrap_or(Ok(None as Option)), - Method::POST => { - // TODO: Limit body size? - let body = to_bytes(body, usize::MAX).await.unwrap(); // TODO: error handling - (!body.is_empty()) - .then(|| serde_json::from_slice(body.to_vec().as_slice())) - .unwrap_or(Ok(None)) - } - _ => unreachable!(), - }; - - let input = match input { - Ok(input) => input, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!( - "Error passing parameters to operation '{}' with key '{:?}': {}", - kind.to_str(), - procedure_name, - _err - ); - - return Response::builder() - .status(StatusCode::NOT_FOUND) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap(); - } - }; - - #[cfg(feature = "tracing")] - tracing::debug!( - "Executing operation '{}' with key '{}' with params {:?}", - kind.to_str(), - procedure_name, - input - ); - - let mut resp = Sender::Response(None); - - let ctx = match ctx_fn.exec(parts, &state).await { - Ok(ctx) => ctx, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing context function: {}", _err); - - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap(); - } - }; - - handle_json_rpc( - ctx, - jsonrpc::Request { - jsonrpc: None, - id: RequestId::Null, - inner: match kind { - ProcedureKind::Query => jsonrpc::RequestInner::Query { - path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? - input, - }, - ProcedureKind::Mutation => jsonrpc::RequestInner::Mutation { - path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? - input, - }, - ProcedureKind::Subscription => { - #[cfg(feature = "tracing")] - tracing::error!("Attempted to execute a subscription operation with HTTP"); - - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap(); - } - }, - }, - router, - &mut resp, - &mut SubscriptionMap::None, - ) - .await; - - match resp { - Sender::Response(Some(resp)) => match serde_json::to_vec(&resp) { - Ok(v) => Response::builder() - .status(StatusCode::OK) - .header("Content-Type", "application/json") - .body(Body::from(v)) - .unwrap(), - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error serializing response: {}", _err); - - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap() - } - }, - _ => unreachable!(), - } -} - -#[cfg(feature = "ws")] -async fn handle_websocket( - ctx_fn: TCtxFn, - mut socket: axum::extract::ws::WebSocket, - parts: Parts, - router: Arc>, - state: TState, -) where - TCtx: Send + Sync + 'static, - TCtxFn: TCtxFunc, - TState: Send + Sync, -{ - use axum::extract::ws::Message; - use futures::StreamExt; - use tokio::sync::mpsc; - - #[cfg(feature = "tracing")] - tracing::debug!("Accepting websocket connection"); - - let mut subscriptions = HashMap::new(); - let (mut tx, mut rx) = mpsc::channel::(100); - - loop { - tokio::select! { - biased; // Note: Order is important here - msg = rx.recv() => { - match socket.send(Message::Text(match serde_json::to_string(&msg) { - Ok(v) => v, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error serializing websocket message: {}", _err); - - continue; - } - })).await { - Ok(_) => {} - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error sending websocket message: {}", _err); - - continue; - } - } - } - msg = socket.next() => { - match msg { - Some(Ok(msg)) => { - let res = match msg { - Message::Text(text) => serde_json::from_str::(&text), - Message::Binary(binary) => serde_json::from_slice(&binary), - Message::Ping(_) | Message::Pong(_) | Message::Close(_) => { - continue; - } - }; - - match res.and_then(|v| match v.is_array() { - true => serde_json::from_value::>(v), - false => serde_json::from_value::(v).map(|v| vec![v]), - }) { - Ok(reqs) => { - for request in reqs { - let ctx = match ctx_fn.exec(parts.clone(), &state).await { - Ok(ctx) => { - ctx - }, - Err(_err) => { - - #[cfg(feature = "tracing")] - tracing::error!("Error executing context function: {}", _err); - - continue; - } - }; - - handle_json_rpc(ctx, request, &router, &mut Sender::Channel(&mut tx), - &mut SubscriptionMap::Ref(&mut subscriptions)).await; - } - }, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error parsing websocket message: {}", _err); - - // TODO: Send report of error to frontend - - continue; - } - }; - } - Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Error in websocket: {}", _err); - - // TODO: Send report of error to frontend - - continue; - }, - None => { - #[cfg(feature = "tracing")] - tracing::debug!("Shutting down websocket connection"); - - // TODO: Send report of error to frontend - - return; - }, - } - } - } - } -} +pub use legacy::endpoint; +pub use v2::endpoint2; diff --git a/crates/axum/src/v2.rs b/crates/axum/src/v2.rs new file mode 100644 index 00000000..7c30a2d4 --- /dev/null +++ b/crates/axum/src/v2.rs @@ -0,0 +1,24 @@ +use axum::Router; +use rspc_core::Procedures; + +use crate::extractors::TCtxFunc; + +pub fn endpoint2( + router: impl Into>, + ctx_fn: TCtxFn, +) -> Router +where + S: Clone + Send + Sync + 'static, + TCtx: Send + Sync + 'static, + TCtxFnMarker: Send + Sync + 'static, + TCtxFn: TCtxFunc, +{ + let flattened = router + .into() + .into_iter() + .map(|(key, value)| (key.join("."), value)); + + // TODO: Flatten keys + + todo!(); +} diff --git a/crates/core/src/procedure.rs b/crates/core/src/procedure.rs index 04f088ca..e5f12cc9 100644 --- a/crates/core/src/procedure.rs +++ b/crates/core/src/procedure.rs @@ -1,6 +1,7 @@ use std::{ any::{type_name, Any}, fmt, + sync::Arc, }; use serde::Deserializer; @@ -11,13 +12,13 @@ use crate::{DynInput, ProcedureStream}; /// /// TODO: Show constructing and executing procedure. pub struct Procedure { - handler: Box ProcedureStream>, + handler: Arc ProcedureStream>, } impl Procedure { pub fn new(handler: impl Fn(TCtx, DynInput) -> ProcedureStream + 'static) -> Self { Self { - handler: Box::new(handler), + handler: Arc::new(handler), } } @@ -48,6 +49,14 @@ impl Procedure { } } +impl Clone for Procedure { + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + } + } +} + impl fmt::Debug for Procedure { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!(); diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 942d060b..411a7c4f 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -88,7 +88,7 @@ async fn main() { .route("/", get(|| async { "Hello 'rspc'!" })) .nest( "/rspc", - rspc_axum::endpoint2(routes, |parts: Parts| { + rspc_axum::endpoint2(routes.clone(), |parts: Parts| { println!("Client requested operation '{}'", parts.uri.path()); Ctx {} }), diff --git a/src/procedure.rs b/src/procedure.rs index 1acbb5f3..e4f6f547 100644 --- a/src/procedure.rs +++ b/src/procedure.rs @@ -74,3 +74,15 @@ impl Procedure2 { // } // } } + +impl Clone for Procedure2 { + fn clone(&self) -> Self { + Self { + kind: self.kind, + input: self.input.clone(), + result: self.result.clone(), + error: self.error.clone(), + inner: self.inner.clone(), + } + } +} diff --git a/src/router.rs b/src/router.rs index 6e928353..0f1790ba 100644 --- a/src/router.rs +++ b/src/router.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::BTreeMap, fmt}; use specta::{datatype::DataType, NamedType, SpectaID, Type, TypeMap}; -use rspc_core::{Procedure, Procedures}; +use rspc_core::Procedures; use crate::{internal::ProcedureKind, Procedure2, State}; @@ -39,6 +39,12 @@ impl Router2 { // self // } + // TODO: Document the order this is run in for `build` + // pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { + // self.setup.push(Box::new(func)); + // self + // } + pub fn nest(mut self, prefix: impl Into>, mut other: Self) -> Self { self.setup.append(&mut other.setup); @@ -53,13 +59,15 @@ impl Router2 { self } - // TODO: Document the order this is run in for `build` - // pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { - // self.setup.push(Box::new(func)); - // self - // } + pub fn merge(mut self, mut other: Self) -> Self { + self.setup.append(&mut other.setup); + self.procedures.extend(other.procedures.into_iter()); + self + } - pub fn build(mut self) -> Result<(impl Into>, TypeMap), ()> { + pub fn build( + mut self, + ) -> Result<(impl Into> + Clone + fmt::Debug, TypeMap), ()> { let mut state = (); for setup in self.setup { setup(&mut state); @@ -91,6 +99,16 @@ impl Router2 { self.0 } } + impl Clone for Impl { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + impl fmt::Debug for Impl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } + } Ok((Impl::(procedures), self.types)) } @@ -117,25 +135,6 @@ impl fmt::Debug for Router2 { } } -impl From> for Router2 { - fn from(router: crate::legacy::Router) -> Self { - crate::interop::legacy_to_modern(router) - } -} - -// TODO: Remove this block with the interop system -impl Router2 { - pub(crate) fn interop_procedures( - &mut self, - ) -> &mut BTreeMap>, Procedure2> { - &mut self.procedures - } - - pub(crate) fn interop_types(&mut self) -> &mut TypeMap { - &mut self.types - } -} - impl<'a, TCtx> IntoIterator for &'a Router2 { type Item = (&'a Vec>, &'a Procedure2); type IntoIter = std::collections::btree_map::Iter<'a, Vec>, Procedure2>; @@ -200,3 +199,23 @@ fn construct_bindings_type( ) } } + +// TODO: Remove this block with the interop system +impl From> for Router2 { + fn from(router: crate::legacy::Router) -> Self { + crate::interop::legacy_to_modern(router) + } +} + +// TODO: Remove this block with the interop system +impl Router2 { + pub(crate) fn interop_procedures( + &mut self, + ) -> &mut BTreeMap>, Procedure2> { + &mut self.procedures + } + + pub(crate) fn interop_types(&mut self) -> &mut TypeMap { + &mut self.types + } +} From 62da9bd63f2babea1b09f21f9f953e1a091d0355 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 4 Dec 2024 16:06:24 +0800 Subject: [PATCH 06/67] d /Users/oscar/exile/specta-website-solidbased; clear; lazygit --- src/procedure.rs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/procedure.rs b/src/procedure.rs index e4f6f547..36bf1c53 100644 --- a/src/procedure.rs +++ b/src/procedure.rs @@ -1,13 +1,13 @@ use specta::datatype::DataType; -use crate::internal::ProcedureKind; +use crate::{internal::ProcedureKind, State}; /// Represents a single operations on the server that can be executed. /// /// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. /// pub struct Procedure2 { - // pub(crate) setup + pub(crate) setup: Vec>, pub(crate) kind: ProcedureKind, pub(crate) input: DataType, pub(crate) result: DataType, @@ -15,16 +15,14 @@ pub struct Procedure2 { pub(crate) inner: rspc_core::Procedure, } -// TODO: `Clone`, `Debug`, `PartialEq`, `Eq`, `Hash` - -// TODO: `Type` which should be like `Record` +// TODO: `Debug`, `PartialEq`, `Eq`, `Hash` impl Procedure2 { // TODO: `fn builder` - pub fn kind(&self) -> ProcedureKind { - self.kind - } + // pub fn kind(&self) -> ProcedureKind { + // self.kind + // } // TODO: Expose all fields @@ -74,15 +72,3 @@ impl Procedure2 { // } // } } - -impl Clone for Procedure2 { - fn clone(&self) -> Self { - Self { - kind: self.kind, - input: self.input.clone(), - result: self.result.clone(), - error: self.error.clone(), - inner: self.inner.clone(), - } - } -} From aa9fe2920748a6d6b2bf29c8520045ae821b80e9 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 4 Dec 2024 18:24:37 +0800 Subject: [PATCH 07/67] `rspc_axum` backend for `rspc_core` + `Router2` include the legacy bindings in it --- crates/axum/Cargo.toml | 6 + crates/axum/src/jsonrpc.rs | 61 +++++ crates/axum/src/jsonrpc_exec.rs | 354 ++++++++++++++++++++++++++++ crates/axum/src/lib.rs | 2 + crates/axum/src/v2.rs | 321 ++++++++++++++++++++++++- crates/core/src/error.rs | 22 +- crates/core/src/lib.rs | 16 ++ crates/core/src/procedure.rs | 8 +- crates/core/src/stream.rs | 120 +++++++++- examples/astro/.astro/settings.json | 5 + examples/astro/.astro/types.d.ts | 1 + examples/astro/src/env.d.ts | 1 + examples/axum/src/main.rs | 40 +++- src/interop.rs | 163 +++++++++++-- src/procedure.rs | 7 +- src/router.rs | 25 +- 16 files changed, 1091 insertions(+), 61 deletions(-) create mode 100644 crates/axum/src/jsonrpc.rs create mode 100644 crates/axum/src/jsonrpc_exec.rs create mode 100644 examples/astro/.astro/settings.json create mode 100644 examples/astro/.astro/types.d.ts diff --git a/crates/axum/Cargo.toml b/crates/axum/Cargo.toml index 44d6f160..f1dd3220 100644 --- a/crates/axum/Cargo.toml +++ b/crates/axum/Cargo.toml @@ -29,3 +29,9 @@ rspc = { version = "0.3.1", path = "../.." } form_urlencoded = "1.2.1" # TODO: use Axum's built in extractor futures = "0.3.31" # TODO: No blocking execution, etc tokio = { version = "1.41.1", optional = true } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. +serde = { version = "1", features = ["derive"] } # TODO: Remove features +specta = { version = "=2.0.0-rc.20", features = [ + "derive", + "serde", + "serde_json", +] } # TODO: Drop all features diff --git a/crates/axum/src/jsonrpc.rs b/crates/axum/src/jsonrpc.rs new file mode 100644 index 00000000..245a3264 --- /dev/null +++ b/crates/axum/src/jsonrpc.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use specta::Type; + +#[derive(Debug, Clone, Deserialize, Serialize, Type, PartialEq, Eq, Hash)] +#[serde(untagged)] +pub enum RequestId { + Null, + Number(u32), + String(String), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] // TODO: Type on this +pub struct Request { + pub jsonrpc: Option, // This is required in the JsonRPC spec but I make it optional. + pub id: RequestId, + #[serde(flatten)] + pub inner: RequestInner, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +pub enum RequestInner { + Query { + path: String, + input: Option, + }, + Mutation { + path: String, + input: Option, + }, + Subscription { + path: String, + input: (RequestId, Option), + }, + SubscriptionStop { + input: RequestId, + }, +} + +#[derive(Debug, Clone, Serialize)] // TODO: Add `specta::Type` when supported +pub struct Response { + pub jsonrpc: &'static str, + pub id: RequestId, + pub result: ResponseInner, +} + +#[derive(Debug, Clone, Serialize, Type)] +#[serde(tag = "type", content = "data", rename_all = "camelCase")] +pub enum ResponseInner { + Event(Value), + Response(Value), + Error(JsonRPCError), +} + +#[derive(Debug, Clone, Serialize, Type)] +pub struct JsonRPCError { + pub code: i32, + pub message: String, + pub data: Option, +} diff --git a/crates/axum/src/jsonrpc_exec.rs b/crates/axum/src/jsonrpc_exec.rs new file mode 100644 index 00000000..6c1b2eaa --- /dev/null +++ b/crates/axum/src/jsonrpc_exec.rs @@ -0,0 +1,354 @@ +use std::{collections::HashMap, sync::Arc}; + +use futures::{Stream, StreamExt}; +use rspc::ExecError; +use rspc_core::ProcedureError; +use serde_json::Value; +use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; + +use crate::{jsonrpc, v2::Routes}; + +use super::jsonrpc::{RequestId, RequestInner, ResponseInner}; + +pub enum SubscriptionMap<'a> { + Ref(&'a mut HashMap>), + Mutex(&'a Mutex>>), + None, +} + +impl<'a> SubscriptionMap<'a> { + pub async fn has_subscription(&self, id: &RequestId) -> bool { + match self { + SubscriptionMap::Ref(map) => map.contains_key(id), + SubscriptionMap::Mutex(map) => { + let map = map.lock().await; + map.contains_key(id) + } + SubscriptionMap::None => unreachable!(), + } + } + + pub async fn insert(&mut self, id: RequestId, tx: oneshot::Sender<()>) { + match self { + SubscriptionMap::Ref(map) => { + map.insert(id, tx); + } + SubscriptionMap::Mutex(map) => { + let mut map = map.lock().await; + map.insert(id, tx); + } + SubscriptionMap::None => unreachable!(), + } + } + + pub async fn remove(&mut self, id: &RequestId) { + match self { + SubscriptionMap::Ref(map) => { + map.remove(id); + } + SubscriptionMap::Mutex(map) => { + let mut map = map.lock().await; + map.remove(id); + } + SubscriptionMap::None => unreachable!(), + } + } +} + +pub enum Sender<'a> { + Channel(&'a mut mpsc::Sender), + ResponseChannel(&'a mut mpsc::UnboundedSender), + Broadcast(&'a broadcast::Sender), + Response(Option), +} + +pub enum Sender2 { + Channel(mpsc::Sender), + ResponseChannel(mpsc::UnboundedSender), + Broadcast(broadcast::Sender), +} + +impl Sender2 { + pub async fn send( + &mut self, + resp: jsonrpc::Response, + ) -> Result<(), mpsc::error::SendError> { + match self { + Self::Channel(tx) => tx.send(resp).await?, + Self::ResponseChannel(tx) => tx.send(resp)?, + Self::Broadcast(tx) => { + let _ = tx.send(resp).map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + } + + Ok(()) + } +} + +impl<'a> Sender<'a> { + pub async fn send( + &mut self, + resp: jsonrpc::Response, + ) -> Result<(), mpsc::error::SendError> { + match self { + Self::Channel(tx) => tx.send(resp).await?, + Self::ResponseChannel(tx) => tx.send(resp)?, + Self::Broadcast(tx) => { + let _ = tx.send(resp).map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + Self::Response(r) => { + *r = Some(resp); + } + } + + Ok(()) + } + + pub fn sender2(&mut self) -> Sender2 { + match self { + Self::Channel(tx) => Sender2::Channel(tx.clone()), + Self::ResponseChannel(tx) => Sender2::ResponseChannel(tx.clone()), + Self::Broadcast(tx) => Sender2::Broadcast(tx.clone()), + Self::Response(_) => unreachable!(), + } + } +} + +pub async fn handle_json_rpc( + ctx: TCtx, + req: jsonrpc::Request, + routes: &Routes, + sender: &mut Sender<'_>, + subscriptions: &mut SubscriptionMap<'_>, +) where + TCtx: 'static, +{ + if req.jsonrpc.is_some() && req.jsonrpc.as_deref() != Some("2.0") { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "invalid JSON-RPC version".into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + + let (path, input, sub_id) = match req.inner { + RequestInner::Query { path, input } => (path, input, None), + RequestInner::Mutation { path, input } => (path, input, None), + RequestInner::Subscription { path, input } => (path, input.1, Some(input.0)), + RequestInner::SubscriptionStop { input } => { + subscriptions.remove(&input).await; + return; + } + }; + + let result = match routes.get(&path) { + Some(procedure) => { + let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); + + // It's really important this is before getting the first value + // Size hints can change after the first value is polled based on implementation. + let is_value = stream.size_hint() == (1, Some(1)); + + let first_value = stream.next(serde_json::value::Serializer).await; + + if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { + first_value + .expect("checked at if above") + .map(ResponseInner::Response) + .unwrap_or_else(|err| { + #[cfg(feature = "tracing")] + tracing::error!("Error executing operation: {:?}", err); + + ResponseInner::Error(match err { + ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error deserializing procedure arguments".to_string(), + data: None, + }, + ProcedureError::Downcast(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error downcasting procedure arguments".to_string(), + data: None, + }, + ProcedureError::Serializer(_) => jsonrpc::JsonRPCError { + code: 500, + message: "error serializing procedure result".to_string(), + data: None, + }, + ProcedureError::Resolver(resolver_error) => { + let legacy_error = resolver_error + .error() + .and_then(|v| v.downcast_ref::()) + .cloned(); + + jsonrpc::JsonRPCError { + code: resolver_error.status() as i32, + message: legacy_error + .map(|v| v.0.clone()) + // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. + .unwrap_or_else(|| resolver_error.to_string()), + data: None, + } + } + }) + }) + } else { + if matches!(sender, Sender::Response(_)) + || matches!(subscriptions, SubscriptionMap::None) + { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "unsupported metho".into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + + if let Some(id) = sub_id { + if matches!(id, RequestId::Null) { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "error creating subscription with null request id" + .into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } else if subscriptions.has_subscription(&id).await { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "error creating subscription with duplicate id".into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + subscriptions.insert(id.clone(), shutdown_tx).await; + let mut sender2 = sender.sender2(); + tokio::spawn(async move { + match first_value { + Some(Ok(v)) => { + let _ = sender2 + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: id.clone(), + result: ResponseInner::Event(v), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {:?}", _err); + }); + } + Some(Err(_err)) => { + #[cfg(feature = "tracing")] + tracing::error!("Subscription error: {:?}", _err); + } + None => return, + } + + loop { + tokio::select! { + biased; // Note: Order matters + _ = &mut shutdown_rx => { + #[cfg(feature = "tracing")] + tracing::debug!("Removing subscription with id '{:?}'", id); + break; + } + v = stream.next(serde_json::value::Serializer) => { + match v { + Some(Ok(v)) => { + let _ = sender2.send(jsonrpc::Response { + jsonrpc: "2.0", + id: id.clone(), + result: ResponseInner::Event(v), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {:?}", _err); + }); + } + Some(Err(_err)) => { + #[cfg(feature = "tracing")] + tracing::error!("Subscription error: {:?}", _err); + } + None => { + break; + } + } + } + } + } + }); + } + + return; + } + } + None => { + #[cfg(feature = "tracing")] + tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); + ResponseInner::Error(jsonrpc::JsonRPCError { + code: 404, + message: "the requested operation is not supported by this server".to_string(), + data: None, + }) + } + }; + + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id, + result, + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {:?}", _err); + }); +} diff --git a/crates/axum/src/lib.rs b/crates/axum/src/lib.rs index e840c5ef..83da5950 100644 --- a/crates/axum/src/lib.rs +++ b/crates/axum/src/lib.rs @@ -6,6 +6,8 @@ )] mod extractors; +mod jsonrpc; +mod jsonrpc_exec; mod legacy; mod v2; diff --git a/crates/axum/src/v2.rs b/crates/axum/src/v2.rs index 7c30a2d4..66c4603a 100644 --- a/crates/axum/src/v2.rs +++ b/crates/axum/src/v2.rs @@ -1,7 +1,26 @@ -use axum::Router; -use rspc_core::Procedures; +use std::collections::HashMap; -use crate::extractors::TCtxFunc; +use axum::{ + body::{to_bytes, Body}, + extract::{Request, State}, + http::{request::Parts, Method, Response, StatusCode}, + response::IntoResponse, + routing::{on, MethodFilter}, + RequestExt, Router, +}; +use rspc_core::{Procedure, Procedures}; +use serde_json::Value; + +// TODO: Remove everything +use rspc::internal::ProcedureKind; + +use crate::{ + extractors::TCtxFunc, + jsonrpc::{self, RequestId}, + jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}, +}; + +pub(crate) type Routes = HashMap>; pub fn endpoint2( router: impl Into>, @@ -13,12 +32,300 @@ where TCtxFnMarker: Send + Sync + 'static, TCtxFn: TCtxFunc, { - let flattened = router + let routes = router .into() .into_iter() - .map(|(key, value)| (key.join("."), value)); + .map(|(key, value)| (key.join("."), value)) + .collect::>(); + + Router::::new().route( + "/:id", + on( + MethodFilter::GET.or(MethodFilter::POST), + move |state: State, req: axum::extract::Request| { + let routes = routes.clone(); + + async move { + match (req.method(), &req.uri().path()[1..]) { + (&Method::GET, "ws") => { + #[cfg(feature = "ws")] + { + let mut req = req; + return req + .extract_parts::() + .await + .unwrap() // TODO: error handling + .on_upgrade(|socket| { + handle_websocket( + ctx_fn, + socket, + req.into_parts().0, + routes, + state.0, + ) + }) + .into_response(); + } + + #[cfg(not(feature = "ws"))] + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("[]")) // TODO: Better error message which frontend is actually setup to handle. + .unwrap() + } + (&Method::GET, _) => { + handle_http(ctx_fn, ProcedureKind::Query, req, &routes, state.0) + .await + .into_response() + } + (&Method::POST, _) => { + handle_http(ctx_fn, ProcedureKind::Mutation, req, &routes, state.0) + .await + .into_response() + } + _ => unreachable!(), + } + } + }, + ), + ) +} + +async fn handle_http( + ctx_fn: TCtxFn, + kind: ProcedureKind, + req: Request, + routes: &Routes, + state: TState, +) -> impl IntoResponse +where + TCtx: Send + Sync + 'static, + TCtxFn: TCtxFunc, + TState: Send + Sync + 'static, +{ + let procedure_name = req.uri().path()[1..].to_string(); // Has to be allocated because `TCtxFn` takes ownership of `req` + let (parts, body) = req.into_parts(); + let input = match parts.method { + Method::GET => parts + .uri + .query() + .map(|query| form_urlencoded::parse(query.as_bytes())) + .and_then(|mut params| params.find(|e| e.0 == "input").map(|e| e.1)) + .map(|v| serde_json::from_str(&v)) + .unwrap_or(Ok(None as Option)), + Method::POST => { + // TODO: Limit body size? + let body = to_bytes(body, usize::MAX).await.unwrap(); // TODO: error handling + (!body.is_empty()) + .then(|| serde_json::from_slice(body.to_vec().as_slice())) + .unwrap_or(Ok(None)) + } + _ => unreachable!(), + }; + + let input = match input { + Ok(input) => input, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!( + "Error passing parameters to operation '{}' with key '{:?}': {}", + kind.to_str(), + procedure_name, + _err + ); + + return Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap(); + } + }; + + #[cfg(feature = "tracing")] + tracing::debug!( + "Executing operation '{}' with key '{}' with params {:?}", + kind.to_str(), + procedure_name, + input + ); + + let mut resp = Sender::Response(None); + + let ctx = match ctx_fn.exec(parts, &state).await { + Ok(ctx) => ctx, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error executing context function: {}", _err); + + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap(); + } + }; + + handle_json_rpc( + ctx, + jsonrpc::Request { + jsonrpc: None, + id: RequestId::Null, + inner: match kind { + ProcedureKind::Query => jsonrpc::RequestInner::Query { + path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? + input, + }, + ProcedureKind::Mutation => jsonrpc::RequestInner::Mutation { + path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? + input, + }, + ProcedureKind::Subscription => { + #[cfg(feature = "tracing")] + tracing::error!("Attempted to execute a subscription operation with HTTP"); + + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap(); + } + }, + }, + routes, + &mut resp, + &mut SubscriptionMap::None, + ) + .await; + + match resp { + Sender::Response(Some(resp)) => match serde_json::to_vec(&resp) { + Ok(v) => Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(v)) + .unwrap(), + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error serializing response: {}", _err); + + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("Content-Type", "application/json") + .body(Body::from(b"[]".as_slice())) + .unwrap() + } + }, + _ => unreachable!(), + } +} + +#[cfg(feature = "ws")] +async fn handle_websocket( + ctx_fn: TCtxFn, + mut socket: axum::extract::ws::WebSocket, + parts: Parts, + routes: Routes, + state: TState, +) where + TCtx: Send + Sync + 'static, + TCtxFn: TCtxFunc, + TState: Send + Sync, +{ + use axum::extract::ws::Message; + use futures::StreamExt; + use tokio::sync::mpsc; + + #[cfg(feature = "tracing")] + tracing::debug!("Accepting websocket connection"); + + let mut subscriptions = HashMap::new(); + let (mut tx, mut rx) = mpsc::channel::(100); + + loop { + tokio::select! { + biased; // Note: Order is important here + msg = rx.recv() => { + match socket.send(Message::Text(match serde_json::to_string(&msg) { + Ok(v) => v, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error serializing websocket message: {}", _err); + + continue; + } + })).await { + Ok(_) => {} + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error sending websocket message: {}", _err); + + continue; + } + } + } + msg = socket.next() => { + match msg { + Some(Ok(msg)) => { + let res = match msg { + Message::Text(text) => serde_json::from_str::(&text), + Message::Binary(binary) => serde_json::from_slice(&binary), + Message::Ping(_) | Message::Pong(_) | Message::Close(_) => { + continue; + } + }; + + match res.and_then(|v| match v.is_array() { + true => serde_json::from_value::>(v), + false => serde_json::from_value::(v).map(|v| vec![v]), + }) { + Ok(reqs) => { + for request in reqs { + let ctx = match ctx_fn.exec(parts.clone(), &state).await { + Ok(ctx) => { + ctx + }, + Err(_err) => { + + #[cfg(feature = "tracing")] + tracing::error!("Error executing context function: {}", _err); + + continue; + } + }; + + handle_json_rpc(ctx, request, &routes, &mut Sender::Channel(&mut tx), + &mut SubscriptionMap::Ref(&mut subscriptions)).await; + } + }, + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::error!("Error parsing websocket message: {}", _err); + + // TODO: Send report of error to frontend + + continue; + } + }; + } + Some(Err(_err)) => { + #[cfg(feature = "tracing")] + tracing::error!("Error in websocket: {}", _err); + + // TODO: Send report of error to frontend + + continue; + }, + None => { + #[cfg(feature = "tracing")] + tracing::debug!("Shutting down websocket connection"); - // TODO: Flatten keys + // TODO: Send report of error to frontend - todo!(); + return; + }, + } + } + } + } } diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 887f0b1e..5f495180 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -65,7 +65,7 @@ impl From for ResolverError { pub struct ResolverError(Repr); impl ResolverError { - pub fn new( + pub fn new( status: u16, value: T, source: Option, @@ -77,9 +77,9 @@ impl ResolverError { } /// TODO - pub fn status(&self) -> &dyn erased_serde::Serialize { + pub fn status(&self) -> u16 { match &self.0 { - Repr::Custom { status, value: _ } => status, + Repr::Custom { status, value: _ } => *status, // We flatten these to `ResolverError` so this won't be hit. Repr::Deserialize(_) => unreachable!(), Repr::Downcast(_) => unreachable!(), @@ -100,7 +100,7 @@ impl ResolverError { } /// TODO - pub fn error(&self) -> Option<&dyn error::Error> { + pub fn error(&self) -> Option<&(dyn error::Error + Send + 'static)> { match &self.0 { Repr::Custom { status: _, @@ -188,21 +188,25 @@ struct ErrorInternal { err: Option, } -trait ErrorInternalExt { +trait ErrorInternalExt: Send { fn value(&self) -> &dyn erased_serde::Serialize; - fn error(&self) -> Option<&dyn error::Error>; + fn error(&self) -> Option<&(dyn error::Error + Send + 'static)>; fn debug(&self) -> Option<&dyn fmt::Debug>; } -impl ErrorInternalExt for ErrorInternal { +impl ErrorInternalExt + for ErrorInternal +{ fn value(&self) -> &dyn erased_serde::Serialize { &self.value } - fn error(&self) -> Option<&dyn error::Error> { - self.err.as_ref().map(|err| err as &dyn error::Error) + fn error(&self) -> Option<&(dyn error::Error + Send + 'static)> { + self.err + .as_ref() + .map(|err| err as &(dyn error::Error + Send + 'static)) } fn debug(&self) -> Option<&dyn fmt::Debug> { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 10e2d951..896a4030 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -30,6 +30,22 @@ pub use stream::ProcedureStream; pub type Procedures = std::collections::BTreeMap>, Procedure>; +// TODO: Remove this once we remove the legacy executor. +#[doc(hidden)] +#[derive(Clone)] +pub struct LegacyErrorInterop(pub String); +impl std::fmt::Debug for LegacyErrorInterop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "LegacyErrorInterop({})", self.0) + } +} +impl std::fmt::Display for LegacyErrorInterop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "LegacyErrorInterop({})", self.0) + } +} +impl std::error::Error for LegacyErrorInterop {} + // TODO: The naming is horid. // Low-level concerns: // - `Procedure` - Holds the handler (and probably type information) diff --git a/crates/core/src/procedure.rs b/crates/core/src/procedure.rs index e5f12cc9..a8e5c232 100644 --- a/crates/core/src/procedure.rs +++ b/crates/core/src/procedure.rs @@ -8,15 +8,19 @@ use serde::Deserializer; use crate::{DynInput, ProcedureStream}; +// TODO: Document the importance of the `size_hint` + /// a single type-erased operation that the server can execute. /// /// TODO: Show constructing and executing procedure. pub struct Procedure { - handler: Arc ProcedureStream>, + handler: Arc ProcedureStream + Send + Sync>, } impl Procedure { - pub fn new(handler: impl Fn(TCtx, DynInput) -> ProcedureStream + 'static) -> Self { + pub fn new( + handler: impl Fn(TCtx, DynInput) -> ProcedureStream + Send + Sync + 'static, + ) -> Self { Self { handler: Arc::new(handler), } diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index 28201911..c0fb178d 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -22,7 +22,7 @@ impl ProcedureStream { /// TODO pub fn from_value(value: Result) -> Self where - T: Serialize + 'static, // TODO: Drop `Serialize`!!! + T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! { Self { src: Box::pin(DynReturnValueFutureCompat { @@ -36,8 +36,8 @@ impl ProcedureStream { /// TODO pub fn from_future(src: S) -> Self where - S: Future> + 'static, - T: Serialize + 'static, // TODO: Drop `Serialize`!!! + S: Future> + Send + 'static, + T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! { Self { src: Box::pin(DynReturnValueFutureCompat { src, value: None }), @@ -47,14 +47,29 @@ impl ProcedureStream { /// TODO pub fn from_stream(src: S) -> Self where - S: Stream> + 'static, - T: Serialize + 'static, // TODO: Drop `Serialize`!!! + S: Stream> + Send + 'static, + T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! { Self { src: Box::pin(DynReturnValueStreamCompat { src, value: None }), } } + // TODO: I'm not sure if we should keep this or not? + // The crate `futures`'s flatten stuff doesn't handle it how we need it so maybe we could patch that instead of having this special case??? + // This is a special case because we need to ensure the `size_hint` is correct. + /// TODO + pub fn from_future_stream(src: F) -> Self + where + F: Future> + Send + 'static, + S: Stream> + Send + 'static, + T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! + { + Self { + src: Box::pin(DynReturnValueStreamFutureCompat::Future { src }), + } + } + // /// TODO // /// // /// TODO: This method doesn't allow reusing the serializer between polls. Maybe remove it??? @@ -70,6 +85,11 @@ impl ProcedureStream { // TODO: Fn to get syncronous value??? + /// TODO + pub fn size_hint(&self) -> (usize, Option) { + self.src.size_hint() + } + /// TODO pub async fn next( &mut self, @@ -99,13 +119,15 @@ impl fmt::Debug for ProcedureStream { } } -trait DynReturnValue { +trait DynReturnValue: Send { fn poll_next_value<'a>( self: Pin<&'a mut Self>, cx: &mut Context<'_>, ) -> Poll>>; fn value(&self) -> &dyn erased_serde::Serialize; + + fn size_hint(&self) -> (usize, Option); } pin_project! { @@ -116,7 +138,7 @@ pin_project! { } } -impl>> DynReturnValue +impl> + Send> DynReturnValue for DynReturnValueFutureCompat where T: Serialize, // TODO: Drop this bound!!! @@ -146,6 +168,10 @@ where // Attempted to access value when `Poll::Ready(None)` was not returned. .expect("unreachable") } + + fn size_hint(&self) -> (usize, Option) { + (1, Some(1)) + } } pin_project! { @@ -156,7 +182,7 @@ pin_project! { } } -impl>> DynReturnValue +impl> + Send> DynReturnValue for DynReturnValueStreamCompat where T: Serialize, // TODO: Drop this bound!!! @@ -187,4 +213,82 @@ where // Attempted to access value when `Poll::Ready(None)` was not returned. .expect("unreachable") } + + fn size_hint(&self) -> (usize, Option) { + self.src.size_hint() + } +} + +pin_project! { + #[project = DynReturnValueStreamFutureCompatProj] + enum DynReturnValueStreamFutureCompat { + Future { + #[pin] src: F, + }, + Stream { + #[pin] src: S, + value: Option, + } + } +} + +impl DynReturnValue for DynReturnValueStreamFutureCompat +where + T: Serialize + Send, // TODO: Drop `Serialize` bound!!! + F: Future> + Send + 'static, + S: Stream> + Send, +{ + // TODO: Cleanup this impl's pattern matching. + fn poll_next_value<'a>( + mut self: Pin<&'a mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + loop { + return match self.as_mut().project() { + DynReturnValueStreamFutureCompatProj::Future { src } => match src.poll(cx) { + Poll::Ready(Ok(result)) => { + self.as_mut().set(DynReturnValueStreamFutureCompat::Stream { + src: result, + value: None, + }); + continue; + } + Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))), + Poll::Pending => return Poll::Pending, + }, + DynReturnValueStreamFutureCompatProj::Stream { src, value } => { + let _ = value.take(); // Reset value to ensure `take` being misused causes it to panic. + match src.poll_next(cx) { + Poll::Ready(Some(v)) => Poll::Ready(Some(match v { + Ok(v) => { + *value = Some(v); + Ok(()) + } + Err(err) => Err(err), + })), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } + }; + } + } + + fn value(&self) -> &dyn erased_serde::Serialize { + match self { + // Attempted to acces value before first `Poll::Ready` was returned. + Self::Future { .. } => panic!("unreachable"), + Self::Stream { value, .. } => value + .as_ref() + // Attempted to access value when `Poll::Ready(None)` was not returned. + .expect("unreachable"), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Future { .. } => (0, None), + Self::Stream { src, .. } => src.size_hint(), + } + } } diff --git a/examples/astro/.astro/settings.json b/examples/astro/.astro/settings.json new file mode 100644 index 00000000..3d30eb35 --- /dev/null +++ b/examples/astro/.astro/settings.json @@ -0,0 +1,5 @@ +{ + "_variables": { + "lastUpdateCheck": 1733304652115 + } +} \ No newline at end of file diff --git a/examples/astro/.astro/types.d.ts b/examples/astro/.astro/types.d.ts new file mode 100644 index 00000000..f964fe0c --- /dev/null +++ b/examples/astro/.astro/types.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/astro/src/env.d.ts b/examples/astro/src/env.d.ts index 8c34fb45..c13bd73c 100644 --- a/examples/astro/src/env.d.ts +++ b/examples/astro/src/env.d.ts @@ -1 +1,2 @@ +/// /// \ No newline at end of file diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 411a7c4f..4791d658 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -14,8 +14,30 @@ struct Ctx {} #[derive(Serialize, Type)] pub struct MyCustomType(String); -#[tokio::main] -async fn main() { +#[derive(Type, Serialize)] +#[serde(tag = "type")] +#[specta(export = false)] +pub enum DeserializationError { + // Is not a map-type so invalid. + A(String), +} + +// http://[::]:4000/rspc/version +// http://[::]:4000/legacy/version + +// http://[::]:4000/rspc/nested.hello +// http://[::]:4000/legacy/nested.hello + +// http://[::]:4000/rspc/error +// http://[::]:4000/legacy/error + +// http://[::]:4000/rspc/echo +// http://[::]:4000/legacy/echo + +// http://[::]:4000/rspc/echo?input=42 +// http://[::]:4000/legacy/echo?input=42 + +fn mount() -> rspc::Router { let inner = rspc::Router::::new().query("hello", |t| t(|_, _: ()| "Hello World!")); let router = rspc::Router::::new() @@ -65,7 +87,12 @@ async fn main() { // })) .build(); - let (routes, types) = Router2::from(router).build().unwrap(); + router +} + +#[tokio::main] +async fn main() { + let (routes, types) = Router2::from(mount()).build().unwrap(); types .export_to( @@ -93,6 +120,13 @@ async fn main() { Ctx {} }), ) + .nest( + "/legacy", + rspc_axum::endpoint(mount().arced(), |parts: Parts| { + println!("Client requested operation '{}'", parts.uri.path()); + Ctx {} + }), + ) .layer(cors); let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 diff --git a/src/interop.rs b/src/interop.rs index 18b1679c..4d8020be 100644 --- a/src/interop.rs +++ b/src/interop.rs @@ -1,11 +1,16 @@ -use std::borrow::Cow; +use std::{borrow::Cow, collections::BTreeMap}; -use futures::{stream, FutureExt, StreamExt, TryStreamExt}; +use futures::{stream, FutureExt, Stream, StreamExt, TryStreamExt}; use rspc_core::{ProcedureStream, ResolverError}; use serde_json::Value; +use specta::{ + datatype::{DataType, EnumRepr, EnumVariant, LiteralType}, + NamedType, Type, +}; use crate::{ internal::{jsonrpc::JsonRPCError, Layer, ProcedureKind, RequestContext, ValueOrStream}, + router::literal_object, Procedure2, Router, Router2, }; @@ -37,6 +42,7 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { .map(|s| s.to_string().into()) .collect::>>(), Procedure2 { + setup: Default::default(), kind, input: p.ty.arg_ty, result: p.ty.result_ty, @@ -65,37 +71,144 @@ fn layer_to_procedure( value: Box>, ) -> rspc_core::Procedure { rspc_core::Procedure::new(move |ctx, input| { - let input: Value = input.deserialize().unwrap(); // TODO: Error handling - let result = value - .call( - ctx, - input, - RequestContext { - kind: kind.clone(), - path: path.clone(), - }, - ) - .unwrap(); // TODO: Error handling + let result = input + .deserialize::() + .map_err(Into::into) + .and_then(|input| { + value + .call( + ctx, + input, + RequestContext { + kind: kind.clone(), + path: path.clone(), + }, + ) + .map_err(|err| { + let err: crate::legacy::Error = err.into(); + ResolverError::new( + err.code.to_status_code(), + (), /* typesafe errors aren't supported in legacy router */ + Some(rspc_core::LegacyErrorInterop(err.message)), + ) + }) + }); - ProcedureStream::from_stream( - async move { - let result = result.into_value_or_stream().await.unwrap(); // TODO: Error handling - - match result { - ValueOrStream::Value(value) => stream::once(async { Ok(value) }).boxed(), - ValueOrStream::Stream(s) => s + match result { + Ok(result) => ProcedureStream::from_future_stream(async move { + match result.into_value_or_stream().await { + Ok(ValueOrStream::Value(value)) => { + Ok(stream::once(async { Ok(value) }).boxed()) + } + Ok(ValueOrStream::Stream(s)) => Ok(s .map_err(|err| { let err = JsonRPCError::from(err); ResolverError::new( - err.code.try_into().unwrap(), + err.code.try_into().unwrap_or(500), err, None::, ) }) - .boxed(), + .boxed()), + Err(err) => { + let err: crate::legacy::Error = err.into(); + let err = + ResolverError::new(err.code.to_status_code(), err.message, err.cause); + // stream::once(async { Err(err) }).boxed() + Err(err) + } } - } - .flatten_stream(), - ) + }), + Err(err) => ProcedureStream::from_value(Err::<(), _>(err)), + } }) } + +fn map_method( + kind: ProcedureKind, + p: &BTreeMap>, Procedure2>, +) -> Vec<(Cow<'static, str>, EnumVariant)> { + p.iter() + .filter(|(_, p)| p.kind() == kind) + .map(|(key, p)| { + let key = key.join(".").to_string(); + ( + key.clone().into(), + specta::internal::construct::enum_variant( + false, + None, + "".into(), + specta::internal::construct::enum_variant_unnamed(vec![ + specta::internal::construct::field( + false, + false, + None, + "".into(), + Some(literal_object( + "".into(), + None, + vec![ + ("key".into(), LiteralType::String(key.clone()).into()), + ("input".into(), p.input.clone()), + ("result".into(), p.result.clone()), + ] + .into_iter(), + )), + ), + ]), + ), + ) + }) + .collect::>() +} + +// TODO: Remove this block with the interop system +pub(crate) fn construct_legacy_bindings_type( + p: &BTreeMap>, Procedure2>, +) -> Vec<(Cow<'static, str>, DataType)> { + #[derive(Type)] + struct Queries; + #[derive(Type)] + struct Mutations; + #[derive(Type)] + struct Subscriptions; + + vec![ + ( + "queries".into(), + specta::internal::construct::r#enum( + "Queries".into(), + Queries::sid(), + EnumRepr::Untagged, + false, + Default::default(), + map_method(ProcedureKind::Query, p), + ) + .into(), + ), + ( + "mutations".into(), + specta::internal::construct::r#enum( + "Mutations".into(), + Mutations::sid(), + EnumRepr::Untagged, + false, + Default::default(), + map_method(ProcedureKind::Mutation, p), + ) + .into(), + ), + ( + "subscriptions".into(), + specta::internal::construct::r#enum( + "Subscriptions".into(), + Subscriptions::sid(), + EnumRepr::Untagged, + false, + Default::default(), + map_method(ProcedureKind::Subscription, p), + ) + .into(), + ), + ] +} diff --git a/src/procedure.rs b/src/procedure.rs index 36bf1c53..0198a127 100644 --- a/src/procedure.rs +++ b/src/procedure.rs @@ -20,9 +20,10 @@ pub struct Procedure2 { impl Procedure2 { // TODO: `fn builder` - // pub fn kind(&self) -> ProcedureKind { - // self.kind - // } + // TODO: Make `pub` + pub(crate) fn kind(&self) -> ProcedureKind { + self.kind + } // TODO: Expose all fields diff --git a/src/router.rs b/src/router.rs index 0f1790ba..62239330 100644 --- a/src/router.rs +++ b/src/router.rs @@ -4,7 +4,7 @@ use specta::{datatype::DataType, NamedType, SpectaID, Type, TypeMap}; use rspc_core::Procedures; -use crate::{internal::ProcedureKind, Procedure2, State}; +use crate::{internal::ProcedureKind, interop::construct_legacy_bindings_type, Procedure2, State}; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { @@ -32,10 +32,11 @@ impl Router2 { // pub fn procedure( // mut self, // key: impl Into>, - // procedure: UnbuiltProcedure, + // procedure: Procedure2, // ) -> Self { // let name = key.into(); // self.procedures.insert(name, procedure); + // self.setup.extend(procedure.setup); // self // } @@ -73,6 +74,8 @@ impl Router2 { setup(&mut state); } + let legacy_types = construct_legacy_bindings_type(&self.procedures); + let (types, procedures): (Vec<_>, BTreeMap<_, _>) = self .procedures .into_iter() @@ -93,6 +96,20 @@ impl Router2 { self.types.insert(Procedures::sid(), ndt); } + { + #[derive(Type)] + struct ProceduresLegacy; + + let s = literal_object( + "ProceduresLegacy".into(), + Some(ProceduresLegacy::sid()), + legacy_types.into_iter(), + ); + let mut ndt = ProceduresLegacy::definition_named_data_type(&mut self.types); + ndt.inner = s.into(); + self.types.insert(ProceduresLegacy::sid(), ndt); + } + struct Impl(Procedures); impl Into> for Impl { fn into(self) -> Procedures { @@ -145,7 +162,7 @@ impl<'a, TCtx> IntoIterator for &'a Router2 { } // TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` -fn literal_object( +pub(crate) fn literal_object( name: Cow<'static, str>, sid: Option, fields: impl Iterator, DataType)>, @@ -201,7 +218,7 @@ fn construct_bindings_type( } // TODO: Remove this block with the interop system -impl From> for Router2 { +impl From> for Router2 { fn from(router: crate::legacy::Router) -> Self { crate::interop::legacy_to_modern(router) } From 26954509f5e04edb2d6ea7a7bebc0d4852569bf3 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 4 Dec 2024 23:57:36 +0800 Subject: [PATCH 08/67] fix axum and tauri --- .gitignore | 1 - crates/axum/Cargo.toml | 14 +- crates/axum/src/extractors.rs | 13 +- crates/axum/src/jsonrpc.rs | 28 +- crates/axum/src/jsonrpc_exec.rs | 4 +- crates/axum/src/legacy.rs | 639 +++++++++++++++---------------- crates/axum/src/lib.rs | 5 +- crates/axum/src/v2.rs | 21 +- crates/core/src/interop.rs | 21 + crates/core/src/lib.rs | 27 +- crates/tauri/Cargo.toml | 16 +- crates/tauri/src/jsonrpc.rs | 79 ++++ crates/tauri/src/jsonrpc_exec.rs | 352 +++++++++++++++++ crates/tauri/src/lib.rs | 34 +- examples/axum/src/main.rs | 13 +- src/interop.rs | 14 +- src/legacy/error.rs | 32 +- src/legacy/internal/mod.rs | 4 +- src/legacy/router_builder.rs | 10 +- 19 files changed, 877 insertions(+), 450 deletions(-) create mode 100644 crates/core/src/interop.rs create mode 100644 crates/tauri/src/jsonrpc.rs create mode 100644 crates/tauri/src/jsonrpc_exec.rs diff --git a/.gitignore b/.gitignore index 7378ac99..d3fa8e50 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ Cargo.lock # Typescript bindings exported by the examples bindings.ts -bindings-legacy.ts # Node node_modules diff --git a/crates/axum/Cargo.toml b/crates/axum/Cargo.toml index f1dd3220..e630e3e4 100644 --- a/crates/axum/Cargo.toml +++ b/crates/axum/Cargo.toml @@ -17,21 +17,15 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -ws = ["dep:tokio", "axum/ws"] +ws = ["axum/ws"] [dependencies] rspc-core = { version = "0.0.0", path = "../core" } axum = "0.7.9" -serde_json = "1.0.133" +serde_json = "1" # TODO: Drop these -rspc = { version = "0.3.1", path = "../.." } form_urlencoded = "1.2.1" # TODO: use Axum's built in extractor -futures = "0.3.31" # TODO: No blocking execution, etc -tokio = { version = "1.41.1", optional = true } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. +futures = "0.3" # TODO: No blocking execution, etc +tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. serde = { version = "1", features = ["derive"] } # TODO: Remove features -specta = { version = "=2.0.0-rc.20", features = [ - "derive", - "serde", - "serde_json", -] } # TODO: Drop all features diff --git a/crates/axum/src/extractors.rs b/crates/axum/src/extractors.rs index 831112d0..eb9487fd 100644 --- a/crates/axum/src/extractors.rs +++ b/crates/axum/src/extractors.rs @@ -1,5 +1,4 @@ use axum::{extract::FromRequestParts, http::request::Parts}; -use rspc::ExecError; use std::future::Future; use std::marker::PhantomData; @@ -9,11 +8,7 @@ where TState: Send + Sync, TCtx: Send + 'static, { - fn exec( - &self, - parts: Parts, - state: &TState, - ) -> impl Future> + Send; + fn exec(&self, parts: Parts, state: &TState) -> impl Future> + Send; } pub struct ZeroArgMarker; @@ -24,7 +19,7 @@ where TState: Send + Sync, TCtx: Send + 'static, { - async fn exec(&self, _: Parts, _: &TState) -> Result { + async fn exec(&self, _: Parts, _: &TState) -> Result { Ok(self.clone()()) } } @@ -40,12 +35,12 @@ macro_rules! impl_fn { TState: Send + Sync, TCtx: Send + 'static { - async fn exec(&self, mut parts: Parts, state: &TState) -> Result + async fn exec(&self, mut parts: Parts, state: &TState) -> Result { $( #[allow(non_snake_case)] let Ok($generics) = $generics::from_request_parts(&mut parts, &state).await else { - return Err(ExecError::AxumExtractorError) + return Err(()) }; )* diff --git a/crates/axum/src/jsonrpc.rs b/crates/axum/src/jsonrpc.rs index 245a3264..dfc0580c 100644 --- a/crates/axum/src/jsonrpc.rs +++ b/crates/axum/src/jsonrpc.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use specta::Type; -#[derive(Debug, Clone, Deserialize, Serialize, Type, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(untagged)] pub enum RequestId { Null, @@ -18,7 +17,7 @@ pub struct Request { pub inner: RequestInner, } -#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] pub enum RequestInner { Query { @@ -45,7 +44,7 @@ pub struct Response { pub result: ResponseInner, } -#[derive(Debug, Clone, Serialize, Type)] +#[derive(Debug, Clone, Serialize)] #[serde(tag = "type", content = "data", rename_all = "camelCase")] pub enum ResponseInner { Event(Value), @@ -53,9 +52,28 @@ pub enum ResponseInner { Error(JsonRPCError), } -#[derive(Debug, Clone, Serialize, Type)] +#[derive(Debug, Clone, Serialize)] pub struct JsonRPCError { pub code: i32, pub message: String, pub data: Option, } + +// TODO: BREAK + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcedureKind { + Query, + Mutation, + Subscription, +} + +impl ProcedureKind { + pub fn to_str(&self) -> &'static str { + match self { + ProcedureKind::Query => "query", + ProcedureKind::Mutation => "mutation", + ProcedureKind::Subscription => "subscription", + } + } +} diff --git a/crates/axum/src/jsonrpc_exec.rs b/crates/axum/src/jsonrpc_exec.rs index 6c1b2eaa..a899edf3 100644 --- a/crates/axum/src/jsonrpc_exec.rs +++ b/crates/axum/src/jsonrpc_exec.rs @@ -1,7 +1,5 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; -use futures::{Stream, StreamExt}; -use rspc::ExecError; use rspc_core::ProcedureError; use serde_json::Value; use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; diff --git a/crates/axum/src/legacy.rs b/crates/axum/src/legacy.rs index d46ded57..076e2183 100644 --- a/crates/axum/src/legacy.rs +++ b/crates/axum/src/legacy.rs @@ -1,320 +1,319 @@ -use std::{collections::HashMap, sync::Arc}; - -use axum::{ - body::{to_bytes, Body}, - extract::{Request, State}, - http::{request::Parts, Method, Response, StatusCode}, - response::IntoResponse, - routing::{on, MethodFilter}, - RequestExt, Router, -}; -use rspc::internal::{ - jsonrpc::{self, handle_json_rpc, RequestId, Sender, SubscriptionMap}, - ProcedureKind, -}; -use rspc_core::Procedures; -use serde_json::Value; - -use crate::extractors::TCtxFunc; - -pub fn endpoint( - router: Arc>, - ctx_fn: TCtxFn, -) -> Router -where - S: Clone + Send + Sync + 'static, - TCtx: Send + Sync + 'static, - TCtxFnMarker: Send + Sync + 'static, - TCtxFn: TCtxFunc, -{ - Router::::new().route( - "/:id", - on( - MethodFilter::GET.or(MethodFilter::POST), - move |state: State, req: axum::extract::Request| { - let router = router.clone(); - - async move { - match (req.method(), &req.uri().path()[1..]) { - (&Method::GET, "ws") => { - #[cfg(feature = "ws")] - { - let mut req = req; - return req - .extract_parts::() - .await - .unwrap() // TODO: error handling - .on_upgrade(|socket| { - handle_websocket( - ctx_fn, - socket, - req.into_parts().0, - router, - state.0, - ) - }) - .into_response(); - } - - #[cfg(not(feature = "ws"))] - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("[]")) // TODO: Better error message which frontend is actually setup to handle. - .unwrap() - } - (&Method::GET, _) => { - handle_http(ctx_fn, ProcedureKind::Query, req, &router, state.0) - .await - .into_response() - } - (&Method::POST, _) => { - handle_http(ctx_fn, ProcedureKind::Mutation, req, &router, state.0) - .await - .into_response() - } - _ => unreachable!(), - } - } - }, - ), - ) -} - -async fn handle_http( - ctx_fn: TCtxFn, - kind: ProcedureKind, - req: Request, - router: &Arc>, - state: TState, -) -> impl IntoResponse -where - TCtx: Send + Sync + 'static, - TCtxFn: TCtxFunc, - TState: Send + Sync + 'static, -{ - let procedure_name = req.uri().path()[1..].to_string(); // Has to be allocated because `TCtxFn` takes ownership of `req` - let (parts, body) = req.into_parts(); - let input = match parts.method { - Method::GET => parts - .uri - .query() - .map(|query| form_urlencoded::parse(query.as_bytes())) - .and_then(|mut params| params.find(|e| e.0 == "input").map(|e| e.1)) - .map(|v| serde_json::from_str(&v)) - .unwrap_or(Ok(None as Option)), - Method::POST => { - // TODO: Limit body size? - let body = to_bytes(body, usize::MAX).await.unwrap(); // TODO: error handling - (!body.is_empty()) - .then(|| serde_json::from_slice(body.to_vec().as_slice())) - .unwrap_or(Ok(None)) - } - _ => unreachable!(), - }; - - let input = match input { - Ok(input) => input, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!( - "Error passing parameters to operation '{}' with key '{:?}': {}", - kind.to_str(), - procedure_name, - _err - ); - - return Response::builder() - .status(StatusCode::NOT_FOUND) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap(); - } - }; - - #[cfg(feature = "tracing")] - tracing::debug!( - "Executing operation '{}' with key '{}' with params {:?}", - kind.to_str(), - procedure_name, - input - ); - - let mut resp = Sender::Response(None); - - let ctx = match ctx_fn.exec(parts, &state).await { - Ok(ctx) => ctx, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing context function: {}", _err); - - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap(); - } - }; - - handle_json_rpc( - ctx, - jsonrpc::Request { - jsonrpc: None, - id: RequestId::Null, - inner: match kind { - ProcedureKind::Query => jsonrpc::RequestInner::Query { - path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? - input, - }, - ProcedureKind::Mutation => jsonrpc::RequestInner::Mutation { - path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? - input, - }, - ProcedureKind::Subscription => { - #[cfg(feature = "tracing")] - tracing::error!("Attempted to execute a subscription operation with HTTP"); - - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap(); - } - }, - }, - router, - &mut resp, - &mut SubscriptionMap::None, - ) - .await; - - match resp { - Sender::Response(Some(resp)) => match serde_json::to_vec(&resp) { - Ok(v) => Response::builder() - .status(StatusCode::OK) - .header("Content-Type", "application/json") - .body(Body::from(v)) - .unwrap(), - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error serializing response: {}", _err); - - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header("Content-Type", "application/json") - .body(Body::from(b"[]".as_slice())) - .unwrap() - } - }, - _ => unreachable!(), - } -} - -#[cfg(feature = "ws")] -async fn handle_websocket( - ctx_fn: TCtxFn, - mut socket: axum::extract::ws::WebSocket, - parts: Parts, - router: Arc>, - state: TState, -) where - TCtx: Send + Sync + 'static, - TCtxFn: TCtxFunc, - TState: Send + Sync, -{ - use axum::extract::ws::Message; - use futures::StreamExt; - use tokio::sync::mpsc; - - #[cfg(feature = "tracing")] - tracing::debug!("Accepting websocket connection"); - - let mut subscriptions = HashMap::new(); - let (mut tx, mut rx) = mpsc::channel::(100); - - loop { - tokio::select! { - biased; // Note: Order is important here - msg = rx.recv() => { - match socket.send(Message::Text(match serde_json::to_string(&msg) { - Ok(v) => v, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error serializing websocket message: {}", _err); - - continue; - } - })).await { - Ok(_) => {} - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error sending websocket message: {}", _err); - - continue; - } - } - } - msg = socket.next() => { - match msg { - Some(Ok(msg)) => { - let res = match msg { - Message::Text(text) => serde_json::from_str::(&text), - Message::Binary(binary) => serde_json::from_slice(&binary), - Message::Ping(_) | Message::Pong(_) | Message::Close(_) => { - continue; - } - }; - - match res.and_then(|v| match v.is_array() { - true => serde_json::from_value::>(v), - false => serde_json::from_value::(v).map(|v| vec![v]), - }) { - Ok(reqs) => { - for request in reqs { - let ctx = match ctx_fn.exec(parts.clone(), &state).await { - Ok(ctx) => { - ctx - }, - Err(_err) => { - - #[cfg(feature = "tracing")] - tracing::error!("Error executing context function: {}", _err); - - continue; - } - }; - - handle_json_rpc(ctx, request, &router, &mut Sender::Channel(&mut tx), - &mut SubscriptionMap::Ref(&mut subscriptions)).await; - } - }, - Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error parsing websocket message: {}", _err); - - // TODO: Send report of error to frontend - - continue; - } - }; - } - Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Error in websocket: {}", _err); - - // TODO: Send report of error to frontend - - continue; - }, - None => { - #[cfg(feature = "tracing")] - tracing::debug!("Shutting down websocket connection"); - - // TODO: Send report of error to frontend - - return; - }, - } - } - } - } -} +// use std::{collections::HashMap, sync::Arc}; + +// use axum::{ +// body::{to_bytes, Body}, +// extract::{Request, State}, +// http::{request::Parts, Method, Response, StatusCode}, +// response::IntoResponse, +// routing::{on, MethodFilter}, +// RequestExt, Router, +// }; +// use rspc::internal::{ +// jsonrpc::{self, handle_json_rpc, RequestId, Sender, SubscriptionMap}, +// ProcedureKind, +// }; +// use serde_json::Value; + +// use crate::extractors::TCtxFunc; + +// pub fn endpoint( +// router: Arc>, +// ctx_fn: TCtxFn, +// ) -> Router +// where +// S: Clone + Send + Sync + 'static, +// TCtx: Send + Sync + 'static, +// TCtxFnMarker: Send + Sync + 'static, +// TCtxFn: TCtxFunc, +// { +// Router::::new().route( +// "/:id", +// on( +// MethodFilter::GET.or(MethodFilter::POST), +// move |state: State, req: axum::extract::Request| { +// let router = router.clone(); + +// async move { +// match (req.method(), &req.uri().path()[1..]) { +// (&Method::GET, "ws") => { +// #[cfg(feature = "ws")] +// { +// let mut req = req; +// return req +// .extract_parts::() +// .await +// .unwrap() // TODO: error handling +// .on_upgrade(|socket| { +// handle_websocket( +// ctx_fn, +// socket, +// req.into_parts().0, +// router, +// state.0, +// ) +// }) +// .into_response(); +// } + +// #[cfg(not(feature = "ws"))] +// Response::builder() +// .status(StatusCode::NOT_FOUND) +// .body(Body::from("[]")) // TODO: Better error message which frontend is actually setup to handle. +// .unwrap() +// } +// (&Method::GET, _) => { +// handle_http(ctx_fn, ProcedureKind::Query, req, &router, state.0) +// .await +// .into_response() +// } +// (&Method::POST, _) => { +// handle_http(ctx_fn, ProcedureKind::Mutation, req, &router, state.0) +// .await +// .into_response() +// } +// _ => unreachable!(), +// } +// } +// }, +// ), +// ) +// } + +// async fn handle_http( +// ctx_fn: TCtxFn, +// kind: ProcedureKind, +// req: Request, +// router: &Arc>, +// state: TState, +// ) -> impl IntoResponse +// where +// TCtx: Send + Sync + 'static, +// TCtxFn: TCtxFunc, +// TState: Send + Sync + 'static, +// { +// let procedure_name = req.uri().path()[1..].to_string(); // Has to be allocated because `TCtxFn` takes ownership of `req` +// let (parts, body) = req.into_parts(); +// let input = match parts.method { +// Method::GET => parts +// .uri +// .query() +// .map(|query| form_urlencoded::parse(query.as_bytes())) +// .and_then(|mut params| params.find(|e| e.0 == "input").map(|e| e.1)) +// .map(|v| serde_json::from_str(&v)) +// .unwrap_or(Ok(None as Option)), +// Method::POST => { +// // TODO: Limit body size? +// let body = to_bytes(body, usize::MAX).await.unwrap(); // TODO: error handling +// (!body.is_empty()) +// .then(|| serde_json::from_slice(body.to_vec().as_slice())) +// .unwrap_or(Ok(None)) +// } +// _ => unreachable!(), +// }; + +// let input = match input { +// Ok(input) => input, +// Err(_err) => { +// #[cfg(feature = "tracing")] +// tracing::error!( +// "Error passing parameters to operation '{}' with key '{:?}': {}", +// kind.to_str(), +// procedure_name, +// _err +// ); + +// return Response::builder() +// .status(StatusCode::NOT_FOUND) +// .header("Content-Type", "application/json") +// .body(Body::from(b"[]".as_slice())) +// .unwrap(); +// } +// }; + +// #[cfg(feature = "tracing")] +// tracing::debug!( +// "Executing operation '{}' with key '{}' with params {:?}", +// kind.to_str(), +// procedure_name, +// input +// ); + +// let mut resp = Sender::Response(None); + +// let ctx = match ctx_fn.exec(parts, &state).await { +// Ok(ctx) => ctx, +// Err(_err) => { +// #[cfg(feature = "tracing")] +// tracing::error!("Error executing context function: {}", _err); + +// return Response::builder() +// .status(StatusCode::INTERNAL_SERVER_ERROR) +// .header("Content-Type", "application/json") +// .body(Body::from(b"[]".as_slice())) +// .unwrap(); +// } +// }; + +// handle_json_rpc( +// ctx, +// jsonrpc::Request { +// jsonrpc: None, +// id: RequestId::Null, +// inner: match kind { +// ProcedureKind::Query => jsonrpc::RequestInner::Query { +// path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? +// input, +// }, +// ProcedureKind::Mutation => jsonrpc::RequestInner::Mutation { +// path: procedure_name.to_string(), // TODO: Lifetime instead of allocate? +// input, +// }, +// ProcedureKind::Subscription => { +// #[cfg(feature = "tracing")] +// tracing::error!("Attempted to execute a subscription operation with HTTP"); + +// return Response::builder() +// .status(StatusCode::INTERNAL_SERVER_ERROR) +// .header("Content-Type", "application/json") +// .body(Body::from(b"[]".as_slice())) +// .unwrap(); +// } +// }, +// }, +// router, +// &mut resp, +// &mut SubscriptionMap::None, +// ) +// .await; + +// match resp { +// Sender::Response(Some(resp)) => match serde_json::to_vec(&resp) { +// Ok(v) => Response::builder() +// .status(StatusCode::OK) +// .header("Content-Type", "application/json") +// .body(Body::from(v)) +// .unwrap(), +// Err(_err) => { +// #[cfg(feature = "tracing")] +// tracing::error!("Error serializing response: {}", _err); + +// Response::builder() +// .status(StatusCode::INTERNAL_SERVER_ERROR) +// .header("Content-Type", "application/json") +// .body(Body::from(b"[]".as_slice())) +// .unwrap() +// } +// }, +// _ => unreachable!(), +// } +// } + +// #[cfg(feature = "ws")] +// async fn handle_websocket( +// ctx_fn: TCtxFn, +// mut socket: axum::extract::ws::WebSocket, +// parts: Parts, +// router: Arc>, +// state: TState, +// ) where +// TCtx: Send + Sync + 'static, +// TCtxFn: TCtxFunc, +// TState: Send + Sync, +// { +// use axum::extract::ws::Message; +// use futures::StreamExt; +// use tokio::sync::mpsc; + +// #[cfg(feature = "tracing")] +// tracing::debug!("Accepting websocket connection"); + +// let mut subscriptions = HashMap::new(); +// let (mut tx, mut rx) = mpsc::channel::(100); + +// loop { +// tokio::select! { +// biased; // Note: Order is important here +// msg = rx.recv() => { +// match socket.send(Message::Text(match serde_json::to_string(&msg) { +// Ok(v) => v, +// Err(_err) => { +// #[cfg(feature = "tracing")] +// tracing::error!("Error serializing websocket message: {}", _err); + +// continue; +// } +// })).await { +// Ok(_) => {} +// Err(_err) => { +// #[cfg(feature = "tracing")] +// tracing::error!("Error sending websocket message: {}", _err); + +// continue; +// } +// } +// } +// msg = socket.next() => { +// match msg { +// Some(Ok(msg)) => { +// let res = match msg { +// Message::Text(text) => serde_json::from_str::(&text), +// Message::Binary(binary) => serde_json::from_slice(&binary), +// Message::Ping(_) | Message::Pong(_) | Message::Close(_) => { +// continue; +// } +// }; + +// match res.and_then(|v| match v.is_array() { +// true => serde_json::from_value::>(v), +// false => serde_json::from_value::(v).map(|v| vec![v]), +// }) { +// Ok(reqs) => { +// for request in reqs { +// let ctx = match ctx_fn.exec(parts.clone(), &state).await { +// Ok(ctx) => { +// ctx +// }, +// Err(_err) => { + +// #[cfg(feature = "tracing")] +// tracing::error!("Error executing context function: {}", _err); + +// continue; +// } +// }; + +// handle_json_rpc(ctx, request, &router, &mut Sender::Channel(&mut tx), +// &mut SubscriptionMap::Ref(&mut subscriptions)).await; +// } +// }, +// Err(_err) => { +// #[cfg(feature = "tracing")] +// tracing::error!("Error parsing websocket message: {}", _err); + +// // TODO: Send report of error to frontend + +// continue; +// } +// }; +// } +// Some(Err(_err)) => { +// #[cfg(feature = "tracing")] +// tracing::error!("Error in websocket: {}", _err); + +// // TODO: Send report of error to frontend + +// continue; +// }, +// None => { +// #[cfg(feature = "tracing")] +// tracing::debug!("Shutting down websocket connection"); + +// // TODO: Send report of error to frontend + +// return; +// }, +// } +// } +// } +// } +// } diff --git a/crates/axum/src/lib.rs b/crates/axum/src/lib.rs index 83da5950..c672ecb8 100644 --- a/crates/axum/src/lib.rs +++ b/crates/axum/src/lib.rs @@ -8,8 +8,7 @@ mod extractors; mod jsonrpc; mod jsonrpc_exec; -mod legacy; +// mod legacy; mod v2; -pub use legacy::endpoint; -pub use v2::endpoint2; +pub use v2::endpoint; diff --git a/crates/axum/src/v2.rs b/crates/axum/src/v2.rs index 66c4603a..5408bf13 100644 --- a/crates/axum/src/v2.rs +++ b/crates/axum/src/v2.rs @@ -11,18 +11,15 @@ use axum::{ use rspc_core::{Procedure, Procedures}; use serde_json::Value; -// TODO: Remove everything -use rspc::internal::ProcedureKind; - use crate::{ extractors::TCtxFunc, - jsonrpc::{self, RequestId}, + jsonrpc::{self, ProcedureKind, RequestId}, jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}, }; pub(crate) type Routes = HashMap>; -pub fn endpoint2( +pub fn endpoint( router: impl Into>, ctx_fn: TCtxFn, ) -> Router @@ -127,12 +124,7 @@ where Ok(input) => input, Err(_err) => { #[cfg(feature = "tracing")] - tracing::error!( - "Error passing parameters to operation '{}' with key '{:?}': {}", - kind.to_str(), - procedure_name, - _err - ); + tracing::error!("Error passing parameters to operation '{procedure_name}': {_err}"); return Response::builder() .status(StatusCode::NOT_FOUND) @@ -143,12 +135,7 @@ where }; #[cfg(feature = "tracing")] - tracing::debug!( - "Executing operation '{}' with key '{}' with params {:?}", - kind.to_str(), - procedure_name, - input - ); + tracing::debug!("Executing operation '{procedure_name}' with params {input:?}"); let mut resp = Sender::Response(None); diff --git a/crates/core/src/interop.rs b/crates/core/src/interop.rs new file mode 100644 index 00000000..ece5c62a --- /dev/null +++ b/crates/core/src/interop.rs @@ -0,0 +1,21 @@ +//! TODO: A temporary module to allow for interop between modern and legacy code. + +use std::sync::Arc; + +use crate::Procedures; + +// TODO: Remove this once we remove the legacy executor. +#[doc(hidden)] +#[derive(Clone)] +pub struct LegacyErrorInterop(pub String); +impl std::fmt::Debug for LegacyErrorInterop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "LegacyErrorInterop({})", self.0) + } +} +impl std::fmt::Display for LegacyErrorInterop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "LegacyErrorInterop({})", self.0) + } +} +impl std::error::Error for LegacyErrorInterop {} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 896a4030..765abc5e 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,11 +1,15 @@ -//! rspc-core +//! rspc-core: The core interface for rspc's ecosystem. //! //! TODO: Describe all the types and why the split? //! TODO: This is kinda like `tower::Service` //! TODO: Why this crate doesn't depend on Specta. //! TODO: Discuss the traits that need to be layered on for this to be useful. //! TODO: Discuss how middleware don't exist here. -// TODO: Crate icon and stuff +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", + html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" +)] // - Returning non-Serialize types (Eg. `File`) via `ProcedureStream`. // @@ -15,37 +19,24 @@ // - `Send` + `Sync` and the issues with single-threaded async runtimes // - `DynInput<'a, 'de>` should really be &'a Input<'de>` but that's hard. // - Finish `Debug` impls +// - Should `Procedure2::error` being `Option` or not? // - Crate documentation mod dyn_input; mod error; +mod interop; mod procedure; mod stream; pub use dyn_input::DynInput; pub use error::{DeserializeError, DowncastError, ProcedureError, ResolverError}; +pub use interop::LegacyErrorInterop; pub use procedure::Procedure; pub use stream::ProcedureStream; pub type Procedures = std::collections::BTreeMap>, Procedure>; -// TODO: Remove this once we remove the legacy executor. -#[doc(hidden)] -#[derive(Clone)] -pub struct LegacyErrorInterop(pub String); -impl std::fmt::Debug for LegacyErrorInterop { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "LegacyErrorInterop({})", self.0) - } -} -impl std::fmt::Display for LegacyErrorInterop { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "LegacyErrorInterop({})", self.0) - } -} -impl std::error::Error for LegacyErrorInterop {} - // TODO: The naming is horid. // Low-level concerns: // - `Procedure` - Holds the handler (and probably type information) diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index c68636ab..742ae33f 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -15,13 +15,11 @@ categories = ["web-programming", "asynchronous"] all-features = true rustdoc-args = ["--cfg", "docsrs"] -# TODO: Cleanup deps [dependencies] -rspc = { version = "0.3.1", path = "../.." } -specta = { version = "1.0.5", features = ["serde", "typescript"] } -serde = { version = "1.0.215", features = ["derive"] } -serde_json = "1.0.133" -thiserror = "2.0.3" -futures = "0.3.31" -tokio = { version = "1.41.1", features = ["sync", "rt", "macros"] } -tauri = { version = "2.1.1" } +rspc-core = { version = "0.0.0", path = "../core" } +tauri = "2" +serde_json = "1" + +# TODO: Drop these +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["sync"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. diff --git a/crates/tauri/src/jsonrpc.rs b/crates/tauri/src/jsonrpc.rs new file mode 100644 index 00000000..dfc0580c --- /dev/null +++ b/crates/tauri/src/jsonrpc.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(untagged)] +pub enum RequestId { + Null, + Number(u32), + String(String), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] // TODO: Type on this +pub struct Request { + pub jsonrpc: Option, // This is required in the JsonRPC spec but I make it optional. + pub id: RequestId, + #[serde(flatten)] + pub inner: RequestInner, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +pub enum RequestInner { + Query { + path: String, + input: Option, + }, + Mutation { + path: String, + input: Option, + }, + Subscription { + path: String, + input: (RequestId, Option), + }, + SubscriptionStop { + input: RequestId, + }, +} + +#[derive(Debug, Clone, Serialize)] // TODO: Add `specta::Type` when supported +pub struct Response { + pub jsonrpc: &'static str, + pub id: RequestId, + pub result: ResponseInner, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type", content = "data", rename_all = "camelCase")] +pub enum ResponseInner { + Event(Value), + Response(Value), + Error(JsonRPCError), +} + +#[derive(Debug, Clone, Serialize)] +pub struct JsonRPCError { + pub code: i32, + pub message: String, + pub data: Option, +} + +// TODO: BREAK + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcedureKind { + Query, + Mutation, + Subscription, +} + +impl ProcedureKind { + pub fn to_str(&self) -> &'static str { + match self { + ProcedureKind::Query => "query", + ProcedureKind::Mutation => "mutation", + ProcedureKind::Subscription => "subscription", + } + } +} diff --git a/crates/tauri/src/jsonrpc_exec.rs b/crates/tauri/src/jsonrpc_exec.rs new file mode 100644 index 00000000..a899edf3 --- /dev/null +++ b/crates/tauri/src/jsonrpc_exec.rs @@ -0,0 +1,352 @@ +use std::collections::HashMap; + +use rspc_core::ProcedureError; +use serde_json::Value; +use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; + +use crate::{jsonrpc, v2::Routes}; + +use super::jsonrpc::{RequestId, RequestInner, ResponseInner}; + +pub enum SubscriptionMap<'a> { + Ref(&'a mut HashMap>), + Mutex(&'a Mutex>>), + None, +} + +impl<'a> SubscriptionMap<'a> { + pub async fn has_subscription(&self, id: &RequestId) -> bool { + match self { + SubscriptionMap::Ref(map) => map.contains_key(id), + SubscriptionMap::Mutex(map) => { + let map = map.lock().await; + map.contains_key(id) + } + SubscriptionMap::None => unreachable!(), + } + } + + pub async fn insert(&mut self, id: RequestId, tx: oneshot::Sender<()>) { + match self { + SubscriptionMap::Ref(map) => { + map.insert(id, tx); + } + SubscriptionMap::Mutex(map) => { + let mut map = map.lock().await; + map.insert(id, tx); + } + SubscriptionMap::None => unreachable!(), + } + } + + pub async fn remove(&mut self, id: &RequestId) { + match self { + SubscriptionMap::Ref(map) => { + map.remove(id); + } + SubscriptionMap::Mutex(map) => { + let mut map = map.lock().await; + map.remove(id); + } + SubscriptionMap::None => unreachable!(), + } + } +} + +pub enum Sender<'a> { + Channel(&'a mut mpsc::Sender), + ResponseChannel(&'a mut mpsc::UnboundedSender), + Broadcast(&'a broadcast::Sender), + Response(Option), +} + +pub enum Sender2 { + Channel(mpsc::Sender), + ResponseChannel(mpsc::UnboundedSender), + Broadcast(broadcast::Sender), +} + +impl Sender2 { + pub async fn send( + &mut self, + resp: jsonrpc::Response, + ) -> Result<(), mpsc::error::SendError> { + match self { + Self::Channel(tx) => tx.send(resp).await?, + Self::ResponseChannel(tx) => tx.send(resp)?, + Self::Broadcast(tx) => { + let _ = tx.send(resp).map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + } + + Ok(()) + } +} + +impl<'a> Sender<'a> { + pub async fn send( + &mut self, + resp: jsonrpc::Response, + ) -> Result<(), mpsc::error::SendError> { + match self { + Self::Channel(tx) => tx.send(resp).await?, + Self::ResponseChannel(tx) => tx.send(resp)?, + Self::Broadcast(tx) => { + let _ = tx.send(resp).map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + Self::Response(r) => { + *r = Some(resp); + } + } + + Ok(()) + } + + pub fn sender2(&mut self) -> Sender2 { + match self { + Self::Channel(tx) => Sender2::Channel(tx.clone()), + Self::ResponseChannel(tx) => Sender2::ResponseChannel(tx.clone()), + Self::Broadcast(tx) => Sender2::Broadcast(tx.clone()), + Self::Response(_) => unreachable!(), + } + } +} + +pub async fn handle_json_rpc( + ctx: TCtx, + req: jsonrpc::Request, + routes: &Routes, + sender: &mut Sender<'_>, + subscriptions: &mut SubscriptionMap<'_>, +) where + TCtx: 'static, +{ + if req.jsonrpc.is_some() && req.jsonrpc.as_deref() != Some("2.0") { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "invalid JSON-RPC version".into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + + let (path, input, sub_id) = match req.inner { + RequestInner::Query { path, input } => (path, input, None), + RequestInner::Mutation { path, input } => (path, input, None), + RequestInner::Subscription { path, input } => (path, input.1, Some(input.0)), + RequestInner::SubscriptionStop { input } => { + subscriptions.remove(&input).await; + return; + } + }; + + let result = match routes.get(&path) { + Some(procedure) => { + let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); + + // It's really important this is before getting the first value + // Size hints can change after the first value is polled based on implementation. + let is_value = stream.size_hint() == (1, Some(1)); + + let first_value = stream.next(serde_json::value::Serializer).await; + + if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { + first_value + .expect("checked at if above") + .map(ResponseInner::Response) + .unwrap_or_else(|err| { + #[cfg(feature = "tracing")] + tracing::error!("Error executing operation: {:?}", err); + + ResponseInner::Error(match err { + ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error deserializing procedure arguments".to_string(), + data: None, + }, + ProcedureError::Downcast(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error downcasting procedure arguments".to_string(), + data: None, + }, + ProcedureError::Serializer(_) => jsonrpc::JsonRPCError { + code: 500, + message: "error serializing procedure result".to_string(), + data: None, + }, + ProcedureError::Resolver(resolver_error) => { + let legacy_error = resolver_error + .error() + .and_then(|v| v.downcast_ref::()) + .cloned(); + + jsonrpc::JsonRPCError { + code: resolver_error.status() as i32, + message: legacy_error + .map(|v| v.0.clone()) + // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. + .unwrap_or_else(|| resolver_error.to_string()), + data: None, + } + } + }) + }) + } else { + if matches!(sender, Sender::Response(_)) + || matches!(subscriptions, SubscriptionMap::None) + { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "unsupported metho".into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + + if let Some(id) = sub_id { + if matches!(id, RequestId::Null) { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "error creating subscription with null request id" + .into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } else if subscriptions.has_subscription(&id).await { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(jsonrpc::JsonRPCError { + code: 400, + message: "error creating subscription with duplicate id".into(), + data: None, + }), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {}", _err); + }); + } + + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + subscriptions.insert(id.clone(), shutdown_tx).await; + let mut sender2 = sender.sender2(); + tokio::spawn(async move { + match first_value { + Some(Ok(v)) => { + let _ = sender2 + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: id.clone(), + result: ResponseInner::Event(v), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {:?}", _err); + }); + } + Some(Err(_err)) => { + #[cfg(feature = "tracing")] + tracing::error!("Subscription error: {:?}", _err); + } + None => return, + } + + loop { + tokio::select! { + biased; // Note: Order matters + _ = &mut shutdown_rx => { + #[cfg(feature = "tracing")] + tracing::debug!("Removing subscription with id '{:?}'", id); + break; + } + v = stream.next(serde_json::value::Serializer) => { + match v { + Some(Ok(v)) => { + let _ = sender2.send(jsonrpc::Response { + jsonrpc: "2.0", + id: id.clone(), + result: ResponseInner::Event(v), + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {:?}", _err); + }); + } + Some(Err(_err)) => { + #[cfg(feature = "tracing")] + tracing::error!("Subscription error: {:?}", _err); + } + None => { + break; + } + } + } + } + } + }); + } + + return; + } + } + None => { + #[cfg(feature = "tracing")] + tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); + ResponseInner::Error(jsonrpc::JsonRPCError { + code: 404, + message: "the requested operation is not supported by this server".to_string(), + data: None, + }) + } + }; + + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id, + result, + }) + .await + .map_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!("Failed to send response: {:?}", _err); + }); +} diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index c5166dbc..7dd3eba4 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -7,25 +7,33 @@ use std::{borrow::Borrow, collections::HashMap, sync::Arc}; +use jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}; +use rspc_core::{Procedure, Procedures}; use tauri::{ + async_runtime::{spawn, Mutex}, plugin::{Builder, TauriPlugin}, - AppHandle, Emitter, Listener, Manager, Runtime, + AppHandle, Emitter, Listener, Runtime, }; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::mpsc; -use rspc::{ - internal::jsonrpc::{self, handle_json_rpc, Sender, SubscriptionMap}, - Router, -}; +mod jsonrpc; +mod jsonrpc_exec; + +pub(crate) type Routes = HashMap>; -pub fn plugin( - router: Arc>, +pub fn plugin( + router: impl Into>, ctx_fn: impl Fn(AppHandle) -> TCtx + Send + Sync + 'static, ) -> TauriPlugin where TCtx: Send + 'static, - TMeta: Send + Sync + 'static, { + let routes = router + .into() + .into_iter() + .map(|(key, value)| (key.join("."), value)) + .collect::>(); + Builder::new("rspc") .setup(|app_handle, _| { let (tx, mut rx) = mpsc::unbounded_channel::(); @@ -33,15 +41,15 @@ where // TODO: Don't keep using a tokio mutex. We don't need to hold it over the await point. let subscriptions = Arc::new(Mutex::new(HashMap::new())); - tokio::spawn({ + spawn({ let app_handle = app_handle.clone(); async move { while let Some(req) = rx.recv().await { let ctx = ctx_fn(app_handle.clone()); - let router = router.clone(); + let routes = routes.clone(); let mut resp_tx = resp_tx.clone(); let subscriptions = subscriptions.clone(); - tokio::spawn(async move { + spawn(async move { handle_json_rpc( ctx, req, @@ -57,7 +65,7 @@ where { let app_handle = app_handle.clone(); - tokio::spawn(async move { + spawn(async move { while let Some(event) = resp_rx.recv().await { let _ = app_handle .emit("plugin:rspc:transport:resp", event) diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 4791d658..6cbc9223 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -41,11 +41,9 @@ fn mount() -> rspc::Router { let inner = rspc::Router::::new().query("hello", |t| t(|_, _: ()| "Hello World!")); let router = rspc::Router::::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings-legacy.ts"), - )) .merge("nested.", inner) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .query("echo", |t| t(|_, v: String| v)) .query("error", |t| { t(|_, _: ()| { @@ -115,14 +113,7 @@ async fn main() { .route("/", get(|| async { "Hello 'rspc'!" })) .nest( "/rspc", - rspc_axum::endpoint2(routes.clone(), |parts: Parts| { - println!("Client requested operation '{}'", parts.uri.path()); - Ctx {} - }), - ) - .nest( - "/legacy", - rspc_axum::endpoint(mount().arced(), |parts: Parts| { + rspc_axum::endpoint(routes, |parts: Parts| { println!("Client requested operation '{}'", parts.uri.path()); Ctx {} }), diff --git a/src/interop.rs b/src/interop.rs index 4d8020be..633943b4 100644 --- a/src/interop.rs +++ b/src/interop.rs @@ -9,7 +9,7 @@ use specta::{ }; use crate::{ - internal::{jsonrpc::JsonRPCError, Layer, ProcedureKind, RequestContext, ValueOrStream}, + internal::{Layer, ProcedureKind, RequestContext, ValueOrStream}, router::literal_object, Procedure2, Router, Router2, }; @@ -57,7 +57,7 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { .insert(key.clone(), procedure) .is_some() { - panic!("Attempted to mount '{key:?}' multiple times. Note: rspc no longer supports different operations (query/mutation/subscription) with overlapping names.") + panic!("Attempted to mount '{key:?}' multiple times.\nrspc no longer supports different operations (query/mutation/subscription) with overlapping names.") } } @@ -65,7 +65,7 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { r } -fn layer_to_procedure( +pub(crate) fn layer_to_procedure( path: String, kind: ProcedureKind, value: Box>, @@ -102,11 +102,11 @@ fn layer_to_procedure( } Ok(ValueOrStream::Stream(s)) => Ok(s .map_err(|err| { - let err = JsonRPCError::from(err); + let err = crate::legacy::Error::from(err); ResolverError::new( - err.code.try_into().unwrap_or(500), - err, - None::, + err.code.to_status_code(), + (), /* typesafe errors aren't supported in legacy router */ + Some(rspc_core::LegacyErrorInterop(err.message)), ) }) .boxed()), diff --git a/src/legacy/error.rs b/src/legacy/error.rs index 3d513fea..e38b54a4 100644 --- a/src/legacy/error.rs +++ b/src/legacy/error.rs @@ -3,8 +3,6 @@ use std::{error, fmt, sync::Arc}; use serde::Serialize; use specta::Type; -use crate::internal::jsonrpc::JsonRPCError; - #[derive(thiserror::Error, Debug)] pub enum ExecError { #[error("the requested operation '{0}' is not supported by this server")] @@ -75,12 +73,12 @@ impl From for Error { } } -impl From for JsonRPCError { - fn from(err: ExecError) -> Self { - let x: Error = err.into(); - x.into() - } -} +// impl From for JsonRPCError { +// fn from(err: ExecError) -> Self { +// let x: Error = err.into(); +// x.into() +// } +// } #[derive(thiserror::Error, Debug)] pub enum ExportError { @@ -97,15 +95,15 @@ pub struct Error { pub(crate) cause: Option>, // We are using `Arc` instead of `Box` so we can clone the error cause `Clone` isn't dyn safe. } -impl From for JsonRPCError { - fn from(err: Error) -> Self { - JsonRPCError { - code: err.code.to_status_code() as i32, - message: err.message, - data: None, - } - } -} +// impl From for JsonRPCError { +// fn from(err: Error) -> Self { +// JsonRPCError { +// code: err.code.to_status_code() as i32, +// message: err.message, +// data: None, +// } +// } +// } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/legacy/internal/mod.rs b/src/legacy/internal/mod.rs index 9508e142..7afac255 100644 --- a/src/legacy/internal/mod.rs +++ b/src/legacy/internal/mod.rs @@ -1,6 +1,6 @@ //! Internal types which power rspc. The module provides no guarantee of compatibility between updates, so you should be careful rely on types from it. -mod jsonrpc_exec; +// mod jsonrpc_exec; mod middleware; mod procedure_builder; mod procedure_store; @@ -11,4 +11,4 @@ pub(crate) use procedure_store::*; // Used by `rspc_axum` pub use middleware::ProcedureKind; -pub mod jsonrpc; +// pub mod jsonrpc; diff --git a/src/legacy/router_builder.rs b/src/legacy/router_builder.rs index 8f3319e0..88155e9c 100644 --- a/src/legacy/router_builder.rs +++ b/src/legacy/router_builder.rs @@ -69,11 +69,11 @@ where TLayerCtx: Send + Sync + 'static, TMiddleware: MiddlewareBuilderLike + Send + 'static, { - /// Attach a configuration to the router. Calling this multiple times will overwrite the previous config. - pub fn config(mut self, config: Config) -> Self { - self.config = config; - self - } + // /// Attach a configuration to the router. Calling this multiple times will overwrite the previous config. + // pub fn config(mut self, config: Config) -> Self { + // self.config = config; + // self + // } pub fn middleware( self, From eb44eab0d729ef51bd6791425c4b0e7067d9de62 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 5 Dec 2024 00:01:50 +0800 Subject: [PATCH 09/67] fix examples --- crates/tauri/src/jsonrpc_exec.rs | 2 +- crates/tauri/src/lib.rs | 2 +- examples/Cargo.toml | 1 + examples/axum/src/main.rs | 2 - examples/src/bin/axum.rs | 41 +++++---- examples/src/bin/cookies.rs | 47 +++++----- examples/src/bin/global_context.rs | 31 ++++--- examples/src/bin/middleware.rs | 133 +++++++++++++++-------------- 8 files changed, 139 insertions(+), 120 deletions(-) diff --git a/crates/tauri/src/jsonrpc_exec.rs b/crates/tauri/src/jsonrpc_exec.rs index a899edf3..b3f81f09 100644 --- a/crates/tauri/src/jsonrpc_exec.rs +++ b/crates/tauri/src/jsonrpc_exec.rs @@ -4,7 +4,7 @@ use rspc_core::ProcedureError; use serde_json::Value; use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; -use crate::{jsonrpc, v2::Routes}; +use crate::{jsonrpc, Routes}; use super::jsonrpc::{RequestId, RequestInner, ResponseInner}; diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index 7dd3eba4..d923d9e5 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -53,7 +53,7 @@ where handle_json_rpc( ctx, req, - &router, + &routes, &mut Sender::ResponseChannel(&mut resp_tx), &mut SubscriptionMap::Mutex(subscriptions.borrow()), ) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 8e178967..0470f477 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] rspc = { path = "../" } specta = "=2.0.0-rc.20" +specta-typescript = "0.0.7" rspc-axum = { path = "../crates/axum" } async-stream = "0.3.6" axum = "0.7.9" diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 6cbc9223..1dbc41c4 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -101,8 +101,6 @@ async fn main() { ) .unwrap(); - // TODO: Export the legacy bindings from a new router - // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! let cors = CorsLayer::new() .allow_methods(Any) diff --git a/examples/src/bin/axum.rs b/examples/src/bin/axum.rs index 5c2e9756..a3017ace 100644 --- a/examples/src/bin/axum.rs +++ b/examples/src/bin/axum.rs @@ -6,29 +6,34 @@ use example::{basic, selection, subscriptions}; use axum::{http::request::Parts, routing::get}; use rspc::{Config, Router}; +use specta_typescript::Typescript; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] async fn main() { let r1 = Router::::new().query("demo", |t| t(|_, _: ()| "Merging Routers!")); - let router = - ::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts"), - )) - // Basic query - .query("version", |t| { - t(|_, _: ()| async move { env!("CARGO_PKG_VERSION") }) - }) - .merge("basic.", basic::mount()) - .merge("subscriptions.", subscriptions::mount()) - .merge("selection.", selection::mount()) - // This middleware changes the TCtx (context type) from `()` to `i32`. All routers being merge under need to take `i32` as their context type. - .middleware(|mw| mw.middleware(|ctx| async move { Ok(ctx.with_ctx(42i32)) })) - .merge("r1.", r1) - .build() - .arced(); // This function is a shortcut to wrap the router in an `Arc`. + let router = ::new() + // Basic query + .query("version", |t| { + t(|_, _: ()| async move { env!("CARGO_PKG_VERSION") }) + }) + .merge("basic.", basic::mount()) + .merge("subscriptions.", subscriptions::mount()) + .merge("selection.", selection::mount()) + // This middleware changes the TCtx (context type) from `()` to `i32`. All routers being merge under need to take `i32` as their context type. + .middleware(|mw| mw.middleware(|ctx| async move { Ok(ctx.with_ctx(42i32)) })) + .merge("r1.", r1) + .build(); + + let (routes, types) = rspc::Router2::from(router).build().unwrap(); + + types + .export_to( + Typescript::default(), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + ) + .unwrap(); let app = axum::Router::new() .with_state(()) @@ -36,7 +41,7 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - rspc_axum::endpoint(router, |parts: Parts| { + rspc_axum::endpoint(routes, |parts: Parts| { println!("Client requested operation '{}'", parts.uri.path()); () diff --git a/examples/src/bin/cookies.rs b/examples/src/bin/cookies.rs index 085bbb43..69161a0b 100644 --- a/examples/src/bin/cookies.rs +++ b/examples/src/bin/cookies.rs @@ -4,6 +4,7 @@ use std::{ops::Add, path::PathBuf}; use axum::routing::get; use rspc::Config; +use specta_typescript::Typescript; use time::OffsetDateTime; use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; use tower_http::cors::{Any, CorsLayer}; @@ -14,28 +15,32 @@ pub struct Ctx { #[tokio::main] async fn main() { - let router = - rspc::Router::::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), - )) - .query("getCookie", |t| { - t(|ctx, _: ()| { - ctx.cookies - .get("myDemoCookie") - .map(|c| c.value().to_string()) - }) + let router = rspc::Router::::new() + .query("getCookie", |t| { + t(|ctx, _: ()| { + ctx.cookies + .get("myDemoCookie") + .map(|c| c.value().to_string()) }) - .mutation("setCookie", |t| { - t(|ctx, new_value: String| { - let mut cookie = Cookie::new("myDemoCookie", new_value); - cookie.set_expires(Some(OffsetDateTime::now_utc().add(time::Duration::DAY))); - cookie.set_path("/"); // Ensure you have this or it will default to `/rspc` which will cause issues. - ctx.cookies.add(cookie); - }) + }) + .mutation("setCookie", |t| { + t(|ctx, new_value: String| { + let mut cookie = Cookie::new("myDemoCookie", new_value); + cookie.set_expires(Some(OffsetDateTime::now_utc().add(time::Duration::DAY))); + cookie.set_path("/"); // Ensure you have this or it will default to `/rspc` which will cause issues. + ctx.cookies.add(cookie); }) - .build() - .arced(); // This function is a shortcut to wrap the router in an `Arc`. + }) + .build(); + + let (routes, types) = rspc::Router2::from(router).build().unwrap(); + + types + .export_to( + Typescript::default(), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + ) + .unwrap(); let app = axum::Router::new() .with_state(()) @@ -43,7 +48,7 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - rspc_axum::endpoint(router, |cookies: Cookies| Ctx { cookies }), + rspc_axum::endpoint(routes, |cookies: Cookies| Ctx { cookies }), ) .layer(CookieManagerLayer::new()) // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! diff --git a/examples/src/bin/global_context.rs b/examples/src/bin/global_context.rs index 7c0d8868..ec9625dc 100644 --- a/examples/src/bin/global_context.rs +++ b/examples/src/bin/global_context.rs @@ -7,6 +7,7 @@ use std::{ }; use rspc::{Config, Router}; +use specta_typescript::Typescript; #[derive(Clone)] pub struct MyCtx { @@ -15,18 +16,22 @@ pub struct MyCtx { #[tokio::main] async fn main() { - let router = - Router::::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts"), - )) - // This is a query so it can be accessed in browser without frontend. A `mutation` - // shoudl be used if the method returns a side effect. - .query("hit", |t| { - t(|ctx, _: ()| ctx.count.fetch_add(1, Ordering::SeqCst)) - }) - .build() - .arced(); + let router = Router::::new() + // This is a query so it can be accessed in browser without frontend. A `mutation` + // shoudl be used if the method returns a side effect. + .query("hit", |t| { + t(|ctx, _: ()| ctx.count.fetch_add(1, Ordering::SeqCst)) + }) + .build(); + + let (routes, types) = rspc::Router2::from(router).build().unwrap(); + + types + .export_to( + Typescript::default(), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + ) + .unwrap(); // AtomicU16 provided interior mutability but if your type does not wrap it in an // `Arc>`. This could be your database connecton or any other value. @@ -34,7 +39,7 @@ async fn main() { let app = axum::Router::new().nest( "/rspc", - rspc_axum::endpoint(router, move || MyCtx { + rspc_axum::endpoint(routes, move || MyCtx { count: count.clone(), }), ); diff --git a/examples/src/bin/middleware.rs b/examples/src/bin/middleware.rs index 11316b51..3750165d 100644 --- a/examples/src/bin/middleware.rs +++ b/examples/src/bin/middleware.rs @@ -3,6 +3,7 @@ use std::{path::PathBuf, time::Duration}; use async_stream::stream; use axum::routing::get; use rspc::{Config, ErrorCode, MiddlewareContext, Router}; +use specta_typescript::Typescript; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; @@ -31,84 +32,88 @@ pub struct AuthenticatedCtx { #[tokio::main] async fn main() { - let router = - Router::::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts"), - )) - // Logger middleware - .middleware(|mw| { - mw.middleware(|mw| async move { - let state = (mw.req.clone(), mw.ctx.clone(), mw.input.clone()); - Ok(mw.with_state(state)) - }) - .resp(|state, result| async move { - println!( - "[LOG] req='{:?}' ctx='{:?}' input='{:?}' result='{:?}'", - state.0, state.1, state.2, result - ); - Ok(result) - }) + let router = Router::::new() + // Logger middleware + .middleware(|mw| { + mw.middleware(|mw| async move { + let state = (mw.req.clone(), mw.ctx.clone(), mw.input.clone()); + Ok(mw.with_state(state)) }) - .query("version", |t| { - t(|_ctx, _: ()| { - println!("ANOTHER QUERY"); - env!("CARGO_PKG_VERSION") - }) + .resp(|state, result| async move { + println!( + "[LOG] req='{:?}' ctx='{:?}' input='{:?}' result='{:?}'", + state.0, state.1, state.2, result + ); + Ok(result) }) - // Auth middleware - .middleware(|mw| { - mw.middleware(|mw| async move { - match mw.ctx.session_id { - Some(ref session_id) => { - let user = db_get_user_from_session(session_id).await; - Ok(mw.with_ctx(AuthenticatedCtx { user })) - } - None => Err(rspc::Error::new( - ErrorCode::Unauthorized, - "Unauthorized".into(), - )), + }) + .query("version", |t| { + t(|_ctx, _: ()| { + println!("ANOTHER QUERY"); + env!("CARGO_PKG_VERSION") + }) + }) + // Auth middleware + .middleware(|mw| { + mw.middleware(|mw| async move { + match mw.ctx.session_id { + Some(ref session_id) => { + let user = db_get_user_from_session(session_id).await; + Ok(mw.with_ctx(AuthenticatedCtx { user })) } - }) + None => Err(rspc::Error::new( + ErrorCode::Unauthorized, + "Unauthorized".into(), + )), + } }) - .query("another", |t| { - t(|_, _: ()| { - println!("ANOTHER QUERY"); - "Another Result!" - }) + }) + .query("another", |t| { + t(|_, _: ()| { + println!("ANOTHER QUERY"); + "Another Result!" }) - .subscription("subscriptions.pings", |t| { - t(|_ctx, _args: ()| { - stream! { - println!("Client subscribed to 'pings'"); - for i in 0..5 { - println!("Sending ping {}", i); - yield "ping".to_string(); - sleep(Duration::from_secs(1)).await; - } + }) + .subscription("subscriptions.pings", |t| { + t(|_ctx, _args: ()| { + stream! { + println!("Client subscribed to 'pings'"); + for i in 0..5 { + println!("Sending ping {}", i); + yield "ping".to_string(); + sleep(Duration::from_secs(1)).await; } - }) + } }) - // Reject all middleware - .middleware(|mw| { - mw.middleware(|_mw| async move { - Err(rspc::Error::new( - ErrorCode::Unauthorized, - "Unauthorized".into(), - )) as Result, _> - }) + }) + // Reject all middleware + .middleware(|mw| { + mw.middleware(|_mw| async move { + Err(rspc::Error::new( + ErrorCode::Unauthorized, + "Unauthorized".into(), + )) as Result, _> }) - // Plugin middleware // TODO: Coming soon! - // .middleware(|mw| mw.openapi(OpenAPIConfig {})) - .build() - .arced(); + }) + // Plugin middleware // TODO: Coming soon! + // .middleware(|mw| mw.openapi(OpenAPIConfig {})) + .build(); + + let (routes, types) = rspc::Router2::from(router).build().unwrap(); + + types + .export_to( + Typescript::default(), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + ) + .unwrap(); let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - rspc_axum::endpoint(router, || UnauthenticatedContext { + rspc_axum::endpoint(routes, || UnauthenticatedContext { session_id: Some("abc".into()), // Change this line to control whether you are authenticated and can access the "another" query. }), ) From 2eca8eadbe7340e52aff6c542e1ed6f36b835689 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 5 Dec 2024 00:03:45 +0800 Subject: [PATCH 10/67] core crate publishing metadata --- crates/core/Cargo.toml | 10 ++++++++-- crates/core/src/lib.rs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index df729d06..d9934192 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "rspc-core" -version = "0.0.0" +description = "Core interface for rspc" +version = "0.0.1" +authors = ["Oscar Beaumont "] edition = "2021" -publish = false # TODO: Metadata +license = "MIT" +repository = "https://github.com/specta-rs/rspc" +documentation = "https://docs.rs/rspc-core" +keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] +categories = ["web-programming", "asynchronous"] # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 765abc5e..04fa64de 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,4 +1,4 @@ -//! rspc-core: The core interface for rspc's ecosystem. +//! rspc-core: Core interface for [rspc](https://docs.rs/rspc). //! //! TODO: Describe all the types and why the split? //! TODO: This is kinda like `tower::Service` From 471d9efda96df0b1173ae605e41a6b5d7caf2459 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 5 Dec 2024 00:59:24 +0800 Subject: [PATCH 11/67] fix header + upgrade Specta --- Cargo.toml | 6 +++--- crates/axum/Cargo.toml | 2 +- crates/tauri/Cargo.toml | 2 +- examples/axum/src/main.rs | 6 +++--- src/legacy/resolver.rs | 13 ++++++------- src/legacy/router.rs | 10 +++++----- src/legacy/router_builder.rs | 5 ++--- src/router.rs | 17 ++++++++++++----- 8 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4b7263b6..dc962c8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,9 +52,9 @@ typeid = "1.0.2" members = ["./crates/*", "./examples", "./examples/axum", "crates/core"] [patch.crates-io] -specta = { git = "https://github.com/specta-rs/specta", rev = "a7bbe7b579c448ff4bcefb0b44489d48d8fd37ee" } -specta-util = { git = "https://github.com/specta-rs/specta", rev = "a7bbe7b579c448ff4bcefb0b44489d48d8fd37ee" } -specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "a7bbe7b579c448ff4bcefb0b44489d48d8fd37ee" } +specta = { git = "https://github.com/specta-rs/specta", rev = "d13f097a43ca8170eaca33df84a2f368e08d63ab" } +specta-util = { git = "https://github.com/specta-rs/specta", rev = "d13f097a43ca8170eaca33df84a2f368e08d63ab" } +specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "d13f097a43ca8170eaca33df84a2f368e08d63ab" } # specta = { path = "/Users/oscar/Desktop/specta/specta" } # specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } diff --git a/crates/axum/Cargo.toml b/crates/axum/Cargo.toml index e630e3e4..07dd42c9 100644 --- a/crates/axum/Cargo.toml +++ b/crates/axum/Cargo.toml @@ -20,7 +20,7 @@ default = [] ws = ["axum/ws"] [dependencies] -rspc-core = { version = "0.0.0", path = "../core" } +rspc-core = { version = "0.0.1", path = "../core" } axum = "0.7.9" serde_json = "1" diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 742ae33f..6402f19d 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -16,7 +16,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -rspc-core = { version = "0.0.0", path = "../core" } +rspc-core = { version = "0.0.1", path = "../core" } tauri = "2" serde_json = "1" diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 1dbc41c4..589f1508 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, time::Duration}; use async_stream::stream; use axum::{http::request::Parts, routing::get}; -use rspc::{Config, Router2}; +use rspc::Router2; use serde::Serialize; use specta::Type; use specta_typescript::Typescript; @@ -43,7 +43,7 @@ fn mount() -> rspc::Router { let router = rspc::Router::::new() .merge("nested.", inner) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) - .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + // .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .query("echo", |t| t(|_, v: String| v)) .query("error", |t| { t(|_, _: ()| { @@ -96,7 +96,7 @@ async fn main() { .export_to( Typescript::default(), // .formatter(specta_typescript::formatter::prettier), - // .header("// My custom header\n") + // .header("// My custom header"), PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), ) .unwrap(); diff --git a/src/legacy/resolver.rs b/src/legacy/resolver.rs index 145dc4a1..3f335f66 100644 --- a/src/legacy/resolver.rs +++ b/src/legacy/resolver.rs @@ -3,8 +3,7 @@ use std::marker::PhantomData; use futures::{Stream, StreamExt}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::Value; -use specta::Type; -use specta::TypeMap; +use specta::{Type, TypeCollection}; use crate::{ internal::{LayerResult, ProcedureDataType}, @@ -16,7 +15,7 @@ pub trait Resolver { fn exec(&self, ctx: TCtx, input: Value) -> Result; - fn typedef(defs: &mut TypeMap) -> ProcedureDataType; + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType; } // pub struct NoArgMarker(/* private */ PhantomData); @@ -84,7 +83,7 @@ where self(ctx, input).into_layer_result() } - fn typedef(defs: &mut TypeMap) -> ProcedureDataType { + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { typedef::(defs) } } @@ -92,7 +91,7 @@ where pub trait StreamResolver { fn exec(&self, ctx: TCtx, input: Value) -> Result; - fn typedef(defs: &mut TypeMap) -> ProcedureDataType; + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType; } pub struct DoubleArgStreamMarker( @@ -113,12 +112,12 @@ where })))) } - fn typedef(defs: &mut TypeMap) -> ProcedureDataType { + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { typedef::(defs) } } -pub fn typedef(defs: &mut TypeMap) -> ProcedureDataType { +pub fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { let arg_ty = TArg::reference(defs, &[]).inner; let result_ty = TResult::reference(defs, &[]).inner; diff --git a/src/legacy/router.rs b/src/legacy/router.rs index 5a61adb8..d092d955 100644 --- a/src/legacy/router.rs +++ b/src/legacy/router.rs @@ -10,7 +10,7 @@ use std::{ use futures::Stream; use serde_json::Value; -use specta::{datatype::FunctionResultVariant, DataType, TypeMap}; +use specta::{datatype::FunctionResultVariant, DataType, TypeCollection}; use specta_typescript::{self as ts, datatype, export_named_datatype, Typescript}; use crate::{ @@ -27,7 +27,7 @@ where pub(crate) queries: ProcedureStore, pub(crate) mutations: ProcedureStore, pub(crate) subscriptions: ProcedureStore, - pub(crate) type_map: TypeMap, + pub(crate) type_map: TypeCollection, pub(crate) phantom: PhantomData, } @@ -107,11 +107,11 @@ where } #[deprecated = "Use `Self::type_map`"] - pub fn typ_store(&self) -> TypeMap { + pub fn typ_store(&self) -> TypeCollection { self.type_map.clone() } - pub fn type_map(&self) -> TypeMap { + pub fn type_map(&self) -> TypeCollection { self.type_map.clone() } @@ -178,7 +178,7 @@ export type Procedures = {{ fn generate_procedures_ts( config: &Typescript, procedures: &BTreeMap>, - type_map: &TypeMap, + type_map: &TypeCollection, ) -> String { match procedures.len() { 0 => "never".to_string(), diff --git a/src/legacy/router_builder.rs b/src/legacy/router_builder.rs index 88155e9c..872bef45 100644 --- a/src/legacy/router_builder.rs +++ b/src/legacy/router_builder.rs @@ -2,8 +2,7 @@ use std::marker::PhantomData; use futures::Stream; use serde::{de::DeserializeOwned, Serialize}; -use specta::Type; -use specta::TypeMap; +use specta::{Type, TypeCollection}; use crate::{ internal::{ @@ -28,7 +27,7 @@ pub struct RouterBuilder< queries: ProcedureStore, mutations: ProcedureStore, subscriptions: ProcedureStore, - type_map: TypeMap, + type_map: TypeCollection, phantom: PhantomData, } diff --git a/src/router.rs b/src/router.rs index 62239330..4ba8740c 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::BTreeMap, fmt}; -use specta::{datatype::DataType, NamedType, SpectaID, Type, TypeMap}; +use specta::{datatype::DataType, NamedType, SpectaID, Type, TypeCollection}; use rspc_core::Procedures; @@ -9,7 +9,7 @@ use crate::{internal::ProcedureKind, interop::construct_legacy_bindings_type, Pr /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { setup: Vec>, - types: TypeMap, + types: TypeCollection, procedures: BTreeMap>, Procedure2>, } @@ -17,7 +17,8 @@ impl Default for Router2 { fn default() -> Self { Self { setup: Default::default(), - types: Default::default(), + types: TypeCollection::default() + .with_header("// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.\n"), procedures: Default::default(), } } @@ -68,7 +69,13 @@ impl Router2 { pub fn build( mut self, - ) -> Result<(impl Into> + Clone + fmt::Debug, TypeMap), ()> { + ) -> Result< + ( + impl Into> + Clone + fmt::Debug, + TypeCollection, + ), + (), + > { let mut state = (); for setup in self.setup { setup(&mut state); @@ -232,7 +239,7 @@ impl Router2 { &mut self.procedures } - pub(crate) fn interop_types(&mut self) -> &mut TypeMap { + pub(crate) fn interop_types(&mut self) -> &mut TypeCollection { &mut self.types } } From e2a381dda09a75961f656a86d9a98983ce70ad0c Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 6 Dec 2024 13:14:03 +0800 Subject: [PATCH 12/67] `rspc-client` prototype + new type exporting setup --- .gitignore | 3 - Cargo.toml | 25 ++- crates/client/Cargo.toml | 23 +++ crates/client/src/lib.rs | 123 ++++++++++++ examples/axum/Cargo.toml | 3 +- examples/axum/src/main.rs | 21 +- examples/bindings.ts | 6 + examples/bindings_t.d.ts | 12 ++ examples/bindings_t.d.ts.map | 1 + examples/client/Cargo.toml | 9 + examples/client/src/bindings.rs | 75 +++++++ examples/client/src/main.rs | 23 +++ examples/src/bin/axum.rs | 2 +- src/interop.rs | 57 ++++-- src/languages.rs | 13 ++ src/languages/rust.rs | 110 ++++++++++ src/languages/typescript.rs | 344 ++++++++++++++++++++++++++++++++ src/lib.rs | 7 +- src/procedure.rs | 24 ++- src/procedure_kind.rs | 38 ++-- src/router.rs | 117 ++--------- src/types.rs | 16 ++ 22 files changed, 886 insertions(+), 166 deletions(-) create mode 100644 crates/client/Cargo.toml create mode 100644 crates/client/src/lib.rs create mode 100644 examples/bindings.ts create mode 100644 examples/bindings_t.d.ts create mode 100644 examples/bindings_t.d.ts.map create mode 100644 examples/client/Cargo.toml create mode 100644 examples/client/src/bindings.rs create mode 100644 examples/client/src/main.rs create mode 100644 src/languages.rs create mode 100644 src/languages/rust.rs create mode 100644 src/languages/typescript.rs create mode 100644 src/types.rs diff --git a/.gitignore b/.gitignore index d3fa8e50..1890973a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,6 @@ Cargo.lock # System Files .DS_Store -# Typescript bindings exported by the examples -bindings.ts - # Node node_modules .pnpm-debug.log* diff --git a/Cargo.toml b/Cargo.toml index dc962c8f..7d228d3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] tracing = ["dep:tracing"] +typescript = [] +rust = [] + [dependencies] # Public rspc-core = { path = "./crates/core" } @@ -30,32 +33,34 @@ specta = { version = "=2.0.0-rc.20", features = [ "serde", "serde_json", ] } # TODO: Drop all features -specta-util = "0.0.7" # Private serde-value = "0.7" erased-serde = "0.4" +specta-typescript = { version = "=0.0.7", features = [] } +specta-rust = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } # Temporary # TODO: Remove -specta-typescript = { version = "=0.0.7", features = [] } serde_json = "1.0.133" # TODO: Drop this thiserror = "2.0.3" tokio = { version = "1.41.1", features = ["sync", "rt", "macros"] } +specta-serde = "0.0.7" tracing = { version = "0.1.40", optional = true } -transient = "0.4.1" -better_any = "0.2.0" +# transient = "0.4.1" +# better_any = "0.2.0" # https://github.com/rust-lang/rust/issues/77125 -typeid = "1.0.2" +# typeid = "1.0.2" [workspace] -members = ["./crates/*", "./examples", "./examples/axum", "crates/core"] +members = ["./crates/*", "./examples", "./examples/axum", "./examples/client", "crates/core"] [patch.crates-io] -specta = { git = "https://github.com/specta-rs/specta", rev = "d13f097a43ca8170eaca33df84a2f368e08d63ab" } -specta-util = { git = "https://github.com/specta-rs/specta", rev = "d13f097a43ca8170eaca33df84a2f368e08d63ab" } -specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "d13f097a43ca8170eaca33df84a2f368e08d63ab" } +specta = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } +specta-serde = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } +specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } # specta = { path = "/Users/oscar/Desktop/specta/specta" } -# specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } # specta-typescript = { path = "/Users/oscar/Desktop/specta/specta-typescript" } +# specta-serde = { path = "/Users/oscar/Desktop/specta/specta-serde" } +# specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml new file mode 100644 index 00000000..c3262314 --- /dev/null +++ b/crates/client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rspc-client" +description = "Rust client for rspc" +version = "0.0.1" +authors = ["Oscar Beaumont "] +edition = "2021" +# license = "MIT" +repository = "https://github.com/specta-rs/rspc" +documentation = "https://docs.rs/rspc-client" +keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] +categories = ["web-programming", "asynchronous"] +publish = false # TODO: This is still very unstable + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +reqwest = { version = "0.12.9", features = ["json"] } +rspc-core = { version = "0.0.1", path = "../core" } +serde = { version = "1.0.215", features = ["derive"] } # TODO: Drop derive feature? +serde_json = "1.0.133" diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs new file mode 100644 index 00000000..7c80ac3f --- /dev/null +++ b/crates/client/src/lib.rs @@ -0,0 +1,123 @@ +//! rspc-client: Rust client for [rspc](https://docs.rs/rspc). +//! +//! # This is really unstable you should be careful using it! +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", + html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" +)] + +// TODO: Change `exec` to `query`/`mutation`/`subscription` with a bound on the incoming operation? +// TODO: Error handling working (typesafe errors + internal errors being throw not panic) +// TODO: Maybe make inner client a trait so the user can use any HTTP client. +// TODO: Treating `reqwest` as a public or private dependency? +// TODO: Supporting transport formats other than JSON? +// TODO: Is this safe to use from the same app that defines the router? If not we should try and forbid it with a compiler error. + +use std::borrow::Cow; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +/// TODO +#[derive(Debug, Clone)] +pub struct Client { + url: Cow<'static, str>, + client: reqwest::Client, +} + +impl Client { + pub fn new(url: impl Into>) -> Self { + Self { + url: url.into(), + client: reqwest::Client::builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )) + .build() + .unwrap(), // TODO: Can this fail? + } + } + + pub async fn exec(&self, input: O::Input) -> Result { + let url = format!( + "{}{}{}", + self.url, + if self.url.ends_with("/") { "" } else { "/" }, + O::KEY + ); + + match O::KIND { + ProcedureKind::Query => { + let res = self + .client + .get(&url) + .query(&[("input", serde_json::to_string(&input).unwrap())]) // TODO: Error handling + .send() + .await + .unwrap(); // TODO: Error handling + + // TODO: This is just ignoring error handling. This client is designed as a prototype not to be used. + #[derive(Deserialize)] + pub struct LegacyFormat { + result: LegacyResult, + } + #[derive(Deserialize)] + pub struct LegacyResult { + #[serde(rename = "type")] + _type: String, + data: serde_json::Value, + } + + let result: LegacyFormat = res.json().await.unwrap(); + Ok(serde_json::from_value(result.result.data).unwrap()) + } + ProcedureKind::Mutation => { + let res = self + .client + .post(&url) + .body(serde_json::to_string(&input).unwrap()) // TODO: Error handling + .send() + .await + .unwrap(); // TODO: Error handling + + // TODO: This is just ignoring error handling. This client is designed as a prototype not to be used. + #[derive(Deserialize)] + pub struct LegacyFormat { + result: LegacyResult, + } + #[derive(Deserialize)] + pub struct LegacyResult { + #[serde(rename = "type")] + _type: String, + data: serde_json::Value, + } + + let result: LegacyFormat = res.json().await.unwrap(); + Ok(serde_json::from_value(result.result.data).unwrap()) + } + ProcedureKind::Subscription => { + // TODO: We will need to implement websocket support somehow. https://github.com/seanmonstar/reqwest/issues/864 + // TODO: Returning a stream + unimplemented!("subscriptions are not supported yet!"); + } + } + } +} + +pub trait Procedure { + type Input: Serialize; + type Output: DeserializeOwned; + type Error: DeserializeOwned; + + const KEY: &'static str; + const KIND: ProcedureKind; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProcedureKind { + Query, + Mutation, + Subscription, +} diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 5d52e96b..39cbeea3 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../../" } +rspc = { path = "../../", features = ["typescript", "rust"] } rspc-axum = { path = "../../crates/axum", features = ["ws"] } tokio = { version = "1.41.1", features = ["full"] } async-stream = "0.3.6" @@ -13,7 +13,6 @@ axum = { version = "0.7.9", features = ["ws"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } -specta-typescript = "0.0.7" serde = { version = "1.0.215", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = [ "derive", diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 589f1508..1eb3f84e 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -5,7 +5,6 @@ use axum::{http::request::Parts, routing::get}; use rspc::Router2; use serde::Serialize; use specta::Type; -use specta_typescript::Typescript; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; @@ -60,7 +59,7 @@ fn mount() -> rspc::Router { v }) }) - .mutation("anotherOne", |t| t(|_, v: String| Ok(MyCustomType(v)))) + // .mutation("anotherOne", |t| t(|_, v: String| Ok(MyCustomType(v)))) .subscription("pings", |t| { t(|_ctx, _args: ()| { stream! { @@ -92,12 +91,22 @@ fn mount() -> rspc::Router { async fn main() { let (routes, types) = Router2::from(mount()).build().unwrap(); - types + rspc::Typescript::default() + // .formatter(specta_typescript::formatter::prettier), + .header("// My custom header") + .enable_source_maps() .export_to( - Typescript::default(), - // .formatter(specta_typescript::formatter::prettier), - // .header("// My custom header"), PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, + ) + .unwrap(); + + // Be aware this is very experimental and doesn't support many types yet. + rspc::Rust::default() + // .header("// My custom header") + .export_to( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../client/src/bindings.rs"), + &types, ) .unwrap(); diff --git a/examples/bindings.ts b/examples/bindings.ts new file mode 100644 index 00000000..bf4b5303 --- /dev/null +++ b/examples/bindings.ts @@ -0,0 +1,6 @@ +// My custom header +// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. + +export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } + +export { Procedures } from './bindings_t'; \ No newline at end of file diff --git a/examples/bindings_t.d.ts b/examples/bindings_t.d.ts new file mode 100644 index 00000000..f98b5417 --- /dev/null +++ b/examples/bindings_t.d.ts @@ -0,0 +1,12 @@ +// My custom header +// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. + +export type Procedures = { + : {}, + : {}, + : {}, + : {}, + : {}, + : {}, + : {}, +} diff --git a/examples/bindings_t.d.ts.map b/examples/bindings_t.d.ts.map new file mode 100644 index 00000000..e3ee8306 --- /dev/null +++ b/examples/bindings_t.d.ts.map @@ -0,0 +1 @@ +{"file":"bindings_t.d.ts","mappings":"","names":[],"sources":[],"version":3} \ No newline at end of file diff --git a/examples/client/Cargo.toml b/examples/client/Cargo.toml new file mode 100644 index 00000000..1356633d --- /dev/null +++ b/examples/client/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example-client" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +rspc-client = { path = "../../crates/client" } +tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/client/src/bindings.rs b/examples/client/src/bindings.rs new file mode 100644 index 00000000..48518278 --- /dev/null +++ b/examples/client/src/bindings.rs @@ -0,0 +1,75 @@ +//! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. + +pub struct sendMsg; + +impl rspc_client::Procedure for sendMsg { + type Input = String; + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Mutation; + const KEY: &'static str = "sendMsg"; +} + +pub struct version; + +impl rspc_client::Procedure for version { + type Input = (); + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "version"; +} + +pub struct transformMe; + +impl rspc_client::Procedure for transformMe { + type Input = (); + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "transformMe"; +} + +pub struct error; + +impl rspc_client::Procedure for error { + type Input = (); + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "error"; +} + +pub struct echo; + +impl rspc_client::Procedure for echo { + type Input = String; + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "echo"; +} + + +pub mod hello { +pub struct hello; + +impl rspc_client::Procedure for hello { + type Input = (); + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "nested.hello"; +} + +} +pub struct pings; + +impl rspc_client::Procedure for pings { + type Input = (); + type Output = String; + type Error = (); + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Subscription; + const KEY: &'static str = "pings"; +} + diff --git a/examples/client/src/main.rs b/examples/client/src/main.rs new file mode 100644 index 00000000..b2901a82 --- /dev/null +++ b/examples/client/src/main.rs @@ -0,0 +1,23 @@ +// This file is generated by the Axum example at `./examples/axum. +mod bindings; + +#[tokio::main] +async fn main() { + let client = rspc_client::Client::new("http://[::]:4000/rspc"); + + println!("{:?}", client.exec::(()).await); + println!( + "{:?}", + client + .exec::("Some random string!".into()) + .await + ); + println!( + "{:?}", + client + .exec::("Hello from rspc Rust client!".into()) + .await + ); + + // TODO: Subscription example. +} diff --git a/examples/src/bin/axum.rs b/examples/src/bin/axum.rs index a3017ace..8f4eb0b9 100644 --- a/examples/src/bin/axum.rs +++ b/examples/src/bin/axum.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use example::{basic, selection, subscriptions}; use axum::{http::request::Parts, routing::get}; -use rspc::{Config, Router}; +use rspc::Router; use specta_typescript::Typescript; use tower_http::cors::{Any, CorsLayer}; diff --git a/src/interop.rs b/src/interop.rs index 633943b4..7ef3a8a5 100644 --- a/src/interop.rs +++ b/src/interop.rs @@ -1,16 +1,16 @@ -use std::{borrow::Cow, collections::BTreeMap}; +use std::{borrow::Cow, collections::BTreeMap, panic::Location}; -use futures::{stream, FutureExt, Stream, StreamExt, TryStreamExt}; +use futures::{stream, StreamExt, TryStreamExt}; use rspc_core::{ProcedureStream, ResolverError}; use serde_json::Value; use specta::{ datatype::{DataType, EnumRepr, EnumVariant, LiteralType}, - NamedType, Type, + NamedType, SpectaID, Type, }; use crate::{ internal::{Layer, ProcedureKind, RequestContext, ValueOrStream}, - router::literal_object, + procedure::ProcedureType, Procedure2, Router, Router2, }; @@ -43,10 +43,13 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { .collect::>>(), Procedure2 { setup: Default::default(), - kind, - input: p.ty.arg_ty, - result: p.ty.result_ty, - error: specta::datatype::DataType::Unknown, + ty: ProcedureType { + kind, + input: p.ty.arg_ty, + output: p.ty.result_ty, + error: specta::datatype::DataType::Unknown, + }, + // location: Location::caller().clone(), // TODO: This needs to actually be correct inner: layer_to_procedure(key, kind, p.exec), }, ) @@ -124,12 +127,12 @@ pub(crate) fn layer_to_procedure( }) } -fn map_method( +fn map_method( kind: ProcedureKind, - p: &BTreeMap>, Procedure2>, + p: &BTreeMap>, ProcedureType>, ) -> Vec<(Cow<'static, str>, EnumVariant)> { p.iter() - .filter(|(_, p)| p.kind() == kind) + .filter(|(_, p)| p.kind == kind) .map(|(key, p)| { let key = key.join(".").to_string(); ( @@ -150,7 +153,7 @@ fn map_method( vec![ ("key".into(), LiteralType::String(key.clone()).into()), ("input".into(), p.input.clone()), - ("result".into(), p.result.clone()), + ("result".into(), p.output.clone()), ] .into_iter(), )), @@ -163,8 +166,8 @@ fn map_method( } // TODO: Remove this block with the interop system -pub(crate) fn construct_legacy_bindings_type( - p: &BTreeMap>, Procedure2>, +pub(crate) fn construct_legacy_bindings_type( + p: &BTreeMap>, ProcedureType>, ) -> Vec<(Cow<'static, str>, DataType)> { #[derive(Type)] struct Queries; @@ -212,3 +215,29 @@ pub(crate) fn construct_legacy_bindings_type( ), ] } + +// TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` +pub(crate) fn literal_object( + name: Cow<'static, str>, + sid: Option, + fields: impl Iterator, DataType)>, +) -> DataType { + specta::internal::construct::r#struct( + name, + sid, + Default::default(), + specta::internal::construct::struct_named( + fields + .into_iter() + .map(|(name, ty)| { + ( + name.into(), + specta::internal::construct::field(false, false, None, "".into(), Some(ty)), + ) + }) + .collect(), + None, + ), + ) + .into() +} diff --git a/src/languages.rs b/src/languages.rs new file mode 100644 index 00000000..7c55c28d --- /dev/null +++ b/src/languages.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "rust")] +#[cfg_attr(docsrs, doc(cfg(feature = "rust")))] +mod rust; +#[cfg(feature = "typescript")] +#[cfg_attr(docsrs, doc(cfg(feature = "typescript")))] +mod typescript; + +#[cfg(feature = "rust")] +#[cfg_attr(docsrs, doc(cfg(feature = "rust")))] +pub use rust::Rust; +#[cfg(feature = "typescript")] +#[cfg_attr(docsrs, doc(cfg(feature = "typescript")))] +pub use typescript::Typescript; diff --git a/src/languages/rust.rs b/src/languages/rust.rs new file mode 100644 index 00000000..7dcafe9f --- /dev/null +++ b/src/languages/rust.rs @@ -0,0 +1,110 @@ +use std::{borrow::Cow, collections::HashMap, path::Path}; + +use specta::datatype::DataType; +use specta_typescript::ExportError; + +use crate::{procedure::ProcedureType, ProcedureKind, Types}; + +pub struct Rust(()); // TODO: specta_rust::Rust + +// TODO: Traits - `Debug`, `Clone`, etc + +impl Default for Rust { + fn default() -> Self { + Self(()) // TODO: specta_typescript::Typescript::default().framework_header("// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.") + } +} + +impl Rust { + // TODO: Clone all methods from `specta_rust::Rust` + + pub fn export_to(&self, path: impl AsRef, types: &Types) -> Result<(), ExportError> { + std::fs::write(path, self.export(types)?)?; + // TODO: Format file + Ok(()) + } + + pub fn export(&self, types: &Types) -> Result { + println!("WARNING: `rspc::Rust` is an unstable feature! Use at your own discretion!"); + + let mut s = "//! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.\n\n".to_string(); + + // TODO: Move to `specta_rust::Rust` which should handle this like we do with Typescript. + for (_, ty) in types.types.into_iter() { + s.push_str(&specta_rust::export_named_datatype(ty).unwrap()) + } + + // TODO: disabling warning on the output??? + + // TODO: We should probs always store procedures as tree + let mut tree = HashMap::new(); + for (key, ty) in types.procedures.iter() { + let mut current = &mut tree; + for part in &key[..(key.len() - 1)] { + let a = current + .entry(part.clone()) + .or_insert_with(|| Todo::Module(HashMap::new())); + match a { + Todo::Procedure(_, _, _) => todo!(), + Todo::Module(hash_map) => current = hash_map, + } + } + current.insert( + key[key.len() - 1].clone(), + Todo::Procedure( + key.join(".").to_string(), + key[key.len() - 1].clone(), + ty.clone(), + ), + ); + } + + for item in tree { + export(&mut s, item.1); + } + + Ok(s) + } +} + +enum Todo { + Procedure(String, Cow<'static, str>, ProcedureType), + Module(HashMap, Todo>), +} + +fn export(s: &mut String, item: Todo) { + match item { + Todo::Procedure(key, ident, ty) => { + let kind = match ty.kind { + ProcedureKind::Query => "Query", + ProcedureKind::Mutation => "Mutation", + ProcedureKind::Subscription => "Subscription", + }; + + let input = specta_rust::datatype(&ty.input).unwrap(); + let output = specta_rust::datatype(&ty.output).unwrap(); + let error = "()"; // TODO: specta_rust::datatype(&ty.error).unwrap(); + + s.push_str(&format!( + r#"pub struct {ident}; + +impl rspc_client::Procedure for {ident} {{ + type Input = {input}; + type Output = {output}; + type Error = {error}; + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::{kind}; + const KEY: &'static str = "{key}"; +}} + +"# + )); + } + Todo::Module(inner) => { + for (key, item) in inner { + s.push_str(&format!("\npub mod {key} {{\n")); + export(s, item); // TODO: Inset all items by the correct nuber of tabs + s.push_str("}\n"); + } + } + } +} diff --git a/src/languages/typescript.rs b/src/languages/typescript.rs new file mode 100644 index 00000000..98964a3b --- /dev/null +++ b/src/languages/typescript.rs @@ -0,0 +1,344 @@ +use std::{borrow::Cow, collections::BTreeMap, path::Path}; + +use serde_json::json; +use specta::{datatype::DataType, internal::detect_duplicate_type_names, NamedType, Type}; +use specta_serde::is_valid_ty; +use specta_typescript::{export_named_datatype, ExportError}; + +use crate::{ + interop::{construct_legacy_bindings_type, literal_object}, + procedure::ProcedureType, + Types, +}; + +pub struct Typescript { + inner: specta_typescript::Typescript, + generate_source_maps: bool, +} + +// TODO: Traits - `Debug`, `Clone`, etc + +impl Default for Typescript { + fn default() -> Self { + Self { + inner: specta_typescript::Typescript::default().framework_header("// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually."), + generate_source_maps: false, + } + } +} + +impl Typescript { + pub fn header(self, header: impl Into>) -> Self { + Self { + inner: self.inner.header(header), + ..self + } + } + + pub fn enable_source_maps(mut self) -> Self { + self.generate_source_maps = true; + self + } + + // TODO: Clone all methods + + pub fn export_to(&self, path: impl AsRef, types: &Types) -> Result<(), ExportError> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let mut bindings = self.export(types)?; + if self.generate_source_maps { + println!( + "WARNING: Typescript source maps in an unstable feature. Use at your own discretion!" + ); + + bindings += "export { Procedures } from './bindings_t';"; + } else { + generate_bindings(&mut bindings, types); + } + std::fs::write(&path, bindings)?; + self.inner.format(&path)?; + + if self.generate_source_maps { + let stem = path.file_stem().unwrap().to_str().unwrap().to_string(); // TODO: Error handling + let d_ts_file_name = format!("{stem}_t.d.ts"); + let d_ts_path = path.parent().unwrap().join(&d_ts_file_name); // TODO: Error handling + let d_ts_map_path = path.parent().unwrap().join(&format!("{stem}_t.d.ts.map")); // TODO: Error handling + + // let mut bindings = construct_file(self); + // bindings += "export * from './bindings_t';"; + + let source_map = SourceMap::default(); + let mut d_ts_file = construct_file(self); + generate_bindings(&mut d_ts_file, types); + + // std::fs::write(&path, bindings)?; + std::fs::write(&d_ts_path, d_ts_file)?; + std::fs::write( + &d_ts_map_path, + source_map.generate( + d_ts_file_name.into(), + "todo".into(), // TODO + ), + )?; + + self.inner.format(&d_ts_path)?; + } + + Ok(()) + } + + pub fn export(&self, types: &Types) -> Result { + // TODO: Add special `Bindings` type + + let legacy_types = construct_legacy_bindings_type(&types.procedures); + + // let bindings_types = types + // .procedures + // .iter() + // .map(|(key, p)| construct_bindings_type(&key, &p)) + // .collect::>(); + + let mut types = types.types.clone(); + + // { + // #[derive(Type)] + // struct Procedures; + + // let s = literal_object( + // "Procedures".into(), + // Some(Procedures::sid()), + // bindings_types.into_iter(), + // ); + // let mut ndt = Procedures::definition_named_data_type(&mut types); + // ndt.inner = s.into(); + // types.insert(Procedures::sid(), ndt); + // } + + { + #[derive(Type)] + struct ProceduresLegacy; + + let s = literal_object( + "ProceduresLegacy".into(), + Some(ProceduresLegacy::sid()), + legacy_types.into_iter(), + ); + let mut ndt = ProceduresLegacy::definition_named_data_type(&mut types); + ndt.inner = s.into(); + types.insert(ProceduresLegacy::sid(), ndt); + } + + self.inner.export(&types) + } + + // pub fn export_ // TODO: Source map (can we make it be inline?) +} + +fn generate_bindings(out: &mut String, types: &Types) { + *out += "export type Procedures = {\n"; + + for (name, ty) in types.procedures.iter() { + *out += "\t"; + // *out += name.join("."); + *out += ": {},\n"; + } + + // let generated_pos = get_current_pos(&types); + // let source_pos = (location.line() as usize, (location.column() - 1) as usize); + // types.push_str(name); + // types.push_str(": { input: string },\n"); + + // map.insert( + // name.to_string(), + // generated_pos, + // source_pos, + // location.file().to_string(), + // ); + + *out += "}\n"; +} + +fn construct_file(this: &Typescript) -> String { + let mut out = this.inner.header.to_string(); + if !out.is_empty() { + out.push('\n'); + } + out += &this.inner.framework_header; + out.push_str("\n\n"); + out +} + +fn construct_bindings_type( + key: &[Cow<'static, str>], + p: &ProcedureType, +) -> (Cow<'static, str>, DataType) { + if key.len() == 1 { + ( + key[0].clone(), + literal_object( + "".into(), + None, + vec![ + ("input".into(), p.input.clone()), + ("output".into(), p.output.clone()), + ("error".into(), p.error.clone()), + ] + .into_iter(), + ), + ) + } else { + ( + key[0].clone(), + literal_object( + "".into(), + None, + vec![construct_bindings_type(&key[1..], p)].into_iter(), + ), + ) + } +} + +#[derive(Default)] +struct SourceMap { + mappings: BTreeMap>, + sources: Vec, + names: Vec, +} + +impl SourceMap { + pub fn insert( + &mut self, + name: String, + (generated_line, generated_col): (usize, usize), + source_pos: (usize, usize), + source_file: String, + ) { + if !self.sources.contains(&source_file) { + self.sources.push(source_file.clone()); + } + let source_id = self.sources.iter().position(|s| *s == source_file).unwrap(); + + if !self.names.contains(&name) { + self.names.push(name.clone()); + } + let name_id = self.names.iter().position(|s| *s == name).unwrap(); + + self.mappings + .entry(generated_line) + .or_insert(Default::default()) + .push((generated_col, source_id, name_id, source_pos)); + } + + pub fn generate(&self, file: Cow<'static, str>, source_base_path: Cow<'static, str>) -> String { + let mut mappings = String::new(); + let mut last_source_line = None::; + let mut last_source_col = None::; + let mut last_source_file = None::; + let mut last_name_id = None::; + + for i in 1..((self.mappings.keys().max().copied().unwrap_or(0)) + 1) { + let mut last_col = None::; + + if let Some(line_mappings) = self.mappings.get(&i) { + for ( + i, + ( + actual_col, + actual_source_file, + actual_name_id, + (actual_source_line, actual_source_col), + ), + ) in line_mappings.iter().enumerate() + { + if i != 0 { + mappings.push(','); + } + + let col = last_col.map(|l| actual_col - l).unwrap_or(*actual_col); + last_col = Some(*actual_col); + + let actual_source_line = *actual_source_line - 1; + let source_line = last_source_line + .map(|l| actual_source_line - l) + .unwrap_or(actual_source_line); + last_source_line = Some(actual_source_line); + + let source_col = last_source_col + .map(|l| actual_source_col - l) + .unwrap_or(*actual_source_col); + last_source_col = Some(*actual_source_col); + + let source_file = last_source_file + .map(|l| actual_source_file - l) + .unwrap_or(*actual_source_file); + last_source_file = Some(*actual_source_file); + + let name_id = last_name_id + .map(|l| actual_name_id - l) + .unwrap_or(*actual_name_id); + last_name_id = Some(*actual_name_id); + + // TODO: Don't integer cast + let input = [ + col as i64, + source_file as i64, + source_line as i64, + source_col as i64, + name_id as i64, + ]; + + mappings.push_str(&generate_vlq_segment(&input)); + } + }; + + mappings.push(';'); + } + + serde_json::to_string(&json!({ + "version": 3, + "file": file, + "sources":self.sources + .iter() + .map(|n| format!("{source_base_path}{n}")) + .collect::>(), + "names": self.names, + "mappings": mappings + })) + .expect("failed to generate source map") + } +} + +fn get_current_pos(s: &String) -> (usize, usize) { + (s.split("\n").count(), s.split("\n").last().unwrap().len()) +} + +// Following copied from: https://docs.rs/sourcemap/latest/src/sourcemap/vlq.rs.html#307-313 + +/// Encodes a VLQ segment from a slice. +pub fn generate_vlq_segment(nums: &[i64]) -> String { + let mut rv = String::new(); + for &num in nums { + encode_vlq(&mut rv, num); + } + rv +} + +const B64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +pub(crate) fn encode_vlq(out: &mut String, num: i64) { + let mut num = if num < 0 { ((-num) << 1) + 1 } else { num << 1 }; + + loop { + let mut digit = num & 0b11111; + num >>= 5; + if num > 0 { + digit |= 1 << 5; + } + out.push(B64_CHARS[digit as usize] as char); + if num == 0 { + break; + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3d2d5f0f..e995807b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,12 +23,17 @@ )] pub(crate) mod interop; +mod languages; mod procedure; mod procedure_kind; mod router; +mod types; -pub use procedure_kind::ProcedureKind2; +#[allow(unused)] +pub use languages::*; +pub use procedure_kind::ProcedureKind; pub use router::Router2; +pub use types::Types; // TODO: These will come in the future. pub(crate) use procedure::Procedure2; diff --git a/src/procedure.rs b/src/procedure.rs index 0198a127..49f2cc9a 100644 --- a/src/procedure.rs +++ b/src/procedure.rs @@ -1,6 +1,17 @@ +use std::panic::Location; + use specta::datatype::DataType; -use crate::{internal::ProcedureKind, State}; +use crate::{ProcedureKind, State}; + +#[derive(Clone)] +pub(crate) struct ProcedureType { + pub(crate) kind: ProcedureKind, + pub(crate) input: DataType, + pub(crate) output: DataType, + pub(crate) error: DataType, + // pub(crate) location: Location<'static>, +} /// Represents a single operations on the server that can be executed. /// @@ -8,10 +19,7 @@ use crate::{internal::ProcedureKind, State}; /// pub struct Procedure2 { pub(crate) setup: Vec>, - pub(crate) kind: ProcedureKind, - pub(crate) input: DataType, - pub(crate) result: DataType, - pub(crate) error: DataType, + pub(crate) ty: ProcedureType, pub(crate) inner: rspc_core::Procedure, } @@ -21,9 +29,9 @@ impl Procedure2 { // TODO: `fn builder` // TODO: Make `pub` - pub(crate) fn kind(&self) -> ProcedureKind { - self.kind - } + // pub(crate) fn kind(&self) -> ProcedureKind2 { + // self.kind + // } // TODO: Expose all fields diff --git a/src/procedure_kind.rs b/src/procedure_kind.rs index 513a021d..13bee04e 100644 --- a/src/procedure_kind.rs +++ b/src/procedure_kind.rs @@ -1,21 +1,23 @@ -use std::fmt; +// use std::fmt; -use specta::Type; +// use specta::Type; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] -#[specta(rename_all = "camelCase")] -pub enum ProcedureKind2 { - Query, - Mutation, - Subscription, -} +// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] +// #[specta(rename_all = "camelCase")] +// pub enum ProcedureKind2 { +// Query, +// Mutation, +// Subscription, +// } -impl fmt::Display for ProcedureKind2 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Query => write!(f, "Query"), - Self::Mutation => write!(f, "Mutation"), - Self::Subscription => write!(f, "Subscription"), - } - } -} +// impl fmt::Display for ProcedureKind2 { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// match self { +// Self::Query => write!(f, "Query"), +// Self::Mutation => write!(f, "Mutation"), +// Self::Subscription => write!(f, "Subscription"), +// } +// } +// } + +pub use crate::internal::ProcedureKind; diff --git a/src/router.rs b/src/router.rs index 4ba8740c..e715a48b 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,10 +1,10 @@ use std::{borrow::Cow, collections::BTreeMap, fmt}; -use specta::{datatype::DataType, NamedType, SpectaID, Type, TypeCollection}; +use specta::{datatype::DataType, SpectaID, TypeCollection}; use rspc_core::Procedures; -use crate::{internal::ProcedureKind, interop::construct_legacy_bindings_type, Procedure2, State}; +use crate::{internal::ProcedureKind, procedure::ProcedureType, Procedure2, State, Types}; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { @@ -17,8 +17,7 @@ impl Default for Router2 { fn default() -> Self { Self { setup: Default::default(), - types: TypeCollection::default() - .with_header("// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.\n"), + types: Default::default(), procedures: Default::default(), } } @@ -67,56 +66,18 @@ impl Router2 { self } - pub fn build( - mut self, - ) -> Result< - ( - impl Into> + Clone + fmt::Debug, - TypeCollection, - ), - (), - > { + pub fn build(self) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { let mut state = (); for setup in self.setup { setup(&mut state); } - let legacy_types = construct_legacy_bindings_type(&self.procedures); - - let (types, procedures): (Vec<_>, BTreeMap<_, _>) = self + let (procedure_types, procedures): (BTreeMap<_, _>, BTreeMap<_, _>) = self .procedures .into_iter() - .map(|(key, p)| (construct_bindings_type(&key, &p), (key, p.inner))) + .map(|(key, p)| ((key.clone(), p.ty), (key, p.inner))) .unzip(); - { - #[derive(Type)] - struct Procedures; - - let s = literal_object( - "Procedures".into(), - Some(Procedures::sid()), - types.into_iter(), - ); - let mut ndt = Procedures::definition_named_data_type(&mut self.types); - ndt.inner = s.into(); - self.types.insert(Procedures::sid(), ndt); - } - - { - #[derive(Type)] - struct ProceduresLegacy; - - let s = literal_object( - "ProceduresLegacy".into(), - Some(ProceduresLegacy::sid()), - legacy_types.into_iter(), - ); - let mut ndt = ProceduresLegacy::definition_named_data_type(&mut self.types); - ndt.inner = s.into(); - self.types.insert(ProceduresLegacy::sid(), ndt); - } - struct Impl(Procedures); impl Into> for Impl { fn into(self) -> Procedures { @@ -134,7 +95,13 @@ impl Router2 { } } - Ok((Impl::(procedures), self.types)) + Ok(( + Impl::(procedures), + Types { + types: self.types, + procedures: procedure_types, + }, + )) } } @@ -143,7 +110,7 @@ impl fmt::Debug for Router2 { let procedure_keys = |kind: ProcedureKind| { self.procedures .iter() - .filter(move |(_, p)| p.kind() == kind) + .filter(move |(_, p)| p.ty.kind == kind) .map(|(k, _)| k.join("::")) .collect::>() }; @@ -168,62 +135,6 @@ impl<'a, TCtx> IntoIterator for &'a Router2 { } } -// TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` -pub(crate) fn literal_object( - name: Cow<'static, str>, - sid: Option, - fields: impl Iterator, DataType)>, -) -> DataType { - specta::internal::construct::r#struct( - name, - sid, - Default::default(), - specta::internal::construct::struct_named( - fields - .into_iter() - .map(|(name, ty)| { - ( - name.into(), - specta::internal::construct::field(false, false, None, "".into(), Some(ty)), - ) - }) - .collect(), - None, - ), - ) - .into() -} - -fn construct_bindings_type( - key: &[Cow<'static, str>], - p: &Procedure2, -) -> (Cow<'static, str>, DataType) { - if key.len() == 1 { - ( - key[0].clone(), - literal_object( - "".into(), - None, - vec![ - ("input".into(), p.input.clone()), - ("result".into(), p.result.clone()), - ("error".into(), p.error.clone()), - ] - .into_iter(), - ), - ) - } else { - ( - key[0].clone(), - literal_object( - "".into(), - None, - vec![construct_bindings_type(&key[1..], p)].into_iter(), - ), - ) - } -} - // TODO: Remove this block with the interop system impl From> for Router2 { fn from(router: crate::legacy::Router) -> Self { diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 00000000..1a1bc4db --- /dev/null +++ b/src/types.rs @@ -0,0 +1,16 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use specta::TypeCollection; + +use crate::procedure::ProcedureType; + +pub struct Types { + pub(crate) types: TypeCollection, + pub(crate) procedures: BTreeMap>, ProcedureType>, +} + +// TODO: Traits + +impl Types { + // TODO: Expose inners for manual exporting logic +} From f4fb57267cc394dbc33e3f132f8864bdc6c515a9 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 6 Dec 2024 15:36:08 +0800 Subject: [PATCH 13/67] jump to definition working --- crates/axum/src/jsonrpc_exec.rs | 12 +- crates/axum/src/v2.rs | 14 +- crates/client/src/lib.rs | 14 +- crates/core/src/lib.rs | 2 +- crates/tauri/src/jsonrpc_exec.rs | 12 +- crates/tauri/src/lib.rs | 10 +- examples/astro/src/components/playground.ts | 11 ++ examples/bindings_t.d.ts | 17 ++- examples/bindings_t.d.ts.map | 2 +- examples/client/src/bindings.rs | 66 ++++---- src/interop.rs | 36 ++++- src/languages/rust.rs | 48 ++---- src/languages/typescript.rs | 157 ++++++++++++-------- src/procedure.rs | 2 +- src/router.rs | 54 +++++-- src/types.rs | 8 +- 16 files changed, 279 insertions(+), 186 deletions(-) create mode 100644 examples/astro/src/components/playground.ts diff --git a/crates/axum/src/jsonrpc_exec.rs b/crates/axum/src/jsonrpc_exec.rs index a899edf3..1aca7dcc 100644 --- a/crates/axum/src/jsonrpc_exec.rs +++ b/crates/axum/src/jsonrpc_exec.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; +use std::{borrow::Cow, collections::HashMap}; -use rspc_core::ProcedureError; +use rspc_core::{ProcedureError, Procedures}; use serde_json::Value; use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; -use crate::{jsonrpc, v2::Routes}; - -use super::jsonrpc::{RequestId, RequestInner, ResponseInner}; +use super::jsonrpc::{self, RequestId, RequestInner, ResponseInner}; pub enum SubscriptionMap<'a> { Ref(&'a mut HashMap>), @@ -121,7 +119,7 @@ impl<'a> Sender<'a> { pub async fn handle_json_rpc( ctx: TCtx, req: jsonrpc::Request, - routes: &Routes, + routes: &Procedures, sender: &mut Sender<'_>, subscriptions: &mut SubscriptionMap<'_>, ) where @@ -155,7 +153,7 @@ pub async fn handle_json_rpc( } }; - let result = match routes.get(&path) { + let result = match routes.get(&Cow::Borrowed(&*path)) { Some(procedure) => { let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); diff --git a/crates/axum/src/v2.rs b/crates/axum/src/v2.rs index 5408bf13..7ff5284e 100644 --- a/crates/axum/src/v2.rs +++ b/crates/axum/src/v2.rs @@ -17,10 +17,8 @@ use crate::{ jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}, }; -pub(crate) type Routes = HashMap>; - pub fn endpoint( - router: impl Into>, + routes: impl Into>, ctx_fn: TCtxFn, ) -> Router where @@ -29,11 +27,7 @@ where TCtxFnMarker: Send + Sync + 'static, TCtxFn: TCtxFunc, { - let routes = router - .into() - .into_iter() - .map(|(key, value)| (key.join("."), value)) - .collect::>(); + let routes = routes.into(); Router::::new().route( "/:id", @@ -92,7 +86,7 @@ async fn handle_http( ctx_fn: TCtxFn, kind: ProcedureKind, req: Request, - routes: &Routes, + routes: &Procedures, state: TState, ) -> impl IntoResponse where @@ -212,7 +206,7 @@ async fn handle_websocket( ctx_fn: TCtxFn, mut socket: axum::extract::ws::WebSocket, parts: Parts, - routes: Routes, + routes: Procedures, state: TState, ) where TCtx: Send + Sync + 'static, diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 7c80ac3f..5ecd2e03 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -14,18 +14,19 @@ // TODO: Supporting transport formats other than JSON? // TODO: Is this safe to use from the same app that defines the router? If not we should try and forbid it with a compiler error. -use std::borrow::Cow; +use std::{borrow::Cow, marker::PhantomData}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; /// TODO #[derive(Debug, Clone)] -pub struct Client { +pub struct Client

{ url: Cow<'static, str>, client: reqwest::Client, + phantom: PhantomData

, } -impl Client { +impl

Client

{ pub fn new(url: impl Into>) -> Self { Self { url: url.into(), @@ -37,10 +38,14 @@ impl Client { )) .build() .unwrap(), // TODO: Can this fail? + phantom: PhantomData, } } - pub async fn exec(&self, input: O::Input) -> Result { + pub async fn exec>( + &self, + input: O::Input, + ) -> Result { let url = format!( "{}{}{}", self.url, @@ -110,6 +115,7 @@ pub trait Procedure { type Input: Serialize; type Output: DeserializeOwned; type Error: DeserializeOwned; + type Procedures; const KEY: &'static str; const KIND: ProcedureKind; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 04fa64de..b6b6cefc 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -35,7 +35,7 @@ pub use procedure::Procedure; pub use stream::ProcedureStream; pub type Procedures = - std::collections::BTreeMap>, Procedure>; + std::collections::HashMap, Procedure>; // TODO: The naming is horid. // Low-level concerns: diff --git a/crates/tauri/src/jsonrpc_exec.rs b/crates/tauri/src/jsonrpc_exec.rs index b3f81f09..1aca7dcc 100644 --- a/crates/tauri/src/jsonrpc_exec.rs +++ b/crates/tauri/src/jsonrpc_exec.rs @@ -1,12 +1,10 @@ -use std::collections::HashMap; +use std::{borrow::Cow, collections::HashMap}; -use rspc_core::ProcedureError; +use rspc_core::{ProcedureError, Procedures}; use serde_json::Value; use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; -use crate::{jsonrpc, Routes}; - -use super::jsonrpc::{RequestId, RequestInner, ResponseInner}; +use super::jsonrpc::{self, RequestId, RequestInner, ResponseInner}; pub enum SubscriptionMap<'a> { Ref(&'a mut HashMap>), @@ -121,7 +119,7 @@ impl<'a> Sender<'a> { pub async fn handle_json_rpc( ctx: TCtx, req: jsonrpc::Request, - routes: &Routes, + routes: &Procedures, sender: &mut Sender<'_>, subscriptions: &mut SubscriptionMap<'_>, ) where @@ -155,7 +153,7 @@ pub async fn handle_json_rpc( } }; - let result = match routes.get(&path) { + let result = match routes.get(&Cow::Borrowed(&*path)) { Some(procedure) => { let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index d923d9e5..b886816c 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -19,20 +19,14 @@ use tokio::sync::mpsc; mod jsonrpc; mod jsonrpc_exec; -pub(crate) type Routes = HashMap>; - pub fn plugin( - router: impl Into>, + routes: impl Into>, ctx_fn: impl Fn(AppHandle) -> TCtx + Send + Sync + 'static, ) -> TauriPlugin where TCtx: Send + 'static, { - let routes = router - .into() - .into_iter() - .map(|(key, value)| (key.join("."), value)) - .collect::>(); + let routes = routes.into(); Builder::new("rspc") .setup(|app_handle, _| { diff --git a/examples/astro/src/components/playground.ts b/examples/astro/src/components/playground.ts new file mode 100644 index 00000000..0c4e19fc --- /dev/null +++ b/examples/astro/src/components/playground.ts @@ -0,0 +1,11 @@ +// TODO: Remove this in the future + +import { Procedures } from "../../../bindings"; + +function createProxy(): { [K in keyof T]: () => T[K] } { + return undefined as any; +} + +const procedures = createProxy(); + +procedures.version(); diff --git a/examples/bindings_t.d.ts b/examples/bindings_t.d.ts index f98b5417..6a9a572d 100644 --- a/examples/bindings_t.d.ts +++ b/examples/bindings_t.d.ts @@ -2,11 +2,14 @@ // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. export type Procedures = { - : {}, - : {}, - : {}, - : {}, - : {}, - : {}, - : {}, + echo: { input: string, output: string, error: unknown }, + error: { input: null, output: string, error: unknown }, + nested: { + hello: { input: null, output: string, error: unknown }, +}, + pings: { input: null, output: string, error: unknown }, + sendMsg: { input: string, output: string, error: unknown }, + transformMe: { input: null, output: string, error: unknown }, + version: { input: null, output: string, error: unknown }, } +//# sourceMappingURL=bindings_t.d.ts.map \ No newline at end of file diff --git a/examples/bindings_t.d.ts.map b/examples/bindings_t.d.ts.map index e3ee8306..25197a78 100644 --- a/examples/bindings_t.d.ts.map +++ b/examples/bindings_t.d.ts.map @@ -1 +1 @@ -{"file":"bindings_t.d.ts","mappings":"","names":[],"sources":[],"version":3} \ No newline at end of file +{"file":"bindings_t.d.ts","mappings":";;;;CAqDmCA;CAAAC;;CAAAC;;CAAAC;CAAAC;CAAAC;CAAAC;","names":["echo","error","hello","pings","sendMsg","transformMe","version"],"sources":["/Users/oscar/Desktop/rspc/src/interop.rs"],"version":3} \ No newline at end of file diff --git a/examples/client/src/bindings.rs b/examples/client/src/bindings.rs index 48518278..de38cf65 100644 --- a/examples/client/src/bindings.rs +++ b/examples/client/src/bindings.rs @@ -1,75 +1,85 @@ //! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -pub struct sendMsg; +pub struct Procedures; -impl rspc_client::Procedure for sendMsg { +pub struct echo; + +impl rspc_client::Procedure for echo { type Input = String; type Output = String; type Error = (); - const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Mutation; - const KEY: &'static str = "sendMsg"; + type Procedures = Procedures; + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "echo"; } -pub struct version; +pub struct error; -impl rspc_client::Procedure for version { +impl rspc_client::Procedure for error { type Input = (); type Output = String; type Error = (); + type Procedures = Procedures; const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; - const KEY: &'static str = "version"; + const KEY: &'static str = "error"; } -pub struct transformMe; -impl rspc_client::Procedure for transformMe { +pub mod hello { + pub use super::Procedures; +pub struct hello; + +impl rspc_client::Procedure for hello { type Input = (); type Output = String; type Error = (); + type Procedures = Procedures; const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; - const KEY: &'static str = "transformMe"; + const KEY: &'static str = "hello"; } -pub struct error; +} +pub struct pings; -impl rspc_client::Procedure for error { +impl rspc_client::Procedure for pings { type Input = (); type Output = String; type Error = (); - const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; - const KEY: &'static str = "error"; + type Procedures = Procedures; + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Subscription; + const KEY: &'static str = "pings"; } -pub struct echo; +pub struct sendMsg; -impl rspc_client::Procedure for echo { +impl rspc_client::Procedure for sendMsg { type Input = String; type Output = String; type Error = (); - const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; - const KEY: &'static str = "echo"; + type Procedures = Procedures; + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Mutation; + const KEY: &'static str = "sendMsg"; } +pub struct transformMe; -pub mod hello { -pub struct hello; - -impl rspc_client::Procedure for hello { +impl rspc_client::Procedure for transformMe { type Input = (); type Output = String; type Error = (); + type Procedures = Procedures; const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; - const KEY: &'static str = "nested.hello"; + const KEY: &'static str = "transformMe"; } -} -pub struct pings; +pub struct version; -impl rspc_client::Procedure for pings { +impl rspc_client::Procedure for version { type Input = (); type Output = String; type Error = (); - const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Subscription; - const KEY: &'static str = "pings"; + type Procedures = Procedures; + const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; + const KEY: &'static str = "version"; } diff --git a/src/interop.rs b/src/interop.rs index 7ef3a8a5..0c822916 100644 --- a/src/interop.rs +++ b/src/interop.rs @@ -11,6 +11,7 @@ use specta::{ use crate::{ internal::{Layer, ProcedureKind, RequestContext, ValueOrStream}, procedure::ProcedureType, + types::TypesOrType, Procedure2, Router, Router2, }; @@ -48,6 +49,9 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { input: p.ty.arg_ty, output: p.ty.result_ty, error: specta::datatype::DataType::Unknown, + // TODO: This location is obviously wrong but the legacy router has no location information. + // This will work properly with the new procedure syntax. + location: Location::caller().clone(), }, // location: Location::caller().clone(), // TODO: This needs to actually be correct inner: layer_to_procedure(key, kind, p.exec), @@ -167,7 +171,7 @@ fn map_method( // TODO: Remove this block with the interop system pub(crate) fn construct_legacy_bindings_type( - p: &BTreeMap>, ProcedureType>, + map: &BTreeMap, TypesOrType>, ) -> Vec<(Cow<'static, str>, DataType)> { #[derive(Type)] struct Queries; @@ -176,6 +180,11 @@ pub(crate) fn construct_legacy_bindings_type( #[derive(Type)] struct Subscriptions; + let mut p = BTreeMap::new(); + for (k, v) in map { + flatten_procedures_for_legacy(&mut p, vec![k.clone()], v.clone()); + } + vec![ ( "queries".into(), @@ -185,7 +194,7 @@ pub(crate) fn construct_legacy_bindings_type( EnumRepr::Untagged, false, Default::default(), - map_method(ProcedureKind::Query, p), + map_method(ProcedureKind::Query, &p), ) .into(), ), @@ -197,7 +206,7 @@ pub(crate) fn construct_legacy_bindings_type( EnumRepr::Untagged, false, Default::default(), - map_method(ProcedureKind::Mutation, p), + map_method(ProcedureKind::Mutation, &p), ) .into(), ), @@ -209,13 +218,32 @@ pub(crate) fn construct_legacy_bindings_type( EnumRepr::Untagged, false, Default::default(), - map_method(ProcedureKind::Subscription, p), + map_method(ProcedureKind::Subscription, &p), ) .into(), ), ] } +fn flatten_procedures_for_legacy( + p: &mut BTreeMap>, ProcedureType>, + key: Vec>, + item: TypesOrType, +) { + match item { + TypesOrType::Type(ty) => { + p.insert(key, ty); + } + TypesOrType::Types(types) => { + for (k, v) in types { + let mut key = key.clone(); + key.push(k.clone()); + flatten_procedures_for_legacy(p, key, v); + } + } + } +} + // TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` pub(crate) fn literal_object( name: Cow<'static, str>, diff --git a/src/languages/rust.rs b/src/languages/rust.rs index 7dcafe9f..c7ad9029 100644 --- a/src/languages/rust.rs +++ b/src/languages/rust.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path}; use specta::datatype::DataType; use specta_typescript::ExportError; -use crate::{procedure::ProcedureType, ProcedureKind, Types}; +use crate::{procedure::ProcedureType, types::TypesOrType, ProcedureKind, Types}; pub struct Rust(()); // TODO: specta_rust::Rust @@ -27,7 +27,7 @@ impl Rust { pub fn export(&self, types: &Types) -> Result { println!("WARNING: `rspc::Rust` is an unstable feature! Use at your own discretion!"); - let mut s = "//! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.\n\n".to_string(); + let mut s = "//! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.\n\npub struct Procedures;\n\n".to_string(); // TODO: Move to `specta_rust::Rust` which should handle this like we do with Typescript. for (_, ty) in types.types.into_iter() { @@ -36,45 +36,17 @@ impl Rust { // TODO: disabling warning on the output??? - // TODO: We should probs always store procedures as tree - let mut tree = HashMap::new(); - for (key, ty) in types.procedures.iter() { - let mut current = &mut tree; - for part in &key[..(key.len() - 1)] { - let a = current - .entry(part.clone()) - .or_insert_with(|| Todo::Module(HashMap::new())); - match a { - Todo::Procedure(_, _, _) => todo!(), - Todo::Module(hash_map) => current = hash_map, - } - } - current.insert( - key[key.len() - 1].clone(), - Todo::Procedure( - key.join(".").to_string(), - key[key.len() - 1].clone(), - ty.clone(), - ), - ); - } - - for item in tree { - export(&mut s, item.1); + for (key, item) in types.procedures.clone().into_iter() { + export(&mut s, key.to_string(), key, item); } Ok(s) } } -enum Todo { - Procedure(String, Cow<'static, str>, ProcedureType), - Module(HashMap, Todo>), -} - -fn export(s: &mut String, item: Todo) { +fn export(s: &mut String, full_key: String, ident: Cow<'static, str>, item: TypesOrType) { match item { - Todo::Procedure(key, ident, ty) => { + TypesOrType::Type(ty) => { let kind = match ty.kind { ProcedureKind::Query => "Query", ProcedureKind::Mutation => "Mutation", @@ -92,17 +64,19 @@ impl rspc_client::Procedure for {ident} {{ type Input = {input}; type Output = {output}; type Error = {error}; + type Procedures = Procedures; const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::{kind}; - const KEY: &'static str = "{key}"; + const KEY: &'static str = "{ident}"; }} "# )); } - Todo::Module(inner) => { + TypesOrType::Types(inner) => { for (key, item) in inner { s.push_str(&format!("\npub mod {key} {{\n")); - export(s, item); // TODO: Inset all items by the correct nuber of tabs + s.push_str(&"\tpub use super::Procedures;\n"); + export(s, format!("{full_key}.{key}"), key, item); // TODO: Inset all items by the correct nuber of tabs s.push_str("}\n"); } } diff --git a/src/languages/typescript.rs b/src/languages/typescript.rs index 98964a3b..de006241 100644 --- a/src/languages/typescript.rs +++ b/src/languages/typescript.rs @@ -1,13 +1,13 @@ -use std::{borrow::Cow, collections::BTreeMap, path::Path}; +use std::{borrow::Cow, collections::BTreeMap, iter::once, path::Path}; use serde_json::json; -use specta::{datatype::DataType, internal::detect_duplicate_type_names, NamedType, Type}; -use specta_serde::is_valid_ty; -use specta_typescript::{export_named_datatype, ExportError}; +use specta::{datatype::DataType, NamedType, Type}; +use specta_typescript::{datatype, export_named_datatype, ExportError}; use crate::{ interop::{construct_legacy_bindings_type, literal_object}, procedure::ProcedureType, + types::TypesOrType, Types, }; @@ -56,7 +56,7 @@ impl Typescript { bindings += "export { Procedures } from './bindings_t';"; } else { - generate_bindings(&mut bindings, types); + generate_bindings(&mut bindings, self, types, |_, _, _| {}); } std::fs::write(&path, bindings)?; self.inner.format(&path)?; @@ -64,15 +64,28 @@ impl Typescript { if self.generate_source_maps { let stem = path.file_stem().unwrap().to_str().unwrap().to_string(); // TODO: Error handling let d_ts_file_name = format!("{stem}_t.d.ts"); + let d_ts_map_file_name = format!("{stem}_t.d.ts.map"); let d_ts_path = path.parent().unwrap().join(&d_ts_file_name); // TODO: Error handling - let d_ts_map_path = path.parent().unwrap().join(&format!("{stem}_t.d.ts.map")); // TODO: Error handling + let d_ts_map_path = path.parent().unwrap().join(&d_ts_map_file_name); // TODO: Error handling // let mut bindings = construct_file(self); // bindings += "export * from './bindings_t';"; - let source_map = SourceMap::default(); + let mut source_map = SourceMap::default(); let mut d_ts_file = construct_file(self); - generate_bindings(&mut d_ts_file, types); + generate_bindings(&mut d_ts_file, self, types, |name, pos, procedure_type| { + source_map.insert( + name.to_string(), + pos, + ( + // TODO: Don't cast + procedure_type.location.line() as usize, + procedure_type.location.column() as usize, + ), + procedure_type.location.file().to_string(), + ); + }); + d_ts_file += &format!("\n//# sourceMappingURL={d_ts_map_file_name}"); // std::fs::write(&path, bindings)?; std::fs::write(&d_ts_path, d_ts_file)?; @@ -80,7 +93,7 @@ impl Typescript { &d_ts_map_path, source_map.generate( d_ts_file_name.into(), - "todo".into(), // TODO + "".into(), // TODO ), )?; @@ -137,28 +150,86 @@ impl Typescript { // pub fn export_ // TODO: Source map (can we make it be inline?) } -fn generate_bindings(out: &mut String, types: &Types) { - *out += "export type Procedures = {\n"; +fn generate_bindings( + out: &mut String, + this: &Typescript, + types: &Types, + mut on_procedure: impl FnMut(&Cow<'static, str>, (usize, usize), &ProcedureType), +) { + fn inner( + out: &mut String, + this: &Typescript, + mut on_procedure: &mut impl FnMut(&Cow<'static, str>, (usize, usize), &ProcedureType), + types: &Types, + source_pos: (usize, usize), + key: &Cow<'static, str>, + item: &TypesOrType, + ) { + match item { + TypesOrType::Type(procedure_type) => { + on_procedure(&key, source_pos, procedure_type); + + // *out += "\t"; // TODO: Correct padding + *out += "{ input: "; + *out += &datatype( + &this.inner, + &specta::datatype::FunctionResultVariant::Value(procedure_type.input.clone()), + &types.types, + ) + .unwrap(); // TODO: Error handling + + *out += ", output: "; + *out += &datatype( + &this.inner, + &specta::datatype::FunctionResultVariant::Value(procedure_type.output.clone()), + &types.types, + ) + .unwrap(); // TODO: Error handling + + *out += ", error: "; + *out += &datatype( + &this.inner, + &specta::datatype::FunctionResultVariant::Value(procedure_type.error.clone()), + &types.types, + ) + .unwrap(); // TODO: Error handling + + *out += " }"; + } + TypesOrType::Types(btree_map) => { + // TODO: Jump to definition on routers + // *out += "name: "; + + *out += "{\n"; + + for (key, item) in btree_map.iter() { + *out += "\t"; + + let source_pos = get_current_pos(out); + + *out += key; + *out += ": "; + inner(out, this, on_procedure, types, source_pos, key, &item); + *out += ",\n"; + } - for (name, ty) in types.procedures.iter() { - *out += "\t"; - // *out += name.join("."); - *out += ": {},\n"; + *out += "}"; + } + } } - // let generated_pos = get_current_pos(&types); - // let source_pos = (location.line() as usize, (location.column() - 1) as usize); - // types.push_str(name); - // types.push_str(": { input: string },\n"); - - // map.insert( - // name.to_string(), - // generated_pos, - // source_pos, - // location.file().to_string(), - // ); - - *out += "}\n"; + *out += "export type Procedures = "; + inner( + out, + this, + &mut on_procedure, + types, + // We know this is only used in `TypesOrType::Type` and we don't parse that so it's value means nothing. + (0, 0), + // We know this is only used in `TypesOrType::Type` and we don't parse that so it's value means nothing. + &"".into(), + &TypesOrType::Types(types.procedures.clone()), + ); } fn construct_file(this: &Typescript) -> String { @@ -171,36 +242,6 @@ fn construct_file(this: &Typescript) -> String { out } -fn construct_bindings_type( - key: &[Cow<'static, str>], - p: &ProcedureType, -) -> (Cow<'static, str>, DataType) { - if key.len() == 1 { - ( - key[0].clone(), - literal_object( - "".into(), - None, - vec![ - ("input".into(), p.input.clone()), - ("output".into(), p.output.clone()), - ("error".into(), p.error.clone()), - ] - .into_iter(), - ), - ) - } else { - ( - key[0].clone(), - literal_object( - "".into(), - None, - vec![construct_bindings_type(&key[1..], p)].into_iter(), - ), - ) - } -} - #[derive(Default)] struct SourceMap { mappings: BTreeMap>, diff --git a/src/procedure.rs b/src/procedure.rs index 49f2cc9a..0c591111 100644 --- a/src/procedure.rs +++ b/src/procedure.rs @@ -10,7 +10,7 @@ pub(crate) struct ProcedureType { pub(crate) input: DataType, pub(crate) output: DataType, pub(crate) error: DataType, - // pub(crate) location: Location<'static>, + pub(crate) location: Location<'static>, } /// Represents a single operations on the server that can be executed. diff --git a/src/router.rs b/src/router.rs index e715a48b..9943e7e2 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,10 +1,14 @@ -use std::{borrow::Cow, collections::BTreeMap, fmt}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap}, + fmt, +}; -use specta::{datatype::DataType, SpectaID, TypeCollection}; +use specta::TypeCollection; use rspc_core::Procedures; -use crate::{internal::ProcedureKind, procedure::ProcedureType, Procedure2, State, Types}; +use crate::{internal::ProcedureKind, types::TypesOrType, Procedure2, State, Types}; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { @@ -32,11 +36,10 @@ impl Router2 { // pub fn procedure( // mut self, // key: impl Into>, - // procedure: Procedure2, + // mut procedure: Procedure2, // ) -> Self { - // let name = key.into(); - // self.procedures.insert(name, procedure); - // self.setup.extend(procedure.setup); + // self.setup.extend(procedure.setup.drain(..)); + // self.procedures.insert(vec![key.into()], procedure); // self // } @@ -46,6 +49,7 @@ impl Router2 { // self // } + // TODO: Yield error if key already exists pub fn nest(mut self, prefix: impl Into>, mut other: Self) -> Self { self.setup.append(&mut other.setup); @@ -56,10 +60,10 @@ impl Router2 { k.push(prefix.clone()); (k, v) })); - self } + // TODO: Yield error if key already exists pub fn merge(mut self, mut other: Self) -> Self { self.setup.append(&mut other.setup); self.procedures.extend(other.procedures.into_iter()); @@ -72,11 +76,27 @@ impl Router2 { setup(&mut state); } - let (procedure_types, procedures): (BTreeMap<_, _>, BTreeMap<_, _>) = self + let mut procedure_types = BTreeMap::new(); + let procedures = self .procedures .into_iter() - .map(|(key, p)| ((key.clone(), p.ty), (key, p.inner))) - .unzip(); + .map(|(key, p)| { + let mut current = &mut procedure_types; + // TODO: if `key.len()` is `0` we might run into issues here. It shouldn't but probs worth protecting. + for part in &key[..(key.len() - 1)] { + let a = current + .entry(part.clone()) + .or_insert_with(|| TypesOrType::Types(Default::default())); + match a { + TypesOrType::Type(_) => unreachable!(), // TODO: Confirm this is unreachable + TypesOrType::Types(map) => current = map, + } + } + current.insert(key[key.len() - 1].clone(), TypesOrType::Type(p.ty)); + + (get_flattened_name(&key), p.inner) + }) + .collect::>(); struct Impl(Procedures); impl Into> for Impl { @@ -111,7 +131,7 @@ impl fmt::Debug for Router2 { self.procedures .iter() .filter(move |(_, p)| p.ty.kind == kind) - .map(|(k, _)| k.join("::")) + .map(|(k, _)| k.join(".")) .collect::>() }; @@ -154,3 +174,13 @@ impl Router2 { &mut self.types } } + +fn get_flattened_name(name: &Vec>) -> Cow<'static, str> { + if name.len() == 1 { + // By cloning we are ensuring we passthrough to the `Cow` to avoid cloning if this is a `&'static str`. + // Doing `.join` will always produce a new `String` removing the `&'static str` optimization. + name[0].clone() + } else { + name.join(".").to_string().into() + } +} diff --git a/src/types.rs b/src/types.rs index 1a1bc4db..3768fb4d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,9 +4,15 @@ use specta::TypeCollection; use crate::procedure::ProcedureType; +#[derive(Clone)] +pub(crate) enum TypesOrType { + Type(ProcedureType), + Types(BTreeMap, TypesOrType>), +} + pub struct Types { pub(crate) types: TypeCollection, - pub(crate) procedures: BTreeMap>, ProcedureType>, + pub(crate) procedures: BTreeMap, TypesOrType>, } // TODO: Traits From 7792a0863bd7bda91117b3e36fb8ad631a5b2b7a Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 6 Dec 2024 17:05:38 +0800 Subject: [PATCH 14/67] new procedure and middleware syntax --- crates/axum/src/lib.rs | 1 + crates/client/src/lib.rs | 1 + crates/core/src/dyn_input.rs | 4 +- crates/core/src/lib.rs | 1 + crates/core/src/procedure.rs | 4 +- crates/core/src/stream.rs | 99 ++++++++++++- crates/tauri/src/lib.rs | 1 + examples/axum/Cargo.toml | 1 + examples/axum/src/main.rs | 103 +++++++++++-- examples/bindings.ts | 15 +- examples/bindings_t.d.ts | 1 + src/lib.rs | 14 +- src/modern/error.rs | 13 ++ src/modern/infallible.rs | 30 ++++ src/modern/middleware.rs | 7 + src/modern/middleware/middleware.rs | 140 ++++++++++++++++++ src/modern/middleware/next.rs | 30 ++++ src/modern/mod.rs | 13 ++ src/modern/procedure.rs | 27 ++++ src/modern/procedure/builder.rs | 115 +++++++++++++++ src/modern/procedure/meta.rs | 55 +++++++ src/modern/procedure/procedure.rs | 185 ++++++++++++++++++++++++ src/modern/procedure/resolver_input.rs | 55 +++++++ src/modern/procedure/resolver_output.rs | 129 +++++++++++++++++ src/modern/state.rs | 95 ++++++++++++ src/modern/stream.rs | 35 +++++ src/procedure.rs | 185 ++++++++++++++++++------ src/router.rs | 15 ++ 28 files changed, 1292 insertions(+), 82 deletions(-) create mode 100644 src/modern/error.rs create mode 100644 src/modern/infallible.rs create mode 100644 src/modern/middleware.rs create mode 100644 src/modern/middleware/middleware.rs create mode 100644 src/modern/middleware/next.rs create mode 100644 src/modern/mod.rs create mode 100644 src/modern/procedure.rs create mode 100644 src/modern/procedure/builder.rs create mode 100644 src/modern/procedure/meta.rs create mode 100644 src/modern/procedure/procedure.rs create mode 100644 src/modern/procedure/resolver_input.rs create mode 100644 src/modern/procedure/resolver_output.rs create mode 100644 src/modern/state.rs create mode 100644 src/modern/stream.rs diff --git a/crates/axum/src/lib.rs b/crates/axum/src/lib.rs index c672ecb8..c0a8e054 100644 --- a/crates/axum/src/lib.rs +++ b/crates/axum/src/lib.rs @@ -1,4 +1,5 @@ //! rspc-axum: Axum integration for [rspc](https://rspc.dev). +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 5ecd2e03..eb55972a 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,6 +1,7 @@ //! rspc-client: Rust client for [rspc](https://docs.rs/rspc). //! //! # This is really unstable you should be careful using it! +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", diff --git a/crates/core/src/dyn_input.rs b/crates/core/src/dyn_input.rs index 80ab8e6b..7cb19db2 100644 --- a/crates/core/src/dyn_input.rs +++ b/crates/core/src/dyn_input.rs @@ -9,8 +9,8 @@ use crate::{DeserializeError, DowncastError}; /// TODO pub struct DynInput<'a, 'de> { - pub(crate) value: Option<&'a mut dyn Any>, - pub(crate) deserializer: Option<&'a mut dyn erased_serde::Deserializer<'de>>, + pub(crate) value: Option<&'a mut (dyn Any + Send)>, + pub(crate) deserializer: Option<&'a mut (dyn erased_serde::Deserializer<'de> + Send)>, pub(crate) type_name: &'static str, } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b6b6cefc..d1427bf5 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -5,6 +5,7 @@ //! TODO: Why this crate doesn't depend on Specta. //! TODO: Discuss the traits that need to be layered on for this to be useful. //! TODO: Discuss how middleware don't exist here. +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", diff --git a/crates/core/src/procedure.rs b/crates/core/src/procedure.rs index a8e5c232..0e444218 100644 --- a/crates/core/src/procedure.rs +++ b/crates/core/src/procedure.rs @@ -26,7 +26,7 @@ impl Procedure { } } - pub fn exec_with_deserializer<'de, D: Deserializer<'de>>( + pub fn exec_with_deserializer<'de, D: Deserializer<'de> + Send>( &self, ctx: TCtx, input: D, @@ -41,7 +41,7 @@ impl Procedure { (self.handler)(ctx, value) } - pub fn exec_with_value(&self, ctx: TCtx, input: T) -> ProcedureStream { + pub fn exec_with_value(&self, ctx: TCtx, input: T) -> ProcedureStream { let mut input = Some(input); let value = DynInput { value: Some(&mut input), diff --git a/crates/core/src/stream.rs b/crates/core/src/stream.rs index c0fb178d..c6e1956c 100644 --- a/crates/core/src/stream.rs +++ b/crates/core/src/stream.rs @@ -29,6 +29,7 @@ impl ProcedureStream { // TODO: Should we do this in a more efficient way??? src: std::future::ready(value), value: None, + done: false, }), } } @@ -40,7 +41,11 @@ impl ProcedureStream { T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! { Self { - src: Box::pin(DynReturnValueFutureCompat { src, value: None }), + src: Box::pin(DynReturnValueFutureCompat { + src, + value: None, + done: false, + }), } } @@ -70,6 +75,20 @@ impl ProcedureStream { } } + // TODO: Rename and replace `Self::from_future_stream`??? + // TODO: I'm not sure if we should keep this or not? + // The crate `futures`'s flatten stuff doesn't handle it how we need it so maybe we could patch that instead of having this special case??? + // This is a special case because we need to ensure the `size_hint` is correct. + /// TODO + pub fn from_future_procedure_stream(src: F) -> Self + where + F: Future> + Send + 'static, + { + Self { + src: Box::pin(DynReturnValueFutureProcedureStreamCompat::Future { src }), + } + } + // /// TODO // /// // /// TODO: This method doesn't allow reusing the serializer between polls. Maybe remove it??? @@ -135,6 +154,7 @@ pin_project! { #[pin] src: S, value: Option, + done: bool, } } @@ -151,13 +171,17 @@ where let this = self.as_mut().project(); let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. match this.src.poll(cx) { - Poll::Ready(value) => Poll::Ready(Some(match value { - Ok(value) => { - *this.value = Some(value); - Ok(()) - } - Err(err) => Err(err), - })), + Poll::Ready(value) => { + *this.done = true; + Poll::Ready(Some(match value { + Ok(value) => { + *this.value = Some(value); + + Ok(()) + } + Err(err) => Err(err), + })) + } Poll::Pending => return Poll::Pending, } } @@ -170,6 +194,9 @@ where } fn size_hint(&self) -> (usize, Option) { + if self.done { + return (0, Some(0)); + } (1, Some(1)) } } @@ -292,3 +319,59 @@ where } } } + +pin_project! { + #[project = DynReturnValueFutureProcedureStreamCompatProj] + enum DynReturnValueFutureProcedureStreamCompat { + Future { + #[pin] src: F, + }, + Inner { + src: ProcedureStream, + } + } +} + +impl DynReturnValue for DynReturnValueFutureProcedureStreamCompat +where + F: Future> + Send + 'static, +{ + // TODO: Cleanup this impl's pattern matching. + fn poll_next_value<'a>( + mut self: Pin<&'a mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + loop { + return match self.as_mut().project() { + DynReturnValueFutureProcedureStreamCompatProj::Future { src } => match src.poll(cx) + { + Poll::Ready(Ok(result)) => { + self.as_mut() + .set(DynReturnValueFutureProcedureStreamCompat::Inner { src: result }); + continue; + } + Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))), + Poll::Pending => return Poll::Pending, + }, + DynReturnValueFutureProcedureStreamCompatProj::Inner { src } => { + src.src.as_mut().poll_next_value(cx) + } + }; + } + } + + fn value(&self) -> &dyn erased_serde::Serialize { + match self { + // Attempted to acces value before first `Poll::Ready` was returned. + Self::Future { .. } => panic!("unreachable"), + Self::Inner { src } => src.src.value(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Future { .. } => (0, None), + Self::Inner { src } => src.src.size_hint(), + } + } +} diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index b886816c..6084d306 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -1,4 +1,5 @@ //! rspc-tauri: Tauri integration for [rspc](https://rspc.dev). +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 39cbeea3..9a3991eb 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -17,3 +17,4 @@ serde = { version = "1.0.215", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = [ "derive", ] } # TODO: Drop all features +thiserror = "2.0.4" diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 1eb3f84e..5a23b7b5 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,13 +1,24 @@ -use std::{path::PathBuf, time::Duration}; +use std::{marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; use async_stream::stream; use axum::{http::request::Parts, routing::get}; -use rspc::Router2; +use rspc::{ + modern::{ + self, + middleware::Middleware, + procedure::{ResolverInput, ResolverOutput}, + Procedure2, + }, + Router2, +}; use serde::Serialize; use specta::Type; +use thiserror::Error; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; +// `Clone` is only required for usage with Websockets +#[derive(Clone)] struct Ctx {} #[derive(Serialize, Type)] @@ -87,14 +98,84 @@ fn mount() -> rspc::Router { router } +#[derive(Debug, Error, Serialize, Type)] +pub enum Error { + #[error("you made a mistake: {0}")] + Mistake(String), +} + +impl modern::Error for Error {} + +pub struct BaseProcedure(PhantomData); +impl BaseProcedure { + pub fn builder( + ) -> modern::procedure::ProcedureBuilder + where + TErr: modern::Error, + TInput: ResolverInput, + TResult: ResolverOutput, + { + Procedure2::builder() // You add default middleware here + } +} + +fn test_unstable_stuff(router: Router2) -> Router2 { + router + .procedure_not_stable("newstuff", { + ::builder().query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) + }) + .procedure_not_stable("newstuff2", { + ::builder() + // .with(invalidation(|ctx: Ctx, key, event| false)) + .with(Middleware::new( + move |ctx: Ctx, input: (), next| async move { + let result = next.exec(ctx, input).await; + result + }, + )) + .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) + }) +} + +#[derive(Debug, Clone, Serialize, Type)] +pub enum InvalidateEvent { + InvalidateKey(String), +} + +fn invalidation( + handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, +) -> Middleware +where + TError: Send + 'static, + TCtx: Clone + Send + 'static, + TInput: Clone + Send + 'static, + TResult: Send + 'static, +{ + let handler = Arc::new(handler); + Middleware::new(move |ctx: TCtx, input: TInput, next| { + let handler = handler.clone(); + async move { + // TODO: Register this with `TCtx` + let ctx2 = ctx.clone(); + let input2 = input.clone(); + let result = next.exec(ctx, input).await; + + // TODO: Unregister this with `TCtx` + result + } + }) +} + #[tokio::main] async fn main() { - let (routes, types) = Router2::from(mount()).build().unwrap(); + let router = Router2::from(mount()); + let router = test_unstable_stuff(router); + let (routes, types) = router.build().unwrap(); rspc::Typescript::default() // .formatter(specta_typescript::formatter::prettier), .header("// My custom header") - .enable_source_maps() + // .enable_source_maps() // TODO: Fix this .export_to( PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), &types, @@ -102,13 +183,13 @@ async fn main() { .unwrap(); // Be aware this is very experimental and doesn't support many types yet. - rspc::Rust::default() - // .header("// My custom header") - .export_to( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../client/src/bindings.rs"), - &types, - ) - .unwrap(); + // rspc::Rust::default() + // // .header("// My custom header") + // .export_to( + // PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../client/src/bindings.rs"), + // &types, + // ) + // .unwrap(); // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! let cors = CorsLayer::new() diff --git a/examples/bindings.ts b/examples/bindings.ts index bf4b5303..ed8d9d23 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,6 +1,17 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } -export { Procedures } from './bindings_t'; \ No newline at end of file +export type Procedures = { + echo: { input: string, output: string, error: unknown }, + error: { input: null, output: string, error: unknown }, + nested: { + hello: { input: null, output: string, error: unknown }, +}, + newstuff: { input: any, output: any, error: any }, + pings: { input: null, output: string, error: unknown }, + sendMsg: { input: string, output: string, error: unknown }, + transformMe: { input: null, output: string, error: unknown }, + version: { input: null, output: string, error: unknown }, +} \ No newline at end of file diff --git a/examples/bindings_t.d.ts b/examples/bindings_t.d.ts index 6a9a572d..06e2677d 100644 --- a/examples/bindings_t.d.ts +++ b/examples/bindings_t.d.ts @@ -7,6 +7,7 @@ export type Procedures = { nested: { hello: { input: null, output: string, error: unknown }, }, + newstuff: { input: any, output: any, error: any }, pings: { input: null, output: string, error: unknown }, sendMsg: { input: string, output: string, error: unknown }, transformMe: { input: null, output: string, error: unknown }, diff --git a/src/lib.rs b/src/lib.rs index e995807b..dc95dc9f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,16 +6,7 @@ //! //! Checkout the official docs at . This documentation is generally written **for authors of middleware and adapter**. //! -// #![forbid(unsafe_code)] // TODO -#![warn( - clippy::all, - clippy::cargo, - clippy::unwrap_used, - clippy::panic, - clippy::todo, - clippy::panic_in_result_fn, - // missing_docs -)] // TODO: Move to workspace lints +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", @@ -35,6 +26,9 @@ pub use procedure_kind::ProcedureKind; pub use router::Router2; pub use types::Types; +#[deprecated = "This stuff is unstable. Don't use it unless you know what your doing"] +pub mod modern; + // TODO: These will come in the future. pub(crate) use procedure::Procedure2; pub(crate) type State = (); diff --git a/src/modern/error.rs b/src/modern/error.rs new file mode 100644 index 00000000..98ef9c43 --- /dev/null +++ b/src/modern/error.rs @@ -0,0 +1,13 @@ +use std::error; + +use serde::Serialize; +use specta::Type; + +pub trait Error: error::Error + Send + Serialize + Type + 'static { + // Warning: Returning > 400 will fallback to `500`. As redirects would be invalid and `200` would break matching. + fn status(&self) -> u16 { + 500 + } +} + +// impl Error for rspc_core:: {} diff --git a/src/modern/infallible.rs b/src/modern/infallible.rs new file mode 100644 index 00000000..ffe943fc --- /dev/null +++ b/src/modern/infallible.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use serde::Serialize; +use specta::Type; + +#[derive(Type, Debug)] +pub enum Infallible {} + +impl fmt::Display for Infallible { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl Serialize for Infallible { + fn serialize(&self, _: S) -> Result + where + S: serde::Serializer, + { + unreachable!() + } +} + +impl std::error::Error for Infallible {} + +impl crate::modern::Error for Infallible { + fn status(&self) -> u16 { + unreachable!() + } +} diff --git a/src/modern/middleware.rs b/src/modern/middleware.rs new file mode 100644 index 00000000..0a344264 --- /dev/null +++ b/src/modern/middleware.rs @@ -0,0 +1,7 @@ +mod middleware; +mod next; + +pub use middleware::Middleware; +pub use next::Next; + +pub(crate) use middleware::MiddlewareHandler; diff --git a/src/modern/middleware/middleware.rs b/src/modern/middleware/middleware.rs new file mode 100644 index 00000000..7b00b281 --- /dev/null +++ b/src/modern/middleware/middleware.rs @@ -0,0 +1,140 @@ +//! This comment contains an overview of the rationale behind the design of the middleware system. +//! NOTE: It is *not* included in the generated Rust docs! +//! +//! For future reference: +//! +//! Having a standalone middleware that is like `fn my_middleware() -> impl Middleware<...>` results in *really* bad error messages. +//! This is because the middleware is defined within the function and then *constrained* at the function boundary. +//! These places are different so the compiler is like lol trait xyz with generics iop does match the trait xyz with generics abc. +//! +//! Instead if the builder function takes a [`MiddlewareBuilder`] the constrain it applied prior to the middleware being defined. +//! This allows the compiler to constrain the types at the middleware definition site which leads to insanely better error messages. +//! +//! Be aware this talk about constraining and definition is just me speaking about what I have observed. +//! This could be completely wrong from a technical perspective. +//! +//! TODO: Explaining why inference across boundaries is not supported. +//! +//! TODO: Explain why we can't have `fn mw(...) -> Middleware` -> It's because of default generics!!! +//! +//! TODO: Why we can't use `const`'s for declaring middleware -> Boxing + +use std::{pin::Pin, sync::Arc}; + +use futures::Future; + +use crate::modern::{procedure::ProcedureMeta, State}; + +use super::Next; + +pub(crate) type MiddlewareHandler = Box< + dyn Fn( + TNextCtx, + TNextInput, + ProcedureMeta, + ) -> Pin> + Send + 'static>> + + Send + + Sync + + 'static, +>; + +/// An abstraction for common logic that can be applied to procedures. +/// +/// A middleware can be used to run custom logic and modify the context, input, and result of the next procedure. This makes is perfect for logging, authentication and many other things! +/// +/// Middleware are applied with [ProcedureBuilder::with](crate::procedure::ProcedureBuilder::with). +/// +/// # Generics +/// +/// - `TError` - The type of the error that can be returned by the middleware. Defined by [ProcedureBuilder::error](crate::procedure::ProcedureBuilder::error). +/// - `TThisCtx` - // TODO +/// - `TThisInput` - // TODO +/// - `TThisResult` - // TODO +/// - `TNextCtx` - // TODO +/// - `TNextInput` - // TODO +/// - `TNextResult` - // TODO +/// +/// TODO: [ +// Context of previous layer (`ctx`), +// Error type, +// The input to the middleware (`input`), +// The result of the middleware (return type of future), +// - This following will default to the input types if not explicitly provided // TODO: Will this be confusing or good? +// The context returned by the middleware (`next.exec({dis_bit}, ...)`), +// The input to the next layer (`next.exec(..., {dis_bit})`), +// The result of the next layer (`let _result: {dis_bit} = next.exec(...)`), +// ] +/// +/// ```rust +/// TODO: Example to show where the generics line up. +/// ``` +/// +/// # Stacking +/// +/// TODO: Guide the user through stacking. +/// +/// # Example +/// +/// TODO: +/// +// TODO: Explain why they are required -> inference not supported across boundaries. +pub struct Middleware< + TError, + TThisCtx, + TThisInput, + TThisResult, + TNextCtx = TThisCtx, + TNextInput = TThisInput, + TNextResult = TThisResult, +> { + pub(crate) setup: Option>, + pub(crate) inner: Box< + dyn FnOnce( + MiddlewareHandler, + ) -> MiddlewareHandler, + >, +} + +// TODO: Debug impl + +impl + Middleware +where + TError: 'static, + TNextCtx: 'static, + TNextInput: 'static, + TNextResult: 'static, +{ + pub fn new> + Send + 'static>( + func: impl Fn(TThisCtx, TThisInput, Next) -> F + + Send + + Sync + + 'static, + ) -> Self { + Self { + setup: None, + inner: Box::new(move |next| { + // TODO: Don't `Arc>` + let next = Arc::new(next); + + Box::new(move |ctx, input, meta| { + let f = func( + ctx, + input, + Next { + meta, + next: next.clone(), + }, + ); + + Box::pin(f) + }) + }), + } + } + + pub fn setup(mut self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { + self.setup = Some(Box::new(func)); + self + } +} diff --git a/src/modern/middleware/next.rs b/src/modern/middleware/next.rs new file mode 100644 index 00000000..e9536bb4 --- /dev/null +++ b/src/modern/middleware/next.rs @@ -0,0 +1,30 @@ +use std::{fmt, sync::Arc}; + +use crate::modern::{middleware::middleware::MiddlewareHandler, procedure::ProcedureMeta}; + +pub struct Next { + // TODO: `pub(super)` over `pub(crate)` + pub(crate) meta: ProcedureMeta, + pub(crate) next: Arc>, +} + +impl fmt::Debug for Next { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Next").finish() + } +} + +impl Next +where + TCtx: 'static, + TInput: 'static, + TReturn: 'static, +{ + pub fn meta(&self) -> ProcedureMeta { + self.meta.clone() + } + + pub async fn exec(&self, ctx: TCtx, input: TInput) -> Result { + (self.next)(ctx, input, self.meta.clone()).await + } +} diff --git a/src/modern/mod.rs b/src/modern/mod.rs new file mode 100644 index 00000000..b019e484 --- /dev/null +++ b/src/modern/mod.rs @@ -0,0 +1,13 @@ +pub mod middleware; +pub mod procedure; + +mod error; +mod infallible; +mod state; +mod stream; + +pub use crate::procedure::Procedure2; +pub use error::Error; +pub use infallible::Infallible; +pub use state::State; +pub use stream::Stream; diff --git a/src/modern/procedure.rs b/src/modern/procedure.rs new file mode 100644 index 00000000..fe0cf95d --- /dev/null +++ b/src/modern/procedure.rs @@ -0,0 +1,27 @@ +//! A procedure holds a single operation that can be executed by the server. +//! +//! A procedure is built up from: +//! - any number of middleware +//! - a single resolver function (of type `query`, `mutation` or `subscription`) +//! +//! Features: +//! - Input types (Serde-compatible or custom) +//! - Result types (Serde-compatible or custom) +//! - [`Future`](#todo) or [`Stream`](#todo) results +//! - Typesafe error handling +//! +//! TODO: Request flow overview +//! TODO: Explain, what a procedure is, return type/struct, middleware, execution order, etc +//! + +mod builder; +mod meta; +mod procedure; +mod resolver_input; +mod resolver_output; + +pub use builder::ProcedureBuilder; +pub use meta::{ProcedureKind, ProcedureMeta}; +// pub use procedure::{Procedure, ProcedureTypeDefinition, UnbuiltProcedure}; +pub use resolver_input::ResolverInput; +pub use resolver_output::ResolverOutput; diff --git a/src/modern/procedure/builder.rs b/src/modern/procedure/builder.rs new file mode 100644 index 00000000..fc57b682 --- /dev/null +++ b/src/modern/procedure/builder.rs @@ -0,0 +1,115 @@ +use std::{fmt, future::Future}; + +use crate::{ + modern::{ + middleware::{Middleware, MiddlewareHandler}, + Error, State, + }, + Procedure2, +}; + +use super::{ProcedureKind, ProcedureMeta}; + +// TODO: Document the generics like `Middleware` +pub struct ProcedureBuilder { + pub(crate) build: Box< + dyn FnOnce( + ProcedureKind, + Vec>, + MiddlewareHandler, + ) -> Procedure2, + >, +} + +impl fmt::Debug + for ProcedureBuilder +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Procedure").finish() + } +} + +impl + ProcedureBuilder +where + TError: Error, + TRootCtx: 'static, + TCtx: 'static, + TInput: 'static, + TResult: 'static, +{ + pub fn with( + self, + mw: Middleware, + ) -> ProcedureBuilder + where + TNextCtx: 'static, + I: 'static, + R: 'static, + { + ProcedureBuilder { + build: Box::new(|ty, mut setups, handler| { + if let Some(setup) = mw.setup { + setups.push(setup); + } + + (self.build)(ty, setups, (mw.inner)(handler)) + }), + } + } + + pub fn setup(self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { + Self { + build: Box::new(|ty, mut setups, handler| { + setups.push(Box::new(func)); + (self.build)(ty, setups, handler) + }), + } + } + + pub fn query> + Send + 'static>( + self, + handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, + ) -> Procedure2 { + (self.build)( + ProcedureKind::Query, + Vec::new(), + Box::new(move |ctx, input, _| Box::pin(handler(ctx, input))), + ) + } + + pub fn mutation> + Send + 'static>( + self, + handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, + ) -> Procedure2 { + (self.build)( + ProcedureKind::Mutation, + Vec::new(), + Box::new(move |ctx, input, _| Box::pin(handler(ctx, input))), + ) + } +} + +// TODO +// impl +// ProcedureBuilder> +// where +// TError: Error, +// TRootCtx: 'static, +// TCtx: 'static, +// TInput: 'static, +// S: futures::Stream> + Send + 'static, +// { +// pub fn subscription> + Send + 'static>( +// self, +// handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, +// ) -> Procedure2 { +// (self.build)( +// ProcedureKind::Subscription, +// Vec::new(), +// Box::new(move |ctx, input, _| { +// Box::pin(handler(ctx, input).map(|s| s.map(|s| crate::modern::Stream(s)))) +// }), +// ) +// } +// } diff --git a/src/modern/procedure/meta.rs b/src/modern/procedure/meta.rs new file mode 100644 index 00000000..bdea8183 --- /dev/null +++ b/src/modern/procedure/meta.rs @@ -0,0 +1,55 @@ +use std::{borrow::Cow, sync::Arc}; + +// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, specta::Type)] +// #[specta(rename_all = "camelCase")] +// pub enum ProcedureKind { +// Query, +// Mutation, +// Subscription, +// } + +// impl fmt::Display for ProcedureKind { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// match self { +// Self::Query => write!(f, "Query"), +// Self::Mutation => write!(f, "Mutation"), +// Self::Subscription => write!(f, "Subscription"), +// } +// } +// } + +pub use crate::ProcedureKind; + +#[derive(Debug, Clone)] +enum ProcedureName { + Static(&'static str), + Dynamic(Arc), +} + +#[derive(Debug, Clone)] +pub struct ProcedureMeta { + name: ProcedureName, + kind: ProcedureKind, +} + +impl ProcedureMeta { + pub(crate) fn new(name: Cow<'static, str>, kind: ProcedureKind) -> Self { + Self { + name: ProcedureName::Dynamic(Arc::new(name.into_owned())), + kind, + } + } +} + +impl ProcedureMeta { + pub fn name(&self) -> &str { + match &self.name { + ProcedureName::Static(name) => name, + ProcedureName::Dynamic(name) => name.as_str(), + } + } + + pub fn kind(&self) -> ProcedureKind { + self.kind + } +} diff --git a/src/modern/procedure/procedure.rs b/src/modern/procedure/procedure.rs new file mode 100644 index 00000000..86f6de87 --- /dev/null +++ b/src/modern/procedure/procedure.rs @@ -0,0 +1,185 @@ +// use std::{borrow::Cow, fmt, sync::Arc}; + +// use futures::FutureExt; +// use specta::{DataType, TypeMap}; + +// use crate::{Error, State}; + +// use super::{ +// exec_input::{AnyInput, InputValueInner}, +// stream::ProcedureStream, +// InternalError, ProcedureBuilder, ProcedureExecInput, ProcedureInput, ProcedureKind, +// ProcedureMeta, ResolverInput, ResolverOutput, +// }; + +// pub(super) type InvokeFn = Arc< +// dyn Fn(TCtx, &mut dyn InputValueInner) -> Result + Send + Sync, +// >; + +// /// Represents a single operations on the server that can be executed. +// /// +// /// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. +// /// +// pub struct Procedure { +// kind: ProcedureKind, +// ty: ProcedureTypeDefinition, +// handler: InvokeFn, +// } + +// impl Clone for Procedure { +// fn clone(&self) -> Self { +// Self { +// kind: self.kind, +// ty: self.ty.clone(), +// handler: self.handler.clone(), +// } +// } +// } + +// impl fmt::Debug for Procedure { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// f.debug_struct("Procedure") +// .field("kind", &self.kind) +// .field("ty", &self.ty) +// .field("handler", &"...") +// .finish() +// } +// } + +// impl Procedure +// where +// TCtx: 'static, +// { +// /// Construct a new procedure using [`ProcedureBuilder`]. +// pub fn builder() -> ProcedureBuilder +// where +// TError: Error, +// // Only the first layer (middleware or the procedure) needs to be a valid input/output type +// I: ResolverInput, +// R: ResolverOutput, +// { +// ProcedureBuilder { +// build: Box::new(|kind, setups, handler| { +// // TODO: Don't be `Arc>` just `Arc<_>` +// let handler = Arc::new(handler); + +// UnbuiltProcedure::new(move |key, state, type_map| { +// let meta = ProcedureMeta::new(key.clone(), kind); +// for setup in setups { +// setup(state, meta.clone()); +// } + +// Procedure { +// kind, +// ty: ProcedureTypeDefinition { +// key, +// kind, +// input: I::data_type(type_map), +// result: R::data_type(type_map), +// }, +// handler: Arc::new(move |ctx, input| { +// let fut = handler( +// ctx, +// I::from_value(ProcedureExecInput::new(input))?, +// meta.clone(), +// ); + +// Ok(R::into_procedure_stream(fut.into_stream())) +// }), +// } +// }) +// }), +// } +// } +// } + +// impl Procedure { +// pub fn kind(&self) -> ProcedureKind { +// self.kind +// } + +// /// Export the [Specta](https://docs.rs/specta) types for this procedure. +// /// +// /// TODO - Use this with `rspc::typescript` +// /// +// /// # Usage +// /// +// /// ```rust +// /// todo!(); # TODO: Example +// /// ``` +// pub fn ty(&self) -> &ProcedureTypeDefinition { +// &self.ty +// } + +// /// Execute a procedure with the given context and input. +// /// +// /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. +// /// +// /// # Usage +// /// +// /// ```rust +// /// use serde_json::Value; +// /// +// /// fn run_procedure(procedure: Procedure) -> Vec { +// /// procedure +// /// .exec((), Value::Null) +// /// .collect::>() +// /// .await +// /// .into_iter() +// /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) +// /// .collect::>() +// /// } +// /// ``` +// pub fn exec<'de, T: ProcedureInput<'de>>( +// &self, +// ctx: TCtx, +// input: T, +// ) -> Result { +// match input.into_deserializer() { +// Ok(deserializer) => { +// let mut input = ::erase(deserializer); +// (self.handler)(ctx, &mut input) +// } +// Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), +// } +// } +// } + +// #[derive(Debug, Clone, PartialEq)] +// pub struct ProcedureTypeDefinition { +// // TODO: Should `key` move onto `Procedure` instead?s +// pub key: Cow<'static, str>, +// pub kind: ProcedureKind, +// pub input: DataType, +// pub result: DataType, +// } + +// pub struct UnbuiltProcedure( +// Box, &mut State, &mut TypeMap) -> Procedure>, +// ); + +// impl fmt::Debug for UnbuiltProcedure { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// f.debug_struct("UnbuiltProcedure").finish() +// } +// } + +// impl UnbuiltProcedure { +// pub(crate) fn new( +// build_fn: impl FnOnce(Cow<'static, str>, &mut State, &mut TypeMap) -> Procedure + 'static, +// ) -> Self { +// Self(Box::new(build_fn)) +// } + +// /// Build the procedure invoking all the setup functions. +// /// +// /// Generally you will not need to call this directly as you can give a [ProcedureFactory] to the [RouterBuilder::procedure] and let it take care of the rest. +// pub fn build( +// self, +// key: Cow<'static, str>, +// state: &mut State, +// type_map: &mut TypeMap, +// ) -> Procedure { +// (self.0)(key, state, type_map) +// } +// } diff --git a/src/modern/procedure/resolver_input.rs b/src/modern/procedure/resolver_input.rs new file mode 100644 index 00000000..ec64e196 --- /dev/null +++ b/src/modern/procedure/resolver_input.rs @@ -0,0 +1,55 @@ +// /// The input to a procedure which is derived from an [`ProcedureInput`](crate::procedure::Argument). +// /// +// /// This trait has a built in implementation for any type which implements [`DeserializeOwned`](serde::de::DeserializeOwned). +// /// +// /// ## How this works? +// /// +// /// [`Self::from_value`] will be provided with a [`ProcedureInput`] which wraps the [`Argument::Value`](super::Argument::Value) from the argument provided to the [`Procedure::exec`](super::Procedure) call. +// /// +// /// Input is responsible for converting this value into the type the user specified for the procedure. +// /// +// /// If the type implements [`DeserializeOwned`](serde::de::DeserializeOwned) we will use Serde, otherwise we will attempt to downcast the value. +// /// +// /// ## Implementation for custom types +// /// +// /// Say you have a type `MyCoolThing` which you want to use as an argument to an rspc procedure: +// /// +// /// ``` +// /// pub struct MyCoolThing(pub String); +// /// +// /// impl ResolverInput for MyCoolThing { +// /// fn from_value(value: ProcedureInput) -> Result { +// /// Ok(todo!()) // Refer to ProcedureInput's docs +// /// } +// /// } +// /// +// /// // You should also implement `ProcedureInput`. +// /// +// /// fn usage_within_rspc() { +// /// ::builder().query(|_, _: MyCoolThing| async move { () }); +// /// } +// /// ``` + +// TODO: Should this be in `rspc_core`??? +// TODO: Maybe rename? + +use serde::de::DeserializeOwned; +use specta::{datatype::DataType, Type, TypeCollection}; + +/// TODO: Restore the above docs but check they are correct +pub trait ResolverInput: Sized + Send + 'static { + fn data_type(types: &mut TypeCollection) -> DataType; + + /// Convert the [`DynInput`] into the type the user specified for the procedure. + fn from_input(input: rspc_core::DynInput) -> Result; +} + +impl ResolverInput for T { + fn data_type(types: &mut TypeCollection) -> DataType { + T::inline(types, specta::Generics::Definition) + } + + fn from_input(input: rspc_core::DynInput) -> Result { + Ok(input.deserialize()?) + } +} diff --git a/src/modern/procedure/resolver_output.rs b/src/modern/procedure/resolver_output.rs new file mode 100644 index 00000000..b2bb8937 --- /dev/null +++ b/src/modern/procedure/resolver_output.rs @@ -0,0 +1,129 @@ +// /// A type which can be returned from a procedure. +// /// +// /// This has a default implementation for all [`Serialize`](serde::Serialize) types. +// /// +// /// ## How this works? +// /// +// /// We call [`Self::into_procedure_stream`] with the stream produced by the users handler and it will produce the [`ProcedureStream`] which is returned from the [`Procedure::exec`](super::Procedure::exec) call. If the user's handler was a [`Future`](std::future::Future) it will be converted into a [`Stream`](futures::Stream) by rspc. +// /// +// /// For each value the [`Self::into_procedure_stream`] implementation **must** defer to [`Self::into_procedure_result`] to convert the value into a [`ProcedureOutput`]. rspc provides a default implementation that takes care of this for you so don't override it unless you have a good reason. +// /// +// /// ## Implementation for custom types +// /// +// /// ```rust +// /// pub struct MyCoolThing(pub String); +// /// +// /// impl ResolverOutput for MyCoolThing { +// /// fn into_procedure_result(self) -> Result { +// /// Ok(todo!()) // Refer to ProcedureOutput's docs +// /// } +// /// } +// /// +// /// fn usage_within_rspc() { +// /// ::builder().query(|_, _: ()| async move { MyCoolThing("Hello, World!".to_string()) }); +// /// } +// /// ``` +// // TODO: Do some testing and set this + add documentation link into it. +// // #[diagnostic::on_unimplemented( +// // message = "Your procedure must return a type that implements `serde::Serialize + specta::Type + 'static`", +// // note = "ResolverOutput requires a `T where T: serde::Serialize + specta::Type + 'static` to be returned from your procedure" +// // )] + +use futures::{Stream, StreamExt}; +use rspc_core::ProcedureStream; +use serde::Serialize; +use specta::{datatype::DataType, Generics, Type, TypeCollection}; + +use crate::modern::Error; + +// TODO: Maybe in `rspc_core`?? + +/// TODO: bring back any correct parts of the docs above +pub trait ResolverOutput: Sized + Send + 'static { + // /// Convert the procedure and any async part of the value into a [`ProcedureStream`]. + // /// + // /// This primarily exists so the [`rspc::Stream`](crate::Stream) implementation can merge it's stream into the procedure stream. + // fn into_procedure_stream( + // procedure: impl Stream> + Send + 'static, + // ) -> ProcedureStream + // where + // TError: Error, + // { + // ProcedureStream::from_stream(procedure.map(|v| v?.into_procedure_result())) + // } + + // /// Convert the value from the user into a [`ProcedureOutput`]. + // fn into_procedure_result(self) -> Result; + + // TODO: Be an associated type instead so we can constrain later for better errors???? + fn data_type(types: &mut TypeCollection) -> DataType; + + fn into_procedure_stream(self) -> ProcedureStream; +} + +// TODO: Should this be `Result`? +impl ResolverOutput for T +where + T: Serialize + Type + Send + 'static, + TError: Error, +{ + fn data_type(types: &mut TypeCollection) -> DataType { + T::inline(types, Generics::Definition) + } + + fn into_procedure_stream(self) -> ProcedureStream { + ProcedureStream::from_value(Ok(self)) + } +} + +impl ResolverOutput for crate::modern::Stream +where + TErr: Send, + S: Stream> + Send + 'static, + T: ResolverOutput, +{ + fn data_type(types: &mut TypeCollection) -> DataType { + T::data_type(types) // TODO: Do we need to do anything special here so the frontend knows this is a stream? + } + + fn into_procedure_stream(self) -> ProcedureStream { + // ProcedureStream::from_value(Ok(self)) + + // ProcedureStream::from_stream( + // self.0 + // .map(|v| match v { + // Ok(s) => { + // s.0.map(|v| v.and_then(|v| v.into_procedure_result())) + // .right_stream() + // } + // Err(err) => once(async move { Err(err) }).left_stream(), + // }) + // .flatten(), + // ) + + todo!(); + } + + // fn into_procedure_stream( + // procedure: impl Stream> + Send + 'static, + // ) -> ProcedureStream + // where + // TErr: Error, + // { + // ProcedureStream::from_stream( + // procedure + // .map(|v| match v { + // Ok(s) => { + // s.0.map(|v| v.and_then(|v| v.into_procedure_result())) + // .right_stream() + // } + // Err(err) => once(async move { Err(err) }).left_stream(), + // }) + // .flatten(), + // ) + // } + + // fn into_procedure_result(self) -> Result { + // panic!("returning nested rspc::Stream's is not currently supported.") + // } +} diff --git a/src/modern/state.rs b/src/modern/state.rs new file mode 100644 index 00000000..52f0602a --- /dev/null +++ b/src/modern/state.rs @@ -0,0 +1,95 @@ +use std::{ + any::{Any, TypeId}, + collections::HashMap, + fmt, + hash::{BuildHasherDefault, Hasher}, +}; + +/// A hasher for `TypeId`s that takes advantage of its known characteristics. +/// +/// Author of `anymap` crate has done research on the topic: +/// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599 +#[derive(Debug, Default)] +struct NoOpHasher(u64); + +impl Hasher for NoOpHasher { + fn write(&mut self, _bytes: &[u8]) { + unimplemented!("This NoOpHasher can only handle u64s") + } + + fn write_u64(&mut self, i: u64) { + self.0 = i; + } + + fn finish(&self) -> u64 { + self.0 + } +} + +pub struct State( + HashMap, BuildHasherDefault>, +); + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("State") + // .field("state", &self.state) // TODO + .finish() + } +} + +impl Default for State { + fn default() -> Self { + Self(Default::default()) + } +} + +impl State { + pub fn get(&self) -> Option<&T> { + self.0.get(&TypeId::of::()).map(|v| { + v.downcast_ref::() + .expect("unreachable: TypeId matches but downcast failed") + }) + } + + pub fn get_mut(&self) -> Option<&T> { + self.0.get(&TypeId::of::()).map(|v| { + v.downcast_ref::() + .expect("unreachable: TypeId matches but downcast failed") + }) + } + + pub fn get_or_init(&mut self, init: impl FnOnce() -> T) -> &T { + self.0 + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(init())) + .downcast_ref::() + .expect("unreachable: TypeId matches but downcast failed") + } + + pub fn get_mut_or_init( + &mut self, + init: impl FnOnce() -> T, + ) -> &mut T { + self.0 + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(init())) + .downcast_mut::() + .expect("unreachable: TypeId matches but downcast failed") + } + + pub fn contains_key(&self) -> bool { + self.0.contains_key(&TypeId::of::()) + } + + pub fn insert(&mut self, t: T) { + self.0.insert(TypeId::of::(), Box::new(t)); + } + + pub fn remove(&mut self) -> Option { + self.0.remove(&TypeId::of::()).map(|v| { + *v.downcast::() + .expect("unreachable: TypeId matches but downcast failed") + }) + } +} diff --git a/src/modern/stream.rs b/src/modern/stream.rs new file mode 100644 index 00000000..7bf42d8e --- /dev/null +++ b/src/modern/stream.rs @@ -0,0 +1,35 @@ +/// Return a [`Stream`](futures::Stream) of values from a [`Procedure::query`](procedure::ProcedureBuilder::query) or [`Procedure::mutation`](procedure::ProcedureBuilder::mutation). +/// +/// ## Why not a subscription? +/// +/// A [`subscription`](procedure::ProcedureBuilder::subscription) must return a [`Stream`](futures::Stream) so it would be fair to question when you would use this. +/// +/// A [`query`](procedure::ProcedureBuilder::query) or [`mutation`](procedure::ProcedureBuilder::mutation) produce a single result where a subscription produces many discrete values. +/// +/// Using [`rspc::Stream`](Self) within a query or mutation will result in your procedure returning a collection (Eg. `Vec`) of [`Stream::Item`](futures::Stream) on the frontend. +/// +/// This means it would be well suited for streaming the result of a computation or database query while a subscription would be well suited for a chat room. +/// +/// ## Usage +/// **WARNING**: This example shows the low-level procedure API. You should refer to [`Rspc`](crate::Rspc) for the high-level API. +/// ```rust +/// use futures::stream::once; +/// +/// ::builder().query(|_, _: ()| async move { rspc::Stream(once(async move { 42 })) }); +/// ``` +/// +pub struct Stream(pub S); + +// WARNING: We can not add an implementation for `Debug` without breaking `rspc_tracing` + +impl Default for Stream { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Clone for Stream { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} diff --git a/src/procedure.rs b/src/procedure.rs index 0c591111..2f4b68eb 100644 --- a/src/procedure.rs +++ b/src/procedure.rs @@ -1,8 +1,15 @@ -use std::panic::Location; +use std::{borrow::Cow, panic::Location, sync::Arc}; +use rspc_core::{Procedure, ProcedureStream}; use specta::datatype::DataType; -use crate::{ProcedureKind, State}; +use crate::{ + modern::{ + procedure::{ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput}, + Error, + }, + ProcedureKind, State, +}; #[derive(Clone)] pub(crate) struct ProcedureType { @@ -26,58 +33,142 @@ pub struct Procedure2 { // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` impl Procedure2 { - // TODO: `fn builder` + #[doc(hidden)] // TODO: Remove this once stable + /// Construct a new procedure using [`ProcedureBuilder`]. + #[track_caller] + pub fn builder() -> ProcedureBuilder + where + TCtx: Send + 'static, + TError: Error, + // Only the first layer (middleware or the procedure) needs to be a valid input/output type + I: ResolverInput, + R: ResolverOutput, + { + ProcedureBuilder { + build: Box::new(|kind, setups, handler| { + // TODO: Don't be `Arc>` just `Arc<_>` + let handler = Arc::new(handler); + + Procedure2 { + setup: Default::default(), + ty: ProcedureType { + kind, + input: DataType::Any, // I::data_type(type_map), + output: DataType::Any, // R::data_type(type_map), + error: DataType::Any, // TODO + location: Location::caller().clone(), + }, + inner: Procedure::new(move |ctx, input| { + // let input: I = I::from_input(input).unwrap(); // TODO: Error handling + + // let key = "todo".to_string().into(); // TODO: Work this out properly + + // let meta = ProcedureMeta::new(key.clone(), kind); + // for setup in setups { + // setup(state, meta.clone()); + // } + + // Procedure { + // kind, + // ty: ProcedureTypeDefinition { + // key, + // kind, + // input: I::data_type(type_map), + // result: R::data_type(type_map), + // }, + // handler: Arc::new(move |ctx, input| { + // let fut = handler( + // ctx, + // I::from_value(ProcedureExecInput::new(input))?, + // meta.clone(), + // ); + + // Ok(R::into_procedure_stream(fut.into_stream())) + // }), + // } + + // let fut = handler( + // ctx, + // I::from_value(ProcedureExecInput::new(input))?, + // meta.clone(), + // ); + + // Ok(R::into_procedure_stream(fut.into_stream())) + + // ProcedureStream::from_value(Ok("todo")) // TODO + // + // TODO: borrow into procedure + let key: Cow<'static, str> = "todo".to_string().into(); // TODO: Work this out properly + let meta = ProcedureMeta::new(key.clone(), kind); + // TODO: END + + let fut = handler( + ctx, + I::from_input(input).unwrap(), // TODO: Error handling + meta.clone(), + ); + + ProcedureStream::from_future_procedure_stream(async move { + Ok(R::into_procedure_stream(fut.await.unwrap())) // TODO: Error handling + + // Ok(futures::stream::once(async move { Ok("todo") })) + }) + }), + } + }), + } + } + + // TODO: Expose all fields // TODO: Make `pub` // pub(crate) fn kind(&self) -> ProcedureKind2 { // self.kind // } - // TODO: Expose all fields - - // /// Export the [Specta](https://docs.rs/specta) types for this procedure. - // /// - // /// TODO - Use this with `rspc::typescript` - // /// - // /// # Usage - // /// - // /// ```rust - // /// todo!(); # TODO: Example - // /// ``` - // pub fn ty(&self) -> &ProcedureTypeDefinition { - // &self.ty - // } + // /// Export the [Specta](https://docs.rs/specta) types for this procedure. + // /// + // /// TODO - Use this with `rspc::typescript` + // /// + // /// # Usage + // /// + // /// ```rust + // /// todo!(); # TODO: Example + // /// ``` + // pub fn ty(&self) -> &ProcedureTypeDefinition { + // &self.ty + // } - // /// Execute a procedure with the given context and input. - // /// - // /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. - // /// - // /// # Usage - // /// - // /// ```rust - // /// use serde_json::Value; - // /// - // /// fn run_procedure(procedure: Procedure) -> Vec { - // /// procedure - // /// .exec((), Value::Null) - // /// .collect::>() - // /// .await - // /// .into_iter() - // /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) - // /// .collect::>() - // /// } - // /// ``` - // pub fn exec<'de, T: ProcedureInput<'de>>( - // &self, - // ctx: TCtx, - // input: T, - // ) -> Result { - // match input.into_deserializer() { - // Ok(deserializer) => { - // let mut input = ::erase(deserializer); - // (self.handler)(ctx, &mut input) - // } - // Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), + // /// Execute a procedure with the given context and input. + // /// + // /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. + // /// + // /// # Usage + // /// + // /// ```rust + // /// use serde_json::Value; + // /// + // /// fn run_procedure(procedure: Procedure) -> Vec { + // /// procedure + // /// .exec((), Value::Null) + // /// .collect::>() + // /// .await + // /// .into_iter() + // /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) + // /// .collect::>() + // /// } + // /// ``` + // pub fn exec<'de, T: ProcedureInput<'de>>( + // &self, + // ctx: TCtx, + // input: T, + // ) -> Result { + // match input.into_deserializer() { + // Ok(deserializer) => { + // let mut input = ::erase(deserializer); + // (self.handler)(ctx, &mut input) // } + // Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), // } + // } } diff --git a/src/router.rs b/src/router.rs index 9943e7e2..a7cc2a86 100644 --- a/src/router.rs +++ b/src/router.rs @@ -184,3 +184,18 @@ fn get_flattened_name(name: &Vec>) -> Cow<'static, str> { name.join(".").to_string().into() } } + +// TODO: Remove once procedure syntax stabilizes +impl Router2 { + #[doc(hidden)] + // TODO: Enforce unique across all methods (query, subscription, etc). Eg. `insert` should yield error if key already exists. + pub fn procedure_not_stable( + mut self, + key: impl Into>, + mut procedure: Procedure2, + ) -> Self { + self.setup.extend(procedure.setup.drain(..)); + self.procedures.insert(vec![key.into()], procedure); + self + } +} From 1fa190559eeffab25a8d23f436b519d70b0ed881 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 6 Dec 2024 17:14:37 +0800 Subject: [PATCH 15/67] migrate middleware from `main-rewrite` --- Cargo.toml | 15 +- examples/Cargo.toml | 2 +- examples/axum/Cargo.toml | 2 +- {crates => integrations}/axum/Cargo.toml | 2 +- .../axum/src/extractors.rs | 0 {crates => integrations}/axum/src/jsonrpc.rs | 0 .../axum/src/jsonrpc_exec.rs | 0 {crates => integrations}/axum/src/legacy.rs | 0 {crates => integrations}/axum/src/lib.rs | 0 {crates => integrations}/axum/src/v2.rs | 0 {crates => integrations}/tauri/Cargo.toml | 2 +- {crates => integrations}/tauri/src/jsonrpc.rs | 0 .../tauri/src/jsonrpc_exec.rs | 0 {crates => integrations}/tauri/src/lib.rs | 0 middleware/README.md | 6 + middleware/invalidation/Cargo.toml | 17 + middleware/invalidation/README.md | 82 +++++ middleware/invalidation/src/lib.rs | 9 + middleware/openapi/Cargo.toml | 19 ++ middleware/openapi/README.md | 3 + middleware/openapi/src/lib.rs | 293 ++++++++++++++++++ middleware/openapi/src/swagger.html | 34 ++ middleware/playground/Cargo.toml | 15 + middleware/playground/README.md | 3 + middleware/playground/src/lib.rs | 7 + middleware/tracing/Cargo.toml | 19 ++ middleware/tracing/README.md | 3 + middleware/tracing/src/lib.rs | 62 ++++ middleware/tracing/src/traceable.rs | 26 ++ 29 files changed, 611 insertions(+), 10 deletions(-) rename {crates => integrations}/axum/Cargo.toml (94%) rename {crates => integrations}/axum/src/extractors.rs (100%) rename {crates => integrations}/axum/src/jsonrpc.rs (100%) rename {crates => integrations}/axum/src/jsonrpc_exec.rs (100%) rename {crates => integrations}/axum/src/legacy.rs (100%) rename {crates => integrations}/axum/src/lib.rs (100%) rename {crates => integrations}/axum/src/v2.rs (100%) rename {crates => integrations}/tauri/Cargo.toml (92%) rename {crates => integrations}/tauri/src/jsonrpc.rs (100%) rename {crates => integrations}/tauri/src/jsonrpc_exec.rs (100%) rename {crates => integrations}/tauri/src/lib.rs (100%) create mode 100644 middleware/README.md create mode 100644 middleware/invalidation/Cargo.toml create mode 100644 middleware/invalidation/README.md create mode 100644 middleware/invalidation/src/lib.rs create mode 100644 middleware/openapi/Cargo.toml create mode 100644 middleware/openapi/README.md create mode 100644 middleware/openapi/src/lib.rs create mode 100644 middleware/openapi/src/swagger.html create mode 100644 middleware/playground/Cargo.toml create mode 100644 middleware/playground/README.md create mode 100644 middleware/playground/src/lib.rs create mode 100644 middleware/tracing/Cargo.toml create mode 100644 middleware/tracing/README.md create mode 100644 middleware/tracing/src/lib.rs create mode 100644 middleware/tracing/src/traceable.rs diff --git a/Cargo.toml b/Cargo.toml index 7d228d3b..0adb9d14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,14 +46,9 @@ thiserror = "2.0.3" tokio = { version = "1.41.1", features = ["sync", "rt", "macros"] } specta-serde = "0.0.7" tracing = { version = "0.1.40", optional = true } -# transient = "0.4.1" -# better_any = "0.2.0" - -# https://github.com/rust-lang/rust/issues/77125 -# typeid = "1.0.2" [workspace] -members = ["./crates/*", "./examples", "./examples/axum", "./examples/client", "crates/core"] +members = ["./integrations/*", "./middleware/*", "./crates/*", "./examples", "./examples/axum", "./examples/client", "crates/core"] [patch.crates-io] specta = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } @@ -64,3 +59,11 @@ specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "bf3a09 # specta-typescript = { path = "/Users/oscar/Desktop/specta/specta-typescript" } # specta-serde = { path = "/Users/oscar/Desktop/specta/specta-serde" } # specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +unwrap_used = { level = "warn", priority = -1 } +panic = { level = "warn", priority = -1 } +todo = { level = "warn", priority = -1 } +panic_in_result_fn = { level = "warn", priority = -1 } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 0470f477..32c258c2 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,7 +8,7 @@ publish = false rspc = { path = "../" } specta = "=2.0.0-rc.20" specta-typescript = "0.0.7" -rspc-axum = { path = "../crates/axum" } +rspc-axum = { path = "../integrations/axum" } async-stream = "0.3.6" axum = "0.7.9" chrono = { version = "0.4.38", features = ["serde"] } diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 9a3991eb..681d17d3 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -6,7 +6,7 @@ publish = false [dependencies] rspc = { path = "../../", features = ["typescript", "rust"] } -rspc-axum = { path = "../../crates/axum", features = ["ws"] } +rspc-axum = { path = "../../integrations/axum", features = ["ws"] } tokio = { version = "1.41.1", features = ["full"] } async-stream = "0.3.6" axum = { version = "0.7.9", features = ["ws"] } diff --git a/crates/axum/Cargo.toml b/integrations/axum/Cargo.toml similarity index 94% rename from crates/axum/Cargo.toml rename to integrations/axum/Cargo.toml index 07dd42c9..529f6daf 100644 --- a/crates/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -20,7 +20,7 @@ default = [] ws = ["axum/ws"] [dependencies] -rspc-core = { version = "0.0.1", path = "../core" } +rspc-core = { version = "0.0.1", path = "../../crates/core" } axum = "0.7.9" serde_json = "1" diff --git a/crates/axum/src/extractors.rs b/integrations/axum/src/extractors.rs similarity index 100% rename from crates/axum/src/extractors.rs rename to integrations/axum/src/extractors.rs diff --git a/crates/axum/src/jsonrpc.rs b/integrations/axum/src/jsonrpc.rs similarity index 100% rename from crates/axum/src/jsonrpc.rs rename to integrations/axum/src/jsonrpc.rs diff --git a/crates/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs similarity index 100% rename from crates/axum/src/jsonrpc_exec.rs rename to integrations/axum/src/jsonrpc_exec.rs diff --git a/crates/axum/src/legacy.rs b/integrations/axum/src/legacy.rs similarity index 100% rename from crates/axum/src/legacy.rs rename to integrations/axum/src/legacy.rs diff --git a/crates/axum/src/lib.rs b/integrations/axum/src/lib.rs similarity index 100% rename from crates/axum/src/lib.rs rename to integrations/axum/src/lib.rs diff --git a/crates/axum/src/v2.rs b/integrations/axum/src/v2.rs similarity index 100% rename from crates/axum/src/v2.rs rename to integrations/axum/src/v2.rs diff --git a/crates/tauri/Cargo.toml b/integrations/tauri/Cargo.toml similarity index 92% rename from crates/tauri/Cargo.toml rename to integrations/tauri/Cargo.toml index 6402f19d..5aaa3ffd 100644 --- a/crates/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -16,7 +16,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -rspc-core = { version = "0.0.1", path = "../core" } +rspc-core = { version = "0.0.1", path = "../../crates/core" } tauri = "2" serde_json = "1" diff --git a/crates/tauri/src/jsonrpc.rs b/integrations/tauri/src/jsonrpc.rs similarity index 100% rename from crates/tauri/src/jsonrpc.rs rename to integrations/tauri/src/jsonrpc.rs diff --git a/crates/tauri/src/jsonrpc_exec.rs b/integrations/tauri/src/jsonrpc_exec.rs similarity index 100% rename from crates/tauri/src/jsonrpc_exec.rs rename to integrations/tauri/src/jsonrpc_exec.rs diff --git a/crates/tauri/src/lib.rs b/integrations/tauri/src/lib.rs similarity index 100% rename from crates/tauri/src/lib.rs rename to integrations/tauri/src/lib.rs diff --git a/middleware/README.md b/middleware/README.md new file mode 100644 index 00000000..da43f66d --- /dev/null +++ b/middleware/README.md @@ -0,0 +1,6 @@ +# Official rspc Middleware + +> [!CAUTION] +> These are not yet stable so use at your own risk. + + diff --git a/middleware/invalidation/Cargo.toml b/middleware/invalidation/Cargo.toml new file mode 100644 index 00000000..e8bbd82b --- /dev/null +++ b/middleware/invalidation/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rspc-invalidation" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[dependencies] +async-stream = "0.3.5" +rspc = { path = "../../" } + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/invalidation/README.md b/middleware/invalidation/README.md new file mode 100644 index 00000000..bc88cf39 --- /dev/null +++ b/middleware/invalidation/README.md @@ -0,0 +1,82 @@ +# rspc Invalidation + +For now this is not going to be released as we need to work out if their is any value to an official middleware, instead of an example project implementing the same thing user-space? + +## Questions + +For my own future reference: https://discord.com/channels/@me/813276814801764382/1263123489477361828 + +### Pull vs Push based invalidation events + +Pull based is where the middleware is applied to the query. +Push based is where the middleware is applied to the mutation. + +I think we want a pull-based so resources can define their dependencies a-la React dependencies array. + +### Stream or not? + +I'm leaning stream-based because it pushes the type safety concern onto the end user. + +```rust +::builder() + // "Pull"-based. Applied to queries. (I personally a "Pull"-based approach is better) + .with(rspc_invalidation::invalidation( + |input, result, operation| operation.key() == "store.set", + )) + .with(rspc_invalidation::invalidation( + // TODO: how is `input().id` even gonna work lol + |input, result, operation| { + operation.key() == "notes.update" && operation.input().id == input.id + }, + )) + // "Push"-based. Applied to mutations. + .with(rspc_invalidation::invalidation( + |input, result, invalidate| invalidate("store.get", ()), + )) + .with(rspc_invalidation::invalidation( + |input, result, operation| invalidate("notes.get", input.id), + )) + // "Pull"-based but with stream. + .with(rspc_invalidation::invalidation(|input: TArgs| { + stream! { + // If practice subscribe to some central event bus for changes + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + yield Invalidate; // pub struct Invalidate; + } + } + })) + .query(...) +``` + +### Exposing result of procedure to invalidation closure + +If we expose result to the invalidate callback either the `Stream` or the value must be `Clone` which is not great, although the constrain can be applied locally by the middleware. + +If we expose the result and use a stream-based approach do we spawn a new invalidation closure for every result? I think this is something we will wanna leave the user in control of but no idea what that API would look like. + +### How do we get `BuiltRouter` into `Procedure`? + +It kinda has to come in via context or we need some magic system within rspc's core. Otherwise we basically have a recursive dependency. + +### Frontend? + +Will we expose a package or will it be on the user to hook it up? + +## Other concerns + +## User activity + +Really we wanna only push invalidation events that are related to parts of the app the user currently has active. An official system would need to take this into account somehow. Maybe some integration with the frontend router and websocket state using the `TCtx`??? + +## Data or invalidation + +If we can be pretty certain the frontend wants the new data we can safely push it straight to the frontend instead of just asking the frontend to refetch. This will be much faster but if your not tracking user-activity it will be way slower because of the potential volume of data. + +Tracking user activity pretty much requires some level of router integration which might be nice to have an abstraction for but it's also hard. + +## Authorization + +**This is why rspc can't own the subscription!!!** + +We should also have a way to take into account authorization and what invalidation events the user is able to see. For something like Spacedrive we never had this problem because we are a desktop app but any web app would require this. \ No newline at end of file diff --git a/middleware/invalidation/src/lib.rs b/middleware/invalidation/src/lib.rs new file mode 100644 index 00000000..cea14843 --- /dev/null +++ b/middleware/invalidation/src/lib.rs @@ -0,0 +1,9 @@ +//! rspc-invalidation: Real-time invalidation support for rspc +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +// TODO: Refer to `../README.md` to see status diff --git a/middleware/openapi/Cargo.toml b/middleware/openapi/Cargo.toml new file mode 100644 index 00000000..0861a5c9 --- /dev/null +++ b/middleware/openapi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rspc-openapi" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[dependencies] +rspc = { path = "../../" } +axum = { version = "0.7.5", default-features = false } +serde_json = "1.0.127" +futures = "0.3.30" + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/openapi/README.md b/middleware/openapi/README.md new file mode 100644 index 00000000..95a80727 --- /dev/null +++ b/middleware/openapi/README.md @@ -0,0 +1,3 @@ +# rspc OpenAPI + +Coming soon... \ No newline at end of file diff --git a/middleware/openapi/src/lib.rs b/middleware/openapi/src/lib.rs new file mode 100644 index 00000000..d87a8130 --- /dev/null +++ b/middleware/openapi/src/lib.rs @@ -0,0 +1,293 @@ +//! rspc-openapi: OpenAPI support for rspc +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +// use std::{borrow::Cow, collections::HashMap, sync::Arc}; + +// use axum::{ +// body::Bytes, +// extract::Query, +// http::{request::Parts, StatusCode}, +// response::Html, +// routing::{delete, get, patch, post, put}, +// Json, +// }; +// use futures::StreamExt; +// use rspc::{ +// modern::{middleware::Middleware, procedure::ResolverInput}, +// Procedure2, +// }; +// use serde_json::json; + +// // TODO: Properly handle inputs from query params +// // TODO: Properly handle responses from query params +// // TODO: Support input's coming from URL. Eg. `/todos/{id}` like tRPC-OpenAPI +// // TODO: Support `application/x-www-form-urlencoded` bodies like tRPC-OpenAPI +// // TODO: Probs put SwaggerUI behind a feature flag + +// pub struct OpenAPI { +// method: &'static str, +// path: Cow<'static, str>, +// } + +// impl OpenAPI { +// // TODO +// // pub fn new(method: Method, path: impl Into>) {} + +// pub fn get(path: impl Into>) -> Self { +// Self { +// method: "GET", +// path: path.into(), +// } +// } + +// pub fn post(path: impl Into>) -> Self { +// Self { +// method: "POST", +// path: path.into(), +// } +// } + +// pub fn put(path: impl Into>) -> Self { +// Self { +// method: "PUT", +// path: path.into(), +// } +// } + +// pub fn patch(path: impl Into>) -> Self { +// Self { +// method: "PATCH", +// path: path.into(), +// } +// } + +// pub fn delete(path: impl Into>) -> Self { +// Self { +// method: "DELETE", +// path: path.into(), +// } +// } + +// // TODO: Configure other OpenAPI stuff like auth??? + +// pub fn build( +// self, +// ) -> Middleware +// where +// TError: 'static, +// TThisCtx: Send + 'static, +// TThisInput: Send + 'static, +// TThisResult: Send + 'static, +// { +// // TODO: Can we have a middleware with only a `setup` function to avoid the extra future boxing??? +// Middleware::new(|ctx, input, next| async move { next.exec(ctx, input).await }).setup( +// move |state, meta| { +// state +// .get_mut_or_init::(Default::default) +// .0 +// .insert((self.method, self.path), meta.name().to_string()); +// }, +// ) +// } +// } + +// // The state that is stored into rspc. +// // A map of (method, path) to procedure name. +// #[derive(Default)] +// struct OpenAPIState(HashMap<(&'static str, Cow<'static, str>), String>); + +// // TODO: Axum should be behind feature flag +// // TODO: Can we decouple webserver from OpenAPI while keeping something maintainable???? +// pub fn mount( +// router: BuiltRouter, +// // TODO: Make Axum extractors work +// ctx_fn: impl Fn(&Parts) -> TCtx + Clone + Send + Sync + 'static, +// ) -> axum::Router +// where +// S: Clone + Send + Sync + 'static, +// TCtx: Send + 'static, +// { +// let mut r = axum::Router::new(); + +// let mut paths: HashMap<_, HashMap<_, _>> = HashMap::new(); +// if let Some(endpoints) = router.state.get::() { +// for ((method, path), procedure_name) in endpoints.0.iter() { +// let procedure = router +// .procedures +// .get(&Cow::Owned(procedure_name.clone())) +// .expect("unreachable: a procedure was registered that doesn't exist") +// .clone(); +// let ctx_fn = ctx_fn.clone(); + +// paths +// .entry(path.clone()) +// .or_default() +// .insert(method.to_lowercase(), procedure.clone()); + +// r = r.route( +// path, +// match *method { +// "GET" => { +// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. +// get( +// move |parts: Parts, query: Query>| async move { +// let ctx = (ctx_fn)(&parts); + +// handle_procedure( +// ctx, +// &mut serde_json::Deserializer::from_str( +// query.get("input").map(|v| &**v).unwrap_or("null"), +// ), +// procedure, +// ) +// .await +// }, +// ) +// } +// "POST" => { +// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. +// post(move |parts: Parts, body: Bytes| async move { +// let ctx = (ctx_fn)(&parts); + +// handle_procedure( +// ctx, +// &mut serde_json::Deserializer::from_slice(&body), +// procedure, +// ) +// .await +// }) +// } +// "PUT" => { +// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. +// put(move |parts: Parts, body: Bytes| async move { +// let ctx = (ctx_fn)(&parts); + +// handle_procedure( +// ctx, +// &mut serde_json::Deserializer::from_slice(&body), +// procedure, +// ) +// .await +// }) +// } +// "PATCH" => { +// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. +// patch(move |parts: Parts, body: Bytes| async move { +// let ctx = (ctx_fn)(&parts); + +// handle_procedure( +// ctx, +// &mut serde_json::Deserializer::from_slice(&body), +// procedure, +// ) +// .await +// }) +// } +// "DELETE" => { +// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. +// delete(move |parts: Parts, body: Bytes| async move { +// let ctx = (ctx_fn)(&parts); + +// handle_procedure( +// ctx, +// &mut serde_json::Deserializer::from_slice(&body), +// procedure, +// ) +// .await +// }) +// } +// _ => panic!("Unsupported method"), +// }, +// ); +// } +// } + +// let schema = Arc::new(json!({ +// "openapi": "3.0.3", +// "info": { +// "title": "rspc OpenAPI", +// "description": "This is a demo of rspc OpenAPI", +// "version": "0.0.0" +// }, +// "paths": paths.into_iter() +// .map(|(path, procedures)| { +// let mut methods = HashMap::new(); +// for (method, procedure) in procedures { +// methods.insert(method.to_string(), json!({ +// "operationId": procedure.ty().key.to_string(), +// "responses": { +// "200": { +// "description": "Successful operation" +// } +// } +// })); +// } + +// (path, methods) +// }) +// .collect::>() +// })); // TODO: Maybe convert to string now cause it will be more efficient to clone + +// r.route( +// // TODO: Allow the user to configure this URL & turn it off +// "/api/docs", +// get(|| async { Html(include_str!("swagger.html")) }), +// ) +// .route( +// // TODO: Allow the user to configure this URL & turn it off +// "/api/openapi.json", +// get(move || async move { Json((*schema).clone()) }), +// ) +// } + +// // Used for `GET` and `POST` endpoints +// async fn handle_procedure<'de, TCtx>( +// ctx: TCtx, +// input: impl ProcedureInput<'de>, +// procedure: Procedure, +// ) -> Result, (StatusCode, Json)> { +// let mut stream = procedure.exec(ctx, input).map_err(|err| { +// ( +// StatusCode::INTERNAL_SERVER_ERROR, +// Json(json!({ +// // TODO: This or not? +// "_rspc_error": err.to_string() +// })), +// ) +// })?; + +// // TODO: Support for streaming +// while let Some(value) = stream.next().await { +// // TODO: We should probs deserialize into buffer instead of value??? +// return match value.map(|v| v.serialize(serde_json::value::Serializer)) { +// Ok(Ok(value)) => Ok(Json(value)), +// Ok(Err(err)) => Err(( +// StatusCode::INTERNAL_SERVER_ERROR, +// Json(json!({ +// "_rspc_error": err.to_string() +// })), +// )), +// Err(err) => Err(( +// StatusCode::from_u16(err.status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), +// Json( +// err.serialize(serde_json::value::Serializer) +// .map_err(|err| { +// ( +// StatusCode::INTERNAL_SERVER_ERROR, +// Json(json!({ +// "_rspc_error": err.to_string() +// })), +// ) +// })?, +// ), +// )), +// }; +// } + +// Ok(Json(serde_json::Value::Null)) +// } diff --git a/middleware/openapi/src/swagger.html b/middleware/openapi/src/swagger.html new file mode 100644 index 00000000..113b1cc5 --- /dev/null +++ b/middleware/openapi/src/swagger.html @@ -0,0 +1,34 @@ + + + + + + + SwaggerUI + + + +

+ + + + + diff --git a/middleware/playground/Cargo.toml b/middleware/playground/Cargo.toml new file mode 100644 index 00000000..d19277c4 --- /dev/null +++ b/middleware/playground/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rspc-playground" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/playground/README.md b/middleware/playground/README.md new file mode 100644 index 00000000..5bdc3658 --- /dev/null +++ b/middleware/playground/README.md @@ -0,0 +1,3 @@ +# rspc Playground + +Coming soon... \ No newline at end of file diff --git a/middleware/playground/src/lib.rs b/middleware/playground/src/lib.rs new file mode 100644 index 00000000..94baa954 --- /dev/null +++ b/middleware/playground/src/lib.rs @@ -0,0 +1,7 @@ +//! rspc-devtools: Devtools for rspc applications +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] diff --git a/middleware/tracing/Cargo.toml b/middleware/tracing/Cargo.toml new file mode 100644 index 00000000..4edee882 --- /dev/null +++ b/middleware/tracing/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rspc-tracing" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[dependencies] +rspc = { path = "../../" } +tracing = "0.1" +futures = "0.3" +tracing-futures = "0.2.5" + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/tracing/README.md b/middleware/tracing/README.md new file mode 100644 index 00000000..c05e1f27 --- /dev/null +++ b/middleware/tracing/README.md @@ -0,0 +1,3 @@ +# rspc tracing + +Coming soon... \ No newline at end of file diff --git a/middleware/tracing/src/lib.rs b/middleware/tracing/src/lib.rs new file mode 100644 index 00000000..7fb9a2fb --- /dev/null +++ b/middleware/tracing/src/lib.rs @@ -0,0 +1,62 @@ +//! rspc-tracing: Tracing support for rspc +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +use std::{fmt, marker::PhantomData}; + +use rspc::modern::middleware::Middleware; +use tracing::info; + +mod traceable; + +pub use traceable::{DebugMarker, StreamMarker, Traceable}; +use tracing_futures::Instrument; + +// TODO: Support for Prometheus metrics and structured logging + +/// TODO +pub fn tracing() -> Middleware +where + TError: fmt::Debug + Send + 'static, + TCtx: Send + 'static, + TInput: fmt::Debug + Send + 'static, + TResult: Traceable + Send + 'static, +{ + Middleware::new(|ctx, input, next| { + let span = tracing::info_span!( + "", + "{} {}", + next.meta().kind().to_string().to_uppercase(), // TODO: Maybe adding color? + next.meta().name() + ); + + async move { + let input_str = format!("{input:?}"); + let start = std::time::Instant::now(); + let result = next.exec(ctx, input).await; + info!( + "took {:?} with input {input_str:?} and returned {:?}", + start.elapsed(), + DebugWrapper(&result, PhantomData::) + ); + + result + } + .instrument(span) + }) +} + +struct DebugWrapper<'a, T: Traceable, TErr, M>(&'a Result, PhantomData); + +impl<'a, T: Traceable, TErr: fmt::Debug, M> fmt::Debug for DebugWrapper<'a, T, TErr, M> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Ok(v) => v.fmt(f), + Err(e) => write!(f, "{e:?}"), + } + } +} diff --git a/middleware/tracing/src/traceable.rs b/middleware/tracing/src/traceable.rs new file mode 100644 index 00000000..e21a6a67 --- /dev/null +++ b/middleware/tracing/src/traceable.rs @@ -0,0 +1,26 @@ +use std::fmt; + +pub trait Traceable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; +} + +#[doc(hidden)] +pub enum DebugMarker {} +impl Traceable for T { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt(f) + } +} + +#[doc(hidden)] +pub enum StreamMarker {} +// `rspc::Stream: !Debug` so the marker will never overlap +impl Traceable for rspc::Stream +where + S: futures::Stream, + S::Item: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!(); // TODO: Finish this + } +} From b1984480c43c657b0b1536ea656622441c774dbf Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 6 Dec 2024 17:28:33 +0800 Subject: [PATCH 16/67] virtual workspace --- Cargo.toml | 68 +-- {crates/client => client}/Cargo.toml | 0 {crates/client => client}/src/lib.rs | 0 {crates/core => core}/Cargo.toml | 0 {crates/core => core}/examples/basic.rs | 0 {crates/core => core}/src/dyn_input.rs | 0 {crates/core => core}/src/error.rs | 0 {crates/core => core}/src/interop.rs | 0 {crates/core => core}/src/lib.rs | 0 {crates/core => core}/src/procedure.rs | 0 {crates/core => core}/src/stream.rs | 0 examples/Cargo.toml | 3 +- examples/axum/Cargo.toml | 2 +- examples/client/Cargo.toml | 2 +- examples/src/bin/axum.rs | 5 +- examples/src/bin/cookies.rs | 5 +- examples/src/bin/global_context.rs | 5 +- examples/src/bin/middleware.rs | 5 +- integrations/axum/Cargo.toml | 2 +- integrations/tauri/Cargo.toml | 2 +- middleware/invalidation/Cargo.toml | 2 +- middleware/openapi/Cargo.toml | 2 +- middleware/openapi/src/lib.rs | 437 +++++++++--------- middleware/tracing/Cargo.toml | 2 +- middleware/tracing/src/traceable.rs | 2 +- rspc/Cargo.toml | 48 ++ {src => rspc/src}/interop.rs | 0 {src => rspc/src}/languages.rs | 0 {src => rspc/src}/languages/rust.rs | 0 {src => rspc/src}/languages/typescript.rs | 0 {src => rspc/src}/legacy/config.rs | 0 {src => rspc/src}/legacy/error.rs | 0 {src => rspc/src}/legacy/internal/jsonrpc.rs | 0 .../src}/legacy/internal/jsonrpc_exec.rs | 0 .../src}/legacy/internal/middleware.rs | 6 + {src => rspc/src}/legacy/internal/mod.rs | 0 .../src}/legacy/internal/procedure_builder.rs | 0 .../src}/legacy/internal/procedure_store.rs | 0 {src => rspc/src}/legacy/middleware.rs | 0 {src => rspc/src}/legacy/mod.rs | 0 {src => rspc/src}/legacy/resolver.rs | 0 {src => rspc/src}/legacy/resolver_result.rs | 0 {src => rspc/src}/legacy/router.rs | 0 {src => rspc/src}/legacy/router_builder.rs | 0 {src => rspc/src}/legacy/selection.rs | 0 {src => rspc/src}/lib.rs | 0 {src => rspc/src}/modern/error.rs | 0 {src => rspc/src}/modern/infallible.rs | 0 {src => rspc/src}/modern/middleware.rs | 0 .../src}/modern/middleware/middleware.rs | 0 {src => rspc/src}/modern/middleware/next.rs | 0 {src => rspc/src}/modern/mod.rs | 2 + {src => rspc/src}/modern/procedure.rs | 0 {src => rspc/src}/modern/procedure/builder.rs | 0 {src => rspc/src}/modern/procedure/meta.rs | 0 .../src}/modern/procedure/procedure.rs | 0 .../src}/modern/procedure/resolver_input.rs | 0 .../src}/modern/procedure/resolver_output.rs | 0 {src => rspc/src}/modern/state.rs | 0 {src => rspc/src}/modern/stream.rs | 0 {src => rspc/src}/procedure.rs | 0 {src => rspc/src}/procedure_kind.rs | 0 {src => rspc/src}/router.rs | 0 {src => rspc/src}/types.rs | 0 {tests => rspc/tests}/typescript.rs | 0 65 files changed, 302 insertions(+), 298 deletions(-) rename {crates/client => client}/Cargo.toml (100%) rename {crates/client => client}/src/lib.rs (100%) rename {crates/core => core}/Cargo.toml (100%) rename {crates/core => core}/examples/basic.rs (100%) rename {crates/core => core}/src/dyn_input.rs (100%) rename {crates/core => core}/src/error.rs (100%) rename {crates/core => core}/src/interop.rs (100%) rename {crates/core => core}/src/lib.rs (100%) rename {crates/core => core}/src/procedure.rs (100%) rename {crates/core => core}/src/stream.rs (100%) create mode 100644 rspc/Cargo.toml rename {src => rspc/src}/interop.rs (100%) rename {src => rspc/src}/languages.rs (100%) rename {src => rspc/src}/languages/rust.rs (100%) rename {src => rspc/src}/languages/typescript.rs (100%) rename {src => rspc/src}/legacy/config.rs (100%) rename {src => rspc/src}/legacy/error.rs (100%) rename {src => rspc/src}/legacy/internal/jsonrpc.rs (100%) rename {src => rspc/src}/legacy/internal/jsonrpc_exec.rs (100%) rename {src => rspc/src}/legacy/internal/middleware.rs (98%) rename {src => rspc/src}/legacy/internal/mod.rs (100%) rename {src => rspc/src}/legacy/internal/procedure_builder.rs (100%) rename {src => rspc/src}/legacy/internal/procedure_store.rs (100%) rename {src => rspc/src}/legacy/middleware.rs (100%) rename {src => rspc/src}/legacy/mod.rs (100%) rename {src => rspc/src}/legacy/resolver.rs (100%) rename {src => rspc/src}/legacy/resolver_result.rs (100%) rename {src => rspc/src}/legacy/router.rs (100%) rename {src => rspc/src}/legacy/router_builder.rs (100%) rename {src => rspc/src}/legacy/selection.rs (100%) rename {src => rspc/src}/lib.rs (100%) rename {src => rspc/src}/modern/error.rs (100%) rename {src => rspc/src}/modern/infallible.rs (100%) rename {src => rspc/src}/modern/middleware.rs (100%) rename {src => rspc/src}/modern/middleware/middleware.rs (100%) rename {src => rspc/src}/modern/middleware/next.rs (100%) rename {src => rspc/src}/modern/mod.rs (88%) rename {src => rspc/src}/modern/procedure.rs (100%) rename {src => rspc/src}/modern/procedure/builder.rs (100%) rename {src => rspc/src}/modern/procedure/meta.rs (100%) rename {src => rspc/src}/modern/procedure/procedure.rs (100%) rename {src => rspc/src}/modern/procedure/resolver_input.rs (100%) rename {src => rspc/src}/modern/procedure/resolver_output.rs (100%) rename {src => rspc/src}/modern/state.rs (100%) rename {src => rspc/src}/modern/stream.rs (100%) rename {src => rspc/src}/procedure.rs (100%) rename {src => rspc/src}/procedure_kind.rs (100%) rename {src => rspc/src}/router.rs (100%) rename {src => rspc/src}/types.rs (100%) rename {tests => rspc/tests}/typescript.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 0adb9d14..00b2888d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,54 +1,14 @@ -[package] -name = "rspc" -description = "A blazing fast and easy to use TRPC server for Rust." -version = "0.3.1" -authors = ["Oscar Beaumont "] -edition = "2021" -license = "MIT" -include = ["/src", "/LICENCE", "/README.md"] -repository = "https://github.com/specta-rs/rspc" -documentation = "https://docs.rs/rspc/latest/rspc" -keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] -categories = ["web-programming", "asynchronous"] - -# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features -[package.metadata."docs.rs"] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] - -[features] -default = [] -tracing = ["dep:tracing"] - -typescript = [] -rust = [] - -[dependencies] -# Public -rspc-core = { path = "./crates/core" } -serde = { version = "1", features = ["derive"] } # TODO: Remove features -futures = "0.3" -specta = { version = "=2.0.0-rc.20", features = [ - "derive", - "serde", - "serde_json", -] } # TODO: Drop all features - -# Private -serde-value = "0.7" -erased-serde = "0.4" -specta-typescript = { version = "=0.0.7", features = [] } -specta-rust = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } - -# Temporary # TODO: Remove -serde_json = "1.0.133" # TODO: Drop this -thiserror = "2.0.3" -tokio = { version = "1.41.1", features = ["sync", "rt", "macros"] } -specta-serde = "0.0.7" -tracing = { version = "0.1.40", optional = true } - [workspace] -members = ["./integrations/*", "./middleware/*", "./crates/*", "./examples", "./examples/axum", "./examples/client", "crates/core"] +resolver = "2" +members = ["./rspc", "./core", "./client", "./integrations/*", "./middleware/*", "./examples", "./examples/axum", "./examples/client"] + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +unwrap_used = { level = "warn", priority = -1 } +panic = { level = "warn", priority = -1 } +todo = { level = "warn", priority = -1 } +panic_in_result_fn = { level = "warn", priority = -1 } [patch.crates-io] specta = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } @@ -59,11 +19,3 @@ specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "bf3a09 # specta-typescript = { path = "/Users/oscar/Desktop/specta/specta-typescript" } # specta-serde = { path = "/Users/oscar/Desktop/specta/specta-serde" } # specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } - -[workspace.lints.clippy] -all = { level = "warn", priority = -1 } -cargo = { level = "warn", priority = -1 } -unwrap_used = { level = "warn", priority = -1 } -panic = { level = "warn", priority = -1 } -todo = { level = "warn", priority = -1 } -panic_in_result_fn = { level = "warn", priority = -1 } diff --git a/crates/client/Cargo.toml b/client/Cargo.toml similarity index 100% rename from crates/client/Cargo.toml rename to client/Cargo.toml diff --git a/crates/client/src/lib.rs b/client/src/lib.rs similarity index 100% rename from crates/client/src/lib.rs rename to client/src/lib.rs diff --git a/crates/core/Cargo.toml b/core/Cargo.toml similarity index 100% rename from crates/core/Cargo.toml rename to core/Cargo.toml diff --git a/crates/core/examples/basic.rs b/core/examples/basic.rs similarity index 100% rename from crates/core/examples/basic.rs rename to core/examples/basic.rs diff --git a/crates/core/src/dyn_input.rs b/core/src/dyn_input.rs similarity index 100% rename from crates/core/src/dyn_input.rs rename to core/src/dyn_input.rs diff --git a/crates/core/src/error.rs b/core/src/error.rs similarity index 100% rename from crates/core/src/error.rs rename to core/src/error.rs diff --git a/crates/core/src/interop.rs b/core/src/interop.rs similarity index 100% rename from crates/core/src/interop.rs rename to core/src/interop.rs diff --git a/crates/core/src/lib.rs b/core/src/lib.rs similarity index 100% rename from crates/core/src/lib.rs rename to core/src/lib.rs diff --git a/crates/core/src/procedure.rs b/core/src/procedure.rs similarity index 100% rename from crates/core/src/procedure.rs rename to core/src/procedure.rs diff --git a/crates/core/src/stream.rs b/core/src/stream.rs similarity index 100% rename from crates/core/src/stream.rs rename to core/src/stream.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 32c258c2..f9e973a8 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -5,9 +5,8 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../" } +rspc = { path = "../rspc", features = ["typescript"] } specta = "=2.0.0-rc.20" -specta-typescript = "0.0.7" rspc-axum = { path = "../integrations/axum" } async-stream = "0.3.6" axum = "0.7.9" diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 681d17d3..815e994c 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../../", features = ["typescript", "rust"] } +rspc = { path = "../../rspc", features = ["typescript", "rust"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } tokio = { version = "1.41.1", features = ["full"] } async-stream = "0.3.6" diff --git a/examples/client/Cargo.toml b/examples/client/Cargo.toml index 1356633d..5877a6d2 100644 --- a/examples/client/Cargo.toml +++ b/examples/client/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" publish = false [dependencies] -rspc-client = { path = "../../crates/client" } +rspc-client = { path = "../../client" } tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/src/bin/axum.rs b/examples/src/bin/axum.rs index 8f4eb0b9..603e6d7f 100644 --- a/examples/src/bin/axum.rs +++ b/examples/src/bin/axum.rs @@ -6,7 +6,6 @@ use example::{basic, selection, subscriptions}; use axum::{http::request::Parts, routing::get}; use rspc::Router; -use specta_typescript::Typescript; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] @@ -28,10 +27,10 @@ async fn main() { let (routes, types) = rspc::Router2::from(router).build().unwrap(); - types + rspc::Typescript::default() .export_to( - Typescript::default(), PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, ) .unwrap(); diff --git a/examples/src/bin/cookies.rs b/examples/src/bin/cookies.rs index 69161a0b..20ba6656 100644 --- a/examples/src/bin/cookies.rs +++ b/examples/src/bin/cookies.rs @@ -4,7 +4,6 @@ use std::{ops::Add, path::PathBuf}; use axum::routing::get; use rspc::Config; -use specta_typescript::Typescript; use time::OffsetDateTime; use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; use tower_http::cors::{Any, CorsLayer}; @@ -35,10 +34,10 @@ async fn main() { let (routes, types) = rspc::Router2::from(router).build().unwrap(); - types + rspc::Typescript::default() .export_to( - Typescript::default(), PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, ) .unwrap(); diff --git a/examples/src/bin/global_context.rs b/examples/src/bin/global_context.rs index ec9625dc..914a4d58 100644 --- a/examples/src/bin/global_context.rs +++ b/examples/src/bin/global_context.rs @@ -7,7 +7,6 @@ use std::{ }; use rspc::{Config, Router}; -use specta_typescript::Typescript; #[derive(Clone)] pub struct MyCtx { @@ -26,10 +25,10 @@ async fn main() { let (routes, types) = rspc::Router2::from(router).build().unwrap(); - types + rspc::Typescript::default() .export_to( - Typescript::default(), PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, ) .unwrap(); diff --git a/examples/src/bin/middleware.rs b/examples/src/bin/middleware.rs index 3750165d..cb5a6cbb 100644 --- a/examples/src/bin/middleware.rs +++ b/examples/src/bin/middleware.rs @@ -3,7 +3,6 @@ use std::{path::PathBuf, time::Duration}; use async_stream::stream; use axum::routing::get; use rspc::{Config, ErrorCode, MiddlewareContext, Router}; -use specta_typescript::Typescript; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; @@ -101,10 +100,10 @@ async fn main() { let (routes, types) = rspc::Router2::from(router).build().unwrap(); - types + rspc::Typescript::default() .export_to( - Typescript::default(), PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, ) .unwrap(); diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 529f6daf..d0df8fb5 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -20,7 +20,7 @@ default = [] ws = ["axum/ws"] [dependencies] -rspc-core = { version = "0.0.1", path = "../../crates/core" } +rspc-core = { version = "0.0.1", path = "../../core" } axum = "0.7.9" serde_json = "1" diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 5aaa3ffd..3e14a1b8 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -16,7 +16,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -rspc-core = { version = "0.0.1", path = "../../crates/core" } +rspc-core = { version = "0.0.1", path = "../../core" } tauri = "2" serde_json = "1" diff --git a/middleware/invalidation/Cargo.toml b/middleware/invalidation/Cargo.toml index e8bbd82b..9ffdf2a4 100644 --- a/middleware/invalidation/Cargo.toml +++ b/middleware/invalidation/Cargo.toml @@ -6,7 +6,7 @@ publish = false # TODO: Crate metadata & publish [dependencies] async-stream = "0.3.5" -rspc = { path = "../../" } +rspc = { path = "../../rspc" } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/middleware/openapi/Cargo.toml b/middleware/openapi/Cargo.toml index 0861a5c9..976b789e 100644 --- a/middleware/openapi/Cargo.toml +++ b/middleware/openapi/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../" } +rspc = { path = "../../rspc" } axum = { version = "0.7.5", default-features = false } serde_json = "1.0.127" futures = "0.3.30" diff --git a/middleware/openapi/src/lib.rs b/middleware/openapi/src/lib.rs index d87a8130..def9f6ab 100644 --- a/middleware/openapi/src/lib.rs +++ b/middleware/openapi/src/lib.rs @@ -6,250 +6,251 @@ html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" )] -// use std::{borrow::Cow, collections::HashMap, sync::Arc}; +use std::{borrow::Cow, collections::HashMap, sync::Arc}; -// use axum::{ -// body::Bytes, -// extract::Query, -// http::{request::Parts, StatusCode}, -// response::Html, -// routing::{delete, get, patch, post, put}, -// Json, -// }; -// use futures::StreamExt; -// use rspc::{ -// modern::{middleware::Middleware, procedure::ResolverInput}, -// Procedure2, -// }; -// use serde_json::json; +use axum::{ + body::Bytes, + extract::Query, + http::{request::Parts, StatusCode}, + response::Html, + routing::{delete, get, patch, post, put}, + Json, +}; +use futures::StreamExt; +use rspc::{ + modern::{middleware::Middleware, procedure::ResolverInput, Procedure2}, + Router2, +}; +use serde_json::json; -// // TODO: Properly handle inputs from query params -// // TODO: Properly handle responses from query params -// // TODO: Support input's coming from URL. Eg. `/todos/{id}` like tRPC-OpenAPI -// // TODO: Support `application/x-www-form-urlencoded` bodies like tRPC-OpenAPI -// // TODO: Probs put SwaggerUI behind a feature flag +// TODO: Properly handle inputs from query params +// TODO: Properly handle responses from query params +// TODO: Support input's coming from URL. Eg. `/todos/{id}` like tRPC-OpenAPI +// TODO: Support `application/x-www-form-urlencoded` bodies like tRPC-OpenAPI +// TODO: Probs put SwaggerUI behind a feature flag -// pub struct OpenAPI { -// method: &'static str, -// path: Cow<'static, str>, -// } +pub struct OpenAPI { + method: &'static str, + path: Cow<'static, str>, +} -// impl OpenAPI { -// // TODO -// // pub fn new(method: Method, path: impl Into>) {} +impl OpenAPI { + // TODO + // pub fn new(method: Method, path: impl Into>) {} -// pub fn get(path: impl Into>) -> Self { -// Self { -// method: "GET", -// path: path.into(), -// } -// } + pub fn get(path: impl Into>) -> Self { + Self { + method: "GET", + path: path.into(), + } + } -// pub fn post(path: impl Into>) -> Self { -// Self { -// method: "POST", -// path: path.into(), -// } -// } + pub fn post(path: impl Into>) -> Self { + Self { + method: "POST", + path: path.into(), + } + } -// pub fn put(path: impl Into>) -> Self { -// Self { -// method: "PUT", -// path: path.into(), -// } -// } + pub fn put(path: impl Into>) -> Self { + Self { + method: "PUT", + path: path.into(), + } + } -// pub fn patch(path: impl Into>) -> Self { -// Self { -// method: "PATCH", -// path: path.into(), -// } -// } + pub fn patch(path: impl Into>) -> Self { + Self { + method: "PATCH", + path: path.into(), + } + } -// pub fn delete(path: impl Into>) -> Self { -// Self { -// method: "DELETE", -// path: path.into(), -// } -// } + pub fn delete(path: impl Into>) -> Self { + Self { + method: "DELETE", + path: path.into(), + } + } -// // TODO: Configure other OpenAPI stuff like auth??? + // TODO: Configure other OpenAPI stuff like auth??? -// pub fn build( -// self, -// ) -> Middleware -// where -// TError: 'static, -// TThisCtx: Send + 'static, -// TThisInput: Send + 'static, -// TThisResult: Send + 'static, -// { -// // TODO: Can we have a middleware with only a `setup` function to avoid the extra future boxing??? -// Middleware::new(|ctx, input, next| async move { next.exec(ctx, input).await }).setup( -// move |state, meta| { -// state -// .get_mut_or_init::(Default::default) -// .0 -// .insert((self.method, self.path), meta.name().to_string()); -// }, -// ) -// } -// } + pub fn build( + self, + ) -> Middleware + where + TError: 'static, + TThisCtx: Send + 'static, + TThisInput: Send + 'static, + TThisResult: Send + 'static, + { + // TODO: Can we have a middleware with only a `setup` function to avoid the extra future boxing??? + Middleware::new(|ctx, input, next| async move { next.exec(ctx, input).await }).setup( + move |state, meta| { + state + .get_mut_or_init::(Default::default) + .0 + .insert((self.method, self.path), meta.name().to_string()); + }, + ) + } +} -// // The state that is stored into rspc. -// // A map of (method, path) to procedure name. -// #[derive(Default)] -// struct OpenAPIState(HashMap<(&'static str, Cow<'static, str>), String>); +// The state that is stored into rspc. +// A map of (method, path) to procedure name. +#[derive(Default)] +struct OpenAPIState(HashMap<(&'static str, Cow<'static, str>), String>); -// // TODO: Axum should be behind feature flag -// // TODO: Can we decouple webserver from OpenAPI while keeping something maintainable???? -// pub fn mount( -// router: BuiltRouter, -// // TODO: Make Axum extractors work -// ctx_fn: impl Fn(&Parts) -> TCtx + Clone + Send + Sync + 'static, -// ) -> axum::Router -// where -// S: Clone + Send + Sync + 'static, -// TCtx: Send + 'static, -// { -// let mut r = axum::Router::new(); +// TODO: Axum should be behind feature flag +// TODO: Can we decouple webserver from OpenAPI while keeping something maintainable???? +pub fn mount( + router: Router2, + // TODO: Make Axum extractors work + ctx_fn: impl Fn(&Parts) -> TCtx + Clone + Send + Sync + 'static, +) -> axum::Router +where + S: Clone + Send + Sync + 'static, + TCtx: Send + 'static, +{ + let mut r = axum::Router::new(); -// let mut paths: HashMap<_, HashMap<_, _>> = HashMap::new(); -// if let Some(endpoints) = router.state.get::() { -// for ((method, path), procedure_name) in endpoints.0.iter() { -// let procedure = router -// .procedures -// .get(&Cow::Owned(procedure_name.clone())) -// .expect("unreachable: a procedure was registered that doesn't exist") -// .clone(); -// let ctx_fn = ctx_fn.clone(); + // let mut paths: HashMap<_, HashMap<_, _>> = HashMap::new(); + // if let Some(endpoints) = router.state.get::() { + // for ((method, path), procedure_name) in endpoints.0.iter() { + // let procedure = router + // .into_iter() + // .find(|(k, _)| k.join(".") == *procedure_name) + // // .get(&Cow::Owned(procedure_name.clone())) + // .expect("unreachable: a procedure was registered that doesn't exist") + // .clone(); + // let ctx_fn = ctx_fn.clone(); -// paths -// .entry(path.clone()) -// .or_default() -// .insert(method.to_lowercase(), procedure.clone()); + // paths + // .entry(path.clone()) + // .or_default() + // .insert(method.to_lowercase(), procedure.clone()); -// r = r.route( -// path, -// match *method { -// "GET" => { -// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. -// get( -// move |parts: Parts, query: Query>| async move { -// let ctx = (ctx_fn)(&parts); + // r = r.route( + // path, + // match *method { + // "GET" => { + // // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. + // get( + // move |parts: Parts, query: Query>| async move { + // let ctx = (ctx_fn)(&parts); -// handle_procedure( -// ctx, -// &mut serde_json::Deserializer::from_str( -// query.get("input").map(|v| &**v).unwrap_or("null"), -// ), -// procedure, -// ) -// .await -// }, -// ) -// } -// "POST" => { -// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. -// post(move |parts: Parts, body: Bytes| async move { -// let ctx = (ctx_fn)(&parts); + // handle_procedure( + // ctx, + // &mut serde_json::Deserializer::from_str( + // query.get("input").map(|v| &**v).unwrap_or("null"), + // ), + // procedure, + // ) + // .await + // }, + // ) + // } + // "POST" => { + // // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. + // post(move |parts: Parts, body: Bytes| async move { + // let ctx = (ctx_fn)(&parts); -// handle_procedure( -// ctx, -// &mut serde_json::Deserializer::from_slice(&body), -// procedure, -// ) -// .await -// }) -// } -// "PUT" => { -// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. -// put(move |parts: Parts, body: Bytes| async move { -// let ctx = (ctx_fn)(&parts); + // handle_procedure( + // ctx, + // &mut serde_json::Deserializer::from_slice(&body), + // procedure, + // ) + // .await + // }) + // } + // "PUT" => { + // // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. + // put(move |parts: Parts, body: Bytes| async move { + // let ctx = (ctx_fn)(&parts); -// handle_procedure( -// ctx, -// &mut serde_json::Deserializer::from_slice(&body), -// procedure, -// ) -// .await -// }) -// } -// "PATCH" => { -// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. -// patch(move |parts: Parts, body: Bytes| async move { -// let ctx = (ctx_fn)(&parts); + // handle_procedure( + // ctx, + // &mut serde_json::Deserializer::from_slice(&body), + // procedure, + // ) + // .await + // }) + // } + // "PATCH" => { + // // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. + // patch(move |parts: Parts, body: Bytes| async move { + // let ctx = (ctx_fn)(&parts); -// handle_procedure( -// ctx, -// &mut serde_json::Deserializer::from_slice(&body), -// procedure, -// ) -// .await -// }) -// } -// "DELETE" => { -// // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. -// delete(move |parts: Parts, body: Bytes| async move { -// let ctx = (ctx_fn)(&parts); + // handle_procedure( + // ctx, + // &mut serde_json::Deserializer::from_slice(&body), + // procedure, + // ) + // .await + // }) + // } + // "DELETE" => { + // // TODO: By moving `procedure` into the closure we hang onto the types for the duration of the program which is probs undesirable. + // delete(move |parts: Parts, body: Bytes| async move { + // let ctx = (ctx_fn)(&parts); -// handle_procedure( -// ctx, -// &mut serde_json::Deserializer::from_slice(&body), -// procedure, -// ) -// .await -// }) -// } -// _ => panic!("Unsupported method"), -// }, -// ); -// } -// } + // handle_procedure( + // ctx, + // &mut serde_json::Deserializer::from_slice(&body), + // procedure, + // ) + // .await + // }) + // } + // _ => panic!("Unsupported method"), + // }, + // ); + // } + // } -// let schema = Arc::new(json!({ -// "openapi": "3.0.3", -// "info": { -// "title": "rspc OpenAPI", -// "description": "This is a demo of rspc OpenAPI", -// "version": "0.0.0" -// }, -// "paths": paths.into_iter() -// .map(|(path, procedures)| { -// let mut methods = HashMap::new(); -// for (method, procedure) in procedures { -// methods.insert(method.to_string(), json!({ -// "operationId": procedure.ty().key.to_string(), -// "responses": { -// "200": { -// "description": "Successful operation" -// } -// } -// })); -// } + // let schema = Arc::new(json!({ + // "openapi": "3.0.3", + // "info": { + // "title": "rspc OpenAPI", + // "description": "This is a demo of rspc OpenAPI", + // "version": "0.0.0" + // }, + // "paths": paths.into_iter() + // .map(|(path, procedures)| { + // let mut methods = HashMap::new(); + // for (method, procedure) in procedures { + // methods.insert(method.to_string(), json!({ + // "operationId": procedure.ty().key.to_string(), + // "responses": { + // "200": { + // "description": "Successful operation" + // } + // } + // })); + // } -// (path, methods) -// }) -// .collect::>() -// })); // TODO: Maybe convert to string now cause it will be more efficient to clone + // (path, methods) + // }) + // .collect::>() + // })); // TODO: Maybe convert to string now cause it will be more efficient to clone -// r.route( -// // TODO: Allow the user to configure this URL & turn it off -// "/api/docs", -// get(|| async { Html(include_str!("swagger.html")) }), -// ) -// .route( -// // TODO: Allow the user to configure this URL & turn it off -// "/api/openapi.json", -// get(move || async move { Json((*schema).clone()) }), -// ) -// } + r.route( + // TODO: Allow the user to configure this URL & turn it off + "/api/docs", + get(|| async { Html(include_str!("swagger.html")) }), + ) + // .route( + // // TODO: Allow the user to configure this URL & turn it off + // "/api/openapi.json", + // get(move || async move { Json((*schema).clone()) }), + // ) +} -// // Used for `GET` and `POST` endpoints -// async fn handle_procedure<'de, TCtx>( +// Used for `GET` and `POST` endpoints +// async fn handle_procedure<'a, 'de, TCtx>( // ctx: TCtx, -// input: impl ProcedureInput<'de>, -// procedure: Procedure, +// input: DynInput<'a, 'de>, +// procedure: Procedure2, // ) -> Result, (StatusCode, Json)> { // let mut stream = procedure.exec(ctx, input).map_err(|err| { // ( diff --git a/middleware/tracing/Cargo.toml b/middleware/tracing/Cargo.toml index 4edee882..d7861280 100644 --- a/middleware/tracing/Cargo.toml +++ b/middleware/tracing/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../" } +rspc = { path = "../../rspc" } tracing = "0.1" futures = "0.3" tracing-futures = "0.2.5" diff --git a/middleware/tracing/src/traceable.rs b/middleware/tracing/src/traceable.rs index e21a6a67..f59e9166 100644 --- a/middleware/tracing/src/traceable.rs +++ b/middleware/tracing/src/traceable.rs @@ -15,7 +15,7 @@ impl Traceable for T { #[doc(hidden)] pub enum StreamMarker {} // `rspc::Stream: !Debug` so the marker will never overlap -impl Traceable for rspc::Stream +impl Traceable for rspc::modern::Stream where S: futures::Stream, S::Item: fmt::Debug, diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml new file mode 100644 index 00000000..32d6e379 --- /dev/null +++ b/rspc/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "rspc" +description = "A blazing fast and easy to use TRPC server for Rust." +version = "0.3.1" +authors = ["Oscar Beaumont "] +edition = "2021" +license = "MIT" +include = ["/src", "/LICENCE", "/README.md"] +repository = "https://github.com/specta-rs/rspc" +documentation = "https://docs.rs/rspc/latest/rspc" +keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] +categories = ["web-programming", "asynchronous"] + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = [] +tracing = ["dep:tracing"] + +typescript = [] +rust = [] + +[dependencies] +# Public +rspc-core = { path = "../core" } +serde = { version = "1", features = ["derive"] } # TODO: Remove features +futures = "0.3" +specta = { version = "=2.0.0-rc.20", features = [ + "derive", + "serde", + "serde_json", +] } # TODO: Drop all features + +# Private +serde-value = "0.7" +erased-serde = "0.4" +specta-typescript = { version = "=0.0.7", features = [] } +specta-rust = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } + +# Temporary # TODO: Remove +serde_json = "1.0.133" # TODO: Drop this +thiserror = "2.0.3" +tokio = { version = "1.41.1", features = ["sync", "rt", "macros"] } +specta-serde = "0.0.7" +tracing = { version = "0.1.40", optional = true } diff --git a/src/interop.rs b/rspc/src/interop.rs similarity index 100% rename from src/interop.rs rename to rspc/src/interop.rs diff --git a/src/languages.rs b/rspc/src/languages.rs similarity index 100% rename from src/languages.rs rename to rspc/src/languages.rs diff --git a/src/languages/rust.rs b/rspc/src/languages/rust.rs similarity index 100% rename from src/languages/rust.rs rename to rspc/src/languages/rust.rs diff --git a/src/languages/typescript.rs b/rspc/src/languages/typescript.rs similarity index 100% rename from src/languages/typescript.rs rename to rspc/src/languages/typescript.rs diff --git a/src/legacy/config.rs b/rspc/src/legacy/config.rs similarity index 100% rename from src/legacy/config.rs rename to rspc/src/legacy/config.rs diff --git a/src/legacy/error.rs b/rspc/src/legacy/error.rs similarity index 100% rename from src/legacy/error.rs rename to rspc/src/legacy/error.rs diff --git a/src/legacy/internal/jsonrpc.rs b/rspc/src/legacy/internal/jsonrpc.rs similarity index 100% rename from src/legacy/internal/jsonrpc.rs rename to rspc/src/legacy/internal/jsonrpc.rs diff --git a/src/legacy/internal/jsonrpc_exec.rs b/rspc/src/legacy/internal/jsonrpc_exec.rs similarity index 100% rename from src/legacy/internal/jsonrpc_exec.rs rename to rspc/src/legacy/internal/jsonrpc_exec.rs diff --git a/src/legacy/internal/middleware.rs b/rspc/src/legacy/internal/middleware.rs similarity index 98% rename from src/legacy/internal/middleware.rs rename to rspc/src/legacy/internal/middleware.rs index 2f9e85e2..c87a82c1 100644 --- a/src/legacy/internal/middleware.rs +++ b/rspc/src/legacy/internal/middleware.rs @@ -193,6 +193,12 @@ impl ProcedureKind { } } +impl ToString for ProcedureKind { + fn to_string(&self) -> String { + self.to_str().to_string() + } +} + // TODO: Maybe rename to `Request` or something else. Also move into Public API cause it might be used in middleware #[derive(Debug, Clone)] pub struct RequestContext { diff --git a/src/legacy/internal/mod.rs b/rspc/src/legacy/internal/mod.rs similarity index 100% rename from src/legacy/internal/mod.rs rename to rspc/src/legacy/internal/mod.rs diff --git a/src/legacy/internal/procedure_builder.rs b/rspc/src/legacy/internal/procedure_builder.rs similarity index 100% rename from src/legacy/internal/procedure_builder.rs rename to rspc/src/legacy/internal/procedure_builder.rs diff --git a/src/legacy/internal/procedure_store.rs b/rspc/src/legacy/internal/procedure_store.rs similarity index 100% rename from src/legacy/internal/procedure_store.rs rename to rspc/src/legacy/internal/procedure_store.rs diff --git a/src/legacy/middleware.rs b/rspc/src/legacy/middleware.rs similarity index 100% rename from src/legacy/middleware.rs rename to rspc/src/legacy/middleware.rs diff --git a/src/legacy/mod.rs b/rspc/src/legacy/mod.rs similarity index 100% rename from src/legacy/mod.rs rename to rspc/src/legacy/mod.rs diff --git a/src/legacy/resolver.rs b/rspc/src/legacy/resolver.rs similarity index 100% rename from src/legacy/resolver.rs rename to rspc/src/legacy/resolver.rs diff --git a/src/legacy/resolver_result.rs b/rspc/src/legacy/resolver_result.rs similarity index 100% rename from src/legacy/resolver_result.rs rename to rspc/src/legacy/resolver_result.rs diff --git a/src/legacy/router.rs b/rspc/src/legacy/router.rs similarity index 100% rename from src/legacy/router.rs rename to rspc/src/legacy/router.rs diff --git a/src/legacy/router_builder.rs b/rspc/src/legacy/router_builder.rs similarity index 100% rename from src/legacy/router_builder.rs rename to rspc/src/legacy/router_builder.rs diff --git a/src/legacy/selection.rs b/rspc/src/legacy/selection.rs similarity index 100% rename from src/legacy/selection.rs rename to rspc/src/legacy/selection.rs diff --git a/src/lib.rs b/rspc/src/lib.rs similarity index 100% rename from src/lib.rs rename to rspc/src/lib.rs diff --git a/src/modern/error.rs b/rspc/src/modern/error.rs similarity index 100% rename from src/modern/error.rs rename to rspc/src/modern/error.rs diff --git a/src/modern/infallible.rs b/rspc/src/modern/infallible.rs similarity index 100% rename from src/modern/infallible.rs rename to rspc/src/modern/infallible.rs diff --git a/src/modern/middleware.rs b/rspc/src/modern/middleware.rs similarity index 100% rename from src/modern/middleware.rs rename to rspc/src/modern/middleware.rs diff --git a/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs similarity index 100% rename from src/modern/middleware/middleware.rs rename to rspc/src/modern/middleware/middleware.rs diff --git a/src/modern/middleware/next.rs b/rspc/src/modern/middleware/next.rs similarity index 100% rename from src/modern/middleware/next.rs rename to rspc/src/modern/middleware/next.rs diff --git a/src/modern/mod.rs b/rspc/src/modern/mod.rs similarity index 88% rename from src/modern/mod.rs rename to rspc/src/modern/mod.rs index b019e484..4f285baf 100644 --- a/src/modern/mod.rs +++ b/rspc/src/modern/mod.rs @@ -11,3 +11,5 @@ pub use error::Error; pub use infallible::Infallible; pub use state::State; pub use stream::Stream; + +pub use rspc_core::DynInput; diff --git a/src/modern/procedure.rs b/rspc/src/modern/procedure.rs similarity index 100% rename from src/modern/procedure.rs rename to rspc/src/modern/procedure.rs diff --git a/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs similarity index 100% rename from src/modern/procedure/builder.rs rename to rspc/src/modern/procedure/builder.rs diff --git a/src/modern/procedure/meta.rs b/rspc/src/modern/procedure/meta.rs similarity index 100% rename from src/modern/procedure/meta.rs rename to rspc/src/modern/procedure/meta.rs diff --git a/src/modern/procedure/procedure.rs b/rspc/src/modern/procedure/procedure.rs similarity index 100% rename from src/modern/procedure/procedure.rs rename to rspc/src/modern/procedure/procedure.rs diff --git a/src/modern/procedure/resolver_input.rs b/rspc/src/modern/procedure/resolver_input.rs similarity index 100% rename from src/modern/procedure/resolver_input.rs rename to rspc/src/modern/procedure/resolver_input.rs diff --git a/src/modern/procedure/resolver_output.rs b/rspc/src/modern/procedure/resolver_output.rs similarity index 100% rename from src/modern/procedure/resolver_output.rs rename to rspc/src/modern/procedure/resolver_output.rs diff --git a/src/modern/state.rs b/rspc/src/modern/state.rs similarity index 100% rename from src/modern/state.rs rename to rspc/src/modern/state.rs diff --git a/src/modern/stream.rs b/rspc/src/modern/stream.rs similarity index 100% rename from src/modern/stream.rs rename to rspc/src/modern/stream.rs diff --git a/src/procedure.rs b/rspc/src/procedure.rs similarity index 100% rename from src/procedure.rs rename to rspc/src/procedure.rs diff --git a/src/procedure_kind.rs b/rspc/src/procedure_kind.rs similarity index 100% rename from src/procedure_kind.rs rename to rspc/src/procedure_kind.rs diff --git a/src/router.rs b/rspc/src/router.rs similarity index 100% rename from src/router.rs rename to rspc/src/router.rs diff --git a/src/types.rs b/rspc/src/types.rs similarity index 100% rename from src/types.rs rename to rspc/src/types.rs diff --git a/tests/typescript.rs b/rspc/tests/typescript.rs similarity index 100% rename from tests/typescript.rs rename to rspc/tests/typescript.rs From c4a6d15a885ffb0a699ab2053d69eda021d2e9f7 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Sat, 7 Dec 2024 00:44:40 +0800 Subject: [PATCH 17/67] add `unstable` and `nolegacy` features to rspc --- core/Cargo.toml | 14 +-- core/examples/basic.rs | 115 ------------------- examples/astro/src/components/playground.ts | 2 + examples/axum/Cargo.toml | 2 +- examples/axum/src/main.rs | 18 +-- examples/bindings.ts | 3 +- rspc/Cargo.toml | 28 +++-- rspc/src/languages/typescript.rs | 45 ++------ rspc/src/legacy/internal/middleware.rs | 23 +--- rspc/src/{ => legacy}/interop.rs | 27 +---- rspc/src/legacy/mod.rs | 1 + rspc/src/lib.rs | 30 ++++- rspc/src/modern/mod.rs | 2 +- rspc/src/modern/procedure/resolver_output.rs | 2 +- rspc/src/procedure.rs | 2 +- rspc/src/procedure_kind.rs | 38 +++--- rspc/src/router.rs | 65 +++++------ rspc/src/util.rs | 31 +++++ 18 files changed, 150 insertions(+), 298 deletions(-) delete mode 100644 core/examples/basic.rs rename rspc/src/{ => legacy}/interop.rs (91%) create mode 100644 rspc/src/util.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index d9934192..419b122a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,19 +15,13 @@ categories = ["web-programming", "asynchronous"] all-features = true rustdoc-args = ["--cfg", "docsrs"] -# TODO: Disable all features for each of them [dependencies] # Public -futures-core = { version = "0.3.31", default-features = false } -serde = { version = "1.0.215", default-features = false } +futures-core = { version = "0.3", default-features = false } +serde = { version = "1", default-features = false } # Private -erased-serde = { version = "0.4.5", default-features = false, features = [ +erased-serde = { version = "0.4", default-features = false, features = [ "std", ] } -pin-project-lite = { version = "0.2.15", default-features = false } - -# TODO: Remove these -[dev-dependencies] -futures = "0.3.31" -serde_json = "1.0.133" +pin-project-lite = { version = "0.2", default-features = false } diff --git a/core/examples/basic.rs b/core/examples/basic.rs deleted file mode 100644 index 93e4fd3a..00000000 --- a/core/examples/basic.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::pin::pin; - -use futures::{stream::poll_fn, StreamExt}; -use rspc_core::{Procedure, ProcedureStream, ResolverError}; - -#[derive(Debug)] -struct File; - -fn main() { - futures::executor::block_on(main2()); -} - -async fn main2() { - // /* Serialize */ - // TODO - - // /* Serialize + Stream */ - let y = Procedure::new(|_ctx, input| { - let input = input.deserialize::(); - // println!("GOT {}", input); - - ProcedureStream::from_stream(futures::stream::iter(vec![ - input.map(|x| x.len()).map_err(Into::into), - Ok(1), - Ok(2), - Ok(3), - Err(ResolverError::new(500, "Not found", None::)), - ])) - }); - // let mut result = y.exec_with_deserializer((), serde_json::Value::String("hello".to_string())); - // while let Some(value) = result.next(serde_json::value::Serializer).await { - // println!("{value:?}"); - // } - - let mut result = y.exec_with_deserializer((), serde_json::Value::Null); - while let Some(value) = result.next(serde_json::value::Serializer).await { - println!("{value:?}"); - } - - // // /* Non-serialize */ - // let y = Procedure::new(|_ctx, input| { - // let input: File = input.value().unwrap(); - // println!("GOT {:?}", input); - // }); - // let result = y.exec_with_value((), File); - - /* Async */ - // let y = Procedure::new(|_ctx, input| { - // let input: String = input.deserialize().unwrap(); - // println!("GOT {}", input); - // async move { - // println!("Async"); - // } - // }); - // let result = y.exec_with_deserializer((), serde_json::Value::String("hello".to_string())); - // let result: ProcedureStream = todo!(); - - // let result = pin!(result); - // let got = poll_fn(|cx| { - // let buf = Vec::new(); - // result.poll_next(cx, &mut buf) - // }) - // .collect() - // .await; - // println!("{:?}", got); - - // todo().await; -} - -async fn todo() { - println!("A"); - - // Side-effect based serializer - // let mut result: ProcedureStream = ProcedureStream::from_stream(futures::stream::iter(vec![ - // Ok(1), - // Ok(2), - // Ok(3), - // Err(ResolverError::new(500, "Not found", None::)), - // ])); - - // TODO: Clean this up + `Stream` adapter. - // loop { - // let mut buf = Vec::new(); - // let Some(result) = result - // .next(&mut serde_json::Serializer::new(&mut buf)) - // .await - // else { - // break; - // }; - // let _result: () = result.unwrap(); // TODO - // println!("{:?}", String::from_utf8_lossy(&buf)); - // } - - // Result based serializer - let mut result: ProcedureStream = ProcedureStream::from_stream(futures::stream::iter(vec![ - Ok(1), - Ok(2), - Ok(3), - Err(ResolverError::new(500, "Not found", None::)), - ])); - - while let Some(value) = result.next(serde_json::value::Serializer).await { - println!("{value:?}"); - } -} - -// let got: Vec = poll_fn(|cx| { -// let mut buf = Vec::new(); // TODO: We alloc per poll, we only need to alloc per-value. -// result -// .poll_next(cx, &mut serde_json::Serializer::new(&mut buf)) -// .map(|x| x.map(|_| String::from_utf8_lossy(&buf).to_string())) -// }) -// .collect() -// .await; -// println!("{:?}", got); diff --git a/examples/astro/src/components/playground.ts b/examples/astro/src/components/playground.ts index 0c4e19fc..d08c89b3 100644 --- a/examples/astro/src/components/playground.ts +++ b/examples/astro/src/components/playground.ts @@ -9,3 +9,5 @@ function createProxy(): { [K in keyof T]: () => T[K] } { const procedures = createProxy(); procedures.version(); + +procedures.newstuff(); diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 815e994c..fe2db1b5 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../../rspc", features = ["typescript", "rust"] } +rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } tokio = { version = "1.41.1", features = ["full"] } async-stream = "0.3.6" diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 5a23b7b5..90776026 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -3,12 +3,7 @@ use std::{marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; use async_stream::stream; use axum::{http::request::Parts, routing::get}; use rspc::{ - modern::{ - self, - middleware::Middleware, - procedure::{ResolverInput, ResolverOutput}, - Procedure2, - }, + middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, Router2, }; use serde::Serialize; @@ -104,14 +99,13 @@ pub enum Error { Mistake(String), } -impl modern::Error for Error {} +impl Error2 for Error {} pub struct BaseProcedure(PhantomData); impl BaseProcedure { - pub fn builder( - ) -> modern::procedure::ProcedureBuilder + pub fn builder() -> ProcedureBuilder where - TErr: modern::Error, + TErr: Error2, TInput: ResolverInput, TResult: ResolverOutput, { @@ -121,10 +115,10 @@ impl BaseProcedure { fn test_unstable_stuff(router: Router2) -> Router2 { router - .procedure_not_stable("newstuff", { + .procedure("newstuff", { ::builder().query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) }) - .procedure_not_stable("newstuff2", { + .procedure("newstuff2", { ::builder() // .with(invalidation(|ctx: Ctx, key, event| false)) .with(Middleware::new( diff --git a/examples/bindings.ts b/examples/bindings.ts index ed8d9d23..f35ada3a 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,7 +1,7 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { echo: { input: string, output: string, error: unknown }, @@ -10,6 +10,7 @@ export type Procedures = { hello: { input: null, output: string, error: unknown }, }, newstuff: { input: any, output: any, error: any }, + newstuff2: { input: any, output: any, error: any }, pings: { input: null, output: string, error: unknown }, sendMsg: { input: string, output: string, error: unknown }, transformMe: { input: null, output: string, error: unknown }, diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml index 32d6e379..a7528122 100644 --- a/rspc/Cargo.toml +++ b/rspc/Cargo.toml @@ -18,31 +18,29 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -tracing = ["dep:tracing"] -typescript = [] -rust = [] +typescript = [] # TODO: "dep:specta-typescript" +rust = ["dep:specta-rust"] + +# TODO: Remove these in future +unstable = [] +nolegacy = [] [dependencies] # Public rspc-core = { path = "../core" } -serde = { version = "1", features = ["derive"] } # TODO: Remove features -futures = "0.3" +serde = "1" +futures = "0.3" # TODO: Drop down to `futures-core` when removing legacy stuff? specta = { version = "=2.0.0-rc.20", features = [ - "derive", "serde", "serde_json", -] } # TODO: Drop all features + "derive", # TODO: remove this +] } # Private -serde-value = "0.7" -erased-serde = "0.4" -specta-typescript = { version = "=0.0.7", features = [] } -specta-rust = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } +specta-typescript = { version = "=0.0.7", features = [] } # TODO: Make optional once legacy stuff is removed - optional = true, +specta-rust = { git = "https://github.com/specta-rs/specta", optional = true, rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } # Temporary # TODO: Remove -serde_json = "1.0.133" # TODO: Drop this +serde_json = "1.0.133" thiserror = "2.0.3" -tokio = { version = "1.41.1", features = ["sync", "rt", "macros"] } -specta-serde = "0.0.7" -tracing = { version = "0.1.40", optional = true } diff --git a/rspc/src/languages/typescript.rs b/rspc/src/languages/typescript.rs index de006241..5b6ffe15 100644 --- a/rspc/src/languages/typescript.rs +++ b/rspc/src/languages/typescript.rs @@ -4,12 +4,7 @@ use serde_json::json; use specta::{datatype::DataType, NamedType, Type}; use specta_typescript::{datatype, export_named_datatype, ExportError}; -use crate::{ - interop::{construct_legacy_bindings_type, literal_object}, - procedure::ProcedureType, - types::TypesOrType, - Types, -}; +use crate::{procedure::ProcedureType, types::TypesOrType, util::literal_object, Types}; pub struct Typescript { inner: specta_typescript::Typescript, @@ -104,33 +99,13 @@ impl Typescript { } pub fn export(&self, types: &Types) -> Result { - // TODO: Add special `Bindings` type - - let legacy_types = construct_legacy_bindings_type(&types.procedures); - - // let bindings_types = types - // .procedures - // .iter() - // .map(|(key, p)| construct_bindings_type(&key, &p)) - // .collect::>(); - - let mut types = types.types.clone(); - - // { - // #[derive(Type)] - // struct Procedures; - - // let s = literal_object( - // "Procedures".into(), - // Some(Procedures::sid()), - // bindings_types.into_iter(), - // ); - // let mut ndt = Procedures::definition_named_data_type(&mut types); - // ndt.inner = s.into(); - // types.insert(Procedures::sid(), ndt); - // } + let mut typess = types.types.clone(); + #[cfg(not(feature = "nolegacy"))] { + let legacy_types = + crate::legacy::interop::construct_legacy_bindings_type(&types.procedures); + #[derive(Type)] struct ProceduresLegacy; @@ -139,12 +114,12 @@ impl Typescript { Some(ProceduresLegacy::sid()), legacy_types.into_iter(), ); - let mut ndt = ProceduresLegacy::definition_named_data_type(&mut types); + let mut ndt = ProceduresLegacy::definition_named_data_type(&mut typess); ndt.inner = s.into(); - types.insert(ProceduresLegacy::sid(), ndt); + typess.insert(ProceduresLegacy::sid(), ndt); } - self.inner.export(&types) + self.inner.export(&typess) } // pub fn export_ // TODO: Source map (can we make it be inline?) @@ -159,7 +134,7 @@ fn generate_bindings( fn inner( out: &mut String, this: &Typescript, - mut on_procedure: &mut impl FnMut(&Cow<'static, str>, (usize, usize), &ProcedureType), + on_procedure: &mut impl FnMut(&Cow<'static, str>, (usize, usize), &ProcedureType), types: &Types, source_pos: (usize, usize), key: &Cow<'static, str>, diff --git a/rspc/src/legacy/internal/middleware.rs b/rspc/src/legacy/internal/middleware.rs index c87a82c1..ce5a8640 100644 --- a/rspc/src/legacy/internal/middleware.rs +++ b/rspc/src/legacy/internal/middleware.rs @@ -176,28 +176,7 @@ where // TODO: Is this a duplicate of any type? // TODO: Move into public API cause it might be used in middleware -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProcedureKind { - Query, - Mutation, - Subscription, -} - -impl ProcedureKind { - pub fn to_str(&self) -> &'static str { - match self { - ProcedureKind::Query => "query", - ProcedureKind::Mutation => "mutation", - ProcedureKind::Subscription => "subscription", - } - } -} - -impl ToString for ProcedureKind { - fn to_string(&self) -> String { - self.to_str().to_string() - } -} +pub use crate::ProcedureKind; // TODO: Maybe rename to `Request` or something else. Also move into Public API cause it might be used in middleware #[derive(Debug, Clone)] diff --git a/rspc/src/interop.rs b/rspc/src/legacy/interop.rs similarity index 91% rename from rspc/src/interop.rs rename to rspc/src/legacy/interop.rs index 0c822916..82cc7024 100644 --- a/rspc/src/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -12,6 +12,7 @@ use crate::{ internal::{Layer, ProcedureKind, RequestContext, ValueOrStream}, procedure::ProcedureType, types::TypesOrType, + util::literal_object, Procedure2, Router, Router2, }; @@ -243,29 +244,3 @@ fn flatten_procedures_for_legacy( } } } - -// TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` -pub(crate) fn literal_object( - name: Cow<'static, str>, - sid: Option, - fields: impl Iterator, DataType)>, -) -> DataType { - specta::internal::construct::r#struct( - name, - sid, - Default::default(), - specta::internal::construct::struct_named( - fields - .into_iter() - .map(|(name, ty)| { - ( - name.into(), - specta::internal::construct::field(false, false, None, "".into(), Some(ty)), - ) - }) - .collect(), - None, - ), - ) - .into() -} diff --git a/rspc/src/legacy/mod.rs b/rspc/src/legacy/mod.rs index 36791237..db67e19a 100644 --- a/rspc/src/legacy/mod.rs +++ b/rspc/src/legacy/mod.rs @@ -1,5 +1,6 @@ mod config; mod error; +pub(crate) mod interop; mod middleware; mod resolver; mod resolver_result; diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index dc95dc9f..148a9142 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -13,12 +13,13 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] -pub(crate) mod interop; mod languages; +pub(crate) mod modern; mod procedure; mod procedure_kind; mod router; mod types; +pub(crate) mod util; #[allow(unused)] pub use languages::*; @@ -26,20 +27,35 @@ pub use procedure_kind::ProcedureKind; pub use router::Router2; pub use types::Types; -#[deprecated = "This stuff is unstable. Don't use it unless you know what your doing"] -pub mod modern; - // TODO: These will come in the future. +#[cfg(not(feature = "unstable"))] +pub(crate) use modern::State; +#[cfg(not(feature = "unstable"))] pub(crate) use procedure::Procedure2; -pub(crate) type State = (); -// TODO: Expose everything from `rspc_core`? +#[cfg(feature = "unstable")] +pub use modern::{ + middleware, procedure::ProcedureBuilder, procedure::ProcedureMeta, procedure::ResolverInput, + procedure::ResolverOutput, Error as Error2, Infallible, State, Stream, +}; +#[cfg(feature = "unstable")] +pub use procedure::Procedure2; + +pub use rspc_core::{ + DeserializeError, DowncastError, DynInput, Procedure, ProcedureError, ProcedureStream, + Procedures, ResolverError, +}; // Legacy stuff +#[cfg(not(feature = "nolegacy"))] mod legacy; +#[cfg(not(feature = "nolegacy"))] +pub(crate) use legacy::interop; + // These remain to respect semver but will all go with the next major. #[allow(deprecated)] +#[cfg(not(feature = "nolegacy"))] pub use legacy::{ internal, test_result_type, test_result_value, typedef, Config, DoubleArgMarker, DoubleArgStreamMarker, Error, ErrorCode, ExecError, ExecKind, ExportError, FutureMarker, @@ -47,3 +63,5 @@ pub use legacy::{ MiddlewareWithResponseHandler, RequestLayer, Resolver, ResultMarker, Router, RouterBuilder, SerializeMarker, StreamResolver, }; +#[cfg(not(feature = "nolegacy"))] +pub use rspc_core::LegacyErrorInterop; diff --git a/rspc/src/modern/mod.rs b/rspc/src/modern/mod.rs index 4f285baf..34b46d90 100644 --- a/rspc/src/modern/mod.rs +++ b/rspc/src/modern/mod.rs @@ -6,7 +6,7 @@ mod infallible; mod state; mod stream; -pub use crate::procedure::Procedure2; +// pub use crate::procedure::Procedure2; pub use error::Error; pub use infallible::Infallible; pub use state::State; diff --git a/rspc/src/modern/procedure/resolver_output.rs b/rspc/src/modern/procedure/resolver_output.rs index b2bb8937..6dc00d5a 100644 --- a/rspc/src/modern/procedure/resolver_output.rs +++ b/rspc/src/modern/procedure/resolver_output.rs @@ -29,7 +29,7 @@ // // note = "ResolverOutput requires a `T where T: serde::Serialize + specta::Type + 'static` to be returned from your procedure" // // )] -use futures::{Stream, StreamExt}; +use futures::Stream; use rspc_core::ProcedureStream; use serde::Serialize; use specta::{datatype::DataType, Generics, Type, TypeCollection}; diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 2f4b68eb..cf3cef33 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -33,7 +33,7 @@ pub struct Procedure2 { // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` impl Procedure2 { - #[doc(hidden)] // TODO: Remove this once stable + #[cfg(feature = "unstable")] /// Construct a new procedure using [`ProcedureBuilder`]. #[track_caller] pub fn builder() -> ProcedureBuilder diff --git a/rspc/src/procedure_kind.rs b/rspc/src/procedure_kind.rs index 13bee04e..049234c6 100644 --- a/rspc/src/procedure_kind.rs +++ b/rspc/src/procedure_kind.rs @@ -1,23 +1,21 @@ -// use std::fmt; +use std::fmt; -// use specta::Type; +use specta::Type; -// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] -// #[specta(rename_all = "camelCase")] -// pub enum ProcedureKind2 { -// Query, -// Mutation, -// Subscription, -// } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] +#[specta(rename_all = "camelCase")] +pub enum ProcedureKind { + Query, + Mutation, + Subscription, +} -// impl fmt::Display for ProcedureKind2 { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// match self { -// Self::Query => write!(f, "Query"), -// Self::Mutation => write!(f, "Mutation"), -// Self::Subscription => write!(f, "Subscription"), -// } -// } -// } - -pub use crate::internal::ProcedureKind; +impl fmt::Display for ProcedureKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Query => write!(f, "Query"), + Self::Mutation => write!(f, "Mutation"), + Self::Subscription => write!(f, "Subscription"), + } + } +} diff --git a/rspc/src/router.rs b/rspc/src/router.rs index a7cc2a86..cf46ec8d 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -8,7 +8,7 @@ use specta::TypeCollection; use rspc_core::Procedures; -use crate::{internal::ProcedureKind, types::TypesOrType, Procedure2, State, Types}; +use crate::{types::TypesOrType, Procedure2, ProcedureKind, State, Types}; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { @@ -33,21 +33,23 @@ impl Router2 { } // TODO: Enforce unique across all methods (query, subscription, etc). Eg. `insert` should yield error if key already exists. - // pub fn procedure( - // mut self, - // key: impl Into>, - // mut procedure: Procedure2, - // ) -> Self { - // self.setup.extend(procedure.setup.drain(..)); - // self.procedures.insert(vec![key.into()], procedure); - // self - // } + #[cfg(feature = "unstable")] + pub fn procedure( + mut self, + key: impl Into>, + mut procedure: Procedure2, + ) -> Self { + self.setup.extend(procedure.setup.drain(..)); + self.procedures.insert(vec![key.into()], procedure); + self + } // TODO: Document the order this is run in for `build` - // pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { - // self.setup.push(Box::new(func)); - // self - // } + #[cfg(feature = "unstable")] + pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { + self.setup.push(Box::new(func)); + self + } // TODO: Yield error if key already exists pub fn nest(mut self, prefix: impl Into>, mut other: Self) -> Self { @@ -71,7 +73,21 @@ impl Router2 { } pub fn build(self) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { - let mut state = (); + self.build_with_state_inner(State::default()) + } + + #[cfg(feature = "unstable")] + pub fn build_with_state( + self, + state: State, + ) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { + self.build_with_state_inner(state) + } + + fn build_with_state_inner( + self, + mut state: State, + ) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { for setup in self.setup { setup(&mut state); } @@ -155,14 +171,14 @@ impl<'a, TCtx> IntoIterator for &'a Router2 { } } -// TODO: Remove this block with the interop system +#[cfg(not(feature = "nolegacy"))] impl From> for Router2 { fn from(router: crate::legacy::Router) -> Self { crate::interop::legacy_to_modern(router) } } -// TODO: Remove this block with the interop system +#[cfg(not(feature = "nolegacy"))] impl Router2 { pub(crate) fn interop_procedures( &mut self, @@ -184,18 +200,3 @@ fn get_flattened_name(name: &Vec>) -> Cow<'static, str> { name.join(".").to_string().into() } } - -// TODO: Remove once procedure syntax stabilizes -impl Router2 { - #[doc(hidden)] - // TODO: Enforce unique across all methods (query, subscription, etc). Eg. `insert` should yield error if key already exists. - pub fn procedure_not_stable( - mut self, - key: impl Into>, - mut procedure: Procedure2, - ) -> Self { - self.setup.extend(procedure.setup.drain(..)); - self.procedures.insert(vec![key.into()], procedure); - self - } -} diff --git a/rspc/src/util.rs b/rspc/src/util.rs new file mode 100644 index 00000000..130ab5e3 --- /dev/null +++ b/rspc/src/util.rs @@ -0,0 +1,31 @@ +// TODO: Move into `langauge/typescript.rs` once legacy stuff is removed + +use std::borrow::Cow; + +use specta::{datatype::DataType, SpectaID}; + +// TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` +pub(crate) fn literal_object( + name: Cow<'static, str>, + sid: Option, + fields: impl Iterator, DataType)>, +) -> DataType { + specta::internal::construct::r#struct( + name, + sid, + Default::default(), + specta::internal::construct::struct_named( + fields + .into_iter() + .map(|(name, ty)| { + ( + name.into(), + specta::internal::construct::field(false, false, None, "".into(), Some(ty)), + ) + }) + .collect(), + None, + ), + ) + .into() +} From 6c7e5cc03dad3434a76ae67d34ab70e7c0a94cce Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Sat, 7 Dec 2024 00:47:16 +0800 Subject: [PATCH 18/67] remove `tracing` feature --- integrations/axum/src/jsonrpc_exec.rs | 56 ++++++++++++------------ integrations/axum/src/v2.rs | 48 ++++++++++---------- integrations/tauri/src/jsonrpc_exec.rs | 56 ++++++++++++------------ integrations/tauri/src/lib.rs | 12 ++--- rspc/src/legacy/internal/jsonrpc_exec.rs | 48 ++++++++++---------- 5 files changed, 110 insertions(+), 110 deletions(-) diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 1aca7dcc..2eaa3701 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -74,8 +74,8 @@ impl Sender2 { Self::ResponseChannel(tx) => tx.send(resp)?, Self::Broadcast(tx) => { let _ = tx.send(resp).map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } } @@ -94,8 +94,8 @@ impl<'a> Sender<'a> { Self::ResponseChannel(tx) => tx.send(resp)?, Self::Broadcast(tx) => { let _ = tx.send(resp).map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } Self::Response(r) => { @@ -138,8 +138,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -168,8 +168,8 @@ pub async fn handle_json_rpc( .expect("checked at if above") .map(ResponseInner::Response) .unwrap_or_else(|err| { - #[cfg(feature = "tracing")] - tracing::error!("Error executing operation: {:?}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); ResponseInner::Error(match err { ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { @@ -220,8 +220,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -240,8 +240,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } else if subscriptions.has_subscription(&id).await { let _ = sender @@ -256,8 +256,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -275,13 +275,13 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Subscription error: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); } None => return, } @@ -290,8 +290,8 @@ pub async fn handle_json_rpc( tokio::select! { biased; // Note: Order matters _ = &mut shutdown_rx => { - #[cfg(feature = "tracing")] - tracing::debug!("Removing subscription with id '{:?}'", id); + // #[cfg(feature = "tracing")] + // tracing::debug!("Removing subscription with id '{:?}'", id); break; } v = stream.next(serde_json::value::Serializer) => { @@ -304,13 +304,13 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Subscription error: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); } None => { break; @@ -326,8 +326,8 @@ pub async fn handle_json_rpc( } } None => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); ResponseInner::Error(jsonrpc::JsonRPCError { code: 404, message: "the requested operation is not supported by this server".to_string(), @@ -344,7 +344,7 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } diff --git a/integrations/axum/src/v2.rs b/integrations/axum/src/v2.rs index 7ff5284e..bf913f54 100644 --- a/integrations/axum/src/v2.rs +++ b/integrations/axum/src/v2.rs @@ -117,8 +117,8 @@ where let input = match input { Ok(input) => input, Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error passing parameters to operation '{procedure_name}': {_err}"); + // #[cfg(feature = "tracing")] + // tracing::error!("Error passing parameters to operation '{procedure_name}': {_err}"); return Response::builder() .status(StatusCode::NOT_FOUND) @@ -128,16 +128,16 @@ where } }; - #[cfg(feature = "tracing")] - tracing::debug!("Executing operation '{procedure_name}' with params {input:?}"); + // #[cfg(feature = "tracing")] + // tracing::debug!("Executing operation '{procedure_name}' with params {input:?}"); let mut resp = Sender::Response(None); let ctx = match ctx_fn.exec(parts, &state).await { Ok(ctx) => ctx, Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing context function: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing context function: {}", _err); return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) @@ -162,8 +162,8 @@ where input, }, ProcedureKind::Subscription => { - #[cfg(feature = "tracing")] - tracing::error!("Attempted to execute a subscription operation with HTTP"); + // #[cfg(feature = "tracing")] + // tracing::error!("Attempted to execute a subscription operation with HTTP"); return Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) @@ -187,8 +187,8 @@ where .body(Body::from(v)) .unwrap(), Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error serializing response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error serializing response: {}", _err); Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) @@ -217,8 +217,8 @@ async fn handle_websocket( use futures::StreamExt; use tokio::sync::mpsc; - #[cfg(feature = "tracing")] - tracing::debug!("Accepting websocket connection"); + // #[cfg(feature = "tracing")] + // tracing::debug!("Accepting websocket connection"); let mut subscriptions = HashMap::new(); let (mut tx, mut rx) = mpsc::channel::(100); @@ -230,16 +230,16 @@ async fn handle_websocket( match socket.send(Message::Text(match serde_json::to_string(&msg) { Ok(v) => v, Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error serializing websocket message: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error serializing websocket message: {}", _err); continue; } })).await { Ok(_) => {} Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error sending websocket message: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error sending websocket message: {}", _err); continue; } @@ -268,8 +268,8 @@ async fn handle_websocket( }, Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing context function: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing context function: {}", _err); continue; } @@ -280,8 +280,8 @@ async fn handle_websocket( } }, Err(_err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error parsing websocket message: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error parsing websocket message: {}", _err); // TODO: Send report of error to frontend @@ -290,16 +290,16 @@ async fn handle_websocket( }; } Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Error in websocket: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error in websocket: {}", _err); // TODO: Send report of error to frontend continue; }, None => { - #[cfg(feature = "tracing")] - tracing::debug!("Shutting down websocket connection"); + // #[cfg(feature = "tracing")] + // tracing::debug!("Shutting down websocket connection"); // TODO: Send report of error to frontend diff --git a/integrations/tauri/src/jsonrpc_exec.rs b/integrations/tauri/src/jsonrpc_exec.rs index 1aca7dcc..2eaa3701 100644 --- a/integrations/tauri/src/jsonrpc_exec.rs +++ b/integrations/tauri/src/jsonrpc_exec.rs @@ -74,8 +74,8 @@ impl Sender2 { Self::ResponseChannel(tx) => tx.send(resp)?, Self::Broadcast(tx) => { let _ = tx.send(resp).map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } } @@ -94,8 +94,8 @@ impl<'a> Sender<'a> { Self::ResponseChannel(tx) => tx.send(resp)?, Self::Broadcast(tx) => { let _ = tx.send(resp).map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } Self::Response(r) => { @@ -138,8 +138,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -168,8 +168,8 @@ pub async fn handle_json_rpc( .expect("checked at if above") .map(ResponseInner::Response) .unwrap_or_else(|err| { - #[cfg(feature = "tracing")] - tracing::error!("Error executing operation: {:?}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); ResponseInner::Error(match err { ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { @@ -220,8 +220,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -240,8 +240,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } else if subscriptions.has_subscription(&id).await { let _ = sender @@ -256,8 +256,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -275,13 +275,13 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Subscription error: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); } None => return, } @@ -290,8 +290,8 @@ pub async fn handle_json_rpc( tokio::select! { biased; // Note: Order matters _ = &mut shutdown_rx => { - #[cfg(feature = "tracing")] - tracing::debug!("Removing subscription with id '{:?}'", id); + // #[cfg(feature = "tracing")] + // tracing::debug!("Removing subscription with id '{:?}'", id); break; } v = stream.next(serde_json::value::Serializer) => { @@ -304,13 +304,13 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Subscription error: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); } None => { break; @@ -326,8 +326,8 @@ pub async fn handle_json_rpc( } } None => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); ResponseInner::Error(jsonrpc::JsonRPCError { code: 404, message: "the requested operation is not supported by this server".to_string(), @@ -344,7 +344,7 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 6084d306..68117872 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -65,8 +65,8 @@ where let _ = app_handle .emit("plugin:rspc:transport:resp", event) .map_err(|err| { - #[cfg(feature = "tracing")] - tracing::error!("failed to emit JSON-RPC response: {}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("failed to emit JSON-RPC response: {}", err); }); } }); @@ -77,14 +77,14 @@ where .send(match serde_json::from_str(event.payload()) { Ok(v) => v, Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!("failed to parse JSON-RPC request: {}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("failed to parse JSON-RPC request: {}", err); return; } }) .map_err(|err| { - #[cfg(feature = "tracing")] - tracing::error!("failed to send JSON-RPC request: {}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("failed to send JSON-RPC request: {}", err); }); }); diff --git a/rspc/src/legacy/internal/jsonrpc_exec.rs b/rspc/src/legacy/internal/jsonrpc_exec.rs index 46da99ca..39e9cd3c 100644 --- a/rspc/src/legacy/internal/jsonrpc_exec.rs +++ b/rspc/src/legacy/internal/jsonrpc_exec.rs @@ -80,8 +80,8 @@ impl Sender2 { Self::ResponseChannel(tx) => tx.send(resp)?, Self::Broadcast(tx) => { let _ = tx.send(resp).map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } } @@ -100,8 +100,8 @@ impl<'a> Sender<'a> { Self::ResponseChannel(tx) => tx.send(resp)?, Self::Broadcast(tx) => { let _ = tx.send(resp).map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } Self::Response(r) => { @@ -140,8 +140,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -186,8 +186,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -203,8 +203,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } else if subscriptions.has_subscription(&id).await { let _ = sender @@ -217,8 +217,8 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); }); } @@ -230,8 +230,8 @@ pub async fn handle_json_rpc( tokio::select! { biased; // Note: Order matters _ = &mut shutdown_rx => { - #[cfg(feature = "tracing")] - tracing::debug!("Removing subscription with id '{:?}'", id); + // #[cfg(feature = "tracing")] + // tracing::debug!("Removing subscription with id '{:?}'", id); break; } v = stream.next() => { @@ -244,13 +244,13 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } Some(Err(_err)) => { - #[cfg(feature = "tracing")] - tracing::error!("Subscription error: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); } None => { break; @@ -265,15 +265,15 @@ pub async fn handle_json_rpc( return; } Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing operation: {:?}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); ResponseInner::Error(err.into()) } }, Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!("Error executing operation: {:?}", err); + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); ResponseInner::Error(err.into()) } }; @@ -286,7 +286,7 @@ pub async fn handle_json_rpc( }) .await .map_err(|_err| { - #[cfg(feature = "tracing")] - tracing::error!("Failed to send response: {:?}", _err); + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); }); } From 377161c24d62d02948180e5dfcbef6d7f253155f Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Sat, 7 Dec 2024 01:36:08 +0800 Subject: [PATCH 19/67] workspace lints + wip `rspc-devtools` --- core/Cargo.toml | 3 + core/src/interop.rs | 4 - core/src/procedure.rs | 4 + examples/axum/Cargo.toml | 1 + examples/axum/src/main.rs | 4 +- integrations/axum/Cargo.toml | 3 + integrations/tauri/Cargo.toml | 3 + .../{playground => devtools}/Cargo.toml | 5 +- middleware/devtools/README.md | 3 + middleware/devtools/src/lib.rs | 76 +++++++++++++++++++ middleware/devtools/src/types.rs | 19 +++++ middleware/playground/README.md | 3 - middleware/playground/src/lib.rs | 7 -- rspc/Cargo.toml | 3 + 14 files changed, 122 insertions(+), 16 deletions(-) rename middleware/{playground => devtools}/Cargo.toml (59%) create mode 100644 middleware/devtools/README.md create mode 100644 middleware/devtools/src/lib.rs create mode 100644 middleware/devtools/src/types.rs delete mode 100644 middleware/playground/README.md delete mode 100644 middleware/playground/src/lib.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 419b122a..e0477f18 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,3 +25,6 @@ erased-serde = { version = "0.4", default-features = false, features = [ "std", ] } pin-project-lite = { version = "0.2", default-features = false } + +[lints] +workspace = true diff --git a/core/src/interop.rs b/core/src/interop.rs index ece5c62a..248e1977 100644 --- a/core/src/interop.rs +++ b/core/src/interop.rs @@ -1,9 +1,5 @@ //! TODO: A temporary module to allow for interop between modern and legacy code. -use std::sync::Arc; - -use crate::Procedures; - // TODO: Remove this once we remove the legacy executor. #[doc(hidden)] #[derive(Clone)] diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 0e444218..de32892d 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -51,6 +51,10 @@ impl Procedure { (self.handler)(ctx, value) } + + pub fn exec_with_dyn_input(&self, ctx: TCtx, input: DynInput) -> ProcedureStream { + (self.handler)(ctx, input) + } } impl Clone for Procedure { diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index fe2db1b5..d46bea51 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -18,3 +18,4 @@ specta = { version = "=2.0.0-rc.20", features = [ "derive", ] } # TODO: Drop all features thiserror = "2.0.4" +rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 90776026..fbfad31d 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -14,7 +14,7 @@ use tower_http::cors::{Any, CorsLayer}; // `Clone` is only required for usage with Websockets #[derive(Clone)] -struct Ctx {} +pub struct Ctx {} #[derive(Serialize, Type)] pub struct MyCustomType(String); @@ -185,6 +185,8 @@ async fn main() { // ) // .unwrap(); + let routes = rspc_devtools::mount(routes, &types); + // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! let cors = CorsLayer::new() .allow_methods(Any) diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index d0df8fb5..0bb33a39 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -29,3 +29,6 @@ form_urlencoded = "1.2.1" # TODO: use Axum's built in extr futures = "0.3" # TODO: No blocking execution, etc tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. serde = { version = "1", features = ["derive"] } # TODO: Remove features + +[lints] +workspace = true diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 3e14a1b8..33bc8051 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -23,3 +23,6 @@ serde_json = "1" # TODO: Drop these serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["sync"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. + +[lints] +workspace = true diff --git a/middleware/playground/Cargo.toml b/middleware/devtools/Cargo.toml similarity index 59% rename from middleware/playground/Cargo.toml rename to middleware/devtools/Cargo.toml index d19277c4..95d9fb3b 100644 --- a/middleware/playground/Cargo.toml +++ b/middleware/devtools/Cargo.toml @@ -1,10 +1,13 @@ [package] -name = "rspc-playground" +name = "rspc-devtools" version = "0.0.0" edition = "2021" publish = false [dependencies] +rspc-core = { path = "../../core" } +serde = { version = "1.0.215", features = ["derive"] } +specta = { version = "=2.0.0-rc.20", features = ["derive"] } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/middleware/devtools/README.md b/middleware/devtools/README.md new file mode 100644 index 00000000..8fb10a18 --- /dev/null +++ b/middleware/devtools/README.md @@ -0,0 +1,3 @@ +# rspc devtools + +Coming soon... diff --git a/middleware/devtools/src/lib.rs b/middleware/devtools/src/lib.rs new file mode 100644 index 00000000..b32075cd --- /dev/null +++ b/middleware/devtools/src/lib.rs @@ -0,0 +1,76 @@ +//! rspc-devtools: Devtools for rspc applications +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +// http://[::]:4000/rspc/~rspc.devtools.meta +// http://[::]:4000/rspc/~rspc.devtools.history + +mod types; + +use std::{ + any::Any, + sync::{Arc, Mutex, PoisonError}, +}; + +use rspc_core::{Procedure, ProcedureStream, Procedures}; +use types::{Metadata, ProcedureMetadata}; + +pub fn mount( + routes: impl Into>, + types: &impl Any, +) -> impl Into> { + let procedures = routes.into(); + let meta = Metadata { + crate_name: env!("CARGO_PKG_NAME"), + crate_version: env!("CARGO_PKG_VERSION"), + rspc_version: env!("CARGO_PKG_VERSION"), + procedures: procedures + .iter() + .map(|(name, _)| (name.to_string(), ProcedureMetadata {})) + .collect(), + }; + let history = Arc::new(Mutex::new(Vec::new())); // TODO: Stream to clients instead of storing in memory + + let mut procedures = procedures + .into_iter() + .map(|(name, procedure)| { + let history = history.clone(); + + ( + name.clone(), + Procedure::new(move |ctx, input| { + let start = std::time::Instant::now(); + let result = procedure.exec_with_dyn_input(ctx, input); + history + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push((name.to_string(), format!("{:?}", start.elapsed()))); + result + }), + ) + }) + .collect::>(); + + procedures.insert( + "~rspc.devtools.meta".into(), + Procedure::new(move |ctx, input| ProcedureStream::from_value(Ok(meta.clone()))), + ); + procedures.insert( + "~rspc.devtools.history".into(), + Procedure::new({ + let history = history.clone(); + move |ctx, input| { + ProcedureStream::from_value(Ok(history + .lock() + .unwrap_or_else(PoisonError::into_inner) + .clone())) + } + }), + ); + + procedures +} diff --git a/middleware/devtools/src/types.rs b/middleware/devtools/src/types.rs new file mode 100644 index 00000000..0e616210 --- /dev/null +++ b/middleware/devtools/src/types.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use serde::Serialize; +use specta::Type; + +#[derive(Clone, Serialize, Type)] +pub struct Metadata { + pub crate_name: &'static str, + pub crate_version: &'static str, + pub rspc_version: &'static str, + pub procedures: HashMap, +} + +#[derive(Clone, Serialize, Type)] +pub struct ProcedureMetadata { + // TODO: input type + // TOOD: output type + // TODO: p99's +} diff --git a/middleware/playground/README.md b/middleware/playground/README.md deleted file mode 100644 index 5bdc3658..00000000 --- a/middleware/playground/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# rspc Playground - -Coming soon... \ No newline at end of file diff --git a/middleware/playground/src/lib.rs b/middleware/playground/src/lib.rs deleted file mode 100644 index 94baa954..00000000 --- a/middleware/playground/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! rspc-devtools: Devtools for rspc applications -#![forbid(unsafe_code)] -#![cfg_attr(docsrs, feature(doc_cfg))] -#![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" -)] diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml index a7528122..2ab4db8a 100644 --- a/rspc/Cargo.toml +++ b/rspc/Cargo.toml @@ -44,3 +44,6 @@ specta-rust = { git = "https://github.com/specta-rs/specta", optional = true, re # Temporary # TODO: Remove serde_json = "1.0.133" thiserror = "2.0.3" + +[lints] +workspace = true From 06279ea8b4c77692d6a3fd4cd237354484f78b70 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Sat, 7 Dec 2024 09:28:54 +0800 Subject: [PATCH 20/67] init cache middleware --- middleware/cache/Cargo.toml | 16 ++++++++++++++++ middleware/cache/README.md | 3 +++ middleware/cache/src/lib.rs | 11 +++++++++++ 3 files changed, 30 insertions(+) create mode 100644 middleware/cache/Cargo.toml create mode 100644 middleware/cache/README.md create mode 100644 middleware/cache/src/lib.rs diff --git a/middleware/cache/Cargo.toml b/middleware/cache/Cargo.toml new file mode 100644 index 00000000..d51a4716 --- /dev/null +++ b/middleware/cache/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rspc-cached" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +rspc-core = { path = "../../core" } + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/cache/README.md b/middleware/cache/README.md new file mode 100644 index 00000000..c562e38f --- /dev/null +++ b/middleware/cache/README.md @@ -0,0 +1,3 @@ +# rspc cache + +Coming soon... diff --git a/middleware/cache/src/lib.rs b/middleware/cache/src/lib.rs new file mode 100644 index 00000000..98eacc12 --- /dev/null +++ b/middleware/cache/src/lib.rs @@ -0,0 +1,11 @@ +//! rspc-cache: Caching middleware for rspc +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +// TODO: Built-in TTL cache +// TODO: Allow defining custom cache lifetimes (copy Next.js cacheLife maybe) +// TODO: Allow defining a remote cache (e.g. Redis) From c1952c3dc5f33d901f77218f27ebd50d2419742d Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 8 Dec 2024 04:07:17 +0800 Subject: [PATCH 21/67] expose window in tauri ctx + use ipc channel --- integrations/tauri/src/new_lib.rs | 128 ++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 integrations/tauri/src/new_lib.rs diff --git a/integrations/tauri/src/new_lib.rs b/integrations/tauri/src/new_lib.rs new file mode 100644 index 00000000..2724aff0 --- /dev/null +++ b/integrations/tauri/src/new_lib.rs @@ -0,0 +1,128 @@ +//! rspc-tauri: Tauri integration for [rspc](https://rspc.dev). +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", + html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" +)] + +use std::{borrow::Borrow, collections::HashMap, future::Future, pin::Pin, sync::Arc}; + +use jsonrpc::RequestId; +use jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}; +use rspc_core::Procedures; +use tauri::{ + async_runtime::{spawn, Mutex}, + generate_handler, + plugin::{Builder, TauriPlugin}, + Manager, +}; +use tokio::sync::oneshot; + +mod jsonrpc; +mod jsonrpc_exec; + +struct State { + subscriptions: Arc>>>, + ctx_fn: TCtxFn, + procedures: Arc>, + phantom: std::marker::PhantomData, +} + +impl State +where + R: tauri::Runtime, + TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, + TCtx: Send + 'static, +{ + fn new(procedures: Procedures, ctx_fn: TCtxFn) -> Self { + Self { + subscriptions: Arc::new(Mutex::new(HashMap::new())), + ctx_fn, + procedures: Arc::new(procedures), + phantom: Default::default(), + } + } +} + +trait HandleRpc: Send + Sync { + fn handle_rpc( + &self, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, + ) -> Pin + Send>>; +} + +impl HandleRpc for State +where + R: tauri::Runtime + Send + Sync, + TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, + TCtx: Send + 'static, +{ + fn handle_rpc( + &self, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, + ) -> Pin + Send>> { + let ctx = (self.ctx_fn)(window); + let procedures = self.procedures.clone(); + let subscriptions = self.subscriptions.clone(); + + let (mut resp_tx, mut resp_rx) = + tokio::sync::mpsc::unbounded_channel::(); + + spawn(async move { + while let Some(resp) = resp_rx.recv().await { + channel.send(resp).ok(); + } + }); + + Box::pin(async move { + handle_json_rpc( + ctx, + req, + &procedures, + &mut Sender::ResponseChannel(&mut resp_tx), + &mut SubscriptionMap::Mutex(subscriptions.borrow()), + ) + .await; + }) + } +} + +type DynState = Arc>; + +#[tauri::command] +async fn handle_rpc( + state: tauri::State<'_, DynState>, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, +) -> Result<(), ()> { + state.handle_rpc(window, channel, req).await; + + Ok(()) +} + +pub fn plugin( + routes: impl Into>, + ctx_fn: TCtxFn, +) -> TauriPlugin +where + R: tauri::Runtime + Send + Sync, + TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, + TCtx: Send + Sync + 'static, +{ + let routes = routes.into(); + + Builder::new("rspc") + .invoke_handler(generate_handler![handle_rpc]) + .setup(move |app_handle, _| { + app_handle.manage(Arc::new(State::new(routes, ctx_fn)) as DynState); + + Ok(()) + }) + .build() +} From 723800878aaf71910e799b4280d2954c60db2ad8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 8 Dec 2024 18:26:59 +0800 Subject: [PATCH 22/67] trim down tauri integration + enumify DynInput --- core/src/dyn_input.rs | 44 +++- core/src/procedure.rs | 12 +- integrations/tauri/Cargo.toml | 6 +- integrations/tauri/src/jsonrpc.rs | 51 +--- integrations/tauri/src/jsonrpc_exec.rs | 350 ------------------------- integrations/tauri/src/lib.rs | 296 ++++++++++++++++----- integrations/tauri/src/new_lib.rs | 128 --------- 7 files changed, 276 insertions(+), 611 deletions(-) delete mode 100644 integrations/tauri/src/jsonrpc_exec.rs delete mode 100644 integrations/tauri/src/new_lib.rs diff --git a/core/src/dyn_input.rs b/core/src/dyn_input.rs index 7cb19db2..51d06790 100644 --- a/core/src/dyn_input.rs +++ b/core/src/dyn_input.rs @@ -9,31 +9,53 @@ use crate::{DeserializeError, DowncastError}; /// TODO pub struct DynInput<'a, 'de> { - pub(crate) value: Option<&'a mut (dyn Any + Send)>, - pub(crate) deserializer: Option<&'a mut (dyn erased_serde::Deserializer<'de> + Send)>, + inner: DynInputInner<'a, 'de>, pub(crate) type_name: &'static str, } +enum DynInputInner<'a, 'de> { + Value(&'a mut (dyn Any + Send)), + Deserializer(&'a mut (dyn erased_serde::Deserializer<'de> + Send)), +} + impl<'a, 'de> DynInput<'a, 'de> { + pub fn new_value(value: &'a mut Option) -> Self { + Self { + inner: DynInputInner::Value(value), + type_name: type_name::(), + } + } + + pub fn new_deserializer + Send>( + deserializer: &'a mut D, + ) -> Self { + Self { + inner: DynInputInner::Deserializer(deserializer), + type_name: type_name::(), + } + } + /// TODO pub fn deserialize>(self) -> Result { - erased_serde::deserialize(self.deserializer.ok_or(DeserializeError( - erased_serde::Error::custom(format!( + let DynInputInner::Deserializer(deserializer) = self.inner else { + return Err(DeserializeError(erased_serde::Error::custom(format!( "attempted to deserialize from value '{}' but expected deserializer", self.type_name - )), - ))?) - .map_err(|err| DeserializeError(err)) + )))); + }; + + erased_serde::deserialize(deserializer).map_err(|err| DeserializeError(err)) } /// TODO pub fn value(self) -> Result { - Ok(self - .value - .ok_or(DowncastError { + let DynInputInner::Value(value) = self.inner else { + return Err(DowncastError { from: None, to: type_name::(), - })? + }); + }; + Ok(value .downcast_mut::>() .ok_or(DowncastError { from: Some(self.type_name), diff --git a/core/src/procedure.rs b/core/src/procedure.rs index de32892d..96084d1c 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -32,22 +32,14 @@ impl Procedure { input: D, ) -> ProcedureStream { let mut deserializer = ::erase(input); - let value = DynInput { - value: None, - deserializer: Some(&mut deserializer), - type_name: type_name::(), - }; + let value = DynInput::new_deserializer(&mut deserializer); (self.handler)(ctx, value) } pub fn exec_with_value(&self, ctx: TCtx, input: T) -> ProcedureStream { let mut input = Some(input); - let value = DynInput { - value: Some(&mut input), - deserializer: None, - type_name: type_name::(), - }; + let value = DynInput::new_value(&mut input); (self.handler)(ctx, value) } diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 33bc8051..5ea3f056 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -22,7 +22,11 @@ serde_json = "1" # TODO: Drop these serde = { version = "1", features = ["derive"] } -tokio = { version = "1", features = ["sync"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. +tokio = { version = "1", features = [ + "sync", + "macros", +] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. +futures = "0.3.31" [lints] workspace = true diff --git a/integrations/tauri/src/jsonrpc.rs b/integrations/tauri/src/jsonrpc.rs index dfc0580c..295f5e64 100644 --- a/integrations/tauri/src/jsonrpc.rs +++ b/integrations/tauri/src/jsonrpc.rs @@ -1,25 +1,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] -#[serde(untagged)] -pub enum RequestId { - Null, - Number(u32), - String(String), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] // TODO: Type on this -pub struct Request { - pub jsonrpc: Option, // This is required in the JsonRPC spec but I make it optional. - pub id: RequestId, - #[serde(flatten)] - pub inner: RequestInner, -} - #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] -pub enum RequestInner { +pub enum Request { Query { path: String, input: Option, @@ -30,23 +14,17 @@ pub enum RequestInner { }, Subscription { path: String, - input: (RequestId, Option), + id: u32, + input: Option, }, SubscriptionStop { - input: RequestId, + id: u32, }, } -#[derive(Debug, Clone, Serialize)] // TODO: Add `specta::Type` when supported -pub struct Response { - pub jsonrpc: &'static str, - pub id: RequestId, - pub result: ResponseInner, -} - #[derive(Debug, Clone, Serialize)] #[serde(tag = "type", content = "data", rename_all = "camelCase")] -pub enum ResponseInner { +pub enum Response { Event(Value), Response(Value), Error(JsonRPCError), @@ -58,22 +36,3 @@ pub struct JsonRPCError { pub message: String, pub data: Option, } - -// TODO: BREAK - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProcedureKind { - Query, - Mutation, - Subscription, -} - -impl ProcedureKind { - pub fn to_str(&self) -> &'static str { - match self { - ProcedureKind::Query => "query", - ProcedureKind::Mutation => "mutation", - ProcedureKind::Subscription => "subscription", - } - } -} diff --git a/integrations/tauri/src/jsonrpc_exec.rs b/integrations/tauri/src/jsonrpc_exec.rs deleted file mode 100644 index 2eaa3701..00000000 --- a/integrations/tauri/src/jsonrpc_exec.rs +++ /dev/null @@ -1,350 +0,0 @@ -use std::{borrow::Cow, collections::HashMap}; - -use rspc_core::{ProcedureError, Procedures}; -use serde_json::Value; -use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; - -use super::jsonrpc::{self, RequestId, RequestInner, ResponseInner}; - -pub enum SubscriptionMap<'a> { - Ref(&'a mut HashMap>), - Mutex(&'a Mutex>>), - None, -} - -impl<'a> SubscriptionMap<'a> { - pub async fn has_subscription(&self, id: &RequestId) -> bool { - match self { - SubscriptionMap::Ref(map) => map.contains_key(id), - SubscriptionMap::Mutex(map) => { - let map = map.lock().await; - map.contains_key(id) - } - SubscriptionMap::None => unreachable!(), - } - } - - pub async fn insert(&mut self, id: RequestId, tx: oneshot::Sender<()>) { - match self { - SubscriptionMap::Ref(map) => { - map.insert(id, tx); - } - SubscriptionMap::Mutex(map) => { - let mut map = map.lock().await; - map.insert(id, tx); - } - SubscriptionMap::None => unreachable!(), - } - } - - pub async fn remove(&mut self, id: &RequestId) { - match self { - SubscriptionMap::Ref(map) => { - map.remove(id); - } - SubscriptionMap::Mutex(map) => { - let mut map = map.lock().await; - map.remove(id); - } - SubscriptionMap::None => unreachable!(), - } - } -} - -pub enum Sender<'a> { - Channel(&'a mut mpsc::Sender), - ResponseChannel(&'a mut mpsc::UnboundedSender), - Broadcast(&'a broadcast::Sender), - Response(Option), -} - -pub enum Sender2 { - Channel(mpsc::Sender), - ResponseChannel(mpsc::UnboundedSender), - Broadcast(broadcast::Sender), -} - -impl Sender2 { - pub async fn send( - &mut self, - resp: jsonrpc::Response, - ) -> Result<(), mpsc::error::SendError> { - match self { - Self::Channel(tx) => tx.send(resp).await?, - Self::ResponseChannel(tx) => tx.send(resp)?, - Self::Broadcast(tx) => { - let _ = tx.send(resp).map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - } - - Ok(()) - } -} - -impl<'a> Sender<'a> { - pub async fn send( - &mut self, - resp: jsonrpc::Response, - ) -> Result<(), mpsc::error::SendError> { - match self { - Self::Channel(tx) => tx.send(resp).await?, - Self::ResponseChannel(tx) => tx.send(resp)?, - Self::Broadcast(tx) => { - let _ = tx.send(resp).map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - Self::Response(r) => { - *r = Some(resp); - } - } - - Ok(()) - } - - pub fn sender2(&mut self) -> Sender2 { - match self { - Self::Channel(tx) => Sender2::Channel(tx.clone()), - Self::ResponseChannel(tx) => Sender2::ResponseChannel(tx.clone()), - Self::Broadcast(tx) => Sender2::Broadcast(tx.clone()), - Self::Response(_) => unreachable!(), - } - } -} - -pub async fn handle_json_rpc( - ctx: TCtx, - req: jsonrpc::Request, - routes: &Procedures, - sender: &mut Sender<'_>, - subscriptions: &mut SubscriptionMap<'_>, -) where - TCtx: 'static, -{ - if req.jsonrpc.is_some() && req.jsonrpc.as_deref() != Some("2.0") { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error(jsonrpc::JsonRPCError { - code: 400, - message: "invalid JSON-RPC version".into(), - data: None, - }), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - - let (path, input, sub_id) = match req.inner { - RequestInner::Query { path, input } => (path, input, None), - RequestInner::Mutation { path, input } => (path, input, None), - RequestInner::Subscription { path, input } => (path, input.1, Some(input.0)), - RequestInner::SubscriptionStop { input } => { - subscriptions.remove(&input).await; - return; - } - }; - - let result = match routes.get(&Cow::Borrowed(&*path)) { - Some(procedure) => { - let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); - - // It's really important this is before getting the first value - // Size hints can change after the first value is polled based on implementation. - let is_value = stream.size_hint() == (1, Some(1)); - - let first_value = stream.next(serde_json::value::Serializer).await; - - if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { - first_value - .expect("checked at if above") - .map(ResponseInner::Response) - .unwrap_or_else(|err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Error executing operation: {:?}", err); - - ResponseInner::Error(match err { - ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { - code: 400, - message: "error deserializing procedure arguments".to_string(), - data: None, - }, - ProcedureError::Downcast(_) => jsonrpc::JsonRPCError { - code: 400, - message: "error downcasting procedure arguments".to_string(), - data: None, - }, - ProcedureError::Serializer(_) => jsonrpc::JsonRPCError { - code: 500, - message: "error serializing procedure result".to_string(), - data: None, - }, - ProcedureError::Resolver(resolver_error) => { - let legacy_error = resolver_error - .error() - .and_then(|v| v.downcast_ref::()) - .cloned(); - - jsonrpc::JsonRPCError { - code: resolver_error.status() as i32, - message: legacy_error - .map(|v| v.0.clone()) - // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. - .unwrap_or_else(|| resolver_error.to_string()), - data: None, - } - } - }) - }) - } else { - if matches!(sender, Sender::Response(_)) - || matches!(subscriptions, SubscriptionMap::None) - { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error(jsonrpc::JsonRPCError { - code: 400, - message: "unsupported metho".into(), - data: None, - }), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - - if let Some(id) = sub_id { - if matches!(id, RequestId::Null) { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error(jsonrpc::JsonRPCError { - code: 400, - message: "error creating subscription with null request id" - .into(), - data: None, - }), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } else if subscriptions.has_subscription(&id).await { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error(jsonrpc::JsonRPCError { - code: 400, - message: "error creating subscription with duplicate id".into(), - data: None, - }), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - subscriptions.insert(id.clone(), shutdown_tx).await; - let mut sender2 = sender.sender2(); - tokio::spawn(async move { - match first_value { - Some(Ok(v)) => { - let _ = sender2 - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: id.clone(), - result: ResponseInner::Event(v), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {:?}", _err); - }); - } - Some(Err(_err)) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Subscription error: {:?}", _err); - } - None => return, - } - - loop { - tokio::select! { - biased; // Note: Order matters - _ = &mut shutdown_rx => { - // #[cfg(feature = "tracing")] - // tracing::debug!("Removing subscription with id '{:?}'", id); - break; - } - v = stream.next(serde_json::value::Serializer) => { - match v { - Some(Ok(v)) => { - let _ = sender2.send(jsonrpc::Response { - jsonrpc: "2.0", - id: id.clone(), - result: ResponseInner::Event(v), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {:?}", _err); - }); - } - Some(Err(_err)) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Subscription error: {:?}", _err); - } - None => { - break; - } - } - } - } - } - }); - } - - return; - } - } - None => { - // #[cfg(feature = "tracing")] - // tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); - ResponseInner::Error(jsonrpc::JsonRPCError { - code: 404, - message: "the requested operation is not supported by this server".to_string(), - data: None, - }) - } - }; - - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id, - result, - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {:?}", _err); - }); -} diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 68117872..9ca45207 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -6,87 +6,253 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] -use std::{borrow::Borrow, collections::HashMap, sync::Arc}; +use std::{ + borrow::Cow, + collections::HashMap, + sync::{Arc, Mutex, MutexGuard}, +}; -use jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}; -use rspc_core::{Procedure, Procedures}; +use jsonrpc::{Request, Response}; +use rspc_core::{ProcedureError, Procedures}; +use serde_json::Value; use tauri::{ - async_runtime::{spawn, Mutex}, + async_runtime::spawn, + generate_handler, plugin::{Builder, TauriPlugin}, - AppHandle, Emitter, Listener, Runtime, + Manager, }; -use tokio::sync::mpsc; +use tokio::sync::oneshot; mod jsonrpc; -mod jsonrpc_exec; -pub fn plugin( - routes: impl Into>, - ctx_fn: impl Fn(AppHandle) -> TCtx + Send + Sync + 'static, -) -> TauriPlugin +struct State { + subscriptions: Mutex>>, + ctx_fn: TCtxFn, + procedures: Procedures, + phantom: std::marker::PhantomData, +} + +impl State where + R: tauri::Runtime, + TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, TCtx: Send + 'static, { - let routes = routes.into(); + fn new(procedures: Procedures, ctx_fn: TCtxFn) -> Self { + Self { + subscriptions: Default::default(), + ctx_fn, + procedures, + phantom: Default::default(), + } + } - Builder::new("rspc") - .setup(|app_handle, _| { - let (tx, mut rx) = mpsc::unbounded_channel::(); - let (resp_tx, mut resp_rx) = mpsc::unbounded_channel::(); - // TODO: Don't keep using a tokio mutex. We don't need to hold it over the await point. - let subscriptions = Arc::new(Mutex::new(HashMap::new())); - - spawn({ - let app_handle = app_handle.clone(); - async move { - while let Some(req) = rx.recv().await { - let ctx = ctx_fn(app_handle.clone()); - let routes = routes.clone(); - let mut resp_tx = resp_tx.clone(); - let subscriptions = subscriptions.clone(); - spawn(async move { - handle_json_rpc( - ctx, - req, - &routes, - &mut Sender::ResponseChannel(&mut resp_tx), - &mut SubscriptionMap::Mutex(subscriptions.borrow()), - ) - .await; + fn subscriptions(&self) -> MutexGuard>> { + self.subscriptions.lock().unwrap() + } + + async fn handle_rpc_impl( + self: Arc, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, + ) { + let (path, input, sub_id) = match req { + Request::Query { path, input } | Request::Mutation { path, input } => { + (path, input, None) + } + Request::Subscription { path, input, id } => (path, input, Some(id)), + Request::SubscriptionStop { id } => { + self.subscriptions().remove(&id); + return; + } + }; + + let ctx = (self.ctx_fn)(window); + + let resp = match self.procedures.get(&Cow::Borrowed(&*path)) { + Some(procedure) => { + let mut stream = + procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); + + // It's really important this is before getting the first value + // Size hints can change after the first value is polled based on implementation. + let is_value = stream.size_hint() == (1, Some(1)); + + let first_value = stream.next(serde_json::value::Serializer).await; + + if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { + first_value + .expect("checked at if above") + .map(Response::Response) + .unwrap_or_else(|err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); + + Response::Error(match err { + ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error deserializing procedure arguments".to_string(), + data: None, + }, + ProcedureError::Downcast(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error downcasting procedure arguments".to_string(), + data: None, + }, + ProcedureError::Serializer(_) => jsonrpc::JsonRPCError { + code: 500, + message: "error serializing procedure result".to_string(), + data: None, + }, + ProcedureError::Resolver(resolver_error) => { + let legacy_error = resolver_error + .error() + .and_then(|v| { + v.downcast_ref::() + }) + .cloned(); + + jsonrpc::JsonRPCError { + code: resolver_error.status() as i32, + message: legacy_error + .map(|v| v.0.clone()) + // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. + .unwrap_or_else(|| resolver_error.to_string()), + data: None, + } + } + }) + }) + } else { + let Some(id) = sub_id else { + return; + }; + + if self.subscriptions().contains_key(&id) { + jsonrpc::Response::Error(jsonrpc::JsonRPCError { + code: 400, + message: "error creating subscription with duplicate id".into(), + data: None, + }) + } else { + let (shutdown_tx, mut shutdown) = oneshot::channel(); + self.subscriptions().insert(id.clone(), shutdown_tx); + + let channel = channel.clone(); + tokio::spawn(async move { + match first_value { + Some(Ok(v)) => { + channel.send(jsonrpc::Response::Event(v)).ok(); + } + Some(Err(_err)) => { + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); + } + None => return, + } + + loop { + tokio::select! { + biased; // Note: Order matters + _ = &mut shutdown => { + // #[cfg(feature = "tracing")] + // tracing::debug!("Removing subscription with id '{:?}'", id); + break; + } + v = stream.next(serde_json::value::Serializer) => { + match v { + Some(Ok(v)) => { + channel.send( + jsonrpc::Response::Event(v) + ).ok(); + } + Some(Err(_err)) => { + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); + } + None => { + break; + } + } + } + } + } }); + + return; } } - }); - - { - let app_handle = app_handle.clone(); - spawn(async move { - while let Some(event) = resp_rx.recv().await { - let _ = app_handle - .emit("plugin:rspc:transport:resp", event) - .map_err(|err| { - // #[cfg(feature = "tracing")] - // tracing::error!("failed to emit JSON-RPC response: {}", err); - }); - } - }); } + None => { + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); + Response::Error(jsonrpc::JsonRPCError { + code: 404, + message: "the requested operation is not supported by this server".to_string(), + data: None, + }) + } + }; - app_handle.listen_any("plugin:rspc:transport", move |event| { - let _ = tx - .send(match serde_json::from_str(event.payload()) { - Ok(v) => v, - Err(err) => { - // #[cfg(feature = "tracing")] - // tracing::error!("failed to parse JSON-RPC request: {}", err); - return; - } - }) - .map_err(|err| { - // #[cfg(feature = "tracing")] - // tracing::error!("failed to send JSON-RPC request: {}", err); - }); - }); + channel.send(resp).ok(); + } +} + +trait HandleRpc: Send + Sync { + fn handle_rpc( + self: Arc, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, + ); +} + +impl HandleRpc for State +where + R: tauri::Runtime + Send + Sync, + TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, + TCtx: Send + 'static, +{ + fn handle_rpc( + self: Arc, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, + ) { + spawn(Self::handle_rpc_impl(self, window, channel, req)); + } +} + +type DynState = Arc>; + +#[tauri::command] +fn handle_rpc( + state: tauri::State<'_, DynState>, + window: tauri::Window, + channel: tauri::ipc::Channel, + req: jsonrpc::Request, +) -> Result<(), ()> { + state.inner().clone().handle_rpc(window, channel, req); + + Ok(()) +} + +pub fn plugin( + routes: impl Into>, + ctx_fn: TCtxFn, +) -> TauriPlugin +where + R: tauri::Runtime + Send + Sync, + TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, + TCtx: Send + Sync + 'static, +{ + let routes = routes.into(); + + Builder::new("rspc") + .invoke_handler(generate_handler![handle_rpc]) + .setup(move |app_handle, _| { + app_handle.manage(Arc::new(State::new(routes, ctx_fn)) as DynState); Ok(()) }) diff --git a/integrations/tauri/src/new_lib.rs b/integrations/tauri/src/new_lib.rs deleted file mode 100644 index 2724aff0..00000000 --- a/integrations/tauri/src/new_lib.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! rspc-tauri: Tauri integration for [rspc](https://rspc.dev). -#![forbid(unsafe_code)] -#![cfg_attr(docsrs, feature(doc_cfg))] -#![doc( - html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", - html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" -)] - -use std::{borrow::Borrow, collections::HashMap, future::Future, pin::Pin, sync::Arc}; - -use jsonrpc::RequestId; -use jsonrpc_exec::{handle_json_rpc, Sender, SubscriptionMap}; -use rspc_core::Procedures; -use tauri::{ - async_runtime::{spawn, Mutex}, - generate_handler, - plugin::{Builder, TauriPlugin}, - Manager, -}; -use tokio::sync::oneshot; - -mod jsonrpc; -mod jsonrpc_exec; - -struct State { - subscriptions: Arc>>>, - ctx_fn: TCtxFn, - procedures: Arc>, - phantom: std::marker::PhantomData, -} - -impl State -where - R: tauri::Runtime, - TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, - TCtx: Send + 'static, -{ - fn new(procedures: Procedures, ctx_fn: TCtxFn) -> Self { - Self { - subscriptions: Arc::new(Mutex::new(HashMap::new())), - ctx_fn, - procedures: Arc::new(procedures), - phantom: Default::default(), - } - } -} - -trait HandleRpc: Send + Sync { - fn handle_rpc( - &self, - window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, - ) -> Pin + Send>>; -} - -impl HandleRpc for State -where - R: tauri::Runtime + Send + Sync, - TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, - TCtx: Send + 'static, -{ - fn handle_rpc( - &self, - window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, - ) -> Pin + Send>> { - let ctx = (self.ctx_fn)(window); - let procedures = self.procedures.clone(); - let subscriptions = self.subscriptions.clone(); - - let (mut resp_tx, mut resp_rx) = - tokio::sync::mpsc::unbounded_channel::(); - - spawn(async move { - while let Some(resp) = resp_rx.recv().await { - channel.send(resp).ok(); - } - }); - - Box::pin(async move { - handle_json_rpc( - ctx, - req, - &procedures, - &mut Sender::ResponseChannel(&mut resp_tx), - &mut SubscriptionMap::Mutex(subscriptions.borrow()), - ) - .await; - }) - } -} - -type DynState = Arc>; - -#[tauri::command] -async fn handle_rpc( - state: tauri::State<'_, DynState>, - window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, -) -> Result<(), ()> { - state.handle_rpc(window, channel, req).await; - - Ok(()) -} - -pub fn plugin( - routes: impl Into>, - ctx_fn: TCtxFn, -) -> TauriPlugin -where - R: tauri::Runtime + Send + Sync, - TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, - TCtx: Send + Sync + 'static, -{ - let routes = routes.into(); - - Builder::new("rspc") - .invoke_handler(generate_handler![handle_rpc]) - .setup(move |app_handle, _| { - app_handle.manage(Arc::new(State::new(routes, ctx_fn)) as DynState); - - Ok(()) - }) - .build() -} From bc0e1f03e05cc3ee7d62bfbf74568449bfec07f6 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 8 Dec 2024 19:04:17 +0800 Subject: [PATCH 23/67] simplify subscription stream handling --- integrations/tauri/Cargo.toml | 5 +- integrations/tauri/src/jsonrpc.rs | 38 --------- integrations/tauri/src/lib.rs | 124 +++++++++++++++++++----------- 3 files changed, 78 insertions(+), 89 deletions(-) delete mode 100644 integrations/tauri/src/jsonrpc.rs diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 5ea3f056..9fbfeb85 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -22,10 +22,7 @@ serde_json = "1" # TODO: Drop these serde = { version = "1", features = ["derive"] } -tokio = { version = "1", features = [ - "sync", - "macros", -] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. +tokio = { version = "1", features = ["sync"] } futures = "0.3.31" [lints] diff --git a/integrations/tauri/src/jsonrpc.rs b/integrations/tauri/src/jsonrpc.rs deleted file mode 100644 index 295f5e64..00000000 --- a/integrations/tauri/src/jsonrpc.rs +++ /dev/null @@ -1,38 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(tag = "method", content = "params", rename_all = "camelCase")] -pub enum Request { - Query { - path: String, - input: Option, - }, - Mutation { - path: String, - input: Option, - }, - Subscription { - path: String, - id: u32, - input: Option, - }, - SubscriptionStop { - id: u32, - }, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "camelCase")] -pub enum Response { - Event(Value), - Response(Value), - Error(JsonRPCError), -} - -#[derive(Debug, Clone, Serialize)] -pub struct JsonRPCError { - pub code: i32, - pub message: String, - pub data: Option, -} diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 9ca45207..c0ac7f4c 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -10,10 +10,12 @@ use std::{ borrow::Cow, collections::HashMap, sync::{Arc, Mutex, MutexGuard}, + task::Poll, }; -use jsonrpc::{Request, Response}; +use futures::{pin_mut, stream, FutureExt, StreamExt}; use rspc_core::{ProcedureError, Procedures}; +use serde::{Deserialize, Serialize}; use serde_json::Value; use tauri::{ async_runtime::spawn, @@ -23,8 +25,6 @@ use tauri::{ }; use tokio::sync::oneshot; -mod jsonrpc; - struct State { subscriptions: Mutex>>, ctx_fn: TCtxFn, @@ -54,8 +54,8 @@ where async fn handle_rpc_impl( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, + channel: tauri::ipc::Channel, + req: Request, ) { let (path, input, sub_id) = match req { Request::Query { path, input } | Request::Mutation { path, input } => { @@ -90,17 +90,17 @@ where // tracing::error!("Error executing operation: {:?}", err); Response::Error(match err { - ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { + ProcedureError::Deserialize(_) => Error { code: 400, message: "error deserializing procedure arguments".to_string(), data: None, }, - ProcedureError::Downcast(_) => jsonrpc::JsonRPCError { + ProcedureError::Downcast(_) => Error { code: 400, message: "error downcasting procedure arguments".to_string(), data: None, }, - ProcedureError::Serializer(_) => jsonrpc::JsonRPCError { + ProcedureError::Serializer(_) => Error { code: 500, message: "error serializing procedure result".to_string(), data: None, @@ -113,7 +113,7 @@ where }) .cloned(); - jsonrpc::JsonRPCError { + Error { code: resolver_error.status() as i32, message: legacy_error .map(|v| v.0.clone()) @@ -130,51 +130,45 @@ where }; if self.subscriptions().contains_key(&id) { - jsonrpc::Response::Error(jsonrpc::JsonRPCError { + Response::Error(Error { code: 400, message: "error creating subscription with duplicate id".into(), data: None, }) } else { - let (shutdown_tx, mut shutdown) = oneshot::channel(); + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); self.subscriptions().insert(id.clone(), shutdown_tx); let channel = channel.clone(); tokio::spawn(async move { - match first_value { - Some(Ok(v)) => { - channel.send(jsonrpc::Response::Event(v)).ok(); + let mut first_value = Some(first_value); + + let mut stream = stream::poll_fn(|cx| { + if let Some(first_value) = first_value.take() { + return Poll::Ready(Some(first_value)); } - Some(Err(_err)) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Subscription error: {:?}", _err); + + if let Poll::Ready(_) = shutdown_rx.poll_unpin(cx) { + return Poll::Ready(None); } - None => return, - } - loop { - tokio::select! { - biased; // Note: Order matters - _ = &mut shutdown => { + let stream_fut = stream.next(serde_json::value::Serializer); + pin_mut!(stream_fut); + + stream_fut.poll_unpin(cx).map(|v| v.map(Some)) + }); + + while let Some(event) = stream.next().await { + match event { + Some(Ok(v)) => { + channel.send(Response::Event(v)).ok(); + } + Some(Err(_err)) => { // #[cfg(feature = "tracing")] - // tracing::debug!("Removing subscription with id '{:?}'", id); - break; + // tracing::error!("Subscription error: {:?}", _err); } - v = stream.next(serde_json::value::Serializer) => { - match v { - Some(Ok(v)) => { - channel.send( - jsonrpc::Response::Event(v) - ).ok(); - } - Some(Err(_err)) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Subscription error: {:?}", _err); - } - None => { - break; - } - } + None => { + break; } } } @@ -187,7 +181,7 @@ where None => { // #[cfg(feature = "tracing")] // tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); - Response::Error(jsonrpc::JsonRPCError { + Response::Error(Error { code: 404, message: "the requested operation is not supported by this server".to_string(), data: None, @@ -203,8 +197,8 @@ trait HandleRpc: Send + Sync { fn handle_rpc( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, + channel: tauri::ipc::Channel, + req: Request, ); } @@ -217,8 +211,8 @@ where fn handle_rpc( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, + channel: tauri::ipc::Channel, + req: Request, ) { spawn(Self::handle_rpc_impl(self, window, channel, req)); } @@ -230,8 +224,8 @@ type DynState = Arc>; fn handle_rpc( state: tauri::State<'_, DynState>, window: tauri::Window, - channel: tauri::ipc::Channel, - req: jsonrpc::Request, + channel: tauri::ipc::Channel, + req: Request, ) -> Result<(), ()> { state.inner().clone().handle_rpc(window, channel, req); @@ -258,3 +252,39 @@ where }) .build() } + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +pub enum Request { + Query { + path: String, + input: Option, + }, + Mutation { + path: String, + input: Option, + }, + Subscription { + path: String, + id: u32, + input: Option, + }, + SubscriptionStop { + id: u32, + }, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type", content = "data", rename_all = "camelCase")] +pub enum Response { + Event(Value), + Response(Value), + Error(Error), +} + +#[derive(Debug, Clone, Serialize)] +pub struct Error { + pub code: i32, + pub message: String, + pub data: Option, +} From bd7f92264e31eb1f05e921c8c9395d128a1e2ef1 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 8 Dec 2024 19:20:03 +0800 Subject: [PATCH 24/67] better document tauri integration --- integrations/tauri/src/lib.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index c0ac7f4c..9693a62b 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -25,14 +25,14 @@ use tauri::{ }; use tokio::sync::oneshot; -struct State { +struct RpcHandler { subscriptions: Mutex>>, ctx_fn: TCtxFn, procedures: Procedures, phantom: std::marker::PhantomData, } -impl State +impl RpcHandler where R: tauri::Runtime, TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, @@ -202,7 +202,7 @@ trait HandleRpc: Send + Sync { ); } -impl HandleRpc for State +impl HandleRpc for RpcHandler where R: tauri::Runtime + Send + Sync, TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, @@ -218,18 +218,20 @@ where } } -type DynState = Arc>; +// Tauri commands can't be generic except for their runtime, +// so we need to store + access the handler behind a trait. +// This way handle_rpc_impl has full access to the generics it was instantiated with, +// while State can be stored a) as a singleton (enforced by the type system!) and b) as type erased Tauri state +struct State(Arc>); #[tauri::command] fn handle_rpc( - state: tauri::State<'_, DynState>, + state: tauri::State<'_, State>, window: tauri::Window, channel: tauri::ipc::Channel, req: Request, -) -> Result<(), ()> { - state.inner().clone().handle_rpc(window, channel, req); - - Ok(()) +) { + state.0.clone().handle_rpc(window, channel, req); } pub fn plugin( @@ -246,7 +248,7 @@ where Builder::new("rspc") .invoke_handler(generate_handler![handle_rpc]) .setup(move |app_handle, _| { - app_handle.manage(Arc::new(State::new(routes, ctx_fn)) as DynState); + app_handle.manage(State(Arc::new(RpcHandler::new(routes, ctx_fn)))); Ok(()) }) From c2f4ac1112341b89a15e87b8f790c6dc14e4f44e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 8 Dec 2024 19:21:15 +0800 Subject: [PATCH 25/67] tauri spaawn --- integrations/tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 9693a62b..543e1e79 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -140,7 +140,7 @@ where self.subscriptions().insert(id.clone(), shutdown_tx); let channel = channel.clone(); - tokio::spawn(async move { + spawn(async move { let mut first_value = Some(first_value); let mut stream = stream::poll_fn(|cx| { From ca5f31d99dc44c9fbad7dde638d5ee07ca5e40be Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sun, 8 Dec 2024 19:24:15 +0800 Subject: [PATCH 26/67] handle first value properly --- integrations/tauri/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 543e1e79..c0a2b3f5 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -145,7 +145,8 @@ where let mut stream = stream::poll_fn(|cx| { if let Some(first_value) = first_value.take() { - return Poll::Ready(Some(first_value)); + // if first_value is None, the stream should stop + return Poll::Ready(first_value.map(Some)); } if let Poll::Ready(_) = shutdown_rx.poll_unpin(cx) { From 585992300d777b2fb01a1a90a146bfa857cb4fa4 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Sun, 8 Dec 2024 23:42:33 +0800 Subject: [PATCH 27/67] improve Tauri integration even more --- core/src/error.rs | 71 ++++++++- integrations/tauri/Cargo.toml | 8 +- integrations/tauri/src/lib.rs | 261 +++++++++++----------------------- 3 files changed, 153 insertions(+), 187 deletions(-) diff --git a/core/src/error.rs b/core/src/error.rs index 5f495180..5a734ea5 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,9 +1,11 @@ -use std::{error, fmt}; +use std::{borrow::Cow, error, fmt}; -use serde::{Serialize, Serializer}; +use serde::{ser::SerializeStruct, Serialize, Serializer}; /// TODO pub enum ProcedureError { + /// Failed to find a procedure with the given name. + NotFound, /// Attempted to deserialize a value but failed. Deserialize(DeserializeError), /// Attempting to downcast a value failed. @@ -14,6 +16,51 @@ pub enum ProcedureError { Resolver(ResolverError), } +impl ProcedureError { + pub fn code(&self) -> u16 { + match self { + Self::NotFound => 404, + Self::Deserialize(_) => 400, + Self::Downcast(_) => 400, + Self::Serializer(_) => 500, + Self::Resolver(err) => err.status(), + } + } + + pub fn serialize(&self, s: Se) -> Result { + match self { + Self::NotFound => s.serialize_none(), + Self::Deserialize(err) => s.serialize_str(&format!("{}", err)), + Self::Downcast(err) => s.serialize_str(&format!("{}", err)), + Self::Serializer(err) => s.serialize_str(&format!("{}", err)), + Self::Resolver(err) => s.serialize_str(&format!("{}", err)), + } + } + + pub fn variant(&self) -> &'static str { + match self { + ProcedureError::NotFound => "NotFound", + ProcedureError::Deserialize(_) => "Deserialize", + ProcedureError::Downcast(_) => "Downcast", + ProcedureError::Serializer(_) => "Serializer", + ProcedureError::Resolver(_) => "Resolver", + } + } + + pub fn message(&self) -> Cow<'static, str> { + match self { + ProcedureError::NotFound => "procedure not found".into(), + ProcedureError::Deserialize(err) => err.0.to_string().into(), + ProcedureError::Downcast(err) => err.to_string().into(), + ProcedureError::Serializer(err) => err.to_string().into(), + ProcedureError::Resolver(err) => err + .error() + .map(|err| err.to_string().into()) + .unwrap_or("resolver error".into()), + } + } +} + impl From for ProcedureError { fn from(err: ResolverError) -> Self { match err.0 { @@ -28,6 +75,7 @@ impl fmt::Debug for ProcedureError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: Proper format match self { + Self::NotFound => write!(f, "NotFound"), Self::Deserialize(err) => write!(f, "Deserialize({:?})", err), Self::Downcast(err) => write!(f, "Downcast({:?})", err), Self::Serializer(err) => write!(f, "Serializer({:?})", err), @@ -44,6 +92,23 @@ impl fmt::Display for ProcedureError { impl error::Error for ProcedureError {} +impl Serialize for ProcedureError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if let ProcedureError::Resolver(err) = self { + return err.value().serialize(serializer); + } + + let mut state = serializer.serialize_struct("ProcedureError", 1)?; + state.serialize_field("_rspc", &true)?; + state.serialize_field("variant", &self.variant())?; + state.serialize_field("message", &self.message())?; + state.end() + } +} + enum Repr { // An actual resolver error. Custom { @@ -87,7 +152,7 @@ impl ResolverError { } /// TODO - pub fn value(&self) -> &dyn erased_serde::Serialize { + pub fn value(&self) -> impl Serialize + '_ { match &self.0 { Repr::Custom { status: _, diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 9fbfeb85..9fe7bf38 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -18,12 +18,8 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] rspc-core = { version = "0.0.1", path = "../../core" } tauri = "2" -serde_json = "1" - -# TODO: Drop these -serde = { version = "1", features = ["derive"] } -tokio = { version = "1", features = ["sync"] } -futures = "0.3.31" +serde = { version = "1", features = ["derive"] } # is a dependency of Tauri anyway +serde_json = "1" # is a dependency of Tauri anyway [lints] workspace = true diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index c0a2b3f5..4a8ebbae 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -9,27 +9,24 @@ use std::{ borrow::Cow, collections::HashMap, - sync::{Arc, Mutex, MutexGuard}, - task::Poll, + sync::{Arc, Mutex, MutexGuard, PoisonError}, }; -use futures::{pin_mut, stream, FutureExt, StreamExt}; use rspc_core::{ProcedureError, Procedures}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{value::Serializer, Value}; use tauri::{ - async_runtime::spawn, + async_runtime::{spawn, JoinHandle}, generate_handler, plugin::{Builder, TauriPlugin}, Manager, }; -use tokio::sync::oneshot; struct RpcHandler { - subscriptions: Mutex>>, + subscriptions: Mutex>>, ctx_fn: TCtxFn, procedures: Procedures, - phantom: std::marker::PhantomData, + phantom: std::marker::PhantomData R>, } impl RpcHandler @@ -38,159 +35,75 @@ where TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, TCtx: Send + 'static, { - fn new(procedures: Procedures, ctx_fn: TCtxFn) -> Self { - Self { - subscriptions: Default::default(), - ctx_fn, - procedures, - phantom: Default::default(), - } - } - - fn subscriptions(&self) -> MutexGuard>> { - self.subscriptions.lock().unwrap() + fn subscriptions(&self) -> MutexGuard>> { + self.subscriptions + .lock() + .unwrap_or_else(PoisonError::into_inner) } - async fn handle_rpc_impl( + fn handle_rpc_impl( self: Arc, window: tauri::Window, channel: tauri::ipc::Channel, req: Request, ) { - let (path, input, sub_id) = match req { - Request::Query { path, input } | Request::Mutation { path, input } => { - (path, input, None) - } - Request::Subscription { path, input, id } => (path, input, Some(id)), - Request::SubscriptionStop { id } => { - self.subscriptions().remove(&id); - return; - } - }; - - let ctx = (self.ctx_fn)(window); + match req { + Request::Request { path, input } => { + let ctx = (self.ctx_fn)(window); + + let id = channel.id(); + let send = move |resp: Option>>| { + channel + .send( + resp.ok_or(Response::Done) + .and_then(|v| { + v.map(|value| Response::Value { code: 200, value }).map_err( + |err| Response::Value { + code: err.code(), + value: serde_json::to_value(err).unwrap(), // TODO: Error handling (can we throw it back into Tauri, else we are at an impasse) + }, + ) + }) + .unwrap_or_else(|e| e), + ) + .ok() + }; + + let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { + send(Some(Err(ProcedureError::NotFound))); + send(None); + return; + }; - let resp = match self.procedures.get(&Cow::Borrowed(&*path)) { - Some(procedure) => { let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); - // It's really important this is before getting the first value - // Size hints can change after the first value is polled based on implementation. - let is_value = stream.size_hint() == (1, Some(1)); - - let first_value = stream.next(serde_json::value::Serializer).await; - - if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { - first_value - .expect("checked at if above") - .map(Response::Response) - .unwrap_or_else(|err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Error executing operation: {:?}", err); - - Response::Error(match err { - ProcedureError::Deserialize(_) => Error { - code: 400, - message: "error deserializing procedure arguments".to_string(), - data: None, - }, - ProcedureError::Downcast(_) => Error { - code: 400, - message: "error downcasting procedure arguments".to_string(), - data: None, - }, - ProcedureError::Serializer(_) => Error { - code: 500, - message: "error serializing procedure result".to_string(), - data: None, - }, - ProcedureError::Resolver(resolver_error) => { - let legacy_error = resolver_error - .error() - .and_then(|v| { - v.downcast_ref::() - }) - .cloned(); - - Error { - code: resolver_error.status() as i32, - message: legacy_error - .map(|v| v.0.clone()) - // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. - .unwrap_or_else(|| resolver_error.to_string()), - data: None, - } - } - }) - }) - } else { - let Some(id) = sub_id else { - return; - }; - - if self.subscriptions().contains_key(&id) { - Response::Error(Error { - code: 400, - message: "error creating subscription with duplicate id".into(), - data: None, - }) - } else { - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - self.subscriptions().insert(id.clone(), shutdown_tx); - - let channel = channel.clone(); - spawn(async move { - let mut first_value = Some(first_value); + let this = self.clone(); + let handle = spawn(async move { + loop { + let value = stream.next(Serializer).await; + let is_finished = value.is_none(); + send(value); - let mut stream = stream::poll_fn(|cx| { - if let Some(first_value) = first_value.take() { - // if first_value is None, the stream should stop - return Poll::Ready(first_value.map(Some)); - } - - if let Poll::Ready(_) = shutdown_rx.poll_unpin(cx) { - return Poll::Ready(None); - } - - let stream_fut = stream.next(serde_json::value::Serializer); - pin_mut!(stream_fut); - - stream_fut.poll_unpin(cx).map(|v| v.map(Some)) - }); + if is_finished { + break; + } + } - while let Some(event) = stream.next().await { - match event { - Some(Ok(v)) => { - channel.send(Response::Event(v)).ok(); - } - Some(Err(_err)) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Subscription error: {:?}", _err); - } - None => { - break; - } - } - } - }); + this.subscriptions().remove(&id); + }); - return; - } + // if the client uses an existing ID, we will assume the previous subscription is no longer required + if let Some(old) = self.subscriptions().insert(id, handle) { + old.abort(); } } - None => { - // #[cfg(feature = "tracing")] - // tracing::error!("Error executing operation: the requested operation '{path}' is not supported by this server"); - Response::Error(Error { - code: 404, - message: "the requested operation is not supported by this server".to_string(), - data: None, - }) + Request::Abort(id) => { + if let Some(h) = self.subscriptions().remove(&id) { + h.abort(); + } } - }; - - channel.send(resp).ok(); + } } } @@ -215,7 +128,7 @@ where channel: tauri::ipc::Channel, req: Request, ) { - spawn(Self::handle_rpc_impl(self, window, channel, req)); + Self::handle_rpc_impl(self, window, channel, req); } } @@ -236,7 +149,7 @@ fn handle_rpc( } pub fn plugin( - routes: impl Into>, + procedures: impl Into>, ctx_fn: TCtxFn, ) -> TauriPlugin where @@ -244,50 +157,42 @@ where TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, TCtx: Send + Sync + 'static, { - let routes = routes.into(); + let procedures = procedures.into(); Builder::new("rspc") .invoke_handler(generate_handler![handle_rpc]) .setup(move |app_handle, _| { - app_handle.manage(State(Arc::new(RpcHandler::new(routes, ctx_fn)))); + if app_handle.manage(State(Arc::new(RpcHandler { + subscriptions: Default::default(), + ctx_fn, + procedures, + phantom: Default::default(), + }))) { + panic!("Attempted to mount `rspc_tauri::plugin` multiple times. Please ensure you only mount it once!"); + } Ok(()) }) .build() } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Deserialize, Serialize)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] -pub enum Request { - Query { - path: String, - input: Option, - }, - Mutation { - path: String, - input: Option, - }, - Subscription { - path: String, - id: u32, - input: Option, - }, - SubscriptionStop { - id: u32, - }, +enum Request { + /// A request to execute a procedure. + Request { path: String, input: Option }, + /// Abort a running task + /// You must provide the ID of the Tauri channel provided when the task was started. + Abort(u32), } -#[derive(Debug, Clone, Serialize)] +#[derive(Clone, Serialize)] #[serde(tag = "type", content = "data", rename_all = "camelCase")] -pub enum Response { - Event(Value), - Response(Value), - Error(Error), -} - -#[derive(Debug, Clone, Serialize)] -pub struct Error { - pub code: i32, - pub message: String, - pub data: Option, +enum Response { + /// A value being returned from a procedure. + /// Based on the code we can determine if it's an error or not. + Value { code: u16, value: Value }, + /// A procedure has been completed. + /// It's important you avoid calling `Request::Abort { id }` after this as it's up to Tauri what happens. + Done, } From 42b694844f330161d094a0c6145a52e58335e0b6 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 01:45:02 +0800 Subject: [PATCH 28/67] more efficent Tauri integration --- core/src/procedure.rs | 6 +-- core/src/stream.rs | 39 ++++++-------- integrations/axum/src/jsonrpc_exec.rs | 32 +++++++++-- integrations/tauri/src/lib.rs | 78 +++++++++++++-------------- 4 files changed, 81 insertions(+), 74 deletions(-) diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 96084d1c..af776edb 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -1,8 +1,4 @@ -use std::{ - any::{type_name, Any}, - fmt, - sync::Arc, -}; +use std::{any::Any, fmt, sync::Arc}; use serde::Deserializer; diff --git a/core/src/stream.rs b/core/src/stream.rs index c6e1956c..67ee69c3 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -5,12 +5,11 @@ use std::{ task::{Context, Poll}, }; -use erased_serde::Serialize; use futures_core::Stream; use pin_project_lite::pin_project; -use serde::Serializer; +use serde::Serialize; -use crate::{ProcedureError, ResolverError}; +use crate::ResolverError; /// TODO // TODO: Rename this type. @@ -109,26 +108,22 @@ impl ProcedureStream { self.src.size_hint() } + // /// TODO + // pub fn poll_next( + // mut self: Pin<&mut Self>, + // cx: &mut Context<'_>, + // ) -> Poll>> { + // self.src + // .as_mut() + // .poll_next_value(cx) + // .map(move |v| v.map(move |v| v.map(move |_: ()| self.src.value()))) + // } + /// TODO - pub async fn next( - &mut self, - serializer: S, - ) -> Option>> { - let mut serializer = Some(serializer); - - poll_fn(|cx| match self.src.as_mut().poll_next_value(cx) { - Poll::Ready(Some(result)) => Poll::Ready(Some(match result { - Ok(()) => { - let value = self.src.value(); - erased_serde::serialize(value, serializer.take().unwrap()) - .map_err(ProcedureError::Serializer) - } - Err(err) => Err(err.into()), - })), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - }) - .await + pub async fn next(&mut self) -> Option> { + poll_fn(|cx| self.src.as_mut().poll_next_value(cx)) + .await + .map(|v| v.map(|_: ()| self.src.value())) } } diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 2eaa3701..56eee88a 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -1,8 +1,16 @@ -use std::{borrow::Cow, collections::HashMap}; +use std::{ + borrow::Cow, + collections::HashMap, + future::{poll_fn, Future}, +}; -use rspc_core::{ProcedureError, Procedures}; +use rspc_core::{ProcedureError, ProcedureStream, Procedures, ResolverError}; +use serde::Serialize; use serde_json::Value; -use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; +use tokio::{ + pin, + sync::{broadcast, mpsc, oneshot, Mutex}, +}; use super::jsonrpc::{self, RequestId, RequestInner, ResponseInner}; @@ -161,7 +169,7 @@ pub async fn handle_json_rpc( // Size hints can change after the first value is polled based on implementation. let is_value = stream.size_hint() == (1, Some(1)); - let first_value = stream.next(serde_json::value::Serializer).await; + let first_value = next(&mut stream).await; if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { first_value @@ -172,6 +180,7 @@ pub async fn handle_json_rpc( // tracing::error!("Error executing operation: {:?}", err); ResponseInner::Error(match err { + ProcedureError::NotFound => unreachable!(), ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { code: 400, message: "error deserializing procedure arguments".to_string(), @@ -294,7 +303,7 @@ pub async fn handle_json_rpc( // tracing::debug!("Removing subscription with id '{:?}'", id); break; } - v = stream.next(serde_json::value::Serializer) => { + v = next(&mut stream) => { match v { Some(Ok(v)) => { let _ = sender2.send(jsonrpc::Response { @@ -348,3 +357,16 @@ pub async fn handle_json_rpc( // tracing::error!("Failed to send response: {:?}", _err); }); } + +async fn next( + stream: &mut ProcedureStream, +) -> Option>> { + let fut = stream.next(); + let mut fut = std::pin::pin!(fut); + poll_fn(|cx| fut.as_mut().poll(cx)).await.map(|v| { + v.map_err(ProcedureError::from).and_then(|v| { + v.serialize(serde_json::value::Serializer) + .map_err(ProcedureError::Serializer) + }) + }) +} diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 4a8ebbae..58bde244 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -14,10 +14,11 @@ use std::{ use rspc_core::{ProcedureError, Procedures}; use serde::{Deserialize, Serialize}; -use serde_json::{value::Serializer, Value}; +use serde_json::Serializer; use tauri::{ async_runtime::{spawn, JoinHandle}, generate_handler, + ipc::{Channel, InvokeResponseBody}, plugin::{Builder, TauriPlugin}, Manager, }; @@ -44,7 +45,7 @@ where fn handle_rpc_impl( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ) { match req { @@ -52,45 +53,28 @@ where let ctx = (self.ctx_fn)(window); let id = channel.id(); - let send = move |resp: Option>>| { - channel - .send( - resp.ok_or(Response::Done) - .and_then(|v| { - v.map(|value| Response::Value { code: 200, value }).map_err( - |err| Response::Value { - code: err.code(), - value: serde_json::to_value(err).unwrap(), // TODO: Error handling (can we throw it back into Tauri, else we are at an impasse) - }, - ) - }) - .unwrap_or_else(|e| e), - ) - .ok() - }; let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { - send(Some(Err(ProcedureError::NotFound))); - send(None); + let err = ProcedureError::<&mut Serializer>>::NotFound; + send(&channel, Some((err.code(), &err))); + send::<()>(&channel, None); return; }; let mut stream = - procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); + procedure.exec_with_deserializer(ctx, input.unwrap_or(serde_json::Value::Null)); let this = self.clone(); let handle = spawn(async move { - loop { - let value = stream.next(Serializer).await; - let is_finished = value.is_none(); - send(value); - - if is_finished { - break; + while let Some(value) = stream.next().await { + match value { + Ok(v) => send(&channel, Some((200, &v))), + Err(err) => send(&channel, Some((err.status(), &err.value()))), } } this.subscriptions().remove(&id); + send::<()>(&channel, None); }); // if the client uses an existing ID, we will assume the previous subscription is no longer required @@ -111,7 +95,7 @@ trait HandleRpc: Send + Sync { fn handle_rpc( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ); } @@ -125,7 +109,7 @@ where fn handle_rpc( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ) { Self::handle_rpc_impl(self, window, channel, req); @@ -142,7 +126,7 @@ struct State(Arc>); fn handle_rpc( state: tauri::State<'_, State>, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ) { state.0.clone().handle_rpc(window, channel, req); @@ -162,7 +146,7 @@ where Builder::new("rspc") .invoke_handler(generate_handler![handle_rpc]) .setup(move |app_handle, _| { - if app_handle.manage(State(Arc::new(RpcHandler { + if !app_handle.manage(State(Arc::new(RpcHandler { subscriptions: Default::default(), ctx_fn, procedures, @@ -180,19 +164,29 @@ where #[serde(tag = "method", content = "params", rename_all = "camelCase")] enum Request { /// A request to execute a procedure. - Request { path: String, input: Option }, + Request { + path: String, + input: Option, + }, /// Abort a running task /// You must provide the ID of the Tauri channel provided when the task was started. Abort(u32), } -#[derive(Clone, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "camelCase")] -enum Response { - /// A value being returned from a procedure. - /// Based on the code we can determine if it's an error or not. - Value { code: u16, value: Value }, - /// A procedure has been completed. - /// It's important you avoid calling `Request::Abort { id }` after this as it's up to Tauri what happens. - Done, +fn send(channel: &Channel, value: Option<(u16, &T)>) { + #[derive(Serialize)] + struct Response<'a, T: Serialize> { + code: u16, + value: &'a T, + } + + match value { + Some((code, value)) => { + let mut buffer = Vec::with_capacity(128); + let mut serializer = Serializer::new(&mut buffer); + Response { code, value }.serialize(&mut serializer).unwrap(); // TODO: Error handling (throw back to Tauri) + channel.send(InvokeResponseBody::Raw(buffer)).ok() + } + None => channel.send(InvokeResponseBody::Raw("DONE".into())).ok(), + }; } From 76ec52596ead3022a4ef2a49627ea73d3f6838f1 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 01:51:00 +0800 Subject: [PATCH 29/67] more efficent Tauri deserialisation --- integrations/tauri/Cargo.toml | 2 +- integrations/tauri/src/lib.rs | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 9fe7bf38..ba20138b 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -19,7 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"] rspc-core = { version = "0.0.1", path = "../../core" } tauri = "2" serde = { version = "1", features = ["derive"] } # is a dependency of Tauri anyway -serde_json = "1" # is a dependency of Tauri anyway +serde_json = { version = "1", features = ["raw_value"] } # is a dependency of Tauri anyway [lints] workspace = true diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 58bde244..c2dc6bd5 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -14,7 +14,7 @@ use std::{ use rspc_core::{ProcedureError, Procedures}; use serde::{Deserialize, Serialize}; -use serde_json::Serializer; +use serde_json::{value::RawValue, Serializer}; use tauri::{ async_runtime::{spawn, JoinHandle}, generate_handler, @@ -50,9 +50,8 @@ where ) { match req { Request::Request { path, input } => { - let ctx = (self.ctx_fn)(window); - let id = channel.id(); + let ctx = (self.ctx_fn)(window); let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { let err = ProcedureError::<&mut Serializer>>::NotFound; @@ -61,8 +60,10 @@ where return; }; - let mut stream = - procedure.exec_with_deserializer(ctx, input.unwrap_or(serde_json::Value::Null)); + let mut stream = match input { + Some(i) => procedure.exec_with_deserializer(ctx, i), + None => procedure.exec_with_deserializer(ctx, serde_json::Value::Null), + }; let this = self.clone(); let handle = spawn(async move { @@ -162,11 +163,12 @@ where #[derive(Deserialize, Serialize)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] -enum Request { +enum Request<'a> { /// A request to execute a procedure. Request { path: String, - input: Option, + #[serde(borrow)] + input: Option<&'a RawValue>, }, /// Abort a running task /// You must provide the ID of the Tauri channel provided when the task was started. From f4f27feae4202d991b208ba385930076962e5c17 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 02:36:26 +0800 Subject: [PATCH 30/67] init placeholder binario crate --- middleware/binario/Cargo.toml | 16 ++++++++++++++++ middleware/binario/README.md | 3 +++ middleware/binario/src/lib.rs | 7 +++++++ 3 files changed, 26 insertions(+) create mode 100644 middleware/binario/Cargo.toml create mode 100644 middleware/binario/README.md create mode 100644 middleware/binario/src/lib.rs diff --git a/middleware/binario/Cargo.toml b/middleware/binario/Cargo.toml new file mode 100644 index 00000000..8c4734d3 --- /dev/null +++ b/middleware/binario/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rspc-binario" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[dependencies] +rspc = { path = "../../rspc" } + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/binario/README.md b/middleware/binario/README.md new file mode 100644 index 00000000..bd22e9bb --- /dev/null +++ b/middleware/binario/README.md @@ -0,0 +1,3 @@ +# rspc Binario + +Coming soon... diff --git a/middleware/binario/src/lib.rs b/middleware/binario/src/lib.rs new file mode 100644 index 00000000..6f5708dc --- /dev/null +++ b/middleware/binario/src/lib.rs @@ -0,0 +1,7 @@ +//! rspc-binario: Binario support for rspc +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] From 598c85e205f463b04674af1fc9dd2bc4d6d65c0b Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 12:09:44 +0800 Subject: [PATCH 31/67] cleanup and generalise `ProcedureStream` + break middleware --- core/src/dyn_input.rs | 2 +- core/src/error.rs | 7 +- core/src/lib.rs | 63 +--- core/src/procedure.rs | 12 +- core/src/stream.rs | 332 +++---------------- examples/axum/src/main.rs | 64 ++-- integrations/axum/src/jsonrpc_exec.rs | 15 +- middleware/devtools/Cargo.toml | 1 + middleware/devtools/src/lib.rs | 14 +- rspc/src/legacy/interop.rs | 54 +-- rspc/src/modern/error.rs | 8 +- rspc/src/modern/infallible.rs | 2 +- rspc/src/modern/middleware/middleware.rs | 13 +- rspc/src/modern/middleware/next.rs | 10 +- rspc/src/modern/procedure/builder.rs | 8 +- rspc/src/modern/procedure/resolver_output.rs | 101 +++--- rspc/src/modern/state.rs | 14 +- rspc/src/procedure.rs | 34 +- 18 files changed, 218 insertions(+), 536 deletions(-) diff --git a/core/src/dyn_input.rs b/core/src/dyn_input.rs index 51d06790..82388c18 100644 --- a/core/src/dyn_input.rs +++ b/core/src/dyn_input.rs @@ -19,7 +19,7 @@ enum DynInputInner<'a, 'de> { } impl<'a, 'de> DynInput<'a, 'de> { - pub fn new_value(value: &'a mut Option) -> Self { + pub fn new_value(value: &'a mut Option) -> Self { Self { inner: DynInputInner::Value(value), type_name: type_name::(), diff --git a/core/src/error.rs b/core/src/error.rs index 5a734ea5..990d3c70 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -130,11 +130,16 @@ impl From for ResolverError { pub struct ResolverError(Repr); impl ResolverError { + // Warning: Returning > 400 will fallback to `500`. As redirects would be invalid and `200` would break matching. pub fn new( - status: u16, + mut status: u16, value: T, source: Option, ) -> Self { + if status < 400 { + status = 500; + } + Self(Repr::Custom { status, value: Box::new(ErrorInternal { value, err: source }), diff --git a/core/src/lib.rs b/core/src/lib.rs index d1427bf5..90bf207a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -5,6 +5,8 @@ //! TODO: Why this crate doesn't depend on Specta. //! TODO: Discuss the traits that need to be layered on for this to be useful. //! TODO: Discuss how middleware don't exist here. +//! +//! TODO: A fundamental flaw of our current architecture is that results must be `'static` (hence can't serialize in-place). This is hard to solve due to `async fn`'s internals being sealed. #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( @@ -12,17 +14,6 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] -// - Returning non-Serialize types (Eg. `File`) via `ProcedureStream`. -// -// - Rename `DynInput` to `DynValue` maybe??? -// - `ProcedureStream` to `impl futures::Stream` adapter. -// - `ProcedureStream::poll_next` - Keep or remove??? -// - `Send` + `Sync` and the issues with single-threaded async runtimes -// - `DynInput<'a, 'de>` should really be &'a Input<'de>` but that's hard. -// - Finish `Debug` impls -// - Should `Procedure2::error` being `Option` or not? -// - Crate documentation - mod dyn_input; mod error; mod interop; @@ -37,53 +28,3 @@ pub use stream::ProcedureStream; pub type Procedures = std::collections::HashMap, Procedure>; - -// TODO: The naming is horid. -// Low-level concerns: -// - `Procedure` - Holds the handler (and probably type information) -// - `Procedure::new(|ctx, input| { ... })` -// - `Procedure::exec_with_deserializer(ctx, input)` -// - `Procedure::exec_with_value(ctx, input)` -// -// - `Input` (prev. `ProcedureExecInput`) - Holds a mutable reference to either a value or a deserializer. -// - Used within function given to `Procedure::new`. -// - Eg. `|ctx, input| input.deserialize::().unwrap()` or -// `|ctx, input| input.value::().unwrap()` -// -// - This type exists so we can slap the generic methods on the dyn-safe types it wraps. -// -// - `Output` - What the resolver function can return. -// - Basically the same as `Input` but reverse direction. -// -// - `ResolverOutput` - What `exec` can return. Either a `Serializer` or `T` (Eg. `File`). -// - Basically the same as `ProcedureInput` but reverse direction. -// -// - `Stream`/`ProcedureType` - Self explanatory. -// -// High-level concerns: -// - `ProcedureBuilder` - The thing that converts the high-level resolver fn to the low-level one. -// - This is where middleware are introduced. The low-level system doesn't know about them. -// -// - `ResolverInput` - A resolver takes `TInput`. This trait allows us to go `rspc_core::Input` to `TInput`. -// - Is implemented for `T: DeserializeOwned` and any custom type (Eg. `File`). -// -// - It would be nice if this could replace by `Input::from_value` and `Input::from_deserializer`. -// However, due to `erased_serde` this is basically impossible. -// -// - `ProcedureInput` - `exec` takes a `T`. This trait allows us to go `T` to `rspc_core::Input`. -// - Is implemented for `T: Deserializer<'de>` and any custom type (Eg. `File`). -// - This would effectively dispatch to `exec_with_deserializer` or `exec_with_value`. -// -// - We could avoid this method an eat the DX (2 methods) and -// loss of typesafety (any `'static` can be parsed in even if missing `Argument1` impl). -// -// - Kinda has to end up in `rspc_core` so it can be used by `rspc_axum` for `File`. -// -// Notes: -// - Due to how erased_serde works `input.deserialize::()` works with `#[serde(borrow)]`. -// However, given we can't `impl for<'a> Fn(_, impl Argument<'a>)` it won't work for the high-level API. -// - -// A decent cause of the bloat is because `T` (Eg. `File`), `Deserializer` and `Deserialize` are all different. You end up with a `ResolverInput` trait which is `Deserialize` + `T` , a `ProcedureInput` trait which is `Deserializer` + `T` and then `ExecInput` which is the dyn-safe output of `ProcedureInput` and is given into `ResolverInput` so it can decode it back to the value the user expects. Then you basically copy the same thing for the output value. I think it might be worth replacing `ProcedureInput` with `Procedure::exec_with_deserializer` and `Procedure::exec_with_value` but i'm not sure we could get away with doing the same thing for `ResolverInput` because that would mean requiring two forms of queries/mutations/subscriptions in the high-level API. That being said `ResolverInput` could probably be broken out of the procedure primitive. - -// TODO: The new system doesn't allocate Serde related `Error`'s and Serde return values, pog. diff --git a/core/src/procedure.rs b/core/src/procedure.rs index af776edb..29d0d31f 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -1,4 +1,4 @@ -use std::{any::Any, fmt, sync::Arc}; +use std::{fmt, sync::Arc}; use serde::Deserializer; @@ -22,6 +22,10 @@ impl Procedure { } } + pub fn exec(&self, ctx: TCtx, input: DynInput) -> ProcedureStream { + (self.handler)(ctx, input) + } + pub fn exec_with_deserializer<'de, D: Deserializer<'de> + Send>( &self, ctx: TCtx, @@ -33,16 +37,12 @@ impl Procedure { (self.handler)(ctx, value) } - pub fn exec_with_value(&self, ctx: TCtx, input: T) -> ProcedureStream { + pub fn exec_with_value(&self, ctx: TCtx, input: T) -> ProcedureStream { let mut input = Some(input); let value = DynInput::new_value(&mut input); (self.handler)(ctx, value) } - - pub fn exec_with_dyn_input(&self, ctx: TCtx, input: DynInput) -> ProcedureStream { - (self.handler)(ctx, input) - } } impl Clone for Procedure { diff --git a/core/src/stream.rs b/core/src/stream.rs index 67ee69c3..ecfed6df 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -1,6 +1,6 @@ use core::fmt; use std::{ - future::{poll_fn, Future}, + future::poll_fn, pin::Pin, task::{Context, Poll}, }; @@ -12,118 +12,54 @@ use serde::Serialize; use crate::ResolverError; /// TODO -// TODO: Rename this type. -pub struct ProcedureStream { - src: Pin>, -} +#[must_use = "ProcedureStream does nothing unless polled"] +pub struct ProcedureStream(Pin>); impl ProcedureStream { /// TODO - pub fn from_value(value: Result) -> Self - where - T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! - { - Self { - src: Box::pin(DynReturnValueFutureCompat { - // TODO: Should we do this in a more efficient way??? - src: std::future::ready(value), - value: None, - done: false, - }), - } - } - - /// TODO - pub fn from_future(src: S) -> Self - where - S: Future> + Send + 'static, - T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! - { - Self { - src: Box::pin(DynReturnValueFutureCompat { - src, - value: None, - done: false, - }), - } - } - - /// TODO - pub fn from_stream(src: S) -> Self + pub fn from_stream(s: S) -> Self where S: Stream> + Send + 'static, - T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! + T: Serialize + Send + Sync + 'static, { - Self { - src: Box::pin(DynReturnValueStreamCompat { src, value: None }), - } + Self(Box::pin(DynReturnImpl { + src: s, + value: None, + })) } - // TODO: I'm not sure if we should keep this or not? - // The crate `futures`'s flatten stuff doesn't handle it how we need it so maybe we could patch that instead of having this special case??? - // This is a special case because we need to ensure the `size_hint` is correct. /// TODO - pub fn from_future_stream(src: F) -> Self + pub fn from_stream_value(s: S) -> Self where - F: Future> + Send + 'static, S: Stream> + Send + 'static, - T: Serialize + Send + 'static, // TODO: Drop `Serialize`!!! + T: Send + Sync + 'static, { - Self { - src: Box::pin(DynReturnValueStreamFutureCompat::Future { src }), - } + Self(todo!()) } - // TODO: Rename and replace `Self::from_future_stream`??? - // TODO: I'm not sure if we should keep this or not? - // The crate `futures`'s flatten stuff doesn't handle it how we need it so maybe we could patch that instead of having this special case??? - // This is a special case because we need to ensure the `size_hint` is correct. /// TODO - pub fn from_future_procedure_stream(src: F) -> Self - where - F: Future> + Send + 'static, - { - Self { - src: Box::pin(DynReturnValueFutureProcedureStreamCompat::Future { src }), - } + pub fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() } - // /// TODO - // /// - // /// TODO: This method doesn't allow reusing the serializer between polls. Maybe remove it??? - // pub fn poll_next( - // &mut self, - // cx: &mut Context<'_>, - // serializer: S, - // ) -> Poll> { - // let mut serializer = &mut ::erase(serializer); - - // self.src.as_mut().poll_next_value(cx) - // } - - // TODO: Fn to get syncronous value??? - /// TODO - pub fn size_hint(&self) -> (usize, Option) { - self.src.size_hint() + pub fn poll_next( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + self.0 + .as_mut() + .poll_next_value(cx) + .map(|v| v.map(|v| v.map(|_: ()| self.0.value()))) } - // /// TODO - // pub fn poll_next( - // mut self: Pin<&mut Self>, - // cx: &mut Context<'_>, - // ) -> Poll>> { - // self.src - // .as_mut() - // .poll_next_value(cx) - // .map(move |v| v.map(move |v| v.map(move |_: ()| self.src.value()))) - // } - /// TODO - pub async fn next(&mut self) -> Option> { - poll_fn(|cx| self.src.as_mut().poll_next_value(cx)) + pub async fn next( + &mut self, + ) -> Option> { + poll_fn(|cx| self.0.as_mut().poll_next_value(cx)) .await - .map(|v| v.map(|_: ()| self.src.value())) + .map(|v| v.map(|_: ()| self.0.value())) } } @@ -139,97 +75,41 @@ trait DynReturnValue: Send { cx: &mut Context<'_>, ) -> Poll>>; - fn value(&self) -> &dyn erased_serde::Serialize; + fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync); fn size_hint(&self) -> (usize, Option); } pin_project! { - struct DynReturnValueFutureCompat{ + struct DynReturnImpl{ #[pin] src: S, value: Option, - done: bool, } } -impl> + Send> DynReturnValue - for DynReturnValueFutureCompat +impl> + Send + 'static> DynReturnValue + for DynReturnImpl where - T: Serialize, // TODO: Drop this bound!!! + T: Send + Sync + Serialize, { - // TODO: Cleanup this impl's pattern matching. fn poll_next_value<'a>( mut self: Pin<&'a mut Self>, cx: &mut Context<'_>, ) -> Poll>> { let this = self.as_mut().project(); let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. - match this.src.poll(cx) { - Poll::Ready(value) => { - *this.done = true; - Poll::Ready(Some(match value { - Ok(value) => { - *this.value = Some(value); - - Ok(()) - } - Err(err) => Err(err), - })) - } - Poll::Pending => return Poll::Pending, - } + this.src.poll_next(cx).map(|v| { + v.map(|v| { + v.map(|v| { + *this.value = Some(v); + () + }) + }) + }) } - fn value(&self) -> &dyn erased_serde::Serialize { - self.value - .as_ref() - // Attempted to access value when `Poll::Ready(None)` was not returned. - .expect("unreachable") - } - - fn size_hint(&self) -> (usize, Option) { - if self.done { - return (0, Some(0)); - } - (1, Some(1)) - } -} - -pin_project! { - struct DynReturnValueStreamCompat{ - #[pin] - src: S, - value: Option, - } -} - -impl> + Send> DynReturnValue - for DynReturnValueStreamCompat -where - T: Serialize, // TODO: Drop this bound!!! -{ - // TODO: Cleanup this impl's pattern matching. - fn poll_next_value<'a>( - mut self: Pin<&'a mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - let this = self.as_mut().project(); - let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. - match this.src.poll_next(cx) { - Poll::Ready(Some(value)) => Poll::Ready(Some(match value { - Ok(value) => { - *this.value = Some(value); - Ok(()) - } - Err(err) => Err(err), - })), - Poll::Ready(None) => return Poll::Ready(None), - Poll::Pending => return Poll::Pending, - } - } - - fn value(&self) -> &dyn erased_serde::Serialize { + fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync) { self.value .as_ref() // Attempted to access value when `Poll::Ready(None)` was not returned. @@ -240,133 +120,3 @@ where self.src.size_hint() } } - -pin_project! { - #[project = DynReturnValueStreamFutureCompatProj] - enum DynReturnValueStreamFutureCompat { - Future { - #[pin] src: F, - }, - Stream { - #[pin] src: S, - value: Option, - } - } -} - -impl DynReturnValue for DynReturnValueStreamFutureCompat -where - T: Serialize + Send, // TODO: Drop `Serialize` bound!!! - F: Future> + Send + 'static, - S: Stream> + Send, -{ - // TODO: Cleanup this impl's pattern matching. - fn poll_next_value<'a>( - mut self: Pin<&'a mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - loop { - return match self.as_mut().project() { - DynReturnValueStreamFutureCompatProj::Future { src } => match src.poll(cx) { - Poll::Ready(Ok(result)) => { - self.as_mut().set(DynReturnValueStreamFutureCompat::Stream { - src: result, - value: None, - }); - continue; - } - Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))), - Poll::Pending => return Poll::Pending, - }, - DynReturnValueStreamFutureCompatProj::Stream { src, value } => { - let _ = value.take(); // Reset value to ensure `take` being misused causes it to panic. - match src.poll_next(cx) { - Poll::Ready(Some(v)) => Poll::Ready(Some(match v { - Ok(v) => { - *value = Some(v); - Ok(()) - } - Err(err) => Err(err), - })), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } - }; - } - } - - fn value(&self) -> &dyn erased_serde::Serialize { - match self { - // Attempted to acces value before first `Poll::Ready` was returned. - Self::Future { .. } => panic!("unreachable"), - Self::Stream { value, .. } => value - .as_ref() - // Attempted to access value when `Poll::Ready(None)` was not returned. - .expect("unreachable"), - } - } - - fn size_hint(&self) -> (usize, Option) { - match self { - Self::Future { .. } => (0, None), - Self::Stream { src, .. } => src.size_hint(), - } - } -} - -pin_project! { - #[project = DynReturnValueFutureProcedureStreamCompatProj] - enum DynReturnValueFutureProcedureStreamCompat { - Future { - #[pin] src: F, - }, - Inner { - src: ProcedureStream, - } - } -} - -impl DynReturnValue for DynReturnValueFutureProcedureStreamCompat -where - F: Future> + Send + 'static, -{ - // TODO: Cleanup this impl's pattern matching. - fn poll_next_value<'a>( - mut self: Pin<&'a mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - loop { - return match self.as_mut().project() { - DynReturnValueFutureProcedureStreamCompatProj::Future { src } => match src.poll(cx) - { - Poll::Ready(Ok(result)) => { - self.as_mut() - .set(DynReturnValueFutureProcedureStreamCompat::Inner { src: result }); - continue; - } - Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))), - Poll::Pending => return Poll::Pending, - }, - DynReturnValueFutureProcedureStreamCompatProj::Inner { src } => { - src.src.as_mut().poll_next_value(cx) - } - }; - } - } - - fn value(&self) -> &dyn erased_serde::Serialize { - match self { - // Attempted to acces value before first `Poll::Ready` was returned. - Self::Future { .. } => panic!("unreachable"), - Self::Inner { src } => src.src.value(), - } - } - - fn size_hint(&self) -> (usize, Option) { - match self { - Self::Future { .. } => (0, None), - Self::Inner { src } => src.src.size_hint(), - } - } -} diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index fbfad31d..d485e0df 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -99,7 +99,11 @@ pub enum Error { Mistake(String), } -impl Error2 for Error {} +impl Error2 for Error { + fn into_resolver_error(self) -> rspc::ResolverError { + rspc::ResolverError::new(500, self.to_string(), None::) + } +} pub struct BaseProcedure(PhantomData); impl BaseProcedure { @@ -121,12 +125,12 @@ fn test_unstable_stuff(router: Router2) -> Router2 { .procedure("newstuff2", { ::builder() // .with(invalidation(|ctx: Ctx, key, event| false)) - .with(Middleware::new( - move |ctx: Ctx, input: (), next| async move { - let result = next.exec(ctx, input).await; - result - }, - )) + // .with(Middleware::new( + // move |ctx: Ctx, input: (), next| async move { + // let result = next.exec(ctx, input).await; + // result + // }, + // )) .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) }) } @@ -136,29 +140,29 @@ pub enum InvalidateEvent { InvalidateKey(String), } -fn invalidation( - handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, -) -> Middleware -where - TError: Send + 'static, - TCtx: Clone + Send + 'static, - TInput: Clone + Send + 'static, - TResult: Send + 'static, -{ - let handler = Arc::new(handler); - Middleware::new(move |ctx: TCtx, input: TInput, next| { - let handler = handler.clone(); - async move { - // TODO: Register this with `TCtx` - let ctx2 = ctx.clone(); - let input2 = input.clone(); - let result = next.exec(ctx, input).await; - - // TODO: Unregister this with `TCtx` - result - } - }) -} +// fn invalidation( +// handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, +// ) -> Middleware +// where +// TError: Send + 'static, +// TCtx: Clone + Send + 'static, +// TInput: Clone + Send + 'static, +// TResult: Send + 'static, +// { +// let handler = Arc::new(handler); +// Middleware::new(move |ctx: TCtx, input: TInput, next| { +// let handler = handler.clone(); +// async move { +// // TODO: Register this with `TCtx` +// let ctx2 = ctx.clone(); +// let input2 = input.clone(); +// let result = next.exec(ctx, input); + +// // TODO: Unregister this with `TCtx` +// result +// } +// }) +// } #[tokio::main] async fn main() { diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 56eee88a..e51d0532 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -151,10 +151,10 @@ pub async fn handle_json_rpc( }); } - let (path, input, sub_id) = match req.inner { - RequestInner::Query { path, input } => (path, input, None), - RequestInner::Mutation { path, input } => (path, input, None), - RequestInner::Subscription { path, input } => (path, input.1, Some(input.0)), + let (path, input, sub_id, is_subscription) = match req.inner { + RequestInner::Query { path, input } => (path, input, None, false), + RequestInner::Mutation { path, input } => (path, input, None, false), + RequestInner::Subscription { path, input } => (path, input.1, Some(input.0), true), RequestInner::SubscriptionStop { input } => { subscriptions.remove(&input).await; return; @@ -164,14 +164,9 @@ pub async fn handle_json_rpc( let result = match routes.get(&Cow::Borrowed(&*path)) { Some(procedure) => { let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); - - // It's really important this is before getting the first value - // Size hints can change after the first value is polled based on implementation. - let is_value = stream.size_hint() == (1, Some(1)); - let first_value = next(&mut stream).await; - if (is_value || stream.size_hint() == (0, Some(0))) && first_value.is_some() { + if !is_subscription { first_value .expect("checked at if above") .map(ResponseInner::Response) diff --git a/middleware/devtools/Cargo.toml b/middleware/devtools/Cargo.toml index 95d9fb3b..e3368691 100644 --- a/middleware/devtools/Cargo.toml +++ b/middleware/devtools/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" publish = false [dependencies] +futures = "0.3.31" rspc-core = { path = "../../core" } serde = { version = "1.0.215", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = ["derive"] } diff --git a/middleware/devtools/src/lib.rs b/middleware/devtools/src/lib.rs index b32075cd..50e675f8 100644 --- a/middleware/devtools/src/lib.rs +++ b/middleware/devtools/src/lib.rs @@ -13,9 +13,11 @@ mod types; use std::{ any::Any, + future, sync::{Arc, Mutex, PoisonError}, }; +use futures::stream; use rspc_core::{Procedure, ProcedureStream, Procedures}; use types::{Metadata, ProcedureMetadata}; @@ -44,7 +46,7 @@ pub fn mount( name.clone(), Procedure::new(move |ctx, input| { let start = std::time::Instant::now(); - let result = procedure.exec_with_dyn_input(ctx, input); + let result = procedure.exec(ctx, input); history .lock() .unwrap_or_else(PoisonError::into_inner) @@ -57,17 +59,21 @@ pub fn mount( procedures.insert( "~rspc.devtools.meta".into(), - Procedure::new(move |ctx, input| ProcedureStream::from_value(Ok(meta.clone()))), + Procedure::new(move |ctx, input| { + let value = Ok(meta.clone()); + ProcedureStream::from_stream(stream::once(future::ready(value))) + }), ); procedures.insert( "~rspc.devtools.history".into(), Procedure::new({ let history = history.clone(); move |ctx, input| { - ProcedureStream::from_value(Ok(history + let value = Ok(history .lock() .unwrap_or_else(PoisonError::into_inner) - .clone())) + .clone()); + ProcedureStream::from_stream(stream::once(future::ready(value))) } }), ); diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy/interop.rs index 82cc7024..2ddcf038 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::BTreeMap, panic::Location}; -use futures::{stream, StreamExt, TryStreamExt}; +use futures::{stream, FutureExt, StreamExt, TryStreamExt}; use rspc_core::{ProcedureStream, ResolverError}; use serde_json::Value; use specta::{ @@ -103,31 +103,39 @@ pub(crate) fn layer_to_procedure( }); match result { - Ok(result) => ProcedureStream::from_future_stream(async move { - match result.into_value_or_stream().await { - Ok(ValueOrStream::Value(value)) => { - Ok(stream::once(async { Ok(value) }).boxed()) - } - Ok(ValueOrStream::Stream(s)) => Ok(s - .map_err(|err| { - let err = crate::legacy::Error::from(err); - ResolverError::new( + Ok(result) => ProcedureStream::from_stream( + async move { + match result.into_value_or_stream().await { + Ok(ValueOrStream::Value(value)) => { + stream::once(async { Ok(value) }).boxed() + } + Ok(ValueOrStream::Stream(s)) => s + .map_err(|err| { + let err = crate::legacy::Error::from(err); + ResolverError::new( + err.code.to_status_code(), + (), /* typesafe errors aren't supported in legacy router */ + Some(rspc_core::LegacyErrorInterop(err.message)), + ) + }) + .boxed(), + Err(err) => { + let err: crate::legacy::Error = err.into(); + let err = ResolverError::new( err.code.to_status_code(), - (), /* typesafe errors aren't supported in legacy router */ - Some(rspc_core::LegacyErrorInterop(err.message)), - ) - }) - .boxed()), - Err(err) => { - let err: crate::legacy::Error = err.into(); - let err = - ResolverError::new(err.code.to_status_code(), err.message, err.cause); - // stream::once(async { Err(err) }).boxed() - Err(err) + err.message, + err.cause, + ); + stream::once(async { Err(err) }).boxed() + } } } - }), - Err(err) => ProcedureStream::from_value(Err::<(), _>(err)), + .into_stream() + .flatten(), + ), + Err(err) => { + ProcedureStream::from_stream(stream::once(async move { Err::<(), _>(err) })) + } } }) } diff --git a/rspc/src/modern/error.rs b/rspc/src/modern/error.rs index 98ef9c43..b4a29863 100644 --- a/rspc/src/modern/error.rs +++ b/rspc/src/modern/error.rs @@ -1,13 +1,9 @@ use std::error; +use rspc_core::ResolverError; use serde::Serialize; use specta::Type; pub trait Error: error::Error + Send + Serialize + Type + 'static { - // Warning: Returning > 400 will fallback to `500`. As redirects would be invalid and `200` would break matching. - fn status(&self) -> u16 { - 500 - } + fn into_resolver_error(self) -> ResolverError; } - -// impl Error for rspc_core:: {} diff --git a/rspc/src/modern/infallible.rs b/rspc/src/modern/infallible.rs index ffe943fc..7a2aa5f7 100644 --- a/rspc/src/modern/infallible.rs +++ b/rspc/src/modern/infallible.rs @@ -24,7 +24,7 @@ impl Serialize for Infallible { impl std::error::Error for Infallible {} impl crate::modern::Error for Infallible { - fn status(&self) -> u16 { + fn into_resolver_error(self) -> rspc_core::ResolverError { unreachable!() } } diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index 7b00b281..3c14dc2f 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -21,18 +21,18 @@ use std::{pin::Pin, sync::Arc}; -use futures::Future; +use futures::{Future, FutureExt, Stream}; use crate::modern::{procedure::ProcedureMeta, State}; use super::Next; -pub(crate) type MiddlewareHandler = Box< +pub(crate) type MiddlewareHandler = Arc< dyn Fn( TNextCtx, TNextInput, ProcedureMeta, - ) -> Pin> + Send + 'static>> + ) -> Pin> + Send + 'static>> + Send + Sync + 'static, @@ -114,10 +114,7 @@ where Self { setup: None, inner: Box::new(move |next| { - // TODO: Don't `Arc>` - let next = Arc::new(next); - - Box::new(move |ctx, input, meta| { + Arc::new(move |ctx, input, meta| { let f = func( ctx, input, @@ -127,7 +124,7 @@ where }, ); - Box::pin(f) + Box::pin(f.into_stream()) }) }), } diff --git a/rspc/src/modern/middleware/next.rs b/rspc/src/modern/middleware/next.rs index e9536bb4..cf8be741 100644 --- a/rspc/src/modern/middleware/next.rs +++ b/rspc/src/modern/middleware/next.rs @@ -1,11 +1,13 @@ -use std::{fmt, sync::Arc}; +use std::fmt; + +use futures::Stream; use crate::modern::{middleware::middleware::MiddlewareHandler, procedure::ProcedureMeta}; pub struct Next { // TODO: `pub(super)` over `pub(crate)` pub(crate) meta: ProcedureMeta, - pub(crate) next: Arc>, + pub(crate) next: MiddlewareHandler, } impl fmt::Debug for Next { @@ -24,7 +26,7 @@ where self.meta.clone() } - pub async fn exec(&self, ctx: TCtx, input: TInput) -> Result { - (self.next)(ctx, input, self.meta.clone()).await + pub fn exec(&self, ctx: TCtx, input: TInput) -> impl Stream> { + (self.next)(ctx, input, self.meta.clone()) } } diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs index fc57b682..c3d96315 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/modern/procedure/builder.rs @@ -1,4 +1,4 @@ -use std::{fmt, future::Future}; +use std::{fmt, future::Future, sync::Arc}; use crate::{ modern::{ @@ -10,6 +10,8 @@ use crate::{ use super::{ProcedureKind, ProcedureMeta}; +use futures::{FutureExt, StreamExt}; + // TODO: Document the generics like `Middleware` pub struct ProcedureBuilder { pub(crate) build: Box< @@ -74,7 +76,7 @@ where (self.build)( ProcedureKind::Query, Vec::new(), - Box::new(move |ctx, input, _| Box::pin(handler(ctx, input))), + Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input).into_stream())), ) } @@ -85,7 +87,7 @@ where (self.build)( ProcedureKind::Mutation, Vec::new(), - Box::new(move |ctx, input, _| Box::pin(handler(ctx, input))), + Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input).into_stream())), ) } } diff --git a/rspc/src/modern/procedure/resolver_output.rs b/rspc/src/modern/procedure/resolver_output.rs index 6dc00d5a..06244c38 100644 --- a/rspc/src/modern/procedure/resolver_output.rs +++ b/rspc/src/modern/procedure/resolver_output.rs @@ -29,8 +29,8 @@ // // note = "ResolverOutput requires a `T where T: serde::Serialize + specta::Type + 'static` to be returned from your procedure" // // )] -use futures::Stream; -use rspc_core::ProcedureStream; +use futures::{Stream, TryStreamExt}; +use rspc_core::{ProcedureStream, ResolverError}; use serde::Serialize; use specta::{datatype::DataType, Generics, Type, TypeCollection}; @@ -40,90 +40,67 @@ use crate::modern::Error; /// TODO: bring back any correct parts of the docs above pub trait ResolverOutput: Sized + Send + 'static { - // /// Convert the procedure and any async part of the value into a [`ProcedureStream`]. - // /// - // /// This primarily exists so the [`rspc::Stream`](crate::Stream) implementation can merge it's stream into the procedure stream. - // fn into_procedure_stream( - // procedure: impl Stream> + Send + 'static, - // ) -> ProcedureStream - // where - // TError: Error, - // { - // ProcedureStream::from_stream(procedure.map(|v| v?.into_procedure_result())) - // } - - // /// Convert the value from the user into a [`ProcedureOutput`]. - // fn into_procedure_result(self) -> Result; + // TODO: This won't allow us to return upcast/downcastable stuff + type T; // : Serialize + Send + Sync + 'static; // TODO: Be an associated type instead so we can constrain later for better errors???? fn data_type(types: &mut TypeCollection) -> DataType; - fn into_procedure_stream(self) -> ProcedureStream; + /// Convert the procedure into a [`Stream`]. + fn into_stream(self) -> impl Stream> + Send + 'static; + + /// Convert the stream into a [`ProcedureStream`]. + fn into_procedure_stream( + stream: impl Stream> + Send + 'static, + ) -> ProcedureStream; } -// TODO: Should this be `Result`? -impl ResolverOutput for T +impl ResolverOutput for T where - T: Serialize + Type + Send + 'static, - TError: Error, + T: Serialize + Type + Send + Sync + 'static, + E: Error, { + type T = T; + fn data_type(types: &mut TypeCollection) -> DataType { T::inline(types, Generics::Definition) } - fn into_procedure_stream(self) -> ProcedureStream { - ProcedureStream::from_value(Ok(self)) + fn into_stream(self) -> impl Stream> + Send + 'static { + futures::stream::once(async move { Ok(self) }) + } + + fn into_procedure_stream( + stream: impl Stream> + Send + 'static, + ) -> ProcedureStream { + ProcedureStream::from_stream(stream) } } impl ResolverOutput for crate::modern::Stream where - TErr: Send, + TErr: Error, S: Stream> + Send + 'static, T: ResolverOutput, + // Should prevent nesting `Stream`s + T::T: Serialize + Send + Sync + 'static, { + type T = T::T; + fn data_type(types: &mut TypeCollection) -> DataType { T::data_type(types) // TODO: Do we need to do anything special here so the frontend knows this is a stream? } - fn into_procedure_stream(self) -> ProcedureStream { - // ProcedureStream::from_value(Ok(self)) - - // ProcedureStream::from_stream( - // self.0 - // .map(|v| match v { - // Ok(s) => { - // s.0.map(|v| v.and_then(|v| v.into_procedure_result())) - // .right_stream() - // } - // Err(err) => once(async move { Err(err) }).left_stream(), - // }) - // .flatten(), - // ) - - todo!(); + fn into_stream(self) -> impl Stream> + Send + 'static { + self.0 + .map_ok(|v| v.into_stream()) + .map_err(|err| err.into_resolver_error()) + .try_flatten() } - // fn into_procedure_stream( - // procedure: impl Stream> + Send + 'static, - // ) -> ProcedureStream - // where - // TErr: Error, - // { - // ProcedureStream::from_stream( - // procedure - // .map(|v| match v { - // Ok(s) => { - // s.0.map(|v| v.and_then(|v| v.into_procedure_result())) - // .right_stream() - // } - // Err(err) => once(async move { Err(err) }).left_stream(), - // }) - // .flatten(), - // ) - // } - - // fn into_procedure_result(self) -> Result { - // panic!("returning nested rspc::Stream's is not currently supported.") - // } + fn into_procedure_stream( + stream: impl Stream> + Send + 'static, + ) -> ProcedureStream { + ProcedureStream::from_stream(stream) + } } diff --git a/rspc/src/modern/state.rs b/rspc/src/modern/state.rs index 52f0602a..e08775b3 100644 --- a/rspc/src/modern/state.rs +++ b/rspc/src/modern/state.rs @@ -45,21 +45,21 @@ impl Default for State { } impl State { - pub fn get(&self) -> Option<&T> { + pub fn get(&self) -> Option<&T> { self.0.get(&TypeId::of::()).map(|v| { v.downcast_ref::() .expect("unreachable: TypeId matches but downcast failed") }) } - pub fn get_mut(&self) -> Option<&T> { + pub fn get_mut(&self) -> Option<&T> { self.0.get(&TypeId::of::()).map(|v| { v.downcast_ref::() .expect("unreachable: TypeId matches but downcast failed") }) } - pub fn get_or_init(&mut self, init: impl FnOnce() -> T) -> &T { + pub fn get_or_init(&mut self, init: impl FnOnce() -> T) -> &T { self.0 .entry(TypeId::of::()) .or_insert_with(|| Box::new(init())) @@ -67,7 +67,7 @@ impl State { .expect("unreachable: TypeId matches but downcast failed") } - pub fn get_mut_or_init( + pub fn get_mut_or_init( &mut self, init: impl FnOnce() -> T, ) -> &mut T { @@ -78,15 +78,15 @@ impl State { .expect("unreachable: TypeId matches but downcast failed") } - pub fn contains_key(&self) -> bool { + pub fn contains_key(&self) -> bool { self.0.contains_key(&TypeId::of::()) } - pub fn insert(&mut self, t: T) { + pub fn insert(&mut self, t: T) { self.0.insert(TypeId::of::(), Box::new(t)); } - pub fn remove(&mut self) -> Option { + pub fn remove(&mut self) -> Option { self.0.remove(&TypeId::of::()).map(|v| { *v.downcast::() .expect("unreachable: TypeId matches but downcast failed") diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index cf3cef33..511f2f05 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,6 +1,7 @@ -use std::{borrow::Cow, panic::Location, sync::Arc}; +use std::{borrow::Cow, panic::Location}; -use rspc_core::{Procedure, ProcedureStream}; +use futures::TryStreamExt; +use rspc_core::Procedure; use specta::datatype::DataType; use crate::{ @@ -44,11 +45,10 @@ impl Procedure2 { I: ResolverInput, R: ResolverOutput, { + use futures::Stream; + ProcedureBuilder { build: Box::new(|kind, setups, handler| { - // TODO: Don't be `Arc>` just `Arc<_>` - let handler = Arc::new(handler); - Procedure2 { setup: Default::default(), ty: ProcedureType { @@ -95,24 +95,22 @@ impl Procedure2 { // Ok(R::into_procedure_stream(fut.into_stream())) - // ProcedureStream::from_value(Ok("todo")) // TODO - // // TODO: borrow into procedure let key: Cow<'static, str> = "todo".to_string().into(); // TODO: Work this out properly let meta = ProcedureMeta::new(key.clone(), kind); // TODO: END - let fut = handler( - ctx, - I::from_input(input).unwrap(), // TODO: Error handling - meta.clone(), - ); - - ProcedureStream::from_future_procedure_stream(async move { - Ok(R::into_procedure_stream(fut.await.unwrap())) // TODO: Error handling - - // Ok(futures::stream::once(async move { Ok("todo") })) - }) + R::into_procedure_stream( + handler( + ctx, + I::from_input(input).unwrap(), // TODO: Error handling + meta.clone(), + ) + .map_ok(|v| v.into_stream()) + .map_err(|err| err.into_resolver_error()) + .try_flatten() + .into_stream(), + ) }), } }), From 21f72122aab8086c9e8c6890271a0c187608f4fc Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 12:25:10 +0800 Subject: [PATCH 32/67] fix middleware api --- examples/axum/src/main.rs | 61 +++++++++++------------- rspc/src/modern/middleware/middleware.rs | 4 +- rspc/src/modern/middleware/next.rs | 4 +- rspc/src/modern/procedure/builder.rs | 4 +- rspc/src/modern/stream.rs | 20 ++++++++ rspc/src/procedure.rs | 3 +- 6 files changed, 57 insertions(+), 39 deletions(-) diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index d485e0df..0aa0d239 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -3,8 +3,8 @@ use std::{marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; use async_stream::stream; use axum::{http::request::Parts, routing::get}; use rspc::{ - middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, - Router2, + middleware::Middleware, Error2, Infallible, Procedure2, ProcedureBuilder, ResolverInput, + ResolverOutput, Router2, }; use serde::Serialize; use specta::Type; @@ -124,13 +124,13 @@ fn test_unstable_stuff(router: Router2) -> Router2 { }) .procedure("newstuff2", { ::builder() - // .with(invalidation(|ctx: Ctx, key, event| false)) - // .with(Middleware::new( - // move |ctx: Ctx, input: (), next| async move { - // let result = next.exec(ctx, input).await; - // result - // }, - // )) + .with(invalidation(|ctx: Ctx, key, event| false)) + .with(Middleware::new( + move |ctx: Ctx, input: (), next| async move { + let result = next.exec(ctx, input).await; + result + }, + )) .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) }) } @@ -140,29 +140,26 @@ pub enum InvalidateEvent { InvalidateKey(String), } -// fn invalidation( -// handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, -// ) -> Middleware -// where -// TError: Send + 'static, -// TCtx: Clone + Send + 'static, -// TInput: Clone + Send + 'static, -// TResult: Send + 'static, -// { -// let handler = Arc::new(handler); -// Middleware::new(move |ctx: TCtx, input: TInput, next| { -// let handler = handler.clone(); -// async move { -// // TODO: Register this with `TCtx` -// let ctx2 = ctx.clone(); -// let input2 = input.clone(); -// let result = next.exec(ctx, input); - -// // TODO: Unregister this with `TCtx` -// result -// } -// }) -// } +fn invalidation( + handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, +) -> Middleware +where + TError: Send + 'static, + TCtx: Clone + Send + 'static, + TInput: Clone + Send + 'static, + TResult: Send + 'static, +{ + let handler = Arc::new(handler); + Middleware::new(move |ctx: TCtx, input: TInput, next| async move { + // TODO: Register this with `TCtx` + let ctx2 = ctx.clone(); + let input2 = input.clone(); + let result = next.exec(ctx, input).await; + + // TODO: Unregister this with `TCtx` + result + }) +} #[tokio::main] async fn main() { diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index 3c14dc2f..b037e275 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -32,7 +32,7 @@ pub(crate) type MiddlewareHandler = A TNextCtx, TNextInput, ProcedureMeta, - ) -> Pin> + Send + 'static>> + ) -> Pin> + Send + 'static>> + Send + Sync + 'static, @@ -124,7 +124,7 @@ where }, ); - Box::pin(f.into_stream()) + Box::pin(f) }) }), } diff --git a/rspc/src/modern/middleware/next.rs b/rspc/src/modern/middleware/next.rs index cf8be741..5a155b1e 100644 --- a/rspc/src/modern/middleware/next.rs +++ b/rspc/src/modern/middleware/next.rs @@ -26,7 +26,7 @@ where self.meta.clone() } - pub fn exec(&self, ctx: TCtx, input: TInput) -> impl Stream> { - (self.next)(ctx, input, self.meta.clone()) + pub async fn exec(&self, ctx: TCtx, input: TInput) -> Result { + (self.next)(ctx, input, self.meta.clone()).await } } diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs index c3d96315..c35eb42d 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/modern/procedure/builder.rs @@ -76,7 +76,7 @@ where (self.build)( ProcedureKind::Query, Vec::new(), - Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input).into_stream())), + Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input))), ) } @@ -87,7 +87,7 @@ where (self.build)( ProcedureKind::Mutation, Vec::new(), - Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input).into_stream())), + Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input))), ) } } diff --git a/rspc/src/modern/stream.rs b/rspc/src/modern/stream.rs index 7bf42d8e..a2791c79 100644 --- a/rspc/src/modern/stream.rs +++ b/rspc/src/modern/stream.rs @@ -1,3 +1,10 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures::StreamExt; + /// Return a [`Stream`](futures::Stream) of values from a [`Procedure::query`](procedure::ProcedureBuilder::query) or [`Procedure::mutation`](procedure::ProcedureBuilder::mutation). /// /// ## Why not a subscription? @@ -33,3 +40,16 @@ impl Clone for Stream { Self(self.0.clone()) } } + +// TODO: I hate this requiring `Unpin` but we couldn't use `pin-project-lite` with the tuple variant. +impl futures::Stream for Stream { + type Item = S::Item; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.0.poll_next_unpin(cx) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 511f2f05..6a623e0f 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -45,7 +45,7 @@ impl Procedure2 { I: ResolverInput, R: ResolverOutput, { - use futures::Stream; + use futures::{FutureExt, Stream}; ProcedureBuilder { build: Box::new(|kind, setups, handler| { @@ -106,6 +106,7 @@ impl Procedure2 { I::from_input(input).unwrap(), // TODO: Error handling meta.clone(), ) + .into_stream() .map_ok(|v| v.into_stream()) .map_err(|err| err.into_resolver_error()) .try_flatten() From 1efb19b2e7fff7f6f4194a7fd4a8420bfd74b56e Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 13:34:31 +0800 Subject: [PATCH 33/67] drop `ProcedureError::Serialize` + properly handle in Tauri integration --- core/src/dyn_input.rs | 17 +-- core/src/error.rs | 105 +++++-------------- core/src/stream.rs | 16 +-- integrations/axum/src/jsonrpc_exec.rs | 73 ++++++------- integrations/tauri/src/lib.rs | 45 +++++--- middleware/openapi/Cargo.toml | 2 +- middleware/openapi/src/lib.rs | 5 +- middleware/tracing/Cargo.toml | 2 +- middleware/tracing/src/lib.rs | 2 +- middleware/tracing/src/traceable.rs | 2 +- rspc/src/legacy/interop.rs | 43 ++++---- rspc/src/modern/procedure/resolver_input.rs | 4 +- rspc/src/modern/procedure/resolver_output.rs | 14 +-- 13 files changed, 140 insertions(+), 190 deletions(-) diff --git a/core/src/dyn_input.rs b/core/src/dyn_input.rs index 82388c18..96238ba8 100644 --- a/core/src/dyn_input.rs +++ b/core/src/dyn_input.rs @@ -5,7 +5,7 @@ use std::{ use serde::{de::Error, Deserialize}; -use crate::{DeserializeError, DowncastError}; +use crate::{DeserializeError, DowncastError, ProcedureError}; /// TODO pub struct DynInput<'a, 'de> { @@ -36,15 +36,18 @@ impl<'a, 'de> DynInput<'a, 'de> { } /// TODO - pub fn deserialize>(self) -> Result { + pub fn deserialize>(self) -> Result { let DynInputInner::Deserializer(deserializer) = self.inner else { - return Err(DeserializeError(erased_serde::Error::custom(format!( - "attempted to deserialize from value '{}' but expected deserializer", - self.type_name - )))); + return Err(ProcedureError::Deserialize(DeserializeError( + erased_serde::Error::custom(format!( + "attempted to deserialize from value '{}' but expected deserializer", + self.type_name + )), + ))); }; - erased_serde::deserialize(deserializer).map_err(|err| DeserializeError(err)) + erased_serde::deserialize(deserializer) + .map_err(|err| ProcedureError::Deserialize(DeserializeError(err))) } /// TODO diff --git a/core/src/error.rs b/core/src/error.rs index 990d3c70..1a5e7c83 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -3,26 +3,23 @@ use std::{borrow::Cow, error, fmt}; use serde::{ser::SerializeStruct, Serialize, Serializer}; /// TODO -pub enum ProcedureError { +pub enum ProcedureError { /// Failed to find a procedure with the given name. NotFound, /// Attempted to deserialize a value but failed. Deserialize(DeserializeError), /// Attempting to downcast a value failed. Downcast(DowncastError), - /// An error occurred while serializing the value returned by the procedure. - Serializer(S::Error), /// An error occurred while running the procedure. Resolver(ResolverError), } -impl ProcedureError { - pub fn code(&self) -> u16 { +impl ProcedureError { + pub fn status(&self) -> u16 { match self { Self::NotFound => 404, Self::Deserialize(_) => 400, Self::Downcast(_) => 400, - Self::Serializer(_) => 500, Self::Resolver(err) => err.status(), } } @@ -32,7 +29,6 @@ impl ProcedureError { Self::NotFound => s.serialize_none(), Self::Deserialize(err) => s.serialize_str(&format!("{}", err)), Self::Downcast(err) => s.serialize_str(&format!("{}", err)), - Self::Serializer(err) => s.serialize_str(&format!("{}", err)), Self::Resolver(err) => s.serialize_str(&format!("{}", err)), } } @@ -42,7 +38,6 @@ impl ProcedureError { ProcedureError::NotFound => "NotFound", ProcedureError::Deserialize(_) => "Deserialize", ProcedureError::Downcast(_) => "Downcast", - ProcedureError::Serializer(_) => "Serializer", ProcedureError::Resolver(_) => "Resolver", } } @@ -52,7 +47,6 @@ impl ProcedureError { ProcedureError::NotFound => "procedure not found".into(), ProcedureError::Deserialize(err) => err.0.to_string().into(), ProcedureError::Downcast(err) => err.to_string().into(), - ProcedureError::Serializer(err) => err.to_string().into(), ProcedureError::Resolver(err) => err .error() .map(|err| err.to_string().into()) @@ -61,38 +55,33 @@ impl ProcedureError { } } -impl From for ProcedureError { +impl From for ProcedureError { fn from(err: ResolverError) -> Self { - match err.0 { - Repr::Custom { .. } => ProcedureError::Resolver(err), - Repr::Deserialize(err) => ProcedureError::Deserialize(err), - Repr::Downcast(downcast) => ProcedureError::Downcast(downcast), - } + ProcedureError::Resolver(err) } } -impl fmt::Debug for ProcedureError { +impl fmt::Debug for ProcedureError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: Proper format match self { Self::NotFound => write!(f, "NotFound"), Self::Deserialize(err) => write!(f, "Deserialize({:?})", err), Self::Downcast(err) => write!(f, "Downcast({:?})", err), - Self::Serializer(err) => write!(f, "Serializer({:?})", err), Self::Resolver(err) => write!(f, "Resolver({:?})", err), } } } -impl fmt::Display for ProcedureError { +impl fmt::Display for ProcedureError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } } -impl error::Error for ProcedureError {} +impl error::Error for ProcedureError {} -impl Serialize for ProcedureError { +impl Serialize for ProcedureError { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -109,25 +98,11 @@ impl Serialize for ProcedureError { } } -enum Repr { - // An actual resolver error. - Custom { - status: u16, - value: Box, - }, - // We hide these in here for DX (being able to do `?`) and then convert them to proper `ProcedureError` variants. - Deserialize(DeserializeError), - Downcast(DowncastError), -} - -impl From for ResolverError { - fn from(err: DowncastError) -> Self { - Self(Repr::Downcast(err)) - } -} - /// TODO -pub struct ResolverError(Repr); +pub struct ResolverError { + status: u16, + value: Box, +} impl ResolverError { // Warning: Returning > 400 will fallback to `500`. As redirects would be invalid and `200` would break matching. @@ -140,62 +115,36 @@ impl ResolverError { status = 500; } - Self(Repr::Custom { + Self { status, value: Box::new(ErrorInternal { value, err: source }), - }) + } } /// TODO pub fn status(&self) -> u16 { - match &self.0 { - Repr::Custom { status, value: _ } => *status, - // We flatten these to `ResolverError` so this won't be hit. - Repr::Deserialize(_) => unreachable!(), - Repr::Downcast(_) => unreachable!(), - } + self.status } /// TODO pub fn value(&self) -> impl Serialize + '_ { - match &self.0 { - Repr::Custom { - status: _, - value: error, - } => error.value(), - // We flatten these to `ResolverError` so this won't be hit. - Repr::Deserialize(_) => unreachable!(), - Repr::Downcast(_) => unreachable!(), - } + self.value.value() } /// TODO pub fn error(&self) -> Option<&(dyn error::Error + Send + 'static)> { - match &self.0 { - Repr::Custom { - status: _, - value: error, - } => error.error(), - // We flatten these to `ResolverError` so this won't be hit. - Repr::Deserialize(_) => unreachable!(), - Repr::Downcast(_) => unreachable!(), - } + self.value.error() } } impl fmt::Debug for ResolverError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.0 { - Repr::Custom { - status, - value: error, - } => { - write!(f, "status: {status:?}, error: {:?}", error.debug()) - } - // In practice these won't be hit. - Repr::Deserialize(err) => write!(f, "Deserialize({err:?})"), - Repr::Downcast(err) => write!(f, "Downcast({err:?})"), - } + write!( + f, + "status: {:?}, error: {:?}", + self.status, + self.value.debug() + ) } } @@ -224,12 +173,6 @@ impl fmt::Display for DeserializeError { impl error::Error for DeserializeError {} -impl From for ResolverError { - fn from(err: DeserializeError) -> Self { - Self(Repr::Deserialize(err)) - } -} - /// TODO pub struct DowncastError { // If `None`, the procedure was got a deserializer but expected a value. diff --git a/core/src/stream.rs b/core/src/stream.rs index ecfed6df..4ba36df9 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -9,7 +9,7 @@ use futures_core::Stream; use pin_project_lite::pin_project; use serde::Serialize; -use crate::ResolverError; +use crate::{ProcedureError, ResolverError}; /// TODO #[must_use = "ProcedureStream does nothing unless polled"] @@ -19,7 +19,7 @@ impl ProcedureStream { /// TODO pub fn from_stream(s: S) -> Self where - S: Stream> + Send + 'static, + S: Stream> + Send + 'static, T: Serialize + Send + Sync + 'static, { Self(Box::pin(DynReturnImpl { @@ -31,7 +31,7 @@ impl ProcedureStream { /// TODO pub fn from_stream_value(s: S) -> Self where - S: Stream> + Send + 'static, + S: Stream> + Send + 'static, T: Send + Sync + 'static, { Self(todo!()) @@ -46,7 +46,7 @@ impl ProcedureStream { pub fn poll_next( &mut self, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { self.0 .as_mut() .poll_next_value(cx) @@ -56,7 +56,7 @@ impl ProcedureStream { /// TODO pub async fn next( &mut self, - ) -> Option> { + ) -> Option> { poll_fn(|cx| self.0.as_mut().poll_next_value(cx)) .await .map(|v| v.map(|_: ()| self.0.value())) @@ -73,7 +73,7 @@ trait DynReturnValue: Send { fn poll_next_value<'a>( self: Pin<&'a mut Self>, cx: &mut Context<'_>, - ) -> Poll>>; + ) -> Poll>>; fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync); @@ -88,7 +88,7 @@ pin_project! { } } -impl> + Send + 'static> DynReturnValue +impl> + Send + 'static> DynReturnValue for DynReturnImpl where T: Send + Sync + Serialize, @@ -96,7 +96,7 @@ where fn poll_next_value<'a>( mut self: Pin<&'a mut Self>, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { let this = self.as_mut().project(); let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. this.src.poll_next(cx).map(|v| { diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index e51d0532..665f8eda 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -4,13 +4,10 @@ use std::{ future::{poll_fn, Future}, }; -use rspc_core::{ProcedureError, ProcedureStream, Procedures, ResolverError}; +use rspc_core::{ProcedureError, ProcedureStream, Procedures}; use serde::Serialize; use serde_json::Value; -use tokio::{ - pin, - sync::{broadcast, mpsc, oneshot, Mutex}, -}; +use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; use super::jsonrpc::{self, RequestId, RequestInner, ResponseInner}; @@ -174,39 +171,7 @@ pub async fn handle_json_rpc( // #[cfg(feature = "tracing")] // tracing::error!("Error executing operation: {:?}", err); - ResponseInner::Error(match err { - ProcedureError::NotFound => unreachable!(), - ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { - code: 400, - message: "error deserializing procedure arguments".to_string(), - data: None, - }, - ProcedureError::Downcast(_) => jsonrpc::JsonRPCError { - code: 400, - message: "error downcasting procedure arguments".to_string(), - data: None, - }, - ProcedureError::Serializer(_) => jsonrpc::JsonRPCError { - code: 500, - message: "error serializing procedure result".to_string(), - data: None, - }, - ProcedureError::Resolver(resolver_error) => { - let legacy_error = resolver_error - .error() - .and_then(|v| v.downcast_ref::()) - .cloned(); - - jsonrpc::JsonRPCError { - code: resolver_error.status() as i32, - message: legacy_error - .map(|v| v.0.clone()) - // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. - .unwrap_or_else(|| resolver_error.to_string()), - data: None, - } - } - }) + ResponseInner::Error(err) }) } else { if matches!(sender, Sender::Response(_)) @@ -355,13 +320,37 @@ pub async fn handle_json_rpc( async fn next( stream: &mut ProcedureStream, -) -> Option>> { +) -> Option> { let fut = stream.next(); let mut fut = std::pin::pin!(fut); poll_fn(|cx| fut.as_mut().poll(cx)).await.map(|v| { - v.map_err(ProcedureError::from).and_then(|v| { - v.serialize(serde_json::value::Serializer) - .map_err(ProcedureError::Serializer) + v.map_err(|err| match &err { + ProcedureError::NotFound => unimplemented!(), // Isn't created by this executor + ProcedureError::Deserialize(_) => jsonrpc::JsonRPCError { + code: 400, + message: "error deserializing procedure arguments".to_string(), + data: None, + }, + ProcedureError::Downcast(_) => unimplemented!(), // Isn't supported by this executor + ProcedureError::Resolver(resolver_err) => { + let legacy_error = resolver_err + .error() + .and_then(|v| v.downcast_ref::()) + .cloned(); + + jsonrpc::JsonRPCError { + code: err.status() as i32, + message: legacy_error + .map(|v| v.0.clone()) + // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. + .unwrap_or_else(|| err.to_string()), + data: None, + } + } + }) + .and_then(|v| { + Ok(v.serialize(serde_json::value::Serializer) + .expect("Error serialzing value")) // This panicking is bad but this is the old exectuor }) }) } diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index c2dc6bd5..87359847 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -13,12 +13,12 @@ use std::{ }; use rspc_core::{ProcedureError, Procedures}; -use serde::{Deserialize, Serialize}; +use serde::{de::Error, Deserialize, Serialize}; use serde_json::{value::RawValue, Serializer}; use tauri::{ async_runtime::{spawn, JoinHandle}, generate_handler, - ipc::{Channel, InvokeResponseBody}, + ipc::{Channel, InvokeResponseBody, IpcResponse}, plugin::{Builder, TauriPlugin}, Manager, }; @@ -45,7 +45,7 @@ where fn handle_rpc_impl( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ) { match req { @@ -54,8 +54,8 @@ where let ctx = (self.ctx_fn)(window); let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { - let err = ProcedureError::<&mut Serializer>>::NotFound; - send(&channel, Some((err.code(), &err))); + let err = ProcedureError::NotFound; + send(&channel, Some((err.status(), &err))); send::<()>(&channel, None); return; }; @@ -70,7 +70,7 @@ where while let Some(value) = stream.next().await { match value { Ok(v) => send(&channel, Some((200, &v))), - Err(err) => send(&channel, Some((err.status(), &err.value()))), + Err(err) => send(&channel, Some((err.status(), &err))), } } @@ -96,7 +96,7 @@ trait HandleRpc: Send + Sync { fn handle_rpc( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ); } @@ -110,7 +110,7 @@ where fn handle_rpc( self: Arc, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ) { Self::handle_rpc_impl(self, window, channel, req); @@ -127,7 +127,7 @@ struct State(Arc>); fn handle_rpc( state: tauri::State<'_, State>, window: tauri::Window, - channel: tauri::ipc::Channel, + channel: tauri::ipc::Channel, req: Request, ) { state.0.clone().handle_rpc(window, channel, req); @@ -175,7 +175,7 @@ enum Request<'a> { Abort(u32), } -fn send(channel: &Channel, value: Option<(u16, &T)>) { +fn send(channel: &Channel, value: Option<(u16, &T)>) { #[derive(Serialize)] struct Response<'a, T: Serialize> { code: u16, @@ -186,9 +186,28 @@ fn send(channel: &Channel, value: Option<(u16, Some((code, value)) => { let mut buffer = Vec::with_capacity(128); let mut serializer = Serializer::new(&mut buffer); - Response { code, value }.serialize(&mut serializer).unwrap(); // TODO: Error handling (throw back to Tauri) - channel.send(InvokeResponseBody::Raw(buffer)).ok() + channel + .send(IpcResultResponse( + Response { code, value } + .serialize(&mut serializer) + .map(|_: ()| InvokeResponseBody::Raw(buffer)) + .map_err(|err| err.to_string()), + )) + .ok() } - None => channel.send(InvokeResponseBody::Raw("DONE".into())).ok(), + None => channel + .send(IpcResultResponse(Ok(InvokeResponseBody::Raw( + "DONE".into(), + )))) + .ok(), }; } + +#[derive(Clone)] +struct IpcResultResponse(Result); + +impl IpcResponse for IpcResultResponse { + fn body(self) -> tauri::Result { + self.0.map_err(|err| serde_json::Error::custom(err).into()) + } +} diff --git a/middleware/openapi/Cargo.toml b/middleware/openapi/Cargo.toml index 976b789e..95bb9836 100644 --- a/middleware/openapi/Cargo.toml +++ b/middleware/openapi/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc" } +rspc = { path = "../../rspc", features = ["unstable"] } axum = { version = "0.7.5", default-features = false } serde_json = "1.0.127" futures = "0.3.30" diff --git a/middleware/openapi/src/lib.rs b/middleware/openapi/src/lib.rs index def9f6ab..547134f6 100644 --- a/middleware/openapi/src/lib.rs +++ b/middleware/openapi/src/lib.rs @@ -17,10 +17,7 @@ use axum::{ Json, }; use futures::StreamExt; -use rspc::{ - modern::{middleware::Middleware, procedure::ResolverInput, Procedure2}, - Router2, -}; +use rspc::{middleware::Middleware, Procedure2, ResolverInput, Router2}; use serde_json::json; // TODO: Properly handle inputs from query params diff --git a/middleware/tracing/Cargo.toml b/middleware/tracing/Cargo.toml index d7861280..a08f2516 100644 --- a/middleware/tracing/Cargo.toml +++ b/middleware/tracing/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc" } +rspc = { path = "../../rspc", features = ["unstable"] } tracing = "0.1" futures = "0.3" tracing-futures = "0.2.5" diff --git a/middleware/tracing/src/lib.rs b/middleware/tracing/src/lib.rs index 7fb9a2fb..54e24553 100644 --- a/middleware/tracing/src/lib.rs +++ b/middleware/tracing/src/lib.rs @@ -8,7 +8,7 @@ use std::{fmt, marker::PhantomData}; -use rspc::modern::middleware::Middleware; +use rspc::middleware::Middleware; use tracing::info; mod traceable; diff --git a/middleware/tracing/src/traceable.rs b/middleware/tracing/src/traceable.rs index f59e9166..e21a6a67 100644 --- a/middleware/tracing/src/traceable.rs +++ b/middleware/tracing/src/traceable.rs @@ -15,7 +15,7 @@ impl Traceable for T { #[doc(hidden)] pub enum StreamMarker {} // `rspc::Stream: !Debug` so the marker will never overlap -impl Traceable for rspc::modern::Stream +impl Traceable for rspc::Stream where S: futures::Stream, S::Item: fmt::Debug, diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy/interop.rs index 2ddcf038..e4ce3b2f 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -79,28 +79,26 @@ pub(crate) fn layer_to_procedure( value: Box>, ) -> rspc_core::Procedure { rspc_core::Procedure::new(move |ctx, input| { - let result = input - .deserialize::() - .map_err(Into::into) - .and_then(|input| { - value - .call( - ctx, - input, - RequestContext { - kind: kind.clone(), - path: path.clone(), - }, + let result = input.deserialize::().and_then(|input| { + value + .call( + ctx, + input, + RequestContext { + kind: kind.clone(), + path: path.clone(), + }, + ) + .map_err(|err| { + let err: crate::legacy::Error = err.into(); + ResolverError::new( + err.code.to_status_code(), + (), /* typesafe errors aren't supported in legacy router */ + Some(rspc_core::LegacyErrorInterop(err.message)), ) - .map_err(|err| { - let err: crate::legacy::Error = err.into(); - ResolverError::new( - err.code.to_status_code(), - (), /* typesafe errors aren't supported in legacy router */ - Some(rspc_core::LegacyErrorInterop(err.message)), - ) - }) - }); + .into() + }) + }); match result { Ok(result) => ProcedureStream::from_stream( @@ -117,6 +115,7 @@ pub(crate) fn layer_to_procedure( (), /* typesafe errors aren't supported in legacy router */ Some(rspc_core::LegacyErrorInterop(err.message)), ) + .into() }) .boxed(), Err(err) => { @@ -126,7 +125,7 @@ pub(crate) fn layer_to_procedure( err.message, err.cause, ); - stream::once(async { Err(err) }).boxed() + stream::once(async { Err(err.into()) }).boxed() } } } diff --git a/rspc/src/modern/procedure/resolver_input.rs b/rspc/src/modern/procedure/resolver_input.rs index ec64e196..6d0a3ee8 100644 --- a/rspc/src/modern/procedure/resolver_input.rs +++ b/rspc/src/modern/procedure/resolver_input.rs @@ -41,7 +41,7 @@ pub trait ResolverInput: Sized + Send + 'static { fn data_type(types: &mut TypeCollection) -> DataType; /// Convert the [`DynInput`] into the type the user specified for the procedure. - fn from_input(input: rspc_core::DynInput) -> Result; + fn from_input(input: rspc_core::DynInput) -> Result; } impl ResolverInput for T { @@ -49,7 +49,7 @@ impl ResolverInput for T { T::inline(types, specta::Generics::Definition) } - fn from_input(input: rspc_core::DynInput) -> Result { + fn from_input(input: rspc_core::DynInput) -> Result { Ok(input.deserialize()?) } } diff --git a/rspc/src/modern/procedure/resolver_output.rs b/rspc/src/modern/procedure/resolver_output.rs index 06244c38..8c69368a 100644 --- a/rspc/src/modern/procedure/resolver_output.rs +++ b/rspc/src/modern/procedure/resolver_output.rs @@ -30,7 +30,7 @@ // // )] use futures::{Stream, TryStreamExt}; -use rspc_core::{ProcedureStream, ResolverError}; +use rspc_core::{ProcedureError, ProcedureStream}; use serde::Serialize; use specta::{datatype::DataType, Generics, Type, TypeCollection}; @@ -47,11 +47,11 @@ pub trait ResolverOutput: Sized + Send + 'static { fn data_type(types: &mut TypeCollection) -> DataType; /// Convert the procedure into a [`Stream`]. - fn into_stream(self) -> impl Stream> + Send + 'static; + fn into_stream(self) -> impl Stream> + Send + 'static; /// Convert the stream into a [`ProcedureStream`]. fn into_procedure_stream( - stream: impl Stream> + Send + 'static, + stream: impl Stream> + Send + 'static, ) -> ProcedureStream; } @@ -66,12 +66,12 @@ where T::inline(types, Generics::Definition) } - fn into_stream(self) -> impl Stream> + Send + 'static { + fn into_stream(self) -> impl Stream> + Send + 'static { futures::stream::once(async move { Ok(self) }) } fn into_procedure_stream( - stream: impl Stream> + Send + 'static, + stream: impl Stream> + Send + 'static, ) -> ProcedureStream { ProcedureStream::from_stream(stream) } @@ -91,7 +91,7 @@ where T::data_type(types) // TODO: Do we need to do anything special here so the frontend knows this is a stream? } - fn into_stream(self) -> impl Stream> + Send + 'static { + fn into_stream(self) -> impl Stream> + Send + 'static { self.0 .map_ok(|v| v.into_stream()) .map_err(|err| err.into_resolver_error()) @@ -99,7 +99,7 @@ where } fn into_procedure_stream( - stream: impl Stream> + Send + 'static, + stream: impl Stream> + Send + 'static, ) -> ProcedureStream { ProcedureStream::from_stream(stream) } From dd6a76e2b1adf2da27e4411b9495618f114e9e11 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 13:55:28 +0800 Subject: [PATCH 34/67] gracefully handle panics in procedures --- core/src/error.rs | 17 ++++++++++---- core/src/stream.rs | 32 +++++++++++++++++++++------ examples/axum/src/main.rs | 1 + examples/bindings.ts | 3 ++- integrations/axum/src/jsonrpc_exec.rs | 1 + 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/core/src/error.rs b/core/src/error.rs index 1a5e7c83..46d83899 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, error, fmt}; +use std::{any::Any, borrow::Cow, error, fmt}; use serde::{ser::SerializeStruct, Serialize, Serializer}; @@ -12,6 +12,9 @@ pub enum ProcedureError { Downcast(DowncastError), /// An error occurred while running the procedure. Resolver(ResolverError), + /// The procedure unexpectedly unwinded. + /// This happens when you panic inside a procedure. + Unwind(Box), } impl ProcedureError { @@ -21,6 +24,7 @@ impl ProcedureError { Self::Deserialize(_) => 400, Self::Downcast(_) => 400, Self::Resolver(err) => err.status(), + Self::Unwind(_) => 500, } } @@ -30,6 +34,7 @@ impl ProcedureError { Self::Deserialize(err) => s.serialize_str(&format!("{}", err)), Self::Downcast(err) => s.serialize_str(&format!("{}", err)), Self::Resolver(err) => s.serialize_str(&format!("{}", err)), + Self::Unwind(_) => s.serialize_none(), } } @@ -39,9 +44,11 @@ impl ProcedureError { ProcedureError::Deserialize(_) => "Deserialize", ProcedureError::Downcast(_) => "Downcast", ProcedureError::Resolver(_) => "Resolver", + ProcedureError::Unwind(_) => "ResolverPanic", } } + // TODO: This should be treated as sanitized and okay for the frontend right? pub fn message(&self) -> Cow<'static, str> { match self { ProcedureError::NotFound => "procedure not found".into(), @@ -51,6 +58,7 @@ impl ProcedureError { .error() .map(|err| err.to_string().into()) .unwrap_or("resolver error".into()), + ProcedureError::Unwind(_) => "resolver panic".into(), } } } @@ -66,9 +74,10 @@ impl fmt::Debug for ProcedureError { // TODO: Proper format match self { Self::NotFound => write!(f, "NotFound"), - Self::Deserialize(err) => write!(f, "Deserialize({:?})", err), - Self::Downcast(err) => write!(f, "Downcast({:?})", err), - Self::Resolver(err) => write!(f, "Resolver({:?})", err), + Self::Deserialize(err) => write!(f, "Deserialize({err:?})"), + Self::Downcast(err) => write!(f, "Downcast({err:?})"), + Self::Resolver(err) => write!(f, "Resolver({err:?})"), + Self::Unwind(err) => write!(f, "ResolverPanic({err:?})"), } } } diff --git a/core/src/stream.rs b/core/src/stream.rs index 4ba36df9..ab712278 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -1,6 +1,7 @@ use core::fmt; use std::{ future::poll_fn, + panic::{catch_unwind, AssertUnwindSafe}, pin::Pin, task::{Context, Poll}, }; @@ -9,7 +10,7 @@ use futures_core::Stream; use pin_project_lite::pin_project; use serde::Serialize; -use crate::{ProcedureError, ResolverError}; +use crate::ProcedureError; /// TODO #[must_use = "ProcedureStream does nothing unless polled"] @@ -24,6 +25,7 @@ impl ProcedureStream { { Self(Box::pin(DynReturnImpl { src: s, + unwound: false, value: None, })) } @@ -84,6 +86,7 @@ pin_project! { struct DynReturnImpl{ #[pin] src: S, + unwound: bool, value: Option, } } @@ -97,16 +100,31 @@ where mut self: Pin<&'a mut Self>, cx: &mut Context<'_>, ) -> Poll>> { + if self.unwound { + // The stream is now done. + return Poll::Ready(None); + } + let this = self.as_mut().project(); - let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. - this.src.poll_next(cx).map(|v| { - v.map(|v| { + let r = catch_unwind(AssertUnwindSafe(|| { + let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. + this.src.poll_next(cx).map(|v| { v.map(|v| { - *this.value = Some(v); - () + v.map(|v| { + *this.value = Some(v); + () + }) }) }) - }) + })); + + match r { + Ok(v) => v, + Err(err) => { + *this.unwound = true; + Poll::Ready(Some(Err(ProcedureError::Unwind(err)))) + } + } } fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync) { diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 0aa0d239..c58a9359 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -48,6 +48,7 @@ fn mount() -> rspc::Router { let router = rspc::Router::::new() .merge("nested.", inner) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("panic", |t| t(|_, _: ()| todo!())) // .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .query("echo", |t| t(|_, v: String| v)) .query("error", |t| { diff --git a/examples/bindings.ts b/examples/bindings.ts index f35ada3a..8b719d68 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,7 +1,7 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { echo: { input: string, output: string, error: unknown }, @@ -11,6 +11,7 @@ export type Procedures = { }, newstuff: { input: any, output: any, error: any }, newstuff2: { input: any, output: any, error: any }, + panic: { input: null, output: null, error: unknown }, pings: { input: null, output: string, error: unknown }, sendMsg: { input: string, output: string, error: unknown }, transformMe: { input: null, output: string, error: unknown }, diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 665f8eda..8e08739c 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -347,6 +347,7 @@ async fn next( data: None, } } + ProcedureError::Unwind(err) => panic!("{err:?}"), // Restore previous behavior lol }) .and_then(|v| { Ok(v.serialize(serde_json::value::Serializer) From 2a4286762f49eef078f5fe511014ad6c9a23db86 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 14:20:29 +0800 Subject: [PATCH 35/67] cleanup `Procedures` --- core/src/dyn_input.rs | 2 ++ examples/axum/src/main.rs | 6 ++--- examples/src/bin/axum.rs | 4 ++-- examples/src/bin/cookies.rs | 4 ++-- examples/src/bin/global_context.rs | 4 ++-- examples/src/bin/middleware.rs | 4 ++-- integrations/axum/src/jsonrpc_exec.rs | 4 ++-- integrations/axum/src/v2.rs | 22 ++++++++--------- integrations/tauri/src/lib.rs | 2 +- middleware/devtools/src/lib.rs | 6 ++--- rspc/src/router.rs | 34 +++++++++++++++++++++------ 11 files changed, 57 insertions(+), 35 deletions(-) diff --git a/core/src/dyn_input.rs b/core/src/dyn_input.rs index 96238ba8..c5ae37c1 100644 --- a/core/src/dyn_input.rs +++ b/core/src/dyn_input.rs @@ -7,6 +7,8 @@ use serde::{de::Error, Deserialize}; use crate::{DeserializeError, DowncastError, ProcedureError}; +// It would be really nice if this with `&'a DynInput<'de>` but that would require `#[repr(transparent)]` with unsafe which is probally not worth it. + /// TODO pub struct DynInput<'a, 'de> { inner: DynInputInner<'a, 'de>, diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index c58a9359..defb7202 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -166,7 +166,7 @@ where async fn main() { let router = Router2::from(mount()); let router = test_unstable_stuff(router); - let (routes, types) = router.build().unwrap(); + let (procedures, types) = router.build().unwrap(); rspc::Typescript::default() // .formatter(specta_typescript::formatter::prettier), @@ -187,7 +187,7 @@ async fn main() { // ) // .unwrap(); - let routes = rspc_devtools::mount(routes, &types); + let procedures = rspc_devtools::mount(procedures, &types); // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! let cors = CorsLayer::new() @@ -199,7 +199,7 @@ async fn main() { .route("/", get(|| async { "Hello 'rspc'!" })) .nest( "/rspc", - rspc_axum::endpoint(routes, |parts: Parts| { + rspc_axum::endpoint(procedures, |parts: Parts| { println!("Client requested operation '{}'", parts.uri.path()); Ctx {} }), diff --git a/examples/src/bin/axum.rs b/examples/src/bin/axum.rs index 603e6d7f..b7f08d0e 100644 --- a/examples/src/bin/axum.rs +++ b/examples/src/bin/axum.rs @@ -25,7 +25,7 @@ async fn main() { .merge("r1.", r1) .build(); - let (routes, types) = rspc::Router2::from(router).build().unwrap(); + let (procedures, types) = rspc::Router2::from(router).build().unwrap(); rspc::Typescript::default() .export_to( @@ -40,7 +40,7 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - rspc_axum::endpoint(routes, |parts: Parts| { + rspc_axum::endpoint(procedures, |parts: Parts| { println!("Client requested operation '{}'", parts.uri.path()); () diff --git a/examples/src/bin/cookies.rs b/examples/src/bin/cookies.rs index 20ba6656..b8becef8 100644 --- a/examples/src/bin/cookies.rs +++ b/examples/src/bin/cookies.rs @@ -32,7 +32,7 @@ async fn main() { }) .build(); - let (routes, types) = rspc::Router2::from(router).build().unwrap(); + let (procedures, types) = rspc::Router2::from(router).build().unwrap(); rspc::Typescript::default() .export_to( @@ -47,7 +47,7 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - rspc_axum::endpoint(routes, |cookies: Cookies| Ctx { cookies }), + rspc_axum::endpoint(procedures, |cookies: Cookies| Ctx { cookies }), ) .layer(CookieManagerLayer::new()) // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! diff --git a/examples/src/bin/global_context.rs b/examples/src/bin/global_context.rs index 914a4d58..ed7e1b50 100644 --- a/examples/src/bin/global_context.rs +++ b/examples/src/bin/global_context.rs @@ -23,7 +23,7 @@ async fn main() { }) .build(); - let (routes, types) = rspc::Router2::from(router).build().unwrap(); + let (procedures, types) = rspc::Router2::from(router).build().unwrap(); rspc::Typescript::default() .export_to( @@ -38,7 +38,7 @@ async fn main() { let app = axum::Router::new().nest( "/rspc", - rspc_axum::endpoint(routes, move || MyCtx { + rspc_axum::endpoint(procedures, move || MyCtx { count: count.clone(), }), ); diff --git a/examples/src/bin/middleware.rs b/examples/src/bin/middleware.rs index cb5a6cbb..503faeae 100644 --- a/examples/src/bin/middleware.rs +++ b/examples/src/bin/middleware.rs @@ -98,7 +98,7 @@ async fn main() { // .middleware(|mw| mw.openapi(OpenAPIConfig {})) .build(); - let (routes, types) = rspc::Router2::from(router).build().unwrap(); + let (procedures, types) = rspc::Router2::from(router).build().unwrap(); rspc::Typescript::default() .export_to( @@ -112,7 +112,7 @@ async fn main() { // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. .nest( "/rspc", - rspc_axum::endpoint(routes, || UnauthenticatedContext { + rspc_axum::endpoint(procedures, || UnauthenticatedContext { session_id: Some("abc".into()), // Change this line to control whether you are authenticated and can access the "another" query. }), ) diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 8e08739c..98a6c824 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -124,7 +124,7 @@ impl<'a> Sender<'a> { pub async fn handle_json_rpc( ctx: TCtx, req: jsonrpc::Request, - routes: &Procedures, + procedures: &Procedures, sender: &mut Sender<'_>, subscriptions: &mut SubscriptionMap<'_>, ) where @@ -158,7 +158,7 @@ pub async fn handle_json_rpc( } }; - let result = match routes.get(&Cow::Borrowed(&*path)) { + let result = match procedures.get(&Cow::Borrowed(&*path)) { Some(procedure) => { let mut stream = procedure.exec_with_deserializer(ctx, input.unwrap_or(Value::Null)); let first_value = next(&mut stream).await; diff --git a/integrations/axum/src/v2.rs b/integrations/axum/src/v2.rs index bf913f54..772c1c26 100644 --- a/integrations/axum/src/v2.rs +++ b/integrations/axum/src/v2.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{borrow::Borrow, collections::HashMap}; use axum::{ body::{to_bytes, Body}, @@ -18,7 +18,7 @@ use crate::{ }; pub fn endpoint( - routes: impl Into>, + procedures: impl Borrow>, ctx_fn: TCtxFn, ) -> Router where @@ -27,14 +27,14 @@ where TCtxFnMarker: Send + Sync + 'static, TCtxFn: TCtxFunc, { - let routes = routes.into(); + let procedures = procedures.borrow().clone(); Router::::new().route( "/:id", on( MethodFilter::GET.or(MethodFilter::POST), move |state: State, req: axum::extract::Request| { - let routes = routes.clone(); + let procedures = procedures.clone(); async move { match (req.method(), &req.uri().path()[1..]) { @@ -51,7 +51,7 @@ where ctx_fn, socket, req.into_parts().0, - routes, + procedures, state.0, ) }) @@ -65,12 +65,12 @@ where .unwrap() } (&Method::GET, _) => { - handle_http(ctx_fn, ProcedureKind::Query, req, &routes, state.0) + handle_http(ctx_fn, ProcedureKind::Query, req, &procedures, state.0) .await .into_response() } (&Method::POST, _) => { - handle_http(ctx_fn, ProcedureKind::Mutation, req, &routes, state.0) + handle_http(ctx_fn, ProcedureKind::Mutation, req, &procedures, state.0) .await .into_response() } @@ -86,7 +86,7 @@ async fn handle_http( ctx_fn: TCtxFn, kind: ProcedureKind, req: Request, - routes: &Procedures, + procedures: &Procedures, state: TState, ) -> impl IntoResponse where @@ -173,7 +173,7 @@ where } }, }, - routes, + procedures, &mut resp, &mut SubscriptionMap::None, ) @@ -206,7 +206,7 @@ async fn handle_websocket( ctx_fn: TCtxFn, mut socket: axum::extract::ws::WebSocket, parts: Parts, - routes: Procedures, + procedures: Procedures, state: TState, ) where TCtx: Send + Sync + 'static, @@ -275,7 +275,7 @@ async fn handle_websocket( } }; - handle_json_rpc(ctx, request, &routes, &mut Sender::Channel(&mut tx), + handle_json_rpc(ctx, request, &procedures, &mut Sender::Channel(&mut tx), &mut SubscriptionMap::Ref(&mut subscriptions)).await; } }, diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 87359847..375a77a4 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -134,7 +134,7 @@ fn handle_rpc( } pub fn plugin( - procedures: impl Into>, + procedures: impl AsRef>, ctx_fn: TCtxFn, ) -> TauriPlugin where diff --git a/middleware/devtools/src/lib.rs b/middleware/devtools/src/lib.rs index 50e675f8..a8273537 100644 --- a/middleware/devtools/src/lib.rs +++ b/middleware/devtools/src/lib.rs @@ -22,10 +22,10 @@ use rspc_core::{Procedure, ProcedureStream, Procedures}; use types::{Metadata, ProcedureMetadata}; pub fn mount( - routes: impl Into>, + procedures: impl Into>, types: &impl Any, -) -> impl Into> { - let procedures = routes.into(); +) -> Procedures { + let procedures = procedures.into(); let meta = Metadata { crate_name: env!("CARGO_PKG_NAME"), crate_version: env!("CARGO_PKG_VERSION"), diff --git a/rspc/src/router.rs b/rspc/src/router.rs index cf46ec8d..f91318eb 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -1,5 +1,5 @@ use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, collections::{BTreeMap, HashMap}, fmt, }; @@ -72,7 +72,15 @@ impl Router2 { self } - pub fn build(self) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { + pub fn build( + self, + ) -> Result< + ( + impl Borrow> + Into> + fmt::Debug, + Types, + ), + (), + > { self.build_with_state_inner(State::default()) } @@ -80,14 +88,26 @@ impl Router2 { pub fn build_with_state( self, state: State, - ) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { + ) -> Result< + ( + impl Borrow> + Into> + fmt::Debug, + Types, + ), + (), + > { self.build_with_state_inner(state) } fn build_with_state_inner( self, mut state: State, - ) -> Result<(impl Into> + Clone + fmt::Debug, Types), ()> { + ) -> Result< + ( + impl Borrow> + Into> + fmt::Debug, + Types, + ), + (), + > { for setup in self.setup { setup(&mut state); } @@ -120,9 +140,9 @@ impl Router2 { self.0 } } - impl Clone for Impl { - fn clone(&self) -> Self { - Self(self.0.clone()) + impl Borrow> for Impl { + fn borrow(&self) -> &Procedures { + &self.0 } } impl fmt::Debug for Impl { From d5e17d78074fd1d3e0fb0ba5f71b83a628f84a2f Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 9 Dec 2024 23:38:51 +0800 Subject: [PATCH 36/67] access `State` in procedure + wip `rspc-cache` --- core/src/dyn_input.rs | 2 +- core/src/procedure.rs | 2 - examples/axum/Cargo.toml | 5 +- examples/axum/src/main.rs | 38 ++++- examples/bindings.ts | 3 +- integrations/axum/Cargo.toml | 3 +- integrations/axum/src/endpoint.rs | 237 +++++++++++++++++++++++++++++ integrations/axum/src/lib.rs | 2 + middleware/cache/Cargo.toml | 5 +- middleware/cache/src/lib.rs | 34 ++++- middleware/cache/src/state.rs | 22 +++ middleware/cache/src/store.rs | 55 +++++++ middleware/devtools/Cargo.toml | 1 + middleware/devtools/src/lib.rs | 1 + middleware/devtools/src/tracing.rs | 8 + rspc/src/legacy/interop.rs | 2 +- rspc/src/modern/middleware/next.rs | 9 +- rspc/src/modern/mod.rs | 2 - rspc/src/modern/procedure/meta.rs | 9 +- rspc/src/procedure.rs | 76 +++------ rspc/src/router.rs | 4 +- 21 files changed, 437 insertions(+), 83 deletions(-) create mode 100644 integrations/axum/src/endpoint.rs create mode 100644 middleware/cache/src/state.rs create mode 100644 middleware/cache/src/store.rs create mode 100644 middleware/devtools/src/tracing.rs diff --git a/core/src/dyn_input.rs b/core/src/dyn_input.rs index c5ae37c1..1996d76a 100644 --- a/core/src/dyn_input.rs +++ b/core/src/dyn_input.rs @@ -7,7 +7,7 @@ use serde::{de::Error, Deserialize}; use crate::{DeserializeError, DowncastError, ProcedureError}; -// It would be really nice if this with `&'a DynInput<'de>` but that would require `#[repr(transparent)]` with unsafe which is probally not worth it. +// It would be really nice if this with `&'a DynInput<'de>` but that would require `#[repr(transparent)]` with can only be constructed with unsafe which is probally not worth it. /// TODO pub struct DynInput<'a, 'de> { diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 29d0d31f..65863788 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -4,8 +4,6 @@ use serde::Deserializer; use crate::{DynInput, ProcedureStream}; -// TODO: Document the importance of the `size_hint` - /// a single type-erased operation that the server can execute. /// /// TODO: Show constructing and executing procedure. diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index d46bea51..2003da99 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -9,7 +9,7 @@ rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } tokio = { version = "1.41.1", features = ["full"] } async-stream = "0.3.6" -axum = { version = "0.7.9", features = ["ws"] } +axum = { version = "0.7.9", features = ["ws", "tokio"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } @@ -19,3 +19,6 @@ specta = { version = "=2.0.0-rc.20", features = [ ] } # TODO: Drop all features thiserror = "2.0.4" rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } +tracing = "0.1.41" +futures = "0.3.31" +rspc-cache = { version = "0.0.0", path = "../../middleware/cache" } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index defb7202..82b4aa4f 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,16 +1,18 @@ use std::{marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; use async_stream::stream; -use axum::{http::request::Parts, routing::get}; +use axum::routing::get; use rspc::{ - middleware::Middleware, Error2, Infallible, Procedure2, ProcedureBuilder, ResolverInput, - ResolverOutput, Router2, + middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, + Router2, }; +use rspc_cache::{cache, cache_ttl, CacheState, Memory}; use serde::Serialize; use specta::Type; use thiserror::Error; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; +use tracing::info; // `Clone` is only required for usage with Websockets #[derive(Clone)] @@ -47,7 +49,13 @@ fn mount() -> rspc::Router { let router = rspc::Router::::new() .merge("nested.", inner) - .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("version", |t| { + t(|_, _: ()| { + info!("Hello World from Version Query!"); + + env!("CARGO_PKG_VERSION") + }) + }) .query("panic", |t| t(|_, _: ()| todo!())) // .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .query("echo", |t| t(|_, v: String| v)) @@ -134,6 +142,17 @@ fn test_unstable_stuff(router: Router2) -> Router2 { )) .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) }) + .setup(CacheState::builder(Memory::new()).mount()) + .procedure("cached", { + ::builder() + .with(cache()) + .query(|_, _: ()| async { + // if input.some_arg {} + cache_ttl(10); + + Ok(env!("CARGO_PKG_VERSION")) + }) + }) } #[derive(Debug, Clone, Serialize, Type)] @@ -197,10 +216,17 @@ async fn main() { let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) + // .nest( + // "/rspc", + // rspc_axum::endpoint(procedures, |parts: Parts| { + // println!("Client requested operation '{}'", parts.uri.path()); + // Ctx {} + // }), + // ) .nest( "/rspc", - rspc_axum::endpoint(procedures, |parts: Parts| { - println!("Client requested operation '{}'", parts.uri.path()); + rspc_axum::Endpoint::builder(procedures).build(|| { + // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this Ctx {} }), ) diff --git a/examples/bindings.ts b/examples/bindings.ts index 8b719d68..9243276f 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,9 +1,10 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { + cached: { input: any, output: any, error: any }, echo: { input: string, output: string, error: unknown }, error: { input: null, output: string, error: unknown }, nested: { diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 0bb33a39..12016ab0 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -21,7 +21,7 @@ ws = ["axum/ws"] [dependencies] rspc-core = { version = "0.0.1", path = "../../core" } -axum = "0.7.9" +axum = { version = "0.7.9", features = ["ws", "json"] } serde_json = "1" # TODO: Drop these @@ -29,6 +29,7 @@ form_urlencoded = "1.2.1" # TODO: use Axum's built in extr futures = "0.3" # TODO: No blocking execution, etc tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. serde = { version = "1", features = ["derive"] } # TODO: Remove features +serde_urlencoded = "0.7.1" [lints] workspace = true diff --git a/integrations/axum/src/endpoint.rs b/integrations/axum/src/endpoint.rs new file mode 100644 index 00000000..1613e671 --- /dev/null +++ b/integrations/axum/src/endpoint.rs @@ -0,0 +1,237 @@ +use std::{ + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use axum::{ + body::{Bytes, HttpBody}, + extract::{FromRequest, Request}, + response::{ + sse::{Event, KeepAlive}, + Sse, + }, + routing::{on, MethodFilter}, +}; +use futures::Stream; +use rspc_core::{ProcedureStream, Procedures}; + +/// Construct a new [`axum::Router`](axum::Router) to expose a given [`rspc::Router`](rspc::Router). +pub struct Endpoint { + procedures: Procedures, + // endpoints: bool, + // websocket: Option TCtx>, + // batching: bool, +} + +impl Endpoint { + // /// Construct a new [`axum::Router`](axum::Router) with all features enabled. + // /// + // /// This will enable all features, if you want to configure which features are enabled you can use [`Endpoint::builder`] instead. + // /// + // /// # Usage + // /// + // /// ```rust + // /// axum::Router::new().nest( + // /// "/rspc", + // /// rspc_axum::Endpoint::new(rspc::Router::new().build().unwrap(), || ()), + // /// ); + // /// ``` + // pub fn new( + // router: BuiltRouter, + // // TODO: Parse this to `Self::build` -> It will make rustfmt result way nicer + // // TODO: Make Axum extractors work + // ctx_fn: impl Fn(&Parts) -> TCtx + Send + Sync + 'static, + // ) -> axum::Router + // where + // S: Clone + Send + Sync + 'static, + // // TODO: Error type??? + // // F: Future> + Send + Sync + 'static, + // TCtx: Clone, + // { + // let mut t = Self::builder(router).with_endpoints(); + // #[cfg(feature = "ws")] + // { + // t = t.with_websocket(); + // } + // t.with_batching().build(ctx_fn) + // } + + // /// Construct a new [`Endpoint`](Endpoint) with no features enabled. + // /// + // /// # Usage + // /// + // /// ```rust + // /// axum::Router::new().nest( + // /// "/rspc", + // /// rspc_axum::Endpoint::builder(rspc::Router::new().build().unwrap()) + // /// // Exposes HTTP endpoints for queries and mutations. + // /// .with_endpoints() + // /// // Exposes a Websocket connection for queries, mutations and subscriptions. + // /// .with_websocket() + // /// // Enables support for the frontend sending batched queries. + // /// .with_batching() + // /// .build(|| ()), + // /// ); + // /// ``` + pub fn builder(router: Procedures) -> Self { + Self { + procedures: router, + // endpoints: false, + // websocket: None, + // batching: false, + } + } + + // /// Enables HTTP endpoints for queries and mutations. + // /// + // /// This is exposed as `/routerName.procedureName` + // pub fn with_endpoints(mut self) -> Self { + // Self { + // endpoints: true, + // ..self + // } + // } + + // /// Exposes a Websocket connection for queries, mutations and subscriptions. + // /// + // /// This is exposed as a `/ws` endpoint. + // #[cfg(feature = "ws")] + // #[cfg_attr(docsrs, doc(cfg(feature = "ws")))] + // pub fn with_websocket(self) -> Self + // where + // TCtx: Clone, + // { + // Self { + // websocket: Some(|ctx| ctx.clone()), + // ..self + // } + // } + + // /// Enables support for the frontend sending batched queries. + // /// + // /// This is exposed as a `/_batch` endpoint. + // pub fn with_batching(self) -> Self + // where + // TCtx: Clone, + // { + // Self { + // batching: true, + // ..self + // } + // } + + // TODO: Axum extractors + + /// Build an [`axum::Router`](axum::Router) with the configured features. + pub fn build(self, ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static) -> axum::Router + where + S: Clone + Send + Sync + 'static, + { + let mut r = axum::Router::new(); + let ctx_fn = Arc::new(ctx_fn); + + for (key, procedure) in self.procedures { + let ctx_fn = ctx_fn.clone(); + r = r.route( + &format!("/{key}"), + on( + MethodFilter::GET.or(MethodFilter::POST), + move |req: Request| { + let ctx = ctx_fn(); + + async move { + let hint = req.body().size_hint(); + let has_body = hint.lower() != 0 || hint.upper() != Some(0); + let stream = if !has_body { + let mut params = form_urlencoded::parse( + req.uri().query().unwrap_or_default().as_ref(), + ); + + match params + .find_map(|(input, value)| (input == "input").then(|| value)) + { + Some(input) => procedure.exec_with_deserializer( + ctx, + &mut serde_json::Deserializer::from_str(&*input), + ), + None => procedure + .exec_with_deserializer(ctx, serde_json::Value::Null), + } + } else { + // TODO + // if !json_content_type(req.headers()) { + // Err(MissingJsonContentType.into()) + // } + + let bytes = Bytes::from_request(req, &()).await.unwrap(); // TODO: Error handling + procedure.exec_with_deserializer( + ctx, + &mut serde_json::Deserializer::from_slice(&bytes), + ) + }; + + // TODO: Status code + // TODO: Json headers + // TODO: Maybe only SSE for subscriptions??? + + Sse::new(ProcedureStreamSSE(stream)).keep_alive(KeepAlive::default()) + } + }, + ), + ); + } + + // TODO: Websocket endpoint + + r + } +} + +struct ProcedureStreamSSE(ProcedureStream); + +impl Stream for ProcedureStreamSSE { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.0 + .poll_next(cx) + // TODO: `v` should be broken out + .map(|v| { + v.map(|v| { + Event::default() + // .event() // TODO: `Ok` vs `Err` - Also serve `StatusCode` + .json_data(&v) + }) + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +// fn json_content_type(headers: &HeaderMap) -> bool { +// let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { +// content_type +// } else { +// return false; +// }; + +// let content_type = if let Ok(content_type) = content_type.to_str() { +// content_type +// } else { +// return false; +// }; + +// let mime = if let Ok(mime) = content_type.parse::() { +// mime +// } else { +// return false; +// }; + +// let is_json_content_type = mime.type_() == "application" +// && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); + +// is_json_content_type +// } diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index c0a8e054..7c2b9dc7 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -6,10 +6,12 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] +mod endpoint; mod extractors; mod jsonrpc; mod jsonrpc_exec; // mod legacy; mod v2; +pub use endpoint::Endpoint; pub use v2::endpoint; diff --git a/middleware/cache/Cargo.toml b/middleware/cache/Cargo.toml index d51a4716..f0dc02dc 100644 --- a/middleware/cache/Cargo.toml +++ b/middleware/cache/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "rspc-cached" +name = "rspc-cache" version = "0.0.0" edition = "2021" publish = false [dependencies] -rspc-core = { path = "../../core" } +moka = { version = "0.12.8", features = ["sync"] } +rspc = { path = "../../rspc", features = ["unstable"] } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/middleware/cache/src/lib.rs b/middleware/cache/src/lib.rs index 98eacc12..7e5414a1 100644 --- a/middleware/cache/src/lib.rs +++ b/middleware/cache/src/lib.rs @@ -6,6 +6,34 @@ html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" )] -// TODO: Built-in TTL cache -// TODO: Allow defining custom cache lifetimes (copy Next.js cacheLife maybe) -// TODO: Allow defining a remote cache (e.g. Redis) +mod state; +mod store; + +pub use state::CacheState; +pub use store::{Memory, Store}; + +use rspc::middleware::Middleware; + +pub fn cache() -> Middleware +where + TError: Send + 'static, + TCtx: Clone + Send + 'static, + TInput: Clone + Send + 'static, + TResult: Send + 'static, +{ + Middleware::new(move |ctx: TCtx, input: TInput, next| async move { + let cache = next.meta().state().get::().unwrap(); // TODO: Error handling + + // let cache = CacheState::builder(Arc::new(Memory)).mount(); + + let result = next.exec(ctx, input).await; + // TODO: Get cache tll + // TODO: Use `Store` + result + }) +} + +/// Set the cache time-to-live (TTL) in seconds +pub fn cache_ttl(ttl: usize) { + todo!(); +} diff --git a/middleware/cache/src/state.rs b/middleware/cache/src/state.rs new file mode 100644 index 00000000..2ed89d2a --- /dev/null +++ b/middleware/cache/src/state.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use rspc::State; + +use crate::Store; + +pub struct CacheState> { + store: S, +} + +impl CacheState { + pub fn builder(store: S) -> Self { + Self { store } + } + + pub fn mount(self) -> impl FnOnce(&mut State) { + let cache = CacheState::builder(Arc::new(self.store)); + move |state: &mut State| { + state.insert(cache); + } + } +} diff --git a/middleware/cache/src/store.rs b/middleware/cache/src/store.rs new file mode 100644 index 00000000..8d1d9236 --- /dev/null +++ b/middleware/cache/src/store.rs @@ -0,0 +1,55 @@ +use std::{any::Any, sync::Arc}; + +use moka::sync::Cache; + +pub trait Store: Send + Sync + 'static { + // fn get(&self, key: &str) -> Option; + // fn set(&self, key: &str, value: &V, ttl: usize); +} + +impl Store for Arc { + // fn get(&self, key: &str) -> Option { + // self.as_ref().get(key) + // } + + // fn set(&self, key: &str, value: &V, ttl: usize) { + // self.as_ref().set(key, value, ttl) + // } +} + +struct Value(Box); + +impl Clone for Value { + fn clone(&self) -> Self { + Self(self.0.dyn_clone()) + } +} + +// TODO: Sealing this better. +pub trait DynClone: Send + Sync { + // Return `Value` instead of `Box` directly for sealing + fn dyn_clone(&self) -> Box; +} +impl DynClone for T { + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub struct Memory(Cache); + +impl Memory { + pub fn new() -> Self { + Self(Cache::new(100)) // TODO: Configurable + } +} + +impl Store for Memory { + // fn get(&self, key: &str) -> Option { + // self.0.get(key).map(|v| v.downcast_ref().clone()) + // } + + // fn set(&self, key: &str, value: &V, ttl: usize) { + // todo!() + // } +} diff --git a/middleware/devtools/Cargo.toml b/middleware/devtools/Cargo.toml index e3368691..b8d7cd15 100644 --- a/middleware/devtools/Cargo.toml +++ b/middleware/devtools/Cargo.toml @@ -9,6 +9,7 @@ futures = "0.3.31" rspc-core = { path = "../../core" } serde = { version = "1.0.215", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = ["derive"] } +tracing = "0.1.41" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/middleware/devtools/src/lib.rs b/middleware/devtools/src/lib.rs index a8273537..228a042f 100644 --- a/middleware/devtools/src/lib.rs +++ b/middleware/devtools/src/lib.rs @@ -9,6 +9,7 @@ // http://[::]:4000/rspc/~rspc.devtools.meta // http://[::]:4000/rspc/~rspc.devtools.history +mod tracing; mod types; use std::{ diff --git a/middleware/devtools/src/tracing.rs b/middleware/devtools/src/tracing.rs new file mode 100644 index 00000000..e1d6645f --- /dev/null +++ b/middleware/devtools/src/tracing.rs @@ -0,0 +1,8 @@ +// pub fn init() { +// ConsoleLayer::builder().with_default_env().init(); +// } + +// pub struct ConsoleLayer { +// current_spans: ThreadLocal>, +// tx: mpsc::Sender, +// } diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy/interop.rs index e4ce3b2f..e56c0567 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -55,7 +55,7 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { location: Location::caller().clone(), }, // location: Location::caller().clone(), // TODO: This needs to actually be correct - inner: layer_to_procedure(key, kind, p.exec), + inner: Box::new(move |_| layer_to_procedure(key, kind, p.exec)), }, ) }); diff --git a/rspc/src/modern/middleware/next.rs b/rspc/src/modern/middleware/next.rs index 5a155b1e..3d529fc9 100644 --- a/rspc/src/modern/middleware/next.rs +++ b/rspc/src/modern/middleware/next.rs @@ -1,8 +1,9 @@ -use std::fmt; +use std::{fmt, sync::Arc}; -use futures::Stream; - -use crate::modern::{middleware::middleware::MiddlewareHandler, procedure::ProcedureMeta}; +use crate::{ + modern::{middleware::middleware::MiddlewareHandler, procedure::ProcedureMeta}, + State, +}; pub struct Next { // TODO: `pub(super)` over `pub(crate)` diff --git a/rspc/src/modern/mod.rs b/rspc/src/modern/mod.rs index 34b46d90..e3ff8319 100644 --- a/rspc/src/modern/mod.rs +++ b/rspc/src/modern/mod.rs @@ -11,5 +11,3 @@ pub use error::Error; pub use infallible::Infallible; pub use state::State; pub use stream::Stream; - -pub use rspc_core::DynInput; diff --git a/rspc/src/modern/procedure/meta.rs b/rspc/src/modern/procedure/meta.rs index bdea8183..303da6ef 100644 --- a/rspc/src/modern/procedure/meta.rs +++ b/rspc/src/modern/procedure/meta.rs @@ -19,6 +19,7 @@ use std::{borrow::Cow, sync::Arc}; // } pub use crate::ProcedureKind; +use crate::State; #[derive(Debug, Clone)] enum ProcedureName { @@ -30,13 +31,15 @@ enum ProcedureName { pub struct ProcedureMeta { name: ProcedureName, kind: ProcedureKind, + state: Arc, } impl ProcedureMeta { - pub(crate) fn new(name: Cow<'static, str>, kind: ProcedureKind) -> Self { + pub(crate) fn new(name: Cow<'static, str>, kind: ProcedureKind, state: Arc) -> Self { Self { name: ProcedureName::Dynamic(Arc::new(name.into_owned())), kind, + state, } } } @@ -52,4 +55,8 @@ impl ProcedureMeta { pub fn kind(&self) -> ProcedureKind { self.kind } + + pub fn state(&self) -> &Arc { + &self.state + } } diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 6a623e0f..07c272a6 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, panic::Location}; +use std::{borrow::Cow, panic::Location, sync::Arc}; -use futures::TryStreamExt; +use futures::{FutureExt, TryStreamExt}; use rspc_core::Procedure; use specta::datatype::DataType; @@ -28,7 +28,7 @@ pub(crate) struct ProcedureType { pub struct Procedure2 { pub(crate) setup: Vec>, pub(crate) ty: ProcedureType, - pub(crate) inner: rspc_core::Procedure, + pub(crate) inner: Box) -> rspc_core::Procedure>, } // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` @@ -45,8 +45,6 @@ impl Procedure2 { I: ResolverInput, R: ResolverOutput, { - use futures::{FutureExt, Stream}; - ProcedureBuilder { build: Box::new(|kind, setups, handler| { Procedure2 { @@ -58,60 +56,24 @@ impl Procedure2 { error: DataType::Any, // TODO location: Location::caller().clone(), }, - inner: Procedure::new(move |ctx, input| { - // let input: I = I::from_input(input).unwrap(); // TODO: Error handling - - // let key = "todo".to_string().into(); // TODO: Work this out properly - - // let meta = ProcedureMeta::new(key.clone(), kind); - // for setup in setups { - // setup(state, meta.clone()); - // } - - // Procedure { - // kind, - // ty: ProcedureTypeDefinition { - // key, - // kind, - // input: I::data_type(type_map), - // result: R::data_type(type_map), - // }, - // handler: Arc::new(move |ctx, input| { - // let fut = handler( - // ctx, - // I::from_value(ProcedureExecInput::new(input))?, - // meta.clone(), - // ); - - // Ok(R::into_procedure_stream(fut.into_stream())) - // }), - // } - - // let fut = handler( - // ctx, - // I::from_value(ProcedureExecInput::new(input))?, - // meta.clone(), - // ); - - // Ok(R::into_procedure_stream(fut.into_stream())) - - // TODO: borrow into procedure + inner: Box::new(move |state| { let key: Cow<'static, str> = "todo".to_string().into(); // TODO: Work this out properly - let meta = ProcedureMeta::new(key.clone(), kind); - // TODO: END - - R::into_procedure_stream( - handler( - ctx, - I::from_input(input).unwrap(), // TODO: Error handling - meta.clone(), + let meta = ProcedureMeta::new(key.clone(), kind, state); + + Procedure::new(move |ctx, input| { + R::into_procedure_stream( + handler( + ctx, + I::from_input(input).unwrap(), // TODO: Error handling + meta.clone(), + ) + .into_stream() + .map_ok(|v| v.into_stream()) + .map_err(|err| err.into_resolver_error()) + .try_flatten() + .into_stream(), ) - .into_stream() - .map_ok(|v| v.into_stream()) - .map_err(|err| err.into_resolver_error()) - .try_flatten() - .into_stream(), - ) + }) }), } }), diff --git a/rspc/src/router.rs b/rspc/src/router.rs index f91318eb..609d63df 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -2,6 +2,7 @@ use std::{ borrow::{Borrow, Cow}, collections::{BTreeMap, HashMap}, fmt, + sync::Arc, }; use specta::TypeCollection; @@ -111,6 +112,7 @@ impl Router2 { for setup in self.setup { setup(&mut state); } + let state = Arc::new(state); let mut procedure_types = BTreeMap::new(); let procedures = self @@ -130,7 +132,7 @@ impl Router2 { } current.insert(key[key.len() - 1].clone(), TypesOrType::Type(p.ty)); - (get_flattened_name(&key), p.inner) + (get_flattened_name(&key), (p.inner)(state.clone())) }) .collect::>(); From 7e95b09f4879794aaeada50b3d8339391abf6099 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 10 Dec 2024 00:17:16 +0800 Subject: [PATCH 37/67] working memory cache --- examples/axum/src/main.rs | 9 ++++- middleware/cache/src/lib.rs | 33 +++++++++++----- middleware/cache/src/memory.rs | 22 +++++++++++ middleware/cache/src/state.rs | 12 +++++- middleware/cache/src/store.rs | 69 +++++++++++++++++++--------------- rspc/src/modern/state.rs | 4 +- 6 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 middleware/cache/src/memory.rs diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 82b4aa4f..e8402b52 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,4 +1,9 @@ -use std::{marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + marker::PhantomData, + path::PathBuf, + sync::Arc, + time::{Duration, SystemTime}, +}; use async_stream::stream; use axum::routing::get; @@ -150,7 +155,7 @@ fn test_unstable_stuff(router: Router2) -> Router2 { // if input.some_arg {} cache_ttl(10); - Ok(env!("CARGO_PKG_VERSION")) + Ok(SystemTime::now()) }) }) } diff --git a/middleware/cache/src/lib.rs b/middleware/cache/src/lib.rs index 7e5414a1..69355d98 100644 --- a/middleware/cache/src/lib.rs +++ b/middleware/cache/src/lib.rs @@ -6,34 +6,49 @@ html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" )] +mod memory; mod state; mod store; +pub use memory::Memory; pub use state::CacheState; -pub use store::{Memory, Store}; +pub use store::Store; use rspc::middleware::Middleware; +use store::Value; pub fn cache() -> Middleware where TError: Send + 'static, - TCtx: Clone + Send + 'static, + TCtx: Send + 'static, TInput: Clone + Send + 'static, - TResult: Send + 'static, + TResult: Clone + Send + Sync + 'static, { Middleware::new(move |ctx: TCtx, input: TInput, next| async move { - let cache = next.meta().state().get::().unwrap(); // TODO: Error handling + let meta = next.meta(); + let cache = meta.state().get::().unwrap(); // TODO: Error handling - // let cache = CacheState::builder(Arc::new(Memory)).mount(); + let key = "todo"; // TODO: Work this out properly + // TODO: Keyed to `TInput` + + if let Some(value) = cache.store().get(key) { + let value: &TResult = value.downcast_ref().unwrap(); // TODO: Error + return Ok(value.clone()); + } + + let result: Result = next.exec(ctx, input).await; + + // TODO: Caching error responses? + if let Ok(value) = &result { + // TODO: Get ttl from `cache_tll` + cache.store().set(key, Value::new(value.clone()), 0); + }; - let result = next.exec(ctx, input).await; - // TODO: Get cache tll - // TODO: Use `Store` result }) } /// Set the cache time-to-live (TTL) in seconds pub fn cache_ttl(ttl: usize) { - todo!(); + // TODO: Implement } diff --git a/middleware/cache/src/memory.rs b/middleware/cache/src/memory.rs new file mode 100644 index 00000000..fbe31474 --- /dev/null +++ b/middleware/cache/src/memory.rs @@ -0,0 +1,22 @@ +use moka::sync::Cache; + +use crate::{store::Value, Store}; + +pub struct Memory(Cache); + +impl Memory { + pub fn new() -> Self { + Self(Cache::new(100)) // TODO: Configurable + } +} + +impl Store for Memory { + fn get(&self, key: &str) -> Option { + self.0.get(key).map(|v| v.clone()) + } + + fn set(&self, key: &str, value: Value, ttl: usize) { + assert_eq!(ttl, 0); // TODO: Implement TTL + self.0.insert(key.to_string(), value); + } +} diff --git a/middleware/cache/src/state.rs b/middleware/cache/src/state.rs index 2ed89d2a..a49e9ff2 100644 --- a/middleware/cache/src/state.rs +++ b/middleware/cache/src/state.rs @@ -13,10 +13,18 @@ impl CacheState { Self { store } } + pub fn store(&self) -> &S { + &self.store + } + + // TODO: Default ttl + pub fn mount(self) -> impl FnOnce(&mut State) { - let cache = CacheState::builder(Arc::new(self.store)); + let cache: Arc = Arc::new(self.store); + let cache: CacheState> = CacheState::>::builder(cache); move |state: &mut State| { - state.insert(cache); + println!("SETUP"); // TODO + state.insert::>>(cache); } } } diff --git a/middleware/cache/src/store.rs b/middleware/cache/src/store.rs index 8d1d9236..622a5b22 100644 --- a/middleware/cache/src/store.rs +++ b/middleware/cache/src/store.rs @@ -1,23 +1,42 @@ use std::{any::Any, sync::Arc}; -use moka::sync::Cache; - pub trait Store: Send + Sync + 'static { - // fn get(&self, key: &str) -> Option; - // fn set(&self, key: &str, value: &V, ttl: usize); + fn get(&self, key: &str) -> Option; + + fn set(&self, key: &str, value: Value, ttl: usize); +} + +impl Store for Arc { + fn get(&self, key: &str) -> Option { + self.as_ref().get(key) + } + + fn set(&self, key: &str, value: Value, ttl: usize) { + self.as_ref().set(key, value, ttl) + } } impl Store for Arc { - // fn get(&self, key: &str) -> Option { - // self.as_ref().get(key) - // } + fn get(&self, key: &str) -> Option { + self.as_ref().get(key) + } - // fn set(&self, key: &str, value: &V, ttl: usize) { - // self.as_ref().set(key, value, ttl) - // } + fn set(&self, key: &str, value: Value, ttl: usize) { + self.as_ref().set(key, value, ttl) + } } -struct Value(Box); +pub struct Value(Box); + +impl Value { + pub fn new(v: T) -> Self { + Self(Box::new(v)) + } + + pub fn downcast_ref(&self) -> Option<&T> { + self.0.inner().downcast_ref() + } +} impl Clone for Value { fn clone(&self) -> Self { @@ -26,30 +45,18 @@ impl Clone for Value { } // TODO: Sealing this better. -pub trait DynClone: Send + Sync { +trait Repr: Send + Sync + 'static { // Return `Value` instead of `Box` directly for sealing - fn dyn_clone(&self) -> Box; + fn dyn_clone(&self) -> Box; + + fn inner(&self) -> &dyn Any; } -impl DynClone for T { - fn dyn_clone(&self) -> Box { +impl Repr for T { + fn dyn_clone(&self) -> Box { Box::new(self.clone()) } -} - -pub struct Memory(Cache); -impl Memory { - pub fn new() -> Self { - Self(Cache::new(100)) // TODO: Configurable + fn inner(&self) -> &dyn Any { + self } } - -impl Store for Memory { - // fn get(&self, key: &str) -> Option { - // self.0.get(key).map(|v| v.downcast_ref().clone()) - // } - - // fn set(&self, key: &str, value: &V, ttl: usize) { - // todo!() - // } -} diff --git a/rspc/src/modern/state.rs b/rspc/src/modern/state.rs index e08775b3..3de3c241 100644 --- a/rspc/src/modern/state.rs +++ b/rspc/src/modern/state.rs @@ -32,9 +32,7 @@ pub struct State( impl fmt::Debug for State { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("State") - // .field("state", &self.state) // TODO - .finish() + f.debug_tuple("State").field(&self.0.keys()).finish() } } From f0965efbf748b0173783fd5a9b703fc893be5595 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 10 Dec 2024 00:31:25 +0800 Subject: [PATCH 38/67] make cache work --- middleware/cache/Cargo.toml | 1 + middleware/cache/src/lib.rs | 96 +++++++++++++++++++++++++++------- middleware/cache/src/memory.rs | 2 +- middleware/cache/src/state.rs | 6 +-- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/middleware/cache/Cargo.toml b/middleware/cache/Cargo.toml index f0dc02dc..dea790a7 100644 --- a/middleware/cache/Cargo.toml +++ b/middleware/cache/Cargo.toml @@ -6,6 +6,7 @@ publish = false [dependencies] moka = { version = "0.12.8", features = ["sync"] } +pin-project-lite = "0.2.15" rspc = { path = "../../rspc", features = ["unstable"] } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/middleware/cache/src/lib.rs b/middleware/cache/src/lib.rs index 69355d98..6b62801d 100644 --- a/middleware/cache/src/lib.rs +++ b/middleware/cache/src/lib.rs @@ -10,6 +10,12 @@ mod memory; mod state; mod store; +use std::{ + cell::Cell, + future::{poll_fn, Future}, + pin::pin, +}; + pub use memory::Memory; pub use state::CacheState; pub use store::Store; @@ -17,6 +23,15 @@ pub use store::Store; use rspc::middleware::Middleware; use store::Value; +thread_local! { + static CACHE_TTL: Cell> = Cell::new(None); +} + +/// Set the cache time-to-live (TTL) in seconds +pub fn cache_ttl(ttl: usize) { + CACHE_TTL.set(Some(ttl)); +} + pub fn cache() -> Middleware where TError: Send + 'static, @@ -24,31 +39,72 @@ where TInput: Clone + Send + 'static, TResult: Clone + Send + Sync + 'static, { - Middleware::new(move |ctx: TCtx, input: TInput, next| async move { - let meta = next.meta(); - let cache = meta.state().get::().unwrap(); // TODO: Error handling + // let todo = poll_fn(|cx| { + // todo!(); + // }); - let key = "todo"; // TODO: Work this out properly - // TODO: Keyed to `TInput` + Middleware::new(move |ctx: TCtx, input: TInput, next| { + async move { + let meta = next.meta(); + let cache = meta.state().get::().unwrap(); // TODO: Error handling - if let Some(value) = cache.store().get(key) { - let value: &TResult = value.downcast_ref().unwrap(); // TODO: Error - return Ok(value.clone()); - } + let key = "todo"; // TODO: Work this out properly + // TODO: Keyed to `TInput` + + if let Some(value) = cache.store().get(key) { + let value: &TResult = value.downcast_ref().unwrap(); // TODO: Error + return Ok(value.clone()); + } + + let fut = next.exec(ctx, input); + let mut fut = pin!(fut); - let result: Result = next.exec(ctx, input).await; + let (ttl, result): (Option, Result) = + poll_fn(|cx| fut.as_mut().poll(cx).map(|v| (CACHE_TTL.get(), v))).await; - // TODO: Caching error responses? - if let Ok(value) = &result { - // TODO: Get ttl from `cache_tll` - cache.store().set(key, Value::new(value.clone()), 0); - }; + // self.project().fut.poll(cx).map(|v| { + // if let Some(ttl) = CACHE_TTL.get() { + // println!("REQUESTED CACHE {:?}", ttl); + // } - result + // v + // }) + + // TODO: Only cache if `ttl` is actually set + if let Some(ttl) = CACHE_TTL.get() { + // TODO: Caching error responses? + if let Ok(value) = &result { + cache.store().set(key, Value::new(value.clone()), ttl); + }; + } + + result + } }) } -/// Set the cache time-to-live (TTL) in seconds -pub fn cache_ttl(ttl: usize) { - // TODO: Implement -} +// pin_project! { +// struct Cached { +// #[pin] +// fut: F, +// } +// } + +// impl Future for Cached { +// type Output = F::Output; + +// fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { +// // match self.project().fut.poll(cx) { +// // Poll::Ready(_) => todo!(), +// // Poll::Pending => todo!(), +// // } + +// self.project().fut.poll(cx).map(|v| { +// if let Some(ttl) = CACHE_TTL.get() { +// println!("REQUESTED CACHE {:?}", ttl); +// } + +// v +// }) +// } +// } diff --git a/middleware/cache/src/memory.rs b/middleware/cache/src/memory.rs index fbe31474..596138c0 100644 --- a/middleware/cache/src/memory.rs +++ b/middleware/cache/src/memory.rs @@ -16,7 +16,7 @@ impl Store for Memory { } fn set(&self, key: &str, value: Value, ttl: usize) { - assert_eq!(ttl, 0); // TODO: Implement TTL + // TODO: Properly set ttl self.0.insert(key.to_string(), value); } } diff --git a/middleware/cache/src/state.rs b/middleware/cache/src/state.rs index a49e9ff2..07452cf2 100644 --- a/middleware/cache/src/state.rs +++ b/middleware/cache/src/state.rs @@ -20,11 +20,9 @@ impl CacheState { // TODO: Default ttl pub fn mount(self) -> impl FnOnce(&mut State) { - let cache: Arc = Arc::new(self.store); - let cache: CacheState> = CacheState::>::builder(cache); + let cache = CacheState::>::builder(Arc::new(self.store)); move |state: &mut State| { - println!("SETUP"); // TODO - state.insert::>>(cache); + state.insert(cache); } } } From a413734dbdfcd6b26d4b1f77b0b669b30ad5c8c6 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 10 Dec 2024 00:33:28 +0800 Subject: [PATCH 39/67] cleanup cache middleware --- middleware/cache/src/lib.rs | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/middleware/cache/src/lib.rs b/middleware/cache/src/lib.rs index 6b62801d..aafef76a 100644 --- a/middleware/cache/src/lib.rs +++ b/middleware/cache/src/lib.rs @@ -62,16 +62,7 @@ where let (ttl, result): (Option, Result) = poll_fn(|cx| fut.as_mut().poll(cx).map(|v| (CACHE_TTL.get(), v))).await; - // self.project().fut.poll(cx).map(|v| { - // if let Some(ttl) = CACHE_TTL.get() { - // println!("REQUESTED CACHE {:?}", ttl); - // } - - // v - // }) - - // TODO: Only cache if `ttl` is actually set - if let Some(ttl) = CACHE_TTL.get() { + if let Some(ttl) = ttl { // TODO: Caching error responses? if let Ok(value) = &result { cache.store().set(key, Value::new(value.clone()), ttl); @@ -82,29 +73,3 @@ where } }) } - -// pin_project! { -// struct Cached { -// #[pin] -// fut: F, -// } -// } - -// impl Future for Cached { -// type Output = F::Output; - -// fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { -// // match self.project().fut.poll(cx) { -// // Poll::Ready(_) => todo!(), -// // Poll::Pending => todo!(), -// // } - -// self.project().fut.poll(cx).map(|v| { -// if let Some(ttl) = CACHE_TTL.get() { -// println!("REQUESTED CACHE {:?}", ttl); -// } - -// v -// }) -// } -// } From 5ccc33adfa7f2f2f1d4705297c1057c62fc495bf Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 10 Dec 2024 13:32:38 +0800 Subject: [PATCH 40/67] tauri example + handleRpc --- Cargo.toml | 12 +- examples/tauri/.gitignore | 2 + examples/tauri/.vscode/extensions.json | 3 + examples/tauri/README.md | 7 + examples/tauri/index.html | 17 + examples/tauri/package.json | 26 ++ examples/tauri/public/tauri.svg | 6 + examples/tauri/public/vite.svg | 1 + examples/tauri/src-tauri/.gitignore | 7 + examples/tauri/src-tauri/Cargo.toml | 26 ++ examples/tauri/src-tauri/build.rs | 3 + .../tauri/src-tauri/capabilities/default.json | 7 + examples/tauri/src-tauri/icons/128x128.png | Bin 0 -> 3512 bytes examples/tauri/src-tauri/icons/128x128@2x.png | Bin 0 -> 7012 bytes examples/tauri/src-tauri/icons/32x32.png | Bin 0 -> 974 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 2863 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 3858 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 3966 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 7737 bytes .../tauri/src-tauri/icons/Square30x30Logo.png | Bin 0 -> 903 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 8591 bytes .../tauri/src-tauri/icons/Square44x44Logo.png | Bin 0 -> 1299 bytes .../tauri/src-tauri/icons/Square71x71Logo.png | Bin 0 -> 2011 bytes .../tauri/src-tauri/icons/Square89x89Logo.png | Bin 0 -> 2468 bytes examples/tauri/src-tauri/icons/StoreLogo.png | Bin 0 -> 1523 bytes examples/tauri/src-tauri/icons/icon.icns | Bin 0 -> 98451 bytes examples/tauri/src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes examples/tauri/src-tauri/icons/icon.png | Bin 0 -> 14183 bytes examples/tauri/src-tauri/src/lib.rs | 23 ++ examples/tauri/src-tauri/src/main.rs | 6 + examples/tauri/src-tauri/tauri.conf.json | 35 ++ examples/tauri/src/App.css | 116 +++++++ examples/tauri/src/App.tsx | 18 + examples/tauri/src/assets/logo.svg | 1 + examples/tauri/src/index.tsx | 5 + examples/tauri/src/vite-env.d.ts | 1 + examples/tauri/tsconfig.json | 26 ++ examples/tauri/tsconfig.node.json | 10 + examples/tauri/vite.config.ts | 32 ++ integrations/tauri/Cargo.toml | 14 +- integrations/tauri/build.rs | 5 + .../autogenerated/commands/handle_rpc.toml | 13 + .../permissions/autogenerated/reference.md | 41 +++ integrations/tauri/permissions/default.toml | 4 + .../tauri/permissions/schemas/schema.json | 315 ++++++++++++++++++ integrations/tauri/src/lib.rs | 85 +++-- packages/tauri/src/index.ts | 78 +---- packages/tauri/src/old.ts | 72 ++++ pnpm-lock.yaml | 143 ++++++++ pnpm-workspace.yaml | 7 +- 50 files changed, 1056 insertions(+), 111 deletions(-) create mode 100644 examples/tauri/.gitignore create mode 100644 examples/tauri/.vscode/extensions.json create mode 100644 examples/tauri/README.md create mode 100644 examples/tauri/index.html create mode 100644 examples/tauri/package.json create mode 100644 examples/tauri/public/tauri.svg create mode 100644 examples/tauri/public/vite.svg create mode 100644 examples/tauri/src-tauri/.gitignore create mode 100644 examples/tauri/src-tauri/Cargo.toml create mode 100644 examples/tauri/src-tauri/build.rs create mode 100644 examples/tauri/src-tauri/capabilities/default.json create mode 100644 examples/tauri/src-tauri/icons/128x128.png create mode 100644 examples/tauri/src-tauri/icons/128x128@2x.png create mode 100644 examples/tauri/src-tauri/icons/32x32.png create mode 100644 examples/tauri/src-tauri/icons/Square107x107Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square142x142Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square150x150Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square284x284Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square30x30Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square310x310Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square44x44Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square71x71Logo.png create mode 100644 examples/tauri/src-tauri/icons/Square89x89Logo.png create mode 100644 examples/tauri/src-tauri/icons/StoreLogo.png create mode 100644 examples/tauri/src-tauri/icons/icon.icns create mode 100644 examples/tauri/src-tauri/icons/icon.ico create mode 100644 examples/tauri/src-tauri/icons/icon.png create mode 100644 examples/tauri/src-tauri/src/lib.rs create mode 100644 examples/tauri/src-tauri/src/main.rs create mode 100644 examples/tauri/src-tauri/tauri.conf.json create mode 100644 examples/tauri/src/App.css create mode 100644 examples/tauri/src/App.tsx create mode 100644 examples/tauri/src/assets/logo.svg create mode 100644 examples/tauri/src/index.tsx create mode 100644 examples/tauri/src/vite-env.d.ts create mode 100644 examples/tauri/tsconfig.json create mode 100644 examples/tauri/tsconfig.node.json create mode 100644 examples/tauri/vite.config.ts create mode 100644 integrations/tauri/build.rs create mode 100644 integrations/tauri/permissions/autogenerated/commands/handle_rpc.toml create mode 100644 integrations/tauri/permissions/autogenerated/reference.md create mode 100644 integrations/tauri/permissions/default.toml create mode 100644 integrations/tauri/permissions/schemas/schema.json create mode 100644 packages/tauri/src/old.ts diff --git a/Cargo.toml b/Cargo.toml index 00b2888d..c5c601e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,16 @@ [workspace] resolver = "2" -members = ["./rspc", "./core", "./client", "./integrations/*", "./middleware/*", "./examples", "./examples/axum", "./examples/client"] +members = [ + "./rspc", + "./core", + "./client", + "./integrations/*", + "./middleware/*", + "./examples", + "./examples/axum", + "./examples/client", + "./examples/tauri/src-tauri", +] [workspace.lints.clippy] all = { level = "warn", priority = -1 } diff --git a/examples/tauri/.gitignore b/examples/tauri/.gitignore new file mode 100644 index 00000000..76add878 --- /dev/null +++ b/examples/tauri/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/examples/tauri/.vscode/extensions.json b/examples/tauri/.vscode/extensions.json new file mode 100644 index 00000000..24d7cc6d --- /dev/null +++ b/examples/tauri/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/examples/tauri/README.md b/examples/tauri/README.md new file mode 100644 index 00000000..648e2c16 --- /dev/null +++ b/examples/tauri/README.md @@ -0,0 +1,7 @@ +# Tauri + Solid + Typescript + +This template should help get you started developing with Tauri, Solid and Typescript in Vite. + +## Recommended IDE Setup + +- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/examples/tauri/index.html b/examples/tauri/index.html new file mode 100644 index 00000000..9c2276ad --- /dev/null +++ b/examples/tauri/index.html @@ -0,0 +1,17 @@ + + + + + + + + Tauri + Solid + Typescript App + + + + +
+ + + + diff --git a/examples/tauri/package.json b/examples/tauri/package.json new file mode 100644 index 00000000..20145f76 --- /dev/null +++ b/examples/tauri/package.json @@ -0,0 +1,26 @@ +{ + "name": "tauri", + "version": "0.1.0", + "description": "", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "tauri": "tauri" + }, + "license": "MIT", + "dependencies": { + "solid-js": "^1.7.8", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-shell": "^2", + "@rspc/tauri": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.2.2", + "vite": "^5.3.1", + "vite-plugin-solid": "^2.8.0", + "@tauri-apps/cli": "^2" + } +} diff --git a/examples/tauri/public/tauri.svg b/examples/tauri/public/tauri.svg new file mode 100644 index 00000000..31b62c92 --- /dev/null +++ b/examples/tauri/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/tauri/public/vite.svg b/examples/tauri/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/tauri/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tauri/src-tauri/.gitignore b/examples/tauri/src-tauri/.gitignore new file mode 100644 index 00000000..b21bd681 --- /dev/null +++ b/examples/tauri/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml new file mode 100644 index 00000000..0a147478 --- /dev/null +++ b/examples/tauri/src-tauri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tauri" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "tauri_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rspc = { path = "../../../rspc", features = ["typescript", "rust", "unstable"] } +tauri-plugin-rspc = { path = "../../../integrations/tauri" } diff --git a/examples/tauri/src-tauri/build.rs b/examples/tauri/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/examples/tauri/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/examples/tauri/src-tauri/capabilities/default.json b/examples/tauri/src-tauri/capabilities/default.json new file mode 100644 index 00000000..6880ac4b --- /dev/null +++ b/examples/tauri/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": ["core:default", "shell:allow-open", "rspc:default"] +} diff --git a/examples/tauri/src-tauri/icons/128x128.png b/examples/tauri/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/128x128@2x.png b/examples/tauri/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/Square107x107Logo.png b/examples/tauri/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca4f27198838968bd60ed7d371bfa23496b7fe5 GIT binary patch literal 2863 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 Y2S8W#vM)6=T>t<807*qoM6N<$f*y@n<^TWy literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/Square284x284Logo.png b/examples/tauri/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c021d2ba76619c08969ab688db3b27f29257aa6f GIT binary patch literal 7737 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 literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/Square30x30Logo.png b/examples/tauri/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..621970023096ed9f494ba18ace15421a45cd65fa GIT binary patch literal 903 zcmV;219<$2P)2 z+CUKPMqaqGiH;zb!R4$B-WXS^YzQr=@UH>k4?*L)&R=zYjBrZenKdc9|JlS$SO*RJ zKt8FSTDAdk1g_WPAO!p^V!AuL;Lm;uQyV;zKq)J3i(;q*;k+pD%f3eltU`PYdy9(k0&%` zuWAPcV6|-y?|?7O1W!KSK}pbk8#~!|FA@(VJkt^V@0lio{afoAeo*f&$W2s6${5!1eKvAGD2$GZwSB98L2ZVS- zKn8ENRkZ*sb!@QugOrQNK3(sy1v%J#m|rpB+h|Nkqa3FRT>74xSs{#&saU2Lf!_Iq zKmuKAESh`gs!fneGWn+nf}l?7jE$HW!Af&vE5=G!QU)U2v&HLIBGXKk4nQx{hsHjL zLPMAo5=*uInFbq7(aa`Y2VX5wCmaeqvECOFv)a>0t>ZaEb*cJccER=BB?KFZhV$c^ znL*l8x*UYZv4WK|j?~Jt6~~F%{pk~z5A*>^M`?r5m9@RJ_x|uEtX(6Vk@Y()MVto* z93wr)%3m%|#OZ~srm>zF(JvDuTq*@;d&^>_BJm5hOU`3FjG70L#Vzv9I?`<7$T@

jU?lMi@tgxr7CqX_r3uw^y4tVU3Pm0sw;|1WSUO%?=bG`*Kmz6u4{#ti;T7AWIBAEh!(Y zz>O01&#X?Ds@L)Sb{CkG#Yz4$3o d@96)?#cz^xWoA}>B$xmI002ovPDHLkV1l3&k#zt7 literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/Square310x310Logo.png b/examples/tauri/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bc04839491e66c07b16ab03743c0c53b4109cc GIT binary patch literal 8591 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 JPDHLkV1lm|RXG3v literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/Square71x71Logo.png b/examples/tauri/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..63440d7984936a9caa89275928d8dce97e4d033b GIT binary patch literal 2011 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∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/icon.ico b/examples/tauri/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/examples/tauri/src-tauri/icons/icon.png b/examples/tauri/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let router = Router2::new().procedure( + "query", + Procedure2::builder().query(|_, _: ()| async { Ok::<(), Infallible>(()) }), + ); + let (procedures, types) = router.build().unwrap(); + + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_rspc::init(procedures, |_| {})) + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/tauri/src-tauri/src/main.rs b/examples/tauri/src-tauri/src/main.rs new file mode 100644 index 00000000..21cf973d --- /dev/null +++ b/examples/tauri/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri_lib::run() +} diff --git a/examples/tauri/src-tauri/tauri.conf.json b/examples/tauri/src-tauri/tauri.conf.json new file mode 100644 index 00000000..861c9c8b --- /dev/null +++ b/examples/tauri/src-tauri/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "tauri", + "version": "0.1.0", + "identifier": "dev.specta.rspc.desktop", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "tauri", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/examples/tauri/src/App.css b/examples/tauri/src/App.css new file mode 100644 index 00000000..233df203 --- /dev/null +++ b/examples/tauri/src/App.css @@ -0,0 +1,116 @@ +.logo.vite:hover { + filter: drop-shadow(0 0 2em #747bff); +} + +.logo.solid:hover { + filter: drop-shadow(0 0 2em #2f5d90); +} +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} diff --git a/examples/tauri/src/App.tsx b/examples/tauri/src/App.tsx new file mode 100644 index 00000000..bb4cc856 --- /dev/null +++ b/examples/tauri/src/App.tsx @@ -0,0 +1,18 @@ +import { createSignal } from "solid-js"; +import logo from "./assets/logo.svg"; +import { invoke } from "@tauri-apps/api/core"; +import "./App.css"; + +import { handleRpc } from "@rspc/tauri"; + +function App() { + handleRpc({ method: "request", params: { path: "query", input: null } }); + + return ( +

+

Welcome to Tauri + Solid

+
+ ); +} + +export default App; diff --git a/examples/tauri/src/assets/logo.svg b/examples/tauri/src/assets/logo.svg new file mode 100644 index 00000000..025aa303 --- /dev/null +++ b/examples/tauri/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tauri/src/index.tsx b/examples/tauri/src/index.tsx new file mode 100644 index 00000000..26b55174 --- /dev/null +++ b/examples/tauri/src/index.tsx @@ -0,0 +1,5 @@ +/* @refresh reload */ +import { render } from "solid-js/web"; +import App from "./App"; + +render(() => , document.getElementById("root") as HTMLElement); diff --git a/examples/tauri/src/vite-env.d.ts b/examples/tauri/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/tauri/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/tauri/tsconfig.json b/examples/tauri/tsconfig.json new file mode 100644 index 00000000..39999584 --- /dev/null +++ b/examples/tauri/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/tauri/tsconfig.node.json b/examples/tauri/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/examples/tauri/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/tauri/vite.config.ts b/examples/tauri/vite.config.ts new file mode 100644 index 00000000..80474aab --- /dev/null +++ b/examples/tauri/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; + +// @ts-expect-error process is a nodejs global +const host = process.env.TAURI_DEV_HOST; + +// https://vitejs.dev/config/ +export default defineConfig(async () => ({ + plugins: [solid()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, +})); diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index ba20138b..33ad0cea 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rspc-tauri" +name = "tauri-plugin-rspc" description = "Tauri adapter for rspc" version = "0.1.1" authors = ["Oscar Beaumont "] @@ -9,6 +9,7 @@ repository = "https://github.com/specta-rs/rspc" documentation = "https://docs.rs/rspc-axum/latest/rspc-axum" keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] categories = ["web-programming", "asynchronous"] +links = "tauri-plugin-rspc" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] @@ -18,8 +19,15 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] rspc-core = { version = "0.0.1", path = "../../core" } tauri = "2" -serde = { version = "1", features = ["derive"] } # is a dependency of Tauri anyway -serde_json = { version = "1", features = ["raw_value"] } # is a dependency of Tauri anyway +serde = { version = "1", features = [ + "derive", +] } # is a dependency of Tauri anyway +serde_json = { version = "1", features = [ + "raw_value", +] } # is a dependency of Tauri anyway [lints] workspace = true + +[build-dependencies] +tauri-plugin = { version = "2.0.0", features = ["build"] } diff --git a/integrations/tauri/build.rs b/integrations/tauri/build.rs new file mode 100644 index 00000000..f6a4e852 --- /dev/null +++ b/integrations/tauri/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &["handle_rpc"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/integrations/tauri/permissions/autogenerated/commands/handle_rpc.toml b/integrations/tauri/permissions/autogenerated/commands/handle_rpc.toml new file mode 100644 index 00000000..3cff88c4 --- /dev/null +++ b/integrations/tauri/permissions/autogenerated/commands/handle_rpc.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-handle-rpc" +description = "Enables the handle_rpc command without any pre-configured scope." +commands.allow = ["handle_rpc"] + +[[permission]] +identifier = "deny-handle-rpc" +description = "Denies the handle_rpc command without any pre-configured scope." +commands.deny = ["handle_rpc"] diff --git a/integrations/tauri/permissions/autogenerated/reference.md b/integrations/tauri/permissions/autogenerated/reference.md new file mode 100644 index 00000000..e62d280a --- /dev/null +++ b/integrations/tauri/permissions/autogenerated/reference.md @@ -0,0 +1,41 @@ +## Default Permission + +Allows making rspc requests + +- `allow-handle-rpc` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`rspc:allow-handle-rpc` + + + +Enables the handle_rpc command without any pre-configured scope. + +
+ +`rspc:deny-handle-rpc` + + + +Denies the handle_rpc command without any pre-configured scope. + +
diff --git a/integrations/tauri/permissions/default.toml b/integrations/tauri/permissions/default.toml new file mode 100644 index 00000000..28ca77de --- /dev/null +++ b/integrations/tauri/permissions/default.toml @@ -0,0 +1,4 @@ +"$schema" = "schemas/schema.json" +[default] +description = "Allows making rspc requests" +permissions = ["allow-handle-rpc"] diff --git a/integrations/tauri/permissions/schemas/schema.json b/integrations/tauri/permissions/schemas/schema.json new file mode 100644 index 00000000..39731813 --- /dev/null +++ b/integrations/tauri/permissions/schemas/schema.json @@ -0,0 +1,315 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the handle_rpc command without any pre-configured scope.", + "type": "string", + "const": "allow-handle-rpc" + }, + { + "description": "Denies the handle_rpc command without any pre-configured scope.", + "type": "string", + "const": "deny-handle-rpc" + }, + { + "description": "Allows making rspc requests", + "type": "string", + "const": "default" + } + ] + } + } +} \ No newline at end of file diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 375a77a4..7012b1ac 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -1,5 +1,5 @@ //! rspc-tauri: Tauri integration for [rspc](https://rspc.dev). -#![forbid(unsafe_code)] +// #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", @@ -14,7 +14,7 @@ use std::{ use rspc_core::{ProcedureError, Procedures}; use serde::{de::Error, Deserialize, Serialize}; -use serde_json::{value::RawValue, Serializer}; +use serde_json::value::RawValue; use tauri::{ async_runtime::{spawn, JoinHandle}, generate_handler, @@ -30,6 +30,13 @@ struct RpcHandler { phantom: std::marker::PhantomData R>, } +// unsafe impl Send for RpcHandler +// where +// TCtxFn: Send, +// TCtx: Send, +// { +// } + impl RpcHandler where R: tauri::Runtime, @@ -55,8 +62,14 @@ where let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { let err = ProcedureError::NotFound; - send(&channel, Some((err.status(), &err))); - send::<()>(&channel, None); + send( + &channel, + Response::Value { + code: err.status(), + value: &err, + }, + ); + send::<()>(&channel, Response::Done); return; }; @@ -69,13 +82,25 @@ where let handle = spawn(async move { while let Some(value) = stream.next().await { match value { - Ok(v) => send(&channel, Some((200, &v))), - Err(err) => send(&channel, Some((err.status(), &err))), + Ok(v) => send( + &channel, + Response::Value { + code: 200, + value: &v, + }, + ), + Err(err) => send( + &channel, + Response::Value { + code: err.status(), + value: &err, + }, + ), } } this.subscriptions().remove(&id); - send::<()>(&channel, None); + send::<()>(&channel, Response::Done); }); // if the client uses an existing ID, we will assume the previous subscription is no longer required @@ -103,7 +128,7 @@ trait HandleRpc: Send + Sync { impl HandleRpc for RpcHandler where - R: tauri::Runtime + Send + Sync, + R: tauri::Runtime, TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, TCtx: Send + 'static, { @@ -133,12 +158,12 @@ fn handle_rpc( state.0.clone().handle_rpc(window, channel, req); } -pub fn plugin( - procedures: impl AsRef>, +pub fn init( + procedures: impl Into>, ctx_fn: TCtxFn, ) -> TauriPlugin where - R: tauri::Runtime + Send + Sync, + R: tauri::Runtime, TCtxFn: Fn(tauri::Window) -> TCtx + Send + Sync + 'static, TCtx: Send + Sync + 'static, { @@ -175,32 +200,20 @@ enum Request<'a> { Abort(u32), } -fn send(channel: &Channel, value: Option<(u16, &T)>) { - #[derive(Serialize)] - struct Response<'a, T: Serialize> { - code: u16, - value: &'a T, - } +#[derive(Serialize)] +enum Response<'a, T: Serialize> { + Value { code: u16, value: &'a T }, + Done, +} - match value { - Some((code, value)) => { - let mut buffer = Vec::with_capacity(128); - let mut serializer = Serializer::new(&mut buffer); - channel - .send(IpcResultResponse( - Response { code, value } - .serialize(&mut serializer) - .map(|_: ()| InvokeResponseBody::Raw(buffer)) - .map_err(|err| err.to_string()), - )) - .ok() - } - None => channel - .send(IpcResultResponse(Ok(InvokeResponseBody::Raw( - "DONE".into(), - )))) - .ok(), - }; +fn send<'a, T: Serialize>(channel: &Channel, value: Response<'a, T>) { + channel + .send(IpcResultResponse( + serde_json::to_string(&value) + .map(|value| InvokeResponseBody::Json(value)) + .map_err(|err| err.to_string()), + )) + .ok(); } #[derive(Clone)] diff --git a/packages/tauri/src/index.ts b/packages/tauri/src/index.ts index d258e8f1..06e135a2 100644 --- a/packages/tauri/src/index.ts +++ b/packages/tauri/src/index.ts @@ -1,72 +1,14 @@ -import { randomId, OperationType, Transport, RSPCError } from "@rspc/client"; -import { listen, UnlistenFn } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { Channel, invoke } from "@tauri-apps/api/core"; -export class TauriTransport implements Transport { - private requestMap = new Map void>(); - private listener?: Promise; - clientSubscriptionCallback?: (id: string, value: any) => void; +type Request = + | { + method: "request"; + params: { path: string; input: null | string }; + } + | { method: "abort"; params: number }; - constructor() { - this.listener = listen("plugin:rspc:transport:resp", (event) => { - const { id, result } = event.payload as any; - if (result.type === "event") { - if (this.clientSubscriptionCallback) - this.clientSubscriptionCallback(id, result.data); - } else if (result.type === "response") { - if (this.requestMap.has(id)) { - this.requestMap.get(id)?.({ type: "response", result: result.data }); - this.requestMap.delete(id); - } - } else if (result.type === "error") { - const { message, code } = result.data; - if (this.requestMap.has(id)) { - this.requestMap.get(id)?.({ type: "error", message, code }); - this.requestMap.delete(id); - } - } else { - console.error(`Received event of unknown method '${result.type}'`); - } - }); - } +type Response = { Value: { code: number; value: T } } | "Done"; - async doRequest( - operation: OperationType, - key: string, - input: any - ): Promise { - if (!this.listener) { - await this.listener; - } - - const id = randomId(); - let resolve: (data: any) => void; - const promise = new Promise((res) => { - resolve = res; - }); - - // @ts-ignore - this.requestMap.set(id, resolve); - - await getCurrentWindow().emit("plugin:rspc:transport", { - id, - method: operation, - params: { - path: key, - input, - }, - }); - - const body = (await promise) as any; - if (body.type === "error") { - const { code, message } = body; - throw new RSPCError(code, message); - } else if (body.type === "response") { - return body.result; - } else { - throw new Error( - `RSPC Tauri doRequest received invalid body type '${body?.type}'` - ); - } - } +export async function handleRpc(req: Request, channel: Channel>) { + await invoke("plugin:rspc|handle_rpc", { req, channel }); } diff --git a/packages/tauri/src/old.ts b/packages/tauri/src/old.ts new file mode 100644 index 00000000..d258e8f1 --- /dev/null +++ b/packages/tauri/src/old.ts @@ -0,0 +1,72 @@ +import { randomId, OperationType, Transport, RSPCError } from "@rspc/client"; +import { listen, UnlistenFn } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; + +export class TauriTransport implements Transport { + private requestMap = new Map void>(); + private listener?: Promise; + clientSubscriptionCallback?: (id: string, value: any) => void; + + constructor() { + this.listener = listen("plugin:rspc:transport:resp", (event) => { + const { id, result } = event.payload as any; + if (result.type === "event") { + if (this.clientSubscriptionCallback) + this.clientSubscriptionCallback(id, result.data); + } else if (result.type === "response") { + if (this.requestMap.has(id)) { + this.requestMap.get(id)?.({ type: "response", result: result.data }); + this.requestMap.delete(id); + } + } else if (result.type === "error") { + const { message, code } = result.data; + if (this.requestMap.has(id)) { + this.requestMap.get(id)?.({ type: "error", message, code }); + this.requestMap.delete(id); + } + } else { + console.error(`Received event of unknown method '${result.type}'`); + } + }); + } + + async doRequest( + operation: OperationType, + key: string, + input: any + ): Promise { + if (!this.listener) { + await this.listener; + } + + const id = randomId(); + let resolve: (data: any) => void; + const promise = new Promise((res) => { + resolve = res; + }); + + // @ts-ignore + this.requestMap.set(id, resolve); + + await getCurrentWindow().emit("plugin:rspc:transport", { + id, + method: operation, + params: { + path: key, + input, + }, + }); + + const body = (await promise) as any; + if (body.type === "error") { + const { code, message } = body; + throw new RSPCError(code, message); + } else if (body.type === "response") { + return body.result; + } else { + throw new Error( + `RSPC Tauri doRequest received invalid body type '${body?.type}'` + ); + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 893bb4bc..7ebc00e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,34 @@ importers: specifier: ^5.6.3 version: 5.6.3 + examples/tauri: + dependencies: + '@rspc/tauri': + specifier: workspace:* + version: link:../../packages/tauri + '@tauri-apps/api': + specifier: ^2 + version: 2.1.1 + '@tauri-apps/plugin-shell': + specifier: ^2 + version: 2.2.0 + solid-js: + specifier: ^1.7.8 + version: 1.9.3 + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.1.0 + typescript: + specifier: ^5.2.2 + version: 5.6.3 + vite: + specifier: ^5.3.1 + version: 5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0) + vite-plugin-solid: + specifier: ^2.8.0 + version: 2.10.2(solid-js@1.9.3)(vite@5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0)) + packages/client: devDependencies: tsup: @@ -1468,6 +1496,74 @@ packages: '@tauri-apps/api@2.1.1': resolution: {integrity: sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==} + '@tauri-apps/cli-darwin-arm64@2.1.0': + resolution: {integrity: sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.1.0': + resolution: {integrity: sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.1.0': + resolution: {integrity: sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.1.0': + resolution: {integrity: sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-arm64-musl@2.1.0': + resolution: {integrity: sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-x64-gnu@2.1.0': + resolution: {integrity: sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-linux-x64-musl@2.1.0': + resolution: {integrity: sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-win32-arm64-msvc@2.1.0': + resolution: {integrity: sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.1.0': + resolution: {integrity: sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.1.0': + resolution: {integrity: sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.1.0': + resolution: {integrity: sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-shell@2.2.0': + resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -5013,6 +5109,53 @@ snapshots: '@tauri-apps/api@2.1.1': {} + '@tauri-apps/cli-darwin-arm64@2.1.0': + optional: true + + '@tauri-apps/cli-darwin-x64@2.1.0': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.1.0': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.1.0': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.1.0': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.1.0': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.1.0': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.1.0': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.1.0': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.1.0': + optional: true + + '@tauri-apps/cli@2.1.0': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.1.0 + '@tauri-apps/cli-darwin-x64': 2.1.0 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.1.0 + '@tauri-apps/cli-linux-arm64-gnu': 2.1.0 + '@tauri-apps/cli-linux-arm64-musl': 2.1.0 + '@tauri-apps/cli-linux-x64-gnu': 2.1.0 + '@tauri-apps/cli-linux-x64-musl': 2.1.0 + '@tauri-apps/cli-win32-arm64-msvc': 2.1.0 + '@tauri-apps/cli-win32-ia32-msvc': 2.1.0 + '@tauri-apps/cli-win32-x64-msvc': 2.1.0 + + '@tauri-apps/plugin-shell@2.2.0': + dependencies: + '@tauri-apps/api': 2.1.1 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 349fff80..e52380e8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: - - packages/* - - examples/astro - - examples/nextjs + - packages/* + - examples/astro + - examples/nextjs + - examples/tauri From c8f80549d41e286b51c067cde7945bac4b2be402 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 10 Dec 2024 14:22:16 +0800 Subject: [PATCH 41/67] drop `rspc::Infallible` + untagged on `tauri-plugin-rspc::Response` --- examples/tauri/src-tauri/Cargo.toml | 12 ++--- .../tauri/src-tauri/capabilities/default.json | 10 ++-- examples/tauri/src-tauri/src/api.rs | 30 ++++++++++++ examples/tauri/src-tauri/src/lib.rs | 13 ++--- examples/tauri/src/App.tsx | 21 ++++---- integrations/tauri/src/lib.rs | 10 +--- packages/tauri/src/index.ts | 21 ++++---- rspc/src/lib.rs | 2 +- rspc/src/modern/infallible.rs | 48 +++++++++---------- rspc/src/modern/mod.rs | 2 +- 10 files changed, 98 insertions(+), 71 deletions(-) create mode 100644 examples/tauri/src-tauri/src/api.rs diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index 0a147478..a7e1e685 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -1,11 +1,9 @@ [package] -name = "tauri" -version = "0.1.0" +name = "example-tauri" +version = "0.0.0" description = "A Tauri App" -authors = ["you"] edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +publish = false [lib] # The `_lib` suffix may seem redundant but it is necessary @@ -19,8 +17,8 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } -tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -rspc = { path = "../../../rspc", features = ["typescript", "rust", "unstable"] } +rspc = { path = "../../../rspc", features = ["typescript", "unstable"] } tauri-plugin-rspc = { path = "../../../integrations/tauri" } +specta = { version = "=2.0.0-rc.20", features = ["derive"] } diff --git a/examples/tauri/src-tauri/capabilities/default.json b/examples/tauri/src-tauri/capabilities/default.json index 6880ac4b..19bdbff3 100644 --- a/examples/tauri/src-tauri/capabilities/default.json +++ b/examples/tauri/src-tauri/capabilities/default.json @@ -1,7 +1,7 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], - "permissions": ["core:default", "shell:allow-open", "rspc:default"] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": ["core:default", "rspc:default"] } diff --git a/examples/tauri/src-tauri/src/api.rs b/examples/tauri/src-tauri/src/api.rs new file mode 100644 index 00000000..71f76e2f --- /dev/null +++ b/examples/tauri/src-tauri/src/api.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use serde::Serialize; +use specta::Type; + +#[derive(Type, Debug)] +pub enum Infallible {} + +impl fmt::Display for Infallible { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl Serialize for Infallible { + fn serialize(&self, _: S) -> Result + where + S: serde::Serializer, + { + unreachable!() + } +} + +impl std::error::Error for Infallible {} + +impl rspc::Error2 for Infallible { + fn into_resolver_error(self) -> rspc::ResolverError { + unreachable!() + } +} diff --git a/examples/tauri/src-tauri/src/lib.rs b/examples/tauri/src-tauri/src/lib.rs index c2294f53..8986603f 100644 --- a/examples/tauri/src-tauri/src/lib.rs +++ b/examples/tauri/src-tauri/src/lib.rs @@ -1,13 +1,12 @@ -use rspc::{Infallible, Procedure2, Router2}; +use api::Infallible; +use rspc::{Procedure2, Router2}; -// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ -#[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) -} +mod api; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + // TODO: Show proper setup + let router = Router2::new().procedure( "query", Procedure2::builder().query(|_, _: ()| async { Ok::<(), Infallible>(()) }), @@ -15,9 +14,7 @@ pub fn run() { let (procedures, types) = router.build().unwrap(); tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_rspc::init(procedures, |_| {})) - .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/examples/tauri/src/App.tsx b/examples/tauri/src/App.tsx index bb4cc856..cf2d2c67 100644 --- a/examples/tauri/src/App.tsx +++ b/examples/tauri/src/App.tsx @@ -1,18 +1,21 @@ -import { createSignal } from "solid-js"; -import logo from "./assets/logo.svg"; -import { invoke } from "@tauri-apps/api/core"; +import { Channel } from "@tauri-apps/api/core"; import "./App.css"; import { handleRpc } from "@rspc/tauri"; function App() { - handleRpc({ method: "request", params: { path: "query", input: null } }); + const channel = new Channel(); + handleRpc( + { method: "request", params: { path: "query", input: null } }, + channel, + ); + channel.onmessage = console.log; - return ( -
-

Welcome to Tauri + Solid

-
- ); + return ( +
+

Welcome to Tauri + Solid

+
+ ); } export default App; diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 7012b1ac..2d742fdb 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -1,5 +1,5 @@ //! rspc-tauri: Tauri integration for [rspc](https://rspc.dev). -// #![forbid(unsafe_code)] +#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", @@ -30,13 +30,6 @@ struct RpcHandler { phantom: std::marker::PhantomData R>, } -// unsafe impl Send for RpcHandler -// where -// TCtxFn: Send, -// TCtx: Send, -// { -// } - impl RpcHandler where R: tauri::Runtime, @@ -201,6 +194,7 @@ enum Request<'a> { } #[derive(Serialize)] +#[serde(untagged)] enum Response<'a, T: Serialize> { Value { code: u16, value: &'a T }, Done, diff --git a/packages/tauri/src/index.ts b/packages/tauri/src/index.ts index 06e135a2..b5bc6a1e 100644 --- a/packages/tauri/src/index.ts +++ b/packages/tauri/src/index.ts @@ -1,14 +1,19 @@ -import { Channel, invoke } from "@tauri-apps/api/core"; +import { type Channel, invoke } from "@tauri-apps/api/core"; type Request = - | { - method: "request"; - params: { path: string; input: null | string }; - } - | { method: "abort"; params: number }; + | { + method: "request"; + params: { path: string; input: null | string }; + } + | { method: "abort"; params: number }; -type Response = { Value: { code: number; value: T } } | "Done"; +type Response = { code: number; value: T } | "Done"; + +// TODO: Seal `Channel` within a standard interface for all "modern links"? +// TODO: handle detect and converting to rspc error class +// TODO: Catch Tauri errors -> Assuming it would happen on `tauri::Error` which happens when serialization fails in Rust. +// TODO: Return closure for cleanup export async function handleRpc(req: Request, channel: Channel>) { - await invoke("plugin:rspc|handle_rpc", { req, channel }); + await invoke("plugin:rspc|handle_rpc", { req, channel }); } diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index 148a9142..2ff4a396 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -36,7 +36,7 @@ pub(crate) use procedure::Procedure2; #[cfg(feature = "unstable")] pub use modern::{ middleware, procedure::ProcedureBuilder, procedure::ProcedureMeta, procedure::ResolverInput, - procedure::ResolverOutput, Error as Error2, Infallible, State, Stream, + procedure::ResolverOutput, Error as Error2, State, Stream, }; #[cfg(feature = "unstable")] pub use procedure::Procedure2; diff --git a/rspc/src/modern/infallible.rs b/rspc/src/modern/infallible.rs index 7a2aa5f7..8773c7ee 100644 --- a/rspc/src/modern/infallible.rs +++ b/rspc/src/modern/infallible.rs @@ -1,30 +1,30 @@ -use std::fmt; +// use std::fmt; -use serde::Serialize; -use specta::Type; +// use serde::Serialize; +// use specta::Type; -#[derive(Type, Debug)] -pub enum Infallible {} +// #[derive(Type, Debug)] +// pub enum Infallible {} -impl fmt::Display for Infallible { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} +// impl fmt::Display for Infallible { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// write!(f, "{self:?}") +// } +// } -impl Serialize for Infallible { - fn serialize(&self, _: S) -> Result - where - S: serde::Serializer, - { - unreachable!() - } -} +// impl Serialize for Infallible { +// fn serialize(&self, _: S) -> Result +// where +// S: serde::Serializer, +// { +// unreachable!() +// } +// } -impl std::error::Error for Infallible {} +// impl std::error::Error for Infallible {} -impl crate::modern::Error for Infallible { - fn into_resolver_error(self) -> rspc_core::ResolverError { - unreachable!() - } -} +// impl crate::modern::Error for Infallible { +// fn into_resolver_error(self) -> rspc_core::ResolverError { +// unreachable!() +// } +// } diff --git a/rspc/src/modern/mod.rs b/rspc/src/modern/mod.rs index e3ff8319..5c5d7732 100644 --- a/rspc/src/modern/mod.rs +++ b/rspc/src/modern/mod.rs @@ -8,6 +8,6 @@ mod stream; // pub use crate::procedure::Procedure2; pub use error::Error; -pub use infallible::Infallible; +// pub use infallible::Infallible; pub use state::State; pub use stream::Stream; From a2635800d1c8d5fc470c44c4ac203291c11fb579 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Dec 2024 16:40:57 +0800 Subject: [PATCH 42/67] duplicate procedure errors --- core/src/procedure.rs | 2 +- rspc/src/modern/middleware/middleware.rs | 18 +-- rspc/src/router.rs | 164 +++++++++++++++++++++-- rspc/src/types.rs | 8 +- 4 files changed, 167 insertions(+), 25 deletions(-) diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 65863788..318591d0 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -53,6 +53,6 @@ impl Clone for Procedure { impl fmt::Debug for Procedure { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!(); + f.debug_struct("Procedure").finish() } } diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index b037e275..a15a3eb0 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -55,15 +55,15 @@ pub(crate) type MiddlewareHandler = A /// - `TNextResult` - // TODO /// /// TODO: [ -// Context of previous layer (`ctx`), -// Error type, -// The input to the middleware (`input`), -// The result of the middleware (return type of future), -// - This following will default to the input types if not explicitly provided // TODO: Will this be confusing or good? -// The context returned by the middleware (`next.exec({dis_bit}, ...)`), -// The input to the next layer (`next.exec(..., {dis_bit})`), -// The result of the next layer (`let _result: {dis_bit} = next.exec(...)`), -// ] +/// Context of previous layer (`ctx`), +/// Error type, +/// The input to the middleware (`input`), +/// The result of the middleware (return type of future), +/// - This following will default to the input types if not explicitly provided // TODO: Will this be confusing or good? +/// The context returned by the middleware (`next.exec({dis_bit}, ...)`), +/// The input to the next layer (`next.exec(..., {dis_bit})`), +/// The result of the next layer (`let _result: {dis_bit} = next.exec(...)`), +/// ] /// /// ```rust /// TODO: Example to show where the generics line up. diff --git a/rspc/src/router.rs b/rspc/src/router.rs index 609d63df..1985fcaf 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -16,6 +16,7 @@ pub struct Router2 { setup: Vec>, types: TypeCollection, procedures: BTreeMap>, Procedure2>, + errors: Vec, } impl Default for Router2 { @@ -24,6 +25,7 @@ impl Default for Router2 { setup: Default::default(), types: Default::default(), procedures: Default::default(), + errors: vec![], } } } @@ -40,8 +42,15 @@ impl Router2 { key: impl Into>, mut procedure: Procedure2, ) -> Self { - self.setup.extend(procedure.setup.drain(..)); - self.procedures.insert(vec![key.into()], procedure); + let key = key.into(); + + if self.procedures.keys().any(|k| k[0] == key) { + self.errors.push(Error::DuplicateProcedures(vec![key])) + } else { + self.setup.extend(procedure.setup.drain(..)); + self.procedures.insert(vec![key], procedure); + } + self } @@ -54,22 +63,51 @@ impl Router2 { // TODO: Yield error if key already exists pub fn nest(mut self, prefix: impl Into>, mut other: Self) -> Self { - self.setup.append(&mut other.setup); - let prefix = prefix.into(); - self.procedures - .extend(other.procedures.into_iter().map(|(mut k, v)| { - k.push(prefix.clone()); - (k, v) - })); + dbg!(&self.procedures.keys().collect::>()); + if self.procedures.keys().any(|k| k[0] == prefix) { + self.errors.push(Error::DuplicateProcedures(vec![prefix])); + } else { + self.setup.append(&mut other.setup); + + self.procedures + .extend(other.procedures.into_iter().map(|(k, v)| { + let mut new_key = vec![prefix.clone()]; + new_key.extend(k); + (new_key, v) + })); + + self.errors + .extend(other.errors.into_iter().map(|e| match e { + Error::DuplicateProcedures(key) => { + let mut new_key = vec![prefix.clone()]; + new_key.extend(key); + Error::DuplicateProcedures(new_key) + } + })); + } + self } // TODO: Yield error if key already exists pub fn merge(mut self, mut other: Self) -> Self { - self.setup.append(&mut other.setup); - self.procedures.extend(other.procedures.into_iter()); + let error_count = self.errors.len(); + + for other_proc in other.procedures.keys() { + if self.procedures.get(other_proc).is_some() { + self.errors + .push(Error::DuplicateProcedures(other_proc.clone())); + } + } + + if self.errors.len() > error_count { + self.setup.append(&mut other.setup); + self.procedures.extend(other.procedures.into_iter()); + self.errors.extend(other.errors); + } + self } @@ -80,7 +118,7 @@ impl Router2 { impl Borrow> + Into> + fmt::Debug, Types, ), - (), + Vec, > { self.build_with_state_inner(State::default()) } @@ -94,7 +132,7 @@ impl Router2 { impl Borrow> + Into> + fmt::Debug, Types, ), - (), + Vec, > { self.build_with_state_inner(state) } @@ -107,8 +145,12 @@ impl Router2 { impl Borrow> + Into> + fmt::Debug, Types, ), - (), + Vec, > { + if self.errors.len() > 0 { + return Err(self.errors); + } + for setup in self.setup { setup(&mut state); } @@ -163,6 +205,11 @@ impl Router2 { } } +#[derive(Debug, PartialEq)] +pub enum Error { + DuplicateProcedures(Vec>), +} + impl fmt::Debug for Router2 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let procedure_keys = |kind: ProcedureKind| { @@ -222,3 +269,92 @@ fn get_flattened_name(name: &Vec>) -> Cow<'static, str> { name.join(".").to_string().into() } } + +#[cfg(test)] +mod test { + use rspc_core::ResolverError; + use serde::Serialize; + use specta::Type; + + use super::*; + + #[test] + fn errors() { + let router = ::new() + .procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ) + .procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ); + + assert_eq!( + router.build().unwrap_err(), + vec![Error::DuplicateProcedures(vec!["abc".into()])] + ); + + let router = ::new() + .procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ) + .merge(::new().procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + )); + + assert_eq!( + router.build().unwrap_err(), + vec![Error::DuplicateProcedures(vec!["abc".into()])] + ); + + let router = ::new() + .nest( + "abc", + ::new().procedure( + "kjl", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ), + ) + .nest( + "abc", + ::new().procedure( + "def", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ), + ); + + assert_eq!( + router.build().unwrap_err(), + vec![Error::DuplicateProcedures(vec!["abc".into()])] + ); + } + + #[derive(Type, Debug)] + pub enum Infallible {} + + impl fmt::Display for Infallible { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } + } + + impl Serialize for Infallible { + fn serialize(&self, _: S) -> Result + where + S: serde::Serializer, + { + unreachable!() + } + } + + impl std::error::Error for Infallible {} + + impl crate::modern::Error for Infallible { + fn into_resolver_error(self) -> ResolverError { + unreachable!() + } + } +} diff --git a/rspc/src/types.rs b/rspc/src/types.rs index 3768fb4d..38ba5f73 100644 --- a/rspc/src/types.rs +++ b/rspc/src/types.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::BTreeMap}; +use std::{borrow::Cow, collections::BTreeMap, fmt}; use specta::TypeCollection; @@ -15,6 +15,12 @@ pub struct Types { pub(crate) procedures: BTreeMap, TypesOrType>, } +impl fmt::Debug for Types { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Types").finish() + } +} + // TODO: Traits impl Types { From cc19437e81ebe5d8758617336252ca7f28fe03c1 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 11 Dec 2024 20:19:40 +0800 Subject: [PATCH 43/67] `Procedures` as struct & include line number on duplicate procedure key error --- core/src/lib.rs | 5 +- core/src/procedure.rs | 17 +- core/src/procedures.rs | 63 +++++++ rspc/src/modern/middleware/middleware.rs | 2 +- rspc/src/procedure.rs | 5 +- rspc/src/router.rs | 215 +++++++---------------- rspc/src/types.rs | 4 +- rspc/tests/router.rs | 80 +++++++++ 8 files changed, 225 insertions(+), 166 deletions(-) create mode 100644 core/src/procedures.rs create mode 100644 rspc/tests/router.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index 90bf207a..ca741316 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -18,13 +18,12 @@ mod dyn_input; mod error; mod interop; mod procedure; +mod procedures; mod stream; pub use dyn_input::DynInput; pub use error::{DeserializeError, DowncastError, ProcedureError, ResolverError}; pub use interop::LegacyErrorInterop; pub use procedure::Procedure; +pub use procedures::Procedures; pub use stream::ProcedureStream; - -pub type Procedures = - std::collections::HashMap, Procedure>; diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 318591d0..011b8a60 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -1,4 +1,4 @@ -use std::{fmt, sync::Arc}; +use std::{any::type_name, fmt, sync::Arc}; use serde::Deserializer; @@ -9,14 +9,18 @@ use crate::{DynInput, ProcedureStream}; /// TODO: Show constructing and executing procedure. pub struct Procedure { handler: Arc ProcedureStream + Send + Sync>, + #[cfg(debug_assertions)] + handler_name: &'static str, } impl Procedure { - pub fn new( - handler: impl Fn(TCtx, DynInput) -> ProcedureStream + Send + Sync + 'static, + pub fn new ProcedureStream + Send + Sync + 'static>( + handler: F, ) -> Self { Self { handler: Arc::new(handler), + #[cfg(debug_assertions)] + handler_name: type_name::(), } } @@ -47,12 +51,17 @@ impl Clone for Procedure { fn clone(&self) -> Self { Self { handler: self.handler.clone(), + #[cfg(debug_assertions)] + handler_name: self.handler_name, } } } impl fmt::Debug for Procedure { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Procedure").finish() + let mut t = f.debug_tuple("Procedure"); + #[cfg(debug_assertions)] + let t = t.field(&self.handler_name); + t.finish() } } diff --git a/core/src/procedures.rs b/core/src/procedures.rs new file mode 100644 index 00000000..c3a8f563 --- /dev/null +++ b/core/src/procedures.rs @@ -0,0 +1,63 @@ +use std::{ + borrow::Cow, + collections::HashMap, + fmt, + ops::{Deref, DerefMut}, +}; + +use crate::Procedure; + +pub struct Procedures(HashMap, Procedure>); + +impl From, Procedure>> for Procedures { + fn from(procedures: HashMap, Procedure>) -> Self { + Self(procedures.into_iter().map(|(k, v)| (k.into(), v)).collect()) + } +} + +impl Clone for Procedures { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Into> for &Procedures { + fn into(self) -> Procedures { + self.clone() + } +} + +impl fmt::Debug for Procedures { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.0.iter()).finish() + } +} + +impl IntoIterator for Procedures { + type Item = (Cow<'static, str>, Procedure); + type IntoIter = std::collections::hash_map::IntoIter, Procedure>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator<(Cow<'static, str>, Procedure)> for Procedures { + fn from_iter, Procedure)>>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl Deref for Procedures { + type Target = HashMap, Procedure>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Procedures { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index a15a3eb0..1a097b06 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -66,7 +66,7 @@ pub(crate) type MiddlewareHandler = A /// ] /// /// ```rust -/// TODO: Example to show where the generics line up. +/// // TODO: Example to show where the generics line up. /// ``` /// /// # Stacking diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 07c272a6..7034aeb9 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -45,8 +45,9 @@ impl Procedure2 { I: ResolverInput, R: ResolverOutput, { + let location = Location::caller().clone(); ProcedureBuilder { - build: Box::new(|kind, setups, handler| { + build: Box::new(move |kind, setups, handler| { Procedure2 { setup: Default::default(), ty: ProcedureType { @@ -54,7 +55,7 @@ impl Procedure2 { input: DataType::Any, // I::data_type(type_map), output: DataType::Any, // R::data_type(type_map), error: DataType::Any, // TODO - location: Location::caller().clone(), + location, }, inner: Box::new(move |state| { let key: Cow<'static, str> = "todo".to_string().into(); // TODO: Work this out properly diff --git a/rspc/src/router.rs b/rspc/src/router.rs index 1985fcaf..fdfbca56 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -2,6 +2,7 @@ use std::{ borrow::{Borrow, Cow}, collections::{BTreeMap, HashMap}, fmt, + panic::Location, sync::Arc, }; @@ -16,7 +17,7 @@ pub struct Router2 { setup: Vec>, types: TypeCollection, procedures: BTreeMap>, Procedure2>, - errors: Vec, + errors: Vec, } impl Default for Router2 { @@ -35,8 +36,8 @@ impl Router2 { Self::default() } - // TODO: Enforce unique across all methods (query, subscription, etc). Eg. `insert` should yield error if key already exists. #[cfg(feature = "unstable")] + #[track_caller] pub fn procedure( mut self, key: impl Into>, @@ -44,8 +45,12 @@ impl Router2 { ) -> Self { let key = key.into(); - if self.procedures.keys().any(|k| k[0] == key) { - self.errors.push(Error::DuplicateProcedures(vec![key])) + if let Some((_, original)) = self.procedures.iter().find(|(k, _)| k[0] == key) { + self.errors.push(DuplicateProcedureKeyError { + path: vec![key], + original: original.ty.location, + duplicate: Location::caller().clone(), + }); } else { self.setup.extend(procedure.setup.drain(..)); self.procedures.insert(vec![key], procedure); @@ -61,44 +66,45 @@ impl Router2 { self } - // TODO: Yield error if key already exists + #[track_caller] pub fn nest(mut self, prefix: impl Into>, mut other: Self) -> Self { let prefix = prefix.into(); - dbg!(&self.procedures.keys().collect::>()); - if self.procedures.keys().any(|k| k[0] == prefix) { - self.errors.push(Error::DuplicateProcedures(vec![prefix])); + if let Some((_, original)) = self.procedures.iter().find(|(k, _)| k[0] == prefix) { + self.errors.push(DuplicateProcedureKeyError { + path: vec![prefix], + original: original.ty.location, + duplicate: Location::caller().clone(), + }); } else { self.setup.append(&mut other.setup); - + self.errors.extend(other.errors.into_iter().map(|e| { + let mut path = vec![prefix.clone()]; + path.extend(e.path); + DuplicateProcedureKeyError { path, ..e } + })); self.procedures .extend(other.procedures.into_iter().map(|(k, v)| { - let mut new_key = vec![prefix.clone()]; - new_key.extend(k); - (new_key, v) - })); - - self.errors - .extend(other.errors.into_iter().map(|e| match e { - Error::DuplicateProcedures(key) => { - let mut new_key = vec![prefix.clone()]; - new_key.extend(key); - Error::DuplicateProcedures(new_key) - } + let mut key = vec![prefix.clone()]; + key.extend(k); + (key, v) })); } self } - // TODO: Yield error if key already exists + #[track_caller] pub fn merge(mut self, mut other: Self) -> Self { let error_count = self.errors.len(); - for other_proc in other.procedures.keys() { - if self.procedures.get(other_proc).is_some() { - self.errors - .push(Error::DuplicateProcedures(other_proc.clone())); + for (k, original) in other.procedures.iter() { + if let Some(new) = self.procedures.get(k) { + self.errors.push(DuplicateProcedureKeyError { + path: k.clone(), + original: original.ty.location, + duplicate: new.ty.location, + }); } } @@ -111,15 +117,7 @@ impl Router2 { self } - pub fn build( - self, - ) -> Result< - ( - impl Borrow> + Into> + fmt::Debug, - Types, - ), - Vec, - > { + pub fn build(self) -> Result<(Procedures, Types), Vec> { self.build_with_state_inner(State::default()) } @@ -127,26 +125,14 @@ impl Router2 { pub fn build_with_state( self, state: State, - ) -> Result< - ( - impl Borrow> + Into> + fmt::Debug, - Types, - ), - Vec, - > { + ) -> Result<(Procedures, Types), Vec> { self.build_with_state_inner(state) } fn build_with_state_inner( self, mut state: State, - ) -> Result< - ( - impl Borrow> + Into> + fmt::Debug, - Types, - ), - Vec, - > { + ) -> Result<(Procedures, Types), Vec> { if self.errors.len() > 0 { return Err(self.errors); } @@ -178,25 +164,8 @@ impl Router2 { }) .collect::>(); - struct Impl(Procedures); - impl Into> for Impl { - fn into(self) -> Procedures { - self.0 - } - } - impl Borrow> for Impl { - fn borrow(&self) -> &Procedures { - &self.0 - } - } - impl fmt::Debug for Impl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.0) - } - } - Ok(( - Impl::(procedures), + Procedures::from(procedures), Types { types: self.types, procedures: procedure_types, @@ -205,11 +174,6 @@ impl Router2 { } } -#[derive(Debug, PartialEq)] -pub enum Error { - DuplicateProcedures(Vec>), -} - impl fmt::Debug for Router2 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let procedure_keys = |kind: ProcedureKind| { @@ -270,91 +234,32 @@ fn get_flattened_name(name: &Vec>) -> Cow<'static, str> { } } -#[cfg(test)] -mod test { - use rspc_core::ResolverError; - use serde::Serialize; - use specta::Type; - - use super::*; - - #[test] - fn errors() { - let router = ::new() - .procedure( - "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), - ) - .procedure( - "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), - ); - - assert_eq!( - router.build().unwrap_err(), - vec![Error::DuplicateProcedures(vec!["abc".into()])] - ); - - let router = ::new() - .procedure( - "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), - ) - .merge(::new().procedure( - "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), - )); - - assert_eq!( - router.build().unwrap_err(), - vec![Error::DuplicateProcedures(vec!["abc".into()])] - ); - - let router = ::new() - .nest( - "abc", - ::new().procedure( - "kjl", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), - ), - ) - .nest( - "abc", - ::new().procedure( - "def", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), - ), - ); - - assert_eq!( - router.build().unwrap_err(), - vec![Error::DuplicateProcedures(vec!["abc".into()])] - ); - } - - #[derive(Type, Debug)] - pub enum Infallible {} - - impl fmt::Display for Infallible { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } - } +pub struct DuplicateProcedureKeyError { + path: Vec>, + original: Location<'static>, + duplicate: Location<'static>, +} - impl Serialize for Infallible { - fn serialize(&self, _: S) -> Result - where - S: serde::Serializer, - { - unreachable!() - } +impl fmt::Debug for DuplicateProcedureKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!( + f, + "Duplicate procedure at path {:?}. Original: {}:{}:{} Duplicate: {}:{}:{}", + self.path, + self.original.file(), + self.original.line(), + self.original.column(), + self.duplicate.file(), + self.duplicate.line(), + self.duplicate.column() + ) } +} - impl std::error::Error for Infallible {} - - impl crate::modern::Error for Infallible { - fn into_resolver_error(self) -> ResolverError { - unreachable!() - } +impl fmt::Display for DuplicateProcedureKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") } } + +impl std::error::Error for DuplicateProcedureKeyError {} diff --git a/rspc/src/types.rs b/rspc/src/types.rs index 38ba5f73..91519d20 100644 --- a/rspc/src/types.rs +++ b/rspc/src/types.rs @@ -17,7 +17,9 @@ pub struct Types { impl fmt::Debug for Types { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Types").finish() + f.debug_struct("Types") + // TODO: Finish this + .finish() } } diff --git a/rspc/tests/router.rs b/rspc/tests/router.rs new file mode 100644 index 00000000..f85419db --- /dev/null +++ b/rspc/tests/router.rs @@ -0,0 +1,80 @@ +use std::fmt; + +use rspc::{Procedure2, Router2}; +use rspc_core::ResolverError; +use serde::Serialize; +use specta::Type; + +#[test] +fn errors() { + let router = ::new() + .procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ) + .procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ); + + assert_eq!( + format!("{:?}", router.build().unwrap_err()), + "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:13:13 Duplicate: rspc/tests/router.rs:15:10\n]" + ); + + let router = ::new() + .procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ) + .merge(::new().procedure( + "abc", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + )); + + assert_eq!(format!("{:?}", router.build().unwrap_err()), "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:32:13 Duplicate: rspc/tests/router.rs:28:13\n]"); + + let router = ::new() + .nest( + "abc", + ::new().procedure( + "kjl", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ), + ) + .nest( + "abc", + ::new().procedure( + "def", + Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + ), + ); + + assert_eq!(format!("{:?}", router.build().unwrap_err()), "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:42:17 Duplicate: rspc/tests/router.rs:45:10\n]"); +} + +#[derive(Type, Debug)] +pub enum Infallible {} + +impl fmt::Display for Infallible { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl Serialize for Infallible { + fn serialize(&self, _: S) -> Result + where + S: serde::Serializer, + { + unreachable!() + } +} + +impl std::error::Error for Infallible {} + +impl rspc::Error2 for Infallible { + fn into_resolver_error(self) -> ResolverError { + unreachable!() + } +} From 6a49b0dcbac78da0dbf74664f5a101d1d3098812 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 17 Dec 2024 03:26:52 +0800 Subject: [PATCH 44/67] basic tauri integration query/mutation working --- examples/axum/src/lib.rs | 191 ++++++++++++++ examples/axum/src/main.rs | 190 +------------- examples/bindings.ts | 22 +- examples/tauri/package.json | 1 + examples/tauri/src-tauri/Cargo.toml | 1 + examples/tauri/src-tauri/src/lib.rs | 12 +- examples/tauri/src/App.tsx | 25 +- integrations/tauri/src/lib.rs | 8 +- packages/client/package.json | 93 +++---- packages/client/src/next/UntypedClient.ts | 134 ++++++++++ packages/client/src/next/index.test.ts | 35 +++ packages/client/src/next/index.ts | 122 +++++++++ packages/client/src/next/types.ts | 51 ++++ packages/client/tsconfig.json | 13 +- packages/query-core/src/index.ts | 20 ++ packages/solid-query/src/index.tsx | 8 +- packages/tanstack-query/package.json | 50 ++++ packages/tanstack-query/src/index.ts | 288 ++++++++++++++++++++++ packages/tanstack-query/tsconfig.json | 8 + packages/tauri/package.json | 8 +- packages/tauri/src/index.ts | 81 ++++-- packages/tauri/src/next.ts | 59 +++++ packages/tauri/src/old.ts | 72 ------ packages/tauri/tsconfig.json | 5 +- pnpm-lock.yaml | 29 ++- rspc/src/languages/typescript.rs | 13 +- 26 files changed, 1173 insertions(+), 366 deletions(-) create mode 100644 examples/axum/src/lib.rs create mode 100644 packages/client/src/next/UntypedClient.ts create mode 100644 packages/client/src/next/index.test.ts create mode 100644 packages/client/src/next/index.ts create mode 100644 packages/client/src/next/types.ts create mode 100644 packages/tanstack-query/package.json create mode 100644 packages/tanstack-query/src/index.ts create mode 100644 packages/tanstack-query/tsconfig.json create mode 100644 packages/tauri/src/next.ts delete mode 100644 packages/tauri/src/old.ts diff --git a/examples/axum/src/lib.rs b/examples/axum/src/lib.rs new file mode 100644 index 00000000..b0d57afd --- /dev/null +++ b/examples/axum/src/lib.rs @@ -0,0 +1,191 @@ +use std::{ + marker::PhantomData, + sync::Arc, + time::{Duration, SystemTime}, +}; + +use async_stream::stream; +use rspc::{ + middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, + Router2, +}; +use rspc_cache::{cache, cache_ttl, CacheState, Memory}; +use serde::Serialize; +use specta::Type; +use thiserror::Error; +use tokio::time::sleep; +use tracing::info; + +// `Clone` is only required for usage with Websockets +#[derive(Clone)] +pub struct Ctx {} + +#[derive(Serialize, Type)] +pub struct MyCustomType(String); + +#[derive(Type, Serialize)] +#[serde(tag = "type")] +#[specta(export = false)] +pub enum DeserializationError { + // Is not a map-type so invalid. + A(String), +} + +// http://[::]:4000/rspc/version +// http://[::]:4000/legacy/version + +// http://[::]:4000/rspc/nested.hello +// http://[::]:4000/legacy/nested.hello + +// http://[::]:4000/rspc/error +// http://[::]:4000/legacy/error + +// http://[::]:4000/rspc/echo +// http://[::]:4000/legacy/echo + +// http://[::]:4000/rspc/echo?input=42 +// http://[::]:4000/legacy/echo?input=42 + +fn mount() -> rspc::Router { + let inner = rspc::Router::::new().query("hello", |t| t(|_, _: ()| "Hello World!")); + + let router = rspc::Router::::new() + .merge("nested.", inner) + .query("version", |t| { + t(|_, _: ()| { + info!("Hello World from Version Query!"); + + env!("CARGO_PKG_VERSION") + }) + }) + .query("panic", |t| t(|_, _: ()| todo!())) + // .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("echo", |t| t(|_, v: String| v)) + .query("error", |t| { + t(|_, _: ()| { + Err(rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Something went wrong".into(), + )) as Result + }) + }) + .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) + .mutation("sendMsg", |t| { + t(|_, v: String| { + println!("Client said '{}'", v); + v + }) + }) + // .mutation("anotherOne", |t| t(|_, v: String| Ok(MyCustomType(v)))) + .subscription("pings", |t| { + t(|_ctx, _args: ()| { + stream! { + println!("Client subscribed to 'pings'"); + for i in 0..5 { + println!("Sending ping {}", i); + yield "ping".to_string(); + sleep(Duration::from_secs(1)).await; + } + } + }) + }) + // TODO: Results being returned from subscriptions + // .subscription("errorPings", |t| t(|_ctx, _args: ()| { + // stream! { + // for i in 0..5 { + // yield Ok("ping".to_string()); + // sleep(Duration::from_secs(1)).await; + // } + // yield Err(rspc::Error::new(ErrorCode::InternalServerError, "Something went wrong".into())); + // } + // })) + .build(); + + router +} + +#[derive(Debug, Error, Serialize, Type)] +pub enum Error { + #[error("you made a mistake: {0}")] + Mistake(String), +} + +impl Error2 for Error { + fn into_resolver_error(self) -> rspc::ResolverError { + rspc::ResolverError::new(500, self.to_string(), None::) + } +} + +pub struct BaseProcedure(PhantomData); +impl BaseProcedure { + pub fn builder() -> ProcedureBuilder + where + TErr: Error2, + TInput: ResolverInput, + TResult: ResolverOutput, + { + Procedure2::builder() // You add default middleware here + } +} + +fn test_unstable_stuff(router: Router2) -> Router2 { + router + .procedure("newstuff", { + ::builder().query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) + }) + .procedure("newstuff2", { + ::builder() + .with(invalidation(|ctx: Ctx, key, event| false)) + .with(Middleware::new( + move |ctx: Ctx, input: (), next| async move { + let result = next.exec(ctx, input).await; + result + }, + )) + .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) + }) + .setup(CacheState::builder(Memory::new()).mount()) + .procedure("cached", { + ::builder() + .with(cache()) + .query(|_, _: ()| async { + // if input.some_arg {} + cache_ttl(10); + + Ok(SystemTime::now()) + }) + }) +} + +#[derive(Debug, Clone, Serialize, Type)] +pub enum InvalidateEvent { + InvalidateKey(String), +} + +fn invalidation( + handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, +) -> Middleware +where + TError: Send + 'static, + TCtx: Clone + Send + 'static, + TInput: Clone + Send + 'static, + TResult: Send + 'static, +{ + let handler = Arc::new(handler); + Middleware::new(move |ctx: TCtx, input: TInput, next| async move { + // TODO: Register this with `TCtx` + let ctx2 = ctx.clone(); + let input2 = input.clone(); + let result = next.exec(ctx, input).await; + + // TODO: Unregister this with `TCtx` + result + }) +} + +pub fn create_router() -> Router2 { + let router = Router2::from(mount()); + let router = test_unstable_stuff(router); + + router +} diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index e8402b52..2dd4664a 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,195 +1,11 @@ -use std::{ - marker::PhantomData, - path::PathBuf, - sync::Arc, - time::{Duration, SystemTime}, -}; - -use async_stream::stream; use axum::routing::get; -use rspc::{ - middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, - Router2, -}; -use rspc_cache::{cache, cache_ttl, CacheState, Memory}; -use serde::Serialize; -use specta::Type; -use thiserror::Error; -use tokio::time::sleep; +use example_axum::{create_router, Ctx}; +use std::path::PathBuf; use tower_http::cors::{Any, CorsLayer}; -use tracing::info; - -// `Clone` is only required for usage with Websockets -#[derive(Clone)] -pub struct Ctx {} - -#[derive(Serialize, Type)] -pub struct MyCustomType(String); - -#[derive(Type, Serialize)] -#[serde(tag = "type")] -#[specta(export = false)] -pub enum DeserializationError { - // Is not a map-type so invalid. - A(String), -} - -// http://[::]:4000/rspc/version -// http://[::]:4000/legacy/version - -// http://[::]:4000/rspc/nested.hello -// http://[::]:4000/legacy/nested.hello - -// http://[::]:4000/rspc/error -// http://[::]:4000/legacy/error - -// http://[::]:4000/rspc/echo -// http://[::]:4000/legacy/echo - -// http://[::]:4000/rspc/echo?input=42 -// http://[::]:4000/legacy/echo?input=42 - -fn mount() -> rspc::Router { - let inner = rspc::Router::::new().query("hello", |t| t(|_, _: ()| "Hello World!")); - - let router = rspc::Router::::new() - .merge("nested.", inner) - .query("version", |t| { - t(|_, _: ()| { - info!("Hello World from Version Query!"); - - env!("CARGO_PKG_VERSION") - }) - }) - .query("panic", |t| t(|_, _: ()| todo!())) - // .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) - .query("echo", |t| t(|_, v: String| v)) - .query("error", |t| { - t(|_, _: ()| { - Err(rspc::Error::new( - rspc::ErrorCode::InternalServerError, - "Something went wrong".into(), - )) as Result - }) - }) - .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) - .mutation("sendMsg", |t| { - t(|_, v: String| { - println!("Client said '{}'", v); - v - }) - }) - // .mutation("anotherOne", |t| t(|_, v: String| Ok(MyCustomType(v)))) - .subscription("pings", |t| { - t(|_ctx, _args: ()| { - stream! { - println!("Client subscribed to 'pings'"); - for i in 0..5 { - println!("Sending ping {}", i); - yield "ping".to_string(); - sleep(Duration::from_secs(1)).await; - } - } - }) - }) - // TODO: Results being returned from subscriptions - // .subscription("errorPings", |t| t(|_ctx, _args: ()| { - // stream! { - // for i in 0..5 { - // yield Ok("ping".to_string()); - // sleep(Duration::from_secs(1)).await; - // } - // yield Err(rspc::Error::new(ErrorCode::InternalServerError, "Something went wrong".into())); - // } - // })) - .build(); - - router -} - -#[derive(Debug, Error, Serialize, Type)] -pub enum Error { - #[error("you made a mistake: {0}")] - Mistake(String), -} - -impl Error2 for Error { - fn into_resolver_error(self) -> rspc::ResolverError { - rspc::ResolverError::new(500, self.to_string(), None::) - } -} - -pub struct BaseProcedure(PhantomData); -impl BaseProcedure { - pub fn builder() -> ProcedureBuilder - where - TErr: Error2, - TInput: ResolverInput, - TResult: ResolverOutput, - { - Procedure2::builder() // You add default middleware here - } -} - -fn test_unstable_stuff(router: Router2) -> Router2 { - router - .procedure("newstuff", { - ::builder().query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) - }) - .procedure("newstuff2", { - ::builder() - .with(invalidation(|ctx: Ctx, key, event| false)) - .with(Middleware::new( - move |ctx: Ctx, input: (), next| async move { - let result = next.exec(ctx, input).await; - result - }, - )) - .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) - }) - .setup(CacheState::builder(Memory::new()).mount()) - .procedure("cached", { - ::builder() - .with(cache()) - .query(|_, _: ()| async { - // if input.some_arg {} - cache_ttl(10); - - Ok(SystemTime::now()) - }) - }) -} - -#[derive(Debug, Clone, Serialize, Type)] -pub enum InvalidateEvent { - InvalidateKey(String), -} - -fn invalidation( - handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, -) -> Middleware -where - TError: Send + 'static, - TCtx: Clone + Send + 'static, - TInput: Clone + Send + 'static, - TResult: Send + 'static, -{ - let handler = Arc::new(handler); - Middleware::new(move |ctx: TCtx, input: TInput, next| async move { - // TODO: Register this with `TCtx` - let ctx2 = ctx.clone(); - let input2 = input.clone(); - let result = next.exec(ctx, input).await; - - // TODO: Unregister this with `TCtx` - result - }) -} #[tokio::main] async fn main() { - let router = Router2::from(mount()); - let router = test_unstable_stuff(router); + let router = create_router(); let (procedures, types) = router.build().unwrap(); rspc::Typescript::default() diff --git a/examples/bindings.ts b/examples/bindings.ts index 9243276f..8d1b0d93 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -4,17 +4,17 @@ export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { - cached: { input: any, output: any, error: any }, - echo: { input: string, output: string, error: unknown }, - error: { input: null, output: string, error: unknown }, + cached: { kind: "query", input: any, output: any, error: any }, + echo: { kind: "query", input: string, output: string, error: unknown }, + error: { kind: "query", input: null, output: string, error: unknown }, nested: { - hello: { input: null, output: string, error: unknown }, + hello: { kind: "query", input: null, output: string, error: unknown }, }, - newstuff: { input: any, output: any, error: any }, - newstuff2: { input: any, output: any, error: any }, - panic: { input: null, output: null, error: unknown }, - pings: { input: null, output: string, error: unknown }, - sendMsg: { input: string, output: string, error: unknown }, - transformMe: { input: null, output: string, error: unknown }, - version: { input: null, output: string, error: unknown }, + newstuff: { kind: "query", input: any, output: any, error: any }, + newstuff2: { kind: "query", input: any, output: any, error: any }, + panic: { kind: "query", input: null, output: null, error: unknown }, + pings: { kind: "subscription", input: null, output: string, error: unknown }, + sendMsg: { kind: "mutation", input: string, output: string, error: unknown }, + transformMe: { kind: "query", input: null, output: string, error: unknown }, + version: { kind: "query", input: null, output: string, error: unknown }, } \ No newline at end of file diff --git a/examples/tauri/package.json b/examples/tauri/package.json index 20145f76..ea3bdfe7 100644 --- a/examples/tauri/package.json +++ b/examples/tauri/package.json @@ -15,6 +15,7 @@ "solid-js": "^1.7.8", "@tauri-apps/api": "^2", "@tauri-apps/plugin-shell": "^2", + "@rspc/client": "workspace:*", "@rspc/tauri": "workspace:*" }, "devDependencies": { diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index a7e1e685..2f1d7da2 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -22,3 +22,4 @@ serde_json = "1" rspc = { path = "../../../rspc", features = ["typescript", "unstable"] } tauri-plugin-rspc = { path = "../../../integrations/tauri" } specta = { version = "=2.0.0-rc.20", features = ["derive"] } +example-axum = { path = "../../axum" } diff --git a/examples/tauri/src-tauri/src/lib.rs b/examples/tauri/src-tauri/src/lib.rs index 8986603f..d0c65cc6 100644 --- a/examples/tauri/src-tauri/src/lib.rs +++ b/examples/tauri/src-tauri/src/lib.rs @@ -1,20 +1,14 @@ -use api::Infallible; -use rspc::{Procedure2, Router2}; +use example_axum::{create_router, Ctx}; mod api; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - // TODO: Show proper setup - - let router = Router2::new().procedure( - "query", - Procedure2::builder().query(|_, _: ()| async { Ok::<(), Infallible>(()) }), - ); + let router = create_router(); let (procedures, types) = router.build().unwrap(); tauri::Builder::default() - .plugin(tauri_plugin_rspc::init(procedures, |_| {})) + .plugin(tauri_plugin_rspc::init(procedures, |_| Ctx {})) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/examples/tauri/src/App.tsx b/examples/tauri/src/App.tsx index cf2d2c67..444f7d94 100644 --- a/examples/tauri/src/App.tsx +++ b/examples/tauri/src/App.tsx @@ -1,21 +1,20 @@ -import { Channel } from "@tauri-apps/api/core"; +import { createClient } from "@rspc/client/next"; +import { tauriExecute } from "@rspc/tauri/next"; + +import { Procedures } from "../../bindings"; + import "./App.css"; -import { handleRpc } from "@rspc/tauri"; +const client = createClient(tauriExecute); function App() { - const channel = new Channel(); - handleRpc( - { method: "request", params: { path: "query", input: null } }, - channel, - ); - channel.onmessage = console.log; + client.error.query().then(console.log); - return ( -
-

Welcome to Tauri + Solid

-
- ); + return ( +
+

Welcome to Tauri + Solid

+
+ ); } export default App; diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 2d742fdb..c6dffe6e 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -67,7 +67,7 @@ where }; let mut stream = match input { - Some(i) => procedure.exec_with_deserializer(ctx, i), + Some(i) => procedure.exec_with_deserializer(ctx, i.as_ref()), None => procedure.exec_with_deserializer(ctx, serde_json::Value::Null), }; @@ -181,12 +181,12 @@ where #[derive(Deserialize, Serialize)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] -enum Request<'a> { +enum Request { /// A request to execute a procedure. Request { path: String, - #[serde(borrow)] - input: Option<&'a RawValue>, + // #[serde(borrow)] + input: Option>, }, /// Abort a running task /// You must provide the ID of the Tauri channel provided when the task was started. diff --git a/packages/client/package.json b/packages/client/package.json index 223858f5..81f2eb58 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,44 +1,53 @@ { - "name": "@rspc/client", - "version": "0.3.1", - "description": "A blazing fast and easy to use TRPC-like server for Rust.", - "keywords": [], - "author": "Oscar Beaumont", - "license": "MIT", - "type": "module", - "main": "dist/index.cjs", - "types": "dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.cjs" - } - }, - "files": [ - "dist" - ], - "scripts": { - "dev": "tsup --watch", - "build": "tsup", - "prepare": "tsup" - }, - "devDependencies": { - "tsup": "^8.3.5", - "typescript": "^5.6.3" - }, - "tsup": { - "entry": [ - "src/index.ts" - ], - "format": [ - "esm", - "cjs" - ], - "dts": true, - "splitting": true, - "clean": true, - "sourcemap": true - } + "name": "@rspc/client", + "version": "0.3.1", + "description": "A blazing fast and easy to use TRPC-like server for Rust.", + "keywords": [], + "author": "Oscar Beaumont", + "license": "MIT", + "type": "module", + "main": "dist/index.cjs", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.cjs" + }, + "./next": { + "types": "./dist/next/index.d.cts", + "import": "./dist/next/index.js", + "default": "./dist/next/index.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "tsup --watch", + "build": "tsup", + "prepare": "tsup" + }, + "devDependencies": { + "tsup": "^8.3.5", + "typescript": "^5.6.3" + }, + "tsup": { + "entry": [ + "src/index.ts", + "src/next/index.ts" + ], + "format": [ + "esm", + "cjs" + ], + "dts": true, + "splitting": true, + "clean": true, + "sourcemap": true + }, + "dependencies": { + "vitest": "^2.1.5" + } } diff --git a/packages/client/src/next/UntypedClient.ts b/packages/client/src/next/UntypedClient.ts new file mode 100644 index 00000000..e7a5f3d4 --- /dev/null +++ b/packages/client/src/next/UntypedClient.ts @@ -0,0 +1,134 @@ +import type { + ExeceuteData, + ExecuteArgs, + ExecuteFn, + SubscriptionObserver, +} from "./types"; + +export function observable( + cb: (subscriber: { next: (value: T) => void; complete(): void }) => void, +) { + let callbacks: Array<(v: T) => void> = []; + let completeCallbacks: Array<() => void> = []; + let done = false; + + cb({ + next: (v) => { + if (done) return; + callbacks.forEach((cb) => cb(v)); + }, + complete: () => { + if (done) return; + done = true; + completeCallbacks.forEach((cb) => cb()); + }, + }); + + return { + subscribe(cb: (v: T) => void) { + if (done) return Promise.resolve(); + + callbacks.push(cb); + return new Promise((res) => { + completeCallbacks.push(() => res()); + }); + }, + get done() { + return done; + }, + }; +} + +export type Observable = ReturnType>; + +export const fetchExecute = (config: { url: string }, args: ExecuteArgs) => { + if (args.type === "subscription") + throw new Error("Subscriptions are not possible with the `fetch` executor"); + + let promise; + if (args.type === "query") { + promise = fetch( + `${config.url}/${args.path}?${new URLSearchParams({ + input: JSON.stringify(args.input), + })}`, + { + method: "GET", + headers: { + Accept: "application/json", + }, + }, + ); + } else { + promise = fetch(`${config.url}/${args.path}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(args.input), + }); + } + + return observable((subscriber) => { + promise + .then(async (r) => { + let json; + + if (r.status === 200) { + subscriber.next(await r.json()); + } else json = (await r.json()) as []; + + json; + }) + .finally(() => subscriber.complete()); + }); +}; + +export class UntypedClient { + constructor(public execute: ExecuteFn) {} + + private async executeAsPromise(args: ExecuteArgs) { + const obs = this.execute(args); + + let data: ExeceuteData | undefined; + + await obs.subscribe((d) => { + data = d; + }); + + if (!data) throw new Error("No data received"); + if (data.type === "error") + throw new Error( + `Error with code '${data.data.code}' occurred`, + data.data.data, + ); + if (data.type === "event") + throw new Error("Received event when expecting resposne"); + + return data.data; + } + + public query(path: string, input: unknown) { + return this.executeAsPromise({ type: "query", path, input }); + } + public mutation(path: string, input: unknown) { + return this.executeAsPromise({ type: "mutation", path, input }); + } + public subscription( + path: string, + input: unknown, + opts?: Partial>, + ) { + const observable = this.execute({ type: "subscription", path, input }); + + // observable.subscribe((response) => { + // if (response.result.type === "started") { + // opts?.onStarted?.(); + // } else if (response.result.type === "stopped") { + // opts?.onStopped?.(); + // } else { + // opts?.onData?.(response.result.data); + // } + // }); + } +} diff --git a/packages/client/src/next/index.test.ts b/packages/client/src/next/index.test.ts new file mode 100644 index 00000000..8f9f97c7 --- /dev/null +++ b/packages/client/src/next/index.test.ts @@ -0,0 +1,35 @@ +import { test } from "vitest"; +import { createClient, observable } from "."; + +type NestedProcedures = { + nested: { + procedures: { + one: { + kind: "query"; + input: string; + output: number; + error: boolean; + }; + two: { + kind: "mutation"; + input: string; + output: { id: string; name: string }; + error: { status: "NOT_FOUND" }; + }; + three: { + kind: "subscription"; + input: string; + output: { id: string; name: string }; + error: { status: "NOT_FOUND" }; + }; + }; + }; +}; + +const client = createClient(() => observable(() => {})); + +test("proxy", () => { + client.nested.procedures.one.query("test"); + client.nested.procedures.two.mutate("test"); + client.nested.procedures.three.subscribe("test"); +}); diff --git a/packages/client/src/next/index.ts b/packages/client/src/next/index.ts new file mode 100644 index 00000000..cd783346 --- /dev/null +++ b/packages/client/src/next/index.ts @@ -0,0 +1,122 @@ +import { UntypedClient } from "./UntypedClient"; +import type { + ProcedureResult, + ProcedureKind as ProcedureKind, + SubscriptionObserver, + ExecuteFn, + Procedure, +} from "./types"; + +export * from "./types"; +export { Observable, observable } from "./UntypedClient"; + +export type ProcedureWithKind = Omit< + Procedure, + "kind" +> & { kind: V }; + +export type Procedures = { + [K in string]: Procedure | Procedures; +}; + +type Unsubscribable = { unsubscribe: () => void }; + +export type VoidIfInputNull< + P extends Procedure, + Else = P["input"], +> = P["input"] extends null ? void : Else; + +type Resolver

= ( + input: VoidIfInputNull

, +) => Promise>; + +type SubscriptionResolver

= ( + input: VoidIfInputNull

, + opts?: Partial>, +) => Unsubscribable; + +export type ProcedureProxyMethods

= + P["kind"] extends "query" + ? { query: Resolver

} + : P["kind"] extends "mutation" + ? { mutate: Resolver

} + : P["kind"] extends "subscription" + ? { subscribe: SubscriptionResolver

} + : never; + +type ClientProceduresProxy

= { + [K in keyof P]: P[K] extends Procedure + ? ProcedureProxyMethods + : P[K] extends Procedures + ? ClientProceduresProxy + : never; +}; + +export type Client

= ClientProceduresProxy

; + +const noop = () => { + // noop +}; + +interface ProxyCallbackOptions { + path: string[]; + args: any[]; +} +type ProxyCallback = (opts: ProxyCallbackOptions) => unknown; + +const clientMethodMap = { + query: "query", + mutate: "mutation", + subscribe: "subscription", +} as const; + +export function createProceduresProxy( + callback: ProxyCallback, + path: string[] = [], +): T { + return new Proxy(noop, { + get(_, key) { + if (typeof key !== "string") return; + + return createProceduresProxy(callback, [...path, key]); + }, + apply(_1, _2, args) { + return callback({ args, path }); + }, + }) as T; +} + +export function createClient

( + execute: ExecuteFn, +): Client

{ + const client = new UntypedClient(execute); + + return createProceduresProxy>(({ args, path }) => { + const procedureType = + clientMethodMap[path.pop() as keyof typeof clientMethodMap]; + + const pathString = path.join("."); + + return (client[procedureType] as any)(pathString, ...args); + }); +} + +export function getQueryKey( + path: string, + input: unknown, +): [string] | [string, unknown] { + return input === undefined ? [path] : [path, input]; +} + +export function traverseClient

( + client: Client, + path: string[], +): ProcedureProxyMethods

{ + let ret: ClientProceduresProxy = client; + + for (const segment of path) { + ret = ret[segment]; + } + + return ret as any; +} diff --git a/packages/client/src/next/types.ts b/packages/client/src/next/types.ts new file mode 100644 index 00000000..3f903f60 --- /dev/null +++ b/packages/client/src/next/types.ts @@ -0,0 +1,51 @@ +import { Observable } from "./UntypedClient"; + +export type JoinPath< + TPath extends string, + TNext extends string, +> = TPath extends "" ? TNext : `${TPath}.${TNext}`; + +export type ProcedureKind = "query" | "mutation" | "subscription"; + +export type Procedure = { + kind: ProcedureKind; + input: unknown; + output: unknown; + error: unknown; +}; + +export type Procedures = { + [K in string]: Procedure | Procedures; +}; + +export type Result = + | { status: "ok"; data: Ok } + | { status: "err"; error: Err }; + +export type ProcedureResult

= Result< + P["output"], + P["error"] +>; + +export interface SubscriptionObserver { + onStarted: () => void; + onData: (value: TValue) => void; + onError: (err: TError) => void; + onStopped: () => void; + onComplete: () => void; +} + +export type ExecuteArgs = { + type: ProcedureKind; + path: string; + input: unknown; +}; +export type ExecuteFn = (args: ExecuteArgs) => Observable; + +export type ExeceuteData = + | { type: "event"; data: any } + | { type: "response"; data: any } + | { + type: "error"; + data: { code: number; data: any }; + }; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 0440eb61..7120524a 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,8 +1,9 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*"], - "exclude": ["dist/**/*"] + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationMap": true + }, + "include": ["src/**/*"], + "exclude": ["dist/**/*"] } diff --git a/packages/query-core/src/index.ts b/packages/query-core/src/index.ts index b2cf97e0..25e407b1 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -1,6 +1,26 @@ import type * as rspc from "@rspc/client"; import * as tanstack from "@tanstack/query-core"; +export function createTanstackQueryProxy< + TProceduresLike extends rspc.ProceduresDef, + TTanstack extends { + queryOptions: (options: any) => any; + infiniteQueryOptions: (options: any) => any; + }, +>() { + type TProcedures = rspc.inferProcedures; + + return function (args: { + queryOptions: TTanstack["queryOptions"]; + infiniteQueryOptions: TTanstack["infiniteQueryOptions"]; + }) { + return { + // queryOptions: args.queryOptions, + // mutationOptions: (): => {}, + }; + }; +} + export interface BaseOptions { rspc?: { client?: rspc.Client }; } diff --git a/packages/solid-query/src/index.tsx b/packages/solid-query/src/index.tsx index 3142c5ec..c15fad63 100644 --- a/packages/solid-query/src/index.tsx +++ b/packages/solid-query/src/index.tsx @@ -1,7 +1,13 @@ import type * as rspc from "@rspc/client"; +import * as solid from "solid-js"; + import * as queryCore from "@rspc/query-core"; import * as tanstack from "@tanstack/solid-query"; -import * as solid from "solid-js"; + +const proxy = queryCore.createTanstackQueryProxy()({ + queryOptions: tanstack.queryOptions, + infiniteQueryOptions: tanstack.infiniteQueryOptions, +}); export * from "@rspc/query-core"; diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json new file mode 100644 index 00000000..ebdaaea8 --- /dev/null +++ b/packages/tanstack-query/package.json @@ -0,0 +1,50 @@ +{ + "name": "@rspc/query-core", + "version": "0.3.1", + "description": "A blazing fast and easy to use TRPC-like server for Rust.", + "keywords": [], + "author": "Oscar Beaumont", + "license": "MIT", + "type": "module", + "main": "dist/index.cjs", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "tsup --watch", + "build": "tsup", + "prepare": "tsup" + }, + "dependencies": { + "@tanstack/query-core": "^5.60.6" + }, + "devDependencies": { + "tsup": "^8.3.5", + "typescript": "^5.6.3" + }, + "peerDependencies": { + "@rspc/client": "workspace:*" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "format": [ + "esm", + "cjs" + ], + "dts": true, + "splitting": true, + "clean": true, + "sourcemap": true + } +} diff --git a/packages/tanstack-query/src/index.ts b/packages/tanstack-query/src/index.ts new file mode 100644 index 00000000..25e407b1 --- /dev/null +++ b/packages/tanstack-query/src/index.ts @@ -0,0 +1,288 @@ +import type * as rspc from "@rspc/client"; +import * as tanstack from "@tanstack/query-core"; + +export function createTanstackQueryProxy< + TProceduresLike extends rspc.ProceduresDef, + TTanstack extends { + queryOptions: (options: any) => any; + infiniteQueryOptions: (options: any) => any; + }, +>() { + type TProcedures = rspc.inferProcedures; + + return function (args: { + queryOptions: TTanstack["queryOptions"]; + infiniteQueryOptions: TTanstack["infiniteQueryOptions"]; + }) { + return { + // queryOptions: args.queryOptions, + // mutationOptions: (): => {}, + }; + }; +} + +export interface BaseOptions { + rspc?: { client?: rspc.Client }; +} + +export interface SubscriptionOptions< + P extends rspc.ProceduresDef, + K extends rspc.inferSubscriptions

["key"] & string, +> extends rspc.SubscriptionOptions> { + enabled?: boolean; + client?: rspc.Client

; +} + +export function createRSPCQueryUtils< + TProceduresLike extends rspc.ProceduresDef, +>( + client: rspc.Client>, + queryClient: tanstack.QueryClient, +) { + type TProcedures = rspc.inferProcedures; + type TBaseOptions = BaseOptions; + type QueryKey = [ + key: K, + ...input: rspc._inferProcedureHandlerInput, + ]; + type AllowedKeys = TProcedures["queries"]["key"] & string; + type RSPCOptions = Omit & TBaseOptions; + type RSPCFetchQueryOptions< + K extends string, + TData = rspc.inferQueryResult, + > = RSPCOptions< + tanstack.FetchQueryOptions< + TData, + rspc.RSPCError, + TData, + QueryKey + > + >; + + type RSPCEnsureFetchQueryOptions< + K extends string, + TData = rspc.inferQueryResult, + > = RSPCOptions< + tanstack.EnsureQueryDataOptions< + TData, + rspc.RSPCError, + TData, + QueryKey + > + >; + + return { + fetch: ( + queryKey: QueryKey, + opts?: RSPCFetchQueryOptions, + ) => + queryClient.fetchQuery({ + ...opts, + queryKey, + queryFn: () => client.query(queryKey), + }), + prefetch: ( + queryKey: QueryKey, + opts?: RSPCFetchQueryOptions, + ) => + queryClient.prefetchQuery({ + ...opts, + queryKey, + queryFn: () => client.query(queryKey), + }), + ensureData: ( + queryKey: QueryKey, + opts?: RSPCEnsureFetchQueryOptions, + ) => + queryClient.ensureQueryData({ + ...opts, + queryKey, + queryFn: () => client.query(queryKey), + }), + invalidate: ( + queryKey: QueryKey, + filters?: Omit, + opts?: tanstack.InvalidateOptions, + ) => queryClient.invalidateQueries({ ...filters, queryKey }, opts), + refetch: ( + queryKey: QueryKey, + filters?: Omit, + opts?: tanstack.RefetchOptions, + ) => queryClient.refetchQueries({ ...filters, queryKey }, opts), + cancel: ( + queryKey: QueryKey, + filters?: Omit, + opts?: tanstack.CancelOptions, + ) => queryClient.cancelQueries({ ...filters, queryKey }, opts), + setData: < + K extends AllowedKeys, + TData = rspc.inferQueryResult, + >( + queryKey: QueryKey, + updater: tanstack.Updater, + options?: tanstack.SetDataOptions, + ) => { + queryClient.setQueryData(queryKey, updater, options); + }, + getData: < + K extends AllowedKeys, + TData = rspc.inferQueryResult, + >( + queryKey: QueryKey, + ) => queryClient.getQueryData(queryKey), + }; +} + +export interface Context { + client: rspc.Client; + queryClient: tanstack.QueryClient; +} + +export type KeyAndInputSkipToken< + P extends rspc.ProceduresDef, + K extends rspc.inferQueries

["key"] & string, + O extends keyof rspc.ProceduresDef = "queries", +> = [ + key: K, + ...input: rspc._inferProcedureHandlerInput | [tanstack.SkipToken], +]; + +export type HookOptions

= T & BaseOptions

; +export type WrapQueryOptions

= Omit< + T, + "queryKey" | "queryFn" +> & + BaseOptions

; +export type WrapMutationOptions

= Omit< + T, + "_defaulted" | "variables" | "mutationKey" +> & + BaseOptions

; + +function isSkipTokenInput(array: any[]): array is [tanstack.SkipToken] { + return array.length === 1 && array[0] === tanstack.skipToken; +} + +export function createQueryHookHelpers

(args: { + useContext(): Context

| null; +}) { + type TBaseOptions = BaseOptions

; + + function useContext() { + const ctx = args.useContext(); + if (!ctx) throw new Error("rspc context provider not found!"); + return ctx; + } + + function useClient() { + return useContext().client; + } + + function getClient(opts?: T) { + return opts?.rspc?.client ?? useClient(); + } + + function useQueryArgs< + K extends rspc.inferQueries

["key"] & string, + O extends WrapQueryOptions< + P, + tanstack.QueryObserverOptions< + rspc.inferQueryResult, + rspc.RSPCError, + rspc.inferQueryResult, + rspc.inferQueryResult, + KeyAndInputSkipToken + > + >, + >(keyAndInput: KeyAndInputSkipToken, opts?: O) { + const client = getClient(opts); + const [key, ...input] = keyAndInput; + + return { + ...opts, + queryKey: keyAndInput, + queryFn: isSkipTokenInput(input) + ? (input[0] as tanstack.SkipToken) + : () => client.query([key, ...input]), + }; + } + + type MutationObserverOptions< + K extends rspc.inferMutations

["key"] & string, + > = WrapMutationOptions< + P, + tanstack.MutationObserverOptions< + rspc.inferMutationResult, + rspc.RSPCError, + rspc.inferMutationInput extends never + ? undefined + : rspc.inferMutationInput, + any + > + >; + + function useMutationArgs< + K extends rspc.inferMutations

["key"] & string, + O extends MutationObserverOptions, + >(key: K, opts?: O) { + const client = getClient(opts); + + return { + ...opts, + mutationKey: [key], + mutationFn: (input) => + client.mutation([key, ...(input ? [input] : [])] as any), + } satisfies tanstack.MutationObserverOptions< + rspc.inferMutationResult, + rspc.RSPCError, + rspc.inferMutationInput extends never + ? undefined + : rspc.inferMutationInput, + any + >; + } + + function handleSubscription< + K extends rspc.inferSubscriptions

["key"] & string, + >( + keyAndInput: KeyAndInputSkipToken, + _opts: () => SubscriptionOptions, + _client: rspc.Client

, + ) { + // function to allow options to be passed in via ref + const opts = _opts(); + const [key, ...input] = keyAndInput; + + if (!(opts.enabled ?? true) || isSkipTokenInput(input)) return; + + const client = opts.client ?? _client; + let isStopped = false; + + const unsubscribe = client.addSubscription([key, ...input], { + onStarted: () => { + if (!isStopped) opts.onStarted?.(); + }, + onData: (d) => { + if (!isStopped) opts.onData(d); + }, + onError: (e) => { + if (!isStopped) opts.onError?.(e); + }, + }); + + return () => { + isStopped = true; + unsubscribe(); + }; + } + + return { + useContext, + getClient, + useClient, + useExtractOps: getClient, + useQueryArgs, + useMutationArgs, + handleSubscription, + }; +} diff --git a/packages/tanstack-query/tsconfig.json b/packages/tanstack-query/tsconfig.json new file mode 100644 index 00000000..0440eb61 --- /dev/null +++ b/packages/tanstack-query/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["dist/**/*"] +} diff --git a/packages/tauri/package.json b/packages/tauri/package.json index d1fcc3e3..29ebbd2e 100644 --- a/packages/tauri/package.json +++ b/packages/tauri/package.json @@ -13,6 +13,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.cjs" + }, + "./next": { + "types": "./dist/next.d.ts", + "import": "./dist/next.js", + "default": "./dist/next.cjs" } }, "types": "dist/index.d.ts", @@ -26,7 +31,8 @@ }, "tsup": { "entry": [ - "src/index.ts" + "src/index.ts", + "src/next.ts" ], "format": [ "esm", diff --git a/packages/tauri/src/index.ts b/packages/tauri/src/index.ts index b5bc6a1e..53997d0f 100644 --- a/packages/tauri/src/index.ts +++ b/packages/tauri/src/index.ts @@ -1,19 +1,72 @@ -import { type Channel, invoke } from "@tauri-apps/api/core"; +import { randomId, OperationType, Transport, RSPCError } from "@rspc/client"; +import { listen, UnlistenFn } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; -type Request = - | { - method: "request"; - params: { path: string; input: null | string }; - } - | { method: "abort"; params: number }; +export class TauriTransport implements Transport { + private requestMap = new Map void>(); + private listener?: Promise; + clientSubscriptionCallback?: (id: string, value: any) => void; -type Response = { code: number; value: T } | "Done"; + constructor() { + this.listener = listen("plugin:rspc:transport:resp", (event) => { + const { id, result } = event.payload as any; + if (result.type === "event") { + if (this.clientSubscriptionCallback) + this.clientSubscriptionCallback(id, result.data); + } else if (result.type === "response") { + if (this.requestMap.has(id)) { + this.requestMap.get(id)?.({ type: "response", result: result.data }); + this.requestMap.delete(id); + } + } else if (result.type === "error") { + const { message, code } = result.data; + if (this.requestMap.has(id)) { + this.requestMap.get(id)?.({ type: "error", message, code }); + this.requestMap.delete(id); + } + } else { + console.error(`Received event of unknown method '${result.type}'`); + } + }); + } -// TODO: Seal `Channel` within a standard interface for all "modern links"? -// TODO: handle detect and converting to rspc error class -// TODO: Catch Tauri errors -> Assuming it would happen on `tauri::Error` which happens when serialization fails in Rust. -// TODO: Return closure for cleanup + async doRequest( + operation: OperationType, + key: string, + input: any, + ): Promise { + if (!this.listener) { + await this.listener; + } -export async function handleRpc(req: Request, channel: Channel>) { - await invoke("plugin:rspc|handle_rpc", { req, channel }); + const id = randomId(); + let resolve: (data: any) => void; + const promise = new Promise((res) => { + resolve = res; + }); + + // @ts-ignore + this.requestMap.set(id, resolve); + + await getCurrentWindow().emit("plugin:rspc:transport", { + id, + method: operation, + params: { + path: key, + input, + }, + }); + + const body = (await promise) as any; + if (body.type === "error") { + const { code, message } = body; + throw new RSPCError(code, message); + } else if (body.type === "response") { + return body.result; + } else { + throw new Error( + `RSPC Tauri doRequest received invalid body type '${body?.type}'`, + ); + } + } } diff --git a/packages/tauri/src/next.ts b/packages/tauri/src/next.ts new file mode 100644 index 00000000..53fd14a6 --- /dev/null +++ b/packages/tauri/src/next.ts @@ -0,0 +1,59 @@ +import { Channel, invoke } from "@tauri-apps/api/core"; +import { ExecuteArgs, ExecuteFn, observable } from "@rspc/client/next"; +import { resolve } from "@tauri-apps/api/path"; + +type Request = + | { method: "request"; params: { path: string; input: any } } + | { method: "abort"; params: number }; + +type Response = { code: number; value: T } | null; + +// TODO: Seal `Channel` within a standard interface for all "modern links"? +// TODO: handle detect and converting to rspc error class +// TODO: Catch Tauri errors -> Assuming it would happen on `tauri::Error` which happens when serialization fails in Rust. +// TODO: Return closure for cleanup + +export async function handleRpc(req: Request, channel: Channel>) { + await invoke("plugin:rspc|handle_rpc", { req, channel }); +} + +export const tauriExecute: ExecuteFn = (args: ExecuteArgs) => { + return observable((subscriber) => { + const channel = new Channel>(); + + channel.onmessage = (response) => { + console.log(response); + if (response === null) subscriber.complete(); + else { + if (response.code === 200) { + // doesn't handle errors + subscriber.next({ + type: "response", + data: response.value, + }); + } else { + // doesn't handle errors + subscriber.next({ + type: "error", + data: { + code: response.code, + data: response.value, + }, + }); + } + } + }; + + handleRpc( + { + method: "request", + params: { + path: args.path, + input: + args.input === undefined || args.input === null ? null : args.input, + }, + }, + channel, + ); + }); +}; diff --git a/packages/tauri/src/old.ts b/packages/tauri/src/old.ts deleted file mode 100644 index d258e8f1..00000000 --- a/packages/tauri/src/old.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { randomId, OperationType, Transport, RSPCError } from "@rspc/client"; -import { listen, UnlistenFn } from "@tauri-apps/api/event"; -import { getCurrentWindow } from "@tauri-apps/api/window"; - -export class TauriTransport implements Transport { - private requestMap = new Map void>(); - private listener?: Promise; - clientSubscriptionCallback?: (id: string, value: any) => void; - - constructor() { - this.listener = listen("plugin:rspc:transport:resp", (event) => { - const { id, result } = event.payload as any; - if (result.type === "event") { - if (this.clientSubscriptionCallback) - this.clientSubscriptionCallback(id, result.data); - } else if (result.type === "response") { - if (this.requestMap.has(id)) { - this.requestMap.get(id)?.({ type: "response", result: result.data }); - this.requestMap.delete(id); - } - } else if (result.type === "error") { - const { message, code } = result.data; - if (this.requestMap.has(id)) { - this.requestMap.get(id)?.({ type: "error", message, code }); - this.requestMap.delete(id); - } - } else { - console.error(`Received event of unknown method '${result.type}'`); - } - }); - } - - async doRequest( - operation: OperationType, - key: string, - input: any - ): Promise { - if (!this.listener) { - await this.listener; - } - - const id = randomId(); - let resolve: (data: any) => void; - const promise = new Promise((res) => { - resolve = res; - }); - - // @ts-ignore - this.requestMap.set(id, resolve); - - await getCurrentWindow().emit("plugin:rspc:transport", { - id, - method: operation, - params: { - path: key, - input, - }, - }); - - const body = (await promise) as any; - if (body.type === "error") { - const { code, message } = body; - throw new RSPCError(code, message); - } else if (body.type === "response") { - return body.result; - } else { - throw new Error( - `RSPC Tauri doRequest received invalid body type '${body?.type}'` - ); - } - } -} diff --git a/packages/tauri/tsconfig.json b/packages/tauri/tsconfig.json index cf99572b..6440ec45 100644 --- a/packages/tauri/tsconfig.json +++ b/packages/tauri/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "../tsconfig.json", "include": ["src/**/*"], - "exclude": ["dist/**/*"] + "exclude": ["dist/**/*"], + "compilerOptions": { + "moduleResolution": "bundler" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ebc00e5..61408912 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: examples/tauri: dependencies: + '@rspc/client': + specifier: workspace:* + version: link:../../packages/client '@rspc/tauri': specifier: workspace:* version: link:../../packages/tauri @@ -118,6 +121,10 @@ importers: version: 2.10.2(solid-js@1.9.3)(vite@5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0)) packages/client: + dependencies: + vitest: + specifier: ^2.1.5 + version: 2.1.5(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0) devDependencies: tsup: specifier: ^8.3.5 @@ -149,7 +156,7 @@ importers: version: link:../client '@rspc/query-core': specifier: workspace:* - version: link:../query-core + version: link:../tanstack-query devDependencies: '@tanstack/react-query': specifier: ^5.61.0 @@ -174,7 +181,7 @@ importers: version: link:../client '@rspc/query-core': specifier: workspace:* - version: link:../query-core + version: link:../tanstack-query devDependencies: '@tanstack/solid-query': specifier: ^5.60.6 @@ -202,7 +209,7 @@ importers: version: link:../client '@rspc/query-core': specifier: workspace:* - version: link:../query-core + version: link:../tanstack-query svelte: specifier: '>=3 <5' version: 4.2.18 @@ -223,6 +230,22 @@ importers: specifier: ^2.1.5 version: 2.1.5(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0) + packages/tanstack-query: + dependencies: + '@rspc/client': + specifier: workspace:* + version: link:../client + '@tanstack/query-core': + specifier: ^5.60.6 + version: 5.60.6 + devDependencies: + tsup: + specifier: ^8.3.5 + version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + typescript: + specifier: ^5.6.3 + version: 5.6.3 + packages/tauri: dependencies: '@rspc/client': diff --git a/rspc/src/languages/typescript.rs b/rspc/src/languages/typescript.rs index 5b6ffe15..dde3f2dd 100644 --- a/rspc/src/languages/typescript.rs +++ b/rspc/src/languages/typescript.rs @@ -4,7 +4,9 @@ use serde_json::json; use specta::{datatype::DataType, NamedType, Type}; use specta_typescript::{datatype, export_named_datatype, ExportError}; -use crate::{procedure::ProcedureType, types::TypesOrType, util::literal_object, Types}; +use crate::{ + procedure::ProcedureType, types::TypesOrType, util::literal_object, ProcedureKind, Types, +}; pub struct Typescript { inner: specta_typescript::Typescript, @@ -145,7 +147,14 @@ fn generate_bindings( on_procedure(&key, source_pos, procedure_type); // *out += "\t"; // TODO: Correct padding - *out += "{ input: "; + *out += "{ kind: "; + *out += match procedure_type.kind { + ProcedureKind::Query => r#""query""#, + ProcedureKind::Mutation => r#""mutation""#, + ProcedureKind::Subscription => r#""subscription""#, + }; + + *out += ", input: "; *out += &datatype( &this.inner, &specta::datatype::FunctionResultVariant::Value(procedure_type.input.clone()), From b8f10d3c346c04234236e9b1cd2b95aaac2d1d38 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 17 Dec 2024 12:25:40 +0800 Subject: [PATCH 45/67] simplify wire format --- examples/tauri/src/App.tsx | 2 +- packages/client/src/next/UntypedClient.ts | 22 ++++++++++------------ packages/client/src/next/index.test.ts | 5 ++++- packages/client/src/next/types.ts | 14 +++++++------- packages/tauri/src/next.ts | 21 +-------------------- 5 files changed, 23 insertions(+), 41 deletions(-) diff --git a/examples/tauri/src/App.tsx b/examples/tauri/src/App.tsx index 444f7d94..d06583c0 100644 --- a/examples/tauri/src/App.tsx +++ b/examples/tauri/src/App.tsx @@ -8,7 +8,7 @@ import "./App.css"; const client = createClient(tauriExecute); function App() { - client.error.query().then(console.log); + client.sendMsg.mutate("bruh").then(console.log); return (

diff --git a/packages/client/src/next/UntypedClient.ts b/packages/client/src/next/UntypedClient.ts index e7a5f3d4..526f93bb 100644 --- a/packages/client/src/next/UntypedClient.ts +++ b/packages/client/src/next/UntypedClient.ts @@ -41,7 +41,10 @@ export function observable( export type Observable = ReturnType>; -export const fetchExecute = (config: { url: string }, args: ExecuteArgs) => { +export const fetchExecute = ( + config: { url: string }, + args: ExecuteArgs, +): ReturnType => { if (args.type === "subscription") throw new Error("Subscriptions are not possible with the `fetch` executor"); @@ -69,7 +72,7 @@ export const fetchExecute = (config: { url: string }, args: ExecuteArgs) => { }); } - return observable((subscriber) => { + return observable((subscriber) => { promise .then(async (r) => { let json; @@ -93,19 +96,14 @@ export class UntypedClient { let data: ExeceuteData | undefined; await obs.subscribe((d) => { - data = d; + if (data === undefined) data = d; }); if (!data) throw new Error("No data received"); - if (data.type === "error") - throw new Error( - `Error with code '${data.data.code}' occurred`, - data.data.data, - ); - if (data.type === "event") - throw new Error("Received event when expecting resposne"); - - return data.data; + if (data.code !== 200) + throw new Error(`Error with code '${data.code}' occurred`, data.value); + + return data.value; } public query(path: string, input: unknown) { diff --git a/packages/client/src/next/index.test.ts b/packages/client/src/next/index.test.ts index 8f9f97c7..f3f60507 100644 --- a/packages/client/src/next/index.test.ts +++ b/packages/client/src/next/index.test.ts @@ -1,5 +1,6 @@ import { test } from "vitest"; import { createClient, observable } from "."; +import { fetchExecute } from "./UntypedClient"; type NestedProcedures = { nested: { @@ -26,7 +27,9 @@ type NestedProcedures = { }; }; -const client = createClient(() => observable(() => {})); +const client = createClient((args) => + fetchExecute({ url: "..." }, args), +); test("proxy", () => { client.nested.procedures.one.query("test"); diff --git a/packages/client/src/next/types.ts b/packages/client/src/next/types.ts index 3f903f60..ce249521 100644 --- a/packages/client/src/next/types.ts +++ b/packages/client/src/next/types.ts @@ -42,10 +42,10 @@ export type ExecuteArgs = { }; export type ExecuteFn = (args: ExecuteArgs) => Observable; -export type ExeceuteData = - | { type: "event"; data: any } - | { type: "response"; data: any } - | { - type: "error"; - data: { code: number; data: any }; - }; +export type ExeceuteData = { code: number; value: any } | null; +// | { type: "event"; data: any } +// | { type: "response"; data: any } +// | { +// type: "error"; +// data: { code: number; data: any }; +// }; diff --git a/packages/tauri/src/next.ts b/packages/tauri/src/next.ts index 53fd14a6..e62ad7f3 100644 --- a/packages/tauri/src/next.ts +++ b/packages/tauri/src/next.ts @@ -1,6 +1,5 @@ import { Channel, invoke } from "@tauri-apps/api/core"; import { ExecuteArgs, ExecuteFn, observable } from "@rspc/client/next"; -import { resolve } from "@tauri-apps/api/path"; type Request = | { method: "request"; params: { path: string; input: any } } @@ -22,26 +21,8 @@ export const tauriExecute: ExecuteFn = (args: ExecuteArgs) => { const channel = new Channel>(); channel.onmessage = (response) => { - console.log(response); if (response === null) subscriber.complete(); - else { - if (response.code === 200) { - // doesn't handle errors - subscriber.next({ - type: "response", - data: response.value, - }); - } else { - // doesn't handle errors - subscriber.next({ - type: "error", - data: { - code: response.code, - data: response.value, - }, - }); - } - } + return subscriber.next(response); }; handleRpc( From 2de248fefa21d754888d0dd94d70d89594f67230 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 18 Dec 2024 12:20:30 +0800 Subject: [PATCH 46/67] `rspc_axum::Endpoint` wip --- core/src/error.rs | 41 +++++--- core/src/lib.rs | 1 + core/src/procedure.rs | 21 ++++- core/src/stream.rs | 37 +++++--- examples/axum/src/main.rs | 3 + examples/bindings.ts | 3 +- integrations/axum/Cargo.toml | 1 + integrations/axum/src/endpoint.rs | 150 ++++++++++++++++++++---------- 8 files changed, 178 insertions(+), 79 deletions(-) diff --git a/core/src/error.rs b/core/src/error.rs index 46d83899..7cc4893c 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,6 +1,11 @@ use std::{any::Any, borrow::Cow, error, fmt}; -use serde::{ser::SerializeStruct, Serialize, Serializer}; +use serde::{ + ser::{Error, SerializeStruct}, + Serialize, Serializer, +}; + +use crate::LegacyErrorInterop; /// TODO pub enum ProcedureError { @@ -28,16 +33,6 @@ impl ProcedureError { } } - pub fn serialize(&self, s: Se) -> Result { - match self { - Self::NotFound => s.serialize_none(), - Self::Deserialize(err) => s.serialize_str(&format!("{}", err)), - Self::Downcast(err) => s.serialize_str(&format!("{}", err)), - Self::Resolver(err) => s.serialize_str(&format!("{}", err)), - Self::Unwind(_) => s.serialize_none(), - } - } - pub fn variant(&self) -> &'static str { match self { ProcedureError::NotFound => "NotFound", @@ -69,6 +64,18 @@ impl From for ProcedureError { } } +impl From for ProcedureError { + fn from(err: DeserializeError) -> Self { + ProcedureError::Deserialize(err) + } +} + +impl From for ProcedureError { + fn from(err: DowncastError) -> Self { + ProcedureError::Downcast(err) + } +} + impl fmt::Debug for ProcedureError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // TODO: Proper format @@ -96,6 +103,12 @@ impl Serialize for ProcedureError { S: Serializer, { if let ProcedureError::Resolver(err) = self { + if let Some(err) = err.error() { + if let Some(v) = err.downcast_ref::() { + return v.0.serialize(serializer); + } + } + return err.value().serialize(serializer); } @@ -168,6 +181,12 @@ impl error::Error for ResolverError {} /// TODO pub struct DeserializeError(pub(crate) erased_serde::Error); +impl DeserializeError { + pub fn custom(err: T) -> Self { + Self(erased_serde::Error::custom(err)) + } +} + impl fmt::Debug for DeserializeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Deserialize({:?})", self.0) diff --git a/core/src/lib.rs b/core/src/lib.rs index ca741316..0e2046d5 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -23,6 +23,7 @@ mod stream; pub use dyn_input::DynInput; pub use error::{DeserializeError, DowncastError, ProcedureError, ResolverError}; +#[doc(hidden)] pub use interop::LegacyErrorInterop; pub use procedure::Procedure; pub use procedures::Procedures; diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 011b8a60..34f60d86 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -1,8 +1,13 @@ -use std::{any::type_name, fmt, sync::Arc}; +use std::{ + any::type_name, + fmt, + panic::{catch_unwind, AssertUnwindSafe}, + sync::Arc, +}; use serde::Deserializer; -use crate::{DynInput, ProcedureStream}; +use crate::{DynInput, ProcedureError, ProcedureStream}; /// a single type-erased operation that the server can execute. /// @@ -25,7 +30,9 @@ impl Procedure { } pub fn exec(&self, ctx: TCtx, input: DynInput) -> ProcedureStream { - (self.handler)(ctx, input) + let (Ok(v) | Err(v)) = catch_unwind(AssertUnwindSafe(|| (self.handler)(ctx, input))) + .map_err(|err| ProcedureError::Unwind(err).into()); + v } pub fn exec_with_deserializer<'de, D: Deserializer<'de> + Send>( @@ -36,14 +43,18 @@ impl Procedure { let mut deserializer = ::erase(input); let value = DynInput::new_deserializer(&mut deserializer); - (self.handler)(ctx, value) + let (Ok(v) | Err(v)) = catch_unwind(AssertUnwindSafe(|| (self.handler)(ctx, value))) + .map_err(|err| ProcedureError::Unwind(err).into()); + v } pub fn exec_with_value(&self, ctx: TCtx, input: T) -> ProcedureStream { let mut input = Some(input); let value = DynInput::new_value(&mut input); - (self.handler)(ctx, value) + let (Ok(v) | Err(v)) = catch_unwind(AssertUnwindSafe(|| (self.handler)(ctx, value))) + .map_err(|err| ProcedureError::Unwind(err).into()); + v } } diff --git a/core/src/stream.rs b/core/src/stream.rs index ab712278..5baa55ff 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -14,7 +14,7 @@ use crate::ProcedureError; /// TODO #[must_use = "ProcedureStream does nothing unless polled"] -pub struct ProcedureStream(Pin>); +pub struct ProcedureStream(Result>, Option>); impl ProcedureStream { /// TODO @@ -23,11 +23,11 @@ impl ProcedureStream { S: Stream> + Send + 'static, T: Serialize + Send + Sync + 'static, { - Self(Box::pin(DynReturnImpl { + Self(Ok(Box::pin(DynReturnImpl { src: s, unwound: false, value: None, - })) + }))) } /// TODO @@ -41,7 +41,10 @@ impl ProcedureStream { /// TODO pub fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + match &self.0 { + Ok(v) => v.size_hint(), + Err(_) => (1, Some(1)), + } } /// TODO @@ -49,19 +52,31 @@ impl ProcedureStream { &mut self, cx: &mut Context<'_>, ) -> Poll>> { - self.0 - .as_mut() - .poll_next_value(cx) - .map(|v| v.map(|v| v.map(|_: ()| self.0.value()))) + match &mut self.0 { + Ok(v) => v + .as_mut() + .poll_next_value(cx) + .map(|v| v.map(|v| v.map(|_: ()| self.0.as_mut().expect("checked above").value()))), + Err(err) => Poll::Ready(err.take().map(Err)), + } } /// TODO pub async fn next( &mut self, ) -> Option> { - poll_fn(|cx| self.0.as_mut().poll_next_value(cx)) - .await - .map(|v| v.map(|_: ()| self.0.value())) + match self { + Self(Ok(v)) => poll_fn(|cx| v.as_mut().poll_next_value(cx)) + .await + .map(|v| v.map(|_: ()| self.0.as_mut().expect("checked above").value())), + Self(Err(err)) => err.take().map(Err), + } + } +} + +impl From for ProcedureStream { + fn from(err: ProcedureError) -> Self { + Self(Err(Some(err))) } } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index e8402b52..61f9bb2e 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -147,6 +147,9 @@ fn test_unstable_stuff(router: Router2) -> Router2 { )) .query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) }) + .procedure("newstuffpanic", { + ::builder().query(|_, _: ()| async move { Ok(todo!()) }) + }) .setup(CacheState::builder(Memory::new()).mount()) .procedure("cached", { ::builder() diff --git a/examples/bindings.ts b/examples/bindings.ts index 9243276f..0a930107 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,7 +1,7 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { cached: { input: any, output: any, error: any }, @@ -12,6 +12,7 @@ export type Procedures = { }, newstuff: { input: any, output: any, error: any }, newstuff2: { input: any, output: any, error: any }, + newstuffpanic: { input: any, output: any, error: any }, panic: { input: null, output: null, error: unknown }, pings: { input: null, output: string, error: unknown }, sendMsg: { input: string, output: string, error: unknown }, diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 12016ab0..1e67535a 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -30,6 +30,7 @@ futures = "0.3" # TODO: No blocking execution, etc tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. serde = { version = "1", features = ["derive"] } # TODO: Remove features serde_urlencoded = "0.7.1" +mime = "0.3.17" [lints] workspace = true diff --git a/integrations/axum/src/endpoint.rs b/integrations/axum/src/endpoint.rs index 1613e671..ad05fca9 100644 --- a/integrations/axum/src/endpoint.rs +++ b/integrations/axum/src/endpoint.rs @@ -1,20 +1,23 @@ use std::{ + convert::Infallible, + future::poll_fn, pin::Pin, sync::Arc, task::{Context, Poll}, }; use axum::{ - body::{Bytes, HttpBody}, + body::{Body, Bytes, HttpBody}, extract::{FromRequest, Request}, + http::{header, HeaderMap, StatusCode}, response::{ sse::{Event, KeepAlive}, - Sse, + IntoResponse, Sse, }, routing::{on, MethodFilter}, }; -use futures::Stream; -use rspc_core::{ProcedureStream, Procedures}; +use futures::{stream::once, Stream, StreamExt, TryStreamExt}; +use rspc_core::{ProcedureError, ProcedureStream, Procedures}; /// Construct a new [`axum::Router`](axum::Router) to expose a given [`rspc::Router`](rspc::Router). pub struct Endpoint { @@ -143,7 +146,7 @@ impl Endpoint { async move { let hint = req.body().size_hint(); let has_body = hint.lower() != 0 || hint.upper() != Some(0); - let stream = if !has_body { + let mut stream = if !has_body { let mut params = form_urlencoded::parse( req.uri().query().unwrap_or_default().as_ref(), ); @@ -159,10 +162,20 @@ impl Endpoint { .exec_with_deserializer(ctx, serde_json::Value::Null), } } else { - // TODO - // if !json_content_type(req.headers()) { - // Err(MissingJsonContentType.into()) - // } + if !json_content_type(req.headers()) { + let err: ProcedureError = rspc_core::DeserializeError::custom( + "Client did not set correct valid 'Content-Type' header", + ) + .into(); + let buf = serde_json::to_vec(&err).unwrap(); // TODO + + return ( + StatusCode::BAD_REQUEST, + [(header::CONTENT_TYPE, "application/json")], + Body::from(buf), + ) + .into_response(); + } let bytes = Bytes::from_request(req, &()).await.unwrap(); // TODO: Error handling procedure.exec_with_deserializer( @@ -171,11 +184,24 @@ impl Endpoint { ) }; - // TODO: Status code - // TODO: Json headers - // TODO: Maybe only SSE for subscriptions??? + let mut stream = ProcedureStreamResponse { + code: None, + stream, + first: None, + }; + stream.first = Some(stream.next().await); - Sse::new(ProcedureStreamSSE(stream)).keep_alive(KeepAlive::default()) + let status = stream + .code + .and_then(|c| StatusCode::try_from(c).ok()) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + ( + status, + [(header::CONTENT_TYPE, "application/json")], + Body::from_stream(stream), + ) + .into_response() } }, ), @@ -188,50 +214,72 @@ impl Endpoint { } } -struct ProcedureStreamSSE(ProcedureStream); +struct ProcedureStreamResponse { + code: Option, + first: Option, Infallible>>>, + stream: ProcedureStream, +} -impl Stream for ProcedureStreamSSE { - type Item = Result; +impl Stream for ProcedureStreamResponse { + type Item = Result, Infallible>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.0 - .poll_next(cx) - // TODO: `v` should be broken out - .map(|v| { - v.map(|v| { - Event::default() - // .event() // TODO: `Ok` vs `Err` - Also serve `StatusCode` - .json_data(&v) - }) - }) + if let Some(first) = self.first.take() { + return Poll::Ready(first); + } + + let (code, mut buf) = { + let Poll::Ready(v) = self.stream.poll_next(cx) else { + return Poll::Pending; + }; + + match v { + Some(Ok(v)) => ( + 200, + Some(serde_json::to_vec(&v).unwrap()), // TODO: Error handling + ), + Some(Err(err)) => ( + err.status(), + Some(serde_json::to_vec(&err).unwrap()), // TODO: Error handling + ), + None => (200, None), + } + }; + + if let Some(buf) = &mut buf { + buf.extend_from_slice(b"\n\n"); + }; + + self.code = Some(code); + Poll::Ready(buf.map(Ok)) } fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + self.stream.size_hint() } } -// fn json_content_type(headers: &HeaderMap) -> bool { -// let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { -// content_type -// } else { -// return false; -// }; - -// let content_type = if let Ok(content_type) = content_type.to_str() { -// content_type -// } else { -// return false; -// }; - -// let mime = if let Ok(mime) = content_type.parse::() { -// mime -// } else { -// return false; -// }; - -// let is_json_content_type = mime.type_() == "application" -// && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); - -// is_json_content_type -// } +fn json_content_type(headers: &HeaderMap) -> bool { + let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + content_type + } else { + return false; + }; + + let content_type = if let Ok(content_type) = content_type.to_str() { + content_type + } else { + return false; + }; + + let mime = if let Ok(mime) = content_type.parse::() { + mime + } else { + return false; + }; + + let is_json_content_type = mime.type_() == "application" + && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); + + is_json_content_type +} From 62ba199e4d4c1217611a35572ab82dd834adde8c Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 18 Dec 2024 12:22:13 +0800 Subject: [PATCH 47/67] cleanup --- DUMP | 80 ----------------------------------------------------------- DUMP2 | 7 ------ TODO | 19 -------------- 3 files changed, 106 deletions(-) delete mode 100644 DUMP delete mode 100644 DUMP2 delete mode 100644 TODO diff --git a/DUMP b/DUMP deleted file mode 100644 index 2a73b46e..00000000 --- a/DUMP +++ /dev/null @@ -1,80 +0,0 @@ -use std::any::Any; - -use serde::{ - de::{value, DeserializeOwned}, - Deserializer, -}; - -pub trait Input<'de>: Sized { - /// TODO - // TODO: This bound is because `dyn Any`. We could probs relax it but it would require work. - type Value: 'static; - - /// TODO - // TODO: Expose a no-op that can be put here??? - type Deserializer: Deserializer<'de>; - - /// TODO - fn into_deserializer(self) -> Result; -} - -impl<'de, D: Deserializer<'de>> Input<'de> for D { - type Value = (); - type Deserializer = D; - - fn into_deserializer(self) -> Result { - Ok(self) - } -} - -enum RawInput<'a, 'de> { - Value(&'a mut dyn Any), - Deserializer(&'a mut dyn erased_serde::Deserializer<'de>), -} - -// TODO: Avoid generics as much as possible!!! -pub struct Procedure { - // TODO: Types - handler: Box, -} - -impl Procedure { - pub fn new(handler: impl Fn((), TInput) + 'static) -> Self - where - TInput: DeserializeOwned + 'static, // TODO: Deserialize<'a>, // TODO: Drop `'static` bound - { - Self { - handler: Box::new(move |_, input| { - let input: TInput = match input { - RawInput::Value(value) => value - .downcast_mut::>() - .unwrap() - .take() - .unwrap(), - RawInput::Deserializer(deserializer) => { - erased_serde::deserialize(deserializer).unwrap() - } - }; - - handler((), input); - }), - } - } - - // TODO: Avoid generics as much as possible!!! - // TODO: Context - // TODO: Async runtime - // TODO: Results (value or serializer) - pub fn exec<'a, 'de>(&'a self, ctx: (), mut input: impl Input<'de> + 'de + 'a) { - match input.into_deserializer() { - Ok(input) => { - let mut input = >::erase(input); - (self.handler)(ctx, RawInput::Deserializer(&mut input)); - } - Err(input) => { - let mut input = Some(input); // Option so we can `downcast_ref` and `take`. - (self.handler)(ctx, RawInput::Value(&mut input)); - } - } - } -} diff --git a/DUMP2 b/DUMP2 deleted file mode 100644 index 03e9673a..00000000 --- a/DUMP2 +++ /dev/null @@ -1,7 +0,0 @@ -pub fn modern_to_legacy(router: Router2) -> Router { - let r = Router::new(); - - todo!(); - - r.build() -} diff --git a/TODO b/TODO deleted file mode 100644 index 63a9f11e..00000000 --- a/TODO +++ /dev/null @@ -1,19 +0,0 @@ -Check everything is moved from the old repo! -Fix warnings -Fix jump to definition with the new syntax - - - - -// #[derive(Deserialize)] -// struct Parameters<'a> { -// #[serde(borrow)] -// input: &'a RawValue, -// } -// -// -// let params: Parameters = serde_urlencoded::from_str( -// req.uri().query().unwrap_or_default(), -// ) -// .unwrap(); // TODO: Error handling -// println!("{:?}", params.input); From 01a0980100080a1d84aa80b792e6ccdf6c1861a8 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 18 Dec 2024 12:54:11 +0800 Subject: [PATCH 48/67] `example-core` + `rspc-actix-web` wip --- .github/workflows/ci.yaml | 80 ------------------------------ Cargo.toml | 2 + examples/actix-web/Cargo.toml | 12 +++++ examples/actix-web/src/main.rs | 57 +++++++++++++++++++++ examples/axum/Cargo.toml | 12 +---- examples/axum/src/main.rs | 2 +- examples/core/Cargo.toml | 18 +++++++ examples/{axum => core}/src/lib.rs | 3 +- integrations/actix-web/Cargo.toml | 27 ++++++++++ integrations/actix-web/src/lib.rs | 77 ++++++++++++++++++++++++++++ integrations/axum/src/lib.rs | 2 +- 11 files changed, 198 insertions(+), 94 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 examples/actix-web/Cargo.toml create mode 100644 examples/actix-web/src/main.rs create mode 100644 examples/core/Cargo.toml rename examples/{axum => core}/src/lib.rs (97%) create mode 100644 integrations/actix-web/Cargo.toml create mode 100644 integrations/actix-web/src/lib.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index b77686d5..00000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,80 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -jobs: - typecheck: - name: Typecheck - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: latest - - - uses: actions/setup-node@v4 - with: - cache: "pnpm" - - - name: Install dependencies - run: pnpm i - - - name: Typecheck - run: pnpm typecheck - - format-lint-biome: - name: Format & Lint (Biome) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: latest - - - uses: actions/setup-node@v4 - with: - cache: "pnpm" - - - name: Install dependencies - run: pnpm i - - - name: Run Biome - run: pnpm exec biome ci . - - format-rust: - name: Format (Cargo) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Check formatting - run: cargo fmt --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Run Clippy - uses: actions-rs-plus/clippy-check@v2 - with: - # no --locked as this is a library and we want to use latest dependencies in CI - args: --workspace --all-features diff --git a/Cargo.toml b/Cargo.toml index c5c601e8..68f751f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ members = [ "./integrations/*", "./middleware/*", "./examples", + "./examples/core", "./examples/axum", + "./examples/actix-web", "./examples/client", "./examples/tauri/src-tauri", ] diff --git a/examples/actix-web/Cargo.toml b/examples/actix-web/Cargo.toml new file mode 100644 index 00000000..a5b3c178 --- /dev/null +++ b/examples/actix-web/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "example-actix-web" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } +example-core = { path = "../core" } +rspc-actix-web = { path = "../../integrations/actix-web", features = [] } +actix-web = "4" +actix-cors = "0.7.0" diff --git a/examples/actix-web/src/main.rs b/examples/actix-web/src/main.rs new file mode 100644 index 00000000..ee0a98de --- /dev/null +++ b/examples/actix-web/src/main.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; + +use actix_cors::Cors; +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use example_core::{create_router, Ctx}; + +#[get("/")] +async fn hello() -> impl Responder { + HttpResponse::Ok().body("Hello world from Actix Web!") +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let router = create_router(); + let (procedures, types) = router.build().unwrap(); + + rspc::Typescript::default() + // .formatter(specta_typescript::formatter::prettier), + .header("// My custom header") + // .enable_source_maps() // TODO: Fix this + .export_to( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, + ) + .unwrap(); + + // Be aware this is very experimental and doesn't support many types yet. + // rspc::Rust::default() + // // .header("// My custom header") + // .export_to( + // PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../client/src/bindings.rs"), + // &types, + // ) + // .unwrap(); + + // let procedures = rspc_devtools::mount(procedures, &types); + + // TODO: CORS + + let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 + println!("listening on http://{addr}/rspc/version"); + HttpServer::new(move || { + App::new() + // Don't use permissive CORS in production! + .wrap(Cors::permissive()) + .service(hello) + .service(web::scope("/rspc").configure( + rspc_actix_web::Endpoint::builder(procedures.clone()).build(|| { + // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this + Ctx {} + }), + )) + }) + .bind(addr)? + .run() + .await +} diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 2003da99..5a0a1ec6 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -7,18 +7,10 @@ publish = false [dependencies] rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } +example-core = { path = "../core" } tokio = { version = "1.41.1", features = ["full"] } -async-stream = "0.3.6" -axum = { version = "0.7.9", features = ["ws", "tokio"] } +axum = "0.7.9" tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } -serde = { version = "1.0.215", features = ["derive"] } -specta = { version = "=2.0.0-rc.20", features = [ - "derive", -] } # TODO: Drop all features -thiserror = "2.0.4" rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } -tracing = "0.1.41" -futures = "0.3.31" -rspc-cache = { version = "0.0.0", path = "../../middleware/cache" } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 2dd4664a..13033538 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,5 +1,5 @@ use axum::routing::get; -use example_axum::{create_router, Ctx}; +use example_core::{create_router, Ctx}; use std::path::PathBuf; use tower_http::cors::{Any, CorsLayer}; diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml new file mode 100644 index 00000000..3663e545 --- /dev/null +++ b/examples/core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-core" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } +async-stream = "0.3.6" +serde = { version = "1.0.215", features = ["derive"] } +specta = { version = "=2.0.0-rc.20", features = [ + "derive", +] } +thiserror = "2.0.4" +rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } +tracing = "0.1.41" +futures = "0.3.31" +rspc-cache = { version = "0.0.0", path = "../../middleware/cache" } diff --git a/examples/axum/src/lib.rs b/examples/core/src/lib.rs similarity index 97% rename from examples/axum/src/lib.rs rename to examples/core/src/lib.rs index 609a4bfb..42f936ad 100644 --- a/examples/axum/src/lib.rs +++ b/examples/core/src/lib.rs @@ -13,7 +13,6 @@ use rspc_cache::{cache, cache_ttl, CacheState, Memory}; use serde::Serialize; use specta::Type; use thiserror::Error; -use tokio::time::sleep; use tracing::info; // `Clone` is only required for usage with Websockets @@ -84,7 +83,7 @@ fn mount() -> rspc::Router { for i in 0..5 { println!("Sending ping {}", i); yield "ping".to_string(); - sleep(Duration::from_secs(1)).await; + // sleep(Duration::from_secs(1)).await; // TODO: Figure this out. Async runtime is now not determined so maybe inject. } } }) diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml new file mode 100644 index 00000000..792071de --- /dev/null +++ b/integrations/actix-web/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rspc-actix-web" +description = "Actix Web adapter for rspc" +version = "0.2.1" +authors = ["Oscar Beaumont "] +edition = "2021" +license = "MIT" +repository = "https://github.com/specta-rs/rspc" +documentation = "https://docs.rs/rspc-axum/latest/rspc-axum" +keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] +categories = ["web-programming", "asynchronous"] + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = [] +# ws = ["axum/ws"] + +[dependencies] +rspc-core = { version = "0.0.1", path = "../../core" } +actix-web = "4" + +[lints] +workspace = true diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs new file mode 100644 index 00000000..b5191e2d --- /dev/null +++ b/integrations/actix-web/src/lib.rs @@ -0,0 +1,77 @@ +//! rspc-actix-web: [Actix Web](https://actix.rs) integration for [rspc](https://rspc.dev). +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", + html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" +)] + +use actix_web::{ + web::{self, ServiceConfig}, + HttpResponse, Resource, +}; +use rspc_core::Procedures; + +pub struct Endpoint { + procedures: Procedures, + // endpoints: bool, + // websocket: Option TCtx>, + // batching: bool, +} + +impl Endpoint { + pub fn builder(router: Procedures) -> Self { + Self { + procedures: router, + // endpoints: false, + // websocket: None, + // batching: false, + } + } + + pub fn build( + self, + ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static, + ) -> impl FnOnce(&mut ServiceConfig) { + |service| { + service.route( + "/ws", + web::to(|| { + // let (res, mut session, stream) = actix_ws::handle(&req, stream)?; + + // let mut stream = stream + // .aggregate_continuations() + // // aggregate continuation frames up to 1MiB + // .max_continuation_size(2_usize.pow(20)); + + // // start task but don't wait for it + // rt::spawn(async move { + // // receive messages from websocket + // while let Some(msg) = stream.next().await { + // match msg { + // Ok(AggregatedMessage::Text(text)) => { + // // echo text message + // session.text(text).await.unwrap(); + // } + + // Ok(AggregatedMessage::Binary(bin)) => { + // // echo binary message + // session.binary(bin).await.unwrap(); + // } + + // Ok(AggregatedMessage::Ping(msg)) => { + // // respond to PING frame with PONG frame + // session.pong(&msg).await.unwrap(); + // } + + // _ => {} + // } + // } + + HttpResponse::NotFound() + }), + ); + service.route("/{route:.*}", web::to(|| HttpResponse::Ok())); + } + } +} diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 7c2b9dc7..01af85d8 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -1,4 +1,4 @@ -//! rspc-axum: Axum integration for [rspc](https://rspc.dev). +//! rspc-axum: [Axum](https://docs.rs/axum) integration for [rspc](https://rspc.dev). #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( From 38024c268e2eb485a29f6cb9d0b18a1f96ec9a68 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 20 Dec 2024 13:54:26 +0800 Subject: [PATCH 49/67] init sfm example --- examples/sfm/.gitignore | 29 + examples/sfm/README.md | 32 + examples/sfm/app.config.ts | 3 + examples/sfm/package.json | 23 + examples/sfm/public/favicon.ico | Bin 0 -> 664 bytes examples/sfm/src/app.css | 39 + examples/sfm/src/app.tsx | 22 + examples/sfm/src/components/Counter.css | 21 + examples/sfm/src/components/Counter.tsx | 11 + examples/sfm/src/entry-client.tsx | 4 + examples/sfm/src/entry-server.tsx | 21 + examples/sfm/src/global.d.ts | 1 + examples/sfm/src/routes/[...404].tsx | 19 + examples/sfm/src/routes/about.tsx | 10 + examples/sfm/src/routes/index.tsx | 19 + examples/sfm/tsconfig.json | 19 + pnpm-lock.yaml | 3102 ++++++++++++++++++++++- pnpm-workspace.yaml | 1 + 18 files changed, 3288 insertions(+), 88 deletions(-) create mode 100644 examples/sfm/.gitignore create mode 100644 examples/sfm/README.md create mode 100644 examples/sfm/app.config.ts create mode 100644 examples/sfm/package.json create mode 100644 examples/sfm/public/favicon.ico create mode 100644 examples/sfm/src/app.css create mode 100644 examples/sfm/src/app.tsx create mode 100644 examples/sfm/src/components/Counter.css create mode 100644 examples/sfm/src/components/Counter.tsx create mode 100644 examples/sfm/src/entry-client.tsx create mode 100644 examples/sfm/src/entry-server.tsx create mode 100644 examples/sfm/src/global.d.ts create mode 100644 examples/sfm/src/routes/[...404].tsx create mode 100644 examples/sfm/src/routes/about.tsx create mode 100644 examples/sfm/src/routes/index.tsx create mode 100644 examples/sfm/tsconfig.json diff --git a/examples/sfm/.gitignore b/examples/sfm/.gitignore new file mode 100644 index 00000000..d16c893d --- /dev/null +++ b/examples/sfm/.gitignore @@ -0,0 +1,29 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi +app.config.timestamp_*.js + +# Environment +.env +.env*.local + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db diff --git a/examples/sfm/README.md b/examples/sfm/README.md new file mode 100644 index 00000000..a84af394 --- /dev/null +++ b/examples/sfm/README.md @@ -0,0 +1,32 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/sfm/app.config.ts b/examples/sfm/app.config.ts new file mode 100644 index 00000000..de7f8310 --- /dev/null +++ b/examples/sfm/app.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "@solidjs/start/config"; + +export default defineConfig({}); diff --git a/examples/sfm/package.json b/examples/sfm/package.json new file mode 100644 index 00000000..af68f6db --- /dev/null +++ b/examples/sfm/package.json @@ -0,0 +1,23 @@ +{ + "name": "example-basic", + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start", + "version": "vinxi version" + }, + "dependencies": { + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.15.0", + "@solidjs/start": "^1.0.10", + "solid-js": "^1.9.2", + "vinxi": "^0.4.3" + }, + "overrides": { + "vite": "5.4.10" + }, + "engines": { + "node": ">=18" + } +} diff --git a/examples/sfm/public/favicon.ico b/examples/sfm/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fb282da0719ef6ab4c1732df93be6216b0d85520 GIT binary patch literal 664 zcmV;J0%!e+P)m9ebk1R zejT~~6f_`?;`cEd!+`7(hw@%%2;?RN8gX-L?z6cM( zKoG@&w+0}f@Pfvwc+deid)qgE!L$ENKYjViZC_Zcr>L(`2oXUT8f0mRQ(6-=HN_Ai zeBBEz3WP+1Cw`m!49Wf!MnZzp5bH8VkR~BcJ1s-j90TAS2Yo4j!J|KodxYR%3Numw zA?gq6e`5@!W~F$_De3yt&uspo&2yLb$(NwcPPI-4LGc!}HdY%jfq@AFs8LiZ4k(p} zZ!c9o+qbWYs-Mg zgdyTALzJX&7QXHdI_DPTFL33;w}88{e6Zk)MX0kN{3DX9uz#O_L58&XRH$Nvvu;fO zf&)7@?C~$z1K<>j0ga$$MIg+5xN;eQ?1-CA=`^Y169@Ab6!vcaNP=hxfKN%@Ly^R* zK1iv*s1Yl6_dVyz8>ZqYhz6J4|3fQ@2LQeX@^%W(B~8>=MoEmBEGGD1;gHXlpX>!W ym)!leA2L@`cpb^hy)P75=I!`pBYxP7<2VfQ3j76qLgzIA0000 ( + + SolidStart - Basic + Index + About + {props.children} + + )} + > + + + ); +} diff --git a/examples/sfm/src/components/Counter.css b/examples/sfm/src/components/Counter.css new file mode 100644 index 00000000..220e1794 --- /dev/null +++ b/examples/sfm/src/components/Counter.css @@ -0,0 +1,21 @@ +.increment { + font-family: inherit; + font-size: inherit; + padding: 1em 2em; + color: #335d92; + background-color: rgba(68, 107, 158, 0.1); + border-radius: 2em; + border: 2px solid rgba(68, 107, 158, 0); + outline: none; + width: 200px; + font-variant-numeric: tabular-nums; + cursor: pointer; +} + +.increment:focus { + border: 2px solid #335d92; +} + +.increment:active { + background-color: rgba(68, 107, 158, 0.2); +} \ No newline at end of file diff --git a/examples/sfm/src/components/Counter.tsx b/examples/sfm/src/components/Counter.tsx new file mode 100644 index 00000000..091fc5d0 --- /dev/null +++ b/examples/sfm/src/components/Counter.tsx @@ -0,0 +1,11 @@ +import { createSignal } from "solid-js"; +import "./Counter.css"; + +export default function Counter() { + const [count, setCount] = createSignal(0); + return ( + + ); +} diff --git a/examples/sfm/src/entry-client.tsx b/examples/sfm/src/entry-client.tsx new file mode 100644 index 00000000..0ca4e3c3 --- /dev/null +++ b/examples/sfm/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from "@solidjs/start/client"; + +mount(() => , document.getElementById("app")!); diff --git a/examples/sfm/src/entry-server.tsx b/examples/sfm/src/entry-server.tsx new file mode 100644 index 00000000..401eff83 --- /dev/null +++ b/examples/sfm/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server"; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/examples/sfm/src/global.d.ts b/examples/sfm/src/global.d.ts new file mode 100644 index 00000000..dc6f10c2 --- /dev/null +++ b/examples/sfm/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/sfm/src/routes/[...404].tsx b/examples/sfm/src/routes/[...404].tsx new file mode 100644 index 00000000..4ea71ec7 --- /dev/null +++ b/examples/sfm/src/routes/[...404].tsx @@ -0,0 +1,19 @@ +import { Title } from "@solidjs/meta"; +import { HttpStatusCode } from "@solidjs/start"; + +export default function NotFound() { + return ( +
+ Not Found + +

Page Not Found

+

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/examples/sfm/src/routes/about.tsx b/examples/sfm/src/routes/about.tsx new file mode 100644 index 00000000..c1c2dcf5 --- /dev/null +++ b/examples/sfm/src/routes/about.tsx @@ -0,0 +1,10 @@ +import { Title } from "@solidjs/meta"; + +export default function About() { + return ( +
+ About +

About

+
+ ); +} diff --git a/examples/sfm/src/routes/index.tsx b/examples/sfm/src/routes/index.tsx new file mode 100644 index 00000000..5d557d81 --- /dev/null +++ b/examples/sfm/src/routes/index.tsx @@ -0,0 +1,19 @@ +import { Title } from "@solidjs/meta"; +import Counter from "~/components/Counter"; + +export default function Home() { + return ( +
+ Hello World +

Hello world!

+ +

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/examples/sfm/tsconfig.json b/examples/sfm/tsconfig.json new file mode 100644 index 00000000..7d5871a0 --- /dev/null +++ b/examples/sfm/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61408912..04d5a696 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,24 @@ importers: specifier: ^5.6.3 version: 5.6.3 + examples/sfm: + dependencies: + '@solidjs/meta': + specifier: ^0.29.4 + version: 0.29.4(solid-js@1.9.3) + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.2(solid-js@1.9.3) + '@solidjs/start': + specifier: ^1.0.10 + version: 1.0.10(solid-js@1.9.3)(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0)) + solid-js: + specifier: ^1.9.2 + version: 1.9.3 + vinxi: + specifier: ^0.4.3 + version: 0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3) + examples/tauri: dependencies: '@rspc/client': @@ -128,7 +146,7 @@ importers: devDependencies: tsup: specifier: ^8.3.5 - version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -144,7 +162,7 @@ importers: devDependencies: tsup: specifier: ^8.3.5 - version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -169,7 +187,7 @@ importers: version: 18.3.1 tsup: specifier: ^8.3.5 - version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -241,7 +259,7 @@ importers: devDependencies: tsup: specifier: ^8.3.5 - version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -257,7 +275,7 @@ importers: version: 2.1.1 tsup: specifier: ^8.3.5 - version: 8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + version: 8.3.5(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -831,6 +849,10 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} + '@babel/standalone@7.26.4': + resolution: {integrity: sha512-SF+g7S2mhTT1b7CHyfNjDkPU1corxg4LPYsyP0x5KuCl+EbtBQHRLqr9N3q7e7+x7NQ5LYxQf8mJ2PmzebLr0A==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -843,9 +865,29 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + + '@cloudflare/kv-asset-handler@0.3.4': + resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} + engines: {node: '>=16.13'} + + '@deno/shim-deno-test@0.5.0': + resolution: {integrity: sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==} + + '@deno/shim-deno@0.19.2': + resolution: {integrity: sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q==} + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild/aix-ppc64@0.20.2': + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -858,6 +900,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.20.2': + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -876,6 +924,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.20.2': + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -888,6 +942,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.20.2': + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -900,6 +960,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.20.2': + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -912,6 +978,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.20.2': + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -924,6 +996,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.20.2': + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -936,6 +1014,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.20.2': + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -948,6 +1032,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.20.2': + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -960,6 +1050,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.20.2': + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -972,6 +1068,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.20.2': + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -990,6 +1092,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.20.2': + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -1002,6 +1110,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.20.2': + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -1014,6 +1128,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.20.2': + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -1026,6 +1146,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.20.2': + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -1038,6 +1164,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.20.2': + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -1050,6 +1182,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.20.2': + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -1062,6 +1200,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/netbsd-x64@0.20.2': + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -1080,6 +1224,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.20.2': + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -1092,6 +1242,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.20.2': + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -1104,6 +1260,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.20.2': + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -1116,6 +1278,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.20.2': + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -1128,6 +1296,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.20.2': + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -1245,10 +1419,17 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1270,6 +1451,23 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@mapbox/node-pre-gyp@2.0.0-rc.0': + resolution: {integrity: sha512-nhSMNprz3WmeRvd8iUs5JqkKr0Ncx46JtPxM3AhXes84XpSJfmIwKeWXRpsr53S7kqPkQfPhzrMFUxSNb23qSA==} + engines: {node: '>=18'} + hasBin: true + + '@netlify/functions@2.8.2': + resolution: {integrity: sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==} + engines: {node: '>=14.0.0'} + + '@netlify/node-cookies@0.1.0': + resolution: {integrity: sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==} + engines: {node: ^14.16.0 || >=16.0.0} + + '@netlify/serverless-functions-api@1.26.1': + resolution: {integrity: sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==} + engines: {node: '>=18.0.0'} + '@next/env@15.0.3': resolution: {integrity: sha512-t9Xy32pjNOvVn2AS+Utt6VmyrshbpfUMhIjFO60gI58deSo/KgLOp31XZ4O+kY/Is8WAGYwA5gR7kOb1eORDBA==} @@ -1336,10 +1534,123 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-wasm@2.3.0': + resolution: {integrity: sha512-ejBAX8H0ZGsD8lSICDNyMbSEtPMWgDL0WFCt/0z7hyf5v8Imz4rAM8xY379mBsECkq/Wdqa5WEDLqtjZ+6NxfA==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + + '@parcel/watcher-wasm@2.5.0': + resolution: {integrity: sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.17.1': + resolution: {integrity: sha512-CEmvaJuG7pm2ylQg53emPmtgm4nW2nxBgwXzbVEHpGas/lGnMyN8Zlkgiz6rPw0unASg6VW3wlz27SOL5XFHYQ==} + + '@redocly/openapi-core@1.26.1': + resolution: {integrity: sha512-xRuVZqMVRFzqjbUCpOTra4tbnmQMWsya996omZMV3WgD084Z6OWB3FXflhAp93E/yAmbWlWZpddw758AyoaLSw==} + engines: {node: '>=14.19.0', npm: '>=7.0.0'} + + '@rollup/plugin-alias@5.1.1': + resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/plugin-babel@6.0.4': resolution: {integrity: sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==} engines: {node: '>=14.0.0'} @@ -1353,6 +1664,33 @@ packages: rollup: optional: true + '@rollup/plugin-commonjs@28.0.2': + resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-inject@5.0.5': + resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/plugin-node-resolve@15.3.0': resolution: {integrity: sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==} engines: {node: '>=14.0.0'} @@ -1362,6 +1700,15 @@ packages: rollup: optional: true + '@rollup/plugin-replace@6.0.2': + resolution: {integrity: sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/plugin-terser@0.1.0': resolution: {integrity: sha512-N2KK+qUfHX2hBzVzM41UWGLrEmcjVC37spC8R3c9mt3oEDFKh3N2e12/lLp9aVSt86veR0TQiCNQXrm8C6aiUQ==} engines: {node: '>=14.0.0'} @@ -1371,6 +1718,15 @@ packages: rollup: optional: true + '@rollup/plugin-terser@0.4.4': + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/pluginutils@5.1.3': resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} engines: {node: '>=14.0.0'} @@ -1485,6 +1841,23 @@ packages: '@shikijs/vscode-textmate@9.3.0': resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@solidjs/meta@0.29.4': + resolution: {integrity: sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g==} + peerDependencies: + solid-js: '>=1.8.4' + + '@solidjs/router@0.15.2': + resolution: {integrity: sha512-UWtliRvOnjfYMONQcTGwtf6BEud5QlF0oHC5L+kcSGYn0jARH5KzC3+3LLZ0al7oQo/5Rc50ssMswPuAuxFvAA==} + peerDependencies: + solid-js: ^1.8.6 + + '@solidjs/start@1.0.10': + resolution: {integrity: sha512-3yg4KraSxc4rXs9dy/3kkqjDhU0JCPsZFLmDl5n6hHRPwtLLac6WUhs2k5VxGzitHaaJM/ZQCfT7i544Mf+4tw==} + '@sveltejs/package@2.3.7': resolution: {integrity: sha512-LYgUkde5GUYqOpXbcoCGUpEH4Ctl3Wj4u4CVZBl56dEeLW5fGHE037ZL1qlK0Ky+QD5uUfwONSeGwIOIighFMQ==} engines: {node: ^16.14 || >=18} @@ -1599,6 +1972,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/braces@3.0.4': + resolution: {integrity: sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1611,9 +1987,15 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-proxy@1.17.15': + resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/micromatch@4.0.9': + resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==} + '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -1641,6 +2023,30 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@vercel/nft@0.27.10': + resolution: {integrity: sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==} + engines: {node: '>=16'} + hasBin: true + + '@vinxi/listhen@1.5.6': + resolution: {integrity: sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw==} + hasBin: true + + '@vinxi/plugin-directives@0.4.3': + resolution: {integrity: sha512-Ey+TRIwyk8871PKhQel8NyZ9B6N0Tvhjo1QIttTyrV0d7BfUpri5GyGygmBY7fHClSE/vqaNCCZIKpTL3NJAEg==} + peerDependencies: + vinxi: ^0.4.3 + + '@vinxi/server-components@0.4.3': + resolution: {integrity: sha512-KVEnQtb+ZlXIEKaUw4r4WZl/rqFeZqSyIRklY1wFiPw7GCJUxbXzISpsJ+HwDhYi9k4n8uZJyQyLHGkoiEiolg==} + peerDependencies: + vinxi: ^0.4.3 + + '@vinxi/server-functions@0.4.3': + resolution: {integrity: sha512-kVYrOrCMHwGvHRwpaeW2/PE7URcGtz4Rk/hIHa2xjt5PGopzzB/Y5GC8YgZjtqSRqo0ElAKsEik7UE6CXH3HXA==} + peerDependencies: + vinxi: ^0.4.3 + '@vitejs/plugin-react@4.3.3': resolution: {integrity: sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1676,17 +2082,52 @@ packages: '@vitest/utils@2.1.5': resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-loose@8.4.0: + resolution: {integrity: sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==} + engines: {node: '>=0.4.0'} + + acorn-typescript@1.4.13: + resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} + peerDependencies: + acorn: '>=8.9.0' + acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@1.4.0: resolution: {integrity: sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==} engines: {node: '>=0.10.0'} @@ -1718,6 +2159,18 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1742,11 +2195,25 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + astro@4.16.13: resolution: {integrity: sha512-Mtd76+BC0zLWqoXpf9xc731AhdH4MNh5JFHYdLRvSH0Nqn48hA64dPGh/cWsJvh/DZFmC0NTZusM1Qq2gyNaVg==} engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1760,6 +2227,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + babel-plugin-jsx-dom-expressions@0.39.3: resolution: {integrity: sha512-6RzmSu21zYPlV2gNwzjGG9FgODtt9hIWnx7L//OIioIEuRcnpDZoY8Tr+I81Cy1SrH4qoDyKpwHHo6uAMAeyPA==} peerDependencies: @@ -1791,12 +2261,25 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.5.0: + resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==} + base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + biome@0.3.3: resolution: {integrity: sha512-4LXjrQYbn9iTXu9Y4SKT7ABzTV0WnLDHCVSd2fPUOKsy1gQ+E4xPFmlY1zcWexoi0j7fGHItlL6OWA2CZ/yYAQ==} hasBin: true @@ -1804,6 +2287,10 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + boxen@8.0.1: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} @@ -1823,9 +2310,16 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-require@5.0.0: resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1836,10 +2330,22 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + c12@2.0.1: + resolution: {integrity: sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} @@ -1865,6 +2371,9 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -1878,14 +2387,29 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.1: resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} engines: {node: '>= 14.16.0'} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@4.1.0: resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1908,10 +2432,22 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + code-point-at@1.1.0: resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} engines: {node: '>=0.10.0'} @@ -1933,6 +2469,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1953,9 +2492,22 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compatx@0.1.8: + resolution: {integrity: sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1963,6 +2515,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1977,10 +2532,34 @@ packages: core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + croner@9.0.0: + resolution: {integrity: sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==} + engines: {node: '>=18.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.2.4: + resolution: {integrity: sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==} + peerDependencies: + uWebSockets.js: '*' + peerDependenciesMeta: + uWebSockets.js: + optional: true + + crossws@0.3.1: + resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} + css-tree@2.3.1: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -1997,6 +2576,37 @@ packages: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} + dax-sh@0.39.2: + resolution: {integrity: sha512-gpuGEkBQM+5y6p4cWaw9+ePy5TNon+fdwFVtTI8leU3UhwhsBfPewRxMXGuQNC+M2b/MDGMlfgpqynkcd0C3FQ==} + + db0@0.2.1: + resolution: {integrity: sha512-BWSFmLaCkfyqbSEZBQINMVNjCVfrogi7GQ2RSy1tmtfK9OXlsup6lUMwLsqSD7FbAjD04eWFdXowSHHUp6SE/Q==} + peerDependencies: + '@electric-sql/pglite': '*' + '@libsql/client': '*' + better-sqlite3: '*' + drizzle-orm: '*' + mysql2: '*' + peerDependenciesMeta: + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + better-sqlite3: + optional: true + drizzle-orm: + optional: true + mysql2: + optional: true + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -2020,14 +2630,36 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -2054,10 +2686,21 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + earlgrey-runtime@0.1.2: resolution: {integrity: sha512-T4qoScXi5TwALDv8nlGTvOuCT8jXcKcxtO8qVdqv46IA2GHJfQzwoBPbkOmORnyhu3A98cVVuhWLsM2CzPljJg==} @@ -2070,6 +2713,9 @@ packages: editor@1.0.0: resolution: {integrity: sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.63: resolution: {integrity: sha512-ddeXKuY9BHo/mw145axlyWjlJ1UBt4WK3AlvkT7W2AbqfRQoacVoRUCF6wL3uIx/8wT9oLKXzI+rFqHHscByaA==} @@ -2085,10 +2731,21 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} @@ -2217,6 +2874,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2231,6 +2893,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -2254,9 +2919,28 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + exit-hook@1.1.1: resolution: {integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==} engines: {node: '>=0.10.0'} @@ -2279,6 +2963,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -2301,6 +2988,9 @@ packages: resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} engines: {node: '>=0.10.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2320,6 +3010,15 @@ packages: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -2331,9 +3030,21 @@ packages: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} engines: {node: '>= 0.12'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fs-extra@0.26.7: resolution: {integrity: sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==} + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + fs-promise@0.5.0: resolution: {integrity: sha512-Y+4F4ujhEcayCJt6JmzcOun9MYGQwz+bVUiuBmTkJImhBHKpBvmVPZR9wtfiF7k3ffwAOAuurygQe+cPLSFQhw==} deprecated: Use mz or fs-extra^3.0 with Promise Support @@ -2353,13 +3064,28 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-port-please@3.1.2: + resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + giget@1.2.3: + resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} + hasBin: true + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -2379,6 +3105,10 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2386,6 +3116,16 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + gzip-size@7.0.0: + resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + h3@1.11.1: + resolution: {integrity: sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==} + + h3@1.13.0: + resolution: {integrity: sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==} + har-schema@2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} engines: {node: '>=4'} @@ -2433,25 +3173,65 @@ packages: hastscript@9.0.0: resolution: {integrity: sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + html-to-image@1.11.11: + resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-shutdown@1.2.2: + resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + httpxy@0.1.5: + resolution: {integrity: sha512-hqLDO+rfststuyEUTWObQK6zHEEmZ/kaIP2/zclGGZn6X8h/ESTWg+WKecQ/e5k4nPswjzZD+q2VqZIbr15CoQ==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + index-to-position@0.1.2: + resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -2465,13 +3245,29 @@ packages: inquirer@0.11.4: resolution: {integrity: sha512-QR+2TW90jnKk9LUUtbcA3yQXKt2rDEKMh6+BAZQIeumtzHexnwVLdPakSslGijXYLJCzFv7GMXbFCn0pA00EUw==} + ioredis@5.4.1: + resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} + engines: {node: '>=12.22.0'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2517,9 +3313,20 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-reference@3.0.2: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -2535,13 +3342,28 @@ packages: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} @@ -2552,13 +3374,24 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -2578,6 +3411,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -2592,6 +3428,9 @@ packages: jsonfile@2.4.0: resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsprim@1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} @@ -2614,6 +3453,17 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + knitwork@1.2.0: + resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + lightningcss-darwin-arm64@1.27.0: resolution: {integrity: sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==} engines: {node: '>= 12.0.0'} @@ -2685,6 +3535,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listhen@1.9.0: + resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} + hasBin: true + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2693,6 +3547,10 @@ packages: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -2703,6 +3561,12 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -2738,6 +3602,12 @@ packages: magic-string@0.30.13: resolution: {integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.2.11: + resolution: {integrity: sha512-6saXbRDA1HMkqbsvHOU6HBjCVgZT460qheRkLhJQHWAbhXoWESI3Kn/dGGXyKs15FFKR85jsUqFx2sMK0wy/5g==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -2790,6 +3660,9 @@ packages: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2890,6 +3763,25 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mime@4.0.6: + resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==} + engines: {node: '>=16'} + hasBin: true + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -2897,14 +3789,47 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.0.1: + resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} + engines: {node: '>= 18'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2913,6 +3838,9 @@ packages: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2952,19 +3880,70 @@ packages: sass: optional: true + nitropack@2.10.4: + resolution: {integrity: sha512-sJiG/MIQlZCVSw2cQrFG1H6mLeSqHlYfFerRjLKz69vUfdu0EL2l0WdOxlQbzJr3mMv/l4cOlCCLzVRzjzzF/g==} + engines: {node: ^16.11.0 || >=17.0.0} + hasBin: true + peerDependencies: + xml2js: ^0.6.2 + peerDependenciesMeta: + xml2js: + optional: true + nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nopt@8.0.0: + resolution: {integrity: sha512-1L/fTJ4UmV/lUxT2Uf006pfZKTvAgCF+chz+0OgBHO8u2Z67pE7AaAUUj7CJy0lXqHmymUvGFt6NE9R3HER0yw==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} engines: {node: '>=0.10.0'} + nypm@0.3.12: + resolution: {integrity: sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -2972,6 +3951,16 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@1.1.4: + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2979,6 +3968,10 @@ packages: resolution: {integrity: sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==} engines: {node: '>=0.10.0'} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -2986,6 +3979,16 @@ packages: oniguruma-to-es@0.4.1: resolution: {integrity: sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openapi-typescript@7.4.4: + resolution: {integrity: sha512-7j3nktnRzlQdlHnHsrcr6Gqz8f80/RhfA2I8s1clPI+jkY0hLNmnYVKBfuUEli5EEgK1B6M+ibdS5REasPlsUw==} + hasBin: true + peerDependencies: + typescript: ^5.x + ora@8.1.1: resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} engines: {node: '>=18'} @@ -3021,12 +4024,20 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parse-json@8.1.0: + resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==} + engines: {node: '>=18'} + parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -3042,6 +4053,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -3049,6 +4064,13 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -3056,6 +4078,9 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -3085,6 +4110,13 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -3115,10 +4147,21 @@ packages: resolution: {integrity: sha512-gYBeFTZLu055D8Vv3cSPox/0iTPtkzxpLroSYYA7WXgRi31WCJ51Uyl8ZiPeUUjyvs2MBzK+S8v9JVUgHU/Sqw==} engines: {node: '>=18.12'} + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3140,6 +4183,22 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -3153,6 +4212,20 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@4.6.0: + resolution: {integrity: sha512-cbAdYt0VcnpN2Bekq7PU+k363ZRsPwJoEEJOEtSJQlJXzwaxt3FIo/uL+KeDSGIjJqtkwyge4KQgD2S2kd+CQw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.0.2: resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} engines: {node: '>= 14.16.0'} @@ -3160,6 +4233,18 @@ packages: readline2@1.0.1: resolution: {integrity: sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g==} + recast@0.23.9: + resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} + engines: {node: '>= 4'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + regenerate-unicode-properties@10.2.0: resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} engines: {node: '>=4'} @@ -3234,6 +4319,17 @@ packages: engines: {node: '>= 6'} deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -3271,11 +4367,25 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup-preset-solid@2.0.1: - resolution: {integrity: sha512-CPJn3SqADlIxhAW3jwZuAFRyZcz7HPeUAz4f+6BzulxHnK4v6tgoTbMvk8vEsfsvHwiTmX93KHIKdf79aTdVSA==} + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true - rollup@3.29.5: - resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + rollup-plugin-visualizer@5.12.0: + resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rollup: + optional: true + + rollup-preset-solid@2.0.1: + resolution: {integrity: sha512-CPJn3SqADlIxhAW3jwZuAFRyZcz7HPeUAz4f+6BzulxHnK4v6tgoTbMvk8vEsfsvHwiTmX93KHIKdf79aTdVSA==} + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true @@ -3297,6 +4407,9 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3306,6 +4419,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -3319,6 +4435,13 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + seroval-plugins@1.1.1: resolution: {integrity: sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==} engines: {node: '>=10'} @@ -3329,6 +4452,16 @@ packages: resolution: {integrity: sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==} engines: {node: '>=10'} + serve-placeholder@2.0.2: + resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3344,6 +4477,14 @@ packages: shiki@1.23.1: resolution: {integrity: sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==} + shikiji-core@0.9.19: + resolution: {integrity: sha512-AFJu/vcNT21t0e6YrfadZ+9q86gvPum6iywRyt1OtIPjPFe25RQnYJyxHQPMLKCCWA992TPxmEmbNcOZCAJclw==} + deprecated: Shikiji is merged back to Shiki v1.0, please migrate over to get the latest updates + + shikiji@0.9.19: + resolution: {integrity: sha512-Kw2NHWktdcdypCj1GkKpXH4o6Vxz8B8TykPlPuLHOGSV8VkhoCLcFOH4k19K4LXAQYRQmxg+0X/eM+m2sLhAkg==} + deprecated: Shikiji is merged back to Shiki v1.0, please migrate over to get the latest updates + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3357,6 +4498,13 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + solid-js@1.9.3: resolution: {integrity: sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==} @@ -3365,6 +4513,12 @@ packages: peerDependencies: solid-js: ^1.3 + solid-use@0.9.0: + resolution: {integrity: sha512-8TGwB4m3qQ7qKo8Lg0pi/ZyyGVmQIjC4sPyxRCH7VPds0BzSsT734PhP3jhR6zMJxoYHM+uoivjq0XdpzXeOJg==} + engines: {node: '>=10'} + peerDependencies: + solid-js: ^1.7 + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3376,6 +4530,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -3394,6 +4552,16 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} @@ -3405,6 +4573,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.21.1: + resolution: {integrity: sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==} + string-width@1.0.2: resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} engines: {node: '>=0.10.0'} @@ -3421,6 +4592,12 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -3444,6 +4621,13 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -3466,6 +4650,10 @@ packages: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} engines: {node: '>=0.8.0'} + supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -3480,11 +4668,35 @@ packages: resolution: {integrity: sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==} engines: {node: '>=16'} + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + terracotta@1.0.6: + resolution: {integrity: sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw==} + engines: {node: '>=10'} + peerDependencies: + solid-js: ^1.8 + terser@5.36.0: resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} engines: {node: '>=10'} hasBin: true + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3495,6 +4707,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3521,10 +4736,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -3613,6 +4835,10 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-fest@4.27.0: resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} engines: {node: '>=16'} @@ -3627,12 +4853,27 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + ultrahtml@1.5.3: resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unctx@2.4.1: + resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==} + + undici-types@5.28.4: + resolution: {integrity: sha512-3OeMF5Lyowe8VW0skf5qaIE7Or3yS9LS7fvMUI0gg4YxpIBVg0L8BxCmROw2CcYhSkpR68Epz7CGc8MPj94Uww==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unenv@1.10.0: + resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -3649,9 +4890,16 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unimport@3.14.5: + resolution: {integrity: sha512-tn890SwFFZxqaJSKQPPd+yygfKSATbM8BZWW1aCR2TJBTs1SDrmLamBueaFtYsGjHtQaRgqEbQflOjN2iW12gA==} + unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} @@ -3679,23 +4927,117 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin@1.16.0: + resolution: {integrity: sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==} + engines: {node: '>=14.0.0'} + + unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + + unstorage@1.14.1: + resolution: {integrity: sha512-0MBKpoVhNLL/Ixvue9lIsrHkwwWW9/f3TRftsYu1R7nZJJyHSdgPMBDjny2op07nirnS3OX6H3u+YDFGld+1Bg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.5.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 + '@deno/kv': '>=0.8.4' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.0' + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.1 + uploadthing: ^7.4.1 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + untildify@3.0.3: resolution: {integrity: sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==} engines: {node: '>=4'} + untun@0.1.3: + resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} + hasBin: true + + untyped@1.5.2: + resolution: {integrity: sha512-eL/8PlhLcMmlMDtNPKhyyz9kEBDS3Uk4yMu/ewlkT2WFbtzScjHWPJLdQLmaGPUKjXzwe9MumOtOgc4Fro96Kg==} + hasBin: true + + unwasm@0.3.9: + resolution: {integrity: sha512-LDxTx/2DkFURUd+BU1vUsF/moj0JsoTvl+2tcg2AUOiEzVturhGGx17/IMgGvKUYdZwr33EJHtChCJuhu9Ouvg==} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urlpattern-polyfill@8.0.2: + resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} + user-home@2.0.0: resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} engines: {node: '>=0.10.0'} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. @@ -3717,6 +5059,10 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vinxi@0.4.3: + resolution: {integrity: sha512-RgJz7RWftML5h/qfPsp3QKVc2FSlvV4+HevpE0yEY2j+PS/I2ULjoSsZDXaR8Ks2WYuFFDzQr8yrox7v8aqkng==} + hasBin: true + vite-node@2.1.5: resolution: {integrity: sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3807,9 +5153,18 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -3826,11 +5181,20 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} @@ -3853,9 +5217,23 @@ packages: xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@2.6.1: resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} @@ -3865,10 +5243,18 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@1.1.1: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod-to-json-schema@3.23.5: resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} peerDependencies: @@ -3948,7 +5334,7 @@ snapshots: '@astrojs/telemetry@3.1.0': dependencies: ci-info: 4.1.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -3978,7 +5364,7 @@ snapshots: '@babel/traverse': 7.25.9 '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4037,7 +5423,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -4648,6 +6034,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/standalone@7.26.4': {} + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -4661,7 +6049,7 @@ snapshots: '@babel/parser': 7.26.2 '@babel/template': 7.25.9 '@babel/types': 7.26.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4671,17 +6059,39 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@cloudflare/kv-asset-handler@0.3.4': + dependencies: + mime: 3.0.0 + + '@deno/shim-deno-test@0.5.0': {} + + '@deno/shim-deno@0.19.2': + dependencies: + '@deno/shim-deno-test': 0.5.0 + which: 4.0.0 + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.20.2': + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true '@esbuild/aix-ppc64@0.24.0': optional: true + '@esbuild/android-arm64@0.20.2': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true @@ -4691,54 +6101,81 @@ snapshots: '@esbuild/android-arm@0.15.18': optional: true + '@esbuild/android-arm@0.20.2': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.24.0': optional: true + '@esbuild/android-x64@0.20.2': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.24.0': optional: true + '@esbuild/darwin-arm64@0.20.2': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.24.0': optional: true + '@esbuild/darwin-x64@0.20.2': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.24.0': optional: true + '@esbuild/freebsd-arm64@0.20.2': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.24.0': optional: true + '@esbuild/freebsd-x64@0.20.2': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.24.0': optional: true + '@esbuild/linux-arm64@0.20.2': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.24.0': optional: true + '@esbuild/linux-arm@0.20.2': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.24.0': optional: true + '@esbuild/linux-ia32@0.20.2': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true @@ -4748,42 +6185,63 @@ snapshots: '@esbuild/linux-loong64@0.15.18': optional: true + '@esbuild/linux-loong64@0.20.2': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.24.0': optional: true + '@esbuild/linux-mips64el@0.20.2': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.24.0': optional: true + '@esbuild/linux-ppc64@0.20.2': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.24.0': optional: true + '@esbuild/linux-riscv64@0.20.2': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.24.0': optional: true + '@esbuild/linux-s390x@0.20.2': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.24.0': optional: true + '@esbuild/linux-x64@0.20.2': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.24.0': optional: true + '@esbuild/netbsd-x64@0.20.2': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true @@ -4793,30 +6251,45 @@ snapshots: '@esbuild/openbsd-arm64@0.24.0': optional: true + '@esbuild/openbsd-x64@0.20.2': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.24.0': optional: true + '@esbuild/sunos-x64@0.20.2': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.24.0': optional: true + '@esbuild/win32-arm64@0.20.2': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.24.0': optional: true + '@esbuild/win32-ia32@0.20.2': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.24.0': optional: true + '@esbuild/win32-x64@0.20.2': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true @@ -4898,6 +6371,8 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@ioredis/commands@1.2.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4907,6 +6382,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -4929,6 +6408,30 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@mapbox/node-pre-gyp@2.0.0-rc.0': + dependencies: + consola: 3.2.3 + detect-libc: 2.0.3 + https-proxy-agent: 7.0.6(supports-color@9.4.0) + node-fetch: 2.7.0 + nopt: 8.0.0 + semver: 7.6.3 + tar: 7.4.3 + transitivePeerDependencies: + - encoding + - supports-color + + '@netlify/functions@2.8.2': + dependencies: + '@netlify/serverless-functions-api': 1.26.1 + + '@netlify/node-cookies@0.1.0': {} + + '@netlify/serverless-functions-api@1.26.1': + dependencies: + '@netlify/node-cookies': 0.1.0 + urlpattern-polyfill: 8.0.2 + '@next/env@15.0.3': {} '@next/swc-darwin-arm64@15.0.3': @@ -4969,89 +6472,239 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@pkgjs/parseargs@0.11.0': + '@parcel/watcher-android-arm64@2.5.0': optional: true - '@rollup/plugin-babel@6.0.4(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@3.29.5)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-imports': 7.25.9 - '@rollup/pluginutils': 5.1.3(rollup@3.29.5) - optionalDependencies: - '@types/babel__core': 7.20.5 - rollup: 3.29.5 - transitivePeerDependencies: - - supports-color - - '@rollup/plugin-node-resolve@15.3.0(rollup@3.29.5)': - dependencies: - '@rollup/pluginutils': 5.1.3(rollup@3.29.5) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.8 - optionalDependencies: - rollup: 3.29.5 - - '@rollup/plugin-terser@0.1.0(rollup@3.29.5)': - dependencies: - terser: 5.36.0 - optionalDependencies: - rollup: 3.29.5 - - '@rollup/pluginutils@5.1.3(rollup@3.29.5)': - dependencies: - '@types/estree': 1.0.6 - estree-walker: 2.0.2 - picomatch: 4.0.2 - optionalDependencies: - rollup: 3.29.5 - - '@rollup/pluginutils@5.1.3(rollup@4.27.3)': - dependencies: - '@types/estree': 1.0.6 - estree-walker: 2.0.2 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.27.3 - - '@rollup/rollup-android-arm-eabi@4.27.3': + '@parcel/watcher-darwin-arm64@2.5.0': optional: true - '@rollup/rollup-android-arm64@4.27.3': + '@parcel/watcher-darwin-x64@2.5.0': optional: true - '@rollup/rollup-darwin-arm64@4.27.3': + '@parcel/watcher-freebsd-x64@2.5.0': optional: true - '@rollup/rollup-darwin-x64@4.27.3': + '@parcel/watcher-linux-arm-glibc@2.5.0': optional: true - '@rollup/rollup-freebsd-arm64@4.27.3': + '@parcel/watcher-linux-arm-musl@2.5.0': optional: true - '@rollup/rollup-freebsd-x64@4.27.3': + '@parcel/watcher-linux-arm64-glibc@2.5.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + '@parcel/watcher-linux-arm64-musl@2.5.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.27.3': + '@parcel/watcher-linux-x64-glibc@2.5.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.27.3': + '@parcel/watcher-linux-x64-musl@2.5.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.27.3': - optional: true + '@parcel/watcher-wasm@2.3.0': + dependencies: + is-glob: 4.0.3 + micromatch: 4.0.8 - '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + '@parcel/watcher-wasm@2.5.0': + dependencies: + is-glob: 4.0.3 + micromatch: 4.0.8 + + '@parcel/watcher-win32-arm64@2.5.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.27.3': + '@parcel/watcher-win32-ia32@2.5.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.27.3': + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher@2.5.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.17.1': {} + + '@redocly/openapi-core@1.26.1(supports-color@9.4.0)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.17.1 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@9.4.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + node-fetch: 2.7.0 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - encoding + - supports-color + + '@rollup/plugin-alias@5.1.1(rollup@4.27.3)': + optionalDependencies: + rollup: 4.27.3 + + '@rollup/plugin-babel@6.0.4(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@3.29.5)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@rollup/pluginutils': 5.1.3(rollup@3.29.5) + optionalDependencies: + '@types/babel__core': 7.20.5 + rollup: 3.29.5 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-commonjs@28.0.2(rollup@4.27.3)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.2(picomatch@4.0.2) + is-reference: 1.2.1 + magic-string: 0.30.13 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/plugin-inject@5.0.5(rollup@4.27.3)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + estree-walker: 2.0.2 + magic-string: 0.30.13 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/plugin-json@6.1.0(rollup@4.27.3)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + optionalDependencies: + rollup: 4.27.3 + + '@rollup/plugin-node-resolve@15.3.0(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@3.29.5) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-node-resolve@15.3.0(rollup@4.27.3)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/plugin-replace@6.0.2(rollup@4.27.3)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + magic-string: 0.30.13 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/plugin-terser@0.1.0(rollup@3.29.5)': + dependencies: + terser: 5.36.0 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/plugin-terser@0.4.4(rollup@4.27.3)': + dependencies: + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.36.0 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/pluginutils@5.1.3(rollup@3.29.5)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/pluginutils@5.1.3(rollup@4.27.3)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/rollup-android-arm-eabi@4.27.3': + optional: true + + '@rollup/rollup-android-arm64@4.27.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.27.3': + optional: true + + '@rollup/rollup-darwin-x64@4.27.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.27.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.27.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.27.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.27.3': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.27.3': optional: true '@rollup/rollup-linux-x64-gnu@4.27.3': @@ -5096,6 +6749,39 @@ snapshots: '@shikijs/vscode-textmate@9.3.0': {} + '@sindresorhus/merge-streams@2.3.0': {} + + '@solidjs/meta@0.29.4(solid-js@1.9.3)': + dependencies: + solid-js: 1.9.3 + + '@solidjs/router@0.15.2(solid-js@1.9.3)': + dependencies: + solid-js: 1.9.3 + + '@solidjs/start@1.0.10(solid-js@1.9.3)(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3))(vite@5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0))': + dependencies: + '@vinxi/plugin-directives': 0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3)) + '@vinxi/server-components': 0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3)) + '@vinxi/server-functions': 0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3)) + defu: 6.1.4 + error-stack-parser: 2.1.4 + html-to-image: 1.11.11 + radix3: 1.1.2 + seroval: 1.1.1 + seroval-plugins: 1.1.1(seroval@1.1.1) + shikiji: 0.9.19 + source-map-js: 1.2.1 + terracotta: 1.0.6(solid-js@1.9.3) + tinyglobby: 0.2.10 + vite-plugin-solid: 2.10.2(solid-js@1.9.3)(vite@5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0)) + transitivePeerDependencies: + - '@testing-library/jest-dom' + - solid-js + - supports-color + - vinxi + - vite + '@sveltejs/package@2.3.7(svelte@4.2.18)(typescript@5.6.3)': dependencies: chokidar: 4.0.1 @@ -5200,6 +6886,8 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@types/braces@3.0.4': {} + '@types/cookie@0.6.0': {} '@types/debug@4.1.12': @@ -5212,10 +6900,18 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/http-proxy@1.17.15': + dependencies: + '@types/node': 22.9.1 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/micromatch@4.0.9': + dependencies: + '@types/braces': 3.0.4 + '@types/ms@0.7.34': {} '@types/nlcst@2.0.3': @@ -5243,6 +6939,82 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@vercel/nft@0.27.10(rollup@4.27.3)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.0-rc.0 + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.2 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vinxi/listhen@1.5.6': + dependencies: + '@parcel/watcher': 2.5.0 + '@parcel/watcher-wasm': 2.3.0 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.2.3 + defu: 6.1.4 + get-port-please: 3.1.2 + h3: 1.11.1 + http-shutdown: 1.2.2 + jiti: 1.21.6 + mlly: 1.7.3 + node-forge: 1.3.1 + pathe: 1.1.2 + std-env: 3.8.0 + ufo: 1.5.4 + untun: 0.1.3 + uqr: 0.1.2 + transitivePeerDependencies: + - uWebSockets.js + + '@vinxi/plugin-directives@0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3))': + dependencies: + '@babel/parser': 7.26.2 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + acorn-loose: 8.4.0 + acorn-typescript: 1.4.13(acorn@8.14.0) + astring: 1.9.0 + magicast: 0.2.11 + recast: 0.23.9 + tslib: 2.8.1 + vinxi: 0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3) + + '@vinxi/server-components@0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3))': + dependencies: + '@vinxi/plugin-directives': 0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3)) + acorn: 8.14.0 + acorn-loose: 8.4.0 + acorn-typescript: 1.4.13(acorn@8.14.0) + astring: 1.9.0 + magicast: 0.2.11 + recast: 0.23.9 + vinxi: 0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3) + + '@vinxi/server-functions@0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3))': + dependencies: + '@vinxi/plugin-directives': 0.4.3(vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3)) + acorn: 8.14.0 + acorn-loose: 8.4.0 + acorn-typescript: 1.4.13(acorn@8.14.0) + astring: 1.9.0 + magicast: 0.2.11 + recast: 0.23.9 + vinxi: 0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3) + '@vitejs/plugin-react@4.3.3(vite@5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 @@ -5294,8 +7066,32 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 + abbrev@2.0.0: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-import-attributes@1.9.5(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-loose@8.4.0: + dependencies: + acorn: 8.14.0 + + acorn-typescript@1.4.13(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + acorn@8.14.0: {} + agent-base@7.1.3: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5307,6 +7103,8 @@ snapshots: dependencies: string-width: 4.2.3 + ansi-colors@4.1.3: {} + ansi-escapes@1.4.0: {} ansi-regex@2.1.1: {} @@ -5325,6 +7123,31 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.6.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.6.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -5343,6 +7166,12 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + astring@1.9.0: {} + astro@4.16.13(@types/node@22.9.1)(lightningcss@1.27.0)(rollup@4.27.3)(terser@5.36.0)(typescript@5.6.3): dependencies: '@astrojs/compiler': 2.10.3 @@ -5365,7 +7194,7 @@ snapshots: common-ancestor-path: 1.0.1 cookie: 0.7.2 cssesc: 3.0.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) deterministic-object-hash: 2.0.2 devalue: 5.1.1 diff: 5.2.0 @@ -5422,6 +7251,10 @@ snapshots: - terser - typescript + async-sema@3.1.1: {} + + async@3.2.6: {} + asynckit@0.4.0: {} aws-sign2@0.7.0: {} @@ -5430,6 +7263,8 @@ snapshots: axobject-query@4.1.0: {} + b4a@1.6.7: {} + babel-plugin-jsx-dom-expressions@0.39.3(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -5473,12 +7308,23 @@ snapshots: balanced-match@1.0.2: {} + bare-events@2.5.0: + optional: true + base-64@1.0.0: {} + base64-js@1.5.1: {} + bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + biome@0.3.3: dependencies: bluebird: 3.7.2 @@ -5493,6 +7339,17 @@ snapshots: bluebird@3.7.2: {} + boxen@7.1.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.3.0 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + boxen@8.0.1: dependencies: ansi-align: 3.0.1 @@ -5524,8 +7381,15 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-require@5.0.0(esbuild@0.24.0): dependencies: esbuild: 0.24.0 @@ -5535,8 +7399,27 @@ snapshots: dependencies: streamsearch: 1.1.0 + c12@2.0.1(magicast@0.3.5): + dependencies: + chokidar: 4.0.1 + confbox: 0.1.8 + defu: 6.1.4 + dotenv: 16.4.7 + giget: 1.2.3 + jiti: 2.4.2 + mlly: 1.7.3 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.2.1 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} + camelcase@7.0.1: {} + camelcase@8.0.0: {} caniuse-lite@1.0.30001680: {} @@ -5563,6 +7446,8 @@ snapshots: chalk@5.3.0: {} + change-case@5.4.4: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -5571,12 +7456,32 @@ snapshots: check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.1: dependencies: readdirp: 4.0.2 + chownr@2.0.0: {} + + chownr@3.0.0: {} + ci-info@4.1.0: {} + citty@0.1.6: + dependencies: + consola: 3.2.3 + cli-boxes@3.0.0: {} cli-cursor@1.0.2: @@ -5593,8 +7498,22 @@ snapshots: client-only@0.0.1: {} + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} + code-point-at@1.1.0: {} code-red@1.0.4: @@ -5623,6 +7542,8 @@ snapshots: color-string: 1.9.1 optional: true + colorette@1.4.0: {} + colorette@2.0.20: {} combined-stream@1.0.8: @@ -5637,12 +7558,28 @@ snapshots: common-ancestor-path@1.0.1: {} + commondir@1.0.1: {} + + compatx@0.1.8: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.6.0 + concat-map@0.0.1: {} + confbox@0.1.8: {} + consola@3.2.3: {} convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + cookie@0.7.2: {} core-js-compat@3.39.0: @@ -5653,12 +7590,27 @@ snapshots: core-util-is@1.0.2: {} + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.6.0 + + croner@9.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + crossws@0.2.4: {} + + crossws@0.3.1: + dependencies: + uncrypto: 0.1.3 + css-tree@2.3.1: dependencies: mdn-data: 2.0.30 @@ -5672,9 +7624,22 @@ snapshots: dependencies: assert-plus: 1.0.0 - debug@4.3.7: + dax-sh@0.39.2: + dependencies: + '@deno/shim-deno': 0.19.2 + undici-types: 5.28.4 + + db0@0.2.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.7(supports-color@9.4.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 9.4.0 decode-named-character-reference@1.0.2: dependencies: @@ -5686,15 +7651,25 @@ snapshots: deepmerge@4.3.1: {} + define-lazy-prop@2.0.0: {} + + defu@6.1.4: {} + delayed-stream@1.0.0: {} + denque@2.1.0: {} + + depd@2.0.0: {} + dequal@2.0.3: {} - detect-libc@1.0.3: - optional: true + destr@2.0.3: {} - detect-libc@2.0.3: - optional: true + destroy@1.2.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.0.3: {} deterministic-object-hash@2.0.2: dependencies: @@ -5710,8 +7685,16 @@ snapshots: dlv@1.1.3: {} + dot-prop@9.0.0: + dependencies: + type-fest: 4.27.0 + + dotenv@16.4.7: {} + dset@3.1.4: {} + duplexer@0.1.2: {} + earlgrey-runtime@0.1.2: dependencies: core-js: 2.6.12 @@ -5728,6 +7711,8 @@ snapshots: editor@1.0.0: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.63: {} emoji-regex-xs@1.0.0: {} @@ -5738,8 +7723,16 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + entities@4.5.0: {} + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-module-lexer@1.5.4: {} esbuild-android-64@0.15.18: @@ -5827,6 +7820,32 @@ snapshots: esbuild-windows-64: 0.15.18 esbuild-windows-arm64: 0.15.18 + esbuild@0.20.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5882,6 +7901,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@5.0.0: {} @@ -5896,8 +7917,28 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} + events@3.3.0: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + exit-hook@1.1.1: {} expect-type@1.1.0: {} @@ -5912,6 +7953,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5935,6 +7978,8 @@ snapshots: escape-string-regexp: 1.0.5 object-assign: 4.1.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5953,6 +7998,8 @@ snapshots: flattie@1.1.1: {} + follow-redirects@1.15.9: {} + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 @@ -5966,6 +8013,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + fresh@0.5.2: {} + fs-extra@0.26.7: dependencies: graceful-fs: 4.2.11 @@ -5974,6 +8023,16 @@ snapshots: path-is-absolute: 1.0.1 rimraf: 2.7.1 + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + fs-promise@0.5.0: dependencies: any-promise: 1.3.0 @@ -5990,12 +8049,29 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.3.0: {} + get-port-please@3.1.2: {} + + get-stream@8.0.1: {} + getpass@0.1.7: dependencies: assert-plus: 1.0.0 + giget@1.2.3: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + defu: 6.1.4 + node-fetch-native: 1.6.4 + nypm: 0.3.12 + ohash: 1.1.4 + pathe: 1.1.2 + tar: 6.2.1 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -6022,6 +8098,15 @@ snapshots: globals@11.12.0: {} + globby@14.0.2: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + graceful-fs@4.2.11: {} gray-matter@4.0.3: @@ -6031,6 +8116,40 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + gzip-size@7.0.0: + dependencies: + duplexer: 0.1.2 + + h3@1.11.1: + dependencies: + cookie-es: 1.2.2 + crossws: 0.2.4 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + ohash: 1.1.4 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + unenv: 1.10.0 + transitivePeerDependencies: + - uWebSockets.js + + h3@1.13.0: + dependencies: + cookie-es: 1.2.2 + crossws: 0.2.4 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + ohash: 1.1.4 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + unenv: 1.10.0 + transitivePeerDependencies: + - uWebSockets.js + har-schema@2.0.0: {} har-validator@5.1.5: @@ -6133,22 +8252,61 @@ snapshots: property-information: 6.5.0 space-separated-tokens: 2.0.2 + hookable@5.5.3: {} + html-entities@2.3.3: {} html-escaper@3.0.3: {} + html-to-image@1.11.11: {} + html-void-elements@3.0.0: {} http-cache-semantics@4.1.1: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-shutdown@1.2.2: {} + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 jsprim: 1.4.2 sshpk: 1.18.0 + https-proxy-agent@7.0.6(supports-color@9.4.0): + dependencies: + agent-base: 7.1.3 + debug: 4.3.7(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + httpxy@0.1.5: {} + + human-signals@5.0.0: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + import-meta-resolve@4.1.0: {} + index-to-position@0.1.2: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -6177,13 +8335,35 @@ snapshots: strip-ansi: 3.0.1 through: 2.3.8 + ioredis@5.4.1: + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.7(supports-color@9.4.0) + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + iron-webcrypto@1.2.1: {} + is-arrayish@0.3.2: optional: true + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-core-module@2.15.1: dependencies: hasown: 2.0.2 + is-docker@2.2.1: {} + is-docker@3.0.0: {} is-extendable@0.1.1: {} @@ -6212,10 +8392,18 @@ snapshots: is-plain-obj@4.1.0: {} + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.6 + is-reference@3.0.2: dependencies: '@types/estree': 1.0.6 + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + is-typedarray@1.0.0: {} is-unicode-supported@1.3.0: {} @@ -6224,12 +8412,24 @@ snapshots: is-what@4.1.16: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + + isarray@1.0.0: {} + isexe@2.0.0: {} + isexe@3.1.1: {} + isstream@0.1.2: {} jackspeak@3.4.3: @@ -6238,13 +8438,18 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@1.21.6: - optional: true + jiti@1.21.6: {} + + jiti@2.4.2: {} joycon@3.1.1: {} + js-levenshtein@1.1.6: {} + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -6260,6 +8465,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} json-stringify-safe@5.0.1: {} @@ -6270,6 +8477,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsprim@1.4.2: dependencies: assert-plus: 1.0.0 @@ -6291,6 +8504,14 @@ snapshots: kleur@4.1.5: {} + klona@2.0.6: {} + + knitwork@1.2.0: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + lightningcss-darwin-arm64@1.27.0: optional: true @@ -6341,6 +8562,29 @@ snapshots: lines-and-columns@1.2.4: {} + listhen@1.9.0: + dependencies: + '@parcel/watcher': 2.5.0 + '@parcel/watcher-wasm': 2.5.0 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.2.3 + crossws: 0.2.4 + defu: 6.1.4 + get-port-please: 3.1.2 + h3: 1.13.0 + http-shutdown: 1.2.2 + jiti: 2.4.2 + mlly: 1.7.3 + node-forge: 1.3.1 + pathe: 1.1.2 + std-env: 3.8.0 + ufo: 1.5.4 + untun: 0.1.3 + uqr: 0.1.2 + transitivePeerDependencies: + - uWebSockets.js + load-tsconfig@0.2.5: {} load-yaml-file@0.2.0: @@ -6350,6 +8594,11 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + local-pkg@0.5.1: + dependencies: + mlly: 1.7.3 + pkg-types: 1.2.1 + locate-character@3.0.0: {} locate-path@5.0.0: @@ -6358,6 +8607,10 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.sortby@4.7.0: {} lodash@3.10.1: {} @@ -6391,6 +8644,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.2.11: + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + recast: 0.23.9 + magicast@0.3.5: dependencies: '@babel/parser': 7.26.2 @@ -6525,6 +8788,8 @@ snapshots: dependencies: is-what: 4.1.16 + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.2: @@ -6699,7 +8964,7 @@ snapshots: micromark@4.0.1: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.2 @@ -6729,22 +8994,63 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@1.6.0: {} + + mime@3.0.0: {} + + mime@4.0.6: {} + + mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 - minipass@7.1.2: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + minizlib@3.0.1: + dependencies: + minipass: 7.1.2 + rimraf: 5.0.10 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 mri@1.2.0: {} mrmime@2.0.0: {} + ms@2.0.0: {} + ms@2.1.3: {} mute-stream@0.0.5: {} @@ -6784,6 +9090,103 @@ snapshots: - '@babel/core' - babel-plugin-macros + nitropack@2.10.4(typescript@5.6.3): + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@netlify/functions': 2.8.2 + '@rollup/plugin-alias': 5.1.1(rollup@4.27.3) + '@rollup/plugin-commonjs': 28.0.2(rollup@4.27.3) + '@rollup/plugin-inject': 5.0.5(rollup@4.27.3) + '@rollup/plugin-json': 6.1.0(rollup@4.27.3) + '@rollup/plugin-node-resolve': 15.3.0(rollup@4.27.3) + '@rollup/plugin-replace': 6.0.2(rollup@4.27.3) + '@rollup/plugin-terser': 0.4.4(rollup@4.27.3) + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + '@types/http-proxy': 1.17.15 + '@vercel/nft': 0.27.10(rollup@4.27.3) + archiver: 7.0.1 + c12: 2.0.1(magicast@0.3.5) + chokidar: 3.6.0 + citty: 0.1.6 + compatx: 0.1.8 + confbox: 0.1.8 + consola: 3.2.3 + cookie-es: 1.2.2 + croner: 9.0.0 + crossws: 0.3.1 + db0: 0.2.1 + defu: 6.1.4 + destr: 2.0.3 + dot-prop: 9.0.0 + esbuild: 0.24.0 + escape-string-regexp: 5.0.0 + etag: 1.8.1 + fs-extra: 11.2.0 + globby: 14.0.2 + gzip-size: 7.0.0 + h3: 1.13.0 + hookable: 5.5.3 + httpxy: 0.1.5 + ioredis: 5.4.1 + jiti: 2.4.2 + klona: 2.0.6 + knitwork: 1.2.0 + listhen: 1.9.0 + magic-string: 0.30.13 + magicast: 0.3.5 + mime: 4.0.6 + mlly: 1.7.3 + node-fetch-native: 1.6.4 + ofetch: 1.4.1 + ohash: 1.1.4 + openapi-typescript: 7.4.4(typescript@5.6.3) + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.2.1 + pretty-bytes: 6.1.1 + radix3: 1.1.2 + rollup: 4.27.3 + rollup-plugin-visualizer: 5.12.0(rollup@4.27.3) + scule: 1.3.0 + semver: 7.6.3 + serve-placeholder: 2.0.2 + serve-static: 1.16.2 + std-env: 3.8.0 + ufo: 1.5.4 + uncrypto: 0.1.3 + unctx: 2.4.1 + unenv: 1.10.0 + unimport: 3.14.5(rollup@4.27.3) + unstorage: 1.14.1(db0@0.2.1)(ioredis@5.4.1) + untyped: 1.5.2 + unwasm: 0.3.9 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - drizzle-orm + - encoding + - idb-keyval + - mysql2 + - supports-color + - typescript + - uWebSockets.js + - uploadthing + nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -6793,20 +9196,67 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-addon-api@7.1.1: {} + + node-fetch-native@1.6.4: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-forge@1.3.1: {} + + node-gyp-build@4.8.4: {} + node-releases@2.0.18: {} + nopt@8.0.0: + dependencies: + abbrev: 2.0.0 + + normalize-path@3.0.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + number-is-nan@1.0.1: {} + nypm@0.3.12: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + execa: 8.0.1 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + oauth-sign@0.9.0: {} object-assign@4.1.1: {} + ofetch@1.4.1: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + + ohash@1.1.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + once@1.4.0: dependencies: wrappy: 1.0.2 onetime@1.1.0: {} + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -6817,6 +9267,24 @@ snapshots: regex: 5.0.2 regex-recursion: 4.2.1 + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openapi-typescript@7.4.4(typescript@5.6.3): + dependencies: + '@redocly/openapi-core': 1.26.1(supports-color@9.4.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.1.0 + supports-color: 9.4.0 + typescript: 5.6.3 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - encoding + ora@8.1.1: dependencies: chalk: 5.3.0 @@ -6854,6 +9322,12 @@ snapshots: package-json-from-dist@1.0.1: {} + parse-json@8.1.0: + dependencies: + '@babel/code-frame': 7.26.2 + index-to-position: 0.1.2 + type-fest: 4.27.0 + parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -6867,6 +9341,8 @@ snapshots: dependencies: entities: 4.5.0 + parseurl@1.3.3: {} + pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -6878,6 +9354,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -6885,10 +9363,16 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + + path-type@5.0.0: {} + pathe@1.1.2: {} pathval@2.0.0: {} + perfect-debounce@1.0.0: {} + performance-now@2.1.0: {} periscopic@3.1.0: @@ -6911,11 +9395,19 @@ snapshots: dependencies: find-up: 4.1.0 - postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.6.1): + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + + pluralize@8.0.0: {} + + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1): dependencies: lilconfig: 3.1.2 optionalDependencies: - jiti: 1.21.6 + jiti: 2.4.2 postcss: 8.4.49 yaml: 2.6.1 @@ -6937,8 +9429,14 @@ snapshots: find-yarn-workspace-root2: 1.2.16 which-pm: 3.0.0 + pretty-bytes@6.1.1: {} + prismjs@1.29.0: {} + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -6956,6 +9454,21 @@ snapshots: queue-microtask@1.2.3: {} + queue-tick@1.0.1: {} + + radix3@1.1.2: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.3 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -6968,6 +9481,32 @@ snapshots: dependencies: loose-envify: 1.4.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@4.6.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.0.2: {} readline2@1.0.1: @@ -6976,6 +9515,20 @@ snapshots: is-fullwidth-code-point: 1.0.0 mute-stream: 0.0.5 + recast@0.23.9: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 @@ -7110,6 +9663,12 @@ snapshots: tunnel-agent: 0.6.0 uuid: 3.4.0 + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + resolve-from@5.0.0: {} resolve@1.22.8: @@ -7159,6 +9718,19 @@ snapshots: dependencies: glob: 7.2.3 + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + rollup-plugin-visualizer@5.12.0(rollup@4.27.3): + dependencies: + open: 8.4.2 + picomatch: 2.3.1 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.27.3 + rollup-preset-solid@2.0.1(@types/babel__core@7.20.5): dependencies: '@babel/core': 7.26.0 @@ -7219,6 +9791,8 @@ snapshots: dependencies: mri: 1.2.0 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -7227,6 +9801,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scule@1.3.0: {} + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -7236,12 +9812,49 @@ snapshots: semver@7.6.3: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + seroval-plugins@1.1.1(seroval@1.1.1): dependencies: seroval: 1.1.1 seroval@1.1.1: {} + serve-placeholder@2.0.2: + dependencies: + defu: 6.1.4 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + sharp@0.33.5: dependencies: color: 4.2.3 @@ -7284,6 +9897,12 @@ snapshots: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 + shikiji-core@0.9.19: {} + + shikiji@0.9.19: + dependencies: + shikiji-core: 0.9.19 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -7295,6 +9914,10 @@ snapshots: sisteransi@1.0.5: {} + slash@5.1.0: {} + + smob@1.5.0: {} + solid-js@1.9.3: dependencies: csstype: 3.1.3 @@ -7310,6 +9933,10 @@ snapshots: transitivePeerDependencies: - supports-color + solid-use@0.9.0(solid-js@1.9.3): + dependencies: + solid-js: 1.9.3 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -7319,6 +9946,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.4: {} + source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 @@ -7341,12 +9970,26 @@ snapshots: stackback@0.0.2: {} + stackframe@1.3.4: {} + + standard-as-callback@2.1.0: {} + + statuses@2.0.1: {} + std-env@3.8.0: {} stdin-discarder@0.2.2: {} streamsearch@1.1.0: {} + streamx@2.21.1: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.5.0 + string-width@1.0.2: dependencies: code-point-at: 1.1.0 @@ -7371,6 +10014,14 @@ snapshots: get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -7392,6 +10043,12 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@3.0.0: {} + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + styled-jsx@5.1.6(react@18.3.1): dependencies: client-only: 0.0.1 @@ -7409,6 +10066,8 @@ snapshots: supports-color@2.0.0: {} + supports-color@9.4.0: {} + supports-preserve-symlinks-flag@1.0.0: {} svelte2tsx@0.7.26(svelte@4.2.18)(typescript@5.6.3): @@ -7435,6 +10094,37 @@ snapshots: magic-string: 0.30.13 periscopic: 3.1.0 + system-architecture@0.1.0: {} + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.21.1 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.1 + mkdirp: 3.0.1 + yallist: 5.0.0 + + terracotta@1.0.6(solid-js@1.9.3): + dependencies: + solid-js: 1.9.3 + solid-use: 0.9.0(solid-js@1.9.3) + terser@5.36.0: dependencies: '@jridgewell/source-map': 0.3.6 @@ -7442,6 +10132,10 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -7452,6 +10146,8 @@ snapshots: through@2.3.8: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.1: {} @@ -7471,11 +10167,15 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tough-cookie@2.5.0: dependencies: psl: 1.10.0 punycode: 2.3.1 + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -7494,17 +10194,17 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(jiti@1.21.6)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1): + tsup@8.3.5(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1): dependencies: bundle-require: 5.0.0(esbuild@0.24.0) cac: 6.7.14 chokidar: 4.0.1 consola: 3.2.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) esbuild: 0.24.0 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.49)(yaml@2.6.1) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1) resolve-from: 5.0.0 rollup: 4.27.3 source-map: 0.8.0-beta.0 @@ -7554,16 +10254,39 @@ snapshots: tweetnacl@0.14.5: {} + type-fest@2.19.0: {} + type-fest@4.27.0: {} typescript@4.9.5: {} typescript@5.6.3: {} + ufo@1.5.4: {} + ultrahtml@1.5.3: {} + uncrypto@0.1.3: {} + + unctx@2.4.1: + dependencies: + acorn: 8.14.0 + estree-walker: 3.0.3 + magic-string: 0.30.17 + unplugin: 2.1.0 + + undici-types@5.28.4: {} + undici-types@6.19.8: {} + unenv@1.10.0: + dependencies: + consola: 3.2.3 + defu: 6.1.4 + mime: 3.0.0 + node-fetch-native: 1.6.4 + pathe: 1.1.2 + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -7575,6 +10298,8 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} + unicorn-magic@0.1.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -7585,6 +10310,25 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unimport@3.14.5(rollup@4.27.3): + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + acorn: 8.14.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.1 + magic-string: 0.30.17 + mlly: 1.7.3 + pathe: 1.1.2 + picomatch: 4.0.2 + pkg-types: 1.2.1 + scule: 1.3.0 + strip-literal: 2.1.1 + unplugin: 1.16.0 + transitivePeerDependencies: + - rollup + unist-util-find-after@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -7627,22 +10371,88 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universalify@2.0.1: {} + + unplugin@1.16.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + + unplugin@2.1.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + + unstorage@1.14.1(db0@0.2.1)(ioredis@5.4.1): + dependencies: + anymatch: 3.1.3 + chokidar: 3.6.0 + citty: 0.1.6 + destr: 2.0.3 + h3: 1.13.0 + listhen: 1.9.0 + lru-cache: 10.4.3 + node-fetch-native: 1.6.4 + ofetch: 1.4.1 + ufo: 1.5.4 + optionalDependencies: + db0: 0.2.1 + ioredis: 5.4.1 + transitivePeerDependencies: + - uWebSockets.js + untildify@3.0.3: {} + untun@0.1.3: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + pathe: 1.1.2 + + untyped@1.5.2: + dependencies: + '@babel/core': 7.26.0 + '@babel/standalone': 7.26.4 + '@babel/types': 7.26.3 + citty: 0.1.6 + defu: 6.1.4 + jiti: 2.4.2 + knitwork: 1.2.0 + scule: 1.3.0 + transitivePeerDependencies: + - supports-color + + unwasm@0.3.9: + dependencies: + knitwork: 1.2.0 + magic-string: 0.30.13 + mlly: 1.7.3 + pathe: 1.1.2 + pkg-types: 1.2.1 + unplugin: 1.16.0 + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 escalade: 3.2.0 picocolors: 1.1.1 + uqr@0.1.2: {} + + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 + urlpattern-polyfill@8.0.2: {} + user-home@2.0.0: dependencies: os-homedir: 1.0.2 + util-deprecate@1.0.2: {} + uuid@3.4.0: {} validate-html-nesting@1.2.2: {} @@ -7668,10 +10478,85 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vinxi@0.4.3(@types/node@22.9.1)(db0@0.2.1)(ioredis@5.4.1)(lightningcss@1.27.0)(terser@5.36.0)(typescript@5.6.3): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@types/micromatch': 4.0.9 + '@vinxi/listhen': 1.5.6 + boxen: 7.1.1 + chokidar: 3.6.0 + citty: 0.1.6 + consola: 3.2.3 + crossws: 0.2.4 + dax-sh: 0.39.2 + defu: 6.1.4 + es-module-lexer: 1.5.4 + esbuild: 0.20.2 + fast-glob: 3.3.2 + get-port-please: 3.1.2 + h3: 1.11.1 + hookable: 5.5.3 + http-proxy: 1.18.1 + micromatch: 4.0.8 + nitropack: 2.10.4(typescript@5.6.3) + node-fetch-native: 1.6.4 + path-to-regexp: 6.3.0 + pathe: 1.1.2 + radix3: 1.1.2 + resolve: 1.22.8 + serve-placeholder: 2.0.2 + serve-static: 1.16.2 + ufo: 1.5.4 + unctx: 2.4.1 + unenv: 1.10.0 + unstorage: 1.14.1(db0@0.2.1)(ioredis@5.4.1) + vite: 5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0) + zod: 3.23.8 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - db0 + - debug + - drizzle-orm + - encoding + - idb-keyval + - ioredis + - less + - lightningcss + - mysql2 + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + - uWebSockets.js + - uploadthing + - xml2js + vite-node@2.1.5(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) es-module-lexer: 1.5.4 pathe: 1.1.2 vite: 5.4.11(@types/node@22.9.1)(lightningcss@1.27.0)(terser@5.36.0) @@ -7728,7 +10613,7 @@ snapshots: '@vitest/spy': 2.1.5 '@vitest/utils': 2.1.5 chai: 5.1.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@9.4.0) expect-type: 1.1.0 magic-string: 0.30.13 pathe: 1.1.2 @@ -7755,8 +10640,17 @@ snapshots: web-namespaces@2.0.1: {} + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + webpack-virtual-modules@0.6.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -7773,11 +10667,19 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + widest-line@5.0.0: dependencies: string-width: 7.2.0 @@ -7804,15 +10706,39 @@ snapshots: xxhash-wasm@1.1.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yaml-ast-parser@0.0.43: {} + yaml@2.6.1: optional: true yargs-parser@21.1.1: {} + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@1.1.1: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.6.0 + zod-to-json-schema@3.23.5(zod@3.23.8): dependencies: zod: 3.23.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e52380e8..337ebb28 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: - examples/astro - examples/nextjs - examples/tauri + - examples/sfm From 38328f26b3be35c31f83be5919b596c5dbade9d6 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 23 Dec 2024 14:55:20 +0800 Subject: [PATCH 50/67] Extensions + SFM wip & `rspc_axum` in userspace --- core/src/error.rs | 14 +- core/src/lib.rs | 5 +- core/src/logger.rs | 9 + core/src/procedure.rs | 3 + core/src/procedures.rs | 53 ++- {rspc/src/modern => core/src}/state.rs | 2 +- core/src/stream.rs | 66 ++++ examples/actix-web/Cargo.toml | 2 + examples/actix-web/src/main.rs | 22 +- examples/axum/Cargo.toml | 8 +- examples/axum/src/main.rs | 335 +++++++++++++++++- examples/bindings.ts | 5 +- examples/core/Cargo.toml | 1 + examples/core/src/lib.rs | 115 ++++-- examples/sfm/src/app.tsx | 2 +- examples/sfm/src/lib.ts | 15 + examples/sfm/src/routes/about.tsx | 7 + examples/sfm/src/routes/index.tsx | 38 ++ examples/tauri/src-tauri/Cargo.toml | 2 +- examples/tauri/src-tauri/src/lib.rs | 9 +- integrations/actix-web/Cargo.toml | 5 + integrations/actix-web/src/lib.rs | 126 +++++-- integrations/axum/Cargo.toml | 2 + integrations/axum/src/endpoint.rs | 124 ++----- integrations/axum/src/jsonrpc_exec.rs | 1 + integrations/axum/src/lib.rs | 2 + integrations/axum/src/request.rs | 64 ++++ integrations/http/Cargo.toml | 32 ++ integrations/http/src/content_type.rs | 56 +++ integrations/http/src/execute.rs | 153 ++++++++ integrations/http/src/file.rs | 8 + integrations/http/src/lib.rs | 22 ++ integrations/http/src/socket.rs | 16 + middleware/cache/src/lib.rs | 4 - middleware/devtools/src/lib.rs | 102 +++--- middleware/invalidation/Cargo.toml | 3 +- middleware/invalidation/src/lib.rs | 150 +++++++- middleware/tracing/src/lib.rs | 2 + middleware/validator/Cargo.toml | 16 + middleware/validator/README.md | 3 + middleware/validator/src/lib.rs | 9 + rspc/src/as_date.rs | 25 ++ rspc/src/legacy/interop.rs | 5 +- rspc/src/lib.rs | 7 +- rspc/src/modern/extension.rs | 35 ++ rspc/src/modern/middleware.rs | 2 + rspc/src/modern/middleware/into_middleware.rs | 97 +++++ rspc/src/modern/middleware/middleware.rs | 4 +- rspc/src/modern/mod.rs | 5 +- rspc/src/modern/procedure.rs | 4 +- rspc/src/modern/procedure/builder.rs | 91 ++--- rspc/src/modern/procedure/erased.rs | 77 ++++ rspc/src/modern/procedure/procedure.rs | 185 ---------- rspc/src/procedure.rs | 63 +++- rspc/src/router.rs | 34 +- 55 files changed, 1732 insertions(+), 515 deletions(-) create mode 100644 core/src/logger.rs rename {rspc/src/modern => core/src}/state.rs (97%) create mode 100644 examples/sfm/src/lib.ts create mode 100644 integrations/axum/src/request.rs create mode 100644 integrations/http/Cargo.toml create mode 100644 integrations/http/src/content_type.rs create mode 100644 integrations/http/src/execute.rs create mode 100644 integrations/http/src/file.rs create mode 100644 integrations/http/src/lib.rs create mode 100644 integrations/http/src/socket.rs create mode 100644 middleware/validator/Cargo.toml create mode 100644 middleware/validator/README.md create mode 100644 middleware/validator/src/lib.rs create mode 100644 rspc/src/as_date.rs create mode 100644 rspc/src/modern/extension.rs create mode 100644 rspc/src/modern/middleware/into_middleware.rs create mode 100644 rspc/src/modern/procedure/erased.rs delete mode 100644 rspc/src/modern/procedure/procedure.rs diff --git a/core/src/error.rs b/core/src/error.rs index 7cc4893c..33348a77 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -7,6 +7,8 @@ use serde::{ use crate::LegacyErrorInterop; +// TODO: Discuss the stability guanrantees of the error handling system. Variant is fixed, message is not. + /// TODO pub enum ProcedureError { /// Failed to find a procedure with the given name. @@ -20,6 +22,9 @@ pub enum ProcedureError { /// The procedure unexpectedly unwinded. /// This happens when you panic inside a procedure. Unwind(Box), + // /// An error occurred while serializing the response. + // /// The error message can be provided should be omitted unless the client is trusted (Eg. Tauri). + // Serializer(Option), // TODO: Sort this out } impl ProcedureError { @@ -30,6 +35,7 @@ impl ProcedureError { Self::Downcast(_) => 400, Self::Resolver(err) => err.status(), Self::Unwind(_) => 500, + // Self::Serializer(_) => 500, } } @@ -40,6 +46,7 @@ impl ProcedureError { ProcedureError::Downcast(_) => "Downcast", ProcedureError::Resolver(_) => "Resolver", ProcedureError::Unwind(_) => "ResolverPanic", + // ProcedureError::Serializer(_) => "Serializer", } } @@ -54,6 +61,10 @@ impl ProcedureError { .map(|err| err.to_string().into()) .unwrap_or("resolver error".into()), ProcedureError::Unwind(_) => "resolver panic".into(), + // ProcedureError::Serializer(err) => err + // .clone() + // .map(Into::into) + // .unwrap_or("serializer error".into()), } } } @@ -85,6 +96,7 @@ impl fmt::Debug for ProcedureError { Self::Downcast(err) => write!(f, "Downcast({err:?})"), Self::Resolver(err) => write!(f, "Resolver({err:?})"), Self::Unwind(err) => write!(f, "ResolverPanic({err:?})"), + // Self::Serializer(err) => write!(f, "Serializer({err:?})"), } } } @@ -112,7 +124,7 @@ impl Serialize for ProcedureError { return err.value().serialize(serializer); } - let mut state = serializer.serialize_struct("ProcedureError", 1)?; + let mut state = serializer.serialize_struct("ProcedureError", 3)?; state.serialize_field("_rspc", &true)?; state.serialize_field("variant", &self.variant())?; state.serialize_field("message", &self.message())?; diff --git a/core/src/lib.rs b/core/src/lib.rs index 0e2046d5..242c46c2 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -17,8 +17,10 @@ mod dyn_input; mod error; mod interop; +mod logger; mod procedure; mod procedures; +mod state; mod stream; pub use dyn_input::DynInput; @@ -27,4 +29,5 @@ pub use error::{DeserializeError, DowncastError, ProcedureError, ResolverError}; pub use interop::LegacyErrorInterop; pub use procedure::Procedure; pub use procedures::Procedures; -pub use stream::ProcedureStream; +pub use state::State; +pub use stream::{ProcedureStream, ProcedureStreamMap, ProcedureStreamValue}; diff --git a/core/src/logger.rs b/core/src/logger.rs new file mode 100644 index 00000000..d984f0c4 --- /dev/null +++ b/core/src/logger.rs @@ -0,0 +1,9 @@ +// #[derive(Clone, Debug)] +// #[non_exhaustive] +// pub enum LogMessage<'a> { +// Execute { name: &'a str }, // TODO: Give enough information to collect time metrics, etc. +// SerializerError(&'a str), +// Custom { reason: &'a str, message: &'a str }, +// } + +// TODO: Clone `ProcedureError` instead of `Event`? diff --git a/core/src/procedure.rs b/core/src/procedure.rs index 34f60d86..62d12119 100644 --- a/core/src/procedure.rs +++ b/core/src/procedure.rs @@ -9,11 +9,14 @@ use serde::Deserializer; use crate::{DynInput, ProcedureError, ProcedureStream}; +// TODO: Discuss cancellation safety + /// a single type-erased operation that the server can execute. /// /// TODO: Show constructing and executing procedure. pub struct Procedure { handler: Arc ProcedureStream + Send + Sync>, + #[cfg(debug_assertions)] handler_name: &'static str, } diff --git a/core/src/procedures.rs b/core/src/procedures.rs index c3a8f563..5dae14be 100644 --- a/core/src/procedures.rs +++ b/core/src/procedures.rs @@ -3,21 +3,43 @@ use std::{ collections::HashMap, fmt, ops::{Deref, DerefMut}, + sync::Arc, }; -use crate::Procedure; +use crate::{Procedure, State}; -pub struct Procedures(HashMap, Procedure>); +pub struct Procedures { + procedures: HashMap, Procedure>, + state: Arc, +} + +impl Procedures { + // TODO: Work out this API. I'm concerned how `rspc_devtools` and `rspc_tracing` fit into this. + // TODO: Also accept `Into` maybe? + pub fn new(procedures: HashMap, Procedure>, state: Arc) -> Self { + Self { procedures, state } + } -impl From, Procedure>> for Procedures { - fn from(procedures: HashMap, Procedure>) -> Self { - Self(procedures.into_iter().map(|(k, v)| (k.into(), v)).collect()) + pub fn state(&self) -> &Arc { + &self.state } } +// TODO: Should this come back?? `State` makes it rough. +// impl From, Procedure>> for Procedures { +// fn from(procedures: HashMap, Procedure>) -> Self { +// Self { +// procedures: procedures.into_iter().map(|(k, v)| (k.into(), v)).collect(), +// } +// } +// } + impl Clone for Procedures { fn clone(&self) -> Self { - Self(self.0.clone()) + Self { + procedures: self.procedures.clone(), + state: self.state.clone(), + } } } @@ -29,7 +51,7 @@ impl Into> for &Procedures { impl fmt::Debug for Procedures { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_map().entries(self.0.iter()).finish() + f.debug_map().entries(self.procedures.iter()).finish() } } @@ -38,26 +60,27 @@ impl IntoIterator for Procedures { type IntoIter = std::collections::hash_map::IntoIter, Procedure>; fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() + self.procedures.into_iter() } } -impl FromIterator<(Cow<'static, str>, Procedure)> for Procedures { - fn from_iter, Procedure)>>(iter: I) -> Self { - Self(iter.into_iter().collect()) - } -} +// impl FromIterator<(Cow<'static, str>, Procedure)> for Procedures { +// fn from_iter, Procedure)>>(iter: I) -> Self { +// Self(iter.into_iter().collect()) +// } +// } +// TODO: Is `Deref` okay for this usecase? impl Deref for Procedures { type Target = HashMap, Procedure>; fn deref(&self) -> &Self::Target { - &self.0 + &self.procedures } } impl DerefMut for Procedures { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.procedures } } diff --git a/rspc/src/modern/state.rs b/core/src/state.rs similarity index 97% rename from rspc/src/modern/state.rs rename to core/src/state.rs index 3de3c241..2628159d 100644 --- a/rspc/src/modern/state.rs +++ b/core/src/state.rs @@ -7,7 +7,7 @@ use std::{ /// A hasher for `TypeId`s that takes advantage of its known characteristics. /// -/// Author of `anymap` crate has done research on the topic: +/// Author of `anymap` crate has done research on this topic: /// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599 #[derive(Debug, Default)] struct NoOpHasher(u64); diff --git a/core/src/stream.rs b/core/src/stream.rs index 5baa55ff..99c869a3 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -72,6 +72,72 @@ impl ProcedureStream { Self(Err(err)) => err.take().map(Err), } } + + /// TODO + pub async fn next_status(&mut self) -> (u16, bool) { + // TODO: Panic if it isn't the start of the stream or not??? + + // TODO: Poll till the first return value and return it's code. + + // TODO: Should we keep polling so we can tell if it's a value or a stream for the content type??? + + // todo!(); + (200, false) + } + + /// TODO + // TODO: Should error be `String` type? + pub fn map Result + Unpin, T>( + self, + map: F, + ) -> ProcedureStreamMap { + ProcedureStreamMap { stream: self, map } + } +} + +pub struct ProcedureStreamMap Result + Unpin, T> { + stream: ProcedureStream, + map: F, +} + +impl Result + Unpin, T> Stream + for ProcedureStreamMap +{ + type Item = T; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + + match this.stream.0.as_mut() { + Ok(v) => v.as_mut().poll_next_value(cx).map(|v| { + v.map(|v| match v { + Ok(()) => match (this.map)(ProcedureStreamValue( + this.stream.0.as_mut().expect("checked above").value(), + )) { + Ok(v) => v, + // TODO: Exposing this error to the client or not? + // TODO: Error type??? + Err(err) => todo!(), + }, + Err(err) => todo!("{err:?}"), + }) + }), + Err(err) => todo!(), + } + } +} + +// TODO: name +pub struct ProcedureStreamValue<'a>(&'a (dyn erased_serde::Serialize + Send + Sync)); +// TODO: `Debug`, etc traits + +impl<'a> Serialize for ProcedureStreamValue<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } } impl From for ProcedureStream { diff --git a/examples/actix-web/Cargo.toml b/examples/actix-web/Cargo.toml index a5b3c178..f9904b31 100644 --- a/examples/actix-web/Cargo.toml +++ b/examples/actix-web/Cargo.toml @@ -10,3 +10,5 @@ example-core = { path = "../core" } rspc-actix-web = { path = "../../integrations/actix-web", features = [] } actix-web = "4" actix-cors = "0.7.0" +actix-multipart = "0.7.2" +futures = "0.3" diff --git a/examples/actix-web/src/main.rs b/examples/actix-web/src/main.rs index ee0a98de..73722484 100644 --- a/examples/actix-web/src/main.rs +++ b/examples/actix-web/src/main.rs @@ -1,14 +1,30 @@ use std::path::PathBuf; use actix_cors::Cors; -use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use actix_multipart::Multipart; +use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; use example_core::{create_router, Ctx}; +use futures::{StreamExt, TryStreamExt}; #[get("/")] async fn hello() -> impl Responder { HttpResponse::Ok().body("Hello world from Actix Web!") } +#[post("/upload")] +async fn upload(mut payload: Multipart) -> impl Responder { + while let Ok(Some(field)) = payload.try_next().await { + println!( + "{:?} {:?} {:?}", + field.name().map(|v| v.to_string()), + field.content_type().map(|v| v.to_string()), + field.collect::>().await + ); + } + + HttpResponse::Ok().body("Done!") +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let router = create_router(); @@ -44,10 +60,12 @@ async fn main() -> std::io::Result<()> { // Don't use permissive CORS in production! .wrap(Cors::permissive()) .service(hello) + .service(upload) .service(web::scope("/rspc").configure( rspc_actix_web::Endpoint::builder(procedures.clone()).build(|| { // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this - Ctx {} + // Ctx {} + todo!(); }), )) }) diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 5a0a1ec6..da5a1a9c 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -9,8 +9,14 @@ rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } example-core = { path = "../core" } tokio = { version = "1.41.1", features = ["full"] } -axum = "0.7.9" +axum = { version = "0.7.9", features = ["multipart"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } +rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } + +futures = "0.3" # TODO +serde_json = "1.0.133" +rspc-http = { version = "0.2.1", path = "../../integrations/http" } +streamunordered = "0.5.4" diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 13033538..74297a65 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,6 +1,22 @@ -use axum::routing::get; +use axum::{ + body::Body, + extract::{Multipart, Request}, + http::{header, HeaderName, StatusCode}, + routing::{get, on, post, MethodFilter, MethodRouter}, + Json, +}; use example_core::{create_router, Ctx}; -use std::path::PathBuf; +use futures::{stream::FuturesUnordered, Stream, StreamExt}; +use rspc::{ProcedureStreamValue, Procedures, State}; +use serde_json::{de::SliceRead, value::RawValue, Value}; +use std::{ + convert::Infallible, + future::Future, + path::PathBuf, + pin::{pin, Pin}, + task::{Context, Poll}, +}; +use streamunordered::{StreamUnordered, StreamYield}; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] @@ -27,7 +43,7 @@ async fn main() { // ) // .unwrap(); - let procedures = rspc_devtools::mount(procedures, &types); + // let procedures = rspc_devtools::mount(procedures, &types); // TODO // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! let cors = CorsLayer::new() @@ -37,6 +53,40 @@ async fn main() { let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) + .route( + "/upload", + post(|mut multipart: Multipart| async move { + println!("{:?}", multipart); + + while let Some(field) = multipart.next_field().await.unwrap() { + println!( + "{:?} {:?} {:?}", + field.name().map(|v| v.to_string()), + field.content_type().map(|v| v.to_string()), + field.collect::>().await + ); + } + + "Done!" + }), + ) + .route( + "/rspc/custom", + post(|| async move { + // println!("{:?}", multipart); + + // while let Some(field) = multipart.next_field().await.unwrap() { + // println!( + // "{:?} {:?} {:?}", + // field.name().map(|v| v.to_string()), + // field.content_type().map(|v| v.to_string()), + // field.collect::>().await + // ); + // } + + todo!(); + }), + ) // .nest( // "/rspc", // rspc_axum::endpoint(procedures, |parts: Parts| { @@ -44,13 +94,14 @@ async fn main() { // Ctx {} // }), // ) - .nest( - "/rspc", - rspc_axum::Endpoint::builder(procedures).build(|| { - // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this - Ctx {} - }), - ) + // .nest( + // "/rspc", + // rspc_axum::Endpoint::builder(procedures).build(|| { + // // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this + // Ctx {} + // }), + // ) + .nest("/rspc", rspc_handler(procedures)) .layer(cors); let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 @@ -59,3 +110,267 @@ async fn main() { .await .unwrap(); } + +pub fn rspc_handler(procedures: Procedures) -> axum::Router { + let mut r = axum::Router::new(); + // TODO: Support file upload and download + // TODO: `rspc_zer` how worky? + + // for (key, procedure) in procedures.clone() { + // r = r.route( + // &format!("/{key}"), + // on( + // MethodFilter::GET.or(MethodFilter::POST), + // move |req: rspc_axum::AxumRequest| async move { + // let mut stream = req.deserialize(|buf| { + // let mut input = serde_json::Deserializer::new(SliceRead::new(buf)); + // procedure.exec_with_deserializer(Ctx {}, &mut input) + // }); + // let (status, is_stream) = stream.next_status().await; + + // ( + // StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + // [ + // ( + // header::CONTENT_TYPE, + // is_stream + // .then_some("application/jsonstream") + // .unwrap_or("application/json"), + // ), + // (HeaderName::from_static("x-rspc"), "1"), + // ], + // Body::from_stream(stream.map(|v| { + // serde_json::to_vec(&v) + // .map_err(|err| err.to_string()) + // .map(Ok::<_, Infallible>) + // })), + // ) + // }, + // ), + // ); + // } + + // TODO: Websocket & batch endpoint + + // // TODO: Supporting zero-flight mutations??? + // // TODO: Streaming back each response separately + // r.route( + // &format!("/~rspc.batch"), + // post(move |mut multipart: Multipart| async move { + // while let Some(mut field) = multipart.next_field().await.unwrap() { + // let name = field.name().unwrap().to_string(); // TODO: Error handling + + // // field.headers() + + // // TODO: Don't use `serde_json::Value` + // let input: Value = match field.content_type() { + // Some("application/json") => { + // // TODO: Error handling + // serde_json::from_slice(field.bytes().await.unwrap().as_ref()).unwrap() + // } + // Some(_) => todo!(), + // None => todo!(), + // }; + + // let procedure = procedures.get(&*name).unwrap(); + // println!("{:?} {:?} {:?}", name, input, procedure); + // } + + // // TODO: Streaming result & configurable content size + // ( + // [(header::CONTENT_TYPE, "application/jsonstream")], + // // Body::from_stream(stream.map(|v| { + // // serde_json::to_vec(&v) + // // .map_err(|err| err.to_string()) + // // .map(Ok::<_, Infallible>) + // // })), + // "Testing", + // ) + // }), + // ) + + // TODO: If Tanstack Query cache key is `input` how does `File` work? + + // TODO: Allowing `GET` requests too? + // TODO: WebSocket upgrade + // TODO: Optional zero-flight mutations + // TODO: Document CDN caching options with this setup + r.route( + "/", + post(move |mut multipart: Multipart| async move { + let mut runtime = StreamUnordered::new(); + + let invalidator = rspc_invalidation::Invalidator::default(); + let ctx = Ctx { + invalidator: invalidator.clone(), + }; + + // TODO: If a file was being uploaded this would require reading the whole body until the `runtime` is polled. + while let Some(field) = multipart.next_field().await.unwrap() { + let name = field.name().unwrap().to_string(); // TODO: Error handling + + // field.headers() + + // TODO: Don't use `serde_json::Value` + let input: Value = match field.content_type() { + // TODO: + // Some("application/json") => { + // // TODO: Error handling + // serde_json::from_slice(field.bytes().await.unwrap().as_ref()).unwrap() + // } + // Some(_) => todo!(), + // None => todo!(), + _ => serde_json::from_slice(field.bytes().await.unwrap().as_ref()).unwrap(), + }; + + let procedure = procedures.get(&*name).unwrap(); + println!("{:?} {:?} {:?}", name, input, procedure); + + let stream = procedure.exec_with_deserializer(ctx.clone(), input); + + runtime.insert(stream.map:: _, Vec>(|v| { + serde_json::to_vec(&v).map_err(|err| err.to_string()) + })); + + // TODO: Spawn onto runtime + // let (status, is_stream) = stream.next_status().await; + + // println!("{:?} {:?}", status, is_stream); + + // ( + // StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + // [ + // ( + // header::CONTENT_TYPE, + // is_stream + // .then_some("application/jsonstream") + // .unwrap_or("application/json"), + // ), + // (HeaderName::from_static("x-rspc"), "1"), + // ], + // Body::from_stream(stream.map(|v| { + // serde_json::to_vec(&v) + // .map_err(|err| err.to_string()) + // .map(Ok::<_, Infallible>) + // })), + // ) + } + + // TODO: Wait until the full stream is Mattrax-style flushed to run this. + let fut = tokio::time::sleep(std::time::Duration::from_secs(1)); + tokio::select! { + _ = runtime.next() => {} + _ = fut => {} + } + + for stream in rspc_invalidation::queue(&invalidator, || ctx.clone(), &procedures) { + runtime.insert(stream.map:: _, Vec>(|v| { + serde_json::to_vec(&v).map_err(|err| err.to_string()) + })); + } + + ( + [(header::CONTENT_TYPE, "text/x-rspc")], + Body::from_stream(Prototype { runtime }), + ) + }), + ) +} + +pub struct Prototype>> { + runtime: StreamUnordered, +} + +// TODO: Should `S: 'static` be a thing? +impl> + 'static> Stream for Prototype { + type Item = Result, Infallible>; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + loop { + let Poll::Ready(v) = self.runtime.poll_next_unpin(cx) else { + return Poll::Pending; + }; + + return Poll::Ready(match v { + Some((v, i)) => match v { + StreamYield::Item(mut v) => { + let id = 0; // TODO: Include identifier to request/query + let identifier = 'O' as u8; // TODO: error, oneshot, event or complete message + // let content_type = ""; // TODO: Include content-type of it + let mut buf = vec![id, identifier]; + buf.append(&mut v); + buf.extend_from_slice(b"\n\n"); + Some(Ok(buf)) + } + StreamYield::Finished(finished_stream) => { + // TODO: Complete messages (unless oneshot maybe) + finished_stream.remove(Pin::new(&mut self.runtime)); + continue; + } + }, + None => None, + }); + } + } +} + +fn encode_msg(a: (), b: (), c: ()) { + todo!(); +} + +// TODO: support `GET` +// r = r.route( +// &format!("/{key}"), +// // TODO: The json decoding is also way less efficent (`serde_json::Value` as intermediate step) +// post(move |json: Option>| async move { +// let mut stream = +// procedure.exec_with_deserializer(Ctx {}, json.map(|v| v.0).unwrap_or_default()); +// let (status, is_stream) = stream.next_status().await; + +// ( +// StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), +// [ +// ( +// header::CONTENT_TYPE, +// is_stream +// .then_some("application/jsonstream") +// .unwrap_or("application/json"), +// ), +// (HeaderName::from_static("x-rspc"), "1"), +// ], +// Body::from_stream(stream.map(|v| { +// serde_json::to_vec(&v) +// .map_err(|err| err.to_string()) +// .map(Ok::<_, Infallible>) +// })), +// ) +// }), +// ); + +// r = r.route( +// &format!("/{key}"), +// // TODO: The `Json` decoding won't return an rspc errors. +// // TODO: The json decoding is also way less efficent (`serde_json::Value` as intermediate step) +// on( +// MethodFilter::GET.or(MethodFilter::POST), +// move |input: rspc_axum::AxumRequest| async move { +// let todo = input.execute( +// &procedure, +// |buf| &mut serde_json::Deserializer::new(SliceRead::new(buf)), +// Ctx {}, +// ); +// }, +// ), +// ); + +// r = r.route( +// &format!("/{key}"), +// post(move |req: Request| async move { +// // TODO + +// "todo" +// }), +// ); + +// let (status, mut stream) = +// rspc_http::into_body(procedure.exec_with_deserializer(Ctx {}, json.0)).await; diff --git a/examples/bindings.ts b/examples/bindings.ts index b1c3e11d..6879bc57 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,7 +1,7 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { cached: { kind: "query", input: any, output: any, error: any }, @@ -13,9 +13,12 @@ export type Procedures = { newstuff: { kind: "query", input: any, output: any, error: any }, newstuff2: { kind: "query", input: any, output: any, error: any }, newstuffpanic: { kind: "query", input: any, output: any, error: any }, + newstuffser: { kind: "query", input: any, output: any, error: any }, panic: { kind: "query", input: null, output: null, error: unknown }, pings: { kind: "subscription", input: null, output: string, error: unknown }, sendMsg: { kind: "mutation", input: string, output: string, error: unknown }, + sfmPost: { kind: "query", input: any, output: any, error: any }, + sfmPostEdit: { kind: "query", input: any, output: any, error: any }, transformMe: { kind: "query", input: null, output: string, error: unknown }, version: { kind: "query", input: null, output: string, error: unknown }, } \ No newline at end of file diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index 3663e545..77ec1a99 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -16,3 +16,4 @@ rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } tracing = "0.1.41" futures = "0.3.31" rspc-cache = { version = "0.0.0", path = "../../middleware/cache" } +rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index 42f936ad..197aabdb 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -1,15 +1,12 @@ -use std::{ - marker::PhantomData, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{marker::PhantomData, time::SystemTime}; use async_stream::stream; use rspc::{ - middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, - Router2, + middleware::Middleware, Error2, Extension, Procedure2, ProcedureBuilder, ResolverInput, + ResolverOutput, Router2, }; use rspc_cache::{cache, cache_ttl, CacheState, Memory}; +use rspc_invalidation::Invalidate; use serde::Serialize; use specta::Type; use thiserror::Error; @@ -17,7 +14,9 @@ use tracing::info; // `Clone` is only required for usage with Websockets #[derive(Clone)] -pub struct Ctx {} +pub struct Ctx { + pub invalidator: Invalidator, +} #[derive(Serialize, Type)] pub struct MyCustomType(String); @@ -117,7 +116,8 @@ impl Error2 for Error { pub struct BaseProcedure(PhantomData); impl BaseProcedure { - pub fn builder() -> ProcedureBuilder + pub fn builder( + ) -> ProcedureBuilder where TErr: Error2, TInput: ResolverInput, @@ -127,6 +127,18 @@ impl BaseProcedure { } } +#[derive(Type)] +struct SerialisationError; +impl Serialize for SerialisationError { + fn serialize(&self, _serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::Error; + Err(S::Error::custom("lol")) + } +} + fn test_unstable_stuff(router: Router2) -> Router2 { router .procedure("newstuff", { @@ -134,7 +146,7 @@ fn test_unstable_stuff(router: Router2) -> Router2 { }) .procedure("newstuff2", { ::builder() - .with(invalidation(|ctx: Ctx, key, event| false)) + // .with(invalidation(|ctx: Ctx, key, event| false)) .with(Middleware::new( move |ctx: Ctx, input: (), next| async move { let result = next.exec(ctx, input).await; @@ -146,6 +158,9 @@ fn test_unstable_stuff(router: Router2) -> Router2 { .procedure("newstuffpanic", { ::builder().query(|_, _: ()| async move { Ok(todo!()) }) }) + .procedure("newstuffser", { + ::builder().query(|_, _: ()| async move { Ok(SerialisationError) }) + }) .setup(CacheState::builder(Memory::new()).mount()) .procedure("cached", { ::builder() @@ -157,33 +172,71 @@ fn test_unstable_stuff(router: Router2) -> Router2 { Ok(SystemTime::now()) }) }) + .procedure("sfmPost", { + ::builder() + .with(Middleware::new( + move |ctx: Ctx, input: (String, ()), next| async move { + let result = next.exec(ctx, input.0).await; + result + }, + )) + .with(Invalidator::with(|event| { + println!("--- BEFORE"); + if let InvalidateEvent::Post { id } = event { + return Invalidate::One((id.to_string(), ())); + } + Invalidate::None + })) + .query(|_, id: String| async { + println!("FETCH POST FROM DB"); + Ok(id) + }) + .with(Invalidator::with(|event| { + println!("--- AFTER"); + if let InvalidateEvent::Post { id } = event { + return Invalidate::One((id.to_string(), ())); + } + Invalidate::None + })) + }) + .procedure("sfmPostEdit", { + ::builder().query(|ctx, id: String| async move { + println!("UPDATE THE POST {id:?}"); + ctx.invalidator.invalidate(InvalidateEvent::Post { id }); + Ok(()) + }) + }) + // .procedure("sfmStatefulPost", { + // ::builder() + // // .with(Invalidator::mw(|ctx, input, event| { + // // event == InvalidateEvent::InvalidateKey(input.id) + // // })) + // .query(|_, id: String| async { + // // Fetch the post from the DB + // Ok(id) + // }) + // }) + // .procedure("fileupload", { + // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) + // }) } -#[derive(Debug, Clone, Serialize, Type)] +// .with(Invalidator::mw(|ctx, input, event| { +// event == InvalidateEvent::InvalidateKey("abc".into()) +// })) +// .with(Invalidator::mw_with_result(|ctx, input, result, event| { +// event == InvalidateEvent::InvalidateKey("abc".into()) +// })) + +#[derive(Debug, Clone, Serialize, Type, PartialEq, Eq)] pub enum InvalidateEvent { + Post { id: String }, InvalidateKey(String), } +pub type Invalidator = rspc_invalidation::Invalidator; -fn invalidation( - handler: impl Fn(TCtx, TInput, InvalidateEvent) -> bool + Send + Sync + 'static, -) -> Middleware -where - TError: Send + 'static, - TCtx: Clone + Send + 'static, - TInput: Clone + Send + 'static, - TResult: Send + 'static, -{ - let handler = Arc::new(handler); - Middleware::new(move |ctx: TCtx, input: TInput, next| async move { - // TODO: Register this with `TCtx` - let ctx2 = ctx.clone(); - let input2 = input.clone(); - let result = next.exec(ctx, input).await; - - // TODO: Unregister this with `TCtx` - result - }) -} +// TODO: Debug, etc +pub struct File(T); pub fn create_router() -> Router2 { let router = Router2::from(mount()); diff --git a/examples/sfm/src/app.tsx b/examples/sfm/src/app.tsx index d1359c8d..21381656 100644 --- a/examples/sfm/src/app.tsx +++ b/examples/sfm/src/app.tsx @@ -7,7 +7,7 @@ import "./app.css"; export default function App() { return ( ( + root={(props) => ( SolidStart - Basic Index diff --git a/examples/sfm/src/lib.ts b/examples/sfm/src/lib.ts new file mode 100644 index 00000000..7eb70d9e --- /dev/null +++ b/examples/sfm/src/lib.ts @@ -0,0 +1,15 @@ +const batch: any[] = []; + +export async function doMutation(op: string) { + batch.push(["MUTATION", op]); + await new Promise((resolve) => setTimeout(resolve, 1000)); +} + +export async function doQuery(op: string) { + batch.push(["query", op]); + await new Promise((resolve) => setTimeout(resolve, 1000)); +} + +export function printBatch() { + console.log(batch); +} diff --git a/examples/sfm/src/routes/about.tsx b/examples/sfm/src/routes/about.tsx index c1c2dcf5..fa1e2ec3 100644 --- a/examples/sfm/src/routes/about.tsx +++ b/examples/sfm/src/routes/about.tsx @@ -1,4 +1,11 @@ import { Title } from "@solidjs/meta"; +import type { RouteDefinition } from "@solidjs/router"; + +export const route = { + preload: () => { + console.log("Preloading data for about"); + }, +} satisfies RouteDefinition; export default function About() { return ( diff --git a/examples/sfm/src/routes/index.tsx b/examples/sfm/src/routes/index.tsx index 5d557d81..e18b2457 100644 --- a/examples/sfm/src/routes/index.tsx +++ b/examples/sfm/src/routes/index.tsx @@ -1,7 +1,33 @@ import { Title } from "@solidjs/meta"; +import { action, redirect, useAction, useHref } from "@solidjs/router"; import Counter from "~/components/Counter"; +import { doMutation } from "~/lib"; +import { FileRoutes } from "@solidjs/start/router"; + +const doThingAction = action(async (input: string) => { + console.log("GOT:", input); + + // Basically: + // - `onSuccess` depends on knowing the return type, hence executing after `doMutation` has returned and causing a waterfall. + // - `redirect`ing on the server depends on know what data is dependant on the new router which requires running `preload` on the server. It can't be in a manifest as it's dynamic on params. + + await doMutation("DO THING"); + // , { + // onSuccess: (v) => { + // // redirect("/about"); + // }, + // } + + console.log(redirect("/about")); // TODO + + // TODO: How can we see the `redirect` through this? + // TODO: Knowing this redirect we need to run it's preload function in the same batch as a the mutation. + return redirect("/about"); +}); export default function Home() { + const doThing = useAction(doThingAction); + return (
Hello World @@ -14,6 +40,18 @@ export default function Home() { {" "} to learn how to build SolidStart apps.

+ + + +
); } + +function getManifest() { + const routes = FileRoutes(); + + // routes.preload(); + + console.log(routes); +} diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index 2f1d7da2..4f47e1ad 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -22,4 +22,4 @@ serde_json = "1" rspc = { path = "../../../rspc", features = ["typescript", "unstable"] } tauri-plugin-rspc = { path = "../../../integrations/tauri" } specta = { version = "=2.0.0-rc.20", features = ["derive"] } -example-axum = { path = "../../axum" } +example-core = { path = "../../core" } diff --git a/examples/tauri/src-tauri/src/lib.rs b/examples/tauri/src-tauri/src/lib.rs index d0c65cc6..3c34e8f0 100644 --- a/examples/tauri/src-tauri/src/lib.rs +++ b/examples/tauri/src-tauri/src/lib.rs @@ -1,4 +1,4 @@ -use example_axum::{create_router, Ctx}; +use example_core::{create_router, Ctx}; mod api; @@ -7,8 +7,13 @@ pub fn run() { let router = create_router(); let (procedures, types) = router.build().unwrap(); + // TODO: Exporting types + tauri::Builder::default() - .plugin(tauri_plugin_rspc::init(procedures, |_| Ctx {})) + .plugin(tauri_plugin_rspc::init(procedures, |_| { + // Ctx {} + todo!(); + })) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 792071de..ff3f4dde 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -21,7 +21,12 @@ default = [] [dependencies] rspc-core = { version = "0.0.1", path = "../../core" } +rspc-http = { path = "../http" } actix-web = "4" +actix-ws = "0.3" +futures-util = "0.3.31" + +futures = "0.3" # TODO: Remove this [lints] workspace = true diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index b5191e2d..ff1f77df 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -6,11 +6,19 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] +use std::{convert::Infallible, sync::Arc}; + use actix_web::{ - web::{self, ServiceConfig}, - HttpResponse, Resource, + body::BodyStream, + http::{header, StatusCode}, + web::{self, Bytes, Payload, ServiceConfig}, + HttpRequest, HttpResponse, }; +use actix_ws::Message; + +use futures_util::StreamExt; use rspc_core::Procedures; +use rspc_http::ExecuteInput; pub struct Endpoint { procedures: Procedures, @@ -33,45 +41,89 @@ impl Endpoint { self, ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static, ) -> impl FnOnce(&mut ServiceConfig) { - |service| { + let ctx_fn = Arc::new(ctx_fn); + move |service| { service.route( "/ws", - web::to(|| { - // let (res, mut session, stream) = actix_ws::handle(&req, stream)?; - - // let mut stream = stream - // .aggregate_continuations() - // // aggregate continuation frames up to 1MiB - // .max_continuation_size(2_usize.pow(20)); - - // // start task but don't wait for it - // rt::spawn(async move { - // // receive messages from websocket - // while let Some(msg) = stream.next().await { - // match msg { - // Ok(AggregatedMessage::Text(text)) => { - // // echo text message - // session.text(text).await.unwrap(); - // } - - // Ok(AggregatedMessage::Binary(bin)) => { - // // echo binary message - // session.binary(bin).await.unwrap(); - // } - - // Ok(AggregatedMessage::Ping(msg)) => { - // // respond to PING frame with PONG frame - // session.pong(&msg).await.unwrap(); - // } - - // _ => {} - // } - // } - - HttpResponse::NotFound() + // TODO: Hook this up properly + web::to(|req: HttpRequest, body: Payload| async move { + let (response, mut session, mut stream) = actix_ws::handle(&req, body)?; + + actix_web::rt::spawn(async move { + session.text("Hello World From rspc").await.unwrap(); + + while let Some(Ok(msg)) = stream.next().await { + match msg { + Message::Ping(bytes) => { + if session.pong(&bytes).await.is_err() { + return; + } + } + + Message::Text(msg) => println!("Got text: {msg}"), + _ => break, + } + } + + let _ = session.close(None).await; + }); + + Ok::<_, actix_web::Error>(response) }), ); - service.route("/{route:.*}", web::to(|| HttpResponse::Ok())); + + // TODO: Making extractors work + + for (key, procedure) in self.procedures { + let ctx_fn = ctx_fn.clone(); + let handler = move |req: HttpRequest, body: Bytes| { + let procedure = procedure.clone(); + let ctx_fn = ctx_fn.clone(); + async move { + let input = if body.is_empty() { + ExecuteInput::Query(req.query_string()) + } else { + // TODO: Error if not JSON content-type + + ExecuteInput::Body(&body) + }; + + let (status, stream) = + rspc_http::execute(&procedure, input, || ctx_fn()).await; + HttpResponse::build( + StatusCode::from_u16(status) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + ) + .insert_header(header::ContentType::json()) + .body(BodyStream::new( + stream.map(|v| Ok::<_, Infallible>(v.into())), + )) + } + }; + + service.route(&key, web::get().to(handler.clone())); + service.route(&key, web::post().to(handler)); + } } } } + +// pub struct TODO(HttpRequest); + +// impl rspc_http::Request for TODO { +// fn method(&self) -> &str { +// self.0.method().as_str() +// } + +// // fn path(&self) -> &str { +// // self.0.path() +// // } + +// // fn query(&self) -> &str { +// // self.0.query_string() +// // } + +// // fn body(&self) { +// // self.0. +// // } +// } diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 1e67535a..7238ebca 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -21,6 +21,7 @@ ws = ["axum/ws"] [dependencies] rspc-core = { version = "0.0.1", path = "../../core" } +rspc-http = { version = "0.2.1", path = "../http" } axum = { version = "0.7.9", features = ["ws", "json"] } serde_json = "1" @@ -31,6 +32,7 @@ tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio: serde = { version = "1", features = ["derive"] } # TODO: Remove features serde_urlencoded = "0.7.1" mime = "0.3.17" +rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } [lints] workspace = true diff --git a/integrations/axum/src/endpoint.rs b/integrations/axum/src/endpoint.rs index ad05fca9..c284d721 100644 --- a/integrations/axum/src/endpoint.rs +++ b/integrations/axum/src/endpoint.rs @@ -18,6 +18,7 @@ use axum::{ }; use futures::{stream::once, Stream, StreamExt, TryStreamExt}; use rspc_core::{ProcedureError, ProcedureStream, Procedures}; +use rspc_http::ExecuteInput; /// Construct a new [`axum::Router`](axum::Router) to expose a given [`rspc::Router`](rspc::Router). pub struct Endpoint { @@ -134,6 +135,8 @@ impl Endpoint { let mut r = axum::Router::new(); let ctx_fn = Arc::new(ctx_fn); + // let logger = self.procedures.get_logger(); + for (key, procedure) in self.procedures { let ctx_fn = ctx_fn.clone(); r = r.route( @@ -141,65 +144,47 @@ impl Endpoint { on( MethodFilter::GET.or(MethodFilter::POST), move |req: Request| { - let ctx = ctx_fn(); + // let ctx = ctx_fn(); async move { let hint = req.body().size_hint(); let has_body = hint.lower() != 0 || hint.upper() != Some(0); - let mut stream = if !has_body { - let mut params = form_urlencoded::parse( - req.uri().query().unwrap_or_default().as_ref(), - ); - match params - .find_map(|(input, value)| (input == "input").then(|| value)) - { - Some(input) => procedure.exec_with_deserializer( - ctx, - &mut serde_json::Deserializer::from_str(&*input), - ), - None => procedure - .exec_with_deserializer(ctx, serde_json::Value::Null), - } + let mut bytes = None; + let input = if !has_body { + ExecuteInput::Query(req.uri().query().unwrap_or_default()) } else { - if !json_content_type(req.headers()) { - let err: ProcedureError = rspc_core::DeserializeError::custom( - "Client did not set correct valid 'Content-Type' header", - ) - .into(); - let buf = serde_json::to_vec(&err).unwrap(); // TODO - - return ( - StatusCode::BAD_REQUEST, - [(header::CONTENT_TYPE, "application/json")], - Body::from(buf), - ) - .into_response(); - } - - let bytes = Bytes::from_request(req, &()).await.unwrap(); // TODO: Error handling - procedure.exec_with_deserializer( - ctx, - &mut serde_json::Deserializer::from_slice(&bytes), + // TODO: bring this back + // if !json_content_type(req.headers()) { + // let err: ProcedureError = rspc_core::DeserializeError::custom( + // "Client did not set correct valid 'Content-Type' header", + // ) + // .into(); + // let buf = serde_json::to_vec(&err).unwrap(); // TODO + + // return ( + // StatusCode::BAD_REQUEST, + // [(header::CONTENT_TYPE, "application/json")], + // Body::from(buf), + // ) + // .into_response(); + // } + + // TODO: Error handling + bytes = Some(Bytes::from_request(req, &()).await.unwrap()); + ExecuteInput::Body( + bytes.as_ref().expect("assigned on previous line"), ) }; - let mut stream = ProcedureStreamResponse { - code: None, - stream, - first: None, - }; - stream.first = Some(stream.next().await); - - let status = stream - .code - .and_then(|c| StatusCode::try_from(c).ok()) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + let (status, stream) = + rspc_http::execute(&procedure, input, || ctx_fn()).await; ( - status, + StatusCode::from_u16(status) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), [(header::CONTENT_TYPE, "application/json")], - Body::from_stream(stream), + Body::from_stream(stream.map(Ok::<_, Infallible>)), ) .into_response() } @@ -214,51 +199,6 @@ impl Endpoint { } } -struct ProcedureStreamResponse { - code: Option, - first: Option, Infallible>>>, - stream: ProcedureStream, -} - -impl Stream for ProcedureStreamResponse { - type Item = Result, Infallible>; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if let Some(first) = self.first.take() { - return Poll::Ready(first); - } - - let (code, mut buf) = { - let Poll::Ready(v) = self.stream.poll_next(cx) else { - return Poll::Pending; - }; - - match v { - Some(Ok(v)) => ( - 200, - Some(serde_json::to_vec(&v).unwrap()), // TODO: Error handling - ), - Some(Err(err)) => ( - err.status(), - Some(serde_json::to_vec(&err).unwrap()), // TODO: Error handling - ), - None => (200, None), - } - }; - - if let Some(buf) = &mut buf { - buf.extend_from_slice(b"\n\n"); - }; - - self.code = Some(code); - Poll::Ready(buf.map(Ok)) - } - - fn size_hint(&self) -> (usize, Option) { - self.stream.size_hint() - } -} - fn json_content_type(headers: &HeaderMap) -> bool { let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { content_type diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 98a6c824..0badc1cf 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -348,6 +348,7 @@ async fn next( } } ProcedureError::Unwind(err) => panic!("{err:?}"), // Restore previous behavior lol + // ProcedureError::Serializer(err) => panic!("{err:?}"), }) .and_then(|v| { Ok(v.serialize(serde_json::value::Serializer) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 01af85d8..6cff77a4 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -11,7 +11,9 @@ mod extractors; mod jsonrpc; mod jsonrpc_exec; // mod legacy; +mod request; mod v2; pub use endpoint::Endpoint; +pub use request::AxumRequest; pub use v2::endpoint; diff --git a/integrations/axum/src/request.rs b/integrations/axum/src/request.rs new file mode 100644 index 00000000..ed60ea38 --- /dev/null +++ b/integrations/axum/src/request.rs @@ -0,0 +1,64 @@ +//! TODO: Use `axum_core` not `axum` + +use axum::{ + async_trait, + body::HttpBody, + extract::{FromRequest, Request}, +}; +use rspc_core::{Procedure, ProcedureStream}; +use serde::Deserializer; + +// TODO: rename? +pub struct AxumRequest { + req: Request, +} + +impl AxumRequest { + pub fn deserialize(self, exec: impl FnOnce(&[u8]) -> T) -> T { + let hint = self.req.body().size_hint(); + let has_body = hint.lower() != 0 || hint.upper() != Some(0); + + // TODO: Matching on incoming method??? + + // let mut bytes = None; + // let input = if !has_body { + // ExecuteInput::Query(req.uri().query().unwrap_or_default()) + // } else { + // // TODO: bring this back + // // if !json_content_type(req.headers()) { + // // let err: ProcedureError = rspc_core::DeserializeError::custom( + // // "Client did not set correct valid 'Content-Type' header", + // // ) + // // .into(); + // // let buf = serde_json::to_vec(&err).unwrap(); // TODO + + // // return ( + // // StatusCode::BAD_REQUEST, + // // [(header::CONTENT_TYPE, "application/json")], + // // Body::from(buf), + // // ) + // // .into_response(); + // // } + + // // TODO: Error handling + // bytes = Some(Bytes::from_request(req, &()).await.unwrap()); + // ExecuteInput::Body( + // bytes.as_ref().expect("assigned on previous line"), + // ) + // }; + + // let (status, stream) = + // rspc_http::execute(&procedure, input, || ctx_fn()).await; + + exec(b"null") + } +} + +#[async_trait] +impl FromRequest for AxumRequest { + type Rejection = (); // TODO: What should this be? + + async fn from_request(req: Request, state: &S) -> Result { + Ok(Self { req }) + } +} diff --git a/integrations/http/Cargo.toml b/integrations/http/Cargo.toml new file mode 100644 index 00000000..94a9185d --- /dev/null +++ b/integrations/http/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "rspc-http" +description = "Generic HTTP adapter for rspc" +version = "0.2.1" +authors = ["Oscar Beaumont "] +edition = "2021" +license = "MIT" +repository = "https://github.com/specta-rs/rspc" +documentation = "https://docs.rs/rspc-axum/latest/rspc-axum" +keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] +categories = ["web-programming", "asynchronous"] + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = [] + +[dependencies] +rspc-core = { version = "0.0.1", path = "../../core" } +serde_json = "1" +form_urlencoded = "1" +futures-core = "0.3" + +futures = "0.3" # TODO: Remove this +erased-serde = "0.4.5" +serde = "1.0.216" + +[lints] +workspace = true diff --git a/integrations/http/src/content_type.rs b/integrations/http/src/content_type.rs new file mode 100644 index 00000000..d7ae7980 --- /dev/null +++ b/integrations/http/src/content_type.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +// use serde::{Serialize, Serializer}; + +// TODO: What should this be called? +// TODO: Matching on headers too or just `Content-Type`? +// TODO: Error handling for decoding errors or no matching content-type +// TODO: `Content-Type` for incoming body decoding as well +// TODO: Client typesafety. Eg. does an endpoint require a specific content-type Eg. `File`? + +// #[derive(Default)] +// pub struct Registry(Option bool>>); + +// impl Registry { +// // pub fn new() -> Self { +// // Self::default() +// // } + +// // TODO: Remove +// // pub fn todo(&self, handler: impl Fn(&str) -> ()) { +// // self.0.push(Arc::new(|ct, v| {})); +// // } + +// pub fn r#match(&self, content_type: &str, value: S) { +// // for f in &self.0 { +// // // f(content_type); +// // } + +// todo!(); +// } +// } + +// TODO: `Default` + `Debug`, `Clone`, etc + +// fn todo() { +// Registry::default().register(|ct, ser| { +// if ct.starts_with("application/json") { +// // TODO: We need to be able to configure `Content-Type` header + +// // ser.serialize() or ser.value() +// // serde_json::to_writer(v, &ct).unwrap(); +// // true +// } else { +// // false +// } +// }) +// } + +// pub trait Request { +// fn method(&self) -> &str; + +// // fn path(&self) -> &str; +// // fn query(&self) -> &str; +// // fn headers(&self); +// // fn body(&self); +// } diff --git a/integrations/http/src/execute.rs b/integrations/http/src/execute.rs new file mode 100644 index 00000000..12d039ca --- /dev/null +++ b/integrations/http/src/execute.rs @@ -0,0 +1,153 @@ +//! Stream body types by framework: +//! Axum - impl Stream, impl Into>> +//! Actix Web - impl Stream> + 'static> +//! Poem - impl Stream, impl Into>> +//! Warp - impl Stream> +//! Tide - N/A supports impl futures::AsyncBufRead +//! Hyper (via http_body_util::StreamBody) - impl Stream, E>>, +//! Rocket - impl Stream> + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures::StreamExt; +use futures_core::Stream; +use rspc_core::ProcedureStream; +use serde::Serializer; + +pub enum ExecuteInput<'a> { + Query(&'a str), + Body(&'a [u8]), +} + +/// TODO: Explain this +// TODO: `Content-Type` header on response??? +pub async fn execute<'a, 'b, TCtx>( + procedure: &'a rspc_core::Procedure, + input: ExecuteInput<'b>, + ctx: impl FnOnce() -> TCtx, +) -> (u16, impl Stream> + Send + 'static) { + let stream = match input { + ExecuteInput::Query(query) => { + let mut params = form_urlencoded::parse(query.as_bytes()); + + match params.find_map(|(input, value)| (input == "input").then(|| value)) { + Some(input) => procedure.exec_with_deserializer( + ctx(), + &mut serde_json::Deserializer::from_str(&*input), + ), + None => procedure.exec_with_deserializer(ctx(), serde_json::Value::Null), + } + } + ExecuteInput::Body(body) => procedure + .exec_with_deserializer(ctx(), &mut serde_json::Deserializer::from_slice(&body)), + }; + + let mut stream = ProcedureStreamResponse { + code: None, + stream, + first: None, + }; + stream.first = Some(stream.next().await); + // TODO: Some(poll_fn(|cx| stream.poll_next(cx)).await); + + // TODO: We should poll past the first value to check if it's the only value and set content-type based on it. `ready(...)` will go done straight away. + + (stream.code.unwrap_or(500), stream) +} + +pub async fn into_body( + stream: ProcedureStream, +) -> (u16, impl Stream> + Send + 'static) { + let mut stream = ProcedureStreamResponse { + code: None, + stream, + first: None, + }; + stream.first = Some(stream.next().await); + // TODO: Some(poll_fn(|cx| stream.poll_next(cx)).await); + + // TODO: We should poll past the first value to check if it's the only value and set content-type based on it. `ready(...)` will go done straight away. + + (stream.code.unwrap_or(500), stream) +} + +// TODO: Sealing fields at least? +struct ProcedureStreamResponse { + code: Option, + first: Option>>, + stream: ProcedureStream, +} + +impl Stream for ProcedureStreamResponse { + type Item = Vec; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Some(first) = self.first.take() { + return Poll::Ready(first); + } + + let (code, mut buf) = { + let Poll::Ready(v) = self.stream.poll_next(cx) else { + return Poll::Pending; + }; + + match v { + Some(Ok(v)) => ( + 200, + Some(serde_json::to_vec(&v).unwrap()), // TODO: Error handling + // .map_err(|err| { + // // TODO: Configure handling of this error and how we log it??? + // serde_json::to_vec(&ProcedureError::Serializer(err.to_string())) + // .expect("bruh") + // })), + ), + Some(Err(err)) => ( + err.status(), + Some(serde_json::to_vec(&err).unwrap()), // TODO: Error handling + ), + None => (200, None), + } + }; + + // TODO: Only after first item + if let Some(buf) = &mut buf { + buf.extend_from_slice(b"\n\n"); + }; + + self.code = Some(code); + Poll::Ready(buf) + } + + fn size_hint(&self) -> (usize, Option) { + self.stream.size_hint() + } +} + +// TODO +// fn json_content_type(headers: &HeaderMap) -> bool { +// let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { +// content_type +// } else { +// return false; +// }; + +// let content_type = if let Ok(content_type) = content_type.to_str() { +// content_type +// } else { +// return false; +// }; + +// let mime = if let Ok(mime) = content_type.parse::() { +// mime +// } else { +// return false; +// }; + +// let is_json_content_type = mime.type_() == "application" +// && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); + +// is_json_content_type +// } diff --git a/integrations/http/src/file.rs b/integrations/http/src/file.rs new file mode 100644 index 00000000..cf3841ef --- /dev/null +++ b/integrations/http/src/file.rs @@ -0,0 +1,8 @@ +//! Multipart types by framework: +//! Axum (multer) - `Multipart::next_field` +//! Actix Web - `impl Stream>` +//! Poem (multer) - `Multipart::next_field` +//! Warp (multipart) - `impl Stream>` +//! Tide - Not supported anymore +//! Rocket (multer with strong wrapper) - ... +//! Hyper - Nothing build in. multer or multipart supported. diff --git a/integrations/http/src/lib.rs b/integrations/http/src/lib.rs new file mode 100644 index 00000000..5e4be095 --- /dev/null +++ b/integrations/http/src/lib.rs @@ -0,0 +1,22 @@ +//! rspc-http: Generic HTTP adapter for [rspc](https://rspc.dev). +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", + html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" +)] + +// TODO: Working extractors w/ Axum and Actix-web +// TODO: Websockets +// TODO: Supporting non-json formats +// TODO: `File` type abstraction + +// TODO: Custom cookies, headers, etc + +mod content_type; +mod execute; +mod file; +mod socket; + +pub use content_type::*; +pub use execute::*; // TODO: {execute, ExecuteInput}; // TODO: Don't do wildcard diff --git a/integrations/http/src/socket.rs b/integrations/http/src/socket.rs new file mode 100644 index 00000000..6207f448 --- /dev/null +++ b/integrations/http/src/socket.rs @@ -0,0 +1,16 @@ +//! Websocket types by framework: +//! Axum (tokio_tungstenite) - https://docs.rs/axum/latest/axum/extract/ws/struct.WebSocket.html +//! Actix Web - https://docs.rs/actix-ws/latest/actix_ws/ +//! Poem (tokio_tungstenite) - https://docs.rs/poem/latest/poem/web/websocket/struct.WebSocket.html +//! Warp (tokio_tungstenite) - https://docs.rs/warp/latest/warp/filters/ws/struct.WebSocket.html +//! Tide (tokio_tungstenite) - https://docs.rs/tide-websockets/latest/tide_websockets/ +//! Rocket (tokio_tungstenite) - https://docs.rs/rocket_ws/latest/rocket_ws/struct.WebSocket.html +//! Hyper - Just use whatever. + +// TODO: Copy this from `rspc_tauri` +struct WebsocketMsg { + // Value + // Done, +} + +pub fn socket() {} diff --git a/middleware/cache/src/lib.rs b/middleware/cache/src/lib.rs index aafef76a..5c92f141 100644 --- a/middleware/cache/src/lib.rs +++ b/middleware/cache/src/lib.rs @@ -39,10 +39,6 @@ where TInput: Clone + Send + 'static, TResult: Clone + Send + Sync + 'static, { - // let todo = poll_fn(|cx| { - // todo!(); - // }); - Middleware::new(move |ctx: TCtx, input: TInput, next| { async move { let meta = next.meta(); diff --git a/middleware/devtools/src/lib.rs b/middleware/devtools/src/lib.rs index 228a042f..4a67c82c 100644 --- a/middleware/devtools/src/lib.rs +++ b/middleware/devtools/src/lib.rs @@ -26,58 +26,60 @@ pub fn mount( procedures: impl Into>, types: &impl Any, ) -> Procedures { - let procedures = procedures.into(); - let meta = Metadata { - crate_name: env!("CARGO_PKG_NAME"), - crate_version: env!("CARGO_PKG_VERSION"), - rspc_version: env!("CARGO_PKG_VERSION"), - procedures: procedures - .iter() - .map(|(name, _)| (name.to_string(), ProcedureMetadata {})) - .collect(), - }; - let history = Arc::new(Mutex::new(Vec::new())); // TODO: Stream to clients instead of storing in memory + // let procedures = procedures.into(); + // let meta = Metadata { + // crate_name: env!("CARGO_PKG_NAME"), + // crate_version: env!("CARGO_PKG_VERSION"), + // rspc_version: env!("CARGO_PKG_VERSION"), + // procedures: procedures + // .iter() + // .map(|(name, _)| (name.to_string(), ProcedureMetadata {})) + // .collect(), + // }; + // let history = Arc::new(Mutex::new(Vec::new())); // TODO: Stream to clients instead of storing in memory - let mut procedures = procedures - .into_iter() - .map(|(name, procedure)| { - let history = history.clone(); + // let mut procedures = procedures + // .into_iter() + // .map(|(name, procedure)| { + // let history = history.clone(); - ( - name.clone(), - Procedure::new(move |ctx, input| { - let start = std::time::Instant::now(); - let result = procedure.exec(ctx, input); - history - .lock() - .unwrap_or_else(PoisonError::into_inner) - .push((name.to_string(), format!("{:?}", start.elapsed()))); - result - }), - ) - }) - .collect::>(); + // ( + // name.clone(), + // Procedure::new(move |ctx, input| { + // let start = std::time::Instant::now(); + // let result = procedure.exec(ctx, input); + // history + // .lock() + // .unwrap_or_else(PoisonError::into_inner) + // .push((name.to_string(), format!("{:?}", start.elapsed()))); + // result + // }), + // ) + // }) + // .collect::>(); - procedures.insert( - "~rspc.devtools.meta".into(), - Procedure::new(move |ctx, input| { - let value = Ok(meta.clone()); - ProcedureStream::from_stream(stream::once(future::ready(value))) - }), - ); - procedures.insert( - "~rspc.devtools.history".into(), - Procedure::new({ - let history = history.clone(); - move |ctx, input| { - let value = Ok(history - .lock() - .unwrap_or_else(PoisonError::into_inner) - .clone()); - ProcedureStream::from_stream(stream::once(future::ready(value))) - } - }), - ); + // procedures.insert( + // "~rspc.devtools.meta".into(), + // Procedure::new(move |ctx, input| { + // let value = Ok(meta.clone()); + // ProcedureStream::from_stream(stream::once(future::ready(value))) + // }), + // ); + // procedures.insert( + // "~rspc.devtools.history".into(), + // Procedure::new({ + // let history = history.clone(); + // move |ctx, input| { + // let value = Ok(history + // .lock() + // .unwrap_or_else(PoisonError::into_inner) + // .clone()); + // ProcedureStream::from_stream(stream::once(future::ready(value))) + // } + // }), + // ); - procedures + // procedures + + todo!(); } diff --git a/middleware/invalidation/Cargo.toml b/middleware/invalidation/Cargo.toml index 9ffdf2a4..2a49abb2 100644 --- a/middleware/invalidation/Cargo.toml +++ b/middleware/invalidation/Cargo.toml @@ -6,7 +6,8 @@ publish = false # TODO: Crate metadata & publish [dependencies] async-stream = "0.3.5" -rspc = { path = "../../rspc" } +rspc = { path = "../../rspc", features = ["unstable"] } +serde_json = "1.0.133" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/middleware/invalidation/src/lib.rs b/middleware/invalidation/src/lib.rs index cea14843..0dc9f742 100644 --- a/middleware/invalidation/src/lib.rs +++ b/middleware/invalidation/src/lib.rs @@ -6,4 +6,152 @@ html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" )] -// TODO: Refer to `../README.md` to see status +use std::{ + any::Any, + sync::{Arc, Mutex, PoisonError}, +}; + +use rspc::{middleware::Middleware, Extension, ProcedureStream, Procedures}; + +#[derive(Default)] +struct State { + closures: Vec () + Send + Sync>>, +} + +#[derive(Debug)] // TODO: Traits but only if the generic also has the trait. +pub enum Invalidate { + None, + All, + One(T), + Many(Vec), +} + +pub struct Invalidator { + // TODO: I don't like this but solving that is *really* hard. + invalidated: Arc>>, +} + +// TODO: `Debug` impl + +impl Default for Invalidator { + fn default() -> Self { + Self { + invalidated: Default::default(), + } + } +} + +impl Clone for Invalidator { + fn clone(&self) -> Self { + Self { + invalidated: self.invalidated.clone(), + } + } +} + +impl Invalidator { + // TODO: Taking `&mut self` will cause major problems with people doing `Arc`. + pub fn invalidate(&self, event: E) { + self.invalidated + .lock() + .unwrap_or_else(PoisonError::into_inner) + .push(event); + } + + // pub fn mw( + // // TODO: With multiple middleware how do we enforce we have the first layers `TInput`? + // handler: impl Fn(&E) -> Invalidate + Send + Sync + 'static, + // ) -> Middleware + // where + // TError: Send + 'static, + // TCtx: Send + 'static, + // TInput: Send + 'static, + // TResult: Send + 'static, + // { + // let handler = Arc::new(handler); + // Middleware::new(move |ctx: TCtx, input: TInput, next| async move { + // let result = next.exec(ctx, input).await; + // result + // }) + // .setup(|state, meta| { + // // TODO: Error out on mutations or subscriptions due to concerns about safety. + + // state + // .get_mut_or_init(|| State::default()) + // .closures + // .push(Arc::new(move |event| { + // match handler(event.downcast_ref().unwrap()) { + // Invalidate::None => println!("{:?} {:?}", meta.name(), "NONE"), + // // TODO: Make these work properly + // Invalidate::All => println!("{:?} {:?}", meta.name(), "ALL"), + // Invalidate::One(input) => println!("{:?} {:?}", meta.name(), "ONE"), + // Invalidate::Many(inputs) => println!("{:?} {:?}", meta.name(), "MANY"), + // } + // })); + // }) + // } + + pub fn with( + // TODO: With multiple middleware how do we enforce we have the first layers `TInput`? + handler: impl Fn(&E) -> Invalidate + Send + Sync + 'static, + ) -> Extension + where + TCtx: Send + 'static, + TInput: Send + 'static, + TResult: Send + 'static, + { + let handler = Arc::new(handler); + Extension::new().setup(|state, meta| { + // TODO: Error out on mutations or subscriptions due to concerns about safety. + + state + .get_mut_or_init(|| State::default()) + .closures + .push(Arc::new(move |event| { + match handler(event.downcast_ref().unwrap()) { + Invalidate::None => println!("{:?} {:?}", meta.name(), "NONE"), + // TODO: Make these work properly + Invalidate::All => println!("{:?} {:?}", meta.name(), "ALL"), + Invalidate::One(input) => println!("{:?} {:?}", meta.name(), "ONE"), + Invalidate::Many(inputs) => println!("{:?} {:?}", meta.name(), "MANY"), + } + })); + }) + } +} + +// TODO: The return type does lack info about which procedure is running +pub fn queue( + invalidator: &Invalidator, + ctx_fn: impl Fn() -> TCtx, + procedures: &Procedures, +) -> Vec { + let mut streams = Vec::new(); + + if let Some(state) = procedures.state().get::() { + let mut invalidated = invalidator + .invalidated + .lock() + .unwrap_or_else(PoisonError::into_inner); + + for mut event in invalidated.drain(..) { + for closure in &state.closures { + closure(&mut event); // TODO: Take in `streams`, `procedures` and `ctx_fn` + } + } + } + + let keys_to_invalidate = vec!["version"]; // TODO: How to work out which procedures to rerun? -> We need some request scoped data. + + for name in keys_to_invalidate { + streams.push( + procedures + .get(name) + .unwrap() + // TODO: Don't deserialize to `serde_json::Value` and make the input type work properly. + .exec_with_deserializer(ctx_fn(), serde_json::Value::Null), + ); + } + + streams +} diff --git a/middleware/tracing/src/lib.rs b/middleware/tracing/src/lib.rs index 54e24553..9520594d 100644 --- a/middleware/tracing/src/lib.rs +++ b/middleware/tracing/src/lib.rs @@ -18,6 +18,8 @@ use tracing_futures::Instrument; // TODO: Support for Prometheus metrics and structured logging +// TODO: Capturing serialization errors in `rspc-axum` + /// TODO pub fn tracing() -> Middleware where diff --git a/middleware/validator/Cargo.toml b/middleware/validator/Cargo.toml new file mode 100644 index 00000000..eca4c749 --- /dev/null +++ b/middleware/validator/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rspc-validator" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[dependencies] +rspc = { path = "../../rspc", features = ["unstable"] } + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/middleware/validator/README.md b/middleware/validator/README.md new file mode 100644 index 00000000..ab67aa55 --- /dev/null +++ b/middleware/validator/README.md @@ -0,0 +1,3 @@ +# rspc Validator + +Coming soon... diff --git a/middleware/validator/src/lib.rs b/middleware/validator/src/lib.rs new file mode 100644 index 00000000..1b1d854d --- /dev/null +++ b/middleware/validator/src/lib.rs @@ -0,0 +1,9 @@ +//! rspc-validator: Input validation for rspc +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +// TODO: Coming soon. diff --git a/rspc/src/as_date.rs b/rspc/src/as_date.rs new file mode 100644 index 00000000..93f74f25 --- /dev/null +++ b/rspc/src/as_date.rs @@ -0,0 +1,25 @@ +use std::fmt; + +pub struct AsDate(T); + +impl fmt::Debug for AsDate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +// TODO: Trait passthroughs (`Debug`, `Clone`, `Deserialize`, etc) + `Deref` & `Into` impls +// TODO: make generic over any `T: Serialize`??? +// impl Serialize for AsDate> { +// fn serialize(&self, serializer: S) -> Result +// where +// S: serde::Serializer, +// { +// // TODO: Should we require a `thread_local` to enable this impl so types are reusable??? +// // TODO: What if the rspc client wants it in a string format? +// let mut s = serializer.serialize_struct("AsDate", 2)?; +// s.serialize_field("~rspc~.date", &true)?; +// s.serialize_field("~rspc~.value", &self.0)?; +// s.end() +// } +// } diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy/interop.rs index e56c0567..3a6b9c33 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::BTreeMap, panic::Location}; +use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData, panic::Location}; use futures::{stream, FutureExt, StreamExt, TryStreamExt}; use rspc_core::{ProcedureStream, ResolverError}; @@ -10,6 +10,7 @@ use specta::{ use crate::{ internal::{Layer, ProcedureKind, RequestContext, ValueOrStream}, + modern::procedure::ErasedProcedure, procedure::ProcedureType, types::TypesOrType, util::literal_object, @@ -43,7 +44,7 @@ pub fn legacy_to_modern(mut router: Router) -> Router2 { key.split(".") .map(|s| s.to_string().into()) .collect::>>(), - Procedure2 { + ErasedProcedure { setup: Default::default(), ty: ProcedureType { kind, diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index 2ff4a396..5c7bf810 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -13,6 +13,7 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] +mod as_date; mod languages; pub(crate) mod modern; mod procedure; @@ -33,17 +34,19 @@ pub(crate) use modern::State; #[cfg(not(feature = "unstable"))] pub(crate) use procedure::Procedure2; +#[cfg(feature = "unstable")] +pub use as_date::AsDate; #[cfg(feature = "unstable")] pub use modern::{ middleware, procedure::ProcedureBuilder, procedure::ProcedureMeta, procedure::ResolverInput, - procedure::ResolverOutput, Error as Error2, State, Stream, + procedure::ResolverOutput, Error as Error2, Extension, Stream, }; #[cfg(feature = "unstable")] pub use procedure::Procedure2; pub use rspc_core::{ DeserializeError, DowncastError, DynInput, Procedure, ProcedureError, ProcedureStream, - Procedures, ResolverError, + ProcedureStreamMap, ProcedureStreamValue, Procedures, ResolverError, State, }; // Legacy stuff diff --git a/rspc/src/modern/extension.rs b/rspc/src/modern/extension.rs new file mode 100644 index 00000000..a7e52909 --- /dev/null +++ b/rspc/src/modern/extension.rs @@ -0,0 +1,35 @@ +use std::marker::PhantomData; + +use rspc_core::State; + +use crate::ProcedureMeta; + +// TODO: `TError`? +// TODO: Explain executor order and why to use over `Middleware`? +pub struct Extension { + pub(crate) setup: Option>, + pub(crate) phantom: PhantomData (TCtx, TInput, TResult)>, + // pub(crate) inner: Box< + // dyn FnOnce( + // MiddlewareHandler, + // ) -> MiddlewareHandler, + // >, +} + +// TODO: Debug impl + +impl Extension { + // TODO: Take in map function + pub fn new() -> Self { + Self { + setup: None, + phantom: PhantomData, + } + } + + // TODO: Allow multiple or error if defined multiple times? + pub fn setup(mut self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { + self.setup = Some(Box::new(func)); + self + } +} diff --git a/rspc/src/modern/middleware.rs b/rspc/src/modern/middleware.rs index 0a344264..179d1916 100644 --- a/rspc/src/modern/middleware.rs +++ b/rspc/src/modern/middleware.rs @@ -1,7 +1,9 @@ +mod into_middleware; mod middleware; mod next; pub use middleware::Middleware; pub use next::Next; +pub(crate) use into_middleware::IntoMiddleware; pub(crate) use middleware::MiddlewareHandler; diff --git a/rspc/src/modern/middleware/into_middleware.rs b/rspc/src/modern/middleware/into_middleware.rs new file mode 100644 index 00000000..04c4eec4 --- /dev/null +++ b/rspc/src/modern/middleware/into_middleware.rs @@ -0,0 +1,97 @@ +use std::marker::PhantomData; + +use crate::{ + modern::{Error, Extension}, + ProcedureBuilder, +}; + +use super::Middleware; + +// TODO: Expose in public API or seal??? +// TODO: This API could lead to bad errors +pub trait IntoMiddleware { + type TNextCtx; + type I; + type R; + + fn build( + self, + this: ProcedureBuilder, + ) -> ProcedureBuilder; +} + +impl + IntoMiddleware + for Middleware +where + // TODO: This stuff could lead to bad errors + // TODO: Could we move them onto the function instead and constrain on `with` too??? + TError: Error, + TRootCtx: 'static, + TNextCtx: 'static, + TCtx: 'static, + TInput: 'static, + TResult: 'static, + TBaseInput: 'static, + I: 'static, + TBaseResult: 'static, + R: 'static, +{ + type TNextCtx = TNextCtx; + type I = I; + type R = R; + + fn build( + self, + this: ProcedureBuilder, + ) -> ProcedureBuilder + { + ProcedureBuilder { + build: Box::new(|ty, mut setups, handler| { + if let Some(setup) = self.setup { + setups.push(setup); + } + + (this.build)(ty, setups, (self.inner)(handler)) + }), + phantom: PhantomData, + } + } +} + +// TODO: Constrain to base types +impl + IntoMiddleware + for Extension +where + // TODO: This stuff could lead to bad errors + // TODO: Could we move them onto the function instead and constrain on `with` too??? + TError: Error, + TRootCtx: 'static, + TCtx: 'static, + TBaseInput: 'static, + I: 'static, + TBaseResult: 'static, + R: 'static, +{ + type TNextCtx = TCtx; + type I = I; + type R = R; + + fn build( + self, + this: ProcedureBuilder, + ) -> ProcedureBuilder + { + ProcedureBuilder { + build: Box::new(|ty, mut setups, handler| { + if let Some(setup) = self.setup { + setups.push(setup); + } + + (this.build)(ty, setups, handler) + }), + phantom: PhantomData, + } + } +} diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index 1a097b06..46e73055 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -22,8 +22,9 @@ use std::{pin::Pin, sync::Arc}; use futures::{Future, FutureExt, Stream}; +use rspc_core::State; -use crate::modern::{procedure::ProcedureMeta, State}; +use crate::modern::procedure::ProcedureMeta; use super::Next; @@ -130,6 +131,7 @@ where } } + // TODO: Allow multiple or error if defined multiple times? pub fn setup(mut self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { self.setup = Some(Box::new(func)); self diff --git a/rspc/src/modern/mod.rs b/rspc/src/modern/mod.rs index 5c5d7732..2631e129 100644 --- a/rspc/src/modern/mod.rs +++ b/rspc/src/modern/mod.rs @@ -2,12 +2,11 @@ pub mod middleware; pub mod procedure; mod error; +mod extension; mod infallible; -mod state; mod stream; - // pub use crate::procedure::Procedure2; pub use error::Error; // pub use infallible::Infallible; -pub use state::State; +pub use extension::Extension; pub use stream::Stream; diff --git a/rspc/src/modern/procedure.rs b/rspc/src/modern/procedure.rs index fe0cf95d..1dce4008 100644 --- a/rspc/src/modern/procedure.rs +++ b/rspc/src/modern/procedure.rs @@ -15,13 +15,13 @@ //! mod builder; +mod erased; mod meta; -mod procedure; mod resolver_input; mod resolver_output; pub use builder::ProcedureBuilder; +pub use erased::ErasedProcedure; pub use meta::{ProcedureKind, ProcedureMeta}; -// pub use procedure::{Procedure, ProcedureTypeDefinition, UnbuiltProcedure}; pub use resolver_input::ResolverInput; pub use resolver_output::ResolverOutput; diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs index c35eb42d..d22580bf 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/modern/procedure/builder.rs @@ -1,63 +1,59 @@ -use std::{fmt, future::Future, sync::Arc}; +use std::{fmt, future::Future, marker::PhantomData, sync::Arc}; use crate::{ - modern::{ - middleware::{Middleware, MiddlewareHandler}, - Error, State, - }, + middleware::IntoMiddleware, + modern::{middleware::MiddlewareHandler, Error}, Procedure2, }; -use super::{ProcedureKind, ProcedureMeta}; +use super::{ErasedProcedure, ProcedureKind, ProcedureMeta}; use futures::{FutureExt, StreamExt}; +use rspc_core::State; -// TODO: Document the generics like `Middleware` -pub struct ProcedureBuilder { +// TODO: Document the generics like `Middleware`. What order should they be in? +pub struct ProcedureBuilder { pub(crate) build: Box< dyn FnOnce( ProcedureKind, Vec>, - MiddlewareHandler, - ) -> Procedure2, + MiddlewareHandler, + ) -> ErasedProcedure, >, + pub(crate) phantom: PhantomData<(TBaseInput, TBaseResult)>, } -impl fmt::Debug - for ProcedureBuilder +impl fmt::Debug + for ProcedureBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Procedure").finish() } } -impl - ProcedureBuilder +impl + ProcedureBuilder where TError: Error, TRootCtx: 'static, TCtx: 'static, TInput: 'static, + TBaseInput: 'static, TResult: 'static, + TBaseResult: 'static, { - pub fn with( + pub fn with< + M: IntoMiddleware, + >( self, - mw: Middleware, - ) -> ProcedureBuilder - where - TNextCtx: 'static, - I: 'static, - R: 'static, + mw: M, + ) -> ProcedureBuilder +// where + // TNextCtx: 'static, + // I: 'static, + // R: 'static, { - ProcedureBuilder { - build: Box::new(|ty, mut setups, handler| { - if let Some(setup) = mw.setup { - setups.push(setup); - } - - (self.build)(ty, setups, (mw.inner)(handler)) - }), - } + mw.build(self) } pub fn setup(self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { @@ -66,29 +62,40 @@ where setups.push(Box::new(func)); (self.build)(ty, setups, handler) }), + phantom: PhantomData, } } pub fn query> + Send + 'static>( self, handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> Procedure2 { - (self.build)( - ProcedureKind::Query, - Vec::new(), - Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input))), - ) + ) -> Procedure2 { + Procedure2 { + build: Box::new(move |setups| { + (self.build)( + ProcedureKind::Query, + setups, + Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input))), + ) + }), + phantom: PhantomData, + } } pub fn mutation> + Send + 'static>( self, handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> Procedure2 { - (self.build)( - ProcedureKind::Mutation, - Vec::new(), - Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input))), - ) + ) -> Procedure2 { + Procedure2 { + build: Box::new(move |setups| { + (self.build)( + ProcedureKind::Mutation, + setups, + Arc::new(move |ctx, input, _| Box::pin(handler(ctx, input))), + ) + }), + phantom: PhantomData, + } } } diff --git a/rspc/src/modern/procedure/erased.rs b/rspc/src/modern/procedure/erased.rs new file mode 100644 index 00000000..a93e6ece --- /dev/null +++ b/rspc/src/modern/procedure/erased.rs @@ -0,0 +1,77 @@ +use std::{borrow::Cow, panic::Location, sync::Arc}; + +use futures::{FutureExt, TryStreamExt}; +use rspc_core::Procedure; +use specta::datatype::DataType; + +use crate::{ + modern::{ + procedure::{ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput}, + Error, + }, + procedure::ProcedureType, + State, +}; + +pub struct ErasedProcedure { + pub(crate) setup: Vec>, + pub(crate) ty: ProcedureType, + pub(crate) inner: Box) -> rspc_core::Procedure>, +} + +// TODO: `Debug`, `PartialEq`, `Eq`, `Hash` + +impl ErasedProcedure { + // TODO: Expose all fields + + // TODO: Make `pub` + // pub(crate) fn kind(&self) -> ProcedureKind2 { + // self.kind + // } + + // /// Export the [Specta](https://docs.rs/specta) types for this procedure. + // /// + // /// TODO - Use this with `rspc::typescript` + // /// + // /// # Usage + // /// + // /// ```rust + // /// todo!(); # TODO: Example + // /// ``` + // pub fn ty(&self) -> &ProcedureTypeDefinition { + // &self.ty + // } + + // /// Execute a procedure with the given context and input. + // /// + // /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. + // /// + // /// # Usage + // /// + // /// ```rust + // /// use serde_json::Value; + // /// + // /// fn run_procedure(procedure: Procedure) -> Vec { + // /// procedure + // /// .exec((), Value::Null) + // /// .collect::>() + // /// .await + // /// .into_iter() + // /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) + // /// .collect::>() + // /// } + // /// ``` + // pub fn exec<'de, T: ProcedureInput<'de>>( + // &self, + // ctx: TCtx, + // input: T, + // ) -> Result { + // match input.into_deserializer() { + // Ok(deserializer) => { + // let mut input = ::erase(deserializer); + // (self.handler)(ctx, &mut input) + // } + // Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), + // } + // } +} diff --git a/rspc/src/modern/procedure/procedure.rs b/rspc/src/modern/procedure/procedure.rs deleted file mode 100644 index 86f6de87..00000000 --- a/rspc/src/modern/procedure/procedure.rs +++ /dev/null @@ -1,185 +0,0 @@ -// use std::{borrow::Cow, fmt, sync::Arc}; - -// use futures::FutureExt; -// use specta::{DataType, TypeMap}; - -// use crate::{Error, State}; - -// use super::{ -// exec_input::{AnyInput, InputValueInner}, -// stream::ProcedureStream, -// InternalError, ProcedureBuilder, ProcedureExecInput, ProcedureInput, ProcedureKind, -// ProcedureMeta, ResolverInput, ResolverOutput, -// }; - -// pub(super) type InvokeFn = Arc< -// dyn Fn(TCtx, &mut dyn InputValueInner) -> Result + Send + Sync, -// >; - -// /// Represents a single operations on the server that can be executed. -// /// -// /// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. -// /// -// pub struct Procedure { -// kind: ProcedureKind, -// ty: ProcedureTypeDefinition, -// handler: InvokeFn, -// } - -// impl Clone for Procedure { -// fn clone(&self) -> Self { -// Self { -// kind: self.kind, -// ty: self.ty.clone(), -// handler: self.handler.clone(), -// } -// } -// } - -// impl fmt::Debug for Procedure { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// f.debug_struct("Procedure") -// .field("kind", &self.kind) -// .field("ty", &self.ty) -// .field("handler", &"...") -// .finish() -// } -// } - -// impl Procedure -// where -// TCtx: 'static, -// { -// /// Construct a new procedure using [`ProcedureBuilder`]. -// pub fn builder() -> ProcedureBuilder -// where -// TError: Error, -// // Only the first layer (middleware or the procedure) needs to be a valid input/output type -// I: ResolverInput, -// R: ResolverOutput, -// { -// ProcedureBuilder { -// build: Box::new(|kind, setups, handler| { -// // TODO: Don't be `Arc>` just `Arc<_>` -// let handler = Arc::new(handler); - -// UnbuiltProcedure::new(move |key, state, type_map| { -// let meta = ProcedureMeta::new(key.clone(), kind); -// for setup in setups { -// setup(state, meta.clone()); -// } - -// Procedure { -// kind, -// ty: ProcedureTypeDefinition { -// key, -// kind, -// input: I::data_type(type_map), -// result: R::data_type(type_map), -// }, -// handler: Arc::new(move |ctx, input| { -// let fut = handler( -// ctx, -// I::from_value(ProcedureExecInput::new(input))?, -// meta.clone(), -// ); - -// Ok(R::into_procedure_stream(fut.into_stream())) -// }), -// } -// }) -// }), -// } -// } -// } - -// impl Procedure { -// pub fn kind(&self) -> ProcedureKind { -// self.kind -// } - -// /// Export the [Specta](https://docs.rs/specta) types for this procedure. -// /// -// /// TODO - Use this with `rspc::typescript` -// /// -// /// # Usage -// /// -// /// ```rust -// /// todo!(); # TODO: Example -// /// ``` -// pub fn ty(&self) -> &ProcedureTypeDefinition { -// &self.ty -// } - -// /// Execute a procedure with the given context and input. -// /// -// /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. -// /// -// /// # Usage -// /// -// /// ```rust -// /// use serde_json::Value; -// /// -// /// fn run_procedure(procedure: Procedure) -> Vec { -// /// procedure -// /// .exec((), Value::Null) -// /// .collect::>() -// /// .await -// /// .into_iter() -// /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) -// /// .collect::>() -// /// } -// /// ``` -// pub fn exec<'de, T: ProcedureInput<'de>>( -// &self, -// ctx: TCtx, -// input: T, -// ) -> Result { -// match input.into_deserializer() { -// Ok(deserializer) => { -// let mut input = ::erase(deserializer); -// (self.handler)(ctx, &mut input) -// } -// Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), -// } -// } -// } - -// #[derive(Debug, Clone, PartialEq)] -// pub struct ProcedureTypeDefinition { -// // TODO: Should `key` move onto `Procedure` instead?s -// pub key: Cow<'static, str>, -// pub kind: ProcedureKind, -// pub input: DataType, -// pub result: DataType, -// } - -// pub struct UnbuiltProcedure( -// Box, &mut State, &mut TypeMap) -> Procedure>, -// ); - -// impl fmt::Debug for UnbuiltProcedure { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// f.debug_struct("UnbuiltProcedure").finish() -// } -// } - -// impl UnbuiltProcedure { -// pub(crate) fn new( -// build_fn: impl FnOnce(Cow<'static, str>, &mut State, &mut TypeMap) -> Procedure + 'static, -// ) -> Self { -// Self(Box::new(build_fn)) -// } - -// /// Build the procedure invoking all the setup functions. -// /// -// /// Generally you will not need to call this directly as you can give a [ProcedureFactory] to the [RouterBuilder::procedure] and let it take care of the rest. -// pub fn build( -// self, -// key: Cow<'static, str>, -// state: &mut State, -// type_map: &mut TypeMap, -// ) -> Procedure { -// (self.0)(key, state, type_map) -// } -// } diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 7034aeb9..51bfb454 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, panic::Location, sync::Arc}; +use std::{borrow::Cow, marker::PhantomData, panic::Location, sync::Arc}; use futures::{FutureExt, TryStreamExt}; use rspc_core::Procedure; @@ -6,10 +6,12 @@ use specta::datatype::DataType; use crate::{ modern::{ - procedure::{ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput}, + procedure::{ + ErasedProcedure, ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput, + }, Error, }, - ProcedureKind, State, + Extension, ProcedureKind, State, }; #[derive(Clone)] @@ -25,19 +27,19 @@ pub(crate) struct ProcedureType { /// /// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. /// -pub struct Procedure2 { - pub(crate) setup: Vec>, - pub(crate) ty: ProcedureType, - pub(crate) inner: Box) -> rspc_core::Procedure>, +pub struct Procedure2 { + pub(crate) build: + Box>) -> ErasedProcedure>, + pub(crate) phantom: PhantomData<(TInput, TResult)>, } // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` -impl Procedure2 { +impl Procedure2 { #[cfg(feature = "unstable")] /// Construct a new procedure using [`ProcedureBuilder`]. #[track_caller] - pub fn builder() -> ProcedureBuilder + pub fn builder() -> ProcedureBuilder where TCtx: Send + 'static, TError: Error, @@ -47,9 +49,24 @@ impl Procedure2 { { let location = Location::caller().clone(); ProcedureBuilder { - build: Box::new(move |kind, setups, handler| { - Procedure2 { - setup: Default::default(), + build: Box::new(move |kind, setup, handler| { + ErasedProcedure { + setup: setup + .into_iter() + .map(|setup| { + let v: Box = + Box::new(move |state: &mut State| { + let key: Cow<'static, str> = "todo".to_string().into(); // TODO: Work this out properly + let meta = ProcedureMeta::new( + key.into(), + kind, + Arc::new(State::default()), // TODO: Can we configure a panic instead of this! + ); + setup(state, meta); + }); + v + }) + .collect::>(), ty: ProcedureType { kind, input: DataType::Any, // I::data_type(type_map), @@ -78,6 +95,22 @@ impl Procedure2 { }), } }), + phantom: PhantomData, + } + } + + pub fn with(self, mw: Extension) -> Self + where + TCtx: 'static, + { + Procedure2 { + build: Box::new(move |mut setups| { + if let Some(setup) = mw.setup { + setups.push(setup); + } + (self.build)(setups) + }), + phantom: PhantomData, } } @@ -134,3 +167,9 @@ impl Procedure2 { // } // } } + +impl Into> for Procedure2 { + fn into(self) -> ErasedProcedure { + (self.build)(Default::default()) + } +} diff --git a/rspc/src/router.rs b/rspc/src/router.rs index fdfbca56..0ca27765 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -10,13 +10,15 @@ use specta::TypeCollection; use rspc_core::Procedures; -use crate::{types::TypesOrType, Procedure2, ProcedureKind, State, Types}; +use crate::{ + modern::procedure::ErasedProcedure, types::TypesOrType, Procedure2, ProcedureKind, State, Types, +}; /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { setup: Vec>, types: TypeCollection, - procedures: BTreeMap>, Procedure2>, + procedures: BTreeMap>, ErasedProcedure>, errors: Vec, } @@ -41,7 +43,7 @@ impl Router2 { pub fn procedure( mut self, key: impl Into>, - mut procedure: Procedure2, + procedure: impl Into>, ) -> Self { let key = key.into(); @@ -52,6 +54,7 @@ impl Router2 { duplicate: Location::caller().clone(), }); } else { + let mut procedure = procedure.into(); self.setup.extend(procedure.setup.drain(..)); self.procedures.insert(vec![key], procedure); } @@ -121,13 +124,13 @@ impl Router2 { self.build_with_state_inner(State::default()) } - #[cfg(feature = "unstable")] - pub fn build_with_state( - self, - state: State, - ) -> Result<(Procedures, Types), Vec> { - self.build_with_state_inner(state) - } + // #[cfg(feature = "unstable")] + // pub fn build_with_state( + // self, + // state: State, + // ) -> Result<(Procedures, Types), Vec> { + // self.build_with_state_inner(state) + // } fn build_with_state_inner( self, @@ -165,7 +168,9 @@ impl Router2 { .collect::>(); Ok(( - Procedures::from(procedures), + Procedures::new(procedures, state), + // TODO: Get rid of this and have `rspc-tracing` mount it + // .with_logger(|event| println!("{event:?}")), Types { types: self.types, procedures: procedure_types, @@ -196,8 +201,9 @@ impl fmt::Debug for Router2 { } impl<'a, TCtx> IntoIterator for &'a Router2 { - type Item = (&'a Vec>, &'a Procedure2); - type IntoIter = std::collections::btree_map::Iter<'a, Vec>, Procedure2>; + type Item = (&'a Vec>, &'a ErasedProcedure); + type IntoIter = + std::collections::btree_map::Iter<'a, Vec>, ErasedProcedure>; fn into_iter(self) -> Self::IntoIter { self.procedures.iter() @@ -215,7 +221,7 @@ impl From> for Router2 { impl Router2 { pub(crate) fn interop_procedures( &mut self, - ) -> &mut BTreeMap>, Procedure2> { + ) -> &mut BTreeMap>, ErasedProcedure> { &mut self.procedures } From 8a7dc869d0d48be9524c88c6e2e6ac4a0d7d475d Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 23 Dec 2024 15:17:14 +0800 Subject: [PATCH 51/67] remove HTTP status codes from core + comment out broken stuff --- core/src/error.rs | 42 +-- core/src/stream.rs | 12 - examples/actix-web/src/main.rs | 15 +- examples/core/src/lib.rs | 6 +- integrations/actix-web/src/lib.rs | 201 ++++++------ integrations/axum/src/endpoint.rs | 450 +++++++++++++------------- integrations/axum/src/jsonrpc_exec.rs | 8 +- integrations/axum/src/lib.rs | 2 +- integrations/http/src/content_type.rs | 56 ---- integrations/http/src/execute.rs | 236 +++++++------- integrations/http/src/lib.rs | 4 +- integrations/tauri/src/lib.rs | 119 +++---- rspc/src/legacy/interop.rs | 8 +- rspc/src/procedure.rs | 11 +- 14 files changed, 537 insertions(+), 633 deletions(-) delete mode 100644 integrations/http/src/content_type.rs diff --git a/core/src/error.rs b/core/src/error.rs index 33348a77..0d43ca7b 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -28,17 +28,6 @@ pub enum ProcedureError { } impl ProcedureError { - pub fn status(&self) -> u16 { - match self { - Self::NotFound => 404, - Self::Deserialize(_) => 400, - Self::Downcast(_) => 400, - Self::Resolver(err) => err.status(), - Self::Unwind(_) => 500, - // Self::Serializer(_) => 500, - } - } - pub fn variant(&self) -> &'static str { match self { ProcedureError::NotFound => "NotFound", @@ -133,52 +122,31 @@ impl Serialize for ProcedureError { } /// TODO -pub struct ResolverError { - status: u16, - value: Box, -} +pub struct ResolverError(Box); impl ResolverError { // Warning: Returning > 400 will fallback to `500`. As redirects would be invalid and `200` would break matching. pub fn new( - mut status: u16, value: T, source: Option, ) -> Self { - if status < 400 { - status = 500; - } - - Self { - status, - value: Box::new(ErrorInternal { value, err: source }), - } - } - - /// TODO - pub fn status(&self) -> u16 { - self.status + Self(Box::new(ErrorInternal { value, err: source })) } /// TODO pub fn value(&self) -> impl Serialize + '_ { - self.value.value() + self.0.value() } /// TODO pub fn error(&self) -> Option<&(dyn error::Error + Send + 'static)> { - self.value.error() + self.0.error() } } impl fmt::Debug for ResolverError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "status: {:?}, error: {:?}", - self.status, - self.value.debug() - ) + write!(f, "ResolverError({:?})", self.0.debug()) } } diff --git a/core/src/stream.rs b/core/src/stream.rs index 99c869a3..e1ffb4ea 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -73,18 +73,6 @@ impl ProcedureStream { } } - /// TODO - pub async fn next_status(&mut self) -> (u16, bool) { - // TODO: Panic if it isn't the start of the stream or not??? - - // TODO: Poll till the first return value and return it's code. - - // TODO: Should we keep polling so we can tell if it's a value or a stream for the content type??? - - // todo!(); - (200, false) - } - /// TODO // TODO: Should error be `String` type? pub fn map Result + Unpin, T>( diff --git a/examples/actix-web/src/main.rs b/examples/actix-web/src/main.rs index 73722484..ccd36627 100644 --- a/examples/actix-web/src/main.rs +++ b/examples/actix-web/src/main.rs @@ -61,13 +61,14 @@ async fn main() -> std::io::Result<()> { .wrap(Cors::permissive()) .service(hello) .service(upload) - .service(web::scope("/rspc").configure( - rspc_actix_web::Endpoint::builder(procedures.clone()).build(|| { - // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this - // Ctx {} - todo!(); - }), - )) + // TODO + // .service(web::scope("/rspc").configure( + // rspc_actix_web::Endpoint::builder(procedures.clone()).build(|| { + // // println!("Client requested operation '{}'", parts.uri.path()); // TODO: Fix this + // // Ctx {} + // todo!(); + // }), + // )) }) .bind(addr)? .run() diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index 197aabdb..5fb2f038 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -110,7 +110,7 @@ pub enum Error { impl Error2 for Error { fn into_resolver_error(self) -> rspc::ResolverError { - rspc::ResolverError::new(500, self.to_string(), None::) + rspc::ResolverError::new(self.to_string(), None::) } } @@ -141,6 +141,9 @@ impl Serialize for SerialisationError { fn test_unstable_stuff(router: Router2) -> Router2 { router + .procedure("withoutBaseProcedure", { + Procedure2::builder::().query(|ctx: Ctx, id: String| async move { Ok(()) }) + }) .procedure("newstuff", { ::builder().query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) }) @@ -206,6 +209,7 @@ fn test_unstable_stuff(router: Router2) -> Router2 { Ok(()) }) }) + // .procedure("sfmStatefulPost", { // ::builder() // // .with(Invalidator::mw(|ctx, input, event| { diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index ff1f77df..62f1bcf3 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -6,107 +6,106 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] -use std::{convert::Infallible, sync::Arc}; - -use actix_web::{ - body::BodyStream, - http::{header, StatusCode}, - web::{self, Bytes, Payload, ServiceConfig}, - HttpRequest, HttpResponse, -}; -use actix_ws::Message; - -use futures_util::StreamExt; -use rspc_core::Procedures; -use rspc_http::ExecuteInput; - -pub struct Endpoint { - procedures: Procedures, - // endpoints: bool, - // websocket: Option TCtx>, - // batching: bool, -} - -impl Endpoint { - pub fn builder(router: Procedures) -> Self { - Self { - procedures: router, - // endpoints: false, - // websocket: None, - // batching: false, - } - } - - pub fn build( - self, - ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static, - ) -> impl FnOnce(&mut ServiceConfig) { - let ctx_fn = Arc::new(ctx_fn); - move |service| { - service.route( - "/ws", - // TODO: Hook this up properly - web::to(|req: HttpRequest, body: Payload| async move { - let (response, mut session, mut stream) = actix_ws::handle(&req, body)?; - - actix_web::rt::spawn(async move { - session.text("Hello World From rspc").await.unwrap(); - - while let Some(Ok(msg)) = stream.next().await { - match msg { - Message::Ping(bytes) => { - if session.pong(&bytes).await.is_err() { - return; - } - } - - Message::Text(msg) => println!("Got text: {msg}"), - _ => break, - } - } - - let _ = session.close(None).await; - }); - - Ok::<_, actix_web::Error>(response) - }), - ); - - // TODO: Making extractors work - - for (key, procedure) in self.procedures { - let ctx_fn = ctx_fn.clone(); - let handler = move |req: HttpRequest, body: Bytes| { - let procedure = procedure.clone(); - let ctx_fn = ctx_fn.clone(); - async move { - let input = if body.is_empty() { - ExecuteInput::Query(req.query_string()) - } else { - // TODO: Error if not JSON content-type - - ExecuteInput::Body(&body) - }; - - let (status, stream) = - rspc_http::execute(&procedure, input, || ctx_fn()).await; - HttpResponse::build( - StatusCode::from_u16(status) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - ) - .insert_header(header::ContentType::json()) - .body(BodyStream::new( - stream.map(|v| Ok::<_, Infallible>(v.into())), - )) - } - }; - - service.route(&key, web::get().to(handler.clone())); - service.route(&key, web::post().to(handler)); - } - } - } -} +// use std::{convert::Infallible, sync::Arc}; + +// use actix_web::{ +// body::BodyStream, +// http::{header, StatusCode}, +// web::{self, Bytes, Payload, ServiceConfig}, +// HttpRequest, HttpResponse, +// }; +// use actix_ws::Message; + +// use futures_util::StreamExt; +// use rspc_core::Procedures; + +// pub struct Endpoint { +// procedures: Procedures, +// // endpoints: bool, +// // websocket: Option TCtx>, +// // batching: bool, +// } + +// impl Endpoint { +// pub fn builder(router: Procedures) -> Self { +// Self { +// procedures: router, +// // endpoints: false, +// // websocket: None, +// // batching: false, +// } +// } + +// pub fn build( +// self, +// ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static, +// ) -> impl FnOnce(&mut ServiceConfig) { +// let ctx_fn = Arc::new(ctx_fn); +// move |service| { +// service.route( +// "/ws", +// // TODO: Hook this up properly +// web::to(|req: HttpRequest, body: Payload| async move { +// let (response, mut session, mut stream) = actix_ws::handle(&req, body)?; + +// actix_web::rt::spawn(async move { +// session.text("Hello World From rspc").await.unwrap(); + +// while let Some(Ok(msg)) = stream.next().await { +// match msg { +// Message::Ping(bytes) => { +// if session.pong(&bytes).await.is_err() { +// return; +// } +// } + +// Message::Text(msg) => println!("Got text: {msg}"), +// _ => break, +// } +// } + +// let _ = session.close(None).await; +// }); + +// Ok::<_, actix_web::Error>(response) +// }), +// ); + +// // TODO: Making extractors work + +// for (key, procedure) in self.procedures { +// let ctx_fn = ctx_fn.clone(); +// let handler = move |req: HttpRequest, body: Bytes| { +// let procedure = procedure.clone(); +// let ctx_fn = ctx_fn.clone(); +// async move { +// let input = if body.is_empty() { +// ExecuteInput::Query(req.query_string()) +// } else { +// // TODO: Error if not JSON content-type + +// ExecuteInput::Body(&body) +// }; + +// let (status, stream) = +// rspc_http::execute(&procedure, input, || ctx_fn()).await; +// HttpResponse::build( +// StatusCode::from_u16(status) +// .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), +// ) +// .insert_header(header::ContentType::json()) +// .body(BodyStream::new( +// stream.map(|v| Ok::<_, Infallible>(v.into())), +// )) +// } +// }; + +// service.route(&key, web::get().to(handler.clone())); +// service.route(&key, web::post().to(handler)); +// } +// } +// } +// } // pub struct TODO(HttpRequest); diff --git a/integrations/axum/src/endpoint.rs b/integrations/axum/src/endpoint.rs index c284d721..817d29c9 100644 --- a/integrations/axum/src/endpoint.rs +++ b/integrations/axum/src/endpoint.rs @@ -1,225 +1,225 @@ -use std::{ - convert::Infallible, - future::poll_fn, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use axum::{ - body::{Body, Bytes, HttpBody}, - extract::{FromRequest, Request}, - http::{header, HeaderMap, StatusCode}, - response::{ - sse::{Event, KeepAlive}, - IntoResponse, Sse, - }, - routing::{on, MethodFilter}, -}; -use futures::{stream::once, Stream, StreamExt, TryStreamExt}; -use rspc_core::{ProcedureError, ProcedureStream, Procedures}; -use rspc_http::ExecuteInput; - -/// Construct a new [`axum::Router`](axum::Router) to expose a given [`rspc::Router`](rspc::Router). -pub struct Endpoint { - procedures: Procedures, - // endpoints: bool, - // websocket: Option TCtx>, - // batching: bool, -} - -impl Endpoint { - // /// Construct a new [`axum::Router`](axum::Router) with all features enabled. - // /// - // /// This will enable all features, if you want to configure which features are enabled you can use [`Endpoint::builder`] instead. - // /// - // /// # Usage - // /// - // /// ```rust - // /// axum::Router::new().nest( - // /// "/rspc", - // /// rspc_axum::Endpoint::new(rspc::Router::new().build().unwrap(), || ()), - // /// ); - // /// ``` - // pub fn new( - // router: BuiltRouter, - // // TODO: Parse this to `Self::build` -> It will make rustfmt result way nicer - // // TODO: Make Axum extractors work - // ctx_fn: impl Fn(&Parts) -> TCtx + Send + Sync + 'static, - // ) -> axum::Router - // where - // S: Clone + Send + Sync + 'static, - // // TODO: Error type??? - // // F: Future> + Send + Sync + 'static, - // TCtx: Clone, - // { - // let mut t = Self::builder(router).with_endpoints(); - // #[cfg(feature = "ws")] - // { - // t = t.with_websocket(); - // } - // t.with_batching().build(ctx_fn) - // } - - // /// Construct a new [`Endpoint`](Endpoint) with no features enabled. - // /// - // /// # Usage - // /// - // /// ```rust - // /// axum::Router::new().nest( - // /// "/rspc", - // /// rspc_axum::Endpoint::builder(rspc::Router::new().build().unwrap()) - // /// // Exposes HTTP endpoints for queries and mutations. - // /// .with_endpoints() - // /// // Exposes a Websocket connection for queries, mutations and subscriptions. - // /// .with_websocket() - // /// // Enables support for the frontend sending batched queries. - // /// .with_batching() - // /// .build(|| ()), - // /// ); - // /// ``` - pub fn builder(router: Procedures) -> Self { - Self { - procedures: router, - // endpoints: false, - // websocket: None, - // batching: false, - } - } - - // /// Enables HTTP endpoints for queries and mutations. - // /// - // /// This is exposed as `/routerName.procedureName` - // pub fn with_endpoints(mut self) -> Self { - // Self { - // endpoints: true, - // ..self - // } - // } - - // /// Exposes a Websocket connection for queries, mutations and subscriptions. - // /// - // /// This is exposed as a `/ws` endpoint. - // #[cfg(feature = "ws")] - // #[cfg_attr(docsrs, doc(cfg(feature = "ws")))] - // pub fn with_websocket(self) -> Self - // where - // TCtx: Clone, - // { - // Self { - // websocket: Some(|ctx| ctx.clone()), - // ..self - // } - // } - - // /// Enables support for the frontend sending batched queries. - // /// - // /// This is exposed as a `/_batch` endpoint. - // pub fn with_batching(self) -> Self - // where - // TCtx: Clone, - // { - // Self { - // batching: true, - // ..self - // } - // } - - // TODO: Axum extractors - - /// Build an [`axum::Router`](axum::Router) with the configured features. - pub fn build(self, ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static) -> axum::Router - where - S: Clone + Send + Sync + 'static, - { - let mut r = axum::Router::new(); - let ctx_fn = Arc::new(ctx_fn); - - // let logger = self.procedures.get_logger(); - - for (key, procedure) in self.procedures { - let ctx_fn = ctx_fn.clone(); - r = r.route( - &format!("/{key}"), - on( - MethodFilter::GET.or(MethodFilter::POST), - move |req: Request| { - // let ctx = ctx_fn(); - - async move { - let hint = req.body().size_hint(); - let has_body = hint.lower() != 0 || hint.upper() != Some(0); - - let mut bytes = None; - let input = if !has_body { - ExecuteInput::Query(req.uri().query().unwrap_or_default()) - } else { - // TODO: bring this back - // if !json_content_type(req.headers()) { - // let err: ProcedureError = rspc_core::DeserializeError::custom( - // "Client did not set correct valid 'Content-Type' header", - // ) - // .into(); - // let buf = serde_json::to_vec(&err).unwrap(); // TODO - - // return ( - // StatusCode::BAD_REQUEST, - // [(header::CONTENT_TYPE, "application/json")], - // Body::from(buf), - // ) - // .into_response(); - // } - - // TODO: Error handling - bytes = Some(Bytes::from_request(req, &()).await.unwrap()); - ExecuteInput::Body( - bytes.as_ref().expect("assigned on previous line"), - ) - }; - - let (status, stream) = - rspc_http::execute(&procedure, input, || ctx_fn()).await; - - ( - StatusCode::from_u16(status) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - [(header::CONTENT_TYPE, "application/json")], - Body::from_stream(stream.map(Ok::<_, Infallible>)), - ) - .into_response() - } - }, - ), - ); - } - - // TODO: Websocket endpoint - - r - } -} - -fn json_content_type(headers: &HeaderMap) -> bool { - let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { - content_type - } else { - return false; - }; - - let content_type = if let Ok(content_type) = content_type.to_str() { - content_type - } else { - return false; - }; - - let mime = if let Ok(mime) = content_type.parse::() { - mime - } else { - return false; - }; - - let is_json_content_type = mime.type_() == "application" - && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); - - is_json_content_type -} +// use std::{ +// convert::Infallible, +// future::poll_fn, +// pin::Pin, +// sync::Arc, +// task::{Context, Poll}, +// }; + +// use axum::{ +// body::{Body, Bytes, HttpBody}, +// extract::{FromRequest, Request}, +// http::{header, HeaderMap, StatusCode}, +// response::{ +// sse::{Event, KeepAlive}, +// IntoResponse, Sse, +// }, +// routing::{on, MethodFilter}, +// }; +// use futures::{stream::once, Stream, StreamExt, TryStreamExt}; +// use rspc_core::{ProcedureError, ProcedureStream, Procedures}; +// use rspc_http::ExecuteInput; + +// /// Construct a new [`axum::Router`](axum::Router) to expose a given [`rspc::Router`](rspc::Router). +// pub struct Endpoint { +// procedures: Procedures, +// // endpoints: bool, +// // websocket: Option TCtx>, +// // batching: bool, +// } + +// impl Endpoint { +// // /// Construct a new [`axum::Router`](axum::Router) with all features enabled. +// // /// +// // /// This will enable all features, if you want to configure which features are enabled you can use [`Endpoint::builder`] instead. +// // /// +// // /// # Usage +// // /// +// // /// ```rust +// // /// axum::Router::new().nest( +// // /// "/rspc", +// // /// rspc_axum::Endpoint::new(rspc::Router::new().build().unwrap(), || ()), +// // /// ); +// // /// ``` +// // pub fn new( +// // router: BuiltRouter, +// // // TODO: Parse this to `Self::build` -> It will make rustfmt result way nicer +// // // TODO: Make Axum extractors work +// // ctx_fn: impl Fn(&Parts) -> TCtx + Send + Sync + 'static, +// // ) -> axum::Router +// // where +// // S: Clone + Send + Sync + 'static, +// // // TODO: Error type??? +// // // F: Future> + Send + Sync + 'static, +// // TCtx: Clone, +// // { +// // let mut t = Self::builder(router).with_endpoints(); +// // #[cfg(feature = "ws")] +// // { +// // t = t.with_websocket(); +// // } +// // t.with_batching().build(ctx_fn) +// // } + +// // /// Construct a new [`Endpoint`](Endpoint) with no features enabled. +// // /// +// // /// # Usage +// // /// +// // /// ```rust +// // /// axum::Router::new().nest( +// // /// "/rspc", +// // /// rspc_axum::Endpoint::builder(rspc::Router::new().build().unwrap()) +// // /// // Exposes HTTP endpoints for queries and mutations. +// // /// .with_endpoints() +// // /// // Exposes a Websocket connection for queries, mutations and subscriptions. +// // /// .with_websocket() +// // /// // Enables support for the frontend sending batched queries. +// // /// .with_batching() +// // /// .build(|| ()), +// // /// ); +// // /// ``` +// pub fn builder(router: Procedures) -> Self { +// Self { +// procedures: router, +// // endpoints: false, +// // websocket: None, +// // batching: false, +// } +// } + +// // /// Enables HTTP endpoints for queries and mutations. +// // /// +// // /// This is exposed as `/routerName.procedureName` +// // pub fn with_endpoints(mut self) -> Self { +// // Self { +// // endpoints: true, +// // ..self +// // } +// // } + +// // /// Exposes a Websocket connection for queries, mutations and subscriptions. +// // /// +// // /// This is exposed as a `/ws` endpoint. +// // #[cfg(feature = "ws")] +// // #[cfg_attr(docsrs, doc(cfg(feature = "ws")))] +// // pub fn with_websocket(self) -> Self +// // where +// // TCtx: Clone, +// // { +// // Self { +// // websocket: Some(|ctx| ctx.clone()), +// // ..self +// // } +// // } + +// // /// Enables support for the frontend sending batched queries. +// // /// +// // /// This is exposed as a `/_batch` endpoint. +// // pub fn with_batching(self) -> Self +// // where +// // TCtx: Clone, +// // { +// // Self { +// // batching: true, +// // ..self +// // } +// // } + +// // TODO: Axum extractors + +// /// Build an [`axum::Router`](axum::Router) with the configured features. +// pub fn build(self, ctx_fn: impl Fn() -> TCtx + Send + Sync + 'static) -> axum::Router +// where +// S: Clone + Send + Sync + 'static, +// { +// let mut r = axum::Router::new(); +// let ctx_fn = Arc::new(ctx_fn); + +// // let logger = self.procedures.get_logger(); + +// for (key, procedure) in self.procedures { +// let ctx_fn = ctx_fn.clone(); +// r = r.route( +// &format!("/{key}"), +// on( +// MethodFilter::GET.or(MethodFilter::POST), +// move |req: Request| { +// // let ctx = ctx_fn(); + +// async move { +// let hint = req.body().size_hint(); +// let has_body = hint.lower() != 0 || hint.upper() != Some(0); + +// let mut bytes = None; +// let input = if !has_body { +// ExecuteInput::Query(req.uri().query().unwrap_or_default()) +// } else { +// // TODO: bring this back +// // if !json_content_type(req.headers()) { +// // let err: ProcedureError = rspc_core::DeserializeError::custom( +// // "Client did not set correct valid 'Content-Type' header", +// // ) +// // .into(); +// // let buf = serde_json::to_vec(&err).unwrap(); // TODO + +// // return ( +// // StatusCode::BAD_REQUEST, +// // [(header::CONTENT_TYPE, "application/json")], +// // Body::from(buf), +// // ) +// // .into_response(); +// // } + +// // TODO: Error handling +// bytes = Some(Bytes::from_request(req, &()).await.unwrap()); +// ExecuteInput::Body( +// bytes.as_ref().expect("assigned on previous line"), +// ) +// }; + +// let (status, stream) = +// rspc_http::execute(&procedure, input, || ctx_fn()).await; + +// ( +// StatusCode::from_u16(status) +// .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), +// [(header::CONTENT_TYPE, "application/json")], +// Body::from_stream(stream.map(Ok::<_, Infallible>)), +// ) +// .into_response() +// } +// }, +// ), +// ); +// } + +// // TODO: Websocket endpoint + +// r +// } +// } + +// fn json_content_type(headers: &HeaderMap) -> bool { +// let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { +// content_type +// } else { +// return false; +// }; + +// let content_type = if let Ok(content_type) = content_type.to_str() { +// content_type +// } else { +// return false; +// }; + +// let mime = if let Ok(mime) = content_type.parse::() { +// mime +// } else { +// return false; +// }; + +// let is_json_content_type = mime.type_() == "application" +// && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); + +// is_json_content_type +// } diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 0badc1cf..be590b73 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -339,7 +339,13 @@ async fn next( .cloned(); jsonrpc::JsonRPCError { - code: err.status() as i32, + code: match err { + ProcedureError::NotFound => 404, + ProcedureError::Deserialize(_) => 400, + ProcedureError::Downcast(_) => 400, + ProcedureError::Resolver(_) => 500, // This is a breaking change. It previously came from the user. + ProcedureError::Unwind(_) => 500, + }, message: legacy_error .map(|v| v.0.clone()) // This probally isn't a great format but we are assuming your gonna use the new router with a new executor for typesafe errors. diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 6cff77a4..0c55cdd0 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -14,6 +14,6 @@ mod jsonrpc_exec; mod request; mod v2; -pub use endpoint::Endpoint; +// pub use endpoint::Endpoint; pub use request::AxumRequest; pub use v2::endpoint; diff --git a/integrations/http/src/content_type.rs b/integrations/http/src/content_type.rs deleted file mode 100644 index d7ae7980..00000000 --- a/integrations/http/src/content_type.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::sync::Arc; - -// use serde::{Serialize, Serializer}; - -// TODO: What should this be called? -// TODO: Matching on headers too or just `Content-Type`? -// TODO: Error handling for decoding errors or no matching content-type -// TODO: `Content-Type` for incoming body decoding as well -// TODO: Client typesafety. Eg. does an endpoint require a specific content-type Eg. `File`? - -// #[derive(Default)] -// pub struct Registry(Option bool>>); - -// impl Registry { -// // pub fn new() -> Self { -// // Self::default() -// // } - -// // TODO: Remove -// // pub fn todo(&self, handler: impl Fn(&str) -> ()) { -// // self.0.push(Arc::new(|ct, v| {})); -// // } - -// pub fn r#match(&self, content_type: &str, value: S) { -// // for f in &self.0 { -// // // f(content_type); -// // } - -// todo!(); -// } -// } - -// TODO: `Default` + `Debug`, `Clone`, etc - -// fn todo() { -// Registry::default().register(|ct, ser| { -// if ct.starts_with("application/json") { -// // TODO: We need to be able to configure `Content-Type` header - -// // ser.serialize() or ser.value() -// // serde_json::to_writer(v, &ct).unwrap(); -// // true -// } else { -// // false -// } -// }) -// } - -// pub trait Request { -// fn method(&self) -> &str; - -// // fn path(&self) -> &str; -// // fn query(&self) -> &str; -// // fn headers(&self); -// // fn body(&self); -// } diff --git a/integrations/http/src/execute.rs b/integrations/http/src/execute.rs index 12d039ca..43f0886e 100644 --- a/integrations/http/src/execute.rs +++ b/integrations/http/src/execute.rs @@ -7,124 +7,124 @@ //! Hyper (via http_body_util::StreamBody) - impl Stream, E>>, //! Rocket - impl Stream> -use std::{ - pin::Pin, - task::{Context, Poll}, -}; - -use futures::StreamExt; -use futures_core::Stream; -use rspc_core::ProcedureStream; -use serde::Serializer; - -pub enum ExecuteInput<'a> { - Query(&'a str), - Body(&'a [u8]), -} - -/// TODO: Explain this -// TODO: `Content-Type` header on response??? -pub async fn execute<'a, 'b, TCtx>( - procedure: &'a rspc_core::Procedure, - input: ExecuteInput<'b>, - ctx: impl FnOnce() -> TCtx, -) -> (u16, impl Stream> + Send + 'static) { - let stream = match input { - ExecuteInput::Query(query) => { - let mut params = form_urlencoded::parse(query.as_bytes()); - - match params.find_map(|(input, value)| (input == "input").then(|| value)) { - Some(input) => procedure.exec_with_deserializer( - ctx(), - &mut serde_json::Deserializer::from_str(&*input), - ), - None => procedure.exec_with_deserializer(ctx(), serde_json::Value::Null), - } - } - ExecuteInput::Body(body) => procedure - .exec_with_deserializer(ctx(), &mut serde_json::Deserializer::from_slice(&body)), - }; - - let mut stream = ProcedureStreamResponse { - code: None, - stream, - first: None, - }; - stream.first = Some(stream.next().await); - // TODO: Some(poll_fn(|cx| stream.poll_next(cx)).await); - - // TODO: We should poll past the first value to check if it's the only value and set content-type based on it. `ready(...)` will go done straight away. - - (stream.code.unwrap_or(500), stream) -} - -pub async fn into_body( - stream: ProcedureStream, -) -> (u16, impl Stream> + Send + 'static) { - let mut stream = ProcedureStreamResponse { - code: None, - stream, - first: None, - }; - stream.first = Some(stream.next().await); - // TODO: Some(poll_fn(|cx| stream.poll_next(cx)).await); - - // TODO: We should poll past the first value to check if it's the only value and set content-type based on it. `ready(...)` will go done straight away. - - (stream.code.unwrap_or(500), stream) -} - -// TODO: Sealing fields at least? -struct ProcedureStreamResponse { - code: Option, - first: Option>>, - stream: ProcedureStream, -} - -impl Stream for ProcedureStreamResponse { - type Item = Vec; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if let Some(first) = self.first.take() { - return Poll::Ready(first); - } - - let (code, mut buf) = { - let Poll::Ready(v) = self.stream.poll_next(cx) else { - return Poll::Pending; - }; - - match v { - Some(Ok(v)) => ( - 200, - Some(serde_json::to_vec(&v).unwrap()), // TODO: Error handling - // .map_err(|err| { - // // TODO: Configure handling of this error and how we log it??? - // serde_json::to_vec(&ProcedureError::Serializer(err.to_string())) - // .expect("bruh") - // })), - ), - Some(Err(err)) => ( - err.status(), - Some(serde_json::to_vec(&err).unwrap()), // TODO: Error handling - ), - None => (200, None), - } - }; - - // TODO: Only after first item - if let Some(buf) = &mut buf { - buf.extend_from_slice(b"\n\n"); - }; - - self.code = Some(code); - Poll::Ready(buf) - } - - fn size_hint(&self) -> (usize, Option) { - self.stream.size_hint() - } -} +// use std::{ +// pin::Pin, +// task::{Context, Poll}, +// }; + +// use futures::StreamExt; +// use futures_core::Stream; +// use rspc_core::ProcedureStream; +// use serde::Serializer; + +// pub enum ExecuteInput<'a> { +// Query(&'a str), +// Body(&'a [u8]), +// } + +// /// TODO: Explain this +// // TODO: `Content-Type` header on response??? +// pub async fn execute<'a, 'b, TCtx>( +// procedure: &'a rspc_core::Procedure, +// input: ExecuteInput<'b>, +// ctx: impl FnOnce() -> TCtx, +// ) -> (u16, impl Stream> + Send + 'static) { +// let stream = match input { +// ExecuteInput::Query(query) => { +// let mut params = form_urlencoded::parse(query.as_bytes()); + +// match params.find_map(|(input, value)| (input == "input").then(|| value)) { +// Some(input) => procedure.exec_with_deserializer( +// ctx(), +// &mut serde_json::Deserializer::from_str(&*input), +// ), +// None => procedure.exec_with_deserializer(ctx(), serde_json::Value::Null), +// } +// } +// ExecuteInput::Body(body) => procedure +// .exec_with_deserializer(ctx(), &mut serde_json::Deserializer::from_slice(&body)), +// }; + +// let mut stream = ProcedureStreamResponse { +// code: None, +// stream, +// first: None, +// }; +// stream.first = Some(stream.next().await); +// // TODO: Some(poll_fn(|cx| stream.poll_next(cx)).await); + +// // TODO: We should poll past the first value to check if it's the only value and set content-type based on it. `ready(...)` will go done straight away. + +// (stream.code.unwrap_or(500), stream) +// } + +// pub async fn into_body( +// stream: ProcedureStream, +// ) -> (u16, impl Stream> + Send + 'static) { +// let mut stream = ProcedureStreamResponse { +// code: None, +// stream, +// first: None, +// }; +// stream.first = Some(stream.next().await); +// // TODO: Some(poll_fn(|cx| stream.poll_next(cx)).await); + +// // TODO: We should poll past the first value to check if it's the only value and set content-type based on it. `ready(...)` will go done straight away. + +// (stream.code.unwrap_or(500), stream) +// } + +// // TODO: Sealing fields at least? +// struct ProcedureStreamResponse { +// code: Option, +// first: Option>>, +// stream: ProcedureStream, +// } + +// impl Stream for ProcedureStreamResponse { +// type Item = Vec; + +// fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { +// if let Some(first) = self.first.take() { +// return Poll::Ready(first); +// } + +// let (code, mut buf) = { +// let Poll::Ready(v) = self.stream.poll_next(cx) else { +// return Poll::Pending; +// }; + +// match v { +// Some(Ok(v)) => ( +// 200, +// Some(serde_json::to_vec(&v).unwrap()), // TODO: Error handling +// // .map_err(|err| { +// // // TODO: Configure handling of this error and how we log it??? +// // serde_json::to_vec(&ProcedureError::Serializer(err.to_string())) +// // .expect("bruh") +// // })), +// ), +// Some(Err(err)) => ( +// err.status(), +// Some(serde_json::to_vec(&err).unwrap()), // TODO: Error handling +// ), +// None => (200, None), +// } +// }; + +// // TODO: Only after first item +// if let Some(buf) = &mut buf { +// buf.extend_from_slice(b"\n\n"); +// }; + +// self.code = Some(code); +// Poll::Ready(buf) +// } + +// fn size_hint(&self) -> (usize, Option) { +// self.stream.size_hint() +// } +// } // TODO // fn json_content_type(headers: &HeaderMap) -> bool { diff --git a/integrations/http/src/lib.rs b/integrations/http/src/lib.rs index 5e4be095..fdb219e4 100644 --- a/integrations/http/src/lib.rs +++ b/integrations/http/src/lib.rs @@ -13,10 +13,8 @@ // TODO: Custom cookies, headers, etc -mod content_type; mod execute; mod file; mod socket; -pub use content_type::*; -pub use execute::*; // TODO: {execute, ExecuteInput}; // TODO: Don't do wildcard +pub use execute::*; diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index c6dffe6e..9e301aaa 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -48,65 +48,66 @@ where channel: tauri::ipc::Channel, req: Request, ) { - match req { - Request::Request { path, input } => { - let id = channel.id(); - let ctx = (self.ctx_fn)(window); - - let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { - let err = ProcedureError::NotFound; - send( - &channel, - Response::Value { - code: err.status(), - value: &err, - }, - ); - send::<()>(&channel, Response::Done); - return; - }; - - let mut stream = match input { - Some(i) => procedure.exec_with_deserializer(ctx, i.as_ref()), - None => procedure.exec_with_deserializer(ctx, serde_json::Value::Null), - }; - - let this = self.clone(); - let handle = spawn(async move { - while let Some(value) = stream.next().await { - match value { - Ok(v) => send( - &channel, - Response::Value { - code: 200, - value: &v, - }, - ), - Err(err) => send( - &channel, - Response::Value { - code: err.status(), - value: &err, - }, - ), - } - } - - this.subscriptions().remove(&id); - send::<()>(&channel, Response::Done); - }); - - // if the client uses an existing ID, we will assume the previous subscription is no longer required - if let Some(old) = self.subscriptions().insert(id, handle) { - old.abort(); - } - } - Request::Abort(id) => { - if let Some(h) = self.subscriptions().remove(&id) { - h.abort(); - } - } - } + todo!(); + // match req { + // Request::Request { path, input } => { + // let id = channel.id(); + // let ctx = (self.ctx_fn)(window); + + // let Some(procedure) = self.procedures.get(&Cow::Borrowed(&*path)) else { + // let err = ProcedureError::NotFound; + // send( + // &channel, + // Response::Value { + // code: err.status(), + // value: &err, + // }, + // ); + // send::<()>(&channel, Response::Done); + // return; + // }; + + // let mut stream = match input { + // Some(i) => procedure.exec_with_deserializer(ctx, i.as_ref()), + // None => procedure.exec_with_deserializer(ctx, serde_json::Value::Null), + // }; + + // let this = self.clone(); + // let handle = spawn(async move { + // while let Some(value) = stream.next().await { + // match value { + // Ok(v) => send( + // &channel, + // Response::Value { + // code: 200, + // value: &v, + // }, + // ), + // Err(err) => send( + // &channel, + // Response::Value { + // code: err.status(), + // value: &err, + // }, + // ), + // } + // } + + // this.subscriptions().remove(&id); + // send::<()>(&channel, Response::Done); + // }); + + // // if the client uses an existing ID, we will assume the previous subscription is no longer required + // if let Some(old) = self.subscriptions().insert(id, handle) { + // old.abort(); + // } + // } + // Request::Abort(id) => { + // if let Some(h) = self.subscriptions().remove(&id) { + // h.abort(); + // } + // } + // } } } diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy/interop.rs index 3a6b9c33..38bb944c 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -93,7 +93,6 @@ pub(crate) fn layer_to_procedure( .map_err(|err| { let err: crate::legacy::Error = err.into(); ResolverError::new( - err.code.to_status_code(), (), /* typesafe errors aren't supported in legacy router */ Some(rspc_core::LegacyErrorInterop(err.message)), ) @@ -112,7 +111,6 @@ pub(crate) fn layer_to_procedure( .map_err(|err| { let err = crate::legacy::Error::from(err); ResolverError::new( - err.code.to_status_code(), (), /* typesafe errors aren't supported in legacy router */ Some(rspc_core::LegacyErrorInterop(err.message)), ) @@ -121,11 +119,7 @@ pub(crate) fn layer_to_procedure( .boxed(), Err(err) => { let err: crate::legacy::Error = err.into(); - let err = ResolverError::new( - err.code.to_status_code(), - err.message, - err.cause, - ); + let err = ResolverError::new(err.message, err.cause); stream::once(async { Err(err.into()) }).boxed() } } diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 51bfb454..1ee9272e 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -39,13 +39,14 @@ impl Procedure2 { #[cfg(feature = "unstable")] /// Construct a new procedure using [`ProcedureBuilder`]. #[track_caller] - pub fn builder() -> ProcedureBuilder + pub fn builder( + ) -> ProcedureBuilder where TCtx: Send + 'static, TError: Error, // Only the first layer (middleware or the procedure) needs to be a valid input/output type - I: ResolverInput, - R: ResolverOutput, + TInput: ResolverInput, + TResult: ResolverOutput, { let location = Location::caller().clone(); ProcedureBuilder { @@ -79,10 +80,10 @@ impl Procedure2 { let meta = ProcedureMeta::new(key.clone(), kind, state); Procedure::new(move |ctx, input| { - R::into_procedure_stream( + TResult::into_procedure_stream( handler( ctx, - I::from_input(input).unwrap(), // TODO: Error handling + TInput::from_input(input).unwrap(), // TODO: Error handling meta.clone(), ) .into_stream() From d45ded7f35210cf31c08b6d847dc716b3516084e Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 23 Dec 2024 15:44:37 +0800 Subject: [PATCH 52/67] basically working `rspc_invalidation` --- examples/axum/src/main.rs | 2 +- examples/bindings.ts | 3 +- examples/core/src/lib.rs | 14 ++-- middleware/invalidation/Cargo.toml | 1 + middleware/invalidation/src/lib.rs | 109 +++++++++++++---------------- 5 files changed, 59 insertions(+), 70 deletions(-) diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 74297a65..be0388ee 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -263,7 +263,7 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { _ = fut => {} } - for stream in rspc_invalidation::queue(&invalidator, || ctx.clone(), &procedures) { + for stream in rspc_invalidation::queue(&invalidator, ctx, &procedures) { runtime.insert(stream.map:: _, Vec>(|v| { serde_json::to_vec(&v).map_err(|err| err.to_string()) })); diff --git a/examples/bindings.ts b/examples/bindings.ts index 6879bc57..1be8d89e 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,7 +1,7 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string } | { key: "withoutBaseProcedure"; input: any; result: any }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { cached: { kind: "query", input: any, output: any, error: any }, @@ -21,4 +21,5 @@ export type Procedures = { sfmPostEdit: { kind: "query", input: any, output: any, error: any }, transformMe: { kind: "query", input: null, output: string, error: unknown }, version: { kind: "query", input: null, output: string, error: unknown }, + withoutBaseProcedure: { kind: "query", input: any, output: any, error: any }, } \ No newline at end of file diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index 5fb2f038..d0df9d6c 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -194,13 +194,13 @@ fn test_unstable_stuff(router: Router2) -> Router2 { println!("FETCH POST FROM DB"); Ok(id) }) - .with(Invalidator::with(|event| { - println!("--- AFTER"); - if let InvalidateEvent::Post { id } = event { - return Invalidate::One((id.to_string(), ())); - } - Invalidate::None - })) + // .with(Invalidator::with(|event| { + // println!("--- AFTER"); + // if let InvalidateEvent::Post { id } = event { + // return Invalidate::One((id.to_string(), ())); + // } + // Invalidate::None + // })) }) .procedure("sfmPostEdit", { ::builder().query(|ctx, id: String| async move { diff --git a/middleware/invalidation/Cargo.toml b/middleware/invalidation/Cargo.toml index 2a49abb2..72ed39d1 100644 --- a/middleware/invalidation/Cargo.toml +++ b/middleware/invalidation/Cargo.toml @@ -7,6 +7,7 @@ publish = false # TODO: Crate metadata & publish [dependencies] async-stream = "0.3.5" rspc = { path = "../../rspc", features = ["unstable"] } +serde = "1.0.216" serde_json = "1.0.133" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/middleware/invalidation/src/lib.rs b/middleware/invalidation/src/lib.rs index 0dc9f742..226ede50 100644 --- a/middleware/invalidation/src/lib.rs +++ b/middleware/invalidation/src/lib.rs @@ -11,17 +11,20 @@ use std::{ sync::{Arc, Mutex, PoisonError}, }; -use rspc::{middleware::Middleware, Extension, ProcedureStream, Procedures}; +use rspc::{Extension, ProcedureStream, Procedures}; +use serde::Serialize; #[derive(Default)] struct State { - closures: Vec () + Send + Sync>>, + closures: + Vec) + Send + Sync>>, } #[derive(Debug)] // TODO: Traits but only if the generic also has the trait. pub enum Invalidate { None, - All, + // TODO: Discuss how `Any` is less efficient because it invalidates instead of pushing new data. + Any, One(T), Many(Vec), } @@ -58,46 +61,13 @@ impl Invalidator { .push(event); } - // pub fn mw( - // // TODO: With multiple middleware how do we enforce we have the first layers `TInput`? - // handler: impl Fn(&E) -> Invalidate + Send + Sync + 'static, - // ) -> Middleware - // where - // TError: Send + 'static, - // TCtx: Send + 'static, - // TInput: Send + 'static, - // TResult: Send + 'static, - // { - // let handler = Arc::new(handler); - // Middleware::new(move |ctx: TCtx, input: TInput, next| async move { - // let result = next.exec(ctx, input).await; - // result - // }) - // .setup(|state, meta| { - // // TODO: Error out on mutations or subscriptions due to concerns about safety. - - // state - // .get_mut_or_init(|| State::default()) - // .closures - // .push(Arc::new(move |event| { - // match handler(event.downcast_ref().unwrap()) { - // Invalidate::None => println!("{:?} {:?}", meta.name(), "NONE"), - // // TODO: Make these work properly - // Invalidate::All => println!("{:?} {:?}", meta.name(), "ALL"), - // Invalidate::One(input) => println!("{:?} {:?}", meta.name(), "ONE"), - // Invalidate::Many(inputs) => println!("{:?} {:?}", meta.name(), "MANY"), - // } - // })); - // }) - // } - pub fn with( // TODO: With multiple middleware how do we enforce we have the first layers `TInput`? handler: impl Fn(&E) -> Invalidate + Send + Sync + 'static, ) -> Extension where TCtx: Send + 'static, - TInput: Send + 'static, + TInput: Serialize + Send + 'static, TResult: Send + 'static, { let handler = Arc::new(handler); @@ -107,23 +77,52 @@ impl Invalidator { state .get_mut_or_init(|| State::default()) .closures - .push(Arc::new(move |event| { - match handler(event.downcast_ref().unwrap()) { - Invalidate::None => println!("{:?} {:?}", meta.name(), "NONE"), - // TODO: Make these work properly - Invalidate::All => println!("{:?} {:?}", meta.name(), "ALL"), - Invalidate::One(input) => println!("{:?} {:?}", meta.name(), "ONE"), - Invalidate::Many(inputs) => println!("{:?} {:?}", meta.name(), "MANY"), - } + .push(Arc::new(move |event, ctx, procedures, streams| { + // TODO: error handling downcast. + // - Can we detect the error on startup and not at runtime? + // - Can we throw onto `Router::build`'s `Result` instead of panicing? + let ctx: TCtx = ctx.downcast_mut::>().unwrap().take().unwrap(); + let event: &E = event.downcast_ref().unwrap(); + let procedures: &Procedures = procedures.downcast_ref().unwrap(); + + match handler(event) { + Invalidate::None => { + println!("{:?} {:?}", meta.name(), "NONE"); // TODO + } + Invalidate::Any => { + println!("{:?} {:?}", meta.name(), "ALL"); // TODO + todo!(); // TODO: make it work + } + Invalidate::One(input) => { + println!("{:?} {:?}", meta.name(), "ONE"); // TODO + + // TODO: Avoid `serde_json::Value`? + let input: serde_json::Value = serde_json::to_value(&input).unwrap(); + + // let name = meta.name(); + let name = "sfmPost"; // TODO: Don't do this once `meta.name()` is correct. + + if let Some(procedure) = procedures.get(name) { + streams.push(procedure.exec_with_deserializer(ctx, input)); + } else { + println!("Procedure not found!"); // TODO: Silently fail in future. + } + } + Invalidate::Many(inputs) => { + println!("{:?} {:?}", meta.name(), "MANY"); + todo!(); + } + }; })); }) } } // TODO: The return type does lack info about which procedure is running -pub fn queue( +// TODO: Should `TCtx` clone vs taking function. This is easier so doing it for now. +pub fn queue( invalidator: &Invalidator, - ctx_fn: impl Fn() -> TCtx, + ctx: TCtx, procedures: &Procedures, ) -> Vec { let mut streams = Vec::new(); @@ -134,24 +133,12 @@ pub fn queue( .lock() .unwrap_or_else(PoisonError::into_inner); - for mut event in invalidated.drain(..) { + for event in invalidated.drain(..) { for closure in &state.closures { - closure(&mut event); // TODO: Take in `streams`, `procedures` and `ctx_fn` + closure(&event, &mut Some(ctx.clone()), procedures, &mut streams); } } } - let keys_to_invalidate = vec!["version"]; // TODO: How to work out which procedures to rerun? -> We need some request scoped data. - - for name in keys_to_invalidate { - streams.push( - procedures - .get(name) - .unwrap() - // TODO: Don't deserialize to `serde_json::Value` and make the input type work properly. - .exec_with_deserializer(ctx_fn(), serde_json::Value::Null), - ); - } - streams } From 43d08581efc3a089d294ceabf283ed436c5ba80c Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 23 Dec 2024 23:18:37 +0800 Subject: [PATCH 53/67] `ProcedureStream` advanced queuing and flushing --- core/src/lib.rs | 2 +- core/src/procedures.rs | 1 + core/src/stream.rs | 309 +++++++++++++++++++++++++++++++------- examples/axum/src/main.rs | 132 ++++++++++------ examples/core/src/lib.rs | 10 ++ 5 files changed, 351 insertions(+), 103 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 242c46c2..7e094bf9 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -30,4 +30,4 @@ pub use interop::LegacyErrorInterop; pub use procedure::Procedure; pub use procedures::Procedures; pub use state::State; -pub use stream::{ProcedureStream, ProcedureStreamMap, ProcedureStreamValue}; +pub use stream::{flush, ProcedureStream, ProcedureStreamMap, ProcedureStreamValue}; diff --git a/core/src/procedures.rs b/core/src/procedures.rs index 5dae14be..e856fd43 100644 --- a/core/src/procedures.rs +++ b/core/src/procedures.rs @@ -9,6 +9,7 @@ use std::{ use crate::{Procedure, State}; pub struct Procedures { + // TODO: Probally `Arc` around map and share that with `State`? procedures: HashMap, Procedure>, state: Arc, } diff --git a/core/src/stream.rs b/core/src/stream.rs index e1ffb4ea..e46b37ff 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -1,9 +1,11 @@ use core::fmt; use std::{ - future::poll_fn, + cell::RefCell, + future::{poll_fn, Future}, panic::{catch_unwind, AssertUnwindSafe}, pin::Pin, - task::{Context, Poll}, + sync::Arc, + task::{Context, Poll, Waker}, }; use futures_core::Stream; @@ -12,9 +14,58 @@ use serde::Serialize; use crate::ProcedureError; +thread_local! { + static CAN_FLUSH: RefCell = RefCell::default(); + static SHOULD_FLUSH: RefCell> = RefCell::default(); +} + /// TODO -#[must_use = "ProcedureStream does nothing unless polled"] -pub struct ProcedureStream(Result>, Option>); +pub async fn flush() { + if CAN_FLUSH.with(|v| *v.borrow()) { + let mut pending = true; + poll_fn(|_| { + if pending { + pending = false; + SHOULD_FLUSH.replace(Some(true)); + return Poll::Pending; + } + + Poll::Ready(()) + }) + .await; + } +} + +enum Inner { + Dyn(Pin>), + Value(Option), +} + +/// TODO +#[must_use = "`ProcedureStream` does nothing unless polled"] +pub struct ProcedureStream { + inner: Inner, + // If `None` flushing is allowed. + // This is the default but will also be set after `flush` is called. + // + // If `Some` then `flush` must be called before the next value is yielded. + // Will poll until the first value and then return `Poll::Pending` and record the waker. + // The stored value will be yielded immediately after `flush` is called. + flush: Option, + // This is set `true` if `Poll::Ready` is called while `flush` is `Some`. + // This informs the stream to yield the value immediately when `flush` is `None` again. + pending_value: bool, +} + +impl From for ProcedureStream { + fn from(err: ProcedureError) -> Self { + Self { + inner: Inner::Value(Some(err)), + flush: None, + pending_value: false, + } + } +} impl ProcedureStream { /// TODO @@ -23,11 +74,35 @@ impl ProcedureStream { S: Stream> + Send + 'static, T: Serialize + Send + Sync + 'static, { - Self(Ok(Box::pin(DynReturnImpl { - src: s, - unwound: false, - value: None, - }))) + Self { + inner: Inner::Dyn(Box::pin(DynReturnImpl { + src: s, + unwound: false, + value: None, + })), + flush: None, + pending_value: false, + } + } + + // TODO: `fn from_future` + + /// TODO + pub fn from_future_stream(f: F) -> Self + where + F: Future> + Send + 'static, + S: Stream> + Send + 'static, + T: Serialize + Send + Sync + 'static, + { + // Self { + // inner: Ok(Box::pin(DynReturnImpl { + // src: f, + // unwound: false, + // value: None, + // })), + // manual_flush: false, + // } + todo!(); } /// TODO @@ -36,14 +111,112 @@ impl ProcedureStream { S: Stream> + Send + 'static, T: Send + Sync + 'static, { - Self(todo!()) + todo!(); + } + + // TODO: `fn from_future_value` + + /// TODO + pub fn from_future_stream_value(f: F) -> Self + where + F: Future> + Send + 'static, + S: Stream> + Send + 'static, + T: Serialize + Send + Sync + 'static, + { + todo!(); + } + + /// By setting this the stream will delay returning any data until instructed by the caller (via `Self::stream`). + /// + /// This allows you to progress an entire runtime of streams until all of them are in a state ready to start returning responses. + /// This mechanism allows anything that could need to modify the HTTP response headers to do so before the body starts being streamed. + /// + /// # Behaviour + /// + /// `ProcedureStream` will poll the underlying stream until the first value is ready. + /// It will then return `Poll::Pending` and go inactive until `Self::stream` is called. + /// When polled for the first time after `Self::stream` is called if a value was already ready it will be immediately returned. + /// It is *guaranteed* that the stream will never yield `Poll::Ready` until `flush` is called if this is set. + /// + /// # Usage + /// + /// It's generally expected you will continue to poll the runtime until some criteria based on `Self::resolved` & `Self::flushable` is met on all streams. + /// Once this is met you can call `Self::stream` on all of the streams at once to begin streaming data. + /// + pub fn require_manual_stream(mut self) -> Self { + // This `Arc` is inefficient but `Waker::noop` is coming soon which will solve it. + self.flush = Some(Arc::new(NoOpWaker).into()); + self + } + + /// Start streaming data. + /// Refer to `Self::require_manual_stream` for more information. + pub fn stream(&mut self) { + if let Some(waker) = self.flush.take() { + waker.wake(); + } + } + + /// Will return `true` if the future has resolved. + /// + /// For a stream created via `Self::from_future*` this will be `true` once the future has resolved and for all other streams this will always be `true`. + pub fn resolved(&self) -> bool { + true // TODO + } + + /// Will return `true` if the stream is ready to start streaming data. + /// + /// This is `false` until the `flush` function is called by the user. + pub fn flushable(&self) -> bool { + false // TODO } /// TODO pub fn size_hint(&self) -> (usize, Option) { - match &self.0 { - Ok(v) => v.size_hint(), - Err(_) => (1, Some(1)), + match &self.inner { + Inner::Dyn(stream) => stream.size_hint(), + Inner::Value(_) => (1, Some(1)), + } + } + + fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll>> { + // Ensure the waker is up to date. + if let Some(waker) = &mut self.flush { + if !waker.will_wake(cx.waker()) { + self.flush = Some(cx.waker().clone()); + } + } + + if self.pending_value { + return if self.flush.is_none() { + // We have a queued value ready to be flushed. + self.pending_value = false; + Poll::Ready(Some(Ok(()))) + } else { + // The async runtime would have no reason to be polling right now but we protect against it anyway. + Poll::Pending + }; + } + + match &mut self.inner { + Inner::Dyn(v) => match v.as_mut().poll_next_value(cx) { + Poll::Ready(v) => { + if self.flush.is_none() { + Poll::Ready(v) + } else { + self.pending_value = true; + Poll::Pending + } + } + Poll::Pending => Poll::Pending, + }, + Inner::Value(v) => { + if self.flush.is_none() { + Poll::Ready(v.take().map(Err)) + } else { + Poll::Pending + } + } } } @@ -52,25 +225,23 @@ impl ProcedureStream { &mut self, cx: &mut Context<'_>, ) -> Poll>> { - match &mut self.0 { - Ok(v) => v - .as_mut() - .poll_next_value(cx) - .map(|v| v.map(|v| v.map(|_: ()| self.0.as_mut().expect("checked above").value()))), - Err(err) => Poll::Ready(err.take().map(Err)), - } + self.poll_inner(cx).map(|v| { + v.map(|v| { + v.map(|_: ()| { + let Inner::Dyn(s) = &mut self.inner else { + unreachable!(); + }; + s.value() + }) + }) + }) } /// TODO pub async fn next( &mut self, ) -> Option> { - match self { - Self(Ok(v)) => poll_fn(|cx| v.as_mut().poll_next_value(cx)) - .await - .map(|v| v.map(|_: ()| self.0.as_mut().expect("checked above").value())), - Self(Err(err)) => err.take().map(Err), - } + poll_fn(move |cx| self.poll_inner(cx)).await } /// TODO @@ -88,6 +259,28 @@ pub struct ProcedureStreamMap Result Result + Unpin, T> ProcedureStreamMap { + /// Start streaming data. + /// Refer to `Self::require_manual_stream` for more information. + pub fn stream(&mut self) { + self.stream.stream(); + } + + /// Will return `true` if the future has resolved. + /// + /// For a stream created via `Self::from_future*` this will be `true` once the future has resolved and for all other streams this will always be `true`. + pub fn resolved(&self) -> bool { + self.stream.resolved() + } + + /// Will return `true` if the stream is ready to start streaming data. + /// + /// This is `false` until the `flush` function is called by the user. + pub fn flushable(&self) -> bool { + self.stream.flushable() + } +} + impl Result + Unpin, T> Stream for ProcedureStreamMap { @@ -96,22 +289,30 @@ impl Result + Unpin, T> Stream fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); - match this.stream.0.as_mut() { - Ok(v) => v.as_mut().poll_next_value(cx).map(|v| { - v.map(|v| match v { - Ok(()) => match (this.map)(ProcedureStreamValue( - this.stream.0.as_mut().expect("checked above").value(), - )) { - Ok(v) => v, - // TODO: Exposing this error to the client or not? - // TODO: Error type??? - Err(err) => todo!(), - }, - Err(err) => todo!("{err:?}"), - }) - }), - Err(err) => todo!(), - } + this.stream.poll_inner(cx).map(|v| { + v.map(|v| { + match v { + Ok(()) => { + let Inner::Dyn(s) = &mut this.stream.inner else { + unreachable!(); + }; + + match (this.map)(ProcedureStreamValue(s.value())) { + Ok(v) => v, + // TODO: Exposing this error to the client or not? + // TODO: Error type??? + Err(err) => todo!(), + } + } + // TODO: Fix this + Err(_) => todo!(), + } + }) + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.stream.size_hint() } } @@ -128,12 +329,6 @@ impl<'a> Serialize for ProcedureStreamValue<'a> { } } -impl From for ProcedureStream { - fn from(err: ProcedureError) -> Self { - Self(Err(Some(err))) - } -} - impl fmt::Debug for ProcedureStream { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!(); @@ -149,6 +344,8 @@ trait DynReturnValue: Send { fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync); fn size_hint(&self) -> (usize, Option); + + fn ready_for_flush(&self) -> bool; } pin_project! { @@ -197,13 +394,21 @@ where } fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync) { - self.value - .as_ref() - // Attempted to access value when `Poll::Ready(None)` was not returned. - .expect("unreachable") + // Attempted to access value when `Poll::Ready(None)` was not returned. + self.value.as_ref().expect("unreachable") } fn size_hint(&self) -> (usize, Option) { self.src.size_hint() } + + fn ready_for_flush(&self) -> bool { + todo!(); + } +} + +// TODO: When stablised replace with - https://doc.rust-lang.org/stable/std/task/struct.Waker.html#method.noop +struct NoOpWaker; +impl std::task::Wake for NoOpWaker { + fn wake(self: std::sync::Arc) {} } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index be0388ee..f7b9f957 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -7,11 +7,12 @@ use axum::{ }; use example_core::{create_router, Ctx}; use futures::{stream::FuturesUnordered, Stream, StreamExt}; -use rspc::{ProcedureStreamValue, Procedures, State}; +use rspc::{ProcedureStream, ProcedureStreamMap, ProcedureStreamValue, Procedures, State}; +use rspc_invalidation::Invalidator; use serde_json::{de::SliceRead, value::RawValue, Value}; use std::{ convert::Infallible, - future::Future, + future::{poll_fn, Future}, path::PathBuf, pin::{pin, Pin}, task::{Context, Poll}, @@ -198,13 +199,22 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { r.route( "/", post(move |mut multipart: Multipart| async move { - let mut runtime = StreamUnordered::new(); - let invalidator = rspc_invalidation::Invalidator::default(); let ctx = Ctx { invalidator: invalidator.clone(), }; + let mut runtime = StreamUnordered::new(); + // TODO: Move onto `Prototype`??? + let spawn = |runtime: &mut StreamUnordered<_>, p: ProcedureStream| { + runtime.insert(p.require_manual_stream().map:: Result, String>, Vec>( + |v| serde_json::to_vec(&v).map_err(|err| err.to_string()), + )); + }; + // TODO: If a file was being uploaded this would require reading the whole body until the `runtime` is polled. while let Some(field) = multipart.next_field().await.unwrap() { let name = field.name().unwrap().to_string(); // TODO: Error handling @@ -226,73 +236,95 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { let procedure = procedures.get(&*name).unwrap(); println!("{:?} {:?} {:?}", name, input, procedure); - let stream = procedure.exec_with_deserializer(ctx.clone(), input); - - runtime.insert(stream.map:: _, Vec>(|v| { - serde_json::to_vec(&v).map_err(|err| err.to_string()) - })); - - // TODO: Spawn onto runtime - // let (status, is_stream) = stream.next_status().await; - - // println!("{:?} {:?}", status, is_stream); - - // ( - // StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - // [ - // ( - // header::CONTENT_TYPE, - // is_stream - // .then_some("application/jsonstream") - // .unwrap_or("application/json"), - // ), - // (HeaderName::from_static("x-rspc"), "1"), - // ], - // Body::from_stream(stream.map(|v| { - // serde_json::to_vec(&v) - // .map_err(|err| err.to_string()) - // .map(Ok::<_, Infallible>) - // })), - // ) - } - - // TODO: Wait until the full stream is Mattrax-style flushed to run this. - let fut = tokio::time::sleep(std::time::Duration::from_secs(1)); - tokio::select! { - _ = runtime.next() => {} - _ = fut => {} + spawn( + &mut runtime, + procedure.exec_with_deserializer(ctx.clone(), input), + ); } - for stream in rspc_invalidation::queue(&invalidator, ctx, &procedures) { - runtime.insert(stream.map:: _, Vec>(|v| { - serde_json::to_vec(&v).map_err(|err| err.to_string()) - })); - } + // TODO: Move onto `Prototype`??? + poll_fn(|cx| match runtime.poll_next_unpin(cx) { + // `ProcedureStream::require_manual_stream` is set. + Poll::Ready(_) => unreachable!(), + Poll::Pending => { + // Once we know all futures are ready, + // we allow them all to flush. + if runtime + .iter_mut() + // TODO: If we want to allow the user to opt-in to manually flush and flush within a stream this won't work. + .all(|stream| stream.resolved() || stream.flushable()) + { + runtime.iter_mut().for_each(|s| s.stream()); + Poll::Ready(()) + } else { + Poll::Pending + } + } + }) + .await; ( [(header::CONTENT_TYPE, "text/x-rspc")], - Body::from_stream(Prototype { runtime }), + Body::from_stream(Prototype { + runtime, + sfm: false, + invalidator, + ctx, + procedures: procedures.clone(), + }), ) }), ) } -pub struct Prototype>> { - runtime: StreamUnordered, +// TODO: This abstraction is soooo bad. +pub struct Prototype { + runtime: StreamUnordered< + ProcedureStreamMap Result, String>, Vec>, + >, + invalidator: Invalidator, + ctx: TCtx, + sfm: bool, + procedures: Procedures, } -// TODO: Should `S: 'static` be a thing? -impl> + 'static> Stream for Prototype { +// impl Prototype {} + +// TODO: Drop `Unpin` requirement +impl Stream for Prototype { type Item = Result, Infallible>; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { loop { + // We spawn SFM's after all futures are resolved, as we don't allow `.invalidate` calls after this point. + // In general you shouldn't be batching mutations so this won't make a difference to performance. + if !self.sfm + && self + .as_mut() + .get_mut() + .runtime + .iter_mut() + .all(|s| s.resolved()) + { + self.sfm = true; + + for stream in + rspc_invalidation::queue(&self.invalidator, self.ctx.clone(), &self.procedures) + { + self.runtime.insert( + stream.map:: Result, String>, Vec>( + |v| serde_json::to_vec(&v).map_err(|err| err.to_string()), + ), + ); + } + } + let Poll::Ready(v) = self.runtime.poll_next_unpin(cx) else { return Poll::Pending; }; return Poll::Ready(match v { - Some((v, i)) => match v { + Some((v, _)) => match v { StreamYield::Item(mut v) => { let id = 0; // TODO: Include identifier to request/query let identifier = 'O' as u8; // TODO: error, oneshot, event or complete message diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index d0df9d6c..dc2ec51b 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -209,6 +209,16 @@ fn test_unstable_stuff(router: Router2) -> Router2 { Ok(()) }) }) + // .procedure("manualFlush", { + // ::builder() + // .manual_flush() + // .query(|ctx, id: String| async move { + // println!("Set cookies"); + // flush().await; + // println!("Do more stuff in background"); + // Ok(()) + // }) + // }) // .procedure("sfmStatefulPost", { // ::builder() From 792e7a6739fc22c4c37d8a1ca2dbb8cebaaa1638 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 24 Dec 2024 12:32:09 +0800 Subject: [PATCH 54/67] use extension for openapi --- middleware/openapi/src/lib.rs | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/middleware/openapi/src/lib.rs b/middleware/openapi/src/lib.rs index 547134f6..da57295d 100644 --- a/middleware/openapi/src/lib.rs +++ b/middleware/openapi/src/lib.rs @@ -17,7 +17,7 @@ use axum::{ Json, }; use futures::StreamExt; -use rspc::{middleware::Middleware, Procedure2, ResolverInput, Router2}; +use rspc::{middleware::Middleware, Extension, Procedure2, ResolverInput, Router2}; use serde_json::json; // TODO: Properly handle inputs from query params @@ -72,24 +72,13 @@ impl OpenAPI { // TODO: Configure other OpenAPI stuff like auth??? - pub fn build( - self, - ) -> Middleware - where - TError: 'static, - TThisCtx: Send + 'static, - TThisInput: Send + 'static, - TThisResult: Send + 'static, - { - // TODO: Can we have a middleware with only a `setup` function to avoid the extra future boxing??? - Middleware::new(|ctx, input, next| async move { next.exec(ctx, input).await }).setup( - move |state, meta| { - state - .get_mut_or_init::(Default::default) - .0 - .insert((self.method, self.path), meta.name().to_string()); - }, - ) + pub fn build(self) -> Extension { + Extension::new().setup(move |state, meta| { + state + .get_mut_or_init::(Default::default) + .0 + .insert((self.method, self.path), meta.name().to_string()); + }) } } From 5ed8808116707b2f481312d25a4b6b7bf3524b94 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 25 Dec 2024 20:30:23 +0800 Subject: [PATCH 55/67] major `ProcedureStream` work + add `DynOutput` --- core/src/dyn_input.rs | 20 +- core/src/dyn_output.rs | 57 +++++ core/src/lib.rs | 6 +- core/src/stream.rs | 346 ++++++++++++++++++++------ examples/axum/src/main.rs | 26 +- integrations/axum/src/jsonrpc_exec.rs | 4 +- rspc/src/lib.rs | 4 +- 7 files changed, 363 insertions(+), 100 deletions(-) create mode 100644 core/src/dyn_output.rs diff --git a/core/src/dyn_input.rs b/core/src/dyn_input.rs index 1996d76a..33f32776 100644 --- a/core/src/dyn_input.rs +++ b/core/src/dyn_input.rs @@ -11,11 +11,11 @@ use crate::{DeserializeError, DowncastError, ProcedureError}; /// TODO pub struct DynInput<'a, 'de> { - inner: DynInputInner<'a, 'de>, + inner: Repr<'a, 'de>, pub(crate) type_name: &'static str, } -enum DynInputInner<'a, 'de> { +enum Repr<'a, 'de> { Value(&'a mut (dyn Any + Send)), Deserializer(&'a mut (dyn erased_serde::Deserializer<'de> + Send)), } @@ -23,23 +23,24 @@ enum DynInputInner<'a, 'de> { impl<'a, 'de> DynInput<'a, 'de> { pub fn new_value(value: &'a mut Option) -> Self { Self { - inner: DynInputInner::Value(value), + inner: Repr::Value(value), type_name: type_name::(), } } - pub fn new_deserializer + Send>( + // TODO: In a perfect world this would be public. + pub(crate) fn new_deserializer + Send>( deserializer: &'a mut D, ) -> Self { Self { - inner: DynInputInner::Deserializer(deserializer), + inner: Repr::Deserializer(deserializer), type_name: type_name::(), } } /// TODO pub fn deserialize>(self) -> Result { - let DynInputInner::Deserializer(deserializer) = self.inner else { + let Repr::Deserializer(deserializer) = self.inner else { return Err(ProcedureError::Deserialize(DeserializeError( erased_serde::Error::custom(format!( "attempted to deserialize from value '{}' but expected deserializer", @@ -53,12 +54,13 @@ impl<'a, 'de> DynInput<'a, 'de> { } /// TODO - pub fn value(self) -> Result { - let DynInputInner::Value(value) = self.inner else { + pub fn value(self) -> Result { + let Repr::Value(value) = self.inner else { return Err(DowncastError { from: None, to: type_name::(), - }); + } + .into()); }; Ok(value .downcast_mut::>() diff --git a/core/src/dyn_output.rs b/core/src/dyn_output.rs new file mode 100644 index 00000000..cdd371e0 --- /dev/null +++ b/core/src/dyn_output.rs @@ -0,0 +1,57 @@ +use std::{ + any::{type_name, Any}, + fmt, +}; + +use serde::Serialize; + +/// TODO +pub struct DynOutput<'a> { + inner: Repr<'a>, + pub(crate) type_name: &'static str, +} + +enum Repr<'a> { + Serialize(&'a (dyn erased_serde::Serialize + Send + Sync)), + Value(&'a mut (dyn Any + Send)), +} + +// TODO: `Debug`, etc traits + +impl<'a> DynOutput<'a> { + pub fn new_value(value: &'a mut Option) -> Self { + Self { + inner: Repr::Value(value), + type_name: type_name::(), + } + } + + pub fn new_serialize(value: &'a mut T) -> Self { + Self { + inner: Repr::Serialize(value), + type_name: type_name::(), + } + } + + /// TODO + pub fn as_serialize(self) -> Option { + match self.inner { + Repr::Serialize(v) => Some(v), + Repr::Value(_) => None, + } + } + + /// TODO + pub fn as_value(self) -> Option { + match self.inner { + Repr::Serialize(_) => None, + Repr::Value(v) => v.downcast_mut::>()?.take().expect("unreachable"), + } + } +} + +impl<'a> fmt::Debug for DynOutput<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!(); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 7e094bf9..e2a2f427 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -6,7 +6,7 @@ //! TODO: Discuss the traits that need to be layered on for this to be useful. //! TODO: Discuss how middleware don't exist here. //! -//! TODO: A fundamental flaw of our current architecture is that results must be `'static` (hence can't serialize in-place). This is hard to solve due to `async fn`'s internals being sealed. +//! TODO: Results must be `'static` because they have to escape the closure. #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( @@ -15,6 +15,7 @@ )] mod dyn_input; +mod dyn_output; mod error; mod interop; mod logger; @@ -24,10 +25,11 @@ mod state; mod stream; pub use dyn_input::DynInput; +pub use dyn_output::DynOutput; pub use error::{DeserializeError, DowncastError, ProcedureError, ResolverError}; #[doc(hidden)] pub use interop::LegacyErrorInterop; pub use procedure::Procedure; pub use procedures::Procedures; pub use state::State; -pub use stream::{flush, ProcedureStream, ProcedureStreamMap, ProcedureStreamValue}; +pub use stream::{flush, ProcedureStream, ProcedureStreamMap}; diff --git a/core/src/stream.rs b/core/src/stream.rs index e46b37ff..e6a5fe4d 100644 --- a/core/src/stream.rs +++ b/core/src/stream.rs @@ -5,14 +5,14 @@ use std::{ panic::{catch_unwind, AssertUnwindSafe}, pin::Pin, sync::Arc, - task::{Context, Poll, Waker}, + task::{ready, Context, Poll, Waker}, }; use futures_core::Stream; use pin_project_lite::pin_project; use serde::Serialize; -use crate::ProcedureError; +use crate::{DynOutput, ProcedureError}; thread_local! { static CAN_FLUSH: RefCell = RefCell::default(); @@ -54,7 +54,7 @@ pub struct ProcedureStream { flush: Option, // This is set `true` if `Poll::Ready` is called while `flush` is `Some`. // This informs the stream to yield the value immediately when `flush` is `None` again. - pending_value: bool, + pending_value: bool, // TODO: Could we just check for a value on `inner`? Less chance of panic in the case of a bug. } impl From for ProcedureStream { @@ -75,8 +75,19 @@ impl ProcedureStream { T: Serialize + Send + Sync + 'static, { Self { - inner: Inner::Dyn(Box::pin(DynReturnImpl { - src: s, + inner: Inner::Dyn(Box::pin(GenericDynReturnValue { + inner: s, + poll: |s, cx| s.poll_next(cx), + size_hint: |s| s.size_hint(), + resolved: |_| true, + as_value: |v| { + DynOutput::new_serialize( + v.as_mut() + // Attempted to access value when `Poll::Ready(None)` was not returned. + .expect("unreachable"), + ) + }, + flushed: false, unwound: false, value: None, })), @@ -85,7 +96,56 @@ impl ProcedureStream { } } - // TODO: `fn from_future` + /// TODO + pub fn from_future(f: F) -> Self + where + F: Future> + Send + 'static, + T: Serialize + Send + Sync + 'static, + { + pin_project! { + #[project = ReprProj] + struct Repr { + #[pin] + inner: Option, + } + } + + Self { + inner: Inner::Dyn(Box::pin(GenericDynReturnValue { + inner: Repr { inner: Some(f) }, + poll: |f, cx| { + let mut this = f.project(); + let v = match this.inner.as_mut().as_pin_mut() { + Some(fut) => ready!(fut.poll(cx)), + None => return Poll::Ready(None), + }; + + this.inner.set(None); + Poll::Ready(Some(v)) + }, + size_hint: |f| { + if f.inner.is_some() { + (1, Some(1)) + } else { + (0, Some(0)) + } + }, + as_value: |v| { + DynOutput::new_serialize( + v.as_mut() + // Attempted to access value when `Poll::Ready(None)` was not returned. + .expect("unreachable"), + ) + }, + resolved: |f| f.inner.is_none(), + flushed: false, + unwound: false, + value: None, + })), + flush: None, + pending_value: false, + } + } /// TODO pub fn from_future_stream(f: F) -> Self @@ -94,15 +154,53 @@ impl ProcedureStream { S: Stream> + Send + 'static, T: Serialize + Send + Sync + 'static, { - // Self { - // inner: Ok(Box::pin(DynReturnImpl { - // src: f, - // unwound: false, - // value: None, - // })), - // manual_flush: false, - // } - todo!(); + pin_project! { + #[project = ReprProj] + enum Repr { + Future { + #[pin] + inner: F, + }, + Stream { + #[pin] + inner: S, + }, + } + } + + Self { + inner: Inner::Dyn(Box::pin(GenericDynReturnValue { + inner: Repr::::Future { inner: f }, + poll: |mut f, cx| loop { + let this = f.as_mut().project(); + match this { + ReprProj::Future { inner } => { + let Poll::Ready(Ok(stream)) = inner.poll(cx) else { + return Poll::Pending; + }; + + f.set(Repr::Stream { inner: stream }); + continue; + } + ReprProj::Stream { inner } => return inner.poll_next(cx), + } + }, + size_hint: |_| (1, Some(1)), + resolved: |f| matches!(f, Repr::Stream { .. }), + as_value: |v| { + DynOutput::new_serialize( + v.as_mut() + // Attempted to access value when `Poll::Ready(None)` was not returned. + .expect("unreachable"), + ) + }, + flushed: false, + unwound: false, + value: None::, + })), + flush: None, + pending_value: false, + } } /// TODO @@ -111,19 +209,116 @@ impl ProcedureStream { S: Stream> + Send + 'static, T: Send + Sync + 'static, { - todo!(); + Self { + inner: Inner::Dyn(Box::pin(GenericDynReturnValue { + inner: s, + poll: |s, cx| s.poll_next(cx), + size_hint: |s| s.size_hint(), + resolved: |_| true, + // We passthrough the whole `Option` intentionally. + as_value: |v| DynOutput::new_value(v), + flushed: false, + unwound: false, + value: None, + })), + flush: None, + pending_value: false, + } } - // TODO: `fn from_future_value` + /// TODO + pub fn from_future_value(f: F) -> Self + where + F: Future> + Send + 'static, + T: Send + Sync + 'static, + { + pin_project! { + #[project = ReprProj] + struct Repr { + #[pin] + inner: Option, + } + } + + Self { + inner: Inner::Dyn(Box::pin(GenericDynReturnValue { + inner: Repr { inner: Some(f) }, + poll: |f, cx| { + let mut this = f.project(); + let v = match this.inner.as_mut().as_pin_mut() { + Some(fut) => ready!(fut.poll(cx)), + None => return Poll::Ready(None), + }; + + this.inner.set(None); + Poll::Ready(Some(v)) + }, + size_hint: |f| { + if f.inner.is_some() { + (1, Some(1)) + } else { + (0, Some(0)) + } + }, + as_value: |v| DynOutput::new_value(v), + resolved: |f| f.inner.is_none(), + flushed: false, + unwound: false, + value: None, + })), + flush: None, + pending_value: false, + } + } /// TODO pub fn from_future_stream_value(f: F) -> Self where F: Future> + Send + 'static, S: Stream> + Send + 'static, - T: Serialize + Send + Sync + 'static, + T: Send + Sync + 'static, { - todo!(); + pin_project! { + #[project = ReprProj] + enum Repr { + Future { + #[pin] + inner: F, + }, + Stream { + #[pin] + inner: S, + }, + } + } + + Self { + inner: Inner::Dyn(Box::pin(GenericDynReturnValue { + inner: Repr::::Future { inner: f }, + poll: |mut f, cx| loop { + let this = f.as_mut().project(); + match this { + ReprProj::Future { inner } => { + let Poll::Ready(Ok(stream)) = inner.poll(cx) else { + return Poll::Pending; + }; + + f.set(Repr::Stream { inner: stream }); + continue; + } + ReprProj::Stream { inner } => return inner.poll_next(cx), + } + }, + size_hint: |_| (1, Some(1)), + resolved: |f| matches!(f, Repr::Stream { .. }), + as_value: |v| DynOutput::new_value(v), + flushed: false, + unwound: false, + value: None::, + })), + flush: None, + pending_value: false, + } } /// By setting this the stream will delay returning any data until instructed by the caller (via `Self::stream`). @@ -144,6 +339,12 @@ impl ProcedureStream { /// Once this is met you can call `Self::stream` on all of the streams at once to begin streaming data. /// pub fn require_manual_stream(mut self) -> Self { + // TODO: When stablised replace with - https://doc.rust-lang.org/stable/std/task/struct.Waker.html#method.noop + struct NoOpWaker; + impl std::task::Wake for NoOpWaker { + fn wake(self: std::sync::Arc) {} + } + // This `Arc` is inefficient but `Waker::noop` is coming soon which will solve it. self.flush = Some(Arc::new(NoOpWaker).into()); self @@ -161,14 +362,20 @@ impl ProcedureStream { /// /// For a stream created via `Self::from_future*` this will be `true` once the future has resolved and for all other streams this will always be `true`. pub fn resolved(&self) -> bool { - true // TODO + match &self.inner { + Inner::Dyn(stream) => stream.resolved(), + Inner::Value(_) => true, + } } /// Will return `true` if the stream is ready to start streaming data. /// /// This is `false` until the `flush` function is called by the user. pub fn flushable(&self) -> bool { - false // TODO + match &self.inner { + Inner::Dyn(stream) => stream.flushed(), + Inner::Value(_) => false, + } } /// TODO @@ -224,29 +431,34 @@ impl ProcedureStream { pub fn poll_next( &mut self, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll, ProcedureError>>> { self.poll_inner(cx).map(|v| { v.map(|v| { v.map(|_: ()| { let Inner::Dyn(s) = &mut self.inner else { - unreachable!(); + unreachable!(); // TODO: Handle this? }; - s.value() + s.as_mut().value() }) }) }) } /// TODO - pub async fn next( - &mut self, - ) -> Option> { - poll_fn(move |cx| self.poll_inner(cx)).await + pub async fn next(&mut self) -> Option, ProcedureError>> { + poll_fn(|cx| self.poll_inner(cx)).await.map(|v| { + v.map(|_: ()| { + let Inner::Dyn(s) = &mut self.inner else { + unreachable!(); // TODO: Handle this? + }; + s.as_mut().value() + }) + }) } /// TODO // TODO: Should error be `String` type? - pub fn map Result + Unpin, T>( + pub fn map Result + Unpin, T>( self, map: F, ) -> ProcedureStreamMap { @@ -254,12 +466,12 @@ impl ProcedureStream { } } -pub struct ProcedureStreamMap Result + Unpin, T> { +pub struct ProcedureStreamMap Result + Unpin, T> { stream: ProcedureStream, map: F, } -impl Result + Unpin, T> ProcedureStreamMap { +impl Result + Unpin, T> ProcedureStreamMap { /// Start streaming data. /// Refer to `Self::require_manual_stream` for more information. pub fn stream(&mut self) { @@ -281,9 +493,7 @@ impl Result + Unpin, T> ProcedureSt } } -impl Result + Unpin, T> Stream - for ProcedureStreamMap -{ +impl Result + Unpin, T> Stream for ProcedureStreamMap { type Item = T; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { @@ -297,7 +507,7 @@ impl Result + Unpin, T> Stream unreachable!(); }; - match (this.map)(ProcedureStreamValue(s.value())) { + match (this.map)(s.as_mut().value()) { Ok(v) => v, // TODO: Exposing this error to the client or not? // TODO: Error type??? @@ -316,19 +526,6 @@ impl Result + Unpin, T> Stream } } -// TODO: name -pub struct ProcedureStreamValue<'a>(&'a (dyn erased_serde::Serialize + Send + Sync)); -// TODO: `Debug`, etc traits - -impl<'a> Serialize for ProcedureStreamValue<'a> { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.0.serialize(serializer) - } -} - impl fmt::Debug for ProcedureStream { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!(); @@ -340,28 +537,35 @@ trait DynReturnValue: Send { self: Pin<&'a mut Self>, cx: &mut Context<'_>, ) -> Poll>>; - - fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync); - + fn value(self: Pin<&mut Self>) -> DynOutput<'_>; fn size_hint(&self) -> (usize, Option); - - fn ready_for_flush(&self) -> bool; + fn resolved(&self) -> bool; + fn flushed(&self) -> bool; } pin_project! { - struct DynReturnImpl{ + struct GenericDynReturnValue { #[pin] - src: S, + inner: S, + // `Stream::poll` + poll: fn(Pin<&mut S>, &mut Context) -> Poll>>, + // `Stream::size_hint` + size_hint: fn(&S) -> (usize, Option), + // convert the current value to a `DynOutput` + as_value: fn(&mut Option) -> DynOutput<'_>, + // detect when the stream has finished it's future if it has one. + resolved: fn(&S) -> bool, + // has the user called `flushed` within it? + flushed: bool, + // has the user panicked? unwound: bool, + // the last yielded value. We place it here for more efficient serialization. + // it also makes `ProcedureStream::require_manual_stream` possible. value: Option, } } -impl> + Send + 'static> DynReturnValue - for DynReturnImpl -where - T: Send + Sync + Serialize, -{ +impl DynReturnValue for GenericDynReturnValue { fn poll_next_value<'a>( mut self: Pin<&'a mut Self>, cx: &mut Context<'_>, @@ -374,7 +578,7 @@ where let this = self.as_mut().project(); let r = catch_unwind(AssertUnwindSafe(|| { let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. - this.src.poll_next(cx).map(|v| { + (this.poll)(this.inner, cx).map(|v| { v.map(|v| { v.map(|v| { *this.value = Some(v); @@ -393,22 +597,18 @@ where } } - fn value(&self) -> &(dyn erased_serde::Serialize + Send + Sync) { - // Attempted to access value when `Poll::Ready(None)` was not returned. - self.value.as_ref().expect("unreachable") + fn value(self: Pin<&mut Self>) -> DynOutput<'_> { + (self.as_value)(self.project().value) } fn size_hint(&self) -> (usize, Option) { - self.src.size_hint() + (self.size_hint)(&self.inner) } - fn ready_for_flush(&self) -> bool { - todo!(); + fn resolved(&self) -> bool { + (self.resolved)(&self.inner) + } + fn flushed(&self) -> bool { + self.flushed } -} - -// TODO: When stablised replace with - https://doc.rust-lang.org/stable/std/task/struct.Waker.html#method.noop -struct NoOpWaker; -impl std::task::Wake for NoOpWaker { - fn wake(self: std::sync::Arc) {} } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index f7b9f957..e91793f0 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -7,7 +7,7 @@ use axum::{ }; use example_core::{create_router, Ctx}; use futures::{stream::FuturesUnordered, Stream, StreamExt}; -use rspc::{ProcedureStream, ProcedureStreamMap, ProcedureStreamValue, Procedures, State}; +use rspc::{DynOutput, ProcedureStream, ProcedureStreamMap, Procedures, State}; use rspc_invalidation::Invalidator; use serde_json::{de::SliceRead, value::RawValue, Value}; use std::{ @@ -207,12 +207,13 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { let mut runtime = StreamUnordered::new(); // TODO: Move onto `Prototype`??? let spawn = |runtime: &mut StreamUnordered<_>, p: ProcedureStream| { - runtime.insert(p.require_manual_stream().map:: Result, String>, Vec>( - |v| serde_json::to_vec(&v).map_err(|err| err.to_string()), - )); + runtime.insert( + p.require_manual_stream() + .map:: Result, String>, Vec>(|v| { + serde_json::to_vec(&v.as_serialize().unwrap()) + .map_err(|err| err.to_string()) + }), + ); }; // TODO: If a file was being uploaded this would require reading the whole body until the `runtime` is polled. @@ -279,9 +280,7 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { // TODO: This abstraction is soooo bad. pub struct Prototype { - runtime: StreamUnordered< - ProcedureStreamMap Result, String>, Vec>, - >, + runtime: StreamUnordered Result, String>, Vec>>, invalidator: Invalidator, ctx: TCtx, sfm: bool, @@ -312,9 +311,10 @@ impl Stream for Prototype { rspc_invalidation::queue(&self.invalidator, self.ctx.clone(), &self.procedures) { self.runtime.insert( - stream.map:: Result, String>, Vec>( - |v| serde_json::to_vec(&v).map_err(|err| err.to_string()), - ), + stream.map:: Result, String>, Vec>(|v| { + serde_json::to_vec(&v.as_serialize().unwrap()) + .map_err(|err| err.to_string()) + }), ); } } diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index be590b73..989bcbfd 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -357,7 +357,9 @@ async fn next( // ProcedureError::Serializer(err) => panic!("{err:?}"), }) .and_then(|v| { - Ok(v.serialize(serde_json::value::Serializer) + Ok(v.as_serialize() + .unwrap() + .serialize(serde_json::value::Serializer) .expect("Error serialzing value")) // This panicking is bad but this is the old exectuor }) }) diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index 5c7bf810..75ef1bf9 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -45,8 +45,8 @@ pub use modern::{ pub use procedure::Procedure2; pub use rspc_core::{ - DeserializeError, DowncastError, DynInput, Procedure, ProcedureError, ProcedureStream, - ProcedureStreamMap, ProcedureStreamValue, Procedures, ResolverError, State, + flush, DeserializeError, DowncastError, DynInput, DynOutput, Procedure, ProcedureError, + ProcedureStream, ProcedureStreamMap, Procedures, ResolverError, State, }; // Legacy stuff From f485eea0be10db9472bf303ce5649e3255622389 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 25 Dec 2024 20:39:42 +0800 Subject: [PATCH 56/67] upgrade dependencies --- client/Cargo.toml | 4 ++-- examples/Cargo.toml | 10 +++++----- examples/axum/Cargo.toml | 4 ++-- examples/core/Cargo.toml | 4 ++-- integrations/tauri/Cargo.toml | 2 +- middleware/devtools/Cargo.toml | 2 +- middleware/invalidation/Cargo.toml | 4 ++-- middleware/openapi/Cargo.toml | 6 +++--- rspc/Cargo.toml | 4 ++-- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index c3262314..ee16d232 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -19,5 +19,5 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] reqwest = { version = "0.12.9", features = ["json"] } rspc-core = { version = "0.0.1", path = "../core" } -serde = { version = "1.0.215", features = ["derive"] } # TODO: Drop derive feature? -serde_json = "1.0.133" +serde = { version = "1.0.216", features = ["derive"] } # TODO: Drop derive feature? +serde_json = "1.0.134" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f9e973a8..495c054c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,10 +10,10 @@ specta = "=2.0.0-rc.20" rspc-axum = { path = "../integrations/axum" } async-stream = "0.3.6" axum = "0.7.9" -chrono = { version = "0.4.38", features = ["serde"] } -serde = { version = "1.0.215", features = ["derive"] } -time = "0.3.36" -tokio = { version = "1.41.1", features = [ +chrono = { version = "0.4.39", features = ["serde"] } +serde = { version = "1.0.216", features = ["derive"] } +time = "0.3.37" +tokio = { version = "1.42.0", features = [ "rt-multi-thread", "macros", "time", @@ -24,4 +24,4 @@ tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } uuid = { version = "1.11.0", features = ["v4", "serde"] } -serde_json = "1.0.133" +serde_json = "1.0.134" diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index da5a1a9c..34e71c58 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -8,7 +8,7 @@ publish = false rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } example-core = { path = "../core" } -tokio = { version = "1.41.1", features = ["full"] } +tokio = { version = "1.42.0", features = ["full"] } axum = { version = "0.7.9", features = ["multipart"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", @@ -17,6 +17,6 @@ rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } futures = "0.3" # TODO -serde_json = "1.0.133" +serde_json = "1.0.134" rspc-http = { version = "0.2.1", path = "../../integrations/http" } streamunordered = "0.5.4" diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index 77ec1a99..3407b006 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -7,11 +7,11 @@ publish = false [dependencies] rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } async-stream = "0.3.6" -serde = { version = "1.0.215", features = ["derive"] } +serde = { version = "1.0.216", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = [ "derive", ] } -thiserror = "2.0.4" +thiserror = "2.0.9" rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } tracing = "0.1.41" futures = "0.3.31" diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index 33ad0cea..ab9d6588 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -30,4 +30,4 @@ serde_json = { version = "1", features = [ workspace = true [build-dependencies] -tauri-plugin = { version = "2.0.0", features = ["build"] } +tauri-plugin = { version = "2.0.3", features = ["build"] } diff --git a/middleware/devtools/Cargo.toml b/middleware/devtools/Cargo.toml index b8d7cd15..4680f3da 100644 --- a/middleware/devtools/Cargo.toml +++ b/middleware/devtools/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] futures = "0.3.31" rspc-core = { path = "../../core" } -serde = { version = "1.0.215", features = ["derive"] } +serde = { version = "1.0.216", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = ["derive"] } tracing = "0.1.41" diff --git a/middleware/invalidation/Cargo.toml b/middleware/invalidation/Cargo.toml index 72ed39d1..3fadf786 100644 --- a/middleware/invalidation/Cargo.toml +++ b/middleware/invalidation/Cargo.toml @@ -5,10 +5,10 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -async-stream = "0.3.5" +async-stream = "0.3.6" rspc = { path = "../../rspc", features = ["unstable"] } serde = "1.0.216" -serde_json = "1.0.133" +serde_json = "1.0.134" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/middleware/openapi/Cargo.toml b/middleware/openapi/Cargo.toml index 95bb9836..0a3d56c9 100644 --- a/middleware/openapi/Cargo.toml +++ b/middleware/openapi/Cargo.toml @@ -6,9 +6,9 @@ publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc", features = ["unstable"] } -axum = { version = "0.7.5", default-features = false } -serde_json = "1.0.127" -futures = "0.3.30" +axum = { version = "0.7.9", default-features = false } +serde_json = "1.0.134" +futures = "0.3.31" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml index 2ab4db8a..49d1f350 100644 --- a/rspc/Cargo.toml +++ b/rspc/Cargo.toml @@ -42,8 +42,8 @@ specta-typescript = { version = "=0.0.7", features = [] } # TODO: Make optional specta-rust = { git = "https://github.com/specta-rs/specta", optional = true, rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } # Temporary # TODO: Remove -serde_json = "1.0.133" -thiserror = "2.0.3" +serde_json = "1.0.134" +thiserror = "2.0.9" [lints] workspace = true From 86cf6e00026c6a2e0c30bf9963244859537120bc Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 25 Dec 2024 20:58:42 +0800 Subject: [PATCH 57/67] @Brendonovich decreed we have `crates/` --- Cargo.toml | 4 +-- {middleware => crates}/README.md | 2 +- {middleware => crates}/binario/Cargo.toml | 0 {middleware => crates}/binario/README.md | 0 {middleware => crates}/binario/src/lib.rs | 0 {middleware => crates}/cache/Cargo.toml | 0 {middleware => crates}/cache/README.md | 0 {middleware => crates}/cache/src/lib.rs | 0 {middleware => crates}/cache/src/memory.rs | 0 {middleware => crates}/cache/src/state.rs | 0 {middleware => crates}/cache/src/store.rs | 0 {client => crates/client}/Cargo.toml | 2 +- {client => crates/client}/src/lib.rs | 2 +- {core => crates/core}/Cargo.toml | 4 +-- crates/core/src/lib.rs | 9 ++++++ {middleware => crates}/devtools/Cargo.toml | 2 +- {middleware => crates}/devtools/README.md | 0 {middleware => crates}/devtools/src/lib.rs | 2 +- .../devtools/src/tracing.rs | 0 {middleware => crates}/devtools/src/types.rs | 0 .../invalidation/Cargo.toml | 0 {middleware => crates}/invalidation/README.md | 0 .../invalidation/src/lib.rs | 0 {middleware => crates}/openapi/Cargo.toml | 0 {middleware => crates}/openapi/README.md | 0 {middleware => crates}/openapi/src/lib.rs | 0 .../openapi/src/swagger.html | 0 crates/procedure/Cargo.toml | 30 +++++++++++++++++++ {core => crates/procedure}/src/dyn_input.rs | 0 {core => crates/procedure}/src/dyn_output.rs | 0 {core => crates/procedure}/src/error.rs | 0 {core => crates/procedure}/src/interop.rs | 0 {core => crates/procedure}/src/lib.rs | 4 ++- {core => crates/procedure}/src/logger.rs | 0 {core => crates/procedure}/src/procedure.rs | 0 {core => crates/procedure}/src/procedures.rs | 0 {core => crates/procedure}/src/state.rs | 0 {core => crates/procedure}/src/stream.rs | 0 {middleware => crates}/tracing/Cargo.toml | 0 {middleware => crates}/tracing/README.md | 0 {middleware => crates}/tracing/src/lib.rs | 0 .../tracing/src/traceable.rs | 0 {middleware => crates}/validator/Cargo.toml | 0 {middleware => crates}/validator/README.md | 0 {middleware => crates}/validator/src/lib.rs | 0 examples/axum/Cargo.toml | 4 +-- examples/client/Cargo.toml | 2 +- examples/core/Cargo.toml | 6 ++-- integrations/actix-web/Cargo.toml | 2 +- integrations/actix-web/src/lib.rs | 2 +- integrations/axum/Cargo.toml | 4 +-- integrations/axum/src/endpoint.rs | 4 +-- integrations/axum/src/jsonrpc_exec.rs | 4 +-- integrations/axum/src/request.rs | 4 +-- integrations/axum/src/v2.rs | 2 +- integrations/http/Cargo.toml | 2 +- integrations/http/src/execute.rs | 4 +-- integrations/tauri/Cargo.toml | 2 +- integrations/tauri/src/lib.rs | 2 +- rspc/Cargo.toml | 2 +- rspc/src/legacy/interop.rs | 10 +++---- rspc/src/lib.rs | 4 +-- rspc/src/modern/error.rs | 2 +- rspc/src/modern/extension.rs | 2 +- rspc/src/modern/infallible.rs | 2 +- rspc/src/modern/middleware/middleware.rs | 2 +- rspc/src/modern/procedure/builder.rs | 2 +- rspc/src/modern/procedure/erased.rs | 4 +-- rspc/src/modern/procedure/resolver_input.rs | 6 ++-- rspc/src/modern/procedure/resolver_output.rs | 4 +-- rspc/src/procedure.rs | 2 +- rspc/src/router.rs | 2 +- rspc/tests/router.rs | 2 +- 73 files changed, 95 insertions(+), 56 deletions(-) rename {middleware => crates}/README.md (78%) rename {middleware => crates}/binario/Cargo.toml (100%) rename {middleware => crates}/binario/README.md (100%) rename {middleware => crates}/binario/src/lib.rs (100%) rename {middleware => crates}/cache/Cargo.toml (100%) rename {middleware => crates}/cache/README.md (100%) rename {middleware => crates}/cache/src/lib.rs (100%) rename {middleware => crates}/cache/src/memory.rs (100%) rename {middleware => crates}/cache/src/state.rs (100%) rename {middleware => crates}/cache/src/store.rs (100%) rename {client => crates/client}/Cargo.toml (92%) rename {client => crates/client}/src/lib.rs (98%) rename {core => crates/core}/Cargo.toml (89%) create mode 100644 crates/core/src/lib.rs rename {middleware => crates}/devtools/Cargo.toml (89%) rename {middleware => crates}/devtools/README.md (100%) rename {middleware => crates}/devtools/src/lib.rs (97%) rename {middleware => crates}/devtools/src/tracing.rs (100%) rename {middleware => crates}/devtools/src/types.rs (100%) rename {middleware => crates}/invalidation/Cargo.toml (100%) rename {middleware => crates}/invalidation/README.md (100%) rename {middleware => crates}/invalidation/src/lib.rs (100%) rename {middleware => crates}/openapi/Cargo.toml (100%) rename {middleware => crates}/openapi/README.md (100%) rename {middleware => crates}/openapi/src/lib.rs (100%) rename {middleware => crates}/openapi/src/swagger.html (100%) create mode 100644 crates/procedure/Cargo.toml rename {core => crates/procedure}/src/dyn_input.rs (100%) rename {core => crates/procedure}/src/dyn_output.rs (100%) rename {core => crates/procedure}/src/error.rs (100%) rename {core => crates/procedure}/src/interop.rs (100%) rename {core => crates/procedure}/src/lib.rs (86%) rename {core => crates/procedure}/src/logger.rs (100%) rename {core => crates/procedure}/src/procedure.rs (100%) rename {core => crates/procedure}/src/procedures.rs (100%) rename {core => crates/procedure}/src/state.rs (100%) rename {core => crates/procedure}/src/stream.rs (100%) rename {middleware => crates}/tracing/Cargo.toml (100%) rename {middleware => crates}/tracing/README.md (100%) rename {middleware => crates}/tracing/src/lib.rs (100%) rename {middleware => crates}/tracing/src/traceable.rs (100%) rename {middleware => crates}/validator/Cargo.toml (100%) rename {middleware => crates}/validator/README.md (100%) rename {middleware => crates}/validator/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 68f751f6..877ad0e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,9 @@ [workspace] resolver = "2" members = [ + "./crates/*", "./rspc", - "./core", - "./client", "./integrations/*", - "./middleware/*", "./examples", "./examples/core", "./examples/axum", diff --git a/middleware/README.md b/crates/README.md similarity index 78% rename from middleware/README.md rename to crates/README.md index da43f66d..3d9d9671 100644 --- a/middleware/README.md +++ b/crates/README.md @@ -1,4 +1,4 @@ -# Official rspc Middleware +# Crates > [!CAUTION] > These are not yet stable so use at your own risk. diff --git a/middleware/binario/Cargo.toml b/crates/binario/Cargo.toml similarity index 100% rename from middleware/binario/Cargo.toml rename to crates/binario/Cargo.toml diff --git a/middleware/binario/README.md b/crates/binario/README.md similarity index 100% rename from middleware/binario/README.md rename to crates/binario/README.md diff --git a/middleware/binario/src/lib.rs b/crates/binario/src/lib.rs similarity index 100% rename from middleware/binario/src/lib.rs rename to crates/binario/src/lib.rs diff --git a/middleware/cache/Cargo.toml b/crates/cache/Cargo.toml similarity index 100% rename from middleware/cache/Cargo.toml rename to crates/cache/Cargo.toml diff --git a/middleware/cache/README.md b/crates/cache/README.md similarity index 100% rename from middleware/cache/README.md rename to crates/cache/README.md diff --git a/middleware/cache/src/lib.rs b/crates/cache/src/lib.rs similarity index 100% rename from middleware/cache/src/lib.rs rename to crates/cache/src/lib.rs diff --git a/middleware/cache/src/memory.rs b/crates/cache/src/memory.rs similarity index 100% rename from middleware/cache/src/memory.rs rename to crates/cache/src/memory.rs diff --git a/middleware/cache/src/state.rs b/crates/cache/src/state.rs similarity index 100% rename from middleware/cache/src/state.rs rename to crates/cache/src/state.rs diff --git a/middleware/cache/src/store.rs b/crates/cache/src/store.rs similarity index 100% rename from middleware/cache/src/store.rs rename to crates/cache/src/store.rs diff --git a/client/Cargo.toml b/crates/client/Cargo.toml similarity index 92% rename from client/Cargo.toml rename to crates/client/Cargo.toml index ee16d232..481398ce 100644 --- a/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -18,6 +18,6 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] reqwest = { version = "0.12.9", features = ["json"] } -rspc-core = { version = "0.0.1", path = "../core" } +rspc-procedure = { version = "0.0.1", path = "../procedure" } serde = { version = "1.0.216", features = ["derive"] } # TODO: Drop derive feature? serde_json = "1.0.134" diff --git a/client/src/lib.rs b/crates/client/src/lib.rs similarity index 98% rename from client/src/lib.rs rename to crates/client/src/lib.rs index eb55972a..a109b8d0 100644 --- a/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,4 +1,4 @@ -//! rspc-client: Rust client for [rspc](https://docs.rs/rspc). +//! Rust client for [`rspc`]. //! //! # This is really unstable you should be careful using it! #![forbid(unsafe_code)] diff --git a/core/Cargo.toml b/crates/core/Cargo.toml similarity index 89% rename from core/Cargo.toml rename to crates/core/Cargo.toml index e0477f18..d52a98c7 100644 --- a/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "rspc-core" -description = "Core interface for rspc" +description = "Core types and traits for rspc" version = "0.0.1" authors = ["Oscar Beaumont "] edition = "2021" license = "MIT" repository = "https://github.com/specta-rs/rspc" -documentation = "https://docs.rs/rspc-core" +documentation = "https://docs.rs/rspc-procedure" keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] categories = ["web-programming", "asynchronous"] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..e6e64ddf --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,9 @@ +//! Core types and traits for [`rspc`]. +//! +//! Middleware and extension authors should prefer to depend on this crate instead of `rspc`. +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", + html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" +)] diff --git a/middleware/devtools/Cargo.toml b/crates/devtools/Cargo.toml similarity index 89% rename from middleware/devtools/Cargo.toml rename to crates/devtools/Cargo.toml index 4680f3da..53b840f3 100644 --- a/middleware/devtools/Cargo.toml +++ b/crates/devtools/Cargo.toml @@ -6,7 +6,7 @@ publish = false [dependencies] futures = "0.3.31" -rspc-core = { path = "../../core" } +rspc-procedure = { path = "../../crates/procedure" } serde = { version = "1.0.216", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = ["derive"] } tracing = "0.1.41" diff --git a/middleware/devtools/README.md b/crates/devtools/README.md similarity index 100% rename from middleware/devtools/README.md rename to crates/devtools/README.md diff --git a/middleware/devtools/src/lib.rs b/crates/devtools/src/lib.rs similarity index 97% rename from middleware/devtools/src/lib.rs rename to crates/devtools/src/lib.rs index 4a67c82c..f3ca3b13 100644 --- a/middleware/devtools/src/lib.rs +++ b/crates/devtools/src/lib.rs @@ -19,7 +19,7 @@ use std::{ }; use futures::stream; -use rspc_core::{Procedure, ProcedureStream, Procedures}; +use rspc_procedure::{Procedure, ProcedureStream, Procedures}; use types::{Metadata, ProcedureMetadata}; pub fn mount( diff --git a/middleware/devtools/src/tracing.rs b/crates/devtools/src/tracing.rs similarity index 100% rename from middleware/devtools/src/tracing.rs rename to crates/devtools/src/tracing.rs diff --git a/middleware/devtools/src/types.rs b/crates/devtools/src/types.rs similarity index 100% rename from middleware/devtools/src/types.rs rename to crates/devtools/src/types.rs diff --git a/middleware/invalidation/Cargo.toml b/crates/invalidation/Cargo.toml similarity index 100% rename from middleware/invalidation/Cargo.toml rename to crates/invalidation/Cargo.toml diff --git a/middleware/invalidation/README.md b/crates/invalidation/README.md similarity index 100% rename from middleware/invalidation/README.md rename to crates/invalidation/README.md diff --git a/middleware/invalidation/src/lib.rs b/crates/invalidation/src/lib.rs similarity index 100% rename from middleware/invalidation/src/lib.rs rename to crates/invalidation/src/lib.rs diff --git a/middleware/openapi/Cargo.toml b/crates/openapi/Cargo.toml similarity index 100% rename from middleware/openapi/Cargo.toml rename to crates/openapi/Cargo.toml diff --git a/middleware/openapi/README.md b/crates/openapi/README.md similarity index 100% rename from middleware/openapi/README.md rename to crates/openapi/README.md diff --git a/middleware/openapi/src/lib.rs b/crates/openapi/src/lib.rs similarity index 100% rename from middleware/openapi/src/lib.rs rename to crates/openapi/src/lib.rs diff --git a/middleware/openapi/src/swagger.html b/crates/openapi/src/swagger.html similarity index 100% rename from middleware/openapi/src/swagger.html rename to crates/openapi/src/swagger.html diff --git a/crates/procedure/Cargo.toml b/crates/procedure/Cargo.toml new file mode 100644 index 00000000..5144745a --- /dev/null +++ b/crates/procedure/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "rspc-procedure" +description = "Interface for a single type-erased operation that the server can execute" +version = "0.0.1" +authors = ["Oscar Beaumont "] +edition = "2021" +license = "MIT" +repository = "https://github.com/specta-rs/rspc" +documentation = "https://docs.rs/rspc-procedure" +keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] +categories = ["web-programming", "asynchronous"] + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +# Public +futures-core = { version = "0.3", default-features = false } +serde = { version = "1", default-features = false } + +# Private +erased-serde = { version = "0.4", default-features = false, features = [ + "std", +] } +pin-project-lite = { version = "0.2", default-features = false } + +[lints] +workspace = true diff --git a/core/src/dyn_input.rs b/crates/procedure/src/dyn_input.rs similarity index 100% rename from core/src/dyn_input.rs rename to crates/procedure/src/dyn_input.rs diff --git a/core/src/dyn_output.rs b/crates/procedure/src/dyn_output.rs similarity index 100% rename from core/src/dyn_output.rs rename to crates/procedure/src/dyn_output.rs diff --git a/core/src/error.rs b/crates/procedure/src/error.rs similarity index 100% rename from core/src/error.rs rename to crates/procedure/src/error.rs diff --git a/core/src/interop.rs b/crates/procedure/src/interop.rs similarity index 100% rename from core/src/interop.rs rename to crates/procedure/src/interop.rs diff --git a/core/src/lib.rs b/crates/procedure/src/lib.rs similarity index 86% rename from core/src/lib.rs rename to crates/procedure/src/lib.rs index e2a2f427..d717cf69 100644 --- a/core/src/lib.rs +++ b/crates/procedure/src/lib.rs @@ -1,4 +1,6 @@ -//! rspc-core: Core interface for [rspc](https://docs.rs/rspc). +//! Interface for a single type-erased operation that the server can execute. +//! +//! HTTP integrations should prefer to depend on this crate instead of `rspc`. //! //! TODO: Describe all the types and why the split? //! TODO: This is kinda like `tower::Service` diff --git a/core/src/logger.rs b/crates/procedure/src/logger.rs similarity index 100% rename from core/src/logger.rs rename to crates/procedure/src/logger.rs diff --git a/core/src/procedure.rs b/crates/procedure/src/procedure.rs similarity index 100% rename from core/src/procedure.rs rename to crates/procedure/src/procedure.rs diff --git a/core/src/procedures.rs b/crates/procedure/src/procedures.rs similarity index 100% rename from core/src/procedures.rs rename to crates/procedure/src/procedures.rs diff --git a/core/src/state.rs b/crates/procedure/src/state.rs similarity index 100% rename from core/src/state.rs rename to crates/procedure/src/state.rs diff --git a/core/src/stream.rs b/crates/procedure/src/stream.rs similarity index 100% rename from core/src/stream.rs rename to crates/procedure/src/stream.rs diff --git a/middleware/tracing/Cargo.toml b/crates/tracing/Cargo.toml similarity index 100% rename from middleware/tracing/Cargo.toml rename to crates/tracing/Cargo.toml diff --git a/middleware/tracing/README.md b/crates/tracing/README.md similarity index 100% rename from middleware/tracing/README.md rename to crates/tracing/README.md diff --git a/middleware/tracing/src/lib.rs b/crates/tracing/src/lib.rs similarity index 100% rename from middleware/tracing/src/lib.rs rename to crates/tracing/src/lib.rs diff --git a/middleware/tracing/src/traceable.rs b/crates/tracing/src/traceable.rs similarity index 100% rename from middleware/tracing/src/traceable.rs rename to crates/tracing/src/traceable.rs diff --git a/middleware/validator/Cargo.toml b/crates/validator/Cargo.toml similarity index 100% rename from middleware/validator/Cargo.toml rename to crates/validator/Cargo.toml diff --git a/middleware/validator/README.md b/crates/validator/README.md similarity index 100% rename from middleware/validator/README.md rename to crates/validator/README.md diff --git a/middleware/validator/src/lib.rs b/crates/validator/src/lib.rs similarity index 100% rename from middleware/validator/src/lib.rs rename to crates/validator/src/lib.rs diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 34e71c58..8ed9c250 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -13,8 +13,8 @@ axum = { version = "0.7.9", features = ["multipart"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } -rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } -rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } +rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } +rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } futures = "0.3" # TODO serde_json = "1.0.134" diff --git a/examples/client/Cargo.toml b/examples/client/Cargo.toml index 5877a6d2..1356633d 100644 --- a/examples/client/Cargo.toml +++ b/examples/client/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" publish = false [dependencies] -rspc-client = { path = "../../client" } +rspc-client = { path = "../../crates/client" } tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index 3407b006..a9bac707 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -12,8 +12,8 @@ specta = { version = "=2.0.0-rc.20", features = [ "derive", ] } thiserror = "2.0.9" -rspc-devtools = { version = "0.0.0", path = "../../middleware/devtools" } +rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } tracing = "0.1.41" futures = "0.3.31" -rspc-cache = { version = "0.0.0", path = "../../middleware/cache" } -rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } +rspc-cache = { version = "0.0.0", path = "../../crates/cache" } +rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index ff3f4dde..cd3a08f8 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -20,7 +20,7 @@ default = [] # ws = ["axum/ws"] [dependencies] -rspc-core = { version = "0.0.1", path = "../../core" } +rspc-procedure = { version = "0.0.1", path = "../../crates/procedure" } rspc-http = { path = "../http" } actix-web = "4" actix-ws = "0.3" diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index 62f1bcf3..26694703 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -17,7 +17,7 @@ // use actix_ws::Message; // use futures_util::StreamExt; -// use rspc_core::Procedures; +// use rspc_procedure::Procedures; // pub struct Endpoint { // procedures: Procedures, diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 7238ebca..6c2a1f78 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -20,7 +20,7 @@ default = [] ws = ["axum/ws"] [dependencies] -rspc-core = { version = "0.0.1", path = "../../core" } +rspc-procedure = { version = "0.0.1", path = "../../crates/procedure" } rspc-http = { version = "0.2.1", path = "../http" } axum = { version = "0.7.9", features = ["ws", "json"] } serde_json = "1" @@ -32,7 +32,7 @@ tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio: serde = { version = "1", features = ["derive"] } # TODO: Remove features serde_urlencoded = "0.7.1" mime = "0.3.17" -rspc-invalidation = { version = "0.0.0", path = "../../middleware/invalidation" } +rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } [lints] workspace = true diff --git a/integrations/axum/src/endpoint.rs b/integrations/axum/src/endpoint.rs index 817d29c9..a06528ac 100644 --- a/integrations/axum/src/endpoint.rs +++ b/integrations/axum/src/endpoint.rs @@ -17,7 +17,7 @@ // routing::{on, MethodFilter}, // }; // use futures::{stream::once, Stream, StreamExt, TryStreamExt}; -// use rspc_core::{ProcedureError, ProcedureStream, Procedures}; +// use rspc_procedure::{ProcedureError, ProcedureStream, Procedures}; // use rspc_http::ExecuteInput; // /// Construct a new [`axum::Router`](axum::Router) to expose a given [`rspc::Router`](rspc::Router). @@ -156,7 +156,7 @@ // } else { // // TODO: bring this back // // if !json_content_type(req.headers()) { -// // let err: ProcedureError = rspc_core::DeserializeError::custom( +// // let err: ProcedureError = rspc_procedure::DeserializeError::custom( // // "Client did not set correct valid 'Content-Type' header", // // ) // // .into(); diff --git a/integrations/axum/src/jsonrpc_exec.rs b/integrations/axum/src/jsonrpc_exec.rs index 989bcbfd..ef304f39 100644 --- a/integrations/axum/src/jsonrpc_exec.rs +++ b/integrations/axum/src/jsonrpc_exec.rs @@ -4,7 +4,7 @@ use std::{ future::{poll_fn, Future}, }; -use rspc_core::{ProcedureError, ProcedureStream, Procedures}; +use rspc_procedure::{ProcedureError, ProcedureStream, Procedures}; use serde::Serialize; use serde_json::Value; use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; @@ -335,7 +335,7 @@ async fn next( ProcedureError::Resolver(resolver_err) => { let legacy_error = resolver_err .error() - .and_then(|v| v.downcast_ref::()) + .and_then(|v| v.downcast_ref::()) .cloned(); jsonrpc::JsonRPCError { diff --git a/integrations/axum/src/request.rs b/integrations/axum/src/request.rs index ed60ea38..6b462c62 100644 --- a/integrations/axum/src/request.rs +++ b/integrations/axum/src/request.rs @@ -5,7 +5,7 @@ use axum::{ body::HttpBody, extract::{FromRequest, Request}, }; -use rspc_core::{Procedure, ProcedureStream}; +use rspc_procedure::{Procedure, ProcedureStream}; use serde::Deserializer; // TODO: rename? @@ -26,7 +26,7 @@ impl AxumRequest { // } else { // // TODO: bring this back // // if !json_content_type(req.headers()) { - // // let err: ProcedureError = rspc_core::DeserializeError::custom( + // // let err: ProcedureError = rspc_procedure::DeserializeError::custom( // // "Client did not set correct valid 'Content-Type' header", // // ) // // .into(); diff --git a/integrations/axum/src/v2.rs b/integrations/axum/src/v2.rs index 772c1c26..03534833 100644 --- a/integrations/axum/src/v2.rs +++ b/integrations/axum/src/v2.rs @@ -8,7 +8,7 @@ use axum::{ routing::{on, MethodFilter}, RequestExt, Router, }; -use rspc_core::{Procedure, Procedures}; +use rspc_procedure::{Procedure, Procedures}; use serde_json::Value; use crate::{ diff --git a/integrations/http/Cargo.toml b/integrations/http/Cargo.toml index 94a9185d..5c562d9c 100644 --- a/integrations/http/Cargo.toml +++ b/integrations/http/Cargo.toml @@ -19,7 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] [dependencies] -rspc-core = { version = "0.0.1", path = "../../core" } +rspc-procedure = { version = "0.0.1", path = "../../crates/procedure" } serde_json = "1" form_urlencoded = "1" futures-core = "0.3" diff --git a/integrations/http/src/execute.rs b/integrations/http/src/execute.rs index 43f0886e..48b7c3a8 100644 --- a/integrations/http/src/execute.rs +++ b/integrations/http/src/execute.rs @@ -14,7 +14,7 @@ // use futures::StreamExt; // use futures_core::Stream; -// use rspc_core::ProcedureStream; +// use rspc_procedure::ProcedureStream; // use serde::Serializer; // pub enum ExecuteInput<'a> { @@ -25,7 +25,7 @@ // /// TODO: Explain this // // TODO: `Content-Type` header on response??? // pub async fn execute<'a, 'b, TCtx>( -// procedure: &'a rspc_core::Procedure, +// procedure: &'a rspc_procedure::Procedure, // input: ExecuteInput<'b>, // ctx: impl FnOnce() -> TCtx, // ) -> (u16, impl Stream> + Send + 'static) { diff --git a/integrations/tauri/Cargo.toml b/integrations/tauri/Cargo.toml index ab9d6588..9e643d71 100644 --- a/integrations/tauri/Cargo.toml +++ b/integrations/tauri/Cargo.toml @@ -17,7 +17,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -rspc-core = { version = "0.0.1", path = "../../core" } +rspc-procedure = { version = "0.0.1", path = "../../crates/procedure" } tauri = "2" serde = { version = "1", features = [ "derive", diff --git a/integrations/tauri/src/lib.rs b/integrations/tauri/src/lib.rs index 9e301aaa..7b5bc772 100644 --- a/integrations/tauri/src/lib.rs +++ b/integrations/tauri/src/lib.rs @@ -12,7 +12,7 @@ use std::{ sync::{Arc, Mutex, MutexGuard, PoisonError}, }; -use rspc_core::{ProcedureError, Procedures}; +use rspc_procedure::{ProcedureError, Procedures}; use serde::{de::Error, Deserialize, Serialize}; use serde_json::value::RawValue; use tauri::{ diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml index 49d1f350..c3634784 100644 --- a/rspc/Cargo.toml +++ b/rspc/Cargo.toml @@ -28,7 +28,7 @@ nolegacy = [] [dependencies] # Public -rspc-core = { path = "../core" } +rspc-procedure = { path = "../crates/procedure" } serde = "1" futures = "0.3" # TODO: Drop down to `futures-core` when removing legacy stuff? specta = { version = "=2.0.0-rc.20", features = [ diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy/interop.rs index 38bb944c..25ebdce4 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy/interop.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData, panic::Location}; use futures::{stream, FutureExt, StreamExt, TryStreamExt}; -use rspc_core::{ProcedureStream, ResolverError}; +use rspc_procedure::{ProcedureStream, ResolverError}; use serde_json::Value; use specta::{ datatype::{DataType, EnumRepr, EnumVariant, LiteralType}, @@ -78,8 +78,8 @@ pub(crate) fn layer_to_procedure( path: String, kind: ProcedureKind, value: Box>, -) -> rspc_core::Procedure { - rspc_core::Procedure::new(move |ctx, input| { +) -> rspc_procedure::Procedure { + rspc_procedure::Procedure::new(move |ctx, input| { let result = input.deserialize::().and_then(|input| { value .call( @@ -94,7 +94,7 @@ pub(crate) fn layer_to_procedure( let err: crate::legacy::Error = err.into(); ResolverError::new( (), /* typesafe errors aren't supported in legacy router */ - Some(rspc_core::LegacyErrorInterop(err.message)), + Some(rspc_procedure::LegacyErrorInterop(err.message)), ) .into() }) @@ -112,7 +112,7 @@ pub(crate) fn layer_to_procedure( let err = crate::legacy::Error::from(err); ResolverError::new( (), /* typesafe errors aren't supported in legacy router */ - Some(rspc_core::LegacyErrorInterop(err.message)), + Some(rspc_procedure::LegacyErrorInterop(err.message)), ) .into() }) diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index 75ef1bf9..70121cf7 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -44,7 +44,7 @@ pub use modern::{ #[cfg(feature = "unstable")] pub use procedure::Procedure2; -pub use rspc_core::{ +pub use rspc_procedure::{ flush, DeserializeError, DowncastError, DynInput, DynOutput, Procedure, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures, ResolverError, State, }; @@ -67,4 +67,4 @@ pub use legacy::{ SerializeMarker, StreamResolver, }; #[cfg(not(feature = "nolegacy"))] -pub use rspc_core::LegacyErrorInterop; +pub use rspc_procedure::LegacyErrorInterop; diff --git a/rspc/src/modern/error.rs b/rspc/src/modern/error.rs index b4a29863..6481289f 100644 --- a/rspc/src/modern/error.rs +++ b/rspc/src/modern/error.rs @@ -1,6 +1,6 @@ use std::error; -use rspc_core::ResolverError; +use rspc_procedure::ResolverError; use serde::Serialize; use specta::Type; diff --git a/rspc/src/modern/extension.rs b/rspc/src/modern/extension.rs index a7e52909..63ef0110 100644 --- a/rspc/src/modern/extension.rs +++ b/rspc/src/modern/extension.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use rspc_core::State; +use rspc_procedure::State; use crate::ProcedureMeta; diff --git a/rspc/src/modern/infallible.rs b/rspc/src/modern/infallible.rs index 8773c7ee..1391e5e7 100644 --- a/rspc/src/modern/infallible.rs +++ b/rspc/src/modern/infallible.rs @@ -24,7 +24,7 @@ // impl std::error::Error for Infallible {} // impl crate::modern::Error for Infallible { -// fn into_resolver_error(self) -> rspc_core::ResolverError { +// fn into_resolver_error(self) -> rspc_procedure::ResolverError { // unreachable!() // } // } diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index 46e73055..ed147b17 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -22,7 +22,7 @@ use std::{pin::Pin, sync::Arc}; use futures::{Future, FutureExt, Stream}; -use rspc_core::State; +use rspc_procedure::State; use crate::modern::procedure::ProcedureMeta; diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs index d22580bf..7326683f 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/modern/procedure/builder.rs @@ -9,7 +9,7 @@ use crate::{ use super::{ErasedProcedure, ProcedureKind, ProcedureMeta}; use futures::{FutureExt, StreamExt}; -use rspc_core::State; +use rspc_procedure::State; // TODO: Document the generics like `Middleware`. What order should they be in? pub struct ProcedureBuilder { diff --git a/rspc/src/modern/procedure/erased.rs b/rspc/src/modern/procedure/erased.rs index a93e6ece..1de03214 100644 --- a/rspc/src/modern/procedure/erased.rs +++ b/rspc/src/modern/procedure/erased.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, panic::Location, sync::Arc}; use futures::{FutureExt, TryStreamExt}; -use rspc_core::Procedure; +use rspc_procedure::Procedure; use specta::datatype::DataType; use crate::{ @@ -16,7 +16,7 @@ use crate::{ pub struct ErasedProcedure { pub(crate) setup: Vec>, pub(crate) ty: ProcedureType, - pub(crate) inner: Box) -> rspc_core::Procedure>, + pub(crate) inner: Box) -> rspc_procedure::Procedure>, } // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` diff --git a/rspc/src/modern/procedure/resolver_input.rs b/rspc/src/modern/procedure/resolver_input.rs index 6d0a3ee8..48d07edc 100644 --- a/rspc/src/modern/procedure/resolver_input.rs +++ b/rspc/src/modern/procedure/resolver_input.rs @@ -30,7 +30,7 @@ // /// } // /// ``` -// TODO: Should this be in `rspc_core`??? +// TODO: Should this be in `rspc_procedure`??? // TODO: Maybe rename? use serde::de::DeserializeOwned; @@ -41,7 +41,7 @@ pub trait ResolverInput: Sized + Send + 'static { fn data_type(types: &mut TypeCollection) -> DataType; /// Convert the [`DynInput`] into the type the user specified for the procedure. - fn from_input(input: rspc_core::DynInput) -> Result; + fn from_input(input: rspc_procedure::DynInput) -> Result; } impl ResolverInput for T { @@ -49,7 +49,7 @@ impl ResolverInput for T { T::inline(types, specta::Generics::Definition) } - fn from_input(input: rspc_core::DynInput) -> Result { + fn from_input(input: rspc_procedure::DynInput) -> Result { Ok(input.deserialize()?) } } diff --git a/rspc/src/modern/procedure/resolver_output.rs b/rspc/src/modern/procedure/resolver_output.rs index 8c69368a..dbe7c95f 100644 --- a/rspc/src/modern/procedure/resolver_output.rs +++ b/rspc/src/modern/procedure/resolver_output.rs @@ -30,13 +30,13 @@ // // )] use futures::{Stream, TryStreamExt}; -use rspc_core::{ProcedureError, ProcedureStream}; +use rspc_procedure::{ProcedureError, ProcedureStream}; use serde::Serialize; use specta::{datatype::DataType, Generics, Type, TypeCollection}; use crate::modern::Error; -// TODO: Maybe in `rspc_core`?? +// TODO: Maybe in `rspc_procedure`?? /// TODO: bring back any correct parts of the docs above pub trait ResolverOutput: Sized + Send + 'static { diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 1ee9272e..3f1f977c 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, marker::PhantomData, panic::Location, sync::Arc}; use futures::{FutureExt, TryStreamExt}; -use rspc_core::Procedure; +use rspc_procedure::Procedure; use specta::datatype::DataType; use crate::{ diff --git a/rspc/src/router.rs b/rspc/src/router.rs index 0ca27765..01530729 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -8,7 +8,7 @@ use std::{ use specta::TypeCollection; -use rspc_core::Procedures; +use rspc_procedure::Procedures; use crate::{ modern::procedure::ErasedProcedure, types::TypesOrType, Procedure2, ProcedureKind, State, Types, diff --git a/rspc/tests/router.rs b/rspc/tests/router.rs index f85419db..cce08dc9 100644 --- a/rspc/tests/router.rs +++ b/rspc/tests/router.rs @@ -1,7 +1,7 @@ use std::fmt; use rspc::{Procedure2, Router2}; -use rspc_core::ResolverError; +use rspc_procedure::ResolverError; use serde::Serialize; use specta::Type; From 3b642e78937656b252956c8f91b8464a57180a8c Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 25 Dec 2024 21:29:46 +0800 Subject: [PATCH 58/67] `README`'s for crates --- crates/README.md | 6 --- crates/binario/README.md | 13 ++++- crates/cache/README.md | 33 ++++++++++++- crates/client/README.md | 34 +++++++++++++ crates/core/README.md | 5 ++ crates/devtools/README.md | 5 +- crates/invalidation/README.md | 84 ++++----------------------------- crates/invalidation/RESEARCH.md | 82 ++++++++++++++++++++++++++++++++ crates/openapi/README.md | 16 ++++++- crates/procedure/README.md | 5 ++ crates/tracing/README.md | 13 ++++- 11 files changed, 210 insertions(+), 86 deletions(-) delete mode 100644 crates/README.md create mode 100644 crates/client/README.md create mode 100644 crates/core/README.md create mode 100644 crates/invalidation/RESEARCH.md create mode 100644 crates/procedure/README.md diff --git a/crates/README.md b/crates/README.md deleted file mode 100644 index 3d9d9671..00000000 --- a/crates/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Crates - -> [!CAUTION] -> These are not yet stable so use at your own risk. - - diff --git a/crates/binario/README.md b/crates/binario/README.md index bd22e9bb..418e2af7 100644 --- a/crates/binario/README.md +++ b/crates/binario/README.md @@ -1,3 +1,12 @@ -# rspc Binario +# rspc 🤝 Binario -Coming soon... +[![docs.rs](https://img.shields.io/crates/v/rspc-binario)](https://docs.rs/rspc-binario) + +> [!CAUTION] +> This crate is a proof of concept. +> +> It is not intended for production use and will likely remain that way. + +Use [Binario](https://github.com/oscartbeaumont/binario) instead of [Serde](https://serde.rs) for serialization and deserialization. + +This is a proof of concept to show that rspc has the ability to support any serialization libraries. diff --git a/crates/cache/README.md b/crates/cache/README.md index c562e38f..9e7cf2ab 100644 --- a/crates/cache/README.md +++ b/crates/cache/README.md @@ -1,3 +1,34 @@ # rspc cache -Coming soon... +[![docs.rs](https://img.shields.io/crates/v/rspc-cache)](https://docs.rs/rspc-cache) + +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. + +Provides a simple way to cache the results of rspc queries with pluggable backends. + +Features: + - Simple to use + - Pluggable backends (memory, redis, etc.) + - Configurable cache TTL + +## Example + +```rust +// TODO: imports + +fn todo() -> Router2 { + Router2::new() + .setup(CacheState::builder(Memory::new()).mount()) + .procedure("my_query", { + ::builder() + .with(cache()) + .query(|_, _: ()| async { + // if input.some_arg {} + cache_ttl(10); + + Ok(SystemTime::now()) + }) + }) +} +``` diff --git a/crates/client/README.md b/crates/client/README.md new file mode 100644 index 00000000..0886a533 --- /dev/null +++ b/crates/client/README.md @@ -0,0 +1,34 @@ +# Rust client + +[![docs.rs](https://img.shields.io/crates/v/rspc-client)](https://docs.rs/rspc-client) + +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. + +Allows you to make queries from a Rust client to an rspc server. + +## Example + +```rust +// This file is generated via the `rspc::Rust` language on your server +mod bindings; + +#[tokio::main] +async fn main() { + let client = rspc_client::Client::new("http://[::]:4000/rspc"); + + println!("{:?}", client.exec::(()).await); + println!( + "{:?}", + client + .exec::("Some random string!".into()) + .await + ); + println!( + "{:?}", + client + .exec::("Hello from rspc Rust client!".into()) + .await + ); +} +``` diff --git a/crates/core/README.md b/crates/core/README.md new file mode 100644 index 00000000..086ca540 --- /dev/null +++ b/crates/core/README.md @@ -0,0 +1,5 @@ +# Core + +[![docs.rs](https://img.shields.io/crates/v/rspc-core)](https://docs.rs/rspc-core) + +Core types and traits for rspc. diff --git a/crates/devtools/README.md b/crates/devtools/README.md index 8fb10a18..665028e3 100644 --- a/crates/devtools/README.md +++ b/crates/devtools/README.md @@ -1,3 +1,6 @@ # rspc devtools -Coming soon... +[![docs.rs](https://img.shields.io/crates/v/rspc-devtools)](https://docs.rs/rspc-devtools) + +> [!CAUTION] +> This crate is an experiment. You shouldn't use it unless you really know what you are doing. diff --git a/crates/invalidation/README.md b/crates/invalidation/README.md index bc88cf39..054a22c3 100644 --- a/crates/invalidation/README.md +++ b/crates/invalidation/README.md @@ -1,82 +1,18 @@ -# rspc Invalidation +# rspc invalidation -For now this is not going to be released as we need to work out if their is any value to an official middleware, instead of an example project implementing the same thing user-space? +[![docs.rs](https://img.shields.io/crates/v/rspc-invalidation)](https://docs.rs/rspc-invalidation) -## Questions +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. -For my own future reference: https://discord.com/channels/@me/813276814801764382/1263123489477361828 +Support for server-initiated invalidation of data. This can be utilised to achieve Single Flight Mutation. -### Pull vs Push based invalidation events +Features: + - Support for Single Flight Mutations + - Support for subscription-based invalidation to invalidate data across all clients -Pull based is where the middleware is applied to the query. -Push based is where the middleware is applied to the mutation. - -I think we want a pull-based so resources can define their dependencies a-la React dependencies array. - -### Stream or not? - -I'm leaning stream-based because it pushes the type safety concern onto the end user. +## Example ```rust -::builder() - // "Pull"-based. Applied to queries. (I personally a "Pull"-based approach is better) - .with(rspc_invalidation::invalidation( - |input, result, operation| operation.key() == "store.set", - )) - .with(rspc_invalidation::invalidation( - // TODO: how is `input().id` even gonna work lol - |input, result, operation| { - operation.key() == "notes.update" && operation.input().id == input.id - }, - )) - // "Push"-based. Applied to mutations. - .with(rspc_invalidation::invalidation( - |input, result, invalidate| invalidate("store.get", ()), - )) - .with(rspc_invalidation::invalidation( - |input, result, operation| invalidate("notes.get", input.id), - )) - // "Pull"-based but with stream. - .with(rspc_invalidation::invalidation(|input: TArgs| { - stream! { - // If practice subscribe to some central event bus for changes - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - yield Invalidate; // pub struct Invalidate; - } - } - })) - .query(...) +// TODO ``` - -### Exposing result of procedure to invalidation closure - -If we expose result to the invalidate callback either the `Stream` or the value must be `Clone` which is not great, although the constrain can be applied locally by the middleware. - -If we expose the result and use a stream-based approach do we spawn a new invalidation closure for every result? I think this is something we will wanna leave the user in control of but no idea what that API would look like. - -### How do we get `BuiltRouter` into `Procedure`? - -It kinda has to come in via context or we need some magic system within rspc's core. Otherwise we basically have a recursive dependency. - -### Frontend? - -Will we expose a package or will it be on the user to hook it up? - -## Other concerns - -## User activity - -Really we wanna only push invalidation events that are related to parts of the app the user currently has active. An official system would need to take this into account somehow. Maybe some integration with the frontend router and websocket state using the `TCtx`??? - -## Data or invalidation - -If we can be pretty certain the frontend wants the new data we can safely push it straight to the frontend instead of just asking the frontend to refetch. This will be much faster but if your not tracking user-activity it will be way slower because of the potential volume of data. - -Tracking user activity pretty much requires some level of router integration which might be nice to have an abstraction for but it's also hard. - -## Authorization - -**This is why rspc can't own the subscription!!!** - -We should also have a way to take into account authorization and what invalidation events the user is able to see. For something like Spacedrive we never had this problem because we are a desktop app but any web app would require this. \ No newline at end of file diff --git a/crates/invalidation/RESEARCH.md b/crates/invalidation/RESEARCH.md new file mode 100644 index 00000000..00ba1b68 --- /dev/null +++ b/crates/invalidation/RESEARCH.md @@ -0,0 +1,82 @@ +## Research + +Some thoughts about the design from a while ago. This can probally be removed once we are happy the solution meets all of the requirements. + +## Questions + +For my own future reference: https://discord.com/channels/@me/813276814801764382/1263123489477361828 + +### Pull vs Push based invalidation events + +Pull based is where the middleware is applied to the query. +Push based is where the middleware is applied to the mutation. + +I think we want a pull-based so resources can define their dependencies a-la React dependencies array. + +### Stream or not? + +I'm leaning stream-based because it pushes the type safety concern onto the end user. + +```rust +::builder() + // "Pull"-based. Applied to queries. (I personally a "Pull"-based approach is better) + .with(rspc_invalidation::invalidation( + |input, result, operation| operation.key() == "store.set", + )) + .with(rspc_invalidation::invalidation( + // TODO: how is `input().id` even gonna work lol + |input, result, operation| { + operation.key() == "notes.update" && operation.input().id == input.id + }, + )) + // "Push"-based. Applied to mutations. + .with(rspc_invalidation::invalidation( + |input, result, invalidate| invalidate("store.get", ()), + )) + .with(rspc_invalidation::invalidation( + |input, result, operation| invalidate("notes.get", input.id), + )) + // "Pull"-based but with stream. + .with(rspc_invalidation::invalidation(|input: TArgs| { + stream! { + // If practice subscribe to some central event bus for changes + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + yield Invalidate; // pub struct Invalidate; + } + } + })) + .query(...) +``` + +### Exposing result of procedure to invalidation closure + +If we expose result to the invalidate callback either the `Stream` or the value must be `Clone` which is not great, although the constrain can be applied locally by the middleware. + +If we expose the result and use a stream-based approach do we spawn a new invalidation closure for every result? I think this is something we will wanna leave the user in control of but no idea what that API would look like. + +### How do we get `BuiltRouter` into `Procedure`? + +It kinda has to come in via context or we need some magic system within rspc's core. Otherwise we basically have a recursive dependency. + +### Frontend? + +Will we expose a package or will it be on the user to hook it up? + +## Other concerns + +## User activity + +Really we wanna only push invalidation events that are related to parts of the app the user currently has active. An official system would need to take this into account somehow. Maybe some integration with the frontend router and websocket state using the `TCtx`??? + +## Data or invalidation + +If we can be pretty certain the frontend wants the new data we can safely push it straight to the frontend instead of just asking the frontend to refetch. This will be much faster but if your not tracking user-activity it will be way slower because of the potential volume of data. + +Tracking user activity pretty much requires some level of router integration which might be nice to have an abstraction for but it's also hard. + +## Authorization + +**This is why rspc can't own the subscription!!!** + +We should also have a way to take into account authorization and what invalidation events the user is able to see. For something like Spacedrive we never had this problem because we are a desktop app but any web app would require this. diff --git a/crates/openapi/README.md b/crates/openapi/README.md index 95a80727..89691097 100644 --- a/crates/openapi/README.md +++ b/crates/openapi/README.md @@ -1,3 +1,17 @@ # rspc OpenAPI -Coming soon... \ No newline at end of file +[![docs.rs](https://img.shields.io/crates/v/rspc-openapi)](https://docs.rs/rspc-openapi) + +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. + +Support for generating an [OpenAPI](https://www.openapis.org) schema and endpoints from your rspc router. + +Features: + - Support for [Swagger](https://swagger.io) and [Scalar](https://scalar.com) + +## Example + +```rust +// Coming soon... +``` diff --git a/crates/procedure/README.md b/crates/procedure/README.md new file mode 100644 index 00000000..5b401e15 --- /dev/null +++ b/crates/procedure/README.md @@ -0,0 +1,5 @@ +# rspc Procedure + +[![docs.rs](https://img.shields.io/crates/v/rspc-procedure)](https://docs.rs/rspc-procedure) + +Interface for a single type-erased operation that the server can execute. diff --git a/crates/tracing/README.md b/crates/tracing/README.md index c05e1f27..d3c9711e 100644 --- a/crates/tracing/README.md +++ b/crates/tracing/README.md @@ -1,3 +1,14 @@ # rspc tracing -Coming soon... \ No newline at end of file +[![docs.rs](https://img.shields.io/crates/v/rspc-tracing)](https://docs.rs/rspc-tracing) + +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. + +Support for [tracing](https://github.com/tokio-rs/tracing) with rspc to collect detailed span information. + +## Example + +```rust +// Coming soon... +``` From 5696ef2c0a5ac80d4445b11db1c5e33a47ce4367 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 25 Dec 2024 22:41:19 +0800 Subject: [PATCH 59/67] fix `ProcedureStream` error handling + `rspc-validator` --- crates/procedure/src/dyn_output.rs | 8 +- crates/procedure/src/error.rs | 12 +-- crates/procedure/src/stream.rs | 137 ++++++++++++++++------------- crates/validator/Cargo.toml | 3 + crates/validator/README.md | 17 +++- crates/validator/src/lib.rs | 64 +++++++++++++- examples/axum/src/main.rs | 36 +++++--- examples/bindings.ts | 3 +- examples/core/Cargo.toml | 5 ++ examples/core/src/lib.rs | 70 +++++++++------ 10 files changed, 246 insertions(+), 109 deletions(-) diff --git a/crates/procedure/src/dyn_output.rs b/crates/procedure/src/dyn_output.rs index cdd371e0..efa1bcf7 100644 --- a/crates/procedure/src/dyn_output.rs +++ b/crates/procedure/src/dyn_output.rs @@ -5,6 +5,8 @@ use std::{ use serde::Serialize; +use crate::ProcedureError; + /// TODO pub struct DynOutput<'a> { inner: Repr<'a>, @@ -45,7 +47,11 @@ impl<'a> DynOutput<'a> { pub fn as_value(self) -> Option { match self.inner { Repr::Serialize(_) => None, - Repr::Value(v) => v.downcast_mut::>()?.take().expect("unreachable"), + Repr::Value(v) => v + .downcast_mut::>>()? + .take() + .expect("unreachable") + .expect("unreachable"), } } } diff --git a/crates/procedure/src/error.rs b/crates/procedure/src/error.rs index 0d43ca7b..4f510c01 100644 --- a/crates/procedure/src/error.rs +++ b/crates/procedure/src/error.rs @@ -104,17 +104,17 @@ impl Serialize for ProcedureError { S: Serializer, { if let ProcedureError::Resolver(err) = self { - if let Some(err) = err.error() { - if let Some(v) = err.downcast_ref::() { - return v.0.serialize(serializer); - } - } + // if let Some(err) = err.error() { + // if let Some(v) = err.downcast_ref::() { + // return v.0.serialize(serializer); + // } + // } return err.value().serialize(serializer); } let mut state = serializer.serialize_struct("ProcedureError", 3)?; - state.serialize_field("_rspc", &true)?; + state.serialize_field("~rspc", &true)?; state.serialize_field("variant", &self.variant())?; state.serialize_field("message", &self.message())?; state.end() diff --git a/crates/procedure/src/stream.rs b/crates/procedure/src/stream.rs index e6a5fe4d..1c1ebdce 100644 --- a/crates/procedure/src/stream.rs +++ b/crates/procedure/src/stream.rs @@ -83,6 +83,9 @@ impl ProcedureStream { as_value: |v| { DynOutput::new_serialize( v.as_mut() + // Error's are caught before `as_value` is called. + .expect("unreachable") + .as_mut() // Attempted to access value when `Poll::Ready(None)` was not returned. .expect("unreachable"), ) @@ -133,6 +136,9 @@ impl ProcedureStream { as_value: |v| { DynOutput::new_serialize( v.as_mut() + // Error's are caught before `as_value` is called. + .expect("unreachable") + .as_mut() // Attempted to access value when `Poll::Ready(None)` was not returned. .expect("unreachable"), ) @@ -190,13 +196,16 @@ impl ProcedureStream { as_value: |v| { DynOutput::new_serialize( v.as_mut() + // Error's are caught before `as_value` is called. + .expect("unreachable") + .as_mut() // Attempted to access value when `Poll::Ready(None)` was not returned. .expect("unreachable"), ) }, flushed: false, unwound: false, - value: None::, + value: None, })), flush: None, pending_value: false, @@ -314,7 +323,7 @@ impl ProcedureStream { as_value: |v| DynOutput::new_value(v), flushed: false, unwound: false, - value: None::, + value: None, })), flush: None, pending_value: false, @@ -386,7 +395,7 @@ impl ProcedureStream { } } - fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll>> { + fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll> { // Ensure the waker is up to date. if let Some(waker) = &mut self.flush { if !waker.will_wake(cx.waker()) { @@ -398,7 +407,7 @@ impl ProcedureStream { return if self.flush.is_none() { // We have a queued value ready to be flushed. self.pending_value = false; - Poll::Ready(Some(Ok(()))) + Poll::Ready(Some(())) } else { // The async runtime would have no reason to be polling right now but we protect against it anyway. Poll::Pending @@ -411,15 +420,21 @@ impl ProcedureStream { if self.flush.is_none() { Poll::Ready(v) } else { - self.pending_value = true; - Poll::Pending + match v { + Some(v) => { + self.pending_value = true; + Poll::Pending + } + None => Poll::Ready(None), + } } } Poll::Pending => Poll::Pending, }, Inner::Value(v) => { if self.flush.is_none() { - Poll::Ready(v.take().map(Err)) + // Poll::Ready(v.take().map(Err)) + todo!(); } else { Poll::Pending } @@ -433,20 +448,6 @@ impl ProcedureStream { cx: &mut Context<'_>, ) -> Poll, ProcedureError>>> { self.poll_inner(cx).map(|v| { - v.map(|v| { - v.map(|_: ()| { - let Inner::Dyn(s) = &mut self.inner else { - unreachable!(); // TODO: Handle this? - }; - s.as_mut().value() - }) - }) - }) - } - - /// TODO - pub async fn next(&mut self) -> Option, ProcedureError>> { - poll_fn(|cx| self.poll_inner(cx)).await.map(|v| { v.map(|_: ()| { let Inner::Dyn(s) = &mut self.inner else { unreachable!(); // TODO: Handle this? @@ -456,9 +457,19 @@ impl ProcedureStream { }) } + /// TODO + pub async fn next(&mut self) -> Option, ProcedureError>> { + poll_fn(|cx| self.poll_inner(cx)).await.map(|_: ()| { + let Inner::Dyn(s) = &mut self.inner else { + unreachable!(); // TODO: Handle this? + }; + s.as_mut().value() + }) + } + /// TODO // TODO: Should error be `String` type? - pub fn map Result + Unpin, T>( + pub fn map) -> Result + Unpin, T>( self, map: F, ) -> ProcedureStreamMap { @@ -466,12 +477,17 @@ impl ProcedureStream { } } -pub struct ProcedureStreamMap Result + Unpin, T> { +pub struct ProcedureStreamMap< + F: FnMut(Result) -> Result + Unpin, + T, +> { stream: ProcedureStream, map: F, } -impl Result + Unpin, T> ProcedureStreamMap { +impl) -> Result + Unpin, T> + ProcedureStreamMap +{ /// Start streaming data. /// Refer to `Self::require_manual_stream` for more information. pub fn stream(&mut self) { @@ -493,29 +509,25 @@ impl Result + Unpin, T> ProcedureStreamMap Result + Unpin, T> Stream for ProcedureStreamMap { +impl) -> Result + Unpin, T> Stream + for ProcedureStreamMap +{ type Item = T; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); this.stream.poll_inner(cx).map(|v| { - v.map(|v| { - match v { - Ok(()) => { - let Inner::Dyn(s) = &mut this.stream.inner else { - unreachable!(); - }; - - match (this.map)(s.as_mut().value()) { - Ok(v) => v, - // TODO: Exposing this error to the client or not? - // TODO: Error type??? - Err(err) => todo!(), - } - } - // TODO: Fix this - Err(_) => todo!(), + v.map(|_: ()| { + let Inner::Dyn(s) = &mut this.stream.inner else { + unreachable!(); + }; + + match (this.map)(s.as_mut().value()) { + Ok(v) => v, + // TODO: Exposing this error to the client or not? + // TODO: Error type??? + Err(err) => todo!(), } }) }) @@ -533,11 +545,8 @@ impl fmt::Debug for ProcedureStream { } trait DynReturnValue: Send { - fn poll_next_value<'a>( - self: Pin<&'a mut Self>, - cx: &mut Context<'_>, - ) -> Poll>>; - fn value(self: Pin<&mut Self>) -> DynOutput<'_>; + fn poll_next_value<'a>(self: Pin<&'a mut Self>, cx: &mut Context<'_>) -> Poll>; + fn value(self: Pin<&mut Self>) -> Result, ProcedureError>; fn size_hint(&self) -> (usize, Option); fn resolved(&self) -> bool; fn flushed(&self) -> bool; @@ -552,24 +561,22 @@ pin_project! { // `Stream::size_hint` size_hint: fn(&S) -> (usize, Option), // convert the current value to a `DynOutput` - as_value: fn(&mut Option) -> DynOutput<'_>, + as_value: fn(&mut Option>) -> DynOutput<'_>, // detect when the stream has finished it's future if it has one. resolved: fn(&S) -> bool, // has the user called `flushed` within it? flushed: bool, // has the user panicked? unwound: bool, - // the last yielded value. We place it here for more efficient serialization. - // it also makes `ProcedureStream::require_manual_stream` possible. - value: Option, + // the last yielded value. We place `T` here so we can type-erase it and avoiding boxing every value. + // we hold `Result<_, ProcedureError>` for `ProcedureStream::require_manual_stream` to bepossible. + // Be extemely careful changing this type as it's used in `DynOutput`'s downcasting! + value: Option>, } } impl DynReturnValue for GenericDynReturnValue { - fn poll_next_value<'a>( - mut self: Pin<&'a mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { + fn poll_next_value<'a>(mut self: Pin<&'a mut Self>, cx: &mut Context<'_>) -> Poll> { if self.unwound { // The stream is now done. return Poll::Ready(None); @@ -580,10 +587,8 @@ impl DynReturnValue for GenericDynReturnValue { let _ = this.value.take(); // Reset value to ensure `take` being misused causes it to panic. (this.poll)(this.inner, cx).map(|v| { v.map(|v| { - v.map(|v| { - *this.value = Some(v); - () - }) + *this.value = Some(v); + () }) }) })); @@ -592,13 +597,23 @@ impl DynReturnValue for GenericDynReturnValue { Ok(v) => v, Err(err) => { *this.unwound = true; - Poll::Ready(Some(Err(ProcedureError::Unwind(err)))) + *this.value = Some(Err(ProcedureError::Unwind(err))); + Poll::Ready(Some(())) } } } - fn value(self: Pin<&mut Self>) -> DynOutput<'_> { - (self.as_value)(self.project().value) + fn value(self: Pin<&mut Self>) -> Result, ProcedureError> { + let this = self.project(); + match this.value { + Some(Err(_)) => { + let Some(Err(err)) = std::mem::replace(this.value, None) else { + unreachable!(); // checked above + }; + Err(err) + } + v => Ok((this.as_value)(v)), + } } fn size_hint(&self) -> (usize, Option) { diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index eca4c749..90589f2f 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -6,6 +6,9 @@ publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc", features = ["unstable"] } +serde = "1" +specta = "=2.0.0-rc.20" +validator = "0.19" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/validator/README.md b/crates/validator/README.md index ab67aa55..120a6822 100644 --- a/crates/validator/README.md +++ b/crates/validator/README.md @@ -1,3 +1,18 @@ # rspc Validator -Coming soon... +[![docs.rs](https://img.shields.io/crates/v/rspc-validator)](https://docs.rs/rspc-validator) + +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. + +Support for [validator](https://docs.rs/validator) with rspc for easy input validation. + +Features: + - Return validator errors to the client. + - JavaScript client for typesafe parsing of errors. // TODO + +## Example + +```rust +// Coming soon... +``` diff --git a/crates/validator/src/lib.rs b/crates/validator/src/lib.rs index 1b1d854d..9d217dbe 100644 --- a/crates/validator/src/lib.rs +++ b/crates/validator/src/lib.rs @@ -1,4 +1,4 @@ -//! rspc-validator: Input validation for rspc +//! Support for [`validator`] with [`rspc`] for easy input validation. #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( @@ -6,4 +6,64 @@ html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" )] -// TODO: Coming soon. +use std::fmt; + +use rspc::middleware::Middleware; +use serde::{ser::SerializeStruct, Serialize}; +use specta::{datatype::DataType, Type}; +use validator::{Validate, ValidationErrors}; + +/// TODO +pub fn validate() -> Middleware +where + TError: From + Send + 'static, + TCtx: Send + 'static, + TInput: Validate + Send + 'static, + TResult: Send + 'static, +{ + Middleware::new(|ctx, input: TInput, next| async move { + match input.validate() { + Ok(()) => next.exec(ctx, input).await, + Err(err) => Err(RspcValidatorError(err).into()), + } + }) +} + +#[derive(Clone)] +pub struct RspcValidatorError(ValidationErrors); + +impl fmt::Debug for RspcValidatorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl fmt::Display for RspcValidatorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl std::error::Error for RspcValidatorError {} + +impl Serialize for RspcValidatorError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("RspcValidatorError", 2)?; + s.serialize_field("~rspc.validator", &true)?; + s.serialize_field("errors", &self.0.field_errors())?; + s.end() + } +} + +// TODO: Proper implementation +impl Type for RspcValidatorError { + fn inline( + _type_map: &mut specta::TypeCollection, + _generics: specta::Generics, + ) -> specta::datatype::DataType { + DataType::Any + } +} diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index e91793f0..d222c10d 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -7,7 +7,7 @@ use axum::{ }; use example_core::{create_router, Ctx}; use futures::{stream::FuturesUnordered, Stream, StreamExt}; -use rspc::{DynOutput, ProcedureStream, ProcedureStreamMap, Procedures, State}; +use rspc::{DynOutput, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures, State}; use rspc_invalidation::Invalidator; use serde_json::{de::SliceRead, value::RawValue, Value}; use std::{ @@ -207,13 +207,18 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { let mut runtime = StreamUnordered::new(); // TODO: Move onto `Prototype`??? let spawn = |runtime: &mut StreamUnordered<_>, p: ProcedureStream| { - runtime.insert( - p.require_manual_stream() - .map:: Result, String>, Vec>(|v| { - serde_json::to_vec(&v.as_serialize().unwrap()) - .map_err(|err| err.to_string()) - }), - ); + runtime.insert(p.require_manual_stream().map::, + ) + -> Result, String>, Vec>( + |v| { + match v { + Ok(v) => serde_json::to_vec(&v.as_serialize().unwrap()), + Err(err) => serde_json::to_vec(&err), + } + .map_err(|err| err.to_string()) + }, + )); }; // TODO: If a file was being uploaded this would require reading the whole body until the `runtime` is polled. @@ -280,7 +285,12 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { // TODO: This abstraction is soooo bad. pub struct Prototype { - runtime: StreamUnordered Result, String>, Vec>>, + runtime: StreamUnordered< + ProcedureStreamMap< + fn(Result) -> Result, String>, + Vec, + >, + >, invalidator: Invalidator, ctx: TCtx, sfm: bool, @@ -311,9 +321,11 @@ impl Stream for Prototype { rspc_invalidation::queue(&self.invalidator, self.ctx.clone(), &self.procedures) { self.runtime.insert( - stream.map:: Result, String>, Vec>(|v| { - serde_json::to_vec(&v.as_serialize().unwrap()) - .map_err(|err| err.to_string()) + stream.map::) -> Result, String>, Vec>(|v| { + match v { + Ok(v) => serde_json::to_vec(&v.as_serialize().unwrap()), + Err(err) => serde_json::to_vec(&err), + }.map_err(|err| err.to_string()) }), ); } diff --git a/examples/bindings.ts b/examples/bindings.ts index 1be8d89e..778a9b93 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,7 +1,7 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string } | { key: "withoutBaseProcedure"; input: any; result: any }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "validator"; input: any; result: any } | { key: "version"; input: null; result: string } | { key: "withoutBaseProcedure"; input: any; result: any }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { cached: { kind: "query", input: any, output: any, error: any }, @@ -20,6 +20,7 @@ export type Procedures = { sfmPost: { kind: "query", input: any, output: any, error: any }, sfmPostEdit: { kind: "query", input: any, output: any, error: any }, transformMe: { kind: "query", input: null, output: string, error: unknown }, + validator: { kind: "query", input: any, output: any, error: any }, version: { kind: "query", input: null, output: string, error: unknown }, withoutBaseProcedure: { kind: "query", input: any, output: any, error: any }, } \ No newline at end of file diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index a9bac707..f9ce3738 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -17,3 +17,8 @@ tracing = "0.1.41" futures = "0.3.31" rspc-cache = { version = "0.0.0", path = "../../crates/cache" } rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } +rspc-validator = { version = "0.0.0", path = "../../crates/validator" } +rspc-binario = { version = "0.0.0", path = "../../crates/binario" } +rspc-tracing = { version = "0.0.0", path = "../../crates/tracing" } +rspc-openapi = { version = "0.0.0", path = "../../crates/openapi" } +validator = { version = "0.19.0", features = ["derive"] } diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index dc2ec51b..a3470416 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -2,15 +2,16 @@ use std::{marker::PhantomData, time::SystemTime}; use async_stream::stream; use rspc::{ - middleware::Middleware, Error2, Extension, Procedure2, ProcedureBuilder, ResolverInput, - ResolverOutput, Router2, + middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, + Router2, }; use rspc_cache::{cache, cache_ttl, CacheState, Memory}; use rspc_invalidation::Invalidate; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use specta::Type; use thiserror::Error; use tracing::info; +use validator::Validate; // `Clone` is only required for usage with Websockets #[derive(Clone)] @@ -21,6 +22,12 @@ pub struct Ctx { #[derive(Serialize, Type)] pub struct MyCustomType(String); +#[derive(Debug, Deserialize, Type, Validate)] +pub struct ValidatedType { + #[validate(email)] + mail: String, +} + #[derive(Type, Serialize)] #[serde(tag = "type")] #[specta(export = false)] @@ -102,15 +109,19 @@ fn mount() -> rspc::Router { router } -#[derive(Debug, Error, Serialize, Type)] +#[derive(Debug, Clone, Error, Serialize, Type)] +#[serde(tag = "type")] pub enum Error { #[error("you made a mistake: {0}")] Mistake(String), + #[error("validation: {0}")] + Validator(#[from] rspc_validator::RspcValidatorError), } impl Error2 for Error { fn into_resolver_error(self) -> rspc::ResolverError { - rspc::ResolverError::new(self.to_string(), None::) + // rspc::ResolverError::new(self.to_string(), Some(self)) // TODO: Typesafe way to achieve this + rspc::ResolverError::new(self.clone(), Some(self)) } } @@ -209,27 +220,36 @@ fn test_unstable_stuff(router: Router2) -> Router2 { Ok(()) }) }) - // .procedure("manualFlush", { - // ::builder() - // .manual_flush() - // .query(|ctx, id: String| async move { - // println!("Set cookies"); - // flush().await; - // println!("Do more stuff in background"); - // Ok(()) - // }) - // }) + // .procedure("sfmStatefulPost", { + // ::builder() + // // .with(Invalidator::mw(|ctx, input, event| { + // // event == InvalidateEvent::InvalidateKey(input.id) + // // })) + // .query(|_, id: String| async { + // // Fetch the post from the DB + // Ok(id) + // }) + // }) + // .procedure("manualFlush", { + // ::builder() + // .manual_flush() + // .query(|ctx, id: String| async move { + // println!("Set cookies"); + // flush().await; + // println!("Do more stuff in background"); + // Ok(()) + // }) + // }) + .procedure("validator", { + ::builder() + .with(rspc_validator::validate()) + .query(|ctx, input: ValidatedType| async move { + println!("{input:?}"); + Ok(()) + }) + }) + // ValidatedType - // .procedure("sfmStatefulPost", { - // ::builder() - // // .with(Invalidator::mw(|ctx, input, event| { - // // event == InvalidateEvent::InvalidateKey(input.id) - // // })) - // .query(|_, id: String| async { - // // Fetch the post from the DB - // Ok(id) - // }) - // }) // .procedure("fileupload", { // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) // }) From a6889caa48c4814767d25749c00ecc0293134ac4 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Wed, 25 Dec 2024 23:55:47 +0800 Subject: [PATCH 60/67] `rspc-zer` prototype --- crates/procedure/src/stream.rs | 5 +- crates/zer/Cargo.toml | 21 +++++ crates/zer/README.md | 18 ++++ crates/zer/src/lib.rs | 156 +++++++++++++++++++++++++++++++++ examples/actix-web/Cargo.toml | 1 + examples/axum/Cargo.toml | 1 + examples/axum/src/main.rs | 52 ++++++----- examples/bindings.ts | 4 +- examples/core/Cargo.toml | 2 + examples/core/src/lib.rs | 31 ++++++- 10 files changed, 264 insertions(+), 27 deletions(-) create mode 100644 crates/zer/Cargo.toml create mode 100644 crates/zer/README.md create mode 100644 crates/zer/src/lib.rs diff --git a/crates/procedure/src/stream.rs b/crates/procedure/src/stream.rs index 1c1ebdce..04dc6fab 100644 --- a/crates/procedure/src/stream.rs +++ b/crates/procedure/src/stream.rs @@ -527,7 +527,10 @@ impl) -> Result + Unpin, T Ok(v) => v, // TODO: Exposing this error to the client or not? // TODO: Error type??? - Err(err) => todo!(), + Err(err) => { + println!("Error serialzing {err:?}"); + todo!(); + } } }) }) diff --git a/crates/zer/Cargo.toml b/crates/zer/Cargo.toml new file mode 100644 index 00000000..3343654a --- /dev/null +++ b/crates/zer/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rspc-zer" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[dependencies] +rspc = { path = "../../rspc", features = ["unstable"] } +serde = "1" +specta = "=2.0.0-rc.20" +serde_json = "1" +jsonwebtoken = { version = "9", default-features = false } +cookie = { version = "0.18.1", features = ["percent-encode"] } + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/crates/zer/README.md b/crates/zer/README.md new file mode 100644 index 00000000..4fdc4377 --- /dev/null +++ b/crates/zer/README.md @@ -0,0 +1,18 @@ +# rspc Zer + +[![docs.rs](https://img.shields.io/crates/v/rspc-zer)](https://docs.rs/rspc-zer) + +> [!CAUTION] +> This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. + +Authorization library for rspc. + +Features: + - Secure session managemen with short-lived JWT tokens. + - OAuth maybe? + +## Example + +```rust +// Coming soon... +``` diff --git a/crates/zer/src/lib.rs b/crates/zer/src/lib.rs new file mode 100644 index 00000000..c1cdb318 --- /dev/null +++ b/crates/zer/src/lib.rs @@ -0,0 +1,156 @@ +//! Authorization library for rspc. +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", + html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" +)] + +use std::{ + borrow::Cow, + fmt, + marker::PhantomData, + str, + sync::{Arc, Mutex, PoisonError}, +}; + +use cookie::Cookie; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize}; +use specta::Type; + +type ResponseCookie = Arc>>; + +pub struct ZerResponse { + cookies: ResponseCookie, +} + +impl ZerResponse { + pub fn set_cookie_header(&self) -> Option { + self.cookies + .lock() + .unwrap_or_else(PoisonError::into_inner) + .clone() + } +} + +pub struct Zer { + cookie_name: Cow<'static, str>, + key: EncodingKey, + key2: DecodingKey, + cookies: Vec>, + resp_cookies: ResponseCookie, + phantom: PhantomData, +} + +impl Clone for Zer { + fn clone(&self) -> Self { + // TODO: Should we `Arc` some stuff? + Self { + cookie_name: self.cookie_name.clone(), + key: self.key.clone(), + key2: self.key2.clone(), + cookies: self.cookies.clone(), + resp_cookies: self.resp_cookies.clone(), + phantom: PhantomData, + } + } +} + +impl fmt::Debug for Zer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Zer").finish() + } +} + +impl Zer { + pub fn from_request( + cookie_name: impl Into>, + secret: &[u8], + cookie: Option>, + ) -> (Self, ZerResponse) { + let mut cookies = vec![]; + if let Some(cookie) = cookie { + // TODO: Error handling + for cookie in Cookie::split_parse_encoded(str::from_utf8(&cookie.as_ref()).unwrap()) { + cookies.push(cookie.unwrap().into_owned()); // TODO: Error handling + } + } + + let resp_cookies = ResponseCookie::default(); + + ( + Self { + cookie_name: cookie_name.into(), + key: EncodingKey::from_secret(secret), + key2: DecodingKey::from_secret(secret), + cookies, + resp_cookies: resp_cookies.clone(), + phantom: PhantomData, + }, + ZerResponse { + cookies: resp_cookies, + }, + ) + } + + pub fn session(&self) -> Result { + self.cookies + .iter() + .find(|cookie| cookie.name() == self.cookie_name) + .map(|cookie| { + let token = cookie.value(); + let mut v = Validation::new(Algorithm::HS256); + v.required_spec_claims = Default::default(); // TODO: This is very insecure! + + // TODO: error handling (maybe move this whole thing into `from_request`) + decode::(token, &self.key2, &v) + .unwrap() + // TODO: Expose the header somehow? + .claims + }) + .ok_or(UnauthorizedError) + } + + // TOOD: Ensure calls to `Self::session` return the new session within the same request + pub fn set_session(&self, session: &S) { + let token = encode(&Header::default(), &session, &self.key).unwrap(); + + self.resp_cookies + .lock() + .unwrap_or_else(PoisonError::into_inner) + // TODO: Don't `Clone` the cookie name + .replace(Cookie::new(self.cookie_name.clone(), token).to_string()); + } +} + +#[derive(Debug, Clone)] +pub struct UnauthorizedError; + +impl fmt::Display for UnauthorizedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unauthorized") + } +} + +impl std::error::Error for UnauthorizedError {} + +impl Serialize for UnauthorizedError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("Unauthorized", 1)?; + s.serialize_field("error", "Unauthorized")?; + s.end() + } +} + +impl Type for UnauthorizedError { + fn inline( + _type_map: &mut specta::TypeCollection, + _generics: specta::Generics, + ) -> specta::datatype::DataType { + specta::datatype::DataType::Primitive(specta::datatype::PrimitiveType::String) + } +} diff --git a/examples/actix-web/Cargo.toml b/examples/actix-web/Cargo.toml index f9904b31..65758687 100644 --- a/examples/actix-web/Cargo.toml +++ b/examples/actix-web/Cargo.toml @@ -12,3 +12,4 @@ actix-web = "4" actix-cors = "0.7.0" actix-multipart = "0.7.2" futures = "0.3" +rspc-zer = { version = "0.0.0", path = "../../crates/zer" } diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 8ed9c250..0fd13802 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -20,3 +20,4 @@ futures = "0.3" # TODO serde_json = "1.0.134" rspc-http = { version = "0.2.1", path = "../../integrations/http" } streamunordered = "0.5.4" +rspc-zer = { version = "0.0.0", path = "../../crates/zer" } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index d222c10d..cb0addd0 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,7 +1,7 @@ use axum::{ body::Body, extract::{Multipart, Request}, - http::{header, HeaderName, StatusCode}, + http::{header, request::Parts, HeaderMap, HeaderName, StatusCode}, routing::{get, on, post, MethodFilter, MethodRouter}, Json, }; @@ -54,23 +54,21 @@ async fn main() { let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) - .route( - "/upload", - post(|mut multipart: Multipart| async move { - println!("{:?}", multipart); - - while let Some(field) = multipart.next_field().await.unwrap() { - println!( - "{:?} {:?} {:?}", - field.name().map(|v| v.to_string()), - field.content_type().map(|v| v.to_string()), - field.collect::>().await - ); - } - - "Done!" - }), - ) + // .route( + // "/upload", + // post(|mut multipart: Multipart| async move { + // println!("{:?}", multipart); + // while let Some(field) = multipart.next_field().await.unwrap() { + // println!( + // "{:?} {:?} {:?}", + // field.name().map(|v| v.to_string()), + // field.content_type().map(|v| v.to_string()), + // field.collect::>().await + // ); + // } + // "Done!" + // }), + // ) .route( "/rspc/custom", post(|| async move { @@ -198,10 +196,16 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { // TODO: Document CDN caching options with this setup r.route( "/", - post(move |mut multipart: Multipart| async move { + post(move |parts: Parts, mut multipart: Multipart| async move { let invalidator = rspc_invalidation::Invalidator::default(); + let (zer, zer_response) = rspc_zer::Zer::from_request( + "session", + "some_secret".as_ref(), + parts.headers.get("cookie"), + ); let ctx = Ctx { invalidator: invalidator.clone(), + zer, }; let mut runtime = StreamUnordered::new(); @@ -240,7 +244,7 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { }; let procedure = procedures.get(&*name).unwrap(); - println!("{:?} {:?} {:?}", name, input, procedure); + // println!("{:?} {:?} {:?}", name, input, procedure); spawn( &mut runtime, @@ -269,8 +273,14 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { }) .await; + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, "text/x-rspc".parse().unwrap()); + if let Some(h) = zer_response.set_cookie_header() { + headers.insert(header::SET_COOKIE, h.parse().unwrap()); + } + ( - [(header::CONTENT_TYPE, "text/x-rspc")], + headers, Body::from_stream(Prototype { runtime, sfm: false, diff --git a/examples/bindings.ts b/examples/bindings.ts index 778a9b93..6dc63eed 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,12 +1,14 @@ // My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "validator"; input: any; result: any } | { key: "version"; input: null; result: string } | { key: "withoutBaseProcedure"; input: any; result: any }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "login"; input: any; result: any } | { key: "me"; input: any; result: any } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "validator"; input: any; result: any } | { key: "version"; input: null; result: string } | { key: "withoutBaseProcedure"; input: any; result: any }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { cached: { kind: "query", input: any, output: any, error: any }, echo: { kind: "query", input: string, output: string, error: unknown }, error: { kind: "query", input: null, output: string, error: unknown }, + login: { kind: "query", input: any, output: any, error: any }, + me: { kind: "query", input: any, output: any, error: any }, nested: { hello: { kind: "query", input: null, output: string, error: unknown }, }, diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index f9ce3738..8b9fbdc0 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -22,3 +22,5 @@ rspc-binario = { version = "0.0.0", path = "../../crates/binario" } rspc-tracing = { version = "0.0.0", path = "../../crates/tracing" } rspc-openapi = { version = "0.0.0", path = "../../crates/openapi" } validator = { version = "0.19.0", features = ["derive"] } +rspc-zer = { version = "0.0.0", path = "../../crates/zer" } +anyhow = "1.0.95" diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index a3470416..c9995407 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -7,16 +7,23 @@ use rspc::{ }; use rspc_cache::{cache, cache_ttl, CacheState, Memory}; use rspc_invalidation::Invalidate; +use rspc_zer::Zer; use serde::{Deserialize, Serialize}; use specta::Type; use thiserror::Error; use tracing::info; use validator::Validate; +#[derive(Clone, Serialize, Deserialize, Type)] +pub struct MySession { + name: String, +} + // `Clone` is only required for usage with Websockets #[derive(Clone)] pub struct Ctx { pub invalidator: Invalidator, + pub zer: Zer, } #[derive(Serialize, Type)] @@ -109,19 +116,27 @@ fn mount() -> rspc::Router { router } -#[derive(Debug, Clone, Error, Serialize, Type)] +#[derive(Debug, Error, Serialize, Type)] #[serde(tag = "type")] pub enum Error { #[error("you made a mistake: {0}")] Mistake(String), #[error("validation: {0}")] Validator(#[from] rspc_validator::RspcValidatorError), + #[error("authorization: {0}")] + Authorization(#[from] rspc_zer::UnauthorizedError), // TODO: This ends up being cringe: `{"type":"Authorization","error":"Unauthorized"}` + #[error("internal error: {0}")] + #[serde(skip)] + InternalError(#[from] anyhow::Error), } impl Error2 for Error { fn into_resolver_error(self) -> rspc::ResolverError { // rspc::ResolverError::new(self.to_string(), Some(self)) // TODO: Typesafe way to achieve this - rspc::ResolverError::new(self.clone(), Some(self)) + rspc::ResolverError::new( + self, + None::, // TODO: `Some(self)` but `anyhow::Error` is not `Clone` + ) } } @@ -243,12 +258,20 @@ fn test_unstable_stuff(router: Router2) -> Router2 { .procedure("validator", { ::builder() .with(rspc_validator::validate()) - .query(|ctx, input: ValidatedType| async move { + .query(|_, input: ValidatedType| async move { println!("{input:?}"); Ok(()) }) }) - // ValidatedType + .procedure("login", { + ::builder().query(|ctx, name: String| async move { + ctx.zer.set_session(&MySession { name }); + Ok(()) + }) + }) + .procedure("me", { + ::builder().query(|ctx, _: ()| async move { Ok(ctx.zer.session()?) }) + }) // .procedure("fileupload", { // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) From 67d48d498aa2c45da93bc3cdea4f5e75e97758c3 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 26 Dec 2024 11:44:03 +0800 Subject: [PATCH 61/67] `rspc-legacy` crate --- Cargo.toml | 14 + crates/binario/Cargo.toml | 4 +- crates/cache/Cargo.toml | 2 +- crates/cache/src/lib.rs | 4 +- crates/client/Cargo.toml | 4 +- crates/core/Cargo.toml | 8 +- crates/devtools/Cargo.toml | 8 +- crates/devtools/src/lib.rs | 4 +- crates/invalidation/Cargo.toml | 5 +- crates/invalidation/src/lib.rs | 4 +- crates/legacy/Cargo.toml | 33 ++ crates/legacy/README.md | 5 + crates/legacy/src/config.rs | 31 ++ crates/legacy/src/error.rs | 196 ++++++++++ crates/legacy/src/internal/jsonrpc.rs | 118 ++++++ crates/legacy/src/internal/jsonrpc_exec.rs | 292 ++++++++++++++ crates/legacy/src/internal/middleware.rs | 237 ++++++++++++ crates/legacy/src/internal/mod.rs | 14 + .../legacy/src/internal/procedure_builder.rs | 33 ++ crates/legacy/src/internal/procedure_store.rs | 53 +++ crates/legacy/src/lib.rs | 54 +++ crates/legacy/src/middleware.rs | 338 ++++++++++++++++ crates/legacy/src/resolver.rs | 125 ++++++ crates/legacy/src/resolver_result.rs | 67 ++++ crates/legacy/src/router.rs | 219 +++++++++++ crates/legacy/src/router_builder.rs | 365 ++++++++++++++++++ crates/legacy/src/selection.rs | 78 ++++ crates/openapi/Cargo.toml | 6 +- crates/openapi/src/lib.rs | 4 +- crates/procedure/Cargo.toml | 8 +- crates/tracing/Cargo.toml | 4 +- crates/tracing/src/lib.rs | 4 +- crates/validator/Cargo.toml | 4 +- crates/validator/src/lib.rs | 4 +- crates/zer/Cargo.toml | 6 +- crates/zer/src/lib.rs | 4 +- rspc/Cargo.toml | 1 + 37 files changed, 2317 insertions(+), 43 deletions(-) create mode 100644 crates/legacy/Cargo.toml create mode 100644 crates/legacy/README.md create mode 100644 crates/legacy/src/config.rs create mode 100644 crates/legacy/src/error.rs create mode 100644 crates/legacy/src/internal/jsonrpc.rs create mode 100644 crates/legacy/src/internal/jsonrpc_exec.rs create mode 100644 crates/legacy/src/internal/middleware.rs create mode 100644 crates/legacy/src/internal/mod.rs create mode 100644 crates/legacy/src/internal/procedure_builder.rs create mode 100644 crates/legacy/src/internal/procedure_store.rs create mode 100644 crates/legacy/src/lib.rs create mode 100644 crates/legacy/src/middleware.rs create mode 100644 crates/legacy/src/resolver.rs create mode 100644 crates/legacy/src/resolver_result.rs create mode 100644 crates/legacy/src/router.rs create mode 100644 crates/legacy/src/router_builder.rs create mode 100644 crates/legacy/src/selection.rs diff --git a/Cargo.toml b/Cargo.toml index 877ad0e0..0afc10a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,20 @@ members = [ "./examples/tauri/src-tauri", ] +[workspace.dependencies] +# Private +specta-typescript = { version = "0.0.7", default-features = false } +pin-project-lite = { version = "0.2", default-features = false } +erased-serde = { version = "0.4", default-features = false } + +# Public +specta = { version = "=2.0.0-rc.20", default-features = false } +serde = { version = "1", default-features = false } +serde_json = { version = "1", default-features = false } +futures = { version = "0.3", default-features = false } +futures-core = { version = "0.3", default-features = false } +tracing = { version = "0.1", default-features = false } + [workspace.lints.clippy] all = { level = "warn", priority = -1 } cargo = { level = "warn", priority = -1 } diff --git a/crates/binario/Cargo.toml b/crates/binario/Cargo.toml index 8c4734d3..d30dff90 100644 --- a/crates/binario/Cargo.toml +++ b/crates/binario/Cargo.toml @@ -1,11 +1,13 @@ [package] name = "rspc-binario" +description = "Binario support for rspc" version = "0.0.0" edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc" } +rspc = { path = "../../rspc", features = ["unstable"] } +specta = { workspace = true } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/cache/Cargo.toml b/crates/cache/Cargo.toml index dea790a7..39e5c3c1 100644 --- a/crates/cache/Cargo.toml +++ b/crates/cache/Cargo.toml @@ -6,7 +6,7 @@ publish = false [dependencies] moka = { version = "0.12.8", features = ["sync"] } -pin-project-lite = "0.2.15" +pin-project-lite = { workspace = true } rspc = { path = "../../rspc", features = ["unstable"] } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/crates/cache/src/lib.rs b/crates/cache/src/lib.rs index 5c92f141..f563db26 100644 --- a/crates/cache/src/lib.rs +++ b/crates/cache/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] mod memory; diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 481398ce..51ecb154 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -19,5 +19,5 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] reqwest = { version = "0.12.9", features = ["json"] } rspc-procedure = { version = "0.0.1", path = "../procedure" } -serde = { version = "1.0.216", features = ["derive"] } # TODO: Drop derive feature? -serde_json = "1.0.134" +serde = { workspace = true, features = ["derive"] } # TODO: Drop derive feature? +serde_json = { workspace = true } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d52a98c7..8fa6a00c 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -17,14 +17,14 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] # Public -futures-core = { version = "0.3", default-features = false } -serde = { version = "1", default-features = false } +futures-core = { workspace = true, default-features = false } +serde = { workspace = true, default-features = false } # Private -erased-serde = { version = "0.4", default-features = false, features = [ +erased-serde = { workspace = true, default-features = false, features = [ "std", ] } -pin-project-lite = { version = "0.2", default-features = false } +pin-project-lite = { workspace = true, default-features = false } [lints] workspace = true diff --git a/crates/devtools/Cargo.toml b/crates/devtools/Cargo.toml index 53b840f3..1ada8008 100644 --- a/crates/devtools/Cargo.toml +++ b/crates/devtools/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" publish = false [dependencies] -futures = "0.3.31" +futures = { workspace = true } rspc-procedure = { path = "../../crates/procedure" } -serde = { version = "1.0.216", features = ["derive"] } -specta = { version = "=2.0.0-rc.20", features = ["derive"] } -tracing = "0.1.41" +serde = { workspace = true, features = ["derive"] } +specta = { workspace = true, features = ["derive"] } +tracing = { workspace = true } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/devtools/src/lib.rs b/crates/devtools/src/lib.rs index f3ca3b13..9b92c99c 100644 --- a/crates/devtools/src/lib.rs +++ b/crates/devtools/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] // http://[::]:4000/rspc/~rspc.devtools.meta diff --git a/crates/invalidation/Cargo.toml b/crates/invalidation/Cargo.toml index 3fadf786..60eb93d9 100644 --- a/crates/invalidation/Cargo.toml +++ b/crates/invalidation/Cargo.toml @@ -5,10 +5,9 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -async-stream = "0.3.6" rspc = { path = "../../rspc", features = ["unstable"] } -serde = "1.0.216" -serde_json = "1.0.134" +serde = { workspace = true } +serde_json = { workspace = true } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/invalidation/src/lib.rs b/crates/invalidation/src/lib.rs index 226ede50..a7fb8775 100644 --- a/crates/invalidation/src/lib.rs +++ b/crates/invalidation/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] use std::{ diff --git a/crates/legacy/Cargo.toml b/crates/legacy/Cargo.toml new file mode 100644 index 00000000..ea7f3775 --- /dev/null +++ b/crates/legacy/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "rspc-legacy" +description = "The rspc 0.3.1 syntax implemented on top of the 0.4.0 core" +version = "0.0.0" +edition = "2021" +publish = false # TODO: Crate metadata & publish + +[features] +default = [] +# Warnings for deprecations +deprecated = [] + +[dependencies] +rspc-procedure = { path = "../procedure" } +serde = { workspace = true } +futures = { workspace = true } +specta = { workspace = true, features = [ + "serde", + "serde_json", + "derive", # TODO: remove this +] } +specta-typescript = { version = "=0.0.7", features = [] } +serde_json = { workspace = true } +thiserror = "2.0.9" +tokio = { version = "1.42.0", features = ["macros", "sync", "rt"] } + +# /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features +[package.metadata."docs.rs"] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/crates/legacy/README.md b/crates/legacy/README.md new file mode 100644 index 00000000..556640a4 --- /dev/null +++ b/crates/legacy/README.md @@ -0,0 +1,5 @@ +# rspc Legacy + +[![docs.rs](https://img.shields.io/crates/v/rspc-legacy)](https://docs.rs/rspc-legacy) + +The rspc 0.3.1 syntax implemented on top of the 0.4.0 core. This is designed to make incremental migration easier. diff --git a/crates/legacy/src/config.rs b/crates/legacy/src/config.rs new file mode 100644 index 00000000..67d34e7c --- /dev/null +++ b/crates/legacy/src/config.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +/// TODO +#[derive(Default)] +pub struct Config { + pub(crate) export_bindings_on_build: Option, + pub(crate) bindings_header: Option<&'static str>, +} + +impl Config { + pub fn new() -> Self { + Default::default() + } + + /// will export the bindings of the generated router to a folder every time the router is built. + /// Note: The bindings are only exported when `debug_assertions` are enabled (Rust is in debug mode). + pub fn export_ts_bindings(mut self, export_path: TPath) -> Self + where + PathBuf: From, + { + self.export_bindings_on_build = Some(PathBuf::from(export_path)); + self + } + + /// allows you to add a custom string to the top of the exported Typescript bindings file. + /// This is useful if you want to disable ESLint or Prettier. + pub fn set_ts_bindings_header(mut self, custom: &'static str) -> Self { + self.bindings_header = Some(custom); + self + } +} diff --git a/crates/legacy/src/error.rs b/crates/legacy/src/error.rs new file mode 100644 index 00000000..3d513fea --- /dev/null +++ b/crates/legacy/src/error.rs @@ -0,0 +1,196 @@ +use std::{error, fmt, sync::Arc}; + +use serde::Serialize; +use specta::Type; + +use crate::internal::jsonrpc::JsonRPCError; + +#[derive(thiserror::Error, Debug)] +pub enum ExecError { + #[error("the requested operation '{0}' is not supported by this server")] + OperationNotFound(String), + #[error("error deserializing procedure arguments: {0}")] + DeserializingArgErr(serde_json::Error), + #[error("error serializing procedure result: {0}")] + SerializingResultErr(serde_json::Error), + #[error("error in axum extractor")] + AxumExtractorError, + #[error("invalid JSON-RPC version")] + InvalidJsonRpcVersion, + #[error("method '{0}' is not supported by this endpoint.")] // TODO: Better error message + UnsupportedMethod(String), + #[error("resolver threw error")] + ErrResolverError(#[from] Error), + #[error("error creating subscription with null id")] + ErrSubscriptionWithNullId, + #[error("error creating subscription with duplicate id")] + ErrSubscriptionDuplicateId, +} + +impl From for Error { + fn from(v: ExecError) -> Error { + match v { + ExecError::OperationNotFound(_) => Error { + code: ErrorCode::NotFound, + message: "the requested operation is not supported by this server".to_string(), + cause: None, + }, + ExecError::DeserializingArgErr(err) => Error { + code: ErrorCode::BadRequest, + message: "error deserializing procedure arguments".to_string(), + cause: Some(Arc::new(err)), + }, + ExecError::SerializingResultErr(err) => Error { + code: ErrorCode::InternalServerError, + message: "error serializing procedure result".to_string(), + cause: Some(Arc::new(err)), + }, + ExecError::AxumExtractorError => Error { + code: ErrorCode::BadRequest, + message: "Error running Axum extractors on the HTTP request".into(), + cause: None, + }, + ExecError::InvalidJsonRpcVersion => Error { + code: ErrorCode::BadRequest, + message: "invalid JSON-RPC version".into(), + cause: None, + }, + ExecError::ErrResolverError(err) => err, + ExecError::UnsupportedMethod(_) => Error { + code: ErrorCode::BadRequest, + message: "unsupported metho".into(), + cause: None, + }, + ExecError::ErrSubscriptionWithNullId => Error { + code: ErrorCode::BadRequest, + message: "error creating subscription with null request id".into(), + cause: None, + }, + ExecError::ErrSubscriptionDuplicateId => Error { + code: ErrorCode::BadRequest, + message: "error creating subscription with duplicate id".into(), + cause: None, + }, + } + } +} + +impl From for JsonRPCError { + fn from(err: ExecError) -> Self { + let x: Error = err.into(); + x.into() + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ExportError { + #[error("IO error exporting bindings: {0}")] + IOErr(#[from] std::io::Error), +} + +#[derive(Debug, Clone, Serialize, Type)] +#[allow(dead_code)] +pub struct Error { + pub(crate) code: ErrorCode, + pub(crate) message: String, + #[serde(skip)] + pub(crate) cause: Option>, // We are using `Arc` instead of `Box` so we can clone the error cause `Clone` isn't dyn safe. +} + +impl From for JsonRPCError { + fn from(err: Error) -> Self { + JsonRPCError { + code: err.code.to_status_code() as i32, + message: err.message, + data: None, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "rspc::Error {{ code: {:?}, message: {} }}", + self.code, self.message + ) + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl Error { + pub const fn new(code: ErrorCode, message: String) -> Self { + Error { + code, + message, + cause: None, + } + } + + pub fn with_cause(code: ErrorCode, message: String, cause: TErr) -> Self + where + TErr: std::error::Error + Send + Sync + 'static, + { + Self { + code, + message, + cause: Some(Arc::new(cause)), + } + } +} + +/// TODO +#[derive(Debug, Clone, Serialize, Type, PartialEq, Eq)] +pub enum ErrorCode { + BadRequest, + Unauthorized, + Forbidden, + NotFound, + Timeout, + Conflict, + PreconditionFailed, + PayloadTooLarge, + MethodNotSupported, + ClientClosedRequest, + InternalServerError, +} + +impl ErrorCode { + pub fn to_status_code(&self) -> u16 { + match self { + ErrorCode::BadRequest => 400, + ErrorCode::Unauthorized => 401, + ErrorCode::Forbidden => 403, + ErrorCode::NotFound => 404, + ErrorCode::Timeout => 408, + ErrorCode::Conflict => 409, + ErrorCode::PreconditionFailed => 412, + ErrorCode::PayloadTooLarge => 413, + ErrorCode::MethodNotSupported => 405, + ErrorCode::ClientClosedRequest => 499, + ErrorCode::InternalServerError => 500, + } + } + + pub const fn from_status_code(status_code: u16) -> Option { + match status_code { + 400 => Some(ErrorCode::BadRequest), + 401 => Some(ErrorCode::Unauthorized), + 403 => Some(ErrorCode::Forbidden), + 404 => Some(ErrorCode::NotFound), + 408 => Some(ErrorCode::Timeout), + 409 => Some(ErrorCode::Conflict), + 412 => Some(ErrorCode::PreconditionFailed), + 413 => Some(ErrorCode::PayloadTooLarge), + 405 => Some(ErrorCode::MethodNotSupported), + 499 => Some(ErrorCode::ClientClosedRequest), + 500 => Some(ErrorCode::InternalServerError), + _ => None, + } + } +} diff --git a/crates/legacy/src/internal/jsonrpc.rs b/crates/legacy/src/internal/jsonrpc.rs new file mode 100644 index 00000000..a93c45e2 --- /dev/null +++ b/crates/legacy/src/internal/jsonrpc.rs @@ -0,0 +1,118 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use specta::Type; + +pub use super::jsonrpc_exec::*; + +#[derive(Debug, Clone, Deserialize, Serialize, Type, PartialEq, Eq, Hash)] +#[serde(untagged)] +pub enum RequestId { + Null, + Number(u32), + String(String), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] // TODO: Type on this +pub struct Request { + pub jsonrpc: Option, // This is required in the JsonRPC spec but I make it optional. + pub id: RequestId, + #[serde(flatten)] + pub inner: RequestInner, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Type)] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +pub enum RequestInner { + Query { + path: String, + input: Option, + }, + Mutation { + path: String, + input: Option, + }, + Subscription { + path: String, + input: (RequestId, Option), + }, + SubscriptionStop { + input: RequestId, + }, +} + +#[derive(Debug, Clone, Serialize)] // TODO: Add `specta::Type` when supported +pub struct Response { + pub jsonrpc: &'static str, + pub id: RequestId, + pub result: ResponseInner, +} + +#[derive(Debug, Clone, Serialize, Type)] +#[serde(tag = "type", content = "data", rename_all = "camelCase")] +pub enum ResponseInner { + Event(Value), + Response(Value), + Error(JsonRPCError), +} + +#[derive(Debug, Clone, Serialize, Type)] +pub struct JsonRPCError { + pub code: i32, + pub message: String, + pub data: Option, +} + +// #[cfg(test)] +// mod tests { +// use std::{fs::File, io::Write, path::PathBuf}; + +// use super::*; + +// #[test] +// fn export_internal_bindings() { +// // let mut file = File::create( +// // PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./packages/client/src/types.ts"), +// // ) +// // .unwrap(); +// // file.write_all( +// // b"// Do not modify this file. It was generated from the Rust types by running ``.\n\n", +// // ) +// // .unwrap(); +// // // TODO: Add an API into Specta which allows exporting a type and all types it depends on. +// // file.write_all(format!("{}\n\n", specta::ts_export::().unwrap()).as_bytes()) +// // .unwrap(); +// // file.write_all(format!("{}\n\n", specta::ts_export::().unwrap()).as_bytes()) +// // .unwrap(); +// } + +// #[test] +// fn test_request_id() { +// // println!( +// // "{}", +// // serde_json::to_string(&Request { +// // jsonrpc: None, +// // id: RequestId::Null, +// // inner: RequestInner::Query { +// // path: "test".into(), +// // input: None, +// // }, +// // }) +// // .unwrap() +// // ); +// todo!(); + +// // TODO: Test serde + +// // TODO: Test specta +// } + +// #[test] +// fn test_jsonrpc_request() { +// todo!(); +// } + +// #[test] +// fn test_jsonrpc_response() { +// todo!(); +// } +// } diff --git a/crates/legacy/src/internal/jsonrpc_exec.rs b/crates/legacy/src/internal/jsonrpc_exec.rs new file mode 100644 index 00000000..39e9cd3c --- /dev/null +++ b/crates/legacy/src/internal/jsonrpc_exec.rs @@ -0,0 +1,292 @@ +use std::{collections::HashMap, sync::Arc}; + +use futures::StreamExt; +use serde_json::Value; +use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; + +use crate::{internal::jsonrpc, ExecError, Router}; + +use super::{ + jsonrpc::{RequestId, RequestInner, ResponseInner}, + ProcedureKind, RequestContext, ValueOrStream, +}; + +// TODO: Deduplicate this function with the httpz integration + +pub enum SubscriptionMap<'a> { + Ref(&'a mut HashMap>), + Mutex(&'a Mutex>>), + None, +} + +impl<'a> SubscriptionMap<'a> { + pub async fn has_subscription(&self, id: &RequestId) -> bool { + match self { + SubscriptionMap::Ref(map) => map.contains_key(id), + SubscriptionMap::Mutex(map) => { + let map = map.lock().await; + map.contains_key(id) + } + SubscriptionMap::None => unreachable!(), + } + } + + pub async fn insert(&mut self, id: RequestId, tx: oneshot::Sender<()>) { + match self { + SubscriptionMap::Ref(map) => { + map.insert(id, tx); + } + SubscriptionMap::Mutex(map) => { + let mut map = map.lock().await; + map.insert(id, tx); + } + SubscriptionMap::None => unreachable!(), + } + } + + pub async fn remove(&mut self, id: &RequestId) { + match self { + SubscriptionMap::Ref(map) => { + map.remove(id); + } + SubscriptionMap::Mutex(map) => { + let mut map = map.lock().await; + map.remove(id); + } + SubscriptionMap::None => unreachable!(), + } + } +} +pub enum Sender<'a> { + Channel(&'a mut mpsc::Sender), + ResponseChannel(&'a mut mpsc::UnboundedSender), + Broadcast(&'a broadcast::Sender), + Response(Option), +} + +pub enum Sender2 { + Channel(mpsc::Sender), + ResponseChannel(mpsc::UnboundedSender), + Broadcast(broadcast::Sender), +} + +impl Sender2 { + pub async fn send( + &mut self, + resp: jsonrpc::Response, + ) -> Result<(), mpsc::error::SendError> { + match self { + Self::Channel(tx) => tx.send(resp).await?, + Self::ResponseChannel(tx) => tx.send(resp)?, + Self::Broadcast(tx) => { + let _ = tx.send(resp).map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); + }); + } + } + + Ok(()) + } +} + +impl<'a> Sender<'a> { + pub async fn send( + &mut self, + resp: jsonrpc::Response, + ) -> Result<(), mpsc::error::SendError> { + match self { + Self::Channel(tx) => tx.send(resp).await?, + Self::ResponseChannel(tx) => tx.send(resp)?, + Self::Broadcast(tx) => { + let _ = tx.send(resp).map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); + }); + } + Self::Response(r) => { + *r = Some(resp); + } + } + + Ok(()) + } + + pub fn sender2(&mut self) -> Sender2 { + match self { + Self::Channel(tx) => Sender2::Channel(tx.clone()), + Self::ResponseChannel(tx) => Sender2::ResponseChannel(tx.clone()), + Self::Broadcast(tx) => Sender2::Broadcast(tx.clone()), + Self::Response(_) => unreachable!(), + } + } +} + +pub async fn handle_json_rpc( + ctx: TCtx, + req: jsonrpc::Request, + router: &Arc>, + sender: &mut Sender<'_>, + subscriptions: &mut SubscriptionMap<'_>, +) where + TCtx: 'static, +{ + if req.jsonrpc.is_some() && req.jsonrpc.as_deref() != Some("2.0") { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error(ExecError::InvalidJsonRpcVersion.into()), + }) + .await + .map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); + }); + } + + let (path, input, procedures, sub_id) = match req.inner { + RequestInner::Query { path, input } => (path, input, router.queries(), None), + RequestInner::Mutation { path, input } => (path, input, router.mutations(), None), + RequestInner::Subscription { path, input } => { + (path, input.1, router.subscriptions(), Some(input.0)) + } + RequestInner::SubscriptionStop { input } => { + subscriptions.remove(&input).await; + return; + } + }; + + let result = match procedures + .get(&path) + .ok_or_else(|| ExecError::OperationNotFound(path.clone())) + .and_then(|v| { + v.exec.call( + ctx, + input.unwrap_or(Value::Null), + RequestContext { + kind: ProcedureKind::Query, + path, + }, + ) + }) { + Ok(op) => match op.into_value_or_stream().await { + Ok(ValueOrStream::Value(v)) => ResponseInner::Response(v), + Ok(ValueOrStream::Stream(mut stream)) => { + if matches!(sender, Sender::Response(_)) + || matches!(subscriptions, SubscriptionMap::None) + { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error( + ExecError::UnsupportedMethod("Subscription".to_string()).into(), + ), + }) + .await + .map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); + }); + } + + if let Some(id) = sub_id { + if matches!(id, RequestId::Null) { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error( + ExecError::ErrSubscriptionWithNullId.into(), + ), + }) + .await + .map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); + }); + } else if subscriptions.has_subscription(&id).await { + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id.clone(), + result: ResponseInner::Error( + ExecError::ErrSubscriptionDuplicateId.into(), + ), + }) + .await + .map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {}", _err); + }); + } + + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + subscriptions.insert(id.clone(), shutdown_tx).await; + let mut sender2 = sender.sender2(); + tokio::spawn(async move { + loop { + tokio::select! { + biased; // Note: Order matters + _ = &mut shutdown_rx => { + // #[cfg(feature = "tracing")] + // tracing::debug!("Removing subscription with id '{:?}'", id); + break; + } + v = stream.next() => { + match v { + Some(Ok(v)) => { + let _ = sender2.send(jsonrpc::Response { + jsonrpc: "2.0", + id: id.clone(), + result: ResponseInner::Event(v), + }) + .await + .map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); + }); + } + Some(Err(_err)) => { + // #[cfg(feature = "tracing")] + // tracing::error!("Subscription error: {:?}", _err); + } + None => { + break; + } + } + } + } + } + }); + } + + return; + } + Err(err) => { + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); + + ResponseInner::Error(err.into()) + } + }, + Err(err) => { + // #[cfg(feature = "tracing")] + // tracing::error!("Error executing operation: {:?}", err); + ResponseInner::Error(err.into()) + } + }; + + let _ = sender + .send(jsonrpc::Response { + jsonrpc: "2.0", + id: req.id, + result, + }) + .await + .map_err(|_err| { + // #[cfg(feature = "tracing")] + // tracing::error!("Failed to send response: {:?}", _err); + }); +} diff --git a/crates/legacy/src/internal/middleware.rs b/crates/legacy/src/internal/middleware.rs new file mode 100644 index 00000000..c75039dd --- /dev/null +++ b/crates/legacy/src/internal/middleware.rs @@ -0,0 +1,237 @@ +use std::{fmt, future::Future, marker::PhantomData, pin::Pin, sync::Arc}; + +use futures::Stream; +use serde_json::Value; +use specta::Type; + +use crate::{ExecError, MiddlewareLike}; + +pub trait MiddlewareBuilderLike { + type LayerContext: 'static; + + fn build(&self, next: T) -> Box> + where + T: Layer; +} + +pub struct MiddlewareMerger +where + TMiddleware: MiddlewareBuilderLike, + TIncomingMiddleware: MiddlewareBuilderLike, +{ + pub middleware: TMiddleware, + pub middleware2: TIncomingMiddleware, + pub phantom: PhantomData<(TCtx, TLayerCtx)>, +} + +impl MiddlewareBuilderLike + for MiddlewareMerger +where + TCtx: 'static, + TLayerCtx: 'static, + TNewLayerCtx: 'static, + TMiddleware: MiddlewareBuilderLike, + TIncomingMiddleware: MiddlewareBuilderLike, +{ + type LayerContext = TNewLayerCtx; + + fn build(&self, next: T) -> Box> + where + T: Layer, + { + self.middleware.build(self.middleware2.build(next)) + } +} + +pub struct MiddlewareLayerBuilder +where + TCtx: Send + Sync + 'static, + TLayerCtx: Send + Sync + 'static, + TNewLayerCtx: Send + Sync + 'static, + TMiddleware: MiddlewareBuilderLike + Send + 'static, + TNewMiddleware: MiddlewareLike, +{ + pub middleware: TMiddleware, + pub mw: TNewMiddleware, + pub phantom: PhantomData<(TCtx, TLayerCtx, TNewLayerCtx)>, +} + +impl MiddlewareBuilderLike + for MiddlewareLayerBuilder +where + TCtx: Send + Sync + 'static, + TLayerCtx: Send + Sync + 'static, + TNewLayerCtx: Send + Sync + 'static, + TMiddleware: MiddlewareBuilderLike + Send + 'static, + TNewMiddleware: MiddlewareLike + Send + Sync + 'static, +{ + type LayerContext = TNewLayerCtx; + + fn build(&self, next: T) -> Box> + where + T: Layer + Sync, + { + self.middleware.build(MiddlewareLayer { + next: Arc::new(next), + mw: self.mw.clone(), + phantom: PhantomData, + }) + } +} + +pub struct MiddlewareLayer +where + TLayerCtx: Send + 'static, + TNewLayerCtx: Send + 'static, + TMiddleware: Layer + 'static, + TNewMiddleware: MiddlewareLike + Send + Sync + 'static, +{ + next: Arc, // TODO: Avoid arcing this if possible + mw: TNewMiddleware, + phantom: PhantomData<(TLayerCtx, TNewLayerCtx)>, +} + +impl Layer + for MiddlewareLayer +where + TLayerCtx: Send + Sync + 'static, + TNewLayerCtx: Send + Sync + 'static, + TMiddleware: Layer + Sync + 'static, + TNewMiddleware: MiddlewareLike + Send + Sync + 'static, +{ + fn call( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + ) -> Result { + self.mw.handle(ctx, input, req, self.next.clone()) + } +} + +pub struct BaseMiddleware(PhantomData) +where + TCtx: 'static; + +impl Default for BaseMiddleware +where + TCtx: 'static, +{ + fn default() -> Self { + Self(PhantomData) + } +} + +impl MiddlewareBuilderLike for BaseMiddleware +where + TCtx: Send + 'static, +{ + type LayerContext = TCtx; + + fn build(&self, next: T) -> Box> + where + T: Layer, + { + Box::new(next) + } +} + +// TODO: Rename this so it doesn't conflict with the middleware builder struct +pub trait Layer: Send + Sync + 'static { + fn call(&self, a: TLayerCtx, b: Value, c: RequestContext) -> Result; +} + +pub struct ResolverLayer +where + TLayerCtx: Send + Sync + 'static, + T: Fn(TLayerCtx, Value, RequestContext) -> Result + + Send + + Sync + + 'static, +{ + pub func: T, + pub phantom: PhantomData, +} + +impl Layer for ResolverLayer +where + TLayerCtx: Send + Sync + 'static, + T: Fn(TLayerCtx, Value, RequestContext) -> Result + + Send + + Sync + + 'static, +{ + fn call(&self, a: TLayerCtx, b: Value, c: RequestContext) -> Result { + (self.func)(a, b, c) + } +} + +impl Layer for Box + 'static> +where + TLayerCtx: 'static, +{ + fn call(&self, a: TLayerCtx, b: Value, c: RequestContext) -> Result { + (**self).call(a, b, c) + } +} + +// TODO: This is a clone of `rspc::ProcedureKind`. I don't like us having both but we need it for the dependency tree to work. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] +#[specta(rename_all = "camelCase")] +pub enum ProcedureKind { + Query, + Mutation, + Subscription, +} + +impl fmt::Display for ProcedureKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Query => write!(f, "Query"), + Self::Mutation => write!(f, "Mutation"), + Self::Subscription => write!(f, "Subscription"), + } + } +} + +// TODO: Maybe rename to `Request` or something else. Also move into Public API cause it might be used in middleware +#[derive(Debug, Clone)] +pub struct RequestContext { + pub kind: ProcedureKind, + pub path: String, // TODO: String slice?? +} + +pub enum ValueOrStream { + Value(Value), + Stream(Pin> + Send>>), +} + +pub enum ValueOrStreamOrFutureStream { + Value(Value), + Stream(Pin> + Send>>), +} + +pub enum LayerResult { + Future(Pin> + Send>>), + Stream(Pin> + Send>>), + FutureValueOrStream(Pin> + Send>>), + FutureValueOrStreamOrFutureStream( + Pin> + Send>>, + ), + Ready(Result), +} + +impl LayerResult { + pub async fn into_value_or_stream(self) -> Result { + match self { + LayerResult::Stream(stream) => Ok(ValueOrStream::Stream(stream)), + LayerResult::Future(fut) => Ok(ValueOrStream::Value(fut.await?)), + LayerResult::FutureValueOrStream(fut) => Ok(fut.await?), + LayerResult::FutureValueOrStreamOrFutureStream(fut) => Ok(match fut.await? { + ValueOrStreamOrFutureStream::Value(val) => ValueOrStream::Value(val), + ValueOrStreamOrFutureStream::Stream(stream) => ValueOrStream::Stream(stream), + }), + LayerResult::Ready(res) => Ok(ValueOrStream::Value(res?)), + } + } +} diff --git a/crates/legacy/src/internal/mod.rs b/crates/legacy/src/internal/mod.rs new file mode 100644 index 00000000..9508e142 --- /dev/null +++ b/crates/legacy/src/internal/mod.rs @@ -0,0 +1,14 @@ +//! Internal types which power rspc. The module provides no guarantee of compatibility between updates, so you should be careful rely on types from it. + +mod jsonrpc_exec; +mod middleware; +mod procedure_builder; +mod procedure_store; + +pub(crate) use middleware::*; +pub(crate) use procedure_builder::*; +pub(crate) use procedure_store::*; + +// Used by `rspc_axum` +pub use middleware::ProcedureKind; +pub mod jsonrpc; diff --git a/crates/legacy/src/internal/procedure_builder.rs b/crates/legacy/src/internal/procedure_builder.rs new file mode 100644 index 00000000..ff680db0 --- /dev/null +++ b/crates/legacy/src/internal/procedure_builder.rs @@ -0,0 +1,33 @@ +use std::{marker::PhantomData, ops::Deref}; + +pub struct UnbuiltProcedureBuilder { + deref_handler: fn(TResolver) -> BuiltProcedureBuilder, + phantom: PhantomData, +} + +impl Default for UnbuiltProcedureBuilder { + fn default() -> Self { + Self { + deref_handler: |resolver| BuiltProcedureBuilder { resolver }, + phantom: PhantomData, + } + } +} + +impl UnbuiltProcedureBuilder { + pub fn resolver(self, resolver: TResolver) -> BuiltProcedureBuilder { + (self.deref_handler)(resolver) + } +} + +impl Deref for UnbuiltProcedureBuilder { + type Target = fn(resolver: TResolver) -> BuiltProcedureBuilder; + + fn deref(&self) -> &Self::Target { + &self.deref_handler + } +} + +pub struct BuiltProcedureBuilder { + pub resolver: TResolver, +} diff --git a/crates/legacy/src/internal/procedure_store.rs b/crates/legacy/src/internal/procedure_store.rs new file mode 100644 index 00000000..432920d9 --- /dev/null +++ b/crates/legacy/src/internal/procedure_store.rs @@ -0,0 +1,53 @@ +use std::collections::BTreeMap; + +use specta::DataType; + +use super::Layer; + +// TODO: Make private +#[derive(Debug)] +pub struct ProcedureDataType { + pub arg_ty: DataType, + pub result_ty: DataType, +} + +// TODO: Make private +pub struct Procedure { + pub exec: Box>, + pub ty: ProcedureDataType, +} + +pub struct ProcedureStore { + name: &'static str, + pub store: BTreeMap>, +} + +impl ProcedureStore { + pub fn new(name: &'static str) -> Self { + Self { + name, + store: Default::default(), + } + } + + pub fn append(&mut self, key: String, exec: Box>, ty: ProcedureDataType) { + #[allow(clippy::panic)] + if key.is_empty() || key == "ws" || key.starts_with("rpc.") || key.starts_with("rspc.") { + panic!( + "rspc error: attempted to create {} operation named '{}', however this name is not allowed.", + self.name, + key + ); + } + + #[allow(clippy::panic)] + if self.store.contains_key(&key) { + panic!( + "rspc error: {} operation already has resolver with name '{}'", + self.name, key + ); + } + + self.store.insert(key, Procedure { exec, ty }); + } +} diff --git a/crates/legacy/src/lib.rs b/crates/legacy/src/lib.rs new file mode 100644 index 00000000..3bd73bf6 --- /dev/null +++ b/crates/legacy/src/lib.rs @@ -0,0 +1,54 @@ +//! The rspc 0.3.1 syntax implemented on top of the 0.4.0 core. +//! +//! This allows incremental migration from the old syntax to the new syntax with the minimal breaking changes. +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" +)] + +mod config; +mod error; +mod middleware; +mod resolver; +mod resolver_result; +mod router; +mod router_builder; +mod selection; + +#[cfg_attr( + feature = "deprecated", + deprecated = "This is replaced by `rspc::Typescript`" +)] +pub use config::Config; +pub use error::{Error, ErrorCode, ExecError, ExportError}; +pub use middleware::{ + Middleware, MiddlewareBuilder, MiddlewareContext, MiddlewareLike, MiddlewareWithResponseHandler, +}; +pub use resolver::{typedef, DoubleArgMarker, DoubleArgStreamMarker, Resolver, StreamResolver}; +pub use resolver_result::{FutureMarker, RequestLayer, ResultMarker, SerializeMarker}; +pub use router::{ExecKind, Router}; +pub use router_builder::RouterBuilder; + +pub mod internal; + +#[cfg_attr( + feature = "deprecated", + deprecated = "This is no longer going to included. You can copy it into your project if you need it." +)] +#[cfg(debug_assertions)] +#[allow(clippy::panic)] +pub fn test_result_type() { + panic!("You should not call `test_type` at runtime. This is just a debugging tool."); +} + +#[cfg_attr( + feature = "deprecated", + deprecated = "This is no longer going to included. You can copy it into your project if you need it." +)] +#[cfg(debug_assertions)] +#[allow(clippy::panic)] +pub fn test_result_value(_: T) { + panic!("You should not call `test_type` at runtime. This is just a debugging tool."); +} diff --git a/crates/legacy/src/middleware.rs b/crates/legacy/src/middleware.rs new file mode 100644 index 00000000..b1a7718c --- /dev/null +++ b/crates/legacy/src/middleware.rs @@ -0,0 +1,338 @@ +use futures::StreamExt; +use serde_json::Value; +use std::{future::Future, marker::PhantomData, sync::Arc}; + +use crate::{ + internal::{Layer, LayerResult, RequestContext, ValueOrStream, ValueOrStreamOrFutureStream}, + ExecError, +}; + +pub trait MiddlewareLike: Clone { + type State: Clone + Send + Sync + 'static; + type NewCtx: Send + 'static; + + fn handle + 'static>( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + next: Arc, + ) -> Result; +} +pub struct MiddlewareContext +where + TState: Send, +{ + pub state: TState, + pub input: Value, + pub ctx: TNewCtx, + pub req: RequestContext, + pub phantom: PhantomData, +} + +// This will match were TState is the default (`()`) so it shouldn't let you call it if you've already swapped the generic +impl MiddlewareContext +where + TLayerCtx: Send, +{ + pub fn with_state(self, state: TState) -> MiddlewareContext + where + TState: Send, + { + MiddlewareContext { + state, + input: self.input, + ctx: self.ctx, + req: self.req, + phantom: PhantomData, + } + } +} + +// This will match were TNewCtx is the default (`TCtx`) so it shouldn't let you call it if you've already swapped the generic +impl MiddlewareContext +where + TLayerCtx: Send, + TState: Send, +{ + pub fn with_ctx( + self, + new_ctx: TNewCtx, + ) -> MiddlewareContext { + MiddlewareContext { + state: self.state, + input: self.input, + ctx: new_ctx, + req: self.req, + phantom: PhantomData, + } + } +} + +pub struct Middleware +where + TState: Send, + TLayerCtx: Send, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, +{ + handler: THandlerFunc, + phantom: PhantomData<(TState, TLayerCtx)>, +} + +impl Clone + for Middleware +where + TState: Send, + TLayerCtx: Send, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, +{ + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + phantom: PhantomData, + } + } +} + +pub struct MiddlewareBuilder(pub PhantomData) +where + TLayerCtx: Send; + +impl MiddlewareBuilder +where + TLayerCtx: Send, +{ + pub fn middleware( + &self, + handler: THandlerFunc, + ) -> Middleware + where + TState: Send, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, + { + Middleware { + handler, + phantom: PhantomData, + } + } +} + +impl + Middleware +where + TState: Send, + TLayerCtx: Send, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, +{ + pub fn resp( + self, + handler: TRespHandlerFunc, + ) -> MiddlewareWithResponseHandler< + TState, + TLayerCtx, + TNewCtx, + THandlerFunc, + THandlerFut, + TRespHandlerFunc, + TRespHandlerFut, + > + where + TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, + TRespHandlerFut: Future> + Send + 'static, + { + MiddlewareWithResponseHandler { + handler: self.handler, + resp_handler: handler, + phantom: PhantomData, + } + } +} + +pub struct MiddlewareWithResponseHandler< + TState, + TLayerCtx, + TNewCtx, + THandlerFunc, + THandlerFut, + TRespHandlerFunc, + TRespHandlerFut, +> where + TState: Send, + TLayerCtx: Send, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, + TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, + TRespHandlerFut: Future> + Send + 'static, +{ + handler: THandlerFunc, + resp_handler: TRespHandlerFunc, + phantom: PhantomData<(TState, TLayerCtx)>, +} + +impl Clone + for MiddlewareWithResponseHandler< + TState, + TLayerCtx, + TNewCtx, + THandlerFunc, + THandlerFut, + TRespHandlerFunc, + TRespHandlerFut, + > +where + TState: Send, + TLayerCtx: Send, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, + TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, + TRespHandlerFut: Future> + Send + 'static, +{ + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + resp_handler: self.resp_handler.clone(), + phantom: PhantomData, + } + } +} + +impl MiddlewareLike + for Middleware +where + TState: Clone + Send + Sync + 'static, + TLayerCtx: Send, + TNewCtx: Send + 'static, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, +{ + type State = TState; + type NewCtx = TNewCtx; + + fn handle + 'static>( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + next: Arc, + ) -> Result { + let handler = (self.handler)(MiddlewareContext { + state: (), + ctx, + input, + req, + phantom: PhantomData, + }); + + Ok(LayerResult::FutureValueOrStream(Box::pin(async move { + let handler = handler.await?; + next.call(handler.ctx, handler.input, handler.req)? + .into_value_or_stream() + .await + }))) + } +} + +enum FutOrValue>> { + Fut(T), + Value(Result), +} + +impl + MiddlewareLike + for MiddlewareWithResponseHandler< + TState, + TLayerCtx, + TNewCtx, + THandlerFunc, + THandlerFut, + TRespHandlerFunc, + TRespHandlerFut, + > +where + TState: Clone + Send + Sync + 'static, + TLayerCtx: Send + 'static, + TNewCtx: Send + 'static, + THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, + THandlerFut: Future, crate::Error>> + + Send + + 'static, + TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, + TRespHandlerFut: Future> + Send + 'static, +{ + type State = TState; + type NewCtx = TNewCtx; + + fn handle + 'static>( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + next: Arc, + ) -> Result { + let handler = (self.handler)(MiddlewareContext { + state: (), + ctx, + input, + req, + // new_ctx: None, + phantom: PhantomData, + }); + + let f = self.resp_handler.clone(); // TODO: Runtime clone is bad. Avoid this! + + Ok(LayerResult::FutureValueOrStreamOrFutureStream(Box::pin( + async move { + let handler = handler.await?; + + Ok( + match next + .call(handler.ctx, handler.input, handler.req)? + .into_value_or_stream() + .await? + { + ValueOrStream::Value(v) => { + ValueOrStreamOrFutureStream::Value(f(handler.state, v).await?) + } + ValueOrStream::Stream(s) => { + ValueOrStreamOrFutureStream::Stream(Box::pin(s.then(move |v| { + let v = match v { + Ok(v) => FutOrValue::Fut(f(handler.state.clone(), v)), + e => FutOrValue::Value(e), + }; + + async move { + match v { + FutOrValue::Fut(fut) => { + fut.await.map_err(ExecError::ErrResolverError) + } + FutOrValue::Value(v) => v, + } + } + }))) + } + }, + ) + }, + ))) + } +} + +// TODO: Middleware functions should be able to be async or sync & return a value or result diff --git a/crates/legacy/src/resolver.rs b/crates/legacy/src/resolver.rs new file mode 100644 index 00000000..3f335f66 --- /dev/null +++ b/crates/legacy/src/resolver.rs @@ -0,0 +1,125 @@ +use std::marker::PhantomData; + +use futures::{Stream, StreamExt}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; +use specta::{Type, TypeCollection}; + +use crate::{ + internal::{LayerResult, ProcedureDataType}, + ExecError, RequestLayer, +}; + +pub trait Resolver { + type Result; + + fn exec(&self, ctx: TCtx, input: Value) -> Result; + + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType; +} + +// pub struct NoArgMarker(/* private */ PhantomData); +// impl Resolver> for TFunc +// where +// TFunc: Fn() -> TResult, +// TResult: IntoLayerResult + Type, +// { +// fn exec(&self, _ctx: TCtx, _arg: Value) -> Result { +// self().into_layer_result() +// } +// +// fn typedef(defs: &mut TypeDefs) -> ProcedureDataType { +// ProcedureDataType { +// arg_ty: <() as Type>::def(DefOpts { +// parent_inline: true, +// type_map: defs, +// }), +// result_ty: ::def(DefOpts { +// parent_inline: true, +// type_map: defs, +// }), +// } +// } +// } +// +// pub struct SingleArgMarker(/* private */ PhantomData); +// impl Resolver> for TFunc +// where +// TFunc: Fn(TCtx) -> TResult, +// TResult: IntoLayerResult, +// { +// fn exec(&self, ctx: TCtx, _arg: Value) -> Result { +// self(ctx).into_layer_result() +// } +// +// fn typedef(defs: &mut TypeDefs) -> ProcedureDataType { +// ProcedureDataType { +// arg_ty: <() as Type>::def(DefOpts { +// parent_inline: true, +// type_map: defs, +// }), +// result_ty: ::def(DefOpts { +// parent_inline: true, +// type_map: defs, +// }), +// } +// } +// } + +pub struct DoubleArgMarker( + /* private */ PhantomData<(TArg, TResultMarker)>, +); +impl Resolver> + for TFunc +where + TArg: DeserializeOwned + Type, + TFunc: Fn(TCtx, TArg) -> TResult, + TResult: RequestLayer, +{ + type Result = TResult; + + fn exec(&self, ctx: TCtx, input: Value) -> Result { + let input = serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?; + self(ctx, input).into_layer_result() + } + + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { + typedef::(defs) + } +} + +pub trait StreamResolver { + fn exec(&self, ctx: TCtx, input: Value) -> Result; + + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType; +} + +pub struct DoubleArgStreamMarker( + /* private */ PhantomData<(TArg, TResult, TStream)>, +); +impl + StreamResolver> for TFunc +where + TArg: DeserializeOwned + Type, + TFunc: Fn(TCtx, TArg) -> TStream, + TStream: Stream + Send + Sync + 'static, + TResult: Serialize + Type, +{ + fn exec(&self, ctx: TCtx, input: Value) -> Result { + let input = serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?; + Ok(LayerResult::Stream(Box::pin(self(ctx, input).map(|v| { + serde_json::to_value(&v).map_err(ExecError::SerializingResultErr) + })))) + } + + fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { + typedef::(defs) + } +} + +pub fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { + let arg_ty = TArg::reference(defs, &[]).inner; + let result_ty = TResult::reference(defs, &[]).inner; + + ProcedureDataType { arg_ty, result_ty } +} diff --git a/crates/legacy/src/resolver_result.rs b/crates/legacy/src/resolver_result.rs new file mode 100644 index 00000000..75d4dc5d --- /dev/null +++ b/crates/legacy/src/resolver_result.rs @@ -0,0 +1,67 @@ +use std::{future::Future, marker::PhantomData}; + +use serde::Serialize; +use specta::Type; + +use crate::{ + internal::{LayerResult, ValueOrStream}, + Error, ExecError, +}; + +pub trait RequestLayer { + type Result: Type; + + fn into_layer_result(self) -> Result; +} + +pub struct SerializeMarker(PhantomData<()>); +impl RequestLayer for T +where + T: Serialize + Type, +{ + type Result = T; + + fn into_layer_result(self) -> Result { + Ok(LayerResult::Ready(Ok( + serde_json::to_value(self).map_err(ExecError::SerializingResultErr)? + ))) + } +} + +pub struct ResultMarker(PhantomData<()>); +impl RequestLayer for Result +where + T: Serialize + Type, +{ + type Result = T; + + fn into_layer_result(self) -> Result { + Ok(LayerResult::Ready(Ok(serde_json::to_value( + self.map_err(ExecError::ErrResolverError)?, + ) + .map_err(ExecError::SerializingResultErr)?))) + } +} + +pub struct FutureMarker(PhantomData); +impl RequestLayer> for TFut +where + TFut: Future + Send + 'static, + T: RequestLayer + Send, +{ + type Result = T::Result; + + fn into_layer_result(self) -> Result { + Ok(LayerResult::Future(Box::pin(async move { + match self + .await + .into_layer_result()? + .into_value_or_stream() + .await? + { + ValueOrStream::Stream(_) => unreachable!(), + ValueOrStream::Value(v) => Ok(v), + } + }))) + } +} diff --git a/crates/legacy/src/router.rs b/crates/legacy/src/router.rs new file mode 100644 index 00000000..bd54e688 --- /dev/null +++ b/crates/legacy/src/router.rs @@ -0,0 +1,219 @@ +use std::{ + collections::BTreeMap, + fs::{self, File}, + io::Write, + marker::PhantomData, + path::{Path, PathBuf}, + pin::Pin, + sync::Arc, +}; + +use futures::Stream; +use serde_json::Value; +use specta::{datatype::FunctionResultVariant, DataType, TypeCollection}; +use specta_typescript::{self as ts, datatype, export_named_datatype, Typescript}; + +use crate::{ + internal::{Procedure, ProcedureKind, ProcedureStore, RequestContext, ValueOrStream}, + Config, ExecError, ExportError, +}; + +#[cfg_attr( + feature = "deprecated", + deprecated = "This is replaced by `rspc::Router`. Refer to the `rspc::compat` module for bridging a legacy router into a modern one." +)] +/// TODO +pub struct Router +where + TCtx: 'static, +{ + pub(crate) config: Config, + pub(crate) queries: ProcedureStore, + pub(crate) mutations: ProcedureStore, + pub(crate) subscriptions: ProcedureStore, + pub(crate) type_map: TypeCollection, + pub(crate) phantom: PhantomData, +} + +// TODO: Move this out of this file +// TODO: Rename?? +pub enum ExecKind { + Query, + Mutation, +} + +impl Router +where + TCtx: 'static, +{ + pub async fn exec( + &self, + ctx: TCtx, + kind: ExecKind, + key: String, + input: Option, + ) -> Result { + let (operations, kind) = match kind { + ExecKind::Query => (&self.queries.store, ProcedureKind::Query), + ExecKind::Mutation => (&self.mutations.store, ProcedureKind::Mutation), + }; + + match operations + .get(&key) + .ok_or_else(|| ExecError::OperationNotFound(key.clone()))? + .exec + .call( + ctx, + input.unwrap_or(Value::Null), + RequestContext { + kind, + path: key.clone(), + }, + )? + .into_value_or_stream() + .await? + { + ValueOrStream::Value(v) => Ok(v), + ValueOrStream::Stream(_) => Err(ExecError::UnsupportedMethod(key)), + } + } + + pub async fn exec_subscription( + &self, + ctx: TCtx, + key: String, + input: Option, + ) -> Result> + Send>>, ExecError> { + match self + .subscriptions + .store + .get(&key) + .ok_or_else(|| ExecError::OperationNotFound(key.clone()))? + .exec + .call( + ctx, + input.unwrap_or(Value::Null), + RequestContext { + kind: ProcedureKind::Subscription, + path: key.clone(), + }, + )? + .into_value_or_stream() + .await? + { + ValueOrStream::Value(_) => Err(ExecError::UnsupportedMethod(key)), + ValueOrStream::Stream(s) => Ok(s), + } + } + + pub fn arced(self) -> Arc { + Arc::new(self) + } + + #[deprecated = "Use `Self::type_map`"] + pub fn typ_store(&self) -> TypeCollection { + self.type_map.clone() + } + + pub fn type_map(&self) -> TypeCollection { + self.type_map.clone() + } + + pub fn queries(&self) -> &BTreeMap> { + &self.queries.store + } + + pub fn mutations(&self) -> &BTreeMap> { + &self.mutations.store + } + + pub fn subscriptions(&self) -> &BTreeMap> { + &self.subscriptions.store + } + + #[allow(clippy::unwrap_used)] // TODO + pub fn export_ts>(&self, export_path: TPath) -> Result<(), ExportError> { + let export_path = PathBuf::from(export_path.as_ref()); + if let Some(export_dir) = export_path.parent() { + fs::create_dir_all(export_dir)?; + } + let mut file = File::create(export_path)?; + if let Some(header) = &self.config.bindings_header { + writeln!(file, "{}", header)?; + } + writeln!(file, "// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.")?; + + let config = Typescript::new().bigint( + ts::BigIntExportBehavior::FailWithReason( + "rspc does not support exporting bigint types (i64, u64, i128, u128) because they are lossily decoded by `JSON.parse` on the frontend. Tracking issue: https://github.com/specta-rs/rspc/issues/93", + ) + ); + + let queries_ts = generate_procedures_ts(&config, &self.queries.store, &self.type_map); + let mutations_ts = generate_procedures_ts(&config, &self.mutations.store, &self.type_map); + let subscriptions_ts = + generate_procedures_ts(&config, &self.subscriptions.store, &self.type_map); + + // TODO: Specta API + writeln!( + file, + r#" +export type Procedures = {{ + queries: {queries_ts}, + mutations: {mutations_ts}, + subscriptions: {subscriptions_ts} +}};"# + )?; + + // Generate type exports (non-Procedures) + for export in self + .type_map + .into_iter() + .map(|(_, ty)| export_named_datatype(&config, ty, &self.type_map).unwrap()) + { + writeln!(file, "\n{}", export)?; + } + + Ok(()) + } +} + +// TODO: Move this out into a Specta API +fn generate_procedures_ts( + config: &Typescript, + procedures: &BTreeMap>, + type_map: &TypeCollection, +) -> String { + match procedures.len() { + 0 => "never".to_string(), + _ => procedures + .iter() + .map(|(key, operation)| { + let input = match &operation.ty.arg_ty { + DataType::Tuple(def) + // This condition is met with an empty enum or `()`. + if def.elements().is_empty() => + { + "never".into() + } + #[allow(clippy::unwrap_used)] // TODO + ty => datatype(config, &FunctionResultVariant::Value(ty.clone()), type_map).unwrap(), + }; + #[allow(clippy::unwrap_used)] // TODO + let result_ts = datatype( + config, + &FunctionResultVariant::Value(operation.ty.result_ty.clone()), + type_map, + ) + .unwrap(); + + // TODO: Specta API + format!( + r#" + {{ key: "{key}", input: {input}, result: {result_ts} }}"# + ) + }) + .collect::>() + .join(" | "), + } +} diff --git a/crates/legacy/src/router_builder.rs b/crates/legacy/src/router_builder.rs new file mode 100644 index 00000000..872bef45 --- /dev/null +++ b/crates/legacy/src/router_builder.rs @@ -0,0 +1,365 @@ +use std::marker::PhantomData; + +use futures::Stream; +use serde::{de::DeserializeOwned, Serialize}; +use specta::{Type, TypeCollection}; + +use crate::{ + internal::{ + BaseMiddleware, BuiltProcedureBuilder, MiddlewareBuilderLike, MiddlewareLayerBuilder, + MiddlewareMerger, ProcedureStore, ResolverLayer, UnbuiltProcedureBuilder, + }, + Config, DoubleArgStreamMarker, ExecError, MiddlewareBuilder, MiddlewareLike, RequestLayer, + Resolver, Router, StreamResolver, +}; + +pub struct RouterBuilder< + TCtx = (), // The is the context the current router was initialised with + TMeta = (), + TMiddleware = BaseMiddleware, +> where + TCtx: Send + Sync + 'static, + TMeta: Send + 'static, + TMiddleware: MiddlewareBuilderLike + Send + 'static, +{ + config: Config, + middleware: TMiddleware, + queries: ProcedureStore, + mutations: ProcedureStore, + subscriptions: ProcedureStore, + type_map: TypeCollection, + phantom: PhantomData, +} + +#[allow(clippy::new_without_default, clippy::new_ret_no_self)] +impl Router +where + TCtx: Send + Sync + 'static, + TMeta: Send + 'static, +{ + pub fn new() -> RouterBuilder> { + RouterBuilder::new() + } +} + +#[allow(clippy::new_without_default)] +impl RouterBuilder> +where + TCtx: Send + Sync + 'static, + TMeta: Send + 'static, +{ + pub fn new() -> Self { + Self { + config: Config::new(), + middleware: BaseMiddleware::default(), + queries: ProcedureStore::new("query"), + mutations: ProcedureStore::new("mutation"), + subscriptions: ProcedureStore::new("subscription"), + type_map: Default::default(), + phantom: PhantomData, + } + } +} + +impl RouterBuilder +where + TCtx: Send + Sync + 'static, + TMeta: Send + 'static, + TLayerCtx: Send + Sync + 'static, + TMiddleware: MiddlewareBuilderLike + Send + 'static, +{ + // /// Attach a configuration to the router. Calling this multiple times will overwrite the previous config. + // pub fn config(mut self, config: Config) -> Self { + // self.config = config; + // self + // } + + pub fn middleware( + self, + builder: impl Fn(MiddlewareBuilder) -> TNewMiddleware, + ) -> RouterBuilder< + TCtx, + TMeta, + MiddlewareLayerBuilder, + > + where + TNewLayerCtx: Send + Sync + 'static, + TNewMiddleware: MiddlewareLike + Send + Sync + 'static, + { + let Self { + config, + middleware, + queries, + mutations, + subscriptions, + type_map, + .. + } = self; + + let mw = builder(MiddlewareBuilder(PhantomData)); + RouterBuilder { + config, + middleware: MiddlewareLayerBuilder { + middleware, + mw, + phantom: PhantomData, + }, + queries, + mutations, + subscriptions, + type_map, + phantom: PhantomData, + } + } + + pub fn query( + mut self, + key: &'static str, + builder: impl Fn( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, + ) -> Self + where + TArg: DeserializeOwned + Type, + TResult: RequestLayer, + TResolver: Fn(TLayerCtx, TArg) -> TResult + Send + Sync + 'static, + { + let resolver = builder(UnbuiltProcedureBuilder::default()).resolver; + self.queries.append( + key.into(), + self.middleware.build(ResolverLayer { + func: move |ctx, input, _| { + resolver.exec( + ctx, + serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?, + ) + }, + phantom: PhantomData, + }), + TResolver::typedef(&mut self.type_map), + ); + self + } + + pub fn mutation( + mut self, + key: &'static str, + builder: impl Fn( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, + ) -> Self + where + TArg: DeserializeOwned + Type, + TResult: RequestLayer, + TResolver: Fn(TLayerCtx, TArg) -> TResult + Send + Sync + 'static, + { + let resolver = builder(UnbuiltProcedureBuilder::default()).resolver; + self.mutations.append( + key.into(), + self.middleware.build(ResolverLayer { + func: move |ctx, input, _| { + resolver.exec( + ctx, + serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?, + ) + }, + phantom: PhantomData, + }), + TResolver::typedef(&mut self.type_map), + ); + self + } + + pub fn subscription( + mut self, + key: &'static str, + builder: impl Fn( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, + ) -> Self + where + TArg: DeserializeOwned + Type, + TStream: Stream + Send + 'static, + TResult: Serialize + Type, + TResolver: Fn(TLayerCtx, TArg) -> TStream + + StreamResolver> + + Send + + Sync + + 'static, + { + let resolver = builder(UnbuiltProcedureBuilder::default()).resolver; + self.subscriptions.append( + key.into(), + self.middleware.build(ResolverLayer { + func: move |ctx, input, _| { + resolver.exec( + ctx, + serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?, + ) + }, + phantom: PhantomData, + }), + TResolver::typedef(&mut self.type_map), + ); + self + } + + pub fn merge( + mut self, + prefix: &'static str, + router: RouterBuilder, + ) -> Self + where + TNewLayerCtx: 'static, + TIncomingMiddleware: + MiddlewareBuilderLike + Send + 'static, + { + #[allow(clippy::panic)] + if prefix.is_empty() || prefix.starts_with("rpc.") || prefix.starts_with("rspc.") { + panic!( + "rspc error: attempted to merge a router with the prefix '{}', however this name is not allowed.", + prefix + ); + } + + // TODO: The `data` field has gotta flow from the root router to the leaf routers so that we don't have to merge user defined types. + + for (key, query) in router.queries.store { + // query.ty.key = format!("{}{}", prefix, key); + self.queries.append( + format!("{}{}", prefix, key), + self.middleware.build(query.exec), + query.ty, + ); + } + + for (key, mutation) in router.mutations.store { + // mutation.ty.key = format!("{}{}", prefix, key); + self.mutations.append( + format!("{}{}", prefix, key), + self.middleware.build(mutation.exec), + mutation.ty, + ); + } + + for (key, subscription) in router.subscriptions.store { + // subscription.ty.key = format!("{}{}", prefix, key); + self.subscriptions.append( + format!("{}{}", prefix, key), + self.middleware.build(subscription.exec), + subscription.ty, + ); + } + + self.type_map.extend(&router.type_map); + + self + } + + /// `legacy_merge` maintains the `merge` functionality prior to release 0.1.3 + /// It will flow the `TMiddleware` and `TCtx` out of the child router to the parent router. + /// This was a confusing behavior and is generally not useful so it has been deprecated. + /// + /// This function will be remove in a future release. If you are using it open a GitHub issue to discuss your use case and longer term solutions for it. + pub fn legacy_merge( + self, + prefix: &'static str, + router: RouterBuilder, + ) -> RouterBuilder< + TCtx, + TMeta, + MiddlewareMerger, + > + where + TNewLayerCtx: 'static, + TIncomingMiddleware: + MiddlewareBuilderLike + Send + 'static, + { + #[allow(clippy::panic)] + if prefix.is_empty() || prefix.starts_with("rpc.") || prefix.starts_with("rspc.") { + panic!( + "rspc error: attempted to merge a router with the prefix '{}', however this name is not allowed.", + prefix + ); + } + + let Self { + config, + middleware, + mut queries, + mut mutations, + mut subscriptions, + mut type_map, + .. + } = self; + + for (key, query) in router.queries.store { + queries.append( + format!("{}{}", prefix, key), + middleware.build(query.exec), + query.ty, + ); + } + + for (key, mutation) in router.mutations.store { + mutations.append( + format!("{}{}", prefix, key), + middleware.build(mutation.exec), + mutation.ty, + ); + } + + for (key, subscription) in router.subscriptions.store { + subscriptions.append( + format!("{}{}", prefix, key), + middleware.build(subscription.exec), + subscription.ty, + ); + } + + type_map.extend(&router.type_map); + + RouterBuilder { + config, + middleware: MiddlewareMerger { + middleware, + middleware2: router.middleware, + phantom: PhantomData, + }, + queries, + mutations, + subscriptions, + type_map, + phantom: PhantomData, + } + } + + pub fn build(self) -> Router { + let Self { + config, + queries, + mutations, + subscriptions, + type_map, + .. + } = self; + + let export_path = config.export_bindings_on_build.clone(); + let router = Router { + config, + queries, + mutations, + subscriptions, + type_map, + phantom: PhantomData, + }; + + #[cfg(debug_assertions)] + #[allow(clippy::unwrap_used)] + if let Some(export_path) = export_path { + router.export_ts(export_path).unwrap(); + } + + router + } +} diff --git a/crates/legacy/src/selection.rs b/crates/legacy/src/selection.rs new file mode 100644 index 00000000..fb607635 --- /dev/null +++ b/crates/legacy/src/selection.rs @@ -0,0 +1,78 @@ +//! The selection macro. +//! +//! WARNING: Wherever this is called you must have the `specta` crate installed. +#[macro_export] +#[cfg_attr( + feature = "deprecated", + deprecated = "Use `specta_util::selection` instead" +)] +macro_rules! selection { + ( $s:expr, { $($n:ident),+ } ) => {{ + #[allow(non_camel_case_types)] + mod selection { + #[derive(serde::Serialize, specta::Type)] + #[specta(inline)] + pub struct Selection<$($n,)*> { + $(pub $n: $n),* + } + } + use selection::Selection; + #[allow(non_camel_case_types)] + Selection { $($n: $s.$n,)* } + }}; + ( $s:expr, [{ $($n:ident),+ }] ) => {{ + #[allow(non_camel_case_types)] + mod selection { + #[derive(serde::Serialize, specta::Type)] + #[specta(inline)] + pub struct Selection<$($n,)*> { + $(pub $n: $n,)* + } + } + use selection::Selection; + #[allow(non_camel_case_types)] + $s.into_iter().map(|v| Selection { $($n: v.$n,)* }).collect::>() + }}; +} + +#[cfg(test)] +mod tests { + use specta::Type; + use specta_typescript::inline; + + fn ts_export_ref(_t: &T) -> String { + inline::(&Default::default()).unwrap() + } + + #[derive(Clone)] + #[allow(dead_code)] + struct User { + pub id: i32, + pub name: String, + pub email: String, + pub age: i32, + pub password: String, + } + + #[test] + fn test_selection_macros() { + let user = User { + id: 1, + name: "Monty Beaumont".into(), + email: "monty@otbeaumont.me".into(), + age: 7, + password: "password123".into(), + }; + + let s1 = selection!(user.clone(), { name, age }); + assert_eq!(s1.name, "Monty Beaumont".to_string()); + assert_eq!(s1.age, 7); + assert_eq!(ts_export_ref(&s1), "{ name: string; age: number }"); + + let users = vec![user; 3]; + let s2 = selection!(users, [{ name, age }]); + assert_eq!(s2[0].name, "Monty Beaumont".to_string()); + assert_eq!(s2[0].age, 7); + assert_eq!(ts_export_ref(&s2), "{ name: string; age: number }[]"); + } +} diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml index 0a3d56c9..594aa111 100644 --- a/crates/openapi/Cargo.toml +++ b/crates/openapi/Cargo.toml @@ -6,9 +6,9 @@ publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc", features = ["unstable"] } -axum = { version = "0.7.9", default-features = false } -serde_json = "1.0.134" -futures = "0.3.31" +axum = { version = "0.7.9", default-features = false } # TODO: Remove this +serde_json = { workspace = true } +futures = { workspace = true } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/openapi/src/lib.rs b/crates/openapi/src/lib.rs index da57295d..9f9f7acb 100644 --- a/crates/openapi/src/lib.rs +++ b/crates/openapi/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] use std::{borrow::Cow, collections::HashMap, sync::Arc}; diff --git a/crates/procedure/Cargo.toml b/crates/procedure/Cargo.toml index 5144745a..ee656667 100644 --- a/crates/procedure/Cargo.toml +++ b/crates/procedure/Cargo.toml @@ -17,14 +17,14 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] # Public -futures-core = { version = "0.3", default-features = false } -serde = { version = "1", default-features = false } +futures-core = { workspace = true, default-features = false } +serde = { workspace = true, default-features = false } # Private -erased-serde = { version = "0.4", default-features = false, features = [ +erased-serde = { workspace = true, default-features = false, features = [ "std", ] } -pin-project-lite = { version = "0.2", default-features = false } +pin-project-lite = { workspace = true, default-features = false } [lints] workspace = true diff --git a/crates/tracing/Cargo.toml b/crates/tracing/Cargo.toml index a08f2516..46771e0f 100644 --- a/crates/tracing/Cargo.toml +++ b/crates/tracing/Cargo.toml @@ -6,8 +6,8 @@ publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc", features = ["unstable"] } -tracing = "0.1" -futures = "0.3" +tracing = { workspace = true } +futures = { workspace = true } tracing-futures = "0.2.5" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index 9520594d..519fa17d 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] use std::{fmt, marker::PhantomData}; diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index 90589f2f..e52e1d9c 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -6,8 +6,8 @@ publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc", features = ["unstable"] } -serde = "1" -specta = "=2.0.0-rc.20" +serde = { workspace = true } +specta = { workspace = true } validator = "0.19" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/crates/validator/src/lib.rs b/crates/validator/src/lib.rs index 9d217dbe..7350f263 100644 --- a/crates/validator/src/lib.rs +++ b/crates/validator/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] use std::fmt; diff --git a/crates/zer/Cargo.toml b/crates/zer/Cargo.toml index 3343654a..2d54ab46 100644 --- a/crates/zer/Cargo.toml +++ b/crates/zer/Cargo.toml @@ -6,9 +6,9 @@ publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc", features = ["unstable"] } -serde = "1" -specta = "=2.0.0-rc.20" -serde_json = "1" +serde = { workspace = true } +specta = { workspace = true } +serde_json = { workspace = true } jsonwebtoken = { version = "9", default-features = false } cookie = { version = "0.18.1", features = ["percent-encode"] } diff --git a/crates/zer/src/lib.rs b/crates/zer/src/lib.rs index c1cdb318..ad211f48 100644 --- a/crates/zer/src/lib.rs +++ b/crates/zer/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] use std::{ diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml index c3634784..402e361c 100644 --- a/rspc/Cargo.toml +++ b/rspc/Cargo.toml @@ -29,6 +29,7 @@ nolegacy = [] [dependencies] # Public rspc-procedure = { path = "../crates/procedure" } +# rspc-legacy = { path = "../crates/legacy" } serde = "1" futures = "0.3" # TODO: Drop down to `futures-core` when removing legacy stuff? specta = { version = "=2.0.0-rc.20", features = [ From f5e3e925296f9b5cb549b429da5180964382af59 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 26 Dec 2024 12:58:06 +0800 Subject: [PATCH 62/67] drop `rspc::legacy` in favor of `rspc_legacy` --- Cargo.toml | 18 +- crates/binario/Cargo.toml | 2 +- crates/cache/Cargo.toml | 2 +- crates/invalidation/Cargo.toml | 2 +- crates/legacy/src/error.rs | 10 + crates/legacy/src/internal/mod.rs | 4 + crates/legacy/src/router.rs | 23 +- crates/legacy/src/router_builder.rs | 10 +- crates/openapi/Cargo.toml | 2 +- crates/tracing/Cargo.toml | 2 +- crates/validator/Cargo.toml | 2 +- crates/zer/Cargo.toml | 2 +- examples/Cargo.toml | 27 -- examples/actix-web/Cargo.toml | 2 +- examples/actix-web/src/main.rs | 4 +- examples/axum/Cargo.toml | 2 +- examples/axum/src/main.rs | 4 +- examples/bindings.ts | 18 +- examples/core/Cargo.toml | 3 +- examples/core/src/lib.rs | 90 +---- examples/legacy-compat/Cargo.toml | 18 + examples/legacy-compat/src/main.rs | 80 ++++ examples/legacy/Cargo.toml | 18 + examples/legacy/src/main.rs | 78 ++++ examples/src/basic.rs | 25 -- examples/src/bin/axum.rs | 62 --- examples/src/bin/cookies.rs | 66 ---- examples/src/bin/global_context.rs | 51 --- examples/src/bin/middleware.rs | 132 ------- examples/src/error_handling.rs | 65 ---- examples/src/lib.rs | 4 - examples/src/selection.rs | 41 -- examples/src/subscriptions.rs | 32 -- examples/tauri/src-tauri/Cargo.toml | 2 +- examples/tauri/src-tauri/src/lib.rs | 4 +- rspc/Cargo.toml | 24 +- rspc/src/languages/typescript.rs | 5 +- rspc/src/{legacy/interop.rs => legacy.rs} | 130 +++---- rspc/src/legacy/config.rs | 31 -- rspc/src/legacy/error.rs | 194 ---------- rspc/src/legacy/internal/jsonrpc.rs | 118 ------ rspc/src/legacy/internal/jsonrpc_exec.rs | 292 -------------- rspc/src/legacy/internal/middleware.rs | 221 ----------- rspc/src/legacy/internal/mod.rs | 14 - rspc/src/legacy/internal/procedure_builder.rs | 33 -- rspc/src/legacy/internal/procedure_store.rs | 53 --- rspc/src/legacy/middleware.rs | 338 ---------------- rspc/src/legacy/mod.rs | 35 -- rspc/src/legacy/resolver.rs | 125 ------ rspc/src/legacy/resolver_result.rs | 67 ---- rspc/src/legacy/router.rs | 215 ----------- rspc/src/legacy/router_builder.rs | 365 ------------------ rspc/src/legacy/selection.rs | 74 ---- rspc/src/lib.rs | 33 +- rspc/src/modern/middleware/middleware.rs | 2 +- rspc/src/modern/procedure/builder.rs | 2 +- rspc/src/modern/procedure/erased.rs | 2 +- rspc/src/modern/procedure/resolver_output.rs | 4 +- rspc/src/modern/stream.rs | 10 +- rspc/src/procedure.rs | 3 +- rspc/src/router.rs | 28 +- 61 files changed, 366 insertions(+), 2959 deletions(-) delete mode 100644 examples/Cargo.toml create mode 100644 examples/legacy-compat/Cargo.toml create mode 100644 examples/legacy-compat/src/main.rs create mode 100644 examples/legacy/Cargo.toml create mode 100644 examples/legacy/src/main.rs delete mode 100644 examples/src/basic.rs delete mode 100644 examples/src/bin/axum.rs delete mode 100644 examples/src/bin/cookies.rs delete mode 100644 examples/src/bin/global_context.rs delete mode 100644 examples/src/bin/middleware.rs delete mode 100644 examples/src/error_handling.rs delete mode 100644 examples/src/lib.rs delete mode 100644 examples/src/selection.rs delete mode 100644 examples/src/subscriptions.rs rename rspc/src/{legacy/interop.rs => legacy.rs} (64%) delete mode 100644 rspc/src/legacy/config.rs delete mode 100644 rspc/src/legacy/error.rs delete mode 100644 rspc/src/legacy/internal/jsonrpc.rs delete mode 100644 rspc/src/legacy/internal/jsonrpc_exec.rs delete mode 100644 rspc/src/legacy/internal/middleware.rs delete mode 100644 rspc/src/legacy/internal/mod.rs delete mode 100644 rspc/src/legacy/internal/procedure_builder.rs delete mode 100644 rspc/src/legacy/internal/procedure_store.rs delete mode 100644 rspc/src/legacy/middleware.rs delete mode 100644 rspc/src/legacy/mod.rs delete mode 100644 rspc/src/legacy/resolver.rs delete mode 100644 rspc/src/legacy/resolver_result.rs delete mode 100644 rspc/src/legacy/router.rs delete mode 100644 rspc/src/legacy/router_builder.rs delete mode 100644 rspc/src/legacy/selection.rs diff --git a/Cargo.toml b/Cargo.toml index 0afc10a1..18b4fa3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,14 +2,15 @@ resolver = "2" members = [ "./crates/*", - "./rspc", - "./integrations/*", - "./examples", - "./examples/core", - "./examples/axum", - "./examples/actix-web", - "./examples/client", - "./examples/tauri/src-tauri", + "./rspc", + "./integrations/*", + "./examples/core", + "./examples/axum", + "./examples/actix-web", + "./examples/client", + "./examples/tauri/src-tauri", + "./examples/legacy", + "./examples/legacy-compat", ] [workspace.dependencies] @@ -24,6 +25,7 @@ serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } futures = { version = "0.3", default-features = false } futures-core = { version = "0.3", default-features = false } +futures-util = { version = "0.3", default-features = false } tracing = { version = "0.1", default-features = false } [workspace.lints.clippy] diff --git a/crates/binario/Cargo.toml b/crates/binario/Cargo.toml index d30dff90..7a3d0cb6 100644 --- a/crates/binario/Cargo.toml +++ b/crates/binario/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } specta = { workspace = true } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/crates/cache/Cargo.toml b/crates/cache/Cargo.toml index 39e5c3c1..dce449f8 100644 --- a/crates/cache/Cargo.toml +++ b/crates/cache/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] moka = { version = "0.12.8", features = ["sync"] } pin-project-lite = { workspace = true } -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/invalidation/Cargo.toml b/crates/invalidation/Cargo.toml index 60eb93d9..9a4385a1 100644 --- a/crates/invalidation/Cargo.toml +++ b/crates/invalidation/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/legacy/src/error.rs b/crates/legacy/src/error.rs index 3d513fea..e5fffac9 100644 --- a/crates/legacy/src/error.rs +++ b/crates/legacy/src/error.rs @@ -142,6 +142,16 @@ impl Error { cause: Some(Arc::new(cause)), } } + + #[doc(hidden)] + pub fn message(&self) -> &str { + &self.message + } + + #[doc(hidden)] + pub fn cause(self) -> Option> { + self.cause + } } /// TODO diff --git a/crates/legacy/src/internal/mod.rs b/crates/legacy/src/internal/mod.rs index 9508e142..b2096270 100644 --- a/crates/legacy/src/internal/mod.rs +++ b/crates/legacy/src/internal/mod.rs @@ -12,3 +12,7 @@ pub(crate) use procedure_store::*; // Used by `rspc_axum` pub use middleware::ProcedureKind; pub mod jsonrpc; + +// Were not exported by rspc 0.3.0 but required by `rspc::legacy` interop layer +#[doc(hidden)] +pub use middleware::{Layer, RequestContext, ValueOrStream}; diff --git a/crates/legacy/src/router.rs b/crates/legacy/src/router.rs index bd54e688..6160bdf1 100644 --- a/crates/legacy/src/router.rs +++ b/crates/legacy/src/router.rs @@ -20,7 +20,7 @@ use crate::{ #[cfg_attr( feature = "deprecated", - deprecated = "This is replaced by `rspc::Router`. Refer to the `rspc::compat` module for bridging a legacy router into a modern one." + deprecated = "This is replaced by `rspc::Router`. Refer to the `rspc::legacy` module for bridging a legacy router into a modern one." )] /// TODO pub struct Router @@ -131,6 +131,27 @@ where &self.subscriptions.store } + #[doc(hidden)] // Used for `rspc::legacy` interop + pub fn into_parts( + self, + ) -> ( + BTreeMap>, + BTreeMap>, + BTreeMap>, + TypeCollection, + ) { + if self.config.export_bindings_on_build.is_some() || self.config.bindings_header.is_some() { + panic!("Note: `rspc_legacy::Config` is ignored by `rspc::Router`. You should set the configuration on `rspc::Typescript` instead."); + } + + ( + self.queries.store, + self.mutations.store, + self.subscriptions.store, + self.type_map, + ) + } + #[allow(clippy::unwrap_used)] // TODO pub fn export_ts>(&self, export_path: TPath) -> Result<(), ExportError> { let export_path = PathBuf::from(export_path.as_ref()); diff --git a/crates/legacy/src/router_builder.rs b/crates/legacy/src/router_builder.rs index 872bef45..653b9e22 100644 --- a/crates/legacy/src/router_builder.rs +++ b/crates/legacy/src/router_builder.rs @@ -68,11 +68,11 @@ where TLayerCtx: Send + Sync + 'static, TMiddleware: MiddlewareBuilderLike + Send + 'static, { - // /// Attach a configuration to the router. Calling this multiple times will overwrite the previous config. - // pub fn config(mut self, config: Config) -> Self { - // self.config = config; - // self - // } + /// Attach a configuration to the router. Calling this multiple times will overwrite the previous config. + pub fn config(mut self, config: Config) -> Self { + self.config = config; + self + } pub fn middleware( self, diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml index 594aa111..30a742f2 100644 --- a/crates/openapi/Cargo.toml +++ b/crates/openapi/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } axum = { version = "0.7.9", default-features = false } # TODO: Remove this serde_json = { workspace = true } futures = { workspace = true } diff --git a/crates/tracing/Cargo.toml b/crates/tracing/Cargo.toml index 46771e0f..c21e2076 100644 --- a/crates/tracing/Cargo.toml +++ b/crates/tracing/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } tracing = { workspace = true } futures = { workspace = true } tracing-futures = "0.2.5" diff --git a/crates/validator/Cargo.toml b/crates/validator/Cargo.toml index e52e1d9c..544fa165 100644 --- a/crates/validator/Cargo.toml +++ b/crates/validator/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } serde = { workspace = true } specta = { workspace = true } validator = "0.19" diff --git a/crates/zer/Cargo.toml b/crates/zer/Cargo.toml index 2d54ab46..7da26be9 100644 --- a/crates/zer/Cargo.toml +++ b/crates/zer/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] -rspc = { path = "../../rspc", features = ["unstable"] } +rspc = { path = "../../rspc" } serde = { workspace = true } specta = { workspace = true } serde_json = { workspace = true } diff --git a/examples/Cargo.toml b/examples/Cargo.toml deleted file mode 100644 index 495c054c..00000000 --- a/examples/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "example" -version = "0.0.0" -edition = "2021" -publish = false - -[dependencies] -rspc = { path = "../rspc", features = ["typescript"] } -specta = "=2.0.0-rc.20" -rspc-axum = { path = "../integrations/axum" } -async-stream = "0.3.6" -axum = "0.7.9" -chrono = { version = "0.4.39", features = ["serde"] } -serde = { version = "1.0.216", features = ["derive"] } -time = "0.3.37" -tokio = { version = "1.42.0", features = [ - "rt-multi-thread", - "macros", - "time", - "sync", -], default-features = false } -tower-cookies = { version = "0.10.0", features = ["axum-core"] } -tower-http = { version = "0.6.2", default-features = false, features = [ - "cors", -] } -uuid = { version = "1.11.0", features = ["v4", "serde"] } -serde_json = "1.0.134" diff --git a/examples/actix-web/Cargo.toml b/examples/actix-web/Cargo.toml index 65758687..b051fe57 100644 --- a/examples/actix-web/Cargo.toml +++ b/examples/actix-web/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } +rspc = { path = "../../rspc", features = ["typescript", "rust"] } example-core = { path = "../core" } rspc-actix-web = { path = "../../integrations/actix-web", features = [] } actix-web = "4" diff --git a/examples/actix-web/src/main.rs b/examples/actix-web/src/main.rs index ccd36627..97d34aee 100644 --- a/examples/actix-web/src/main.rs +++ b/examples/actix-web/src/main.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use actix_cors::Cors; use actix_multipart::Multipart; use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; -use example_core::{create_router, Ctx}; +use example_core::{mount, Ctx}; use futures::{StreamExt, TryStreamExt}; #[get("/")] @@ -27,7 +27,7 @@ async fn upload(mut payload: Multipart) -> impl Responder { #[actix_web::main] async fn main() -> std::io::Result<()> { - let router = create_router(); + let router = mount(); let (procedures, types) = router.build().unwrap(); rspc::Typescript::default() diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 0fd13802..241984cd 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } +rspc = { path = "../../rspc", features = ["typescript", "rust"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } example-core = { path = "../core" } tokio = { version = "1.42.0", features = ["full"] } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index cb0addd0..c9fd2bb3 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -5,7 +5,7 @@ use axum::{ routing::{get, on, post, MethodFilter, MethodRouter}, Json, }; -use example_core::{create_router, Ctx}; +use example_core::{mount, Ctx}; use futures::{stream::FuturesUnordered, Stream, StreamExt}; use rspc::{DynOutput, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures, State}; use rspc_invalidation::Invalidator; @@ -22,7 +22,7 @@ use tower_http::cors::{Any, CorsLayer}; #[tokio::main] async fn main() { - let router = create_router(); + let router = mount(); let (procedures, types) = router.build().unwrap(); rspc::Typescript::default() diff --git a/examples/bindings.ts b/examples/bindings.ts index 6dc63eed..d4d1c31c 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,28 +1,12 @@ -// My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "cached"; input: any; result: any } | { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "login"; input: any; result: any } | { key: "me"; input: any; result: any } | { key: "nested.hello"; input: null; result: string } | { key: "newstuff"; input: any; result: any } | { key: "newstuff2"; input: any; result: any } | { key: "newstuffpanic"; input: any; result: any } | { key: "newstuffser"; input: any; result: any } | { key: "panic"; input: null; result: null } | { key: "sfmPost"; input: any; result: any } | { key: "sfmPostEdit"; input: any; result: any } | { key: "transformMe"; input: null; result: string } | { key: "validator"; input: any; result: any } | { key: "version"; input: null; result: string } | { key: "withoutBaseProcedure"; input: any; result: any }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } +export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } export type Procedures = { - cached: { kind: "query", input: any, output: any, error: any }, echo: { kind: "query", input: string, output: string, error: unknown }, error: { kind: "query", input: null, output: string, error: unknown }, - login: { kind: "query", input: any, output: any, error: any }, - me: { kind: "query", input: any, output: any, error: any }, - nested: { - hello: { kind: "query", input: null, output: string, error: unknown }, -}, - newstuff: { kind: "query", input: any, output: any, error: any }, - newstuff2: { kind: "query", input: any, output: any, error: any }, - newstuffpanic: { kind: "query", input: any, output: any, error: any }, - newstuffser: { kind: "query", input: any, output: any, error: any }, - panic: { kind: "query", input: null, output: null, error: unknown }, pings: { kind: "subscription", input: null, output: string, error: unknown }, sendMsg: { kind: "mutation", input: string, output: string, error: unknown }, - sfmPost: { kind: "query", input: any, output: any, error: any }, - sfmPostEdit: { kind: "query", input: any, output: any, error: any }, transformMe: { kind: "query", input: null, output: string, error: unknown }, - validator: { kind: "query", input: any, output: any, error: any }, version: { kind: "query", input: null, output: string, error: unknown }, - withoutBaseProcedure: { kind: "query", input: any, output: any, error: any }, } \ No newline at end of file diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index 8b9fbdc0..d447595e 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -rspc = { path = "../../rspc", features = ["typescript", "rust", "unstable"] } +rspc = { path = "../../rspc", features = ["typescript", "rust"] } async-stream = "0.3.6" serde = { version = "1.0.216", features = ["derive"] } specta = { version = "=2.0.0-rc.20", features = [ @@ -24,3 +24,4 @@ rspc-openapi = { version = "0.0.0", path = "../../crates/openapi" } validator = { version = "0.19.0", features = ["derive"] } rspc-zer = { version = "0.0.0", path = "../../crates/zer" } anyhow = "1.0.95" +binario = "0.0.1" diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index c9995407..705b94ef 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -5,6 +5,7 @@ use rspc::{ middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, Router2, }; +use rspc_binario::Binario; use rspc_cache::{cache, cache_ttl, CacheState, Memory}; use rspc_invalidation::Invalidate; use rspc_zer::Zer; @@ -43,79 +44,6 @@ pub enum DeserializationError { A(String), } -// http://[::]:4000/rspc/version -// http://[::]:4000/legacy/version - -// http://[::]:4000/rspc/nested.hello -// http://[::]:4000/legacy/nested.hello - -// http://[::]:4000/rspc/error -// http://[::]:4000/legacy/error - -// http://[::]:4000/rspc/echo -// http://[::]:4000/legacy/echo - -// http://[::]:4000/rspc/echo?input=42 -// http://[::]:4000/legacy/echo?input=42 - -fn mount() -> rspc::Router { - let inner = rspc::Router::::new().query("hello", |t| t(|_, _: ()| "Hello World!")); - - let router = rspc::Router::::new() - .merge("nested.", inner) - .query("version", |t| { - t(|_, _: ()| { - info!("Hello World from Version Query!"); - - env!("CARGO_PKG_VERSION") - }) - }) - .query("panic", |t| t(|_, _: ()| todo!())) - // .mutation("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) - .query("echo", |t| t(|_, v: String| v)) - .query("error", |t| { - t(|_, _: ()| { - Err(rspc::Error::new( - rspc::ErrorCode::InternalServerError, - "Something went wrong".into(), - )) as Result - }) - }) - .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) - .mutation("sendMsg", |t| { - t(|_, v: String| { - println!("Client said '{}'", v); - v - }) - }) - // .mutation("anotherOne", |t| t(|_, v: String| Ok(MyCustomType(v)))) - .subscription("pings", |t| { - t(|_ctx, _args: ()| { - stream! { - println!("Client subscribed to 'pings'"); - for i in 0..5 { - println!("Sending ping {}", i); - yield "ping".to_string(); - // sleep(Duration::from_secs(1)).await; // TODO: Figure this out. Async runtime is now not determined so maybe inject. - } - } - }) - }) - // TODO: Results being returned from subscriptions - // .subscription("errorPings", |t| t(|_ctx, _args: ()| { - // stream! { - // for i in 0..5 { - // yield Ok("ping".to_string()); - // sleep(Duration::from_secs(1)).await; - // } - // yield Err(rspc::Error::new(ErrorCode::InternalServerError, "Something went wrong".into())); - // } - // })) - .build(); - - router -} - #[derive(Debug, Error, Serialize, Type)] #[serde(tag = "type")] pub enum Error { @@ -165,8 +93,8 @@ impl Serialize for SerialisationError { } } -fn test_unstable_stuff(router: Router2) -> Router2 { - router +pub fn mount() -> Router2 { + Router2::new() .procedure("withoutBaseProcedure", { Procedure2::builder::().query(|ctx: Ctx, id: String| async move { Ok(()) }) }) @@ -272,6 +200,11 @@ fn test_unstable_stuff(router: Router2) -> Router2 { .procedure("me", { ::builder().query(|ctx, _: ()| async move { Ok(ctx.zer.session()?) }) }) + // .procedure("binario", { + // #[derive(binario::Encode)] + // pub struct Input {} + // ::builder().query(|ctx, _: Binario| async move { Ok(()) }) + // }) // .procedure("fileupload", { // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) @@ -294,10 +227,3 @@ pub type Invalidator = rspc_invalidation::Invalidator; // TODO: Debug, etc pub struct File(T); - -pub fn create_router() -> Router2 { - let router = Router2::from(mount()); - let router = test_unstable_stuff(router); - - router -} diff --git a/examples/legacy-compat/Cargo.toml b/examples/legacy-compat/Cargo.toml new file mode 100644 index 00000000..2e4af51a --- /dev/null +++ b/examples/legacy-compat/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-legacy-compat" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +tokio = { version = "1", features = ["full"] } +rspc = { path = "../../rspc", features = ["typescript", "rust", "legacy"] } +rspc-axum = { path = "../../integrations/axum", features = ["ws"] } +rspc-legacy = { path = "../../crates/legacy" } +axum = { version = "0.7.9", features = ["multipart"] } +tower-http = { version = "0.6.2", default-features = false, features = [ + "cors", +] } +futures.workspace = true +serde_json.workspace = true +async-stream = "0.3.6" diff --git a/examples/legacy-compat/src/main.rs b/examples/legacy-compat/src/main.rs new file mode 100644 index 00000000..91b2cd50 --- /dev/null +++ b/examples/legacy-compat/src/main.rs @@ -0,0 +1,80 @@ +//! This example shows using `rspc_legacy` directly. +//! This is not intended for permanent use, but instead it is designed to allow an incremental migration from rspc 0.3.0. + +use std::{path::PathBuf, time::Duration}; + +use async_stream::stream; +use axum::{http::request::Parts, routing::get}; +use rspc_legacy::{Error, ErrorCode, Router, RouterBuilder}; +use tokio::time::sleep; +use tower_http::cors::{Any, CorsLayer}; + +pub(crate) struct Ctx {} + +fn mount() -> RouterBuilder { + Router::::new() + .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("echo", |t| t(|_, v: String| v)) + .query("error", |t| { + t(|_, _: ()| { + Err(Error::new( + ErrorCode::InternalServerError, + "Something went wrong".into(), + )) as Result + }) + }) + .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) + .mutation("sendMsg", |t| { + t(|_, v: String| { + println!("Client said '{}'", v); + v + }) + }) + .subscription("pings", |t| { + t(|_ctx, _args: ()| { + stream! { + println!("Client subscribed to 'pings'"); + for i in 0..5 { + println!("Sending ping {}", i); + yield "ping".to_string(); + sleep(Duration::from_secs(1)).await; + } + } + }) + }) +} + +#[tokio::main] +async fn main() { + let (procedures, types) = rspc::Router2::from(mount().build()).build().unwrap(); + + rspc::Typescript::default() + .export_to( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + &types, + ) + .unwrap(); + + // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! + let cors = CorsLayer::new() + .allow_methods(Any) + .allow_headers(Any) + .allow_origin(Any); + + let app = axum::Router::new() + .route("/", get(|| async { "Hello from rspc legacy!" })) + .nest( + "/rspc", + rspc_axum::endpoint(procedures, |parts: Parts| { + println!("Client requested operation '{}'", parts.uri.path()); + Ctx {} + }), + ) + .layer(cors); + + let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 + println!("listening on http://{}/rspc/version", addr); + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) + .await + .unwrap(); +} diff --git a/examples/legacy/Cargo.toml b/examples/legacy/Cargo.toml new file mode 100644 index 00000000..b29b480e --- /dev/null +++ b/examples/legacy/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-legacy" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +tokio = { version = "1", features = ["full"] } +rspc = { path = "../../rspc", features = ["typescript", "rust", "legacy"] } +rspc-axum = { path = "../../integrations/axum", features = ["ws"] } +rspc-legacy = { path = "../../crates/legacy" } +axum = { version = "0.7.9", features = ["multipart"] } +tower-http = { version = "0.6.2", default-features = false, features = [ + "cors", +] } +futures.workspace = true +serde_json.workspace = true +async-stream = "0.3.6" diff --git a/examples/legacy/src/main.rs b/examples/legacy/src/main.rs new file mode 100644 index 00000000..1323c27c --- /dev/null +++ b/examples/legacy/src/main.rs @@ -0,0 +1,78 @@ +//! This example shows using `rspc_legacy` directly. +//! This is not intended for permanent use, but instead it is designed to allow an incremental migration from rspc 0.3.0. + +use std::{path::PathBuf, time::Duration}; + +use async_stream::stream; +use axum::routing::get; +use rspc_legacy::{Config, Error, ErrorCode, Router, RouterBuilder}; +use tokio::time::sleep; +use tower_http::cors::{Any, CorsLayer}; + +pub(crate) struct Ctx {} + +fn mount() -> RouterBuilder { + Router::::new() + .config( + Config::new().export_ts_bindings( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), + ), + ) + .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("echo", |t| t(|_, v: String| v)) + .query("error", |t| { + t(|_, _: ()| { + Err(Error::new( + ErrorCode::InternalServerError, + "Something went wrong".into(), + )) as Result + }) + }) + .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) + .mutation("sendMsg", |t| { + t(|_, v: String| { + println!("Client said '{}'", v); + v + }) + }) + .subscription("pings", |t| { + t(|_ctx, _args: ()| { + stream! { + println!("Client subscribed to 'pings'"); + for i in 0..5 { + println!("Sending ping {}", i); + yield "ping".to_string(); + sleep(Duration::from_secs(1)).await; + } + } + }) + }) +} + +#[tokio::main] +async fn main() { + let router = mount().build().arced(); + + // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! + let cors = CorsLayer::new() + .allow_methods(Any) + .allow_headers(Any) + .allow_origin(Any); + + let app = axum::Router::new() + .route("/", get(|| async { "Hello from rspc legacy!" })) + // .nest( + // "/rspc", + // rspc_axum::endpoint(procedures, |parts: Parts| { + // println!("Client requested operation '{}'", parts.uri.path()); + // Ctx {} + // }), + // ) + .layer(cors); + + let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 + println!("listening on http://{}/rspc/version", addr); + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) + .await + .unwrap(); +} diff --git a/examples/src/basic.rs b/examples/src/basic.rs deleted file mode 100644 index 40175830..00000000 --- a/examples/src/basic.rs +++ /dev/null @@ -1,25 +0,0 @@ -use rspc::{Router, RouterBuilder}; - -// We merge this router into the main router in `main.rs`. -// This router shows how to do basic queries and mutations and how they tak -pub fn mount() -> RouterBuilder { - Router::new() - .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) - .query("echo", |t| t(|_, v: String| v)) - .query("echoAsync", |t| t(|_, _: i32| async move { 42 })) - .query("error", |t| { - t(|_, _: ()| { - Err(rspc::Error::new( - rspc::ErrorCode::InternalServerError, - "Something went wrong".into(), - )) as Result - }) - }) - .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) - .mutation("sendMsg", |t| { - t(|_, v: String| { - println!("Client said '{}'", v); - v - }) - }) -} diff --git a/examples/src/bin/axum.rs b/examples/src/bin/axum.rs deleted file mode 100644 index b7f08d0e..00000000 --- a/examples/src/bin/axum.rs +++ /dev/null @@ -1,62 +0,0 @@ -#![allow(clippy::unused_unit)] - -use std::path::PathBuf; - -use example::{basic, selection, subscriptions}; - -use axum::{http::request::Parts, routing::get}; -use rspc::Router; -use tower_http::cors::{Any, CorsLayer}; - -#[tokio::main] -async fn main() { - let r1 = Router::::new().query("demo", |t| t(|_, _: ()| "Merging Routers!")); - - let router = ::new() - // Basic query - .query("version", |t| { - t(|_, _: ()| async move { env!("CARGO_PKG_VERSION") }) - }) - .merge("basic.", basic::mount()) - .merge("subscriptions.", subscriptions::mount()) - .merge("selection.", selection::mount()) - // This middleware changes the TCtx (context type) from `()` to `i32`. All routers being merge under need to take `i32` as their context type. - .middleware(|mw| mw.middleware(|ctx| async move { Ok(ctx.with_ctx(42i32)) })) - .merge("r1.", r1) - .build(); - - let (procedures, types) = rspc::Router2::from(router).build().unwrap(); - - rspc::Typescript::default() - .export_to( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), - &types, - ) - .unwrap(); - - let app = axum::Router::new() - .with_state(()) - .route("/", get(|| async { "Hello 'rspc'!" })) - // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. - .nest( - "/rspc", - rspc_axum::endpoint(procedures, |parts: Parts| { - println!("Client requested operation '{}'", parts.uri.path()); - - () - }), - ) - // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! - .layer( - CorsLayer::new() - .allow_methods(Any) - .allow_headers(Any) - .allow_origin(Any), - ); - - let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 - println!("listening on http://{}/rspc/version", addr); - axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) - .await - .unwrap(); -} diff --git a/examples/src/bin/cookies.rs b/examples/src/bin/cookies.rs deleted file mode 100644 index b8becef8..00000000 --- a/examples/src/bin/cookies.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Using tower_cookies as an Axum extractor right now is the best way to work with cookies from rspc. -//! An official API will likely exist in the future but this works well for now. -use std::{ops::Add, path::PathBuf}; - -use axum::routing::get; -use rspc::Config; -use time::OffsetDateTime; -use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; -use tower_http::cors::{Any, CorsLayer}; - -pub struct Ctx { - cookies: Cookies, -} - -#[tokio::main] -async fn main() { - let router = rspc::Router::::new() - .query("getCookie", |t| { - t(|ctx, _: ()| { - ctx.cookies - .get("myDemoCookie") - .map(|c| c.value().to_string()) - }) - }) - .mutation("setCookie", |t| { - t(|ctx, new_value: String| { - let mut cookie = Cookie::new("myDemoCookie", new_value); - cookie.set_expires(Some(OffsetDateTime::now_utc().add(time::Duration::DAY))); - cookie.set_path("/"); // Ensure you have this or it will default to `/rspc` which will cause issues. - ctx.cookies.add(cookie); - }) - }) - .build(); - - let (procedures, types) = rspc::Router2::from(router).build().unwrap(); - - rspc::Typescript::default() - .export_to( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), - &types, - ) - .unwrap(); - - let app = axum::Router::new() - .with_state(()) - .route("/", get(|| async { "Hello 'rspc'!" })) - // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. - .nest( - "/rspc", - rspc_axum::endpoint(procedures, |cookies: Cookies| Ctx { cookies }), - ) - .layer(CookieManagerLayer::new()) - // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! - .layer( - CorsLayer::new() - .allow_methods(Any) - .allow_headers(Any) - .allow_origin(Any), - ); - - let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 - println!("listening on http://{}/rspc/version", addr); - axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) - .await - .unwrap(); -} diff --git a/examples/src/bin/global_context.rs b/examples/src/bin/global_context.rs deleted file mode 100644 index ed7e1b50..00000000 --- a/examples/src/bin/global_context.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::{ - path::PathBuf, - sync::{ - atomic::{AtomicU16, Ordering}, - Arc, - }, -}; - -use rspc::{Config, Router}; - -#[derive(Clone)] -pub struct MyCtx { - count: Arc, -} - -#[tokio::main] -async fn main() { - let router = Router::::new() - // This is a query so it can be accessed in browser without frontend. A `mutation` - // shoudl be used if the method returns a side effect. - .query("hit", |t| { - t(|ctx, _: ()| ctx.count.fetch_add(1, Ordering::SeqCst)) - }) - .build(); - - let (procedures, types) = rspc::Router2::from(router).build().unwrap(); - - rspc::Typescript::default() - .export_to( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), - &types, - ) - .unwrap(); - - // AtomicU16 provided interior mutability but if your type does not wrap it in an - // `Arc>`. This could be your database connecton or any other value. - let count = Arc::new(AtomicU16::new(0)); - - let app = axum::Router::new().nest( - "/rspc", - rspc_axum::endpoint(procedures, move || MyCtx { - count: count.clone(), - }), - ); - - let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 - println!("listening on http://{}/rspc/hit", addr); - axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) - .await - .unwrap(); -} diff --git a/examples/src/bin/middleware.rs b/examples/src/bin/middleware.rs deleted file mode 100644 index 503faeae..00000000 --- a/examples/src/bin/middleware.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::{path::PathBuf, time::Duration}; - -use async_stream::stream; -use axum::routing::get; -use rspc::{Config, ErrorCode, MiddlewareContext, Router}; -use tokio::time::sleep; -use tower_http::cors::{Any, CorsLayer}; - -#[derive(Debug, Clone)] -pub struct UnauthenticatedContext { - pub session_id: Option, -} - -#[derive(Debug, Clone)] -#[allow(unused)] -pub struct User { - name: String, -} - -async fn db_get_user_from_session(_session_id: &str) -> User { - User { - name: "Monty Beaumont".to_string(), - } -} - -#[derive(Debug, Clone)] -#[allow(unused)] -pub struct AuthenticatedCtx { - user: User, -} - -#[tokio::main] -async fn main() { - let router = Router::::new() - // Logger middleware - .middleware(|mw| { - mw.middleware(|mw| async move { - let state = (mw.req.clone(), mw.ctx.clone(), mw.input.clone()); - Ok(mw.with_state(state)) - }) - .resp(|state, result| async move { - println!( - "[LOG] req='{:?}' ctx='{:?}' input='{:?}' result='{:?}'", - state.0, state.1, state.2, result - ); - Ok(result) - }) - }) - .query("version", |t| { - t(|_ctx, _: ()| { - println!("ANOTHER QUERY"); - env!("CARGO_PKG_VERSION") - }) - }) - // Auth middleware - .middleware(|mw| { - mw.middleware(|mw| async move { - match mw.ctx.session_id { - Some(ref session_id) => { - let user = db_get_user_from_session(session_id).await; - Ok(mw.with_ctx(AuthenticatedCtx { user })) - } - None => Err(rspc::Error::new( - ErrorCode::Unauthorized, - "Unauthorized".into(), - )), - } - }) - }) - .query("another", |t| { - t(|_, _: ()| { - println!("ANOTHER QUERY"); - "Another Result!" - }) - }) - .subscription("subscriptions.pings", |t| { - t(|_ctx, _args: ()| { - stream! { - println!("Client subscribed to 'pings'"); - for i in 0..5 { - println!("Sending ping {}", i); - yield "ping".to_string(); - sleep(Duration::from_secs(1)).await; - } - } - }) - }) - // Reject all middleware - .middleware(|mw| { - mw.middleware(|_mw| async move { - Err(rspc::Error::new( - ErrorCode::Unauthorized, - "Unauthorized".into(), - )) as Result, _> - }) - }) - // Plugin middleware // TODO: Coming soon! - // .middleware(|mw| mw.openapi(OpenAPIConfig {})) - .build(); - - let (procedures, types) = rspc::Router2::from(router).build().unwrap(); - - rspc::Typescript::default() - .export_to( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), - &types, - ) - .unwrap(); - - let app = axum::Router::new() - .route("/", get(|| async { "Hello 'rspc'!" })) - // Attach the rspc router to your axum router. The closure is used to generate the request context for each request. - .nest( - "/rspc", - rspc_axum::endpoint(procedures, || UnauthenticatedContext { - session_id: Some("abc".into()), // Change this line to control whether you are authenticated and can access the "another" query. - }), - ) - // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! - .layer( - CorsLayer::new() - .allow_methods(Any) - .allow_headers(Any) - .allow_origin(Any), - ); - - let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 - println!("listening on http://{}/rspc/version", addr); - axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) - .await - .unwrap(); -} diff --git a/examples/src/error_handling.rs b/examples/src/error_handling.rs deleted file mode 100644 index 3f493161..00000000 --- a/examples/src/error_handling.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{error, fmt}; - -use rspc::{Error, ErrorCode, Router, RouterBuilder}; - -pub enum MyCustomError { - IAmBroke, -} - -impl From for Error { - fn from(_: MyCustomError) -> Self { - Error::new(ErrorCode::InternalServerError, "I am broke".into()) - } -} - -#[derive(Debug)] -pub enum CustomRustError { - GenericError, -} - -impl fmt::Display for CustomRustError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "some Rust error!") - } -} - -impl error::Error for CustomRustError {} - -// We merge this router into the main router in `main.rs`. -// This router shows how to do error handling -pub fn mount() -> RouterBuilder { - Router::new() - .query("ok", |t| { - t(|_, _args: ()| Ok("Hello World".into()) as Result) - }) - .query("err", |t| { - t(|_, _args: ()| { - Err(Error::new( - ErrorCode::BadRequest, - "This is a custom error!".into(), - )) as Result - }) - }) - .query("errWithCause", |t| { - t(|_, _args: ()| { - Err(Error::with_cause( - ErrorCode::BadRequest, - "This is a custom error!".into(), - CustomRustError::GenericError, - )) as Result - }) - }) - .query("customErr", |t| { - t(|_, _args: ()| Ok(Err(MyCustomError::IAmBroke)?)) - }) - .query("customErrUsingInto", |t| { - t(|_, _args: ()| Err(MyCustomError::IAmBroke.into()) as Result) - }) - .query("asyncCustomError", |t| { - t( - |_, _args: ()| async move { - Err(MyCustomError::IAmBroke.into()) as Result - }, - ) - }) -} diff --git a/examples/src/lib.rs b/examples/src/lib.rs deleted file mode 100644 index 6919e701..00000000 --- a/examples/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod basic; -pub mod error_handling; -pub mod selection; -pub mod subscriptions; diff --git a/examples/src/selection.rs b/examples/src/selection.rs deleted file mode 100644 index 60867d6e..00000000 --- a/examples/src/selection.rs +++ /dev/null @@ -1,41 +0,0 @@ -use rspc::{selection, Router, RouterBuilder}; -use specta::Type; - -#[derive(Type)] -pub struct User { - pub id: i32, - pub name: String, - pub age: i32, - pub password: String, -} - -// We merge this router into the main router in `main.rs`. -// This router shows how to do basic queries and mutations and how they tak -pub fn mount() -> RouterBuilder { - Router::new() - .query("customSelection", |t| { - t(|_, _: ()| { - // The user come from your database. - let user = User { - id: 1, - name: "Monty Beaumont".to_string(), - age: 7, - password: "password".to_string(), - }; - - selection!(user, { id, name, age }) - }) - }) - .query("customSelectionOnList", |t| { - t(|_, _: ()| { - // The users come from your database. - let users = vec![User { - id: 1, - name: "Monty Beaumont".to_string(), - age: 7, - password: "password".to_string(), - }]; - selection!(users, [{ id, name, age }]) - }) - }) -} diff --git a/examples/src/subscriptions.rs b/examples/src/subscriptions.rs deleted file mode 100644 index cbf6c143..00000000 --- a/examples/src/subscriptions.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::time::Duration; - -use async_stream::stream; -use rspc::{Router, RouterBuilder}; -use tokio::time::sleep; - -// We merge this router into the main router in `main.rs`. -// This router shows how to do subscriptions. -pub fn mount() -> RouterBuilder { - Router::new().subscription("pings", |t| { - t(|_ctx, _args: ()| { - stream! { - println!("Client subscribed to 'pings'"); - for i in 0..5 { - println!("Sending ping {}", i); - yield "ping".to_string(); - sleep(Duration::from_secs(1)).await; - } - } - }) - }) - // TODO: Results being returned from subscriptions - // .subscription("errorPings", |t| t(|_ctx, _args: ()| { - // stream! { - // for i in 0..5 { - // yield Ok("ping".to_string()); - // sleep(Duration::from_secs(1)).await; - // } - // yield Err(rspc::Error::new(ErrorCode::InternalServerError, "Something went wrong".into())); - // } - // })) -} diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index 4f47e1ad..eda9774d 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -19,7 +19,7 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" -rspc = { path = "../../../rspc", features = ["typescript", "unstable"] } +rspc = { path = "../../../rspc", features = ["typescript"] } tauri-plugin-rspc = { path = "../../../integrations/tauri" } specta = { version = "=2.0.0-rc.20", features = ["derive"] } example-core = { path = "../../core" } diff --git a/examples/tauri/src-tauri/src/lib.rs b/examples/tauri/src-tauri/src/lib.rs index 3c34e8f0..603c7a99 100644 --- a/examples/tauri/src-tauri/src/lib.rs +++ b/examples/tauri/src-tauri/src/lib.rs @@ -1,10 +1,10 @@ -use example_core::{create_router, Ctx}; +use example_core::{mount, Ctx}; mod api; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let router = create_router(); + let router = mount(); let (procedures, types) = router.build().unwrap(); // TODO: Exporting types diff --git a/rspc/Cargo.toml b/rspc/Cargo.toml index 402e361c..127aad2f 100644 --- a/rspc/Cargo.toml +++ b/rspc/Cargo.toml @@ -19,32 +19,28 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -typescript = [] # TODO: "dep:specta-typescript" +typescript = ["dep:specta-typescript", "dep:serde_json"] rust = ["dep:specta-rust"] -# TODO: Remove these in future -unstable = [] -nolegacy = [] +# TODO: Remove +legacy = ["dep:rspc-legacy"] [dependencies] # Public rspc-procedure = { path = "../crates/procedure" } -# rspc-legacy = { path = "../crates/legacy" } -serde = "1" -futures = "0.3" # TODO: Drop down to `futures-core` when removing legacy stuff? -specta = { version = "=2.0.0-rc.20", features = [ +rspc-legacy = { path = "../crates/legacy", optional = true } +serde = { workspace = true } +futures-util = { workspace = true, features = ["alloc"] } +specta = { workspace = true, features = [ "serde", "serde_json", - "derive", # TODO: remove this + "derive", # TODO: remove this ] } # Private -specta-typescript = { version = "=0.0.7", features = [] } # TODO: Make optional once legacy stuff is removed - optional = true, +specta-typescript = { version = "=0.0.7", optional = true, features = [] } +serde_json = { workspace = true, optional = true } specta-rust = { git = "https://github.com/specta-rs/specta", optional = true, rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } -# Temporary # TODO: Remove -serde_json = "1.0.134" -thiserror = "2.0.9" - [lints] workspace = true diff --git a/rspc/src/languages/typescript.rs b/rspc/src/languages/typescript.rs index dde3f2dd..756b4c46 100644 --- a/rspc/src/languages/typescript.rs +++ b/rspc/src/languages/typescript.rs @@ -103,10 +103,9 @@ impl Typescript { pub fn export(&self, types: &Types) -> Result { let mut typess = types.types.clone(); - #[cfg(not(feature = "nolegacy"))] + #[cfg(feature = "legacy")] { - let legacy_types = - crate::legacy::interop::construct_legacy_bindings_type(&types.procedures); + let legacy_types = crate::legacy::construct_legacy_bindings_type(&types.procedures); #[derive(Type)] struct ProceduresLegacy; diff --git a/rspc/src/legacy/interop.rs b/rspc/src/legacy.rs similarity index 64% rename from rspc/src/legacy/interop.rs rename to rspc/src/legacy.rs index 25ebdce4..bafe73b3 100644 --- a/rspc/src/legacy/interop.rs +++ b/rspc/src/legacy.rs @@ -1,77 +1,67 @@ -use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData, panic::Location}; +//! TODO: Explain how to do it. -use futures::{stream, FutureExt, StreamExt, TryStreamExt}; +use std::{borrow::Cow, collections::BTreeMap, panic::Location}; + +use futures_util::{stream, FutureExt, StreamExt, TryStreamExt}; +use rspc_legacy::internal::{Layer, RequestContext, ValueOrStream}; use rspc_procedure::{ProcedureStream, ResolverError}; use serde_json::Value; use specta::{ datatype::{DataType, EnumRepr, EnumVariant, LiteralType}, - NamedType, SpectaID, Type, + NamedType, Type, }; use crate::{ - internal::{Layer, ProcedureKind, RequestContext, ValueOrStream}, - modern::procedure::ErasedProcedure, - procedure::ProcedureType, - types::TypesOrType, - util::literal_object, - Procedure2, Router, Router2, + modern::procedure::ErasedProcedure, procedure::ProcedureType, types::TypesOrType, + util::literal_object, ProcedureKind, }; -pub fn legacy_to_modern(mut router: Router) -> Router2 { - let mut r = Router2::new(); +impl From> for crate::Router2 { + fn from(router: rspc_legacy::Router) -> Self { + let mut r = crate::Router2::new(); - let bridged_procedures = router - .queries - .store - .into_iter() - .map(|v| (ProcedureKind::Query, v)) - .chain( - router - .mutations - .store - .into_iter() - .map(|v| (ProcedureKind::Mutation, v)), - ) - .chain( - router - .subscriptions - .store - .into_iter() - .map(|v| (ProcedureKind::Subscription, v)), - ) - .map(|(kind, (key, p))| { - ( - key.split(".") - .map(|s| s.to_string().into()) - .collect::>>(), - ErasedProcedure { - setup: Default::default(), - ty: ProcedureType { - kind, - input: p.ty.arg_ty, - output: p.ty.result_ty, - error: specta::datatype::DataType::Unknown, - // TODO: This location is obviously wrong but the legacy router has no location information. - // This will work properly with the new procedure syntax. - location: Location::caller().clone(), - }, - // location: Location::caller().clone(), // TODO: This needs to actually be correct - inner: Box::new(move |_| layer_to_procedure(key, kind, p.exec)), - }, + let (queries, mutations, subscriptions, mut type_map) = router.into_parts(); + + let bridged_procedures = queries + .into_iter() + .map(|v| (ProcedureKind::Query, v)) + .chain(mutations.into_iter().map(|v| (ProcedureKind::Mutation, v))) + .chain( + subscriptions + .into_iter() + .map(|v| (ProcedureKind::Subscription, v)), ) - }); + .map(|(kind, (key, p))| { + ( + key.split(".") + .map(|s| s.to_string().into()) + .collect::>>(), + ErasedProcedure { + setup: Default::default(), + ty: ProcedureType { + kind, + input: p.ty.arg_ty, + output: p.ty.result_ty, + error: specta::datatype::DataType::Unknown, + // TODO: This location is obviously wrong but the legacy router has no location information. + // This will work properly with the new procedure syntax. + location: Location::caller().clone(), + }, + // location: Location::caller().clone(), // TODO: This needs to actually be correct + inner: Box::new(move |_| layer_to_procedure(key.to_string(), kind, p.exec)), + }, + ) + }); - for (key, procedure) in bridged_procedures { - if r.interop_procedures() - .insert(key.clone(), procedure) - .is_some() - { - panic!("Attempted to mount '{key:?}' multiple times.\nrspc no longer supports different operations (query/mutation/subscription) with overlapping names.") + for (key, procedure) in bridged_procedures { + if r.procedures.insert(key.clone(), procedure).is_some() { + panic!("Attempted to mount '{key:?}' multiple times.\nrspc no longer supports different operations (query/mutation/subscription) with overlapping names.") + } } - } - r.interop_types().extend(&mut router.type_map); - r + r.types.extend(&mut type_map); + r + } } pub(crate) fn layer_to_procedure( @@ -86,15 +76,23 @@ pub(crate) fn layer_to_procedure( ctx, input, RequestContext { - kind: kind.clone(), + kind: match kind { + ProcedureKind::Query => rspc_legacy::internal::ProcedureKind::Query, + ProcedureKind::Mutation => { + rspc_legacy::internal::ProcedureKind::Mutation + } + ProcedureKind::Subscription => { + rspc_legacy::internal::ProcedureKind::Subscription + } + }, path: path.clone(), }, ) .map_err(|err| { - let err: crate::legacy::Error = err.into(); + let err: rspc_legacy::Error = err.into(); ResolverError::new( (), /* typesafe errors aren't supported in legacy router */ - Some(rspc_procedure::LegacyErrorInterop(err.message)), + Some(rspc_procedure::LegacyErrorInterop(err.message().into())), ) .into() }) @@ -109,17 +107,17 @@ pub(crate) fn layer_to_procedure( } Ok(ValueOrStream::Stream(s)) => s .map_err(|err| { - let err = crate::legacy::Error::from(err); + let err = rspc_legacy::Error::from(err); ResolverError::new( (), /* typesafe errors aren't supported in legacy router */ - Some(rspc_procedure::LegacyErrorInterop(err.message)), + Some(rspc_procedure::LegacyErrorInterop(err.message().into())), ) .into() }) .boxed(), Err(err) => { - let err: crate::legacy::Error = err.into(); - let err = ResolverError::new(err.message, err.cause); + let err: rspc_legacy::Error = err.into(); + let err = ResolverError::new(err.message().to_string(), err.cause()); stream::once(async { Err(err.into()) }).boxed() } } diff --git a/rspc/src/legacy/config.rs b/rspc/src/legacy/config.rs deleted file mode 100644 index 67d34e7c..00000000 --- a/rspc/src/legacy/config.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::path::PathBuf; - -/// TODO -#[derive(Default)] -pub struct Config { - pub(crate) export_bindings_on_build: Option, - pub(crate) bindings_header: Option<&'static str>, -} - -impl Config { - pub fn new() -> Self { - Default::default() - } - - /// will export the bindings of the generated router to a folder every time the router is built. - /// Note: The bindings are only exported when `debug_assertions` are enabled (Rust is in debug mode). - pub fn export_ts_bindings(mut self, export_path: TPath) -> Self - where - PathBuf: From, - { - self.export_bindings_on_build = Some(PathBuf::from(export_path)); - self - } - - /// allows you to add a custom string to the top of the exported Typescript bindings file. - /// This is useful if you want to disable ESLint or Prettier. - pub fn set_ts_bindings_header(mut self, custom: &'static str) -> Self { - self.bindings_header = Some(custom); - self - } -} diff --git a/rspc/src/legacy/error.rs b/rspc/src/legacy/error.rs deleted file mode 100644 index e38b54a4..00000000 --- a/rspc/src/legacy/error.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::{error, fmt, sync::Arc}; - -use serde::Serialize; -use specta::Type; - -#[derive(thiserror::Error, Debug)] -pub enum ExecError { - #[error("the requested operation '{0}' is not supported by this server")] - OperationNotFound(String), - #[error("error deserializing procedure arguments: {0}")] - DeserializingArgErr(serde_json::Error), - #[error("error serializing procedure result: {0}")] - SerializingResultErr(serde_json::Error), - #[error("error in axum extractor")] - AxumExtractorError, - #[error("invalid JSON-RPC version")] - InvalidJsonRpcVersion, - #[error("method '{0}' is not supported by this endpoint.")] // TODO: Better error message - UnsupportedMethod(String), - #[error("resolver threw error")] - ErrResolverError(#[from] Error), - #[error("error creating subscription with null id")] - ErrSubscriptionWithNullId, - #[error("error creating subscription with duplicate id")] - ErrSubscriptionDuplicateId, -} - -impl From for Error { - fn from(v: ExecError) -> Error { - match v { - ExecError::OperationNotFound(_) => Error { - code: ErrorCode::NotFound, - message: "the requested operation is not supported by this server".to_string(), - cause: None, - }, - ExecError::DeserializingArgErr(err) => Error { - code: ErrorCode::BadRequest, - message: "error deserializing procedure arguments".to_string(), - cause: Some(Arc::new(err)), - }, - ExecError::SerializingResultErr(err) => Error { - code: ErrorCode::InternalServerError, - message: "error serializing procedure result".to_string(), - cause: Some(Arc::new(err)), - }, - ExecError::AxumExtractorError => Error { - code: ErrorCode::BadRequest, - message: "Error running Axum extractors on the HTTP request".into(), - cause: None, - }, - ExecError::InvalidJsonRpcVersion => Error { - code: ErrorCode::BadRequest, - message: "invalid JSON-RPC version".into(), - cause: None, - }, - ExecError::ErrResolverError(err) => err, - ExecError::UnsupportedMethod(_) => Error { - code: ErrorCode::BadRequest, - message: "unsupported metho".into(), - cause: None, - }, - ExecError::ErrSubscriptionWithNullId => Error { - code: ErrorCode::BadRequest, - message: "error creating subscription with null request id".into(), - cause: None, - }, - ExecError::ErrSubscriptionDuplicateId => Error { - code: ErrorCode::BadRequest, - message: "error creating subscription with duplicate id".into(), - cause: None, - }, - } - } -} - -// impl From for JsonRPCError { -// fn from(err: ExecError) -> Self { -// let x: Error = err.into(); -// x.into() -// } -// } - -#[derive(thiserror::Error, Debug)] -pub enum ExportError { - #[error("IO error exporting bindings: {0}")] - IOErr(#[from] std::io::Error), -} - -#[derive(Debug, Clone, Serialize, Type)] -#[allow(dead_code)] -pub struct Error { - pub(crate) code: ErrorCode, - pub(crate) message: String, - #[serde(skip)] - pub(crate) cause: Option>, // We are using `Arc` instead of `Box` so we can clone the error cause `Clone` isn't dyn safe. -} - -// impl From for JsonRPCError { -// fn from(err: Error) -> Self { -// JsonRPCError { -// code: err.code.to_status_code() as i32, -// message: err.message, -// data: None, -// } -// } -// } - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "rspc::Error {{ code: {:?}, message: {} }}", - self.code, self.message - ) - } -} - -impl error::Error for Error { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - None - } -} - -impl Error { - pub const fn new(code: ErrorCode, message: String) -> Self { - Error { - code, - message, - cause: None, - } - } - - pub fn with_cause(code: ErrorCode, message: String, cause: TErr) -> Self - where - TErr: std::error::Error + Send + Sync + 'static, - { - Self { - code, - message, - cause: Some(Arc::new(cause)), - } - } -} - -/// TODO -#[derive(Debug, Clone, Serialize, Type, PartialEq, Eq)] -pub enum ErrorCode { - BadRequest, - Unauthorized, - Forbidden, - NotFound, - Timeout, - Conflict, - PreconditionFailed, - PayloadTooLarge, - MethodNotSupported, - ClientClosedRequest, - InternalServerError, -} - -impl ErrorCode { - pub fn to_status_code(&self) -> u16 { - match self { - ErrorCode::BadRequest => 400, - ErrorCode::Unauthorized => 401, - ErrorCode::Forbidden => 403, - ErrorCode::NotFound => 404, - ErrorCode::Timeout => 408, - ErrorCode::Conflict => 409, - ErrorCode::PreconditionFailed => 412, - ErrorCode::PayloadTooLarge => 413, - ErrorCode::MethodNotSupported => 405, - ErrorCode::ClientClosedRequest => 499, - ErrorCode::InternalServerError => 500, - } - } - - pub const fn from_status_code(status_code: u16) -> Option { - match status_code { - 400 => Some(ErrorCode::BadRequest), - 401 => Some(ErrorCode::Unauthorized), - 403 => Some(ErrorCode::Forbidden), - 404 => Some(ErrorCode::NotFound), - 408 => Some(ErrorCode::Timeout), - 409 => Some(ErrorCode::Conflict), - 412 => Some(ErrorCode::PreconditionFailed), - 413 => Some(ErrorCode::PayloadTooLarge), - 405 => Some(ErrorCode::MethodNotSupported), - 499 => Some(ErrorCode::ClientClosedRequest), - 500 => Some(ErrorCode::InternalServerError), - _ => None, - } - } -} diff --git a/rspc/src/legacy/internal/jsonrpc.rs b/rspc/src/legacy/internal/jsonrpc.rs deleted file mode 100644 index a93c45e2..00000000 --- a/rspc/src/legacy/internal/jsonrpc.rs +++ /dev/null @@ -1,118 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use specta::Type; - -pub use super::jsonrpc_exec::*; - -#[derive(Debug, Clone, Deserialize, Serialize, Type, PartialEq, Eq, Hash)] -#[serde(untagged)] -pub enum RequestId { - Null, - Number(u32), - String(String), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] // TODO: Type on this -pub struct Request { - pub jsonrpc: Option, // This is required in the JsonRPC spec but I make it optional. - pub id: RequestId, - #[serde(flatten)] - pub inner: RequestInner, -} - -#[derive(Debug, Clone, Deserialize, Serialize, Type)] -#[serde(tag = "method", content = "params", rename_all = "camelCase")] -pub enum RequestInner { - Query { - path: String, - input: Option, - }, - Mutation { - path: String, - input: Option, - }, - Subscription { - path: String, - input: (RequestId, Option), - }, - SubscriptionStop { - input: RequestId, - }, -} - -#[derive(Debug, Clone, Serialize)] // TODO: Add `specta::Type` when supported -pub struct Response { - pub jsonrpc: &'static str, - pub id: RequestId, - pub result: ResponseInner, -} - -#[derive(Debug, Clone, Serialize, Type)] -#[serde(tag = "type", content = "data", rename_all = "camelCase")] -pub enum ResponseInner { - Event(Value), - Response(Value), - Error(JsonRPCError), -} - -#[derive(Debug, Clone, Serialize, Type)] -pub struct JsonRPCError { - pub code: i32, - pub message: String, - pub data: Option, -} - -// #[cfg(test)] -// mod tests { -// use std::{fs::File, io::Write, path::PathBuf}; - -// use super::*; - -// #[test] -// fn export_internal_bindings() { -// // let mut file = File::create( -// // PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./packages/client/src/types.ts"), -// // ) -// // .unwrap(); -// // file.write_all( -// // b"// Do not modify this file. It was generated from the Rust types by running ``.\n\n", -// // ) -// // .unwrap(); -// // // TODO: Add an API into Specta which allows exporting a type and all types it depends on. -// // file.write_all(format!("{}\n\n", specta::ts_export::().unwrap()).as_bytes()) -// // .unwrap(); -// // file.write_all(format!("{}\n\n", specta::ts_export::().unwrap()).as_bytes()) -// // .unwrap(); -// } - -// #[test] -// fn test_request_id() { -// // println!( -// // "{}", -// // serde_json::to_string(&Request { -// // jsonrpc: None, -// // id: RequestId::Null, -// // inner: RequestInner::Query { -// // path: "test".into(), -// // input: None, -// // }, -// // }) -// // .unwrap() -// // ); -// todo!(); - -// // TODO: Test serde - -// // TODO: Test specta -// } - -// #[test] -// fn test_jsonrpc_request() { -// todo!(); -// } - -// #[test] -// fn test_jsonrpc_response() { -// todo!(); -// } -// } diff --git a/rspc/src/legacy/internal/jsonrpc_exec.rs b/rspc/src/legacy/internal/jsonrpc_exec.rs deleted file mode 100644 index 39e9cd3c..00000000 --- a/rspc/src/legacy/internal/jsonrpc_exec.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use futures::StreamExt; -use serde_json::Value; -use tokio::sync::{broadcast, mpsc, oneshot, Mutex}; - -use crate::{internal::jsonrpc, ExecError, Router}; - -use super::{ - jsonrpc::{RequestId, RequestInner, ResponseInner}, - ProcedureKind, RequestContext, ValueOrStream, -}; - -// TODO: Deduplicate this function with the httpz integration - -pub enum SubscriptionMap<'a> { - Ref(&'a mut HashMap>), - Mutex(&'a Mutex>>), - None, -} - -impl<'a> SubscriptionMap<'a> { - pub async fn has_subscription(&self, id: &RequestId) -> bool { - match self { - SubscriptionMap::Ref(map) => map.contains_key(id), - SubscriptionMap::Mutex(map) => { - let map = map.lock().await; - map.contains_key(id) - } - SubscriptionMap::None => unreachable!(), - } - } - - pub async fn insert(&mut self, id: RequestId, tx: oneshot::Sender<()>) { - match self { - SubscriptionMap::Ref(map) => { - map.insert(id, tx); - } - SubscriptionMap::Mutex(map) => { - let mut map = map.lock().await; - map.insert(id, tx); - } - SubscriptionMap::None => unreachable!(), - } - } - - pub async fn remove(&mut self, id: &RequestId) { - match self { - SubscriptionMap::Ref(map) => { - map.remove(id); - } - SubscriptionMap::Mutex(map) => { - let mut map = map.lock().await; - map.remove(id); - } - SubscriptionMap::None => unreachable!(), - } - } -} -pub enum Sender<'a> { - Channel(&'a mut mpsc::Sender), - ResponseChannel(&'a mut mpsc::UnboundedSender), - Broadcast(&'a broadcast::Sender), - Response(Option), -} - -pub enum Sender2 { - Channel(mpsc::Sender), - ResponseChannel(mpsc::UnboundedSender), - Broadcast(broadcast::Sender), -} - -impl Sender2 { - pub async fn send( - &mut self, - resp: jsonrpc::Response, - ) -> Result<(), mpsc::error::SendError> { - match self { - Self::Channel(tx) => tx.send(resp).await?, - Self::ResponseChannel(tx) => tx.send(resp)?, - Self::Broadcast(tx) => { - let _ = tx.send(resp).map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - } - - Ok(()) - } -} - -impl<'a> Sender<'a> { - pub async fn send( - &mut self, - resp: jsonrpc::Response, - ) -> Result<(), mpsc::error::SendError> { - match self { - Self::Channel(tx) => tx.send(resp).await?, - Self::ResponseChannel(tx) => tx.send(resp)?, - Self::Broadcast(tx) => { - let _ = tx.send(resp).map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - Self::Response(r) => { - *r = Some(resp); - } - } - - Ok(()) - } - - pub fn sender2(&mut self) -> Sender2 { - match self { - Self::Channel(tx) => Sender2::Channel(tx.clone()), - Self::ResponseChannel(tx) => Sender2::ResponseChannel(tx.clone()), - Self::Broadcast(tx) => Sender2::Broadcast(tx.clone()), - Self::Response(_) => unreachable!(), - } - } -} - -pub async fn handle_json_rpc( - ctx: TCtx, - req: jsonrpc::Request, - router: &Arc>, - sender: &mut Sender<'_>, - subscriptions: &mut SubscriptionMap<'_>, -) where - TCtx: 'static, -{ - if req.jsonrpc.is_some() && req.jsonrpc.as_deref() != Some("2.0") { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error(ExecError::InvalidJsonRpcVersion.into()), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - - let (path, input, procedures, sub_id) = match req.inner { - RequestInner::Query { path, input } => (path, input, router.queries(), None), - RequestInner::Mutation { path, input } => (path, input, router.mutations(), None), - RequestInner::Subscription { path, input } => { - (path, input.1, router.subscriptions(), Some(input.0)) - } - RequestInner::SubscriptionStop { input } => { - subscriptions.remove(&input).await; - return; - } - }; - - let result = match procedures - .get(&path) - .ok_or_else(|| ExecError::OperationNotFound(path.clone())) - .and_then(|v| { - v.exec.call( - ctx, - input.unwrap_or(Value::Null), - RequestContext { - kind: ProcedureKind::Query, - path, - }, - ) - }) { - Ok(op) => match op.into_value_or_stream().await { - Ok(ValueOrStream::Value(v)) => ResponseInner::Response(v), - Ok(ValueOrStream::Stream(mut stream)) => { - if matches!(sender, Sender::Response(_)) - || matches!(subscriptions, SubscriptionMap::None) - { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error( - ExecError::UnsupportedMethod("Subscription".to_string()).into(), - ), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - - if let Some(id) = sub_id { - if matches!(id, RequestId::Null) { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error( - ExecError::ErrSubscriptionWithNullId.into(), - ), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } else if subscriptions.has_subscription(&id).await { - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id.clone(), - result: ResponseInner::Error( - ExecError::ErrSubscriptionDuplicateId.into(), - ), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {}", _err); - }); - } - - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - subscriptions.insert(id.clone(), shutdown_tx).await; - let mut sender2 = sender.sender2(); - tokio::spawn(async move { - loop { - tokio::select! { - biased; // Note: Order matters - _ = &mut shutdown_rx => { - // #[cfg(feature = "tracing")] - // tracing::debug!("Removing subscription with id '{:?}'", id); - break; - } - v = stream.next() => { - match v { - Some(Ok(v)) => { - let _ = sender2.send(jsonrpc::Response { - jsonrpc: "2.0", - id: id.clone(), - result: ResponseInner::Event(v), - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {:?}", _err); - }); - } - Some(Err(_err)) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Subscription error: {:?}", _err); - } - None => { - break; - } - } - } - } - } - }); - } - - return; - } - Err(err) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Error executing operation: {:?}", err); - - ResponseInner::Error(err.into()) - } - }, - Err(err) => { - // #[cfg(feature = "tracing")] - // tracing::error!("Error executing operation: {:?}", err); - ResponseInner::Error(err.into()) - } - }; - - let _ = sender - .send(jsonrpc::Response { - jsonrpc: "2.0", - id: req.id, - result, - }) - .await - .map_err(|_err| { - // #[cfg(feature = "tracing")] - // tracing::error!("Failed to send response: {:?}", _err); - }); -} diff --git a/rspc/src/legacy/internal/middleware.rs b/rspc/src/legacy/internal/middleware.rs deleted file mode 100644 index ce5a8640..00000000 --- a/rspc/src/legacy/internal/middleware.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::{future::Future, marker::PhantomData, pin::Pin, sync::Arc}; - -use futures::Stream; -use serde_json::Value; - -use crate::{ExecError, MiddlewareLike}; - -pub trait MiddlewareBuilderLike { - type LayerContext: 'static; - - fn build(&self, next: T) -> Box> - where - T: Layer; -} - -pub struct MiddlewareMerger -where - TMiddleware: MiddlewareBuilderLike, - TIncomingMiddleware: MiddlewareBuilderLike, -{ - pub middleware: TMiddleware, - pub middleware2: TIncomingMiddleware, - pub phantom: PhantomData<(TCtx, TLayerCtx)>, -} - -impl MiddlewareBuilderLike - for MiddlewareMerger -where - TCtx: 'static, - TLayerCtx: 'static, - TNewLayerCtx: 'static, - TMiddleware: MiddlewareBuilderLike, - TIncomingMiddleware: MiddlewareBuilderLike, -{ - type LayerContext = TNewLayerCtx; - - fn build(&self, next: T) -> Box> - where - T: Layer, - { - self.middleware.build(self.middleware2.build(next)) - } -} - -pub struct MiddlewareLayerBuilder -where - TCtx: Send + Sync + 'static, - TLayerCtx: Send + Sync + 'static, - TNewLayerCtx: Send + Sync + 'static, - TMiddleware: MiddlewareBuilderLike + Send + 'static, - TNewMiddleware: MiddlewareLike, -{ - pub middleware: TMiddleware, - pub mw: TNewMiddleware, - pub phantom: PhantomData<(TCtx, TLayerCtx, TNewLayerCtx)>, -} - -impl MiddlewareBuilderLike - for MiddlewareLayerBuilder -where - TCtx: Send + Sync + 'static, - TLayerCtx: Send + Sync + 'static, - TNewLayerCtx: Send + Sync + 'static, - TMiddleware: MiddlewareBuilderLike + Send + 'static, - TNewMiddleware: MiddlewareLike + Send + Sync + 'static, -{ - type LayerContext = TNewLayerCtx; - - fn build(&self, next: T) -> Box> - where - T: Layer + Sync, - { - self.middleware.build(MiddlewareLayer { - next: Arc::new(next), - mw: self.mw.clone(), - phantom: PhantomData, - }) - } -} - -pub struct MiddlewareLayer -where - TLayerCtx: Send + 'static, - TNewLayerCtx: Send + 'static, - TMiddleware: Layer + 'static, - TNewMiddleware: MiddlewareLike + Send + Sync + 'static, -{ - next: Arc, // TODO: Avoid arcing this if possible - mw: TNewMiddleware, - phantom: PhantomData<(TLayerCtx, TNewLayerCtx)>, -} - -impl Layer - for MiddlewareLayer -where - TLayerCtx: Send + Sync + 'static, - TNewLayerCtx: Send + Sync + 'static, - TMiddleware: Layer + Sync + 'static, - TNewMiddleware: MiddlewareLike + Send + Sync + 'static, -{ - fn call( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - ) -> Result { - self.mw.handle(ctx, input, req, self.next.clone()) - } -} - -pub struct BaseMiddleware(PhantomData) -where - TCtx: 'static; - -impl Default for BaseMiddleware -where - TCtx: 'static, -{ - fn default() -> Self { - Self(PhantomData) - } -} - -impl MiddlewareBuilderLike for BaseMiddleware -where - TCtx: Send + 'static, -{ - type LayerContext = TCtx; - - fn build(&self, next: T) -> Box> - where - T: Layer, - { - Box::new(next) - } -} - -// TODO: Rename this so it doesn't conflict with the middleware builder struct -pub trait Layer: Send + Sync + 'static { - fn call(&self, a: TLayerCtx, b: Value, c: RequestContext) -> Result; -} - -pub struct ResolverLayer -where - TLayerCtx: Send + Sync + 'static, - T: Fn(TLayerCtx, Value, RequestContext) -> Result - + Send - + Sync - + 'static, -{ - pub func: T, - pub phantom: PhantomData, -} - -impl Layer for ResolverLayer -where - TLayerCtx: Send + Sync + 'static, - T: Fn(TLayerCtx, Value, RequestContext) -> Result - + Send - + Sync - + 'static, -{ - fn call(&self, a: TLayerCtx, b: Value, c: RequestContext) -> Result { - (self.func)(a, b, c) - } -} - -impl Layer for Box + 'static> -where - TLayerCtx: 'static, -{ - fn call(&self, a: TLayerCtx, b: Value, c: RequestContext) -> Result { - (**self).call(a, b, c) - } -} - -// TODO: Is this a duplicate of any type? -// TODO: Move into public API cause it might be used in middleware -pub use crate::ProcedureKind; - -// TODO: Maybe rename to `Request` or something else. Also move into Public API cause it might be used in middleware -#[derive(Debug, Clone)] -pub struct RequestContext { - pub kind: ProcedureKind, - pub path: String, // TODO: String slice?? -} - -pub enum ValueOrStream { - Value(Value), - Stream(Pin> + Send>>), -} - -pub enum ValueOrStreamOrFutureStream { - Value(Value), - Stream(Pin> + Send>>), -} - -pub enum LayerResult { - Future(Pin> + Send>>), - Stream(Pin> + Send>>), - FutureValueOrStream(Pin> + Send>>), - FutureValueOrStreamOrFutureStream( - Pin> + Send>>, - ), - Ready(Result), -} - -impl LayerResult { - pub async fn into_value_or_stream(self) -> Result { - match self { - LayerResult::Stream(stream) => Ok(ValueOrStream::Stream(stream)), - LayerResult::Future(fut) => Ok(ValueOrStream::Value(fut.await?)), - LayerResult::FutureValueOrStream(fut) => Ok(fut.await?), - LayerResult::FutureValueOrStreamOrFutureStream(fut) => Ok(match fut.await? { - ValueOrStreamOrFutureStream::Value(val) => ValueOrStream::Value(val), - ValueOrStreamOrFutureStream::Stream(stream) => ValueOrStream::Stream(stream), - }), - LayerResult::Ready(res) => Ok(ValueOrStream::Value(res?)), - } - } -} diff --git a/rspc/src/legacy/internal/mod.rs b/rspc/src/legacy/internal/mod.rs deleted file mode 100644 index 7afac255..00000000 --- a/rspc/src/legacy/internal/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Internal types which power rspc. The module provides no guarantee of compatibility between updates, so you should be careful rely on types from it. - -// mod jsonrpc_exec; -mod middleware; -mod procedure_builder; -mod procedure_store; - -pub(crate) use middleware::*; -pub(crate) use procedure_builder::*; -pub(crate) use procedure_store::*; - -// Used by `rspc_axum` -pub use middleware::ProcedureKind; -// pub mod jsonrpc; diff --git a/rspc/src/legacy/internal/procedure_builder.rs b/rspc/src/legacy/internal/procedure_builder.rs deleted file mode 100644 index ff680db0..00000000 --- a/rspc/src/legacy/internal/procedure_builder.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::{marker::PhantomData, ops::Deref}; - -pub struct UnbuiltProcedureBuilder { - deref_handler: fn(TResolver) -> BuiltProcedureBuilder, - phantom: PhantomData, -} - -impl Default for UnbuiltProcedureBuilder { - fn default() -> Self { - Self { - deref_handler: |resolver| BuiltProcedureBuilder { resolver }, - phantom: PhantomData, - } - } -} - -impl UnbuiltProcedureBuilder { - pub fn resolver(self, resolver: TResolver) -> BuiltProcedureBuilder { - (self.deref_handler)(resolver) - } -} - -impl Deref for UnbuiltProcedureBuilder { - type Target = fn(resolver: TResolver) -> BuiltProcedureBuilder; - - fn deref(&self) -> &Self::Target { - &self.deref_handler - } -} - -pub struct BuiltProcedureBuilder { - pub resolver: TResolver, -} diff --git a/rspc/src/legacy/internal/procedure_store.rs b/rspc/src/legacy/internal/procedure_store.rs deleted file mode 100644 index 432920d9..00000000 --- a/rspc/src/legacy/internal/procedure_store.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::collections::BTreeMap; - -use specta::DataType; - -use super::Layer; - -// TODO: Make private -#[derive(Debug)] -pub struct ProcedureDataType { - pub arg_ty: DataType, - pub result_ty: DataType, -} - -// TODO: Make private -pub struct Procedure { - pub exec: Box>, - pub ty: ProcedureDataType, -} - -pub struct ProcedureStore { - name: &'static str, - pub store: BTreeMap>, -} - -impl ProcedureStore { - pub fn new(name: &'static str) -> Self { - Self { - name, - store: Default::default(), - } - } - - pub fn append(&mut self, key: String, exec: Box>, ty: ProcedureDataType) { - #[allow(clippy::panic)] - if key.is_empty() || key == "ws" || key.starts_with("rpc.") || key.starts_with("rspc.") { - panic!( - "rspc error: attempted to create {} operation named '{}', however this name is not allowed.", - self.name, - key - ); - } - - #[allow(clippy::panic)] - if self.store.contains_key(&key) { - panic!( - "rspc error: {} operation already has resolver with name '{}'", - self.name, key - ); - } - - self.store.insert(key, Procedure { exec, ty }); - } -} diff --git a/rspc/src/legacy/middleware.rs b/rspc/src/legacy/middleware.rs deleted file mode 100644 index b1a7718c..00000000 --- a/rspc/src/legacy/middleware.rs +++ /dev/null @@ -1,338 +0,0 @@ -use futures::StreamExt; -use serde_json::Value; -use std::{future::Future, marker::PhantomData, sync::Arc}; - -use crate::{ - internal::{Layer, LayerResult, RequestContext, ValueOrStream, ValueOrStreamOrFutureStream}, - ExecError, -}; - -pub trait MiddlewareLike: Clone { - type State: Clone + Send + Sync + 'static; - type NewCtx: Send + 'static; - - fn handle + 'static>( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - next: Arc, - ) -> Result; -} -pub struct MiddlewareContext -where - TState: Send, -{ - pub state: TState, - pub input: Value, - pub ctx: TNewCtx, - pub req: RequestContext, - pub phantom: PhantomData, -} - -// This will match were TState is the default (`()`) so it shouldn't let you call it if you've already swapped the generic -impl MiddlewareContext -where - TLayerCtx: Send, -{ - pub fn with_state(self, state: TState) -> MiddlewareContext - where - TState: Send, - { - MiddlewareContext { - state, - input: self.input, - ctx: self.ctx, - req: self.req, - phantom: PhantomData, - } - } -} - -// This will match were TNewCtx is the default (`TCtx`) so it shouldn't let you call it if you've already swapped the generic -impl MiddlewareContext -where - TLayerCtx: Send, - TState: Send, -{ - pub fn with_ctx( - self, - new_ctx: TNewCtx, - ) -> MiddlewareContext { - MiddlewareContext { - state: self.state, - input: self.input, - ctx: new_ctx, - req: self.req, - phantom: PhantomData, - } - } -} - -pub struct Middleware -where - TState: Send, - TLayerCtx: Send, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, -{ - handler: THandlerFunc, - phantom: PhantomData<(TState, TLayerCtx)>, -} - -impl Clone - for Middleware -where - TState: Send, - TLayerCtx: Send, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, -{ - fn clone(&self) -> Self { - Self { - handler: self.handler.clone(), - phantom: PhantomData, - } - } -} - -pub struct MiddlewareBuilder(pub PhantomData) -where - TLayerCtx: Send; - -impl MiddlewareBuilder -where - TLayerCtx: Send, -{ - pub fn middleware( - &self, - handler: THandlerFunc, - ) -> Middleware - where - TState: Send, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, - { - Middleware { - handler, - phantom: PhantomData, - } - } -} - -impl - Middleware -where - TState: Send, - TLayerCtx: Send, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, -{ - pub fn resp( - self, - handler: TRespHandlerFunc, - ) -> MiddlewareWithResponseHandler< - TState, - TLayerCtx, - TNewCtx, - THandlerFunc, - THandlerFut, - TRespHandlerFunc, - TRespHandlerFut, - > - where - TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, - TRespHandlerFut: Future> + Send + 'static, - { - MiddlewareWithResponseHandler { - handler: self.handler, - resp_handler: handler, - phantom: PhantomData, - } - } -} - -pub struct MiddlewareWithResponseHandler< - TState, - TLayerCtx, - TNewCtx, - THandlerFunc, - THandlerFut, - TRespHandlerFunc, - TRespHandlerFut, -> where - TState: Send, - TLayerCtx: Send, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, - TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, - TRespHandlerFut: Future> + Send + 'static, -{ - handler: THandlerFunc, - resp_handler: TRespHandlerFunc, - phantom: PhantomData<(TState, TLayerCtx)>, -} - -impl Clone - for MiddlewareWithResponseHandler< - TState, - TLayerCtx, - TNewCtx, - THandlerFunc, - THandlerFut, - TRespHandlerFunc, - TRespHandlerFut, - > -where - TState: Send, - TLayerCtx: Send, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, - TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, - TRespHandlerFut: Future> + Send + 'static, -{ - fn clone(&self) -> Self { - Self { - handler: self.handler.clone(), - resp_handler: self.resp_handler.clone(), - phantom: PhantomData, - } - } -} - -impl MiddlewareLike - for Middleware -where - TState: Clone + Send + Sync + 'static, - TLayerCtx: Send, - TNewCtx: Send + 'static, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, -{ - type State = TState; - type NewCtx = TNewCtx; - - fn handle + 'static>( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - next: Arc, - ) -> Result { - let handler = (self.handler)(MiddlewareContext { - state: (), - ctx, - input, - req, - phantom: PhantomData, - }); - - Ok(LayerResult::FutureValueOrStream(Box::pin(async move { - let handler = handler.await?; - next.call(handler.ctx, handler.input, handler.req)? - .into_value_or_stream() - .await - }))) - } -} - -enum FutOrValue>> { - Fut(T), - Value(Result), -} - -impl - MiddlewareLike - for MiddlewareWithResponseHandler< - TState, - TLayerCtx, - TNewCtx, - THandlerFunc, - THandlerFut, - TRespHandlerFunc, - TRespHandlerFut, - > -where - TState: Clone + Send + Sync + 'static, - TLayerCtx: Send + 'static, - TNewCtx: Send + 'static, - THandlerFunc: Fn(MiddlewareContext) -> THandlerFut + Clone, - THandlerFut: Future, crate::Error>> - + Send - + 'static, - TRespHandlerFunc: Fn(TState, Value) -> TRespHandlerFut + Clone + Sync + Send + 'static, - TRespHandlerFut: Future> + Send + 'static, -{ - type State = TState; - type NewCtx = TNewCtx; - - fn handle + 'static>( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - next: Arc, - ) -> Result { - let handler = (self.handler)(MiddlewareContext { - state: (), - ctx, - input, - req, - // new_ctx: None, - phantom: PhantomData, - }); - - let f = self.resp_handler.clone(); // TODO: Runtime clone is bad. Avoid this! - - Ok(LayerResult::FutureValueOrStreamOrFutureStream(Box::pin( - async move { - let handler = handler.await?; - - Ok( - match next - .call(handler.ctx, handler.input, handler.req)? - .into_value_or_stream() - .await? - { - ValueOrStream::Value(v) => { - ValueOrStreamOrFutureStream::Value(f(handler.state, v).await?) - } - ValueOrStream::Stream(s) => { - ValueOrStreamOrFutureStream::Stream(Box::pin(s.then(move |v| { - let v = match v { - Ok(v) => FutOrValue::Fut(f(handler.state.clone(), v)), - e => FutOrValue::Value(e), - }; - - async move { - match v { - FutOrValue::Fut(fut) => { - fut.await.map_err(ExecError::ErrResolverError) - } - FutOrValue::Value(v) => v, - } - } - }))) - } - }, - ) - }, - ))) - } -} - -// TODO: Middleware functions should be able to be async or sync & return a value or result diff --git a/rspc/src/legacy/mod.rs b/rspc/src/legacy/mod.rs deleted file mode 100644 index db67e19a..00000000 --- a/rspc/src/legacy/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -mod config; -mod error; -pub(crate) mod interop; -mod middleware; -mod resolver; -mod resolver_result; -mod router; -mod router_builder; -mod selection; - -pub use config::Config; -pub use error::{Error, ErrorCode, ExecError, ExportError}; -pub use middleware::{ - Middleware, MiddlewareBuilder, MiddlewareContext, MiddlewareLike, MiddlewareWithResponseHandler, -}; -pub use resolver::{typedef, DoubleArgMarker, DoubleArgStreamMarker, Resolver, StreamResolver}; -pub use resolver_result::{FutureMarker, RequestLayer, ResultMarker, SerializeMarker}; -pub use router::{ExecKind, Router}; -pub use router_builder::RouterBuilder; - -pub mod internal; - -#[deprecated = "Not going to be included in 0.4.0. The function is 5 lines so copy into your project!"] -#[cfg(debug_assertions)] -#[allow(clippy::panic)] -pub fn test_result_type() { - panic!("You should not call `test_type` at runtime. This is just a debugging tool."); -} - -#[deprecated = "Not going to be included in 0.4.0. The function is 5 lines so copy into your project!"] -#[cfg(debug_assertions)] -#[allow(clippy::panic)] -pub fn test_result_value(_: T) { - panic!("You should not call `test_type` at runtime. This is just a debugging tool."); -} diff --git a/rspc/src/legacy/resolver.rs b/rspc/src/legacy/resolver.rs deleted file mode 100644 index 3f335f66..00000000 --- a/rspc/src/legacy/resolver.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::marker::PhantomData; - -use futures::{Stream, StreamExt}; -use serde::{de::DeserializeOwned, Serialize}; -use serde_json::Value; -use specta::{Type, TypeCollection}; - -use crate::{ - internal::{LayerResult, ProcedureDataType}, - ExecError, RequestLayer, -}; - -pub trait Resolver { - type Result; - - fn exec(&self, ctx: TCtx, input: Value) -> Result; - - fn typedef(defs: &mut TypeCollection) -> ProcedureDataType; -} - -// pub struct NoArgMarker(/* private */ PhantomData); -// impl Resolver> for TFunc -// where -// TFunc: Fn() -> TResult, -// TResult: IntoLayerResult + Type, -// { -// fn exec(&self, _ctx: TCtx, _arg: Value) -> Result { -// self().into_layer_result() -// } -// -// fn typedef(defs: &mut TypeDefs) -> ProcedureDataType { -// ProcedureDataType { -// arg_ty: <() as Type>::def(DefOpts { -// parent_inline: true, -// type_map: defs, -// }), -// result_ty: ::def(DefOpts { -// parent_inline: true, -// type_map: defs, -// }), -// } -// } -// } -// -// pub struct SingleArgMarker(/* private */ PhantomData); -// impl Resolver> for TFunc -// where -// TFunc: Fn(TCtx) -> TResult, -// TResult: IntoLayerResult, -// { -// fn exec(&self, ctx: TCtx, _arg: Value) -> Result { -// self(ctx).into_layer_result() -// } -// -// fn typedef(defs: &mut TypeDefs) -> ProcedureDataType { -// ProcedureDataType { -// arg_ty: <() as Type>::def(DefOpts { -// parent_inline: true, -// type_map: defs, -// }), -// result_ty: ::def(DefOpts { -// parent_inline: true, -// type_map: defs, -// }), -// } -// } -// } - -pub struct DoubleArgMarker( - /* private */ PhantomData<(TArg, TResultMarker)>, -); -impl Resolver> - for TFunc -where - TArg: DeserializeOwned + Type, - TFunc: Fn(TCtx, TArg) -> TResult, - TResult: RequestLayer, -{ - type Result = TResult; - - fn exec(&self, ctx: TCtx, input: Value) -> Result { - let input = serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?; - self(ctx, input).into_layer_result() - } - - fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { - typedef::(defs) - } -} - -pub trait StreamResolver { - fn exec(&self, ctx: TCtx, input: Value) -> Result; - - fn typedef(defs: &mut TypeCollection) -> ProcedureDataType; -} - -pub struct DoubleArgStreamMarker( - /* private */ PhantomData<(TArg, TResult, TStream)>, -); -impl - StreamResolver> for TFunc -where - TArg: DeserializeOwned + Type, - TFunc: Fn(TCtx, TArg) -> TStream, - TStream: Stream + Send + Sync + 'static, - TResult: Serialize + Type, -{ - fn exec(&self, ctx: TCtx, input: Value) -> Result { - let input = serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?; - Ok(LayerResult::Stream(Box::pin(self(ctx, input).map(|v| { - serde_json::to_value(&v).map_err(ExecError::SerializingResultErr) - })))) - } - - fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { - typedef::(defs) - } -} - -pub fn typedef(defs: &mut TypeCollection) -> ProcedureDataType { - let arg_ty = TArg::reference(defs, &[]).inner; - let result_ty = TResult::reference(defs, &[]).inner; - - ProcedureDataType { arg_ty, result_ty } -} diff --git a/rspc/src/legacy/resolver_result.rs b/rspc/src/legacy/resolver_result.rs deleted file mode 100644 index 75d4dc5d..00000000 --- a/rspc/src/legacy/resolver_result.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::{future::Future, marker::PhantomData}; - -use serde::Serialize; -use specta::Type; - -use crate::{ - internal::{LayerResult, ValueOrStream}, - Error, ExecError, -}; - -pub trait RequestLayer { - type Result: Type; - - fn into_layer_result(self) -> Result; -} - -pub struct SerializeMarker(PhantomData<()>); -impl RequestLayer for T -where - T: Serialize + Type, -{ - type Result = T; - - fn into_layer_result(self) -> Result { - Ok(LayerResult::Ready(Ok( - serde_json::to_value(self).map_err(ExecError::SerializingResultErr)? - ))) - } -} - -pub struct ResultMarker(PhantomData<()>); -impl RequestLayer for Result -where - T: Serialize + Type, -{ - type Result = T; - - fn into_layer_result(self) -> Result { - Ok(LayerResult::Ready(Ok(serde_json::to_value( - self.map_err(ExecError::ErrResolverError)?, - ) - .map_err(ExecError::SerializingResultErr)?))) - } -} - -pub struct FutureMarker(PhantomData); -impl RequestLayer> for TFut -where - TFut: Future + Send + 'static, - T: RequestLayer + Send, -{ - type Result = T::Result; - - fn into_layer_result(self) -> Result { - Ok(LayerResult::Future(Box::pin(async move { - match self - .await - .into_layer_result()? - .into_value_or_stream() - .await? - { - ValueOrStream::Stream(_) => unreachable!(), - ValueOrStream::Value(v) => Ok(v), - } - }))) - } -} diff --git a/rspc/src/legacy/router.rs b/rspc/src/legacy/router.rs deleted file mode 100644 index d092d955..00000000 --- a/rspc/src/legacy/router.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::{ - collections::BTreeMap, - fs::{self, File}, - io::Write, - marker::PhantomData, - path::{Path, PathBuf}, - pin::Pin, - sync::Arc, -}; - -use futures::Stream; -use serde_json::Value; -use specta::{datatype::FunctionResultVariant, DataType, TypeCollection}; -use specta_typescript::{self as ts, datatype, export_named_datatype, Typescript}; - -use crate::{ - internal::{Procedure, ProcedureKind, ProcedureStore, RequestContext, ValueOrStream}, - Config, ExecError, ExportError, -}; - -/// TODO -pub struct Router -where - TCtx: 'static, -{ - pub(crate) config: Config, - pub(crate) queries: ProcedureStore, - pub(crate) mutations: ProcedureStore, - pub(crate) subscriptions: ProcedureStore, - pub(crate) type_map: TypeCollection, - pub(crate) phantom: PhantomData, -} - -// TODO: Move this out of this file -// TODO: Rename?? -pub enum ExecKind { - Query, - Mutation, -} - -impl Router -where - TCtx: 'static, -{ - pub async fn exec( - &self, - ctx: TCtx, - kind: ExecKind, - key: String, - input: Option, - ) -> Result { - let (operations, kind) = match kind { - ExecKind::Query => (&self.queries.store, ProcedureKind::Query), - ExecKind::Mutation => (&self.mutations.store, ProcedureKind::Mutation), - }; - - match operations - .get(&key) - .ok_or_else(|| ExecError::OperationNotFound(key.clone()))? - .exec - .call( - ctx, - input.unwrap_or(Value::Null), - RequestContext { - kind, - path: key.clone(), - }, - )? - .into_value_or_stream() - .await? - { - ValueOrStream::Value(v) => Ok(v), - ValueOrStream::Stream(_) => Err(ExecError::UnsupportedMethod(key)), - } - } - - pub async fn exec_subscription( - &self, - ctx: TCtx, - key: String, - input: Option, - ) -> Result> + Send>>, ExecError> { - match self - .subscriptions - .store - .get(&key) - .ok_or_else(|| ExecError::OperationNotFound(key.clone()))? - .exec - .call( - ctx, - input.unwrap_or(Value::Null), - RequestContext { - kind: ProcedureKind::Subscription, - path: key.clone(), - }, - )? - .into_value_or_stream() - .await? - { - ValueOrStream::Value(_) => Err(ExecError::UnsupportedMethod(key)), - ValueOrStream::Stream(s) => Ok(s), - } - } - - pub fn arced(self) -> Arc { - Arc::new(self) - } - - #[deprecated = "Use `Self::type_map`"] - pub fn typ_store(&self) -> TypeCollection { - self.type_map.clone() - } - - pub fn type_map(&self) -> TypeCollection { - self.type_map.clone() - } - - pub fn queries(&self) -> &BTreeMap> { - &self.queries.store - } - - pub fn mutations(&self) -> &BTreeMap> { - &self.mutations.store - } - - pub fn subscriptions(&self) -> &BTreeMap> { - &self.subscriptions.store - } - - #[allow(clippy::unwrap_used)] // TODO - pub fn export_ts>(&self, export_path: TPath) -> Result<(), ExportError> { - let export_path = PathBuf::from(export_path.as_ref()); - if let Some(export_dir) = export_path.parent() { - fs::create_dir_all(export_dir)?; - } - let mut file = File::create(export_path)?; - if let Some(header) = &self.config.bindings_header { - writeln!(file, "{}", header)?; - } - writeln!(file, "// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.")?; - - let config = Typescript::new().bigint( - ts::BigIntExportBehavior::FailWithReason( - "rspc does not support exporting bigint types (i64, u64, i128, u128) because they are lossily decoded by `JSON.parse` on the frontend. Tracking issue: https://github.com/specta-rs/rspc/issues/93", - ) - ); - - let queries_ts = generate_procedures_ts(&config, &self.queries.store, &self.type_map); - let mutations_ts = generate_procedures_ts(&config, &self.mutations.store, &self.type_map); - let subscriptions_ts = - generate_procedures_ts(&config, &self.subscriptions.store, &self.type_map); - - // TODO: Specta API - writeln!( - file, - r#" -export type Procedures = {{ - queries: {queries_ts}, - mutations: {mutations_ts}, - subscriptions: {subscriptions_ts} -}};"# - )?; - - // Generate type exports (non-Procedures) - for export in self - .type_map - .into_iter() - .map(|(_, ty)| export_named_datatype(&config, ty, &self.type_map).unwrap()) - { - writeln!(file, "\n{}", export)?; - } - - Ok(()) - } -} - -// TODO: Move this out into a Specta API -fn generate_procedures_ts( - config: &Typescript, - procedures: &BTreeMap>, - type_map: &TypeCollection, -) -> String { - match procedures.len() { - 0 => "never".to_string(), - _ => procedures - .iter() - .map(|(key, operation)| { - let input = match &operation.ty.arg_ty { - DataType::Tuple(def) - // This condition is met with an empty enum or `()`. - if def.elements().is_empty() => - { - "never".into() - } - #[allow(clippy::unwrap_used)] // TODO - ty => datatype(config, &FunctionResultVariant::Value(ty.clone()), type_map).unwrap(), - }; - #[allow(clippy::unwrap_used)] // TODO - let result_ts = datatype( - config, - &FunctionResultVariant::Value(operation.ty.result_ty.clone()), - type_map, - ) - .unwrap(); - - // TODO: Specta API - format!( - r#" - {{ key: "{key}", input: {input}, result: {result_ts} }}"# - ) - }) - .collect::>() - .join(" | "), - } -} diff --git a/rspc/src/legacy/router_builder.rs b/rspc/src/legacy/router_builder.rs deleted file mode 100644 index 872bef45..00000000 --- a/rspc/src/legacy/router_builder.rs +++ /dev/null @@ -1,365 +0,0 @@ -use std::marker::PhantomData; - -use futures::Stream; -use serde::{de::DeserializeOwned, Serialize}; -use specta::{Type, TypeCollection}; - -use crate::{ - internal::{ - BaseMiddleware, BuiltProcedureBuilder, MiddlewareBuilderLike, MiddlewareLayerBuilder, - MiddlewareMerger, ProcedureStore, ResolverLayer, UnbuiltProcedureBuilder, - }, - Config, DoubleArgStreamMarker, ExecError, MiddlewareBuilder, MiddlewareLike, RequestLayer, - Resolver, Router, StreamResolver, -}; - -pub struct RouterBuilder< - TCtx = (), // The is the context the current router was initialised with - TMeta = (), - TMiddleware = BaseMiddleware, -> where - TCtx: Send + Sync + 'static, - TMeta: Send + 'static, - TMiddleware: MiddlewareBuilderLike + Send + 'static, -{ - config: Config, - middleware: TMiddleware, - queries: ProcedureStore, - mutations: ProcedureStore, - subscriptions: ProcedureStore, - type_map: TypeCollection, - phantom: PhantomData, -} - -#[allow(clippy::new_without_default, clippy::new_ret_no_self)] -impl Router -where - TCtx: Send + Sync + 'static, - TMeta: Send + 'static, -{ - pub fn new() -> RouterBuilder> { - RouterBuilder::new() - } -} - -#[allow(clippy::new_without_default)] -impl RouterBuilder> -where - TCtx: Send + Sync + 'static, - TMeta: Send + 'static, -{ - pub fn new() -> Self { - Self { - config: Config::new(), - middleware: BaseMiddleware::default(), - queries: ProcedureStore::new("query"), - mutations: ProcedureStore::new("mutation"), - subscriptions: ProcedureStore::new("subscription"), - type_map: Default::default(), - phantom: PhantomData, - } - } -} - -impl RouterBuilder -where - TCtx: Send + Sync + 'static, - TMeta: Send + 'static, - TLayerCtx: Send + Sync + 'static, - TMiddleware: MiddlewareBuilderLike + Send + 'static, -{ - // /// Attach a configuration to the router. Calling this multiple times will overwrite the previous config. - // pub fn config(mut self, config: Config) -> Self { - // self.config = config; - // self - // } - - pub fn middleware( - self, - builder: impl Fn(MiddlewareBuilder) -> TNewMiddleware, - ) -> RouterBuilder< - TCtx, - TMeta, - MiddlewareLayerBuilder, - > - where - TNewLayerCtx: Send + Sync + 'static, - TNewMiddleware: MiddlewareLike + Send + Sync + 'static, - { - let Self { - config, - middleware, - queries, - mutations, - subscriptions, - type_map, - .. - } = self; - - let mw = builder(MiddlewareBuilder(PhantomData)); - RouterBuilder { - config, - middleware: MiddlewareLayerBuilder { - middleware, - mw, - phantom: PhantomData, - }, - queries, - mutations, - subscriptions, - type_map, - phantom: PhantomData, - } - } - - pub fn query( - mut self, - key: &'static str, - builder: impl Fn( - UnbuiltProcedureBuilder, - ) -> BuiltProcedureBuilder, - ) -> Self - where - TArg: DeserializeOwned + Type, - TResult: RequestLayer, - TResolver: Fn(TLayerCtx, TArg) -> TResult + Send + Sync + 'static, - { - let resolver = builder(UnbuiltProcedureBuilder::default()).resolver; - self.queries.append( - key.into(), - self.middleware.build(ResolverLayer { - func: move |ctx, input, _| { - resolver.exec( - ctx, - serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?, - ) - }, - phantom: PhantomData, - }), - TResolver::typedef(&mut self.type_map), - ); - self - } - - pub fn mutation( - mut self, - key: &'static str, - builder: impl Fn( - UnbuiltProcedureBuilder, - ) -> BuiltProcedureBuilder, - ) -> Self - where - TArg: DeserializeOwned + Type, - TResult: RequestLayer, - TResolver: Fn(TLayerCtx, TArg) -> TResult + Send + Sync + 'static, - { - let resolver = builder(UnbuiltProcedureBuilder::default()).resolver; - self.mutations.append( - key.into(), - self.middleware.build(ResolverLayer { - func: move |ctx, input, _| { - resolver.exec( - ctx, - serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?, - ) - }, - phantom: PhantomData, - }), - TResolver::typedef(&mut self.type_map), - ); - self - } - - pub fn subscription( - mut self, - key: &'static str, - builder: impl Fn( - UnbuiltProcedureBuilder, - ) -> BuiltProcedureBuilder, - ) -> Self - where - TArg: DeserializeOwned + Type, - TStream: Stream + Send + 'static, - TResult: Serialize + Type, - TResolver: Fn(TLayerCtx, TArg) -> TStream - + StreamResolver> - + Send - + Sync - + 'static, - { - let resolver = builder(UnbuiltProcedureBuilder::default()).resolver; - self.subscriptions.append( - key.into(), - self.middleware.build(ResolverLayer { - func: move |ctx, input, _| { - resolver.exec( - ctx, - serde_json::from_value(input).map_err(ExecError::DeserializingArgErr)?, - ) - }, - phantom: PhantomData, - }), - TResolver::typedef(&mut self.type_map), - ); - self - } - - pub fn merge( - mut self, - prefix: &'static str, - router: RouterBuilder, - ) -> Self - where - TNewLayerCtx: 'static, - TIncomingMiddleware: - MiddlewareBuilderLike + Send + 'static, - { - #[allow(clippy::panic)] - if prefix.is_empty() || prefix.starts_with("rpc.") || prefix.starts_with("rspc.") { - panic!( - "rspc error: attempted to merge a router with the prefix '{}', however this name is not allowed.", - prefix - ); - } - - // TODO: The `data` field has gotta flow from the root router to the leaf routers so that we don't have to merge user defined types. - - for (key, query) in router.queries.store { - // query.ty.key = format!("{}{}", prefix, key); - self.queries.append( - format!("{}{}", prefix, key), - self.middleware.build(query.exec), - query.ty, - ); - } - - for (key, mutation) in router.mutations.store { - // mutation.ty.key = format!("{}{}", prefix, key); - self.mutations.append( - format!("{}{}", prefix, key), - self.middleware.build(mutation.exec), - mutation.ty, - ); - } - - for (key, subscription) in router.subscriptions.store { - // subscription.ty.key = format!("{}{}", prefix, key); - self.subscriptions.append( - format!("{}{}", prefix, key), - self.middleware.build(subscription.exec), - subscription.ty, - ); - } - - self.type_map.extend(&router.type_map); - - self - } - - /// `legacy_merge` maintains the `merge` functionality prior to release 0.1.3 - /// It will flow the `TMiddleware` and `TCtx` out of the child router to the parent router. - /// This was a confusing behavior and is generally not useful so it has been deprecated. - /// - /// This function will be remove in a future release. If you are using it open a GitHub issue to discuss your use case and longer term solutions for it. - pub fn legacy_merge( - self, - prefix: &'static str, - router: RouterBuilder, - ) -> RouterBuilder< - TCtx, - TMeta, - MiddlewareMerger, - > - where - TNewLayerCtx: 'static, - TIncomingMiddleware: - MiddlewareBuilderLike + Send + 'static, - { - #[allow(clippy::panic)] - if prefix.is_empty() || prefix.starts_with("rpc.") || prefix.starts_with("rspc.") { - panic!( - "rspc error: attempted to merge a router with the prefix '{}', however this name is not allowed.", - prefix - ); - } - - let Self { - config, - middleware, - mut queries, - mut mutations, - mut subscriptions, - mut type_map, - .. - } = self; - - for (key, query) in router.queries.store { - queries.append( - format!("{}{}", prefix, key), - middleware.build(query.exec), - query.ty, - ); - } - - for (key, mutation) in router.mutations.store { - mutations.append( - format!("{}{}", prefix, key), - middleware.build(mutation.exec), - mutation.ty, - ); - } - - for (key, subscription) in router.subscriptions.store { - subscriptions.append( - format!("{}{}", prefix, key), - middleware.build(subscription.exec), - subscription.ty, - ); - } - - type_map.extend(&router.type_map); - - RouterBuilder { - config, - middleware: MiddlewareMerger { - middleware, - middleware2: router.middleware, - phantom: PhantomData, - }, - queries, - mutations, - subscriptions, - type_map, - phantom: PhantomData, - } - } - - pub fn build(self) -> Router { - let Self { - config, - queries, - mutations, - subscriptions, - type_map, - .. - } = self; - - let export_path = config.export_bindings_on_build.clone(); - let router = Router { - config, - queries, - mutations, - subscriptions, - type_map, - phantom: PhantomData, - }; - - #[cfg(debug_assertions)] - #[allow(clippy::unwrap_used)] - if let Some(export_path) = export_path { - router.export_ts(export_path).unwrap(); - } - - router - } -} diff --git a/rspc/src/legacy/selection.rs b/rspc/src/legacy/selection.rs deleted file mode 100644 index ead3b540..00000000 --- a/rspc/src/legacy/selection.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! The selection macro. -//! -//! WARNING: Wherever this is called you must have the `specta` crate installed. -#[macro_export] -macro_rules! selection { - ( $s:expr, { $($n:ident),+ } ) => {{ - #[allow(non_camel_case_types)] - mod selection { - #[derive(serde::Serialize, specta::Type)] - #[specta(inline)] - pub struct Selection<$($n,)*> { - $(pub $n: $n),* - } - } - use selection::Selection; - #[allow(non_camel_case_types)] - Selection { $($n: $s.$n,)* } - }}; - ( $s:expr, [{ $($n:ident),+ }] ) => {{ - #[allow(non_camel_case_types)] - mod selection { - #[derive(serde::Serialize, specta::Type)] - #[specta(inline)] - pub struct Selection<$($n,)*> { - $(pub $n: $n,)* - } - } - use selection::Selection; - #[allow(non_camel_case_types)] - $s.into_iter().map(|v| Selection { $($n: v.$n,)* }).collect::>() - }}; -} - -#[cfg(test)] -mod tests { - use specta::Type; - use specta_typescript::inline; - - fn ts_export_ref(_t: &T) -> String { - inline::(&Default::default()).unwrap() - } - - #[derive(Clone)] - #[allow(dead_code)] - struct User { - pub id: i32, - pub name: String, - pub email: String, - pub age: i32, - pub password: String, - } - - #[test] - fn test_selection_macros() { - let user = User { - id: 1, - name: "Monty Beaumont".into(), - email: "monty@otbeaumont.me".into(), - age: 7, - password: "password123".into(), - }; - - let s1 = selection!(user.clone(), { name, age }); - assert_eq!(s1.name, "Monty Beaumont".to_string()); - assert_eq!(s1.age, 7); - assert_eq!(ts_export_ref(&s1), "{ name: string; age: number }"); - - let users = vec![user; 3]; - let s2 = selection!(users, [{ name, age }]); - assert_eq!(s2[0].name, "Monty Beaumont".to_string()); - assert_eq!(s2[0].age, 7); - assert_eq!(ts_export_ref(&s2), "{ name: string; age: number }[]"); - } -} diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index 70121cf7..fb7ae797 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -22,49 +22,24 @@ mod router; mod types; pub(crate) mod util; +#[cfg(feature = "legacy")] +#[cfg_attr(docsrs, doc(cfg(feature = "legacy")))] +pub mod legacy; + #[allow(unused)] pub use languages::*; pub use procedure_kind::ProcedureKind; pub use router::Router2; pub use types::Types; -// TODO: These will come in the future. -#[cfg(not(feature = "unstable"))] -pub(crate) use modern::State; -#[cfg(not(feature = "unstable"))] -pub(crate) use procedure::Procedure2; - -#[cfg(feature = "unstable")] pub use as_date::AsDate; -#[cfg(feature = "unstable")] pub use modern::{ middleware, procedure::ProcedureBuilder, procedure::ProcedureMeta, procedure::ResolverInput, procedure::ResolverOutput, Error as Error2, Extension, Stream, }; -#[cfg(feature = "unstable")] pub use procedure::Procedure2; pub use rspc_procedure::{ flush, DeserializeError, DowncastError, DynInput, DynOutput, Procedure, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures, ResolverError, State, }; - -// Legacy stuff -#[cfg(not(feature = "nolegacy"))] -mod legacy; - -#[cfg(not(feature = "nolegacy"))] -pub(crate) use legacy::interop; - -// These remain to respect semver but will all go with the next major. -#[allow(deprecated)] -#[cfg(not(feature = "nolegacy"))] -pub use legacy::{ - internal, test_result_type, test_result_value, typedef, Config, DoubleArgMarker, - DoubleArgStreamMarker, Error, ErrorCode, ExecError, ExecKind, ExportError, FutureMarker, - Middleware, MiddlewareBuilder, MiddlewareContext, MiddlewareLike, - MiddlewareWithResponseHandler, RequestLayer, Resolver, ResultMarker, Router, RouterBuilder, - SerializeMarker, StreamResolver, -}; -#[cfg(not(feature = "nolegacy"))] -pub use rspc_procedure::LegacyErrorInterop; diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/modern/middleware/middleware.rs index ed147b17..35db9807 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/modern/middleware/middleware.rs @@ -21,7 +21,7 @@ use std::{pin::Pin, sync::Arc}; -use futures::{Future, FutureExt, Stream}; +use futures_util::{Future, FutureExt, Stream}; use rspc_procedure::State; use crate::modern::procedure::ProcedureMeta; diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs index 7326683f..1fe32399 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/modern/procedure/builder.rs @@ -8,7 +8,7 @@ use crate::{ use super::{ErasedProcedure, ProcedureKind, ProcedureMeta}; -use futures::{FutureExt, StreamExt}; +use futures_util::{FutureExt, StreamExt}; use rspc_procedure::State; // TODO: Document the generics like `Middleware`. What order should they be in? diff --git a/rspc/src/modern/procedure/erased.rs b/rspc/src/modern/procedure/erased.rs index 1de03214..dd1e1a25 100644 --- a/rspc/src/modern/procedure/erased.rs +++ b/rspc/src/modern/procedure/erased.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, panic::Location, sync::Arc}; -use futures::{FutureExt, TryStreamExt}; +use futures_util::{FutureExt, TryStreamExt}; use rspc_procedure::Procedure; use specta::datatype::DataType; diff --git a/rspc/src/modern/procedure/resolver_output.rs b/rspc/src/modern/procedure/resolver_output.rs index dbe7c95f..5b3bc0ef 100644 --- a/rspc/src/modern/procedure/resolver_output.rs +++ b/rspc/src/modern/procedure/resolver_output.rs @@ -29,7 +29,7 @@ // // note = "ResolverOutput requires a `T where T: serde::Serialize + specta::Type + 'static` to be returned from your procedure" // // )] -use futures::{Stream, TryStreamExt}; +use futures_util::{Stream, TryStreamExt}; use rspc_procedure::{ProcedureError, ProcedureStream}; use serde::Serialize; use specta::{datatype::DataType, Generics, Type, TypeCollection}; @@ -67,7 +67,7 @@ where } fn into_stream(self) -> impl Stream> + Send + 'static { - futures::stream::once(async move { Ok(self) }) + futures_util::stream::once(async move { Ok(self) }) } fn into_procedure_stream( diff --git a/rspc/src/modern/stream.rs b/rspc/src/modern/stream.rs index a2791c79..b9de701a 100644 --- a/rspc/src/modern/stream.rs +++ b/rspc/src/modern/stream.rs @@ -3,7 +3,7 @@ use std::{ task::{Context, Poll}, }; -use futures::StreamExt; +use futures_util::StreamExt; /// Return a [`Stream`](futures::Stream) of values from a [`Procedure::query`](procedure::ProcedureBuilder::query) or [`Procedure::mutation`](procedure::ProcedureBuilder::mutation). /// @@ -25,24 +25,24 @@ use futures::StreamExt; /// ::builder().query(|_, _: ()| async move { rspc::Stream(once(async move { 42 })) }); /// ``` /// -pub struct Stream(pub S); +pub struct Stream(pub S); // WARNING: We can not add an implementation for `Debug` without breaking `rspc_tracing` -impl Default for Stream { +impl Default for Stream { fn default() -> Self { Self(Default::default()) } } -impl Clone for Stream { +impl Clone for Stream { fn clone(&self) -> Self { Self(self.0.clone()) } } // TODO: I hate this requiring `Unpin` but we couldn't use `pin-project-lite` with the tuple variant. -impl futures::Stream for Stream { +impl futures_util::Stream for Stream { type Item = S::Item; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 3f1f977c..dd6a4d3e 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, marker::PhantomData, panic::Location, sync::Arc}; -use futures::{FutureExt, TryStreamExt}; +use futures_util::{FutureExt, TryStreamExt}; use rspc_procedure::Procedure; use specta::datatype::DataType; @@ -36,7 +36,6 @@ pub struct Procedure2 { // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` impl Procedure2 { - #[cfg(feature = "unstable")] /// Construct a new procedure using [`ProcedureBuilder`]. #[track_caller] pub fn builder( diff --git a/rspc/src/router.rs b/rspc/src/router.rs index 01530729..49f3a769 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -17,8 +17,9 @@ use crate::{ /// TODO: Examples exporting types and with `rspc_axum` pub struct Router2 { setup: Vec>, - types: TypeCollection, - procedures: BTreeMap>, ErasedProcedure>, + // TODO: Seal these once `rspc-legacy` is gone. + pub(crate) types: TypeCollection, + pub(crate) procedures: BTreeMap>, ErasedProcedure>, errors: Vec, } @@ -38,7 +39,6 @@ impl Router2 { Self::default() } - #[cfg(feature = "unstable")] #[track_caller] pub fn procedure( mut self, @@ -63,7 +63,6 @@ impl Router2 { } // TODO: Document the order this is run in for `build` - #[cfg(feature = "unstable")] pub fn setup(mut self, func: impl FnOnce(&mut State) + 'static) -> Self { self.setup.push(Box::new(func)); self @@ -124,7 +123,6 @@ impl Router2 { self.build_with_state_inner(State::default()) } - // #[cfg(feature = "unstable")] // pub fn build_with_state( // self, // state: State, @@ -210,26 +208,6 @@ impl<'a, TCtx> IntoIterator for &'a Router2 { } } -#[cfg(not(feature = "nolegacy"))] -impl From> for Router2 { - fn from(router: crate::legacy::Router) -> Self { - crate::interop::legacy_to_modern(router) - } -} - -#[cfg(not(feature = "nolegacy"))] -impl Router2 { - pub(crate) fn interop_procedures( - &mut self, - ) -> &mut BTreeMap>, ErasedProcedure> { - &mut self.procedures - } - - pub(crate) fn interop_types(&mut self) -> &mut TypeCollection { - &mut self.types - } -} - fn get_flattened_name(name: &Vec>) -> Cow<'static, str> { if name.len() == 1 { // By cloning we are ensuring we passthrough to the `Cow` to avoid cloning if this is a `&'static str`. From 36dfe67134a6cbf1eabed8854b1effec42be9484 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 26 Dec 2024 13:33:09 +0800 Subject: [PATCH 63/67] cleanup naming of modern stuff --- crates/binario/Cargo.toml | 3 ++- crates/openapi/src/lib.rs | 4 ++-- examples/axum/Cargo.toml | 2 +- examples/core/src/lib.rs | 15 +++++++-------- examples/legacy-compat/src/main.rs | 2 +- examples/tauri/src-tauri/src/api.rs | 2 +- rspc/src/legacy.rs | 4 ++-- rspc/src/lib.rs | 13 ++++++++----- rspc/src/modern/procedure/builder.rs | 10 +++++----- rspc/src/procedure.rs | 12 ++++++------ rspc/src/router.rs | 12 ++++++------ rspc/tests/router.rs | 28 ++++++++++++++-------------- 12 files changed, 55 insertions(+), 52 deletions(-) diff --git a/crates/binario/Cargo.toml b/crates/binario/Cargo.toml index 7a3d0cb6..9d8bc767 100644 --- a/crates/binario/Cargo.toml +++ b/crates/binario/Cargo.toml @@ -3,10 +3,11 @@ name = "rspc-binario" description = "Binario support for rspc" version = "0.0.0" edition = "2021" -publish = false # TODO: Crate metadata & publish +publish = false # TODO: Crate metadata & publish [dependencies] rspc = { path = "../../rspc" } +# rspc-procedure = { path = "../procedure" } specta = { workspace = true } # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features diff --git a/crates/openapi/src/lib.rs b/crates/openapi/src/lib.rs index 9f9f7acb..fdab43fc 100644 --- a/crates/openapi/src/lib.rs +++ b/crates/openapi/src/lib.rs @@ -17,7 +17,7 @@ use axum::{ Json, }; use futures::StreamExt; -use rspc::{middleware::Middleware, Extension, Procedure2, ResolverInput, Router2}; +use rspc::{middleware::Middleware, Extension, Procedure, ResolverInput, Router}; use serde_json::json; // TODO: Properly handle inputs from query params @@ -90,7 +90,7 @@ struct OpenAPIState(HashMap<(&'static str, Cow<'static, str>), String>); // TODO: Axum should be behind feature flag // TODO: Can we decouple webserver from OpenAPI while keeping something maintainable???? pub fn mount( - router: Router2, + router: Router, // TODO: Make Axum extractors work ctx_fn: impl Fn(&Parts) -> TCtx + Clone + Send + Sync + 'static, ) -> axum::Router diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 241984cd..22fa69ff 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -16,7 +16,7 @@ tower-http = { version = "0.6.2", default-features = false, features = [ rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } -futures = "0.3" # TODO +futures = "0.3" # TODO serde_json = "1.0.134" rspc-http = { version = "0.2.1", path = "../../integrations/http" } streamunordered = "0.5.4" diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index 705b94ef..e7825624 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -2,8 +2,7 @@ use std::{marker::PhantomData, time::SystemTime}; use async_stream::stream; use rspc::{ - middleware::Middleware, Error2, Procedure2, ProcedureBuilder, ResolverInput, ResolverOutput, - Router2, + middleware::Middleware, Procedure, ProcedureBuilder, ResolverInput, ResolverOutput, Router, }; use rspc_binario::Binario; use rspc_cache::{cache, cache_ttl, CacheState, Memory}; @@ -58,7 +57,7 @@ pub enum Error { InternalError(#[from] anyhow::Error), } -impl Error2 for Error { +impl rspc::Error for Error { fn into_resolver_error(self) -> rspc::ResolverError { // rspc::ResolverError::new(self.to_string(), Some(self)) // TODO: Typesafe way to achieve this rspc::ResolverError::new( @@ -73,11 +72,11 @@ impl BaseProcedure { pub fn builder( ) -> ProcedureBuilder where - TErr: Error2, + TErr: rspc::Error, TInput: ResolverInput, TResult: ResolverOutput, { - Procedure2::builder() // You add default middleware here + Procedure::builder() // You add default middleware here } } @@ -93,10 +92,10 @@ impl Serialize for SerialisationError { } } -pub fn mount() -> Router2 { - Router2::new() +pub fn mount() -> Router { + Router::new() .procedure("withoutBaseProcedure", { - Procedure2::builder::().query(|ctx: Ctx, id: String| async move { Ok(()) }) + Procedure::builder::().query(|ctx: Ctx, id: String| async move { Ok(()) }) }) .procedure("newstuff", { ::builder().query(|_, _: ()| async { Ok(env!("CARGO_PKG_VERSION")) }) diff --git a/examples/legacy-compat/src/main.rs b/examples/legacy-compat/src/main.rs index 91b2cd50..500f86ee 100644 --- a/examples/legacy-compat/src/main.rs +++ b/examples/legacy-compat/src/main.rs @@ -46,7 +46,7 @@ fn mount() -> RouterBuilder { #[tokio::main] async fn main() { - let (procedures, types) = rspc::Router2::from(mount().build()).build().unwrap(); + let (procedures, types) = rspc::Router::from(mount().build()).build().unwrap(); rspc::Typescript::default() .export_to( diff --git a/examples/tauri/src-tauri/src/api.rs b/examples/tauri/src-tauri/src/api.rs index 71f76e2f..1b2735a5 100644 --- a/examples/tauri/src-tauri/src/api.rs +++ b/examples/tauri/src-tauri/src/api.rs @@ -23,7 +23,7 @@ impl Serialize for Infallible { impl std::error::Error for Infallible {} -impl rspc::Error2 for Infallible { +impl rspc::Error for Infallible { fn into_resolver_error(self) -> rspc::ResolverError { unreachable!() } diff --git a/rspc/src/legacy.rs b/rspc/src/legacy.rs index bafe73b3..a58d8f87 100644 --- a/rspc/src/legacy.rs +++ b/rspc/src/legacy.rs @@ -16,9 +16,9 @@ use crate::{ util::literal_object, ProcedureKind, }; -impl From> for crate::Router2 { +impl From> for crate::Router { fn from(router: rspc_legacy::Router) -> Self { - let mut r = crate::Router2::new(); + let mut r = crate::Router::new(); let (queries, mutations, subscriptions, mut type_map) = router.into_parts(); diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index fb7ae797..c1093d97 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -29,17 +29,20 @@ pub mod legacy; #[allow(unused)] pub use languages::*; pub use procedure_kind::ProcedureKind; -pub use router::Router2; +pub use router::Router; pub use types::Types; pub use as_date::AsDate; pub use modern::{ middleware, procedure::ProcedureBuilder, procedure::ProcedureMeta, procedure::ResolverInput, - procedure::ResolverOutput, Error as Error2, Extension, Stream, + procedure::ResolverOutput, Error, Extension, Stream, }; -pub use procedure::Procedure2; +pub use procedure::Procedure; +// We only re-export types that are useful for a general user. pub use rspc_procedure::{ - flush, DeserializeError, DowncastError, DynInput, DynOutput, Procedure, ProcedureError, - ProcedureStream, ProcedureStreamMap, Procedures, ResolverError, State, + flush, DynInput, ProcedureError, ProcedureStream, Procedures, ResolverError, State, }; + +// TODO: Potentially remove these once Axum stuff is sorted. +pub use rspc_procedure::{DynOutput, ProcedureStreamMap}; diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/modern/procedure/builder.rs index 1fe32399..340298a4 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/modern/procedure/builder.rs @@ -3,7 +3,7 @@ use std::{fmt, future::Future, marker::PhantomData, sync::Arc}; use crate::{ middleware::IntoMiddleware, modern::{middleware::MiddlewareHandler, Error}, - Procedure2, + Procedure, }; use super::{ErasedProcedure, ProcedureKind, ProcedureMeta}; @@ -69,8 +69,8 @@ where pub fn query> + Send + 'static>( self, handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> Procedure2 { - Procedure2 { + ) -> Procedure { + Procedure { build: Box::new(move |setups| { (self.build)( ProcedureKind::Query, @@ -85,8 +85,8 @@ where pub fn mutation> + Send + 'static>( self, handler: impl Fn(TCtx, TInput) -> F + Send + Sync + 'static, - ) -> Procedure2 { - Procedure2 { + ) -> Procedure { + Procedure { build: Box::new(move |setups| { (self.build)( ProcedureKind::Mutation, diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index dd6a4d3e..e90c909d 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, marker::PhantomData, panic::Location, sync::Arc}; use futures_util::{FutureExt, TryStreamExt}; -use rspc_procedure::Procedure; + use specta::datatype::DataType; use crate::{ @@ -27,7 +27,7 @@ pub(crate) struct ProcedureType { /// /// A [`Procedure`] is built from a [`ProcedureBuilder`] and holds the type information along with the logic to execute the operation. /// -pub struct Procedure2 { +pub struct Procedure { pub(crate) build: Box>) -> ErasedProcedure>, pub(crate) phantom: PhantomData<(TInput, TResult)>, @@ -35,7 +35,7 @@ pub struct Procedure2 { // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` -impl Procedure2 { +impl Procedure { /// Construct a new procedure using [`ProcedureBuilder`]. #[track_caller] pub fn builder( @@ -78,7 +78,7 @@ impl Procedure2 { let key: Cow<'static, str> = "todo".to_string().into(); // TODO: Work this out properly let meta = ProcedureMeta::new(key.clone(), kind, state); - Procedure::new(move |ctx, input| { + rspc_procedure::Procedure::new(move |ctx, input| { TResult::into_procedure_stream( handler( ctx, @@ -103,7 +103,7 @@ impl Procedure2 { where TCtx: 'static, { - Procedure2 { + Procedure { build: Box::new(move |mut setups| { if let Some(setup) = mw.setup { setups.push(setup); @@ -168,7 +168,7 @@ impl Procedure2 { // } } -impl Into> for Procedure2 { +impl Into> for Procedure { fn into(self) -> ErasedProcedure { (self.build)(Default::default()) } diff --git a/rspc/src/router.rs b/rspc/src/router.rs index 49f3a769..c91b54b9 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -11,11 +11,11 @@ use specta::TypeCollection; use rspc_procedure::Procedures; use crate::{ - modern::procedure::ErasedProcedure, types::TypesOrType, Procedure2, ProcedureKind, State, Types, + modern::procedure::ErasedProcedure, types::TypesOrType, Procedure, ProcedureKind, State, Types, }; /// TODO: Examples exporting types and with `rspc_axum` -pub struct Router2 { +pub struct Router { setup: Vec>, // TODO: Seal these once `rspc-legacy` is gone. pub(crate) types: TypeCollection, @@ -23,7 +23,7 @@ pub struct Router2 { errors: Vec, } -impl Default for Router2 { +impl Default for Router { fn default() -> Self { Self { setup: Default::default(), @@ -34,7 +34,7 @@ impl Default for Router2 { } } -impl Router2 { +impl Router { pub fn new() -> Self { Self::default() } @@ -177,7 +177,7 @@ impl Router2 { } } -impl fmt::Debug for Router2 { +impl fmt::Debug for Router { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let procedure_keys = |kind: ProcedureKind| { self.procedures @@ -198,7 +198,7 @@ impl fmt::Debug for Router2 { } } -impl<'a, TCtx> IntoIterator for &'a Router2 { +impl<'a, TCtx> IntoIterator for &'a Router { type Item = (&'a Vec>, &'a ErasedProcedure); type IntoIter = std::collections::btree_map::Iter<'a, Vec>, ErasedProcedure>; diff --git a/rspc/tests/router.rs b/rspc/tests/router.rs index cce08dc9..f75dc8a1 100644 --- a/rspc/tests/router.rs +++ b/rspc/tests/router.rs @@ -1,20 +1,20 @@ use std::fmt; -use rspc::{Procedure2, Router2}; +use rspc::{Procedure, Router}; use rspc_procedure::ResolverError; use serde::Serialize; use specta::Type; #[test] fn errors() { - let router = ::new() + let router = ::new() .procedure( "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), ) .procedure( "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), ); assert_eq!( @@ -22,31 +22,31 @@ fn errors() { "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:13:13 Duplicate: rspc/tests/router.rs:15:10\n]" ); - let router = ::new() + let router = ::new() .procedure( "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), ) - .merge(::new().procedure( + .merge(::new().procedure( "abc", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), )); assert_eq!(format!("{:?}", router.build().unwrap_err()), "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:32:13 Duplicate: rspc/tests/router.rs:28:13\n]"); - let router = ::new() + let router = ::new() .nest( "abc", - ::new().procedure( + ::new().procedure( "kjl", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), ), ) .nest( "abc", - ::new().procedure( + ::new().procedure( "def", - Procedure2::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), + Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), ), ); @@ -73,7 +73,7 @@ impl Serialize for Infallible { impl std::error::Error for Infallible {} -impl rspc::Error2 for Infallible { +impl rspc::Error for Infallible { fn into_resolver_error(self) -> ResolverError { unreachable!() } From 6081d0e30dc9cd8c10472f486deddc0a701cb9b7 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 26 Dec 2024 13:41:43 +0800 Subject: [PATCH 64/67] flatten `rspc::modern` into root --- rspc/src/{modern => }/error.rs | 0 rspc/src/{modern => }/extension.rs | 0 rspc/src/legacy.rs | 6 ++- rspc/src/lib.rs | 20 ++++++---- rspc/src/{modern => }/middleware.rs | 0 .../middleware/into_middleware.rs | 5 +-- .../src/{modern => }/middleware/middleware.rs | 2 +- rspc/src/{modern => }/middleware/next.rs | 7 +--- rspc/src/{modern => }/mod.rs | 0 rspc/src/modern/infallible.rs | 30 --------------- rspc/src/modern/procedure.rs | 27 ------------- rspc/src/procedure.rs | 38 ++++++++++++++----- rspc/src/{modern => }/procedure/builder.rs | 6 +-- rspc/src/{modern => }/procedure/erased.rs | 8 +--- rspc/src/{modern => }/procedure/meta.rs | 3 +- .../{modern => }/procedure/resolver_input.rs | 0 .../{modern => }/procedure/resolver_output.rs | 4 +- rspc/src/router.rs | 2 +- rspc/src/{modern => }/stream.rs | 0 19 files changed, 57 insertions(+), 101 deletions(-) rename rspc/src/{modern => }/error.rs (100%) rename rspc/src/{modern => }/extension.rs (100%) rename rspc/src/{modern => }/middleware.rs (100%) rename rspc/src/{modern => }/middleware/into_middleware.rs (97%) rename rspc/src/{modern => }/middleware/middleware.rs (99%) rename rspc/src/{modern => }/middleware/next.rs (84%) rename rspc/src/{modern => }/mod.rs (100%) delete mode 100644 rspc/src/modern/infallible.rs delete mode 100644 rspc/src/modern/procedure.rs rename rspc/src/{modern => }/procedure/builder.rs (96%) rename rspc/src/{modern => }/procedure/erased.rs (93%) rename rspc/src/{modern => }/procedure/meta.rs (96%) rename rspc/src/{modern => }/procedure/resolver_input.rs (100%) rename rspc/src/{modern => }/procedure/resolver_output.rs (97%) rename rspc/src/{modern => }/stream.rs (100%) diff --git a/rspc/src/modern/error.rs b/rspc/src/error.rs similarity index 100% rename from rspc/src/modern/error.rs rename to rspc/src/error.rs diff --git a/rspc/src/modern/extension.rs b/rspc/src/extension.rs similarity index 100% rename from rspc/src/modern/extension.rs rename to rspc/src/extension.rs diff --git a/rspc/src/legacy.rs b/rspc/src/legacy.rs index a58d8f87..9e64935e 100644 --- a/rspc/src/legacy.rs +++ b/rspc/src/legacy.rs @@ -12,8 +12,10 @@ use specta::{ }; use crate::{ - modern::procedure::ErasedProcedure, procedure::ProcedureType, types::TypesOrType, - util::literal_object, ProcedureKind, + procedure::{ErasedProcedure, ProcedureType}, + types::TypesOrType, + util::literal_object, + ProcedureKind, }; impl From> for crate::Router { diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index c1093d97..e5938d40 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -13,12 +13,16 @@ html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" )] +pub mod middleware; + mod as_date; +mod error; +mod extension; mod languages; -pub(crate) mod modern; mod procedure; mod procedure_kind; mod router; +mod stream; mod types; pub(crate) mod util; @@ -26,19 +30,19 @@ pub(crate) mod util; #[cfg_attr(docsrs, doc(cfg(feature = "legacy")))] pub mod legacy; +pub use as_date::AsDate; +pub use error::Error; +pub use extension::Extension; #[allow(unused)] pub use languages::*; +pub use procedure::{ + ErasedProcedure, Procedure, ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput, +}; pub use procedure_kind::ProcedureKind; pub use router::Router; +pub use stream::Stream; pub use types::Types; -pub use as_date::AsDate; -pub use modern::{ - middleware, procedure::ProcedureBuilder, procedure::ProcedureMeta, procedure::ResolverInput, - procedure::ResolverOutput, Error, Extension, Stream, -}; -pub use procedure::Procedure; - // We only re-export types that are useful for a general user. pub use rspc_procedure::{ flush, DynInput, ProcedureError, ProcedureStream, Procedures, ResolverError, State, diff --git a/rspc/src/modern/middleware.rs b/rspc/src/middleware.rs similarity index 100% rename from rspc/src/modern/middleware.rs rename to rspc/src/middleware.rs diff --git a/rspc/src/modern/middleware/into_middleware.rs b/rspc/src/middleware/into_middleware.rs similarity index 97% rename from rspc/src/modern/middleware/into_middleware.rs rename to rspc/src/middleware/into_middleware.rs index 04c4eec4..8c069942 100644 --- a/rspc/src/modern/middleware/into_middleware.rs +++ b/rspc/src/middleware/into_middleware.rs @@ -1,9 +1,6 @@ use std::marker::PhantomData; -use crate::{ - modern::{Error, Extension}, - ProcedureBuilder, -}; +use crate::{Error, Extension, ProcedureBuilder}; use super::Middleware; diff --git a/rspc/src/modern/middleware/middleware.rs b/rspc/src/middleware/middleware.rs similarity index 99% rename from rspc/src/modern/middleware/middleware.rs rename to rspc/src/middleware/middleware.rs index 35db9807..c8ddb8a8 100644 --- a/rspc/src/modern/middleware/middleware.rs +++ b/rspc/src/middleware/middleware.rs @@ -24,7 +24,7 @@ use std::{pin::Pin, sync::Arc}; use futures_util::{Future, FutureExt, Stream}; use rspc_procedure::State; -use crate::modern::procedure::ProcedureMeta; +use crate::ProcedureMeta; use super::Next; diff --git a/rspc/src/modern/middleware/next.rs b/rspc/src/middleware/next.rs similarity index 84% rename from rspc/src/modern/middleware/next.rs rename to rspc/src/middleware/next.rs index 3d529fc9..47f7f126 100644 --- a/rspc/src/modern/middleware/next.rs +++ b/rspc/src/middleware/next.rs @@ -1,9 +1,6 @@ -use std::{fmt, sync::Arc}; +use std::fmt; -use crate::{ - modern::{middleware::middleware::MiddlewareHandler, procedure::ProcedureMeta}, - State, -}; +use crate::{middleware::MiddlewareHandler, procedure::ProcedureMeta}; pub struct Next { // TODO: `pub(super)` over `pub(crate)` diff --git a/rspc/src/modern/mod.rs b/rspc/src/mod.rs similarity index 100% rename from rspc/src/modern/mod.rs rename to rspc/src/mod.rs diff --git a/rspc/src/modern/infallible.rs b/rspc/src/modern/infallible.rs deleted file mode 100644 index 1391e5e7..00000000 --- a/rspc/src/modern/infallible.rs +++ /dev/null @@ -1,30 +0,0 @@ -// use std::fmt; - -// use serde::Serialize; -// use specta::Type; - -// #[derive(Type, Debug)] -// pub enum Infallible {} - -// impl fmt::Display for Infallible { -// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -// write!(f, "{self:?}") -// } -// } - -// impl Serialize for Infallible { -// fn serialize(&self, _: S) -> Result -// where -// S: serde::Serializer, -// { -// unreachable!() -// } -// } - -// impl std::error::Error for Infallible {} - -// impl crate::modern::Error for Infallible { -// fn into_resolver_error(self) -> rspc_procedure::ResolverError { -// unreachable!() -// } -// } diff --git a/rspc/src/modern/procedure.rs b/rspc/src/modern/procedure.rs deleted file mode 100644 index 1dce4008..00000000 --- a/rspc/src/modern/procedure.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! A procedure holds a single operation that can be executed by the server. -//! -//! A procedure is built up from: -//! - any number of middleware -//! - a single resolver function (of type `query`, `mutation` or `subscription`) -//! -//! Features: -//! - Input types (Serde-compatible or custom) -//! - Result types (Serde-compatible or custom) -//! - [`Future`](#todo) or [`Stream`](#todo) results -//! - Typesafe error handling -//! -//! TODO: Request flow overview -//! TODO: Explain, what a procedure is, return type/struct, middleware, execution order, etc -//! - -mod builder; -mod erased; -mod meta; -mod resolver_input; -mod resolver_output; - -pub use builder::ProcedureBuilder; -pub use erased::ErasedProcedure; -pub use meta::{ProcedureKind, ProcedureMeta}; -pub use resolver_input::ResolverInput; -pub use resolver_output::ResolverOutput; diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index e90c909d..69f86907 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -1,18 +1,38 @@ +//! A procedure holds a single operation that can be executed by the server. +//! +//! A procedure is built up from: +//! - any number of middleware +//! - a single resolver function (of type `query`, `mutation` or `subscription`) +//! +//! Features: +//! - Input types (Serde-compatible or custom) +//! - Result types (Serde-compatible or custom) +//! - [`Future`](#todo) or [`Stream`](#todo) results +//! - Typesafe error handling +//! +//! TODO: Request flow overview +//! TODO: Explain, what a procedure is, return type/struct, middleware, execution order, etc +//! + +mod builder; +mod erased; +mod meta; +mod resolver_input; +mod resolver_output; + +pub use builder::ProcedureBuilder; +pub use erased::ErasedProcedure; +pub use meta::ProcedureMeta; +pub use resolver_input::ResolverInput; +pub use resolver_output::ResolverOutput; + use std::{borrow::Cow, marker::PhantomData, panic::Location, sync::Arc}; use futures_util::{FutureExt, TryStreamExt}; use specta::datatype::DataType; -use crate::{ - modern::{ - procedure::{ - ErasedProcedure, ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput, - }, - Error, - }, - Extension, ProcedureKind, State, -}; +use crate::{Error, Extension, ProcedureKind, State}; #[derive(Clone)] pub(crate) struct ProcedureType { diff --git a/rspc/src/modern/procedure/builder.rs b/rspc/src/procedure/builder.rs similarity index 96% rename from rspc/src/modern/procedure/builder.rs rename to rspc/src/procedure/builder.rs index 340298a4..f33998f9 100644 --- a/rspc/src/modern/procedure/builder.rs +++ b/rspc/src/procedure/builder.rs @@ -1,14 +1,12 @@ use std::{fmt, future::Future, marker::PhantomData, sync::Arc}; use crate::{ - middleware::IntoMiddleware, - modern::{middleware::MiddlewareHandler, Error}, - Procedure, + middleware::{IntoMiddleware, MiddlewareHandler}, + Error, Procedure, }; use super::{ErasedProcedure, ProcedureKind, ProcedureMeta}; -use futures_util::{FutureExt, StreamExt}; use rspc_procedure::State; // TODO: Document the generics like `Middleware`. What order should they be in? diff --git a/rspc/src/modern/procedure/erased.rs b/rspc/src/procedure/erased.rs similarity index 93% rename from rspc/src/modern/procedure/erased.rs rename to rspc/src/procedure/erased.rs index dd1e1a25..2a0e6b1a 100644 --- a/rspc/src/modern/procedure/erased.rs +++ b/rspc/src/procedure/erased.rs @@ -5,12 +5,8 @@ use rspc_procedure::Procedure; use specta::datatype::DataType; use crate::{ - modern::{ - procedure::{ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput}, - Error, - }, - procedure::ProcedureType, - State, + procedure::{ProcedureBuilder, ProcedureMeta, ProcedureType, ResolverInput, ResolverOutput}, + Error, State, }; pub struct ErasedProcedure { diff --git a/rspc/src/modern/procedure/meta.rs b/rspc/src/procedure/meta.rs similarity index 96% rename from rspc/src/modern/procedure/meta.rs rename to rspc/src/procedure/meta.rs index 303da6ef..feab8d15 100644 --- a/rspc/src/modern/procedure/meta.rs +++ b/rspc/src/procedure/meta.rs @@ -18,8 +18,7 @@ use std::{borrow::Cow, sync::Arc}; // } // } -pub use crate::ProcedureKind; -use crate::State; +use crate::{ProcedureKind, State}; #[derive(Debug, Clone)] enum ProcedureName { diff --git a/rspc/src/modern/procedure/resolver_input.rs b/rspc/src/procedure/resolver_input.rs similarity index 100% rename from rspc/src/modern/procedure/resolver_input.rs rename to rspc/src/procedure/resolver_input.rs diff --git a/rspc/src/modern/procedure/resolver_output.rs b/rspc/src/procedure/resolver_output.rs similarity index 97% rename from rspc/src/modern/procedure/resolver_output.rs rename to rspc/src/procedure/resolver_output.rs index 5b3bc0ef..48761aae 100644 --- a/rspc/src/modern/procedure/resolver_output.rs +++ b/rspc/src/procedure/resolver_output.rs @@ -34,7 +34,7 @@ use rspc_procedure::{ProcedureError, ProcedureStream}; use serde::Serialize; use specta::{datatype::DataType, Generics, Type, TypeCollection}; -use crate::modern::Error; +use crate::Error; // TODO: Maybe in `rspc_procedure`?? @@ -77,7 +77,7 @@ where } } -impl ResolverOutput for crate::modern::Stream +impl ResolverOutput for crate::Stream where TErr: Error, S: Stream> + Send + 'static, diff --git a/rspc/src/router.rs b/rspc/src/router.rs index c91b54b9..a6fbb786 100644 --- a/rspc/src/router.rs +++ b/rspc/src/router.rs @@ -11,7 +11,7 @@ use specta::TypeCollection; use rspc_procedure::Procedures; use crate::{ - modern::procedure::ErasedProcedure, types::TypesOrType, Procedure, ProcedureKind, State, Types, + procedure::ErasedProcedure, types::TypesOrType, Procedure, ProcedureKind, State, Types, }; /// TODO: Examples exporting types and with `rspc_axum` diff --git a/rspc/src/modern/stream.rs b/rspc/src/stream.rs similarity index 100% rename from rspc/src/modern/stream.rs rename to rspc/src/stream.rs From 322c8dccce3fa2679af041b5a351764c7adf8791 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 26 Dec 2024 16:09:14 +0800 Subject: [PATCH 65/67] `rspc-binario` prototype --- crates/binario/Cargo.toml | 4 +- crates/binario/client.ts | 17 +++++ crates/binario/src/lib.rs | 90 +++++++++++++++++++++++- crates/procedure/src/dyn_output.rs | 7 +- crates/procedure/src/stream.rs | 12 ++-- crates/zer/src/lib.rs | 18 ++++- examples/axum/Cargo.toml | 6 ++ examples/axum/src/main.rs | 98 +++++++++++++++++++++------ examples/bindings.ts | 21 +++--- examples/core/Cargo.toml | 7 +- examples/core/src/lib.rs | 27 ++++++-- rspc/src/procedure/resolver_output.rs | 3 +- 12 files changed, 254 insertions(+), 56 deletions(-) create mode 100644 crates/binario/client.ts diff --git a/crates/binario/Cargo.toml b/crates/binario/Cargo.toml index 9d8bc767..c63ee729 100644 --- a/crates/binario/Cargo.toml +++ b/crates/binario/Cargo.toml @@ -6,9 +6,11 @@ edition = "2021" publish = false # TODO: Crate metadata & publish [dependencies] +binario = "0.0.2" +futures.workspace = true # TODO: Drop this or drop down to `futures-util` rspc = { path = "../../rspc" } -# rspc-procedure = { path = "../procedure" } specta = { workspace = true } +tokio = "1.42.0" # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features [package.metadata."docs.rs"] diff --git a/crates/binario/client.ts b/crates/binario/client.ts new file mode 100644 index 00000000..61663dad --- /dev/null +++ b/crates/binario/client.ts @@ -0,0 +1,17 @@ +// TODO: This is not stable, just a demonstration of how it could work. + +(async () => { + const resp = await fetch( + "http://localhost:4000/rspc/binario?procedure=binario", + { + method: "POST", + headers: { + "Content-Type": "text/x-binario", + }, + // { name: "Oscar" } + body: new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114]), + }, + ); + + console.log(resp.status, await resp.clone().arrayBuffer()); +})(); diff --git a/crates/binario/src/lib.rs b/crates/binario/src/lib.rs index 6f5708dc..2def0c07 100644 --- a/crates/binario/src/lib.rs +++ b/crates/binario/src/lib.rs @@ -2,6 +2,92 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_logo_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png", - html_favicon_url = "https://github.com/oscartbeaumont/rspc/raw/main/docs/public/logo.png" + html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", + html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] + +use std::pin::Pin; + +use binario::{encode, Decode, Encode}; +use futures::{executor::block_on, Stream}; +use rspc::{ + middleware::Middleware, DynInput, ProcedureError, ProcedureStream, ResolverInput, + ResolverOutput, +}; +use specta::{datatype::DataType, Generics, Type, TypeCollection}; +use tokio::io::AsyncRead; + +enum Repr { + Bytes(Vec), + Stream(Pin>), +} + +pub struct BinarioInput(Repr); + +impl BinarioInput { + pub fn from_bytes(bytes: Vec) -> Self { + Self(Repr::Bytes(bytes)) + } + + pub fn from_stream(stream: impl AsyncRead + Send + 'static) -> Self { + Self(Repr::Stream(Box::pin(stream))) + } +} + +pub struct Binario(pub T); + +impl ResolverInput for Binario { + fn data_type(types: &mut TypeCollection) -> DataType { + T::inline(types, Generics::Definition) + } + + fn from_input(input: DynInput) -> Result { + let stream: BinarioInput = input.value()?; + + // TODO: `block_on` bad + match stream.0 { + Repr::Bytes(bytes) => block_on(binario::decode::(bytes.as_slice())), + Repr::Stream(stream) => block_on(binario::decode::(stream)), + } + .map_err(|err| panic!("{err:?}")) // TODO: Error handling + .map(Self) + } +} + +// TODO: Streaming instead of this +pub struct BinarioOutput(pub Vec); + +impl ResolverOutput for Binario { + type T = BinarioOutput; + + fn data_type(types: &mut TypeCollection) -> DataType { + T::inline(types, Generics::Definition) + } + + fn into_stream(self) -> impl Stream> + Send + 'static { + futures::stream::once(async move { + let mut buf = Vec::new(); + encode(&self.0, &mut buf).await.unwrap(); // TODO: Error handling + Ok(BinarioOutput(buf)) + }) + } + + fn into_procedure_stream( + stream: impl Stream> + Send + 'static, + ) -> ProcedureStream { + ProcedureStream::from_stream_value(stream) + } +} + +pub fn binario( +) -> Middleware, Binario, TCtx, TInput, TResult> +where + TError: Send + 'static, + TCtx: Send + 'static, + TInput: Decode + Send + 'static, + TResult: Encode + Send + Sync + 'static, +{ + Middleware::new(move |ctx: TCtx, input: Binario, next| async move { + next.exec(ctx, input.0).await.map(Binario) + }) +} diff --git a/crates/procedure/src/dyn_output.rs b/crates/procedure/src/dyn_output.rs index efa1bcf7..fe036de1 100644 --- a/crates/procedure/src/dyn_output.rs +++ b/crates/procedure/src/dyn_output.rs @@ -21,7 +21,8 @@ enum Repr<'a> { // TODO: `Debug`, etc traits impl<'a> DynOutput<'a> { - pub fn new_value(value: &'a mut Option) -> Self { + // TODO: We depend on the type of `T` can we either force it so this can be public? + pub(crate) fn new_value(value: &'a mut T) -> Self { Self { inner: Repr::Value(value), type_name: type_name::(), @@ -48,10 +49,10 @@ impl<'a> DynOutput<'a> { match self.inner { Repr::Serialize(_) => None, Repr::Value(v) => v - .downcast_mut::>>()? + .downcast_mut::>>()? .take() .expect("unreachable") - .expect("unreachable"), + .ok(), } } } diff --git a/crates/procedure/src/stream.rs b/crates/procedure/src/stream.rs index 04dc6fab..75ce842f 100644 --- a/crates/procedure/src/stream.rs +++ b/crates/procedure/src/stream.rs @@ -469,7 +469,7 @@ impl ProcedureStream { /// TODO // TODO: Should error be `String` type? - pub fn map) -> Result + Unpin, T>( + pub fn map) -> Result, T>( self, map: F, ) -> ProcedureStreamMap { @@ -477,17 +477,12 @@ impl ProcedureStream { } } -pub struct ProcedureStreamMap< - F: FnMut(Result) -> Result + Unpin, - T, -> { +pub struct ProcedureStreamMap) -> Result, T> { stream: ProcedureStream, map: F, } -impl) -> Result + Unpin, T> - ProcedureStreamMap -{ +impl) -> Result, T> ProcedureStreamMap { /// Start streaming data. /// Refer to `Self::require_manual_stream` for more information. pub fn stream(&mut self) { @@ -509,6 +504,7 @@ impl) -> Result + Unpin, T } } +// TODO: Drop `Unpin` requirement impl) -> Result + Unpin, T> Stream for ProcedureStreamMap { diff --git a/crates/zer/src/lib.rs b/crates/zer/src/lib.rs index ad211f48..c9202837 100644 --- a/crates/zer/src/lib.rs +++ b/crates/zer/src/lib.rs @@ -16,6 +16,7 @@ use std::{ use cookie::Cookie; use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use rspc::{ProcedureError, ResolverError}; use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize}; use specta::Type; @@ -68,7 +69,7 @@ impl Zer { cookie_name: impl Into>, secret: &[u8], cookie: Option>, - ) -> (Self, ZerResponse) { + ) -> Result<(Self, ZerResponse), ProcedureError> { let mut cookies = vec![]; if let Some(cookie) = cookie { // TODO: Error handling @@ -79,7 +80,13 @@ impl Zer { let resp_cookies = ResponseCookie::default(); - ( + // TODO: Being the `ResolverError` makes this not typesafe. We probally need a separate variant. + // return Err(ProcedureError::Resolver(ResolverError::new( + // "zer says bruh", + // None::, + // ))); + + Ok(( Self { cookie_name: cookie_name.into(), key: EncodingKey::from_secret(secret), @@ -91,7 +98,12 @@ impl Zer { ZerResponse { cookies: resp_cookies, }, - ) + )) + } + + // TODO: Allow one which errors when accessing any of the methods? + pub fn noop() -> Self { + todo!(); } pub fn session(&self) -> Result { diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index 22fa69ff..d004bffd 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -21,3 +21,9 @@ serde_json = "1.0.134" rspc-http = { version = "0.2.1", path = "../../integrations/http" } streamunordered = "0.5.4" rspc-zer = { version = "0.0.0", path = "../../crates/zer" } +serde.workspace = true +binario = "0.0.2" +rspc-binario = { version = "0.0.0", path = "../../crates/binario" } +form_urlencoded = "1.2.1" +axum-extra = { version = "0.9.6", features = ["async-read-body"] } +tokio-util = { version = "0.7.13", features = ["compat"] } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index c9fd2bb3..5b64dc6b 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -5,9 +5,11 @@ use axum::{ routing::{get, on, post, MethodFilter, MethodRouter}, Json, }; +use axum_extra::body::AsyncReadBody; use example_core::{mount, Ctx}; -use futures::{stream::FuturesUnordered, Stream, StreamExt}; +use futures::{stream::FuturesUnordered, Stream, StreamExt, TryStreamExt}; use rspc::{DynOutput, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures, State}; +use rspc_binario::BinarioOutput; use rspc_invalidation::Invalidator; use serde_json::{de::SliceRead, value::RawValue, Value}; use std::{ @@ -18,6 +20,7 @@ use std::{ task::{Context, Poll}, }; use streamunordered::{StreamUnordered, StreamYield}; +use tokio_util::compat::FuturesAsyncReadCompatExt; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] @@ -112,8 +115,64 @@ async fn main() { pub fn rspc_handler(procedures: Procedures) -> axum::Router { let mut r = axum::Router::new(); + + r = r.route( + "/binario", + // This endpoint lacks batching, SFM's, etc but that's fine. + post({ + let procedures = procedures.clone(); + + move |parts: Parts, body: Body| async move { + let invalidator = rspc_invalidation::Invalidator::default(); + let (zer, _zer_response) = rspc_zer::Zer::from_request( + "session", + "some_secret".as_ref(), + parts.headers.get("cookie"), + ) + .unwrap(); // TODO: Error handling + let ctx = Ctx { + invalidator: invalidator.clone(), + zer, + }; + + // if parts.headers.get("Content-Type") != Some(&"text/x-binario".parse().unwrap()) { + // // TODO: Error handling + // } + + let mut params = form_urlencoded::parse(parts.uri.query().unwrap_or("").as_bytes()); + let procedure_name = params + .find(|(key, _)| key == "procedure") + .map(|(_, value)| value) + .unwrap(); // TODO: Error handling + + let procedure = procedures.get(&procedure_name).unwrap(); // TODO: Error handling + + let stream = procedure.exec_with_value( + ctx.clone(), + rspc_binario::BinarioInput::from_stream( + body.into_data_stream() + .map_err(|err| todo!()) // TODO: Error handling + .into_async_read() + .compat(), + ), + ); + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, "text/x-binario".parse().unwrap()); + + ( + headers, + Body::from_stream(stream.map(|v| match v { + Ok(v) => Ok(Ok::<_, Infallible>( + v.as_value::().unwrap().0, + )), + Err(err) => todo!("{err:?}"), + })), + ) + } + }), + ); + // TODO: Support file upload and download - // TODO: `rspc_zer` how worky? // for (key, procedure) in procedures.clone() { // r = r.route( @@ -202,7 +261,8 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { "session", "some_secret".as_ref(), parts.headers.get("cookie"), - ); + ) + .unwrap(); // TODO: Error handling let ctx = Ctx { invalidator: invalidator.clone(), zer, @@ -229,27 +289,25 @@ pub fn rspc_handler(procedures: Procedures) -> axum::Router { while let Some(field) = multipart.next_field().await.unwrap() { let name = field.name().unwrap().to_string(); // TODO: Error handling - // field.headers() - - // TODO: Don't use `serde_json::Value` - let input: Value = match field.content_type() { - // TODO: - // Some("application/json") => { - // // TODO: Error handling - // serde_json::from_slice(field.bytes().await.unwrap().as_ref()).unwrap() - // } - // Some(_) => todo!(), - // None => todo!(), - _ => serde_json::from_slice(field.bytes().await.unwrap().as_ref()).unwrap(), - }; - let procedure = procedures.get(&*name).unwrap(); - // println!("{:?} {:?} {:?}", name, input, procedure); + // TODO: Error handling spawn( &mut runtime, - procedure.exec_with_deserializer(ctx.clone(), input), - ); + match field.content_type() { + // Some("text/x-binario") => procedure.exec_with_value( + // ctx.clone(), + // // TODO: Stream decoding is pretty rough with multipart so we omit it for now. + // rspc_binario::BinarioStream(field.bytes().await.unwrap().to_vec()), + // ), + _ => procedure.exec_with_deserializer( + ctx.clone(), + // TODO: Don't use `serde_json::Value` + serde_json::from_slice::(field.bytes().await.unwrap().as_ref()) + .unwrap(), + ), + }, + ) } // TODO: Move onto `Prototype`??? diff --git a/examples/bindings.ts b/examples/bindings.ts index d4d1c31c..cd92a15f 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -1,12 +1,17 @@ +// My custom header // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. -export type ProceduresLegacy = { queries: { key: "echo"; input: string; result: string } | { key: "error"; input: null; result: string } | { key: "transformMe"; input: null; result: string } | { key: "version"; input: null; result: string }; mutations: { key: "sendMsg"; input: string; result: string }; subscriptions: { key: "pings"; input: null; result: string } } - export type Procedures = { - echo: { kind: "query", input: string, output: string, error: unknown }, - error: { kind: "query", input: null, output: string, error: unknown }, - pings: { kind: "subscription", input: null, output: string, error: unknown }, - sendMsg: { kind: "mutation", input: string, output: string, error: unknown }, - transformMe: { kind: "query", input: null, output: string, error: unknown }, - version: { kind: "query", input: null, output: string, error: unknown }, + binario: { kind: "query", input: any, output: any, error: any }, + cached: { kind: "query", input: any, output: any, error: any }, + login: { kind: "query", input: any, output: any, error: any }, + me: { kind: "query", input: any, output: any, error: any }, + newstuff: { kind: "query", input: any, output: any, error: any }, + newstuff2: { kind: "query", input: any, output: any, error: any }, + newstuffpanic: { kind: "query", input: any, output: any, error: any }, + newstuffser: { kind: "query", input: any, output: any, error: any }, + sfmPost: { kind: "query", input: any, output: any, error: any }, + sfmPostEdit: { kind: "query", input: any, output: any, error: any }, + validator: { kind: "query", input: any, output: any, error: any }, + withoutBaseProcedure: { kind: "query", input: any, output: any, error: any }, } \ No newline at end of file diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index d447595e..0f60274b 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -8,9 +8,7 @@ publish = false rspc = { path = "../../rspc", features = ["typescript", "rust"] } async-stream = "0.3.6" serde = { version = "1.0.216", features = ["derive"] } -specta = { version = "=2.0.0-rc.20", features = [ - "derive", -] } +specta = { version = "=2.0.0-rc.20", features = ["derive"] } thiserror = "2.0.9" rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } tracing = "0.1.41" @@ -24,4 +22,5 @@ rspc-openapi = { version = "0.0.0", path = "../../crates/openapi" } validator = { version = "0.19.0", features = ["derive"] } rspc-zer = { version = "0.0.0", path = "../../crates/zer" } anyhow = "1.0.95" -binario = "0.0.1" +binario = "0.0.2" +pin-project = "1.1.7" diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index e7825624..10d22f6f 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -1,6 +1,8 @@ use std::{marker::PhantomData, time::SystemTime}; use async_stream::stream; +use binario::encode; +use futures::executor::block_on; use rspc::{ middleware::Middleware, Procedure, ProcedureBuilder, ResolverInput, ResolverOutput, Router, }; @@ -199,11 +201,26 @@ pub fn mount() -> Router { .procedure("me", { ::builder().query(|ctx, _: ()| async move { Ok(ctx.zer.session()?) }) }) - // .procedure("binario", { - // #[derive(binario::Encode)] - // pub struct Input {} - // ::builder().query(|ctx, _: Binario| async move { Ok(()) }) - // }) + .procedure("binario", { + #[derive(Debug, binario::Encode, binario::Decode, Type)] + pub struct Input { + name: String, + } + + // let mut buf = vec![]; + // block_on(encode( + // &Input { + // name: "Oscar".to_string(), + // }, + // &mut buf, + // )) + // .unwrap(); + // println!("{:?}", buf); + + ::builder() + .with(rspc_binario::binario()) + .query(|_, input: Input| async move { Ok(input) }) + }) // .procedure("fileupload", { // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) diff --git a/rspc/src/procedure/resolver_output.rs b/rspc/src/procedure/resolver_output.rs index 48761aae..342ae7aa 100644 --- a/rspc/src/procedure/resolver_output.rs +++ b/rspc/src/procedure/resolver_output.rs @@ -40,8 +40,7 @@ use crate::Error; /// TODO: bring back any correct parts of the docs above pub trait ResolverOutput: Sized + Send + 'static { - // TODO: This won't allow us to return upcast/downcastable stuff - type T; // : Serialize + Send + Sync + 'static; + type T; // TODO: Be an associated type instead so we can constrain later for better errors???? fn data_type(types: &mut TypeCollection) -> DataType; From b782394516f7c50c409e06a97e3eac840e7c7c1b Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Thu, 26 Dec 2024 20:15:45 +0800 Subject: [PATCH 66/67] `example-binario` + remove `block_on` decoding input --- Cargo.toml | 1 + crates/binario/Cargo.toml | 2 +- crates/binario/client.ts | 17 ---- crates/binario/src/lib.rs | 68 +++++++++----- crates/procedure/src/error.rs | 8 -- examples/axum/Cargo.toml | 17 ++-- examples/axum/src/main.rs | 118 +++--------------------- examples/binario/Cargo.toml | 21 +++++ examples/binario/client.ts | 41 +++++++++ examples/binario/src/main.rs | 127 ++++++++++++++++++++++++++ examples/bindings.ts | 1 - examples/core/Cargo.toml | 16 ++-- examples/core/src/lib.rs | 28 +----- examples/tauri/src-tauri/src/api.rs | 2 +- rspc/src/error.rs | 11 +-- rspc/src/procedure.rs | 2 +- rspc/src/procedure/resolver_output.rs | 2 +- rspc/tests/router.rs | 4 +- 18 files changed, 274 insertions(+), 212 deletions(-) delete mode 100644 crates/binario/client.ts create mode 100644 examples/binario/Cargo.toml create mode 100644 examples/binario/client.ts create mode 100644 examples/binario/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 18b4fa3b..28271340 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "./examples/tauri/src-tauri", "./examples/legacy", "./examples/legacy-compat", + "./examples/binario", ] [workspace.dependencies] diff --git a/crates/binario/Cargo.toml b/crates/binario/Cargo.toml index c63ee729..e283995d 100644 --- a/crates/binario/Cargo.toml +++ b/crates/binario/Cargo.toml @@ -7,7 +7,7 @@ publish = false # TODO: Crate metadata & publish [dependencies] binario = "0.0.2" -futures.workspace = true # TODO: Drop this or drop down to `futures-util` +futures-util.workspace = true rspc = { path = "../../rspc" } specta = { workspace = true } tokio = "1.42.0" diff --git a/crates/binario/client.ts b/crates/binario/client.ts deleted file mode 100644 index 61663dad..00000000 --- a/crates/binario/client.ts +++ /dev/null @@ -1,17 +0,0 @@ -// TODO: This is not stable, just a demonstration of how it could work. - -(async () => { - const resp = await fetch( - "http://localhost:4000/rspc/binario?procedure=binario", - { - method: "POST", - headers: { - "Content-Type": "text/x-binario", - }, - // { name: "Oscar" } - body: new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114]), - }, - ); - - console.log(resp.status, await resp.clone().arrayBuffer()); -})(); diff --git a/crates/binario/src/lib.rs b/crates/binario/src/lib.rs index 2def0c07..05248cc8 100644 --- a/crates/binario/src/lib.rs +++ b/crates/binario/src/lib.rs @@ -6,10 +6,10 @@ html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" )] -use std::pin::Pin; +use std::{error, fmt, marker::PhantomData, pin::Pin}; use binario::{encode, Decode, Encode}; -use futures::{executor::block_on, Stream}; +use futures_util::{stream, Stream}; use rspc::{ middleware::Middleware, DynInput, ProcedureError, ProcedureStream, ResolverInput, ResolverOutput, @@ -34,30 +34,25 @@ impl BinarioInput { } } -pub struct Binario(pub T); +pub struct TypedBinarioInput(pub BinarioInput, pub PhantomData); -impl ResolverInput for Binario { +impl ResolverInput for TypedBinarioInput { fn data_type(types: &mut TypeCollection) -> DataType { T::inline(types, Generics::Definition) } fn from_input(input: DynInput) -> Result { - let stream: BinarioInput = input.value()?; - - // TODO: `block_on` bad - match stream.0 { - Repr::Bytes(bytes) => block_on(binario::decode::(bytes.as_slice())), - Repr::Stream(stream) => block_on(binario::decode::(stream)), - } - .map_err(|err| panic!("{err:?}")) // TODO: Error handling - .map(Self) + Ok(Self(input.value()?, PhantomData)) } } // TODO: Streaming instead of this pub struct BinarioOutput(pub Vec); +pub struct TypedBinarioOutput(pub T); -impl ResolverOutput for Binario { +impl ResolverOutput + for TypedBinarioOutput +{ type T = BinarioOutput; fn data_type(types: &mut TypeCollection) -> DataType { @@ -65,7 +60,7 @@ impl ResolverOutput fo } fn into_stream(self) -> impl Stream> + Send + 'static { - futures::stream::once(async move { + stream::once(async move { let mut buf = Vec::new(); encode(&self.0, &mut buf).await.unwrap(); // TODO: Error handling Ok(BinarioOutput(buf)) @@ -79,15 +74,46 @@ impl ResolverOutput fo } } -pub fn binario( -) -> Middleware, Binario, TCtx, TInput, TResult> +pub fn binario() -> Middleware< + TError, + TCtx, + TypedBinarioInput, + TypedBinarioOutput, + TCtx, + TInput, + TResult, +> where - TError: Send + 'static, + TError: From + Send + 'static, TCtx: Send + 'static, TInput: Decode + Send + 'static, TResult: Encode + Send + Sync + 'static, { - Middleware::new(move |ctx: TCtx, input: Binario, next| async move { - next.exec(ctx, input.0).await.map(Binario) - }) + Middleware::new( + move |ctx: TCtx, input: TypedBinarioInput, next| async move { + let input = match input.0 .0 { + Repr::Bytes(bytes) => binario::decode::(bytes.as_slice()).await, + Repr::Stream(stream) => binario::decode::(stream).await, + } + .map_err(DeserializeError)?; + + next.exec(ctx, input).await.map(TypedBinarioOutput) + }, + ) +} + +pub struct DeserializeError(pub std::io::Error); + +impl fmt::Debug for DeserializeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } } + +impl fmt::Display for DeserializeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl error::Error for DeserializeError {} diff --git a/crates/procedure/src/error.rs b/crates/procedure/src/error.rs index 4f510c01..3ac94f51 100644 --- a/crates/procedure/src/error.rs +++ b/crates/procedure/src/error.rs @@ -5,8 +5,6 @@ use serde::{ Serialize, Serializer, }; -use crate::LegacyErrorInterop; - // TODO: Discuss the stability guanrantees of the error handling system. Variant is fixed, message is not. /// TODO @@ -104,12 +102,6 @@ impl Serialize for ProcedureError { S: Serializer, { if let ProcedureError::Resolver(err) = self { - // if let Some(err) = err.error() { - // if let Some(v) = err.downcast_ref::() { - // return v.0.serialize(serializer); - // } - // } - return err.value().serialize(serializer); } diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index d004bffd..116aab4f 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -7,23 +7,18 @@ publish = false [dependencies] rspc = { path = "../../rspc", features = ["typescript", "rust"] } rspc-axum = { path = "../../integrations/axum", features = ["ws"] } +rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } +rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } +rspc-http = { version = "0.2.1", path = "../../integrations/http" } +rspc-zer = { version = "0.0.0", path = "../../crates/zer" } example-core = { path = "../core" } + tokio = { version = "1.42.0", features = ["full"] } axum = { version = "0.7.9", features = ["multipart"] } tower-http = { version = "0.6.2", default-features = false, features = [ "cors", ] } -rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } -rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } - -futures = "0.3" # TODO +futures = "0.3" serde_json = "1.0.134" -rspc-http = { version = "0.2.1", path = "../../integrations/http" } streamunordered = "0.5.4" -rspc-zer = { version = "0.0.0", path = "../../crates/zer" } serde.workspace = true -binario = "0.0.2" -rspc-binario = { version = "0.0.0", path = "../../crates/binario" } -form_urlencoded = "1.2.1" -axum-extra = { version = "0.9.6", features = ["async-read-body"] } -tokio-util = { version = "0.7.13", features = ["compat"] } diff --git a/examples/axum/src/main.rs b/examples/axum/src/main.rs index 5b64dc6b..662a0c3f 100644 --- a/examples/axum/src/main.rs +++ b/examples/axum/src/main.rs @@ -1,26 +1,22 @@ use axum::{ body::Body, - extract::{Multipart, Request}, - http::{header, request::Parts, HeaderMap, HeaderName, StatusCode}, - routing::{get, on, post, MethodFilter, MethodRouter}, - Json, + extract::Multipart, + http::{header, request::Parts, HeaderMap}, + routing::{get, post}, }; -use axum_extra::body::AsyncReadBody; use example_core::{mount, Ctx}; -use futures::{stream::FuturesUnordered, Stream, StreamExt, TryStreamExt}; -use rspc::{DynOutput, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures, State}; -use rspc_binario::BinarioOutput; +use futures::{Stream, StreamExt}; +use rspc::{DynOutput, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures}; use rspc_invalidation::Invalidator; -use serde_json::{de::SliceRead, value::RawValue, Value}; +use serde_json::Value; use std::{ convert::Infallible, - future::{poll_fn, Future}, + future::poll_fn, path::PathBuf, - pin::{pin, Pin}, + pin::Pin, task::{Context, Poll}, }; use streamunordered::{StreamUnordered, StreamYield}; -use tokio_util::compat::FuturesAsyncReadCompatExt; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] @@ -56,39 +52,7 @@ async fn main() { .allow_origin(Any); let app = axum::Router::new() - .route("/", get(|| async { "Hello 'rspc'!" })) - // .route( - // "/upload", - // post(|mut multipart: Multipart| async move { - // println!("{:?}", multipart); - // while let Some(field) = multipart.next_field().await.unwrap() { - // println!( - // "{:?} {:?} {:?}", - // field.name().map(|v| v.to_string()), - // field.content_type().map(|v| v.to_string()), - // field.collect::>().await - // ); - // } - // "Done!" - // }), - // ) - .route( - "/rspc/custom", - post(|| async move { - // println!("{:?}", multipart); - - // while let Some(field) = multipart.next_field().await.unwrap() { - // println!( - // "{:?} {:?} {:?}", - // field.name().map(|v| v.to_string()), - // field.content_type().map(|v| v.to_string()), - // field.collect::>().await - // ); - // } - - todo!(); - }), - ) + .route("/", get(|| async { "rspc 🤝 Axum!" })) // .nest( // "/rspc", // rspc_axum::endpoint(procedures, |parts: Parts| { @@ -114,63 +78,7 @@ async fn main() { } pub fn rspc_handler(procedures: Procedures) -> axum::Router { - let mut r = axum::Router::new(); - - r = r.route( - "/binario", - // This endpoint lacks batching, SFM's, etc but that's fine. - post({ - let procedures = procedures.clone(); - - move |parts: Parts, body: Body| async move { - let invalidator = rspc_invalidation::Invalidator::default(); - let (zer, _zer_response) = rspc_zer::Zer::from_request( - "session", - "some_secret".as_ref(), - parts.headers.get("cookie"), - ) - .unwrap(); // TODO: Error handling - let ctx = Ctx { - invalidator: invalidator.clone(), - zer, - }; - - // if parts.headers.get("Content-Type") != Some(&"text/x-binario".parse().unwrap()) { - // // TODO: Error handling - // } - - let mut params = form_urlencoded::parse(parts.uri.query().unwrap_or("").as_bytes()); - let procedure_name = params - .find(|(key, _)| key == "procedure") - .map(|(_, value)| value) - .unwrap(); // TODO: Error handling - - let procedure = procedures.get(&procedure_name).unwrap(); // TODO: Error handling - - let stream = procedure.exec_with_value( - ctx.clone(), - rspc_binario::BinarioInput::from_stream( - body.into_data_stream() - .map_err(|err| todo!()) // TODO: Error handling - .into_async_read() - .compat(), - ), - ); - let mut headers = HeaderMap::new(); - headers.insert(header::CONTENT_TYPE, "text/x-binario".parse().unwrap()); - - ( - headers, - Body::from_stream(stream.map(|v| match v { - Ok(v) => Ok(Ok::<_, Infallible>( - v.as_value::().unwrap().0, - )), - Err(err) => todo!("{err:?}"), - })), - ) - } - }), - ); + let r = axum::Router::new(); // TODO: Support file upload and download @@ -426,9 +334,9 @@ impl Stream for Prototype { } } -fn encode_msg(a: (), b: (), c: ()) { - todo!(); -} +// fn encode_msg(a: (), b: (), c: ()) { +// todo!(); +// } // TODO: support `GET` // r = r.route( diff --git a/examples/binario/Cargo.toml b/examples/binario/Cargo.toml new file mode 100644 index 00000000..98f370e5 --- /dev/null +++ b/examples/binario/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "example-binario" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +rspc = { path = "../../rspc", features = ["typescript", "rust"] } +rspc-binario = { version = "0.0.0", path = "../../crates/binario" } +specta = { workspace = true, features = ["derive"] } +tokio = { version = "1.42.0", features = ["full"] } +axum = { version = "0.7.9", features = ["multipart"] } +tower-http = { version = "0.6.2", default-features = false, features = [ + "cors", +] } +futures = "0.3" +form_urlencoded = "1.2.1" +axum-extra = { version = "0.9.6", features = ["async-read-body"] } +tokio-util = { version = "0.7.13", features = ["compat"] } +binario = "0.0.2" +pin-project = "1.1.7" diff --git a/examples/binario/client.ts b/examples/binario/client.ts new file mode 100644 index 00000000..64a60f36 --- /dev/null +++ b/examples/binario/client.ts @@ -0,0 +1,41 @@ +// TODO: This is not stable, just a demonstration of how it could work. + +(async () => { + const resp = await fetch( + "http://localhost:4000/rspc/binario?procedure=binario", + { + method: "POST", + headers: { + "Content-Type": "text/x-binario", + }, + // { name: "Oscar" } + body: new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114]), + }, + ); + if (!resp.ok) throw new Error(`Failed to fetch ${resp.status}`); + if (resp.headers.get("content-type") !== "text/x-binario") + throw new Error("Invalid content type"); + + const result = new Uint8Array(await resp.clone().arrayBuffer()); + const expected = new Uint8Array([ + 5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114, + ]); + if (!isEqualBytes(result, expected)) + throw new Error("Result doesn't match expected value"); + + console.log("Success!", result); +})(); + +function isEqualBytes(bytes1: Uint8Array, bytes2: Uint8Array): boolean { + if (bytes1.length !== bytes2.length) { + return false; + } + + for (let i = 0; i < bytes1.length; i++) { + if (bytes1[i] !== bytes2[i]) { + return false; + } + } + + return true; +} diff --git a/examples/binario/src/main.rs b/examples/binario/src/main.rs new file mode 100644 index 00000000..7eed7aa5 --- /dev/null +++ b/examples/binario/src/main.rs @@ -0,0 +1,127 @@ +//! An example of exposing rspc via Binario (instead of Serde) w/ Axum. +//! This is more to prove it's possible than something you should actually copy. + +use axum::{ + body::Body, + http::{header, request::Parts, HeaderMap}, + routing::{get, post}, +}; +use futures::TryStreamExt; +use rspc::{Procedure, ProcedureBuilder, Procedures, ResolverInput, ResolverOutput}; +use rspc_binario::BinarioOutput; +use specta::Type; +use std::{convert::Infallible, marker::PhantomData}; +use tokio_util::compat::FuturesAsyncReadCompatExt; +use tower_http::cors::{Any, CorsLayer}; + +pub enum Error { + Binario(rspc_binario::DeserializeError), +} +impl From for Error { + fn from(err: rspc_binario::DeserializeError) -> Self { + Error::Binario(err) + } +} +impl rspc::Error for Error { + fn into_procedure_error(self) -> rspc::ProcedureError { + todo!(); // TODO: Work this out + } +} + +type Ctx = (); +pub struct BaseProcedure(PhantomData); +impl BaseProcedure { + pub fn builder( + ) -> ProcedureBuilder + where + TErr: rspc::Error, + TInput: ResolverInput, + TResult: ResolverOutput, + { + Procedure::builder() // You add default middleware here + } +} + +pub fn mount() -> rspc::Router<()> { + rspc::Router::new().procedure("binario", { + #[derive(Debug, binario::Encode, binario::Decode, Type)] + pub struct Input { + name: String, + } + + ::builder() + .with(rspc_binario::binario()) + .query(|_, input: Input| async move { Ok(input) }) + }) +} + +#[tokio::main] +async fn main() { + let router = mount(); + let (procedures, _types) = router.build().unwrap(); + + // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! + let cors = CorsLayer::new() + .allow_methods(Any) + .allow_headers(Any) + .allow_origin(Any); + + let app = axum::Router::new() + .route("/", get(|| async { "Hello 'rspc'!" })) + .nest("/rspc", rspc_binario_handler(procedures)) + .layer(cors); + + let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 + println!("listening on http://{}/rspc/binario", addr); + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) + .await + .unwrap(); +} + +pub fn rspc_binario_handler(procedures: Procedures<()>) -> axum::Router { + // This endpoint lacks batching, SFMs and more but that's not really the point of this example. + axum::Router::new().route( + "/binario", + post({ + let procedures = procedures.clone(); + + move |parts: Parts, body: Body| async move { + let ctx = (); + + // if parts.headers.get("Content-Type") != Some(&"text/x-binario".parse().unwrap()) { + // // TODO: Error handling + // } + + let mut params = form_urlencoded::parse(parts.uri.query().unwrap_or("").as_bytes()); + let procedure_name = params + .find(|(key, _)| key == "procedure") + .map(|(_, value)| value) + .unwrap(); // TODO: Error handling + + let procedure = procedures.get(&procedure_name).unwrap(); // TODO: Error handling + + let stream = procedure.exec_with_value( + ctx.clone(), + rspc_binario::BinarioInput::from_stream( + body.into_data_stream() + .map_err(|err| todo!()) // TODO: Error handling + .into_async_read() + .compat(), + ), + ); + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, "text/x-binario".parse().unwrap()); + + ( + headers, + Body::from_stream(stream.map(|v| match v { + Ok(v) => Ok(Ok::<_, Infallible>( + v.as_value::().unwrap().0, + )), + Err(err) => todo!("{err:?}"), + })), + ) + } + }), + ) +} diff --git a/examples/bindings.ts b/examples/bindings.ts index cd92a15f..a77eddf1 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -2,7 +2,6 @@ // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. export type Procedures = { - binario: { kind: "query", input: any, output: any, error: any }, cached: { kind: "query", input: any, output: any, error: any }, login: { kind: "query", input: any, output: any, error: any }, me: { kind: "query", input: any, output: any, error: any }, diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index 0f60274b..5b4806f1 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -6,21 +6,17 @@ publish = false [dependencies] rspc = { path = "../../rspc", features = ["typescript", "rust"] } -async-stream = "0.3.6" -serde = { version = "1.0.216", features = ["derive"] } -specta = { version = "=2.0.0-rc.20", features = ["derive"] } -thiserror = "2.0.9" rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } -tracing = "0.1.41" -futures = "0.3.31" rspc-cache = { version = "0.0.0", path = "../../crates/cache" } rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } rspc-validator = { version = "0.0.0", path = "../../crates/validator" } -rspc-binario = { version = "0.0.0", path = "../../crates/binario" } rspc-tracing = { version = "0.0.0", path = "../../crates/tracing" } rspc-openapi = { version = "0.0.0", path = "../../crates/openapi" } -validator = { version = "0.19.0", features = ["derive"] } rspc-zer = { version = "0.0.0", path = "../../crates/zer" } +serde = { workspace = true, features = ["derive"] } +specta = { workspace = true, features = ["derive"] } +tracing = { workspace = true } +async-stream = "0.3.6" +thiserror = "2.0.9" +validator = { version = "0.19.0", features = ["derive"] } anyhow = "1.0.95" -binario = "0.0.2" -pin-project = "1.1.7" diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index 10d22f6f..f41e822d 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -1,19 +1,14 @@ use std::{marker::PhantomData, time::SystemTime}; -use async_stream::stream; -use binario::encode; -use futures::executor::block_on; use rspc::{ middleware::Middleware, Procedure, ProcedureBuilder, ResolverInput, ResolverOutput, Router, }; -use rspc_binario::Binario; use rspc_cache::{cache, cache_ttl, CacheState, Memory}; use rspc_invalidation::Invalidate; use rspc_zer::Zer; use serde::{Deserialize, Serialize}; use specta::Type; use thiserror::Error; -use tracing::info; use validator::Validate; #[derive(Clone, Serialize, Deserialize, Type)] @@ -60,12 +55,13 @@ pub enum Error { } impl rspc::Error for Error { - fn into_resolver_error(self) -> rspc::ResolverError { + fn into_procedure_error(self) -> rspc::ProcedureError { // rspc::ResolverError::new(self.to_string(), Some(self)) // TODO: Typesafe way to achieve this rspc::ResolverError::new( self, None::, // TODO: `Some(self)` but `anyhow::Error` is not `Clone` ) + .into() } } @@ -201,26 +197,6 @@ pub fn mount() -> Router { .procedure("me", { ::builder().query(|ctx, _: ()| async move { Ok(ctx.zer.session()?) }) }) - .procedure("binario", { - #[derive(Debug, binario::Encode, binario::Decode, Type)] - pub struct Input { - name: String, - } - - // let mut buf = vec![]; - // block_on(encode( - // &Input { - // name: "Oscar".to_string(), - // }, - // &mut buf, - // )) - // .unwrap(); - // println!("{:?}", buf); - - ::builder() - .with(rspc_binario::binario()) - .query(|_, input: Input| async move { Ok(input) }) - }) // .procedure("fileupload", { // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) diff --git a/examples/tauri/src-tauri/src/api.rs b/examples/tauri/src-tauri/src/api.rs index 1b2735a5..cb089b49 100644 --- a/examples/tauri/src-tauri/src/api.rs +++ b/examples/tauri/src-tauri/src/api.rs @@ -24,7 +24,7 @@ impl Serialize for Infallible { impl std::error::Error for Infallible {} impl rspc::Error for Infallible { - fn into_resolver_error(self) -> rspc::ResolverError { + fn into_procedure_error(self) -> rspc::ProcedureError { unreachable!() } } diff --git a/rspc/src/error.rs b/rspc/src/error.rs index 6481289f..95f1a7c0 100644 --- a/rspc/src/error.rs +++ b/rspc/src/error.rs @@ -1,9 +1,6 @@ -use std::error; +use rspc_procedure::ProcedureError; -use rspc_procedure::ResolverError; -use serde::Serialize; -use specta::Type; - -pub trait Error: error::Error + Send + Serialize + Type + 'static { - fn into_resolver_error(self) -> ResolverError; +// TODO: Drop bounds on this cause they can be added at the impl. +pub trait Error: 'static { + fn into_procedure_error(self) -> ProcedureError; } diff --git a/rspc/src/procedure.rs b/rspc/src/procedure.rs index 69f86907..d9ee6c4c 100644 --- a/rspc/src/procedure.rs +++ b/rspc/src/procedure.rs @@ -107,7 +107,7 @@ impl Procedure { ) .into_stream() .map_ok(|v| v.into_stream()) - .map_err(|err| err.into_resolver_error()) + .map_err(|err| err.into_procedure_error()) .try_flatten() .into_stream(), ) diff --git a/rspc/src/procedure/resolver_output.rs b/rspc/src/procedure/resolver_output.rs index 342ae7aa..10b318ec 100644 --- a/rspc/src/procedure/resolver_output.rs +++ b/rspc/src/procedure/resolver_output.rs @@ -93,7 +93,7 @@ where fn into_stream(self) -> impl Stream> + Send + 'static { self.0 .map_ok(|v| v.into_stream()) - .map_err(|err| err.into_resolver_error()) + .map_err(|err| err.into_procedure_error()) .try_flatten() } diff --git a/rspc/tests/router.rs b/rspc/tests/router.rs index f75dc8a1..905b49ed 100644 --- a/rspc/tests/router.rs +++ b/rspc/tests/router.rs @@ -1,6 +1,6 @@ use std::fmt; -use rspc::{Procedure, Router}; +use rspc::{Procedure, ProcedureError, Router}; use rspc_procedure::ResolverError; use serde::Serialize; use specta::Type; @@ -74,7 +74,7 @@ impl Serialize for Infallible { impl std::error::Error for Infallible {} impl rspc::Error for Infallible { - fn into_resolver_error(self) -> ResolverError { + fn into_procedure_error(self) -> ProcedureError { unreachable!() } } From 07ac2f033e3f760d8c8c03fb5144bad840f9e436 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 27 Dec 2024 14:05:31 +0800 Subject: [PATCH 67/67] `rpc-binario` streaming --- crates/binario/src/lib.rs | 77 ++++++++++++++++++++++----- crates/procedure/src/dyn_input.rs | 1 + examples/binario/Cargo.toml | 1 - examples/binario/client.ts | 19 ++++++- examples/binario/src/main.rs | 60 +++++++++++++++------ examples/bindings.ts | 1 + examples/core/Cargo.toml | 1 + examples/core/src/lib.rs | 8 +++ rspc/src/lib.rs | 2 +- rspc/src/procedure/resolver_output.rs | 4 +- rspc/src/stream.rs | 8 +-- 11 files changed, 144 insertions(+), 38 deletions(-) diff --git a/crates/binario/src/lib.rs b/crates/binario/src/lib.rs index 05248cc8..2851e611 100644 --- a/crates/binario/src/lib.rs +++ b/crates/binario/src/lib.rs @@ -1,4 +1,13 @@ //! rspc-binario: Binario support for rspc +//! +//! TODO: +//! - Support for streaming the result. Right now we encode into a buffer. +//! - `BinarioDeserializeError` should end up as a `ProcedureError::Deserialize` not `ProcedureError::Resolver` +//! - Binario needs impl for `()` for procedures with no input. +//! - Client integration +//! - Cleanup HTTP endpoint on `example-binario`. Maybe don't use HTTP cause Axum's model doesn't mesh with Binario? +//! - Maybe actix-web example to show portability. Might be interesting with the fact that Binario depends on `tokio::AsyncRead`. +//! #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( @@ -9,7 +18,7 @@ use std::{error, fmt, marker::PhantomData, pin::Pin}; use binario::{encode, Decode, Encode}; -use futures_util::{stream, Stream}; +use futures_util::{stream, Stream, StreamExt}; use rspc::{ middleware::Middleware, DynInput, ProcedureError, ProcedureStream, ResolverInput, ResolverOutput, @@ -46,23 +55,65 @@ impl ResolverInput for TypedBinarioInput { } } -// TODO: Streaming instead of this +// TODO: This should probs be a stream not a buffer. +// Binario currently only supports `impl AsyncRead` not `impl Stream` pub struct BinarioOutput(pub Vec); -pub struct TypedBinarioOutput(pub T); +pub struct TypedBinarioOutput(pub T, pub PhantomData M>); -impl ResolverOutput - for TypedBinarioOutput +pub(crate) mod sealed { + use super::*; + + pub trait ValidBinarioOutput: Send + 'static { + type T: Encode + Send + Sync + 'static; + fn data_type(types: &mut TypeCollection) -> DataType; + fn into_stream( + self, + ) -> impl Stream> + Send + 'static; + } + pub enum ValueMarker {} + impl ValidBinarioOutput for T { + type T = T; + fn data_type(types: &mut TypeCollection) -> DataType { + T::inline(types, Generics::Definition) + } + fn into_stream( + self, + ) -> impl Stream> + Send + 'static { + stream::once(async move { Ok(self) }) + } + } + pub enum StreamMarker {} + impl ValidBinarioOutput for rspc::Stream + where + S::Item: Encode + Type + Send + Sync + 'static, + { + type T = S::Item; + + fn data_type(types: &mut TypeCollection) -> DataType { + S::Item::inline(types, Generics::Definition) + } + fn into_stream( + self, + ) -> impl Stream> + Send + 'static { + self.0.map(|v| Ok(v)) + } + } +} + +impl> ResolverOutput + for TypedBinarioOutput { type T = BinarioOutput; fn data_type(types: &mut TypeCollection) -> DataType { - T::inline(types, Generics::Definition) + T::data_type(types) } fn into_stream(self) -> impl Stream> + Send + 'static { - stream::once(async move { + // TODO: Encoding into a buffer is not how Binario is intended to work but it's how rspc needs it. + self.0.into_stream().then(|v| async move { let mut buf = Vec::new(); - encode(&self.0, &mut buf).await.unwrap(); // TODO: Error handling + encode(&v?, &mut buf).await.unwrap(); // TODO: Error handling Ok(BinarioOutput(buf)) }) } @@ -74,11 +125,11 @@ impl ResolverOutput } } -pub fn binario() -> Middleware< +pub fn binario() -> Middleware< TError, TCtx, TypedBinarioInput, - TypedBinarioOutput, + TypedBinarioOutput, TCtx, TInput, TResult, @@ -87,7 +138,7 @@ where TError: From + Send + 'static, TCtx: Send + 'static, TInput: Decode + Send + 'static, - TResult: Encode + Send + Sync + 'static, + TResult: sealed::ValidBinarioOutput, { Middleware::new( move |ctx: TCtx, input: TypedBinarioInput, next| async move { @@ -97,7 +148,9 @@ where } .map_err(DeserializeError)?; - next.exec(ctx, input).await.map(TypedBinarioOutput) + next.exec(ctx, input) + .await + .map(|v| TypedBinarioOutput(v, PhantomData)) }, ) } diff --git a/crates/procedure/src/dyn_input.rs b/crates/procedure/src/dyn_input.rs index 33f32776..98768b7f 100644 --- a/crates/procedure/src/dyn_input.rs +++ b/crates/procedure/src/dyn_input.rs @@ -21,6 +21,7 @@ enum Repr<'a, 'de> { } impl<'a, 'de> DynInput<'a, 'de> { + // TODO: Explain invariant on `Option` + enforce it pub fn new_value(value: &'a mut Option) -> Self { Self { inner: Repr::Value(value), diff --git a/examples/binario/Cargo.toml b/examples/binario/Cargo.toml index 98f370e5..52464d00 100644 --- a/examples/binario/Cargo.toml +++ b/examples/binario/Cargo.toml @@ -15,7 +15,6 @@ tower-http = { version = "0.6.2", default-features = false, features = [ ] } futures = "0.3" form_urlencoded = "1.2.1" -axum-extra = { version = "0.9.6", features = ["async-read-body"] } tokio-util = { version = "0.7.13", features = ["compat"] } binario = "0.0.2" pin-project = "1.1.7" diff --git a/examples/binario/client.ts b/examples/binario/client.ts index 64a60f36..04e69cb1 100644 --- a/examples/binario/client.ts +++ b/examples/binario/client.ts @@ -21,9 +21,26 @@ 5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114, ]); if (!isEqualBytes(result, expected)) - throw new Error("Result doesn't match expected value"); + throw new Error(`Result doesn't match expected value. Got ${result}`); console.log("Success!", result); + + const resp2 = await fetch( + "http://localhost:4000/rspc/binario?procedure=streaming", + { + method: "POST", + headers: { + "Content-Type": "text/x-binario", + }, + // { name: "Oscar" } + body: new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114]), + }, + ); + if (!resp2.ok) throw new Error(`Failed to fetch ${resp2.status}`); + if (resp2.headers.get("content-type") !== "text/x-binario") + throw new Error("Invalid content type"); + + console.log(await resp2.arrayBuffer()); })(); function isEqualBytes(bytes1: Uint8Array, bytes2: Uint8Array): boolean { diff --git a/examples/binario/src/main.rs b/examples/binario/src/main.rs index 7eed7aa5..ffcdbf0a 100644 --- a/examples/binario/src/main.rs +++ b/examples/binario/src/main.rs @@ -42,17 +42,29 @@ impl BaseProcedure { } } +#[derive(Debug, Clone, binario::Encode, binario::Decode, Type)] +pub struct Input { + name: String, +} + pub fn mount() -> rspc::Router<()> { - rspc::Router::new().procedure("binario", { - #[derive(Debug, binario::Encode, binario::Decode, Type)] - pub struct Input { - name: String, - } - - ::builder() - .with(rspc_binario::binario()) - .query(|_, input: Input| async move { Ok(input) }) - }) + rspc::Router::new() + .procedure("binario", { + ::builder() + .with(rspc_binario::binario()) + .query(|_, input: Input| async move { Ok(input) }) + }) + .procedure("streaming", { + ::builder() + .with(rspc_binario::binario()) + .query(|_, input: Input| async move { + Ok(rspc::Stream(futures::stream::iter([ + input.clone(), + input.clone(), + input, + ]))) + }) + }) } #[tokio::main] @@ -104,7 +116,7 @@ pub fn rspc_binario_handler(procedures: Procedures<()>) -> axum::Router { ctx.clone(), rspc_binario::BinarioInput::from_stream( body.into_data_stream() - .map_err(|err| todo!()) // TODO: Error handling + .map_err(|_err| todo!()) // TODO: Error handling .into_async_read() .compat(), ), @@ -112,13 +124,29 @@ pub fn rspc_binario_handler(procedures: Procedures<()>) -> axum::Router { let mut headers = HeaderMap::new(); headers.insert(header::CONTENT_TYPE, "text/x-binario".parse().unwrap()); + let mut first = true; ( headers, - Body::from_stream(stream.map(|v| match v { - Ok(v) => Ok(Ok::<_, Infallible>( - v.as_value::().unwrap().0, - )), - Err(err) => todo!("{err:?}"), + Body::from_stream(stream.map(move |v| { + let buf = match v { + Ok(v) => Ok(Ok::<_, Infallible>( + v.as_value::().unwrap().0, + )), + Err(err) => todo!("{err:?}"), + }; + + if first { + first = false; + buf + } else { + buf.map(|v| { + v.map(|mut v| { + let mut buf = vec!['\n' as u8, '\n' as u8]; + buf.append(&mut v); + buf + }) + }) + } })), ) } diff --git a/examples/bindings.ts b/examples/bindings.ts index a77eddf1..db805b95 100644 --- a/examples/bindings.ts +++ b/examples/bindings.ts @@ -11,6 +11,7 @@ export type Procedures = { newstuffser: { kind: "query", input: any, output: any, error: any }, sfmPost: { kind: "query", input: any, output: any, error: any }, sfmPostEdit: { kind: "query", input: any, output: any, error: any }, + streamInStreamInStreamInStream: { kind: "query", input: any, output: any, error: any }, validator: { kind: "query", input: any, output: any, error: any }, withoutBaseProcedure: { kind: "query", input: any, output: any, error: any }, } \ No newline at end of file diff --git a/examples/core/Cargo.toml b/examples/core/Cargo.toml index 5b4806f1..59080245 100644 --- a/examples/core/Cargo.toml +++ b/examples/core/Cargo.toml @@ -20,3 +20,4 @@ async-stream = "0.3.6" thiserror = "2.0.9" validator = { version = "0.19.0", features = ["derive"] } anyhow = "1.0.95" +futures.workspace = true diff --git a/examples/core/src/lib.rs b/examples/core/src/lib.rs index f41e822d..5a4b05b9 100644 --- a/examples/core/src/lib.rs +++ b/examples/core/src/lib.rs @@ -197,6 +197,14 @@ pub fn mount() -> Router { .procedure("me", { ::builder().query(|ctx, _: ()| async move { Ok(ctx.zer.session()?) }) }) + .procedure("streamInStreamInStreamInStream", { + // You would never actually do this but it's just checking how the system behaves + ::builder().query(|_, _: ()| async move { + Ok(rspc::Stream(rspc::Stream(rspc::Stream( + futures::stream::once(async move { Ok(42) }), + )))) + }) + }) // .procedure("fileupload", { // ::builder().query(|_, _: File| async { Ok(env!("CARGO_PKG_VERSION")) }) diff --git a/rspc/src/lib.rs b/rspc/src/lib.rs index e5938d40..354bf501 100644 --- a/rspc/src/lib.rs +++ b/rspc/src/lib.rs @@ -6,7 +6,7 @@ //! //! Checkout the official docs at . This documentation is generally written **for authors of middleware and adapter**. //! -#![forbid(unsafe_code)] +// #![forbid(unsafe_code)] // TODO #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", diff --git a/rspc/src/procedure/resolver_output.rs b/rspc/src/procedure/resolver_output.rs index 10b318ec..0b6ff31a 100644 --- a/rspc/src/procedure/resolver_output.rs +++ b/rspc/src/procedure/resolver_output.rs @@ -81,8 +81,6 @@ where TErr: Error, S: Stream> + Send + 'static, T: ResolverOutput, - // Should prevent nesting `Stream`s - T::T: Serialize + Send + Sync + 'static, { type T = T::T; @@ -100,6 +98,6 @@ where fn into_procedure_stream( stream: impl Stream> + Send + 'static, ) -> ProcedureStream { - ProcedureStream::from_stream(stream) + T::into_procedure_stream(stream) } } diff --git a/rspc/src/stream.rs b/rspc/src/stream.rs index b9de701a..cf5083ee 100644 --- a/rspc/src/stream.rs +++ b/rspc/src/stream.rs @@ -41,12 +41,12 @@ impl Clone for Stream { } } -// TODO: I hate this requiring `Unpin` but we couldn't use `pin-project-lite` with the tuple variant. -impl futures_util::Stream for Stream { +impl futures_util::Stream for Stream { type Item = S::Item; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.0.poll_next_unpin(cx) + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // TODO: Using `pin-project-lite` would be nice but I don't think it supports tuple variants and I don't want the macros of `pin-project`. + unsafe { self.map_unchecked_mut(|v| &mut v.0) }.poll_next_unpin(cx) } fn size_hint(&self) -> (usize, Option) {