Skip to content
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

Automerge document module with hard-coded schema #3

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
225 changes: 79 additions & 146 deletions src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,77 +22,44 @@ use std::cell::{OnceCell, RefCell};

use adw::prelude::*;
use adw::subclass::prelude::*;
use automerge::transaction::Transactable;
use automerge::PatchAction;
use automerge::ReadDoc;
use automerge::{AutoCommit, ObjType};
use gettextrs::gettext;
use gtk::{gio, glib};
use tokio::sync::{mpsc, oneshot};

use crate::config::VERSION;
use crate::document::Document;
use crate::glib::closure_local;
use crate::network;
use crate::{AardvarkTextBuffer, AardvarkWindow};

mod imp {
use automerge::PatchAction;

use super::*;

#[derive(Debug)]
pub struct AardvarkApplication {
window: OnceCell<AardvarkWindow>,
automerge: RefCell<AutoCommit>,
#[allow(dead_code)]
backend_shutdown_tx: oneshot::Sender<()>,
document: Document,
tx: mpsc::Sender<Vec<u8>>,
rx: RefCell<Option<mpsc::Receiver<Vec<u8>>>>,
#[allow(dead_code)]
backend_shutdown: oneshot::Sender<()>,
}

impl AardvarkApplication {
fn update_text(&self, position: i32, del: i32, text: &str) {
let mut doc = self.automerge.borrow_mut();

let root = match doc.get(automerge::ROOT, "root").expect("root exists") {
Some(root) => root.1,
None => doc
.put_object(automerge::ROOT, "root", ObjType::Text)
.expect("inserting map at root"),
};
println!("root = {}", root);

doc.splice_text(&root, position as usize, del as isize, text)
.unwrap();

// move the diff pointer forward to current position
doc.update_diff_cursor();

/*
let patches = doc.diff_incremental();
for patch in patches.iter() {
println!("{}", patch.action);
match &patch.action {
PatchAction::SpliceText { index: _, value: _, marks: _ } => {},
PatchAction::DeleteSeq { index: _, length: _ } => {},
PatchAction::PutMap { key: _, value: _, conflict: _ } => {},
PatchAction::PutSeq { index: _, value: _, conflict: _ } => {},
PatchAction::Insert { index: _, values: _ } => {},
PatchAction::Increment { prop: _, value: _ } => {},
PatchAction::Conflict { prop: _ } => {},
PatchAction::DeleteMap { key: _ } => {},
PatchAction::Mark { marks: _ } => {},
}
}
*/

{
let bytes = doc.save_incremental();
let tx = self.tx.clone();
glib::spawn_future_local(async move {
if let Err(e) = tx.send(bytes).await {
println!("{}", e);
}
});
}
self.document
.update(position, del, text)
.expect("update automerge document after text update");

let bytes = self.document.save_incremental();
let tx = self.tx.clone();
glib::spawn_future_local(async move {
tx.send(bytes)
.await
.expect("sending message to networking backend");
});
}
}

Expand All @@ -103,12 +70,12 @@ mod imp {
type ParentType = adw::Application;

fn new() -> Self {
let automerge = RefCell::new(AutoCommit::new());
let (backend_shutdown_tx, tx, rx) = network::run().expect("running p2p backend");
let document = Document::default();
let (backend_shutdown, tx, rx) = network::run().expect("running p2p backend");

AardvarkApplication {
automerge,
backend_shutdown_tx,
document,
backend_shutdown,
tx,
rx: RefCell::new(Some(rx)),
window: OnceCell::new(),
Expand All @@ -126,105 +93,79 @@ mod imp {
}

impl ApplicationImpl for AardvarkApplication {
// We connect to the activate callback to create a window when the application
// has been launched. Additionally, this callback notifies us when the user
// tries to launch a "second instance" of the application. When they try
// to do that, we'll just present any existing window.
// We connect to the activate callback to create a window when the application has been
// launched. Additionally, this callback notifies us when the user tries to launch a
// "second instance" of the application. When they try to do that, we'll just present any
// existing window.
fn activate(&self) {
let application = self.obj();

// Get the current window or create one if necessary
let window = self
.window
.get_or_init(|| {
let window = AardvarkWindow::new(&*application);
let app = application.clone();
let mut rx = application.imp().rx.take().unwrap();
let w = window.clone();
let window = self.window.get_or_init(|| {
let window = AardvarkWindow::new(&*application);
let mut rx = application
.imp()
.rx
.take()
.expect("rx should be given at this point");

{
let window = window.clone();
let application = application.clone();

glib::spawn_future_local(async move {
while let Some(bytes) = rx.recv().await {
println!("got {:?}", bytes);
let text = {
let mut doc_local = app.imp().automerge.borrow_mut();
doc_local.load_incremental(&bytes).unwrap();
println!("LOCAL:");
print_document(&*doc_local);

let root = match doc_local
.get(automerge::ROOT, "root")
.expect("root exists")
{
Some(root) => root.1,
None => doc_local
.put_object(automerge::ROOT, "root", ObjType::Text)
.expect("inserting map at root"),
};
println!("root = {}", root);

// get the latest changes
let patches = doc_local.diff_incremental();
for patch in patches.iter() {
println!("PATCH RECEIVED: {}", patch.action);
match &patch.action {
PatchAction::SpliceText {
index,
value,
marks: _,
} => {
w.splice_text_view(
*index as i32,
0,
value.make_string().as_str(),
);
}
PatchAction::DeleteSeq { index, length } => {
w.splice_text_view(*index as i32, *length as i32, "");
}
PatchAction::PutMap {
key: _,
value: _,
conflict: _,
} => {}
PatchAction::PutSeq {
index: _,
value: _,
conflict: _,
} => {}
PatchAction::Insert {
index: _,
values: _,
} => {}
PatchAction::Increment { prop: _, value: _ } => {}
PatchAction::Conflict { prop: _ } => {}
PatchAction::DeleteMap { key: _ } => {}
PatchAction::Mark { marks: _ } => {}
let document = &application.imp().document;

// Apply remote changes to our local text CRDT
if let Err(err) = document.load_incremental(&bytes) {
eprintln!(
"failed applying text change from remote peer to automerge document: {err}"
);
continue;
}

// Get latest changes and apply them to our local text buffer
for patch in document.diff_incremental() {
match &patch.action {
PatchAction::SpliceText { index, value, .. } => {
window.splice_text_view(
*index as i32,
0,
value.make_string().as_str(),
);
}
PatchAction::DeleteSeq { index, length } => {
window.splice_text_view(*index as i32, *length as i32, "");
}
_ => (),
}
}

doc_local.text(&root).unwrap()
};
dbg!(&text);
dbg!(document.text());
}
});
}

window
})
.clone();
window
});

let app = application.clone();
window.clone().get_text_buffer().connect_closure(
"text-change",
false,
closure_local!(|_buffer: AardvarkTextBuffer,
position: i32,
del: i32,
text: &str| {
app.imp().update_text(position, del, text);
}),
);
{
let application = application.clone();
window.get_text_buffer().connect_closure(
"text-change",
false,
closure_local!(|_buffer: AardvarkTextBuffer,
position: i32,
del: i32,
text: &str| {
application.imp().update_text(position, del, text);
}),
);
}

// Ask the window manager/compositor to present the window
window.upcast::<gtk::Window>().present();
window.clone().upcast::<gtk::Window>().present();
}
}

Expand Down Expand Up @@ -272,11 +213,3 @@ impl AardvarkApplication {
about.present(Some(&window));
}
}

fn print_document<R>(doc: &R)
where
R: ReadDoc,
{
let serialized = serde_json::to_string_pretty(&automerge::AutoSerde::from(doc)).unwrap();
println!("{serialized}");
}
111 changes: 111 additions & 0 deletions src/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use std::cell::RefCell;
use std::fmt;

use anyhow::Result;
use automerge::transaction::Transactable;
use automerge::{AutoCommit, AutoSerde, ObjId, ObjType, Patch, ReadDoc};

/// Hard-coded automerge document schema in bytes representation for "Aardvark".
///
/// Creating a local document based on this schema allows peers to independently do so as they'll
/// all have the same schema and object ids in the end. Otherwise peers wouldn't be able to merge
/// their changes into each other's documents as the id's wouldn't match.
///
/// Read more here:
/// <https://automerge.org/docs/cookbook/modeling-data/#setting-up-an-initial-document-structure>
const DOCUMENT_SCHEMA: [u8; 119] = [
133, 111, 74, 131, 14, 200, 8, 95, 0, 109, 1, 16, 163, 64, 79, 49, 42, 30, 77, 109, 146, 45,
91, 5, 214, 2, 217, 205, 1, 252, 203, 208, 39, 6, 89, 188, 223, 101, 41, 50, 160, 144, 47, 147,
187, 74, 77, 252, 185, 64, 18, 211, 205, 23, 118, 97, 221, 216, 176, 1, 239, 6, 1, 2, 3, 2, 19,
2, 35, 2, 64, 2, 86, 2, 7, 21, 5, 33, 2, 35, 2, 52, 1, 66, 2, 86, 2, 128, 1, 2, 127, 0, 127, 1,
127, 1, 127, 0, 127, 0, 127, 7, 127, 3, 100, 111, 99, 127, 0, 127, 1, 1, 127, 4, 127, 0, 127,
0, 0,
];

/// Identifier in automerge document path where we store the text.
const DOCUMENT_OBJ_ID: &str = "doc";

pub struct Document {
doc: RefCell<AutoCommit>,
}

impl Document {
#[allow(dead_code)]
pub fn new() -> Self {
let mut doc = AutoCommit::new();
doc.put_object(automerge::ROOT, DOCUMENT_OBJ_ID, ObjType::Text)
.expect("inserting text object at root");
Self {
doc: RefCell::new(doc),
}
}

pub fn from_bytes(bytes: &[u8]) -> Self {
let doc = AutoCommit::load(bytes).expect("load automerge document from bytes");
Self {
doc: RefCell::new(doc),
}
}

fn text_object(&self) -> ObjId {
let doc = self.doc.borrow();
let (_value, obj_id) = doc
.get(automerge::ROOT, DOCUMENT_OBJ_ID)
.unwrap_or_default()
.expect("text object at root");
obj_id
}

pub fn update(&self, position: i32, del: i32, text: &str) -> Result<()> {
let text_obj = self.text_object();
let mut doc = self.doc.borrow_mut();
doc.splice_text(&text_obj, position as usize, del as isize, text)?;
// Move the diff pointer forward to current position
doc.update_diff_cursor();
Ok(())
}

pub fn load_incremental(&self, bytes: &[u8]) -> Result<()> {
let mut doc = self.doc.borrow_mut();
doc.load_incremental(&bytes)?;
Ok(())
}

pub fn diff_incremental(&self) -> Vec<Patch> {
let mut doc = self.doc.borrow_mut();
doc.diff_incremental()
}

pub fn text(&self) -> String {
let text_obj = self.text_object();
let doc = self.doc.borrow();
doc.text(&text_obj)
.expect("text to be given in automerge document")
}

#[allow(dead_code)]
pub fn save(&self) -> Vec<u8> {
let mut doc = self.doc.borrow_mut();
doc.save()
}

pub fn save_incremental(&self) -> Vec<u8> {
let mut doc = self.doc.borrow_mut();
doc.save_incremental()
}
}

impl Default for Document {
fn default() -> Self {
Self::from_bytes(&DOCUMENT_SCHEMA)
}
}

impl fmt::Debug for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let doc = self.doc.borrow();
let json = serde_json::to_string_pretty(&AutoSerde::from(&*doc))
.expect("serialize automerge document to JSON");
write!(f, "{}", json)
}
}
Loading