From c5a76d4a7d9b7de9c78a534091a1b775bc5e9c9a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Apr 2016 14:41:34 +0200 Subject: [PATCH] Persist the entire state on reload This fixes an annoying problem that prevented the state of marker layers from being saved when the window was reloaded either via `Cmd+R` in DevTools or via `Ctrl+Option+Cmd+L` in Atom. The issue was that we were *always* scheduling `saveState` on an idle callback: `window.onbeforeunload`, however, doesn't wait for that event before closing the window, and thus that state was never saved in those situations. The solution is to use idle callbacks only during the critical code path (i.e. on mousedown and keydown), but save it synchronously otherwise. Saving something to IndexedDB is actually asynchronous too, but it seems like Chrome fulfills `put` requests that get executed right during `onbeforeunload`. --- spec/atom-environment-spec.coffee | 29 ++++++++++++++++++++++++----- src/atom-environment.coffee | 31 ++++++++++++++++--------------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 846083b0e..a78190ab7 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -208,12 +208,15 @@ describe "AtomEnvironment", -> waitsForPromise -> atom.loadState().then (state) -> expect(state).toEqual(serializedState) - it "saves state on keydown, mousedown, and when the editor window unloads", -> + it "saves state when the CPU is idle after a keydown or mousedown event", -> spyOn(atom, 'saveState') + idleCallbacks = [] + spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback) keydown = new KeyboardEvent('keydown') atom.document.dispatchEvent(keydown) advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false}) expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true}) @@ -221,17 +224,33 @@ describe "AtomEnvironment", -> mousedown = new MouseEvent('mousedown') atom.document.dispatchEvent(mousedown) advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false}) expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true}) - atom.saveState.reset() + it "saves state immediately when unloading the editor window, ignoring pending and successive mousedown/keydown events", -> + spyOn(atom, 'saveState') + idleCallbacks = [] + spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback) + + mousedown = new MouseEvent('mousedown') + atom.document.dispatchEvent(mousedown) atom.unloadEditorWindow() - mousedown = new MouseEvent('mousedown') - atom.document.dispatchEvent(mousedown) - advanceClock atom.saveStateDebounceInterval expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true}) expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false}) + atom.saveState.reset() + advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() + expect(atom.saveState).not.toHaveBeenCalled() + + atom.saveState.reset() + mousedown = new MouseEvent('mousedown') + atom.document.dispatchEvent(mousedown) + advanceClock atom.saveStateDebounceInterval + idleCallbacks.shift()() + expect(atom.saveState).not.toHaveBeenCalled() + it "serializes the project state with all the options supplied in saveState", -> spyOn(atom.project, 'serialize').andReturn({foo: 42}) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index f50ae9d5b..da3e989f2 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -234,13 +234,14 @@ class AtomEnvironment extends Model checkPortableHomeWritable() attachSaveStateListeners: -> - saveState = => @saveState({isUnloading: false}) unless @unloaded - debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval) - @document.addEventListener('mousedown', debouncedSaveState, true) - @document.addEventListener('keydown', debouncedSaveState, true) + saveState = _.debounce((=> + window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded + ), @saveStateDebounceInterval) + @document.addEventListener('mousedown', saveState, true) + @document.addEventListener('keydown', saveState, true) @disposables.add new Disposable => - @document.removeEventListener('mousedown', debouncedSaveState, true) - @document.removeEventListener('keydown', debouncedSaveState, true) + @document.removeEventListener('mousedown', saveState, true) + @document.removeEventListener('keydown', saveState, true) setConfigSchema: -> @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} @@ -655,6 +656,7 @@ class AtomEnvironment extends Model # Call this method when establishing a real application window. startEditorWindow: -> + @unloaded = false @loadState().then (state) => @windowDimensions = state?.windowDimensions @displayWindow().then => @@ -842,16 +844,15 @@ class AtomEnvironment extends Model return Promise.resolve() unless @enablePersistence new Promise (resolve, reject) => - window.requestIdleCallback => - return if not @project + return if not @project - state = @serialize(options) - savePromise = - if storageKey = @getStateKey(@project?.getPaths()) - @stateStore.save(storageKey, state) - else - @applicationDelegate.setTemporaryWindowState(state) - savePromise.catch(reject).then(resolve) + state = @serialize(options) + savePromise = + if storageKey = @getStateKey(@project?.getPaths()) + @stateStore.save(storageKey, state) + else + @applicationDelegate.setTemporaryWindowState(state) + savePromise.catch(reject).then(resolve) loadState: -> if @enablePersistence