Benjamin Humphrey Benjamin Humphrey - 27 days ago 6
Javascript Question

How can I structure my app to use localStorage when running on a website, and chrome.storage when running as a Chrome App?

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?

Answer

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