IndexedDB cursors - javascript

my question is pretty simple.
I can't understand an example from the MDN article about iterating a result from an IndexedDB. Specifically, I can't see any loop used for iteration. There is no for/while/do cycle. Here is the example:
function displayData() {
var transaction = db.transaction(['rushAlbumList'], "readonly");
var objectStore = transaction.objectStore('rushAlbumList');
objectStore.openCursor().onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
var listItem = document.createElement('li');
listItem.innerHTML = cursor.value.albumTitle + ', ' + cursor.value.year;
list.appendChild(listItem);
cursor.continue();
} else {
console.log('Entries all displayed.');
}
};
};

The "loop" happens implicitly. Each successful advance of the cursor results in a "success" event, which will trigger a call to the handler assigned to the "onsuccess" property of the request. Thus, the iteration happens because of this line:
cursor.continue();

Related

Workaround for new Edge Indexeddb bug?

Microsoft released update kb4088776 in the past couple days, which has had a devastating effect on performance of indexedDb openCursor.
The simple fiddle here shows the problem. With the update, the "retrieval" time is 40 seconds or more. Prior to the update, it is around 1 second.
https://jsfiddle.net/L7q55ad6/23/
Relevant retrieval portion is here:
var _currentVer = 1;
function _openDatabase(fnSuccess) {
var _custDb = window.indexedDB.open("MyDatabase", _currentVer);
_custDb.onsuccess = function (event) {
var db = event.target.result;
fnSuccess(db);
}
_custDb.onerror = function (event) {
_custDb = null;
fnSuccess(null); // should use localData
}
_custDb.onupgradeneeded = function (event) {
var db = event.target.result;
var txn = event.target.transaction;
// Create an objectStore for this database
if (event.oldVersion < _currentVer) {
var customer = db.createObjectStore("customer", { keyPath: "guid" });
var index = customer.createIndex("by_id", "id", { unique: false });
}
};
}
function _retrieveCustomers(fn) {
_openDatabase(function (db) {
if (db == null)
{
alert("not supported");
return;
}
var customers = [];
var transaction = db.transaction("customer", "readonly");
var objectStore = transaction.objectStore("customer");
if (typeof objectStore.getAll === 'function') {
console.log("using getAll");
objectStore.getAll().onsuccess = function (event) {
fn(event.target.result);
};
}
else {
console.log("using openCursor");
objectStore.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
customers.push(cursor.value);
cursor.continue();
}
else {
fn(customers);
}
};
}
});
}
The time to create and add the customers is basically normal, only the retrieval is bad. Edge has never supported the getAll method and it still doesn't after the update.
The only workaround I can think of would be to use localStorage instead, but unfortunately our data set is too large to fit into the 10MB limit. It is actually faster now to retrieve from our servers and convert the text to javascript objects, defeating the main purpose of indexeddb.
I don't have Edge so I can't test this, but does it happen with get too, or just openCursor? If get still performs well, you could store an index (in your example, the list of primary keys; in your real app, maybe something more complicated) in localStorage, and then use that to call get on each one.

Firebase, can't listen on child's ref() - JavaScript

I just started to use Firebase with JavaScript, I've read the doc on Firebase's site, but it talks just about the immediate reference of a node which invokes on('child_added',function(){}).
I'm trying to find a particular node to listen for.
But it seems that it can't invoke the handler startListening when data is changed.
Here is my code
var myFire = new Firebase('https://myProject.firebaseio.com/chat');
var myConversation = "notAssigned";
myFire.once("value", function(snapshot) {
snapshot.forEach(function(childSnapshot) {
childSnapshot.forEach(function(i){
var user = i.val();
if (user == "10") myConversation = childSnapshot.child("messages").ref();
});
});
});
var textInput = document.querySelector('#text');
var postButton = document.querySelector('#post');
postButton.addEventListener("click", function() {
var msgUser = "10";
var msgText = textInput.value;
var date = js_dd_mm_yyyy_hh_mm_ss (); //function that returns actual date
myConversation.push({content:msgText,date:date, status:"1", type:"text", user:msgUser});
textInput.value = "";
});
/** Function to add a data listener **/
alert (myConversation.key());
var startListening = function() {
myConversation.on('child_added', function(snapshot) {
alert("here i am");
alert (snapshot.content);
});
}
// Begin listening for data
startListening();
I think that I fail to manage references, although when I invoke push data is actually loaded, that is, when I look at Database in Firebase's console, the push method has successful pushed new node.

How to deal with asyncronous javascript in loops?

I have a forloop like this:
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname);
myphone.get(function(phonenumbers){
if(myphone.phonearray){
myperson.save();
//Can I put a break here?;
}
});
}
What it does is that it searches for phone-numbers in a database based on various first-names. What I want to achieve is that once it finds a number associated with any of the first names, it performs myperson.save and then stops all the iterations, so that no duplicates get saved. Sometimes, none of the names return any phone-numbers.
myphone.get contains a server request and the callback is triggered on success
If I put a break inside the response, what will happen with the other iterations of the loop? Most likely the other http-requests have already been initiated. I don't want them to perform the save. One solution I have thought of is to put a variable outside of the forloop and set it to save, and then check when the other callbacks get's triggered, but I'm not sure if that's the best way to go.
You could write a helper function to restrict invocations:
function callUntilTrue(cb) {
var done = false;
return function () {
if (done) {
log("previous callback succeeded. not calling others.");
return;
}
var res = cb.apply(null, arguments);
done = !! res;
};
}
var myperson = {
firstname: {
"tom": null,
"jerry": null,
"micky": null
},
save: function () {
log("save " + JSON.stringify(this, null, 2));
}
};
var cb = function (myperson_, phonenumbers) {
if (myperson_.phonearray) {
log("person already has phone numbers. returning.");
return false;
}
if (phonenumbers.length < 1) {
log("response has no phone numbers. returning.");
return false;
}
log("person has no existing phone numbers. saving ", phonenumbers);
myperson_.phonearray = phonenumbers;
myperson_.save();
return true;
};
var restrictedCb = callUntilTrue(cb.bind(null, myperson));
for (var name in myperson.firstname) {
var myphone = new phone(myperson, name);
myphone.get(restrictedCb);
}
Sample Console:
results for tom-0 after 1675 ms
response has no phone numbers. returning.
results for jerry-1 after 1943 ms
person has no existing phone numbers. saving , [
"jerry-1-0-number"
]
save {
"firstname": {
"tom": null,
"jerry": null,
"micky": null
},
"phonearray": [
"jerry-1-0-number"
]
}
results for micky-2 after 4440 ms
previous callback succeeded. not calling others.
Full example in this jsfiddle with fake timeouts.
EDIT Added HTML output as well as console.log.
The first result callback will only ever happen after the loop, because of the single-threaded nature of javascript and because running code isn't interrupted if events arrive.
If you you still want requests to happen in parallel, you may use a flag
var saved = false;
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname /* name? */);
myphone.get(function(phonenumbers){
if (!saved && myphone.phonearray){
saved = true;
myperson.save();
}
});
}
This will not cancel any pending requests, however, just prevent the save once they return.
It would be better if your .get() would return something cancelable (the request itself, maybe).
var saved = false;
var requests = [];
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname /* name? */);
var r;
requests.push(r = myphone.get(function(phonenumbers){
// Remove current request.
requests = requests.filter(function(i) {
return r !== i;
});
if (saved || !myphone.phonearray) {
return;
}
saved = true;
// Kill other pending/unfinished requests.
requests.forEach(function(r) {
 r.abort();
});
myperson.save();
}));
}
Even better, don't start all requests at once. Instead construct an array of all possible combinations, have a counter (a semaphore) and only start X requests.
var saved = false;
var requests = [];
// Use requests.length as the implicit counter.
var waiting = []; // Wait queue.
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname /* name? */);
var r;
if (requests.length >= 4) {
// Put in wait queue instead.
waiting.push(myphone);
continue;
}
requests.push(r = myphone.get(function cb(phonenumbers){
// Remove current request.
requests = requests.filter(function(i) {
return r !== i;
});
if (saved) {
return;
}
if (!myphone.phonearray) {
// Start next request.
var w = waiting.shift();
if (w) {
requests.push(w.get(cb));
)
return;
}
saved = true;
// Kill other pending/unfinished requests.
requests.forEach(function(r) {
r.abort();
});
myperson.save();
}));
}

Deleting a record from desktop Chrome IndexedDB vs from android Chrome IndexedDB

I am trying to delete a record from IndexedDB using the fallowing code:
DB.indexedDB.IDBTransaction.READ_WRITE="readwrite";
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
if ('webkitIndexedDB' in window) {
window.IDBTransaction = window.webkitIDBTransaction;
window.IDBKeyRange = window.webkitIDBKeyRange;
DB.indexedDB.IDBTransaction.READ_WRITE=IDBTransaction.READ_WRITE;
}
DB.indexedDB.idelete = function( storeName, indexValue, index, keyPathfield ){
var db = DB.indexedDB.db;
var transaction = db.transaction([storeName], DB.indexedDB.IDBTransaction.READ_WRITE);
var store = transaction.objectStore(storeName);
var sindex = store.index(index);
sindex.get(indexValue).onsuccess = function (event){
store.delete(event.target.result[keyPathfield]).onsuccess = function(event) {
document.getElementById("result").innerHTML+="deleted<br>";
};
}
}
it calls onsuccess but when I add a new record with the same indexValue and call idelete again and search for the record using the fallowing code:
DB.indexedDB.readAll=function(storeName, index){
var results=document.getElementById("result");
var db = DB.indexedDB.db;
var transaction = db.transaction([storeName]);
var store = transaction.objectStore(storeName);
var key = IDBKeyRange.lowerBound(0);
var cursor = store.openCursor(key);
var x=0;
cursor.onsuccess = function(event){
var result = event.target.result;
if(result)
{
x++;
var charx=x.toString();
results.innerHTML+=charx+result.value[index]+"<br>";
result.continue();
}
else
return;
}
if I am using Windows Chrome, the record is deleted correctly. But if I am using Android Chrome version M18.1, readAll can still find the record but idelete can't delete it because it was actually deleted.
How about just open key cursor instead of actually retrieving it.
sindex.openKeyCursor(indexValue).onsuccess = function (event){
var cursor = event.target.result;
if (cursor) {
// cursor.delete();
var key = cursor.key;
var primaryKey = cursor.primaryKey;
store.delete(primaryKey).onsuccess = function(event) {
document.getElementById("result").innerHTML+= key + ' (' + primaryKey + ") deleted<br>";
};
}
} else {
document.getElementById("result").innerHTML+= indexValue + ' not found.<br>";
}
}
The problem here is that there are multiple transactions in flight at once - I can tell this becuause idelete() doesn't return the transaction it creates, so there's no way to guarantee that readAll() executes after idelete().
instead you're going to need to make idelete() return the transaction, and then handle that:
foo = idelete(....).oncomplete = function() { readAll()...}
I think the fact that it happens to work in android is just a fluke of the implementation of chrome (which is single-process on android, so events tend to run more serially there)

javascript curtain (used to idicate busy process) not displaying for entire process. Probaly I dont understand the callback wll enough

I have a process where a user puts in a comma delimited list that is then processed one item at a time. I want to be able to indicate to the user that it is processing and let them know when it is done. So I used the curtain idea from Borgar's replay to ... Div Over Page.
This worked but the curtain disappears well before the process is done. I believe it is because each call in the forEach loop inside the importIDs function is called asynchronously thus returning control back before it completes. (I know that is the idea behind asynchronous code.) So what do I need to do to keep the curtain up until it is done?
HTML that calls the function
<label>Import list:</label><input style="width: 30em;" type="text" id="jcDelimitedList"/><input onclick="importIDs('jcDelimitedList','selectedJobCodes','AddJobCode');" type="button" value="Do It"/>
import function
importIDs = function(dList,nodeId,actionName){
busyProcess(function(){
var ids = dojo.byId(dList).value;
dojo.forEach(ids.split(","),function(entry,i){doAssosiate(nodeId,actionName,null,dojo.byId(entry));});
});
};
which calls the busy function
busyProcess = function(callback){
var ret;
var curtain = document.body.appendChild(document.createElement('div'));
curtain.id = "curtain";
curtain.onkeypress = curtain.onclick = function(){return false;};
try{
ret = callback();
}finally{
curtain.parentNode.removeChild(curtain);
}
return ret;
};
which in turn processes the passed in loop that calls doAssosiate for each element in the array:
doAssosiate = function(nodeID,actionName,evt,aNode){
if(aNode==null)return;
var node = dojo.byId(nodeID);
var newNode;
var target = evt!=null?evt.target:aNode;
newNode = dojo.clone(target);
var tID = target.id;
var sUrl = "action/groups." + actionName + "?id=" + tID + "&groupID=" + groupID + bustCache("&");
var get = getDefered(sUrl);
get.addCallback(function(data){
node.appendChild(newNode);
target.parentNode.removeChild(target);
return data;
});
get.addCallback(function(data){
dojo.behavior.apply();
return data;
});
};
which runs each url with getDefered
getDefered = function(url){
console.log(url);
return dojo.xhrGet({
url:url
});
};
I think I have all the relevant code above. I thought using sending the loop through the busy process would hold until finished and then return instead it holds until it fires off each iteration and then returns before they are complete.
As always thanks for any input and criticism.
A couple of interesting bugs in the above. Mainly if the list of ids in the array is to large it troughs more traffic at the database then it can handle. So I went to a recursive function instead of the foreach loop. Then removed the responsibility of turning off the curtain from busyProcess function and added it to the recursive call that turned the curtain off on exit of the recursion. For anyone that cares below are the changed functions. Also change to use dojox.widget.Standby for the curtain.
busyProcess = function(callback){
var ret;
document.body.appendChild(standby.domNode);
standby.show();
ret = callback();
return ret;
};
instead of calling doAssosiate now it calls assosiateAll;
importIDs = function(dList,nodeId,actionName){
busyProcess(function(){
var ids = dojo.byId(dList).value;
var sourceNode = dojo.byId(nodeId);
assosiateAll(ids.split(","),0,sourceNode,actionName);
});
};
assosiateAll = function(idArray,idx,sourceNode,actionName){
if(idx <= idArray.length ){
var target = dojo.byId(idArray[idx]);
if(target == null){
idx++;
assosiateAll(idArray,idx,sourceNode,actionName);
}else{
var newNode = dojo.clone(target);
var tID = target.id;
var sUrl = "action/groups." + actionName + "?id=" + tID + "&groupID=" + groupID + bustCache("&");
var get = getDefered(sUrl);
get.addCallback(function(data){
sourceNode.appendChild(newNode);
target.parentNode.removeChild(target);
return data;
});
get.addCallback(function(data){
idx++;
assosiateAll(idArray,idx,sourceNode,actionName);
return data;
});
get.addCallback(function(data){
dojo.behavior.apply();
if (idx == (idArray.length -1)) {
standby.hide();
}
return data;
});
}
}
};

Categories

Resources