I recently had to correct security issues in a web-application (that I didn't create). The security problem was, it was using non-http-only cookies. So I had to set the session-cookie http-only, which means you can't read (and set) the cookie's value anymore from javascript. So far so seamingly easy.
The deeper problem was, the web-application used
JSON.parse(readCookie(cookieName)).some_value
on a million places.
So in order to not have to re-write "a million lines of code", I had to create an ajax-endpoint that gave me the http-cookie's content as JSON and rewrite readCookie to use SYNCHRONOUS ajax requests (instead of reading the cookie), because the rest of the horrible code expects readCookie to be synchronous at these million places, because reading a cookie is synchronous.
The problem now is, I get a lot of
Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.
which spams the debug console, let alone the possibility someone decides to remove this functionality.
I am therefore looking into the new ES async/await keywords, to see if that could help somehow in making a asynchronous ajax-request synchronously (i know I have to use wrappers for IE 11).
So far, I read these pages
https://www.twilio.com/blog/2015/10/asyncawait-the-hero-javascript-deserved.html
https://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html
https://jakearchibald.com/2014/es7-async-functions/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
but it looks like all the new async stuff seems to only cater to the problem of writing asynchronous code easier, not enabling interop between asynchronous and existing synchronous code. Using the information I read, I can now await the result of an asynchronous ajax-call like it was synchronous, but the problem is - await is only allowed in async-methods... Which means even if I can await the result like it was synchronous, the getCookie method would still have to be async, which makes all the stuff appear to be completely pointless (unless your entire code would-be async, which it certainly isn't when you don't start from scratch)...
I can't seem to find any information on how to interop between synchronous and asynchronous code.
For example, in C#, I can call an async-method from a synchronous context with .Result, e.g.
AsyncContext.RunTask(MyAsyncMethod).Result;
or easier but less deadlock-safe like
MyAsyncMethod(args).Result;
Is there any way to achieve the same in JavaScript ?
It seems to make little sense to spread async around, when the rest of the codebase is synchronous, without any possibility of interop... Is there really still no way to achieve this in JavaScript in 2017 AD ?
I emphasize again:
I know how I can make a synchronous ajax-call, and I know how to use async ajax calls with callbacks and/or promises.
But what I'm unable to figure out is how to synchronize an async-ajax-call (no callback) so it can be used from code that expects to be run synchronously (in "a million places") !
This is what I have tried so far:
(Note that whether I use loadQuote
or main
, the text "Ron once said" still appears first in the debug-console, which should not be the case if the asynchronous ajax-call had been resolved synchronously)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="en" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="google" value="notranslate" />
<!--
<meta name="author" content="name" />
<meta name="description" content="description here" />
<meta name="keywords" content="keywords,here" />
<link rel="shortcut icon" href="favicon.ico" type="image/vnd.microsoft.icon" />
<link rel="stylesheet" href="stylesheet.css" type="text/css" />
-->
<title>Title</title>
<style type="text/css" media="all">
body
{
background-color: #0c70b4;
color: #546775;
font: normal 400 18px "PT Sans", sans-serif;
-webkit-font-smoothing: antialiased;
}
</style>
<script type="text/javascript">
<!--
// http://localhost:57566/foobar/ajax/json.ashx
var ajax = {};
ajax.x = function () {
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
}
var versions = [
"MSXML2.XmlHttp.6.0",
"MSXML2.XmlHttp.5.0",
"MSXML2.XmlHttp.4.0",
"MSXML2.XmlHttp.3.0",
"MSXML2.XmlHttp.2.0",
"Microsoft.XmlHttp"
];
var xhr;
for (var i = 0; i < versions.length; i++) {
try {
xhr = new ActiveXObject(versions[i]);
break;
} catch (e) {
}
}
return xhr;
};
ajax.send = function (url, callback, method, data, async) {
if (async === undefined) {
async = true;
}
var x = ajax.x();
x.open(method, url, async);
x.onreadystatechange = function () {
if (x.readyState == 4) {
callback(x.responseText)
}
};
if (method == 'POST') {
x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
}
x.send(data)
};
ajax.get = function (url, data, callback, async) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
}
ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async)
};
ajax.post = function (url, data, callback, async) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
}
ajax.send(url, callback, 'POST', query.join('&'), async)
};
///////////
function testAjaxCall() {
ajax.get("./ajax/json.ashx", null, function (bError, strMessage, iStatus)
{
console.log("args:", arguments);
console.log("Error:", bError);
console.log("Message:", strMessage);
console.log("Status:", iStatus);
}
, true
);
}
-->
</script>
</head>
<body>
<script type="text/javascript">
function getQuote() {
var quote;
return new Promise(function (resolve, reject) {
ajax.get("./ajax/json.ashx", null, function (bError, strMessage, iStatus) {
// console.log("args:", arguments);
// console.log("Error:", bError);
// console.log("Message:", strMessage);
// console.log("Status:", iStatus);
quote = bError;
resolve(quote)
}, true);
/*
request('./ajax/json.ashx', function (error, response, body) {
quote = body;
resolve(quote);
});
*/
});
}
async function main() {
var quote = await getQuote();
console.log("quote: ", quote);
}
function myGetQuote() {
var quote = async function () { return await getQuote(); };
console.log("quote: ", quote);
return quote;
}
function spawn(generatorFunc) {
function continuer(verb, arg) {
var result;
try {
result = generator[verb](arg);
} catch (err) {
return Promise.reject(err);
}
if (result.done) {
return result.value;
} else {
return Promise.resolve(result.value).then(onFulfilled, onRejected);
}
}
var generator = generatorFunc();
var onFulfilled = continuer.bind(continuer, "next");
var onRejected = continuer.bind(continuer, "throw");
return onFulfilled();
}
function loadQuote()
{
return spawn(function *() {
try {
let story = yield getQuote();
console.log("story:", story);
// addHtmlToPage(story.heading);
// for (let chapter of story.chapterURLs.map(getJSON)) { addHtmlToPage((yield chapter).html); } addTextToPage("All done");
} catch (err) {
//addTextToPage("Argh, broken: " + err.message);
console.log("Argh, broken: " + err.message);
}
//document.querySelector('.spinner').style.display = 'none';
});
}
function autorun()
{
console.clear();
// main();
// main();
loadQuote();
//var quote = myGetQuote();
// console.log("quote: ", quote);
console.log('Ron once said,');
}
if (document.addEventListener) document.addEventListener("DOMContentLoaded", autorun, false);
else if (document.attachEvent) document.attachEvent("onreadystatechange", autorun);
else window.onload = autorun;
</script>
</body>
</html>
Async/await helps you write synchronous-looking JavaScript code that works asynchronously. Await is in an async function to ensure that all promises that are returned in the function are synchronized. With async/await, there's no use of callbacks.
Or we can say await is only used with an async function. The await keyword is used in an async function to ensure that all promises returned in the async function are synchronized, ie. they wait for each other. Await eliminates the use of callbacks in .
You can use the await keyword on its own (outside of an async function) within a JavaScript module.
but the problem is - await is only allowed in async-methods.
Exactly, and no, there's no workaround for that. JavaScript's run-to-completion semantics demand that synchronous functions complete before any pending asynchronous action (such as the callback to an XHR handler for an async XHR call) can run.
The way JavaScript runs on a given thread is that it processes a queue of jobs1:
(It's a bit more complicated than that, there are two levels to it, but that's not relevant to this particular question.)
XHR completions and such are jobs that get scheduled in the queue. There is no way to pause a job, run another job from the queue, and the pick up the paused job. async
/await
provide dramatically simpler syntax for handling asynchronous operations, but they don't change the nature of the job queue.
The only solution I see for your situation is to go async all the way to the top level. This may not be as complicated as you might think (or maybe it will be). In many cases it's adding async
in front of function
on a lot of functions. However, making those functions asynchronous is likely to have significant knock-on effects (for instance, something that was synchronous in an event handler becoming asynchronous changes the timing of what happens in relation to the UI).
For example, consider this synchronous code:
var btn = document.getElementById("btn");
btn.addEventListener("click", handler, false);
function handler(e) {
console.log("handler triggered");
doSomething();
console.log("handler done");
}
function doSomething() {
doThis();
doThat();
doTheOther();
}
function doThis() {
console.log("doThis - start & end");
}
function doThat() {
console.log("doThat - start");
// do something that takes a while
var stop = Date.now() + 1000;
while (Date.now() < stop) {
// wait
}
console.log("doThat - end");
}
function doTheOther() {
console.log("doThat - start & end");
}
.as-console.wrapper {
max-height: 80% !important;
}
<input type="button" id="btn" value="Click Me">
<p id="text"></p>
Now we want to make make doThat
async (note: will only work on a recent browser supporting async
/await
, like Chrome; sadly Stack Snippet's Babel config doesn't include them, so we can't use that option):
var btn = document.getElementById("btn");
btn.addEventListener("click", handler, false);
// handler can't be async
function handler(e) {
console.log("handler triggered");
doSomething();
console.log("handler done");
}
// doSomething can be
async function doSomething() {
doThis();
await doThat();
doTheOther();
}
function doThis() {
console.log("doThis - start & end");
}
// make doThat async
async function doThat() {
console.log("doThat - start");
// simulate beginning async operation with setTimeout
return new Promise(resolve => {
setTimeout(() => {
// do something that takes a while
var stop = Date.now() + 1000;
while (Date.now() < stop) {
// wait
}
console.log("doThat - end (async)");
}, 0);
});
}
function doTheOther() {
console.log("doThat - start & end");
}
.as-console.wrapper {
max-height: 80% !important;
}
<input type="button" id="btn" value="Click Me">
<p id="text"></p>
The key thing there is we went async as soon as we could, in doSomething
(since handler
can't be async). But of course, that changes the timing of the work in relation to the handler. (Of course, we probably should have updated handler
to catch errors from the promise `doSomething() returns.)
1 That's the JavaScript spec terminology. The HTML5 spec (which also touches on this) calls them "tasks" instead of "jobs".
There's is a problem with your approach. First, for part of code to await
for async
operation to finish, it must be wrapped itself in a async
function.
For example:
async function asyncExample () {
try {
const response = await myPromise()
// the code here will wait for the
// promise to fullfil
} catch (error) {
// the code here will execute if the promise fails
}
}
function nonAsyncExample () {
asyncExample ()
console.log('this will not wait for the async to finish')
// as it's not wrapped in an async function itself
}
You could try to declare the autorun()
function as async
, but that may lead to additional complications.
My suggestion, if your JS app has an entry point, it's triggered by an onload
event, try to do your ajax call before this point and then store it locally in a variable and query it from there.
For example, if your code looks like:
function init () {
// perform initialisations here
}
document.addEventListener("DOMContentLoaded", init)
change that to be
document.addEventListener("DOMContentLoaded", function () {
getAjaxConfig().then(function (response) {
window.cookieStash = response
init()
}
})
and get your data from the cookieStash
in the rest of the application. You won't need to wait for anything else.
Short answer: no there is no way to make async code run synchronous in JS as you know it from C#. Making everything asynchronous is a possible solution.
However, since you also control the server side, I have another suggestion (bit of a hack): send along the required information (cookie content) as metadata of the request, e.g. as HTML meta tag for page requests or HTTP response header for XHR requests, and store it somewhere.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With