Skip to content

Commit 4f9d7d5

Browse files
committed
Added file watching
1 parent 5f09875 commit 4f9d7d5

File tree

5 files changed

+256
-27
lines changed

5 files changed

+256
-27
lines changed

Cargo.lock

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/lib/backend/ipc.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,51 @@ export async function invoke<T>(command: string, params: Record<string, unknown>
103103
}
104104
}
105105

106+
let webListener: WebListener | undefined;
107+
106108
export function listen<T>(event: EventName, handle: EventCallback<T>) {
107109
if (import.meta.env.VITE_BUILD_TARGET === 'electron') {
110+
if (!webListener) {
111+
webListener = new WebListener();
112+
}
113+
108114
// TODO: Listening in electron
109-
return async () => {};
115+
return webListener.listen({ name: event, handle });
110116
} else {
111117
const unlisten = listenTauri(event, handle);
112118
return async () => await unlisten.then((unlistenFn) => unlistenFn());
113119
}
114120
}
121+
122+
class WebListener {
123+
private socket: WebSocket | undefined;
124+
private count = 0;
125+
private handlers: { name: EventName; handle: EventCallback<any> }[] = [];
126+
127+
listen(handler: { name: EventName; handle: EventCallback<any> }): () => void {
128+
this.handlers.push(handler);
129+
this.count++;
130+
if (!this.socket) {
131+
this.socket = new WebSocket('ws://localhost:6978/ws');
132+
this.socket.addEventListener('message', (event) => {
133+
const data: { name: string; payload: any } = JSON.parse(event.data);
134+
for (const handler of this.handlers) {
135+
if (handler.name === data.name) {
136+
// The id is an artifact from tauri, we don't use it so
137+
// I've used a random value
138+
handler.handle({ event: data.name, payload: data.payload, id: 69 });
139+
}
140+
}
141+
});
142+
}
143+
144+
return () => {
145+
this.handlers = this.handlers.filter((h) => h !== handler);
146+
this.count--;
147+
if (this.count === 0) {
148+
this.socket?.close();
149+
this.socket = undefined;
150+
}
151+
};
152+
}
153+
}

crates/but-server/Cargo.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ doctest = false
1717

1818
[dependencies]
1919
serde.workspace = true
20-
axum = "0.8.4"
20+
axum = { version = "0.8.4", features = ["ws"] }
21+
futures-util = { version = "0.3", default-features = false, features = [
22+
"sink",
23+
"std",
24+
] }
2125
tower = "0.5.2"
2226
tower-http = { version = "0.6.5", features = ["cors"] }
2327
tokio = { workspace = true, features = ["full"] }
@@ -55,15 +59,20 @@ but-graph.workspace = true
5559
gitbutler-oplog.workspace = true
5660
but-hunk-dependency.workspace = true
5761
serde-error = "0.1.3"
58-
gix = { workspace = true, features = ["worktree-mutation", "blocking-http-transport-curl"] }
62+
gix = { workspace = true, features = [
63+
"worktree-mutation",
64+
"blocking-http-transport-curl",
65+
] }
5966
git2.workspace = true
6067
gitbutler-feedback.workspace = true
6168
gitbutler-error.workspace = true
6269
gitbutler-diff.workspace = true
70+
gitbutler-watcher.workspace = true
6371
but-rules.workspace = true
6472
but-action.workspace = true
6573
reqwest = { version = "0.12", features = ["json"] }
6674
gitbutler-forge.workspace = true
6775
open = "5.3"
6876
url = "2.5"
6977
gitbutler-id.workspace = true
78+
uuid.workspace = true

crates/but-server/src/lib.rs

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
use std::sync::Arc;
1+
use std::{collections::HashMap, sync::Arc};
22

3-
use axum::{Json, Router, routing::get};
3+
use axum::{
4+
Json, Router,
5+
extract::{
6+
WebSocketUpgrade,
7+
ws::{Message, WebSocket},
8+
},
9+
response::IntoResponse,
10+
routing::{any, get},
11+
};
412
use but_settings::AppSettingsWithDiskSync;
13+
use futures_util::{SinkExt, StreamExt as _};
514
use serde::{Deserialize, Serialize};
615
use serde_json::json;
716
use tokio::sync::Mutex;
@@ -34,11 +43,13 @@ mod virtual_branches;
3443
mod workspace;
3544
mod zip;
3645

46+
#[derive(Clone)]
3747
pub(crate) struct RequestContext {
3848
app_settings: Arc<AppSettingsWithDiskSync>,
3949
user_controller: Arc<gitbutler_user::Controller>,
4050
project_controller: Arc<gitbutler_project::Controller>,
4151
active_projects: Arc<Mutex<ActiveProjects>>,
52+
broadcaster: Arc<Mutex<Broadcaster>>,
4253
}
4354

4455
#[derive(Serialize, Deserialize)]
@@ -55,6 +66,13 @@ pub(crate) struct Request {
5566
params: serde_json::Value,
5667
}
5768

69+
#[derive(Debug, Serialize, Clone)]
70+
#[serde(rename_all = "camelCase")]
71+
pub(crate) struct FrontendEvent {
72+
name: String,
73+
payload: serde_json::Value,
74+
}
75+
5876
pub async fn run() {
5977
let cors = CorsLayer::new()
6078
.allow_methods(Any)
@@ -70,25 +88,35 @@ pub async fn run() {
7088
.expect("missing config dir")
7189
.join("gitbutler-server");
7290

73-
let app_settings = Arc::new(
74-
AppSettingsWithDiskSync::new(config_dir.clone()).expect("failed to create app settings"),
75-
);
76-
let user_controller = Arc::new(gitbutler_user::Controller::from_path(&app_data_dir));
77-
let project_controller = Arc::new(gitbutler_project::Controller::from_path(&app_data_dir));
78-
let active_projects = Arc::new(Mutex::new(ActiveProjects::new()));
91+
let broadcaster = Arc::new(Mutex::new(Broadcaster {
92+
senders: HashMap::new(),
93+
}));
94+
95+
let ctx = RequestContext {
96+
app_settings: Arc::new(
97+
AppSettingsWithDiskSync::new(config_dir.clone())
98+
.expect("failed to create app settings"),
99+
),
100+
user_controller: Arc::new(gitbutler_user::Controller::from_path(&app_data_dir)),
101+
project_controller: Arc::new(gitbutler_project::Controller::from_path(&app_data_dir)),
102+
active_projects: Arc::new(Mutex::new(ActiveProjects::new())),
103+
broadcaster: broadcaster.clone(),
104+
};
79105

80106
// build our application with a single route
81107
let app = Router::new()
82108
.route(
83109
"/",
84-
get(|| async { "Hello, World!" }).post(move |req| {
85-
let ctx = RequestContext {
86-
app_settings: Arc::clone(&app_settings),
87-
user_controller: Arc::clone(&user_controller),
88-
project_controller: Arc::clone(&project_controller),
89-
active_projects: Arc::clone(&active_projects),
90-
};
91-
handle_command(req, ctx)
110+
get(|| async { "Hello, World!" }).post({
111+
let ctx = ctx.clone();
112+
move |req| handle_command(req, ctx)
113+
}),
114+
)
115+
.route(
116+
"/ws",
117+
any({
118+
let broadcaster = broadcaster.clone();
119+
async move |req| handle_ws_request(req, broadcaster).await
92120
}),
93121
)
94122
.layer(ServiceBuilder::new().layer(cors));
@@ -99,6 +127,54 @@ pub async fn run() {
99127
axum::serve(listener, app).await.unwrap();
100128
}
101129

130+
struct Broadcaster {
131+
senders: HashMap<uuid::Uuid, tokio::sync::mpsc::UnboundedSender<FrontendEvent>>,
132+
}
133+
134+
impl Broadcaster {
135+
fn send(&self, event: FrontendEvent) {
136+
for sender in self.senders.values() {
137+
let _ = sender.send(event.clone());
138+
}
139+
}
140+
}
141+
142+
async fn handle_ws_request(
143+
ws: WebSocketUpgrade,
144+
broadcaster: Arc<Mutex<Broadcaster>>,
145+
) -> impl IntoResponse {
146+
ws.on_upgrade(move |socket| handle_websocket(socket, broadcaster))
147+
}
148+
149+
async fn handle_websocket(socket: WebSocket, broadcaster: Arc<Mutex<Broadcaster>>) {
150+
let (send, mut recv) = tokio::sync::mpsc::unbounded_channel();
151+
let id = uuid::Uuid::new_v4();
152+
broadcaster.lock().await.senders.insert(id, send);
153+
154+
let (mut socket_send, mut socket_recv) = socket.split();
155+
let thread = tokio::spawn(async move {
156+
while let Some(event) = recv.recv().await {
157+
socket_send
158+
.send(Message::Text(serde_json::to_string(&event).unwrap().into()))
159+
.await
160+
.unwrap();
161+
}
162+
});
163+
164+
while let Some(Ok(msg)) = socket_recv.next().await {
165+
#[allow(clippy::single_match)]
166+
match msg {
167+
Message::Close(_) => {
168+
thread.abort();
169+
break;
170+
}
171+
_ => {}
172+
}
173+
}
174+
175+
broadcaster.lock().await.senders.remove(&id);
176+
}
177+
102178
async fn handle_command(
103179
Json(request): Json<Request>,
104180
ctx: RequestContext,

0 commit comments

Comments
 (0)