use std::future::Future;
use std::pin::Pin;
-use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, DbErr, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder};
+use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder, DbErr, ExprTrait};
use tokio::runtime::Runtime;
use std::sync::{Arc};
use std::thread::JoinHandle;
use tokio::sync::{broadcast, mpsc};
use tokio::sync::broadcast::error::SendError;
-use migration::{Migrator, MigratorTrait};
+use migration::{Expr, Migrator, MigratorTrait};
use ui_rs::ui;
use entity::{collection, note, notecontent, profile};
use entity::profile::Entity as Profile;
use entity::collection::{create_notebook_hierarchy, CollectionType, Entity as Collection, Node};
-use entity::note::{Entity as Note};
+use entity::note::{Column, Entity as Note};
use entity::notecontent::{Entity as NoteContent};
+use migration::prelude::Utc;
use crate::lockmanager::LockManager;
pub struct Backend {
}
pub fn save_note<F>(&self, id: NoteId, note: note::ActiveModel, content: Option<notecontent::ActiveModel>, callback: F)
- where F: FnOnce(Result<(note::Model, i32), DbErr>) + Send + 'static {
+ where F: FnOnce(SaveNoteResult) + Send + 'static {
let bhandle = self.clone();
let cmd = Box::pin(async move {
- let result = if note.note_id.is_set() {
- note.update(&bhandle.backend.db).await
+ let result = if let Set(note_id) = note.note_id {
+ let mut update = Note::update_many();
+ if let Set(kind) = note.kind {
+ update = update.col_expr(Column::Kind, Expr::value(kind));
+ }
+ if let Set(title) = note.title {
+ update = update.col_expr(Column::Title, Expr::value(title));
+ }
+
+ let version = if let Set(version) = note.version {
+ version
+ } else {
+ // not setting version when calling save_note is a mistake and will probably
+ // lead to a VersionConflict
+ 0
+ };
+
+ let uresult = update
+ .col_expr(Column::Lastmodified, Expr::value(Utc::now()))
+ .col_expr(Column::Version, Expr::col(Column::Version).add(1))
+ .filter(Column::NoteId.eq(note_id))
+ .filter(Column::Version.eq(version))
+ .exec(&bhandle.backend.db).await;
+
+ match uresult {
+ Ok(updated) => {
+ if updated.rows_affected != 1 {
+ Err(SaveNoteResult::VersionConflict)
+ } else {
+ let result = Note::find_by_id(note_id).one(&bhandle.backend.db).await;
+ match result {
+ Ok(Some(note)) => Ok(note),
+ Ok(None) => Err(SaveNoteResult::Error(DbErr::Custom("note not found".into()))),
+ Err(e) => Err(SaveNoteResult::Error(e)),
+ }
+ }
+ },
+ Err(e) => {
+ Err(SaveNoteResult::Error(e))
+ }
+ }
} else {
- note.insert(&bhandle.backend.db).await
+ let result = note.insert(&bhandle.backend.db).await;
+ match result {
+ Ok(note) => Ok(note),
+ Err(e) => Err(SaveNoteResult::Error(e)),
+ }
};
match result {
Ok(note) => {
let note_id = note.note_id;
- let result: Result<(note::Model, i32), DbErr>;
+ let result: SaveNoteResult;
if let Some(mut content) = content {
let result2 = if content.id.is_set() {
content.update(&bhandle.backend.db).await
match result2 {
Ok(ctn) => {
- result = Ok((note.clone(), ctn.id));
+ result = SaveNoteResult::Ok((note.clone(), ctn.id));
}
Err(e) => {
- result = Err(e);
+ result = SaveNoteResult::Error(e);
}
}
} else {
- result = Ok((note.clone(), 0));
+ result = SaveNoteResult::Ok((note.clone(), 0));
}
if result.is_ok() {
callback(result);
},
Err(e) => {
- callback(Err(e));
+ callback(e);
}
}
});
let _ = self.tx.send(cmd);
}
}
+
+
+pub enum SaveNoteResult {
+ Ok((entity::note::Model, i32)),
+ VersionConflict,
+ Error(DbErr)
+}
+
+impl SaveNoteResult {
+ pub fn is_ok(&self) -> bool {
+ matches!(self, SaveNoteResult::Ok(_))
+ }
+
+ /*
+ pub fn is_err(&self) -> bool {
+ matches!(self, SaveNoteResult::Error(_))
+ }
+ */
+}
\ No newline at end of file
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::sync::Arc;
pub struct LockManager {
- locks: Arc<std::sync::Mutex<HashMap<i32, bool>>>,
+ locks: Arc<std::sync::Mutex<HashSet<i32>>>,
+ revisions: Arc<std::sync::Mutex<HashMap<i32, u64>>>,
}
impl LockManager {
pub fn new() -> Self {
LockManager {
- locks: Arc::new(std::sync::Mutex::new(HashMap::new())),
+ locks: Arc::new(std::sync::Mutex::new(HashSet::new())),
+ revisions: Arc::new(std::sync::Mutex::new(HashMap::new())),
}
}
- fn locks(&self) -> std::sync::MutexGuard<'_, HashMap<i32, bool>> {
+ fn locks(&self) -> std::sync::MutexGuard<'_, HashSet<i32>> {
self.locks.lock().unwrap_or_else(|e| {
// Very unlikely that the mutex gets poisoned (probably only by OOM), and if it does, who cares
- println!("Error: LockManager poisoned");
+ println!("Error: LockManager locks poisoned");
self.locks.clear_poison();
e.into_inner()
})
}
+ fn revisions(&self) -> std::sync::MutexGuard<'_, HashMap<i32, u64>> {
+ self.revisions.lock().unwrap_or_else(|e| {
+ // Very unlikely that the mutex gets poisoned (probably only by OOM), and if it does, who cares
+ println!("Error: LockManager revisions poisoned");
+ self.revisions.clear_poison();
+ e.into_inner()
+ })
+ }
+
pub fn lock_note(&self, note_id: i32) -> Option<NoteLock> {
let mut locks = self.locks();
- if locks.contains_key(¬e_id) {
+ if locks.contains(¬e_id) {
println!("note {} already locked", note_id);
return None
}
println!("lock note {}", note_id);
- locks.insert(note_id, true);
-
+ locks.insert(note_id);
+
let note_lock = NoteLock {
locks: self.locks.clone(),
note_id: note_id
};
Some(note_lock)
}
+
+
}
pub struct NoteLock {
- locks: Arc<std::sync::Mutex<HashMap<i32, bool>>>,
- note_id: i32
+ locks: Arc<std::sync::Mutex<HashSet<i32>>>,
+ pub note_id: i32
}
impl Drop for NoteLock {
let mut locks = self.locks.lock().unwrap_or_else(|e|{ e.into_inner()});
locks.remove(&self.note_id);
}
-}
\ No newline at end of file
+}
+
+pub struct NoteRevision {
+ revisions: Arc<std::sync::Mutex<HashMap<i32, u64>>>,
+ pub note_id: i32,
+ pub revision: u64
+}
use entity::note::NoteType;
use ui_rs::{action, ui_actions, UiModel};
use ui_rs::ui::*;
-use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate};
+use crate::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate, SaveNoteResult};
use crate::lockmanager::NoteLock;
use crate::window::NoteTypeTabView;
pub collection_id: i32,
pub content_id: i32,
+ pub version: i64,
pub kind: NoteType,
id: id,
lock: None,
content_id: 0,
+ version: 0,
kind: NoteType::PlainTextNote,
extract_title: false,
title_start: -1,
pub fn init_from_model(&mut self, model: &entity::note::Model) {
self.id = NoteId::Id(model.note_id);
+ self.version = model.version;
let tab = match model.kind {
NoteType::PlainTextNote => NoteTypeTabView::TextArea,
kind: Set(self.kind.clone()),
title: Set(title),
lastmodified: Set(Utc::now().into()),
- created
+ created: created,
+ version: Set(self.version)
};
let notecontent = entity::notecontent::ActiveModel {
self.backend.save_note(self.id.clone(), note, Some(notecontent), |result|{
proxy.call_mainthread(move |_doc, note|{
match result {
- Ok((notemodel, content_id)) => {
+ SaveNoteResult::Ok((notemodel, content_id)) => {
note.id = NoteId::Id(notemodel.note_id);
note.content_id = content_id;
- println!("Note saved: note_id: {}, content_id: {}", notemodel.note_id, content_id);
+ note.version = notemodel.version;
+ println!("Note saved: note_id: {}, content_id: {}, version: {}", notemodel.note_id, content_id, note.version);
},
- Err(error) => {
- println!("Failed to save note: {}", error);
+ SaveNoteResult::VersionConflict => {
+ println!("Failed to save note: version conflict");
+ },
+ SaveNoteResult::Error(e) => {
+ println!("Failed to save note: {}", e);
}
}
});
pub created: DateTimeWithTimeZone,
#[sea_orm(has_one)]
- pub content: HasOne<crate::notecontent::Entity>
+ pub content: HasOne<crate::notecontent::Entity>,
+
+ pub version: i64
}
#[derive(EnumIter, DeriveActiveEnum, Clone, Debug, PartialEq, Default)]
kind: Default::default(),
title: Default::default(),
lastmodified: Default::default(),
- created: Default::default()
+ created: Default::default(),
+ version: 0
}
}
}
.col(string("title"))
.col(timestamp_with_time_zone("lastmodified"))
.col(timestamp_with_time_zone("created"))
+ .col(big_integer("version"))
.to_owned()
).await?;