-
-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
//! TODO: Document how this example works. | ||
//! | ||
//! TODO: Expand this example to one which pushes data directly to the frontend. | ||
use std::{ | ||
collections::HashMap, | ||
future::Future, | ||
sync::{Arc, Mutex, PoisonError}, | ||
}; | ||
|
||
use async_stream::stream; | ||
use rspc::middleware::Middleware; | ||
use serde::{Deserialize, Serialize}; | ||
use specta::Type; | ||
use tokio::sync::broadcast; | ||
|
||
use super::{BaseProcedure, Context, Router}; | ||
|
||
#[derive(Clone)] | ||
pub struct Ctx { | ||
keys: Arc<Mutex<HashMap<String, String>>>, | ||
tx: broadcast::Sender<InvalidateEvent>, | ||
} | ||
|
||
impl Ctx { | ||
pub fn new() -> Arc<Self> { | ||
Arc::new(Self { | ||
keys: Default::default(), | ||
tx: broadcast::channel(100).0, | ||
}) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Serialize, Type)] | ||
pub enum InvalidateEvent { | ||
InvalidateKey(String), | ||
} | ||
|
||
#[derive(Deserialize, Type)] | ||
struct SetKeyInput { | ||
key: String, | ||
value: String, | ||
} | ||
|
||
pub fn mount() -> Router { | ||
Router::new() | ||
.procedure("get", { | ||
<BaseProcedure>::builder() | ||
// TODO: Why does `TCtx` need a hardcoded type??? | ||
.with(invalidation(|ctx: Context, key, _result| async move { | ||
ctx.invalidation | ||
.tx | ||
.send(InvalidateEvent::InvalidateKey(key)) | ||
.unwrap(); | ||
})) | ||
.mutation(|ctx, key: String| async move { | ||
let value = ctx | ||
.invalidation | ||
.keys | ||
.lock() | ||
.unwrap_or_else(PoisonError::into_inner) | ||
.get(&key) | ||
.cloned(); | ||
|
||
Ok(value) | ||
}) | ||
}) | ||
.procedure("set", { | ||
<BaseProcedure>::builder().mutation(|ctx, input: SetKeyInput| async move { | ||
ctx.invalidation | ||
.keys | ||
.lock() | ||
.unwrap_or_else(PoisonError::into_inner) | ||
.insert(input.key, input.value); | ||
|
||
Ok(()) | ||
}) | ||
}) | ||
.procedure("invalidation", { | ||
// The frontend will subscribe to this for when to invalidate queries. | ||
<BaseProcedure>::builder().subscription(|ctx, _: ()| async move { | ||
Ok(stream! { | ||
let mut tx = ctx.invalidation.tx.subscribe(); | ||
while let Ok(msg) = tx.recv().await { | ||
yield Ok(msg); | ||
} | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
fn invalidation<TError, TCtx, TInput, TResult, F>( | ||
handler: impl Fn(TCtx, TInput, &Result<TResult, TError>) -> F + Send + Sync + 'static, | ||
) -> Middleware<TError, TCtx, TInput, TResult> | ||
where | ||
TError: Send + 'static, | ||
TCtx: Clone + Send + 'static, | ||
TInput: Clone + Send + 'static, | ||
TResult: Send + 'static, | ||
F: Future<Output = ()> + Send + 'static, | ||
{ | ||
let handler = Arc::new(handler); | ||
Middleware::new(move |ctx: TCtx, input: TInput, next| { | ||
let handler = handler.clone(); | ||
async move { | ||
let ctx2 = ctx.clone(); | ||
let input2 = input.clone(); | ||
let result = next.exec(ctx, input).await; | ||
handler(ctx2, input2, &result).await; | ||
result | ||
} | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
// TODO: Library middleware |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// TODO: Streaming DB data | ||
// TODO: File upload | ||
// TODO: File download |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[package] | ||
name = "rspc-tauri" | ||
version = "0.0.1" | ||
edition = "2021" | ||
|
||
[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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
//! rspc-tauri: Support for exposing rspc via Tauri IPC | ||
Check warning on line 1 in integrations/tauri/src/lib.rs GitHub Actions / Clippypackage `rspc` is missing `package.readme` metadata
Check warning on line 1 in integrations/tauri/src/lib.rs GitHub Actions / Clippypackage `rspc-axum` is missing `package.readme` metadata
Check warning on line 1 in integrations/tauri/src/lib.rs GitHub Actions / Clippypackage `rspc-tauri` is missing `package.description` metadata
|
||
#![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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<BaseProcedure>::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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# rspc Playground | ||
|
||
Coming soon... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
//! rspc-playground: Playground for testing rspc procedures | ||
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc` is missing `package.readme` metadata
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc-axum` is missing `package.readme` metadata
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc-tauri` is missing `package.description` metadata
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc-tauri` is missing `either package.license or package.license_file` metadata
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc-tauri` is missing `package.repository` metadata
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc-tauri` is missing `package.readme` metadata
Check warning on line 1 in middleware/playground/src/lib.rs GitHub Actions / Clippypackage `rspc-tauri` is missing `package.keywords` metadata
|
||
#![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: A HTML playground for testing rspc procedures kinda like SwaggerUI. |