More reliable method for mutexed localStorage saves

Fixes #1883
This commit is contained in:
John Firebaugh
2013-10-10 14:59:48 -07:00
parent 7b09a97f6c
commit 81bc280aba
9 changed files with 156 additions and 60 deletions

View File

@@ -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 \

View File

@@ -34,7 +34,10 @@
<script src='js/lib/marked.js'></script>
<script src='js/id/id.js'></script>
<script src='js/id/util.js'></script>
<script src='js/id/util/session_mutex.js'></script>
<script src='js/id/services/taginfo.js'></script>
<script src='js/id/services/wikipedia.js'></script>

View File

@@ -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

View File

@@ -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());
});

View File

@@ -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;
};

View File

@@ -189,6 +189,8 @@
<script src='../js/id/presets/collection.js'></script>
<script src='../js/id/presets/field.js'></script>
<script src='../js/id/util/session_mutex.js'></script>
<script src='../js/id/validate.js'></script>
<script src='../js/lib/locale.js'></script>
@@ -265,7 +267,9 @@
<script src="spec/geo.js"></script>
<script src="spec/taginfo.js"></script>
<script src="spec/util.js"></script>
<script src='spec/util/session_mutex.js'></script>
<script src="spec/behavior/hash.js"></script>
<script src="spec/behavior/hover.js"></script>

View File

@@ -87,7 +87,9 @@
<script src="spec/geo.js"></script>
<script src="spec/taginfo.js"></script>
<script src="spec/util.js"></script>
<script src='spec/util/session_mutex.js'></script>
<script src="spec/behavior/hash.js"></script>
<script src="spec/behavior/hover.js"></script>

View File

@@ -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'});

View File

@@ -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.
});
});