From 81bc280aba9696bd1d1d0fcf33ea541a512e3525 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 10 Oct 2013 14:59:48 -0700 Subject: [PATCH] More reliable method for mutexed localStorage saves Fixes #1883 --- Makefile | 1 + index.html | 3 ++ js/id/core/history.js | 26 +++++---- js/id/ui.js | 4 ++ js/id/util/session_mutex.js | 36 +++++++++++++ test/index.html | 4 ++ test/index_packaged.html | 2 + test/spec/core/history.js | 46 ---------------- test/spec/util/session_mutex.js | 94 +++++++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+), 60 deletions(-) create mode 100644 js/id/util/session_mutex.js create mode 100644 test/spec/util/session_mutex.js diff --git a/Makefile b/Makefile index 4180af5a7..e4f23e97f 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ dist/iD.js: \ js/id/id.js \ js/id/services/*.js \ js/id/util.js \ + js/id/util/*.js \ js/id/geo.js \ js/id/geo/*.js \ js/id/actions.js \ diff --git a/index.html b/index.html index 3b387de5e..f36b434f7 100644 --- a/index.html +++ b/index.html @@ -34,7 +34,10 @@ + + + diff --git a/js/id/core/history.js b/js/id/core/history.js index d99cdf014..98f27a6d7 100644 --- a/js/id/core/history.js +++ b/js/id/core/history.js @@ -2,7 +2,7 @@ iD.History = function(context) { var stack, index, tree, imageryUsed = ['Bing'], dispatch = d3.dispatch('change', 'undone', 'redone'), - lock = false; + lock = iD.util.SessionMutex('lock'); function perform(actions) { actions = Array.prototype.slice.call(actions); @@ -273,39 +273,37 @@ iD.History = function(context) { }, save: function() { - if (!lock) return history; - context.storage(getKey('lock'), null); - context.storage(getKey('saved_history'), this.toJSON() || null); + if (lock.locked()) context.storage(getKey('saved_history'), history.toJSON() || null); return history; }, clearSaved: function() { - if (!lock) return; - context.storage(getKey('saved_history'), null); + if (lock.locked()) context.storage(getKey('saved_history'), null); + return history; }, lock: function() { - if (context.storage(getKey('lock'))) return false; - context.storage(getKey('lock'), true); - lock = true; - return lock; + return lock.lock(); + }, + + unlock: function() { + lock.unlock(); }, // is iD not open in another window and it detects that // there's a history stored in localStorage that's recoverable? restorableChanges: function() { - return lock && !!context.storage(getKey('saved_history')); + return lock.locked() && !!context.storage(getKey('saved_history')); }, // load history from a version stored in localStorage restore: function() { - if (!lock) return; + if (!lock.locked()) return; var json = context.storage(getKey('saved_history')); - if (json) this.fromJSON(json); + if (json) history.fromJSON(json); context.storage(getKey('saved_history', null)); - }, _getKey: getKey diff --git a/js/id/ui.js b/js/id/ui.js index eb48b6c8b..7b7f01714 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -126,6 +126,10 @@ iD.ui = function(context) { return context.save(); }; + window.onunload = function() { + context.history().unlock(); + }; + d3.select(window).on('resize.editor', function() { map.dimensions(m.dimensions()); }); diff --git a/js/id/util/session_mutex.js b/js/id/util/session_mutex.js new file mode 100644 index 000000000..2554b0114 --- /dev/null +++ b/js/id/util/session_mutex.js @@ -0,0 +1,36 @@ +// A per-domain session mutex backed by a cookie and dead man's +// switch. If the session crashes, the mutex will auto-release +// after 5 seconds. + +iD.util.SessionMutex = function(name) { + var mutex = {}, + intervalID; + + function renew() { + var expires = new Date(); + expires.setSeconds(expires.getSeconds() + 5); + document.cookie = name + '=1; expires=' + expires.toUTCString(); + } + + mutex.lock = function() { + if (intervalID) return true; + var cookie = document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1"); + if (cookie) return false; + renew(); + intervalID = window.setInterval(renew, 4000); + return true; + }; + + mutex.unlock = function() { + if (!intervalID) return; + document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + clearInterval(intervalID); + intervalID = null; + }; + + mutex.locked = function() { + return !!intervalID; + }; + + return mutex; +}; diff --git a/test/index.html b/test/index.html index 4632e1760..0dda115a0 100644 --- a/test/index.html +++ b/test/index.html @@ -189,6 +189,8 @@ + + @@ -265,7 +267,9 @@ + + diff --git a/test/index_packaged.html b/test/index_packaged.html index 9fe8137b2..5ecd80579 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -87,7 +87,9 @@ + + diff --git a/test/spec/core/history.js b/test/spec/core/history.js index 115ef2bdd..ad68efff4 100644 --- a/test/spec/core/history.js +++ b/test/spec/core/history.js @@ -244,52 +244,6 @@ describe("iD.History", function () { }); }); - describe("#lock", function() { - it("acquires lock if possible", function() { - expect(history.lock()).to.be.true; - expect(history.lock()).to.be.false; - }); - }); - - describe("#save", function() { - it("doesn't do anything if it doesn't have the lock", function() { - var key = history._getKey('saved_history'); - context.storage(key, null); - history.save(); - expect(context.storage(key)).to.be.null; - context.storage(key, 'something'); - expect(context.storage(key)).to.equal('something'); - history.save(); - context.storage(key, null); - }); - - it("saves to localStorage", function() { - var node = iD.Node({ id: 'n' }); - history.lock(); - history.perform(iD.actions.AddEntity(node)); - history.save(); - var saved = JSON.parse(context.storage(history._getKey('saved_history'))); - expect(saved.stack[1].modified[0]).to.eql('nv0'); - }); - }); - - describe("#restore", function() { - it("saves and restores a created and deleted entities", function() { - var node = iD.Node({ id: 'n' }), - node2 = iD.Node({ id: 'n2' }); - history.lock(); - history.perform(iD.actions.AddEntity(node)); - history.perform(iD.actions.AddEntity(node2)); - history.perform(iD.actions.DeleteNode('n2')); - history.save(); - history.reset(); - expect(history.graph().hasEntity('n')).to.be.undefined - history.restore(); - expect(history.graph().entity('n').id).to.equal('n'); - expect(history.graph().hasEntity('n2')).to.be.undefined; - }); - }); - describe("#toJSON", function() { it("generates v2 JSON", function() { var node = iD.Node({id: 'n-1'}); diff --git a/test/spec/util/session_mutex.js b/test/spec/util/session_mutex.js new file mode 100644 index 000000000..ee9aa3bea --- /dev/null +++ b/test/spec/util/session_mutex.js @@ -0,0 +1,94 @@ +describe("iD.util.SessionMutex", function() { + var clock, a, b; + + beforeEach(function () { + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function() { + clock.restore(); + if (a) a.unlock(); + if (b) b.unlock(); + }); + + describe("#lock", function() { + it("returns true when it gets a lock", function() { + a = iD.util.SessionMutex('name'); + expect(a.lock()).to.equal(true); + }); + + it("returns true when already locked", function() { + a = iD.util.SessionMutex('name'); + a.lock(); + expect(a.lock()).to.equal(true); + }); + + it("returns false when the lock is held by another session", function() { + a = iD.util.SessionMutex('name'); + a.lock(); + + b = iD.util.SessionMutex('name'); + expect(b.lock()).to.equal(false); + }); + }); + + describe("#locked", function() { + it("returns false by default", function() { + a = iD.util.SessionMutex('name'); + expect(a.locked()).to.equal(false); + }); + + it("returns true when locked", function() { + a = iD.util.SessionMutex('name'); + a.lock(); + expect(a.locked()).to.equal(true); + }); + + it("returns false when unlocked", function() { + a = iD.util.SessionMutex('name'); + a.lock(); + a.unlock(); + expect(a.locked()).to.equal(false); + }); + }); + + describe("#unlock", function() { + it("unlocks the mutex", function() { + a = iD.util.SessionMutex('name'); + a.lock(); + a.unlock(); + + b = iD.util.SessionMutex('name'); + expect(b.lock()).to.equal(true); + }); + + it("does nothing when the lock is held by another session", function() { + a = iD.util.SessionMutex('name'); + a.lock(); + + b = iD.util.SessionMutex('name'); + b.unlock(); + + expect(a.locked()).to.equal(true); + }); + + it("does nothing when not locked", function() { + a = iD.util.SessionMutex('name'); + a.unlock(); + expect(a.locked()).to.equal(false); + }); + }); + + it("namespaces locks", function() { + a = iD.util.SessionMutex('a'); + a.lock(); + + b = iD.util.SessionMutex('b'); + expect(b.locked()).to.equal(false); + expect(b.lock()).to.equal(true); + }); + + it("automatically unlocks when a session crashes", function() { + // Tested manually. + }); +});