API Docs for: 1.0b
Show:

File: src/drivers/IndexedDB.js

var Bucket = Bucket || {};

!function (ns, utils) {
    /**
     * @module Driver.IndexedDB
     */

    var driver,
        db_version = 1;

    /**
     * @class Driver.IndexedDB
     *
     * @constructor
     * @extends Driver
     */
    driver = Bucket.registerDriver('IndexedDB', {
        name: 'IndexedDB',
        wrap : ['exists','get','set','fetchByKeyRange','clear','remove','getLength','getDBConnection'],
        
        openDB: function (callback) {
            this.logger.log('_openDB start');

            var $this = this,
                db_req;

            function onupgradeneeded(e) {
                var db, trans = null, store;

                switch (e.type) {
                    case 'success':
                        db = e.currentTarget.result.db;
                        trans = e.currentTarget.result;
                        break;

                    case 'upgradeneeded':
                        db = e.currentTarget.result;
                        break;
                }

                $this.logger.log('onupgradeneeded occur, new version is: ', db.version);

                //TODO: data migration between versions

                // Clear all ObjectStores in this database
                while (db.objectStoreNames.length > 0) {
                    db.deleteObjectStore(db.objectStoreNames[0]);
                }

                try {
                    store = db.createObjectStore($this.table_name, {'keyPath': 'key'});
                    store.createIndex("key", "key", {unique: true});
                    if (trans !== null) {
                        trans.oncomplete = function (e) {
                            callback && callback(null);
                        };
                    } else {
                        callback && callback(null);
                    }
                } catch (e) {
                    callback && callback($this.generateError(e));
                }
            }

            db_req = driver.getDB().open(this.db_name, db_version);
            this.logger.log('open DB request', db_req);

            db_req.onsuccess = function (e) {
                $this.logger.log('open DB request success event', e);
                // keep db reference.
                $this.db = e.target.result;

                // if the version isn't change fire opendb and break logic.
                if (parseInt(db_version, 10) === parseInt($this.db.version || 0, 10)) {
                    callback && callback(null);
                    return;
                }

                $this.logger.log('request version is higher then current, performing upgrade...');

                // use older version of database onupgradeneeded (webkit)
                if (typeof $this.db.setVersion === 'function') {
                    var version_req;

                    version_req = $this.db.setVersion(db_version);

                    version_req.onsuccess = onupgradeneeded;

                    version_req.onerror = function (e) {
                        $this.logger.log('setVersion error event', e);
                    };

                    version_req.onblocked = function (e) {
                        $this.logger.log('setVersion blocked event', e);
                    };

                    $this.logger.log('manual version upgrade for webkit', version_req);
                }
            };

            db_req.onupgradeneeded = onupgradeneeded;

            db_req.onerror = function (e) {
                $this.logger.log('error in db request', e);
                callback && callback($this.generateError(e));
            };
        },

        init: function () {
            // Database properties
            this.db_name = this.options.db_name + "." + this.options.table_name;
            this.table_name = this.options.table_name;
            
            // Init instance's logger
            this.logger = ns.Logger.getLogger(this.name + " " + this.db_name + "_" + this.table_name, ns.Logger.logLevels.ERROR);
            this.logger.log('init');

            this.wrapMethods();

            // Initiate connection to the indexedDB database;
            this.state = driver.STATES.CONNECTING;
            this.openDB(function (error) {
                if (error === null) {
                    this.state = driver.STATES.CONNECTED;
                    this.logger.log('openDB success fireEvent load:latched');
                    this.fireEvent('load:latched');
                } else {
                    this.state = driver.STATES.DISCONNECTED;
                    this.logger.log('openDB callback with error:', error);
                }
            }.bind(this));

            return this;
        },

        wrapMethods: function () {
            var i,
                name,
                method_list = this.wrap,
                $this = this;
            
            function wrap(name,method){
                $this[name] = function () {
                    var timeout,
                        args= Array.prototype.splice.call(arguments,0),
                        cb_index ,cb;

                    function run(){
                        timeout = $this.initTimeout(cb, name);
                        method.apply($this,args);
                    }

                    for (cb_index=0; cb = args[cb_index]; cb_index++){
                        if (typeof cb =='function') break;
                    }

                    if (!cb){
                        args.push(function(){
                            $this.clearTimeout(timeout);
                        });
                    }else {
                        args[cb_index] = function(){
                            $this.clearTimeout(timeout);
                            cb.apply(null, arguments);
                        };
                    }

                    if (this.state === driver.STATES.CONNECTED) {
                        run();
                    } else {
                        this.addEvent('load:once', function () {
                            run();
                        });
                    }
                }.bind($this);
            }
            
            for (i = 0; name = method_list[i]; i++) {
                wrap(name,this[name]);
            }
        },

        clear: function (callback) {
            this.logger.log('clear');

            var $this = this,
                trans,
                store,
                clear_req;

            try {
                trans = this.db.transaction([this.table_name], driver.TRANS_TYPES.READ_WRITE);
                store = trans.objectStore(this.table_name);
                clear_req = store.clear();

                clear_req.onsuccess = function (e) {
                    $this.logger.log('objectStore clear success event', e);
                };

                clear_req.onerror = function (e) {
                    $this.logger.log('objectStore clear error event', e);
                };

                trans.oncomplete = function (e) {
                    $this.logger.log('clear transaction complete event', e);
                    callback && callback(null);
                };

                trans.onerror = function (e) {
                    $this.logger.log('clear transaction error event', e);
                    callback && callback($this.generateError(e));
                };
            } catch (e) {
                callback && callback($this.generateError(e));
            }

            return this.$parent('clear', arguments);
        },

        fetchByKeyRange: function (callback, each, key_range) {
            var $this = this,
                cursor,
                trans,
                store,
                items = {};

            try {
                trans = this.db.transaction([this.table_name], driver.TRANS_TYPES.READ_ONLY);
                store = trans.objectStore(this.table_name);

                // We open a cursor and attach events.
                cursor = store.openCursor(key_range);

                cursor.onsuccess = function (e) {
                    var result = e.target.result;
                    if (!result) {
                        return;
                    }

                    if (each) {
                        callback && callback(null, result.value.key, JSON.parse(result.value.value));
                    } else {
                        items[result.value.key] = JSON.parse(result.value.value);
                    }

                    // The success event handler is fired once for each entry.
                    // So call "continue" on your result object.
                    // This lets you iterate across the data
                    result['continue']();
                };

                cursor.onerror = function (e) {
                    callback && callback($this.generateError(e));
                };


                if (!each) {
                    trans.oncomplete = function (e) {
                        callback && callback(null, items);
                    };
                }
            } catch (e) {
                callback && callback($this.generateError(e));
            }
        },

        fetchAll: function (callback, each) {
            callback && this.fetchByKeyRange(callback, each, null);
        },

        each: function (callback) {
            this.logger.log('each');

            callback && this.fetchAll(callback, true);

            return this.$parent('each', arguments);
        },

        exists: function (key, callback) {
            this.logger.log('exists', key);

            var $this = this,
                trans,
                store,
                index,
                get_req;

            function req_onsuccess(e) {
                callback && callback(null, e.target.result != null);
            }

            try {
                trans = this.db.transaction([this.table_name], driver.TRANS_TYPES.READ_ONLY);
                store = trans.objectStore(this.table_name);
                index = store.index('key');
                get_req = index.getKey(key);
                get_req.onsuccess = req_onsuccess;

                trans.onerror = function (e) {
                    callback && callback($this.generateError(e));
                };
            } catch (e) {
                callback && callback($this.generateError(e));
            }
            
            return this.$parent('exists', arguments);
        },

        get: function (key, callback) {
            this.logger.log('get', key);

            var $this = this,
                keys = [],
                i,
                trans,
                store,
                req,
                count = 0,
                empty = true,
                return_object = true,
                values = {};

            function req_onsuccess(e) {
                var result = e.target.result;

                if (result) {
                    empty = false;
                    values[result.key] = JSON.parse(result.value);
                }

                count++;

                if (count == keys.length) finish();
            }

            function req_onerror(e) {
                callback && callback($this.generateError(e));
            }

            function finish(){
                if (empty) {
                    values = null;
                }

                else if (return_object === false) {
                    values = values[key];
                }

                callback && callback(null, values);
            }

            try {
                trans = this.db.transaction([this.table_name], driver.TRANS_TYPES.READ_ONLY);
                store = trans.objectStore(this.table_name);

                if (typeof key === 'string' || typeof key === 'number') {
                    return_object = false;
                    keys.push(key);
                } else {
                    keys = key;
                }

                for (i = 0; i < keys.length; ++i) {
                    req = store.get(keys[i]);

                    req.onerror = req_onerror;
                    req.onsuccess = req_onsuccess;
                }
            } catch (e) {
                callback && callback($this.generateError(e));
            }

            return this.$parent('get', arguments);
        },

        getAll: function (callback) {
            this.logger.log('getAll');

            callback && this.fetchAll(callback);

            return this.$parent('getAll', arguments);
        },

        getKeys: function (callback) {
            this.logger.log('getKeys');

            this.getAll(function (error, items) {
                var key, keys = [];

                if (error !== null) {
                    callback && callback(error);
                    return;
                }

                for (key in items) {
                    if (items.hasOwnProperty(key)) {
                        keys.push(key);
                    }
                }

                callback && callback(null, keys);
            });

            return this.$parent('getKeys', arguments);
        },

        remove: function (key, callback) {
            this.logger.log('remove', key);

            var $this = this,
                keys = [],
                trans,
                store,
                i,
                request;

            function request_onsuccess(e) {
                $this.logger.log('objectStore delete success event', e);
            }

            function request_onerror(e) {
                $this.logger.log('objectStore delete error event', e);
            }

            try {
                trans = this.db.transaction(this.table_name, driver.TRANS_TYPES.READ_WRITE);
                store = trans.objectStore(this.table_name);

                if (typeof key === 'string' || typeof key === 'number') {
                    keys.push(key);
                } else {
                    keys = key;
                }

                for (i = 0; i < keys.length; ++i) {
                    request = store['delete'](keys[i]);
                    request.onsuccess = request_onsuccess;
                    request.onerror = request_onerror;
                }

                trans.oncomplete = function (e) {
                    $this.logger.log('remove transaction complete event', e);
                    callback && callback(null);
                };

                trans.onerror = function (e) {
                    $this.logger.log('remove transaction error event', e);
                    callback && callback($this.generateError(e));
                };

            } catch (e) {
                callback && callback($this.generateError(e));
            }

            return this.$parent('remove', arguments);
        },

        set: function (key, value, callback) {
            this.logger.log('set', key, value);

            var $this = this,
                map,
                trans,
                store,
                k,
                add_req;

            if (typeof key === 'string' || typeof key === 'number') {
                map = {};
                map[key] = value;
            } else {
                map = key;
            }

            try {
                trans = this.db.transaction([this.table_name], driver.TRANS_TYPES.READ_WRITE);
                store = trans.objectStore(this.table_name);

                function add_req_onsuccess(e) {
                    $this.logger.log('add request success event ', e);
                }

                function add_req_onerror(e) {
                    $this.logger.log('add request error event ', e);
                }

                for (k in map) {
                    if (map.hasOwnProperty(k)) {
                        try {
                            add_req = store.put({
                                'key': k,
                                'value': JSON.stringify(map[k])
                            });
                            add_req.onsuccess = add_req_onsuccess;
                            add_req.onerror = add_req_onerror;
                        } catch (e) {
                            callback && callback($this.generateError(e));
                        }
                    }
                }

                trans.oncomplete = function (e) {
                    $this.logger.log('add transaction complete event', e);
                    callback && callback(null);
                };

                trans.onerror = function (e) {
                    $this.logger.log('add transaction error event', e);
                    callback && callback($this.generateError(e));
                };
            } catch (e) {
                callback && callback($this.generateError(e));
            }

            return this.$parent('set', arguments);
        },

        test: function () {
            var result = !!driver.getDB();
            
            if(/firefox/.test(navigator.userAgent.toLowerCase())){
                result = result && (16 <= parseInt(navigator.userAgent.toLowerCase().match(/firefox\/(\d*)/)[1]));
            }
            
            if(window.webkitIndexedDB){
                result = false;
            }
            
            return result;
        },

        getLength: function (callback) {
            this.logger.log('getLength');

            var $this = this,
                trans,
                store,
                req;

            try {
                trans = this.db.transaction(this.table_name, driver.TRANS_TYPES.READ_WRITE);
                store = trans.objectStore(this.table_name);
                req = store.count();

                req.onsuccess = function (e) {
                    callback && callback(null, parseInt(e.target.result || 0, 10));
                };

                req.onerror = function (e) {
                    callback && callback(e);
                };
            } catch (e) {
                callback && callback($this.generateError(e));
            }

            return this.$parent('getLength', arguments);
        },

        getDBConnection: function (cb) {
            cb(this.db);
        },

        generateError: function (e) {
            var type, msg;

            if (e.code) {
                type = driver.ERROR_MAP[e.code];
                msg = e.message;
            } else if (e.target) {
                type = driver.ERROR_MAP[e.target.errorCode];
                msg = (e.target.webkitErrorMessage) ? e.target.webkitErrorMessage : e.target.error.name;
            }else {
                type = arguments[0];
                msg = arguments[1];
            }

            return this.$parent('generateError', [type, msg, e]);
        },

        destroy: function () {
            var err;
            try {
                this.db.close();
            } catch (e) {
                err = this.generateError(e);
            }

            return this.$parent('destroy',{error:err});
        }
    });

    driver.getDB = function () {
        return window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.oIndexedDB || window.msIndexedDB;
    };
    driver.getTransaction = function () {
        return window.IDBTransaction || window.webkitIDBTransaction || window.mozIDBTransaction || window.oIDBTransaction || window.msIDBTransaction;
    };
    driver.getKeyRange = function () {
        return window.IDBKeyRange || window.webkitIDBKeyRange || window.mozIDBKeyRange || window.oIDBKeyRange || window.msIDBKeyRange;
    };
    driver.getObjectStore = function() {
        return window.IDBObjectStore || window.webkitIDBObjectStore || window.mozIDBObjectStore || window.oIDBObjectStore || window.msIDBObjectStore;
    };

    driver.TRANS_TYPES = {
        READ_ONLY: 'readonly',
        READ_WRITE: 'readwrite',
        VERSION_CHANGE: 'versionchange'
    };

    driver.STATES = {
        DISCONNECTED: 0,
        CONNECTING: 1,
        CONNECTED: 2
    };

    driver.ERROR_MAP = {};
    driver.ERROR_MAP[DOMException.NOT_FOUND_ERR] = Bucket.Error.NOT_FOUND_ERR;
    driver.ERROR_MAP[DOMException.CONSTRAINT_ERR] = Bucket.Error.CONSTRAINT_ERR;
    driver.ERROR_MAP[DOMException.NOT_ALLOWED_ERR] = Bucket.Error.PERMISSION_ERR;
    driver.ERROR_MAP[DOMException.READ_ONLY_ERR] = Bucket.Error.PERMISSION_ERR;
    driver.ERROR_MAP[DOMException.QUOTA_ERR] = Bucket.Error.QUOTA_ERR;

}.apply(Bucket, [Bucket, Bucket.utils]);