Save work periodically to reduce loss from crashes (#1580)
* Add auto saving * Fix autosave dispatching message but not saving document * Clamp set_timeout delay * Auto save all documents instead of only the active * Add with_editor to simplify code * Update consts * Simplify some more * Fix typo * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
f265fa693e
commit
dc7de4d973
|
|
@ -76,3 +76,4 @@ pub const DEFAULT_FONT_STYLE: &str = "Normal (400)";
|
||||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||||
|
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub enum PortfolioMessage {
|
||||||
message: DocumentMessage,
|
message: DocumentMessage,
|
||||||
},
|
},
|
||||||
AutoSaveActiveDocument,
|
AutoSaveActiveDocument,
|
||||||
|
AutoSaveAllDocuments,
|
||||||
AutoSaveDocument {
|
AutoSaveDocument {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,18 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
||||||
if let Some(document_id) = self.active_document_id {
|
if let Some(document_id) = self.active_document_id {
|
||||||
if let Some(document) = self.active_document_mut() {
|
if let Some(document) = self.active_document_mut() {
|
||||||
document.set_auto_save_state(true);
|
document.set_auto_save_state(true);
|
||||||
}
|
|
||||||
responses.add(PortfolioMessage::AutoSaveDocument { document_id });
|
responses.add(PortfolioMessage::AutoSaveDocument { document_id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
PortfolioMessage::AutoSaveAllDocuments => {
|
||||||
|
for (document_id, document) in self.documents.iter_mut() {
|
||||||
|
if !document.is_auto_saved() {
|
||||||
|
document.set_auto_save_state(true);
|
||||||
|
responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
PortfolioMessage::AutoSaveDocument { document_id } => {
|
PortfolioMessage::AutoSaveDocument { document_id } => {
|
||||||
let document = self.documents.get(&document_id).unwrap();
|
let document = self.documents.get(&document_id).unwrap();
|
||||||
responses.add(FrontendMessage::TriggerIndexedDbWriteDocument {
|
responses.add(FrontendMessage::TriggerIndexedDbWriteDocument {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ features = [
|
||||||
"CanvasRenderingContext2d",
|
"CanvasRenderingContext2d",
|
||||||
"Document",
|
"Document",
|
||||||
"HtmlCanvasElement",
|
"HtmlCanvasElement",
|
||||||
|
"IdleRequestOptions"
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.dev]
|
[package.metadata.wasm-pack.profile.dev]
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use serde::Serialize;
|
||||||
use serde_wasm_bindgen::{self, from_value};
|
use serde_wasm_bindgen::{self, from_value};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::time::Duration;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
/// Set the random seed used by the editor by calling this from JS upon initialization.
|
/// Set the random seed used by the editor by calling this from JS upon initialization.
|
||||||
|
|
@ -47,8 +48,27 @@ pub fn wasm_memory() -> JsValue {
|
||||||
wasm_bindgen::memory()
|
wasm_bindgen::memory()
|
||||||
}
|
}
|
||||||
|
|
||||||
// To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell we must make all methods take a non mutable reference to self.
|
/// Helper function for calling JS's `requestAnimationFrame` with the given closure
|
||||||
// Not doing this creates an issue when rust calls into JS which calls back to rust in the same call stack.
|
fn request_animation_frame(f: &Closure<dyn FnMut()>) {
|
||||||
|
web_sys::window()
|
||||||
|
.expect("No global `window` exists")
|
||||||
|
.request_animation_frame(f.as_ref().unchecked_ref())
|
||||||
|
.expect("Failed to call `requestAnimationFrame`");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function for calling JS's `setTimeout` with the given closure and delay
|
||||||
|
fn set_timeout(f: &Closure<dyn FnMut()>, delay: Duration) {
|
||||||
|
let delay = delay.clamp(Duration::ZERO, Duration::from_millis(i32::MAX as u64)).as_millis() as i32;
|
||||||
|
web_sys::window()
|
||||||
|
.expect("No global `window` exists")
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(f.as_ref().unchecked_ref(), delay)
|
||||||
|
.expect("Failed to call `setTimeout`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell we must make all methods take a non-mutable reference to self.
|
||||||
|
/// Not doing this creates an issue when Rust calls into JS which calls back to Rust in the same call stack.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct JsEditorHandle {
|
pub struct JsEditorHandle {
|
||||||
|
|
@ -56,53 +76,68 @@ pub struct JsEditorHandle {
|
||||||
frontend_message_handler_callback: js_sys::Function,
|
frontend_message_handler_callback: js_sys::Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window() -> web_sys::Window {
|
/// Provides access to the `Editor` instance and its `JsEditorHandle` by calling the given closure with them as arguments.
|
||||||
web_sys::window().expect("no global `window` exists")
|
fn call_closure_with_editor_and_handle(mut f: impl FnMut(&mut Editor, &mut JsEditorHandle)) {
|
||||||
|
EDITOR_INSTANCES.with(|instances| {
|
||||||
|
JS_EDITOR_HANDLES.with(|handles| {
|
||||||
|
instances
|
||||||
|
.try_borrow_mut()
|
||||||
|
.map(|mut editors| {
|
||||||
|
for (id, editor) in editors.iter_mut() {
|
||||||
|
let Ok(mut handles) = handles.try_borrow_mut() else {
|
||||||
|
log::error!("Failed to borrow editor handles");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(js_editor) = handles.get_mut(&id) else {
|
||||||
|
log::error!("Editor ID ({id}) has no corresponding JsEditorHandle ID");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the closure with the editor and its handle
|
||||||
|
f(editor, js_editor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| log::error!("Failed to borrow editor instances"));
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_animation_frame(f: &Closure<dyn FnMut()>) {
|
|
||||||
//window().request_idle_callback(f.as_ref().unchecked_ref()).unwrap();
|
|
||||||
window().request_animation_frame(f.as_ref().unchecked_ref()).expect("should register `requestAnimationFrame` OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends a message to the dispatcher in the Editor Backend
|
|
||||||
async fn poll_node_graph_evaluation() {
|
async fn poll_node_graph_evaluation() {
|
||||||
// Process no further messages after a crash to avoid spamming the console
|
// Process no further messages after a crash to avoid spamming the console
|
||||||
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
editor::node_graph_executor::run_node_graph().await;
|
editor::node_graph_executor::run_node_graph().await;
|
||||||
|
|
||||||
// Get the editor instances, dispatch the message, and store the `FrontendMessage` queue response
|
call_closure_with_editor_and_handle(|editor, handle| {
|
||||||
EDITOR_INSTANCES
|
|
||||||
.with(|instances| {
|
|
||||||
JS_EDITOR_HANDLES.with(|handles| {
|
|
||||||
// Mutably borrow the editors, and if successful, we can access them in the closure
|
|
||||||
instances.try_borrow_mut().map(|mut editors| {
|
|
||||||
// Get the editor instance for this editor ID, then dispatch the message to the backend, and return its response `FrontendMessage` queue
|
|
||||||
for (id, editor) in editors.iter_mut() {
|
|
||||||
let handles = handles.borrow_mut();
|
|
||||||
let handle = handles.get(id).unwrap();
|
|
||||||
let mut messages = VecDeque::new();
|
let mut messages = VecDeque::new();
|
||||||
editor.poll_node_graph_evaluation(&mut messages);
|
editor.poll_node_graph_evaluation(&mut messages);
|
||||||
|
|
||||||
// Send each `FrontendMessage` to the JavaScript frontend
|
// Send each `FrontendMessage` to the JavaScript frontend
|
||||||
|
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
|
||||||
let mut responses = Vec::new();
|
|
||||||
for message in messages.into_iter() {
|
|
||||||
responses.extend(editor.handle_message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
for response in responses.into_iter() {
|
|
||||||
handle.send_frontend_message_to_js(response);
|
handle.send_frontend_message_to_js(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
|
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|_| log::error!("Failed to borrow editor instances"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auto_save_all_documents() {
|
||||||
|
// Process no further messages after a crash to avoid spamming the console
|
||||||
|
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
call_closure_with_editor_and_handle(|editor, handle| {
|
||||||
|
for message in editor.handle_message(PortfolioMessage::AutoSaveAllDocuments) {
|
||||||
|
handle.send_frontend_message_to_js(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl JsEditorHandle {
|
impl JsEditorHandle {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
|
|
@ -186,29 +221,47 @@ impl JsEditorHandle {
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = initAfterFrontendReady)]
|
#[wasm_bindgen(js_name = initAfterFrontendReady)]
|
||||||
pub fn init_after_frontend_ready(&self, platform: String) {
|
pub fn init_after_frontend_ready(&self, platform: String) {
|
||||||
|
// Send initialization messages
|
||||||
let platform = match platform.as_str() {
|
let platform = match platform.as_str() {
|
||||||
"Windows" => Platform::Windows,
|
"Windows" => Platform::Windows,
|
||||||
"Mac" => Platform::Mac,
|
"Mac" => Platform::Mac,
|
||||||
"Linux" => Platform::Linux,
|
"Linux" => Platform::Linux,
|
||||||
_ => Platform::Unknown,
|
_ => Platform::Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.dispatch(GlobalsMessage::SetPlatform { platform });
|
self.dispatch(GlobalsMessage::SetPlatform { platform });
|
||||||
self.dispatch(Message::Init);
|
self.dispatch(Message::Init);
|
||||||
|
|
||||||
|
// Poll node graph evaluation on `requestAnimationFrame`
|
||||||
|
{
|
||||||
let f = std::rc::Rc::new(RefCell::new(None));
|
let f = std::rc::Rc::new(RefCell::new(None));
|
||||||
let g = f.clone();
|
let g = f.clone();
|
||||||
|
|
||||||
*g.borrow_mut() = Some(Closure::new(move || {
|
*g.borrow_mut() = Some(Closure::new(move || {
|
||||||
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
||||||
|
|
||||||
// Schedule ourself for another requestAnimationFrame callback.
|
// Schedule ourself for another requestAnimationFrame callback
|
||||||
request_animation_frame(f.borrow().as_ref().unwrap());
|
request_animation_frame(f.borrow().as_ref().unwrap());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
request_animation_frame(g.borrow().as_ref().unwrap());
|
request_animation_frame(g.borrow().as_ref().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto save all documents on `setTimeout`
|
||||||
|
{
|
||||||
|
let f = std::rc::Rc::new(RefCell::new(None));
|
||||||
|
let g = f.clone();
|
||||||
|
|
||||||
|
*g.borrow_mut() = Some(Closure::new(move || {
|
||||||
|
auto_save_all_documents();
|
||||||
|
|
||||||
|
// Schedule ourself for another setTimeout callback
|
||||||
|
set_timeout(f.borrow().as_ref().unwrap(), Duration::from_secs(editor::consts::AUTO_SAVE_TIMEOUT_SECONDS));
|
||||||
|
}));
|
||||||
|
|
||||||
|
set_timeout(g.borrow().as_ref().unwrap(), Duration::from_secs(editor::consts::AUTO_SAVE_TIMEOUT_SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = tauriResponse)]
|
#[wasm_bindgen(js_name = tauriResponse)]
|
||||||
pub fn tauri_response(&self, _message: JsValue) {
|
pub fn tauri_response(&self, _message: JsValue) {
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue