UBCache.js

var Q = require('./libs/q');
// Originally found on  from https://github.com/mozilla/localForage
var dbInfo = {
    name: 'UB',
    stores: ['permanent','session', 'userData'],
    version: 1
};

// Initialize IndexedDB; fall back to vendor-prefixed versions if needed.
//noinspection JSUnresolvedVariable
var iDB = window.indexedDB || window.webkitIndexedDB ||
    window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB;


/**
 * @classdesc
 * Client side cache. Wrapper around <a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB">indexedDB</a>
 *
 * Contain functions for simple key/value manipulations and advanced (entity related) manipulations.
 *
 * Create separate <a href="https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase">database</a>
 * for each connection inside application.
 *
 * For every database will create three <a href ="https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase.createObjectStore">store</a>
 *
 *  - **permanent** for data persistent between sessions and
 *  - **session** for data, live inside user session only (from login to login)
 *  - **user** for custom data
 *
 * Usage sample

        var c = new UBCache('mydb');
        c.put([
            {key: 'note1', value: {prop1: 1, prop2: 'do something'} },
            {key: 'note2', value: 'do something else'}
        ]).done();
        c.get('note1').done(UB.logDebug); //output result to console
        c.clear().done();
        c.get('note1').done(function(value){
            console.log(value === undefined ? 'all cleared': 'hm... something wrong')
        });

 * @class UBCache
 * @author pavel.mash on 17.04.2014.
 * @param {String} dbName Name of indexedDB database we create. Usually this is {@link UBConnection#baseURL}. Constructor lower case dbName during creation
 * @param {Number} [version] Optionally database version.
 */
function UBCache(dbName, version) {
    var
        cache = Object.create(UBCache.prototype),
        openRequest,
        deferred = Q.defer(),
        _dbPromise = deferred.promise;
    /** indexedDB name
     * @property {String} dbName
     * @readonly
     */
    cache.dbName = dbName.toLowerCase();
    /**
     * Must be call before access to UBCache methods
     * @method
     * @private
     * @returns {Promise} resolved to IDBDatabase
     */
    cache.ready = function(){
        return _dbPromise;
    };

    if(!iDB){
        deferred.reject({errMsg: 'unsupportedBrowser', errDetails: 'indexedDB not found'});
    }
    openRequest = iDB.open(cache.dbName, version || 1);
    openRequest.onerror = function withStoreOnError(e) {
        deferred.reject(e); // openRequest.error.name
    };
    openRequest.onblocked = function(){
        deferred.reject({errMsg: 'databaseIsBlocked', errDetails: 'we need to upgrade database, but some other browser tab also open it'});
    };
    openRequest.onupgradeneeded = function withStoreOnUpgradeNeeded(e) {
        // First time setup: create an empty object stores
        var db = e.target.result, tx = e.target.transaction;
        //noinspection JSUnresolvedVariable
        console.log('upgrading database "' + db.name + '" from version ' + e.oldVersion+
            ' to version ' + e.newVersion + '...');
        dbInfo.stores.forEach(function(storeName){
            //noinspection JSUnresolvedVariable
            if (!db.objectStoreNames.contains(storeName)) {
                db.createObjectStore(storeName);
            } else {
                //noinspection JSUnresolvedFunction
                tx.objectStore(storeName).clear();
            }
        });
    };
    openRequest.onsuccess = function withStoreOnSuccess() {
        var db = openRequest.result;
        deferred.resolve(db);
    };

    return cache;
}

/**
 * SESSION store name
 * @readonly
 * @type {String}
 */
UBCache.SESSION = 'session';
/**
 * PERMANENT store name
 * @readonly
 * @type {String}
 */
UBCache.PERMANENT = 'permanent';

/**
 * Possible cache types for businnes logic data
 * @readonly
 * @enum
 */
UBCache.cacheTypes = {
    /**
     * Кэширование не осуществляется. Запрос на сервер отправляется всегда.
     *
     * @type String
     */
    None: "None",

    /**
     * Кэширование осуществляется на уровне сущности. Запрос на сервер отправляется всегда. При этом в запрос добавляется версия закэшированных данных, если таковые имеются.
     * Результат запроса содержит
     * или данные и версию данных, которые помещаются в кэш;
     * или флаг notModified. В этом случае данные считываются из кэша.
     *
     * Если в запросе в whereList присутствует ID - кэширование не осуществляется. Запрос на сервер отправляется всегда.
     *
     * @type String
     */
        Entity: "Entity",

    /**
     * Кэширование осуществляется на уровне сессии. Запрос на сервер отправляется только один раз при старте сессии. При старте сессии все закэшированные сущности удаляются из кэша.
     *
     * Если в запросе в whereList присутствует ID - кэширование не осуществляется. Запрос на сервер отправляется всегда.
     *
     * @type String
     */
        Session: "Session",

    /**
     * Кеширование осуществляется на уровне сессии и сущности. Запрос на сервер отправляетсятолько один раз при старте сессии. При этом в запрос добавляется версия закэшированных данных, если таковые имеются.
     * Результат запроса содержит
     * или данные и версию данных, которые помещаются в кэш;
     * или флаг notModified. В этом случае данные считываются из кэша.
     *
     * Если в запросе в whereList присутствует ID - кэширование не осуществляется. Запрос на сервер отправляется всегда.
     *
     * @type String
     */
        SessionEntity: "SessionEntity"
};

/**
 * Predefined callback functions, called when indexedDB transaction complete.
 * Can be customized after UBCache is created.
 * Default implementation will do nothing
 * @type {function(e)}
 */
UBCache.prototype.onTransactionComplete = function(e) {
    // if (e.target.mode !== 'readonly'){
    //   UB.logDebug('IDB ' + e.target.mode + ' transaction complete');
    // }
};

/**
 * Predefined callback functions, called when indexedDB transaction aborted.
 * Can be customized after UBCache is created.
 * Default implementation will put error to log
 * @type {function(e)}
 */
UBCache.prototype.onTransactionAbort = function(e) {
    if (window.console){
        window.console.error('IDB transaction aborted: '+ (e.target.error.message || e.target.errorCode))
    }
};
/**
 * Predefined callback functions, called when error occurred during indexedDB transaction.
 * Can be customized after UBCache is created.
 * Default implementation will put error to log
 * @type {function(e)}
 */
UBCache.prototype.onTransactionError = function(e) {
    if (window.console){
        window.console.error('IDB transaction failed: ' + e.target.errorCode)
    }
};
/**
 * Start transaction (with specified mode) on specified store and resolve to  IDBObjectStore
 * @method
 * @private
 * @param {String} storeName
 * @param {Boolean} [forUpdate] if true - open store for update, else(default) is 'readonly'.
 * @returns {Promise} resolved to IDBObjectStore
 */
UBCache.prototype.openStore = function(storeName, forUpdate){
    var me = this;
    storeName = storeName || 'userData';
    return me.ready().then(function(db){
        var transaction = db.transaction([storeName], forUpdate ? 'readwrite' : 'readonly');
        transaction.oncomplete = me.onTransactionComplete;
        transaction.onabort = me.onTransactionAbort;
        transaction.onerror = me.onTransactionError;
        //noinspection JSUnresolvedFunction
        return transaction.objectStore(storeName);
    });
};
/**
 * Retrieve data from store by key. If key not found - resolve result to `undefined`
 * @method
 * @param {String} key
 * @param {String} [storeName] default to 'userData'
 * @returns {Promise} resolved to key value.
 */
UBCache.prototype.get = function(key, storeName) {
    var me = this;
    return me.openStore(storeName).then(function(store) {
        var deferred = Q.defer(),
            req = store.get(key);
        req.onsuccess = function getItemOnSuccess() {
            var value = req.result;
            deferred.resolve(value);
        };
        req.onerror = function getItemOnError() {
            deferred.reject({errMsg: req.error.name});
        };
        return deferred.promise;
    });
};

/**
 * Retrieves all values from store. **This is slow operation - try to avoid it**
 * @param {String} [storeName] default to 'userData'
 * @returns {Promise} resolved to Array of store keys
 */
UBCache.prototype.getAllKeys = function(storeName) {
    var me = this, results = [], deferred = Q.defer();
    return me.openStore(storeName).then(function(store){
        var req;
        //noinspection JSUnresolvedFunction
        req = store.openCursor();
        req.onsuccess = function(e) {
            var cursor = e.target.result;
            if(cursor){
                results.push(cursor.key);
                cursor['continue'](); //MPV - for compiler - do not use reserved word in dot notation
            } else {
                deferred.resolve(results);
            }
        };
        req.onerror = function(e) {
            deferred.reject(e.target.result);
        };
        return deferred.promise;
    });
};

/**
 * Put one or several values to store (in single transaction).
 * Modifies existing values or inserts as new value if nonexistent.
 *
 * **If value === `undefined` we put null instead, to understand in future get this is null value or key not exist**
 * @param {{key: string, value}|Array<{key: string, value}>} data
 * @param [storeName] default to 'userData'
 * @returns {Promise}
 */
UBCache.prototype.put = function(data, storeName) {
    var me = this;
    return me.openStore(storeName, true).then(function(store){
        var req,
            deferred = Q.defer();
        if (Array.isArray(data)) {
            data.forEach(function(item, i){
                req = store.put(item.value === undefined ? null : item.value, item.key);
                req.onnotify = function(e) {
                    deferred.notify(e.target.result);
                };
                req.onerror = function(e) {
                    deferred.reject(e.target.result);
                };
                req.onsuccess = function(e) {
                    if(i === data.length - 1) {
                        deferred.resolve(e.target.result);
                    }
                };
            });
        } else {
            req = store.put(data.value === undefined ? null : data.value, data.key);
            req.onsuccess = req.onerror = function(e) {
                deferred.resolve(e.target.result);
            };
        }
        return deferred.promise;
    });
};

/**
 * Removes all data from the store
 * @param {String} [storeName] default to 'userData'
 * @returns {Promise}
 */
UBCache.prototype.clear = function(storeName){
    return this.openStore(storeName, true).then(function(store){
        var deferred = Q.defer(),
            req = store.clear();
        req.onsuccess = req.onerror = function(e) {
            deferred.resolve(e.target.result);
        };
        return deferred.promise;
    });
};

/**
 * Remove data from store.
 *
 * - If key is *String* - we delete one key;
 * - If key is *Array*  - we delete all keys in array;
 *
 * @method
 * @example

//remove data with key = 'key1' from userData store
$App.cache.remove('key1').done();

//remove 2 rows: with key = 'key1' and 'key2'  from session store
$App.cache.remove(['key1', 'key2'], UBCache.SESSION).done();

 * @param {String|Array<String>|RegExp} key
 * @param [storeName] default to 'userData'
 * @returns {Promise}
 */
UBCache.prototype.remove = function(key, storeName){
    var me = this;
    return me.openStore(storeName, true).then(function(store){
        var deferred = Q.defer(), req;
        if (typeof key === 'string'){
            req = store['delete'](key); //MPV compiler
            req.onsuccess = req.onerror = function(e) {
                deferred.resolve(e.target.result);
            };
        } else if (Array.isArray(key)){ //non empty array
            if (key.length) {
                key.forEach(function (item, i) {
                    req = store['delete'](item); //MPV compiler
                    req.onnotify = function (e) {
                        deferred.notify(e.target.result);
                    };
                    req.onerror = function (e) {
                        deferred.reject(e.target.result);
                    };
                    req.onsuccess = function (e) {
                        if (i === key.length - 1) {
                            deferred.resolve(e.target.result);
                        }
                    };
                });
            } else {
                deferred.resolve(true); // empty array passed - nothing to delete
            }
        } else {
           return deferred.reject({errMsg: 'invalid key for UBCache.remove call'});
        }
        return deferred.promise;
    });
};

/**
 * Remove data from store where keys match regExp.
 * Internally use {@link UBCache#getAllKeys} so is slow.
 * Better to use `remove([key1, ..keyN])`
 * @method
 *
 * @example

console.time('removeIfMach');
$App.cache.removeIfMach(/^admin:ru:cdn_/, 'permanent').done(function(){
   console.timeEnd('removeIfMach');
})

 * @param {RegExp} regExp
 * @param [storeName] default to 'userData'
 * @returns {Promise}
 */
UBCache.prototype.removeIfMach = function(regExp, storeName){
    var me = this;
    return me.getAllKeys(storeName).then(function(allKeys){
        var machKeys = allKeys.filter(function(item){
            return regExp.test(item);
        });
        return me.remove(machKeys, storeName);
    });
};

module.exports = UBCache;