mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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
5 changed files with 110 additions and 46 deletions
|
@ -76,3 +76,4 @@ pub const DEFAULT_FONT_STYLE: &str = "Normal (400)";
|
|||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||
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,
|
||||
},
|
||||
AutoSaveActiveDocument,
|
||||
AutoSaveAllDocuments,
|
||||
AutoSaveDocument {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
|
|
|
@ -76,8 +76,16 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
|
|||
if let Some(document_id) = self.active_document_id {
|
||||
if let Some(document) = self.active_document_mut() {
|
||||
document.set_auto_save_state(true);
|
||||
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 });
|
||||
}
|
||||
responses.add(PortfolioMessage::AutoSaveDocument { document_id });
|
||||
}
|
||||
}
|
||||
PortfolioMessage::AutoSaveDocument { document_id } => {
|
||||
|
|
|
@ -41,6 +41,7 @@ features = [
|
|||
"CanvasRenderingContext2d",
|
||||
"Document",
|
||||
"HtmlCanvasElement",
|
||||
"IdleRequestOptions"
|
||||
]
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev]
|
||||
|
|
|
@ -24,6 +24,7 @@ use serde::Serialize;
|
|||
use serde_wasm_bindgen::{self, from_value};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
// 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.
|
||||
/// Helper function for calling JS's `requestAnimationFrame` with the given closure
|
||||
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]
|
||||
#[derive(Clone)]
|
||||
pub struct JsEditorHandle {
|
||||
|
@ -56,53 +76,68 @@ pub struct JsEditorHandle {
|
|||
frontend_message_handler_callback: js_sys::Function,
|
||||
}
|
||||
|
||||
fn window() -> web_sys::Window {
|
||||
web_sys::window().expect("no global `window` exists")
|
||||
/// Provides access to the `Editor` instance and its `JsEditorHandle` by calling the given closure with them as arguments.
|
||||
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() {
|
||||
// Process no further messages after a crash to avoid spamming the console
|
||||
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor::node_graph_executor::run_node_graph().await;
|
||||
|
||||
// Get the editor instances, dispatch the message, and store the `FrontendMessage` queue response
|
||||
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();
|
||||
editor.poll_node_graph_evaluation(&mut messages);
|
||||
// Send each `FrontendMessage` to the JavaScript frontend
|
||||
call_closure_with_editor_and_handle(|editor, handle| {
|
||||
let mut messages = VecDeque::new();
|
||||
editor.poll_node_graph_evaluation(&mut messages);
|
||||
|
||||
let mut responses = Vec::new();
|
||||
for message in messages.into_iter() {
|
||||
responses.extend(editor.handle_message(message));
|
||||
}
|
||||
// Send each `FrontendMessage` to the JavaScript frontend
|
||||
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
|
||||
handle.send_frontend_message_to_js(response);
|
||||
}
|
||||
|
||||
for response in responses.into_iter() {
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|_| log::error!("Failed to borrow editor instances"));
|
||||
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
impl JsEditorHandle {
|
||||
#[wasm_bindgen(constructor)]
|
||||
|
@ -186,27 +221,45 @@ impl JsEditorHandle {
|
|||
|
||||
#[wasm_bindgen(js_name = initAfterFrontendReady)]
|
||||
pub fn init_after_frontend_ready(&self, platform: String) {
|
||||
// Send initialization messages
|
||||
let platform = match platform.as_str() {
|
||||
"Windows" => Platform::Windows,
|
||||
"Mac" => Platform::Mac,
|
||||
"Linux" => Platform::Linux,
|
||||
_ => Platform::Unknown,
|
||||
};
|
||||
|
||||
self.dispatch(GlobalsMessage::SetPlatform { platform });
|
||||
self.dispatch(Message::Init);
|
||||
|
||||
let f = std::rc::Rc::new(RefCell::new(None));
|
||||
let g = f.clone();
|
||||
// Poll node graph evaluation on `requestAnimationFrame`
|
||||
{
|
||||
let f = std::rc::Rc::new(RefCell::new(None));
|
||||
let g = f.clone();
|
||||
|
||||
*g.borrow_mut() = Some(Closure::new(move || {
|
||||
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
||||
*g.borrow_mut() = Some(Closure::new(move || {
|
||||
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
||||
|
||||
// Schedule ourself for another requestAnimationFrame callback.
|
||||
request_animation_frame(f.borrow().as_ref().unwrap());
|
||||
}));
|
||||
// Schedule ourself for another requestAnimationFrame callback
|
||||
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)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue