-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Session auto-restore #12
base: tokyo-jan-2012
Are you sure you want to change the base?
Changes from all commits
b6be002
523da72
903b3d0
cbc66d5
7a5f4ce
8c939f7
dfa8d87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"testPath": "/static-files/test/" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
/** | ||
* lscache library | ||
* Copyright (c) 2011, Pamela Fox | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
/*jshint undef:true, browser:true */ | ||
|
||
/** | ||
* Creates a namespace for the lscache functions. | ||
*/ | ||
var lscache = (function() { | ||
// Prefix for all lscache keys | ||
var CACHE_PREFIX = 'webxray-lscache-'; | ||
|
||
// Suffix for the key name on the expiration items in localStorage | ||
var CACHE_SUFFIX = '-cacheexpiration'; | ||
|
||
// expiration date radix (set to Base-36 for most space savings) | ||
var EXPIRY_RADIX = 10; | ||
|
||
// time resolution in minutes | ||
var EXPIRY_UNITS = 60 * 1000; | ||
|
||
// ECMAScript max Date (epoch + 1e8 days) | ||
var MAX_DATE = Math.floor(8.64e15/EXPIRY_UNITS); | ||
|
||
var cachedStorage; | ||
var cachedJSON; | ||
|
||
// Determines if localStorage is supported in the browser; | ||
// result is cached for better performance instead of being run each time. | ||
// Feature detection is based on how Modernizr does it; | ||
// it's not straightforward due to FF4 issues. | ||
// It's not run at parse-time as it takes 200ms in Android. | ||
function supportsStorage() { | ||
var key = '__lscachetest__'; | ||
var value = key; | ||
|
||
if (cachedStorage !== undefined) { | ||
return cachedStorage; | ||
} | ||
|
||
try { | ||
setItem(key, value); | ||
removeItem(key); | ||
cachedStorage = true; | ||
} catch (exc) { | ||
cachedStorage = false; | ||
} | ||
return cachedStorage; | ||
} | ||
|
||
// Determines if native JSON (de-)serialization is supported in the browser. | ||
function supportsJSON() { | ||
/*jshint eqnull:true */ | ||
if (cachedJSON === undefined) { | ||
cachedJSON = (window.JSON != null); | ||
} | ||
return cachedJSON; | ||
} | ||
|
||
/** | ||
* Returns the full string for the localStorage expiration item. | ||
* @param {String} key | ||
* @return {string} | ||
*/ | ||
function expirationKey(key) { | ||
return key + CACHE_SUFFIX; | ||
} | ||
|
||
/** | ||
* Returns the number of minutes since the epoch. | ||
* @return {number} | ||
*/ | ||
function currentTime() { | ||
return Math.floor((new Date().getTime())/EXPIRY_UNITS); | ||
} | ||
|
||
/** | ||
* Wrapper functions for localStorage methods | ||
*/ | ||
|
||
function getItem(key) { | ||
return localStorage.getItem(CACHE_PREFIX + key); | ||
} | ||
|
||
function setItem(key, value) { | ||
// Fix for iPad issue - sometimes throws QUOTA_EXCEEDED_ERR on setItem. | ||
localStorage.removeItem(CACHE_PREFIX + key); | ||
localStorage.setItem(CACHE_PREFIX + key, value); | ||
} | ||
|
||
function removeItem(key) { | ||
localStorage.removeItem(CACHE_PREFIX + key); | ||
} | ||
|
||
return { | ||
|
||
/** | ||
* Stores the value in localStorage. Expires after specified number of minutes. | ||
* @param {string} key | ||
* @param {Object|string} value | ||
* @param {number} time | ||
*/ | ||
set: function(key, value, time) { | ||
if (!supportsStorage()) return; | ||
|
||
// If we don't get a string value, try to stringify | ||
// In future, localStorage may properly support storing non-strings | ||
// and this can be removed. | ||
if (typeof value !== 'string') { | ||
if (!supportsJSON()) return; | ||
try { | ||
value = JSON.stringify(value); | ||
} catch (e) { | ||
// Sometimes we can't stringify due to circular refs | ||
// in complex objects, so we won't bother storing then. | ||
return; | ||
} | ||
} | ||
|
||
try { | ||
setItem(key, value); | ||
} catch (e) { | ||
if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { | ||
// If we exceeded the quota, then we will sort | ||
// by the expire time, and then remove the N oldest | ||
var storedKeys = []; | ||
var storedKey; | ||
for (var i = 0; i < localStorage.length; i++) { | ||
storedKey = localStorage.key(i); | ||
|
||
if (storedKey.indexOf(CACHE_PREFIX) === 0 && storedKey.indexOf(CACHE_SUFFIX) < 0) { | ||
var mainKey = storedKey.substr(CACHE_PREFIX.length); | ||
var exprKey = expirationKey(mainKey); | ||
var expiration = getItem(exprKey); | ||
if (expiration) { | ||
expiration = parseInt(expiration, EXPIRY_RADIX); | ||
} else { | ||
// TODO: Store date added for non-expiring items for smarter removal | ||
expiration = MAX_DATE; | ||
} | ||
storedKeys.push({ | ||
key: mainKey, | ||
size: (getItem(mainKey)||'').length, | ||
expiration: expiration | ||
}); | ||
} | ||
} | ||
// Sorts the keys with oldest expiration time last | ||
storedKeys.sort(function(a, b) { return (b.expiration-a.expiration); }); | ||
|
||
var targetSize = (value||'').length; | ||
while (storedKeys.length && targetSize > 0) { | ||
storedKey = storedKeys.pop(); | ||
removeItem(storedKey.key); | ||
removeItem(expirationKey(storedKey.key)); | ||
targetSize -= storedKey.size; | ||
} | ||
try { | ||
setItem(key, value); | ||
} catch (e) { | ||
// value may be larger than total quota | ||
return; | ||
} | ||
} else { | ||
// If it was some other error, just give up. | ||
return; | ||
} | ||
} | ||
|
||
// If a time is specified, store expiration info in localStorage | ||
if (time) { | ||
setItem(expirationKey(key), (currentTime() + time).toString(EXPIRY_RADIX)); | ||
} else { | ||
// In case they previously set a time, remove that info from localStorage. | ||
removeItem(expirationKey(key)); | ||
} | ||
}, | ||
|
||
/** | ||
* Retrieves specified value from localStorage, if not expired. | ||
* @param {string} key | ||
* @return {string|Object} | ||
*/ | ||
get: function(key) { | ||
if (!supportsStorage()) return null; | ||
|
||
// Return the de-serialized item if not expired | ||
var exprKey = expirationKey(key); | ||
var expr = getItem(exprKey); | ||
|
||
if (expr) { | ||
var expirationTime = parseInt(expr, EXPIRY_RADIX); | ||
|
||
// Check if we should actually kick item out of storage | ||
if (currentTime() >= expirationTime) { | ||
removeItem(key); | ||
removeItem(exprKey); | ||
return null; | ||
} | ||
} | ||
|
||
// Tries to de-serialize stored value if its an object, and returns the normal value otherwise. | ||
var value = getItem(key); | ||
if (!value || !supportsJSON()) { | ||
return value; | ||
} | ||
|
||
try { | ||
// We can't tell if its JSON or a string, so we try to parse | ||
return JSON.parse(value); | ||
} catch (e) { | ||
// If we can't parse, it's probably because it isn't an object | ||
return value; | ||
} | ||
}, | ||
|
||
/** | ||
* Removes a value from localStorage. | ||
* Equivalent to 'delete' in memcache, but that's a keyword in JS. | ||
* @param {string} key | ||
*/ | ||
remove: function(key) { | ||
if (!supportsStorage()) return null; | ||
removeItem(key); | ||
removeItem(expirationKey(key)); | ||
}, | ||
|
||
/** | ||
* Returns whether local storage is supported. | ||
* Currently exposed for testing purposes. | ||
* @return {boolean} | ||
*/ | ||
supported: function() { | ||
return supportsStorage(); | ||
}, | ||
|
||
/** | ||
* Flushes all lscache items and expiry markers without affecting rest of localStorage | ||
*/ | ||
flush: function() { | ||
if (!supportsStorage()) return; | ||
|
||
// Loop in reverse as removing items will change indices of tail | ||
for (var i = localStorage.length-1; i >= 0 ; --i) { | ||
var key = localStorage.key(i); | ||
if (key.indexOf(CACHE_PREFIX) === 0) { | ||
localStorage.removeItem(key); | ||
} | ||
} | ||
} | ||
}; | ||
})(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,17 @@ | |
// If the user has made changes to the page, we don't want them | ||
// to be able to navigate away from it without facing a modal | ||
// dialog. | ||
function ModalUnloadBlocker(commandManager) { | ||
function ModalUnloadBlocker(commandManager, cb) { | ||
function beforeUnload(event) { | ||
if (commandManager.canUndo()) { | ||
event.preventDefault(); | ||
return jQuery.locale.get("input:unload-blocked"); | ||
cb(); | ||
|
||
// Since we are saving the user's work before they leave and | ||
// auto-restoring it if they come back, don't bother them | ||
// with a modal dialog. | ||
|
||
//event.preventDefault(); | ||
//return jQuery.locale.get("input:unload-blocked"); | ||
} | ||
} | ||
|
||
|
@@ -67,8 +73,22 @@ | |
}); | ||
var touchToolbar = canBeTouched() ? jQuery.touchToolbar(input) : null; | ||
var indicator = jQuery.blurIndicator(input, window); | ||
var modalUnloadBlocker = ModalUnloadBlocker(commandManager); | ||
|
||
var modalUnloadBlocker = ModalUnloadBlocker(commandManager, | ||
saveRecording); | ||
var RECORDING_KEY = "recording-" + window.location.href; | ||
|
||
function saveRecording() { | ||
// Store emergency rescue data for 5 minutes. | ||
var RECORDING_PERSIST_TIME = 5 * 60; | ||
|
||
if (commandManager.canUndo()) { | ||
var recording = commandManager.getRecording(); | ||
lscache.set(RECORDING_KEY, JSON.parse(recording), | ||
RECORDING_PERSIST_TIME); | ||
} else | ||
lscache.remove(RECORDING_KEY); | ||
} | ||
|
||
var self = jQuery.eventEmitter({ | ||
persistence: persistence, | ||
start: function() { | ||
|
@@ -78,10 +98,25 @@ | |
focused.on('change', hud.onFocusChange); | ||
input.activate(); | ||
$(window).focus(); | ||
if (!commandManager.canUndo()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is commandManager.canUndo() basically idiomatic for detecting a new goggles session? It might be nice to give it a more clearly named helper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is a bit funky, because there's already a pre-existing behavior in the goggles (around line 95) where we call So, given that context, the call to
and then change line 101 to be:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Err... actually, now that I think about it, there are probably some cases where we might want to both load the history from the DOM and load the cached history, e.g. when a user is building on an existing published hack. Hmm, might have to noodle on this a bit more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, ok, seems more complicated than I thought. I didn't realize there were two different ways to store history. I wouldn't necessarily hold up the pull for this, but it might merit a TODO or something. |
||
// See if we can emergency-restore the user's previous session. | ||
var recording = lscache.get(RECORDING_KEY); | ||
if (recording) | ||
try { | ||
commandManager.playRecording(JSON.stringify(recording)); | ||
} catch (e) { | ||
// Corrupt recording, or page has changed in a way | ||
// that we can't replay the recording, so get rid of it. | ||
lscache.remove(RECORDING_KEY); | ||
if (window.console && window.console.error) | ||
console.error(e); | ||
} | ||
} | ||
}, | ||
unload: function() { | ||
if (!isUnloaded) { | ||
isUnloaded = true; | ||
saveRecording(); | ||
focused.destroy(); | ||
focused = null; | ||
input.deactivate(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this comment should just replace the comment on line 12. Otherwise they contradict each other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, good point. Will do that... and actually, we should probably rename this class to something more descriptive than
ModalUnloadBlocker
, since that's not at all what it's doing anymore. Maybe something likeBeforeUnloadManager
or something.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, works for me.