Skip to content

WIP: 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

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
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,
},
}
67 changes: 67 additions & 0 deletions backend/src/collab/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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>,
}

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

pub fn fork(&self) -> Self {
Document {
buffer: self.buffer.clone(),
history: self.history.clone(),
notify: Notify::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
}

pub fn get_history_since(&self, revision: usize) -> Vec<Action> {
if revision < self.history.len() {
self.history[revision..].to_owned()
} else {
Vec::new()
}
}
}
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;
Loading
Loading