Reputation: 3950
I have a simple web app I've built which uses localStorage
to save a set of tasks as stringified JSON. It's also a Chrome extension on the Chrome Web Store, and the code base is exactly the same for both the extension and the site that runs on a VPS at http://supersimpletasks.com.
I'd like to migrate my extension to a Chrome App so I can have access to the chrome.storage.sync
API which would allow task sync across devices for my users. Using chrome.storage would also give me more flexibility later if I wanted to store more than 5mb of data.
However chrome.storage
won't work when my app is served from supersimpletasks.com - I would need to use localStorage instead.
As I understand it, localStorage
is synchronous and chrome.storage
is asynchronous which means quite a lot of rewriting of methods like the ones below. These two methods are responsible for retrieving tasks and saving tasks from localStorage.
@getAllTasks: ->
allTasks = localStorage.getItem(DB.db_key)
allTasks = JSON.parse(allTasks) || Arrays.default_data
allTasks
@setAllTasks: (allTasks) ->
localStorage.setItem(DB.db_key, JSON.stringify(allTasks))
Views.showTasks(allTasks)
How can I structure my application to work with either localStorage or chrome.storage depending on the environment? What problems can I expect to run into?
Upvotes: 4
Views: 1162
Reputation: 3950
This is the code I ended up with which works well for what I want to do.
It's not true asynchronous code, especially with the ChromeStorage.set() method in the Storage API where I'm not using the callback. Ideally you would want to use the callback to do some error handling.
localStorage or chrome.storage
Firstly, the code to determine whether to use localStorage or chrome.storage. The variable is appended to the window so it's available globally.
if !!window.chrome and chrome.storage
window.storageType = ChromeStorage
else
window.storageType = LocalStorage
Storage API
Next, the storage API which makes use of 'Class' in Coffeescript. It's not completely abstracted at the moment. I still have some code in there to handle migrating from localStorage to chrome.storage. The LocalStorage class is faux asynchronous.
class LocalStorage
# Gets a generic value from localStorage given a particular key
# Parses the JSON so it's an object instead of a string
@get: (key, callback) ->
value = localStorage.getItem(key)
value = JSON.parse(value)
callback(value)
# Synchronously gets the stuff from localStorage
@getSync: (key) ->
value = localStorage.getItem(key)
JSON.parse(value)
# Sets something to localStorage given a key and value
@set: (key, value) ->
value = JSON.stringify(value)
localStorage.setItem(key, value)
# Removes something from localStorage given a key
@remove: (key) ->
localStorage.removeItem(key)
class ChromeStorage
# Return all the tasks given the key
# At the moment the key is 'todo' for most calls
@get: (key, callback) ->
chrome.storage.sync.get key, (value) ->
value = value[key] || null || LocalStorage.getSync(key)
callback(value)
# Set all the tasks given the key 'todo' and the thing we're setting
# Usually a JSON array of all the tasks
@set: (key, value, callback) ->
params = {}
params[key] = value
chrome.storage.sync.set params, () ->
# Remove a whole entry from chrome.storage.sync given its key
@remove: (key) ->
chrome.storage.sync.remove key, () ->
# Listen for changes and run Views.showTasks when a change happens
if !!window.chrome and chrome.storage
chrome.storage.onChanged.addListener (changes, namespace) ->
for key of changes
if key == DB.db_key
storageChange = changes[key]
Views.showTasks(storageChange.newValue)
Usage example
Lastly, here's an example of how I'm using the Storage API in my code. This method saves a new task. DB.db_key is a variable that represents the key to use in storage.
# Sets a new task
# Receives name which is in the input
@setNewTask: (name) ->
# Only do this stuff if the input isn't blank
unless name == ''
# Sends the task to @createTask() to make a new task
newTask = @createTask(name)
# Get all the tasks
window.storageType.get DB.db_key, (allTasks) ->
# Adds that new task to the end of the array
allTasks.push newTask
# Save all the tasks
window.storageType.set(DB.db_key, allTasks)
# Show the tasks
Views.showTasks(allTasks)
The GitHub repository is here: https://github.com/humphreybc/super-simple-tasks
Upvotes: 1
Reputation: 34038
The solution to this problem is to create your own storage API. You've identified that localStorage is synchronous while Chrome storage is asynchronous, but this is a problem easily solved by just simply treating everything as if it is asynchronous.
Create your own API, and then use it in place of all of the other calls. A quick find/replace in your code can swap out the localStorage calls with the new API.
function LocalStorageAsync() {
/**
* Parses a boolean from a string, or the boolean if an actual boolean argument is passed in.
*
* @param {String|Boolean} bool A string representation of a boolean value
* @return {Boolean} Returns a boolean value, if the string can be parsed as a bool.
*/
function parseBool(bool) {
if (typeof bool !== 'string' && typeof bool !== 'boolean')
throw new Error('bool is not of type boolean or string');
if (typeof bool == 'boolean') return bool;
return bool === 'true' ? true : false;
}
/**
* store the key value pair and fire the callback function.
*/
this.setItem = function(key, value, callback) {
if(chrome && chrome.storage) {
chrome.storage.local.set({key: key, value: value}, callback);
} else {
var type = typeof value;
var serializedValue = value;
if(type === 'object') {
serializedValue = JSON.stringify(value);
}
value = type + '::typeOf::' + serializedValue;
window.localStorage.setItem(key, value);
callback();
}
}
/**
* Get the item from storage and fire the callback.
*/
this.getItem = function(key, callback) {
if(chrome && chrome.storage) {
chrome.storage.local.get(key, callback);
} else {
var stronglyTypedValue = window.localStorage.getItem(key);
var type = stronglyTypedValue.split('::typeOf::')[0];
var valueAsString = stronglyTypedValue.split('::typeOf::')[1];
var value;
if(type === 'object') {
value = JSON.parse(valueAsString);
} else if(type === 'boolean') {
value = parseBool(valueAsString);
} else if(type === 'number') {
value = parseFloat(valueAsString);
} else if(type === 'string') {
value = valueAsString;
}
callback(value);
}
}
}
// usage example
l = new LocalStorageAsync();
l.setItem('test',[1,2,3], function() {console.log('test');});
l.getItem('test', function(e) { console.log(e);});
The one problem this solution below overcomes, aside from treating everything as asynchronous, is that it also accounts for the fact that localStorage converts everything to a string. By preserving the type information as metadata, we ensure that what comes out of the getItem operation is the same data type as what goes in.
What's more, using a variant of the factory pattern, you can create two concrete inner subclasses and return the appropriate one based on the environment:
function LocalStorageAsync() {
var private = {};
private.LocalStorage = function() {
function parseBool(bool) {
if (typeof bool !== 'string' && typeof bool !== 'boolean')
throw new Error('bool is not of type boolean or string');
if (typeof bool == 'boolean') return bool;
return bool === 'true' ? true : false;
}
this.setItem = function(key, value, callback) { /* localStorage impl... */ };
this.getItem = function(key, callback) { /* ... */ };
};
private.ChromeStorage = function() {
this.setItem = function(key, value, callback) { /* chrome.storage impl... */ };
this.getItem = function(key, callback) { /* ... */ };
}
if(chrome && chrome.storage)
return new private.ChromeStorage();
else
return new private.LocalStorage();
};
Upvotes: 4