(function ($, undefined) { 'use strict'; var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange; var IDBCursor = window.IDBCursor || window.webkitIDBCursor || {}; if (typeof IDBCursor.PREV === "undefined") { IDBCursor.PREV = "prev"; } if (typeof IDBCursor.NEXT === "undefined") { IDBCursor.NEXT = "next"; } /** * Best to use the constant IDBTransaction since older version support numeric types while the latest spec * supports strings */ var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction; function getDefaultTransaction(mode) { var result = null; switch (mode) { case 0: case 1: case "readwrite": case "readonly": result = mode; break; default: result = IDBTransaction.READ_WRITE || "readwrite"; } return result; } $.extend({ /** * The IndexedDB object used to open databases * @param {Object} dbName - name of the database * @param {Object} config - version, onupgradeneeded, onversionchange, schema */ "indexedDB": function (dbName, config) { if (config) { // Parse the config argument if (typeof config === "number") config = { "version": config }; var version = config.version; if (config.schema && !version) { var max = -1; for (var key in config.schema) { max = max > key ? max : key; } version = config.version || max; } } var wrap = { "request": function (req, args) { return $.Deferred(function (dfd) { try { var idbRequest = typeof req === "function" ? req(args) : req; idbRequest.onsuccess = function (e) { dfd.resolveWith(idbRequest, [idbRequest.result, e]); }; idbRequest.onerror = function (e) { dfd.rejectWith(idbRequest, [idbRequest.error, e]); }; if (typeof idbRequest.onblocked !== "undefined" && idbRequest.onblocked === null) { idbRequest.onblocked = function (e) { var res; try { res = idbRequest.result; } catch (e) { res = null; // Required for Older Chrome versions, accessing result causes error } dfd.notifyWith(idbRequest, [res, e]); }; } if (typeof idbRequest.onupgradeneeded !== "undefined" && idbRequest.onupgradeneeded === null) { idbRequest.onupgradeneeded = function (e) { dfd.notifyWith(idbRequest, [idbRequest.result, e]); }; } } catch (e) { e.name = "exception"; dfd.rejectWith(idbRequest, ["exception", e]); } }); }, // Wraps the IDBTransaction to return promises, and other dependent methods "transaction": function (idbTransaction) { return { "objectStore": function (storeName) { try { return wrap.objectStore(idbTransaction.objectStore(storeName)); } catch (e) { idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort(); return wrap.objectStore(null); } }, "createObjectStore": function (storeName, storeParams) { try { return wrap.objectStore(idbTransaction.db.createObjectStore(storeName, storeParams)); } catch (e) { idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort(); } }, "deleteObjectStore": function (storeName) { try { idbTransaction.db.deleteObjectStore(storeName); } catch (e) { idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort(); } }, "abort": function () { idbTransaction.abort(); } }; }, "objectStore": function (idbObjectStore) { var result = {}; // Define CRUD operations var crudOps = ["add", "put", "get", "delete", "clear", "count"]; for (var i = 0; i < crudOps.length; i++) { result[crudOps[i]] = (function (op) { return function () { return wrap.request(function (args) { return idbObjectStore[op].apply(idbObjectStore, args); }, arguments); }; })(crudOps[i]); } result.each = function (callback, range, direction) { return wrap.cursor(function () { if (direction) { return idbObjectStore.openCursor(wrap.range(range), direction); } else { return idbObjectStore.openCursor(wrap.range(range)); } }, callback); }; result.index = function (name) { return wrap.index(function () { return idbObjectStore.index(name); }); }; result.createIndex = function (prop, options, indexName) { if (arguments.length === 2 && typeof options === "string") { indexName = arguments[1]; options = null; } if (!indexName) { indexName = prop; } return wrap.index(function () { return idbObjectStore.createIndex(indexName, prop, options); }); }; result.deleteIndex = function (indexName) { return idbObjectStore.deleteIndex(indexName); }; return result; }, "range": function (r) { if ($.isArray(r)) { if (r.length === 1) { return IDBKeyRange.only(r[0]); } else { return IDBKeyRange.bound(r[0], r[1], (typeof r[2] === 'undefined') ? false : r[2], (typeof r[3] === 'undefined') ? false : r[3]); } } else if (typeof r === "undefined") { return null; } else { return r; } }, "cursor": function (idbCursor, callback) { return $.Deferred(function (dfd) { try { var cursorReq = typeof idbCursor === "function" ? idbCursor() : idbCursor; cursorReq.onsuccess = function (e) { if (!cursorReq.result) { dfd.resolveWith(cursorReq, [null, e]); return; } var elem = { // Delete, update do not move "delete": function () { return wrap.request(function () { return cursorReq.result["delete"](); }); }, "update": function (data) { return wrap.request(function () { return cursorReq.result["update"](data); }); }, "next": function (key) { this.data = key; }, "key": cursorReq.result.key, "value": cursorReq.result.value }; dfd.notifyWith(cursorReq, [elem, e]); var result = callback.apply(cursorReq, [elem]); try { if (result === false) { dfd.resolveWith(cursorReq, [null, e]); } else if (typeof result === "number") { cursorReq.result["advance"].apply(cursorReq.result, [result]); } else { if (elem.data) cursorReq.result["continue"].apply(cursorReq.result, [elem.data]); else cursorReq.result["continue"](); } } catch (e) { dfd.rejectWith(cursorReq, [cursorReq.result, e]); } }; cursorReq.onerror = function (e) { dfd.rejectWith(cursorReq, [cursorReq.result, e]); }; } catch (e) { e.type = "exception"; dfd.rejectWith(cursorReq, [null, e]); } }); }, "index": function (index) { try { var idbIndex = (typeof index === "function" ? index() : index); } catch (e) { idbIndex = null; } return { "each": function (callback, range, direction) { return wrap.cursor(function () { if (direction) { return idbIndex.openCursor(wrap.range(range), direction); } else { return idbIndex.openCursor(wrap.range(range)); } }, callback); }, "eachKey": function (callback, range, direction) { return wrap.cursor(function () { if (direction) { return idbIndex.openKeyCursor(wrap.range(range), direction); } else { return idbIndex.openKeyCursor(wrap.range(range)); } }, callback); }, "get": function (key) { if (typeof idbIndex.get === "function") { return wrap.request(idbIndex.get(key)); } else { return idbIndex.openCursor(wrap.range(key)); } }, "count": function () { if (typeof idbIndex.count === "function") { return wrap.request(idbIndex.count()); } else { throw "Count not implemented for cursors"; } }, "getKey": function (key) { if (typeof idbIndex.getKey === "function") { return wrap.request(idbIndex.getKey(key)); } else { return idbIndex.openKeyCursor(wrap.range(key)); } } }; } }; // Start with opening the database var dbPromise = wrap.request(function () { return version ? indexedDB.open(dbName, parseInt(version)) : indexedDB.open(dbName); }); dbPromise.then(function (db, e) { db.onversionchange = function () { // Try to automatically close the database if there is a version change request if (!(config && config.onversionchange && config.onversionchange() !== false)) { db.close(); } }; }, function (error, e) { // Nothing much to do if an error occurs }, function (db, e) { if (e && e.type === "upgradeneeded") { if (config && config.schema) { // Assuming that version is always an integer for (var i = e.oldVersion + 1; i <= e.newVersion; i++) { typeof config.schema[i] === "function" && config.schema[i].call(this, wrap.transaction(this.transaction)); } } if (config && typeof config.upgrade === "function") { config.upgrade.call(this, wrap.transaction(this.transaction)); } } }); return $.extend(dbPromise, { "cmp": function (key1, key2) { return indexedDB.cmp(key1, key2); }, "deleteDatabase": function () { // Kinda looks ugly coz DB is opened before it needs to be deleted. // Blame it on the API return $.Deferred(function (dfd) { dbPromise.then(function (db, e) { db.close(); wrap.request(function () { return indexedDB.deleteDatabase(dbName); }).then(function (result, e) { dfd.resolveWith(this, [result, e]); }, function (error, e) { dfd.rejectWith(this, [error, e]); }, function (db, e) { dfd.notifyWith(this, [db, e]); }); }, function (error, e) { dfd.rejectWith(this, [error, e]); }, function (db, e) { dfd.notifyWith(this, [db, e]); }); }); }, "transaction": function (storeNames, mode) { !$.isArray(storeNames) && (storeNames = [storeNames]); mode = getDefaultTransaction(mode); return $.Deferred(function (dfd) { dbPromise.then(function (db, e) { var idbTransaction; try { idbTransaction = db.transaction(storeNames, mode); idbTransaction.onabort = idbTransaction.onerror = function (e) { dfd.rejectWith(idbTransaction, [e]); }; idbTransaction.oncomplete = function (e) { dfd.resolveWith(idbTransaction, [e]); }; } catch (e) { e.type = "exception"; dfd.rejectWith(this, [e]); return; } try { dfd.notifyWith(idbTransaction, [wrap.transaction(idbTransaction)]); } catch (e) { e.type = "exception"; dfd.rejectWith(this, [e]); } }, function (err, e) { dfd.rejectWith(this, [e, err]); }, function (res, e) { //dfd.notifyWith(this, ["", e]); }); }); }, "objectStore": function (storeName, mode) { var me = this, result = {}; function op(callback) { return $.Deferred(function (dfd) { function onTransactionProgress(trans, callback) { try { callback(trans.objectStore(storeName)).then(function (result, e) { dfd.resolveWith(this, [result, e]); }, function (err, e) { dfd.rejectWith(this, [err, e]); }); } catch (e) { e.name = "exception"; dfd.rejectWith(trans, [e, e]); } } me.transaction(storeName, getDefaultTransaction(mode)).then(function () { // Nothing to do when transaction is complete }, function (err, e) { // If transaction fails, CrudOp fails if (err.code === err.NOT_FOUND_ERR && (mode === true || typeof mode === "object")) { var db = this.result; db.close(); dbPromise = wrap.request(function () { return indexedDB.open(dbName, (parseInt(db.version, 10) || 1) + 1); }); dbPromise.then(function (db, e) { db.onversionchange = function () { // Try to automatically close the database if there is a version change request if (!(config && config.onversionchange && config.onversionchange() !== false)) { db.close(); } }; me.transaction(storeName, getDefaultTransaction(mode)).then(function () { // Nothing much to do }, function (err, e) { dfd.rejectWith(this, [err, e]); }, function (trans, e) { onTransactionProgress(trans, callback); }); }, function (err, e) { dfd.rejectWith(this, [err, e]); }, function (db, e) { if (e.type === "upgradeneeded") { try { db.createObjectStore(storeName, mode === true ? { "autoIncrement": true } : mode); } catch (ex) { dfd.rejectWith(this, [ex, e]); } } }); } else { dfd.rejectWith(this, [err, e]); } }, function (trans) { onTransactionProgress(trans, callback); }); }); } function crudOp(opName, args) { return op(function (wrappedObjectStore) { return wrappedObjectStore[opName].apply(wrappedObjectStore, args); }); } function indexOp(opName, indexName, args) { return op(function (wrappedObjectStore) { var index = wrappedObjectStore.index(indexName); return index[opName].apply(index[opName], args); }); } var crud = ["add", "delete", "get", "put", "clear", "count", "each"]; for (var i = 0; i < crud.length; i++) { result[crud[i]] = (function (op) { return function () { return crudOp(op, arguments); }; })(crud[i]); } result.index = function (indexName) { return { "each": function (callback, range, direction) { return indexOp("each", indexName, [callback, range, direction]); }, "eachKey": function (callback, range, direction) { return indexOp("eachKey", indexName, [callback, range, direction]); }, "get": function (key) { return indexOp("get", indexName, [key]); }, "count": function () { return indexOp("count", indexName, []); }, "getKey": function (key) { return indexOp("getKey", indexName, [key]); } }; }; return result; } }); } }); $.indexedDB.IDBCursor = IDBCursor; $.indexedDB.IDBTransaction = IDBTransaction; $.idb = $.indexedDB; })(jQuery);