"Cannot call method 'transaction' of undefined" error in indexedDB - javascript

Below is my code. It runs fine for the first time but once I refresh the page it generates an uncaught TypeError: Cannot call method 'transaction' of undefined.
Basically I retrieve from data from a database and encode it into a usable JSON datatype to avoid insert issues into my tables.
$(document).ready( function() {
//prefixes of implementation that we want to test
window.indexedDB = window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB;
//prefixes of window.IDB objects
window.IDBTransaction = window.IDBTransaction ||
window.webkitIDBTransaction ||
window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange ||
window.webkitIDBKeyRange ||
window.msIDBKeyRange
if(!window.indexedDB){ //alert user if browser does not support
window.alert("Your browser doesn't support a stable version of IndexedDB.")
}
var request = window.indexedDB.open("runningClub", 1); //create a runningClub database
var browserDB;
request.onsuccess = function(event){
browserDB = request.result;
};
var raceIDs = [];
$(".raceID").each( function () {
raceIDs.push(" raceID = " + $(this).val());
});
$.ajax({
url: 'controller/AJAX/userAction.php',
type: 'post',
dataType: 'json',
data: { statement : "getRaceResult", raceIDs : raceIDs },
success: function(data) {
const results = data;
request.onupgradeneeded = function(event){
var browserDB = event.target.result;
var objectStore = browserDB.createObjectStore("raceResult", {keyPath: "resultID"});
for(var i in results){
objectStore.add(results[i]);
}
}
var objectStore = browserDB.transaction("raceResult").objectStore("raceResult");
objectStore.openCursor().onsuccess = function(event){
var cursor = event.target.result;
if (cursor){
$("#memberName").append("<option>" + cursor.value.memberName + "</option>");
cursor.continue();
}
};
}
});
});

Thanks for posting the (almost) entirety of your code. Unfortunately I can't fully replicate your issue without knowing the structure of the data object coming from your backend. Mind publishing that as well?
That said, I'm rather certain your issue is the use of const.
const results = data;
I do not believe this type of data to be "structured clone"-able. Given that JSON is fully clonable, which is what I imagine to be returned from your API (vs. e.g. JavaScript that could be evaulated to produce a function), the usage of this keyword seems the likely culprit of your issues.
Update 1 Working through this, quite a few issues with your async programming.
database is opened before ajax request, rather than after successful callback. seems prudent to move this into the callback.
browserDB is defined in a success callback that isn't guaranteed to run before the async operation completes (thus browserDB becomes undefined)
request.onsuccess is called twice (you may not have realized this, because it's weird behavior): both when no upgradeneeded event fires and when an upgradeneeded event fires, so you're redefining browserDB perhaps accidentally when the DB doesn't yet exist
Update 2 Here's a mostly working, simplified Fiddle. I'm sad at how much time I spent making your code work.
More issues (beyond const):
hardcoded database version
reusing versionchange event for adding/getting data is a bad idea
need to use a readwrite transaction to add data
openCursor - but don't supply parameters for resultID keypath
you are not listening for successful adds before getting the data
you are not chaining your success events for adds before getting the data
The current loop approach was untenable. But I got it working with a single add and a cursor get. You can refactor back into a multiple-add approach but you'll need to watch your async (based on what I see, I'd suggest you stick with simple).
$.ajax({
url: '/echo/json/',
type: 'post',
dataType: 'json',
data: {
json: JSON.stringify({
resultID: new Date().getTime(),
raceID: "1",
memberID: "824",
runtime: "00:31:26",
memberName‌: "Mark Hudspith"
})
},
success: function (data) {
var request,
upgrade = false,
doTx = function (db, entry) {
addData(db, entry, function () {
getData(db);
});
},
getData = function (db) {
db.transaction(["raceResult"], "readonly").objectStore("raceResult").openCursor(IDBKeyRange.lowerBound("0")).onsuccess = function (event) {
var cursor = event.target.result;
if (null !== cursor) {
console.log("YOUR DATA IS HERE", cursor.value.memberName‌);
cursor.continue();
}
};
},
addData = function (db, entry, finished) {
var tx = db.transaction(["raceResult"], "readwrite");
tx.addEventListener('complete', function (e) {
finished();
});
tx.objectStore("raceResult").add(entry);
};
request = window.indexedDB.open("runningClub");
request.onsuccess = function (event) {
if (!upgrade) {
doTx(request.result, data);
}
};
request.onupgradeneeded = function (event) {
var db = event.target.result;
db.createObjectStore("raceResult", {
keyPath: "resultID"
});
setTimeout(function () {
doTx(db, data);
}, 5000);
}
}
});

Related

How to insert Data into IndexedDB within an AJAX response?

lets say I have a the following code that creates the database OUTSIDE of my Ajax call:
// This works on all devices/browsers, and uses IndexedDBShim as a final fallback
var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
// Open (or create) the database
var open = indexedDB.open("MyDatabase", 1);
// Create the schema
open.onupgradeneeded = function() {
var db = open.result;
var store = db.createObjectStore("MyObjectStore", {keyPath: "id"});
var index = store.createIndex("NameIndex", ["name.last", "name.first"]);
};
and then I want to store some data into this database within my Ajax call like so:
$.ajax({
url: "https://sometise.com,
headers: {
"Authorization": "TOKEN"
},
type: "GET",
success: function (data) {
$(data).each(function (index, el) {
var myArray = {
ID: el.id,
Content: data
}
///ADDING THE ABOVE ARRAY TO THE DATABASE LIKE SO HERE
open.onsuccess = function() {
// Start a new transaction
var db = open.result;
var tx = db.transaction("MyObjectStore", "readwrite");
var store = tx.objectStore("MyObjectStore");
var index = store.index("NameIndex");
// Add some data
store.put({id: el.id, name: {first: myArray, last: "Doe"}});
// Close the db when the transaction is done
tx.oncomplete = function() {
db.close();
};
}
});
}
});
but the above code doesn't do anything!
any pointers would be appreciated.
The open.onsuccess event gets fired only once. This should happen as soon as the DB is ready, which likely happens before the ajax call receives a response from the server. So the event gets fired before you attach the listener, meaning it never gets handled.
You have to store the onsuccess result outside of the ajax callback. Or you can use a promisified version of IDB like https://github.com/jakearchibald/idb and await it inside the ajax callback.
Edit: An example using promises, await and the idb library I mentioned above:
const db = openDB(…);
async function doPost() {
const response = fetch('https://somesite.com')
const tx = (await db).transaction("MyObjectStore", "readwrite");
const store = tx.objectStore("MyObjectStore");
const index = store.index("NameIndex");
// Add some data
await store.put(await response);
await tx.done;
}
Should work like this, but I would recommend reading up about promises in general. Also consider using modern language features like const/let and fetch, they usually make live much easier.

Error "A mutation operation was attempted on a database that did not allow mutations." when trying to open a transaction inside success event of ajax

I just started my research on indexeddb for storing the data on the client-side. I was able to perform basic CRUD operations so I started to code the actual implementation case where user should retrieve the data from server in the form of json and then store it in the indexedDB objectStore. Now this is where I am facing problem, I know both ajax and transactions in IndexedDB are async ,so I'm calling transactions in the success event, it throws me error saying "InvalidStateError : A mutation operation was attempted on a database that did not allow mutations." on the line where I call transaction to open.
Here is the javascript code and the json file
// Javascript IndexedDB start
var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
// open db
var dbase;
var gotWidgetTemplates = false;
var widgetTemplets;
function init() {
var databaseName = "ADTClientDB";
var databaseVersion = 1;
var open = indexedDB.open(databaseName, databaseVersion);
open.onupgradeneeded = function (e) {
var db = e.target.result;
if (!db.objectStoreNames.contains("WidgetTemplate")) {
var widgetTemplateObj = db.createObjectStore("WidgetTemplate", {
keyPath: 'name' });
widgetTemplateObj.createIndex("image", "image", { unique: false });
widgetTemplateObj.createIndex("html", "html", { unique: false });
widgetTemplateObj.createIndex("css", "css", { unique: false });
widgetTemplateObj.createIndex("rules", "rules", { multiEntry: true });
}
}
open.onsuccess = function (e1) {
dbase = e1.target.result;
getTemplates(dbase);
dbase.close();
}
open.onerror = function () {
alert("Error opening the database");
}
function getTemplates(dbase) {
$.ajax({
url: "../Sample Json files/widgetTemplates.json",
type: "GET",
success:
function (data) {
var obj = JSON.parse(JSON.stringify(data));
widgetTemplets = obj.widgets_info;
gotWidgetTemplates = true;
if (gotWidgetTemplates && widgetTemplets !== null) {
// I tried putting the transaction and store here,it was giving me same error..
for (var temp in widgetTemplets) {
// error on the next line
var trans = dbase.transaction(["WidgetTemplate"], "readwrite");
var store = trans.objectStore("WidgetTemplate");
store.add(widgetTemplets[temp]);
}
}
},
error: function (error) {
console.log(arguments);
}
});
}
I tried putting setTimeout() like this
function addTemps(dbase) {
if (gotWidgetTemplates && widgetTemplets !== null) {
var trans = dbase.transaction(["WidgetTemplate"], "readwrite");
var store = trans.objectStore("WidgetTemplate");
for (var temp in widgetTemplets) {
store.add(widgetTemplets[temp]);
}
}
else {
setTimeout(addTemps, 7000);
}
}
Still it gives me either same error on the line of creating transaction or "tooManyRecursions" error.
I tried putting promise into this like
$.get("../Sample Json files/widgetTemplates.json").then(
function (data0) {
console.log(data0);
var obj = JSON.parse(JSON.stringify(data0));
widgetTemplets = obj.widgets_info;
var trans1 = dbase.transaction(["WidgetTemplate"], "readwrite");
var store1 = trans1.objectStore("WidgetTemplate");
for (var temp in widgetTemplets) {
store1.add(widgetTemplets[temp]);
}
console.log(obj);
}, function (xhr, state, error) {
debugger;
console.log(arguments);
});
It gives me same error on the transaction line.
Here is the json
{
"widgets_info": [
{
"name": "TextInput",
"image": "TEXTINPUT.PNG",
"html": "<label for=\"uname\">LABEL</label> <input type=\"text\" id = \"uname\" name=\"uname\">",
"css": "{width : 30%; height:10%;}",
"rules": {
"on the event of": [
"NONE",
"Single left mouse click"
],
"do these": [
"NONE",
"Trigger behavior"
]
}
}
]
}
I could see a transaction is possible inside ajax success link (Ajax IndexedDB Delete Current Sucesfull Upload)
I want to know why I cannot achieve this.I tried what all I can do, I browsed all the stuff I can but I cannot find the solution. A solution/reason for this would be greatly appreciated.Thanks!!

Javascript functions order

I'm making a jquery library to use an application with the json rpc protocol but I'm stuck with a little problem.
This is the fiddle that shows the code (obviously it can't work): https://jsfiddle.net/L9qkkxLe/3/.
;(function($) {
$.lib = function(options) {
var outputHTML = [],
plugin = this;
var APIcall = function(api_method, api_params) {
request = {};
request.id = Math.floor((Math.random() * 100) + 1);
request.jsonrpc = '2.0';
request.method = api_method;
request.params = (api_params) ? api_params : [];
$.ajax({
type: "POST",
url: "http://localhost:8898/jsonrpc",
data: JSON.stringify(request),
timeout: 3000,
beforeSend: function(xhr) {
xhr.setRequestHeader('Authorization', window.btoa(options.username + ":" + options.password));
},
success: function(data) {
handleData(data, api_method);
},
error: function(jqXHR, textStatus, errorThrown) {
log("Connection time out: can't reach it. Try changing the settings.");
isConnected = "false";
},
dataType: "json"
});
}
var handleData = function(data, method) {
if (method == "getgenres") {
outputHTML = data.result.genres; //I need data.result.genres to return in getgenres function
}
}
var log = function(msg) {
if (options.debug == true) console.log(msg);
}
plugin.getgenres = function() {
APIcall("getgenres");
return outputHTML; //This is sadly empty.
}
};
}(jQuery));
var init = new $.lib();
console.log(init.getgenres());
I need that the getgenres function returns data.result.genres but actually it returns an empty array because getgenres is called for first and only after the handleData function gives to outputHTML the value that I need.
You are performing an asynchronous AJAX request, which means you can't actually get back the data immediately. There are two ways to solve your issue: making it synchronous (easy but ill advised) or using a callback (a little bit more complex but generally accepted):
In your getgenres function, you could accept one more parameter: callback
plugin.getgenres = function(callback) {
/* Dont forget APIcall already took two parameters in, so callback has to be the third in line! */
APIcall("getgenres", false, callback);
}
Now modify your APIcall function to accept your callback:
var APIcall = function(api_method, api_params, callback) { ... }
And call the callback from the successful completion call - instead of having a handler method in between wrapped in a function, you can simply pass the anonymous function. So instead of success: function(data){ handle(data); }, just use:
success: callback
The anonymous function that we will pass to it will receive as its first parameter the data you were passing to the handler. Now you can do the following:
var myGenres = [];
var init = new $.lib();
init.getgenres(function(data){
/* Now your data is actually loaded and available here. */
myGenres = data;
console.log(myGenres);
});
I would like to point out that there are many better ways to handle this, including turning this into a Constructor (More here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) instead of the strange amalgamation of functions and variables you have now, as well as using JS Promises (here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) to make this easier. But the basic gist should be here.
Update (potential implementation)
Because I mentioned that this could be done in a way that I think is clearer to read and use. I do not know all use cases for this, but from the provided example I would change the code to something looking like the following. Please also note I am not an expert on jQuery plugins, so I am avoiding plugging into jQuery and just using it as an easy AJAX call.
function getAjax(){
if(!window.jQuery || !window.$) throw("jQuery is required for this plugin to function.");
this.data = [];
this.request = '';
return this;
}
getAjax.prototype = {
createRequest : function(method, parameters){
this.request = {};
this.request.id = Math.floor((Math.random() * 100) + 1);
this.request.jsonrpc = '2.0';
this.request.method = method;
this.request.params = parameters || [];
return this;
},
callRequest : function(options, callback, error){
var self = this;
// We could also `throw` here as you need to set up a request before calling it.
if(!this.request) return this;
else {
$.ajax({
// We will allow passing a type and url using the options and use sensible defaults.
type: options.type || "POST",
url: options.url || "http://localhost:8898/jsonrpc",
// Here we use the request we made earlier.
data: JSON.stringify(this.request),
timeout: options.timeout || 3000,
beforeSend: function(xhr){
xhr.setRequestHeader(
'Authorization',
window.btoa( options.username + ":" + options.password)
);
},
// We will also store all the made request in this object. That could be useful later, but it's not necessary. After that, we call the callback.
success: function(data){
var store = {request:self.request, data: data};
self.data.push(store);
// Call the callback and bind `this` to it so we can use `this` to access potentially pther data. Also, pass the results as arguments.
callback(data, self.request.id).bind(self);
},
// Error function!
error: error,
dataType: options.dataType || "json"
});
}
return this;
}
}
// Example use
new getAjax().createRequest('getgenres').callRequest({
username: 'myusername',
password: 'mypassword'
}, function(data, id){
// Success! Do with your data what you want.
console.log(data);
}, function(e){
// Error!
alert('An error has occurred: ' + e.statusText);
console.log(e);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
What I do in those occasions is this:
You are supplying a method. So put a reference to the a callback function. In this case plugin.getGenresFinalize. When handleData is called it will fire that callBack function. This way you can pass multiple methods to the api call for different types of data.
plugin.getgenres = function() {
APIcall(this.getgenresFinalize);
}
plugin.getgenresFinalize = function(data) {
console.log(data);
}
var handleData = function(data, method) {
method(data);
}

Ajax IndexedDB Delete Current Sucesfull Upload

I posted something similar yesterday but it works but only deleted the last object in the data.
What I want to happen
This ajax upload will be handling a lot of data, so I'm using indexeddb. This will also be use on mobile phones. So I wanted it to upload one item at a time and if one item failed to have only deleted the previous items out of the data so they wouldn't need to upload everything again.
I have tried async = false, This works exactly how i want it but this freezers browser.
Current Code Tried to comment out any bits that could be confusing, currently this only deletes the last item once finished.
function uploadData(e) {
//Get Database
var transaction = db.transaction(["data"], "readonly");
var objectStore = transaction.objectStore("data");
var cursor = objectStore.openCursor();
//Starts Looping
cursor.onsuccess = function(e) {
var res = e.target.result;
if (res) {
if (navigator.onLine) {
$('.popup-heading').text('Uploading...');
var passData = {
client_id: res.value.client_id,
parent_id: res.value.parent_id,
storename: res.value.storename,
image: res.value.image,
key: res.key,
};
var jsonData = JSON.stringify(passData);
$.ajax({
url: "{{ path('destination_app_ajax') }}",
type: "post",
// Works but freezes browser
/*async, flase*/
data: {
"json": passData
},
success: function(JsonData) {
//Delete item once successfull
var t = db.transaction(["data"], "readwrite");
var request = t.objectStore("data").delete(passData.key);
t.oncomplete = function(event) {
console.log('item deleted');
};
},
error: function() {
$('.popup-heading').text('Upload Failed!');
}
});
} else {
$('.popup-heading').text('Please find stronger signal or wifi connection');
}
res.
continue ();
}
}
}
It sounds like you have a scope issue with passData. Inside of your loop, but before you defined var passData = ... try wrapping the codeblock with an anonymous function:
(function() {
/* Your code here */
}());
That should prevent passData from leaking into the global scope, which seems to be why your IDB code only works on the last loop. (passData is being redefined each time before your AJAX response is able to complete.)
Update: There is no loop, you're dealing with callbacks. What I see happening is that you're redefining your onsuccess handler on each Ajax request (and overwriting all values but the last), reusing the same transaction. Try moving this transaction code into the success callback for the AJAX request:
//Get Database
var transaction = db.transaction(["data"], "readonly");
var objectStore = transaction.objectStore("data");
var cursor = objectStore.openCursor();
That will create a closure and commit your delete transaction on each response. That means one transaction per AJAX request, and one onsuccess callback per AJAX request (with no redefining).
The only solution I found worked with this was to send the Key via ajax to php then delete from that.
HTML
var passData = {
.......
key: res.key,
};
.....
$.ajax({
url: "yourscript.php",
type: "post",
data: {
"json": passData
},
success: function(JsonData) {
jsonKey = JSON.parse(JsonData);
//Delete item once successfull
var t = db.transaction(["data"], "readwrite");
var request = t.objectStore("data").delete(parseInt(jsonKey.key));
t.oncomplete = function(event) {
console.log('item deleted', jsonKey.key);
};
}
PHP
$data = $_POST['json'];
$returnKey = json_encode(
array(
'key' => $data['key']
)
);

Dynamically append content to an element with jQuery

I would like to be able to have an ajax get update the text in a span tag each time it is fired.
$.ajax({
type: 'GET',
url: "JSON URL",
cache: false,
contentType: 'application/json',
dataType: 'json',
success: function(html){
$('#status_frame_span').prepend(html.status)
alert(html.status)
},
error: function(jq,stats,errmes) {
alert("Error" + errmes);
}
});
the first time it fires, the content of the json returned from the URL is properly prepended to the span. however for subsequent firings it is not updated.
How do I ensure that with each firing the content gets updated?
What triggers the call to the server? Is it a button or link inside of the HTML being updated? if it is, the event handler may be lost when the UI is updated. Or, something else is losing the event handler, which doesn't call the method to fire the get request, etc.
HTH.
Of course your view is updated only once: you are calling the server only once!
If, as your tags suggest, you are using long polling (please make sure that's the case, I'm not sure you have a very clear idea of what is an event, a poll and a distant call), then you need to make a new request each time you've received one!
In both your success and error handlers, you have to recursively make an AJAX call to the server. You also have to set a timeout for the calls, which could cancel them and start a new one after, for example, 30 seconds.
You should also implement some kind of throttling for recursive calls, unless you're 99.99% sure the server page will never send errors. Otherwise, you'll kill your client.
For the sake of completeness, I have to add this would be a great use-case for HTML5 SSE or WebSocket. But they're not ready for production usage yet.
it does not work that way - if the success callback is called - the connection has been closed so your long polling will be dead once the request is completed.
The idea behind long polling is that you keep the connection alive. Configure your server properly so that it will hold the connection open as long as possible (set timeout as high as possible).
Here's an approach from my coffee break (not tested):
Server
Every message has to end with the delimiter ::PART::
The server must be properly configured this means set the timeout as high as possible!
Client (Browser)
// setup longpoll, check all 250ms for new data in the stream
var myPoller = new LongPoll('some-url', 250);
// bind connection lost
myPoller.bind('longpoll:end', function(evt) {
alert('connection lost - trying reconnect');
});
// bind error event
myPoller.bind('longpoll:error', function(evt, errmsg) {
alert('error: ' + errmsg);
});
// bind data event
myPoller.bind('longpoll:data', function(evt, data) {
try {
// try to parse json
data = $.parseJSON(data);
// prepend
$('#status_frame_span').prepend(data.status);
} catch(e) {
// invalid json
alert('invalid json: ' + data);
}
});
longpoll.js
var LongPoll = function(url, timeout) {
// url we connect to
this.url = url;
// running?
this.isRunning = false;
// timer for checking the stream
this.timer = null;
// the length of the received data
this.dataLength = 0;
/*
The messages has to be delimited by the delimiter like:
first data::PART::second data::PART::third data::PART::
*/
this.delimiter = RegExp.new("::PART::", 'gm');
// residue from previous transmission
this.residue = ''
};
// connect to server
LongPoll.prototype.connect = function() {
var self = this;
// reset data length
this.dataLength = 0;
// reset residue
this.residue = '';
// start ajax request
this.xhr = $.ajax({
type: 'GET',
url: this.url,
cache: false,
contentType: 'application/json',
dataType: 'text',
success: function(){
// the connection is dead!
self.xhr = null;
// trigger event
$(self).trigger('longpoll:end');
// reconnect if still running
if(self.isRunning) {
self.connect();
}
},
error: function(jq,stats,errmes) {
// stop timer and connection
self.stop();
$(self).trigger('longpoll:error', errmes);
}
});
};
// process data
LongPoll.prototype.process = function(buffer) {
var self = this;
// check if there is anything new
if(buffer.length > this.dataLength) {
var newData = this.residue + buffer.substring(this.dataLength, buffer.length);
// reset residue
this.residue = '';
// store the new position
this.dataLength = buffer.length;
// split data
var dataParts = newData.split(this.delimiter);
// how many full parts?
var fullParts = newData.match(this.delimiter).length;
if(dataParts.length > fullParts) {
// pop residue (incomplete message)
this.residue += dataParts.pop();
}
$.each(dataParts, function(index, part) {
// broadcast data parts
$(self).trigger('longpoll:data', $.trim(data));
});
}
};
// check for data
LongPoll.prototype.receive = function() {
var self = this;
// connection still there?
if(this.xhr) {
// process buffer
this.process(this.xhr.responseText);
}
};
// start long poll
LongPoll.prototype.start = function() {
var self = this;
// set flag
this.isRunning = true;
this.timer = setInterval(function() { self.receive(); }, this.timeout);
this.connect();
};
// stop long poll
LongPoll.prototype.stop = function() {
// set flag
this.isRunning = false;
// clear timer
clearInterval(this.timer);
if(this.xhr) {
// abort request
this.xhr.abort();
}
};

Categories