mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
rspc over P2P (#2112)
* wip: rspc over p2p * wip * rspc over P2P * Cleanup + error handling * slight cleanup * Using Hyper for HTTP streaming + websockets
This commit is contained in:
parent
f7a7b00e37
commit
aa0b4abf85
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -7692,6 +7692,7 @@ dependencies = [
|
||||||
"hostname",
|
"hostname",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-range",
|
"http-range",
|
||||||
|
"hyper",
|
||||||
"image",
|
"image",
|
||||||
"int-enum",
|
"int-enum",
|
||||||
"itertools 0.12.0",
|
"itertools 0.12.0",
|
||||||
|
@ -7744,6 +7745,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
|
@ -13,6 +13,7 @@ use axum::{
|
||||||
response::Response,
|
response::Response,
|
||||||
RequestPartsExt,
|
RequestPartsExt,
|
||||||
};
|
};
|
||||||
|
use http::Method;
|
||||||
use hyper::server::{accept::Accept, conn::AddrIncoming};
|
use hyper::server::{accept::Accept, conn::AddrIncoming};
|
||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
use sd_core::{custom_uri, Node, NodeError};
|
use sd_core::{custom_uri, Node, NodeError};
|
||||||
|
@ -116,7 +117,7 @@ pub async fn sd_server_plugin<R: Runtime>(
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct QueryParams {
|
struct QueryParams {
|
||||||
token: String,
|
token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_middleware<B>(
|
async fn auth_middleware<B>(
|
||||||
|
@ -128,16 +129,19 @@ async fn auth_middleware<B>(
|
||||||
where
|
where
|
||||||
B: Send,
|
B: Send,
|
||||||
{
|
{
|
||||||
let req = if query.token != auth_token {
|
let req = if query.token.as_ref() != Some(&auth_token) {
|
||||||
let (mut parts, body) = request.into_parts();
|
let (mut parts, body) = request.into_parts();
|
||||||
|
|
||||||
let auth: TypedHeader<Authorization<Bearer>> = parts
|
// We don't check auth for OPTIONS requests cause the CORS middleware will handle it
|
||||||
.extract()
|
if parts.method != Method::OPTIONS {
|
||||||
.await
|
let auth: TypedHeader<Authorization<Bearer>> = parts
|
||||||
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
.extract()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
||||||
|
|
||||||
if auth.token() != auth_token {
|
if auth.token() != auth_token {
|
||||||
return Err(StatusCode::UNAUTHORIZED);
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Request::from_parts(parts, body)
|
Request::from_parts(parts, body)
|
||||||
|
|
|
@ -53,6 +53,12 @@ export const platform = {
|
||||||
constructServerUrl(`/file/${libraryId}/${locationLocalId}/${filePathId}`),
|
constructServerUrl(`/file/${libraryId}/${locationLocalId}/${filePathId}`),
|
||||||
getFileUrlByPath: (path) =>
|
getFileUrlByPath: (path) =>
|
||||||
constructServerUrl(`/local-file-by-path/${encodeURIComponent(path)}`),
|
constructServerUrl(`/local-file-by-path/${encodeURIComponent(path)}`),
|
||||||
|
getRemoteRspcEndpoint: (remote_identity) => ({
|
||||||
|
url: `${customUriServerUrl?.[0]}/remote/${encodeURIComponent(remote_identity)}/rspc`,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${customUriAuthToken}`
|
||||||
|
}
|
||||||
|
}),
|
||||||
openLink: shell.open,
|
openLink: shell.open,
|
||||||
getOs,
|
getOs,
|
||||||
openDirectoryPickerDialog: (opts) => {
|
openDirectoryPickerDialog: (opts) => {
|
||||||
|
|
|
@ -49,6 +49,9 @@ const platform: Platform = {
|
||||||
locationLocalId
|
locationLocalId
|
||||||
)}/${encodeURIComponent(filePathId)}`,
|
)}/${encodeURIComponent(filePathId)}`,
|
||||||
getFileUrlByPath: (path) => `${spacedriveURL}/local-file-by-path/${encodeURIComponent(path)}`,
|
getFileUrlByPath: (path) => `${spacedriveURL}/local-file-by-path/${encodeURIComponent(path)}`,
|
||||||
|
getRemoteRspcEndpoint: (remote_identity) => ({
|
||||||
|
url: `${spacedriveURL}/remote/${encodeURIComponent(remote_identity)}/rspc`
|
||||||
|
}),
|
||||||
openLink: (url) => window.open(url, '_blank')?.focus(),
|
openLink: (url) => window.open(url, '_blank')?.focus(),
|
||||||
confirm: (message, cb) => cb(window.confirm(message)),
|
confirm: (message, cb) => cb(window.confirm(message)),
|
||||||
auth: {
|
auth: {
|
||||||
|
|
|
@ -66,6 +66,7 @@ regex = { workspace = true }
|
||||||
reqwest = { workspace = true, features = ["json", "native-tls-vendored"] }
|
reqwest = { workspace = true, features = ["json", "native-tls-vendored"] }
|
||||||
rmp-serde = { workspace = true }
|
rmp-serde = { workspace = true }
|
||||||
rspc = { workspace = true, features = [
|
rspc = { workspace = true, features = [
|
||||||
|
"axum",
|
||||||
"uuid",
|
"uuid",
|
||||||
"chrono",
|
"chrono",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -127,6 +128,8 @@ aws-config = "1.0.3"
|
||||||
aws-credential-types = "1.0.3"
|
aws-credential-types = "1.0.3"
|
||||||
base91 = "0.1.0"
|
base91 = "0.1.0"
|
||||||
sd-actors = { version = "0.1.0", path = "../crates/actors" }
|
sd-actors = { version = "0.1.0", path = "../crates/actors" }
|
||||||
|
tower-service = "0.3.2"
|
||||||
|
hyper = { version = "=0.14.28", features = ["http1", "server", "client"] }
|
||||||
|
|
||||||
# Override features of transitive dependencies
|
# Override features of transitive dependencies
|
||||||
[dependencies.openssl]
|
[dependencies.openssl]
|
||||||
|
|
|
@ -238,7 +238,9 @@ pub(crate) fn mount() -> Arc<Router> {
|
||||||
<sd_prisma::prisma::object::Data as specta::NamedType>::SID,
|
<sd_prisma::prisma::object::Data as specta::NamedType>::SID,
|
||||||
def,
|
def,
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
let r = r
|
||||||
.build(
|
.build(
|
||||||
#[allow(clippy::let_and_return)]
|
#[allow(clippy::let_and_return)]
|
||||||
{
|
{
|
||||||
|
|
|
@ -28,8 +28,9 @@ use async_stream::stream;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{self, Body, BoxBody, Full, StreamBody},
|
body::{self, Body, BoxBody, Full, StreamBody},
|
||||||
extract::{self, State},
|
extract::{self, State},
|
||||||
http::{HeaderValue, Request, Response, StatusCode},
|
http::{HeaderMap, HeaderValue, Request, Response, StatusCode},
|
||||||
middleware,
|
middleware,
|
||||||
|
response::IntoResponse,
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
@ -320,6 +321,36 @@ pub fn router(node: Arc<Node>) -> Router<()> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/remote/:identity/rspc/*path",
|
||||||
|
get(
|
||||||
|
|State(state): State<LocalState>,
|
||||||
|
extract::Path((identity, rest)): extract::Path<(String, String)>,
|
||||||
|
mut request: Request<Body>| async move {
|
||||||
|
let identity = match RemoteIdentity::from_str(&identity) {
|
||||||
|
Ok(identity) => identity,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error parsing identity '{}': {}", identity, err);
|
||||||
|
return (StatusCode::BAD_REQUEST, HeaderMap::new(), vec![])
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
*request.uri_mut() = format!("/{rest}")
|
||||||
|
.parse()
|
||||||
|
.expect("url was validated by Axum");
|
||||||
|
|
||||||
|
match operations::remote_rspc(state.node.p2p.p2p.clone(), identity, request)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response.into_response(),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error doing remote rspc query with '{identity}': {err:?}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
.route_layer(middleware::from_fn(cors_middleware))
|
.route_layer(middleware::from_fn(cors_middleware))
|
||||||
.with_state({
|
.with_state({
|
||||||
let file_metadata_cache = Arc::new(Cache::new(150));
|
let file_metadata_cache = Arc::new(Cache::new(150));
|
||||||
|
|
|
@ -158,13 +158,23 @@ impl Node {
|
||||||
init_data.apply(&node.libraries, &node).await?;
|
init_data.apply(&node.libraries, &node).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let router = api::mount();
|
||||||
|
|
||||||
// Be REALLY careful about ordering here or you'll get unreliable deadlock's!
|
// Be REALLY careful about ordering here or you'll get unreliable deadlock's!
|
||||||
locations_actor.start(node.clone());
|
locations_actor.start(node.clone());
|
||||||
node.libraries.init(&node).await?;
|
node.libraries.init(&node).await?;
|
||||||
jobs_actor.start(node.clone());
|
jobs_actor.start(node.clone());
|
||||||
start_p2p(node.clone());
|
start_p2p(
|
||||||
|
node.clone(),
|
||||||
let router = api::mount();
|
router
|
||||||
|
.clone()
|
||||||
|
.endpoint({
|
||||||
|
let node = node.clone();
|
||||||
|
move |_| node.clone()
|
||||||
|
})
|
||||||
|
.axum::<()>()
|
||||||
|
.into_make_service(),
|
||||||
|
);
|
||||||
|
|
||||||
info!("Spacedrive online.");
|
info!("Spacedrive online.");
|
||||||
Ok((node, router))
|
Ok((node, router))
|
||||||
|
|
|
@ -7,6 +7,8 @@ use crate::{
|
||||||
Node,
|
Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use axum::routing::IntoMakeService;
|
||||||
|
|
||||||
use sd_p2p2::{
|
use sd_p2p2::{
|
||||||
flume::{bounded, Receiver},
|
flume::{bounded, Receiver},
|
||||||
Libp2pPeerId, Listener, Mdns, Peer, QuicTransport, RemoteIdentity, UnicastStream, P2P,
|
Libp2pPeerId, Listener, Mdns, Peer, QuicTransport, RemoteIdentity, UnicastStream, P2P,
|
||||||
|
@ -17,12 +19,15 @@ use serde_json::json;
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
convert::Infallible,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
sync::{atomic::AtomicBool, Arc, Mutex, PoisonError},
|
sync::{atomic::AtomicBool, Arc, Mutex, PoisonError},
|
||||||
};
|
};
|
||||||
|
use tower_service::Service;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tracing::{error, info};
|
use tracing::info;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{P2PEvents, PeerMetadata};
|
use super::{P2PEvents, PeerMetadata};
|
||||||
|
@ -44,7 +49,13 @@ impl P2PManager {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
node_config: Arc<config::Manager>,
|
node_config: Arc<config::Manager>,
|
||||||
libraries: Arc<crate::library::Libraries>,
|
libraries: Arc<crate::library::Libraries>,
|
||||||
) -> Result<(Arc<P2PManager>, impl FnOnce(Arc<Node>)), String> {
|
) -> Result<
|
||||||
|
(
|
||||||
|
Arc<P2PManager>,
|
||||||
|
impl FnOnce(Arc<Node>, IntoMakeService<axum::Router<()>>),
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
> {
|
||||||
let (tx, rx) = bounded(25);
|
let (tx, rx) = bounded(25);
|
||||||
let p2p = P2P::new(SPACEDRIVE_APP_ID, node_config.get().await.identity, tx);
|
let p2p = P2P::new(SPACEDRIVE_APP_ID, node_config.get().await.identity, tx);
|
||||||
let (quic, lp2p_peer_id) = QuicTransport::spawn(p2p.clone())?;
|
let (quic, lp2p_peer_id) = QuicTransport::spawn(p2p.clone())?;
|
||||||
|
@ -70,8 +81,8 @@ impl P2PManager {
|
||||||
this.p2p.listeners()
|
this.p2p.listeners()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok((this.clone(), |node| {
|
Ok((this.clone(), |node, router| {
|
||||||
tokio::spawn(start(this, node, rx));
|
tokio::spawn(start(this, node, rx, router));
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,10 +231,13 @@ async fn start(
|
||||||
this: Arc<P2PManager>,
|
this: Arc<P2PManager>,
|
||||||
node: Arc<Node>,
|
node: Arc<Node>,
|
||||||
rx: Receiver<UnicastStream>,
|
rx: Receiver<UnicastStream>,
|
||||||
|
mut service: IntoMakeService<axum::Router<()>>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
while let Ok(mut stream) = rx.recv_async().await {
|
while let Ok(mut stream) = rx.recv_async().await {
|
||||||
let this = this.clone();
|
let this = this.clone();
|
||||||
let node = node.clone();
|
let node = node.clone();
|
||||||
|
let mut service = unwrap_infallible(service.call(()).await);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
println!("APPLICATION GOT STREAM: {:?}", stream); // TODO
|
println!("APPLICATION GOT STREAM: {:?}", stream); // TODO
|
||||||
|
|
||||||
|
@ -286,6 +300,14 @@ async fn start(
|
||||||
|
|
||||||
error!("Failed to handle file request");
|
error!("Failed to handle file request");
|
||||||
}
|
}
|
||||||
|
Header::Http => {
|
||||||
|
let remote = stream.remote_identity();
|
||||||
|
let Err(err) = operations::rspc::receiver(stream, &mut service).await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
error!("Failed to handling rspc request with '{remote}': {err:?}");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -309,3 +331,10 @@ pub fn into_listener2(l: &[Listener]) -> Vec<Listener2> {
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
|
||||||
|
match result {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => match err {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
pub mod ping;
|
pub mod ping;
|
||||||
pub mod request_file;
|
pub mod request_file;
|
||||||
|
pub mod rspc;
|
||||||
pub mod spacedrop;
|
pub mod spacedrop;
|
||||||
|
|
||||||
pub use request_file::request_file;
|
pub use request_file::request_file;
|
||||||
|
pub use rspc::remote_rspc;
|
||||||
pub use spacedrop::spacedrop;
|
pub use spacedrop::spacedrop;
|
||||||
|
|
53
core/src/p2p/operations/rspc.rs
Normal file
53
core/src/p2p/operations/rspc.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use std::{error::Error, sync::Arc};
|
||||||
|
|
||||||
|
use axum::{body::Body, http, Router};
|
||||||
|
use hyper::{server::conn::Http, Response};
|
||||||
|
use sd_p2p2::{RemoteIdentity, UnicastStream, P2P};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::p2p::Header;
|
||||||
|
|
||||||
|
/// Transfer an rspc query to a remote node.
|
||||||
|
#[allow(unused)]
|
||||||
|
pub async fn remote_rspc(
|
||||||
|
p2p: Arc<P2P>,
|
||||||
|
identity: RemoteIdentity,
|
||||||
|
request: http::Request<axum::body::Body>,
|
||||||
|
) -> Result<Response<Body>, Box<dyn Error>> {
|
||||||
|
let peer = p2p
|
||||||
|
.peers()
|
||||||
|
.get(&identity)
|
||||||
|
.ok_or("Peer not found, has it been discovered?")?
|
||||||
|
.clone();
|
||||||
|
let mut stream = peer.new_stream().await?;
|
||||||
|
|
||||||
|
stream.write_all(&Header::Http.to_bytes()).await?;
|
||||||
|
|
||||||
|
let (mut sender, conn) = hyper::client::conn::handshake(stream).await?;
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
if let Err(err) = conn.await {
|
||||||
|
println!("Connection error: {:?}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sender.send_request(request).await.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn receiver(
|
||||||
|
stream: UnicastStream,
|
||||||
|
service: &mut Router,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
debug!(
|
||||||
|
"Received http request from peer '{}'",
|
||||||
|
stream.remote_identity(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Http::new()
|
||||||
|
.http1_only(true)
|
||||||
|
.http1_keep_alive(true)
|
||||||
|
.serve_connection(stream, service)
|
||||||
|
.with_upgrades()
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ pub enum Header {
|
||||||
Spacedrop(SpaceblockRequests),
|
Spacedrop(SpaceblockRequests),
|
||||||
Sync(Uuid),
|
Sync(Uuid),
|
||||||
File(HeaderFile),
|
File(HeaderFile),
|
||||||
|
// A HTTP server used for rspc requests and streaming files
|
||||||
|
Http,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -86,6 +88,7 @@ impl Header {
|
||||||
i => return Err(HeaderError::HeaderFileDiscriminatorInvalid(i)),
|
i => return Err(HeaderError::HeaderFileDiscriminatorInvalid(i)),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
5 => Ok(Self::Http),
|
||||||
d => Err(HeaderError::DiscriminatorInvalid(d)),
|
d => Err(HeaderError::DiscriminatorInvalid(d)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +119,7 @@ impl Header {
|
||||||
buf.extend_from_slice(&range.to_bytes());
|
buf.extend_from_slice(&range.to_bytes());
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
Self::Http => vec![5],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ export default function DebugSection() {
|
||||||
<Icon component={ShareNetwork} />
|
<Icon component={ShareNetwork} />
|
||||||
P2P
|
P2P
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
<SidebarLink to="debug/p2p-rspc">
|
||||||
|
<Icon component={ShareNetwork} />
|
||||||
|
P2P (rspc)
|
||||||
|
</SidebarLink>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,5 +5,6 @@ export const debugRoutes = [
|
||||||
{ path: 'cloud', lazy: () => import('./cloud') },
|
{ path: 'cloud', lazy: () => import('./cloud') },
|
||||||
{ path: 'sync', lazy: () => import('./sync') },
|
{ path: 'sync', lazy: () => import('./sync') },
|
||||||
{ path: 'actors', lazy: () => import('./actors') },
|
{ path: 'actors', lazy: () => import('./actors') },
|
||||||
{ path: 'p2p', lazy: () => import('./p2p') }
|
{ path: 'p2p', lazy: () => import('./p2p') },
|
||||||
|
{ path: 'p2p-rspc', lazy: () => import('./p2p-rspc') }
|
||||||
] satisfies RouteObject[];
|
] satisfies RouteObject[];
|
||||||
|
|
82
interface/app/$libraryId/debug/p2p-rspc.tsx
Normal file
82
interface/app/$libraryId/debug/p2p-rspc.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { httpLink, initRspc, type AlphaClient } from '@oscartbeaumont-sd/rspc-client/v2';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDiscoveredPeers, type Procedures } from '@sd/client';
|
||||||
|
import { Button } from '@sd/ui';
|
||||||
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
// TODO: Handle if P2P is disabled
|
||||||
|
const [activePeer, setActivePeer] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
{activePeer ? (
|
||||||
|
<P2PInfo peer={activePeer} />
|
||||||
|
) : (
|
||||||
|
<PeerSelector setActivePeer={setActivePeer} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function PeerSelector({ setActivePeer }: { setActivePeer: (peer: string) => void }) {
|
||||||
|
const peers = useDiscoveredPeers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Nodes:</h1>
|
||||||
|
{peers.size === 0 ? (
|
||||||
|
<p>No peers found...</p>
|
||||||
|
) : (
|
||||||
|
<ul>
|
||||||
|
{[...peers.entries()].map(([id, _node]) => (
|
||||||
|
<li key={id}>
|
||||||
|
{id}
|
||||||
|
<Button onClick={() => setActivePeer(id)}>Connect</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function P2PInfo({ peer }: { peer: string }) {
|
||||||
|
const platform = usePlatform();
|
||||||
|
const ref = useRef<AlphaClient<Procedures>>();
|
||||||
|
const [result, setResult] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: Cleanup when URL changed
|
||||||
|
const endpoint = platform.getRemoteRspcEndpoint(peer);
|
||||||
|
ref.current = initRspc<Procedures>({
|
||||||
|
links: [
|
||||||
|
httpLink({
|
||||||
|
url: endpoint.url,
|
||||||
|
headers: endpoint.headers
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}, [peer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
ref.current.query(['nodeState']).then((data) => setResult(JSON.stringify(data, null, 2)));
|
||||||
|
}, [ref, result]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h1>Connected with: {peer}</h1>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
ref.current
|
||||||
|
?.query(['nodeState'])
|
||||||
|
.then((data) => setResult(JSON.stringify(data, null, 2)));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refetch
|
||||||
|
</Button>
|
||||||
|
<pre>{result}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,6 +17,10 @@ export type Platform = {
|
||||||
getThumbnailUrlByThumbKey: (thumbKey: string[]) => string;
|
getThumbnailUrlByThumbKey: (thumbKey: string[]) => string;
|
||||||
getFileUrl: (libraryId: string, locationLocalId: number, filePathId: number) => string;
|
getFileUrl: (libraryId: string, locationLocalId: number, filePathId: number) => string;
|
||||||
getFileUrlByPath: (path: string) => string;
|
getFileUrlByPath: (path: string) => string;
|
||||||
|
getRemoteRspcEndpoint: (remote_identity: string) => {
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
};
|
||||||
openLink: (url: string) => void;
|
openLink: (url: string) => void;
|
||||||
// Tauri patches `window.confirm` to return `Promise` not `bool`
|
// Tauri patches `window.confirm` to return `Promise` not `bool`
|
||||||
confirm(msg: string, cb: (result: boolean) => void): void;
|
confirm(msg: string, cb: (result: boolean) => void): void;
|
||||||
|
|
Loading…
Reference in a new issue