Cacheando en cliente con HTML 5 Web Storage API

La semana pasada estuvimos valorando la posibilidad de reducir el número de peticiones a una serie de datos, los cuales era muy poco probable que cambiaran en un espacio corto de tiempo. Al principio, estos datos se solicitaban cada vez que el usuario navegada por las páginas del sitio, lo que provocaba una espera considerable debido a toda la información que teníamos que cruzar en servidor y procesar después. El primer paso fue transformar esta petición a una asíncrona, para que el usuario no sufriera una espera de tiempo considerable. Aún así, el tiempo que tardaba en mostrarse la información era bastante alto, por lo que se decidió cachear esa información en cliente con una serie de habilidades: que sólo fuera válido para la sesión en curso, que cada cierto tiempo pudiera refrescar esos datos y, además, que pudiéramos indicar cada cuánto tiempo era dicha actualización, dependiendo del tipo de datos.

En este post voy a retomar una característica de la que ya hablé tiempo atrás, Web Storage Api, la cual nos permite almacenar información en cliente y con ciertas ventajas respecto a las cookies. Para este escenario he implementado el siguiente código:

var store = function () {

    //private variables
    var storage = null,
    keyName = null,
        EXPIRATION_SUFIX = '-expiration';

    //private functions
    var getExpirationKey = function () {
        return keyName + EXPIRATION_SUFIX;
    },

    set = function (value, expiration) {

        del(keyName);

        storage[keyName] = value;

        if (expiration) storage[getExpirationKey()] = new Date().getTime() + expiration * 60000;

    },

    get = function () {

        if (storage[getExpirationKey(keyName)] !== undefined) {
            var expirationDate = new Date(parseInt(storage[getExpirationKey()]));

            if (expirationDate < new Date()) {
            
            //throw "'" + keyName + "' is obsolete";
            //return null;
            del();
            return "'" + keyName + "' is obsolete";
            }
        }

        return storage[keyName];
    },

    del = function () {
        if (storage[getExpirationKey()] !== undefined) storage.removeItem(getExpirationKey());

        storage.removeItem(keyName);
    };


    var session = function (key) {

keyName = key;
        storage = window.sessionStorage;
        return {
            set: set,
            get: get,
            del: del
        };
    },

    local = function (key) {

keyName = key;
        storage = window.localStorage;
        return {
            set: set,
            get: get,
            del: del
        };

    };

    return {
        session: session,
        local: local
    };

}();

En primer lugar, es necesario saber que Web Storage API tiene dos interfaces a nuestra disposición: localStorage y sessionStorage. La primera de ellas, nos permite almacenar información persistente en cliente y la misma seguirá ahí incluso cuando el navegador haya sido completamente cerrado. La segunda trabaja de la misma forma pero la información sólo estará disponible para esa sesión (tab), donde el usuario está haciendo uso de nuestra aplicación. Si bien en el contexto que exponía anteriormente sólo era necesaria la implementación de sessionStorage, me parecía buena idea que ambas pudieran manejarse de una forma muy similar, gracias a este wrapper.

El código es bastante sencillo y, como podemos ver, normalmente se realizan tres operaciones sobre estas interfaces: recuperar un valor (get), añadir uno nuevo (set) y eliminar los ya existentes (del).

   set = function (value, expiration) {

        del(keyName);

        storage[keyName] = value;

        if (expiration) storage[getExpirationKey()] = new Date().getTime() + expiration * 60000;

    },

    get = function () {

        if (storage[getExpirationKey(keyName)] !== undefined) {
            var expirationDate = new Date(parseInt(storage[getExpirationKey()]));

            if (expirationDate < new Date()) {
            
            //throw "'" + keyName + "' is obsolete";
            //return null;
            del();
            return "'" + keyName + "' is obsolete";
            }
        }

        return storage[keyName];
    },

    del = function () {
        if (storage[getExpirationKey()] !== undefined) storage.removeItem(getExpirationKey());

        storage.removeItem(keyName);
    };

Una de las acciones que se echa en falta a la hora de trabajar con Web Storage es la incapacidad de poder indicar un tiempo de expiración a la información almacenada, por lo que es necesario que seamos nosotros los que manejemos este escenario. En el código anterior vemos que la función set tiene un parámetro llamado expiration, el cual nos permite establecer el número de minutos que queremos que esa clave y ese valor sean válidos para nosotros. Por otro lado, en la parte correspondiente al get, antes de devolver el valor para la clave indicada, comprobamos si hay algún registro de expiración y, de ser así, verificamos si la fecha almacenada es menor que la fecha actual. En tal caso eliminaremos el registro del localStorage/sessionStorage y retornaremos una excepción, un null o un mensaje, con el objetivo de hacer saber que ese valor ha dejado de estar disponible. En el caso de la eliminación de una clave nos aseguramos de que la misma y su fecha de caducidad sea también eliminado.
Este registro de expiración se compone de la misma clave con el sufijo -expiration (podría ser cualquier cosa en realidad). En él guardamos la fecha actual, en el formato que nos proporciona getTime, con la suma de minutos pasados como parámetros al insertar el registro (storage[getExpirationKey()] = new Date().getTime() + expiration * 60000;).

Para que fuera lo más friendly posible, de este objeto store solamente se exponen local y session, funciones que reciben la clave (key) con la que vamos a operar y asignan a la variable privada storage cuál de las dos interfaces vamos a utilizar.

La forma de trabajar con ello es la siguiente:

window.onload = function(){

store.session('sessionKey').set('Hello World from session storage!',1);
store.local('localKey').set('Hello World from local storage!');

document.getElementById('btnGetItem').addEventListener("click",function(){
console.log('SessionKey: ' + store.session('sessionKey').get());
console.log('LocalKey: ' + store.local('localKey').get());
});
};

En el código anterior, vemos que al finalizar la carga de la página se están incluyendo dos claves, cada una en un storage distinto. En el primer caso se le asigna además un tiempo de expiración de un minuto, pero en el segundo se ha decidido no aplicar ningún valor. Por último tenemos un handler de un botón donde consulta ambas claves. La primera expirará pasado un minuto y, por otro lado, la que está almacenada en localStorage seguirá estando disponible.
Cacheando Web Storage Api

Sin embargo, es muy importante recordar que toda la información que se almacena es texto plano, por lo que debemos preocuparnos de la información sensible y cifrarla si fuera necesario.

Espero que sea de utilidad.

¡Saludos!