Merge branch 'main' of github.com:spacedriveapp/spacedrive

This commit is contained in:
Ericson Fogo Soares 2022-07-12 09:12:43 -03:00
commit 471babf4c0
6 changed files with 188 additions and 71 deletions

View file

@ -1,9 +1,15 @@
use sdcore::{ClientCommand, ClientQuery, CoreEvent, CoreResponse, Node, NodeController};
use std::{env, path::Path};
use std::{
collections::HashSet,
env,
path::Path,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use actix::{
Actor, AsyncContext, ContextFutureSpawner, Handler, Message, StreamHandler,
WrapFuture,
Actor, ActorContext, Addr, AsyncContext, Context, ContextFutureSpawner, Handler,
Message, StreamHandler, WrapFuture,
};
use actix_web::{
get, http::StatusCode, web, App, Error, HttpRequest, HttpResponse, HttpServer,
@ -16,10 +22,85 @@ use tokio::sync::mpsc;
const DATA_DIR_ENV_VAR: &'static str = "DATA_DIR";
#[derive(Serialize)]
pub struct Event(CoreEvent);
impl Message for Event {
type Result = ();
}
struct EventServer {
clients: Arc<RwLock<HashSet<Addr<Socket>>>>,
}
impl Actor for EventServer {
type Context = Context<Self>;
}
impl EventServer {
pub fn listen(mut event_receiver: mpsc::Receiver<CoreEvent>) -> Addr<Self> {
let server = Self {
clients: Arc::new(RwLock::new(HashSet::new())),
};
let clients = server.clients.clone();
tokio::spawn(async move {
let mut last = Instant::now();
while let Some(event) = event_receiver.recv().await {
match event {
CoreEvent::InvalidateQueryDebounced(_) => {
let current = Instant::now();
if current.duration_since(last) > Duration::from_millis(1000 / 60)
{
last = current;
for client in clients.read().unwrap().iter() {
client.do_send(Event(event.clone()));
}
}
},
event => {
for client in clients.read().unwrap().iter() {
client.do_send(Event(event.clone()));
}
},
}
}
});
server.start()
}
}
enum EventServerOperation {
Connect(Addr<Socket>),
Disconnect(Addr<Socket>),
}
impl Message for EventServerOperation {
type Result = ();
}
impl Handler<EventServerOperation> for EventServer {
type Result = ();
fn handle(
&mut self,
msg: EventServerOperation,
_: &mut Context<Self>,
) -> Self::Result {
match msg {
EventServerOperation::Connect(addr) => {
self.clients.write().unwrap().insert(addr)
},
EventServerOperation::Disconnect(addr) => {
self.clients.write().unwrap().remove(&addr)
},
};
}
}
/// Define HTTP actor
struct Socket {
_event_receiver: web::Data<mpsc::Receiver<CoreEvent>>,
core: web::Data<NodeController>,
node_controller: web::Data<NodeController>,
event_server: web::Data<Addr<EventServer>>,
}
impl Actor for Socket {
@ -62,18 +143,21 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
},
};
let core = self.core.clone();
let core = self.node_controller.clone();
self.event_server
.do_send(EventServerOperation::Connect(ctx.address()));
let recipient = ctx.address().recipient();
let fut = async move {
match msg.payload {
SocketMessagePayload::Query(query) => {
match core.query(query).await {
Ok(response) => recipient.do_send(SocketResponse {
id: msg.id.clone(),
payload: SocketResponsePayload::Query(response),
}),
Ok(response) => {
recipient.do_send(SocketResponse::Response {
id: msg.id.clone(),
payload: response,
})
},
Err(err) => {
println!("query error: {:?}", err);
// Err(err.to_string())
@ -82,10 +166,12 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
},
SocketMessagePayload::Command(command) => {
match core.command(command).await {
Ok(response) => recipient.do_send(SocketResponse {
id: msg.id.clone(),
payload: SocketResponsePayload::Query(response),
}),
Ok(response) => {
recipient.do_send(SocketResponse::Response {
id: msg.id.clone(),
payload: response,
})
},
Err(err) => {
println!("command error: {:?}", err);
// Err(err.to_string())
@ -102,27 +188,35 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Socket {
_ => (),
}
}
fn finished(&mut self, ctx: &mut Self::Context) {
self.event_server
.do_send(EventServerOperation::Disconnect(ctx.address()));
ctx.stop();
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase", tag = "type", content = "data")]
pub enum SocketResponsePayload {
Query(CoreResponse),
impl Handler<Event> for Socket {
type Result = ();
fn handle(&mut self, msg: Event, ctx: &mut Self::Context) {
ctx.text(serde_json::to_string(&SocketResponse::Event(msg.0)).unwrap());
}
}
#[derive(Message, Serialize)]
#[serde(rename_all = "camelCase", tag = "type", content = "data")]
#[rtype(result = "()")]
struct SocketResponse {
id: String,
payload: SocketResponsePayload,
enum SocketResponse {
Response { id: String, payload: CoreResponse },
Event(CoreEvent),
}
impl Handler<SocketResponse> for Socket {
type Result = ();
fn handle(&mut self, msg: SocketResponse, ctx: &mut Self::Context) {
let string = serde_json::to_string(&msg).unwrap();
ctx.text(string);
ctx.text(serde_json::to_string(&msg).unwrap());
}
}
@ -140,13 +234,13 @@ async fn healthcheck() -> impl Responder {
async fn ws_handler(
req: HttpRequest,
stream: web::Payload,
event_receiver: web::Data<mpsc::Receiver<CoreEvent>>,
controller: web::Data<NodeController>,
server: web::Data<Addr<EventServer>>,
) -> Result<HttpResponse, Error> {
let resp = ws::start(
Socket {
_event_receiver: event_receiver,
core: controller,
node_controller: controller,
event_server: server,
},
&req,
stream,
@ -154,12 +248,6 @@ async fn ws_handler(
resp
}
#[get("/file/{file:.*}")]
async fn file() -> impl Responder {
// TODO
format!("OK")
}
async fn not_found() -> impl Responder {
HttpResponse::build(StatusCode::OK).body("We're past the event horizon...")
}
@ -168,15 +256,16 @@ async fn not_found() -> impl Responder {
async fn main() -> std::io::Result<()> {
let (event_receiver, controller) = setup().await;
let server = web::Data::new(EventServer::listen(event_receiver));
println!("Listening http://localhost:8080");
HttpServer::new(move || {
App::new()
.app_data(event_receiver.clone())
.app_data(controller.clone())
.app_data(server.clone())
.service(index)
.service(healthcheck)
.service(ws_handler)
.service(file)
.default_service(web::route().to(not_found))
})
.bind(("0.0.0.0", 8080))?
@ -184,10 +273,7 @@ async fn main() -> std::io::Result<()> {
.await
}
async fn setup() -> (
web::Data<mpsc::Receiver<CoreEvent>>,
web::Data<NodeController>,
) {
async fn setup() -> (mpsc::Receiver<CoreEvent>, web::Data<NodeController>) {
let data_dir_path = match env::var(DATA_DIR_ENV_VAR) {
Ok(path) => Path::new(&path).to_path_buf(),
Err(_e) => {
@ -207,5 +293,5 @@ async fn setup() -> (
let (controller, event_receiver, node) = Node::new(data_dir_path).await;
tokio::spawn(node.start());
(web::Data::new(event_receiver), web::Data::new(controller))
(event_receiver, web::Data::new(controller))
}

View file

@ -34,7 +34,6 @@ class Transport extends BaseTransport {
.then(() => {
this.websocket = ws;
this.attachEventListeners();
console.log('Reconnected!');
})
.catch((err) => this.reconnect(timeoutIndex++));
}, timeout);
@ -44,33 +43,34 @@ class Transport extends BaseTransport {
this.websocket.addEventListener('message', (event) => {
if (!event.data) return;
const { id, payload } = JSON.parse(event.data);
const { type: msg_type, data: msg_data } = JSON.parse(event.data);
const { type, data } = payload;
if (type === 'event') {
this.emit('core_event', data);
} else if (type === 'query' || type === 'command') {
if (msg_type === 'response') {
const id = msg_data.id;
if (this.requestMap.has(id)) {
this.requestMap.get(id)?.(data);
this.requestMap.get(id)?.({ data: msg_data.payload.data });
this.requestMap.delete(id);
}
} else if (msg_type === 'event') {
this.emit('core_event', msg_data);
} else {
console.error(`Received response message of type ${msg_type} which is not valid!`);
}
});
this.websocket.addEventListener('close', () => {
console.log('GONE');
this.reconnect();
});
}
async query(query: ClientQuery) {
if (websocket.readyState == 0) {
if (this.websocket.readyState == 0) {
let resolve: () => void;
const promise = new Promise((res) => {
resolve = () => res(undefined);
});
// @ts-ignore
websocket.addEventListener('open', resolve);
this.websocket.addEventListener('open', resolve);
await promise;
}
@ -89,6 +89,16 @@ class Transport extends BaseTransport {
return await promise;
}
async command(command: ClientCommand) {
if (this.websocket.readyState == 0) {
let resolve: () => void;
const promise = new Promise((res) => {
resolve = () => res(undefined);
});
// @ts-ignore
this.websocket.addEventListener('open', resolve);
await promise;
}
const id = randomId();
let resolve: (data: any) => void;

View file

@ -372,7 +372,7 @@ pub enum LibraryCommand {
}
/// is a query destined for the core
#[derive(Serialize, Deserialize, Debug, TS)]
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[serde(tag = "key", content = "params")]
#[ts(export)]
pub enum ClientQuery {
@ -388,7 +388,7 @@ pub enum ClientQuery {
}
/// is a query destined for a specific library which is loaded into the core.
#[derive(Serialize, Deserialize, Debug, TS)]
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[serde(tag = "key", content = "params")]
#[ts(export)]
pub enum LibraryQuery {
@ -407,7 +407,7 @@ pub enum LibraryQuery {
}
// represents an event this library can emit
#[derive(Serialize, Deserialize, Debug, TS)]
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[serde(tag = "key", content = "data")]
#[ts(export)]
pub enum CoreEvent {
@ -462,7 +462,7 @@ pub enum CoreError {
LibraryError(#[from] library::LibraryError),
}
#[derive(Serialize, Deserialize, Debug, TS)]
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[ts(export)]
pub enum CoreResource {
Client,

View file

@ -165,8 +165,21 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
icon: CogIcon,
onPress: () => navigate('library-settings/general')
},
{ name: 'Add Library', icon: PlusIcon },
{ name: 'Lock', icon: LockClosedIcon }
{
name: 'Add Library',
icon: PlusIcon,
onPress: () => {
alert('todo');
// TODO: Show Dialog defined in `LibrariesSettings.tsx`
}
},
{
name: 'Lock',
icon: LockClosedIcon,
onPress: () => {
alert('todo');
}
}
// { name: 'Hide', icon: EyeOffIcon }
]
]}

View file

@ -13,8 +13,9 @@ import {
Tag,
TerminalWindow
} from 'phosphor-react';
import React, { DetailedHTMLProps, HTMLAttributes } from 'react';
import React, { DetailedHTMLProps, HTMLAttributes, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { AppPropsContext } from '../../AppPropsContext';
import { Shortcut } from '../primitive/Shortcut';
import { DefaultProps } from '../primitive/types';
@ -28,6 +29,7 @@ export interface TopBarButtonProps
left?: boolean;
right?: boolean;
}
interface SearchBarProps extends DefaultProps {}
const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) => {
return (
@ -49,6 +51,23 @@ const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) =>
);
};
const SearchBar: React.FC<SearchBarProps> = (props) => { //TODO: maybe pass the appProps, so we can have the context in the TopBar if needed again
const appProps = useContext(AppPropsContext);
return (
<div className="relative flex h-7">
<input
placeholder="Search"
className="w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-500 bg-[#F6F2F6] border border-gray-50 dark:bg-gray-650 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
/>
<div className="space-x-1 absolute top-[2px] right-1">
<Shortcut chars={ appProps?.platform === "macOS" || appProps?.platform === "browser" ? "⌘K" : "CTRL+K" } />
{/* <Shortcut chars="S" /> */}
</div>
</div>
);
}
export const TopBar: React.FC<TopBarProps> = (props) => {
const { locationId } = useExplorerStore();
const { mutate: generateThumbsForLocation } = useLibraryCommand('GenerateThumbsForLocation', {
@ -88,16 +107,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
<TopBarButton icon={FolderPlus} />
<TopBarButton icon={TerminalWindow} />
</div>
<div className="relative flex h-7">
<input
placeholder="Search"
className="w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-500 bg-[#F6F2F6] border border-gray-50 dark:bg-gray-650 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
/>
<div className="space-x-1 absolute top-[2px] right-1">
<Shortcut chars="⌘K" />
{/* <Shortcut chars="S" /> */}
</div>
</div>
<SearchBar />
<div className="flex mx-8 space-x-2">
<TopBarButton icon={Key} />
<TopBarButton icon={Cloud} />

View file

@ -1,4 +1,4 @@
import { useBridgeQuery } from '@sd/client';
import { useLibraryQuery } from '@sd/client';
import React from 'react';
import LocationListItem from '../../components/location/LocationListItem';
@ -13,9 +13,7 @@ import { SettingsHeader } from '../../components/settings/SettingsHeader';
// ];
export default function LocationSettings() {
const { data: locations } = useBridgeQuery('SysGetLocations');
console.log({ locations });
const { data: locations } = useLibraryQuery('SysGetLocations');
return (
<SettingsContainer>