cell
Version:
Single unit of I/O computations
1,612 lines (1,403 loc) • 49.2 kB
JavaScript
/**
* @name cell
* Single unit of I/O computations
*
* Version: 0.1.0 (Sat, 10 May 2014 23:36:10 GMT)
* Homepage: http://makesites.org/cell
*
* @author makesites
* Initiated by: Makis Tracend (@tracend)
*
* @cc_on Copyright © Makesites.org
* @license MIT License
*/
/*global window:false, self:false, define:false, module:false */
/**
* @license IDBWrapper - A cross-browser wrapper for IndexedDB
* Copyright (c) 2011 - 2013 Jens Arps
* http://jensarps.de/
*
* Licensed under the MIT (X11) license
*/
(function (name, definition, global) {
if (typeof define === 'function') {
define(definition);
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = definition();
} else {
global[name] = definition();
}
})('IDBStore', function () {
'use strict';
var defaultErrorHandler = function (error) {
throw error;
};
var defaults = {
storeName: 'Store',
storePrefix: 'IDBWrapper-',
dbVersion: 1,
keyPath: 'id',
autoIncrement: true,
onStoreReady: function () {
},
onError: defaultErrorHandler,
indexes: []
};
/**
*
* The IDBStore constructor
*
* @constructor
* @name IDBStore
* @version 1.4.1
*
* @param {Object} [kwArgs] An options object used to configure the store and
* set callbacks
* @param {String} [kwArgs.storeName='Store'] The name of the store
* @param {String} [kwArgs.storePrefix='IDBWrapper-'] A prefix that is
* internally used to construct the name of the database, which will be
* kwArgs.storePrefix + kwArgs.storeName
* @param {Number} [kwArgs.dbVersion=1] The version of the store
* @param {String} [kwArgs.keyPath='id'] The key path to use. If you want to
* setup IDBWrapper to work with out-of-line keys, you need to set this to
* `null`
* @param {Boolean} [kwArgs.autoIncrement=true] If set to true, IDBStore will
* automatically make sure a unique keyPath value is present on each object
* that is stored.
* @param {Function} [kwArgs.onStoreReady] A callback to be called when the
* store is ready to be used.
* @param {Function} [kwArgs.onError=throw] A callback to be called when an
* error occurred during instantiation of the store.
* @param {Array} [kwArgs.indexes=[]] An array of indexData objects
* defining the indexes to use with the store. For every index to be used
* one indexData object needs to be passed in the array.
* An indexData object is defined as follows:
* @param {Object} [kwArgs.indexes.indexData] An object defining the index to
* use
* @param {String} kwArgs.indexes.indexData.name The name of the index
* @param {String} [kwArgs.indexes.indexData.keyPath] The key path of the index
* @param {Boolean} [kwArgs.indexes.indexData.unique] Whether the index is unique
* @param {Boolean} [kwArgs.indexes.indexData.multiEntry] Whether the index is multi entry
* @param {Function} [onStoreReady] A callback to be called when the store
* is ready to be used.
* @example
// create a store for customers with an additional index over the
// `lastname` property.
var myCustomerStore = new IDBStore({
dbVersion: 1,
storeName: 'customer-index',
keyPath: 'customerid',
autoIncrement: true,
onStoreReady: populateTable,
indexes: [
{ name: 'lastname', keyPath: 'lastname', unique: false, multiEntry: false }
]
});
* @example
// create a generic store
var myCustomerStore = new IDBStore({
storeName: 'my-data-store',
onStoreReady: function(){
// start working with the store.
}
});
*/
var IDBStore = function (kwArgs, onStoreReady) {
if (typeof onStoreReady == 'undefined' && typeof kwArgs == 'function') {
onStoreReady = kwArgs;
}
if (Object.prototype.toString.call(kwArgs) != '[object Object]') {
kwArgs = {};
}
for (var key in defaults) {
this[key] = typeof kwArgs[key] != 'undefined' ? kwArgs[key] : defaults[key];
}
this.dbName = this.storePrefix + this.storeName;
this.dbVersion = parseInt(this.dbVersion, 10) || 1;
onStoreReady && (this.onStoreReady = onStoreReady);
var env = typeof window == 'object' ? window : self;
this.idb = env.indexedDB || env.webkitIndexedDB || env.mozIndexedDB;
this.keyRange = env.IDBKeyRange || env.webkitIDBKeyRange || env.mozIDBKeyRange;
this.features = {
hasAutoIncrement: !env.mozIndexedDB
};
this.consts = {
'READ_ONLY': 'readonly',
'READ_WRITE': 'readwrite',
'VERSION_CHANGE': 'versionchange',
'NEXT': 'next',
'NEXT_NO_DUPLICATE': 'nextunique',
'PREV': 'prev',
'PREV_NO_DUPLICATE': 'prevunique'
};
this.openDB();
};
IDBStore.prototype = /** @lends IDBStore */ {
/**
* A pointer to the IDBStore ctor
*
* @type IDBStore
*/
constructor: IDBStore,
/**
* The version of IDBStore
*
* @type String
*/
version: '1.4.1',
/**
* A reference to the IndexedDB object
*
* @type Object
*/
db: null,
/**
* The full name of the IndexedDB used by IDBStore, composed of
* this.storePrefix + this.storeName
*
* @type String
*/
dbName: null,
/**
* The version of the IndexedDB used by IDBStore
*
* @type Number
*/
dbVersion: null,
/**
* A reference to the objectStore used by IDBStore
*
* @type Object
*/
store: null,
/**
* The store name
*
* @type String
*/
storeName: null,
/**
* The key path
*
* @type String
*/
keyPath: null,
/**
* Whether IDBStore uses autoIncrement
*
* @type Boolean
*/
autoIncrement: null,
/**
* The indexes used by IDBStore
*
* @type Array
*/
indexes: null,
/**
* A hashmap of features of the used IDB implementation
*
* @type Object
* @proprty {Boolean} autoIncrement If the implementation supports
* native auto increment
*/
features: null,
/**
* The callback to be called when the store is ready to be used
*
* @type Function
*/
onStoreReady: null,
/**
* The callback to be called if an error occurred during instantiation
* of the store
*
* @type Function
*/
onError: null,
/**
* The internal insertID counter
*
* @type Number
* @private
*/
_insertIdCount: 0,
/**
* Opens an IndexedDB; called by the constructor.
*
* Will check if versions match and compare provided index configuration
* with existing ones, and update indexes if necessary.
*
* Will call this.onStoreReady() if everything went well and the store
* is ready to use, and this.onError() is something went wrong.
*
* @private
*
*/
openDB: function () {
var openRequest = this.idb.open(this.dbName, this.dbVersion);
var preventSuccessCallback = false;
openRequest.onerror = function (error) {
var gotVersionErr = false;
if ('error' in error.target) {
gotVersionErr = error.target.error.name == 'VersionError';
} else if ('errorCode' in error.target) {
gotVersionErr = error.target.errorCode == 12;
}
if (gotVersionErr) {
this.onError(new Error('The version number provided is lower than the existing one.'));
} else {
this.onError(error);
}
}.bind(this);
openRequest.onsuccess = function (event) {
if (preventSuccessCallback) {
return;
}
if(this.db){
this.onStoreReady();
return;
}
this.db = event.target.result;
if(typeof this.db.version == 'string'){
this.onError(new Error('The IndexedDB implementation in this browser is outdated. Please upgrade your browser.'));
return;
}
if(!this.db.objectStoreNames.contains(this.storeName)){
// We should never ever get here.
// Lets notify the user anyway.
this.onError(new Error('Something is wrong with the IndexedDB implementation in this browser. Please upgrade your browser.'));
return;
}
var emptyTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
this.store = emptyTransaction.objectStore(this.storeName);
// check indexes
var existingIndexes = Array.prototype.slice.call(this.getIndexList());
this.indexes.forEach(function(indexData){
var indexName = indexData.name;
if(!indexName){
preventSuccessCallback = true;
this.onError(new Error('Cannot create index: No index name given.'));
return;
}
this.normalizeIndexData(indexData);
if(this.hasIndex(indexName)){
// check if it complies
var actualIndex = this.store.index(indexName);
var complies = this.indexComplies(actualIndex, indexData);
if(!complies){
preventSuccessCallback = true;
this.onError(new Error('Cannot modify index "' + indexName + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.'));
}
existingIndexes.splice(existingIndexes.indexOf(indexName), 1);
} else {
preventSuccessCallback = true;
this.onError(new Error('Cannot create new index "' + indexName + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.'));
}
}, this);
if (existingIndexes.length) {
preventSuccessCallback = true;
this.onError(new Error('Cannot delete index(es) "' + existingIndexes.toString() + '" for current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.'));
}
preventSuccessCallback || this.onStoreReady();
}.bind(this);
openRequest.onupgradeneeded = function(/* IDBVersionChangeEvent */ event){
this.db = event.target.result;
if(this.db.objectStoreNames.contains(this.storeName)){
this.store = event.target.transaction.objectStore(this.storeName);
} else {
var optionalParameters = { autoIncrement: this.autoIncrement };
if (this.keyPath !== null) {
optionalParameters.keyPath = this.keyPath;
}
this.store = this.db.createObjectStore(this.storeName, optionalParameters);
}
var existingIndexes = Array.prototype.slice.call(this.getIndexList());
this.indexes.forEach(function(indexData){
var indexName = indexData.name;
if(!indexName){
preventSuccessCallback = true;
this.onError(new Error('Cannot create index: No index name given.'));
}
this.normalizeIndexData(indexData);
if(this.hasIndex(indexName)){
// check if it complies
var actualIndex = this.store.index(indexName);
var complies = this.indexComplies(actualIndex, indexData);
if(!complies){
// index differs, need to delete and re-create
this.store.deleteIndex(indexName);
this.store.createIndex(indexName, indexData.keyPath, { unique: indexData.unique, multiEntry: indexData.multiEntry });
}
existingIndexes.splice(existingIndexes.indexOf(indexName), 1);
} else {
this.store.createIndex(indexName, indexData.keyPath, { unique: indexData.unique, multiEntry: indexData.multiEntry });
}
}, this);
if (existingIndexes.length) {
existingIndexes.forEach(function(_indexName){
this.store.deleteIndex(_indexName);
}, this);
}
}.bind(this);
},
/**
* Deletes the database used for this store if the IDB implementations
* provides that functionality.
*/
deleteDatabase: function () {
if (this.idb.deleteDatabase) {
this.idb.deleteDatabase(this.dbName);
}
},
/*********************
* data manipulation *
*********************/
/**
* Puts an object into the store. If an entry with the given id exists,
* it will be overwritten. This method has a different signature for inline
* keys and out-of-line keys; please see the examples below.
*
* @param {*} [key] The key to store. This is only needed if IDBWrapper
* is set to use out-of-line keys. For inline keys - the default scenario -
* this can be omitted.
* @param {Object} value The data object to store.
* @param {Function} [onSuccess] A callback that is called if insertion
* was successful.
* @param {Function} [onError] A callback that is called if insertion
* failed.
* @returns {IDBTransaction} The transaction used for this operation.
* @example
// Storing an object, using inline keys (the default scenario):
var myCustomer = {
customerid: 2346223,
lastname: 'Doe',
firstname: 'John'
};
myCustomerStore.put(myCustomer, mySuccessHandler, myErrorHandler);
// Note that passing success- and error-handlers is optional.
* @example
// Storing an object, using out-of-line keys:
var myCustomer = {
lastname: 'Doe',
firstname: 'John'
};
myCustomerStore.put(2346223, myCustomer, mySuccessHandler, myErrorHandler);
// Note that passing success- and error-handlers is optional.
*/
put: function (key, value, onSuccess, onError) {
if (this.keyPath !== null) {
onError = onSuccess;
onSuccess = value;
value = key;
}
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
var hasSuccess = false,
result = null,
putRequest;
var putTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
putTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
putTransaction.onabort = onError;
putTransaction.onerror = onError;
if (this.keyPath !== null) { // in-line keys
this._addIdPropertyIfNeeded(value);
putRequest = putTransaction.objectStore(this.storeName).put(value);
} else { // out-of-line keys
putRequest = putTransaction.objectStore(this.storeName).put(value, key);
}
putRequest.onsuccess = function (event) {
hasSuccess = true;
result = event.target.result;
};
putRequest.onerror = onError;
return putTransaction;
},
/**
* Retrieves an object from the store. If no entry exists with the given id,
* the success handler will be called with null as first and only argument.
*
* @param {*} key The id of the object to fetch.
* @param {Function} [onSuccess] A callback that is called if fetching
* was successful. Will receive the object as only argument.
* @param {Function} [onError] A callback that will be called if an error
* occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
get: function (key, onSuccess, onError) {
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
var hasSuccess = false,
result = null;
var getTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
getTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
getTransaction.onabort = onError;
getTransaction.onerror = onError;
var getRequest = getTransaction.objectStore(this.storeName).get(key);
getRequest.onsuccess = function (event) {
hasSuccess = true;
result = event.target.result;
};
getRequest.onerror = onError;
return getTransaction;
},
/**
* Removes an object from the store.
*
* @param {*} key The id of the object to remove.
* @param {Function} [onSuccess] A callback that is called if the removal
* was successful.
* @param {Function} [onError] A callback that will be called if an error
* occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
remove: function (key, onSuccess, onError) {
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
var hasSuccess = false,
result = null;
var removeTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
removeTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
removeTransaction.onabort = onError;
removeTransaction.onerror = onError;
var deleteRequest = removeTransaction.objectStore(this.storeName)['delete'](key);
deleteRequest.onsuccess = function (event) {
hasSuccess = true;
result = event.target.result;
};
deleteRequest.onerror = onError;
return removeTransaction;
},
/**
* Runs a batch of put and/or remove operations on the store.
*
* @param {Array} dataArray An array of objects containing the operation to run
* and the data object (for put operations).
* @param {Function} [onSuccess] A callback that is called if all operations
* were successful.
* @param {Function} [onError] A callback that is called if an error
* occurred during one of the operations.
* @returns {IDBTransaction} The transaction used for this operation.
*/
batch: function (dataArray, onSuccess, onError) {
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
if(Object.prototype.toString.call(dataArray) != '[object Array]'){
onError(new Error('dataArray argument must be of type Array.'));
}
var batchTransaction = this.db.transaction([this.storeName] , this.consts.READ_WRITE);
batchTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(hasSuccess);
};
batchTransaction.onabort = onError;
batchTransaction.onerror = onError;
var count = dataArray.length;
var called = false;
var hasSuccess = false;
var onItemSuccess = function () {
count--;
if (count === 0 && !called) {
called = true;
hasSuccess = true;
}
};
dataArray.forEach(function (operation) {
var type = operation.type;
var key = operation.key;
var value = operation.value;
var onItemError = function (err) {
batchTransaction.abort();
if (!called) {
called = true;
onError(err, type, key);
}
};
if (type == 'remove') {
var deleteRequest = batchTransaction.objectStore(this.storeName)['delete'](key);
deleteRequest.onsuccess = onItemSuccess;
deleteRequest.onerror = onItemError;
} else if (type == 'put') {
var putRequest;
if (this.keyPath !== null) { // in-line keys
this._addIdPropertyIfNeeded(value);
putRequest = batchTransaction.objectStore(this.storeName).put(value);
} else { // out-of-line keys
putRequest = batchTransaction.objectStore(this.storeName).put(value, key);
}
putRequest.onsuccess = onItemSuccess;
putRequest.onerror = onItemError;
}
}, this);
return batchTransaction;
},
/**
* Takes an array of objects and stores them in a single transaction.
*
* @param {Array} dataArray An array of objects to store
* @param {Function} [onSuccess] A callback that is called if all operations
* were successful.
* @param {Function} [onError] A callback that is called if an error
* occurred during one of the operations.
* @returns {IDBTransaction} The transaction used for this operation.
*/
putBatch: function (dataArray, onSuccess, onError) {
var batchData = dataArray.map(function(item){
return { type: 'put', value: item };
});
return this.batch(batchData, onSuccess, onError);
},
/**
* Takes an array of keys and removes matching objects in a single
* transaction.
*
* @param {Array} keyArray An array of keys to remove
* @param {Function} [onSuccess] A callback that is called if all operations
* were successful.
* @param {Function} [onError] A callback that is called if an error
* occurred during one of the operations.
* @returns {IDBTransaction} The transaction used for this operation.
*/
removeBatch: function (keyArray, onSuccess, onError) {
var batchData = keyArray.map(function(key){
return { type: 'remove', key: key };
});
return this.batch(batchData, onSuccess, onError);
},
/**
* Takes an array of keys and fetches matching objects
*
* @param {Array} keyArray An array of keys identifying the objects to fetch
* @param {Function} [onSuccess] A callback that is called if all operations
* were successful.
* @param {Function} [onError] A callback that is called if an error
* occurred during one of the operations.
* @param {String} [arrayType='sparse'] The type of array to pass to the
* success handler. May be one of 'sparse', 'dense' or 'skip'. Defaults to
* 'sparse'. This parameter specifies how to handle the situation if a get
* operation did not throw an error, but there was no matching object in
* the database. In most cases, 'sparse' provides the most desired
* behavior. See the examples for details.
* @returns {IDBTransaction} The transaction used for this operation.
* @example
// given that there are two objects in the database with the keypath
// values 1 and 2, and the call looks like this:
myStore.getBatch([1, 5, 2], onError, function (data) { … }, arrayType);
// this is what the `data` array will be like:
// arrayType == 'sparse':
// data is a sparse array containing two entries and having a length of 3:
[Object, 2: Object]
0: Object
2: Object
length: 3
__proto__: Array[0]
// calling forEach on data will result in the callback being called two
// times, with the index parameter matching the index of the key in the
// keyArray.
// arrayType == 'dense':
// data is a dense array containing three entries and having a length of 3,
// where data[1] is of type undefined:
[Object, undefined, Object]
0: Object
1: undefined
2: Object
length: 3
__proto__: Array[0]
// calling forEach on data will result in the callback being called three
// times, with the index parameter matching the index of the key in the
// keyArray, but the second call will have undefined as first argument.
// arrayType == 'skip':
// data is a dense array containing two entries and having a length of 2:
[Object, Object]
0: Object
1: Object
length: 2
__proto__: Array[0]
// calling forEach on data will result in the callback being called two
// times, with the index parameter not matching the index of the key in the
// keyArray.
*/
getBatch: function (keyArray, onSuccess, onError, arrayType) {
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
arrayType || (arrayType = 'sparse');
if(Object.prototype.toString.call(keyArray) != '[object Array]'){
onError(new Error('keyArray argument must be of type Array.'));
}
var batchTransaction = this.db.transaction([this.storeName] , this.consts.READ_ONLY);
batchTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
batchTransaction.onabort = onError;
batchTransaction.onerror = onError;
var data = [];
var count = keyArray.length;
var called = false;
var hasSuccess = false;
var result = null;
var onItemSuccess = function (event) {
if (event.target.result || arrayType == 'dense') {
data.push(event.target.result);
} else if (arrayType == 'sparse') {
data.length++;
}
count--;
if (count === 0) {
called = true;
hasSuccess = true;
result = data;
}
};
keyArray.forEach(function (key) {
var onItemError = function (err) {
called = true;
result = err;
onError(err);
batchTransaction.abort();
};
var getRequest = batchTransaction.objectStore(this.storeName).get(key);
getRequest.onsuccess = onItemSuccess;
getRequest.onerror = onItemError;
}, this);
return batchTransaction;
},
/**
* Fetches all entries in the store.
*
* @param {Function} [onSuccess] A callback that is called if the operation
* was successful. Will receive an array of objects.
* @param {Function} [onError] A callback that will be called if an error
* occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
getAll: function (onSuccess, onError) {
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
var getAllTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
var store = getAllTransaction.objectStore(this.storeName);
if (store.getAll) {
this._getAllNative(getAllTransaction, store, onSuccess, onError);
} else {
this._getAllCursor(getAllTransaction, store, onSuccess, onError);
}
return getAllTransaction;
},
/**
* Implements getAll for IDB implementations that have a non-standard
* getAll() method.
*
* @param {Object} getAllTransaction An open READ transaction.
* @param {Object} store A reference to the store.
* @param {Function} onSuccess A callback that will be called if the
* operation was successful.
* @param {Function} onError A callback that will be called if an
* error occurred during the operation.
* @private
*/
_getAllNative: function (getAllTransaction, store, onSuccess, onError) {
var hasSuccess = false,
result = null;
getAllTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
getAllTransaction.onabort = onError;
getAllTransaction.onerror = onError;
var getAllRequest = store.getAll();
getAllRequest.onsuccess = function (event) {
hasSuccess = true;
result = event.target.result;
};
getAllRequest.onerror = onError;
},
/**
* Implements getAll for IDB implementations that do not have a getAll()
* method.
*
* @param {Object} getAllTransaction An open READ transaction.
* @param {Object} store A reference to the store.
* @param {Function} onSuccess A callback that will be called if the
* operation was successful.
* @param {Function} onError A callback that will be called if an
* error occurred during the operation.
* @private
*/
_getAllCursor: function (getAllTransaction, store, onSuccess, onError) {
var all = [],
hasSuccess = false,
result = null;
getAllTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
getAllTransaction.onabort = onError;
getAllTransaction.onerror = onError;
var cursorRequest = store.openCursor();
cursorRequest.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
all.push(cursor.value);
cursor['continue']();
}
else {
hasSuccess = true;
result = all;
}
};
cursorRequest.onError = onError;
},
/**
* Clears the store, i.e. deletes all entries in the store.
*
* @param {Function} [onSuccess] A callback that will be called if the
* operation was successful.
* @param {Function} [onError] A callback that will be called if an
* error occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
clear: function (onSuccess, onError) {
onError || (onError = defaultErrorHandler);
onSuccess || (onSuccess = noop);
var hasSuccess = false,
result = null;
var clearTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
clearTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
clearTransaction.onabort = onError;
clearTransaction.onerror = onError;
var clearRequest = clearTransaction.objectStore(this.storeName).clear();
clearRequest.onsuccess = function (event) {
hasSuccess = true;
result = event.target.result;
};
clearRequest.onerror = onError;
return clearTransaction;
},
/**
* Checks if an id property needs to present on a object and adds one if
* necessary.
*
* @param {Object} dataObj The data object that is about to be stored
* @private
*/
_addIdPropertyIfNeeded: function (dataObj) {
if (!this.features.hasAutoIncrement && typeof dataObj[this.keyPath] == 'undefined') {
dataObj[this.keyPath] = this._insertIdCount++ + Date.now();
}
},
/************
* indexing *
************/
/**
* Returns a DOMStringList of index names of the store.
*
* @return {DOMStringList} The list of index names
*/
getIndexList: function () {
return this.store.indexNames;
},
/**
* Checks if an index with the given name exists in the store.
*
* @param {String} indexName The name of the index to look for
* @return {Boolean} Whether the store contains an index with the given name
*/
hasIndex: function (indexName) {
return this.store.indexNames.contains(indexName);
},
/**
* Normalizes an object containing index data and assures that all
* properties are set.
*
* @param {Object} indexData The index data object to normalize
* @param {String} indexData.name The name of the index
* @param {String} [indexData.keyPath] The key path of the index
* @param {Boolean} [indexData.unique] Whether the index is unique
* @param {Boolean} [indexData.multiEntry] Whether the index is multi entry
*/
normalizeIndexData: function (indexData) {
indexData.keyPath = indexData.keyPath || indexData.name;
indexData.unique = !!indexData.unique;
indexData.multiEntry = !!indexData.multiEntry;
},
/**
* Checks if an actual index complies with an expected index.
*
* @param {Object} actual The actual index found in the store
* @param {Object} expected An Object describing an expected index
* @return {Boolean} Whether both index definitions are identical
*/
indexComplies: function (actual, expected) {
var complies = ['keyPath', 'unique', 'multiEntry'].every(function (key) {
// IE10 returns undefined for no multiEntry
if (key == 'multiEntry' && actual[key] === undefined && expected[key] === false) {
return true;
}
// Compound keys
if (key == 'keyPath' && Object.prototype.toString.call(expected[key]) == '[object Array]') {
var exp = expected.keyPath;
var act = actual.keyPath;
// IE10 can't handle keyPath sequences and stores them as a string.
// The index will be unusable there, but let's still return true if
// the keyPath sequence matches.
if (typeof act == 'string') {
return exp.toString() == act;
}
// Chrome/Opera stores keyPath squences as DOMStringList, Firefox
// as Array
if ( ! (typeof act.contains == 'function' || typeof act.indexOf == 'function') ) {
return false;
}
if (act.length !== exp.length) {
return false;
}
for (var i = 0, m = exp.length; i<m; i++) {
if ( ! ( (act.contains && act.contains(exp[i])) || act.indexOf(exp[i] !== -1) )) {
return false;
}
}
return true;
}
return expected[key] == actual[key];
});
return complies;
},
/**********
* cursor *
**********/
/**
* Iterates over the store using the given options and calling onItem
* for each entry matching the options.
*
* @param {Function} onItem A callback to be called for each match
* @param {Object} [options] An object defining specific options
* @param {Object} [options.index=null] An IDBIndex to operate on
* @param {String} [options.order=ASC] The order in which to provide the
* results, can be 'DESC' or 'ASC'
* @param {Boolean} [options.autoContinue=true] Whether to automatically
* iterate the cursor to the next result
* @param {Boolean} [options.filterDuplicates=false] Whether to exclude
* duplicate matches
* @param {Object} [options.keyRange=null] An IDBKeyRange to use
* @param {Boolean} [options.writeAccess=false] Whether grant write access
* to the store in the onItem callback
* @param {Function} [options.onEnd=null] A callback to be called after
* iteration has ended
* @param {Function} [options.onError=throw] A callback to be called
* if an error occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
iterate: function (onItem, options) {
options = mixin({
index: null,
order: 'ASC',
autoContinue: true,
filterDuplicates: false,
keyRange: null,
writeAccess: false,
onEnd: null,
onError: defaultErrorHandler
}, options || {});
var directionType = options.order.toLowerCase() == 'desc' ? 'PREV' : 'NEXT';
if (options.filterDuplicates) {
directionType += '_NO_DUPLICATE';
}
var hasSuccess = false;
var cursorTransaction = this.db.transaction([this.storeName], this.consts[options.writeAccess ? 'READ_WRITE' : 'READ_ONLY']);
var cursorTarget = cursorTransaction.objectStore(this.storeName);
if (options.index) {
cursorTarget = cursorTarget.index(options.index);
}
cursorTransaction.oncomplete = function () {
if (!hasSuccess) {
options.onError(null);
return;
}
if (options.onEnd) {
options.onEnd();
} else {
onItem(null);
}
};
cursorTransaction.onabort = options.onError;
cursorTransaction.onerror = options.onError;
var cursorRequest = cursorTarget.openCursor(options.keyRange, this.consts[directionType]);
cursorRequest.onerror = options.onError;
cursorRequest.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
onItem(cursor.value, cursor, cursorTransaction);
if (options.autoContinue) {
cursor['continue']();
}
} else {
hasSuccess = true;
}
};
return cursorTransaction;
},
/**
* Runs a query against the store and passes an array containing matched
* objects to the success handler.
*
* @param {Function} onSuccess A callback to be called when the operation
* was successful.
* @param {Object} [options] An object defining specific query options
* @param {Object} [options.index=null] An IDBIndex to operate on
* @param {String} [options.order=ASC] The order in which to provide the
* results, can be 'DESC' or 'ASC'
* @param {Boolean} [options.filterDuplicates=false] Whether to exclude
* duplicate matches
* @param {Object} [options.keyRange=null] An IDBKeyRange to use
* @param {Function} [options.onError=throw] A callback to be called if an error
* occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
query: function (onSuccess, options) {
var result = [];
options = options || {};
options.onEnd = function () {
onSuccess(result);
};
return this.iterate(function (item) {
result.push(item);
}, options);
},
/**
*
* Runs a query against the store, but only returns the number of matches
* instead of the matches itself.
*
* @param {Function} onSuccess A callback to be called if the opration
* was successful.
* @param {Object} [options] An object defining specific options
* @param {Object} [options.index=null] An IDBIndex to operate on
* @param {Object} [options.keyRange=null] An IDBKeyRange to use
* @param {Function} [options.onError=throw] A callback to be called if an error
* occurred during the operation.
* @returns {IDBTransaction} The transaction used for this operation.
*/
count: function (onSuccess, options) {
options = mixin({
index: null,
keyRange: null
}, options || {});
var onError = options.onError || defaultErrorHandler;
var hasSuccess = false,
result = null;
var cursorTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
cursorTransaction.oncomplete = function () {
var callback = hasSuccess ? onSuccess : onError;
callback(result);
};
cursorTransaction.onabort = onError;
cursorTransaction.onerror = onError;
var cursorTarget = cursorTransaction.objectStore(this.storeName);
if (options.index) {
cursorTarget = cursorTarget.index(options.index);
}
var countRequest = cursorTarget.count(options.keyRange);
countRequest.onsuccess = function (evt) {
hasSuccess = true;
result = evt.target.result;
};
countRequest.onError = onError;
return cursorTransaction;
},
/**************/
/* key ranges */
/**************/
/**
* Creates a key range using specified options. This key range can be
* handed over to the count() and iterate() methods.
*
* Note: You must provide at least one or both of "lower" or "upper" value.
*
* @param {Object} options The options for the key range to create
* @param {*} [options.lower] The lower bound
* @param {Boolean} [options.excludeLower] Whether to exclude the lower
* bound passed in options.lower from the key range
* @param {*} [options.upper] The upper bound
* @param {Boolean} [options.excludeUpper] Whether to exclude the upper
* bound passed in options.upper from the key range
* @param {*} [options.only] A single key value. Use this if you need a key
* range that only includes one value for a key. Providing this
* property invalidates all other properties.
* @return {Object} The IDBKeyRange representing the specified options
*/
makeKeyRange: function(options){
/*jshint onecase:true */
var keyRange,
hasLower = typeof options.lower != 'undefined',
hasUpper = typeof options.upper != 'undefined',
isOnly = typeof options.only != 'undefined';
switch(true){
case isOnly:
keyRange = this.keyRange.only(options.only);
break;
case hasLower && hasUpper:
keyRange = this.keyRange.bound(options.lower, options.upper, options.excludeLower, options.excludeUpper);
break;
case hasLower:
keyRange = this.keyRange.lowerBound(options.lower, options.excludeLower);
break;
case hasUpper:
keyRange = this.keyRange.upperBound(options.upper, options.excludeUpper);
break;
default:
throw new Error('Cannot create KeyRange. Provide one or both of "lower" or "upper" value, or an "only" value.');
}
return keyRange;
}
};
/** helpers **/
var noop = function () {
};
var empty = {};
var mixin = function (target, source) {
var name, s;
for (name in source) {
s = source[name];
if (s !== empty[name] && s !== target[name]) {
target[name] = s;
}
}
return target;
};
IDBStore.version = IDBStore.prototype.version;
return IDBStore;
}, this);
(function() {
var DB, leveldb, nope, okay, _ref,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
DB = (function() {
function DB(path) {
this.path = path;
this.loaded = __bind(this.loaded, this);
this.ready = __bind(this.ready, this);
this.setup();
}
DB.prototype.ready = function(fn) {
if (this.isReady) {
return fn();
}
return this.onReady = fn;
};
DB.prototype.loaded = function() {
this.isReady = true;
if (typeof this.onReady === "function") {
this.onReady();
}
return this.onReady = null;
};
return DB;
})();
if (typeof process !== "undefined" && process !== null ? (_ref = process.versions) != null ? _ref.node : void 0 : void 0) {
leveldb = require('leveldb');
DB.prototype.setup = function(cb) {
var _this = this;
return leveldb.open(this.path, {
create_if_missing: true
}, function(err, db) {
if (err) {
throw err;
}
_this.store = db;
_this.loaded();
return typeof cb === "function" ? cb() : void 0;
});
};
DB.prototype.get = function(key, fn) {
return this.store.get(key, fn);
};
DB.prototype.put = function(key, val, fn) {
return this.store.put(key, val.toString(), fn);
};
DB.prototype.del = function(key, fn) {
return this.store.del(key, fn);
};
DB.prototype.getAll = function(fn) {
return this.store.iterator(function(err, iterator) {
var data;
if (err) {
return fn(err);
}
data = {};
return iterator.forRange(function(err, key, val) {
if (err) {
return fn(err);
}
return data[key] = val;
}, function() {
return fn(null, data);
});
});
};
DB.prototype.clear = function(fn) {
var _this = this;
return leveldb.destroy(this.path, {}, function() {
return _this.setup(fn);
});
};
} else {
DB.prototype.setup = function() {
return this.store = new IDBStore({
keyPath: 'id',
autoIncrement: false,
onStoreReady: this.loaded
});
};
okay = function(fn) {
return function(res) {
return fn(null, res);
};
};
nope = function(fn) {
return function(err) {
return fn(err, null);
};
};
DB.prototype.get = function(key, fn) {
return this.store.get(key, function(data) {
return fn(null, data != null ? data.val : void 0);
}, nope(fn));
};
DB.prototype.put = function(key, val, fn) {
var data;
data = {
id: key,
val: val
};
return this.store.put(data, okay(fn), nope(fn));
};
DB.prototype.del = function(key, fn) {
return this.store.remove(key, okay(fn), nope(fn));
};
DB.prototype.getAll = function(fn) {
return this.store.getAll(function(res) {
var data, obj, _i, _len;
data = {};
for (_i = 0, _len = res.length; _i < _len; _i++) {
obj = res[_i];
data[obj.id] = obj.val;
}
return fn(null, data);
}, nope(fn));
};
DB.prototype.clear = function(fn) {
return this.store.clear(function(res) {
return fn(null, res);
});
};
}
if ((typeof module !== "undefined" && module !== null ? module.exports : void 0) != null) {
module.exports = DB;
} else {
window.ILDB = DB;
}
}).call(this);
(function(lib) {
if (typeof define === 'function' && define.amd) {
define("cell", [], lib);
} else if (typeof exports === 'object') {
module.exports = lib(require('ildb'));
} else {
window.Cell = lib(window.ILDB);
}
})(function(ILDB) {
var db, queue,
_queue = [];
var defaults = {
"store": "cell.db"
};
var Cell = function( options ){
// constructor
var self = this;
// - setting options
options = options || {};
this.options = defaults;
Object.extend(this.options, options);
// - setup DB
db = new ILDB( this.options.store );
// update the status on ready
db.ready(function() {
//return typeof db.clear === "function" ? db.clear(done) : void 0;
self.status.ready = true;
self._processQueue();
});
return this;
}
// Methods
Cell.prototype = {
status: {
ready: false
},
// temp data container
_data: {
},
// Data interface
set: function( data ){
var self = this;
for( var key in data ){
// save data in memory
this._data[key] = data[key];
if( this.status.ready ){
db.put(key, data[key], function(err) {
if(err) console.log(err);
// delete temp data
//delete self._data[key];
// callback?
});
} else {
queue("set", arguments);
}
}
},
get: function( key, cb ){
var self = this;
if( this.status.ready ){
db.get(key, function(err, val) {
if(err) console.log(err);
if( cb ) cb( val );
});
} else {
queue("get", arguments);
}
},
remove: function( key ){
if( this.status.ready ){
db.del(key, function(err) {
if(err) console.log(err);
// callback?
});
} else {
queue("remove", arguments);
}
},
all: function( cb ){
db.getAll(function(err, all) {
if(err) console.log(err);
if( cb ) cb( all );
});
},
// Logic
// - define a single method
define: function( name, method ){
// validation?
this[name] = method;
},
// - Extend with custom methods
extend: function( methods ){
// validation?
for( var name in methods ){
this[name] = methods[name];
}
},
// - Checks the state of an item
check: function( options ){
options = options || {};
// fallbacks
var key = options.key || false;
var value = options.value || false;
var cb = options.cb || function(){};
// prerequisite
if( !key ) return;
// if a value is provided
this.get(key, function( stored ){
// exit now if there's no value
if( !stored ) return cb( false );
// if value compare
if( value ) return cb( (stored == value) );
// return the timestamp of the stored item
//...
});
},
// Persistance
// - Saves existing data is a separate DB
save: function( name ){
// fallback
name = name || (new Date()).getTime();
// set the store
var store = "cell_"+ name;
// create a separate cell instance
var cell = new Cell({
store: store
});
// get all data
this.all(function( data ){
// save in new store
cell.set( data );
});
// keep a reference to the name
return name;
},
// - Loads data from a persistant state
load: function( name ){
// fallback
name = name || false;
// prerequisite
if( !name ) return;
// set the store
var store = "cell_"+ name;
var cell = new Cell({
store: store
});
// get all existing data
cell.all(function( data ){
// delete existing data first?
// save in store
this.set( data );
});
},
// Internal methods
_processQueue: function(){
for(var i in _queue){
var action = _queue[i].action;
var args = _queue[i].args;
this[action].apply(this, args);
}
}
};
// store methods
/*
store = {
put: function(key, value){
},
get: function(){
},
del: function(){
}
}
*/
queue = function(action, args){
_queue.push({
action: action,
args: args
});
}
// Helpers
Object.extend = function(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor && source[property].constructor === Object) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
return Cell;
});