IndexedDB key generator resets after put-transaction - angularjs

This problem has me stumped.
For some reason, the autoincrementing key generator in indexedDB resets after performing and update on an existing object with a put-transaction, leading to overwrites of data in the database.
For my app, I'm using a self written IndexedDB service for angularJS with all the basic CRUD functions implemented.
I may also add that I'm developing with Ionic Framework, even though I doubt that is to blame.
Considering the service is a work-in-progress, I've let the key path for an object store default to "id" with an autoincrementing strategy.
The indices for the given store, nevertheless, are up to the user to decide in a specific object.
As an example:
dbHelper.objectStores = [{'employees',
indices: [{indexName: 'name', isUnique: false},
{indexName: 'phone', isUnique: true}]}];
This would, unless already created in the db, create the object store 'employees' with indices 'name' and 'phone', where 'phone' would have to be a unique value while 'name' would not.
Here is the implementation of the openDB function.
Please note that dbHelper.objectStores is supposed to be empty as it's up to the user to assign these properties before opening the db(or else it is defaulted).
angular.module('dbProvider', [])
.factory('$db', ['$window', function($window) {
// DB Object
var dbHelper = {};
// Properties - Are given defaults unless assigned manually by user before openDB is invoked.
dbHelper.dbName = 'defaultDB';
dbHelper.dbVersion = 1;
dbHelper.objectStores = [];
dbHelper.openDB = function(onCompleteCallback, onErrorCallback) {
console.log('Atempting to open db with name ' + dbHelper.dbName + '.');
var request = $window.indexedDB.open(dbHelper.dbName, dbHelper.dbVersion);
// Invoked by indexedDB if version changes
request.onupgradeneeded = function(e) {
console.log('Version change. Current version: ' + dbHelper.dbVersion);
var db = e.target.result;
e.target.transaction.onerror = onErrorCallback;
if(dbHelper.objectStores.length === 0) {
dbHelper.objectStores.push({name:'defaultStore', indices: []});
}
for(var store in dbHelper.objectStores) {
if(db.objectStoreNames.contains(dbHelper.objectStores[store].name)) {
console.log(dbHelper.objectStores[store].name + ' deleted.');
db.deleteObjectStore(dbHelper.objectStores[store].name);
}
var newStore = db.createObjectStore(dbHelper.objectStores[store].name, {keyPath: "id", autoIncrement: true});
for(var index in dbHelper.objectStores[store].indices) {
newStore.createIndex(dbHelper.objectStores[store].indices[index].indexName,
dbHelper.objectStores[store].indices[index].indexName,
{unique : dbHelper.objectStores[store].indices[index].isUnique});
}
console.log(dbHelper.objectStores[store].name + ' created.');
}
};
request.onsuccess = function(e) {
console.log('DB ' + dbHelper.dbName + ' open.');
dbHelper.indexedDB.db = e.target.result;
onCompleteCallback();
};
request.onerror = onErrorCallback;
};
Here are some of the CRUD functions(the ones in question):
dbHelper.findItemWithIndex = function(keyValue, storename,
onCompleteCallback,onErrorCallback) {
var db = dbHelper.indexedDB.db;
var trans = db.transaction([storename], "readwrite");
var store = trans.objectStore(storename);
var index = store.index(keyValue.key);
index.get(keyValue.value).onsuccess = function(event) {
onCompleteCallback(event.target.result);
};
};
dbHelper.addItemToStore = function(item, storename,
onCompleteCallback, onErrorCallback) {
var db = dbHelper.indexedDB.db;
var trans = db.transaction([storename], "readwrite");
var store = trans.objectStore(storename);
var request = store.add(item);
trans.oncomplete = onCompleteCallback;
request.onerror = onErrorCallback;
};
dbHelper.deleteItemFromStore = function(itemId, storename,
onCompleteCallback, onErrorCallback) {
var db = dbHelper.indexedDB.db;
var trans = db.transaction([storename], "readwrite");
var store = trans.objectStore(storename);
var request = store.delete(itemId);
trans.oncomplete = onCompleteCallback;
request.onerror = onErrorCallback;
};
dbHelper.updateItem = function(item, storename, onCompleteCallback, onErrorCallback) {
var db = dbHelper.indexedDB.db;
var trans = db.transaction([storename], "readwrite");
var store = trans.objectStore(storename);
var request = store.put(item);
trans.oncomplete = onCompleteCallback;
request.onerror = onErrorCallback;
};
Finally, the code from the controller where the transactions are invoked.
The strategy here, is that the item is added to the db using the addItemToStore function the first time it is persisted, and then afterwards the updateItem function.
After adding the first time, the object is immediately fetched in order to keep working on it with the assigned id from the db.
$scope.updateTemplate = function() {
console.log('Saving..');
var onCompleteCallback = {};
if(!$scope.formTemplate.firstSave) {
onCompleteCallback = $scope.updateModel;
} else {
$scope.formTemplate.firstSave = false;
onCompleteCallback = $scope.setId;
}
$db.updateItem($scope.formTemplate, $scope.objectStore.name,
onCompleteCallback, $scope.dbError);
};
$scope.newItem = function() {
$db.addItemToStore($scope.formTemplate, $scope.objectStore.name,
$scope.setId, $scope.dbError);
};
$scope.setId = function() {
$db.findItemWithIndex(
{key: 'title',
value: $scope.formTemplate.title},
$scope.objectStore.name,
function(result) {
console.log(JSON.stringify(result));
$scope.formTemplate = result;
},
function(error) {
$scope.dbError(error);
});
}
It's here everything goes to hell.
I add an object, go back to another view and find it in the list with id=1.
I add another object, go back to the list view, and there it is with id=2.
And so forth and so forth..
Then, after updating either of the objects with the $scope.updateTemplate function, which also works like a charm, things get interesting:
The next object added gets id=1 and totally erases good old numero uno from earlier.
The next objects also get id's that cause them to replace the already existing objects.
What could cause this?
For testing I'm using Safari 8 in OS 10.10 and I'm deploying to an LGG2 with KitKat 4.4.2.

To be honest, I skimmed, but I saw this, "Safari 8" - the latest iOS and Safari have serious bugs with IndexedDB: http://www.raymondcamden.com/2014/9/25/IndexedDB-on-iOS-8--Broken-Bad

In iOS9, many of the IndexedDb bugs are fixed, but not all. We are currently testing on iOS9 Beta 2 and this particular bug that you found is not fixed.
We were able to work around this problem by not using autoincrement on our object stores. We just manually find the max key value and increment that.
Inserting an object looks something like this:
var store = db.transaction([entity], "readwrite").objectStore(entity);
store.openCursor(null, "prev").onsuccess = function (event) {
var maxKey = event.target.result.key || 0;
object.id = maxKey + 1;
store.add(object);
}

Related

Custom query in angular-indexedDB

I am using bramski/angular-indexedDB in my application. Basic CRUD operations are working fine, but the custom queries are not working as expected.
I am using the code
angular.module('myModuleName', ['indexedDB'])
.config(function ($indexedDBProvider) {
$indexedDBProvider
.connection('myIndexedDB')
.upgradeDatabase(1, function(event, db, tx){
var objStore = db.createObjectStore('people', {keyPath: 'ssn'});
objStore.createIndex('name_idx', 'age', {unique: false});
objStore.createIndex('name_idx, age_idx', ['name', 'age'] , {unique: false});
});
Basic query operations are working like follows
$indexedDB.openStore('people', function(x){
var find = x.query();
find = find.$eq('John');
find = find.$index("name_idx");
x.eachWhere(find).then(function(e){
$scope.list= e;
});
});
which results following query.
select * from people where name='John'
But, in the above scenario how we can execute custom quires like
select * from people where name='John' and age='25';
delete from people where name='John' and age='25';
The library you are using doesn't have complex queries, however you can write a pure-js solution for it, similar to this:
First you need to define your index as:
objStore.createIndex('name_age_idx', ['name', 'age'] , {unique: false});
Then you can have a search query for only those values that match the search result
searchIndexedDB = function (name, age, callback) {
var request = indexedDB.open(dbName);
request.onsuccess = function(e) {
var db = e.target.result;
var trans = db.transaction(objectStoreName, 'readonly');
var store = trans.objectStore(objectStoreName);
var index = store.index('name_age_idx');
var keyRange = IDBKeyRange.only([name, age]);
// open the index for all objects with the same name and age
var openCursorRequest = index.openCursor(keyRange);
openCursorRequest.onsuccess = function(e) {
let result = e.target.result;
// first check if value is found
if(result){
callback(result.value); // your callback will be called per object
// result.delete() - to delete your object
result.continue(); // to continue itterating - calls the next cursor request
}
};
trans.oncomplete = function(e) {
db.close();
};
openCursorRequest.onerror = function(e) {
console.log("Error Getting: ", e);
};
};
request.onerror = myStorage.indexedDB.onerror;
}
If you need a range from and too index, all you need is change the keyrange to:
var keyRange = IDBKeyRange.bound([name,fromAge], [value, toAge]);

How to merge REST call results in Angular app more efficiently

I have an Angular SPA running on a SharePoint 2013 page. In the code, I'm using $q to pull data from 10 different SharePoint lists using REST and then merging them into one JSON object for use in a grid. The code runs and outputs the intended merged data but it's leaky and crashes the browser after a while.
Here's the code in the service:
factory.getGridInfo = function() {
var deferred = $q.defer();
var list_1a = CRUDFactory.getListItems("ListA", "column1,column2,column3");
var list_1b = CRUDFactory.getListItems("ListB", "column1,column2,column3");
var list_2a = CRUDFactory.getListItems("ListC", "column4");
var list_2b = CRUDFactory.getListItems("ListD", "column4");
var list_3a = CRUDFactory.getListItems("ListE", "column5");
var list_3b = CRUDFactory.getListItems("ListF", "column5");
var list_4a = CRUDFactory.getListItems("ListG", "column6");
var list_4b = CRUDFactory.getListItems("ListH", "column6");
var list_5a = CRUDFactory.getListItems("ListI", "column7");
var list_5b = CRUDFactory.getListItems("ListJ", "column7");
$q.all([list_1a, list_1b, list_2a, list_2b, list_3a, list_3b, list_4a, list_4b, list_5a, list_5b])
.then(function(results){
var results_1a = results[0].data.d.results;
var results_1b = results[1].data.d.results;
var results_2a = results[2].data.d.results;
var results_2b = results[3].data.d.results;
var results_3a = results[4].data.d.results;
var results_3b = results[5].data.d.results;
var results_4a = results[6].data.d.results;
var results_4b = results[7].data.d.results;
var results_5a = results[8].data.d.results;
var results_5b = results[9].data.d.results;
var combined_1 = results_1a.concat(results_1b);
var combined_2 = results_2a.concat(results_2b);
var combined_3 = results_3a.concat(results_3b);
var combined_4 = results_4a.concat(results_4b);
var combined_5 = results_5a.concat(results_5b);
for(var i = 0; i < combined_1.length; i++){
var currObj = combined_1[i];
currObj["column4"] = combined_2[i].column4;
currObj["column5"] = combined_3[i].column5;
currObj["column6"] = combined_4[i].column6;
currObj["column7"] = combined_5[i].column7;
factory.newObjectArray[i] = currObj;
}
deferred.resolve(factory.newObjectArray);
},
function (error) {
deferred.reject(error);
});
return deferred.promise;
};
Here's the REST call in CRUDFactory:
factory.getListItems = function (listName, columns){
var webUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getByTitle('"+listName+"')/items?$select="+columns+"&$top=5000";
var options = {
headers: { "Accept": "application/json; odata=verbose" },
method: 'GET',
url: webUrl
};
return $http(options);
};
And then here's the controller bit:
$scope.refreshGridData = function(){
$scope.hideLoadingGif = false;
$scope.GridData = "";
GlobalFactory.getGridInfo()
.then(function(){
$scope.GridData = GlobalFactory.newObjectArray;
$scope.hideLoadingGif = true;
});
};
UPDATE 1: Per request, Here's the HTML (just a simple div that we're using angular-ui-grid on)
<div ui-grid="GridOptions" class="grid" ui-grid-selection ui-grid-exporter ui-grid-save-state></div>
This code starts by declaring some get calls and then uses $q.all to iterate over the calls and get the data. It then stores the results and merges them down to 5 total arrays. Then, because my list structure is proper and static, I'm able to iterate over one of the merged arrays and pull data from the other arrays into one master array that I'm assigning to factory.newObjectArray, which I'm declaring as a global in my service and using as my grid data source.
The code runs and doesn't kick any errors up but the issue is with (I believe) the "getGridInfo" function. If I don't comment out any of the REST calls, the browser uses 45 MB of data that doesn't get picked up by GC which is then compounded for each click until the session is ended or crashes. If I comment out all the calls but one, my page only uses 18.4 MB of memory, which is high but I can live with it.
So what's the deal? Do I need to destroy something somewhere? If so, what and how? Or does this relate back to the REST function I'm using?
UPDATE 2: The return result that the grid is using (the factory.newObjectArray) contains a total of 5,450 items and each item has about 80 properties after the merge. The code above is simplified and shows the pulling of a couple columns per list, but in actuality, I'm pulling 5-10 columns per list.
At the end of the day you are dealing with a lot of data, so memory problems are potentially always going to be an issue and you should probably consider whether you need to have all the data in memory.
The main goal you should probably be trying to achieve is limiting duplication of arrays, and trying to keep the memory footprint as low as possible, and freeing memory as fast as possible when you're done processing.
Please consider the following. You mention the actual number of columns being returned are more than your example so I have taken that into account.
factory.getGridInfo = function () {
var deferred = $q.defer(),
// list definitions
lists = [
{ name: 'ListA', columns: ['column1', 'column2', 'column3'] },
{ name: 'ListB', columns: ['column1', 'column2', 'column3'], combineWith: 'ListA' },
{ name: 'ListC', columns: ['column4'] },
{ name: 'ListD', columns: ['column4'], combineWith: 'ListC' },
{ name: 'ListE', columns: ['column5'] },
{ name: 'ListF', columns: ['column5'], combineWith: 'ListE' },
{ name: 'ListG', columns: ['column6'] },
{ name: 'ListH', columns: ['column6'], combineWith: 'ListG' },
{ name: 'ListI', columns: ['column7'] },
{ name: 'ListJ', columns: ['column7'], combineWith: 'ListI' },
],
// Combines two arrays without creating a new array, mindful of lenth limitations
combineArrays = function (a, b) {
var len = b.length;
for (var i = 0; i < len; i = i + 5000) {
a.unshift.apply(a, b.slice(i, i + 5000));
}
};
$q.all(lists.map(function (list) { return CRUDFactory.getListItems(list.name, list.columns.join()); }))
.then(function (results) {
var listResultMap = {}, var baseList = 'ListA';
// map our results to our list names
for(var i = 0; i < results.length; i++) {
listResultMap[lists[i].name] = results[i].data.d.results;
}
// loop around our lists
for(var i = 0; i < lists.length; i++) {
var listName = lists[i].name, combineWith = lists[i].combineWith;
if(combineWith) {
combineArrays(listResultMap[combineWith], listResultMap[listName]);
delete listResultMap[listName];
}
}
// build result
factory.newObjectArray = listResultMap[baseList].map(function(item) {
for(var i = 0; i < lists.length; i++) {
if(list.name !== baseList) {
for(var c = 0; c < lists[i].columns.length; c++) {
var columnName = lists[i].columns[c];
item[columnName] = listResultMap[list.name][columnName];
}
}
}
return item;
});
// clean up our remaining results
for (var i = 0; i < results.length; i++) {
delete results[i].data.d.results;
delete results[i];
}
deferred.resolve(factory.newObjectArray);
},
function (error) {
deferred.reject(error);
});
return deferred.promise;
};
I would suggest to add some sort of paging option... It's perhaps not a great idea to add all results to one big list.
Next i would suggest against ng-repeat or add a "track by" to the repeat function.
Check out: http://www.alexkras.com/11-tips-to-improve-angularjs-performance/
Fiddler your queries, the issue is probably rendering all the elements in the dom... Which could be kinda slow ( investigate)

GeoFire geoQuery returning data

I'm newbie with Firebase + GeoFire and I'm having trouble with geoFire query function.
I want to add in an array the results from geoQuery function and return it in a function. But the data I have manipulating inside geoQuery.on method seems out of scope or not available or due to promises, I dont know... the fact is outside the geoquery.on method the variable sellers is empty.
How can I return results from geoQuery and save it into a return variable
//Set seller position in firebase db
var setPosition = function() {
navigator.geolocation.getCurrentPosition(setPositionSuccess, error, options);
//navigator.geolocation.watchPosition(setPositionSuccess, positionError, { enableHighAccuracy:true })
};
//Get sellers near buyer position
var getSellersForCurrentPosition = function() {
navigator.geolocation.getCurrentPosition(getPositionSuccess, error, options);
//navigator.geolocation.watchPosition(positionSuccess, positionError, { enableHighAccuracy:true })
};
//Callback function from html 5 geo api
function getPositionSuccess(pos) {
var crd = pos.coords;
var currentPosition = [crd.latitude, crd.longitude];
// Query radius
var radiusInKm = 2;
var firebaseRef = new Firebase(FBURL + "/geofire/sellers/");
var geoFire = new GeoFire(firebaseRef);
var geoQuery = geoFire.query({
center: currentPosition,
radius: radiusInKm
});
var sellers = [];
var oneSeller = {};
var onKeyEnteredRegistration = geoQuery.on("key_entered", function(key, location, distance) {
oneSeller = {
id: key,
distance: distance,
location: location
};
sellers.push(oneSeller);
});
var onReadyRegistration = geoQuery.on("ready", function() {
geoQuery.cancel();
});
return sellers;
}
By the way, how accurate is html5 geolocation? Is it different between desktop browser and mobile browser?
Geofire monitors the sellers in the range you indicate. Any time a seller enters/exits the range, it fires a key_entered or key_exited event. These events can happen at any time after you start the query. In JavaScript terms this is often described as: the callbacks happen asynchronously.
A simple event flow, might explain what happens best:
you call getPositionSuccess()
you start a Geoquery to monitor the sellers that are in range: geoFire.query()
no sellers are immediately in range, so your callback doesn't fire
the getPositionSuccess() function is done and exits
a seller comes in range
GeoFire fires the key_entered event and your callback runs
but getPositionSuccess() has already exited, so how can it return a value?
Even if you were to wait for the first seller to come into range before returning (not possible in a browser, but it is possible in other languages/environments), how will you return the value when a second seller comes in range?
For this reason, you have to deal with asynchronous data differently. Typically you do this by moving the code that would call the getPositionSuccess() function into the function.
Say you are now trying to do this:
var sellers = getPositionSuccess(pos);
sellers.forEach(function(seller) {
addSellerToMap(seller);
});
To handle the asynchronous nature of the events, you'd move this code into getPositionSuccess:
//Callback function from html 5 geo api
function getPositionSuccess(pos) {
var crd = pos.coords;
var currentPosition = [crd.latitude, crd.longitude];
// Query radius
var radiusInKm = 2;
var firebaseRef = new Firebase(FBURL + "/geofire/sellers/");
var geoFire = new GeoFire(firebaseRef);
var geoQuery = geoFire.query({
center: currentPosition,
radius: radiusInKm
});
var oneSeller = {};
geoQuery.on("key_entered", function(key, location, distance) {
oneSeller = {
id: key,
distance: distance,
location: location
};
addSellerToMap(oneSeller);
});
}
I understand that in your use-case your sellers won't move, so it may be more intuitive to think of them as a static list. But even in this case, the results are loaded from a remote database and it will take some time before that data is loaded. The modern web loads data asynchronously and all your code will have to deal with it in a way similar to what I outlined above.

Add variable to an existing json object

request = myService.getCases();
request.then(
function(payload) {
$scope.cases = payload.data;
var time = Math.floor((Date.now() - Date.parse($scope.cases[i].date_case_modified))/(60000*60*24));
$scope.cases.duration.push(time);
}
});
Inside the controller I am trying to tack on the cases.duration onto the cases object but it wont add it onto the object that is returned. Any ideas?
I think you just need to introduce a forEach as shown here:
request = myService.getCases();
request.then(
function(payload) {
$scope.cases = payload.data;
angular.forEach($scope.cases, function (el) {
var time = Math.floor((Date.now() - Date.parse(el.date_case_modified))/(60000*60*24));
el.duration = time;
});
}
});
Hope this helps

How to extend returned objects in the list returned by $asArray?

I'm having trouble decorate the objects in my list returned by $asArray in angularfire with a new method (not decorating the array itself).
The angularfire documentation seems to suggest that the right way to do this is to override the $$added method in the factory for $FirebaseArray, returning a new object that either encapsulates or extends the snapshot that gets passed in to that method. From the documentation:
// an object to return in our JokeFactory
app.factory("Joke", function($firebaseUtils) {
function Joke(snapshot) {
this.$id = snapshot.name();
this.update(snapshot);
}
Joke.prototype = {
update: function(snapshot) {
// apply changes to this.data instead of directly on `this`
this.data = snapshot.val();
},
makeJoke: function() {
alert("Why did the " + this.animal + " cross the " + this.obstacle + "?");
},
toJSON: function() {
// since we didn't store our data directly on `this`, we need to return
// it in parsed format. We can use the util function to remove $ variables
// and get it ready to ship
return $firebaseUtils.toJSON(this.data);
}
};
return Joke;
});
app.factory("JokeFactory", function($FirebaseArray, Joke) {
return $FirebaseArray.$extendFactory({
// change the added behavior to return Joke objects
$$added: function(snap) {
return new Joke(snap);
},
// override the update behavior to call Joke.update()
$$updated: function(snap) {
this.$getRecord(snap.name()).update(snap);
}
});
});
However, when I do this in my code, nothing ever gets added to the array, although I can see from outputting to the console that it is getting called.
var printMessageObjConstructor = function(snap) {
this.$id = snap.name();
this.snapshot = snap;
this.$update = function(snap) {
this.snapshot = snap;
};
this.printMessage = function() {
return this.author + "'s question is: " + this.body;
};
};
var ref = new Firebase("https://danculley-test.firebaseio.com/questions");
//What Am I Doing Wrong Here?
var arrayFactory = $FirebaseArray.$extendFactory({
$$added: function(snap, prevChild) {
var x = new printMessageObjConstructor(snap);
console.log("I am being called from FirebaseDecoratedCtlOverloadAddedinNewObj.");
return x;
},
$createObject: function(snap) {
return new printMessageObjConstructor(snap);
},
$$updated: function(snap) {
var i = this.$indexFor(snap.name());
var q = this.$list[i];
q.$update(snap);
}
});
var sync = $firebase(ref, {arrayFactory:arrayFactory});
var list = sync.$asArray();
list.$loaded(function(list) {
$scope.questions = list;
});
I've set up a new plunk stripped down to show the issue with a couple other use cases that I've tried. (The actual method I'm adding is more complex and isn't related to the view, but I wanted to do something simple to reproduce the issue.)
I think the issue is that I don't quite understand what exactly $$added is supposed to return, or what additional behavior beside returning the value to be stored $$added is supposed to have. There also doesn't really seem to be an $$added on the prototype or on $FirebaseArray to call as a super to get the default behavior. Can someone point me in the right direction?
UPDATE
For the benefit of others, after reviewing the like that Kato posted, I was able to solve the issue by adding the following, almost all copied directly from the source except for the commented line below.
$$added: function(snap, prevChild) {
var i = this.$indexFor(snap.name());
if( i === -1 ) {
var rec = snap.val();
if( !angular.isObject(rec) ) {
rec = { $value: rec };
}
rec.$id = snap.name();
rec.$priority = snap.getPriority();
$firebaseUtils.applyDefaults(rec, this.$$defaults);
//This is the line that I added to what I copied from the source
angular.extend(rec, printMessageObj);
this._process('child_added', rec, prevChild);
}
}
For the benefit of others, after reviewing the link that Kato posted, I was able to solve the issue by adding the following, almost all copied directly from the source except for the commented line below.
$$added: function(snap, prevChild) {
var i = this.$indexFor(snap.name());
if( i === -1 ) {
var rec = snap.val();
if( !angular.isObject(rec) ) {
rec = { $value: rec };
}
rec.$id = snap.name();
rec.$priority = snap.getPriority();
$firebaseUtils.applyDefaults(rec, this.$$defaults);
//This is the line that I added to what I copied from the source
angular.extend(rec, printMessageObj);
this._process('child_added', rec, prevChild);
}
}

Resources