/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
'use strict';
/** @module store/indexeddb */
var utils = require('./../utils.js');
// Imports.
var throwErrorCallback = utils.throwErrorCallback;
var delay = utils.delay;
var indexedDB = utils.inBrowser() ? window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.indexedDB : undefined;
var IDBKeyRange = utils.inBrowser() ? window.IDBKeyRange || window.webkitIDBKeyRange : undefined;
var IDBTransaction = utils.inBrowser() ? window.IDBTransaction || window.webkitIDBTransaction || {} : {} ;
var IDBT_READ_ONLY = IDBTransaction.READ_ONLY || "readonly";
var IDBT_READ_WRITE = IDBTransaction.READ_WRITE || "readwrite";
/** Returns either a specific error handler or the default error handler
* @param {Function} error - The specific error handler
* @param {Function} defaultError - The default error handler
* @returns {Function} The error callback
*/
function getError(error, defaultError) {
return function (e) {
var errorFunc = error || defaultError;
if (!errorFunc) {
return;
}
// Old api quota exceeded error support.
if (Object.prototype.toString.call(e) === "[object IDBDatabaseException]") {
if (e.code === 11 /* IndexedDb disk quota exceeded */) {
errorFunc({ name: "QuotaExceededError", error: e });
return;
}
errorFunc(e);
return;
}
var errName;
try {
var errObj = e.target.error || e;
errName = errObj.name;
} catch (ex) {
errName = (e.type === "blocked") ? "IndexedDBBlocked" : "UnknownError";
}
errorFunc({ name: errName, error: e });
};
}
/** Opens the store object's indexed db database.
* @param {IndexedDBStore} store - The store object
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
function openStoreDb(store, success, error) {
var storeName = store.name;
var dbName = "_odatajs_" + storeName;
var request = indexedDB.open(dbName);
request.onblocked = error;
request.onerror = error;
request.onupgradeneeded = function () {
var db = request.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
};
request.onsuccess = function (event) {
var db = request.result;
if (!db.objectStoreNames.contains(storeName)) {
// Should we use the old style api to define the database schema?
if ("setVersion" in db) {
var versionRequest = db.setVersion("1.0");
versionRequest.onsuccess = function () {
var transaction = versionRequest.transaction;
transaction.oncomplete = function () {
success(db);
};
db.createObjectStore(storeName, null, false);
};
versionRequest.onerror = error;
versionRequest.onblocked = error;
return;
}
// The database doesn't have the expected store.
// Fabricate an error object for the event for the schema mismatch
// and error out.
event.target.error = { name: "DBSchemaMismatch" };
error(event);
return;
}
db.onversionchange = function(event) {
event.target.close();
};
success(db);
};
}
/** Opens a new transaction to the store
* @param {IndexedDBStore} store - The store object
* @param {Integer} mode - The read/write mode of the transaction (constants from IDBTransaction)
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
function openTransaction(store, mode, success, error) {
var storeName = store.name;
var storeDb = store.db;
var errorCallback = getError(error, store.defaultError);
if (storeDb) {
success(storeDb.transaction(storeName, mode));
return;
}
openStoreDb(store, function (db) {
store.db = db;
success(db.transaction(storeName, mode));
}, errorCallback);
}
/** Creates a new IndexedDBStore.
* @class IndexedDBStore
* @constructor
* @param {String} name - The name of the store.
* @returns {Object} The new IndexedDBStore.
*/
function IndexedDBStore(name) {
this.name = name;
}
/** Creates a new IndexedDBStore.
* @method module:store/indexeddb~IndexedDBStore.create
* @param {String} name - The name of the store.
* @returns {Object} The new IndexedDBStore.
*/
IndexedDBStore.create = function (name) {
if (IndexedDBStore.isSupported()) {
return new IndexedDBStore(name);
}
throw { message: "IndexedDB is not supported on this browser" };
};
/** Returns whether IndexedDB is supported.
* @method module:store/indexeddb~IndexedDBStore.isSupported
* @returns {Boolean} True if IndexedDB is supported, false otherwise.
*/
IndexedDBStore.isSupported = function () {
return !!indexedDB;
};
/** Adds a key/value pair to the store
* @method module:store/indexeddb~IndexedDBStore#add
* @param {String} key - The key
* @param {Object} value - The value
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.add = function (key, value, success, error) {
var name = this.name;
var defaultError = this.defaultError;
var keys = [];
var values = [];
if (key instanceof Array) {
keys = key;
values = value;
} else {
keys = [key];
values = [value];
}
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onabort = getError(error, defaultError, key, "add");
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(key, value);
}
};
for (var i = 0; i < keys.length && i < values.length; i++) {
transaction.objectStore(name).add({ v: values[i] }, keys[i]);
}
}, error);
};
/** Adds or updates a key/value pair in the store
* @method module:store/indexeddb~IndexedDBStore#addOrUpdate
* @param {String} key - The key
* @param {Object} value - The value
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.addOrUpdate = function (key, value, success, error) {
var name = this.name;
var defaultError = this.defaultError;
var keys = [];
var values = [];
if (key instanceof Array) {
keys = key;
values = value;
} else {
keys = [key];
values = [value];
}
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onabort = getError(error, defaultError);
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(key, value);
}
};
for (var i = 0; i < keys.length && i < values.length; i++) {
var record = { v: values[i] };
transaction.objectStore(name).put(record, keys[i]);
}
}, error);
};
/** Clears the store
* @method module:store/indexeddb~IndexedDBStore#clear
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.clear = function (success, error) {
var name = this.name;
var defaultError = this.defaultError;
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onerror = getError(error, defaultError);
transaction.oncomplete = function () {
success();
};
transaction.objectStore(name).clear();
}, error);
};
/** Closes the connection to the database
* @method module:store/indexeddb~IndexedDBStore#close
*/
IndexedDBStore.prototype.close = function () {
if (this.db) {
this.db.close();
this.db = null;
}
};
/** Returns whether the store contains a key
* @method module:store/indexeddb~IndexedDBStore#contains
* @param {String} key - The key
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.contains = function (key, success, error) {
var name = this.name;
var defaultError = this.defaultError;
openTransaction(this, IDBT_READ_ONLY, function (transaction) {
var objectStore = transaction.objectStore(name);
var request = objectStore.get(key);
transaction.oncomplete = function () {
success(!!request.result);
};
transaction.onerror = getError(error, defaultError);
}, error);
};
IndexedDBStore.prototype.defaultError = throwErrorCallback;
/** Gets all the keys from the store
* @method module:store/indexeddb~IndexedDBStore#getAllKeys
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.getAllKeys = function (success, error) {
var name = this.name;
var defaultError = this.defaultError;
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
var results = [];
transaction.oncomplete = function () {
success(results);
};
var request = transaction.objectStore(name).openCursor();
request.onerror = getError(error, defaultError);
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
results.push(cursor.key);
// Some tools have issues because continue is a javascript reserved word.
cursor["continue"].call(cursor);
}
};
}, error);
};
/** Identifies the underlying mechanism used by the store.
*/
IndexedDBStore.prototype.mechanism = "indexeddb";
/** Reads the value for the specified key
* @method module:store/indexeddb~IndexedDBStore#read
* @param {String} key - The key
* @param {Function} success - The success callback
* @param {Function} error - The error callback
* If the key does not exist, the success handler will be called with value = undefined
*/
IndexedDBStore.prototype.read = function (key, success, error) {
var name = this.name;
var defaultError = this.defaultError;
var keys = (key instanceof Array) ? key : [key];
openTransaction(this, IDBT_READ_ONLY, function (transaction) {
var values = [];
transaction.onerror = getError(error, defaultError, key, "read");
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(keys[0], values[0]);
}
};
for (var i = 0; i < keys.length; i++) {
// Some tools have issues because get is a javascript reserved word.
var objectStore = transaction.objectStore(name);
var request = objectStore.get.call(objectStore, keys[i]);
request.onsuccess = function (event) {
var record = event.target.result;
values.push(record ? record.v : undefined);
};
}
}, error);
};
/** Removes the specified key from the store
* @method module:store/indexeddb~IndexedDBStore#remove
* @param {String} key - The key
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.remove = function (key, success, error) {
var name = this.name;
var defaultError = this.defaultError;
var keys = (key instanceof Array) ? key : [key];
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onerror = getError(error, defaultError);
transaction.oncomplete = function () {
success();
};
for (var i = 0; i < keys.length; i++) {
// Some tools have issues because continue is a javascript reserved word.
var objectStore = transaction.objectStore(name);
objectStore["delete"].call(objectStore, keys[i]);
}
}, error);
};
/** Updates a key/value pair in the store
* @method module:store/indexeddb~IndexedDBStore#update
* @param {String} key - The key
* @param {Object} value - The value
* @param {Function} success - The success callback
* @param {Function} error - The error callback
*/
IndexedDBStore.prototype.update = function (key, value, success, error) {
var name = this.name;
var defaultError = this.defaultError;
var keys = [];
var values = [];
if (key instanceof Array) {
keys = key;
values = value;
} else {
keys = [key];
values = [value];
}
openTransaction(this, IDBT_READ_WRITE, function (transaction) {
transaction.onabort = getError(error, defaultError);
transaction.oncomplete = function () {
if (key instanceof Array) {
success(keys, values);
} else {
success(key, value);
}
};
for (var i = 0; i < keys.length && i < values.length; i++) {
var request = transaction.objectStore(name).openCursor(IDBKeyRange.only(keys[i]));
var record = { v: values[i] };
request.pair = { key: keys[i], value: record };
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
cursor.update(event.target.pair.value);
} else {
transaction.abort();
}
}
}
}, error);
};
module.exports = IndexedDBStore;