Connecting to a non-existent web socket server results in loud errors being logged to the console, usually to the tune of ... net::ERR_CONNECTION_REFUSED.
Anyone have an idea for a hackaround to silence this output? XMLHttpRequest won't work since it yields the same verbose error output if the server is not reachable.
The goal here is to test if the server is available, if it is then connect to it, otherwise use a fallback, and to do this without spamming the console with error output.
Chrome itself is emitting these messages, and there is no way to block them. This is a function of how chrome was built; whenever a ResourceFetcher object attempts to fetch a resource, its response is passed back to its context, and if there's an error, the browser prints it to the console - see here.
Similar question can be found here.
If you'd like, you can use a chrome console filter as this question discusses to block these errors in your console, but there is no way to programmatically block the messages.
I don't know why do you want to prevent this error output. I guess you just want to get rid of them when debugging. So I provide a work around here may be just useful for debugging.
Live demo: http://blackmiaool.com/soa/43012334/boot.html
How to use it?
Open the demo page, click the "boot" button, it will open a new tab. Click the "test" button in the new tab and check the result below. If you want to get a positive result, change the url to wss://echo.websocket.org.
Why?
By using post message, we can make browser tabs communicate with each other. So we can move those error output to a tab that we don't concern.
P.S. You can refresh the target page freely without loosing the connection between it and boot page.
P.P.S You can also use storage event to achieve this.
boot.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>boot page</title>
</head>
<body>
<button onclick="boot()">boot</button>
<p>BTW, you can boot the page without the button if you are willing to allow the "pop-up"</p>
<script>
var targetWindow;
function init() {
targetWindow
}
function boot() {
targetWindow = window.open("target.html");
}
boot();
window.addEventListener('message', function(e) {
var msg = e.data;
var {
action,
url,
origin,
} = msg;
if (action === "testUrl") {
let ws = new WebSocket(url);
ws.addEventListener("error", function() {
targetWindow.postMessage({
action: "urlResult",
url,
data: false,
}, origin);
ws.close();
});
ws.addEventListener("open", function() {
targetWindow.postMessage({
action: "urlResult",
url,
data: true,
}, origin);
ws.close();
});
}
});
</script>
</body>
</html>
target.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>target page</title>
</head>
<body>
<h4>input the url you want to test:</h4>
<textarea type="text" id="input" style="width:300px;height:100px;">
</textarea>
<br>
<div>try <span style="color:red">wss://echo.websocket.org</span> for success result(may be slow)</div>
<button onclick="test()">test</button>
<div id="output"></div>
<script>
var origin = location.origin;
var testUrl = origin.replace(/^https?/, "ws") + "/abcdef"; //not available of course
document.querySelector("#input").value = testUrl;
function output(val) {
document.querySelector("#output").textContent = val;
}
function test() {
if (window.opener) {
window.opener.postMessage({
action: "testUrl",
url: document.querySelector("#input").value,
origin,
}, origin);
} else {
alert("opener is not available");
}
}
window.addEventListener('message', function(e) {
var msg = e.data;
if (msg.action === "urlResult") {
output(`test ${msg.url} result: ${msg.data}`);
}
});
</script>
</body>
</html>
Related
I'm attempting to pull "webId%22:%22" var string from a script tag using JS for a Chrome extension. The example I'm currently working with allows me to pull the page title.
// payload.js
chrome.runtime.sendMessage(document.title);
// popup.js
window.addEventListener('load', function (evt) {
chrome.extension.getBackgroundPage().chrome.tabs.executeScript(null, {
file: 'payload.js'
});;
});
// Listen to messages from the payload.js script and write to popout.html
chrome.runtime.onMessage.addListener(function (message) {
document.getElementById('pagetitle').innerHTML = message;
});
//HTML popup
<!doctype html>
<html>
<head>
<title>WebID</title>
<script src="popup.js"></script>
<script src="testButton.js"></script>
</head>
<body>
<button onclick="myFunction()">Try it</button>
<p id="body"></p>
<h3>It's working</h1>
<p id='pagetitle'>This is where the webID would be if I could get this stupid thing to work.</p>
</body>
</html>
What I am trying to pull is the webID from below:
<script if="inlineJs">;(function() { window.hydra = {}; hydra.state =
JSON.parse(decodeURI("%7B%22cache%22:%7B%22Co.context.configCtx%22:%7B%22webId%22:%22gmps-salvadore%22,%22locale%22:%22en_US%22,%22version%22:%22LIVE%22,%22page%22:%22HomePage%22,%22secureSiteId%22:%22b382ca78958d10048eda00145edef68b%22%7D,%22features%22:%7B%22directivePerfSwitch%22:true,%22enable.directive.localisation%22:true,%22enable.directive.thumbnailGallery%22:true,%22enable.new.newstaticmap%22:false,%22disable.forms.webId%22:false,%22use.hydra.popup.title.override.via.url%22:true,%22enable.directive.geoloc.enableHighAccuracy%22:true,%22use.hydra.theme.service%22:true,%22disable.ajax.options.contentType%22:false,%22dealerLocator.map.use.markerClustering%22:true,%22hydra.open.login.popup.on.cs.click%22:false,%22hydra.consumerlogin.use.secure.cookie%22:true,%22use.hydra.directive.vertical.thumbnailGallery.onpopup%22:true,%22hydra.encrypt.data.to.login.service%22:true,%22disable.dealerlocator.fix.loading%22:false,%22use.hydra.date.formatting%22:true,%22use.hydra.optimized.style.directive.updates%22:false,%22hydra.click.pmp.button.on.myaccount.page%22:true,%22use.hydra.fix.invalid.combination.of.filters%22:true,%22disable.vsr.view.from.preference%22:false%7D%7D,%22store%22:%7B%22properties%22:%7B%22routePrefix%22:%22/hydra-graph%22%7D%7D%7D")); }());</script>
You have inline code in onclick attribute which won't work, see this answer.
You can see an error message in devtools console for the popup: right-click it, then "Inspect".
The simplest solution is to attach a click listener in your popup.js file.
The popup is an extension page and thus it can access chrome.tabs.executeScript directly without chrome.extension.getBackgroundPage(), just don't forget to add the necessary permissions in manifest.json, preferably "activeTab".
For such a simple data extraction you don't even need a separate content script file or messaging, just provide code string in executeScript.
chrome.tabs.executeScript({
code: '(' + (() => {
for (const el of document.querySelectorAll('script[if="inlineJs"]')) {
const m = el.textContent.match(/"([^"]*%22webId%22[^"]*)"/);
if (m) return m[1];
}
}) + ')()',
}, results => {
if (!chrome.runtime.lastError && results) {
document.querySelector('p').textContent = decodeURI(results[0]);
}
});
I am using Firechat and I am able to successfully initiate the chat window. I am using Firebase custom authentication and I can login without any problem. However, I now try to create a new chat room and then enter it. Based on the Firechat documentation I did the following:
<!doctype html>
<html>
<head>
<title>Test</title>
<meta charset="UTF-8" />
<script src='https://cdn.firebase.com/js/client/2.0.2/firebase.js'></script>
<link rel='stylesheet' href='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.css' />
<script src='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.js'></script>
</head>
<body>
<script type='text/javascript'>
var fireBase = new Firebase("https://XXXXXXXXX.firebaseio.com/");
function initChat(authData) {
var Firechat = new FirechatUI(fireBase, document.getElementById('firechat'));
Firechat.setUser(authData.uid, "Username");
Firechat.createRoom("Test chat room", "public");
}
fireBase.authWithCustomToken("UNIQUE_TOKEN", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
console.log("Login successful", authData);
initChat(authData);
}
});
</script>
<div id='firechat'>
</div>
</body>
</html>
In the javascript console I can see that login is successful:
Login successful Object { auth: Object, expires: XXXXXXXXX, token: "XXXXXXXX…", uid: "XXXXXX", provider: "custom" }
But the createRoom function is not found:
TypeError: Firechat.createRoom is not a function
Any idea what is going wrong here?
From the docs:
Firechat.createRoom(roomName, roomType, callback(roomId))
Creates a new room with the given name (string) and type (string - public or private) and invokes the callback with the room ID on completion.
It would seem that you do not have a callback.
Firechat.prototype.createRoom = function(roomName, roomType, callback) {
var self = this,
newRoomRef = this._roomRef.push();
var newRoom = {
id: newRoomRef.name(),
name: roomName,
type: roomType || 'public',
createdByUserId: this._userId,
createdAt: Firebase.ServerValue.TIMESTAMP
};
if (roomType === 'private') {
newRoom.authorizedUsers = {};
newRoom.authorizedUsers[this._userId] = true;
}
newRoomRef.set(newRoom, function(error) {
if (!error) {
self.enterRoom(newRoomRef.name());
}
if (callback) {
callback(newRoomRef.name());
}
});
};
Source: https://firechat.firebaseapp.com/docs/firechat.html
So it turns out that there are two classes (is that the right word) used in the Firechat javascript plugin:
var chat = new FirechatUI
var chat = new Firechat
Because they seem so similar I did not notice the difference. Nowhere in the documentation have I been able to find details of the FirechatUI instance (even though this code is recommended on the github readme).
So anyway, the thing is that new FirechatUI loads the actual UI for the chat. new Firechat loads the API that allows you to talk to the chat plugin (but NOT to the UI). This is an important difference. The documentation found here only relates to the API so if you initiate a new Firechat instance. However, the trick is to get the UI up and running and then interact with it directly (doing things like creating new rooms or entering rooms). I have honestly not found out how to do this the official/recommended way. The only thing I've been able to come up with is a hack. It's ugly, but it works. The code below includes functionality to create a new chat room (using Firechat) and to open a particular chatroom in the UI (that bit is hacked as I couldn't find a way to interact with the UI directly).
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Title</title>
<!-- jQuery -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js'></script>
<!-- Firebase -->
<script src='https://cdn.firebase.com/js/client/2.1.0/firebase.js'></script>
<!-- Firechat -->
<link rel='stylesheet' href='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.css' />
<script src='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.js'></script>
<!-- This plugin here: https://gist.github.com/buu700/4200601 -->
<script type="text/javascript" src="js/arrive.js"></script>
<style type="text/css">
#firechat{width:400px}
</style>
</head>
<body>
<h1>Test</h1>
<script type='text/javascript'>
var fireBase = new Firebase("https://XXXXXX.firebaseio.com/");
function roomChatSetup(authData) {
var chat = new Firechat(fireBase);
chat.setUser(authData.uid, "My User Name", function(user) {
console.log("Creating chatroom...");
chat.createRoom("New Chatroom Name", "public", function(roomId) {
console.log("Created room "+roomId);
});
$("#firechat").html("<div class='alert alert-success'>Your chatroom has been set up. Refresh to view</div>");
});
}
function initChat(authData) {
var chatUI = new FirechatUI(fireBase, document.getElementById('firechat'));
chatUI.setUser(authData.uid, "My User Name");
console.log("Simulating clicks...");
$("#firechat-tab-content div.tab-pane").waitUntilExists(function(){
console.log("Close all other tabs by simulating clicking the X button");
$("#firechat-tab-content div.tab-pane:not(#XXXXXXXXX) a.close").click(); // XXXXX should have the chatroom name of the one you want to open
});
$("#firechat-btn-rooms").waitUntilExists(function(){
$("#firechat-btn-rooms").click();
console.log("Open submenu to load all possible rooms");
});
$("li[data-room-id='XXXXXXXXXXXXX']").waitUntilExists(function(){
$("li[data-room-id='XXXXXXXXXX'] a").click();
console.log("Simulating clicking on chatroom XXXXXXXXXXXXXX");
});
}
fireBase.authWithCustomToken("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
console.log("Login successful", authData);
// Here you can use a programming language to decide. If you already have a
// chatroom, run initChat. If you don't, run createRoom. I haven't been
// able to run them both at the same time.
initChat(authData);
// or:
roomChatSetup(authData);
}
});
</script>
<div id="firechat"></div>
</body>
</html>
The FirechatUI object is separate from the Firechat object. FirechatUI does not have the same methods that Firechat does.
In order to get the associated Firechat object from a FirechatUI object you can do the following:
let chatUI = new FirechatUI(chatRef, document.getElementById("firechat-wrapper"));
let chat = chatUI._chat;
You can then do any normal Firechat operations without any issues.
chat.setUser(user.uid, firstName, function(user) {
chat.resumeSession();
});
Please keep in mind that the _chat element is not really supposed to be used (as you can tell from the naming convention), but since FirechatUI does not properly expose enough functionality this is probably the cleanest way to do it.
The javascript example for "search by keyword" that is given at the google developers page isn't working for me. https://developers.google.com/youtube/v3/code_samples/javascript
When I run the code, I get a disabled search box with "cats" inside. Also, the example doesn't explain how to write in the API key as opposed to the Client ID. It says it's possible, but gives no concrete example of how to do it. Can someone point out where this code is going wrong. The code for the two .js files and the html is as follows:
auth.js file:
// The client ID is obtained from the Google Developers Console
// at https://console.developers.google.com/.
// If you run this code from a server other than http://localhost,
// you need to register your own client ID.
var OAUTH2_CLIENT_ID = '__YOUR_CLIENT_ID__';
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/youtube'
];
// Upon loading, the Google APIs JS client automatically invokes this callback.
googleApiClientReady = function() {
gapi.auth.init(function() {
window.setTimeout(checkAuth, 1);
});
}
// Attempt the immediate OAuth 2.0 client flow as soon as the page loads.
// If the currently logged-in Google Account has previously authorized
// the client specified as the OAUTH2_CLIENT_ID, then the authorization
// succeeds with no user intervention. Otherwise, it fails and the
// user interface that prompts for authorization needs to display.
function checkAuth() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: true
}, handleAuthResult);
}
// Handle the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
// Authorization was successful. Hide authorization prompts and show
// content that should be visible after authorization succeeds.
$('.pre-auth').hide();
$('.post-auth').show();
loadAPIClientInterfaces();
} else {
// Make the #login-link clickable. Attempt a non-immediate OAuth 2.0
// client flow. The current function is called when that flow completes.
$('#login-link').click(function() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
});
}
}
// Load the client interfaces for the YouTube Analytics and Data APIs, which
// are required to use the Google APIs JS client. More info is available at
// http://code.google.com/p/google-api-javascript-client /wiki/GettingStarted#Loading_the_Client
function loadAPIClientInterfaces() {
gapi.client.load('youtube', 'v3', function() {
handleAPILoaded();
});
}
search.js file:
// After the API loads, call a function to enable the search box.
function handleAPILoaded() {
$('#search-button').attr('disabled', false);
}
// Search for a specified string.
function search() {
var q = $('#query').val();
var request = gapi.client.youtube.search.list({
q: q,
part: 'snippet'
});
request.execute(function(response) {
var str = JSON.stringify(response.result);
$('#search-container').html('<pre>' + str + '</pre>');
});
}
search.html
<!doctype html>
<html>
<head>
<title>Search</title>
</head>
<body>
<div id="buttons">
<label> <input id="query" value='cats' type="text"/><button id="search-button" disabled onclick="search()">Search</button></label>
</div>
<div id="search-container">
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="auth.js"></script>
<script src="search.js"></script>
<script src="https://apis.google.com/js/client.js?onload=googleApiClientReady"> </script>
</body>
</html>
The documentation is misleading a bit (one might even say incorrect); the HTML for the "search by keyword" is loading the same "auth.js" that the other two examples on that page are, but it doesn't then have any HTML elements to actually trigger the login process (i.e. a "login button" if a user isn't already authorized) like the other two examples do. Bascially, if you're using that provided auth.js, you need to have, in your HTML, a section that looks like this:
<div id="login-container" class="pre-auth">
This application requires access to your YouTube account.
Please authorize to continue.
</div>
Then, you can also add the class of "post-auth" on a new div that wraps around your existing buttons and search container. The demo will then, when a user visits, only present the login link; when clicked on, and when a user allows the authorization, then the login link will be hidden and your search area will be shown (and the button enabled). That's just how the demo is set up.
Of course, search by keyword does NOT require oAuth2, and so (to answer your 2nd question) you might find it easier to A) remove the handleApiLoaded method (so your button is never disabled), and B) call gapi.client.load() manually (i.e. not triggered by an oAuth success). Then, call gapi.client.setApiKey({YOUR KEY}) so that all of your requests will include your key in their header.
Thanks so much jlmcdonald for your help. It took me a while to figure out the second part of your response, but I finally had success. The following html gets the example on the google developers webpage to work. Note though, at first I was getting a 401 error. To fix it, I had to go to the google developers console and select my project. Then, APIs&auth->consent screen and then fill in the email address and product name:
<!doctype html>
<html>
<head>
<title>Search</title>
</head>
<body>
<div id="login-container" class="pre-auth">
This application requires access to your YouTube account.
Please authorize to continue.
</div>
<div id="buttons" class="post-auth">
<label> <input id="query" value='cats' type="text"/><button id="search-button" disabled onclick="search()">Search</button></label>
</div>
<div id="search-container">
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="/files/theme/auth.js"></script>
<script src="/files/theme/search.js"></script>
<script src="https://apis.google.com/js/client.js?onload=googleApiClientReady"> </script>
</body>
</html>
As you noted in your response, oAuth2 isn't necessary for a simple keyword search. The following is some html that just uses the API key. I didn't reference the two .js files like before, rather, I just included the script in the html. Your post at gapi.client.youtube is undefined? really helped me figure it out:
<!doctype html>
<html>
<head>
<title>Search</title>
</head>
<body>
<div id="buttons">
<label> <input id="query" value='cats' type="text"/><button id="search-button" onclick="keyWordsearch()">Search</button></label>
</div>
<div id="search-container">
</div>
<script>
function keyWordsearch(){
gapi.client.setApiKey('API key here');
gapi.client.load('youtube', 'v3', function() {
makeRequest();
});
}
function makeRequest() {
var q = $('#query').val();
var request = gapi.client.youtube.search.list({
q: q,
part: 'snippet'
});
request.execute(function(response) {
var str = JSON.stringify(response.result);
$('#search-container').html('<pre>' + str + '</pre>');
});
}
</script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="https://apis.google.com/js/client.js?onload=googleApiClientReady"> </script>
</body>
</html>
Now that I got the search part, could you explain how I can display the thumbnails and titles of the results and then when I click them, the video opens in an embedded player on the same page? Thanks.
Thank you for your coding. Let me share my code:
function makeRequest(){
var q = $('#query').val();
var request = gapi.client.youtube.search.list({
q: q,
part: 'snippet'
});
request.execute(function(response){
var str = JSON.stringify(response.result,'',4);
$('#search-container').val( str);
makeControl(response);
});
}
function makeControl(resp){
var stList = '<table id="res1" border="1" cellspacing="1" width="100%"><tbody>';
for (var i=0; i<resp.items.length;i++){
var vid = resp.items[i].id.videoId;
var tit = resp.items[i].snippet.title;
if(typeof vid != 'undefined'){
stList += '<tr><td style="width:80px;vertical-align:top">'+
'<a class="show" href="#" title="'+ vid + '" onclick="playVid(this);'+
' return false">'+
'<img width="80" height="60" src="http://img.youtube.com/vi/'+
vid +'/default.jpg"></a></td>'+
'<td><b>'+i+'</b>-'+ tit +'</td></tr>';
}
}
document.getElementById('list1').innerHTML = stList + '</tbody></table>';
//HTML: <div id="list1"
//style="width:853px;height:400px;overflow:auto;background-color:#EEEEEE">
//</div>
}
function playVid(thi){
var st = 'https://www.youtube.com/embed/'+thi.title+'?autoplay=1';
document.getElementById('player').src = st;
//HTML: <iframe id="player" width="853" height="480" src="" frameborder="1" allowfullscreen>
//</iframe>
}
I can't seem to get websocket communication to work in the Play Framework version 2.1.
I created a simple test that does nothing but send messages back and forth with a push of a button. All the code for it is below. But nothing shows up except for the button.
Has anybody seen this problem or can someone tell me what I may be doing wrong in the code below?
I am using the latest version of Chrome.
Here is my simple setup.
In Application.java
public static Result index() {
return ok(index.render());
}
public static WebSocket<String> sockHandler() {
return new WebSocket<String>() {
// called when the websocket is established
public void onReady(WebSocket.In<String> in,
WebSocket.Out<String> out) {
// register a callback for processing instream events
in.onMessage(new Callback<String>() {
public void invoke(String event) {
System.out.println(event);
}
});
// write out a greeting
out.write("I'm contacting you regarding your recent websocket.");
}
};
}
In Routes File
GET / controllers.Application.index()
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
GET /greeter controllers.Application.sockHandler()
In Index.Scala.html
#main(null) {
<div class="greeting"></div>
<button class="send">Send</button>
<script type="text/javascript" charset="utf-8">
$(function() {
var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var sock = new WS("#routes.Application.sockHandler()")
sock.onmessage = function(event) {
$('.greeting').append(event.data)
}
$('button.send').click(function() {
sock.send("I'm sending a message now.")
});
})
</script>
}
In Main.scala.html
#(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>#title</title>
<link rel="stylesheet" media="screen" href="#routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="#routes.Assets.at("images/favicon.png")">
<script src="#routes.Assets.at("javascripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
</head>
<body>
#content
</body>
The problem is in
var sock = new WS("#routes.Application.sockHandler()")
you have to specify the protocol and the complete url in the format: ws://localhost:9000/greeter.
Check this question to do it in javascript: How to construct a WebSocket URI relative to the page URI?
you can use a Route's webSocketURL() method to retrieve a url that can be passed to a WebSocket's constructor. Here's an example from Play's websocket-chat sample code:
$(function() {
var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var chatSocket = new WS("#routes.Application.chat(username).webSocketURL()")
var sendMessage = function() {
chatSocket.send(JSON.stringify(
{text: $("#talk").val()}
))
$("#talk").val('')
}
// ...
So in your code you can use something like
var sock = new WS("#routes.Application.sockHandler().webSocketURL()");
Personally I don't like intermingling interpolated code with JS, since I think that any code executing on the client should only be concerned with the state of the client, and not the server (not to mention it makes refactoring the script out into an external file impossible), so I tend to do something like this:
<div class="container app-container"
data-ws-uri="#routes.Application.WSUri.webSocketURL()">
.......
</div>
Then in my JS I can just do something like
var sock = new WS(document.querySelector(".app-container").dataset.wsUri);
// ....
I'm experiencing problems making database transactions on IOS devices.
If the user doesn't touch the phone, everything works like expected.
If the user taps/scrolls/touches the screen, some transactions directly call their successCallback, without ever calling the actual transaction callback.
Simplified example here: http://jsfiddle.net/Tk9rv/
To test, just open http://jsfiddle.net/Tk9rv/embedded/result/ in your mobile safari on IOS and do not touch the device while loading. You will see a list of debug messages being generated looking like this:
database is running
table will be cleared
store method called for '10'.
about to insert '10'.
transaction successful for '10'
store method called for '9'.
about to insert '9'.
transaction successful for '9'
store method called for '8'.
about to insert '8'.
transaction successful for '8'
[...]
Now, reload the page and while loading, scroll and tap randomly.
You will see some "about to insert..." messages are missing.
database is running
table will be cleared
store method called for '10'.
about to insert '10'.
transaction successful for '10'
store method called for '9'.
about to insert '9'.
transaction successful for '9'
store method called for '8'.
transaction successful for '8' <-- WHERE IS MY "about to insert '8'." ???
store method called for '7'.
about to insert '7'.
transaction successful for '7'
[...]
This is because the transactionCallback is completely skipped! But WHY? And WHY does the successCallback fire instead of the errorCallback?
[This is a simplified example, please do not tell me not to do this setTimeout stuff. In the real world, there is data being loaded async and then being inserted... :) ]
I think there is a similar problem here HTML5 Web SQL transaction Missing In Action but there is no solution or hint either.
Any ideas? I'm stuck... Thanks!
Our testing showed that this behaviour would also occur whenever the keyboard was displaying and would prevent transactions in an onblur or onfocus event (although, not onkey{down|up|press}). Using setTimeout would cause terrible performance issues. We had found that the .transaction() success callback would be triggered, even if the transaction callback was not called, so came up with this method that works well for us:
var oldOpenDatabase = window.openDatabase;
window.openDatabase = function() {
var db = oldOpenDatabase.apply(window, arguments);
var oldTrans = db.transaction;
db.transaction = function(callback, err, suc) {
var db = this;
var params = arguments;
var hasRun = false;
oldTrans.call(db, function(tx){
hasRun = true; callback(tx);
}, err||function(){}, function(tx) {
if(hasRun){
if (suc != undefined)
suc(tx);
return;
}
else {
oldTrans.apply(db, params);
}
});
}
return db;
}
This code checks that the transaction callback has fired in the success handler and, if not, gives it another chance.
Web Workers and OpenDatabaseSync fix the same problem on my site. Except web workers are not supported on iOS4.x and Android so we can implement the old way for those and web workers on iOS5.
The very basic example below uses a loop to do a single insert 500 times writing to the screen after each insert. At the end a query is made to return the number of rows in the table. Now we can see transactions are not skipped even with a lot of scrolling.
This example is designed so you can test the resilience to scrolling during the inserts on iPhone or iPad.
Two pages are required, an html page and separate javascript file for the worker. So for the html page;
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title></title>
<script src="../scripts/jquery-1.6.2.min.js" type="text/javascript"></script>
<style type="text/css">
body
{
background-color:Black;
}
.number
{
color:#00FF00;
font-family:Courier New;
font-size:12pt;
}
button
{
position:fixed;
right:5px;
top:5px;
padding:15px;
}
</style>
<script type="text/javascript">
var worker;
$(document).ready(function () {
worker = new Worker('worker1.js');
worker.addEventListener('message', function (e) {
$("#test").html(new Date().toLocaleTimeString() + " " + e.data.message + "<BR/>" + $("#test").html());
if (e.data.complete == true) {
worker.terminate();
}
}, false);
});
function stop() {
worker.terminate();
}
</script>
</head>
<body>
<form id="form1" onsubmit="return false">
<div id="test" class="number">
<button onclick="stop()">Stop</button>
</div>
</form>
</body>
</html>
and worker1.js ;
var wdb = {};
wdb.db = openDatabaseSync('test', '1.0', 'DB test', 1024);
doQuery("CREATE TABLE IF NOT EXISTS test(abc INTEGER PRIMARY KEY);");
doQuery("DELETE FROM test"); // optional
for (var i = 0; i < 500; i++) {
doQuery("INSERT INTO test(abc) VALUES( " + i + " );");
postMessage({ message: "Inserted " + i.toString(), complete: false });
}
var summary= doQuery("SELECT COUNT(*) AS Total FROM test;");
postMessage({ message: "Found " + summary.item(0).Total.toString() + " rows", complete: true });
function doQuery(sql) {
wdb.db.transaction(function (tx) {
rs = tx.executeSql(sql, []);
});
return rs.rows;
}