0

I made a block in which the request to the specified URL occurs.

Inside this block, I can work with the data from response, but outside this block I can not get this data because of asynchrony.

Is it possible to simulate a synchronous request in blockly or in some other way, save the received data to a global variable?

enter image description here

code of the created block:

Blockly.Blocks['request'] =
'<block type="request">'
+ '     <value name="URL">'
+ '         <shadow type="text">'
+ '             <field name="TEXT">text</field>'
+ '         </shadow>'
+ '     </value>'
+ '     <value name="LOG">'
+ '     </value>'
+ '     <value name="WITH_STATEMENT">'
+ '     </value>'
+ '     <mutation with_statement="false"></mutation>'
+ '</block>';

Blockly.Blocks['request'] = {
init: function() {
    this.appendDummyInput('TEXT')
        .appendField('request');

    this.appendValueInput('URL')
        .appendField('URL');

    this.appendDummyInput('WITH_STATEMENT')
        .appendField('with results')
        .appendField(new Blockly.FieldCheckbox('FALSE', function (option) {
            var delayInput = (option == true);
            this.sourceBlock_.updateShape_(delayInput);
        }), 'WITH_STATEMENT');

    this.appendDummyInput('LOG')
        .appendField('log level')
        .appendField(new Blockly.FieldDropdown([
            ['none',  ''],
            ['info',  'log'],
            ['debug', 'debug'],
            ['warning',  'warn'],
            ['error', 'error']
        ]), 'LOG');

    this.setInputsInline(false);
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);

    this.setColour(230);
    this.setTooltip('Request URL');
    this.setHelpUrl('https://github.com/request/request');
},
mutationToDom: function() {
    var container = document.createElement('mutation');
    container.setAttribute('with_statement', this.getFieldValue('WITH_STATEMENT') === 'TRUE');
    return container;
},
domToMutation: function(xmlElement) {
    this.updateShape_(xmlElement.getAttribute('with_statement') == 'true');
},
updateShape_: function(withStatement) {
    // Add or remove a statement Input.
    var inputExists = this.getInput('STATEMENT');

    if (withStatement) {
        if (!inputExists) {
            this.appendStatementInput('STATEMENT');
        }
    } else if (inputExists) {
        this.removeInput('STATEMENT');
    }
}};

Blockly.JavaScript['request'] = function(block) {
var logLevel = block.getFieldValue('LOG');
var URL = Blockly.JavaScript.valueToCode(block, 'URL', Blockly.JavaScript.ORDER_ATOMIC);
var withStatement = block.getFieldValue('WITH_STATEMENT');

var logText;
if (logLevel) {
    logText = 'console.' + logLevel + '("request: " + ' + URL + ');\n'
} else {
    logText = '';
}

if (withStatement === 'TRUE') {
    var statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
    if (statement) {

        var xmlhttp = "var xmlHttp = new XMLHttpRequest();";
        var xmlopen = "xmlHttp.open('POST', " + URL + ", true);";
        var xmlheaders = "xmlHttp.setRequestHeader('Content-type', 'application/json');\n" + 
                         "xmlHttp.setRequestHeader('Authorization', 'Bearer psokmCxKjfhk7qHLeYd1');";
        var xmlonload = "xmlHttp.onload = function() {\n" +
                        "  console.log('recieved:' + this.response);\n" +
                        "  var response = this.response;\n" +
                        "  var brightness = 'brightness: ' + JSON.parse(this.response).payload.devices[0].brightness;\n" +
                        "  " + statement + "\n" +
                        "}";

        var json = JSON.stringify({
            "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
            "inputs": [{
                "intent": "action.devices.QUERY",
                "payload": {
                    "devices": [{
                        "id": "0",
                        "customData": {
                            "smartHomeProviderId": "FkldJVJCmDNSaoLkoq0txiz8Byf2Hr"
                        }
                    }]
                }
            }]
        });

        var xmlsend = "xmlHttp.send('" + json + "');";

        var code = xmlhttp + '\n' + xmlopen + '\n' + xmlheaders + '\n' + xmlonload + '\n' + xmlsend;
        return code;

    } else {

        var xmlhttp = "var xmlHttp = new XMLHttpRequest();";
        var xmlopen = "xmlHttp.open('POST', " + URL + ", true);";
        var xmlheaders = "xmlHttp.setRequestHeader('Content-type', 'application/json');\n" + 
                         "xmlHttp.setRequestHeader('Authorization', 'Bearer psokmCxKjfhk7qHLeYd1');";
        var xmlonload = "xmlHttp.onload = function() {\n" +
                        "  console.log('recieved:' + this.response);\n" +
                        "}";

        var json = JSON.stringify({
            "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
            "inputs": [{
                "intent": "action.devices.QUERY",
                "payload": {
                    "devices": [{
                        "id": "0",
                        "customData": {
                            "smartHomeProviderId": "FkldJVJCmDNSaoLkoq0txiz8Byf2Hr"
                        }
                    }]
                }
            }]
        });

        var xmlsend = "xmlHttp.send('" + json + "');";

        var code = xmlhttp + '\n' + xmlopen + '\n' + xmlheaders + '\n' + xmlonload + '\n' + xmlsend;
        return code;
    }
} else {
        var xmlhttp = "var xmlHttp = new XMLHttpRequest();";
        var xmlopen = "xmlHttp.open('POST', " + URL + ", true);";
        var xmlheaders = "xmlHttp.setRequestHeader('Content-type', 'application/json');\n" + 
                         "xmlHttp.setRequestHeader('Authorization', 'Bearer psokmCxKjfhk7qHLeYd1');";
        var xmlonload = "xmlHttp.onload = function() {\n" +
                        "  console.log('recieved:' + this.response);\n" +
                        "}";

        var json = JSON.stringify({
            "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf",
            "inputs": [{
                "intent": "action.devices.QUERY",
                "payload": {
                    "devices": [{
                        "id": "0",
                        "customData": {
                            "smartHomeProviderId": "FkldJVJCmDNSaoLkoq0txiz8Byf2Hr"
                        }
                    }]
                }
            }]
        });

        var xmlsend = "xmlHttp.send('" + json + "');";

        var code = xmlhttp + '\n' + xmlopen + '\n' + xmlheaders + '\n' + xmlonload + '\n' + xmlsend;
        return code;
}};
Andrew Lohr
  • 5,380
  • 1
  • 26
  • 38
DSV
  • 11
  • Please add the image inline. – mbuechmann Aug 16 '18 at 13:32
  • 2
    Saving to a global variable is certainly possible, but it won't help you: you never know whether the variable already has received the value and whether you can use it yet. – Bergi Aug 17 '18 at 12:51

2 Answers2

2

We actually use Promises extensively in our Blockly environment. My suggestion would be to Promisify this and then use a generator function. For example, we use the co library to wrap our generated code, then use yield statements for asynchronous values to make them behave synchronously. For example:

For a block setup similar to this --

A variable block setting a variable called getUsername to the value from the DB, Logged In User Record in User Name, then toasting the user about it

The generated code would be something like this (simplified) --

co(function* () {
    var getUsername;
    // getFieldValue makes an asynchronous call to our database
    getUsername = (yield getFieldValue("username", "my user record Id", "users"));
    Promise.all(inputPromises)
    let inputPromises = [];
    inputPromises.push('User name is');
    inputPromises.push(getUsername);

    yield new Promise(function(resolve, reject) {
        Promise.all(inputPromises).then(function(inputResults) {
            let [TITLE, MESSAGE] = inputResults;
            let activity = "toastMessage";
            let LEVEL = "success";
            try {
                var params = {message: MESSAGE, title: TITLE, level: LEVEL};
                interface.notification(params);
                return resolve();
            } catch(err) {
                return reject(err);
            }

        }).catch(reject);

    });
    return true;
}

As you may have noticed, though, this isn't always as easy as just sticking a "yield" before the block. Depending on your setup, you may have to get more creative using Promise.all to get values in your block, etc. (We actually wound up editing a bunch of the Blockly core blocks to append 'yield' in front of inputs which had a "promise" type set amongst their output types in order to make this work, but based on your setup, this may be overkill.)

Obviously, you would need to make sure this was either transpiled or run in an environment which supports ES6.

Of course, once you enter an asynchronous setup, there's not really any going back -- co functions themselves return a Promise, so you will need to deal with that appropriately. But in general, we've found this to be a pretty robust solution, and I'm happy to help you figure it out in more detail.

Amber B.
  • 1,134
  • 10
  • 20
  • Why not use `async`/`await` over `co`+generators? – Bergi Aug 17 '18 at 12:52
  • We don't really have extensive ES7 features and weren't ready to use that setup, but if you are ES7 ready, then that's also a good option. – Amber B. Aug 17 '18 at 13:00
  • Also, we unfortunately need IE11 support. – Amber B. Aug 17 '18 at 13:39
  • Afaik IE11 supports neither ES6 generators nor ES8 async/await, so if you transpile anyway you might just as well use the modern syntax – Bergi Aug 17 '18 at 13:45
  • 2
    Okay, the honest answer is "We made this setup back in late 2016/early 2017 and haven't updated to async/await since because it works fine, we have higher priorities to address, and we vaguely remember something about IE11 support being more complicated than our existing transpiling but haven't looked into it because it hasn't been a priority". I will do a bit more research on this now that you bring it up, however, because we're doing a major Blockly overhaul right now, and because I want to make sure my answer has everything up to date. – Amber B. Aug 17 '18 at 13:55
  • "We actually wound up editing a bunch of the Blockly core blocks" sounds tremendously, that the blockly lib has no solution for "async blocks". I mean it's not a super unconventional use case to receive data from an API. – DaTebe Aug 17 '18 at 14:26
  • Thanks for your answer! I realized the idea of implementation, but I'm new to developing with blockly, so it's difficult for me to imagine how this idea is implemented "block-by-block". Can you give me some code examples, where the code for each block from the scheme described by you is generated? – DSV Aug 28 '18 at 09:04
  • I have tried to Promisify my api call as you said. Now I have something like [this](https://pastebin.com/xfNHP0fn). But when I'm trying to set variable to the response the generated code looks like `response = ([object Promise]);`. I have no 'yield' in front of input. And while running the code I'm getting `SyntaxError: missing ] after element list`. So how can I solve this problem? Create my own 'set variable to'-block with 'yield' or is there another way? – DSV Aug 28 '18 at 12:46
  • @DSV From your example, it doesn't look like you're generating the JavaScript code itself? The general Blockly paradigm is to generate a string which is valid code for the language you're using (in this case, JavaScript). So for example, an absolute value block might have code like `code = 'Math.abs(' + arg + ')';` rather than `code = Math.abs(arg)';`. Your generated code should contain the Promise, not itself be/include a Promise. – Amber B. Aug 30 '18 at 13:46
1

You can have blocks that execute asynchronously without Promises, async functions, or callbacks by using the JS Interpreter (docs, GitHub), conveniently written by the same guy that created Blockly. JS Interpreter is a JavaScript-in-JavaScript implementation. This does mean there needs to be a lot of code to connect the functions/commands of the main JS VM to the interpreter's embedded implementation.

Blockly has a few demonstrations of doing this (src). In particular, you'll want to investigate async-execution.html and the wait block implementation. You can find the wait block running live here.

Conveniently, the interpreter's doc's section on External API happens to use XMLHttpRequest as its example implementation. This should should be a good starting point for your request block's implementation.

Anm
  • 3,291
  • 2
  • 29
  • 40