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;