Skip to content

feat: add frontend-backend file synchronization #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 51 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f2b6b95
wip
Brayan-724 Apr 15, 2025
3aca19b
feat(frontend/ws): Add websocket types
Brayan-724 Apr 15, 2025
6dd1dfb
chore(styles): Suppress `mixed-decl` warnings
Brayan-724 Apr 16, 2025
64a5695
refactor(frontend): Remove `solid-icons` dependency
Brayan-724 Apr 18, 2025
0f293fa
feat(frontend): Add devtools
Brayan-724 Apr 18, 2025
4b0394c
Merge branch 'main' into feat/frontend-sync
Brayan-724 Apr 18, 2025
f175521
fix(backend): i'm so dumb
Brayan-724 Apr 18, 2025
78a0e09
wip
Brayan-724 Apr 20, 2025
528bd51
feat(backend/auth): add `/auth/me` for jwt check
Brayan-724 Apr 21, 2025
e9816ea
feat(frontend/auth): Refresh jwt at start
Brayan-724 Apr 21, 2025
c1f08a5
chore(backend): Remove `cola` dependency
Brayan-724 Apr 22, 2025
dd91afa
wip
Brayan-724 Apr 25, 2025
6089728
fix(backend/collab): Apply actions in compose
Brayan-724 Apr 26, 2025
204cc93
fix(backend/ws): Use broadcast for access update
Brayan-724 Apr 26, 2025
a405ffd
style(backend): remove some warnings
Brayan-724 Apr 26, 2025
c4093cd
wip: updating tests
Brayan-724 Apr 26, 2025
e889a42
fix(backend/ws): Broadcast sync message
Brayan-724 Apr 26, 2025
da2b0b9
feat(backend/ws): Broadcast files
Brayan-724 Apr 26, 2025
48ac25a
feat(backend/ws): More descriptive syntax errors
Brayan-724 Apr 26, 2025
7fd3a1b
feat(backend/ws): Implement file deletion
Brayan-724 Apr 26, 2025
2dd5b91
chore(backend/test): Use session id for sync actions
Brayan-724 Apr 26, 2025
d2510b3
chore(frontend/ws): Update message types
Brayan-724 Apr 26, 2025
c68f9dc
feat(frontend/colab): Create project at bootstrap
Brayan-724 Apr 26, 2025
a75eca8
fix(frontend/auth): `username` is not used anymore
Brayan-724 Apr 26, 2025
0dfd1e6
feat(backend/project): Add get project route
Brayan-724 Apr 27, 2025
a421278
fix(frotend/colab): Handle project not found
Brayan-724 Apr 28, 2025
be2eca7
chore(frontend/editor): Create file when open it
Brayan-724 Apr 29, 2025
ed3e331
feat(frontend/editor): Send sync operations
Brayan-724 Apr 29, 2025
9274c56
fix(backend/collab): Handle out of bounds actions
Brayan-724 Apr 29, 2025
b067adc
feat(backend/project): Create `main.rs` when creating project
Brayan-724 Apr 29, 2025
1dc457f
revert(frontend/editor): Create file when open it
Brayan-724 Apr 29, 2025
af82b12
chore(backend/project): Remove newline at beginning
Brayan-724 Apr 29, 2025
2246a34
feat(backend): Send document info
Brayan-724 Apr 29, 2025
20940ad
feat(frontend): Add files sync
Brayan-724 Apr 29, 2025
c10490d
fix(frontend/sidebar): Did not use scroll bar
Brayan-724 Apr 29, 2025
ab4c1c9
fix(frontend/sidebar): Close sidebar in small screens
Brayan-724 Apr 29, 2025
a205f06
chore(backend/ws): Send all history in sync
Brayan-724 Apr 29, 2025
6eea0cf
feat(frontend/editor): Receive and apply sync actions
Brayan-724 Apr 29, 2025
4671e4f
chore(frontend/editor): untrack dockview
Brayan-724 Apr 29, 2025
6af292e
fix(frontend/panels): Pass owner to code panel
Brayan-724 Apr 29, 2025
f031142
feat(frontend/editor): Background syncronization
Brayan-724 Apr 30, 2025
7ec6827
fix(frontend/colab): AuthInfo can be null, dumb
Brayan-724 Apr 30, 2025
e362987
fix(frontend/editor): Check if both exists
Brayan-724 Apr 30, 2025
aceddaf
fix(frontend/panels): nasty hack for release mode
Brayan-724 Apr 30, 2025
b30d7c6
fix(frontend/editor): Force cursor update
Brayan-724 Apr 30, 2025
a77a7fa
feat(frontend): Add toast alerts
Brayan-724 May 1, 2025
92fc323
feat(frontend): Restrict editable content to non-editor user
Brayan-724 May 1, 2025
e680560
chore(frontend/toast): Use more saturated colors in dark-mode
Brayan-724 May 1, 2025
a361539
feat(frontend/editor): Add ui for shared cursors
Brayan-724 May 2, 2025
7df41dd
feat(backend): Synchronize cursor
Brayan-724 May 2, 2025
cee6581
feat(frontend/editor): Synchronize cursors
Brayan-724 May 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ actix = "0.13"
actix-cors = "0.7.1"
actix_error_proc = { version = "1.1.4", features = ["thiserror"] }
actix-web = "4"
actix-ws = "0.3.0"
chrono = "0.4"
cola = "0.5.0"
dotenv = "0.15.0"
env_logger = "0.11.7"
futures = "0.3"
Expand All @@ -26,9 +26,9 @@ tokio.workspace = true
uuid = { version = "1.3", features = ["serde", "v4"] }

# Para los tests de integracion
[dev-dependencies]
awc = "3" # Cliente HTTP y WebSocket para Actix
actix-rt = { version = "2.10.0", features = ["macros"] }
futures-util = "0.3"
lazy_static = "1.4"
once_cell = "1.17"
actix-ws = "0.3.0"
3 changes: 2 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ cargo run --bin backend
```
para ejecutar los tests
```
cargo test --test integration_test
cargo test --no-fail-fast --test integration_test
cargo test --no-fail-fast --bin backend --verbose
```

## Things to know
Expand Down
1 change: 0 additions & 1 deletion backend/src/auth/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::utils::expect_var;
#[derive(Deserialize, Debug)]
pub struct GitHubUser {
pub login: String,
pub name: Option<String>,
pub avatar_url: String,
}

Expand Down
5 changes: 3 additions & 2 deletions backend/src/auth/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::sync::LazyLock;

use actix_web::HttpRequest;
use chrono::{Duration, Utc};
use chrono::{Duration, TimeDelta, Utc};
use serde::{Deserialize, Serialize};

use crate::expect_var;
use crate::http_errors::HttpErrors;

pub static JWT_SECRET: LazyLock<String> = LazyLock::new(|| expect_var!("JWT_SECRET"));
const JWT_EXP: TimeDelta = Duration::hours(12);

#[derive(Debug, Serialize, Deserialize)]
pub struct RgUserData {
Expand All @@ -19,7 +20,7 @@ pub struct RgUserData {

impl RgUserData {
pub fn new(id: String, name: String, is_guest: bool) -> Self {
let exp = Utc::now() + Duration::hours(12);
let exp = Utc::now() + JWT_EXP;

Self {
id,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pub mod github;
pub mod handlers;
pub mod jwt;
pub mod routes;
19 changes: 13 additions & 6 deletions backend/src/auth/handlers.rs → backend/src/auth/routes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use actix_error_proc::{proof_route, HttpResult};
use actix_web::{get, web, HttpResponse, Responder};
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse};
use serde::Deserialize;
use uuid::Uuid;
Expand All @@ -18,7 +18,7 @@ pub struct AuthRequest {
}

#[get("/auth")]
pub async fn oauth(oauth: web::Data<OAuthData>) -> impl Responder {
pub async fn auth(oauth: web::Data<OAuthData>) -> impl Responder {
let (auth_url, _csrf_token) = oauth
.client
.authorize_url(CsrfToken::new_random)
Expand All @@ -30,8 +30,15 @@ pub async fn oauth(oauth: web::Data<OAuthData>) -> impl Responder {
.finish()
}

#[get("/auth/me")]
pub async fn me(req: HttpRequest) -> HttpResult<HttpErrors> {
let user_info = jwt::get_user_info(&req)?;

Ok(HttpResponse::Ok().json(user_info))
}

#[get("/auth/callback")]
async fn auth_callback(
async fn callback(
query: web::Query<AuthRequest>,
oauth_data: web::Data<OAuthData>,
) -> HttpResult<HttpErrors> {
Expand Down Expand Up @@ -69,8 +76,8 @@ struct GuestLoginRequest {
guest_name: String,
}

#[proof_route(post("/login-guest"))]
async fn guest_jwt(body: web::Json<GuestLoginRequest>) -> HttpResult<HttpErrors> {
#[proof_route(post("/auth/guest"))]
async fn login_guest(body: web::Json<GuestLoginRequest>) -> HttpResult<HttpErrors> {
let guest_name = &body.guest_name;
let guest_uuid = Uuid::new_v4().to_string();
let jwt = RgUserData::new(guest_uuid.clone(), guest_name.clone(), true).encode()?;
Expand All @@ -87,7 +94,7 @@ struct UpdateNameRequest {
new_name: String,
}

#[proof_route(post("/update-name"))]
#[proof_route(post("/auth/update"))]
async fn update_name(
body: web::Json<UpdateNameRequest>,
req: actix_web::HttpRequest,
Expand Down
16 changes: 16 additions & 0 deletions backend/src/collab/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
Insertion {
from: usize,
text: String,
owner: String,
},
Deletion {
from: usize,
to: usize,
owner: String,
},
}
89 changes: 89 additions & 0 deletions backend/src/collab/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::collections::HashMap;

use serde::Serialize;
use tokio::sync::Notify;

use super::ot::{apply_actions, transform_actions};
use super::Action;

#[derive(Debug)]
pub struct Document {
pub buffer: String,
/// Users can subscribe to document events
pub notify: Notify,
pub history: Vec<Action>,
pub cursors: HashMap<String, Vec<(usize, usize)>>,
}

#[derive(Clone, Debug, Serialize)]
pub struct DocumentInfo {
pub text: String,
pub revision: usize,
}

impl From<&Document> for DocumentInfo {
fn from(value: &Document) -> Self {
Self {
text: value.buffer.clone(),
revision: value.history.len(),
}
}
}

impl Document {
pub fn new() -> Self {
Document {
buffer: String::new(),
notify: Notify::new(),
history: Vec::new(),
cursors: HashMap::new(),
}
}

pub fn new_with(buffer: String) -> Self {
Document {
buffer,
notify: Notify::new(),
history: Vec::new(),
cursors: HashMap::new(),
}
}

pub fn fork(&self) -> Self {
Document {
buffer: self.buffer.clone(),
history: self.history.clone(),
notify: Notify::new(),
cursors: HashMap::new(),
}
}

pub fn revision(&self) -> usize {
self.history.len()
}

/// Add actions to document history.
/// - Transform desynchorized actions
/// - Notify to document listeners
pub fn compose(&mut self, revision: usize, mut actions: Vec<Action>) -> Vec<Action> {
if revision == self.revision() {
self.buffer = apply_actions(&self.buffer, &actions);
self.history.extend(actions.iter().cloned());
self.notify.notify_waiters();
return actions;
} else if revision > self.history.len() {
log::warn!("Someone comes from the future");
return Vec::new();
}

let desynchronized_history = &self.history[revision..];

transform_actions(actions.as_mut_slice(), desynchronized_history);

self.buffer = apply_actions(&self.buffer, &actions);
self.history.extend_from_slice(&actions);
self.notify.notify_waiters();

actions
}
}
6 changes: 6 additions & 0 deletions backend/src/collab/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod action;
mod document;
pub mod ot;

pub use action::Action;
pub use document::{Document, DocumentInfo};
Loading
Loading